naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

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

プログラミング関連 Python 約39日前
2018年1月17日1:26
「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で実装できます。
        # →キー
        # カーソルが一番右になければ、カーソル位置を一つ右に移動
        elif key == curses.KEY_RIGHT:
            if now_x < max_x:
                stdscr.move(now_y, now_x+1)


後は、他の移動キーも同様に実装します。
        # ←キー
        # カーソルが一番左になければ、カーソル位置を一つ左に移動
        elif 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