Python curses

From wikinotes

Curses

Python Curses Libraries

(Python) Curses

Python has it's own implementation of curses built in. Originally, I was under the impression that this was not cross-platform, so I turned to other solutions like unicurses, and pdcurses. However, apparently once compiled, python scripts requiring curses can be run even in windows. The majority of the notes below pertain to the python implementation of curses, but the others are supposed to be farily similar.


UniCurses

Unicurses is a COMPLETELY CROSS PLATFORM IMPLEMENTATION OF CURSES!!!!

http://pyunicurses.sourceforge.net/

Installation

windows (manual install)

#MinGw
* install mingw http://www.mingw.org/
** Install Full Base Package when prompted (will take 2hrs)

#PDcurses
* Download PDcurses (3.4 or newer) http://pdcurses.sourceforge.net
* Extract to a folder, navigate to win32 folder in cmd
** <C:\mingw\bin\mingw32-make.exe -f mingwin32.mak DLL=Y
* The result will be pdcurses.dll

#UniCurses
* Download And Extract Unicurses http://sourceforge.net/projects/pyunicurses/
* Copy pdcurses.dll to the root of your unicurses folder (with setup.py, unicurses.py)
* For each version of Python you want to install it for (works with portable too!) execute:

python2 setup.py install
python3 setup.py install
python setup.py install



* test that it worked by running one of the programs in /demos
cp pdcurses.dll demos/
cd demos
python2 test_colors.py

* OMFG 5 hours later I have a curses module installed


Unix Install

## install pdcurses (I don't think this is required)
packer -S pdcurses
packer -S python2-unicurses

## test install
cd Unicuirses-*/demos
python2 test_panels_advanced.py

Examples

References
https://docs.python.org/3.3/howto/curses.html Python Documentation
https://docs.python.org/2/library/curses.panel.html Python Curses.Panels Documentation
http://gnosis.cx/publish/programming/charming_python_6.html Tutorial
Man Pages
man curs_* (Individual) Man pages for many curses commands
Books
Wiley Programmer's Guide to NCurses

Basic Window

#!/usr/bin/env python2
## Creating a basic window
import curses

screen = curses.initscr()				## create screen object

screen.border(0)							## Draw border around screen

screen.addstr(5, 25, "test str")		## create text at Y(5),X(25)
												## text coordinates are measured in
												## ascii characters, not pixesl

screen.refresh()							## Draw window. Curses, to be more efficient,
												## has you build the entire window/contents,
												## then figures out the most efficient way
												## of drawing it onscreen.

screen.getch()								## getCharacter. Execution pauses and waits for
												## user input

curses.endwin()

Curses Window With User Input

#!/usr/bin/env python2
## Basic Window with User-Input

import curses
screen = curses.initscr()

## Modify Terminal behavior
curses.noecho()				## Do not display user key-presses onscreen
curses.curs_set(0)			## Remove Cursor from screen
screen.keypad(1)				## sets mode when entering keypresses

screen.addstr('test string')
screen.addstr('reverse-style string', curses.A_REVERSE)

while True:											## Endless loop for user-input
	event = screen.getch()
	if   event == ord("q"): break				## ord('') returns 0/1 if a value is entered.
	elif event == ord("p"):						## for regular'a','b','0' not big deal, but useful
		screen.clear()								## because also accepts ascii sequences for ctrl/shft
		screen.addstr("user pressed p")		## modifiers
	elif event == ord("P"):
		screen.clear()
		screen.addstr("user pressed P")

curses.endwin()

Wrapping Curses For Errors

## directly importing curses into your python script will
## screw up the traceback errors. using curses.wrapper 
## prevents that.

from curses import wrapper

def main(stdscr):
	import curses
	stdscr.clear()
	stdscr.addstr('hello')
	stdscr.refresh()
	stdscr.getch()

wrapper(main)

Testing

Testing values in curses leaves you in a bit of a bind.

  • You cannot print to the screen, it will not show up.
  • I wrote statusLine that writes to the bottom-left of the window. BUT, you must have the curses screen object.

So, for the sake of testing new programs, I'm using a second gui framework to create messageboxes with testvalues.

pip2 install easygui

