3
2
2012
6

为 Chito 修改 Markdown

我使用 Markdown 写博客已经有段时间了,但是一直以来有个小小的问题:对于代码块,markdown 生成的是一个<pre>标签里套一个<code>标签。缩进四个空格还好,用 Vim 的列编辑就行了(>操作不行,因为空行不会被缩进),可是删除这些<code>标签并加上相应的语言标识很烦。于是有了以下 Python 代码,使用的是 Python 版的 markdown,支持使用~~~~作为代码分隔符,如:

~~~~python|这是 Python 代码
print('Hello Python!')
~~~~

将会被翻译为

<pre class="brush: python;" title="这是 Python 代码">print('Hello Python!')
</pre>

程序如下:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import sys
from itertools import takewhile

import markdown
from lxml.html import fromstring, tostring

def parseAttr(s):
  a = s.split('|')
  if len(a) > 3:
    raise ValueError('Too many attributes')
  a = list(map(str.strip, a))
  if len(a) == 3:
    a[2] = bool(a[2])
  elif len(a) == 2:
    a.append(False)
  elif len(a) == 1:
    a.extend(['', False])
  else:
    a = ['plain', '', False]
  if not a[0]:
    a[0] = 'plain'
  return a

def analyseAttrs(text):
  '''Attributes are defined like this:

  ~~~~lang|title|collapse

  In place of ``collapse``, anything not empty is considered true.
  '''
  incode = False
  lines = []
  attrs = []
  istilda = lambda ch: ch == '~'
  for l in text.split('\n'):
    if l.startswith('~~~~'):
      if not incode:
        incode = len(tuple(takewhile(istilda, l)))
        attr = parseAttr(l.lstrip('~'))
        attrs.append(attr)
        l = tildas = '~' * incode
      else:
        if l.find(tildas) == 0:
          incode = False
    lines.append(l)
  return '\n'.join(lines), attrs

def applyAttrs(html, attrs):
  doc = fromstring(html)
  for i, code in enumerate(doc.xpath('//pre/code')):
    pre = code.getparent()
    text = pre[0].text
    del pre[:]
    pre.text = text
    attr = attrs[i]
    c = 'brush: %s;' % attr[0]
    if attr[2]:
      c += ' collapse: true;'
    pre.set('class', c)
    if attr[1]:
      pre.set('title', attr[1])
  return tostring(doc, encoding=str)[5:-6] + '\n'

def main():
  text = sys.stdin.read()
  text, attrs = analyseAttrs(text)
  out = markdown.markdown(text, ['fenced_code'])
  out = applyAttrs(out, attrs)
  sys.stdout.write(out)

if __name__ == '__main__':
  main()
Category: python | Tags: chito markdown python
3
2
2012
6

在 fcitx 中切换国标与传统引号

国家标准使用这些引号:‘’“”,而我发现传统中文的引号更漂亮:「」『』。我切换到传统引号已经有一段时间了,但最近发现有时还是需要使用国标引号,而 fcitx 的现任开发者认为不需要加入该切换功能。好在 fcitx 的配置文件都是文本,又有 fcitx-remote 工具,所以自己很容易地手动实现了两个版本的——Haskell 版本纯粹是练习用,因为没有扩展路径中的~的现成函数,所以只好自己找了个实现,代码有些长。

import Control.Applicative ((<$>))
import System.Cmd (rawSystem)
import System.Directory (getHomeDirectory)
import System.Posix.User
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

main = do
  file <- getFile
  TIO.readFile file >>= (TIO.writeFile file) . (T.map (trChar "“”‘’『』「」" "『』「」“”‘’"))
  reloadFcitx

getFile :: IO FilePath
getFile = expandUser "~/.config/fcitx/data/punc.mb.zh_CN"

reloadFcitx :: IO ()
reloadFcitx = rawSystem "fcitx-remote" ["fcitx-remote", "-r"] >> return ()

expandUser :: FilePath -> IO FilePath
expandUser "~"         = getHomeDirectory
expandUser ('~':'/':p) = fmap (++ "/" ++ p) getHomeDirectory
expandUser ('~':up)    = let (u, p) = break (== '/') up
                             in fmap (++ tail p) (homeDirectory <$> getUserEntryForName u)
expandUser p           = return p

trChar :: [Char] -> [Char] -> Char -> Char
trChar from to ch = case i of
                         Just i -> to !! i
                         _      -> ch
                         where i = elemIndex ch from

Python 版本就很简洁了:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os

m = str.maketrans('“”‘’『』「」', '『』「」“”‘’')
file = os.path.expanduser("~/.config/fcitx/data/punc.mb.zh_CN")

