naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

Python、cursesでシンプルなエディタを作る

約271日前 2018年1月17日1:26
プログラミング関連
Python
「Pythonで、nanoっぽいエディタを作るシリーズ」の一つです。
https://torina.top/detail/438/

今回はシンプルなエディタを作ります。curses.texdpadモジュールにはEmacsに似たキーバインドをサポートしているエディタがあり、まずはそれを使ってみましょう。

import curses
from curses import textpad


def main(stdscr):
textpad.Textbox(stdscr).edit()


if __name__ == '__main__':
curses.wrapper(main)





文字の入力自体はできるのですが、texdpadモジュールでは日本語等のマルチバイト文字が使えません。(Pythonコード的には、ord(char)が32〜126までの文字しか表示されない)そこで、自分で作っていきましょう。
エディタにまず必要なのは、文字を入力するとその文字が表示されることです。以下のようにします。

import curses


def main(stdscr):
stdscr.clear()
while True:
key = stdscr.getch() # 入力された文字の取得
stdscr.addch(key)
stdscr.refresh()


curses.wrapper(main)


while ループの中で、入力された文字を取得(getch)し、それをエディタに入力(addch)、そして更新(refresh)するだけですね。ここから、終了条件も加えましょう。Ctrl+Xで終了するようにします。curses.ascii.CANという定数で、それがCtrl+Xなのかを判断できます。

import curses
import curses.ascii


def main(stdscr):
stdscr.clear()
while True:
key = stdscr.getch() # 入力された文字の取得

# Ctrl+Xで終了
if key == curses.ascii.CAN:
break

# 他のキー、普通の文字等
else:
stdscr.addch(key)

stdscr.refresh()


curses.wrapper(main)



BackSpaceキーで前の文字を削除できるようにしましょう。削除するためには、削除位置を指定(delch(y, x))するか、削除位置に移動して削除する(move(y, x); delch())必要があります。なので、現在のカーソル位置を毎回取得するようにもしました。

import curses
import curses.ascii


def main(stdscr):
stdscr.clear()
while True:
key = stdscr.getch() # 入力された文字の取得
now_y, now_x = stdscr.getyx() # 現在のカーソル位置

# Ctrl+Xで終了
if key == curses.ascii.CAN:
break

# 削除関連キー
elif key in (curses.ascii.BS, curses.KEY_BACKSPACE):
stdscr.delch(now_y, now_x-1) # 今のカーソルの、一つ前の文字を削除

# 他のキー、普通の文字等
else:
stdscr.addch(key)

stdscr.refresh()


curses.wrapper(main)


これでバックスペースでの削除はできるようになったのですが、まだ問題があります。一番左で削除しようとすると以下のようなエラーとなるのです。

Traceback (most recent call last):
File "main.py", line 24, in <module>
curses.wrapper(main)
File "/usr/lib/python3.5/curses/__init__.py", line 94, in wrapper
return func(stdscr, *args, **kwds)
File "main.py", line 17, in main
stdscr.delch(now_y, now_x-1) # 今のカーソルの、一つ前の文字を削除
_curses.error: [mv]wdelch() returned ERR


現在カーソルのx座標が0より大きいときだけ、削除するようにしましょう。

# 削除関連キー
# カーソルが一番左になければ、前の文字を削除
elif key in (curses.ascii.BS, curses.KEY_BACKSPACE):
if now_x > 0:
stdscr.delch(now_y, now_x-1)



→キーでカーソルを右に移動するようにしましょう。カーソルが一番右になければ、カーソル位置を一つ右に移動すればよさそうです。このためには、ウィンドウの最大を取得しておく必要があります。以下のように書き換えておきましょう。

while True:
key = stdscr.getch() # 入力された文字の取得
now_y, now_x = stdscr.getyx() # 現在のカーソル位置
max_y, max_x = stdscr.getmaxyx() # ウィンドウの最大取得

# 文字を表示できる場所はウィンドウの最大に-1した場所まで
max_y -= 1
max_x -= 1



→キーでのカーソル移動処理は、moveで実装できます。

# →キー
# カーソルが一番右になければ、カーソル位置を一つ右に移動
if key == curses.KEY_RIGHT:
if now_x < max_x:
stdscr.move(now_y, now_x+1)


後は、他の移動キーも同様に実装します。

# ←キー
# カーソルが一番左になければ、カーソル位置を一つ左に移動
if key == curses.KEY_LEFT:
if now_x > 0:
stdscr.move(now_y, now_x-1)

# →キー
# カーソルが一番右になければ、カーソル位置を一つ右に移動
elif key == curses.KEY_RIGHT:
if now_x < max_x:
stdscr.move(now_y, now_x+1)

# ↓キー
# カーソルが一番下になければ、カーソル位置を一つ下に移動
elif key == curses.KEY_DOWN:
if now_y < max_y:
stdscr.move(now_y+1, now_x)

# ↑キー
# カーソルが一番上になければ、カーソル位置を一つ上に移動
elif key == curses.KEY_UP:
if now_y > 0:
stdscr.move(now_y-1, now_x)



delchもそうでしたが、addchはカーソルも移動されます。一番右でaddchをすると自動で次の行に移動するのはいいのですが、例えば一番下の行でエンターで改行しようとしたり、一番右下でaddchをすると(max_y+1, 0)にカーソル移動しようとしてエラーとなります。条件分岐でそれらをやめるには、以下のような感じになるでしょう。

# 他のキー、普通の文字等
else:
# 一番右下(max_y, max_x)でのaddchはカーソルが範囲外に行きエラーとなる(max_y+1, 0に移動でエラー)
# そのため、現在カーソルが一番右下の場合は無視
if now_x == max_x and now_y == max_y:
pass

# 一番下での改行キーは無視する
elif now_y == max_y and key in (curses.KEY_ENTER, 10):
pass

# それ以外の場合は単純に入力文字をaddchで追加
# カーソル位置の移動も勝手にする。x == max_x の場合は次行に移動する
else:
stdscr.addch(now_y, now_x, key)



もっと単純な方法として、とりあえず文字入力してエラーが出たら無視する、という方法もあります。

# 他のキー、普通の文字等
else:
try:
stdscr.addch(now_y, now_x, key)
except curses.error:
pass



textpadモジュールのTextboxは良いサンプルコードです。時間があれば眺めてみましょう。
https://github.com/python/cpython/blob/master/Lib/curses/textpad.py