beniya 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module Beniya
6
+ # コマンドモードのUI - Tab補完とフローティングウィンドウでの結果表示
7
+ class CommandModeUI
8
+ def initialize(command_mode, dialog_renderer)
9
+ @command_mode = command_mode
10
+ @dialog_renderer = dialog_renderer
11
+ end
12
+
13
+ # 入力文字列に対する補完候補を取得
14
+ # @param input [String] 現在の入力文字列
15
+ # @return [Array<String>] 補完候補の配列
16
+ def autocomplete(input)
17
+ # 利用可能なコマンド一覧を取得
18
+ available = @command_mode.available_commands.map(&:to_s)
19
+
20
+ # 入力が空の場合は全てのコマンドを返す
21
+ return available if input.empty?
22
+
23
+ # 入力に一致するコマンドをフィルタリング
24
+ available.select { |cmd| cmd.start_with?(input) }
25
+ end
26
+
27
+ # コマンドを補完する
28
+ # @param input [String] 現在の入力文字列
29
+ # @return [String] 補完後の文字列
30
+ def complete_command(input)
31
+ suggestions = autocomplete(input)
32
+
33
+ # マッチするものがない場合は元の入力を返す
34
+ return input if suggestions.empty?
35
+
36
+ # 一つだけマッチする場合はそれを返す
37
+ return suggestions.first if suggestions.length == 1
38
+
39
+ # 複数マッチする場合は共通プレフィックスを返す
40
+ find_common_prefix(suggestions)
41
+ end
42
+
43
+ # コマンド実行結果をフローティングウィンドウで表示
44
+ # @param result [String, nil] コマンド実行結果
45
+ def show_result(result)
46
+ # nil または空文字列の場合は何も表示しない
47
+ return if result.nil? || result.empty?
48
+
49
+ # 結果を行に分割
50
+ result_lines = result.split("\n")
51
+
52
+ # エラーメッセージかどうかを判定
53
+ is_error = result.include?("⚠️") || result.include?("エラー")
54
+
55
+ # ウィンドウの色設定
56
+ if is_error
57
+ border_color = "\e[31m" # Red
58
+ title_color = "\e[1;31m" # Bold red
59
+ content_color = "\e[37m" # White
60
+ else
61
+ border_color = "\e[32m" # Green
62
+ title_color = "\e[1;32m" # Bold green
63
+ content_color = "\e[37m" # White
64
+ end
65
+
66
+ # ウィンドウタイトル
67
+ title = "コマンド実行結果"
68
+
69
+ # コンテンツ行を構築
70
+ content_lines = [""] + result_lines + ["", "Press any key to close"]
71
+
72
+ # ウィンドウサイズを計算
73
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
74
+ title: title,
75
+ min_width: 40,
76
+ max_width: 100
77
+ })
78
+
79
+ # 中央位置を計算
80
+ x, y = @dialog_renderer.calculate_center(width, height)
81
+
82
+ # フローティングウィンドウを描画
83
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
84
+ border_color: border_color,
85
+ title_color: title_color,
86
+ content_color: content_color
87
+ })
88
+
89
+ # キー入力を待つ
90
+ STDIN.getch
91
+
92
+ # ウィンドウをクリア
93
+ @dialog_renderer.clear_area(x, y, width, height)
94
+ end
95
+
96
+ private
97
+
98
+ # 文字列配列の共通プレフィックスを見つける
99
+ # @param strings [Array<String>] 文字列配列
100
+ # @return [String] 共通プレフィックス
101
+ def find_common_prefix(strings)
102
+ return "" if strings.empty?
103
+ return strings.first if strings.length == 1
104
+
105
+ # 最短の文字列の長さを取得
106
+ min_length = strings.map(&:length).min
107
+
108
+ # 各文字位置で全ての文字列が同じ文字を持っているかチェック
109
+ common_length = 0
110
+ min_length.times do |i|
111
+ char = strings.first[i]
112
+ if strings.all? { |s| s[i] == char }
113
+ common_length = i + 1
114
+ else
115
+ break
116
+ end
117
+ end
118
+
119
+ strings.first[0...common_length]
120
+ end
121
+ end
122
+ end
@@ -136,6 +136,8 @@ module Beniya
136
136
  show_zoxide_menu
137
137
  when '1', '2', '3', '4', '5', '6', '7', '8', '9' # number keys - go to bookmark
