beniya 0.1.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/README.md +288 -0
- data/README_EN.md +289 -0
- data/beniya.gemspec +40 -0
- data/bin/beniya +31 -0
- data/config_example.rb +88 -0
- data/lib/beniya/application.rb +28 -0
- data/lib/beniya/color_helper.rb +150 -0
- data/lib/beniya/config.rb +179 -0
- data/lib/beniya/config_loader.rb +110 -0
- data/lib/beniya/directory_listing.rb +99 -0
- data/lib/beniya/file_opener.rb +114 -0
- data/lib/beniya/file_preview.rb +172 -0
- data/lib/beniya/health_checker.rb +224 -0
- data/lib/beniya/keybind_handler.rb +332 -0
- data/lib/beniya/terminal_ui.rb +469 -0
- data/lib/beniya/version.rb +5 -0
- data/lib/beniya.rb +17 -0
- metadata +161 -0
data/config_example.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# beniya configuration example
|
4
|
+
# Copy this file to ~/.config/beniya/config.rb to customize your settings
|
5
|
+
|
6
|
+
# Cross-platform file opener method
|
7
|
+
def get_system_open_command
|
8
|
+
case RbConfig::CONFIG['host_os']
|
9
|
+
when /mswin|mingw|cygwin/
|
10
|
+
'start' # Windows
|
11
|
+
when /darwin/
|
12
|
+
'open' # macOS
|
13
|
+
when /linux|bsd/
|
14
|
+
'xdg-open' # Linux/BSD
|
15
|
+
else
|
16
|
+
'open' # Fallback to open
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the appropriate open command for current platform
|
21
|
+
SYSTEM_OPEN = get_system_open_command
|
22
|
+
|
23
|
+
# Language setting
|
24
|
+
# Available languages: 'en' (English), 'ja' (Japanese)
|
25
|
+
# If not specified, language will be auto-detected from environment variables
|
26
|
+
LANGUAGE = 'en' # or 'ja'
|
27
|
+
|
28
|
+
# Application associations
|
29
|
+
# Define which applications to use for opening different file types
|
30
|
+
APPLICATIONS = {
|
31
|
+
# Text files - open with 'code' (VS Code)
|
32
|
+
%w[txt md rb py js ts html css json xml yaml yml] => 'code',
|
33
|
+
|
34
|
+
# Image files - open with default system app
|
35
|
+
%w[jpg jpeg png gif bmp svg webp] => SYSTEM_OPEN,
|
36
|
+
|
37
|
+
# Video files - open with default system app
|
38
|
+
%w[mp4 avi mkv mov wmv] => SYSTEM_OPEN,
|
39
|
+
|
40
|
+
# Documents - open with default system app
|
41
|
+
%w[pdf doc docx xls xlsx ppt pptx] => SYSTEM_OPEN,
|
42
|
+
|
43
|
+
# Default application for unspecified file types
|
44
|
+
:default => SYSTEM_OPEN
|
45
|
+
}
|
46
|
+
|
47
|
+
# Color scheme
|
48
|
+
# Define colors for different types of files and UI elements
|
49
|
+
# You can use various color formats:
|
50
|
+
# - Symbols: :blue, :red, :green, :yellow, :cyan, :magenta, :white, :black
|
51
|
+
# - HSL: {hsl: [hue(0-360), saturation(0-100), lightness(0-100)]}
|
52
|
+
# - RGB: {rgb: [red(0-255), green(0-255), blue(0-255)]}
|
53
|
+
# - HEX: {hex: "#ff0000"}
|
54
|
+
# - ANSI codes: "34" or 34
|
55
|
+
COLORS = {
|
56
|
+
# HSL color examples (Hue: 0-360, Saturation: 0-100%, Lightness: 0-100%)
|
57
|
+
directory: { hsl: [220, 80, 60] }, # Blue directory entries
|
58
|
+
file: { hsl: [0, 0, 90] }, # Light gray regular files
|
59
|
+
executable: { hsl: [120, 70, 50] }, # Green executable files
|
60
|
+
selected: { hsl: [50, 90, 70] }, # Yellow currently selected item
|
61
|
+
preview: { hsl: [180, 60, 65] }, # Cyan preview panel
|
62
|
+
|
63
|
+
# You can also mix different formats:
|
64
|
+
# directory: :blue, # Traditional symbol
|
65
|
+
# file: {rgb: [200, 200, 200]}, # RGB format
|
66
|
+
# executable: {hex: "#00ff00"}, # HEX format
|
67
|
+
# selected: "93", # ANSI code (bright yellow)
|
68
|
+
}
|
69
|
+
|
70
|
+
# Key bindings
|
71
|
+
# Customize keyboard shortcuts (not yet fully implemented)
|
72
|
+
KEYBINDS = {
|
73
|
+
quit: %w[q ESC],
|
74
|
+
up: %w[k UP],
|
75
|
+
down: %w[j DOWN],
|
76
|
+
left: %w[h LEFT],
|
77
|
+
right: %w[l RIGHT ENTER],
|
78
|
+
top: %w[g],
|
79
|
+
bottom: %w[G],
|
80
|
+
refresh: %w[r],
|
81
|
+
search: %w[/],
|
82
|
+
open_file: %w[o SPACE]
|
83
|
+
}
|
84
|
+
|
85
|
+
# You can also set language via environment variable:
|
86
|
+
# export BENIYA_LANG=ja # Set to 'ja' for Japanese, 'en' for English
|
87
|
+
# Note: Only BENIYA_LANG is used for language detection
|
88
|
+
# System LANG variable is ignored to ensure English is the default
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Beniya
|
4
|
+
class Application
|
5
|
+
def initialize(start_directory = Dir.pwd)
|
6
|
+
@start_directory = File.expand_path(start_directory)
|
7
|
+
# Load configuration including language settings
|
8
|
+
ConfigLoader.load_config
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
# 各コンポーネントを初期化
|
13
|
+
directory_listing = DirectoryListing.new(@start_directory)
|
14
|
+
keybind_handler = KeybindHandler.new
|
15
|
+
file_preview = FilePreview.new
|
16
|
+
terminal_ui = TerminalUI.new
|
17
|
+
|
18
|
+
# アプリケーション開始
|
19
|
+
terminal_ui.start(directory_listing, keybind_handler, file_preview)
|
20
|
+
rescue Interrupt
|
21
|
+
puts "\n\n#{ConfigLoader.message('app.interrupted')}"
|
22
|
+
rescue StandardError => e
|
23
|
+
puts "\n#{ConfigLoader.message('app.error_occurred')}: #{e.message}"
|
24
|
+
puts e.backtrace.first(5).join("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Beniya
|
4
|
+
class ColorHelper
|
5
|
+
# HSLからRGBへの変換
|
6
|
+
def self.hsl_to_rgb(hue, saturation, lightness)
|
7
|
+
h = hue.to_f / 360.0
|
8
|
+
s = saturation.to_f / 100.0
|
9
|
+
l = lightness.to_f / 100.0
|
10
|
+
|
11
|
+
if s == 0
|
12
|
+
# 彩度が0の場合(グレースケール)
|
13
|
+
r = g = b = l
|
14
|
+
else
|
15
|
+
hue2rgb = lambda do |p, q, t|
|
16
|
+
t += 1 if t < 0
|
17
|
+
t -= 1 if t > 1
|
18
|
+
return p + (q - p) * 6 * t if t < 1.0/6
|
19
|
+
return q if t < 1.0/2
|
20
|
+
return p + (q - p) * (2.0/3 - t) * 6 if t < 2.0/3
|
21
|
+
p
|
22
|
+
end
|
23
|
+
|
24
|
+
q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
25
|
+
p = 2 * l - q
|
26
|
+
|
27
|
+
r = hue2rgb.call(p, q, h + 1.0/3)
|
28
|
+
g = hue2rgb.call(p, q, h)
|
29
|
+
b = hue2rgb.call(p, q, h - 1.0/3)
|
30
|
+
end
|
31
|
+
|
32
|
+
[(r * 255).round, (g * 255).round, (b * 255).round]
|
33
|
+
end
|
34
|
+
|
35
|
+
# 色設定をANSIエスケープコードに変換
|
36
|
+
def self.color_to_ansi(color_config)
|
37
|
+
case color_config
|
38
|
+
when Hash
|
39
|
+
if color_config[:hsl]
|
40
|
+
# HSL形式: {hsl: [240, 100, 50]}
|
41
|
+
hue, saturation, lightness = color_config[:hsl]
|
42
|
+
r, g, b = hsl_to_rgb(hue, saturation, lightness)
|
43
|
+
"\e[38;2;#{r};#{g};#{b}m"
|
44
|
+
elsif color_config[:rgb]
|
45
|
+
# RGB形式: {rgb: [100, 150, 200]}
|
46
|
+
r, g, b = color_config[:rgb]
|
47
|
+
"\e[38;2;#{r};#{g};#{b}m"
|
48
|
+
elsif color_config[:hex]
|
49
|
+
# HEX形式: {hex: "#ff0000"}
|
50
|
+
hex = color_config[:hex].gsub('#', '')
|
51
|
+
r = hex[0..1].to_i(16)
|
52
|
+
g = hex[2..3].to_i(16)
|
53
|
+
b = hex[4..5].to_i(16)
|
54
|
+
"\e[38;2;#{r};#{g};#{b}m"
|
55
|
+
else
|
56
|
+
# デフォルト(白)
|
57
|
+
"\e[37m"
|
58
|
+
end
|
59
|
+
when Symbol
|
60
|
+
# 従来のシンボル形式をANSIコードに変換
|
61
|
+
symbol_to_ansi(color_config)
|
62
|
+
when String
|
63
|
+
# 直接ANSIコードまたは名前が指定された場合
|
64
|
+
if color_config.match?(/^\d+$/)
|
65
|
+
"\e[#{color_config}m"
|
66
|
+
else
|
67
|
+
name_to_ansi(color_config)
|
68
|
+
end
|
69
|
+
when Integer
|
70
|
+
# 数値が直接指定された場合
|
71
|
+
"\e[#{color_config}m"
|
72
|
+
else
|
73
|
+
# デフォルト(白)
|
74
|
+
"\e[37m"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# シンボルをANSIコードに変換
|
79
|
+
def self.symbol_to_ansi(symbol)
|
80
|
+
case symbol
|
81
|
+
when :black then "\e[30m"
|
82
|
+
when :red then "\e[31m"
|
83
|
+
when :green then "\e[32m"
|
84
|
+
when :yellow then "\e[33m"
|
85
|
+
when :blue then "\e[34m"
|
86
|
+
when :magenta then "\e[35m"
|
87
|
+
when :cyan then "\e[36m"
|
88
|
+
when :white then "\e[37m"
|
89
|
+
when :bright_black then "\e[90m"
|
90
|
+
when :bright_red then "\e[91m"
|
91
|
+
when :bright_green then "\e[92m"
|
92
|
+
when :bright_yellow then "\e[93m"
|
93
|
+
when :bright_blue then "\e[94m"
|
94
|
+
when :bright_magenta then "\e[95m"
|
95
|
+
when :bright_cyan then "\e[96m"
|
96
|
+
when :bright_white then "\e[97m"
|
97
|
+
else "\e[37m" # デフォルト(白)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# 色名をANSIコードに変換
|
102
|
+
def self.name_to_ansi(name)
|
103
|
+
symbol_to_ansi(name.to_sym)
|
104
|
+
end
|
105
|
+
|
106
|
+
# 背景色用のANSIコードを生成
|
107
|
+
def self.color_to_bg_ansi(color_config)
|
108
|
+
ansi_code = color_to_ansi(color_config)
|
109
|
+
# 前景色(38)を背景色(48)に変換
|
110
|
+
ansi_code.gsub('38;', '48;')
|
111
|
+
end
|
112
|
+
|
113
|
+
# リセットコード
|
114
|
+
def self.reset
|
115
|
+
"\e[0m"
|
116
|
+
end
|
117
|
+
|
118
|
+
# 選択状態(反転表示)用のANSIコードを生成
|
119
|
+
def self.color_to_selected_ansi(color_config)
|
120
|
+
color_code = color_to_ansi(color_config)
|
121
|
+
# 反転表示を追加
|
122
|
+
color_code.gsub("\e[", "\e[7;").gsub("m", ";7m")
|
123
|
+
end
|
124
|
+
|
125
|
+
# プリセットHSLカラー
|
126
|
+
def self.preset_hsl_colors
|
127
|
+
{
|
128
|
+
# ディレクトリ用の青系
|
129
|
+
directory_blue: { hsl: [220, 80, 60] },
|
130
|
+
directory_cyan: { hsl: [180, 70, 55] },
|
131
|
+
|
132
|
+
# 実行ファイル用の緑系
|
133
|
+
executable_green: { hsl: [120, 70, 50] },
|
134
|
+
executable_lime: { hsl: [90, 80, 55] },
|
135
|
+
|
136
|
+
# テキストファイル用
|
137
|
+
text_white: { hsl: [0, 0, 90] },
|
138
|
+
text_gray: { hsl: [0, 0, 70] },
|
139
|
+
|
140
|
+
# 選択状態用
|
141
|
+
selected_yellow: { hsl: [50, 90, 70] },
|
142
|
+
selected_orange: { hsl: [30, 85, 65] },
|
143
|
+
|
144
|
+
# プレビュー用
|
145
|
+
preview_cyan: { hsl: [180, 60, 65] },
|
146
|
+
preview_purple: { hsl: [270, 50, 70] }
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Beniya
|
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' => 'beniya interrupted',
|
14
|
+
'app.error_occurred' => 'Error occurred',
|
15
|
+
'app.terminated' => 'beniya 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
|
+
|
33
|
+
# UI messages
|
34
|
+
'ui.operation_prompt' => 'Operation: ',
|
35
|
+
|
36
|
+
# Help text
|
37
|
+
'help.full' => 'j/k:move h:back l:enter o/Space:open g/G:top/bottom r:refresh q:quit',
|
38
|
+
'help.short' => 'j/k:move h:back l:enter o:open q:quit',
|
39
|
+
|
40
|
+
# Health check messages
|
41
|
+
'health.title' => 'beniya Health Check',
|
42
|
+
'health.ruby_version' => 'Ruby version',
|
43
|
+
'health.required_gems' => 'Required gems',
|
44
|
+
'health.fzf' => 'fzf (file search)',
|
45
|
+
'health.rga' => 'rga (content search)',
|
46
|
+
'health.file_opener' => 'System file opener',
|
47
|
+
'health.summary' => 'Summary:',
|
48
|
+
'health.ok' => 'OK',
|
49
|
+
'health.warnings' => 'Warnings',
|
50
|
+
'health.errors' => 'Errors',
|
51
|
+
'health.all_passed' => 'All checks passed! beniya is ready to use.',
|
52
|
+
'health.critical_missing' => 'Some critical components are missing. beniya may not work properly.',
|
53
|
+
'health.optional_missing' => 'Some optional features are unavailable. Basic functionality will work.',
|
54
|
+
'health.all_gems_installed' => 'All required gems installed',
|
55
|
+
'health.missing_gems' => 'Missing gems',
|
56
|
+
'health.gem_install_instruction' => 'Run: gem install',
|
57
|
+
'health.tool_not_found' => 'not found',
|
58
|
+
'health.unknown_platform' => 'Unknown platform',
|
59
|
+
'health.file_open_may_not_work' => 'File opening may not work properly',
|
60
|
+
'health.macos_opener' => 'macOS file opener',
|
61
|
+
'health.linux_opener' => 'Linux file opener',
|
62
|
+
'health.windows_opener' => 'Windows file opener',
|
63
|
+
'health.install_brew' => 'Install: brew install',
|
64
|
+
'health.install_apt' => 'Install: apt install',
|
65
|
+
'health.install_guide' => 'Check installation guide for your platform',
|
66
|
+
'health.rga_releases' => 'Install: https://github.com/phiresky/ripgrep-all/releases',
|
67
|
+
'health.ruby_upgrade_needed' => 'Please upgrade Ruby to version 2.7.0 or higher'
|
68
|
+
},
|
69
|
+
|
70
|
+
'ja' => {
|
71
|
+
# Application messages
|
72
|
+
'app.interrupted' => 'beniyaを中断しました',
|
73
|
+
'app.error_occurred' => 'エラーが発生しました',
|
74
|
+
'app.terminated' => 'beniyaを終了しました',
|
75
|
+
|
76
|
+
# File operations
|
77
|
+
'file.not_found' => 'ファイルが見つかりません',
|
78
|
+
'file.not_readable' => 'ファイルを読み取れません',
|
79
|
+
'file.read_error' => 'ファイル読み込みエラー',
|
80
|
+
'file.binary_file' => 'バイナリファイル',
|
81
|
+
'file.cannot_preview' => 'プレビューできません',
|
82
|
+
'file.encoding_error' => '文字エンコーディングエラー - ファイルを読み取れません',
|
83
|
+
'file.preview_error' => 'プレビューエラー',
|
84
|
+
'file.error_prefix' => 'エラー',
|
85
|
+
|
86
|
+
# Keybind messages
|
87
|
+
'keybind.invalid_key' => '無効なキー',
|
88
|
+
'keybind.search_text' => '検索テキスト: ',
|
89
|
+
'keybind.no_matches' => 'マッチするものが見つかりません。',
|
90
|
+
'keybind.press_any_key' => '何かキーを押して続行...',
|
91
|
+
|
92
|
+
# UI messages
|
93
|
+
'ui.operation_prompt' => '操作: ',
|
94
|
+
|
95
|
+
# Help text
|
96
|
+
'help.full' => 'j/k:移動 h:戻る l:入る o/Space:開く g/G:先頭/末尾 r:更新 q:終了',
|
97
|
+
'help.short' => 'j/k:移動 h:戻る l:入る o:開く q:終了',
|
98
|
+
|
99
|
+
# Health check messages
|
100
|
+
'health.title' => 'beniya ヘルスチェック',
|
101
|
+
'health.ruby_version' => 'Ruby バージョン',
|
102
|
+
'health.required_gems' => '必須 gem',
|
103
|
+
'health.fzf' => 'fzf (ファイル検索)',
|
104
|
+
'health.rga' => 'rga (内容検索)',
|
105
|
+
'health.file_opener' => 'システムファイルオープナー',
|
106
|
+
'health.summary' => 'サマリー:',
|
107
|
+
'health.ok' => 'OK',
|
108
|
+
'health.warnings' => '警告',
|
109
|
+
'health.errors' => 'エラー',
|
110
|
+
'health.all_passed' => '全てのチェックが完了しました!beniyaは使用可能です。',
|
111
|
+
'health.critical_missing' => '重要なコンポーネントが不足しています。beniyaは正常に動作しない可能性があります。',
|
112
|
+
'health.optional_missing' => 'オプション機能が利用できません。基本機能は動作します。',
|
113
|
+
'health.all_gems_installed' => '全ての必須gemがインストールされています',
|
114
|
+
'health.missing_gems' => '不足しているgem',
|
115
|
+
'health.gem_install_instruction' => '実行: gem install',
|
116
|
+
'health.tool_not_found' => 'が見つかりません',
|
117
|
+
'health.unknown_platform' => '不明なプラットフォーム',
|
118
|
+
'health.file_open_may_not_work' => 'ファイルオープンが正常に動作しない可能性があります',
|
119
|
+
'health.macos_opener' => 'macOS ファイルオープナー',
|
120
|
+
'health.linux_opener' => 'Linux ファイルオープナー',
|
121
|
+
'health.windows_opener' => 'Windows ファイルオープナー',
|
122
|
+
'health.install_brew' => 'インストール: brew install',
|
123
|
+
'health.install_apt' => 'インストール: apt install',
|
124
|
+
'health.install_guide' => 'お使いのプラットフォーム向けのインストールガイドを確認してください',
|
125
|
+
'health.rga_releases' => 'インストール: https://github.com/phiresky/ripgrep-all/releases',
|
126
|
+
'health.ruby_upgrade_needed' => 'Rubyをバージョン2.7.0以上にアップグレードしてください'
|
127
|
+
}
|
128
|
+
}.freeze
|
129
|
+
|
130
|
+
class << self
|
131
|
+
def current_language
|
132
|
+
@current_language ||= detect_language
|
133
|
+
end
|
134
|
+
|
135
|
+
def current_language=(lang)
|
136
|
+
if AVAILABLE_LANGUAGES.include?(lang.to_s)
|
137
|
+
@current_language = lang.to_s
|
138
|
+
else
|
139
|
+
raise ArgumentError, "Unsupported language: #{lang}. Available: #{AVAILABLE_LANGUAGES.join(', ')}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def message(key, **interpolations)
|
144
|
+
msg = MESSAGES.dig(current_language, key) || MESSAGES.dig(DEFAULT_LANGUAGE, key) || key
|
145
|
+
|
146
|
+
# Simple interpolation support
|
147
|
+
interpolations.each do |placeholder, value|
|
148
|
+
msg = msg.gsub("%{#{placeholder}}", value.to_s)
|
149
|
+
end
|
150
|
+
|
151
|
+
msg
|
152
|
+
end
|
153
|
+
|
154
|
+
def available_languages
|
155
|
+
AVAILABLE_LANGUAGES.dup
|
156
|
+
end
|
157
|
+
|
158
|
+
def reset_language!
|
159
|
+
@current_language = nil
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def detect_language
|
165
|
+
# Only BENIYA_LANG environment variable takes precedence
|
166
|
+
# This ensures English is default unless explicitly requested
|
167
|
+
env_lang = ENV['BENIYA_LANG']
|
168
|
+
|
169
|
+
if env_lang && !env_lang.empty?
|
170
|
+
# Extract language code (e.g., 'ja_JP.UTF-8' -> 'ja')
|
171
|
+
lang_code = env_lang.split(/[_.]/).first&.downcase
|
172
|
+
return lang_code if AVAILABLE_LANGUAGES.include?(lang_code)
|
173
|
+
end
|
174
|
+
|
175
|
+
DEFAULT_LANGUAGE
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'config'
|
4
|
+
|
5
|
+
module Beniya
|
6
|
+
class ConfigLoader
|
7
|
+
CONFIG_PATH = File.expand_path('~/.config/beniya/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,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Beniya
|
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
|
+
private
|
71
|
+
|
72
|
+
def determine_file_type(path)
|
73
|
+
return 'directory' if File.directory?(path)
|
74
|
+
return 'executable' if File.executable?(path) && !File.directory?(path)
|
75
|
+
|
76
|
+
'file'
|
77
|
+
end
|
78
|
+
|
79
|
+
def safe_file_size(path)
|
80
|
+
File.size(path)
|
81
|
+
rescue StandardError
|
82
|
+
0
|
83
|
+
end
|
84
|
+
|
85
|
+
def safe_file_mtime(path)
|
86
|
+
File.mtime(path)
|
87
|
+
rescue StandardError
|
88
|
+
Time.now
|
89
|
+
end
|
90
|
+
|
91
|
+
def sort_entries!
|
92
|
+
@entries.sort_by! do |entry|
|
93
|
+
# ディレクトリを最初に、その後ファイル名でソート
|
94
|
+
[entry[:type] == 'directory' ? 0 : 1, entry[:name].downcase]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|