curses
Pythonの標準ライブラリに含まれる低レベルなターミナル制御ライブラリ。Unix系システムでのTUIアプリケーション開発の基盤となります。
curses
cursesは、Pythonの標準ライブラリに含まれる低レベルなターミナル制御ライブラリです。Unix系システムのncursesライブラリをPythonから利用できるようにしたもので、多くのTUIライブラリの基盤技術として使用されています。直接的な画面制御と柔軟なカスタマイズが可能です。
主な特徴
標準ライブラリ
- 追加インストール不要: Python標準ライブラリに含まれる
- 安定性: 長年にわたる実績と安定した動作
- 互換性: Unix系システムでの高い互換性
低レベル制御
- 直接制御: 画面、カーソル、キーボードの直接制御
- 高いパフォーマンス: 最適化された画面更新
- 柔軟性: 細かな制御が可能
基盤技術
- 他ライブラリの土台: 多くのTUIライブラリがcursesをベースに構築
- 学習価値: TUIプログラミングの基礎を学べる
- カスタマイズ: 独自のTUIライブラリ開発が可能
インストール
# 標準ライブラリのため、追加インストール不要
import curses
基本的な使用方法
初期化と基本構造
import curses
def main(stdscr):
# 画面の初期化
curses.curs_set(0) # カーソルを非表示
stdscr.nodelay(1) # ノンブロッキングキー入力
stdscr.timeout(100) # 100msタイムアウト
# 色の初期化
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_RED, -1)
curses.init_pair(2, curses.COLOR_GREEN, -1)
curses.init_pair(3, curses.COLOR_BLUE, -1)
height, width = stdscr.getmaxyx()
while True:
stdscr.clear()
# テキストの表示
stdscr.addstr(0, 0, "Hello, curses!")
stdscr.addstr(1, 0, f"Terminal size: {width}x{height}")
# 色付きテキスト
stdscr.addstr(3, 0, "Red text", curses.color_pair(1))
stdscr.addstr(4, 0, "Green text", curses.color_pair(2))
stdscr.addstr(5, 0, "Blue text", curses.color_pair(3))
# キー入力処理
key = stdscr.getch()
if key == ord('q'):
break
elif key != -1:
stdscr.addstr(7, 0, f"Key pressed: {chr(key) if 32 <= key <= 126 else key}")
stdscr.refresh()
# cursesアプリケーションの実行
curses.wrapper(main)
ウィンドウの作成と管理
import curses
def main(stdscr):
curses.curs_set(0)
curses.start_color()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_RED)
height, width = stdscr.getmaxyx()
# メインウィンドウの作成
main_win = curses.newwin(height-2, width-2, 1, 1)
main_win.box()
main_win.addstr(0, 2, " Main Window ")
# サブウィンドウの作成
sub_win = curses.newwin(10, 30, 5, 10)
sub_win.bkgd(' ', curses.color_pair(1))
sub_win.box()
sub_win.addstr(1, 2, "Sub Window")
sub_win.addstr(3, 2, "Press 'q' to quit")
# ポップアップウィンドウ
popup_win = curses.newwin(6, 25, 8, 40)
popup_win.bkgd(' ', curses.color_pair(2))
popup_win.box()
popup_win.addstr(1, 2, "Popup Window")
popup_win.addstr(3, 2, "Any key to close")
# 初期描画
stdscr.refresh()
main_win.refresh()
sub_win.refresh()
show_popup = False
while True:
key = stdscr.getch()
if key == ord('q'):
break
elif key == ord('p'):
show_popup = not show_popup
elif show_popup and key != -1:
show_popup = False
# ポップアップの表示/非表示
if show_popup:
popup_win.refresh()
else:
popup_win.clear()
popup_win.refresh()
# メインウィンドウの再描画
main_win.refresh()
sub_win.refresh()
curses.wrapper(main)
実践的な例
シンプルなテキストエディタ
import curses
class SimpleEditor:
def __init__(self, stdscr):
self.stdscr = stdscr
self.lines = [""]
self.cursor_y = 0
self.cursor_x = 0
self.scroll_y = 0
curses.curs_set(1) # カーソル表示
curses.start_color()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
self.height, self.width = stdscr.getmaxyx()
self.text_height = self.height - 2 # ステータスライン分を除く
def draw(self):
self.stdscr.clear()
# ステータスライン
status = f"Line: {self.cursor_y + 1}, Col: {self.cursor_x + 1} | Press Ctrl+Q to quit"
self.stdscr.addstr(0, 0, status[:self.width-1], curses.color_pair(1))
# テキスト表示
for i in range(self.text_height):
line_num = i + self.scroll_y
if line_num < len(self.lines):
line = self.lines[line_num]
if len(line) <= self.width - 1:
self.stdscr.addstr(i + 1, 0, line)
else:
self.stdscr.addstr(i + 1, 0, line[:self.width-1])
# カーソル位置を設定
screen_y = self.cursor_y - self.scroll_y + 1
if 0 <= screen_y < self.height:
self.stdscr.move(screen_y, min(self.cursor_x, self.width - 1))
self.stdscr.refresh()
def handle_key(self, key):
if key == 17: # Ctrl+Q
return False
elif key == curses.KEY_UP:
if self.cursor_y > 0:
self.cursor_y -= 1
self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
self.adjust_scroll()
elif key == curses.KEY_DOWN:
if self.cursor_y < len(self.lines) - 1:
self.cursor_y += 1
self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
self.adjust_scroll()
elif key == curses.KEY_LEFT:
if self.cursor_x > 0:
self.cursor_x -= 1
elif self.cursor_y > 0:
self.cursor_y -= 1
self.cursor_x = len(self.lines[self.cursor_y])
self.adjust_scroll()
elif key == curses.KEY_RIGHT:
if self.cursor_x < len(self.lines[self.cursor_y]):
self.cursor_x += 1
elif self.cursor_y < len(self.lines) - 1:
self.cursor_y += 1
self.cursor_x = 0
self.adjust_scroll()
elif key == curses.KEY_BACKSPACE or key == 127:
if self.cursor_x > 0:
line = self.lines[self.cursor_y]
self.lines[self.cursor_y] = line[:self.cursor_x-1] + line[self.cursor_x:]
self.cursor_x -= 1
elif self.cursor_y > 0:
# 前の行と結合
prev_line = self.lines[self.cursor_y - 1]
curr_line = self.lines[self.cursor_y]
self.lines[self.cursor_y - 1] = prev_line + curr_line
del self.lines[self.cursor_y]
self.cursor_y -= 1
self.cursor_x = len(prev_line)
self.adjust_scroll()
elif key == 10 or key == 13: # Enter
line = self.lines[self.cursor_y]
self.lines[self.cursor_y] = line[:self.cursor_x]
self.lines.insert(self.cursor_y + 1, line[self.cursor_x:])
self.cursor_y += 1
self.cursor_x = 0
self.adjust_scroll()
elif 32 <= key <= 126: # 印刷可能文字
line = self.lines[self.cursor_y]
self.lines[self.cursor_y] = line[:self.cursor_x] + chr(key) + line[self.cursor_x:]
self.cursor_x += 1
return True
def adjust_scroll(self):
# スクロール調整
if self.cursor_y < self.scroll_y:
self.scroll_y = self.cursor_y
elif self.cursor_y >= self.scroll_y + self.text_height:
self.scroll_y = self.cursor_y - self.text_height + 1
def run(self):
while True:
self.draw()
key = self.stdscr.getch()
if not self.handle_key(key):
break
def main(stdscr):
editor = SimpleEditor(stdscr)
editor.run()
curses.wrapper(main)
メニューシステム
import curses
class Menu:
def __init__(self, stdscr, items, title="Menu"):
self.stdscr = stdscr
self.items = items
self.title = title
self.selected = 0
curses.curs_set(0)
curses.start_color()
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) # 選択項目
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # タイトル
self.height, self.width = stdscr.getmaxyx()
# メニューウィンドウのサイズ計算
self.menu_height = len(items) + 4
self.menu_width = max(len(item) for item in items) + 6
self.menu_width = max(self.menu_width, len(title) + 4)
# 中央配置
self.start_y = (self.height - self.menu_height) // 2
self.start_x = (self.width - self.menu_width) // 2
self.win = curses.newwin(self.menu_height, self.menu_width,
self.start_y, self.start_x)
def draw(self):
self.win.clear()
self.win.box()
# タイトル
title_x = (self.menu_width - len(self.title)) // 2
self.win.addstr(1, title_x, self.title, curses.color_pair(2))
# メニュー項目
for i, item in enumerate(self.items):
y = i + 3
x = 2
if i == self.selected:
self.win.addstr(y, x, f"> {item}", curses.color_pair(1))
else:
self.win.addstr(y, x, f" {item}")
self.win.refresh()
def handle_key(self, key):
if key == curses.KEY_UP:
self.selected = (self.selected - 1) % len(self.items)
elif key == curses.KEY_DOWN:
self.selected = (self.selected + 1) % len(self.items)
elif key == 10 or key == 13: # Enter
return self.selected
elif key == 27: # ESC
return -1
return None
def run(self):
while True:
self.draw()
key = self.stdscr.getch()
result = self.handle_key(key)
if result is not None:
return result
def main(stdscr):
# メインメニュー
main_items = ["New File", "Open File", "Settings", "About", "Exit"]
while True:
menu = Menu(stdscr, main_items, "Main Menu")
choice = menu.run()
if choice == 0: # New File
stdscr.clear()
stdscr.addstr(5, 5, "New File selected. Press any key to continue...")
stdscr.refresh()
stdscr.getch()
elif choice == 1: # Open File
stdscr.clear()
stdscr.addstr(5, 5, "Open File selected. Press any key to continue...")
stdscr.refresh()
stdscr.getch()
elif choice == 2: # Settings
settings_items = ["Theme", "Fonts", "Shortcuts", "Back"]
settings_menu = Menu(stdscr, settings_items, "Settings")
settings_choice = settings_menu.run()
if settings_choice != -1 and settings_choice != 3:
stdscr.clear()
stdscr.addstr(5, 5, f"Settings: {settings_items[settings_choice]} selected")
stdscr.addstr(6, 5, "Press any key to continue...")
stdscr.refresh()
stdscr.getch()
elif choice == 3: # About
stdscr.clear()
stdscr.addstr(5, 5, "Simple curses application")
stdscr.addstr(6, 5, "Press any key to continue...")
stdscr.refresh()
stdscr.getch()
elif choice == 4 or choice == -1: # Exit
break
curses.wrapper(main)
他のライブラリとの比較
特徴 | curses | Blessed | Rich | Textual |
---|---|---|---|---|
インストール | 標準 | pip | pip | pip |
複雑さ | 高 | 低 | 中 | 中 |
制御レベル | 低レベル | 中レベル | 高レベル | 高レベル |
パフォーマンス | 最高 | 高 | 中 | 中 |
学習コスト | 高 | 低 | 中 | 中 |
使用事例
- システムツール: 低レベルなシステム管理ツール
- 高性能アプリ: パフォーマンスが重要なアプリケーション
- 学習目的: TUIプログラミングの基礎学習
- ライブラリ開発: 独自TUIライブラリの基盤
注意事項とベストプラクティス
プラットフォーム対応
import curses
import sys
def main():
try:
# Windowsでは動作しない場合がある
curses.wrapper(run_app)
except ImportError:
print("curses is not available on this platform")
sys.exit(1)
エラーハンドリング
import curses
def safe_addstr(win, y, x, text, attr=0):
"""安全な文字列描画"""
try:
win.addstr(y, x, text, attr)
except curses.error:
# 画面境界外への描画エラーを無視
pass
コミュニティとサポート
- 公式ドキュメント: Python公式ドキュメントで詳細に解説
- 安定性: 長年にわたる実績
- 互換性: Unix系システムでの高い互換性
- 学習リソース: 豊富なチュートリアルと例題
cursesは、TUIアプリケーション開発の基盤技術として、高いパフォーマンスと柔軟性を提供する重要なライブラリです。