TTY

ターミナルアプリケーション開発のためのツールキット。CLIパーサー、プロンプト、プログレスバーなど多数のコンポーネントを提供。

rubycliterminaltoolkit

フレームワーク

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"