import easygui
easygui.easygui.msgbox('Error: blah blah blah')

Screen Info

import os
os.putenv('COLUMNS', '')		## unset environment variables so terminfo is read
os.putenv('LINES',	 '')		## unset environment variables so terminfo is read

height  = curses.LINES			## Height/Width of current window
width   = curses.COLS
curses.termattrs()				## Terminal Attributes (series of ints? not very useful)
curses.is_term_resized()		## returns true if resized

Location Info

curses.move(y,x)					## Move Cursor
screen.paryx()						## get topleft coordinates of window in relation to it's parent.
										## return -1,-1 if no parent

curses.getyx()						## get cursor pos in relation to it's parent
curses.getsyx()					## get cursor pos
curses.setsyx()					## set cursor pos

Clear

win.clear()							## clear window (on next refresh)
win.refresh()						## redraw window

curses.move(y,x)					## move cursor
win.clrtobot()						## Clear from cursor to bottom of screen
win.clrtoeol()						## Clear from cursor to end of line

win.delch(y,x)						## delete character at y,x
win.deleteln()						## delete line under cursor
win.erase()							## clear window contents

Windows

win = curses.newwin(  height, width,   startY, startX )						## Create Window
win = curses.subwin(  height, width,   startY, startX )						## Create Subwin (unsure of distinction)
win.border(0)
win.addstr( 1,1, 'windowtext' )														## Text is positioned relative to the top left
win.addstr( 2,1, 'windowtext2')														## of it's parent control
win.refresh()																				## NOTE: if text belongs to window, refreshing the window's
																								 #       parent screen WILL NOT refresh both the window and screen.
																								 #       you must explicitly refresh the window

Pads

## pads are windows that can extend beyond the displayed
## screen.

pad = curses.newpad( totalSizeY,totalSizeX)																## Create New Pad (Total Size) 
pad = curses.subpad(  height, width,   startY, startX )												## Create SubPad (unsure of distinction)
pad.border(0)																										## Draw Border around Pad
pad.addstr( 1, 1, 'teststuffmore')																			## Add String to 1,1 relative to pad

pad.refresh( scrollY,scrollX,  startRefreshY, startRefreshX, endRefreshY,endRefreshX ) 	## Draw Pad, with scroll offset (scrollY, scrollX)
																												 		 # From pad coordinates (startRefreshY,startRefreshX)
																														 # to pad coordinate     (endRefreshY, endRefreshX)
																														 # (this can be used for scrolling)
																														 # (PAD COORDINATES ARE ALSO USED TO PLACE PADS)

Window/Pad Manipulation

win.is_wintouched()																			## True if window changed since last refresh
win.scroll( lines )																			## Scroll Window by n lines'
win.touchwin()																					## Mark entire window as needing redraw (on next refresh)
win.untouchwin()																				## Mark entire window as 'clean' not needing redraw (on next refresh)
win.resize(y,x)																				## Resize Window

win.mvwin( y, x )																				## Move Existing Window so top left at (y,x)
win.mvderwin( y, x )																			## Move Existing Window within Another Window/Pad

win.putwin( file )																			## Save Window to file
win.getwin( file )																			## Build window from file


Scrolling

There two ways that you can process scrolling in curses. The first is to set win.scrollok(1), which allows you to load more lines into your window than the window size would normally allow for. The second method is to use a pad, which can be scrolled with pad.refresh( scrollY, scrollX, posY, posX, sizeY, sizeX ). There is no scrollbar built into curses, but it wouldn't be very difficult to build a little module to serve that function.

Despite the clear instructions, I have been unable to get scrollok() and idlok() to work. Pads are simpler.

## Windows 
# (NOTE) I haven't been able to get this to work
win = curses.newwin( 15,30, 0,0 )		## create win
win.idlok(1)									## enable scrolling
win.scrollok(1)								## enable scrolling
#win.setscrreg( 1, 20 )						## I think this may be the source of my problem


## Pads
pad = curses.newpad( 15, 30 )				## create pad
pad.refresh( 10,0,  0,0,  15,30 ) 		## scroll down 10 lines

Panels

