TTY
ターミナルアプリケーション開発のためのツールキット。CLIパーサー、プロンプト、プログレスバーなど多数のコンポーネントを提供。
フレームワーク
TTY
概要
TTYは、ターミナルアプリケーション開発のためのツールキットです。CLIパーサー、プロンプト、プログレスバーなど多数のコンポーネントを提供します。モジュラーアプローチにより、必要な機能だけを選択して使用できるため、カスタマイズ性を重視する開発者に人気です。美しく対話的なターミナルアプリケーションの開発を支援します。
詳細
TTYは、ターミナルアプリケーション開発のための包括的なツールキットです。30以上の独立したgemで構成されており、プロジェクトの要件に応じて必要なコンポーネントのみを選択して使用できます。各コンポーネントは単独で使用可能で、他のCLIライブラリとの組み合わせも容易です。
主なコンポーネント
- TTY::Command: システムコマンドの実行
- TTY::Prompt: 対話的なプロンプト
- TTY::ProgressBar: プログレスバー表示
- TTY::Table: テーブル表示
- TTY::Box: ボックス描画
- TTY::Spinner: スピナーアニメーション
- TTY::Screen: ターミナル情報取得
- TTY::Cursor: カーソル制御
- TTY::Color: 色管理
メリット・デメリット
メリット
- モジュラー設計: 必要な機能のみを選択して使用可能
- 豊富なコンポーネント: ターミナルアプリケーション開発に必要な機能が揃っている
- 美しいUI: 視覚的に魅力的なターミナル表示
- カスタマイズ可能: 高度なカスタマイズとスタイリング
- 独立性: 各コンポーネントが独立して使用可能
- アクティブな開発: 継続的な改善とアップデート
- 充実したドキュメント: 詳細な使用例とAPI文書
デメリット
- 学習コスト: 多数のコンポーネントの理解に時間が必要
- 依存関係: 多くのコンポーネントを使用すると依存関係が増加
- 複雑性: シンプルなツールには過剰な場合がある
- Ruby特化: Ruby以外の言語では使用不可
主要リンク
書き方の例
インストール
# 全コンポーネント
gem install tty
# 個別コンポーネント
gem install tty-prompt
gem install tty-progressbar
gem install tty-table
TTY::Promptの使用例
require 'tty-prompt'
prompt = TTY::Prompt.new
# 基本的な入力
name = prompt.ask('お名前は?')
# パスワード入力
password = prompt.mask('パスワードを入力してください:')
# 数値入力(バリデーション付き)
age = prompt.ask('年齢は?', convert: :int) do |q|
q.validate(/^\d+$/, 'Please enter a valid number')
q.validate(proc { |v| v.to_i > 0 }, 'Age must be greater than 0')
end
# 選択肢
framework = prompt.select('好きなフレームワークは?') do |menu|
menu.choice 'Rails'
menu.choice 'Sinatra'
menu.choice 'Hanami'
menu.choice 'Roda'
end
# 複数選択
languages = prompt.multi_select('使用できる言語は?') do |menu|
menu.choice 'Ruby'
menu.choice 'Python'
menu.choice 'JavaScript'
menu.choice 'Go'
menu.choice 'Rust'
end
# Yes/No確認
confirmed = prompt.yes?('設定を保存しますか?')
# 結果表示
puts "\n=== 入力結果 ==="
puts "名前: #{name}"
puts "年齢: #{age}"
puts "フレームワーク: #{framework}"
puts "言語: #{languages.join(', ')}"
puts "保存: #{confirmed ? 'はい' : 'いいえ'}"
TTY::ProgressBarの使用例
require 'tty-progressbar'
# 基本的なプログレスバー
bar = TTY::ProgressBar.new("ダウンロード中 [:bar] :percent", total: 100)
100.times do |i|
sleep(0.05)
bar.advance(1)
end
puts "\nダウンロード完了!"
# 複数のプログレスバー
bars = TTY::ProgressBar::Multi.new("全体の進行状況:")
download_bar = bars.register("ダウンロード [:bar] :percent", total: 100)
process_bar = bars.register("処理中 [:bar] :percent", total: 50)
upload_bar = bars.register("アップロード [:bar] :percent", total: 30)
# 並行処理をシミュレート
threads = []
threads << Thread.new do
100.times { download_bar.advance(1); sleep(0.02) }
end
threads << Thread.new do
50.times { process_bar.advance(1); sleep(0.04) }
end
threads << Thread.new do
30.times { upload_bar.advance(1); sleep(0.06) }
end
threads.each(&:join)
TTY::Tableの使用例
require 'tty-table'
# 基本的なテーブル
header = ['名前', '年齢', '職業', '都市']
rows = [
['田中太郎', 30, 'エンジニア', '東京'],
['佐藤花子', 25, 'デザイナー', '大阪'],
['鈴木一郎', 35, 'マネージャー', '名古屋']
]
table = TTY::Table.new(header, rows)
puts table.render(:unicode)
# スタイル付きテーブル
table = TTY::Table.new do |t|
t << ['商品名', '価格', '在庫', 'カテゴリ']
t << :separator
t << ['ノートPC', '¥80,000', '15', 'コンピュータ']
t << ['マウス', '¥2,500', '50', '周辺機器']
t << ['キーボード', '¥8,000', '25', '周辺機器']
end
puts table.render(:unicode, padding: [0, 1], border: {top: '=', bottom: '='})
# アライメント
table = TTY::Table.new do |t|
t << ['左揃え', '中央揃え', '右揃え']
t << ['Left', 'Center', 'Right']
t << ['L', 'C', 'R']
end
puts table.render do |renderer|
renderer.alignments = [:left, :center, :right]
renderer.padding = [0, 1]
renderer.border.style = :thick
end
TTY::Boxの使用例
require 'tty-box'
# 基本的なボックス
box = TTY::Box.frame(
width: 50,
height: 10,
align: :center,
padding: 1,
title: {top_left: " 情報 "}
) do
"これはボックスの中のテキストです。\n複数行のテキストも\n表示できます。"
end
puts box
# スタイル付きボックス
success_box = TTY::Box.success(
"処理が正常に完了しました!",
width: 40,
align: :center,
padding: 1
)
puts success_box
warning_box = TTY::Box.warn(
"注意: この操作は元に戻せません。",
width: 40,
align: :center,
padding: 1
)
puts warning_box
error_box = TTY::Box.error(
"エラー: ファイルが見つかりません。",
width: 40,
align: :center,
padding: 1
)
puts error_box
TTY::Spinnerの使用例
require 'tty-spinner'
# 基本的なスピナー
spinner = TTY::Spinner.new("[:spinner] データを読み込み中...")
spinner.auto_spin
sleep(3) # 処理をシミュレート
spinner.success("完了!")
# 複数のスピナー
spinners = TTY::Spinner::Multi.new("[:spinner] 複数タスク実行中...")
sp1 = spinners.register("[:spinner] タスク1...")
sp2 = spinners.register("[:spinner] タスク2...")
sp3 = spinners.register("[:spinner] タスク3...")
spinners.auto_spin
sleep(2)
sp1.success("タスク1完了")
sleep(1)
sp2.success("タスク2完了")
sleep(1)
sp3.success("タスク3完了")
spinners.success("すべてのタスクが完了しました!")
TTY::Commandの使用例
require 'tty-command'
cmd = TTY::Command.new
# 基本的なコマンド実行
result = cmd.run('ls -la')
puts "終了コード: #{result.exit_status}"
puts "出力: #{result.out}"
# エラーハンドリング付きコマンド実行
begin
result = cmd.run('nonexistent-command')
rescue TTY::Command::ExitError => err
puts "コマンド実行エラー: #{err.message}"
end
# 複数コマンドの実行
commands = [
'echo "Hello"',
'date',
'whoami'
]
commands.each do |command|
puts "実行中: #{command}"
result = cmd.run(command)
puts "結果: #{result.out.chomp}"
puts "---"
end
# パイプ処理
result = cmd.run('ps aux | grep ruby | head -5')
puts result.out
統合例: ファイルマネージャー
#!/usr/bin/env ruby
require 'tty-prompt'
require 'tty-table'
require 'tty-box'
require 'tty-progressbar'
require 'tty-command'
require 'fileutils'
class FileManager
def initialize
@prompt = TTY::Prompt.new
@cmd = TTY::Command.new(printer: :null)
end
def run
loop do
display_banner
action = main_menu
case action
when 'list'
list_files
when 'copy'
copy_files
when 'move'
move_files
when 'delete'
delete_files
when 'create'
create_directory
when 'search'
search_files
when 'exit'
exit_app
break
end
@prompt.keypress("\n続行するには何かキーを押してください...")
end
end
private
def display_banner
system('clear')
banner = TTY::Box.frame(
width: 60,
height: 5,
align: :center,
title: {top_center: " ファイルマネージャー v1.0 "},
border: :thick
) do
"Ruby TTY ツールキットを使用したファイル管理ツール"
end
puts banner
puts
end
def main_menu
@prompt.select('操作を選択してください:') do |menu|
menu.choice '📁 ファイル一覧', 'list'
menu.choice '📋 ファイルコピー', 'copy'
menu.choice '✂️ ファイル移動', 'move'
menu.choice '🗑️ ファイル削除', 'delete'
menu.choice '📂 ディレクトリ作成', 'create'
menu.choice '🔍 ファイル検索', 'search'
menu.choice '🚪 終了', 'exit'
end
end
def list_files
directory = @prompt.ask('ディレクトリパス:', default: '.')
unless Dir.exist?(directory)
error_box = TTY::Box.error("ディレクトリが見つかりません: #{directory}")
puts error_box
return
end
files = Dir.entries(directory).reject { |f| f == '.' || f == '..' }
if files.empty?
info_box = TTY::Box.info("ディレクトリは空です")
puts info_box
return
end
rows = files.map do |file|
path = File.join(directory, file)
stat = File.stat(path)
type = File.directory?(path) ? 'DIR' : 'FILE'
size = File.directory?(path) ? '-' : format_size(stat.size)
modified = stat.mtime.strftime('%Y-%m-%d %H:%M')
[file, type, size, modified]
end
table = TTY::Table.new(['名前', 'タイプ', 'サイズ', '更新日時'], rows)
puts table.render(:unicode, padding: [0, 1])
end
def copy_files
source = @prompt.ask('コピー元ファイル/ディレクトリ:') do |q|
q.validate(proc { |v| File.exist?(v) }, 'ファイルが存在しません')
end
destination = @prompt.ask('コピー先:')
confirm = @prompt.yes?("#{source} を #{destination} にコピーしますか?")
return unless confirm
begin
if File.directory?(source)
FileUtils.cp_r(source, destination)
else
FileUtils.cp(source, destination)
end
success_box = TTY::Box.success("コピーが完了しました")
puts success_box
rescue => e
error_box = TTY::Box.error("コピー中にエラーが発生しました: #{e.message}")
puts error_box
end
end
def move_files
source = @prompt.ask('移動元ファイル/ディレクトリ:') do |q|
q.validate(proc { |v| File.exist?(v) }, 'ファイルが存在しません')
end
destination = @prompt.ask('移動先:')
confirm = @prompt.yes?("#{source} を #{destination} に移動しますか?")
return unless confirm
begin
FileUtils.mv(source, destination)
success_box = TTY::Box.success("移動が完了しました")
puts success_box
rescue => e
error_box = TTY::Box.error("移動中にエラーが発生しました: #{e.message}")
puts error_box
end
end
def delete_files
files = @prompt.ask('削除するファイル/ディレクトリ(スペース区切り):').split
existing_files = files.select { |f| File.exist?(f) }
if existing_files.empty?
warning_box = TTY::Box.warn("指定されたファイルは存在しません")
puts warning_box
return
end
puts "削除対象:"
existing_files.each { |f| puts " - #{f}" }
confirm = @prompt.yes?("これらのファイルを削除しますか?")
return unless confirm
bar = TTY::ProgressBar.new("削除中 [:bar] :current/:total", total: existing_files.size)
existing_files.each do |file|
begin
File.directory?(file) ? FileUtils.rm_rf(file) : FileUtils.rm(file)
rescue => e
puts "エラー: #{file} - #{e.message}"
end
bar.advance(1)
end
success_box = TTY::Box.success("削除が完了しました")
puts success_box
end
def create_directory
name = @prompt.ask('作成するディレクトリ名:')
if Dir.exist?(name)
warning_box = TTY::Box.warn("ディレクトリは既に存在します: #{name}")
puts warning_box
return
end
begin
FileUtils.mkdir_p(name)
success_box = TTY::Box.success("ディレクトリが作成されました: #{name}")
puts success_box
rescue => e
error_box = TTY::Box.error("ディレクトリ作成中にエラーが発生しました: #{e.message}")
puts error_box
end
end
def search_files
pattern = @prompt.ask('検索パターン:')
directory = @prompt.ask('検索ディレクトリ:', default: '.')
begin
results = Dir.glob(File.join(directory, "**/*#{pattern}*"))
if results.empty?
info_box = TTY::Box.info("パターンにマッチするファイルが見つかりませんでした")
puts info_box
else
puts "検索結果:"
results.each { |result| puts " #{result}" }
end
rescue => e
error_box = TTY::Box.error("検索中にエラーが発生しました: #{e.message}")
puts error_box
end
end
def exit_app
farewell_box = TTY::Box.frame(
width: 40,
align: :center,
padding: 1,
border: :thick
) do
"ファイルマネージャーを終了します\nご利用ありがとうございました!"
end
puts farewell_box
end
def format_size(size)
units = ['B', 'KB', 'MB', 'GB']
unit_index = 0
while size >= 1024 && unit_index < units.length - 1
size /= 1024.0
unit_index += 1
end
"#{size.round(1)}#{units[unit_index]}"
end
end
# アプリケーション実行
if __FILE__ == $0
file_manager = FileManager.new
file_manager.run
end
実行例
# ファイルマネージャーの実行
ruby file_manager.rb
# 個別コンポーネントの使用
ruby -r tty-prompt -e "puts TTY::Prompt.new.ask('Your name?')"
ruby -r tty-table -e "puts TTY::Table.new([['Name', 'Age'], ['John', 30]]).render"