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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +188 -0
- data/CHANGELOG_v0.4.0.md +146 -0
- data/CHANGELOG_v0.5.0.md +26 -0
- data/CHANGELOG_v0.6.0.md +182 -0
- data/CHANGELOG_v0.7.0.md +280 -0
- data/CHANGELOG_v0.8.0.md +267 -0
- data/CHANGELOG_v0.9.0.md +279 -0
- data/README.md +631 -0
- data/README_EN.md +561 -0
- data/Rakefile +156 -0
- data/bin/rufio +34 -0
- data/config_example.rb +88 -0
- data/docs/PLUGIN_GUIDE.md +431 -0
- data/docs/plugin_example.rb +119 -0
- data/lib/rufio/application.rb +32 -0
- data/lib/rufio/bookmark.rb +115 -0
- data/lib/rufio/bookmark_manager.rb +173 -0
- data/lib/rufio/color_helper.rb +150 -0
- data/lib/rufio/command_mode.rb +72 -0
- data/lib/rufio/command_mode_ui.rb +168 -0
- data/lib/rufio/config.rb +199 -0
- data/lib/rufio/config_loader.rb +110 -0
- data/lib/rufio/dialog_renderer.rb +127 -0
- data/lib/rufio/directory_listing.rb +113 -0
- data/lib/rufio/file_opener.rb +140 -0
- data/lib/rufio/file_operations.rb +231 -0
- data/lib/rufio/file_preview.rb +200 -0
- data/lib/rufio/filter_manager.rb +114 -0
- data/lib/rufio/health_checker.rb +246 -0
- data/lib/rufio/keybind_handler.rb +828 -0
- data/lib/rufio/logger.rb +103 -0
- data/lib/rufio/plugin.rb +89 -0
- data/lib/rufio/plugin_config.rb +59 -0
- data/lib/rufio/plugin_manager.rb +84 -0
- data/lib/rufio/plugins/file_operations.rb +44 -0
- data/lib/rufio/selection_manager.rb +79 -0
- data/lib/rufio/terminal_ui.rb +630 -0
- data/lib/rufio/text_utils.rb +108 -0
- data/lib/rufio/version.rb +5 -0
- data/lib/rufio/zoxide_integration.rb +188 -0
- data/lib/rufio.rb +33 -0
- data/publish_gem.zsh +131 -0
- data/rufio.gemspec +40 -0
- data/test_delete/test1.txt +1 -0
- data/test_delete/test2.txt +1 -0
- metadata +189 -0
data/lib/rufio/config.rb
ADDED
|
@@ -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
|
+
|