Pythonでテキストエディタ

# きっかけ
学習用としてテキストエディタ作ってみたい！というのがきっかけです。
windows環境でもLinux環境(ubuntu)でもデバッグ等がやりやすいのでPythonを使いました。

## cursesとは
> curses（カーシス、カーズィス）はUnix系システムでの端末制御ライブラリである。テキストユーザインタフェース (TUI) アプリケーションを作成するのに使われる。名称は“cursor optimization”に由来する。
> [curses - Wikipedia](https://ja.wikipedia.org/wiki/Curses)

TUI/CUIで文字の配置などをデザインしたり、キーボードの処理などができます。
Qiitaで調べてみるとC言語で使ってる方が多い感じのライブラリです。

Pythonでは標準ライブラリとしてすでに用意されていて、特に導入する必要はありません。
(Windowsではないらしく、PyPiにwindows-cursesが用意されているそうです。pipでインストールすれば同じように使えます)

またPython公式が解説を出しています。(が、半分英語です)
　[Python で Curses プログラミング](https://docs.python.org/ja/3/howto/curses.html)

...翻訳してもよくわからなかったので、深い理解はいったん置いておいてテキストエディタを作る手掛かりを探しまして～(cursesは「呪い」という訳が出ることだけわかりました。)

ググって見かけた以下のサイトを参考に作っていきました。
　[【PythonでつくるTUI（Text User Interface）】cursesで作るTUIアプリの作り方](https://knt60345blog.com/curses1/)

# テキストエディタ的なのをつくる
早速作っていきます。
「的な」ものをつくるのが目標なので、今回はVimのようなものを作ります。(cursesもVimをもとに作られたらしいですが...)

## cursesの簡単な使い方
### 初期化と文字の表示
「画面の好きなところに文字を表示させる！」ってだけなら以下のコードでOKです
```Python:test.py
import curses

def main(stdscr):
    #この中に動作を書く
    stdscr.clear()

    stdscr.addstr(0,0,"aaaaaaaaa")

    stdscr.refresh()
curses.wrapper(main)
```
・curses.clear()で画面をすべて消去します。
・stdscr.addstr(0,0,[string])で、座標に文字を表示します。cursesでの座標指定では左上から始まり、(y,x)の順番です。
・stdscr.refresh()で画面の更新を行います。これより前のaddstr()を画面に表示します。


これらの機能を関数にまとめ、それをcurses.wrapper([function])に入れることで動作します。

cursesはキーボード入力が制御されるだけでなくウィンドウも制御されるため、異常終了したときなどにターミナルがめちゃめちゃになります。
```Python:test.py
import curses

stdscr = curses.initscr()
```
cursesを初期化して表示OFF、Enterなしで即入力などの設定をした後、ちゃんとした設定をして終了しないとターミナルがバグります。LinuxかWindowsによってどうなるかは変わりますが。

:::note warn
Pythonインタプリタでこのまま入力するとたぶん何も映らなくなります。
Ctrl-ZかCtrl-Cで強制終了！　もしくは見えないまま"curses.endwin()"と入力
::: 

wrapper()で包むことでこうしたことを防げます。初期化もしてくれるし、強制終了しても勝手に元に戻してくれます。

### ポインタ操作と文字入力
cursesにはポインタが操作できます。
指定した座標にポインタを置くことができます。
また、1文字ずつ入力もできます。

以下のプログラムでは、「'q'と入力されるまでポインタが前後される」ようになっています。
```Python:test.py
import curses
def main(stdscr):
    k = 0
    stdscr.clear()
    #カーソル座標
    cursor_x = 1
    cursor_y = 1
    while key != ord('q'):
        cursor_x = 1 if cursor_x == 0 else 0
        stdscr.move(cursor_y,cursor_x)
        stdscr.refresh()
        key = stdscr.getch() #キー入力を取得する
curses.wrapper(main)
```

カーソルが負の位置や、ウィンドウサイズを超えるとエラーが起きます。
また、キーボードからの入力はすべて十進のASCIIコードとなって返されます。F2やDeleteなどのファンクションキーも数値として返ってきます。

:::note info
ファンクションキーなどは256以上の値として返ってきます
:::

## Textboxオブジェクト
Emacs風にできるテキストウィジェットモジュールがあるそうなんでそれを使おうと思ったのですが、windows版だとcurses.textpadがないらしくよくわからないのでいったん寝かせて熟成させます。

## Vimまがいのものをつくる
そうしていろいろしたものがこちらになります。
```Python:textediter.py
#!usr/bin/env python
# -*- coding: utf-8 -*-

"""
py-text editer version 1.0

こういうの書きたいよね
"""

import curses

version = 0.1

UP,DOWN,LEFT,RIGHT,ENTER,BS,ESC,TAB = curses.KEY_UP,curses.KEY_DOWN,curses.KEY_LEFT,curses.KEY_RIGHT,10,263,27,9

def main(stdscr):
	#変数定義[ k::入力キー, (x,y)::カーソルポインタ]
	#左側に999までのラインナンバーを表示するため、"000|"の4つ開ける = x,y は (5,2)から
	k,x,y = 0,5,2
	current_line = 0	#現在のライン
	inp = ""			#入力文字
	inpstr = []			#入力文字たち
	#画面のリフレッシュ
	stdscr.clear()
	stdscr.refresh()
	
	#色定義
	curses.start_color()	#カラーを使う
	curses.init_pair(1,curses.COLOR_WHITE, curses.COLOR_CYAN)	#文字色を白、背景色をシアンに
	curses.init_pair(2,curses.COLOR_BLACK, curses.COLOR_WHITE)	#文字色を黒、背景色を白に
	curses.init_pair(3,curses.COLOR_WHITE,curses.COLOR_MAGENTA)	#fg:白、bg:マゼンタ
	
	main_flag = True
	
	while main_flag:
		#configモードに移行
		if k == ESC:
			stdscr.refresh()
			config_txt = ""
			while True:
				if k == ord('q'):
					main_flag = False
					break
				elif k == ord('i'):
					break
				elif k == ord('h'):
					config_txt = "q : Quit , i : Insert mode"[:width-1]
					continue
				k = stdscr.getch()
				stdscr.attron(curses.color_pair(2))
				stdscr.addstr(height-1,0," "*(width-1))
				stdscr.addstr(height-1,1,config_txt+chr(k))
				stdscr.attroff(curses.color_pair(2))
				#k = stdscr.getch()
		
		if main_flag == False:
			break
			
		#ウィンドウの最大サイズを取得
		height,width = stdscr.getmaxyx()
		#画面のリフレッシュ
		stdscr.clear()
		stdscr.refresh()

		
		#固定表示
		def fix_disp():
			#下段に表示するテキスト(ステータスバー)
			statusbar_txt = "Esc : Quit"[:width-1]	#文字が狭い場合に切り取るため
			stdscr.attron(curses.color_pair(2))		#カラー設定をON
			stdscr.addstr(height-1,0," "*(width-1))
			stdscr.addstr(height-1,1,statusbar_txt)	#下部の文字を印字
			stdscr.attroff(curses.color_pair(2))		#カラー設定をOFF
			
			#上段に表示するテキスト(タイトルバー)
			titlebar_txt = f"Py-Editer v%1.1f"[:width-1] % version
			stdscr.attron(curses.color_pair(3))
			stdscr.addstr(0,0," "*(width-1))
			stdscr.addstr(0,1,titlebar_txt)
			stdscr.attroff(curses.color_pair(2))
			#２行目に横線を追加
			stdscr.addstr(1,0,"-"*(width-1))
		fix_disp()
		
		#行数表示
		counter = 0
		for i in range(height-3):
			stdscr.addstr(2+counter,0,f"%3d|"%(i+1))
			counter += 1
		#入力
		#画面リフレッシュ工程をまとめる
		def refresh():
			stdscr.clear()
			fix_disp()
			counter = 0
			for i in inpstr:
				stdscr.addstr(2+counter,5,i)
				counter += 1
			#行数表示
			counter = 0
			for i in range(height-3):
				stdscr.addstr(2+counter,0,f"%3d|"%(i+1))
				counter += 1
            stdscr.refresh()
		
		if k == BS:	#Backspace入力で1文字消す
			x -= 1
			inp = inp[:-1]	#1文字消去
			refresh()
			stdscr.addstr(2+current_line,5,inp)
		elif k == ENTER:	#Enter入力で改行
			inpstr.append(inp)
			current_line += 1
			refresh()
			inp = ""
			x = 5
			y = 2 + len(inpstr)
		elif (32 <= k and k <= 126) or k == TAB:	#ASCIIコードで制御文字を抜いた英数字 + TAB
			refresh()
			inp += chr(k)
			stdscr.addstr(2+current_line,5,inp)
			x += 1
		#カーソル操作
		elif k == UP:
			y -= 1
		elif k == DOWN:
			y += 1
		elif k == LEFT:
			x -= 1
		elif k == RIGHT:
			x += 1
		#カーソルのオーバーフロー対策
		if x < 5:
			x = 5
		elif x > width-1:
			x = width-1
		elif y < 2:
			y = 2
		elif y > height-2:
			y = height-2
		
		stdscr.move(y,x)
		
        stdscr.refresh()
		#キー値取得
		k = stdscr.getch()
	
	curses.nocbreak()
	stdscr.keypad(False)
	curses.echo()
	curses.endwin()

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

```

説明がありませんでしたが、背景色なども指定できます。
ubuntu上でコーディングしたため背景色がシアンになってるため、Windowsだと目障りなカラーです。
addstr()は上書きされるため、バックスペース(文字消去)のときには全部リセットする必要があります。textboxモジュールにはこういう問題がないんでしょうが仕方ありません。

これで一応、Vimもどきが完成しました。出力もできないしテキストエディタとは言えませんがそうした機能は簡単に後付けできると思うのでまた今度。


ｱﾗｼﾀ~