summaryrefslogtreecommitdiff
path: root/blessings
diff options
context:
space:
mode:
authorErik Rose <grinch@grinchcentral.com>2015-02-03 00:42:53 -0500
committerErik Rose <grinch@grinchcentral.com>2015-02-03 00:42:53 -0500
commit3d8ea2b1ce2bd66018d1d08bf7105089ba495bf6 (patch)
treee16c98d0039a492cfc67653635d293daaca998cf /blessings
parent2405ab79a7c92aadadf437500d790eae2e65fb0f (diff)
parent7eb082718b8cd02a7a0c55e873a3f16b96d64ca6 (diff)
downloadblessings-3d8ea2b1ce2bd66018d1d08bf7105089ba495bf6.tar.gz
Rename instances of "blessed" to "blessings", including the package dir itself. Fix #75.
Diffstat (limited to 'blessings')
-rw-r--r--blessings/__init__.py16
-rw-r--r--blessings/_binterms.py872
-rw-r--r--blessings/formatters.py332
-rw-r--r--blessings/keyboard.py276
-rw-r--r--blessings/sequences.py688
-rw-r--r--blessings/terminal.py783
-rw-r--r--blessings/tests/__init__.py0
-rw-r--r--blessings/tests/accessories.py240
-rw-r--r--blessings/tests/test_core.py457
-rw-r--r--blessings/tests/test_formatters.py403
-rw-r--r--blessings/tests/test_keyboard.py902
-rw-r--r--blessings/tests/test_length_sequence.py342
-rw-r--r--blessings/tests/test_sequences.py549
-rw-r--r--blessings/tests/test_wrap.py105
-rw-r--r--blessings/tests/wall.ans7
15 files changed, 5972 insertions, 0 deletions
diff --git a/blessings/__init__.py b/blessings/__init__.py
new file mode 100644
index 0000000..3622d0e
--- /dev/null
+++ b/blessings/__init__.py
@@ -0,0 +1,16 @@
+"""
+A thin, practical wrapper around terminal capabilities in Python
+
+http://pypi.python.org/pypi/blessings
+"""
+import platform as _platform
+if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'):
+ # Good till 3.2.10
+ # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string.
+ raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 '
+ 'support due to http://bugs.python.org/issue10570.')
+
+
+from .terminal import Terminal
+
+__all__ = ('Terminal',)
diff --git a/blessings/_binterms.py b/blessings/_binterms.py
new file mode 100644
index 0000000..6c93ee0
--- /dev/null
+++ b/blessings/_binterms.py
@@ -0,0 +1,872 @@
+""" Exports a list of binary terminals blessings is not able to cope with. """
+#: This list of terminals is manually managed, it describes all of the terminals
+#: that blessings cannot measure the sequence length for; they contain
+#: binary-packed capabilities instead of numerics, so it is not possible to
+#: build regular expressions in the way that sequences.py does.
+#:
+#: This may be generated by exporting TEST_BINTERMS, then analyzing the
+#: jUnit result xml written to the project folder.
+binary_terminals = u"""
+9term
+aaa+dec
+aaa+rv
+aaa+rv-100
+aaa+rv-30
+aaa-rv-unk
+abm80
+abm85
+abm85e
+abm85h
+abm85h-old
+act4
+act5
+addrinfo
+adds980
+adm+sgr
+adm+sgr-100
+adm+sgr-30
+adm11
+adm1178
+adm12
+adm1a
+adm2
+adm20
+adm21
+adm22
+adm3
+adm31
+adm31-old
+adm3a
+adm3a+
+adm42
+adm42-ns
+adm5
+aepro
+aj510
+aj830
+alto-h19
+altos4
+altos7
+altos7pc
+ampex175
+ampex175-b
+ampex210
+ampex232
+ampex232w
+ampex80
+annarbor4080
+ansi+arrows
+ansi+csr
+ansi+cup
+ansi+enq
+ansi+erase
+ansi+idc
+ansi+idl
+ansi+idl1
+ansi+inittabs
+ansi+local
+ansi+local1
+ansi+pp
+ansi+rca
+ansi+rep
+ansi+sgr
+ansi+sgrbold
+ansi+sgrdim
+ansi+sgrso
+ansi+sgrul
+ansi+tabs
+ansi-color-2-emx
+ansi-color-3-emx
+ansi-emx
+ansi-mini
+ansi-mr
+ansi-mtabs
+ansi-nt
+ansi.sys
+ansi.sys-old
+ansi.sysk
+ansi77
+apollo
+apple-80
+apple-ae
+apple-soroc
+apple-uterm
+apple-uterm-vb
+apple-videx
+apple-videx2
+apple-videx3
+apple-vm80
+apple2e
+apple2e-p
+apple80p
+appleII
+appleIIgs
+atari
+att4415+nl
+att4420
+att4424m
+att5310
+att5310-100
+att5310-30
+att5620-s
+avatar
+avatar0
+avatar0+
+avt+s
+aws
+awsc
+bantam
+basis
+beacon
+beehive
+blit
+bq300-8
+bq300-8-pc
+bq300-8-pc-rv
+bq300-8-pc-w
+bq300-8-pc-w-rv
+bq300-8rv
+bq300-8w
+bq300-w-8rv
+c100
+c100-rv
+c108
+c108-4p
+c108-rv
+c108-rv-4p
+c108-w
+ca22851
+cbblit
+cbunix
+cci
+cci-100
+cci-30
+cdc456
+cdc721
+cdc721-esc
+cdc721-esc-100
+cdc721-esc-30
+cdc721ll
+cdc752
+cdc756
+cit101e
+cit101e-132
+cit101e-n
+cit101e-n132
+cit80
+citoh
+citoh-6lpi
+citoh-8lpi
+citoh-comp
+citoh-elite
+citoh-pica
+citoh-prop
+coco3
+color_xterm
+commodore
+contel300
+contel301
+cops10
+ct8500
+ctrm
+ctrm-100
+ctrm-30
+cyb110
+cyb83
+d132
+d200
+d200-100
+d200-30
+d210-dg
+d210-dg-100
+d210-dg-30
+d211-dg
+d211-dg-100
+d211-dg-30
+d216-dg
+d216-dg-100
+d216-dg-30
+d216-unix
+d216-unix-25
+d217-unix
+d217-unix-25
+d220
+d220-100
+d220-30
+d220-7b
+d220-7b-100
+d220-7b-30
+d220-dg
+d220-dg-100
+d220-dg-30
+d230c
+d230c-100
+d230c-30
+d230c-dg
+d230c-dg-100
+d230c-dg-30
+d400
+d400-100
+d400-30
+d410-dg
+d410-dg-100
+d410-dg-30
+d412-dg
+d412-dg-100
+d412-dg-30
+d412-unix
+d412-unix-25
+d412-unix-s
+d412-unix-sr
+d412-unix-w
+d413-unix
+d413-unix-25
+d413-unix-s
+d413-unix-sr
+d413-unix-w
+d414-unix
+d414-unix-25
+d414-unix-s
+d414-unix-sr
+d414-unix-w
+d430c-dg
+d430c-dg-100
+d430c-dg-30
+d430c-dg-ccc
+d430c-dg-ccc-100
+d430c-dg-ccc-30
+d430c-unix
+d430c-unix-100
+d430c-unix-25
+d430c-unix-25-100
+d430c-unix-25-30
+d430c-unix-25-ccc
+d430c-unix-30
+d430c-unix-ccc
+d430c-unix-s
+d430c-unix-s-ccc
+d430c-unix-sr
+d430c-unix-sr-ccc
+d430c-unix-w
+d430c-unix-w-ccc
+d470c
+d470c-7b
+d470c-dg
+d555-dg
+d577-dg
+d800
+delta
+dg+ccc
+dg+color
+dg+color8
+dg+fixed
+dg-generic
+dg200
+dg210
+dg211
+dg450
+dg460-ansi
+dg6053
+dg6053-old
+dgkeys+11
+dgkeys+15
+dgkeys+7b
+dgkeys+8b
+dgmode+color
+dgmode+color8
+dgunix+ccc
+dgunix+fixed
+diablo1620
+diablo1620-m8
+diablo1640
+diablo1640-lm
+diablo1740-lm
+digilog
+djgpp203
+dm1520
+dm2500
+dm3025
+dm3045
+dmchat
+dmterm
+dp8242
+dt100
+dt100w
+dt110
+dt80-sas
+dtc300s
+dtc382
+dumb
+dw1
+dw2
+dw3
+dw4
+dwk
+ecma+color
+ecma+sgr
+elks
+elks-glasstty
+elks-vt52
+emu
+ep40
+ep48
+esprit
+esprit-am
+ex155
+f100
+f100-rv
+f110
+f110-14
+f110-14w
+f110-w
+f200
+f200-w
+f200vi
+f200vi-w
+falco
+falco-p
+fos
+fox
+gator-52
+gator-52t
+glasstty
+gnome
+gnome+pcfkeys
+gnome-2007
+gnome-2008
+gnome-256color
+gnome-fc5
+gnome-rh72
+gnome-rh80
+gnome-rh90
+go140
+go140w
+gs6300
+gsi
+gt40
+gt42
+guru+rv
+guru+s
+h19
+h19-bs
+h19-g
+h19-u
+h19-us
+h19k
+ha8675
+ha8686
+hft-old
+hmod1
+hp+arrows
+hp+color
+hp+labels
+hp+pfk+arrows
+hp+pfk+cr
+hp+pfk-cr
+hp+printer
+hp2
+hp236
+hp262x
+hp2641a
+hp300h
+hp700-wy
+hp70092
+hp9837
+hp9845
+hpterm
+hpterm-color
+hz1000
+hz1420
+hz1500
+hz1510
+hz1520
+hz1520-noesc
+hz1552
+hz1552-rv
+hz2000
+i100
+i400
+ibcs2
+ibm+16color
+ibm+color
+ibm-apl
+ibm-system1
+ibm3101
+ibm3151
+ibm3161
+ibm3161-C
+ibm3162
+ibm3164
+ibm327x
+ibm5081-c
+ibm8514-c
+ibmaed
+ibmapa8c
+ibmapa8c-c
+ibmega
+ibmega-c
+ibmmono
+ibmvga
+ibmvga-c
+icl6404
+icl6404-w
+ifmr
+ims-ansi
+ims950
+ims950-b
+ims950-rv
+intertube
+intertube2
+intext
+intext2
+kaypro
+kermit
+kermit-am
+klone+acs
+klone+color
+klone+koi8acs
+klone+sgr
+klone+sgr-dumb
+klone+sgr8
+konsole
+konsole+pcfkeys
+konsole-16color
+konsole-256color
+konsole-base
+konsole-linux
+konsole-solaris
+konsole-vt100
+konsole-vt420pc
+konsole-xf3x
+konsole-xf4x
+kt7
+kt7ix
+ln03
+ln03-w
+lpr
+luna
+megatek
+mgterm
+microb
+mime
+mime-fb
+mime-hb
+mime2a
+mime2a-s
+mime314
+mime3a
+mime3ax
+minitel1
+minitel1b
+mlterm+pcfkeys
+mm340
+modgraph2
+msk227
+msk22714
+msk227am
+mt70
+ncr160vppp
+ncr160vpwpp
+ncr160wy50+pp
+ncr160wy50+wpp
+ncr160wy60pp
+ncr160wy60wpp
+ncr260vppp
+ncr260vpwpp
+ncr260wy325pp
+ncr260wy325wpp
+ncr260wy350pp
+ncr260wy350wpp
+ncr260wy50+pp
+ncr260wy50+wpp
+ncr260wy60pp
+ncr260wy60wpp
+ncr7900i
+ncr7900iv
+ncr7901
+ncrvt100an
+ncrvt100wan
+ndr9500
+ndr9500-25
+ndr9500-25-mc
+ndr9500-25-mc-nl
+ndr9500-25-nl
+ndr9500-mc
+ndr9500-mc-nl
+ndr9500-nl
+nec5520
+newhpkeyboard
+nextshell
+northstar
+nsterm+c
+nsterm+c41
+nsterm+s
+nwp511
+oblit
+oc100
+oldpc3
+origpc3
+osborne
+osborne-w
+osexec
+otek4112
+owl
+p19
+pc-coherent
+pc-venix
+pc6300plus
+pcix
+pckermit
+pckermit120
+pe1251
+pe7000c
+pe7000m
+pilot
+pmcons
+prism2
+prism4
+prism5
+pro350
+psterm-fast
+psterm-fast-100
+psterm-fast-30
+pt100
+pt210
+pt250
+pty
+qansi
+qansi-g
+qansi-m
+qansi-t
+qansi-w
+qdss
+qnx
+qnx-100
+qnx-30
+qnxm
+qnxm-100
+qnxm-30
+qnxt
+qnxt-100
+qnxt-30
+qnxt2
+qnxtmono
+qnxtmono-100
+qnxtmono-30
+qnxw
+qnxw-100
+qnxw-30
+qume5
+qvt101
+qvt101+
+qvt102
+qvt119+
+qvt119+-25
+qvt119+-25-w
+qvt119+-w
+rbcomm
+rbcomm-nam
+rbcomm-w
+rca
+regent100
+regent20
+regent25
+regent40
+regent40+
+regent60
+rt6221
+rt6221-w
+rtpc
+rxvt+pcfkeys
+scanset
+screen+fkeys
+screen-16color
+screen-16color-bce
+screen-16color-bce-s
+screen-16color-s
+screen-256color
+screen-256color-bce
+screen-256color-bce-s
+screen-256color-s
+screen-bce
+screen-s
+screen-w
+screen.linux
+screen.rxvt
+screen.teraterm
+screen.xterm-r6
+screen2
+screen3
+screwpoint
+sibo
+simterm
+soroc120
+soroc140
+st52
+superbee-xsb
+superbeeic
+superbrain
+swtp
+synertek
+t10
+t1061
+t1061f
+t3700
+t3800
+tandem6510
+tandem653
+tandem653-100
+tandem653-30
+tek
+tek4013
+tek4014
+tek4014-sm
+tek4015
+tek4015-sm
+tek4023
+tek4105
+tek4107
+tek4113-nd
+tek4205
+tek4205-100
+tek4205-30
+tek4207-s
+teraterm
+teraterm2.3
+teraterm4.59
+terminet1200
+ti700
+ti931
+trs16
+trs2
+tt
+tty33
+tty37
+tty43
+tvi803
+tvi9065
+tvi910
+tvi910+
+tvi912
+tvi912b
+tvi912b+2p
+tvi912b+dim
+tvi912b+dim-100
+tvi912b+dim-30
+tvi912b+mc
+tvi912b+mc-100
+tvi912b+mc-30
+tvi912b+printer
+tvi912b+vb
+tvi912b-2p
+tvi912b-2p-mc
+tvi912b-2p-mc-100
+tvi912b-2p-mc-30
+tvi912b-2p-p
+tvi912b-2p-unk
+tvi912b-mc
+tvi912b-mc-100
+tvi912b-mc-30
+tvi912b-p
+tvi912b-unk
+tvi912b-vb
+tvi912b-vb-mc
+tvi912b-vb-mc-100
+tvi912b-vb-mc-30
+tvi912b-vb-p
+tvi912b-vb-unk
+tvi920b
+tvi920b+fn
+tvi920b-2p
+tvi920b-2p-mc
+tvi920b-2p-mc-100
+tvi920b-2p-mc-30
+tvi920b-2p-p
+tvi920b-2p-unk
+tvi920b-mc
+tvi920b-mc-100
+tvi920b-mc-30
+tvi920b-p
+tvi920b-unk
+tvi920b-vb
+tvi920b-vb-mc
+tvi920b-vb-mc-100
+tvi920b-vb-mc-30
+tvi920b-vb-p
+tvi920b-vb-unk
+tvi921
+tvi924
+tvi925
+tvi925-hi
+tvi92B
+tvi92D
+tvi950
+tvi950-2p
+tvi950-4p
+tvi950-rv
+tvi950-rv-2p
+tvi950-rv-4p
+tvipt
+unknown
+vanilla
+vc303
+vc404
+vc404-s
+vc414
+vc415
+vi200
+vi200-f
+vi200-rv
+vi50
+vi500
+vi50adm
+vi55
+viewpoint
+vp3a+
+vp60
+vp90
+vremote
+vt100+enq
+vt100+fnkeys
+vt100+keypad
+vt100+pfkeys
+vt100-s
+vt102+enq
+vt200-js
+vt220+keypad
+vt50h
+vt52
+vt61
+wsiris
+wy100
+wy100q
+wy120
+wy120-25
+wy120-vb
+wy160
+wy160-25
+wy160-42
+wy160-43
+wy160-tek
+wy160-tek-100
+wy160-tek-30
+wy160-vb
+wy30
+wy30-mc
+wy30-mc-100
+wy30-mc-30
+wy30-vb
+wy325
+wy325-25
+wy325-42
+wy325-43
+wy325-vb
+wy350
+wy350-100
+wy350-30
+wy350-vb
+wy350-vb-100
+wy350-vb-30
+wy350-w
+wy350-w-100
+wy350-w-30
+wy350-wvb
+wy350-wvb-100
+wy350-wvb-30
+wy370
+wy370-100
+wy370-105k
+wy370-105k-100
+wy370-105k-30
+wy370-30
+wy370-EPC
+wy370-EPC-100
+wy370-EPC-30
+wy370-nk
+wy370-nk-100
+wy370-nk-30
+wy370-rv
+wy370-rv-100
+wy370-rv-30
+wy370-tek
+wy370-tek-100
+wy370-tek-30
+wy370-vb
+wy370-vb-100
+wy370-vb-30
+wy370-w
+wy370-w-100
+wy370-w-30
+wy370-wvb
+wy370-wvb-100
+wy370-wvb-30
+wy50
+wy50-mc
+wy50-mc-100
+wy50-mc-30
+wy50-vb
+wy60
+wy60-25
+wy60-42
+wy60-43
+wy60-vb
+wy99-ansi
+wy99a-ansi
+wy99f
+wy99fa
+wy99gt
+wy99gt-25
+wy99gt-vb
+wy99gt-tek
+wyse-vp
+xerox1720
+xerox820
+xfce
+xnuppc+100x37
+xnuppc+112x37
+xnuppc+128x40
+xnuppc+128x48
+xnuppc+144x48
+xnuppc+160x64
+xnuppc+200x64
+xnuppc+200x75
+xnuppc+256x96
+xnuppc+80x25
+xnuppc+80x30
+xnuppc+90x30
+xnuppc+c
+xnuppc+c-100
+xnuppc+c-30
+xtalk
+xtalk-100
+xtalk-30
+xterm+256color
+xterm+256color-100
+xterm+256color-30
+xterm+88color
+xterm+88color-100
+xterm+88color-30
+xterm+app
+xterm+edit
+xterm+noapp
+xterm+pc+edit
+xterm+pcc0
+xterm+pcc1
+xterm+pcc2
+xterm+pcc3
+xterm+pce2
+xterm+pcf0
+xterm+pcf2
+xterm+pcfkeys
+xterm+r6f2
+xterm+vt+edit
+xterm-vt52
+z100
+z100bw
+z29
+zen30
+zen50
+ztx
+""".split()
+
+__all__ = ('binary_terminals',)
diff --git a/blessings/formatters.py b/blessings/formatters.py
new file mode 100644
index 0000000..54f090c
--- /dev/null
+++ b/blessings/formatters.py
@@ -0,0 +1,332 @@
+"This sub-module provides formatting functions."
+import curses
+import sys
+
+_derivatives = ('on', 'bright', 'on_bright',)
+
+_colors = set('black red green yellow blue magenta cyan white'.split())
+_compoundables = set('bold underline reverse blink dim italic shadow '
+ 'standout subscript superscript'.split())
+
+#: Valid colors and their background (on), bright, and bright-bg derivatives.
+COLORS = set(['_'.join((derivitive, color))
+ for derivitive in _derivatives
+ for color in _colors]) | _colors
+
+#: All valid compoundable names.
+COMPOUNDABLES = (COLORS | _compoundables)
+
+if sys.version_info[0] == 3:
+ text_type = str
+ basestring = str
+else:
+ text_type = unicode # noqa
+
+
+class ParameterizingString(text_type):
+ """A Unicode string which can be called as a parameterizing termcap.
+
+ For example::
+
+ >> term = Terminal()
+ >> color = ParameterizingString(term.color, term.normal, 'color')
+ >> color(9)('color #9')
+ u'\x1b[91mcolor #9\x1b(B\x1b[m'
+ """
+
+ def __new__(cls, *args):
+ """P.__new__(cls, cap, [normal, [name]])
+
+ :arg cap: parameterized string suitable for curses.tparm()
+ :arg normal: terminating sequence for this capability.
+ :arg name: name of this terminal capability.
+ """
+ assert len(args) and len(args) < 4, args
+ new = text_type.__new__(cls, args[0])
+ new._normal = len(args) > 1 and args[1] or u''
+ new._name = len(args) > 2 and args[2] or u'<not specified>'
+ return new
+
+ def __call__(self, *args):
+ """P(*args) -> FormattingString()
+
+ Return evaluated terminal capability (self), receiving arguments
+ ``*args``, followed by the terminating sequence (self.normal) into
+ a FormattingString capable of being called.
+ """
+ try:
+ # Re-encode the cap, because tparm() takes a bytestring in Python
+ # 3. However, appear to be a plain Unicode string otherwise so
+ # concats work.
+ attr = curses.tparm(self.encode('latin1'), *args).decode('latin1')
+ return FormattingString(attr, self._normal)
+ except TypeError as err:
+ # If the first non-int (i.e. incorrect) arg was a string, suggest
+ # something intelligent:
+ if len(args) and isinstance(args[0], basestring):
+ raise TypeError(
+ "A native or nonexistent capability template, %r received"
+ " invalid argument %r: %s. You probably misspelled a"
+ " formatting call like `bright_red'" % (
+ self._name, args, err))
+ # Somebody passed a non-string; I don't feel confident
+ # guessing what they were trying to do.
+ raise
+ except curses.error as err:
+ # ignore 'tparm() returned NULL', you won't get any styling,
+ # even if does_styling is True. This happens on win32 platforms
+ # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed
+ if "tparm() returned NULL" not in text_type(err):
+ raise
+ return NullCallableString()
+
+
+class ParameterizingProxyString(text_type):
+ """A Unicode string which can be called to proxy missing termcap entries.
+
+ For example::
+
+ >>> from blessings import Terminal
+ >>> term = Terminal('screen')
+ >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa')
+ >>> hpa(9)
+ u''
+ >>> fmt = u'\x1b[{0}G'
+ >>> fmt_arg = lambda *arg: (arg[0] + 1,)
+ >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa')
+ >>> hpa(9)
+ u'\x1b[10G'
+ """
+
+ def __new__(cls, *args):
+ """P.__new__(cls, (fmt, callable), [normal, [name]])
+
+ :arg fmt: format string suitable for displaying terminal sequences.
+ :arg callable: receives __call__ arguments for formatting fmt.
+ :arg normal: terminating sequence for this capability.
+ :arg name: name of this terminal capability.
+ """
+ assert len(args) and len(args) < 4, args
+ assert type(args[0]) is tuple, args[0]
+ assert callable(args[0][1]), args[0][1]
+ new = text_type.__new__(cls, args[0][0])
+ new._fmt_args = args[0][1]
+ new._normal = len(args) > 1 and args[1] or u''
+ new._name = len(args) > 2 and args[2] or u'<not specified>'
+ return new
+
+ def __call__(self, *args):
+ """P(*args) -> FormattingString()
+
+ Return evaluated terminal capability format, (self), using callable
+ ``self._fmt_args`` receiving arguments ``*args``, followed by the
+ terminating sequence (self.normal) into a FormattingString capable
+ of being called.
+ """
+ return FormattingString(self.format(*self._fmt_args(*args)),
+ self._normal)
+
+
+def get_proxy_string(term, attr):
+ """ Proxy and return callable StringClass for proxied attributes.
+
+ We know that some kinds of terminal kinds support sequences that the
+ terminfo database always report -- such as the 'move_x' attribute for
+ terminal type 'screen' and 'ansi', or 'hide_cursor' for 'ansi'.
+
+ Returns instance of ParameterizingProxyString or NullCallableString.
+ """
+ # normalize 'screen-256color', or 'ansi.sys' to its basic names
+ term_kind = next(iter(_kind for _kind in ('screen', 'ansi',)
+ if term.kind.startswith(_kind)), term)
+ return {
+ 'screen': {
+ # proxy move_x/move_y for 'screen' terminal type.
+ 'hpa': ParameterizingProxyString(
+ (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ 'vpa': ParameterizingProxyString(
+ (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ },
+ 'ansi': {
+ # proxy show/hide cursor for 'ansi' terminal type.
+ 'civis': ParameterizingProxyString(
+ (u'\x1b[?25l', lambda *arg: ()), term.normal, attr),
+ 'cnorm': ParameterizingProxyString(
+ (u'\x1b[?25h', lambda *arg: ()), term.normal, attr),
+ 'hpa': ParameterizingProxyString(
+ (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ 'vpa': ParameterizingProxyString(
+ (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ }
+ }.get(term_kind, {}).get(attr, None)
+
+
+class FormattingString(text_type):
+ """A Unicode string which can be called using ``text``,
+ returning a new string, ``attr`` + ``text`` + ``normal``::
+
+ >> style = FormattingString(term.bright_blue, term.normal)
+ >> style('Big Blue')
+ u'\x1b[94mBig Blue\x1b(B\x1b[m'
+ """
+
+ def __new__(cls, *args):
+ """P.__new__(cls, sequence, [normal])
+ :arg sequence: terminal attribute sequence.
+ :arg normal: terminating sequence for this attribute.
+ """
+ assert 1 <= len(args) <= 2, args
+ new = text_type.__new__(cls, args[0])
+ new._normal = len(args) > 1 and args[1] or u''
+ return new
+
+ def __call__(self, text):
+ """P(text) -> unicode
+
+ Return string ``text``, joined by specified video attribute,
+ (self), and followed by reset attribute sequence (term.normal).
+ """
+ if len(self):
+ return u''.join((self, text, self._normal))
+ return text
+
+
+class NullCallableString(text_type):
+ """A dummy callable Unicode to stand in for ``FormattingString`` and
+ ``ParameterizingString`` for terminals that cannot perform styling.
+ """
+ def __new__(cls):
+ new = text_type.__new__(cls, u'')
+ return new
+
+ def __call__(self, *args):
+ """Return a Unicode or whatever you passed in as the first arg
+ (hopefully a string of some kind).
+
+ When called with an int as the first arg, return an empty Unicode. An
+ int is a good hint that I am a ``ParameterizingString``, as there are
+ only about half a dozen string-returning capabilities listed in
+ terminfo(5) which accept non-int arguments, they are seldom used.
+
+ When called with a non-int as the first arg (no no args at all), return
+ the first arg, acting in place of ``FormattingString`` without
+ any attributes.
+ """
+ if len(args) != 1 or isinstance(args[0], int):
+ # I am acting as a ParameterizingString.
+
+ # tparm can take not only ints but also (at least) strings as its
+ # 2nd...nth argument. But we don't support callable parameterizing
+ # capabilities that take non-ints yet, so we can cheap out here.
+
+ # TODO(erikrose): Go through enough of the motions in the
+ # capability resolvers to determine which of 2 special-purpose
+ # classes, NullParameterizableString or NullFormattingString,
+ # to return, and retire this one.
+
+ # As a NullCallableString, even when provided with a parameter,
+ # such as t.color(5), we must also still be callable, fe:
+ #
+ # >>> t.color(5)('shmoo')
+ #
+ # is actually simplified result of NullCallable()() on terminals
+ # without color support, so turtles all the way down: we return
+ # another instance.
+ return NullCallableString()
+ return args[0]
+
+
+def split_compound(compound):
+ """Split a possibly compound format string into segments.
+
+ >>> split_compound('bold_underline_bright_blue_on_red')
+ ['bold', 'underline', 'bright_blue', 'on_red']
+
+ """
+ merged_segs = []
+ # These occur only as prefixes, so they can always be merged:
+ mergeable_prefixes = ['on', 'bright', 'on_bright']
+ for s in compound.split('_'):
+ if merged_segs and merged_segs[-1] in mergeable_prefixes:
+ merged_segs[-1] += '_' + s
+ else:
+ merged_segs.append(s)
+ return merged_segs
+
+
+def resolve_capability(term, attr):
+ """Return a Unicode string for the terminal capability ``attr``,
+ or an empty string if not found, or if terminal is without styling
+ capabilities.
+ """
+ # Decode sequences as latin1, as they are always 8-bit bytes, so when
+ # b'\xff' is returned, this must be decoded to u'\xff'.
+ if not term.does_styling:
+ return u''
+ val = curses.tigetstr(term._sugar.get(attr, attr))
+ return u'' if val is None else val.decode('latin1')
+
+
+def resolve_color(term, color):
+ """resolve_color(T, color) -> FormattingString()
+
+ Resolve a ``color`` name to callable capability, ``FormattingString``
+ unless ``term.number_of_colors`` is 0, then ``NullCallableString``.
+
+ Valid ``color`` capabilities names are any of the simple color
+ names, such as ``red``, or compounded, such as ``on_bright_green``.
+ """
+ # NOTE(erikrose): Does curses automatically exchange red and blue and cyan
+ # and yellow when a terminal supports setf/setb rather than setaf/setab?
+ # I'll be blasted if I can find any documentation. The following
+ # assumes it does.
+ color_cap = (term._background_color if 'on_' in color else
+ term._foreground_color)
+
+ # curses constants go up to only 7, so add an offset to get at the
+ # bright colors at 8-15:
+ offset = 8 if 'bright_' in color else 0
+ base_color = color.rsplit('_', 1)[-1]
+ if term.number_of_colors == 0:
+ return NullCallableString()
+
+ attr = 'COLOR_%s' % (base_color.upper(),)
+ fmt_attr = color_cap(getattr(curses, attr) + offset)
+ return FormattingString(fmt_attr, term.normal)
+
+
+def resolve_attribute(term, attr):
+ """Resolve a sugary or plain capability name, color, or compound
+ formatting name into a *callable* unicode string capability,
+ ``ParameterizingString`` or ``FormattingString``.
+ """
+ # A simple color, such as `red' or `blue'.
+ if attr in COLORS:
+ return resolve_color(term, attr)
+
+ # A direct compoundable, such as `bold' or `on_red'.
+ if attr in COMPOUNDABLES:
+ sequence = resolve_capability(term, attr)
+ return FormattingString(sequence, term.normal)
+
+ # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE
+ # call for each compounding section, joined and returned as
+ # a completed completed FormattingString.
+ formatters = split_compound(attr)
+ if all(fmt in COMPOUNDABLES for fmt in formatters):
+ resolution = (resolve_attribute(term, fmt) for fmt in formatters)
+ return FormattingString(u''.join(resolution), term.normal)
+ else:
+ # otherwise, this is our end-game: given a sequence such as 'csr'
+ # (change scrolling region), return a ParameterizingString instance,
+ # that when called, performs and returns the final string after curses
+ # capability lookup is performed.
+ tparm_capseq = resolve_capability(term, attr)
+ if not tparm_capseq:
+ # and, for special terminals, such as 'screen', provide a Proxy
+ # ParameterizingString for attributes they do not claim to support,
+ # but actually do! (such as 'hpa' and 'vpa').
+ proxy = get_proxy_string(term, term._sugar.get(attr, attr))
+ if proxy is not None:
+ return proxy
+ return ParameterizingString(tparm_capseq, term.normal, attr)
diff --git a/blessings/keyboard.py b/blessings/keyboard.py
new file mode 100644
index 0000000..280606d
--- /dev/null
+++ b/blessings/keyboard.py
@@ -0,0 +1,276 @@
+"This sub-module provides 'keyboard awareness'."
+
+__author__ = 'Jeff Quast <contact@jeffquast.com>'
+__license__ = 'MIT'
+
+__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',)
+
+import curses.has_key
+import collections
+import curses
+import sys
+
+if hasattr(collections, 'OrderedDict'):
+ OrderedDict = collections.OrderedDict
+else:
+ # python 2.6 requires 3rd party library
+ import ordereddict
+ OrderedDict = ordereddict.OrderedDict
+
+get_curses_keycodes = lambda: dict(
+ ((keyname, getattr(curses, keyname))
+ for keyname in dir(curses)
+ if keyname.startswith('KEY_'))
+)
+
+# override a few curses constants with easier mnemonics,
+# there may only be a 1:1 mapping, so for those who desire
+# to use 'KEY_DC' from, perhaps, ported code, recommend
+# that they simply compare with curses.KEY_DC.
+CURSES_KEYCODE_OVERRIDE_MIXIN = (
+ ('KEY_DELETE', curses.KEY_DC),
+ ('KEY_INSERT', curses.KEY_IC),
+ ('KEY_PGUP', curses.KEY_PPAGE),
+ ('KEY_PGDOWN', curses.KEY_NPAGE),
+ ('KEY_ESCAPE', curses.KEY_EXIT),
+ ('KEY_SUP', curses.KEY_SR),
+ ('KEY_SDOWN', curses.KEY_SF),
+ ('KEY_UP_LEFT', curses.KEY_A1),
+ ('KEY_UP_RIGHT', curses.KEY_A3),
+ ('KEY_CENTER', curses.KEY_B2),
+ ('KEY_BEGIN', curses.KEY_BEG),
+)
+
+# Inject KEY_{names} that we think would be useful, there are no curses
+# definitions for the keypad keys. We need keys that generate multibyte
+# sequences, though it is useful to have some aliases for basic control
+# characters such as TAB.
+_lastval = max(get_curses_keycodes().values())
+for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT',
+ 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2',
+ 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'):
+ _lastval += 1
+ setattr(curses, 'KEY_{0}'.format(key), _lastval)
+
+if sys.version_info[0] == 3:
+ text_type = str
+ unichr = chr
+else:
+ text_type = unicode # noqa
+
+
+class Keystroke(text_type):
+ """A unicode-derived class for describing keyboard input returned by
+ the ``inkey()`` method of ``Terminal``, which may, at times, be a
+ multibyte sequence, providing properties ``is_sequence`` as ``True``
+ when the string is a known sequence, and ``code``, which returns an
+ integer value that may be compared against the terminal class attributes
+ such as ``KEY_LEFT``.
+ """
+ def __new__(cls, ucs='', code=None, name=None):
+ new = text_type.__new__(cls, ucs)
+ new._name = name
+ new._code = code
+ return new
+
+ @property
+ def is_sequence(self):
+ "Whether the value represents a multibyte sequence (bool)."
+ return self._code is not None
+
+ def __repr__(self):
+ return self._name is None and text_type.__repr__(self) or self._name
+ __repr__.__doc__ = text_type.__doc__
+
+ @property
+ def name(self):
+ "String-name of key sequence, such as ``'KEY_LEFT'`` (str)."
+ return self._name
+
+ @property
+ def code(self):
+ "Integer keycode value of multibyte sequence (int)."
+ return self._code
+
+
+def get_keyboard_codes():
+ """get_keyboard_codes() -> dict
+
+ Returns dictionary of (code, name) pairs for curses keyboard constant
+ values and their mnemonic name. Such as key ``260``, with the value of
+ its identity, ``KEY_LEFT``. These are derived from the attributes by the
+ same of the curses module, with the following exceptions:
+
+ * ``KEY_DELETE`` in place of ``KEY_DC``
+ * ``KEY_INSERT`` in place of ``KEY_IC``
+ * ``KEY_PGUP`` in place of ``KEY_PPAGE``
+ * ``KEY_PGDOWN`` in place of ``KEY_NPAGE``
+ * ``KEY_ESCAPE`` in place of ``KEY_EXIT``
+ * ``KEY_SUP`` in place of ``KEY_SR``
+ * ``KEY_SDOWN`` in place of ``KEY_SF``
+ """
+ keycodes = OrderedDict(get_curses_keycodes())
+ keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN)
+
+ # invert dictionary (key, values) => (values, key), preferring the
+ # last-most inserted value ('KEY_DELETE' over 'KEY_DC').
+ return dict(zip(keycodes.values(), keycodes.keys()))
+
+
+def _alternative_left_right(term):
+ """_alternative_left_right(T) -> dict
+
+ Return dict of sequences ``term._cuf1``, and ``term._cub1``,
+ valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available.
+
+ some terminals report a different value for *kcuf1* than *cuf1*, but
+ actually send the value of *cuf1* for right arrow key (which is
+ non-destructive space).
+ """
+ keymap = dict()
+ if term._cuf1 and term._cuf1 != u' ':
+ keymap[term._cuf1] = curses.KEY_RIGHT
+ if term._cub1 and term._cub1 != u'\b':
+ keymap[term._cub1] = curses.KEY_LEFT
+ return keymap
+
+
+def get_keyboard_sequences(term):
+ """get_keyboard_sequences(T) -> (OrderedDict)
+
+ Initialize and return a keyboard map and sequence lookup table,
+ (sequence, constant) from blessings Terminal instance ``term``,
+ where ``sequence`` is a multibyte input sequence, such as u'\x1b[D',
+ and ``constant`` is a constant, such as term.KEY_LEFT. The return
+ value is an OrderedDict instance, with their keys sorted longest-first.
+ """
+ # A small gem from curses.has_key that makes this all possible,
+ # _capability_names: a lookup table of terminal capability names for
+ # keyboard sequences (fe. kcub1, key_left), keyed by the values of
+ # constants found beginning with KEY_ in the main curses module
+ # (such as KEY_LEFT).
+ #
+ # latin1 encoding is used so that bytes in 8-bit range of 127-255
+ # have equivalent chr() and unichr() values, so that the sequence
+ # 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 (
+ (curses.tigetstr(cap), val)
+ for (val, cap) in capability_names.items()
+ ) if seq
+ ) if term.does_styling else ())
+
+ sequence_map.update(_alternative_left_right(term))
+ sequence_map.update(DEFAULT_SEQUENCE_MIXIN)
+
+ # This is for fast lookup matching of sequences, preferring
+ # full-length sequence such as ('\x1b[D', KEY_LEFT)
+ # over simple sequences such as ('\x1b', KEY_EXIT).
+ return OrderedDict((
+ (seq, sequence_map[seq]) for seq in sorted(
+ sequence_map.keys(), key=len, reverse=True)))
+
+
+def resolve_sequence(text, mapper, codes):
+ """resolve_sequence(text, mapper, codes) -> Keystroke()
+
+ Returns first matching Keystroke() instance for sequences found in
+ ``mapper`` beginning with input ``text``, where ``mapper`` is an
+ OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by
+ their integer value (260), and ``codes`` is a dict of integer values (260)
+ paired by their mnemonic name, 'KEY_LEFT'.
+ """
+ for sequence, code in mapper.items():
+ if text.startswith(sequence):
+ return Keystroke(ucs=sequence, code=code, name=codes[code])
+ return Keystroke(ucs=text and text[0] or u'')
+
+"""In a perfect world, terminal emulators would always send exactly what
+the terminfo(5) capability database plans for them, accordingly by the
+value of the ``TERM`` name they declare.
+
+But this isn't a perfect world. Many vt220-derived terminals, such as
+those declaring 'xterm', will continue to send vt220 codes instead of
+their native-declared codes, for backwards-compatibility.
+
+This goes for many: rxvt, putty, iTerm.
+
+These "mixins" are used for *all* terminals, regardless of their type.
+
+Furthermore, curses does not provide sequences sent by the keypad,
+at least, it does not provide a way to distinguish between keypad 0
+and numeric 0.
+"""
+DEFAULT_SEQUENCE_MIXIN = (
+ # these common control characters (and 127, ctrl+'?') mapped to
+ # an application key definition.
+ (unichr(10), curses.KEY_ENTER),
+ (unichr(13), curses.KEY_ENTER),
+ (unichr(8), curses.KEY_BACKSPACE),
+ (unichr(9), curses.KEY_TAB),
+ (unichr(27), curses.KEY_EXIT),
+ (unichr(127), curses.KEY_DC),
+
+ (u"\x1b[A", curses.KEY_UP),
+ (u"\x1b[B", curses.KEY_DOWN),
+ (u"\x1b[C", curses.KEY_RIGHT),
+ (u"\x1b[D", curses.KEY_LEFT),
+ (u"\x1b[F", curses.KEY_END),
+ (u"\x1b[H", curses.KEY_HOME),
+ # not sure where these are from .. please report
+ (u"\x1b[K", curses.KEY_END),
+ (u"\x1b[U", curses.KEY_NPAGE),
+ (u"\x1b[V", curses.KEY_PPAGE),
+
+ # keys sent after term.smkx (keypad_xmit) is emitted, source:
+ # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys
+ # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes
+ #
+ # keypad, numlock on
+ (u"\x1bOM", curses.KEY_ENTER), # return
+ (u"\x1bOj", curses.KEY_KP_MULTIPLY), # *
+ (u"\x1bOk", curses.KEY_KP_ADD), # +
+ (u"\x1bOl", curses.KEY_KP_SEPARATOR), # ,
+ (u"\x1bOm", curses.KEY_KP_SUBTRACT), # -
+ (u"\x1bOn", curses.KEY_KP_DECIMAL), # .
+ (u"\x1bOo", curses.KEY_KP_DIVIDE), # /
+ (u"\x1bOX", curses.KEY_KP_EQUAL), # =
+ (u"\x1bOp", curses.KEY_KP_0), # 0
+ (u"\x1bOq", curses.KEY_KP_1), # 1
+ (u"\x1bOr", curses.KEY_KP_2), # 2
+ (u"\x1bOs", curses.KEY_KP_3), # 3
+ (u"\x1bOt", curses.KEY_KP_4), # 4
+ (u"\x1bOu", curses.KEY_KP_5), # 5
+ (u"\x1bOv", curses.KEY_KP_6), # 6
+ (u"\x1bOw", curses.KEY_KP_7), # 7
+ (u"\x1bOx", curses.KEY_KP_8), # 8
+ (u"\x1bOy", curses.KEY_KP_9), # 9
+
+ # keypad, numlock off
+ (u"\x1b[1~", curses.KEY_FIND), # find
+ (u"\x1b[2~", curses.KEY_IC), # insert (0)
+ (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute"
+ (u"\x1b[4~", curses.KEY_SELECT), # select
+ (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9)
+ (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3)
+ (u"\x1b[7~", curses.KEY_HOME), # home
+ (u"\x1b[8~", curses.KEY_END), # end
+ (u"\x1b[OA", curses.KEY_UP), # up (8)
+ (u"\x1b[OB", curses.KEY_DOWN), # down (2)
+ (u"\x1b[OC", curses.KEY_RIGHT), # right (6)
+ (u"\x1b[OD", curses.KEY_LEFT), # left (4)
+ (u"\x1b[OF", curses.KEY_END), # end (1)
+ (u"\x1b[OH", curses.KEY_HOME), # home (7)
+
+ # The vt220 placed F1-F4 above the keypad, in place of actual
+ # F1-F4 were local functions (hold screen, print screen,
+ # set up, data/talk, break).
+ (u"\x1bOP", curses.KEY_F1),
+ (u"\x1bOQ", curses.KEY_F2),
+ (u"\x1bOR", curses.KEY_F3),
+ (u"\x1bOS", curses.KEY_F4),
+)
diff --git a/blessings/sequences.py b/blessings/sequences.py
new file mode 100644
index 0000000..80649c3
--- /dev/null
+++ b/blessings/sequences.py
@@ -0,0 +1,688 @@
+# encoding: utf-8
+" This sub-module provides 'sequence awareness' for blessings."
+
+__author__ = 'Jeff Quast <contact@jeffquast.com>'
+__license__ = 'MIT'
+
+__all__ = ('init_sequence_patterns', 'Sequence', 'SequenceTextWrapper',)
+
+# built-ins
+import functools
+import textwrap
+import warnings
+import math
+import sys
+import re
+
+# local
+from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED
+
+# 3rd-party
+import wcwidth # https://github.com/jquast/wcwidth
+
+_BINTERM_UNSUPPORTED_MSG = (
+ u"Terminal kind {0!r} contains binary-packed capabilities, blessings "
+ u"is likely to fail to measure the length of its sequences.")
+
+if sys.version_info[0] == 3:
+ text_type = str
+else:
+ text_type = unicode # noqa
+
+
+def _merge_sequences(inp):
+ """Merge a list of input sequence patterns for use in a regular expression.
+ Order by lengthyness (full sequence set precedent over subset),
+ and exclude any empty (u'') sequences.
+ """
+ return sorted(list(filter(None, inp)), key=len, reverse=True)
+
+
+def _build_numeric_capability(term, cap, optional=False,
+ base_num=99, nparams=1):
+ """ Build regexp from capabilities having matching numeric
+ parameter contained within termcap value: n->(\d+).
+ """
+ _cap = getattr(term, cap)
+ opt = '?' if optional else ''
+ if _cap:
+ args = (base_num,) * nparams
+ cap_re = re.escape(_cap(*args))
+ for num in range(base_num - 1, base_num + 2):
+ # search for matching ascii, n-1 through n+1
+ if str(num) in cap_re:
+ # modify & return n to matching digit expression
+ cap_re = cap_re.replace(str(num), r'(\d+)%s' % (opt,))
+ return cap_re
+ warnings.warn('Unknown parameter in %r (%r, %r)' % (cap, _cap, cap_re))
+ return None # no such capability
+
+
+def _build_any_numeric_capability(term, cap, num=99, nparams=1):
+ """ Build regexp from capabilities having *any* digit parameters
+ (substitute matching \d with pattern \d and return).
+ """
+ _cap = getattr(term, cap)
+ if _cap:
+ cap_re = re.escape(_cap(*((num,) * nparams)))
+ cap_re = re.sub('(\d+)', r'(\d+)', cap_re)
+ if r'(\d+)' in cap_re:
+ return cap_re
+ warnings.warn('Missing numerics in %r, %r' % (cap, cap_re))
+ return None # no such capability
+
+
+def get_movement_sequence_patterns(term):
+ """ Build and return set of regexp for capabilities of ``term`` known
+ to cause movement.
+ """
+ bnc = functools.partial(_build_numeric_capability, term)
+
+ return set([
+ # carriage_return
+ re.escape(term.cr),
+ # column_address: Horizontal position, absolute
+ bnc(cap='hpa'),
+ # row_address: Vertical position #1 absolute
+ bnc(cap='vpa'),
+ # cursor_address: Move to row #1 columns #2
+ bnc(cap='cup', nparams=2),
+ # cursor_down: Down one line
+ re.escape(term.cud1),
+ # cursor_home: Home cursor (if no cup)
+ re.escape(term.home),
+ # cursor_left: Move left one space
+ re.escape(term.cub1),
+ # cursor_right: Non-destructive space (move right one space)
+ re.escape(term.cuf1),
+ # cursor_up: Up one line
+ re.escape(term.cuu1),
+ # param_down_cursor: Down #1 lines
+ bnc(cap='cud', optional=True),
+ # restore_cursor: Restore cursor to position of last save_cursor
+ re.escape(term.rc),
+ # clear_screen: clear screen and home cursor
+ re.escape(term.clear),
+ # enter/exit_fullscreen: switch to alternate screen buffer
+ re.escape(term.enter_fullscreen),
+ re.escape(term.exit_fullscreen),
+ # forward cursor
+ term._cuf,
+ # backward cursor
+ term._cub,
+ ])
+
+
+def get_wontmove_sequence_patterns(term):
+ """ Build and return set of regexp for capabilities of ``term`` known
+ not to cause any movement.
+ """
+ bnc = functools.partial(_build_numeric_capability, term)
+ bna = functools.partial(_build_any_numeric_capability, term)
+
+ return list([
+ # print_screen: Print contents of screen
+ re.escape(term.mc0),
+ # prtr_off: Turn off printer
+ re.escape(term.mc4),
+ # prtr_on: Turn on printer
+ re.escape(term.mc5),
+ # save_cursor: Save current cursor position (P)
+ re.escape(term.sc),
+ # set_tab: Set a tab in every row, current columns
+ re.escape(term.hts),
+ # enter_bold_mode: Turn on bold (extra bright) mode
+ re.escape(term.bold),
+ # enter_standout_mode
+ re.escape(term.standout),
+ # enter_subscript_mode
+ re.escape(term.subscript),
+ # enter_superscript_mode
+ re.escape(term.superscript),
+ # enter_underline_mode: Begin underline mode
+ re.escape(term.underline),
+ # enter_blink_mode: Turn on blinking
+ re.escape(term.blink),
+ # enter_dim_mode: Turn on half-bright mode
+ re.escape(term.dim),
+ # cursor_invisible: Make cursor invisible
+ re.escape(term.civis),
+ # cursor_visible: Make cursor very visible
+ re.escape(term.cvvis),
+ # cursor_normal: Make cursor appear normal (undo civis/cvvis)
+ re.escape(term.cnorm),
+ # clear_all_tabs: Clear all tab stops
+ re.escape(term.tbc),
+ # change_scroll_region: Change region to line #1 to line #2
+ bnc(cap='csr', nparams=2),
+ # clr_bol: Clear to beginning of line
+ re.escape(term.el1),
+ # clr_eol: Clear to end of line
+ re.escape(term.el),
+ # clr_eos: Clear to end of screen
+ re.escape(term.clear_eos),
+ # delete_character: Delete character
+ re.escape(term.dch1),
+ # delete_line: Delete line (P*)
+ re.escape(term.dl1),
+ # erase_chars: Erase #1 characters
+ bnc(cap='ech'),
+ # insert_line: Insert line (P*)
+ re.escape(term.il1),
+ # parm_dch: Delete #1 characters
+ bnc(cap='dch'),
+ # parm_delete_line: Delete #1 lines
+ bnc(cap='dl'),
+ # exit_alt_charset_mode: End alternate character set (P)
+ re.escape(term.rmacs),
+ # exit_am_mode: Turn off automatic margins
+ re.escape(term.rmam),
+ # exit_attribute_mode: Turn off all attributes
+ re.escape(term.sgr0),
+ # exit_ca_mode: Strings to end programs using cup
+ re.escape(term.rmcup),
+ # exit_insert_mode: Exit insert mode
+ re.escape(term.rmir),
+ # exit_standout_mode: Exit standout mode
+ re.escape(term.rmso),
+ # exit_underline_mode: Exit underline mode
+ re.escape(term.rmul),
+ # flash_hook: Flash switch hook
+ re.escape(term.hook),
+ # flash_screen: Visible bell (may not move cursor)
+ re.escape(term.flash),
+ # keypad_local: Leave 'keyboard_transmit' mode
+ re.escape(term.rmkx),
+ # keypad_xmit: Enter 'keyboard_transmit' mode
+ re.escape(term.smkx),
+ # meta_off: Turn off meta mode
+ re.escape(term.rmm),
+ # meta_on: Turn on meta mode (8th-bit on)
+ re.escape(term.smm),
+ # orig_pair: Set default pair to its original value
+ re.escape(term.op),
+ # parm_ich: Insert #1 characters
+ bnc(cap='ich'),
+ # parm_index: Scroll forward #1
+ bnc(cap='indn'),
+ # parm_insert_line: Insert #1 lines
+ bnc(cap='il'),
+ # erase_chars: Erase #1 characters
+ bnc(cap='ech'),
+ # parm_rindex: Scroll back #1 lines
+ bnc(cap='rin'),
+ # parm_up_cursor: Up #1 lines
+ bnc(cap='cuu'),
+ # scroll_forward: Scroll text up (P)
+ re.escape(term.ind),
+ # scroll_reverse: Scroll text down (P)
+ re.escape(term.rev),
+ # tab: Tab to next 8-space hardware tab stop
+ re.escape(term.ht),
+ # set_a_background: Set background color to #1, using ANSI escape
+ bna(cap='setab', num=1),
+ bna(cap='setab', num=(term.number_of_colors - 1)),
+ # set_a_foreground: Set foreground color to #1, using ANSI escape
+ bna(cap='setaf', num=1),
+ bna(cap='setaf', num=(term.number_of_colors - 1)),
+ ] + [
+ # set_attributes: Define video attributes #1-#9 (PG9)
+ # ( not *exactly* legal, being extra forgiving. )
+ bna(cap='sgr', nparams=_num) for _num in range(1, 10)
+ # reset_{1,2,3}string: Reset string
+ ] + list(map(re.escape, (term.r1, term.r2, term.r3,))))
+
+
+def init_sequence_patterns(term):
+ """Given a Terminal instance, ``term``, this function processes
+ and parses several known terminal capabilities, and builds and
+ returns a dictionary database of regular expressions, which may
+ be re-attached to the terminal by attributes of the same key-name:
+
+ ``_re_will_move``
+ any sequence matching this pattern will cause the terminal
+ cursor to move (such as *term.home*).
+
+ ``_re_wont_move``
+ any sequence matching this pattern will not cause the cursor
+ to move (such as *term.bold*).
+
+ ``_re_cuf``
+ regular expression that matches term.cuf(N) (move N characters forward),
+ or None if temrinal is without cuf sequence.
+
+ ``_cuf1``
+ *term.cuf1* sequence (cursor forward 1 character) as a static value.
+
+ ``_re_cub``
+ regular expression that matches term.cub(N) (move N characters backward),
+ or None if terminal is without cub sequence.
+
+ ``_cub1``
+ *term.cuf1* sequence (cursor backward 1 character) as a static value.
+
+ These attributes make it possible to perform introspection on strings
+ containing sequences generated by this terminal, to determine the
+ printable length of a string.
+ """
+ if term.kind in _BINTERM_UNSUPPORTED:
+ warnings.warn(_BINTERM_UNSUPPORTED_MSG.format(term.kind))
+
+ # Build will_move, a list of terminal capabilities that have
+ # indeterminate effects on the terminal cursor position.
+ _will_move = set()
+ if term.does_styling:
+ _will_move = _merge_sequences(get_movement_sequence_patterns(term))
+
+ # Build wont_move, a list of terminal capabilities that mainly affect
+ # video attributes, for use with measure_length().
+ _wont_move = set()
+ if term.does_styling:
+ _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term))
+ _wont_move += [
+ # some last-ditch match efforts; well, xterm and aixterm is going
+ # to throw \x1b(B and other oddities all around, so, when given
+ # input such as ansi art (see test using wall.ans), and well,
+ # theres no reason a vt220 terminal shouldn't be able to recognize
+ # blue_on_red, even if it didn't cause it to be generated. these
+ # are final "ok, i will match this, anyway"
+ re.escape(u'\x1b') + r'\[(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m',
+ re.escape(u'\x1b(B'),
+ ]
+
+ # compile as regular expressions, OR'd.
+ _re_will_move = re.compile('(%s)' % ('|'.join(_will_move)))
+ _re_wont_move = re.compile('(%s)' % ('|'.join(_wont_move)))
+
+ # static pattern matching for horizontal_distance(ucs, term)
+ bnc = functools.partial(_build_numeric_capability, term)
+
+ # parm_right_cursor: Move #1 characters to the right
+ _cuf = bnc(cap='cuf', optional=True)
+ _re_cuf = re.compile(_cuf) if _cuf else None
+
+ # cursor_right: Non-destructive space (move right one space)
+ _cuf1 = term.cuf1
+
+ # parm_left_cursor: Move #1 characters to the left
+ _cub = bnc(cap='cub', optional=True)
+ _re_cub = re.compile(_cub) if _cub else None
+
+ # cursor_left: Move left one space
+ _cub1 = term.cub1
+
+ return {'_re_will_move': _re_will_move,
+ '_re_wont_move': _re_wont_move,
+ '_re_cuf': _re_cuf,
+ '_re_cub': _re_cub,
+ '_cuf1': _cuf1,
+ '_cub1': _cub1, }
+
+
+class SequenceTextWrapper(textwrap.TextWrapper):
+ def __init__(self, width, term, **kwargs):
+ self.term = term
+ textwrap.TextWrapper.__init__(self, width, **kwargs)
+
+ def _wrap_chunks(self, chunks):
+ """
+ escape-sequence aware variant of _wrap_chunks. Though
+ movement sequences, such as term.left() are certainly not
+ honored, sequences such as term.bold() are, and are not
+ broken mid-sequence.
+ """
+ lines = []
+ if self.width <= 0 or not isinstance(self.width, int):
+ raise ValueError("invalid width %r(%s) (must be integer > 0)" % (
+ self.width, type(self.width)))
+ term = self.term
+ drop_whitespace = not hasattr(self, 'drop_whitespace'
+ ) or self.drop_whitespace
+ chunks.reverse()
+ while chunks:
+ cur_line = []
+ cur_len = 0
+ if lines:
+ indent = self.subsequent_indent
+ else:
+ indent = self.initial_indent
+ width = self.width - len(indent)
+ if drop_whitespace and (
+ Sequence(chunks[-1], term).strip() == '' and lines):
+ del chunks[-1]
+ while chunks:
+ chunk_len = Sequence(chunks[-1], term).length()
+ if cur_len + chunk_len <= width:
+ cur_line.append(chunks.pop())
+ cur_len += chunk_len
+ else:
+ break
+ if chunks and Sequence(chunks[-1], term).length() > width:
+ self._handle_long_word(chunks, cur_line, cur_len, width)
+ if drop_whitespace and (
+ cur_line and Sequence(cur_line[-1], term).strip() == ''):
+ del cur_line[-1]
+ if cur_line:
+ lines.append(indent + u''.join(cur_line))
+ return lines
+
+ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
+ """_handle_long_word(chunks : [string],
+ cur_line : [string],
+ cur_len : int, width : int)
+
+ Handle a chunk of text (most likely a word, not whitespace) that
+ is too long to fit in any line.
+ """
+ # Figure out when indent is larger than the specified width, and make
+ # sure at least one character is stripped off on every pass
+ if width < 1:
+ space_left = 1
+ else:
+ space_left = width - cur_len
+
+ # If we're allowed to break long words, then do so: put as much
+ # of the next chunk onto the current line as will fit.
+ if self.break_long_words:
+ term = self.term
+ chunk = reversed_chunks[-1]
+ nxt = 0
+ for idx in range(0, len(chunk)):
+ if idx == nxt:
+ # at sequence, point beyond it,
+ nxt = idx + measure_length(chunk[idx:], term)
+ if nxt <= idx:
+ # point beyond next sequence, if any,
+ # otherwise point to next character
+ nxt = idx + measure_length(chunk[idx:], term) + 1
+ if Sequence(chunk[:nxt], term).length() > space_left:
+ break
+ else:
+ # our text ends with a sequence, such as in text
+ # u'!\x1b(B\x1b[m', set index at at end (nxt)
+ idx = nxt
+
+ cur_line.append(chunk[:idx])
+ reversed_chunks[-1] = chunk[idx:]
+
+ # Otherwise, we have to preserve the long word intact. Only add
+ # it to the current line if there's nothing already there --
+ # that minimizes how much we violate the width constraint.
+ elif not cur_line:
+ cur_line.append(reversed_chunks.pop())
+
+ # If we're not allowed to break long words, and there's already
+ # text on the current line, do nothing. Next time through the
+ # main loop of _wrap_chunks(), we'll wind up here again, but
+ # cur_len will be zero, so the next line will be entirely
+ # devoted to the long word that we can't handle right now.
+
+
+SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__
+
+
+class Sequence(text_type):
+ """
+ This unicode-derived class understands the effect of escape sequences
+ of printable length, allowing a properly implemented .rjust(), .ljust(),
+ .center(), and .len()
+ """
+
+ def __new__(cls, sequence_text, term):
+ """Sequence(sequence_text, term) -> unicode object
+
+ :arg sequence_text: A string containing sequences.
+ :arg term: Terminal instance this string was created with.
+ """
+ new = text_type.__new__(cls, sequence_text)
+ new._term = term
+ return new
+
+ def ljust(self, width, fillchar=u' '):
+ """S.ljust(width, fillchar) -> unicode
+
+ Returns string derived from unicode string ``S``, left-adjusted
+ by trailing whitespace padding ``fillchar``."""
+ rightside = fillchar * int((max(0.0, float(width - self.length())))
+ / float(len(fillchar)))
+ return u''.join((self, rightside))
+
+ def rjust(self, width, fillchar=u' '):
+ """S.rjust(width, fillchar=u'') -> unicode
+
+ Returns string derived from unicode string ``S``, right-adjusted
+ by leading whitespace padding ``fillchar``."""
+ leftside = fillchar * int((max(0.0, float(width - self.length())))
+ / float(len(fillchar)))
+ return u''.join((leftside, self))
+
+ def center(self, width, fillchar=u' '):
+ """S.center(width, fillchar=u'') -> unicode
+
+ Returns string derived from unicode string ``S``, centered
+ and surrounded with whitespace padding ``fillchar``."""
+ split = max(0.0, float(width) - self.length()) / 2
+ leftside = fillchar * int((max(0.0, math.floor(split)))
+ / float(len(fillchar)))
+ rightside = fillchar * int((max(0.0, math.ceil(split)))
+ / float(len(fillchar)))
+ return u''.join((leftside, self, rightside))
+
+ def length(self):
+ """S.length() -> int
+
+ Returns printable length of unicode string ``S`` that may contain
+ terminal sequences.
+
+ Although accounted for, strings containing sequences such as
+ ``term.clear`` will not give accurate returns, it is not
+ considered lengthy (a length of 0). Combining characters,
+ are also not considered lengthy.
+
+ Strings containing ``term.left`` or ``\b`` will cause "overstrike",
+ but a length less than 0 is not ever returned. So ``_\b+`` is a
+ length of 1 (``+``), but ``\b`` is simply a length of 0.
+
+ Some characters may consume more than one cell, mainly those CJK
+ Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode
+ as half or full-width characters.
+
+ For example:
+ >>> from blessings import Terminal
+ >>> from blessings.sequences import Sequence
+ >>> term = Terminal()
+ >>> Sequence(term.clear + term.red(u'コンニチハ')).length()
+ 5
+ """
+ # because combining characters may return -1, "clip" their length to 0.
+ clip = functools.partial(max, 0)
+ return sum(clip(wcwidth.wcwidth(w_char))
+ for w_char in self.strip_seqs())
+
+ def strip(self, chars=None):
+ """S.strip([chars]) -> unicode
+
+ Return a copy of the string S with terminal sequences removed, and
+ leading and trailing whitespace removed.
+
+ If chars is given and not None, remove characters in chars instead.
+ """
+ return self.strip_seqs().strip(chars)
+
+ def lstrip(self, chars=None):
+ """S.lstrip([chars]) -> unicode
+
+ Return a copy of the string S with terminal sequences and leading
+ whitespace removed.
+
+ If chars is given and not None, remove characters in chars instead.
+ """
+ return self.strip_seqs().lstrip(chars)
+
+ def rstrip(self, chars=None):
+ """S.rstrip([chars]) -> unicode
+
+ Return a copy of the string S with terminal sequences and trailing
+ whitespace removed.
+
+ If chars is given and not None, remove characters in chars instead.
+ """
+ return self.strip_seqs().rstrip(chars)
+
+ def strip_seqs(self):
+ """S.strip_seqs() -> unicode
+
+ Return a string without sequences for a string that contains
+ sequences for the Terminal with which they were created.
+
+ Where sequence ``move_right(n)`` is detected, it is replaced with
+ ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected,
+ those last-most characters are destroyed.
+
+ All other sequences are simply removed. An example,
+ >>> from blessings import Terminal
+ >>> from blessings.sequences import Sequence
+ >>> term = Terminal()
+ >>> Sequence(term.clear + term.red(u'test')).strip_seqs()
+ u'test'
+ """
+ # nxt: points to first character beyond current escape sequence.
+ # width: currently estimated display length.
+ input = self.padd()
+ outp = u''
+ nxt = 0
+ for idx in range(0, len(input)):
+ if idx == nxt:
+ # at sequence, point beyond it,
+ nxt = idx + measure_length(input[idx:], self._term)
+ if nxt <= idx:
+ # append non-sequence to outp,
+ outp += input[idx]
+ # point beyond next sequence, if any,
+ # otherwise point to next character
+ nxt = idx + measure_length(input[idx:], self._term) + 1
+ return outp
+
+ def padd(self):
+ """S.padd() -> unicode
+ Make non-destructive space or backspace into destructive ones.
+
+ Where sequence ``move_right(n)`` is detected, it is replaced with
+ ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is
+ detected, those last-most characters are destroyed.
+ """
+ outp = u''
+ nxt = 0
+ for idx in range(0, text_type.__len__(self)):
+ width = horizontal_distance(self[idx:], self._term)
+ if width != 0:
+ nxt = idx + measure_length(self[idx:], self._term)
+ if width > 0:
+ outp += u' ' * width
+ elif width < 0:
+ outp = outp[:width]
+ if nxt <= idx:
+ outp += self[idx]
+ nxt = idx + 1
+ return outp
+
+
+def measure_length(ucs, term):
+ """measure_length(S, term) -> int
+
+ Returns non-zero for string ``S`` that begins with a terminal sequence,
+ that is: the width of the first unprintable sequence found in S. For use
+ as a *next* pointer to skip past sequences. If string ``S`` is not a
+ sequence, 0 is returned.
+
+ A sequence may be a typical terminal sequence beginning with Escape
+ (``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``,
+ ...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in),
+ ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they
+ need only match the capabilities of attributes ``_re_will_move`` and
+ ``_re_wont_move`` of terminal ``term``.
+ """
+
+ # simple terminal control characters,
+ ctrl_seqs = u'\a\b\r\n\x0e\x0f'
+
+ if any([ucs.startswith(_ch) for _ch in ctrl_seqs]):
+ return 1
+
+ # known multibyte sequences,
+ matching_seq = term and (
+ term._re_will_move.match(ucs) or
+ term._re_wont_move.match(ucs) or
+ term._re_cub and term._re_cub.match(ucs) or
+ term._re_cuf and term._re_cuf.match(ucs)
+ )
+
+ if matching_seq:
+ start, end = matching_seq.span()
+ return end
+
+ # none found, must be printable!
+ return 0
+
+
+def termcap_distance(ucs, cap, unit, term):
+ """termcap_distance(S, cap, unit, term) -> int
+
+ Match horizontal distance by simple ``cap`` capability name, ``cub1`` or
+ ``cuf1``, with string matching the sequences identified by Terminal
+ instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and
+ left, respectively.
+
+ Otherwise, by regular expression (using dynamic regular expressions built
+ using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR
+ sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``).
+
+ Returns 0 if unmatched.
+ """
+ assert cap in ('cuf', 'cub')
+ # match cub1(left), cuf1(right)
+ one = getattr(term, '_%s1' % (cap,))
+ if one and ucs.startswith(one):
+ return unit
+
+ # match cub(n), cuf(n) using regular expressions
+ re_pattern = getattr(term, '_re_%s' % (cap,))
+ _dist = re_pattern and re_pattern.match(ucs)
+ if _dist:
+ return unit * int(_dist.group(1))
+
+ return 0
+
+
+def horizontal_distance(ucs, term):
+ """horizontal_distance(S, term) -> int
+
+ Returns Integer ``<n>`` in SGR sequence of form ``<ESC>[<n>C``
+ (T.move_right(n)), or ``-(n)`` in sequence of form ``<ESC>[<n>D``
+ (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0.
+
+ Tabstop (``\t``) cannot be correctly calculated, as the relative column
+ position cannot be determined: 8 is always (and, incorrectly) returned.
+ """
+
+ if ucs.startswith('\b'):
+ return -1
+
+ elif ucs.startswith('\t'):
+ # As best as I can prove it, a tabstop is always 8 by default.
+ # Though, given that blessings is:
+ #
+ # 1. unaware of the output device's current cursor position, and
+ # 2. unaware of the location the callee may chose to output any
+ # given string,
+ #
+ # It is not possible to determine how many cells any particular
+ # \t would consume on the output device!
+ return 8
+
+ return (termcap_distance(ucs, 'cub', -1, term) or
+ termcap_distance(ucs, 'cuf', 1, term) or
+ 0)
diff --git a/blessings/terminal.py b/blessings/terminal.py
new file mode 100644
index 0000000..60fecbe
--- /dev/null
+++ b/blessings/terminal.py
@@ -0,0 +1,783 @@
+"This primary module provides the Terminal class."
+# standard modules
+import collections
+import contextlib
+import functools
+import warnings
+import platform
+import codecs
+import curses
+import locale
+import select
+import struct
+import time
+import sys
+import os
+
+try:
+ import termios
+ import fcntl
+ import tty
+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
+else:
+ HAS_TTY = True
+
+try:
+ from io import UnsupportedOperation as IOUnsupportedOperation
+except ImportError:
+ class IOUnsupportedOperation(Exception):
+ """A dummy exception to take the place of Python 3's
+ ``io.UnsupportedOperation`` in Python 2.5"""
+
+try:
+ _ = InterruptedError
+ del _
+except NameError:
+ # alias py2 exception to py3
+ InterruptedError = select.error
+
+# local imports
+from .formatters import (
+ ParameterizingString,
+ NullCallableString,
+ resolve_capability,
+ resolve_attribute,
+)
+
+from .sequences import (
+ init_sequence_patterns,
+ SequenceTextWrapper,
+ Sequence,
+)
+
+from .keyboard import (
+ get_keyboard_sequences,
+ get_keyboard_codes,
+ resolve_sequence,
+)
+
+
+class Terminal(object):
+ """A wrapper for curses and related terminfo(5) terminal capabilities.
+
+ Instance attributes:
+
+ ``stream``
+ The stream the terminal outputs to. It's convenient to pass the stream
+ around with the terminal; it's almost always needed when the terminal
+ is and saves sticking lots of extra args on client functions in
+ practice.
+ """
+
+ #: Sugary names for commonly-used capabilities
+ _sugar = dict(
+ save='sc',
+ restore='rc',
+ # 'clear' clears the whole screen.
+ clear_eol='el',
+ clear_bol='el1',
+ clear_eos='ed',
+ position='cup', # deprecated
+ enter_fullscreen='smcup',
+ exit_fullscreen='rmcup',
+ move='cup',
+ move_x='hpa',
+ move_y='vpa',
+ move_left='cub1',
+ move_right='cuf1',
+ move_up='cuu1',
+ move_down='cud1',
+ hide_cursor='civis',
+ normal_cursor='cnorm',
+ reset_colors='op', # oc doesn't work on my OS X terminal.
+ normal='sgr0',
+ reverse='rev',
+ italic='sitm',
+ no_italic='ritm',
+ shadow='sshm',
+ no_shadow='rshm',
+ standout='smso',
+ no_standout='rmso',
+ subscript='ssubm',
+ no_subscript='rsubm',
+ superscript='ssupm',
+ no_superscript='rsupm',
+ underline='smul',
+ no_underline='rmul')
+
+ def __init__(self, kind=None, stream=None, force_styling=False):
+ """Initialize the terminal.
+
+ If ``stream`` is not a tty, I will default to returning an empty
+ Unicode string for all capability values, so things like piping your
+ output to a file won't strew escape sequences all over the place. The
+ ``ls`` command sets a precedent for this: it defaults to columnar
+ output when being sent to a tty and one-item-per-line when not.
+
+ :arg kind: A terminal string as taken by ``setupterm()``. Defaults to
+ the value of the ``TERM`` environment variable.
+ :arg stream: A file-like object representing the terminal. Defaults to
+ the original value of stdout, like ``curses.initscr()`` does.
+ :arg force_styling: Whether to force the emission of capabilities, even
+ if we don't seem to be in a terminal. This comes in handy if users
+ are trying to pipe your output through something like ``less -r``,
+ which supports terminal codes just fine but doesn't appear itself
+ to be a terminal. Just expose a command-line option, and set
+ ``force_styling`` based on it. Terminal initialization sequences
+ will be sent to ``stream`` if it has a file descriptor and to
+ ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them
+ somewhere, and stdout is probably where the output is ultimately
+ headed. If not, stderr is probably bound to the same terminal.)
+
+ If you want to force styling to not happen, pass
+ ``force_styling=None``.
+
+ """
+ global _CUR_TERM
+ self.keyboard_fd = None
+
+ # default stream is stdout, keyboard only valid as stdin when
+ # output stream is stdout and output stream is a tty
+ if stream is None or stream == sys.__stdout__:
+ stream = sys.__stdout__
+ self.keyboard_fd = sys.__stdin__.fileno()
+
+ try:
+ stream_fd = (stream.fileno() if hasattr(stream, 'fileno')
+ and callable(stream.fileno) else None)
+ except IOUnsupportedOperation:
+ stream_fd = None
+
+ self._is_a_tty = stream_fd is not None and os.isatty(stream_fd)
+ self._does_styling = ((self.is_a_tty or force_styling) and
+ force_styling is not None)
+
+ # keyboard_fd only non-None if both stdin and stdout is a tty.
+ self.keyboard_fd = (self.keyboard_fd
+ if self.keyboard_fd is not None and
+ self.is_a_tty and os.isatty(self.keyboard_fd)
+ else None)
+ self._normal = None # cache normal attr, preventing recursive lookups
+
+ # The descriptor to direct terminal initialization sequences to.
+ # sys.__stdout__ seems to always have a descriptor of 1, even if output
+ # is redirected.
+ self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno()
+ or stream_fd)
+ self._kind = kind or os.environ.get('TERM', 'unknown')
+
+ if self.does_styling:
+ # Make things like tigetstr() work. Explicit args make setupterm()
+ # work even when -s is passed to nosetests. Lean toward sending
+ # init sequences to the stream if it has a file descriptor, and
+ # send them to stdout as a fallback, since they have to go
+ # somewhere.
+ try:
+ if (platform.python_implementation() == 'PyPy' and
+ isinstance(self._kind, unicode)):
+ # pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131
+ # TypeError: initializer for ctype 'char *' must be a str
+ curses.setupterm(self._kind.encode('ascii'), self._init_descriptor)
+ else:
+ curses.setupterm(self._kind, self._init_descriptor)
+ except curses.error as err:
+ warnings.warn('Failed to setupterm(kind={0!r}): {1}'
+ .format(self._kind, err))
+ self._kind = None
+ self._does_styling = False
+ else:
+ if _CUR_TERM is None or self._kind == _CUR_TERM:
+ _CUR_TERM = self._kind
+ else:
+ warnings.warn(
+ 'A terminal of kind "%s" has been requested; due to an'
+ ' internal python curses bug, terminal capabilities'
+ ' for a terminal of kind "%s" will continue to be'
+ ' returned for the remainder of this process.' % (
+ self._kind, _CUR_TERM,))
+
+ for re_name, re_val in init_sequence_patterns(self).items():
+ setattr(self, re_name, re_val)
+
+ # build database of int code <=> KEY_NAME
+ self._keycodes = get_keyboard_codes()
+
+ # store attributes as: self.KEY_NAME = code
+ for key_code, key_name in self._keycodes.items():
+ setattr(self, key_name, key_code)
+
+ # build database of sequence <=> KEY_NAME
+ self._keymap = get_keyboard_sequences(self)
+
+ self._keyboard_buf = collections.deque()
+ if self.keyboard_fd is not None:
+ locale.setlocale(locale.LC_ALL, '')
+ self._encoding = locale.getpreferredencoding() or 'ascii'
+ try:
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
+ except LookupError as err:
+ warnings.warn('%s, fallback to ASCII for keyboard.' % (err,))
+ self._encoding = 'ascii'
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
+
+ self.stream = stream
+
+ def __getattr__(self, attr):
+ """Return a terminal capability as Unicode string.
+
+ For example, ``term.bold`` is a unicode string that may be prepended
+ to text to set the video attribute for bold, which should also be
+ terminated with the pairing ``term.normal``.
+
+ This capability is also callable, so you can use ``term.bold("hi")``
+ which results in the joining of (term.bold, "hi", term.normal).
+
+ Compound formatters may also be used, for example:
+ ``term.bold_blink_red_on_green("merry x-mas!")``.
+
+ For a parametrized capability such as ``cup`` (cursor_address), pass
+ the parameters as arguments ``some_term.cup(line, column)``. See
+ manual page terminfo(5) for a complete list of capabilities.
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ val = resolve_attribute(self, attr)
+ # Cache capability codes.
+ setattr(self, attr, val)
+ return val
+
+ @property
+ def kind(self):
+ """Name of this terminal type as string."""
+ return self._kind
+
+ @property
+ def does_styling(self):
+ """Whether this instance will emit terminal sequences (bool)."""
+ return self._does_styling
+
+ @property
+ def is_a_tty(self):
+ """Whether the ``stream`` associated with this instance is a terminal
+ (bool)."""
+ return self._is_a_tty
+
+ @property
+ def height(self):
+ """T.height -> int
+
+ The height of the terminal in characters.
+ """
+ return self._height_and_width().ws_row
+
+ @property
+ def width(self):
+ """T.width -> int
+
+ The width of the terminal in characters.
+ """
+ return self._height_and_width().ws_col
+
+ @staticmethod
+ def _winsize(fd):
+ """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel)
+
+ The tty connected by file desriptor fd is queried for its window size,
+ and returned as a collections.namedtuple instance WINSZ.
+
+ May raise exception IOError.
+ """
+ if HAS_TTY:
+ data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF)
+ return WINSZ(*struct.unpack(WINSZ._FMT, data))
+ return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0)
+
+ def _height_and_width(self):
+ """Return a tuple of (terminal height, terminal width).
+ """
+ # TODO(jquast): hey kids, even if stdout is redirected to a file,
+ # we can still query sys.__stdin__.fileno() for our terminal size.
+ # -- of course, if both are redirected, we have no use for this fd.
+ for fd in (self._init_descriptor, sys.__stdout__):
+ try:
+ if fd is not None:
+ return self._winsize(fd)
+ except IOError:
+ pass
+
+ return WINSZ(ws_row=int(os.getenv('LINES', '25')),
+ ws_col=int(os.getenv('COLUMNS', '80')),
+ ws_xpixel=None,
+ ws_ypixel=None)
+
+ @contextlib.contextmanager
+ def location(self, x=None, y=None):
+ """Return a context manager for temporarily moving the cursor.
+
+ Move the cursor to a certain position on entry, let you print stuff
+ there, then return the cursor to its original position::
+
+ term = Terminal()
+ with term.location(2, 5):
+ print 'Hello, world!'
+ for x in xrange(10):
+ print 'I can do it %i times!' % x
+
+ Specify ``x`` to move to a certain column, ``y`` to move to a certain
+ row, both, or neither. If you specify neither, only the saving and
+ restoration of cursor position will happen. This can be useful if you
+ simply want to restore your place after doing some manual cursor
+ movement.
+
+ """
+ # Save position and move to the requested column, row, or both:
+ self.stream.write(self.save)
+ if x is not None and y is not None:
+ self.stream.write(self.move(y, x))
+ elif x is not None:
+ self.stream.write(self.move_x(x))
+ elif y is not None:
+ self.stream.write(self.move_y(y))
+ try:
+ yield
+ finally:
+ # Restore original cursor position:
+ self.stream.write(self.restore)
+
+ @contextlib.contextmanager
+ def fullscreen(self):
+ """Return a context manager that enters fullscreen mode while inside it
+ and restores normal mode on leaving.
+
+ Fullscreen mode is characterized by instructing the terminal emulator
+ to store and save the current screen state (all screen output), switch
+ to "alternate screen". Upon exiting, the previous screen state is
+ returned.
+
+ This call may not be tested; only one screen state may be saved at a
+ time.
+ """
+ self.stream.write(self.enter_fullscreen)
+ try:
+ yield
+ finally:
+ self.stream.write(self.exit_fullscreen)
+
+ @contextlib.contextmanager
+ def hidden_cursor(self):
+ """Return a context manager that hides the cursor upon entering,
+ and makes it visible again upon exiting."""
+ self.stream.write(self.hide_cursor)
+ try:
+ yield
+ finally:
+ self.stream.write(self.normal_cursor)
+
+ @property
+ def color(self):
+ """Returns capability that sets the foreground color.
+
+ The capability is unparameterized until called and passed a number
+ (0-15), at which point it returns another string which represents a
+ specific color change. This second string can further be called to
+ color a piece of text and set everything back to normal afterward.
+
+ :arg num: The number, 0-15, of the color
+
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._foreground_color,
+ self.normal, 'color')
+
+ @property
+ def on_color(self):
+ "Returns capability that sets the background color."
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._background_color,
+ self.normal, 'on_color')
+
+ @property
+ def normal(self):
+ "Returns sequence that resets video attribute."
+ if self._normal:
+ return self._normal
+ self._normal = resolve_capability(self, 'normal')
+ return self._normal
+
+ @property
+ def number_of_colors(self):
+ """Return the number of colors the terminal supports.
+
+ Common values are 0, 8, 16, 88, and 256. Most commonly
+ this may be used to test color capabilities at all::
+
+ if term.number_of_colors:
+ ..."""
+ # trim value to 0, as tigetnum('colors') returns -1 if no support,
+ # -2 if no such capability.
+ return max(0, self.does_styling and curses.tigetnum('colors') or -1)
+
+ @property
+ def _foreground_color(self):
+ return self.setaf or self.setf
+
+ @property
+ def _background_color(self):
+ return self.setab or self.setb
+
+ def ljust(self, text, width=None, fillchar=u' '):
+ """T.ljust(text, [width], [fillchar]) -> unicode
+
+ Return string ``text``, left-justified by printable length ``width``.
+ Padding is done using the specified fill character (default is a
+ space). Default ``width`` is the attached terminal's width. ``text``
+ may contain terminal sequences."""
+ if width is None:
+ width = self.width
+ return Sequence(text, self).ljust(width, fillchar)
+
+ def rjust(self, text, width=None, fillchar=u' '):
+ """T.rjust(text, [width], [fillchar]) -> unicode
+
+ Return string ``text``, right-justified by printable length ``width``.
+ Padding is done using the specified fill character (default is a
+ space). Default ``width`` is the attached terminal's width. ``text``
+ may contain terminal sequences."""
+ if width is None:
+ width = self.width
+ return Sequence(text, self).rjust(width, fillchar)
+
+ def center(self, text, width=None, fillchar=u' '):
+ """T.center(text, [width], [fillchar]) -> unicode
+
+ Return string ``text``, centered by printable length ``width``.
+ Padding is done using the specified fill character (default is a
+ space). Default ``width`` is the attached terminal's width. ``text``
+ may contain terminal sequences."""
+ if width is None:
+ width = self.width
+ return Sequence(text, self).center(width, fillchar)
+
+ def length(self, text):
+ """T.length(text) -> int
+
+ Return the printable length of string ``text``, which may contain
+ terminal sequences. Strings containing sequences such as 'clear',
+ which repositions the cursor, does not give accurate results, and
+ their printable length is evaluated *0*..
+ """
+ return Sequence(text, self).length()
+
+ def strip(self, text, chars=None):
+ """T.strip(text) -> unicode
+
+ Return string ``text`` with terminal sequences removed, and leading
+ and trailing whitespace removed.
+ """
+ return Sequence(text, self).strip(chars)
+
+ def rstrip(self, text, chars=None):
+ """T.rstrip(text) -> unicode
+
+ Return string ``text`` with terminal sequences and trailing whitespace
+ removed.
+ """
+ return Sequence(text, self).rstrip(chars)
+
+ def lstrip(self, text, chars=None):
+ """T.lstrip(text) -> unicode
+
+ Return string ``text`` with terminal sequences and leading whitespace
+ removed.
+ """
+ return Sequence(text, self).lstrip(chars)
+
+ def strip_seqs(self, text):
+ """T.strip_seqs(text) -> unicode
+
+ Return string ``text`` stripped only of its sequences.
+ """
+ return Sequence(text, self).strip_seqs()
+
+ def wrap(self, text, width=None, **kwargs):
+ """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode]
+
+ Wrap paragraphs containing escape sequences ``text`` to the full
+ ``width`` of Terminal instance *T*, unless ``width`` is specified.
+ Wrapped by the virtual printable length, irregardless of the video
+ attribute sequences it may contain, allowing text containing colors,
+ bold, underline, etc. to be wrapped.
+
+ Returns a list of strings that may contain escape sequences. See
+ ``textwrap.TextWrapper`` for all available additional kwargs to
+ customize wrapping behavior such as ``subsequent_indent``.
+ """
+ width = self.width if width is None else width
+ lines = []
+ for line in text.splitlines():
+ lines.extend(
+ (_linewrap for _linewrap in SequenceTextWrapper(
+ width=width, term=self, **kwargs).wrap(text))
+ if line.strip() else (u'',))
+
+ return lines
+
+ def _next_char(self):
+ """T._next_char() -> unicode
+
+ Read and decode next byte from keyboard stream. May return u''
+ if decoding is not yet complete, or completed unicode character.
+ Should always return bytes when self._char_is_ready() returns True.
+
+ Implementors of input streams other than os.read() on the stdin fd
+ should derive and override this method.
+ """
+ assert self.keyboard_fd is not None
+ byte = os.read(self.keyboard_fd, 1)
+ return self._keyboard_decoder.decode(byte, final=False)
+
+ def _char_is_ready(self, timeout=None, interruptable=True):
+ """T._char_is_ready([timeout=None]) -> bool
+
+ Returns True if a keypress has been detected on keyboard.
+
+ When ``timeout`` is 0, this call is non-blocking, Otherwise blocking
+ until keypress is detected (default). When ``timeout`` is a positive
+ number, returns after ``timeout`` seconds have elapsed.
+
+ If input is not a terminal, False is always returned.
+ """
+ # Special care is taken to handle a custom SIGWINCH handler, which
+ # causes select() to be interrupted with errno 4 (EAGAIN) --
+ # it is ignored, and a new timeout value is derived from the previous,
+ # unless timeout becomes negative, because signal handler has blocked
+ # beyond timeout, then False is returned. Otherwise, when timeout is 0,
+ # we continue to block indefinitely (default).
+ stime = time.time()
+ check_w, check_x, ready_r = [], [], [None, ]
+ check_r = [self.keyboard_fd] if self.keyboard_fd is not None else []
+
+ while HAS_TTY and True:
+ try:
+ ready_r, ready_w, ready_x = select.select(
+ check_r, check_w, check_x, timeout)
+ except InterruptedError:
+ if not interruptable:
+ return u''
+ if timeout is not None:
+ # subtract time already elapsed,
+ timeout -= time.time() - stime
+ if timeout > 0:
+ continue
+ # no time remains after handling exception (rare)
+ ready_r = []
+ break
+ else:
+ break
+
+ return False if self.keyboard_fd is None else check_r == ready_r
+
+ @contextlib.contextmanager
+ def keystroke_input(self, raw=False):
+ """Return a context manager that sets up the terminal to do
+ key-at-a-time input.
+
+ On entering the context manager, "cbreak" mode is activated, disabling
+ line buffering of keyboard input and turning off automatic echoing of
+ input. (You must explicitly print any input if you'd like it shown.)
+ Also referred to as 'rare' mode, this is the opposite of 'cooked' mode,
+ the default for most shells.
+
+ If ``raw`` is True, enter "raw" mode instead. Raw mode differs in that
+ the interrupt, quit, suspend, and flow control characters are all
+ passed through as their raw character values instead of generating a
+ signal.
+
+ More information can be found in the manual page for curses.h,
+ http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak
+
+ The python manual for curses,
+ http://docs.python.org/2/library/curses.html
+
+ Note also that setcbreak sets VMIN = 1 and VTIME = 0,
+ http://www.unixwiz.net/techtips/termios-vmin-vtime.html
+ """
+ if HAS_TTY and self.keyboard_fd is not None:
+ # Save current terminal mode:
+ save_mode = termios.tcgetattr(self.keyboard_fd)
+ mode_setter = tty.setraw if raw else tty.setcbreak
+ mode_setter(self.keyboard_fd, termios.TCSANOW)
+ try:
+ yield
+ finally:
+ # Restore prior mode:
+ termios.tcsetattr(self.keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def keypad(self):
+ """
+ Context manager that enables keypad input (*keyboard_transmit* mode).
+
+ This enables the effect of calling the curses function keypad(3x):
+ display terminfo(5) capability `keypad_xmit` (smkx) upon entering,
+ and terminfo(5) capability `keypad_local` (rmkx) upon exiting.
+
+ On an IBM-PC keypad of ttype *xterm*, with numlock off, the
+ lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``.
+
+ However, upon entering keypad mode, ``\\x1b[OF`` is transmitted,
+ translating to ``KEY_LL`` (lower-left key), allowing diagonal
+ direction keys to be determined.
+ """
+ try:
+ self.stream.write(self.smkx)
+ yield
+ finally:
+ self.stream.write(self.rmkx)
+
+ def keystroke(self, timeout=None, esc_delay=0.35, interruptable=True):
+ """T.keystroke(timeout=None, [esc_delay, [interruptable]]) -> Keystroke
+
+ Receive next keystroke from keyboard (stdin), blocking until a
+ keypress is received or ``timeout`` elapsed, if specified.
+
+ When used without the context manager ``cbreak``, stdin remains
+ line-buffered, and this function will block until return is pressed,
+ even though only one unicode character is returned at a time..
+
+ The value returned is an instance of ``Keystroke``, with properties
+ ``is_sequence``, and, when True, non-None values for attributes
+ ``code`` and ``name``. The value of ``code`` may be compared against
+ attributes of this terminal beginning with *KEY*, such as
+ ``KEY_ESCAPE``.
+
+ To distinguish between ``KEY_ESCAPE`` and sequences beginning with
+ escape, the ``esc_delay`` specifies the amount of time after receiving
+ the escape character (chr(27)) to seek for the completion
+ of other application keys before returning ``KEY_ESCAPE``.
+
+ Normally, when this function is interrupted by a signal, such as the
+ installment of SIGWINCH, this function will ignore this interruption
+ and continue to poll for input up to the ``timeout`` specified. If
+ you'd rather this function return ``u''`` early, specify ``False`` for
+ ``interruptable``.
+ """
+ # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1',
+ # what do we do with that? Surely, something useful.
+ # comparator to term.KEY_meta('x') ?
+ # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest;
+ # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate
+ # attributes. comparator to term.KEY_ctrl('z') ?
+
+ if timeout is None and self.keyboard_fd is None:
+ raise NoKeyboard(
+ 'Waiting for a keystroke on a terminal with no keyboard '
+ 'attached and no timeout would take a long time. Add a '
+ 'timeout and revise your program logic.')
+
+ def time_left(stime, timeout):
+ """time_left(stime, timeout) -> float
+
+ Returns time-relative time remaining before ``timeout``
+ after time elapsed since ``stime``.
+ """
+ if timeout is not None:
+ if timeout == 0:
+ return 0
+ return max(0, timeout - (time.time() - stime))
+
+ resolve = functools.partial(resolve_sequence,
+ mapper=self._keymap,
+ codes=self._keycodes)
+
+ stime = time.time()
+
+ # re-buffer previously received keystrokes,
+ ucs = u''
+ while self._keyboard_buf:
+ ucs += self._keyboard_buf.pop()
+
+ # receive all immediately available bytes
+ while self._char_is_ready(0):
+ ucs += self._next_char()
+
+ # decode keystroke, if any
+ ks = resolve(text=ucs)
+
+ # so long as the most immediately received or buffered keystroke is
+ # incomplete, (which may be a multibyte encoding), block until until
+ # one is received.
+ while not ks and self._char_is_ready(time_left(stime, timeout),
+ interruptable):
+ ucs += self._next_char()
+ ks = resolve(text=ucs)
+
+ # handle escape key (KEY_ESCAPE) vs. escape sequence (which begins
+ # with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when
+ # received. This is not optimal, but causes least delay when
+ # (currently unhandled, and rare) "meta sends escape" is used,
+ # or when an unsupported sequence is sent.
+ if ks.code == self.KEY_ESCAPE:
+ esctime = time.time()
+ while (ks.code == self.KEY_ESCAPE and
+ self._char_is_ready(time_left(esctime, esc_delay))):
+ ucs += self._next_char()
+ ks = resolve(text=ucs)
+
+ # buffer any remaining text received
+ self._keyboard_buf.extendleft(ucs[len(ks):])
+ return ks
+
+
+class NoKeyboard(Exception):
+ """Exception raised when a Terminal that has no means of input connected is
+ asked to retrieve a keystroke without an infinite timeout."""
+
+
+# From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al):
+#
+# "After the call to setupterm(), the global variable cur_term is set to
+# point to the current structure of terminal capabilities. By calling
+# setupterm() for each terminal, and saving and restoring cur_term, it
+# is possible for a program to use two or more terminals at once."
+#
+# However, if you study Python's ./Modules/_cursesmodule.c, you'll find:
+#
+# if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) {
+#
+# Python - perhaps wrongly - will not allow for re-initialisation of new
+# terminals through setupterm(), so the value of cur_term cannot be changed
+# once set: subsequent calls to setupterm() have no effect.
+#
+# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton.
+# This global variable reflects that, and a warning is emitted if somebody
+# expects otherwise.
+
+_CUR_TERM = None
+
+WINSZ = collections.namedtuple('WINSZ', (
+ 'ws_row', # /* rows, in characters */
+ 'ws_col', # /* columns, in characters */
+ 'ws_xpixel', # /* horizontal size, pixels */
+ 'ws_ypixel', # /* vertical size, pixels */
+))
+#: format of termios structure
+WINSZ._FMT = 'hhhh'
+#: buffer of termios structure appropriate for ioctl argument
+WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT)
diff --git a/blessings/tests/__init__.py b/blessings/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/blessings/tests/__init__.py
diff --git a/blessings/tests/accessories.py b/blessings/tests/accessories.py
new file mode 100644
index 0000000..8cb3f5b
--- /dev/null
+++ b/blessings/tests/accessories.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+"""Accessories for automated py.test runner."""
+# std
+from __future__ import with_statement
+import contextlib
+import subprocess
+import functools
+import traceback
+import termios
+import random
+import codecs
+import curses
+import sys
+import pty
+import os
+
+# local
+from blessings import Terminal
+
+# 3rd
+import pytest
+
+if sys.version_info[0] == 3:
+ text_type = str
+else:
+ text_type = unicode # noqa
+
+TestTerminal = functools.partial(Terminal, kind='xterm-256color')
+SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n'
+RECV_SEMAPHORE = b'SEMAPHORE\r\n'
+all_xterms_params = ['xterm', 'xterm-256color']
+many_lines_params = [30, 100]
+many_columns_params = [1, 10]
+from blessings._binterms import binary_terminals
+default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi']
+if os.environ.get('TEST_ALLTERMS'):
+ try:
+ available_terms = [
+ _term.split(None, 1)[0] for _term in
+ subprocess.Popen(('toe', '-a'),
+ stdout=subprocess.PIPE,
+ close_fds=True)
+ .communicate()[0].splitlines()]
+ except OSError:
+ all_terms_params = default_all_terms
+else:
+ available_terms = default_all_terms
+all_terms_params = list(set(available_terms) - (
+ set(binary_terminals) if not os.environ.get('TEST_BINTERMS')
+ else set())) or default_all_terms
+
+
+class as_subprocess(object):
+ """This helper executes test cases in a child process,
+ avoiding a python-internal bug of _curses: setupterm()
+ may not be called more than once per process.
+ """
+ _CHILD_PID = 0
+ encoding = 'utf8'
+
+ def __init__(self, func):
+ self.func = func
+
+ def __call__(self, *args, **kwargs):
+ pid, master_fd = pty.fork()
+ if pid is self._CHILD_PID:
+ # child process executes function, raises exception
+ # if failed, causing a non-zero exit code, using the
+ # protected _exit() function of ``os``; to prevent the
+ # 'SystemExit' exception from being thrown.
+ try:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ self.func(*args, **kwargs)
+ except Exception:
+ e_type, e_value, e_tb = sys.exc_info()
+ o_err = list()
+ for line in traceback.format_tb(e_tb):
+ o_err.append(line.rstrip().encode('utf-8'))
+ o_err.append(('-=' * 20).encode('ascii'))
+ o_err.extend([_exc.rstrip().encode('utf-8') for _exc in
+ traceback.format_exception_only(
+ e_type, e_value)])
+ os.write(sys.__stdout__.fileno(), b'\n'.join(o_err))
+ os.close(sys.__stdout__.fileno())
+ os.close(sys.__stderr__.fileno())
+ os.close(sys.__stdin__.fileno())
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(1)
+ else:
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ exc_output = text_type()
+ decoder = codecs.getincrementaldecoder(self.encoding)()
+ while True:
+ try:
+ _exc = os.read(master_fd, 65534)
+ except OSError:
+ # linux EOF
+ break
+ if not _exc:
+ # bsd EOF
+ break
+ exc_output += decoder.decode(_exc)
+
+ # parent process asserts exit code is 0, causing test
+ # to fail if child process raised an exception/assertion
+ pid, status = os.waitpid(pid, 0)
+ os.close(master_fd)
+
+ # Display any output written by child process
+ # (esp. any AssertionError exceptions written to stderr).
+ exc_output_msg = 'Output in child process:\n%s\n%s\n%s' % (
+ u'=' * 40, exc_output, u'=' * 40,)
+ assert exc_output == '', exc_output_msg
+
+ # Also test exit status is non-zero
+ assert os.WEXITSTATUS(status) == 0
+
+
+def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE,
+ encoding='utf8', timeout=10):
+ """Read file descriptor ``fd`` until ``semaphore`` is found.
+
+ Used to ensure the child process is awake and ready. For timing
+ tests; without a semaphore, the time to fork() would be (incorrectly)
+ included in the duration of the test, which can be very length on
+ continuous integration servers (such as Travis-CI).
+ """
+ # note that when a child process writes xyz\\n, the parent
+ # process will read xyz\\r\\n -- this is how pseudo terminals
+ # behave; a virtual terminal requires both carriage return and
+ # line feed, it is only for convenience that \\n does both.
+ outp = text_type()
+ decoder = codecs.getincrementaldecoder(encoding)()
+ semaphore = semaphore.decode('ascii')
+ while not outp.startswith(semaphore):
+ try:
+ _exc = os.read(fd, 1)
+ except OSError: # Linux EOF
+ break
+ if not _exc: # BSD EOF
+ break
+ outp += decoder.decode(_exc, final=False)
+ assert outp.startswith(semaphore), (
+ 'Semaphore not recv before EOF '
+ '(expected: %r, got: %r)' % (semaphore, outp,))
+ return outp[len(semaphore):]
+
+
+def read_until_eof(fd, encoding='utf8'):
+ """Read file descriptor ``fd`` until EOF. Return decoded string."""
+ decoder = codecs.getincrementaldecoder(encoding)()
+ outp = text_type()
+ while True:
+ try:
+ _exc = os.read(fd, 100)
+ except OSError: # linux EOF
+ break
+ if not _exc: # bsd EOF
+ break
+ outp += decoder.decode(_exc, final=False)
+ return outp
+
+
+@contextlib.contextmanager
+def echo_off(fd):
+ """Ensure any bytes written to pty fd are not duplicated as output."""
+ try:
+ attrs = termios.tcgetattr(fd)
+ attrs[3] = attrs[3] & ~termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
+ yield
+ finally:
+ attrs[3] = attrs[3] | termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
+
+
+def unicode_cap(cap):
+ """Return the result of ``tigetstr`` except as Unicode."""
+ try:
+ val = curses.tigetstr(cap)
+ except curses.error:
+ val = None
+ if val:
+ return val.decode('latin1')
+ return u''
+
+
+def unicode_parm(cap, *parms):
+ """Return the result of ``tparm(tigetstr())`` except as Unicode."""
+ try:
+ cap = curses.tigetstr(cap)
+ except curses.error:
+ cap = None
+ if cap:
+ try:
+ val = curses.tparm(cap, *parms)
+ except curses.error:
+ val = None
+ if val:
+ return val.decode('latin1')
+ return u''
+
+
+@pytest.fixture(params=binary_terminals)
+def unsupported_sequence_terminals(request):
+ """Terminals that emit warnings for unsupported sequence-awareness."""
+ return request.param
+
+
+@pytest.fixture(params=all_xterms_params)
+def xterms(request):
+ """Common kind values for xterm terminals."""
+ return request.param
+
+
+@pytest.fixture(params=all_terms_params)
+def all_terms(request):
+ """Common kind values for all kinds of terminals."""
+ return request.param
+
+
+@pytest.fixture(params=many_lines_params)
+def many_lines(request):
+ """Various number of lines for screen height."""
+ return request.param
+
+
+@pytest.fixture(params=many_columns_params)
+def many_columns(request):
+ """Various number of columns for screen width."""
+ return request.param
diff --git a/blessings/tests/test_core.py b/blessings/tests/test_core.py
new file mode 100644
index 0000000..ce1e838
--- /dev/null
+++ b/blessings/tests/test_core.py
@@ -0,0 +1,457 @@
+# -*- coding: utf-8 -*-
+"Core blessings Terminal() tests."
+
+# std
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+import collections
+import warnings
+import platform
+import locale
+import sys
+import imp
+import os
+
+# local
+from .accessories import (
+ as_subprocess,
+ TestTerminal,
+ unicode_cap,
+ all_terms
+)
+
+# 3rd party
+import mock
+import pytest
+
+
+def test_export_only_Terminal():
+ "Ensure only Terminal instance is exported for import * statements."
+ import blessings
+ assert blessings.__all__ == ('Terminal',)
+
+
+def test_null_location(all_terms):
+ "Make sure ``location()`` with no args just does position restoration."
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ with t.location():
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'), unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_flipped_location_move(all_terms):
+ "``location()`` and ``move()`` receive counter-example arguments."
+ @as_subprocess
+ def child(kind):
+ buf = StringIO()
+ t = TestTerminal(stream=buf, force_styling=True)
+ y, x = 10, 20
+ with t.location(y, x):
+ xy_val = t.move(x, y)
+ yx_val = buf.getvalue()[len(t.sc):]
+ assert xy_val == yx_val
+
+ child(all_terms)
+
+
+def test_yield_keypad():
+ "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local."
+ @as_subprocess
+ def child(kind):
+ # given,
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ expected_output = u''.join((t.smkx, t.rmkx))
+
+ # exercise,
+ with t.keypad():
+ pass
+
+ # verify.
+ assert (t.stream.getvalue() == expected_output)
+
+ child(kind='xterm')
+
+
+def test_null_fileno():
+ "Make sure ``Terminal`` works when ``fileno`` is ``None``."
+ @as_subprocess
+ def child():
+ # This simulates piping output to another program.
+ out = StringIO()
+ out.fileno = None
+ t = TestTerminal(stream=out)
+ assert (t.save == u'')
+
+ child()
+
+
+def test_number_of_colors_without_tty():
+ "``number_of_colors`` should return 0 when there's no tty."
+ @as_subprocess
+ def child_256_nostyle():
+ t = TestTerminal(stream=StringIO())
+ assert (t.number_of_colors == 0)
+
+ @as_subprocess
+ def child_256_forcestyle():
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ assert (t.number_of_colors == 256)
+
+ @as_subprocess
+ def child_8_forcestyle():
+ t = TestTerminal(kind='ansi', stream=StringIO(),
+ force_styling=True)
+ assert (t.number_of_colors == 8)
+
+ @as_subprocess
+ def child_0_forcestyle():
+ t = TestTerminal(kind='vt220', stream=StringIO(),
+ force_styling=True)
+ assert (t.number_of_colors == 0)
+
+ child_0_forcestyle()
+ child_8_forcestyle()
+ child_256_forcestyle()
+ child_256_nostyle()
+
+
+def test_number_of_colors_with_tty():
+ "test ``number_of_colors`` 0, 8, and 256."
+ @as_subprocess
+ def child_256():
+ t = TestTerminal()
+ assert (t.number_of_colors == 256)
+
+ @as_subprocess
+ def child_8():
+ t = TestTerminal(kind='ansi')
+ assert (t.number_of_colors == 8)
+
+ @as_subprocess
+ def child_0():
+ t = TestTerminal(kind='vt220')
+ assert (t.number_of_colors == 0)
+
+ child_0()
+ child_8()
+ child_256()
+
+
+def test_init_descriptor_always_initted(all_terms):
+ "Test height and width with non-tty Terminals."
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO())
+ assert t._init_descriptor == sys.__stdout__.fileno()
+ assert (isinstance(t.height, int))
+ assert (isinstance(t.width, int))
+ assert t.height == t._height_and_width()[0]
+ assert t.width == t._height_and_width()[1]
+
+ child(all_terms)
+
+
+def test_force_styling_none(all_terms):
+ "If ``force_styling=None`` is used, don't ever do styling."
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, force_styling=None)
+ assert (t.save == '')
+ assert (t.color(9) == '')
+ assert (t.bold('oi') == 'oi')
+
+ child(all_terms)
+
+
+def test_setupterm_singleton_issue33():
+ "A warning is emitted if a new terminal ``kind`` is used per process."
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("error", category=UserWarning)
+
+ # instantiate first terminal, of type xterm-256color
+ term = TestTerminal(force_styling=True)
+
+ try:
+ # a second instantiation raises UserWarning
+ term = TestTerminal(kind="vt220", force_styling=True)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert (err.args[0].startswith(
+ 'A terminal of kind "vt220" has been requested')
+ ), err.args[0]
+ assert ('a terminal of kind "xterm-256color" will '
+ 'continue to be returned' in err.args[0]), err.args[0]
+ else:
+ # unless term is not a tty and setupterm() is not called
+ assert not term.is_a_tty or False, 'Should have thrown exception'
+ warnings.resetwarnings()
+
+ child()
+
+
+def test_setupterm_invalid_issue39():
+ "A warning is emitted if TERM is invalid."
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=878089
+
+ # if TERM is unset, defaults to 'unknown', which should
+ # fail to lookup and emit a warning, only.
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("error", category=UserWarning)
+
+ try:
+ term = TestTerminal(kind='unknown', force_styling=True)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0] == (
+ "Failed to setupterm(kind='unknown'): "
+ "setupterm: could not find terminal")
+ else:
+ assert not term.is_a_tty and not term.does_styling, (
+ 'Should have thrown exception')
+ warnings.resetwarnings()
+
+ child()
+
+
+def test_setupterm_invalid_has_no_styling():
+ "An unknown TERM type does not perform styling."
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=878089
+
+ # if TERM is unset, defaults to 'unknown', which should
+ # fail to lookup and emit a warning, only.
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("ignore", category=UserWarning)
+
+ term = TestTerminal(kind='unknown', force_styling=True)
+ assert term.kind is None
+ assert term.does_styling is False
+ assert term.number_of_colors == 0
+ warnings.resetwarnings()
+
+ child()
+
+
+@pytest.mark.skipif(platform.python_implementation() == 'PyPy',
+ reason='PyPy freezes')
+def test_missing_ordereddict_uses_module(monkeypatch):
+ "ordereddict module is imported when without collections.OrderedDict."
+ import blessings.keyboard
+
+ if hasattr(collections, 'OrderedDict'):
+ monkeypatch.delattr('collections.OrderedDict')
+
+ try:
+ imp.reload(blessings.keyboard)
+ except ImportError as err:
+ assert err.args[0] in ("No module named ordereddict", # py2
+ "No module named 'ordereddict'") # py3
+ sys.modules['ordereddict'] = mock.Mock()
+ sys.modules['ordereddict'].OrderedDict = -1
+ imp.reload(blessings.keyboard)
+ assert blessings.keyboard.OrderedDict == -1
+ del sys.modules['ordereddict']
+ monkeypatch.undo()
+ imp.reload(blessings.keyboard)
+ else:
+ assert platform.python_version_tuple() < ('2', '7') # reached by py2.6
+
+
+@pytest.mark.skipif(platform.python_implementation() == 'PyPy',
+ reason='PyPy freezes')
+def test_python3_2_raises_exception(monkeypatch):
+ "Test python version 3.0 through 3.2 raises an exception."
+ import blessings
+
+ monkeypatch.setattr('platform.python_version_tuple',
+ lambda: ('3', '2', '2'))
+
+ try:
+ imp.reload(blessings)
+ except ImportError as err:
+ assert err.args[0] == (
+ 'Blessings needs Python 3.2.3 or greater for Python 3 '
+ 'support due to http://bugs.python.org/issue10570.')
+ monkeypatch.undo()
+ imp.reload(blessings)
+ else:
+ assert False, 'Exception should have been raised'
+
+
+def test_IOUnsupportedOperation_dummy(monkeypatch):
+ "Ensure dummy exception is used when io is without UnsupportedOperation."
+ import blessings.terminal
+ import io
+ if hasattr(io, 'UnsupportedOperation'):
+ monkeypatch.delattr('io.UnsupportedOperation')
+
+ imp.reload(blessings.terminal)
+ assert blessings.terminal.IOUnsupportedOperation.__doc__.startswith(
+ "A dummy exception to take the place of")
+ monkeypatch.undo()
+ imp.reload(blessings.terminal)
+
+
+def test_without_dunder():
+ "Ensure dunder does not remain in module (py2x InterruptedError test."
+ import blessings.terminal
+ assert '_' not in dir(blessings.terminal)
+
+
+def test_IOUnsupportedOperation():
+ "Ensure stream that throws IOUnsupportedOperation results in non-tty."
+ @as_subprocess
+ def child():
+ import blessings.terminal
+
+ def side_effect():
+ raise blessings.terminal.IOUnsupportedOperation
+
+ mock_stream = mock.Mock()
+ mock_stream.fileno = side_effect
+
+ term = TestTerminal(stream=mock_stream)
+ assert term.stream == mock_stream
+ assert term.does_styling is False
+ assert term.is_a_tty is False
+ assert term.number_of_colors is 0
+
+ child()
+
+
+def test_winsize_IOError_returns_environ():
+ """When _winsize raises IOError, defaults from os.environ given."""
+ @as_subprocess
+ def child():
+ def side_effect(fd):
+ raise IOError
+
+ term = TestTerminal()
+ term._winsize = side_effect
+ os.environ['COLUMNS'] = '1984'
+ os.environ['LINES'] = '1888'
+ assert term._height_and_width() == (1888, 1984, None, None)
+
+ child()
+
+
+def test_yield_fullscreen(all_terms):
+ "Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen."
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ t.enter_fullscreen = u'BEGIN'
+ t.exit_fullscreen = u'END'
+ with t.fullscreen():
+ pass
+ expected_output = u''.join((t.enter_fullscreen, t.exit_fullscreen))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_yield_hidden_cursor(all_terms):
+ "Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor."
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ t.hide_cursor = u'BEGIN'
+ t.normal_cursor = u'END'
+ with t.hidden_cursor():
+ pass
+ expected_output = u''.join((t.hide_cursor, t.normal_cursor))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_no_preferredencoding_fallback_ascii():
+ "Ensure empty preferredencoding value defaults to ascii."
+ @as_subprocess
+ def child():
+ with mock.patch('locale.getpreferredencoding') as get_enc:
+ get_enc.return_value = u''
+ t = TestTerminal()
+ assert t._encoding == 'ascii'
+
+ child()
+
+
+def test_unknown_preferredencoding_warned_and_fallback_ascii():
+ "Ensure a locale without a codecs incrementaldecoder emits a warning."
+ @as_subprocess
+ def child():
+ with mock.patch('locale.getpreferredencoding') as get_enc:
+ with warnings.catch_warnings(record=True) as warned:
+ get_enc.return_value = '---unknown--encoding---'
+ t = TestTerminal()
+ assert t._encoding == 'ascii'
+ assert len(warned) == 1
+ assert issubclass(warned[-1].category, UserWarning)
+ assert "fallback to ASCII" in str(warned[-1].message)
+
+ child()
+
+
+def test_win32_missing_tty_modules(monkeypatch):
+ "Ensure dummy exception is used when io is without UnsupportedOperation."
+ @as_subprocess
+ def child():
+ OLD_STYLE = False
+ try:
+ original_import = getattr(__builtins__, '__import__')
+ OLD_STYLE = True
+ except AttributeError:
+ original_import = __builtins__['__import__']
+
+ tty_modules = ('termios', 'fcntl', 'tty')
+
+ def __import__(name, *args, **kwargs):
+ if name in tty_modules:
+ raise ImportError
+ return original_import(name, *args, **kwargs)
+
+ for module in tty_modules:
+ sys.modules.pop(module, None)
+
+ warnings.filterwarnings("error", category=UserWarning)
+ try:
+ if OLD_STYLE:
+ __builtins__.__import__ = __import__
+ else:
+ __builtins__['__import__'] = __import__
+ try:
+ import blessings.terminal
+ imp.reload(blessings.terminal)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0] == blessings.terminal.msg_nosupport
+
+ warnings.filterwarnings("ignore", category=UserWarning)
+ import blessings.terminal
+ imp.reload(blessings.terminal)
+ assert blessings.terminal.HAS_TTY is False
+ term = blessings.terminal.Terminal('ansi')
+ assert term.height == 24
+ assert term.width == 80
+
+ finally:
+ if OLD_STYLE:
+ setattr(__builtins__, '__import__', original_import)
+ else:
+ __builtins__['__import__'] = original_import
+ warnings.resetwarnings()
+ import blessings.terminal
+ imp.reload(blessings.terminal)
+
+ child()
diff --git a/blessings/tests/test_formatters.py b/blessings/tests/test_formatters.py
new file mode 100644
index 0000000..3184140
--- /dev/null
+++ b/blessings/tests/test_formatters.py
@@ -0,0 +1,403 @@
+# -*- coding: utf-8 -*-
+"""Tests string formatting functions."""
+import curses
+import mock
+
+
+def test_parameterizing_string_args_unspecified(monkeypatch):
+ """Test default args of formatters.ParameterizingString."""
+ from blessings.formatters import ParameterizingString, FormattingString
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ tparm = lambda *args: u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)).encode('latin1')
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ # given,
+ pstr = ParameterizingString(u'')
+
+ # excersize __new__
+ assert str(pstr) == u''
+ assert pstr._normal == u''
+ assert pstr._name == u'<not specified>'
+
+ # excersize __call__
+ zero = pstr(0)
+ assert type(zero) is FormattingString
+ assert zero == u'~0'
+ assert zero('text') == u'~0text'
+
+ # excersize __call__ with multiple args
+ onetwo = pstr(1, 2)
+ assert type(onetwo) is FormattingString
+ assert onetwo == u'~1~2'
+ assert onetwo('text') == u'~1~2text'
+
+
+def test_parameterizing_string_args(monkeypatch):
+ """Test basic formatters.ParameterizingString."""
+ from blessings.formatters import ParameterizingString, FormattingString
+
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ tparm = lambda *args: u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)).encode('latin1')
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ # given,
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ # excersize __new__
+ assert str(pstr) == u'cap'
+ assert pstr._normal == u'norm'
+ assert pstr._name == u'seq-name'
+
+ # excersize __call__
+ zero = pstr(0)
+ assert type(zero) is FormattingString
+ assert zero == u'cap~0'
+ assert zero('text') == u'cap~0textnorm'
+
+ # excersize __call__ with multiple args
+ onetwo = pstr(1, 2)
+ assert type(onetwo) is FormattingString
+ assert onetwo == u'cap~1~2'
+ assert onetwo('text') == u'cap~1~2textnorm'
+
+
+def test_parameterizing_string_type_error(monkeypatch):
+ """Test formatters.ParameterizingString raising TypeError"""
+ from blessings.formatters import ParameterizingString
+
+ def tparm_raises_TypeError(*args):
+ raise TypeError('custom_err')
+
+ monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError)
+
+ # given,
+ pstr = ParameterizingString(u'cap', u'norm', u'cap-name')
+
+ # ensure TypeError when given a string raises custom exception
+ try:
+ pstr('XYZ')
+ assert False, "previous call should have raised TypeError"
+ except TypeError as err:
+ assert (err.args[0] == ( # py3x
+ "A native or nonexistent capability template, "
+ "'cap-name' received invalid argument ('XYZ',): "
+ "custom_err. You probably misspelled a "
+ "formatting call like `bright_red'") or
+ err.args[0] == (
+ "A native or nonexistent capability template, "
+ "u'cap-name' received invalid argument ('XYZ',): "
+ "custom_err. You probably misspelled a "
+ "formatting call like `bright_red'"))
+
+ # ensure TypeError when given an integer raises its natural exception
+ try:
+ pstr(0)
+ assert False, "previous call should have raised TypeError"
+ except TypeError as err:
+ assert err.args[0] == "custom_err"
+
+
+def test_formattingstring(monkeypatch):
+ """Test formatters.FormattingString"""
+ from blessings.formatters import FormattingString
+
+ # given, with arg
+ pstr = FormattingString(u'attr', u'norm')
+
+ # excersize __call__,
+ assert pstr._normal == u'norm'
+ assert str(pstr) == u'attr'
+ assert pstr('text') == u'attrtextnorm'
+
+ # given, without arg
+ pstr = FormattingString(u'', u'norm')
+ assert pstr('text') == u'text'
+
+
+def test_nullcallablestring(monkeypatch):
+ """Test formatters.NullCallableString"""
+ from blessings.formatters import (NullCallableString)
+
+ # given, with arg
+ pstr = NullCallableString()
+
+ # excersize __call__,
+ assert str(pstr) == u''
+ assert pstr('text') == u'text'
+ assert pstr('text', 1) == u''
+ assert pstr('text', 'moretext') == u''
+ assert pstr(99, 1) == u''
+ assert pstr() == u''
+ assert pstr(0) == u''
+
+
+def test_split_compound():
+ """Test formatters.split_compound."""
+ from blessings.formatters import split_compound
+
+ assert split_compound(u'') == [u'']
+ assert split_compound(u'a_b_c') == [u'a', u'b', u'c']
+ assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c']
+ assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c']
+ assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c']
+
+
+def test_resolve_capability(monkeypatch):
+ """Test formatters.resolve_capability and term sugaring """
+ from blessings.formatters import resolve_capability
+
+ # given, always returns a b'seq'
+ tigetstr = lambda attr: ('seq-%s' % (attr,)).encode('latin1')
+ monkeypatch.setattr(curses, 'tigetstr', tigetstr)
+ term = mock.Mock()
+ term._sugar = dict(mnemonic='xyz')
+
+ # excersize
+ assert resolve_capability(term, 'mnemonic') == u'seq-xyz'
+ assert resolve_capability(term, 'natural') == u'seq-natural'
+
+ # given, where tigetstr returns None
+ tigetstr_none = lambda attr: None
+ monkeypatch.setattr(curses, 'tigetstr', tigetstr_none)
+
+ # excersize,
+ assert resolve_capability(term, 'natural') == u''
+
+ # given, where does_styling is False
+ def raises_exception(*args):
+ assert False, "Should not be called"
+ term.does_styling = False
+ monkeypatch.setattr(curses, 'tigetstr', raises_exception)
+
+ # excersize,
+ assert resolve_capability(term, 'natural') == u''
+
+
+def test_resolve_color(monkeypatch):
+ """Test formatters.resolve_color."""
+ from blessings.formatters import (resolve_color,
+ FormattingString,
+ NullCallableString)
+
+ color_cap = lambda digit: 'seq-%s' % (digit,)
+ monkeypatch.setattr(curses, 'COLOR_RED', 1984)
+
+ # given, terminal with color capabilities
+ term = mock.Mock()
+ term._background_color = color_cap
+ term._foreground_color = color_cap
+ term.number_of_colors = -1
+ term.normal = 'seq-normal'
+
+ # excersize,
+ red = resolve_color(term, 'red')
+ assert type(red) == FormattingString
+ assert red == u'seq-1984'
+ assert red('text') == u'seq-1984textseq-normal'
+
+ # excersize bold, +8
+ bright_red = resolve_color(term, 'bright_red')
+ assert type(bright_red) == FormattingString
+ assert bright_red == u'seq-1992'
+ assert bright_red('text') == u'seq-1992textseq-normal'
+
+ # given, terminal without color
+ term.number_of_colors = 0
+
+ # excersize,
+ red = resolve_color(term, 'red')
+ assert type(red) == NullCallableString
+ assert red == u''
+ assert red('text') == u'text'
+
+ # excesize bold,
+ bright_red = resolve_color(term, 'bright_red')
+ assert type(bright_red) == NullCallableString
+ assert bright_red == u''
+ assert bright_red('text') == u'text'
+
+
+def test_resolve_attribute_as_color(monkeypatch):
+ """ Test simple resolve_attribte() given color name. """
+ import blessings
+ from blessings.formatters import resolve_attribute
+
+ resolve_color = lambda term, digit: 'seq-%s' % (digit,)
+ COLORS = set(['COLORX', 'COLORY'])
+ COMPOUNDABLES = set(['JOINT', 'COMPOUND'])
+ monkeypatch.setattr(blessings.formatters, 'resolve_color', resolve_color)
+ monkeypatch.setattr(blessings.formatters, 'COLORS', COLORS)
+ monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
+ term = mock.Mock()
+ assert resolve_attribute(term, 'COLORX') == u'seq-COLORX'
+
+
+def test_resolve_attribute_as_compoundable(monkeypatch):
+ """ Test simple resolve_attribte() given a compoundable. """
+ import blessings
+ from blessings.formatters import resolve_attribute, FormattingString
+
+ resolve_cap = lambda term, digit: 'seq-%s' % (digit,)
+ COMPOUNDABLES = set(['JOINT', 'COMPOUND'])
+ monkeypatch.setattr(blessings.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ monkeypatch.setattr(blessings.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ compound = resolve_attribute(term, 'JOINT')
+ assert type(compound) is FormattingString
+ assert str(compound) == u'seq-JOINT'
+ assert compound('text') == u'seq-JOINTtextseq-normal'
+
+
+def test_resolve_attribute_non_compoundables(monkeypatch):
+ """ Test recursive compounding of resolve_attribute(). """
+ import blessings
+ from blessings.formatters import resolve_attribute, ParameterizingString
+ uncompoundables = lambda attr: ['split', 'compound']
+ resolve_cap = lambda term, digit: 'seq-%s' % (digit,)
+ monkeypatch.setattr(blessings.formatters,
+ 'split_compound',
+ uncompoundables)
+ monkeypatch.setattr(blessings.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ tparm = lambda *args: u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)).encode('latin1')
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ # given
+ pstr = resolve_attribute(term, 'not-a-compoundable')
+ assert type(pstr) == ParameterizingString
+ assert str(pstr) == u'seq-not-a-compoundable'
+ # this is like calling term.move_x(3)
+ assert pstr(3) == u'seq-not-a-compoundable~3'
+ # this is like calling term.move_x(3)('text')
+ assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal'
+
+
+def test_resolve_attribute_recursive_compoundables(monkeypatch):
+ """ Test recursive compounding of resolve_attribute(). """
+ import blessings
+ from blessings.formatters import resolve_attribute, FormattingString
+
+ # patch,
+ resolve_cap = lambda term, digit: 'seq-%s' % (digit,)
+ monkeypatch.setattr(blessings.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ tparm = lambda *args: u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)).encode('latin1')
+ monkeypatch.setattr(curses, 'tparm', tparm)
+ monkeypatch.setattr(curses, 'COLOR_RED', 6502)
+ monkeypatch.setattr(curses, 'COLOR_BLUE', 6800)
+
+ color_cap = lambda digit: 'seq-%s' % (digit,)
+ term = mock.Mock()
+ term._background_color = color_cap
+ term._foreground_color = color_cap
+ term.normal = 'seq-normal'
+
+ # given,
+ pstr = resolve_attribute(term, 'bright_blue_on_red')
+
+ # excersize,
+ assert type(pstr) == FormattingString
+ assert str(pstr) == 'seq-6808seq-6502'
+ assert pstr('text') == 'seq-6808seq-6502textseq-normal'
+
+
+def test_pickled_parameterizing_string(monkeypatch):
+ """Test pickle-ability of a formatters.ParameterizingString."""
+ from blessings.formatters import ParameterizingString, FormattingString
+
+ # simply send()/recv() over multiprocessing Pipe, a simple
+ # pickle.loads(dumps(...)) did not reproduce this issue,
+ from multiprocessing import Pipe
+ import pickle
+
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ tparm = lambda *args: u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)).encode('latin1')
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ # given,
+ pstr = ParameterizingString(u'seqname', u'norm', u'cap-name')
+
+ # multiprocessing Pipe implicitly pickles.
+ r, w = Pipe()
+
+ # excersize picklability of ParameterizingString
+ for proto_num in range(pickle.HIGHEST_PROTOCOL):
+ assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num))
+ w.send(pstr)
+ r.recv() == pstr
+
+ # excersize picklability of FormattingString
+ # -- the return value of calling ParameterizingString
+ zero = pstr(0)
+ for proto_num in range(pickle.HIGHEST_PROTOCOL):
+ assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num))
+ w.send(zero)
+ r.recv() == zero
+
+
+def test_tparm_returns_null(monkeypatch):
+ """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """
+ # on win32, any calls to tparm raises curses.error with message,
+ # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
+ from blessings.formatters import ParameterizingString, NullCallableString
+
+ def tparm(*args):
+ raise curses.error("tparm() returned NULL")
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ value = pstr(u'x')
+ assert type(value) is NullCallableString
+
+
+def test_tparm_other_exception(monkeypatch):
+ """ Test 'tparm() returned NULL' is caught (win32 PDCurses systems). """
+ # on win32, any calls to tparm raises curses.error with message,
+ # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
+ from blessings.formatters import ParameterizingString, NullCallableString
+
+ def tparm(*args):
+ raise curses.error("unexpected error in tparm()")
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ try:
+ pstr(u'x')
+ assert False, "previous call should have raised curses.error"
+ except curses.error:
+ pass
diff --git a/blessings/tests/test_keyboard.py b/blessings/tests/test_keyboard.py
new file mode 100644
index 0000000..393b248
--- /dev/null
+++ b/blessings/tests/test_keyboard.py
@@ -0,0 +1,902 @@
+# -*- coding: utf-8 -*-
+"Tests for keyboard support."
+# std imports
+import functools
+import tempfile
+try:
+ from StringIO import StringIO
+except ImportError:
+ import io
+ StringIO = io.StringIO
+import platform
+import signal
+import curses
+import time
+import math
+import tty # NOQA
+import pty
+import sys
+import os
+
+# local
+from .accessories import (
+ read_until_eof,
+ read_until_semaphore,
+ SEND_SEMAPHORE,
+ RECV_SEMAPHORE,
+ as_subprocess,
+ TestTerminal,
+ SEMAPHORE,
+ all_terms,
+ echo_off,
+ xterms,
+)
+
+# 3rd-party
+import pytest
+import mock
+
+if sys.version_info[0] == 3:
+ unichr = chr
+
+
+def test_char_is_ready_interrupted():
+ "_char_is_ready() should not be interrupted with a signal handler."
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input(raw=True):
+ assert term.keystroke(timeout=1.05) == u''
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch is True
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.kill(pid, signal.SIGWINCH)
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+def test_char_is_ready_interrupted_nonetype():
+ "_char_is_ready() should also allow interruption with timeout of None."
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input(raw=True):
+ term.keystroke(timeout=1)
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch is True
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(0.05)
+ os.kill(pid, signal.SIGWINCH)
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+def test_char_is_ready_interrupted_interruptable():
+ "_char_is_ready() may be interrupted when interruptable=False."
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input(raw=True):
+ term.keystroke(timeout=1.05, interruptable=False)
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch is True
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(0.05)
+ os.kill(pid, signal.SIGWINCH)
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_char_is_ready_interrupted_nonetype_interruptable():
+ """_char_is_ready() may be interrupted when interruptable=False with
+ timeout None."""
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input(raw=True):
+ term.keystroke(timeout=None, interruptable=False)
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch is True
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(0.05)
+ os.kill(pid, signal.SIGWINCH)
+ os.write(master_fd, b'X')
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_input_no_kb():
+ "keystroke_input() should not call tty.setcbreak() without keyboard."
+ @as_subprocess
+ def child():
+ with tempfile.NamedTemporaryFile() as stream:
+ term = TestTerminal(stream=stream)
+ with mock.patch("tty.setcbreak") as mock_setcbreak:
+ with term.keystroke_input():
+ assert not mock_setcbreak.called
+ assert term.keyboard_fd is None
+ child()
+
+
+def test_notty_kb_is_None():
+ "keyboard_fd should be None when os.isatty returns False."
+ # in this scenerio, stream is sys.__stdout__,
+ # but os.isatty(0) is False,
+ # such as when piping output to less(1)
+ @as_subprocess
+ def child():
+ with mock.patch("os.isatty") as mock_isatty:
+ mock_isatty.return_value = False
+ term = TestTerminal()
+ assert term.keyboard_fd is None
+ child()
+
+
+def test_raw_input_no_kb():
+ "keystroke_input(raw=True) should not call tty.setraw() without keyboard."
+ @as_subprocess
+ def child():
+ with tempfile.NamedTemporaryFile() as stream:
+ term = TestTerminal(stream=stream)
+ with mock.patch("tty.setraw") as mock_setraw:
+ with term.keystroke_input(raw=True):
+ assert not mock_setraw.called
+ assert term.keyboard_fd is None
+ child()
+
+
+def test_char_is_ready_no_kb():
+ "_char_is_ready() always immediately returns False without a keyboard."
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=StringIO())
+ stime = time.time()
+ assert term.keyboard_fd is None
+ assert term._char_is_ready(timeout=1.1) is False
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+def test_keystroke_0s_keystroke_input_noinput():
+ "0-second keystroke without input; '' should be returned."
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+def test_keystroke_0s_keystroke_input_noinput_nokb():
+ "0-second keystroke without data in input stream and no keyboard/tty."
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=StringIO())
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+def test_keystroke_1s_keystroke_input_noinput():
+ "1-second keystroke without input; '' should be returned after ~1 second."
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+def test_keystroke_1s_keystroke_input_noinput_nokb():
+ "1-second keystroke without input or keyboard."
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=StringIO())
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+def test_keystroke_0s_keystroke_input_with_input():
+ "0-second keystroke with input; Keypress should be immediately returned."
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ # child pauses, writes semaphore and begins awaiting input
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ inp = term.keystroke(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'x'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'x'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_keystroke_input_with_input_slowly():
+ "0-second keystroke with input; Keypress should be immediately returned."
+ pid, master_fd = pty.fork()
+ if pid is 0:
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ # child pauses, writes semaphore and begins awaiting input
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ while True:
+ inp = term.keystroke(timeout=0.5)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if inp == 'X':
+ break
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'a'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'b'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'cdefgh'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'X'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'abcdefghX'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_0s_keystroke_input_multibyte_utf8():
+ "0-second keystroke with multibyte utf-8 input; should decode immediately."
+ # utf-8 bytes represent "latin capital letter upsilon".
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ inp = term.keystroke(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'\u01b1'.encode('utf-8'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'Ʊ'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None or
+ platform.python_implementation() == 'PyPy',
+ reason="travis-ci nor pypy handle ^C very well.")
+def test_keystroke_0s_raw_input_ctrl_c():
+ "0-second keystroke with raw allows receiving ^C."
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ with term.keystroke_input(raw=True):
+ os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE)
+ inp = term.keystroke(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('latin1'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ # ensure child is in raw mode before sending ^C,
+ read_until_semaphore(master_fd)
+ os.write(master_fd, u'\x03'.encode('latin1'))
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert (output == u'\x03' or
+ output == u'' and not os.isatty(0))
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_0s_keystroke_input_sequence():
+ "0-second keystroke with multibyte sequence; should decode immediately."
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ inp = term.keystroke(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.name.encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, u'\x1b[D'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'KEY_LEFT'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_1s_keystroke_input_with_input():
+ "1-second keystroke w/multibyte sequence; should return after ~1 second."
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ inp = term.keystroke(timeout=3)
+ os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(1)
+ os.write(master_fd, u'\x1b[C'.encode('ascii'))
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'KEY_RIGHT'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+def test_esc_delay_keystroke_input_035():
+ "esc_delay will cause a single ESC (\\x1b) to delay for 0.35."
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=5)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert 34 <= int(duration_ms) <= 45, duration_ms
+
+
+def test_esc_delay_keystroke_input_135():
+ "esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35."
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=5, esc_delay=1.35)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+ assert 134 <= int(duration_ms) <= 145, int(duration_ms)
+
+
+def test_esc_delay_keystroke_input_timout_0():
+ """esc_delay still in effect with timeout of 0 ("nonblocking")."""
+ pid, master_fd = pty.fork()
+ if pid is 0: # child
+ try:
+ cov = __import__('cov_core_init').init()
+ except ImportError:
+ cov = None
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.keystroke_input():
+ stime = time.time()
+ inp = term.keystroke(timeout=0)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert 34 <= int(duration_ms) <= 45, int(duration_ms)
+
+
+def test_keystroke_default_args():
+ "Test keyboard.Keystroke constructor with default arguments."
+ from blessings.keyboard import Keystroke
+ ks = Keystroke()
+ assert ks._name is None
+ assert ks.name == ks._name
+ assert ks._code is None
+ assert ks.code == ks._code
+ assert u'x' == u'x' + ks
+ assert ks.is_sequence is False
+ assert repr(ks) in ("u''", # py26, 27
+ "''",) # py33
+
+
+def test_a_keystroke():
+ "Test keyboard.Keystroke constructor with set arguments."
+ from blessings.keyboard import Keystroke
+ ks = Keystroke(ucs=u'x', code=1, name=u'the X')
+ assert ks._name is u'the X'
+ assert ks.name == ks._name
+ assert ks._code is 1
+ assert ks.code == ks._code
+ assert u'xx' == u'x' + ks
+ assert ks.is_sequence is True
+ assert repr(ks) == "the X"
+
+
+def test_get_keyboard_codes():
+ "Test all values returned by get_keyboard_codes are from curses."
+ from blessings.keyboard import (
+ get_keyboard_codes,
+ CURSES_KEYCODE_OVERRIDE_MIXIN,
+ )
+ exemptions = dict(CURSES_KEYCODE_OVERRIDE_MIXIN)
+ for value, keycode in get_keyboard_codes().items():
+ if keycode in exemptions:
+ assert value == exemptions[keycode]
+ continue
+ assert hasattr(curses, keycode)
+ assert getattr(curses, keycode) == value
+
+
+def test_alternative_left_right():
+ "Test _alternative_left_right behavior for space/backspace."
+ from blessings.keyboard import _alternative_left_right
+ term = mock.Mock()
+ term._cuf1 = u''
+ term._cub1 = u''
+ assert not bool(_alternative_left_right(term))
+ term._cuf1 = u' '
+ term._cub1 = u'\b'
+ assert not bool(_alternative_left_right(term))
+ term._cuf1 = u'seq-right'
+ term._cub1 = u'seq-left'
+ assert (_alternative_left_right(term) == {
+ u'seq-right': curses.KEY_RIGHT,
+ u'seq-left': curses.KEY_LEFT})
+
+
+def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms):
+ "Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT."
+ from blessings.keyboard import get_keyboard_sequences
+
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ keymap = get_keyboard_sequences(term)
+ if term._cuf1:
+ assert term._cuf1 in keymap
+ assert keymap[term._cuf1] == term.KEY_RIGHT
+ if term._cub1:
+ assert term._cub1 in keymap
+ if term._cub1 == '\b':
+ assert keymap[term._cub1] == term.KEY_BACKSPACE
+ else:
+ assert keymap[term._cub1] == term.KEY_LEFT
+
+ child(all_terms)
+
+
+def test_get_keyboard_sequences_sort_order(xterms):
+ "ordereddict ensures sequences are ordered longest-first."
+ @as_subprocess
+ def child():
+ term = TestTerminal(force_styling=True)
+ maxlen = None
+ for sequence, code in term._keymap.items():
+ if maxlen is not None:
+ assert len(sequence) <= maxlen
+ assert sequence
+ maxlen = len(sequence)
+ child()
+
+
+def test_get_keyboard_sequence(monkeypatch):
+ "Test keyboard.get_keyboard_sequence. "
+ import curses.has_key
+ import blessings.keyboard
+
+ (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3)
+ (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split()
+ (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, SEQ_ALT_CUF1, SEQ_ALT_CUB1) = (
+ b'seq-small-a',
+ b'seq-large-abcdefg',
+ b'seq-mixin',
+ b'seq-alt-cuf1',
+ b'seq-alt-cub1_')
+
+ # patch curses functions
+ monkeypatch.setattr(curses, 'tigetstr',
+ lambda cap: {CAP_SMALL: SEQ_SMALL,
+ CAP_LARGE: SEQ_LARGE}[cap])
+
+ monkeypatch.setattr(curses.has_key, '_capability_names',
+ dict(((KEY_SMALL, CAP_SMALL,),
+ (KEY_LARGE, CAP_LARGE,))))
+
+ # patch global sequence mix-in
+ monkeypatch.setattr(blessings.keyboard,
+ 'DEFAULT_SEQUENCE_MIXIN', (
+ (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),))
+
+ # patch for _alternative_left_right
+ term = mock.Mock()
+ term._cuf1 = SEQ_ALT_CUF1.decode('latin1')
+ term._cub1 = SEQ_ALT_CUB1.decode('latin1')
+ keymap = blessings.keyboard.get_keyboard_sequences(term)
+
+ assert list(keymap.items()) == [
+ (SEQ_LARGE.decode('latin1'), KEY_LARGE),
+ (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT),
+ (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT),
+ (SEQ_SMALL.decode('latin1'), KEY_SMALL),
+ (SEQ_MIXIN.decode('latin1'), KEY_MIXIN)]
+
+
+def test_resolve_sequence():
+ "Test resolve_sequence for order-dependent mapping."
+ from blessings.keyboard import resolve_sequence, OrderedDict
+ mapper = OrderedDict(((u'SEQ1', 1),
+ (u'SEQ2', 2),
+ # takes precedence over LONGSEQ, first-match
+ (u'KEY_LONGSEQ_longest', 3),
+ (u'LONGSEQ', 4),
+ # wont match, LONGSEQ is first-match in this order
+ (u'LONGSEQ_longer', 5),
+ # falls through for L{anything_else}
+ (u'L', 6)))
+ codes = {1: u'KEY_SEQ1',
+ 2: u'KEY_SEQ2',
+ 3: u'KEY_LONGSEQ_longest',
+ 4: u'KEY_LONGSEQ',
+ 5: u'KEY_LONGSEQ_longer',
+ 6: u'KEY_L'}
+ ks = resolve_sequence(u'', mapper, codes)
+ assert ks == u''
+ assert ks.name is None
+ assert ks.code is None
+ assert ks.is_sequence is False
+ assert repr(ks) in ("u''", # py26, 27
+ "''",) # py33
+
+ ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes)
+ assert ks == u'n'
+ assert ks.name is None
+ assert ks.code is None
+ assert ks.is_sequence is False
+ assert repr(ks) in (u"u'n'", "'n'",)
+
+ ks = resolve_sequence(u'SEQ1', mapper, codes)
+ assert ks == u'SEQ1'
+ assert ks.name == u'KEY_SEQ1'
+ assert ks.code is 1
+ assert ks.is_sequence is True
+ assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1")
+
+ ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes)
+ assert ks == u'LONGSEQ'
+ assert ks.name == u'KEY_LONGSEQ'
+ assert ks.code is 4
+ assert ks.is_sequence is True
+ assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ")
+
+ ks = resolve_sequence(u'LONGSEQ', mapper, codes)
+ assert ks == u'LONGSEQ'
+ assert ks.name == u'KEY_LONGSEQ'
+ assert ks.code is 4
+ assert ks.is_sequence is True
+ assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ")
+
+ ks = resolve_sequence(u'Lxxxxx', mapper, codes)
+ assert ks == u'L'
+ assert ks.name == u'KEY_L'
+ assert ks.code is 6
+ assert ks.is_sequence is True
+ assert repr(ks) in (u"KEY_L", "KEY_L")
+
+
+def test_keypad_mixins_and_aliases():
+ """ Test PC-Style function key translations when in ``keypad`` mode."""
+ # Key plain app modified
+ # Up ^[[A ^[OA ^[[1;mA
+ # Down ^[[B ^[OB ^[[1;mB
+ # Right ^[[C ^[OC ^[[1;mC
+ # Left ^[[D ^[OD ^[[1;mD
+ # End ^[[F ^[OF ^[[1;mF
+ # Home ^[[H ^[OH ^[[1;mH
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ from blessings.keyboard import resolve_sequence
+
+ resolve = functools.partial(resolve_sequence,
+ mapper=term._keymap,
+ codes=term._keycodes)
+
+ assert resolve(unichr(10)).name == "KEY_ENTER"
+ assert resolve(unichr(13)).name == "KEY_ENTER"
+ assert resolve(unichr(8)).name == "KEY_BACKSPACE"
+ assert resolve(unichr(9)).name == "KEY_TAB"
+ assert resolve(unichr(27)).name == "KEY_ESCAPE"
+ assert resolve(unichr(127)).name == "KEY_DELETE"
+ assert resolve(u"\x1b[A").name == "KEY_UP"
+ assert resolve(u"\x1b[B").name == "KEY_DOWN"
+ assert resolve(u"\x1b[C").name == "KEY_RIGHT"
+ assert resolve(u"\x1b[D").name == "KEY_LEFT"
+ assert resolve(u"\x1b[U").name == "KEY_PGDOWN"
+ assert resolve(u"\x1b[V").name == "KEY_PGUP"
+ assert resolve(u"\x1b[H").name == "KEY_HOME"
+ assert resolve(u"\x1b[F").name == "KEY_END"
+ assert resolve(u"\x1b[K").name == "KEY_END"
+ assert resolve(u"\x1bOM").name == "KEY_ENTER"
+ assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY"
+ assert resolve(u"\x1bOk").name == "KEY_KP_ADD"
+ assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR"
+ assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT"
+ assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL"
+ assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE"
+ assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL"
+ assert resolve(u"\x1bOp").name == "KEY_KP_0"
+ assert resolve(u"\x1bOq").name == "KEY_KP_1"
+ assert resolve(u"\x1bOr").name == "KEY_KP_2"
+ assert resolve(u"\x1bOs").name == "KEY_KP_3"
+ assert resolve(u"\x1bOt").name == "KEY_KP_4"
+ assert resolve(u"\x1bOu").name == "KEY_KP_5"
+ assert resolve(u"\x1bOv").name == "KEY_KP_6"
+ assert resolve(u"\x1bOw").name == "KEY_KP_7"
+ assert resolve(u"\x1bOx").name == "KEY_KP_8"
+ assert resolve(u"\x1bOy").name == "KEY_KP_9"
+ assert resolve(u"\x1b[1~").name == "KEY_FIND"
+ assert resolve(u"\x1b[2~").name == "KEY_INSERT"
+ assert resolve(u"\x1b[3~").name == "KEY_DELETE"
+ assert resolve(u"\x1b[4~").name == "KEY_SELECT"
+ assert resolve(u"\x1b[5~").name == "KEY_PGUP"
+ assert resolve(u"\x1b[6~").name == "KEY_PGDOWN"
+ assert resolve(u"\x1b[7~").name == "KEY_HOME"
+ assert resolve(u"\x1b[8~").name == "KEY_END"
+ assert resolve(u"\x1b[OA").name == "KEY_UP"
+ assert resolve(u"\x1b[OB").name == "KEY_DOWN"
+ assert resolve(u"\x1b[OC").name == "KEY_RIGHT"
+ assert resolve(u"\x1b[OD").name == "KEY_LEFT"
+ assert resolve(u"\x1b[OF").name == "KEY_END"
+ assert resolve(u"\x1b[OH").name == "KEY_HOME"
+ assert resolve(u"\x1bOP").name == "KEY_F1"
+ assert resolve(u"\x1bOQ").name == "KEY_F2"
+ assert resolve(u"\x1bOR").name == "KEY_F3"
+ assert resolve(u"\x1bOS").name == "KEY_F4"
+
+ child('xterm')
diff --git a/blessings/tests/test_length_sequence.py b/blessings/tests/test_length_sequence.py
new file mode 100644
index 0000000..0eac165
--- /dev/null
+++ b/blessings/tests/test_length_sequence.py
@@ -0,0 +1,342 @@
+# encoding: utf-8
+import itertools
+import platform
+import termios
+import struct
+import fcntl
+import sys
+import os
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+from .accessories import (
+ all_terms,
+ as_subprocess,
+ TestTerminal,
+ many_columns,
+ many_lines,
+)
+
+import pytest
+
+
+def test_length_cjk():
+ @as_subprocess
+ def child():
+ term = TestTerminal(kind='xterm-256color')
+
+ # given,
+ given = term.bold_red(u'コンニチハ, セカイ!')
+ expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,))
+
+ # exercise,
+ assert term.length(given) == expected
+
+ child()
+
+
+def test_length_ansiart():
+ @as_subprocess
+ def child():
+ import codecs
+ from blessings.sequences import Sequence
+ term = TestTerminal(kind='xterm-256color')
+ # this 'ansi' art contributed by xzip!impure for another project,
+ # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded.
+ fname = os.path.join(os.path.dirname(__file__), 'wall.ans')
+ lines = codecs.open(fname, 'r', 'utf-8').readlines()
+ assert term.length(lines[0]) == 67 # ^[[64C^[[34m▄▓▄
+ assert term.length(lines[1]) == 75
+ assert term.length(lines[2]) == 78
+ assert term.length(lines[3]) == 78
+ assert term.length(lines[4]) == 78
+ assert term.length(lines[5]) == 78
+ assert term.length(lines[6]) == 77
+ child()
+
+
+def test_sequence_length(all_terms):
+ """Ensure T.length(string containing sequence) is correct."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ # Create a list of ascii characters, to be separated
+ # by word, to be zipped up with a cycling list of
+ # terminal sequences. Then, compare the length of
+ # each, the basic plain_text.__len__ vs. the Terminal
+ # method length. They should be equal.
+ plain_text = (u'The softest things of the world '
+ u'Override the hardest things of the world '
+ u'That which has no substance '
+ u'Enters into that which has no openings')
+ if t.bold:
+ assert (t.length(t.bold) == 0)
+ assert (t.length(t.bold(u'x')) == 1)
+ assert (t.length(t.bold_red) == 0)
+ assert (t.length(t.bold_red(u'x')) == 1)
+ assert (t.strip(t.bold) == u'')
+ assert (t.rstrip(t.bold) == u'')
+ assert (t.lstrip(t.bold) == u'')
+ assert (t.strip(t.bold(u' x ')) == u'x')
+ assert (t.strip(t.bold(u'z x q'), 'zq') == u' x ')
+ assert (t.rstrip(t.bold(u' x ')) == u' x')
+ assert (t.lstrip(t.bold(u' x ')) == u'x ')
+ assert (t.strip(t.bold_red) == u'')
+ assert (t.rstrip(t.bold_red) == u'')
+ assert (t.lstrip(t.bold_red) == u'')
+ assert (t.strip(t.bold_red(u' x ')) == u'x')
+ assert (t.rstrip(t.bold_red(u' x ')) == u' x')
+ assert (t.lstrip(t.bold_red(u' x ')) == u'x ')
+ assert (t.strip_seqs(t.bold) == u'')
+ assert (t.strip_seqs(t.bold(u' x ')) == u' x ')
+ assert (t.strip_seqs(t.bold_red) == u'')
+ assert (t.strip_seqs(t.bold_red(u' x ')) == u' x ')
+
+ if t.underline:
+ assert (t.length(t.underline) == 0)
+ assert (t.length(t.underline(u'x')) == 1)
+ assert (t.length(t.underline_red) == 0)
+ assert (t.length(t.underline_red(u'x')) == 1)
+ assert (t.strip(t.underline) == u'')
+ assert (t.strip(t.underline(u' x ')) == u'x')
+ assert (t.strip(t.underline_red) == u'')
+ assert (t.strip(t.underline_red(u' x ')) == u'x')
+ assert (t.rstrip(t.underline_red(u' x ')) == u' x')
+ assert (t.lstrip(t.underline_red(u' x ')) == u'x ')
+ assert (t.strip_seqs(t.underline) == u'')
+ assert (t.strip_seqs(t.underline(u' x ')) == u' x ')
+ assert (t.strip_seqs(t.underline_red) == u'')
+ assert (t.strip_seqs(t.underline_red(u' x ')) == u' x ')
+
+ if t.reverse:
+ assert (t.length(t.reverse) == 0)
+ assert (t.length(t.reverse(u'x')) == 1)
+ assert (t.length(t.reverse_red) == 0)
+ assert (t.length(t.reverse_red(u'x')) == 1)
+ assert (t.strip(t.reverse) == u'')
+ assert (t.strip(t.reverse(u' x ')) == u'x')
+ assert (t.strip(t.reverse_red) == u'')
+ assert (t.strip(t.reverse_red(u' x ')) == u'x')
+ assert (t.rstrip(t.reverse_red(u' x ')) == u' x')
+ assert (t.lstrip(t.reverse_red(u' x ')) == u'x ')
+ assert (t.strip_seqs(t.reverse) == u'')
+ assert (t.strip_seqs(t.reverse(u' x ')) == u' x ')
+ assert (t.strip_seqs(t.reverse_red) == u'')
+ assert (t.strip_seqs(t.reverse_red(u' x ')) == u' x ')
+
+ if t.blink:
+ assert (t.length(t.blink) == 0)
+ assert (t.length(t.blink(u'x')) == 1)
+ assert (t.length(t.blink_red) == 0)
+ assert (t.length(t.blink_red(u'x')) == 1)
+ assert (t.strip(t.blink) == u'')
+ assert (t.strip(t.blink(u' x ')) == u'x')
+ assert (t.strip(t.blink(u'z x q'), u'zq') == u' x ')
+ assert (t.strip(t.blink_red) == u'')
+ assert (t.strip(t.blink_red(u' x ')) == u'x')
+ assert (t.strip_seqs(t.blink) == u'')
+ assert (t.strip_seqs(t.blink(u' x ')) == u' x ')
+ assert (t.strip_seqs(t.blink_red) == u'')
+ assert (t.strip_seqs(t.blink_red(u' x ')) == u' x ')
+
+ if t.home:
+ assert (t.length(t.home) == 0)
+ assert (t.strip(t.home) == u'')
+ if t.clear_eol:
+ assert (t.length(t.clear_eol) == 0)
+ assert (t.strip(t.clear_eol) == u'')
+ if t.enter_fullscreen:
+ assert (t.length(t.enter_fullscreen) == 0)
+ assert (t.strip(t.enter_fullscreen) == u'')
+ if t.exit_fullscreen:
+ assert (t.length(t.exit_fullscreen) == 0)
+ assert (t.strip(t.exit_fullscreen) == u'')
+
+ # horizontally, we decide move_down and move_up are 0,
+ assert (t.length(t.move_down) == 0)
+ assert (t.length(t.move_down(2)) == 0)
+ assert (t.length(t.move_up) == 0)
+ assert (t.length(t.move_up(2)) == 0)
+
+ # other things aren't so simple, somewhat edge cases,
+ # moving backwards and forwards horizontally must be
+ # accounted for as a "length", as <x><move right 10><y>
+ # will result in a printed column length of 12 (even
+ # though columns 2-11 are non-destructive space
+ assert (t.length(u'x\b') == 0)
+ assert (t.strip(u'x\b') == u'')
+
+ # XXX why are some terminals width of 9 here ??
+ assert (t.length(u'\t') in (8, 9))
+ assert (t.strip(u'\t') == u'')
+ assert (t.length(u'_' + t.move_left) == 0)
+
+ if t.cub:
+ assert (t.length((u'_' * 10) + t.cub(10)) == 0)
+
+ assert (t.length(t.move_right) == 1)
+
+ if t.cuf:
+ assert (t.length(t.cuf(10)) == 10)
+
+ # vertical spacing is unaccounted as a 'length'
+ assert (t.length(t.move_up) == 0)
+ assert (t.length(t.cuu(10)) == 0)
+ assert (t.length(t.move_down) == 0)
+ assert (t.length(t.cud(10)) == 0)
+
+ # this is how manpages perform underlining, this is done
+ # with the 'overstrike' capability of teletypes, and aparently
+ # less(1), '123' -> '1\b_2\b_3\b_'
+ text_wseqs = u''.join(itertools.chain(
+ *zip(plain_text, itertools.cycle(['\b_']))))
+ assert (t.length(text_wseqs) == len(plain_text))
+
+ child(all_terms)
+
+
+def test_env_winsize():
+ """Test height and width is appropriately queried in a pty."""
+ @as_subprocess
+ def child():
+ # set the pty's virtual window size
+ os.environ['COLUMNS'] = '99'
+ os.environ['LINES'] = '11'
+ t = TestTerminal(stream=StringIO())
+ save_init = t._init_descriptor
+ save_stdout = sys.__stdout__
+ try:
+ t._init_descriptor = None
+ sys.__stdout__ = None
+ winsize = t._height_and_width()
+ width = t.width
+ height = t.height
+ finally:
+ t._init_descriptor = save_init
+ sys.__stdout__ = save_stdout
+ assert winsize.ws_col == width == 99
+ assert winsize.ws_row == height == 11
+
+ child()
+
+
+@pytest.mark.skipif(platform.python_implementation() == 'PyPy',
+ reason='PyPy fails TIOCSWINSZ')
+def test_winsize(many_lines, many_columns):
+ """Test height and width is appropriately queried in a pty."""
+ @as_subprocess
+ def child(lines=25, cols=80):
+ # set the pty's virtual window size
+ val = struct.pack('HHHH', lines, cols, 0, 0)
+ fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val)
+ t = TestTerminal()
+ winsize = t._height_and_width()
+ assert t.width == cols
+ assert t.height == lines
+ assert winsize.ws_col == cols
+ assert winsize.ws_row == lines
+
+ child(lines=many_lines, cols=many_columns)
+
+
+@pytest.mark.skipif(platform.python_implementation() == 'PyPy',
+ reason='PyPy fails TIOCSWINSZ')
+def test_Sequence_alignment(all_terms):
+ """Tests methods related to Sequence class, namely ljust, rjust, center."""
+ @as_subprocess
+ def child(kind, lines=25, cols=80):
+ # set the pty's virtual window size
+ val = struct.pack('HHHH', lines, cols, 0, 0)
+ fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val)
+ t = TestTerminal(kind=kind)
+
+ pony_msg = 'pony express, all aboard, choo, choo!'
+ pony_len = len(pony_msg)
+ pony_colored = u''.join(
+ ['%s%s' % (t.color(n % 7), ch,)
+ for n, ch in enumerate(pony_msg)])
+ pony_colored += t.normal
+ ladjusted = t.ljust(pony_colored)
+ radjusted = t.rjust(pony_colored)
+ centered = t.center(pony_colored)
+ assert (t.length(pony_colored) == pony_len)
+ assert (t.length(centered.strip()) == pony_len)
+ assert (t.length(centered) == len(pony_msg.center(t.width)))
+ assert (t.length(ladjusted.strip()) == pony_len)
+ assert (t.length(ladjusted) == len(pony_msg.ljust(t.width)))
+ assert (t.length(radjusted.strip()) == pony_len)
+ assert (t.length(radjusted) == len(pony_msg.rjust(t.width)))
+
+ child(kind=all_terms)
+
+
+def test_sequence_is_movement_false(all_terms):
+ """Test parser about sequences that do not move the cursor."""
+ @as_subprocess
+ def child_mnemonics_wontmove(kind):
+ from blessings.sequences import measure_length
+ t = TestTerminal(kind=kind)
+ assert (0 == measure_length(u'', t))
+ # not even a mbs
+ assert (0 == measure_length(u'xyzzy', t))
+ # negative numbers, though printable as %d, do not result
+ # in movement; just garbage. Also not a valid sequence.
+ assert (0 == measure_length(t.cuf(-333), t))
+ assert (len(t.clear_eol) == measure_length(t.clear_eol, t))
+ # various erases don't *move*
+ assert (len(t.clear_bol) == measure_length(t.clear_bol, t))
+ assert (len(t.clear_eos) == measure_length(t.clear_eos, t))
+ assert (len(t.bold) == measure_length(t.bold, t))
+ # various paints don't move
+ assert (len(t.red) == measure_length(t.red, t))
+ assert (len(t.civis) == measure_length(t.civis, t))
+ if t.cvvis:
+ assert (len(t.cvvis) == measure_length(t.cvvis, t))
+ assert (len(t.underline) == measure_length(t.underline, t))
+ assert (len(t.reverse) == measure_length(t.reverse, t))
+ for _num in range(t.number_of_colors):
+ assert (len(t.color(_num)) == measure_length(t.color(_num), t))
+ assert (len(t.normal) == measure_length(t.normal, t))
+ assert (len(t.normal_cursor) == measure_length(t.normal_cursor, t))
+ assert (len(t.hide_cursor) == measure_length(t.hide_cursor, t))
+ assert (len(t.save) == measure_length(t.save, t))
+ assert (len(t.italic) == measure_length(t.italic, t))
+ assert (len(t.standout) == measure_length(t.standout, t)
+ ), (t.standout, t._wont_move)
+
+ child_mnemonics_wontmove(all_terms)
+
+
+def test_sequence_is_movement_true(all_terms):
+ """Test parsers about sequences that move the cursor."""
+ @as_subprocess
+ def child_mnemonics_willmove(kind):
+ from blessings.sequences import measure_length
+ t = TestTerminal(kind=kind)
+ # movements
+ assert (len(t.move(98, 76)) ==
+ measure_length(t.move(98, 76), t))
+ assert (len(t.move(54)) ==
+ measure_length(t.move(54), t))
+ assert not t.cud1 or (len(t.cud1) ==
+ measure_length(t.cud1, t))
+ assert not t.cub1 or (len(t.cub1) ==
+ measure_length(t.cub1, t))
+ assert not t.cuf1 or (len(t.cuf1) ==
+ measure_length(t.cuf1, t))
+ assert not t.cuu1 or (len(t.cuu1) ==
+ measure_length(t.cuu1, t))
+ assert not t.cub or (len(t.cub(333)) ==
+ measure_length(t.cub(333), t))
+ assert not t.cuf or (len(t.cuf(333)) ==
+ measure_length(t.cuf(333), t))
+ assert not t.home or (len(t.home) ==
+ measure_length(t.home, t))
+ assert not t.restore or (len(t.restore) ==
+ measure_length(t.restore, t))
+ assert not t.clear or (len(t.clear) ==
+ measure_length(t.clear, t))
+
+ child_mnemonics_willmove(all_terms)
diff --git a/blessings/tests/test_sequences.py b/blessings/tests/test_sequences.py
new file mode 100644
index 0000000..f42a52c
--- /dev/null
+++ b/blessings/tests/test_sequences.py
@@ -0,0 +1,549 @@
+# -*- coding: utf-8 -*-
+"""Tests for Terminal() sequences and sequence-awareness."""
+# std imports
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+import platform
+import random
+import sys
+import os
+
+# local
+from .accessories import (
+ unsupported_sequence_terminals,
+ all_terms,
+ as_subprocess,
+ TestTerminal,
+ unicode_parm,
+ many_columns,
+ unicode_cap,
+)
+
+# 3rd-party
+import pytest
+import mock
+
+
+def test_capability():
+ """Check that capability lookup works."""
+ @as_subprocess
+ def child():
+ # Also test that Terminal grabs a reasonable default stream. This test
+ # assumes it will be run from a tty.
+ t = TestTerminal()
+ sc = unicode_cap('sc')
+ assert t.save == sc
+ assert t.save == sc # Make sure caching doesn't screw it up.
+
+ child()
+
+
+def test_capability_without_tty():
+ """Assert capability templates are '' when stream is not a tty."""
+ @as_subprocess
+ def child():
+ t = TestTerminal(stream=StringIO())
+ assert t.save == u''
+ assert t.red == u''
+
+ child()
+
+
+def test_capability_with_forced_tty():
+ """force styling should return sequences even for non-ttys."""
+ @as_subprocess
+ def child():
+ t = TestTerminal(stream=StringIO(), force_styling=True)
+ assert t.save == unicode_cap('sc')
+
+ child()
+
+
+def test_parametrization():
+ """Test parameterizing a capability."""
+ @as_subprocess
+ def child():
+ assert TestTerminal().cup(3, 4) == unicode_parm('cup', 3, 4)
+
+ child()
+
+
+def test_height_and_width():
+ """Assert that ``height_and_width()`` returns full integers."""
+ @as_subprocess
+ def child():
+ t = TestTerminal() # kind shouldn't matter.
+ assert isinstance(t.height, int)
+ assert isinstance(t.width, int)
+
+ child()
+
+
+def test_stream_attr():
+ """Make sure Terminal ``stream`` is stdout by default."""
+ @as_subprocess
+ def child():
+ assert TestTerminal().stream == sys.__stdout__
+
+ child()
+
+
+@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None,
+ reason="travis-ci does not have binary-packed terminals.")
+def test_emit_warnings_about_binpacked():
+ """Test known binary-packed terminals (kermit, avatar) emit a warning."""
+ from blessings.sequences import _BINTERM_UNSUPPORTED_MSG
+ from blessings._binterms import binary_terminals
+
+ @as_subprocess
+ def child(kind):
+ import warnings
+ warnings.filterwarnings("error", category=RuntimeWarning)
+ warnings.filterwarnings("error", category=UserWarning)
+
+ try:
+ TestTerminal(kind=kind, force_styling=True)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or
+ err.args[0].startswith('Unknown parameter in ') or
+ err.args[0].startswith('Failed to setupterm(')
+ ), err
+ else:
+ assert 'warnings should have been emitted.'
+ warnings.resetwarnings()
+
+ # any binary terminal should do.
+ child(binary_terminals[random.randrange(len(binary_terminals))])
+
+
+def test_unit_binpacked_unittest():
+ """Unit Test known binary-packed terminals emit a warning (travis-safe)."""
+ import warnings
+ from blessings._binterms import binary_terminals
+ from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG,
+ init_sequence_patterns)
+ warnings.filterwarnings("error", category=UserWarning)
+ term = mock.Mock()
+ term.kind = binary_terminals[random.randrange(len(binary_terminals))]
+
+ try:
+ init_sequence_patterns(term)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind)
+ else:
+ assert False, 'Previous stmt should have raised exception.'
+ warnings.resetwarnings()
+
+
+def test_merge_sequences():
+ """Test sequences are filtered and ordered longest-first."""
+ from blessings.sequences import _merge_sequences
+ input_list = [u'a', u'aa', u'aaa', u'']
+ output_expected = [u'aaa', u'aa', u'a']
+ assert (_merge_sequences(input_list) == output_expected)
+
+
+def test_location_with_styling(all_terms):
+ """Make sure ``location()`` works on all terminals."""
+ @as_subprocess
+ def child_with_styling(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ with t.location(3, 4):
+ t.stream.write(u'hi')
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ unicode_parm('cup', 4, 3),
+ u'hi', unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ child_with_styling(all_terms)
+
+
+def test_location_without_styling():
+ """Make sure ``location()`` silently passes without styling."""
+ @as_subprocess
+ def child_without_styling():
+ """No side effect for location as a context manager without styling."""
+ t = TestTerminal(stream=StringIO(), force_styling=None)
+
+ with t.location(3, 4):
+ t.stream.write(u'hi')
+
+ assert t.stream.getvalue() == u'hi'
+
+ child_without_styling()
+
+
+def test_horizontal_location(all_terms):
+ """Make sure we can move the cursor horizontally without changing rows."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ with t.location(x=5):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ unicode_parm('hpa', 5),
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output), (
+ repr(t.stream.getvalue()), repr(expected_output))
+
+ # skip 'screen', hpa is proxied (see later tests)
+ if all_terms != 'screen':
+ child(all_terms)
+
+
+def test_vertical_location(all_terms):
+ """Make sure we can move the cursor horizontally without changing rows."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ with t.location(y=5):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ unicode_parm('vpa', 5),
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ # skip 'screen', vpa is proxied (see later tests)
+ if all_terms != 'screen':
+ child(all_terms)
+
+
+def test_inject_move_x():
+ """Test injection of hpa attribute for screen/ansi (issue #55)."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ COL = 5
+ with t.location(x=COL):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ u'\x1b[{0}G'.format(COL + 1),
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+ assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1))
+
+ child('screen')
+ child('screen-256color')
+ child('ansi')
+
+
+def test_inject_move_y():
+ """Test injection of vpa attribute for screen/ansi (issue #55)."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ ROW = 5
+ with t.location(y=ROW):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ u'\x1b[{0}d'.format(ROW + 1),
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+ assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1))
+
+ child('screen')
+ child('screen-256color')
+ child('ansi')
+
+
+def test_inject_civis_and_cnorm_for_ansi():
+ """Test injection of cvis attribute for ansi."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ with t.hidden_cursor():
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ u'\x1b[?25l\x1b[?25h',
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ child('ansi')
+
+
+def test_zero_location(all_terms):
+ """Make sure ``location()`` pays attention to 0-valued args."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=True)
+ with t.location(0, 0):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'),
+ unicode_parm('cup', 0, 0),
+ unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_mnemonic_colors(all_terms):
+ """Make sure color shortcuts work."""
+ @as_subprocess
+ def child(kind):
+ def color(t, num):
+ return t.number_of_colors and unicode_parm('setaf', num) or ''
+
+ def on_color(t, num):
+ return t.number_of_colors and unicode_parm('setab', num) or ''
+
+ # Avoid testing red, blue, yellow, and cyan, since they might someday
+ # change depending on terminal type.
+ t = TestTerminal(kind=kind)
+ assert (t.white == color(t, 7))
+ assert (t.green == color(t, 2)) # Make sure it's different than white.
+ assert (t.on_black == on_color(t, 0))
+ assert (t.on_green == on_color(t, 2))
+ assert (t.bright_black == color(t, 8))
+ assert (t.bright_green == color(t, 10))
+ assert (t.on_bright_black == on_color(t, 8))
+ assert (t.on_bright_green == on_color(t, 10))
+
+ child(all_terms)
+
+
+def test_callable_numeric_colors(all_terms):
+ """``color(n)`` should return a formatting wrapper."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ if t.magenta:
+ assert t.color(5)('smoo') == t.magenta + 'smoo' + t.normal
+ else:
+ assert t.color(5)('smoo') == 'smoo'
+
+ if t.on_magenta:
+ assert t.on_color(5)('smoo') == t.on_magenta + 'smoo' + t.normal
+ else:
+ assert t.color(5)(u'smoo') == 'smoo'
+
+ if t.color(4):
+ assert t.color(4)(u'smoo') == t.color(4) + u'smoo' + t.normal
+ else:
+ assert t.color(4)(u'smoo') == 'smoo'
+
+ if t.on_green:
+ assert t.on_color(2)('smoo') == t.on_green + u'smoo' + t.normal
+ else:
+ assert t.on_color(2)('smoo') == 'smoo'
+
+ if t.on_color(6):
+ assert t.on_color(6)('smoo') == t.on_color(6) + u'smoo' + t.normal
+ else:
+ assert t.on_color(6)('smoo') == 'smoo'
+
+ child(all_terms)
+
+
+def test_null_callable_numeric_colors(all_terms):
+ """``color(n)`` should be a no-op on null terminals."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=StringIO(), kind=kind)
+ assert (t.color(5)('smoo') == 'smoo')
+ assert (t.on_color(6)('smoo') == 'smoo')
+
+ child(all_terms)
+
+
+def test_naked_color_cap(all_terms):
+ """``term.color`` should return a stringlike capability."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ assert (t.color + '' == t.setaf + '')
+
+ child(all_terms)
+
+
+def test_formatting_functions(all_terms):
+ """Test simple and compound formatting wrappers."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ # test simple sugar,
+ if t.bold:
+ expected_output = u''.join((t.bold, u'hi', t.normal))
+ else:
+ expected_output = u'hi'
+ assert t.bold(u'hi') == expected_output
+ # Plain strs for Python 2.x
+ if t.green:
+ expected_output = u''.join((t.green, 'hi', t.normal))
+ else:
+ expected_output = u'hi'
+ assert t.green('hi') == expected_output
+ # Test unicode
+ if t.underline:
+ expected_output = u''.join((t.underline, u'boö', t.normal))
+ else:
+ expected_output = u'boö'
+ assert (t.underline(u'boö') == expected_output)
+
+ if t.subscript:
+ expected_output = u''.join((t.subscript, u'[1]', t.normal))
+ else:
+ expected_output = u'[1]'
+
+ assert (t.subscript(u'[1]') == expected_output)
+
+ child(all_terms)
+
+
+def test_compound_formatting(all_terms):
+ """Test simple and compound formatting wrappers."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ if any((t.bold, t.green)):
+ expected_output = u''.join((t.bold, t.green, u'boö', t.normal))
+ else:
+ expected_output = u'boö'
+ assert t.bold_green(u'boö') == expected_output
+
+ if any((t.on_bright_red, t.bold, t.bright_green, t.underline)):
+ expected_output = u''.join(
+ (t.on_bright_red, t.bold, t.bright_green, t.underline, u'meh',
+ t.normal))
+ else:
+ expected_output = u'meh'
+ assert (t.on_bright_red_bold_bright_green_underline('meh')
+ == expected_output)
+
+ child(all_terms)
+
+
+def test_formatting_functions_without_tty(all_terms):
+ """Test crazy-ass formatting wrappers when there's no tty."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=StringIO(), force_styling=False)
+ assert (t.bold(u'hi') == u'hi')
+ assert (t.green('hi') == u'hi')
+ # Test non-ASCII chars, no longer really necessary:
+ assert (t.bold_green(u'boö') == u'boö')
+ assert (t.bold_underline_green_on_red('loo') == u'loo')
+ assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh')
+
+ child(all_terms)
+
+
+def test_nice_formatting_errors(all_terms):
+ """Make sure you get nice hints if you misspell a formatting wrapper."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ try:
+ t.bold_misspelled('hey')
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0]
+ try:
+ t.bold_misspelled(u'hey') # unicode
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0]
+
+ try:
+ t.bold_misspelled(None) # an arbitrary non-string
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' not in e.args[0]
+
+ if platform.python_implementation() != 'PyPy':
+ # PyPy fails to toss an exception, Why?!
+ try:
+ t.bold_misspelled('a', 'b') # >1 string arg
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0], e.args
+
+ child(all_terms)
+
+
+def test_null_callable_string(all_terms):
+ """Make sure NullCallableString tolerates all kinds of args."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=StringIO(), kind=kind)
+ assert (t.clear == '')
+ assert (t.move(1 == 2) == '')
+ assert (t.move_x(1) == '')
+ assert (t.bold() == '')
+ assert (t.bold('', 'x', 'huh?') == '')
+ assert (t.bold('', 9876) == '')
+ assert (t.uhh(9876) == '')
+ assert (t.clear('x') == 'x')
+
+ child(all_terms)
+
+
+def test_bnc_parameter_emits_warning():
+ """A fake capability without target digits emits a warning."""
+ import warnings
+ from blessings.sequences import _build_numeric_capability
+
+ # given,
+ warnings.filterwarnings("error", category=UserWarning)
+ term = mock.Mock()
+ fake_cap = lambda *args: u'NO-DIGIT'
+ term.fake_cap = fake_cap
+
+ # excersize,
+ try:
+ _build_numeric_capability(term, 'fake_cap', base_num=1984)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0].startswith('Unknown parameter in ')
+ else:
+ assert False, 'Previous stmt should have raised exception.'
+ warnings.resetwarnings()
+
+
+def test_bna_parameter_emits_warning():
+ """A fake capability without any digits emits a warning."""
+ import warnings
+ from blessings.sequences import _build_any_numeric_capability
+
+ # given,
+ warnings.filterwarnings("error", category=UserWarning)
+ term = mock.Mock()
+ fake_cap = lambda *args: 'NO-DIGIT'
+ term.fake_cap = fake_cap
+
+ # excersize,
+ try:
+ _build_any_numeric_capability(term, 'fake_cap')
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0].startswith('Missing numerics in ')
+ else:
+ assert False, 'Previous stmt should have raised exception.'
+ warnings.resetwarnings()
+
+
+def test_padd():
+ """ Test terminal.padd(seq). """
+ @as_subprocess
+ def child():
+ from blessings.sequences import Sequence
+ from blessings import Terminal
+ term = Terminal('xterm-256color')
+ assert Sequence('xyz\b', term).padd() == u'xy'
+ assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz'
+
+ child()
diff --git a/blessings/tests/test_wrap.py b/blessings/tests/test_wrap.py
new file mode 100644
index 0000000..e5f7a15
--- /dev/null
+++ b/blessings/tests/test_wrap.py
@@ -0,0 +1,105 @@
+import platform
+import textwrap
+import termios
+import struct
+import fcntl
+import sys
+
+from .accessories import (
+ as_subprocess,
+ TestTerminal,
+ many_columns,
+ all_terms,
+)
+
+import pytest
+
+
+def test_SequenceWrapper_invalid_width():
+ """Test exception thrown from invalid width"""
+ WIDTH = -3
+
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ try:
+ my_wrapped = term.wrap(u'------- -------------', WIDTH)
+ except ValueError as err:
+ assert err.args[0] == (
+ "invalid width %r(%s) (must be integer > 0)" % (
+ WIDTH, type(WIDTH)))
+ else:
+ assert False, 'Previous stmt should have raised exception.'
+ del my_wrapped # assigned but never used
+
+ child()
+
+
+@pytest.mark.parametrize("kwargs", [
+ dict(break_long_words=False,
+ drop_whitespace=False,
+ subsequent_indent=''),
+ dict(break_long_words=False,
+ drop_whitespace=True,
+ subsequent_indent=''),
+ dict(break_long_words=False,
+ drop_whitespace=False,
+ subsequent_indent=' '),
+ dict(break_long_words=False,
+ drop_whitespace=True,
+ subsequent_indent=' '),
+ dict(break_long_words=True,
+ drop_whitespace=False,
+ subsequent_indent=''),
+ dict(break_long_words=True,
+ drop_whitespace=True,
+ subsequent_indent=''),
+ dict(break_long_words=True,
+ drop_whitespace=False,
+ subsequent_indent=' '),
+ dict(break_long_words=True,
+ drop_whitespace=True,
+ subsequent_indent=' '),
+])
+def test_SequenceWrapper(all_terms, many_columns, kwargs):
+ """Test that text wrapping matches internal extra options."""
+ @as_subprocess
+ def child(term, width, kwargs):
+ # build a test paragraph, along with a very colorful version
+ term = TestTerminal()
+ pgraph = u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 '
+ attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse',
+ 'red_reverse', 'red_on_white', 'superscript',
+ 'subscript', 'on_bright_white')
+ term.bright_red('x')
+ term.on_bright_blue('x')
+ term.underline('x')
+ term.reverse('x')
+ term.red_reverse('x')
+ term.red_on_white('x')
+ term.superscript('x')
+ term.subscript('x')
+ term.on_bright_white('x')
+
+ pgraph_colored = u''.join([
+ getattr(term, (attributes[idx % len(attributes)]))(char)
+ if char != u' ' else u' '
+ for idx, char in enumerate(pgraph)])
+
+ internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs)
+ my_wrapped = term.wrap(pgraph, width=width, **kwargs)
+ my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs)
+
+ # ensure we textwrap ascii the same as python
+ assert internal_wrapped == my_wrapped
+
+ # ensure content matches for each line, when the sequences are
+ # stripped back off of each line
+ for line_no, (left, right) in enumerate(
+ zip(internal_wrapped, my_wrapped_colored)):
+ assert left == term.strip_seqs(right)
+
+ # ensure our colored textwrap is the same paragraph length
+ assert (len(internal_wrapped) == len(my_wrapped_colored))
+
+ child(all_terms, many_columns, kwargs)
diff --git a/blessings/tests/wall.ans b/blessings/tests/wall.ans
new file mode 100644
index 0000000..081b4d2
--- /dev/null
+++ b/blessings/tests/wall.ans
@@ -0,0 +1,7 @@
+▄▓▄
+ ░░ █▀▀█▀██▀████████▀█▀▀█ █▀▀█▀██▀▀█▀█xz(imp)
+ ▄▄▄ ▓█████ ▄▄ █▀▀█░ ▀▀▀▀▀ █████▓ ▓█████ ░▓ █▀▀█░ ▓█████ ░▓ █▀▀█░ ░▄ ░░
+ ▐▄░▄▀ ▀▀▀▀▀▀ █▓ ▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ ▀▀▀▀▀▀ ██ ▀▀▀▀▀ █▄▌▌▄▄
+ █▓██ ░▓████ ▐ ████░ ░████ ▄█ ████▓░ ░▓████ ██ ████░ ░▓████ ██ ████░ █▐▄▄█░
+ ██▓▀ ░▓████ █ ████░ ░████ ▐ ████▓░ ░▓████ ▄█▌████░ ░▓████ ▄█▌████░ ▌█████
+ ▀ ░▓████▄█▄▄███ ░ ░ ███▄▄▄▄████▓░ ░▓████▄▄▄▄███ ░ ░▓████▄▄▄▄███ ░ ▀▀▀▓