How to scroll text in Python/Curses subwindow?
In my Python script which uses Curses, I have a subwin to which some text is assigned. Because the text length may be longer than the window size, the text should be scrollable.
It does开发者_如何学Cn't seem that there is any CSS-"overflow" like attribute for Curses windows. The Python/Curses docs are also rather cryptic on this aspect.
Does anybody here have an idea how I can code a scrollable Curses subwindow using Python and actually scroll through it?
\edit: more precise question
OK with window.scroll
it was too complicated to move the content of the window. Instead, curses.newpad
did it for me.
Create a pad:
mypad = curses.newpad(40,60)
mypad_pos = 0
mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
Then you can scroll by increasing/decreasing mypad_pos
depending on the input from window.getch()
in cmd
:
if cmd == curses.KEY_DOWN:
mypad_pos += 1
mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
elif cmd == curses.KEY_UP:
mypad_pos -= 1
mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
Right, I was a bit confused on how to utilize pads (in order to scroll text), and still couldn't figure it out after reading this post; especially since I wanted to use it in a context of the content being an existing "array of lines". So I prepared a small example, that shows similarities (and differences) between newpad
and subpad
:
#!/usr/bin/env python2.7
import curses
# content - array of lines (list)
mylines = ["Line {0} ".format(id)*3 for id in range(1,11)]
import pprint
pprint.pprint(mylines)
def main(stdscr):
hlines = begin_y = begin_x = 5 ; wcols = 10
# calculate total content size
padhlines = len(mylines)
padwcols = 0
for line in mylines:
if len(line) > padwcols: padwcols = len(line)
padhlines += 2 ; padwcols += 2 # allow border
stdscr.addstr("padhlines "+str(padhlines)+" padwcols "+str(padwcols)+"; ")
# both newpad and subpad are <class '_curses.curses window'>:
mypadn = curses.newpad(padhlines, padwcols)
mypads = stdscr.subpad(padhlines, padwcols, begin_y, begin_x+padwcols+4)
stdscr.addstr(str(type(mypadn))+" "+str(type(mypads)) + "\n")
mypadn.scrollok(1)
mypadn.idlok(1)
mypads.scrollok(1)
mypads.idlok(1)
mypadn.border(0) # first ...
mypads.border(0) # ... border
for line in mylines:
mypadn.addstr(padhlines-1,1, line)
mypadn.scroll(1)
mypads.addstr(padhlines-1,1, line)
mypads.scroll(1)
mypadn.border(0) # second ...
mypads.border(0) # ... border
# refresh parent first, to render the texts on top
#~ stdscr.refresh()
# refresh the pads next
mypadn.refresh(0,0, begin_y,begin_x, begin_y+hlines, begin_x+padwcols)
mypads.refresh()
mypads.touchwin()
mypadn.touchwin()
stdscr.touchwin() # no real effect here
#stdscr.refresh() # not here! overwrites newpad!
mypadn.getch()
# even THIS command erases newpad!
# (unless stdscr.refresh() previously):
stdscr.getch()
curses.wrapper(main)
When you run this, at first you will get something like (newpad
left, subpad
right):
┌────────────────────────┐ ┌────────────────────────┐
│Line 1 Line 1 Line 1 ───│ │Line 1 Line 1 Line 1 ───│
│Line 2 Line 2 Line 2 │ │Line 2 Line 2 Line 2 │
│Line 3 Line 3 Line 3 │ │Line 3 Line 3 Line 3 │
│Line 4 Line 4 Line 4 │ │Line 4 Line 4 Line 4 │
│Line 5 Line 5 Line 5 │ │Line 5 Line 5 Line 5 │
│Line 6 Line 6 Line 6 │
│Line 7 Line 7 Line 7 │
│Line 8 Line 8 Line 8 │
│Line 9 Line 9 Line 9 │
│Line 10 Line 10 Line 10 │
└────────────────────────┘
Some notes:
- Both
newpad
andsubpad
should have their width/height sized to the content (num lines/max line width of the array of lines) + eventual border space - In both cases, you could allow extra lines with
scrollok()
- but not extra width - In both cases, you basically "push" a line at the bottom of the pad; and then
scroll()
up to make room for the next - The special
refresh
method thatnewpad
has, then allows for just a region of this "whole content" to be shown on screen;subpad
more-less has to be shown in the size it was instantiated in - If you draw the borders of the pads before adding content strings - then the borders will scroll up too (that is the
───
piece shown at the...Line 1 ───│
part).
Useful links:
- (n)curses pad in python not working
- http://threeseas.net/vic/vic/terminal.py
- How do I delete a curse window in python and restore background window?
Set the window.scrollok(True).
Documentation
I wanted to use a scrolling pad to display content of some large text files but this didn't work well because texts can have line breaks and it was pretty hard to figure out how many characters to display at a time to fit the good number of columns and rows.
So I decided to first split my text files in lines of exactly COLUMNS characters, padding with spaces when lines were too short. Then scrolling the text become more easy.
Here is a sample code to display any text file:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import curses
import locale
import sys
def main(filename, filecontent, encoding="utf-8"):
try:
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
curses.curs_set(0)
stdscr.keypad(1)
rows, columns = stdscr.getmaxyx()
stdscr.border()
bottom_menu = u"(↓) Next line | (↑) Previous line | (→) Next page | (←) Previous page | (q) Quit".encode(encoding).center(columns - 4)
stdscr.addstr(rows - 1, 2, bottom_menu, curses.A_REVERSE)
out = stdscr.subwin(rows - 2, columns - 2, 1, 1)
out_rows, out_columns = out.getmaxyx()
out_rows -= 1
lines = map(lambda x: x + " " * (out_columns - len(x)), reduce(lambda x, y: x + y, [[x[i:i+out_columns] for i in xrange(0, len(x), out_columns)] for x in filecontent.expandtabs(4).splitlines()]))
stdscr.refresh()
line = 0
while 1:
top_menu = (u"Lines %d to %d of %d of %s" % (line + 1, min(len(lines), line + out_rows), len(lines), filename)).encode(encoding).center(columns - 4)
stdscr.addstr(0, 2, top_menu, curses.A_REVERSE)
out.addstr(0, 0, "".join(lines[line:line+out_rows]))
stdscr.refresh()
out.refresh()
c = stdscr.getch()
if c == ord("q"):
break
elif c == curses.KEY_DOWN:
if len(lines) - line > out_rows:
line += 1
elif c == curses.KEY_UP:
if line > 0:
line -= 1
elif c == curses.KEY_RIGHT:
if len(lines) - line >= 2 * out_rows:
line += out_rows
elif c == curses.KEY_LEFT:
if line >= out_rows:
line -= out_rows
finally:
curses.nocbreak(); stdscr.keypad(0); curses.echo(); curses.curs_set(1)
curses.endwin()
if __name__ == '__main__':
locale.setlocale(locale.LC_ALL, '')
encoding = locale.getpreferredencoding()
try:
filename = sys.argv[1]
except:
print "Usage: python %s FILENAME" % __file__
else:
try:
with open(filename) as f:
filecontent = f.read()
except:
print "Unable to open file %s" % filename
else:
main(filename, filecontent, encoding)
The main trick is the line:
lines = map(lambda x: x + " " * (out_columns - len(x)), reduce(lambda x, y: x + y, [[x[i:i+out_columns] for i in xrange(0, len(x), out_columns)] for x in filecontent.expandtabs(4).splitlines()]))
First, tabulations in the text are converted to spaces, then I used splitlines() method to convert my text in array of lines. But some lines may be longer than our COLUMNS number, so I splitted each line in chunk of COLUMNS characters and then used reduce to transform the resulting list in a list of lines. Finally, I used map to pad each line with trailing spaces so that its length is exactly COLUMNS characters.
Hope this helps.
This is the answer of this question: How to make a scrolling menu in python-curses
This code allows you to create a little scrolling menu in a box from a list of strings.
You can also use this code getting the list of strings from a sqlite query or from a csv file.
To edit the max number of rows of the menu you just have to edit max_row
.
If you press enter the program will print the selected string value and its position.
from __future__ import division #You don't need this in Python3
import curses
from math import *
screen = curses.initscr()
curses.noecho()
curses.cbreak()
curses.start_color()
screen.keypad( 1 )
curses.init_pair(1,curses.COLOR_BLACK, curses.COLOR_CYAN)
highlightText = curses.color_pair( 1 )
normalText = curses.A_NORMAL
screen.border( 0 )
curses.curs_set( 0 )
max_row = 10 #max number of rows
box = curses.newwin( max_row + 2, 64, 1, 1 )
box.box()
strings = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "l", "m", "n" ] #list of strings
row_num = len( strings )
pages = int( ceil( row_num / max_row ) )
position = 1
page = 1
for i in range( 1, max_row + 1 ):
if row_num == 0:
box.addstr( 1, 1, "There aren't strings", highlightText )
else:
if (i == position):
box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
else:
box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], normalText )
if i == row_num:
break
screen.refresh()
box.refresh()
x = screen.getch()
while x != 27:
if x == curses.KEY_DOWN:
if page == 1:
if position < i:
position = position + 1
else:
if pages > 1:
page = page + 1
position = 1 + ( max_row * ( page - 1 ) )
elif page == pages:
if position < row_num:
position = position + 1
else:
if position < max_row + ( max_row * ( page - 1 ) ):
position = position + 1
else:
page = page + 1
position = 1 + ( max_row * ( page - 1 ) )
if x == curses.KEY_UP:
if page == 1:
if position > 1:
position = position - 1
else:
if position > ( 1 + ( max_row * ( page - 1 ) ) ):
position = position - 1
else:
page = page - 1
position = max_row + ( max_row * ( page - 1 ) )
if x == curses.KEY_LEFT:
if page > 1:
page = page - 1
position = 1 + ( max_row * ( page - 1 ) )
if x == curses.KEY_RIGHT:
if page < pages:
page = page + 1
position = ( 1 + ( max_row * ( page - 1 ) ) )
if x == ord( "\n" ) and row_num != 0:
screen.erase()
screen.border( 0 )
screen.addstr( 14, 3, "YOU HAVE PRESSED '" + strings[ position - 1 ] + "' ON POSITION " + str( position ) )
box.erase()
screen.border( 0 )
box.border( 0 )
for i in range( 1 + ( max_row * ( page - 1 ) ), max_row + 1 + ( max_row * ( page - 1 ) ) ):
if row_num == 0:
box.addstr( 1, 1, "There aren't strings", highlightText )
else:
if ( i + ( max_row * ( page - 1 ) ) == position + ( max_row * ( page - 1 ) ) ):
box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
else:
box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], normalText )
if i == row_num:
break
screen.refresh()
box.refresh()
x = screen.getch()
curses.endwin()
exit()
Another way is to print the visible items using for-loop on list with slice notation. That is, you choose a specific part of the list to display, then add or subtract an increment to change the range every time you press a key up or down.
like list[ y : y + coverage ]
where y is the start and coverage determines how many items in the list you'd like to display.
from curses import wrapper
import curses
def main(stdscr):
mY = curses.LINES
win = curses.newwin(100,50,0,50)
win.keypad(True)
numbers = [n for n in range(0,1001)]
ylen = len(numbers)
iny = 0
border_y = mY-5
def scroll(window):
[window.addstr(y, 0, f'{b} \n') for y, b in enumerate(numbers[iny:iny+border_y])]
window.refresh()
scroll(win)
### KEY PRESS ###
while(True):
ch = win.getkey()
if ch == 'KEY_UP':
if(iny>0):
iny-=1
scroll(win)
elif ch == 'KEY_DOWN':
if(iny<ylen-border_y):
iny+=1
scroll(win)
elif ch == 'q':
break
wrapper(main)
精彩评论