c = open(file).read().translate(m)
open(file, 'w').write(c)
os.execvp('fcitx-remote', ['fcitx-remote', '-r'])
Category: Linux | Tags: fcitx Haskell python
1
9
2012
5

一个 Python 调试函数

Python 有个code模块,可以在程序中开个 REPL 交互命令行,就像 Python 解释器的交互执行一样,调试时非常方便。为了偷懒,我又把它包装了下,写下了repl函数(on github):

def repl(local, histfile=None, banner=None):
  import readline
  import rlcompleter
  readline.parse_and_bind('tab: complete')
  if histfile is not None and os.path.exists(histfile):
    # avoid duplicate reading
    readline.clear_history()
    readline.set_history_length(10000)
    readline.read_history_file(histfile)
  import code
  readline.set_completer(rlcompleter.Completer(local).complete)
  code.interact(local=local, banner=banner)
  if histfile is not None:
    readline.write_history_file(histfile)

之所以要现在把这个函数拿出来,是因为我终于解决了一件让我郁闷很久的问题——补全。历史记录是早就弄好了的,可是补全却经常不给力,补不出东西来,只有少数时候比较正常。这个和 Python 解释器自己的 REPL 不一样。最近在开发 XMPP 群,经常要用到,于是终于去读了rlcompleter.py的代码。还好不长,很快就搞定了:默认使用的是__main__.__dict__这个里边的对象进行补全,而不是globals()。给readline重新设置下补全函数就好了:

readline.set_completer(rlcompleter.Completer(local).complete)
Category: python | Tags: python
12
28
2011
11

利用脚本提升 Wine QQ 登录体验

我从某处下载的QQ2010,其它都好,就是登录时焦点在密码框时,QQ就会崩溃。解决办法是使用QQ自带的软键盘输入密码。但在这个「半字母顺序」排列软键盘上找需要的需要实在费事。作为一名 Linuxer,我自然得想办法将其自动化。

很久之前就已经看到这个Xpresser软件,但可惜的是,它在Arch下跑不起来。但我从中学到了简单的图像匹配,再加上自己对 Xtest 的了解,解决方案呼之欲出。

本来是三个月前就打算写篇文章的,因各种原因迟迟未写。现在因为各种原因再次折腾这家伙,还是写出来分享一下吧。使用OpenCV做图像匹配部分我就不写了,有兴趣的自己去看 Xpresser 或者 winterpy 中的代码。

首先,介绍一下依赖。本脚本依赖众多的东西,其中我自己写的部分在 winterpy 里有,主要是 OpenCV 图像匹配,以及之前写过的 Xtest 调用使用 GDK 截图。最终,我利用它们写成了 xauto.py 库,功能还十分欠缺,但自动登录Wine QQ足够了,因为我做这些的主要目的就是这可恶的QQ。

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import sys
from xauto import XAuto, Image

QQNo = 'YourQQNo'
QQPwd = 'YourQQPassword'

def main():
  if os.fork() == 0:
    if os.fork() == 0:
      os.execlp('rwine', 'rwine')
    else:
      sys.exit()
  os.chdir(os.path.split(sys.argv[0])[0])

  rect = (20, 150, 500, 500)
  xa = XAuto()
  w, h = xa.screensize
  target_w, target_h = 500, 300
  w, h = w - target_w, h - target_h
  w, h = w // 2, h // 2
  center = (w, h, target_w, target_w)
  xa.default_rect = center

  xa.find_and_click('ok.png', repeat=10) or sys.exit('click 确定')
  xa.find_and_click('qq.png', repeat=10) or sys.exit('find qq no input')
  xa.wait(1)

  for k in QQNo:
    xa.key(k)

  xa.wait(0.4)
  pwd_pos = xa.find('input_pwd.png')
  xa.click(pwd_pos)
  caps = Image('caps.png')
  xa.wait(0.4)
  for ch in QQPwd:
    xa.find_and_click('%s.png' % ch) or sys.exit(2)
    xa.wait(0.1)
    xa.find_and_moveto(caps)
    xa.wait(0.1)
  xa.moveto(pwd_pos)
  xa.wait(0.4)
  xa.find_and_click('login.png')

if __name__ == '__main__':
  main()

