rufio 0.9.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +188 -0
  3. data/CHANGELOG_v0.4.0.md +146 -0
  4. data/CHANGELOG_v0.5.0.md +26 -0
  5. data/CHANGELOG_v0.6.0.md +182 -0
  6. data/CHANGELOG_v0.7.0.md +280 -0
  7. data/CHANGELOG_v0.8.0.md +267 -0
  8. data/CHANGELOG_v0.9.0.md +279 -0
  9. data/README.md +631 -0
  10. data/README_EN.md +561 -0
  11. data/Rakefile +156 -0
  12. data/bin/rufio +34 -0
  13. data/config_example.rb +88 -0
  14. data/docs/PLUGIN_GUIDE.md +431 -0
  15. data/docs/plugin_example.rb +119 -0
  16. data/lib/rufio/application.rb +32 -0
  17. data/lib/rufio/bookmark.rb +115 -0
  18. data/lib/rufio/bookmark_manager.rb +173 -0
  19. data/lib/rufio/color_helper.rb +150 -0
  20. data/lib/rufio/command_mode.rb +72 -0
  21. data/lib/rufio/command_mode_ui.rb +168 -0
  22. data/lib/rufio/config.rb +199 -0
  23. data/lib/rufio/config_loader.rb +110 -0
  24. data/lib/rufio/dialog_renderer.rb +127 -0
  25. data/lib/rufio/directory_listing.rb +113 -0
  26. data/lib/rufio/file_opener.rb +140 -0
  27. data/lib/rufio/file_operations.rb +231 -0
  28. data/lib/rufio/file_preview.rb +200 -0
  29. data/lib/rufio/filter_manager.rb +114 -0
  30. data/lib/rufio/health_checker.rb +246 -0
  31. data/lib/rufio/keybind_handler.rb +828 -0
  32. data/lib/rufio/logger.rb +103 -0
  33. data/lib/rufio/plugin.rb +89 -0
  34. data/lib/rufio/plugin_config.rb +59 -0
  35. data/lib/rufio/plugin_manager.rb +84 -0
  36. data/lib/rufio/plugins/file_operations.rb +44 -0
  37. data/lib/rufio/selection_manager.rb +79 -0
  38. data/lib/rufio/terminal_ui.rb +630 -0
  39. data/lib/rufio/text_utils.rb +108 -0
  40. data/lib/rufio/version.rb +5 -0
  41. data/lib/rufio/zoxide_integration.rb +188 -0
  42. data/lib/rufio.rb +33 -0
  43. data/publish_gem.zsh +131 -0
  44. data/rufio.gemspec +40 -0
  45. data/test_delete/test1.txt +1 -0
  46. data/test_delete/test2.txt +1 -0
  47. metadata +189 -0
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ class Config
5
+ # Default language settings
6
+ DEFAULT_LANGUAGE = 'en'
7
+ AVAILABLE_LANGUAGES = %w[en ja].freeze
8
+
9
+ # Multi-language message definitions
10
+ MESSAGES = {
11
+ 'en' => {
12
+ # Application messages
13
+ 'app.interrupted' => 'rufio interrupted',
14
+ 'app.error_occurred' => 'Error occurred',
15
+ 'app.terminated' => 'rufio terminated',
16
+
17
+ # File operations
18
+ 'file.not_found' => 'File not found',
19
+ 'file.not_readable' => 'File not readable',
20
+ 'file.read_error' => 'File read error',
21
+ 'file.binary_file' => 'Binary file',
22
+ 'file.cannot_preview' => 'Cannot preview',
23
+ 'file.encoding_error' => 'Character encoding error - cannot read file',
24
+ 'file.preview_error' => 'Preview error',
25
+ 'file.error_prefix' => 'Error',
26
+
27
+ # Keybind messages
28
+ 'keybind.invalid_key' => 'invalid key',
29
+ 'keybind.search_text' => 'Search text: ',
30
+ 'keybind.no_matches' => 'No matches found.',
31
+ 'keybind.press_any_key' => 'Press any key to continue...',
32
+ 'keybind.input_filename' => 'Enter filename: ',
33
+ 'keybind.input_dirname' => 'Enter directory name: ',
34
+ 'keybind.invalid_filename' => 'Invalid filename (cannot contain / or \\)',
35
+ 'keybind.invalid_dirname' => 'Invalid directory name (cannot contain / or \\)',
36
+ 'keybind.file_exists' => 'File already exists',
37
+ 'keybind.directory_exists' => 'Directory already exists',
38
+ 'keybind.file_created' => 'File created',
39
+ 'keybind.directory_created' => 'Directory created',
40
+ 'keybind.creation_error' => 'Creation error',
41
+
42
+ # UI messages
43
+ 'ui.operation_prompt' => 'Operation: ',
44
+
45
+ # Help text
46
+ 'help.full' => 'j/k:move h:back l:enter o:open g/G:top/bottom r:refresh f:filter s:search F:content a/A:create m/p/x:ops b:bookmark z:zoxide 1-9:goto q:quit',
47
+ 'help.short' => 'j/k:move h:back l:enter o:open f:filter s:search b:bookmark z:zoxide 1-9:goto q:quit',
48
+
49
+ # Health check messages
50
+ 'health.title' => 'rufio Health Check',
51
+ 'health.ruby_version' => 'Ruby version',
52
+ 'health.required_gems' => 'Required gems',
53
+ 'health.fzf' => 'fzf (file search)',
54
+ 'health.rga' => 'rga (content search)',
55
+ 'health.zoxide' => 'zoxide (directory history)',
56
+ 'health.file_opener' => 'System file opener',
57
+ 'health.summary' => 'Summary:',
58
+ 'health.ok' => 'OK',
59
+ 'health.warnings' => 'Warnings',
60
+ 'health.errors' => 'Errors',
61
+ 'health.all_passed' => 'All checks passed! rufio is ready to use.',
62
+ 'health.critical_missing' => 'Some critical components are missing. rufio may not work properly.',
63
+ 'health.optional_missing' => 'Some optional features are unavailable. Basic functionality will work.',
64
+ 'health.all_gems_installed' => 'All required gems installed',
65
+ 'health.missing_gems' => 'Missing gems',
66
+ 'health.gem_install_instruction' => 'Run: gem install',
67
+ 'health.tool_not_found' => 'not found',
68
+ 'health.unknown_platform' => 'Unknown platform',
69
+ 'health.file_open_may_not_work' => 'File opening may not work properly',
70
+ 'health.macos_opener' => 'macOS file opener',
71
+ 'health.linux_opener' => 'Linux file opener',
72
+ 'health.windows_opener' => 'Windows file opener',
73
+ 'health.install_brew' => 'Install: brew install',
74
+ 'health.install_apt' => 'Install: apt install',
75
+ 'health.install_guide' => 'Check installation guide for your platform',
76
+ 'health.rga_releases' => 'Install: https://github.com/phiresky/ripgrep-all/releases',
77
+ 'health.ruby_upgrade_needed' => 'Please upgrade Ruby to version 2.7.0 or higher'
78
+ },
79
+
80
+ 'ja' => {
81
+ # Application messages
82
+ 'app.interrupted' => 'rufioを中断しました',
83
+ 'app.error_occurred' => 'エラーが発生しました',
84
+ 'app.terminated' => 'rufioを終了しました',
85
+
86
+ # File operations
87
+ 'file.not_found' => 'ファイルが見つかりません',
88
+ 'file.not_readable' => 'ファイルを読み取れません',
89
+ 'file.read_error' => 'ファイル読み込みエラー',
90
+ 'file.binary_file' => 'バイナリファイル',
91
+ 'file.cannot_preview' => 'プレビューできません',
92
+ 'file.encoding_error' => '文字エンコーディングエラー - ファイルを読み取れません',
93
+ 'file.preview_error' => 'プレビューエラー',
94
+ 'file.error_prefix' => 'エラー',
95
+
96
+ # Keybind messages
97
+ 'keybind.invalid_key' => '無効なキー',
98
+ 'keybind.search_text' => '検索テキスト: ',
99
+ 'keybind.no_matches' => 'マッチするものが見つかりません。',
100
+ 'keybind.press_any_key' => '何かキーを押して続行...',
101
+ 'keybind.input_filename' => 'ファイル名を入力: ',
102
+ 'keybind.input_dirname' => 'ディレクトリ名を入力: ',
103
+ 'keybind.invalid_filename' => '無効なファイル名(/や\\を含むことはできません)',
104
+ 'keybind.invalid_dirname' => '無効なディレクトリ名(/や\\を含むことはできません)',
105
+ 'keybind.file_exists' => 'ファイルが既に存在します',
106
+ 'keybind.directory_exists' => 'ディレクトリが既に存在します',
107
+ 'keybind.file_created' => 'ファイルを作成しました',
108
+ 'keybind.directory_created' => 'ディレクトリを作成しました',
109
+ 'keybind.creation_error' => '作成エラー',
110
+
111
+ # UI messages
112
+ 'ui.operation_prompt' => '操作: ',
113
+
114
+ # Help text
115
+ 'help.full' => 'j/k:移動 h:戻る l:入る o:開く g/G:先頭/末尾 r:更新 f:絞込 s:検索 F:内容 a/A:作成 m/p/x:操作 b:ブックマーク z:zoxide 1-9:移動 q:終了',
116
+ 'help.short' => 'j/k:移動 h:戻る l:入る o:開く f:絞込 s:検索 b:ブックマーク z:zoxide 1-9:移動 q:終了',
117
+
118
+ # Health check messages
119
+ 'health.title' => 'rufio ヘルスチェック',
120
+ 'health.ruby_version' => 'Ruby バージョン',
121
+ 'health.required_gems' => '必須 gem',
122
+ 'health.fzf' => 'fzf (ファイル検索)',
123
+ 'health.rga' => 'rga (内容検索)',
124
+ 'health.zoxide' => 'zoxide (ディレクトリ履歴)',
125
+ 'health.file_opener' => 'システムファイルオープナー',
126
+ 'health.summary' => 'サマリー:',
127
+ 'health.ok' => 'OK',
128
+ 'health.warnings' => '警告',
129
+ 'health.errors' => 'エラー',
130
+ 'health.all_passed' => '全てのチェックが完了しました!rufioは使用可能です。',
131
+ 'health.critical_missing' => '重要なコンポーネントが不足しています。rufioは正常に動作しない可能性があります。',
132
+ 'health.optional_missing' => 'オプション機能が利用できません。基本機能は動作します。',
133
+ 'health.all_gems_installed' => '全ての必須gemがインストールされています',
134
+ 'health.missing_gems' => '不足しているgem',
135
+ 'health.gem_install_instruction' => '実行: gem install',
136
+ 'health.tool_not_found' => 'が見つかりません',
137
+ 'health.unknown_platform' => '不明なプラットフォーム',
138
+ 'health.file_open_may_not_work' => 'ファイルオープンが正常に動作しない可能性があります',
139
+ 'health.macos_opener' => 'macOS ファイルオープナー',
140
+ 'health.linux_opener' => 'Linux ファイルオープナー',
141
+ 'health.windows_opener' => 'Windows ファイルオープナー',
142
+ 'health.install_brew' => 'インストール: brew install',
143
+ 'health.install_apt' => 'インストール: apt install',
144
+ 'health.install_guide' => 'お使いのプラットフォーム向けのインストールガイドを確認してください',
145
+ 'health.rga_releases' => 'インストール: https://github.com/phiresky/ripgrep-all/releases',
146
+ 'health.ruby_upgrade_needed' => 'Rubyをバージョン2.7.0以上にアップグレードしてください'
147
+ }
148
+ }.freeze
149
+
150
+ class << self
151
+ def current_language
152
+ @current_language ||= detect_language
153
+ end
154
+
155
+ def current_language=(lang)
156
+ if AVAILABLE_LANGUAGES.include?(lang.to_s)
157
+ @current_language = lang.to_s
158
+ else
159
+ raise ArgumentError, "Unsupported language: #{lang}. Available: #{AVAILABLE_LANGUAGES.join(', ')}"
160
+ end
161
+ end
162
+
163
+ def message(key, **interpolations)
164
+ msg = MESSAGES.dig(current_language, key) || MESSAGES.dig(DEFAULT_LANGUAGE, key) || key
165
+
166
+ # Simple interpolation support
167
+ interpolations.each do |placeholder, value|
168
+ msg = msg.gsub("%{#{placeholder}}", value.to_s)
169
+ end
170
+
171
+ msg
172
+ end
173
+
174
+ def available_languages
175
+ AVAILABLE_LANGUAGES.dup
176
+ end
177
+
178
+ def reset_language!
179
+ @current_language = nil
180
+ end
181
+
182
+ private
183
+
184
+ def detect_language
185
+ # Only BENIYA_LANG environment variable takes precedence
186
+ # This ensures English is default unless explicitly requested
187
+ env_lang = ENV['BENIYA_LANG']
188
+
189
+ if env_lang && !env_lang.empty?
190
+ # Extract language code (e.g., 'ja_JP.UTF-8' -> 'ja')
191
+ lang_code = env_lang.split(/[_.]/).first&.downcase
192
+ return lang_code if AVAILABLE_LANGUAGES.include?(lang_code)
193
+ end
194
+
195
+ DEFAULT_LANGUAGE
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+
5
+ module Rufio
6
+ class ConfigLoader
7
+ CONFIG_PATH = File.expand_path('~/.config/rufio/config.rb').freeze
8
+
9
+ class << self
10
+ def load_config
11
+ @config ||= if File.exist?(CONFIG_PATH)
12
+ load_config_file
13
+ else
14
+ default_config
15
+ end
16
+ end
17
+
18
+ def reload_config!
19
+ @config = nil
20
+ load_config
21
+ end
22
+
23
+ def applications
24
+ load_config[:applications]
25
+ end
26
+
27
+ def colors
28
+ load_config[:colors]
29
+ end
30
+
31
+ def keybinds
32
+ load_config[:keybinds]
33
+ end
34
+
35
+ def language
36
+ load_config[:language] || Config.current_language
37
+ end
38
+
39
+ def set_language(lang)
40
+ Config.current_language = lang
41
+ # Update config if it's user-defined
42
+ if @config
43
+ @config[:language] = lang
44
+ end
45
+ end
46
+
47
+ def message(key, **interpolations)
48
+ Config.message(key, **interpolations)
49
+ end
50
+
51
+ private
52
+
53
+ def load_config_file
54
+ # 設定ファイルを実行してグローバル定数を定義
55
+ load CONFIG_PATH
56
+ config = {
57
+ applications: Object.const_get(:APPLICATIONS),
58
+ colors: Object.const_get(:COLORS),
59
+ keybinds: Object.const_get(:KEYBINDS)
60
+ }
61
+
62
+ # Load language setting if defined
63
+ if Object.const_defined?(:LANGUAGE)
64
+ language = Object.const_get(:LANGUAGE)
65
+ config[:language] = language
66
+ Config.current_language = language if Config.available_languages.include?(language.to_s)
67
+ end
68
+
69
+ config
70
+ rescue StandardError => e
71
+ warn "Failed to load config file: #{e.message}"
72
+ warn 'Using default configuration'
73
+ default_config
74
+ end
75
+
76
+ def default_config
77
+ {
78
+ applications: {
79
+ %w[txt md rb py js html css json xml yaml yml] => 'code',
80
+ %w[jpg jpeg png gif bmp svg webp] => 'open',
81
+ %w[mp4 avi mkv mov wmv] => 'open',
82
+ %w[pdf] => 'open',
83
+ %w[doc docx xls xlsx ppt pptx] => 'open',
84
+ :default => 'open'
85
+ },
86
+ colors: {
87
+ directory: { hsl: [220, 80, 60] }, # Blue
88
+ file: { hsl: [0, 0, 90] }, # Light gray
89
+ executable: { hsl: [120, 70, 50] }, # Green
90
+ selected: { hsl: [50, 90, 70] }, # Yellow
91
+ preview: { hsl: [180, 60, 65] } # Cyan
92
+ },
93
+ keybinds: {
94
+ quit: %w[q ESC],
95
+ up: %w[k UP],
96
+ down: %w[j DOWN],
97
+ left: %w[h LEFT],
98
+ right: %w[l RIGHT ENTER],
99
+ top: %w[g],
100
+ bottom: %w[G],
101
+ refresh: %w[r],
102
+ search: %w[/],
103
+ open_file: %w[o SPACE]
104
+ }
105
+ }
106
+ end
107
+ end
108
+ end
109
+ end
110
+
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'text_utils'
4
+
5
+ module Rufio
6
+ # Renders floating dialog windows in the terminal
7
+ class DialogRenderer
8
+ include TextUtils
9
+
10
+ # Draw a floating window with title, content, and customizable colors
11
+ # @param x [Integer] X position (column)
12
+ # @param y [Integer] Y position (row)
13
+ # @param width [Integer] Window width
14
+ # @param height [Integer] Window height
15
+ # @param title [String, nil] Window title (optional)
16
+ # @param content_lines [Array<String>] Content lines to display
17
+ # @param options [Hash] Customization options
18
+ # @option options [String] :border_color Border color ANSI code
19
+ # @option options [String] :title_color Title color ANSI code
20
+ # @option options [String] :content_color Content color ANSI code
21
+ def draw_floating_window(x, y, width, height, title, content_lines, options = {})
22
+ # Default options
23
+ border_color = options[:border_color] || "\e[37m" # White
24
+ title_color = options[:title_color] || "\e[1;33m" # Bold yellow
25
+ content_color = options[:content_color] || "\e[37m" # White
26
+ reset_color = "\e[0m"
27
+
28
+ # Draw top border
29
+ print "\e[#{y};#{x}H#{border_color}┌#{'─' * (width - 2)}┐#{reset_color}"
30
+
31
+ # Draw title line if title exists
32
+ if title
33
+ title_width = TextUtils.display_width(title)
34
+ title_padding = (width - 2 - title_width) / 2
35
+ padded_title = ' ' * title_padding + title
36
+ title_line = TextUtils.pad_string_to_width(padded_title, width - 2)
37
+ print "\e[#{y + 1};#{x}H#{border_color}│#{title_color}#{title_line}#{border_color}│#{reset_color}"
38
+
39
+ # Draw title separator
40
+ print "\e[#{y + 2};#{x}H#{border_color}├#{'─' * (width - 2)}┤#{reset_color}"
41
+ content_start_y = y + 3
42
+ else
43
+ content_start_y = y + 1
44
+ end
45
+
46
+ # Draw content lines
47
+ content_height = title ? height - 4 : height - 2
48
+ content_lines.each_with_index do |line, index|
49
+ break if index >= content_height
50
+
51
+ line_y = content_start_y + index
52
+ line_content = TextUtils.pad_string_to_width(line, width - 2)
53
+ print "\e[#{line_y};#{x}H#{border_color}│#{content_color}#{line_content}#{border_color}│#{reset_color}"
54
+ end
55
+
56
+ # Fill remaining lines with empty space
57
+ remaining_lines = content_height - content_lines.length
58
+ remaining_lines.times do |i|
59
+ line_y = content_start_y + content_lines.length + i
60
+ empty_line = ' ' * (width - 2)
61
+ print "\e[#{line_y};#{x}H#{border_color}│#{empty_line}│#{reset_color}"
62
+ end
63
+
64
+ # Draw bottom border
65
+ bottom_y = y + height - 1
66
+ print "\e[#{bottom_y};#{x}H#{border_color}└#{'─' * (width - 2)}┘#{reset_color}"
67
+ end
68
+
69
+ # Calculate center position for a window
70
+ # @param content_width [Integer] Window width
71
+ # @param content_height [Integer] Window height
72
+ # @return [Array<Integer>] [x, y] position
73
+ def calculate_center(content_width, content_height)
74
+ # Get terminal size
75
+ console = IO.console
76
+ if console
77
+ screen_width, screen_height = console.winsize.reverse
78
+ else
79
+ screen_width = 80
80
+ screen_height = 24
81
+ end
82
+
83
+ # Calculate center position
84
+ x = [(screen_width - content_width) / 2, 1].max
85
+ y = [(screen_height - content_height) / 2, 1].max
86
+
87
+ [x, y]
88
+ end
89
+
90
+ # Clear a rectangular area on the screen
91
+ # @param x [Integer] X position
92
+ # @param y [Integer] Y position
93
+ # @param width [Integer] Area width
94
+ # @param height [Integer] Area height
95
+ def clear_area(x, y, width, height)
96
+ height.times do |row|
97
+ print "\e[#{y + row};#{x}H#{' ' * width}"
98
+ end
99
+ end
100
+
101
+ # Calculate appropriate dimensions for content
102
+ # @param content_lines [Array<String>] Content lines
103
+ # @param options [Hash] Options
104
+ # @option options [String, nil] :title Window title
105
+ # @option options [Integer] :min_width Minimum width (default: 30)
106
+ # @option options [Integer] :max_width Maximum width (default: 80)
107
+ # @return [Array<Integer>] [width, height]
108
+ def calculate_dimensions(content_lines, options = {})
109
+ title = options[:title]
110
+ min_width = options[:min_width] || 30
111
+ max_width = options[:max_width] || 80
112
+
113
+ # Calculate required width based on content
114
+ max_content_width = content_lines.map { |line| TextUtils.display_width(line) }.max || 0
115
+ title_width = title ? TextUtils.display_width(title) : 0
116
+ required_width = [max_content_width, title_width].max + 4 # +4 for borders and padding
117
+
118
+ width = [[required_width, min_width].max, max_width].min
119
+
120
+ # Calculate height: borders + title (if exists) + separator + content
121
+ height = content_lines.length + 2 # +2 for top and bottom borders
122
+ height += 2 if title # +2 for title line and separator
123
+
124
+ [width, height]
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Rufio
6
+ class DirectoryListing
7
+ attr_reader :current_path
8
+
9
+ def initialize(path = Dir.pwd)
10
+ @current_path = File.expand_path(path)
11
+ @entries = []
12
+ refresh
13
+ end
14
+
15
+ def list_entries
16
+ @entries
17
+ end
18
+
19
+ def refresh
20
+ return unless File.directory?(@current_path)
21
+
22
+ @entries = []
23
+
24
+ Dir.entries(@current_path).each do |name|
25
+ next if name == '.'
26
+
27
+ full_path = File.join(@current_path, name)
28
+ entry = {
29
+ name: name,
30
+ path: full_path,
31
+ type: determine_file_type(full_path),
32
+ size: safe_file_size(full_path),
33
+ modified: safe_file_mtime(full_path)
34
+ }
35
+ @entries << entry
36
+ end
37
+
38
+ sort_entries!
39
+ end
40
+
41
+ def navigate_to(target)
42
+ return false if target.nil? || target.empty?
43
+
44
+ new_path = File.join(@current_path, target)
45
+
46
+ if File.directory?(new_path) && File.readable?(new_path)
47
+ @current_path = File.expand_path(new_path)
48
+ refresh
49
+ true
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def navigate_to_parent
56
+ parent_path = File.dirname(@current_path)
57
+
58
+ # 同じパスの場合は移動しない(ルートディレクトリに到達)
59
+ return false if parent_path == @current_path
60
+
61
+ if File.directory?(parent_path) && File.readable?(parent_path)
62
+ @current_path = parent_path
63
+ refresh
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ def navigate_to_path(path)
71
+ return false if path.nil? || path.empty?
72
+
73
+ expanded_path = File.expand_path(path)
74
+
75
+ if File.directory?(expanded_path) && File.readable?(expanded_path)
76
+ @current_path = expanded_path
77
+ refresh
78
+ true
79
+ else
80
+ false
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def determine_file_type(path)
87
+ return 'directory' if File.directory?(path)
88
+ return 'executable' if File.executable?(path) && !File.directory?(path)
89
+
90
+ 'file'
91
+ end
92
+
93
+ def safe_file_size(path)
94
+ File.size(path)
95
+ rescue StandardError
96
+ 0
97
+ end
98
+
99
+ def safe_file_mtime(path)
100
+ File.mtime(path)
101
+ rescue StandardError
102
+ Time.now
103
+ end
104
+
105
+ def sort_entries!
106
+ @entries.sort_by! do |entry|
107
+ # ディレクトリを最初に、その後ファイル名でソート
108
+ [entry[:type] == 'directory' ? 0 : 1, entry[:name].downcase]
109
+ end
110
+ end
111
+ end
112
+ end
113
+
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ class FileOpener
5
+ def initialize
6
+ @config_loader = ConfigLoader
7
+ end
8
+
9
+ def open_file(file_path)
10
+ return false unless File.exist?(file_path)
11
+ return false if File.directory?(file_path)
12
+
13
+ application = find_application_for_file(file_path)
14
+ execute_command(application, file_path)
15
+ end
16
+
17
+ def open_file_with_line(file_path, line_number)
18
+ return false unless File.exist?(file_path)
19
+ return false if File.directory?(file_path)
20
+
21
+ application = find_application_for_file(file_path)
22
+ execute_command_with_line(application, file_path, line_number)
23
+ end
24
+
25
+ def open_directory_in_explorer(directory_path)
26
+ return false unless File.exist?(directory_path)
27
+ return false unless File.directory?(directory_path)
28
+
29
+ execute_explorer_command(directory_path)
30
+ end
31
+
32
+ private
33
+
34
+ def find_application_for_file(file_path)
35
+ extension = File.extname(file_path).downcase.sub('.', '')
36
+ applications = @config_loader.applications
37
+
38
+ applications.each do |extensions, app|
39
+ return app if extensions.is_a?(Array) && extensions.include?(extension)
40
+ end
41
+
42
+ applications[:default] || 'open'
43
+ end
44
+
45
+ def execute_command(application, file_path)
46
+ quoted_path = quote_shell_argument(file_path)
47
+
48
+ case RbConfig::CONFIG['host_os']
49
+ when /mswin|mingw|cygwin/
50
+ # Windows
51
+ system("start \"\" \"#{file_path}\"")
52
+ when /darwin/
53
+ # macOS
54
+ if application == 'open'
55
+ system("open #{quoted_path}")
56
+ else
57
+ # VSCodeなど特定のアプリケーション
58
+ system("#{application} #{quoted_path}")
59
+ end
60
+ else
61
+ # Linux/Unix
62
+ if application == 'open'
63
+ system("xdg-open #{quoted_path}")
64
+ else
65
+ system("#{application} #{quoted_path}")
66
+ end
67
+ end
68
+ rescue StandardError => e
69
+ warn "Failed to open file: #{e.message}"
70
+ false
71
+ end
72
+
73
+ def execute_command_with_line(application, file_path, line_number)
74
+ quoted_path = quote_shell_argument(file_path)
75
+
76
+ case RbConfig::CONFIG['host_os']
77
+ when /mswin|mingw|cygwin/
78
+ # Windows
79
+ if application.include?('code')
80
+ system("#{application} --goto #{quoted_path}:#{line_number}")
81
+ else
82
+ system("start \"\" \"#{file_path}\"")
83
+ end
84
+ when /darwin/
85
+ # macOS
86
+ if application == 'open'
87
+ system("open #{quoted_path}")
88
+ elsif application.include?('code')
89
+ system("#{application} --goto #{quoted_path}:#{line_number}")
90
+ elsif application.include?('vim') || application.include?('nvim')
91
+ system("#{application} +#{line_number} #{quoted_path}")
92
+ else
93
+ system("#{application} #{quoted_path}")
94
+ end
95
+ else
96
+ # Linux/Unix
97
+ if application == 'open'
98
+ system("xdg-open #{quoted_path}")
99
+ elsif application.include?('code')
100
+ system("#{application} --goto #{quoted_path}:#{line_number}")
101
+ elsif application.include?('vim') || application.include?('nvim')
102
+ system("#{application} +#{line_number} #{quoted_path}")
103
+ else
104
+ system("#{application} #{quoted_path}")
105
+ end
106
+ end
107
+ rescue StandardError => e
108
+ warn "Failed to open file: #{e.message}"
109
+ false
110
+ end
111
+
112
+ def quote_shell_argument(argument)
113
+ if argument.include?(' ') || argument.include?("'") || argument.include?('"')
114
+ '"' + argument.gsub('"', '\"') + '"'
115
+ else
116
+ argument
117
+ end
118
+ end
119
+
120
+ def execute_explorer_command(directory_path)
121
+ quoted_path = quote_shell_argument(directory_path)
122
+
123
+ case RbConfig::CONFIG['host_os']
124
+ when /mswin|mingw|cygwin/
125
+ # Windows
126
+ system("explorer #{quoted_path}")
127
+ when /darwin/
128
+ # macOS
129
+ system("open #{quoted_path}")
130
+ else
131
+ # Linux/Unix
132
+ system("xdg-open #{quoted_path}")
133
+ end
134
+ rescue StandardError => e
135
+ warn "Failed to open directory: #{e.message}"
136
+ false
137
+ end
138
+ end
139
+ end
140
+