The Panel library is used when programming curses windows that require windows to be stacked on top of each other. Rather than tracking the depth of every window, and refreshing them all in that order, the panel library keeps track of that information for you, and panel.update_panels() queues refreshs in the correct order for the next scr.refresh().

from curses import panel

## Create/Refresh
panel.newpanel( win )				## Create Panel containing Window
panel.update_panels()				## Mark All Panels for redrawing ( next scr.refresh() )
MyPanel.window()						## Returns Panel's Window


## Panel Stack Info
panel.bottom_panel()					## Return panel on bottom
panel.top_panel()						## Return panel on top
MyPanel.above()						## Return panel on top of MyPanel
MyPanel.below()						## Return panel below MyPanel


## Move Panels in Stack
MyPanel.bottom()						## Move Panel to bottom
MyPanel.top()							## Move Panel to top
MyPanel.replace( win )				## Replace Panel's window with another window


## Show/Hide Panels
MyPanel.hide()							## Hide Panel
MyPanel.show()							## Show Panel
MyPanel.hidden()						## True if panel is hidden


## Pointers (doctags)
MyPanel.set_userptr()				## assign pointer (like doctag)
MyPanel.userptr()						## get pointer

Geometry

curses.hline( y,x, char, length )						## Draw Horiz    line at y,x of char of length 
curses.vline( y,x, char, length )						## Draw Vertical line at y,x of char of length

Text & Objects

## Create Text
##
scr.addstr(  startY, startX, 'this is some text')		# Create/Place text
scr.addchr(  startY, startX,  ord('a') )					# Insert single ASCII character
scr.insstr(  y, x, string )                           # Insert string at y,x (moving characters to right)

scr.instr( y,x, length )										# Return 'length' characters after y,x


scr.getch( y,x )													# get character at y,x

win = curses.win(10,10)											# Place text within GUI element
win.addstr(  startY, startX, 'text in window ' )


## Text Attributes
##
scr.addstr('text', curses.A_BLINK     )					## Blinking text
scr.addstr('text', curses.A_BOLD      )					## Bold text
scr.addstr('text', curses.A_DIM       )					## Half Bright Text (not working?)
scr.addstr('text', curses.A_REVERSE   )					## Inverse text colours (selection)
scr.addstr('text', curses.A_STANDOUT  )					## Italic
scr.addstr('text', curses.A_UNDERLINE )					## Underline


### Colour
##
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE )	## (FG/BG) Define Colour pair 
scr.addstr( 'text', curses.color_pair(1) )						## User colorPair for text
																				## colorpair(0) is hardwired to white on black. Cannot be changed

rgb = curses.color_pair(1)												## Query Colours

Color

## Initialize 256 Colours
curses.use_default_colors()												## use terminal colours (if supported) (makes transparency avail)
for i in range(0, curses.COLORS):										## initialize all 256 terminal colours
	curses.init_pair(i + 1, i, -1)										 # color_pair(255) == 255