几点说明:

  1. 执行以下命令禁止QQ记住用户信息,这样再次启动时焦点会在输入QQ号的地方而不是会导致崩溃的密码框。如果你使用我给的压缩包的话应该可以跳过。
    rm -rf Users/All\ Users
    mkdir Users/All\ Users
    chmod -w Users/All\ Users
    
  2. 需要 wine 1.3.32 或更低,以及 wine_gecko 1.3 或更低。新版本在调用 IE 的组件进行显示时会崩溃,这包括「消息管理器」、「查看聊天历史」、「聊天窗口」的侧栏等。
  3. 我执行的是自己包装过的具有隐私保护功能的「rwine」程序。不过也不是特别安全,QQ仍能够访问剪贴板、截图等。
  4. 密码当然是明文保存。你觉得有必要折腾的话可以自己修改。
  5. 程序中需要的图片自己截。应该很容易知道应该截哪里。这样也避免了字体不同导致图像匹配失败。
  6. 此版本的 QQ 可以在这里下载:115 网盘

另注:更简洁好用的 TM2009 没有 wine 成功,登录时弹出错误


2014年3月25日更新:TM2009 以及 TM2013 后来均 Wine 成功了,并且在输入密码时不会崩溃。详情见此文

Category: Linux | Tags: python QQ wine 腾讯
11
28
2011
7

弄了个支持 readline 的 MongoDB shell

不知道怎么搞的,新版 MongoDB 自带的 mongo shell 现在不支持 readline,而且使用一个极简到工作不正常的 linenoise。编译时加上 readline 也没用。虽然内建了简单的历史记录和补全,可是历史记录不能搜索,补全只能像 Vim 命令行默认的那样一个一个切换不能像 bash/zsh 那样全部列出来。这些还不是最令人郁闷的。最让我受不了的问题和十年前的 DOS 版 WPS 里遇到的差不多——当年的 WPS 里删汉字一次只删一半,现在的 mongo shell 里删汉字一次只删三分之一!而且光标定位是错的,按字节算的!

于是乎拿 Python 写了一个 shell。不愧是 Python,内置的东西真不错,不到100行就写好了。不过用到了自己另外的库函数,另外用到了自己的 colorless 程序来高亮显示查询结果。如果不想要 pygments 这个依赖的话,可以用 less 程序代替。以下贴个无高亮版的,高亮版的见 github

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import sys
import os
from pprint import pprint
import subprocess

from pymongo import Connection
import pymongo.cursor

# 这个模块位于 github 上的 winterpy 仓库的 pylib/cli.py
from cli import repl

import locale
locale.setlocale(locale.LC_ALL, '')
del locale

host = 'localhost'
port = 27017
db = 'test'

def displayfunc(value):
  if isinstance(value, pymongo.cursor.Cursor):
    p = subprocess.Popen(['less', '-RFX'], stdin=subprocess.PIPE,
                        universal_newlines=True)
    pprint(list(value), stream=p.stdin)
    p.stdin.close()
    p.wait()
  else:
    pprint(value)

def main():
  global db
  conn = Connection(host=host, port=port)
  db = conn[db]

  v = globals().copy()
  v.update(locals())
  del v['repl'], v['argv'], v['main'], v['v'], v['host'], v['port']
  del v['displayfunc'], v['subprocess']
  del v['__name__'], v['__cached__'], v['__doc__'], v['__file__'], v['__package__']
  sys.displayhook = displayfunc

  repl(
    v, os.path.expanduser('~/.mongo_history'),
    banner = 'Python MongoDB console',
  )

if __name__ == '__main__':
  argv = sys.argv
  if len(argv) == 2:
    if '/' in argv[1]:
      host, db = argv[1].split('/', 1)
    if ':' in host:
      host, port = host.split(':', 1)
  elif len(argv) == 1:
    pass
  else:
    sys.exit('argument error')

  main()
Category: python | Tags: python MongoDB linenoise readline
10
14
2011
9

通过命名管道进行异步通信

需求是这样子的:一个程序要提供一个IPC接口,接收异步的命令。这个接口应该尽量简单,能像/proc下的文件那样通过写入数据来通信,所以我选中了命名管道。读取命名管道很简单,像普通文件那样打开然后读取就可以了。但这样做的问题是,在没有写者的时候open会阻塞。man 2 open下找到了两个标志位:O_ASYNCO_NONBLOCK。我被排在前面的O_ASYNC骗了,它只是读写时使用信号进行异步操作,open依旧阻塞。继续向后翻,才看到O_NONBLOCK,还特意注明了Neither the open() nor any subsequent operations on the file descriptor which is returned will cause the calling process to wait.

试了试,发现open并不像读写时那样在将阻塞时返回EWOULDBLOCK错误,而是返回了一个可用的文件描述符。既然文件描述符都有了,接下来自然毫无悬念地select了。完整的演示代码如下:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import time
import select

