curses Module Complexity¶
The curses package prices three separate things, and confusing them is where
terminal applications lose their speed.
Allocating a window costs its whole cell array. Editing cells is local work against that array, and reaches the terminal only where the window was told to push each change out as it happens. Otherwise the update step is what talks to the terminal, and it writes the cells that actually differ from what it believes the screen already shows.
Four costs follow from that split. Batching every window through
noutrefresh() and one doupdate() runs the screen diff once instead of once
per window. A derived window shares its parent's cells, so derwin() is priced
in rows where newwin() is priced in cells. instr() and getstr() stop at a
fixed byte ceiling that no argument raises, and that ceiling moved on 3.14. And
curses.panel charges for the whole deck twice over: reordering it reconciles
the panel that moved against every other one, and handing back a panel object
walks a module-global list of every live panel.
Complexity Reference¶
Size variables: r, c = a window's rows and columns, so r·c is its cells; R, C =
the terminal's rows and columns, which curses.LINES and curses.COLS report;
d = cells whose contents differ between the virtual screen and the physical
screen when the update runs; n = characters or cells an operation is asked to
span; W = cells summed over every window curses currently holds; o = cells two
windows overlap in; a = ancestors above a derived window; p = panel objects
alive in the process; h = rows two panels can overlap in; s = bytes in a
terminfo capability string; e = bytes in the terminal's terminfo description.
Initialization and teardown¶
| Operation | Time | Space | Notes |
|---|---|---|---|
initscr() |
O(R·C) | O(R·C) | Allocates stdscr and the physical-screen image, then copies the ACS_* names plus LINES and COLS from _curses onto the curses package. None of those names exists before the first call |
endwin() |
O(1) | O(1) | Restores the terminal modes. The windows survive, and a later refresh() resumes curses |
isendwin() |
O(1) | O(1) | |
wrapper(func, /, *args, **kwds) |
O(R·C) + func | O(R·C) | initscr(), noecho(), cbreak(), keypad(1) and start_color(), then the call. Whether func returned or raised, the finally puts back keypad(0), echo() and nocbreak() and calls endwin() - which is why a traceback stays readable. start_color() has no inverse and is not undone |
setupterm(term=None, fd=-1) |
O(e) | O(e) | Reads and parses the terminfo entry from the database. initscr() calls it |
use_env(flag) |
O(1) | O(1) | On, the size comes from LINES and COLUMNS where they are set, and from the terminal otherwise. Off, it comes from the terminfo entry, whatever the terminal is really doing. Set it before setupterm() or initscr() |
filter() |
O(1) | O(1) | Restricts curses to the one line the cursor starts on. Call it before initscr() |
error |
— | — | Most wrappers turn the C library's ERR into this, which is how a write past a window's edge surfaces. A few return a sentinel instead - getch() gives -1, tigetnum() gives a negative number |
ncurses_version |
O(1) | O(1) | A named tuple of major, minor, patch |
Terminal modes and input settings¶
Every entry here sets a flag or reads one back. None of them scans a window.
| Operation | Time | Space | Notes |
|---|---|---|---|
cbreak() / nocbreak(), raw() / noraw() |
O(1) | O(1) | |
echo() / noecho(), nl() / nonl() |
O(1) | O(1) | |
halfdelay(tenths) |
O(1) | O(1) | cbreak with a read timeout |
qiflush() / noqiflush(), intrflush(win, flag) |
O(1) | O(1) | |
typeahead(fd) |
O(1) | O(1) | -1 disables the check that aborts a slow update when input is waiting |
meta(flag) |
O(1) | O(1) | |
curs_set(visibility) |
O(1) | O(1) | Returns the previous visibility |
napms(ms) |
blocks for ms | O(1) | The calling thread sleeps |
delay_output(ms) |
O(1) | O(1) | Pays the pause with padding characters where the terminal has one to send, and sleeps where it does not |
beep() / flash() |
O(1) | O(1) | |
ungetch(ch) / unget_wch(ch) |
O(1) | O(1) | |
flushinp() |
O(1) | O(1) | Discards unread input, including anything ungetch() pushed |
def_prog_mode() / def_shell_mode() |
O(1) | O(1) | |
reset_prog_mode() / reset_shell_mode() |
O(1) | O(1) | |
savetty() / resetty() |
O(1) | O(1) | |
get_escdelay() / set_escdelay(ms) |
O(1) | O(1) | |
get_tabsize() / set_tabsize(size) |
O(1) | O(1) | |
getsyx() / setsyx(y, x) |
O(1) | O(1) | The virtual screen's cursor, which the next doupdate() leaves the terminal at |
keyname(k) / unctrl(ch) |
O(1) | O(1) | Both return bytes. unctrl renders the meta bit as an M- prefix, where curses.ascii.unctrl uses ! and returns str |
has_key(k) |
O(1) | O(1) | Importing curses.has_key binds the module of that name over this function, which then raises TypeError when called |
erasechar() / killchar() |
O(1) | O(1) | |
baudrate() / termattrs() / longname() / termname() |
O(1) | O(1) | |
has_colors() / can_change_color() |
O(1) | O(1) | |
has_extended_color_support() |
O(1) | O(1) | |
has_ic() / has_il() |
O(1) | O(1) | |
update_lines_cols() |
O(1) | O(1) | Re-reads LINES and COLS onto the module |
is_term_resized(nlines, ncols) |
O(1) | O(1) | Compares against the current size; no allocation |
resize_term(nlines, ncols) |
O(W) | O(W) | Visits every window curses holds, not just stdscr, and reallocates the ones the new size reaches. W counts the old cells and the new ones, so growing a screen costs more than the size it started at |
resizeterm(nlines, ncols) |
O(W) | O(W) | resize_term() plus repositioning and a queued KEY_RESIZE |
Colours and mouse¶
| Operation | Time | Space | Notes |
|---|---|---|---|
start_color() |
O(1) | O(1) | Also copies COLORS and COLOR_PAIRS onto the curses package; neither exists before the call |
use_default_colors() |
O(1) | O(1) | The same call underneath with -1, -1, and the only spelling available before 3.14 |
assume_default_colors(fg, bg) |
O(1) | O(1) | Python 3.14 and later. Before that only use_default_colors() is exposed |
pair_content(pair_number) |
O(1) | O(1) | |
init_pair(pair_number, fg, bg) |
O(R·C) | O(1) | Scans the screen for cells carrying the pair. Where the pair is one the screen is painted in, it marks them, and the next update repaints those cells instead of diffing. The call itself writes nothing |
init_color(color_number, r, g, b) / color_content(color_number) |
O(1) | O(1) | init_color needs can_change_color() |
color_pair(pair_number) / pair_number(attr) |
O(1) | O(1) | Bit shifts between a pair number and the attribute carrying it |
mousemask(mousemask) |
O(1) | O(1) | Returns (availmask, oldmask); an unsupported event silently drops out of the available mask |
mouseinterval(interval) |
O(1) | O(1) | Returns the previous interval |
getmouse() / ungetmouse(id, x, y, z, bstate) |
O(1) | O(1) | getmouse() pops the event KEY_MOUSE announced |
Terminfo¶
| Operation | Time | Space | Notes |
|---|---|---|---|
tigetflag(capname) / tigetnum(capname) |
O(1) | O(1) | Neither raises. A capability this terminal lacks gives 0 and -1; a name that is not a capability of that kind at all gives -1 and -2 |
tigetstr(capname) |
O(s) | O(s) | s = bytes in a capability string. It is copied out of the entry; the database is not scanned. None where the terminal lacks it |
tparm(str[, ...]) |
O(s) | O(s) | The capability string is interpreted once per call |
putp(string) |
O(s) | O(1) | Writes to stdout, outside the virtual screen curses is tracking |
window: creation¶
curses.window cannot be instantiated; every window comes from one of these.
| Operation | Time | Space | Notes |
|---|---|---|---|
newwin(nlines, ncols[, begin_y, begin_x]) |
O(r·c) | O(r·c) | Allocates the cell array. 0 for a dimension means "to the edge of the screen" |
newpad(nlines, ncols) |
O(r·c) | O(r·c) | A window with no screen position, and free of the screen's size limit. It is pushed out through the six-argument refresh() or noutrefresh() |
window.subwin(...) / window.derwin(...) / window.subpad(...) |
O(r) | O(r) | Only the row pointers are new; the cells are the parent's, so a write through either is visible in the other and the column count does not enter the bound. subwin places the child by screen coordinates, derwin and subpad by coordinates within the parent |
window.resize(nlines, ncols) |
O(r·c) | O(r·c) | |
window.mvwin(new_y, new_x) |
O(r) | O(1) | Moves the window on the screen and marks its lines to go out again |
window.mvderwin(y, x) |
O(r) | O(1) | Moves the view over the parent's cells, which means rebinding one row pointer per row |
window.putwin(file) |
O(r·c) | O(1) | Writes a header and every cell, so the file grows with the window's area |
getwin(file) |
O(r·c) | O(r·c) | Rebuilds the window putwin() wrote |
window.encoding |
O(1) | O(1) | The codec a single non-ASCII character is folded through by insch(), echochar(), hline(), vline() and the background calls. On a build with wide-character support addch() and addstr() take a wide path instead and never reach it; without one, addstr() encodes through it too. It starts at the locale's |
window: writing cells¶
These edit the window's own array and leave the next update to decide what
showing it costs, provided the window is in the default modes. echochar()
refreshes as it writes; under immedok(True) every write below refreshes too;
and under syncok(True) every write also propagates touches up the chain of
ancestors, at syncup()'s cost.
| Operation | Time | Space | Notes |
|---|---|---|---|
window.addch(ch[, attr]) |
O(1) | O(1) | For an ordinary character that fits. A tab, a newline, or a write at the last cell of a scrolling window moves the line or the whole window instead, at that operation's cost |
window.insch(ch[, attr]) |
O(c) | O(1) | Shifts the rest of the line right |
window.echochar(ch[, attr]) |
O(1) + one update | O(1) | addch() followed by a refresh, which also carries out whatever else was pending in the window |
window.addstr(str[, attr]) / window.addnstr(str, n[, attr]) |
O(len(str) + n) | O(len(str)) for a str |
n = characters written, which addnstr caps. A str argument is converted whole - to wide characters, or to bytes on a narrow build - before any of it is written, so a long string with a small n still pays for the conversion. Bounds a window that does not scroll. Wrapping past the last line costs a scroll() each time it happens, ordinary characters included; a newline also clears the rest of its own line and a tab moves to the next stop - the same prices addch() pays |
window.insstr(str[, attr]) / window.insnstr(str, n[, attr]) |
O(len(str) + n + c) | O(len(str)) for a str |
The same conversion, and the rest of the line shifts right. These never scroll |
window.hline(ch, n) / window.vline(ch, n) |
O(n) | O(1) | |
window.chgat(n, attr) |
O(n) | O(1) | Re-attributes cells without rewriting their characters |
window.border(...) / window.box(...) |
O(r + c) | O(1) | The perimeter only |
window.delch([y, x]) |
O(c) | O(1) | |
window.deleteln() / window.insertln() / window.insdelln(nlines) |
O(r·c) | O(1) | Every line below the cursor moves |
window.scroll([lines]) |
O(r·c) | O(1) | Needs scrollok(True) |
window.erase() |
O(r·c) | O(1) | Blanks every cell |
window.clear() |
O(r·c) | O(1) | erase() plus clearok(True), so the next update repaints rather than diffing |
window.clrtoeol() / window.clrtobot() |
O(c) / O(r·c) | O(1) | |
window.bkgd(ch[, attr]) |
O(r·c) | O(1) | Rewrites the background of every existing cell |
window.bkgdset(ch[, attr]) |
O(1) | O(1) | Applies to cells written afterwards only, which is the whole difference from bkgd() |
window.overlay(destwin[, ...]) / window.overwrite(destwin[, ...]) |
O(o) | O(1) | overlay leaves the destination alone where the source cell is blank; overwrite copies the blank over it |
window.attron(attr) / attroff(attr) / attrset(attr) |
O(1) | O(1) | |
window.standout() / window.standend() |
O(1) | O(1) | |
window.move(new_y, new_x) |
O(1) | O(1) |
window: reading cells and input¶
| Operation | Time | Space | Notes |
|---|---|---|---|
window.inch([y, x]) |
O(1) | O(1) | The cell as an integer: the character byte under A_CHARTEXT, the rendition under A_ATTRIBUTES |
window.instr([y, x][, n]) |
O(min(n, cap)) | O(min(n, cap)) | cap is 2047 bytes on 3.14 and 1023 before it. A larger n is silently clamped, so this can never read a wide row whole |
window.getstr([y, x][, n]) |
O(min(n, cap)) + the input | O(min(n, cap)) | The same ceiling on the result. It bounds what comes back, not the keystrokes read to get there: line editing and characters past the ceiling are still processed |
window.getch([y, x]) |
O(1) + the wait | O(1) | nodelay(True) makes the wait zero and timeout(ms) bounds it; with nothing to read this returns -1. A window holding changes that have not been pushed out is refreshed first, at that cost |
window.get_wch([y, x]) / window.getkey([y, x]) |
O(1) + the wait | O(1) | The same wait, but with nothing to read these two raise curses.error rather than returning a sentinel |
window.getyx() / getbegyx() / getmaxyx() / getparyx() |
O(1) | O(1) | getparyx() is (-1, -1) for a window that has no parent |
window.getbkgd() |
O(1) | O(1) | |
window.enclose(y, x) |
O(1) | O(1) | Whether a screen position falls inside the window |
window: the update path¶
| Operation | Time | Space | Notes |
|---|---|---|---|
window.noutrefresh() |
O(r + cells on touched lines), plus syncdown() for a derived window |
O(1), O(a) for a derived window | Copies the window into the virtual screen, skipping the lines nothing touched. No terminal traffic either way. A derived window takes its ancestors' touches on first, so a small window under a deep chain is not cheap |
doupdate() |
O(R·C) | O(1) | Diffs the virtual screen against the physical one and writes output for the d cells that differ, plus whatever cursor movement that takes. With no difference and the cursor where it was, nothing is written |
window.refresh() |
noutrefresh() + O(R·C) |
O(1), O(a) for a derived window | noutrefresh() then doupdate(), so every window refreshed this way pays the screen diff again |
window.redrawwin() / window.redrawln(beg, num) |
O(r·c) / O(num·c) | O(1) | Discards what curses believes the terminal shows of those cells, so the next update writes them in full where touchwin() produces no output at all. Priced in cells, where touching is priced in lines |
window.touchwin() / window.touchline(start[, count]) |
O(r) / O(count) | O(1) | Marks lines changed in the window. The update still diffs against the physical screen, so touching alone produces no output |
window.untouchwin() |
O(r) | O(1) | |
window.is_wintouched() / window.is_linetouched(line) |
O(r) / O(1) | O(1) | |
window.syncup() / window.syncdown() |
O(rows summed over the chain) | O(a) | Carry touches up or down the chain of derived windows. Each level is told which of its lines changed, so a deep chain of tall windows costs more than a deep chain of short ones, and the walk itself is one frame per level |
window.cursyncup() |
O(a) | O(1) | Carries only the cursor position, so this one is the depth alone and the rows do not enter it |
window.clearok(flag) |
O(1) | O(1) | The next update repaints the whole screen instead of diffing |
window.immedok(flag) |
O(1) | O(1) | Makes every later write refresh immediately, turning one update per frame into one per write |
window.idlok(flag) / window.idcok(flag) |
O(1) | O(1) | Whether the update may use the terminal's insert/delete line and character capabilities |
window.leaveok(flag) / window.scrollok(flag) / window.syncok(flag) |
O(1) | O(1) | |
window.keypad(flag) / window.nodelay(flag) / window.notimeout(flag) |
O(1) | O(1) | |
window.timeout(delay) / window.setscrreg(top, bottom) |
O(1) | O(1) |
curses.ascii¶
Every function tests or masks one character code and returns at once. c may
be a one-character str or an int; nothing here allocates more than the
single-character result.
| Operation | Time | Space | Notes |
|---|---|---|---|
isalnum(c) / isalpha(c) / isascii(c) / isblank(c) |
O(1) | O(1) | |
iscntrl(c) / isctrl(c) / ismeta(c) |
O(1) | O(1) | iscntrl counts DEL as a control character; isctrl stops below SP |
isdigit(c) / isxdigit(c) / isgraph(c) / isprint(c) |
O(1) | O(1) | |
islower(c) / isupper(c) / ispunct(c) / isspace(c) |
O(1) | O(1) | |
ascii(c) / ctrl(c) / alt(c) |
O(1) | O(1) | Mask to 7 bits, to 5 bits, or set the meta bit. The result is str for a str argument and int for an int |
unctrl(c) |
O(1) | O(1) | Always str, with ! for the meta bit, where curses.unctrl returns bytes and uses M- |
controlnames |
O(1) | O(1) | The 33 names NUL through US, then SP, indexed by code. Tab and newline appear as HT and LF |
curses.panel¶
Two separate costs run through this module, and both grow with p. Changing the deck makes curses reconcile the panel against the others in it. Separately, the module keeps one global list of every live panel object, newest first, and walks it whenever a call has to hand a panel back; that list is in creation order, not deck order, so its half of the cost tracks how long ago the wanted panel was made rather than where it sits.
| Operation | Time | Space | Notes |
|---|---|---|---|
new_panel(win) |
O(1) | O(1) | Puts the panel on top of the deck. Letting the last reference to a panel go is the expensive half: taking it back out of the deck is O(p) |
panel.top() / panel.bottom() / panel.hide() |
O(p) | O(1) | Each reorders the deck and reconciles it again |
panel.move(y, x) |
O(p + r) | O(1) | The window moves, at mvwin()'s cost, and what the panel covers changes, so the deck is reconciled too |
panel.show() / panel.hidden() / panel.window() |
O(1) | O(1) | show() costs the same whatever the deck holds, which is what separates it from hide() |
panel.set_userptr(obj) / panel.userptr() |
O(1) | O(1) | userptr() raises curses.panel.error when nothing was set |
top_panel() / bottom_panel() |
O(p) | O(1) | Both walk the global list. top_panel() is usually the newest panel and so found at once; bottom_panel() is usually the oldest and so found last |
panel.above() / panel.below() |
O(p) | O(1) | The same walk |
panel.replace(win) |
O(p) | O(1) | The walk, then the swap |
update_panels() |
O(p²·(1 + h)) plus a noutrefresh() per panel |
O(1), or the deepest staged window's O(a) | h = rows a pair of panels overlaps in. Every panel is reconciled against every other over the rows they share, and every panel is then staged into the virtual screen, so its own cells - and its ancestors, if it is a derived window - count too |
error |
— | — | Distinct from curses.error |
curses.textpad¶
| Operation | Time | Space | Notes |
|---|---|---|---|
rectangle(win, uly, ulx, lry, lrx) |
O(height + width) | O(1) | Two hlines, two vlines and the four corners. The lower-right corner must be inside the window |
Textbox(win, insert_mode=False) |
O(1) | O(1) | Also sets keypad(1) on the window |
Textbox.do_command(ch) |
O(1) to O(r·c) for an edit, refresh() for Ctrl-L |
O(1), the window's for Ctrl-L |
A printable key is O(1) in overwrite mode; in insert mode it pushes the rest of the line right a cell at a time, and past the end of a line it carries on into the next. The cursor and kill keys scan a line for its last non-blank, O(c). Ctrl-O and Ctrl-K on an empty line move every line below. Ctrl-L is the one that leaves the box: it refreshes, which is priced against the screen and not the box |
Textbox.gather() |
O(r·c) | O(r·c) | Reads every cell back one at a time. With stripspaces set it still scans each row to find where the blanks begin |
Textbox.edit(validate=None) |
one do_command and refresh per key, then gather() |
O(r·c) | Blocks on getch() until do_command() returns 0, which Ctrl-G makes it do |
Textbox.stripspaces |
O(1) | O(1) | Whether trailing blanks are dropped from gather() and where Ctrl-E lands. On by default |
One doupdate, not one refresh per window¶
window.refresh() is noutrefresh() followed by doupdate(). The first half
is proportional to what the window itself touched; the second diffs the whole
virtual screen against the physical one. Refreshing several windows separately
runs that diff once each where one run would do, and shows every intermediate
screen on the way; batching updates once, from the final virtual screen.
import curses
def main(stdscr):
rows = [curses.newwin(1, 20, y, 0) for y in range(5)] # O(r*c) each
for y, win in enumerate(rows):
win.addstr(0, 0, f"row {y}") # O(n), no terminal traffic
# One screen diff for all five windows, not five.
for win in rows:
win.noutrefresh() # O(touched cells)
curses.doupdate() # O(R*C), writes the diff
# Nothing changed since, so the next update has nothing to write.
curses.doupdate()
# A second refresh of an untouched window is not free either way: it still
# runs the diff. Only the touch flags make it cheap.
assert rows[0].is_wintouched() is False
rows[0].touchwin()
assert rows[0].is_wintouched() is True
# Touching marks the window, not the terminal: the update still compares
# against what the screen shows, and finds no difference.
rows[0].noutrefresh()
curses.doupdate()
# redrawwin() is the one that forces bytes out, by discarding what curses
# believes the terminal is displaying.
rows[0].redrawwin()
rows[0].refresh()
curses.wrapper(main)
Derived windows share their parent's cells¶
newwin() allocates rows × columns of storage. derwin(), subwin() and
subpad() allocate a row pointer per row and point it into the parent, so
their cost does not grow with the width, and a write through one is a write
through the other.
import curses
def main(stdscr):
parent = curses.newwin(10, 40, 0, 0) # O(r*c)
view = parent.derwin(3, 10, 2, 5) # O(r), and shares parent's cells
view.addstr(0, 0, "ZZZ")
assert parent.instr(2, 5, 3) == b"ZZZ" # the parent already has it
parent.addstr(2, 5, "QQQ")
assert view.instr(0, 0, 3) == b"QQQ" # and the traffic runs both ways
# derwin() places the child inside the parent; subwin() uses screen
# coordinates for the same spot.
assert view.getbegyx() == (2, 5)
assert view.getparyx() == (2, 5)
assert parent.subwin(3, 10, 2, 5).getbegyx() == (2, 5)
# A pad is free of the screen's size limit, so it can be far larger than
# the terminal - and it costs its whole area.
pad = curses.newpad(200, 200) # O(r*c)
assert pad.getmaxyx() == (200, 200)
curses.wrapper(main)
instr() and getstr() stop at a fixed ceiling¶
Both read into a buffer whose size the module fixes. The n argument can only
lower it. A row wider than the ceiling cannot be read back in one call, and the
ceiling itself moved on 3.14.
import curses
import sys
CAP = 2047 if sys.version_info >= (3, 14) else 1023
def main(stdscr):
pad = curses.newpad(2, 4000)
pad.addstr(0, 0, "z" * 3000)
# Asking for more than the ceiling is clamped, not an error.
assert len(pad.instr(0, 0, 3000)) == CAP # O(min(n, CAP))
assert len(pad.instr(0, 0)) == CAP # the default is the ceiling too
assert len(pad.instr(0, 0, 10)) == 10 # a smaller n does apply
# inch() is the way to read one cell without the ceiling in play.
assert pad.inch(0, 0) & curses.A_CHARTEXT == ord("z")
curses.wrapper(main)
Reordering a panel deck costs the whole deck¶
Raising a panel, lowering one, hiding one and moving one all make curses
reconcile that panel against every other panel in the deck, and so does letting
the last reference to one go. Separately, the calls that hand a panel object
back search a module-global list that new_panel() prepends to, so the one
that finds the oldest panel searches the whole of it.
What is left costs the same whatever the deck holds: creating a panel, showing
a hidden one again, and reading back hidden(), window() or the user
pointer. show() does put its panel on top; it is only the reconciliation it
skips.
import curses
import curses.panel
def main(stdscr):
windows = [curses.newwin(3, 10, y, y) for y in range(4)]
panels = [curses.panel.new_panel(w) for w in windows] # O(1) each
# The deck is newest-on-top.
assert curses.panel.top_panel() is panels[-1] # O(p), a list walk
assert curses.panel.bottom_panel() is panels[0] # O(p), a list walk
assert panels[0].below() is None # O(p), a list walk
panels[0].top() # O(p), a deck change
assert curses.panel.top_panel() is panels[0]
panels[0].hide() # O(p), a deck change
assert panels[0].hidden() is True # O(1)
assert panels[1].window() is windows[1] # O(1)
assert curses.panel.top_panel() is panels[-1]
panels[0].show() # O(1)
# update_panels() reconciles every panel against every other - O(p^2) - and
# leaves the writing to doupdate().
curses.panel.update_panels() # O(p^2)
curses.doupdate() # O(R*C), writes the diff
curses.wrapper(main)
Reading a box back costs its area¶
Textbox.gather() reads the window one cell at a time, and with stripspaces
on it also scans each row for the last non-blank. Both terms are the window's
area, so the cost is the box's size rather than the amount typed into it.
import curses
import curses.ascii
import curses.textpad
def main(stdscr):
win = curses.newwin(3, 20, 0, 0)
box = curses.textpad.Textbox(win) # O(1)
for ch in "hello":
box.do_command(ord(ch)) # O(1) here; O(c) in insert mode
# stripspaces trims each row to one blank past its last non-blank, and
# drops the all-blank rows entirely - newlines included. The boundary is
# the text's, so parking the cursor elsewhere does not move it.
win.move(2, 19)
assert box.stripspaces == 1
assert box.gather() == "hello \n" # O(r*c)
box.stripspaces = 0
assert box.gather() == "hello" + " " * 15 + "\n" + (" " * 20 + "\n") * 2
# Ctrl-G is the key edit() stops on, and do_command reports it as 0.
assert box.do_command(curses.ascii.BEL) == 0
# curses.ascii never looks past the one character it is given.
assert curses.ascii.isctrl(curses.ascii.BEL) is True
assert curses.ascii.unctrl(1) == "^A" # str, and '!' for the meta bit
assert curses.unctrl(1) == b"^A" # bytes, and 'M-' for it
assert curses.ascii.controlnames[9] == "HT"
curses.wrapper(main)