有一天,我发现了一个很好用的 Python shell——bpython。它使用了 ncurses 来做界面,使用了 pygments 来高亮代码,怎么看都比 ipython 漂亮,更不用说 Python 自己的了。不过既然它使用了 ncurses,麻烦也就来了——ncurses 不支持 readline!虽然有些模拟,但终究是不好用,M-f M-b不起使用,M-数字也不能用。于是我再次去 google 同时使用 ncurses 和 readline 这两个库的办法。
功夫不负有心人,这次终于 google 到了点有用的东西:
The basic idea is to use call rl_callback_read_char() when input from the user is available (determined with select, or similar), then print 'rl_line_buffer' as you would any other string in ncurses, and optionally set A_REVERSE on the position indicated by rl_point. (or just reposition the cursor I guess, either works...)
不过可惜的是,这封邮件给出的代码在我这里并没有跑起来。其实跑起来了也用处不大,因为我需要的那部分代码独立性太差了,还是得重写。
花了一个下午,我终于弄出了一个雏形。
首先,这个rl_callback_read_char()
是这么用的(文档):
#include<stdio.h>
#include<readline/readline.h>
int main(void){
int cont = 1;
void callback(char *text){
if(text == NULL){
rl_callback_handler_remove();
putchar('\n');
cont = 0;
}else{
printf("%s.\n", text);
}
}
rl_callback_handler_install(">> ", callback);
while(cont){
rl_callback_read_char();
}
return 0;
}
首先安装个回调函数,它将在 readline 读取到一行内容时调用。当标准输入可用的时候,调用rl_callback_read_char()
来读取字符。另外注意,这里我用了 gcc 的嵌套函数支持,免得弄出不少全局变量。
知道怎么用rl_callback_read_char()
之后,就可以按那封邮件所说的,把它和 ncurses 联合起来了。代码修改自NCURSES Programming HOWTO。思路很简单,readline 负责读取并处理用户输入,显示是自己处理的。不过作为中文用户,纠结了半天的中文问题。最开始是有了 ncurses 之后,中文显示异常。这个是通过setlocale
解决的。然后又是光标放的位置不对。于是又用上了我同样不熟悉的 wchar,自己计算光标应该放在哪里。
#define _XOPEN_SOURCE 700 /* for wcswidth and 700 is for mbsnrtowcs */
#include<wchar.h>
#include<ncurses.h> /* ncurses.h includes stdio.h */
#include<stdlib.h>
#include<string.h>
#include<readline/readline.h>
#include<locale.h>
int mygetstr(char *str, int y, int x){
WINDOW *win;
int size, col;
int ok = 0;
int width;
wchar_t wstr[80];
char *p;
getmaxyx(stdscr, size, col);
void getaline(char *s){
str = s;
rl_callback_handler_remove();
ok = 1;
}
rl_callback_handler_install("", getaline);
win = newwin(1, col-x, y, x);
while(1){
rl_callback_read_char();
if(ok)
break;
werase(win);
strncpy(str, rl_line_buffer, 80);
p = str;
/* how many column chars before cursor occupies? */
size = mbsnrtowcs(wstr, (const char**)&p, rl_point, 80, NULL);
width = wcswidth(wstr, size);
mvwprintw(win, 0, 0, "%s", str);
/* put the cursor at right column */
wmove(win, 0, width);
wrefresh(win);
}
delwin(win);
return 0;
}
int main(){
char mesg[] = "Enter a string: ";
char str[80];
int row, col;
setlocale(LC_ALL, ""); /* make ncurses handle Chinese correctly */
initscr();
getmaxyx(stdscr, row, col);
mvprintw(row / 2, (col - strlen(mesg)) / 2, "%s", mesg);
refresh();
mygetstr(str, row / 2, (col + strlen(mesg)) / 2);
mvprintw(LINES - 2, 0, "You Entered: %s", str);
getch();
endwin();
return 0;
}
注意:此代码只是演示用,缓冲区溢出什么的我都没处理。
终于搞定了 C 下结合两者的使用,接下来 Python 版思路是有了,但因为其标准库 readline 中没有提供rl_callback_read_char()
函数,所以只能用 ctypes 了。下面是在 Python 里使用rl_callback_read_char()
的示例,ncurses 部分我暂时不想折腾了。
#!/usr/bin/env python3
# vim:fileencoding=utf-8
import sys
import readline
import ctypes
import ctypes.util
rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)
def callback(s):
if s is None:
rllib.rl_callback_handler_remove()
sys.stdout.write('\n')
sys.exit()
elif not s:
pass
else:
print('%s.' % s.decode())
# 这样也可以
# print(readline.get_line_buffer())
cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)
rllib.rl_callback_handler_install.restype = None
rllib.rl_callback_handler_install(ctypes.c_char_p(b">> "), cbfunc(callback))
while True:
rllib.rl_callback_read_char()
2011年8月4日更新:今天终于完成了个 quick and dirty 的 Python 版:
#!/usr/bin/env python3
# vim:fileencoding=utf-8
import sys
import readline
import ctypes
import ctypes.util
import curses
import struct
from 字符集 import width
rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)
def getstr(win, y, x):
_, col = win.getmaxyx()
inputbox = curses.newwin(1, col-x, y, x)
ret = ''
ok = False
def callback(s):
nonlocal ok, ret
if s is None:
rllib.rl_callback_handler_remove()
raise EOFError
elif not s:
ok = True
else:
ret = s.decode()
ok = True
cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)
rllib.rl_callback_handler_install.restype = None
rllib.rl_callback_handler_install(ctypes.c_char_p(b""), cbfunc(callback))
while True:
rllib.rl_callback_read_char()
if ok:
break
inputbox.erase()
# 这样获取的值不对。。。
# bbuf = ctypes.string_at(rllib.rl_line_buffer)
buf = readline.get_line_buffer()
bbuf = buf.encode()
inputbox.addstr(0, 0, buf)
rl_point = struct.unpack('I', ctypes.string_at(rllib.rl_point, 4))[0]
w = width(bbuf[:rl_point].decode())
inputbox.move(0, w)
inputbox.refresh()
del inputbox
return ret
msg = '输入字符串:'
win = curses.initscr()
curses.noecho()
row, col = win.getmaxyx()
win.addstr(row // 2, (col - width(msg)) // 2, msg)
win.refresh()
s = getstr(win, row // 2, (col + width(msg)) // 2)
win.addstr(row - 2, 0, '你输入了: ' + s)
win.getch()
curses.endwin()