fd = os.open('test', os.O_NONBLOCK | os.O_RDONLY)
while True:
  if not select.select([fd], [], [], 1)[0]:
    print('waiting...')
  else:
    got = os.read(fd, 1024).decode().rstrip()
    if not got:
      os.close(fd)
      fd = os.open('test', os.O_NONBLOCK | os.O_RDONLY)
    else:
      print('got', got)
Category: Linux | Tags: linux python fifo 异步
10
13
2011
2

GM 脚本:在 Chito 后台评论列表中显示评论者的地址位置

GreaseMonkey 代码如下:

// ==UserScript==
// @name           is-programmer 后台评论地理位置显示
// @namespace      http://lilydjwg.is-programmer.com/
// @description    通过 JSONP 查询 IP 地址对应的地理位置并显示
// @include        http://*.is-programmer.com/admin/comments*
// @include        http://*.is-programmer.com/admin/messages*
// ==/UserScript==

var qurl = function(ips){
  return 'http://localhost:2000/queryip?q=' + ips.join(',') + '&cb=?';
};

var letsJQuery = function(){
  var ip_header = document.evaluate('//th[@class="helpHed" and text()="IP"]', document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);
  $(ip_header).after('<th class="helpHed">地址</th>');
  var ip_cells = document.getElementsByClassName('comment_ip_col');
  var ips = [];
  var i;
  for(i=0, len=ip_cells.length; i<len; i++){
    ips.push(ip_cells[i].textContent);
  }
  $.getJSON(qurl(ips), function(data){
    var ans = data.ans;
    for(i=0, len=ip_cells.length; i<len; i++){
      $(ip_cells[i]).after('<td class="comment_addr_col">'+ans[i]+'</td>');
    }
  });
};

function GM_wait(){
  if(typeof unsafeWindow.jQuery == 'undefined') {
    window.setTimeout(GM_wait, 500);
  }else{
    $ = unsafeWindow.jQuery;
    letsJQuery();
  }
}

GM_wait();

光有这个脚本是不够的,因为没有 IP 地址数据库。我不想像这样用 chrome 权限调用子进程之类的手段,而是从本地 HTTP server 取得数据,这样以后可以方便地扩展。HTTP server 使用 Python 的 tornado 框架写成,名字是“Web Service Provider”的缩写:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

from subprocess import getoutput
from functools import lru_cache
import json

import tornado.web
import tornado.httpserver
from tornado.options import define, options

@lru_cache()
def lookupip(ip):
  return getoutput('cip ' + ip).replace('CZ88.NET', '').strip() or '-'

class IPHandler(tornado.web.RequestHandler):
  def get(self):
    q = self.get_argument('q').split(',')
    addr = []
    for ip in q:
      a = lookupip(ip)
      if 'illegal' in a:
        a = '(错误)'
      elif '\n' in a:
        a = ''
      addr.append(a)

    ans = {
      'ans': addr,
    }
    cb = self.get_argument('cb', None)
    if cb:
      self.set_header('Content-Type', 'text/plain; charset=utf-8')
      self.write('%s(%s)' % (cb, json.dumps(ans, ensure_ascii=False)))
    else:
      self.write(ans)

def main():
  define("port", default=2000, help="run on the given port", type=int)

  tornado.options.parse_command_line()
  application = tornado.web.Application([
    (r'/queryip$', IPHandler),
  ],
    debug=True,
  )
  application.listen(options.port)
  tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
  try:
    main()
  except KeyboardInterrupt:
    pass

用的是 Python 3.2,我很喜欢它的lru_cache装饰器。

9
24
2011
8

通过PyGObject调用GDK截图

Linux 下截个图挺麻烦的。最开始我想学 scrot 使用 Xlib,结果因为看不懂而放弃,转而使用GDK。搜到了TualatriX的这篇《几十行代码构造一个截屏软件》。虽然才不到50行的 C 代码,但我还是觉得有点长。

本来准备像上次的《使用Xtest模拟鼠标点击》一样写成 Python 模块的,后来从 Vayn 那里看到原来可以通过 PyGObject 来调用 GTK 及 GDK 等等(hello world 程序)。于是我也用这种方式完成了截图的代码,才十几行,原理和TualatriX的完全一样。

import mimetypes
from gi.repository import Gdk