138
138
  goto_bookmark(key.to_i)
139
+ when ':' # : - command mode
140
+ activate_command_mode
139
141
  else
140
142
  false # #{ConfigLoader.message('keybind.invalid_key')}
141
143
  end
@@ -769,6 +771,12 @@ module Beniya
769
771
  end
770
772
  end
771
773
 
774
+ # コマンドモードを起動
775
+ def activate_command_mode
776
+ @terminal_ui&.activate_command_mode
777
+ true
778
+ end
779
+
772
780
  private
773
781
 
774
782
  # カーソルを画面下部の入力行に移動
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beniya
4
+ # プラグインを格納するモジュール
5
+ module Plugins
6
+ end
7
+
8
+ # プラグインの基底クラス
9
+ class Plugin
10
+ # 依存gemが不足している場合に投げられるエラー
11
+ class DependencyError < StandardError; end
12
+
13
+ class << self
14
+ # 継承時に自動的にPluginManagerに登録する
15
+ def inherited(subclass)
16
+ super
17
+ PluginManager.register(subclass)
18
+ end
19
+
20
+ # 依存gemを宣言する
21
+ def requires(*gems)
22
+ @required_gems ||= []
23
+ @required_gems.concat(gems)
24
+ end
25
+
26
+ # 宣言された依存gemのリストを取得する
27
+ def required_gems
28
+ @required_gems || []
29
+ end
30
+ end
31
+
32
+ # 初期化時に依存gemをチェックする
33
+ def initialize
34
+ check_dependencies!
35
+ end
36
+
37
+ # プラグイン名(必須オーバーライド)
38
+ def name
39
+ raise NotImplementedError, "#{self.class}#name must be implemented"
40
+ end
41
+
42
+ # プラグインの説明(オプション)
43
+ def description
44
+ ""
45
+ end
46
+
47
+ # プラグインのバージョン(オプション)
48
+ def version
49
+ "1.0.0"
50
+ end
51
+
52
+ # コマンド定義(オプション)
53
+ # { command_name: method(:method_name) } の形式で返す
54
+ def commands
55
+ {}
56
+ end
57
+
58
+ private
59
+
60
+ # 依存gemが全て利用可能かチェックする
61
+ def check_dependencies!
62
+ required_gems = self.class.required_gems
63
+ return if required_gems.empty?
64
+
65
+ missing_gems = []
66
+
67
+ required_gems.each do |gem_name|
68
+ begin
69
+ Gem::Specification.find_by_name(gem_name)
70
+ rescue Gem::LoadError
71
+ missing_gems << gem_name
72
+ end
73
+ end
74
+
75
+ return if missing_gems.empty?
76
+
77
+ # 不足しているgemがある場合はエラーを投げる
78
+ error_message = <<~ERROR
79
+ Plugin '#{name}' は以下のgemに依存していますが、インストールされていません:
80
+ - #{missing_gems.join("\n - ")}
81
+
82
+ 以下のコマンドでインストールしてください:
83
+ gem install #{missing_gems.join(' ')}
84
+ ERROR
85
+
86
+ raise DependencyError, error_message
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Beniya
6
+ # プラグインの設定を管理するクラス
7
+ class PluginConfig
8
+ class << self
9
+ # 設定ファイルを読み込む
10
+ def load
11
+ config_path = File.expand_path('~/.beniya/config.yml')
12
+
13
+ if File.exist?(config_path)
14
+ begin
15
+ @config = YAML.load_file(config_path) || {}
16
+ rescue StandardError => e
17
+ warn "⚠️ Failed to load config file: #{e.message}"
18
+ @config = {}
19
+ end
20
+ else
21
+ # 設定ファイルが存在しない場合はデフォルト設定(空のハッシュ)
22
+ @config = {}
23
+ end
24
+ end
25
+
26
+ # プラグインが有効かどうかをチェックする
27
+ def plugin_enabled?(name)
28
+ # 設定が未読み込みの場合は読み込む
29
+ load if @config.nil?
30
+
31
+ # pluginsセクションがない場合は全プラグイン有効
32
+ return true unless @config.is_a?(Hash) && @config['plugins']
33
+
34
+ plugins_config = @config['plugins']
35
+ return true unless plugins_config.is_a?(Hash)
36
+
37
+ # プラグイン名を小文字に統一して検索
38
+ normalized_name = name.to_s.downcase
39
+
40
+ # 設定のキーも小文字に変換して検索
41
+ plugin_setting = nil
42
+ plugins_config.each do |key, value|
43
+ if key.downcase == normalized_name
44
+ plugin_setting = value
45
+ break
46
+ end
47
+ end
48
+
49
+ # 設定が存在しない場合は有効とみなす
50
+ return true if plugin_setting.nil?
51
+
52
+ # enabled設定を確認(デフォルトはtrue)
53
+ return true unless plugin_setting.is_a?(Hash)
54
+
55
+ plugin_setting.fetch('enabled', true)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beniya
4
+ # プラグインを管理するクラス
5
+ class PluginManager
6
+ class << self
7
+ # 登録済みプラグインクラスのリスト
8
+ def plugins
9
+ @plugins ||= []
10
+ end
11
+
12
+ # プラグインを登録する
13
+ def register(plugin_class)
14
+ @plugins ||= []
15
+ @plugins << plugin_class unless @plugins.include?(plugin_class)
16
+ end
17
+
18
+ # 全プラグインを読み込む(本体同梱 + ユーザープラグイン)
19
+ def load_all
20
+ load_builtin_plugins
21
+ load_user_plugins
22
+ end
23
+
24
+ # 有効なプラグインインスタンスのリストを取得
25
+ def enabled_plugins
26
+ return @enabled_plugins if @enabled_plugins
27
+
28
+ @enabled_plugins = []
29
+
30
+ plugins.each do |plugin_class|
31
+ # プラグイン名を取得(クラス名から推測)
32
+ plugin_name = plugin_class.name.split('::').last
33
+
34
+ # PluginConfigで有効かチェック
35
+ next unless PluginConfig.plugin_enabled?(plugin_name)
36
+
37
+ # プラグインのインスタンスを作成
38
+ begin
39
+ plugin_instance = plugin_class.new
40
+ @enabled_plugins << plugin_instance
41
+ rescue Plugin::DependencyError => e
42
+ warn "⚠️ #{e.message}"
43
+ # プラグインは無効化されるが、beniyaは起動継続
44
+ rescue StandardError => e
45
+ warn "⚠️ Failed to load plugin #{plugin_name}: #{e.message}"
46
+ end
47
+ end
48
+
49
+ @enabled_plugins
50
+ end
51
+
52
+ private
53
+
54
+ # 本体同梱プラグインを読み込む
55
+ def load_builtin_plugins
56
+ # plugin_manager.rbは/lib/beniya/にあるので、pluginsディレクトリは同じディレクトリ内
57
+ builtin_plugins_dir = File.join(__dir__, 'plugins')
58
+ return unless Dir.exist?(builtin_plugins_dir)
59
+
60
+ Dir.glob(File.join(builtin_plugins_dir, '*.rb')).sort.each do |file|
61
+ begin
62
+ require file
63
+ rescue StandardError => e
64
+ warn "⚠️ Failed to load builtin plugin #{File.basename(file)}: #{e.message}"
65
+ end
66
+ end
67
+ end
68
+
69
+ # ユーザープラグインを読み込む
70
+ def load_user_plugins
71
+ user_plugins_dir = File.expand_path('~/.beniya/plugins')
72
+ return unless Dir.exist?(user_plugins_dir)
73
+
74
+ Dir.glob(File.join(user_plugins_dir, '*.rb')).sort.each do |file|
75
+ begin
76
+ require file
77
+ rescue SyntaxError, StandardError => e
78
+ warn "⚠️ Failed to load user plugin #{File.basename(file)}: #{e.message}"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beniya
4
+ module Plugins
5
+ # 基本的なファイル操作を提供するプラグイン
6
+ class FileOperations < Plugin
7
+ def name
8
+ "FileOperations"
9
+ end
10
+
11
+ def description
12
+ "基本的なファイル操作(コピー、移動、削除)"
13
+ end
14
+
15
+ def commands
16
+ {
17
+ copy: method(:copy),
18
+ move: method(:move),
19
+ delete: method(:delete)
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ # ファイルコピー(スタブ実装)
26
+ def copy
27
+ # 実装は将来追加
28
+ nil
29
+ end
30
+
31
+ # ファイル移動(スタブ実装)
32
+ def move
33
+ # 実装は将来追加
34
+ nil
35
+ end
36
+
37
+ # ファイル削除(スタブ実装)
38
+ def delete
39
+ # 実装は将来追加
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
@@ -43,6 +43,11 @@ module Beniya
43
43
  @screen_height = DEFAULT_SCREEN_HEIGHT
44
44
  end
45
45
  @running = false
46
+ @command_mode_active = false
47
+ @command_input = ""
48
+ @command_mode = CommandMode.new
49
+ @dialog_renderer = DialogRenderer.new
50
+ @command_mode_ui = CommandModeUI.new(@command_mode, @dialog_renderer)
46
51
  end
47
52
 
48
53
  def start(directory_listing, keybind_handler, file_preview)
@@ -126,8 +131,16 @@ module Beniya
126
131
  # footer
127
132
  draw_footer
128
133
 
129
- # move cursor to invisible position
130
- print "\e[#{@screen_height};#{@screen_width}H"
134
+ # コマンド実行結果を表示
135
+ draw_command_result
136
+
137
+ # コマンドモードがアクティブな場合はコマンド入力欄を表示
138
+ if @command_mode_active
139
+ draw_command_input
140
+ else
141
+ # move cursor to invisible position
142
+ print "\e[#{@screen_height};#{@screen_width}H"
143
+ end
131
144
  end
132
145
 
133
146
  def draw_header
@@ -543,6 +556,12 @@ module Beniya
543
556
  end
544
557
  end
545
558
 
559
+ # コマンドモードがアクティブな場合は、コマンド入力を処理
560
+ if @command_mode_active
561
+ handle_command_input(input)
562
+ return
563
+ end
564
+
546
565
  # キーバインドハンドラーに処理を委譲
547
566
  result = @keybind_handler.handle_key(input)
548
567
 
@@ -551,6 +570,90 @@ module Beniya
551
570
  @running = false
552
571
  end
553
572
  end
573
+
574
+ # コマンドモード関連のメソッドは public にする
575
+ public
576
+
577
+ # コマンドモードを起動
578
+ def activate_command_mode
579
+ @command_mode_active = true
580
+ @command_input = ""
581
+ end
582
+
583
+ # コマンドモードを終了
584
+ def deactivate_command_mode
585
+ @command_mode_active = false
586
+ @command_input = ""
587
+ end
588
+
589
+ # コマンドモードがアクティブかどうか
590
+ def command_mode_active?
591
+ @command_mode_active
592
+ end
593
+
594
+ # コマンド入力を処理
595
+ def handle_command_input(input)
596
+ case input
597
+ when "\r", "\n"
598
+ # Enter キーでコマンドを実行
599
+ execute_command(@command_input)
600
+ deactivate_command_mode
601
+ when "\e"
602
+ # Escape キーでコマンドモードをキャンセル
603
+ deactivate_command_mode
604
+ when "\t"
605
+ # Tab キーで補完
606
+ @command_input = @command_mode_ui.complete_command(@command_input)
607
+ when "\u007F", "\b"
608
+ # Backspace
609
+ @command_input.chop! unless @command_input.empty?
610
+ else
611
+ # 通常の文字を追加
612
+ @command_input += input if input.length == 1
613
+ end
614
+ end
615
+
616
+ # コマンドを実行
617
+ def execute_command(command_string)
618
+ return if command_string.nil? || command_string.empty?
619
+
620
+ result = @command_mode.execute(command_string)
621
+
622
+ # コマンド実行結果をフローティングウィンドウで表示
623
+ @command_mode_ui.show_result(result) if result
624
+
625
+ # 画面を再描画
626
+ draw_screen
627
+ end
628
+
629
+ # コマンド入力欄を描画
630
+ def draw_command_input
631
+ # 画面最下部に描画
632
+ print "\e[#{@screen_height};1H"
633
+ print "\e[2K" # 行をクリア
634
+
635
+ # コマンドプロンプトと入力を表示
636
+ prompt = ":"
637
+ print "#{prompt}#{@command_input}"
638
+
639
+ # カーソルを表示
640
+ print "\e[?25h"
641
+ end
642
+
643
+ # コマンド実行結果を描画
644
+ def draw_command_result
645
+ return unless @command_result && @command_result_time
646
+
647
+ # 3秒間だけ表示
648
+ if Time.now - @command_result_time < 3
649
+ print "\e[#{@screen_height - 1};1H"
650
+ print "\e[2K" # 行をクリア
651
+ print @command_result
652
+ else
653
+ @command_result = nil
654
+ @command_result_time = nil
655
+ end
656
+ end
554
657
  end
555
658
  end
556
659
 
@@ -37,20 +37,36 @@ module Beniya
37
37
  def truncate_to_width(string, max_width)
38
38
  return string if display_width(string) <= max_width
39
39
 
40
- result = ''
41
- current_width = 0
40
+ # If max_width is enough for ellipsis, truncate and add ellipsis
41
+ if max_width >= ELLIPSIS_MIN_WIDTH
42
+ result = ''
43
+ current_width = 0
44
+ target_width = max_width - ELLIPSIS_MIN_WIDTH
45
+
46
+ string.each_char do |char|
47
+ char_width = display_width(char)
48
+ break if current_width + char_width > target_width
49
+
50
+ result += char
51
+ current_width += char_width
52
+ end
42
53
 
43
- string.each_char do |char|
44
- char_width = display_width(char)
45
- break if current_width + char_width > max_width
54
+ result + ELLIPSIS
55
+ else
56
+ # Not enough room for ellipsis, just truncate
57
+ result = ''
58
+ current_width = 0
46
59
 
47
- result += char
48
- current_width += char_width
49
- end
60
+ string.each_char do |char|
61
+ char_width = display_width(char)
62
+ break if current_width + char_width > max_width
50
63
 
51
- # Add ellipsis if there's room
52
- result += ELLIPSIS if max_width >= ELLIPSIS_MIN_WIDTH && current_width <= max_width - ELLIPSIS_MIN_WIDTH
53
- result
64
+ result += char
65
+ current_width += char_width
66
+ end
67
+
68
+ result
69
+ end
54
70
  end
55
71
 
56
72
  # Pad string to target_width with spaces
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Beniya
4
- VERSION = '0.6.3'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/beniya.rb CHANGED
@@ -21,6 +21,13 @@ require_relative "beniya/application"
21
21
  require_relative "beniya/file_opener"
22
22
  require_relative "beniya/health_checker"
23
23
 
24
+ # プラグインシステム
25
+ require_relative "beniya/plugin_config"
26
+ require_relative "beniya/plugin"
27
+ require_relative "beniya/plugin_manager"
28
+ require_relative "beniya/command_mode"
29
+ require_relative "beniya/command_mode_ui"
30
+
24
31
  module Beniya
25
32
  class Error < StandardError; end
26
33
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beniya
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-23 00:00:00.000000000 Z
10
+ date: 2025-12-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: io-console
@@ -120,17 +120,23 @@ files:
120
120
  - CHANGELOG_v0.4.0.md
121
121
  - CHANGELOG_v0.5.0.md
122
122
  - CHANGELOG_v0.6.0.md
123
+ - CHANGELOG_v0.7.0.md
124
+ - CHANGELOG_v0.8.0.md
123
125
  - README.md
124
126
  - README_EN.md
125
127
  - Rakefile
126
128
  - beniya.gemspec
127
129
  - bin/beniya
128
130
  - config_example.rb
131
+ - docs/PLUGIN_GUIDE.md
132
+ - docs/plugin_example.rb
129
133
  - lib/beniya.rb
130
134
  - lib/beniya/application.rb
131
135
  - lib/beniya/bookmark.rb
132
136
  - lib/beniya/bookmark_manager.rb
133
137
  - lib/beniya/color_helper.rb
138
+ - lib/beniya/command_mode.rb
139
+ - lib/beniya/command_mode_ui.rb
134
140
  - lib/beniya/config.rb
135
141
  - lib/beniya/config_loader.rb
136
142
  - lib/beniya/dialog_renderer.rb
@@ -142,6 +148,10 @@ files:
142
148
  - lib/beniya/health_checker.rb
143
149
  - lib/beniya/keybind_handler.rb
144
150
  - lib/beniya/logger.rb
151
+ - lib/beniya/plugin.rb
152
+ - lib/beniya/plugin_config.rb
153
+ - lib/beniya/plugin_manager.rb
154
+ - lib/beniya/plugins/file_operations.rb
145
155
  - lib/beniya/selection_manager.rb
146
156
  - lib/beniya/terminal_ui.rb
147
157
  - lib/beniya/text_utils.rb