curses.addstr('text', curses.color_pair(255)

## Per-Object Colours
curses.init_color( colorNum, r, g, b )									## Change Colour RGB (0-1000)
curses.init_pair( 1, curses.COLOR_RED, curses.COLOR_BLACK )		## (FG/BG) Colour Pair for text etc.
curses.init_pair( 2, curses.COLOR_RED, -1 )							## Use FG of red, on top of Default BG Color

## Wrap Objects in a Colour (or bold,blink etc)
win.attron( curses.color_pair(200) )									## Use ColorPair(1) for all items drawn until attroff
win.attroff( curses.color_pair(200) )									## Stop using ColorPair(1) for all drawn items

Capture Data

screen.getch()										## capture key
screen.ungetch()									## if nested 'getch', pass captured key to next 'getch'

screen.unctrl()									## capture key, but returned key is the escaped version
														## of key (ex:  Ctrl+l    ^l)

Mouse Integration

curses.mousemask(1)							## Make getch() listen for Mouse Events

while True:
	event = screen.getch() 
	if event == curses.KEY_MOUSE:			## Mouse Events returned as 'curses.KEY_MOUSE'

		var = curses.getmouse()				## If mouse event, returns:
													 # BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_CLICKED, BUTTON_DOUBLE_CLICKED,
													 # BUTTON_TRIPLE_CLICKED, BUTTON_SHIFT, BUTTON_CTRL, BUTTON_ALT

		y,x = screen.getsyx()				## Get current cursor position

Terminal/Window Resize

Curses is supposed to return the event 'curses.KEY_RESIZE' if it detects that the window has been resized. I've had some problems, the window hasbeen instead returning 'curses.ERR'. The source of the problem is:

If the environment variables LINES or COLUMNS are set, this overrides the library's 
use of the window size obtained from the operating system. Thus, even if a SIGWINCH 
is received, no screen size change may be recorded. In that case, no KEY_RESIZE is 
queued for the next call to getch; an ERR will be returned instead. 
import os

os.putenv('LINES',   '')				## You must unset these environment variables so that TERMINFO
os.putenv('COLUMNS', '')				## can be read ( and so functions like curses.getmaxyx() work )

while True:									## Resize events are returned as 'curses.KEY_RESIZE'
	key = scr.getch()
	if   key == curses.KEY_RESIZE  :

Sample Functions

The above is a pretty good list of the main tools that you'll be using from the curses library. But despite the building blocks being available, it may still be useful to see some examples of common types of GUIs in code.

Menu List/Colours

Creates a menu from the array items[]. Use j/k to navigate menu, highlighting the current selection in green.

from curses import *
import os
 
os.putenv('LINES', '')
os.putenv('COLUMNS', '')
 

def refreshMenu( win, items, sel, offset ):
	"""
	redraws window/processes selection
	"""
	height  = LINES; width = COLS;
	for i in xrange(0, len(items)):
		if   (sel     == i):    
			win.move( (i+3), 0 )
			win.clrtoeol()
			win.attron( A_REVERSE       )
			win.attron( color_pair(150) )
			win.addstr( (i+3), (offset+2), ('-'+ items[i]) )		## highlight window
			win.attroff( A_REVERSE )
			win.attroff( color_pair(200) )

		else :                  
			win.clrtoeol()
			win.addstr( (i+3), (offset+2), ('-'+ items[i]) )		## print string
	win.border(0)
	win.refresh()


def main(s):
	"""
	Initializes curses window, Gets user Input
	"""
	## Terminal Settings
	curs_set(0)											## Remove Cursor from screen

	## Init Colours
	use_default_colors()
	s.clear()
	s.refresh()
	for i in range(0, COLORS):
		init_pair(i + 1, i, -1)

	## MenuList
	items = [ 'apples', 'oranges', 'bananas', 'fires', 'drwho' ]

	## Build Window
	height  = LINES; width = COLS;
	win     = newwin( height, width, 0, 0 )
	title   = 'My Test Menu'
	sel     = 0 
	offset  = ((width - len(title))/2)

	win.border(0)
	win.addstr( 2, offset, title )				## Print Title
	win.refresh()

	## Get User Input
	while True:
		refreshMenu( win, items, sel, offset )
		key = win.getch()
		if   (key == ord('k')):
			if (sel != 0):
				sel=(sel-1)
				
		elif (key == ord('j')):
			if (sel < (len(items)-1) ):
				sel=(sel+1)

		elif (key == ord('q')):
			break
		win.refresh()


wrapper(main)

Basic Panel Usage

Creates two overlapping windows. When the 'j' key is pressed, the window on top is swapped.

#!/usr/bin/env python2
import os
from   curses import wrapper
os.putenv('LINES', '')
os.putenv('COLUMNS', '')

	
def main(scr):
	import curses
	from   curses import panel
	import os

	## Setup Screen
	curses.start_color()
	scr.clear()
	scr.refresh()
	curses.mousemask(1)

	## Build Overlapping Windows
	menuA = curses.newwin( 3, 5 )		 	## menuA
	menuA.mvwin( 3,3 )
	menuA.border(0)
	menuA.refresh()

	menuB = curses.newwin( 3, 5)			## menuB
	menuB.mvwin( 4,5 )
	menuB.border(0)
	menuB.refresh()

	## Create Panels to manage windows
	panelsA = panel.new_panel( menuA )
	panelsB = panel.new_panel( menuB )
	panel.update_panels();

	
	## User Input
	scr.refresh()
	tmpvar = ''
	while True:
		key = scr.getch()

		if  key == ord('q') :
			break

		## If key j, swap the panel on top
		elif key == ord('j') :
			toppanel = panel.top_panel()
			if toppanel == panelsA :
				scr.addstr( 11, 2, 'panelB Moved On Top' )
				panelsB.top()
			else :
				scr.addstr( 11, 2, 'panelA Moved On Top' )
				panelsA.top()

		panel.update_panels();
		scr.refresh()

wrapper(main)

Resizeable Window

Creates window (with border) that is the maximum size of the terminal screen allows for. When the screen is resized, the window is resized as well.

#!/usr/bin/env python2
import os
from   curses import wrapper

os.putenv('LINES',   '')			## You must unset these environment variables so that TERMINFO
os.putenv('COLUMNS', '')			## can be read ( and so functions like curses.getmaxyx() work )

def main(scr):
	import curses
	import os

	## Setup Screen
	curses.start_color()
	scr.clear()
	scr.refresh()

	## Window
	y,x = scr.getmaxyx()
	win = curses.newwin( y, x)
	win.border(0)
	win.refresh()

	
	## Input
	scr.refresh()
	tmpvar = ''
	while True:
		key = scr.getch()

		## On detected resize, redraw window to maxumum size
		if   key == curses.KEY_RESIZE  :
			win.clear()
			y,x         = scr.getmaxyx()
			var         = ( str(y) +','+ str(x) )
			win.addstr( 5,3, var)
			win.resize( y, x )
			win.border(0)
			win.refresh()

		elif key == ord('q') :
			break

wrapper(main)

Scrollable Window

This is much longer than it needs to be, but I thought I'd include some visual steps so you can understand a little better how it works. The pad needs to be made big enough to accomodate all of the text. If you want a border, you'll probably want to stick the pad in a window. I should make a script to do this for me...

#!/usr/bin/env python2
import os
from   curses import wrapper
os.putenv('LINES', '')
os.putenv('COLUMNS', '')

	
def main(scr):
	import curses


	### STEP 1: CREATE PAD
	###

	## Setup Screen
	scr.clear()
	scr.refresh()

	## Draw Initial Pad, and show
	## user it's size
	pad = curses.newpad( 15,30 )
	pad.border(0)
	pad.refresh( 0,0,  0,0,  15,30 )
	key = scr.getch()



	### STEP 2: RESIZE PAD TO ACCOMODATE LOTS OF TEXT
	###

	## Increase Pad Size as Req'd
	scr.clear()
	pad.clear()
	scr.refresh()
	pad.refresh( 0,0,  0,0,  15,30)
	for i in xrange(1, 40):
		y,x = pad.getmaxyx()					## Resize Pad if Req'd
		if ( i >= y ):
			pad.resize( (i+1),x )

		#pad.refresh( 0,0,  0,0, y,x )		## Add string to Pad
		var = ('scrolltest ' + str(i))
		pad.addstr(  i, 3, var)


	## Draw Pad Modified Pad
	y,x = pad.getmaxyx()
	pad.border(0)
	pad.refresh( 0,0,  0,0, y,x )
	scr.refresh()

	## Pause for User
	key = scr.getch()



	### STEP 3: MAKE PAD SMALL AGAIN, IMPLEMENT SCROLL
	scr.clear()								## Clear Screen
	pad.clear()
	scr.refresh()
	y,x = pad.getmaxyx()
	pad.refresh( 0,0,  0,0,  y,x)
	scr.refresh()

	for i in xrange(1, 40):
		var = ('scrolltest ' + str(i))
		pad.addstr(  i, 3, var)

	pad.refresh( 1,0,  0,0,  15,15)


	scrollY = 0;
	while True:
		key = scr.getch()
		if   ( key == ord('q') ):
			break

		elif ( key == ord('k')) :
			if ( scrollY > 1 ):
				scrollY = scrollY-1
			pad.refresh( scrollY,0,  0,0,  15,15)
			
		elif ( key == ord('j')) :
			if (scrollY < x):
				scrollY = scrollY+1
			pad.refresh( scrollY,0,  0,0,  15,15)

		scr.refresh()


wrapper(main)