def screenshot(filename, rect=None, filetype=None):
  screen = Gdk.Screen.get_default()
  if rect is None:
    rect = (0, 0, screen.width(), screen.height())
  if filetype is None:
    t = mimetypes.guess_type(filename)[0]
    if t is None:
      raise ValueError('cannot guess filetype for filename: %s' % filename)
    filetype = t.split('/')[1]

  rootwin = screen.get_root_window()
  pixbuf = Gdk.pixbuf_get_from_window(rootwin, *rect)
  pixbuf.savev(filename, filetype, (), ())

不过没有找到PyGObject的文档。官方说可以自己从 gir 文件生成,但是那个脚本在最新版的代码中才有,而那个代码也要求Glib非常新,我的 Arch 上都没有那么新,于是作罢。所以用法除了自己按 GDK 的文档猜就是 Google 了。那个savev的参数我都找到mono的文档去了。。。

PyGObject 默认是使用 GTK 3。也可以指定使用 GTK 2:

import gi
gi.require_version("Gdk", "2.0")
gi.require_version("Gtk", "2.0")
from gi.repository import Gdk, Gtk

/usr/lib/girepository-1.0/下还有一些typelib文件,说明这些库都有 GObject Introspection 支持,可以用包括 Python 3 在内的任何其支持的语言访问。不过我调用 xlib 时出错了:

>>> from gi.repository import xlib
>>> d = xlib.open_display()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.2/site-packages/gi/types.py", line 44, in function
    return info.invoke(*args)
glib.GError: Could not locate XOpenDisplay: `XOpenDisplay': python3: undefined symbol: XOpenDisplay

GObject Introspection 这个东西挺好的,除了文档。文档啊,就算不能支持 Python 的 docstring,至少也弄个 HTML 版出来啊,现在只有堆 XML 文件叫我怎么情何以堪啊,现在用 PyGObject 写代码就像在猜谜。。。

最后,代码的 github 链接

Category: python | Tags: gtk linux python
9
21
2011
2

使用Xtest模拟鼠标点击

最近做一个小工具,需要模拟鼠标点击事件。当然,我可不想去调用 xdotool 或者 xmacro,效率什么不说,光是添加这么个罕见的依赖就不喜欢。顺便也好练习下 C 编程。

Xtest 的函数名长参数列表也长,不过用起来很简单。我所需要调用的函数就两个:

  • XTestFakeMotionEvent:把鼠标光标移动到指定坐标;
  • XTestFakeButtonEvent:模拟鼠标键

Xtest 的函数手册都在一个 manpage 里。看一下就知道用法了。

XTestFakeMotionEvent有五个参数,第一个是Display指针,然后依次是屏幕号、坐标和延时。屏幕号写-1就是默认了。延时我用0就好了。XTestFakeButtonEvent有四个参数,第一个依旧是Display指针,然后是按键号、是不是按下(还是放开按键)、延时。左键是1其它依次递加。不知道为什么这些函数要有个延时的参数。

#include<X11/Xlib.h>
#include<X11/extensions/XTest.h>

/* ... */

int clickAt(int x, int y){
  Display *dpy = XOpenDisplay(NULL);
  if(dpy == NULL){
    return 0;
  }

  XEvent event;

  /* get info about current pointer position */
  XQueryPointer(dpy, RootWindow(dpy, DefaultScreen(dpy)),
      &event.xbutton.root, &event.xbutton.window,
      &event.xbutton.x_root, &event.xbutton.y_root,
      &event.xbutton.x, &event.xbutton.y,
      &event.xbutton.state);

  XTestFakeMotionEvent(dpy, -1, x, y, 0);
  XTestFakeButtonEvent(dpy, 1, 1, 0);
  XTestFakeButtonEvent(dpy, 1, 0, 0);
  /* place the mouse where it was */
  XTestFakeMotionEvent(dpy, -1, event.xbutton.x, event.xbutton.y, 0);
  XCloseDisplay(dpy);
  return 1;
}

这个函数实现了点击指定的屏幕坐标,完事之后再把鼠标光标移回去。最开始是没有移回去的,然后测试的时候我经常找不到鼠标光标了。。。。

C 语言用起来挺不爽的,所以后来做了个 Python 模块。不过功能很不完整,以后有需要时再慢慢加啦。要是谁有兴趣也可以自己加了后给我发 pull request 就更好了。代码地址:https://github.com/lilydjwg/winterpy/blob/master/pyso/X.c,编译命令:

gcc -O2 -shared -lX11 -lXtst `pkg-config --cflags --libs python3` X.c -o X.so

编译后import X,然后help(X)就知道用法了。

Category: Linux | Tags: python C代码 X Window
7
31
2011
5

在 ncurses 中使用 readline

有一天,我发现了一个很好用的 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()
      
Category: Linux | Tags: C代码 ncurses python readline

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com