rufio 0.11.0 → 0.20.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 +4 -4
- data/CHANGELOG_v0.20.0.md +166 -0
- data/README.md +66 -14
- data/lib/rufio/bookmark.rb +19 -3
- data/lib/rufio/bookmark_manager.rb +111 -13
- data/lib/rufio/config_loader.rb +15 -2
- data/lib/rufio/file_operations.rb +66 -0
- data/lib/rufio/file_preview.rb +1 -1
- data/lib/rufio/keybind_handler.rb +434 -24
- data/lib/rufio/project_command.rb +147 -0
- data/lib/rufio/project_log.rb +68 -0
- data/lib/rufio/project_mode.rb +58 -0
- data/lib/rufio/terminal_ui.rb +497 -4
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +5 -0
- metadata +5 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Rufio
|
|
7
|
+
# プロジェクトログ - コマンド実行ログを管理する
|
|
8
|
+
class ProjectLog
|
|
9
|
+
def initialize(log_dir)
|
|
10
|
+
@log_dir = log_dir
|
|
11
|
+
FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# ログを保存する
|
|
15
|
+
# @param project_name [String] プロジェクト名
|
|
16
|
+
# @param command [String] 実行したコマンド
|
|
17
|
+
# @param output [String] コマンドの出力
|
|
18
|
+
# @return [String] 保存したログファイルのパス
|
|
19
|
+
def save(project_name, command, output)
|
|
20
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
21
|
+
log_filename = "#{project_name}_#{timestamp}.log"
|
|
22
|
+
log_path = File.join(@log_dir, log_filename)
|
|
23
|
+
|
|
24
|
+
log_content = <<~LOG
|
|
25
|
+
Project: #{project_name}
|
|
26
|
+
Command: #{command}
|
|
27
|
+
Timestamp: #{Time.now}
|
|
28
|
+
|
|
29
|
+
Output:
|
|
30
|
+
#{output}
|
|
31
|
+
LOG
|
|
32
|
+
|
|
33
|
+
File.write(log_path, log_content)
|
|
34
|
+
log_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ログディレクトリに移動する
|
|
38
|
+
# @return [Hash] ログディレクトリ情報
|
|
39
|
+
def navigate_to_log_dir
|
|
40
|
+
{
|
|
41
|
+
path: @log_dir
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ログファイルの一覧を取得(新しい順)
|
|
46
|
+
# @return [Array<String>] ログファイル名の配列
|
|
47
|
+
def list_log_files
|
|
48
|
+
log_files = Dir.glob(File.join(@log_dir, '*.log'))
|
|
49
|
+
|
|
50
|
+
# ファイルの更新時刻でソート(新しい順)
|
|
51
|
+
log_files.sort_by { |f| -File.mtime(f).to_i }
|
|
52
|
+
.map { |f| File.basename(f) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# ログファイルのプレビューを取得
|
|
56
|
+
# @param filename [String] ログファイル名
|
|
57
|
+
# @return [String] ログファイルの内容
|
|
58
|
+
def preview(filename)
|
|
59
|
+
log_path = File.join(@log_dir, filename)
|
|
60
|
+
|
|
61
|
+
return '' unless File.exist?(log_path)
|
|
62
|
+
|
|
63
|
+
File.read(log_path)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
''
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# プロジェクトモード - ブックマークしたプロジェクトの管理とコマンド実行
|
|
5
|
+
class ProjectMode
|
|
6
|
+
attr_reader :selected_path, :selected_name
|
|
7
|
+
|
|
8
|
+
def initialize(bookmark, log_dir)
|
|
9
|
+
@bookmark = bookmark
|
|
10
|
+
@log_dir = log_dir
|
|
11
|
+
@active = false
|
|
12
|
+
@selected_path = nil
|
|
13
|
+
@selected_name = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# プロジェクトモードをアクティブにする
|
|
17
|
+
def activate
|
|
18
|
+
@active = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# プロジェクトモードを非アクティブにする
|
|
22
|
+
def deactivate
|
|
23
|
+
@active = false
|
|
24
|
+
@selected_path = nil
|
|
25
|
+
@selected_name = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# プロジェクトモードがアクティブかどうか
|
|
29
|
+
def active?
|
|
30
|
+
@active
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ブックマーク一覧を取得
|
|
34
|
+
def list_bookmarks
|
|
35
|
+
return [] unless @active
|
|
36
|
+
|
|
37
|
+
@bookmark.list
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# ブックマークを番号で選択
|
|
41
|
+
def select_bookmark(number)
|
|
42
|
+
return false unless @active
|
|
43
|
+
|
|
44
|
+
bookmark = @bookmark.find_by_number(number)
|
|
45
|
+
return false unless bookmark
|
|
46
|
+
|
|
47
|
+
@selected_path = bookmark[:path]
|
|
48
|
+
@selected_name = bookmark[:name]
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 選択をクリア
|
|
53
|
+
def clear_selection
|
|
54
|
+
@selected_path = nil
|
|
55
|
+
@selected_name = nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -48,6 +48,13 @@ module Rufio
|
|
|
48
48
|
@command_mode = CommandMode.new
|
|
49
49
|
@dialog_renderer = DialogRenderer.new
|
|
50
50
|
@command_mode_ui = CommandModeUI.new(@command_mode, @dialog_renderer)
|
|
51
|
+
|
|
52
|
+
# Project mode
|
|
53
|
+
@project_mode = nil
|
|
54
|
+
@project_command = nil
|
|
55
|
+
@project_log = nil
|
|
56
|
+
@in_project_mode = false
|
|
57
|
+
@in_log_mode = false
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
def start(directory_listing, keybind_handler, file_preview)
|
|
@@ -112,6 +119,12 @@ module Rufio
|
|
|
112
119
|
# move cursor to top of screen (don't clear)
|
|
113
120
|
print "\e[H"
|
|
114
121
|
|
|
122
|
+
# プロジェクトモードの場合は専用の画面を描画
|
|
123
|
+
if @in_project_mode
|
|
124
|
+
draw_project_mode_screen
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
115
128
|
# header (2 lines)
|
|
116
129
|
draw_header
|
|
117
130
|
draw_base_directory_info
|
|
@@ -421,7 +434,7 @@ module Rufio
|
|
|
421
434
|
width = 0
|
|
422
435
|
string.each_char do |char|
|
|
423
436
|
# 全角文字の判定
|
|
424
|
-
width += if char.ord > 127 || char.match?(/[
|
|
437
|
+
width += if char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/)
|
|
425
438
|
2
|
|
426
439
|
else
|
|
427
440
|
1
|
|
@@ -438,7 +451,7 @@ module Rufio
|
|
|
438
451
|
result = ''
|
|
439
452
|
|
|
440
453
|
string.each_char do |char|
|
|
441
|
-
char_width = char.ord > 127 || char.match?(/[
|
|
454
|
+
char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
|
|
442
455
|
|
|
443
456
|
if current_width + char_width > max_width
|
|
444
457
|
# "..."を追加できるかチェック
|
|
@@ -464,7 +477,7 @@ module Rufio
|
|
|
464
477
|
punct_break_point = nil
|
|
465
478
|
|
|
466
479
|
line.each_char.with_index do |char, index|
|
|
467
|
-
char_width = char.ord > 127 || char.match?(/[
|
|
480
|
+
char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
|
|
468
481
|
|
|
469
482
|
break if current_width + char_width > max_width
|
|
470
483
|
|
|
@@ -566,7 +579,7 @@ module Rufio
|
|
|
566
579
|
end
|
|
567
580
|
|
|
568
581
|
# キーバインドハンドラーに処理を委譲
|
|
569
|
-
|
|
582
|
+
_result = @keybind_handler.handle_key(input)
|
|
570
583
|
|
|
571
584
|
# 終了処理(qキーのみ)
|
|
572
585
|
if input == 'q'
|
|
@@ -680,6 +693,486 @@ module Rufio
|
|
|
680
693
|
# Redraw the screen
|
|
681
694
|
draw_screen
|
|
682
695
|
end
|
|
696
|
+
|
|
697
|
+
# プロジェクトモードを設定
|
|
698
|
+
def set_project_mode(project_mode, project_command, project_log)
|
|
699
|
+
@project_mode = project_mode
|
|
700
|
+
@project_command = project_command
|
|
701
|
+
@project_log = project_log
|
|
702
|
+
@in_project_mode = true
|
|
703
|
+
@in_log_mode = false
|
|
704
|
+
refresh_display
|
|
705
|
+
draw_screen
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# プロジェクトモードを終了
|
|
709
|
+
def exit_project_mode
|
|
710
|
+
@in_project_mode = false
|
|
711
|
+
@in_log_mode = false
|
|
712
|
+
@project_mode = nil
|
|
713
|
+
@project_command = nil
|
|
714
|
+
@project_log = nil
|
|
715
|
+
refresh_display
|
|
716
|
+
draw_screen
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# ログモードに入る
|
|
720
|
+
def enter_log_mode(project_log)
|
|
721
|
+
@in_log_mode = true
|
|
722
|
+
@project_log = project_log
|
|
723
|
+
refresh_display
|
|
724
|
+
draw_screen
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# プロジェクトモード画面を描画
|
|
728
|
+
def draw_project_mode_screen
|
|
729
|
+
# header
|
|
730
|
+
print "\e[1;1H" # Move to top-left
|
|
731
|
+
header = @in_log_mode ? "📋 Project Mode - Logs" : "📁 Project Mode - Bookmarks"
|
|
732
|
+
print "\e[44m\e[97m#{header.ljust(@screen_width)}\e[0m\n"
|
|
733
|
+
print "\e[0m#{' ' * @screen_width}\n"
|
|
734
|
+
|
|
735
|
+
# calculate dimensions
|
|
736
|
+
content_height = @screen_height - HEADER_FOOTER_MARGIN
|
|
737
|
+
left_width = (@screen_width * LEFT_PANEL_RATIO).to_i
|
|
738
|
+
right_width = @screen_width - left_width
|
|
739
|
+
|
|
740
|
+
if @in_log_mode
|
|
741
|
+
# ログモード: ログファイル一覧と内容
|
|
742
|
+
draw_log_list(left_width, content_height)
|
|
743
|
+
draw_log_preview(right_width, content_height, left_width)
|
|
744
|
+
else
|
|
745
|
+
# ブックマークモード: プロジェクト一覧と詳細
|
|
746
|
+
draw_bookmark_list(left_width, content_height)
|
|
747
|
+
draw_bookmark_detail(right_width, content_height, left_width)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# footer(通常モードと同じスタイル)
|
|
751
|
+
footer_line = @screen_height
|
|
752
|
+
print "\e[#{footer_line};1H"
|
|
753
|
+
footer_text = if @in_log_mode
|
|
754
|
+
"ESC:exit log j/k:move"
|
|
755
|
+
else
|
|
756
|
+
"SPACE:select l:logs ::cmd r:rename d:delete ESC:exit j/k:move"
|
|
757
|
+
end
|
|
758
|
+
# 文字列を確実に画面幅に合わせる
|
|
759
|
+
footer_content = footer_text.ljust(@screen_width)[0...@screen_width]
|
|
760
|
+
print "\e[7m#{footer_content}\e[0m"
|
|
761
|
+
|
|
762
|
+
# move cursor to invisible position
|
|
763
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# ブックマーク一覧を描画
|
|
767
|
+
def draw_bookmark_list(width, height)
|
|
768
|
+
bookmarks = @project_mode.list_bookmarks
|
|
769
|
+
current_index = @keybind_handler.current_index
|
|
770
|
+
|
|
771
|
+
print "\e[#{CONTENT_START_LINE};1H"
|
|
772
|
+
|
|
773
|
+
if bookmarks.empty?
|
|
774
|
+
print " No bookmarks found"
|
|
775
|
+
(height - 1).times { puts ' ' * width }
|
|
776
|
+
return
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
selected_name = @project_mode.selected_name
|
|
780
|
+
|
|
781
|
+
bookmarks.each_with_index do |bookmark, index|
|
|
782
|
+
line_num = CONTENT_START_LINE + index
|
|
783
|
+
break if index >= height
|
|
784
|
+
|
|
785
|
+
# 選択マーク(通常モードと同じ)
|
|
786
|
+
is_project_selected = (bookmark[:name] == selected_name)
|
|
787
|
+
selection_mark = is_project_selected ? "✓ " : " "
|
|
788
|
+
|
|
789
|
+
# ブックマーク名を表示
|
|
790
|
+
name = bookmark[:name]
|
|
791
|
+
max_name_length = width - 4 # selection_mark分を除く
|
|
792
|
+
display_name = name.length > max_name_length ? name[0...max_name_length - 3] + '...' : name
|
|
793
|
+
line_content = "#{selection_mark}#{display_name}".ljust(width)
|
|
794
|
+
|
|
795
|
+
if index == current_index
|
|
796
|
+
# カーソル位置は選択色でハイライト
|
|
797
|
+
selected_color = ColorHelper.color_to_selected_ansi(ConfigLoader.colors[:selected])
|
|
798
|
+
print "\e[#{line_num};1H#{selected_color}#{line_content[0...width]}#{ColorHelper.reset}"
|
|
799
|
+
else
|
|
800
|
+
# 選択済みブックマークは緑背景、黒文字
|
|
801
|
+
if is_project_selected
|
|
802
|
+
print "\e[#{line_num};1H\e[42m\e[30m#{line_content[0...width]}\e[0m"
|
|
803
|
+
else
|
|
804
|
+
print "\e[#{line_num};1H#{line_content[0...width]}"
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# 残りの行をクリア
|
|
810
|
+
remaining_lines = height - bookmarks.length
|
|
811
|
+
remaining_lines.times do |i|
|
|
812
|
+
line_num = CONTENT_START_LINE + bookmarks.length + i
|
|
813
|
+
print "\e[#{line_num};1H#{' ' * width}"
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# ブックマーク詳細を描画
|
|
818
|
+
def draw_bookmark_detail(width, height, left_offset)
|
|
819
|
+
bookmarks = @project_mode.list_bookmarks
|
|
820
|
+
current_index = @keybind_handler.current_index
|
|
821
|
+
|
|
822
|
+
return if bookmarks.empty? || current_index >= bookmarks.length
|
|
823
|
+
|
|
824
|
+
bookmark = bookmarks[current_index]
|
|
825
|
+
path = bookmark[:path]
|
|
826
|
+
|
|
827
|
+
# ディレクトリ内容を取得
|
|
828
|
+
details = [
|
|
829
|
+
"Project: #{bookmark[:name]}",
|
|
830
|
+
"Path: #{path}",
|
|
831
|
+
"",
|
|
832
|
+
"Directory contents:",
|
|
833
|
+
""
|
|
834
|
+
]
|
|
835
|
+
|
|
836
|
+
# ディレクトリが存在する場合、内容を表示
|
|
837
|
+
if Dir.exist?(path)
|
|
838
|
+
begin
|
|
839
|
+
entries = Dir.entries(path).reject { |e| e == '.' || e == '..' }.sort
|
|
840
|
+
|
|
841
|
+
# 最大表示数を計算(ヘッダー分を引く)
|
|
842
|
+
max_entries = height - details.length
|
|
843
|
+
|
|
844
|
+
entries.take(max_entries).each do |entry|
|
|
845
|
+
full_path = File.join(path, entry)
|
|
846
|
+
icon = File.directory?(full_path) ? '📁' : '📄'
|
|
847
|
+
details << " #{icon} #{entry}"
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# 表示しきれない場合
|
|
851
|
+
if entries.length > max_entries
|
|
852
|
+
details << " ... and #{entries.length - max_entries} more"
|
|
853
|
+
end
|
|
854
|
+
rescue => e
|
|
855
|
+
details << " Error reading directory: #{e.message}"
|
|
856
|
+
end
|
|
857
|
+
else
|
|
858
|
+
details << " Directory does not exist"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# 各行にセパレータと内容を表示(通常モードと同じ)
|
|
862
|
+
height.times do |i|
|
|
863
|
+
line_num = CONTENT_START_LINE + i
|
|
864
|
+
|
|
865
|
+
# セパレータを表示
|
|
866
|
+
cursor_position = left_offset + CURSOR_OFFSET
|
|
867
|
+
print "\e[#{line_num};#{cursor_position}H"
|
|
868
|
+
print '│'
|
|
869
|
+
|
|
870
|
+
# 右画面の内容を表示
|
|
871
|
+
if i < details.length
|
|
872
|
+
line = details[i]
|
|
873
|
+
safe_width = width - 2
|
|
874
|
+
content = " #{line}"
|
|
875
|
+
content = content[0...safe_width] if content.length > safe_width
|
|
876
|
+
print content
|
|
877
|
+
|
|
878
|
+
# 残りをスペースで埋める
|
|
879
|
+
remaining = safe_width - content.length
|
|
880
|
+
print ' ' * remaining if remaining > 0
|
|
881
|
+
else
|
|
882
|
+
# 空行
|
|
883
|
+
print ' ' * (width - 2)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# ログファイル一覧を描画
|
|
889
|
+
def draw_log_list(width, height)
|
|
890
|
+
log_files = @project_log.list_log_files
|
|
891
|
+
current_index = @keybind_handler.current_index
|
|
892
|
+
|
|
893
|
+
print "\e[#{CONTENT_START_LINE};1H"
|
|
894
|
+
|
|
895
|
+
if log_files.empty?
|
|
896
|
+
print " No log files found"
|
|
897
|
+
(height - 1).times { puts ' ' * width }
|
|
898
|
+
return
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
log_files.each_with_index do |filename, index|
|
|
902
|
+
line_num = CONTENT_START_LINE + index
|
|
903
|
+
break if index >= height
|
|
904
|
+
|
|
905
|
+
cursor_mark = index == current_index ? '>' : ' '
|
|
906
|
+
display_name = filename.ljust(width - 3)
|
|
907
|
+
|
|
908
|
+
if index == current_index
|
|
909
|
+
print "\e[#{line_num};1H\e[7m#{cursor_mark} #{display_name[0...width-3]}\e[0m"
|
|
910
|
+
else
|
|
911
|
+
print "\e[#{line_num};1H #{display_name[0...width-3]}"
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# 残りの行をクリア
|
|
916
|
+
remaining_lines = height - log_files.length
|
|
917
|
+
remaining_lines.times do |i|
|
|
918
|
+
line_num = CONTENT_START_LINE + log_files.length + i
|
|
919
|
+
print "\e[#{line_num};1H#{' ' * width}"
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# ログプレビューを描画
|
|
924
|
+
def draw_log_preview(width, height, left_offset)
|
|
925
|
+
log_files = @project_log.list_log_files
|
|
926
|
+
current_index = @keybind_handler.current_index
|
|
927
|
+
|
|
928
|
+
return if log_files.empty? || current_index >= log_files.length
|
|
929
|
+
|
|
930
|
+
filename = log_files[current_index]
|
|
931
|
+
content = @project_log.preview(filename)
|
|
932
|
+
|
|
933
|
+
lines = content.split("\n")
|
|
934
|
+
|
|
935
|
+
# 各行にセパレータと内容を表示(通常モードと同じ)
|
|
936
|
+
height.times do |i|
|
|
937
|
+
line_num = CONTENT_START_LINE + i
|
|
938
|
+
|
|
939
|
+
# セパレータを表示
|
|
940
|
+
cursor_position = left_offset + CURSOR_OFFSET
|
|
941
|
+
print "\e[#{line_num};#{cursor_position}H"
|
|
942
|
+
print '│'
|
|
943
|
+
|
|
944
|
+
# 右画面の内容を表示
|
|
945
|
+
if i < lines.length
|
|
946
|
+
line = lines[i]
|
|
947
|
+
safe_width = width - 2
|
|
948
|
+
content = " #{line}"
|
|
949
|
+
content = content[0...safe_width] if content.length > safe_width
|
|
950
|
+
print content
|
|
951
|
+
|
|
952
|
+
# 残りをスペースで埋める
|
|
953
|
+
remaining = safe_width - content.length
|
|
954
|
+
print ' ' * remaining if remaining > 0
|
|
955
|
+
else
|
|
956
|
+
# 空行
|
|
957
|
+
print ' ' * (width - 2)
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# ログモードを終了してプロジェクトモードに戻る
|
|
963
|
+
def exit_log_mode
|
|
964
|
+
@in_log_mode = false
|
|
965
|
+
refresh_display
|
|
966
|
+
draw_screen
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# プロジェクト未選択メッセージ
|
|
970
|
+
def show_project_not_selected_message
|
|
971
|
+
content_lines = [
|
|
972
|
+
'',
|
|
973
|
+
'Please select a project first by pressing SPACE',
|
|
974
|
+
'',
|
|
975
|
+
'Press any key to continue...'
|
|
976
|
+
]
|
|
977
|
+
|
|
978
|
+
width = 50
|
|
979
|
+
height = 8
|
|
980
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
981
|
+
|
|
982
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, 'No Project Selected', content_lines, {
|
|
983
|
+
border_color: "\e[33m", # Yellow (warning)
|
|
984
|
+
title_color: "\e[1;33m", # Bold yellow
|
|
985
|
+
content_color: "\e[37m" # White
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
require 'io/console'
|
|
989
|
+
IO.console.getch
|
|
990
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
991
|
+
|
|
992
|
+
# 画面を再描画
|
|
993
|
+
refresh_display
|
|
994
|
+
draw_screen
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
# プロジェクトモードでコマンドを実行
|
|
998
|
+
def activate_project_command_mode(project_mode, project_command, project_log)
|
|
999
|
+
return unless project_mode.selected_path
|
|
1000
|
+
|
|
1001
|
+
# スクリプトまたはコマンドを選択
|
|
1002
|
+
choice = show_script_or_command_dialog(project_mode.selected_name, project_command)
|
|
1003
|
+
return unless choice
|
|
1004
|
+
|
|
1005
|
+
command = nil
|
|
1006
|
+
result = nil
|
|
1007
|
+
|
|
1008
|
+
if choice[:type] == :script
|
|
1009
|
+
# スクリプトを実行
|
|
1010
|
+
command = "ruby script: #{choice[:value]}"
|
|
1011
|
+
result = project_command.execute_script(choice[:value], project_mode.selected_path)
|
|
1012
|
+
else
|
|
1013
|
+
# 通常のコマンドを実行
|
|
1014
|
+
command = choice[:value]
|
|
1015
|
+
result = project_command.execute(command, project_mode.selected_path)
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
# ログを保存
|
|
1019
|
+
project_log.save(project_mode.selected_name, command, result[:output])
|
|
1020
|
+
|
|
1021
|
+
# 結果を表示
|
|
1022
|
+
show_project_command_result_dialog(command, result)
|
|
1023
|
+
|
|
1024
|
+
# 画面を再描画
|
|
1025
|
+
refresh_display
|
|
1026
|
+
draw_screen
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# スクリプトまたはコマンドを選択
|
|
1030
|
+
def show_script_or_command_dialog(project_name, project_command)
|
|
1031
|
+
scripts = project_command.list_scripts
|
|
1032
|
+
|
|
1033
|
+
content_lines = [
|
|
1034
|
+
'',
|
|
1035
|
+
"Project: #{project_name}",
|
|
1036
|
+
''
|
|
1037
|
+
]
|
|
1038
|
+
|
|
1039
|
+
if scripts.empty?
|
|
1040
|
+
content_lines << 'No scripts found in scripts directory'
|
|
1041
|
+
content_lines << " (#{project_command.scripts_dir})"
|
|
1042
|
+
content_lines << ''
|
|
1043
|
+
content_lines << 'Press C to enter custom command'
|
|
1044
|
+
content_lines << 'Press ESC to cancel'
|
|
1045
|
+
else
|
|
1046
|
+
content_lines << 'Available scripts:'
|
|
1047
|
+
content_lines << ''
|
|
1048
|
+
scripts.each_with_index do |script, index|
|
|
1049
|
+
content_lines << " #{index + 1}. #{script}"
|
|
1050
|
+
end
|
|
1051
|
+
content_lines << ''
|
|
1052
|
+
content_lines << 'Press 1-9 to select script'
|
|
1053
|
+
content_lines << 'Press C to enter custom command'
|
|
1054
|
+
content_lines << 'Press ESC to cancel'
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
width = 70
|
|
1058
|
+
height = [content_lines.length + 4, 25].min
|
|
1059
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
1060
|
+
|
|
1061
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, 'Execute in Project', content_lines, {
|
|
1062
|
+
border_color: "\e[32m",
|
|
1063
|
+
title_color: "\e[1;32m",
|
|
1064
|
+
content_color: "\e[37m"
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
require 'io/console'
|
|
1068
|
+
choice = nil
|
|
1069
|
+
|
|
1070
|
+
loop do
|
|
1071
|
+
input = IO.console.getch.downcase
|
|
1072
|
+
|
|
1073
|
+
case input
|
|
1074
|
+
when "\e" # ESC
|
|
1075
|
+
break
|
|
1076
|
+
when 'c' # Custom command
|
|
1077
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
1078
|
+
command = show_project_command_input_dialog(project_name)
|
|
1079
|
+
choice = { type: :command, value: command } if command && !command.empty?
|
|
1080
|
+
break
|
|
1081
|
+
when '1'..'9'
|
|
1082
|
+
number = input.to_i
|
|
1083
|
+
if number > 0 && number <= scripts.length
|
|
1084
|
+
choice = { type: :script, value: scripts[number - 1] }
|
|
1085
|
+
break
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
1091
|
+
choice
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
# プロジェクトコマンド入力ダイアログ
|
|
1095
|
+
def show_project_command_input_dialog(project_name)
|
|
1096
|
+
title = "Execute Command in: #{project_name}"
|
|
1097
|
+
prompt = "Enter command:"
|
|
1098
|
+
|
|
1099
|
+
@dialog_renderer.show_input_dialog(title, prompt, {
|
|
1100
|
+
border_color: "\e[32m", # Green
|
|
1101
|
+
title_color: "\e[1;32m", # Bold green
|
|
1102
|
+
content_color: "\e[37m" # White
|
|
1103
|
+
})
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
# プロジェクトコマンド結果ダイアログ
|
|
1107
|
+
def show_project_command_result_dialog(command, result)
|
|
1108
|
+
title = result[:success] ? "Command Success" : "Command Failed"
|
|
1109
|
+
|
|
1110
|
+
# 出力を最初の10行まで表示
|
|
1111
|
+
output_lines = (result[:output] || result[:error] || '').split("\n").take(10)
|
|
1112
|
+
|
|
1113
|
+
content_lines = [
|
|
1114
|
+
'',
|
|
1115
|
+
"Command: #{command}",
|
|
1116
|
+
'',
|
|
1117
|
+
"Output:",
|
|
1118
|
+
''
|
|
1119
|
+
] + output_lines
|
|
1120
|
+
|
|
1121
|
+
if output_lines.length >= 10
|
|
1122
|
+
content_lines << '... (see log for full output)'
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
content_lines << ''
|
|
1126
|
+
content_lines << 'Press any key to continue...'
|
|
1127
|
+
|
|
1128
|
+
width = 80
|
|
1129
|
+
height = [content_lines.length + 4, 20].min
|
|
1130
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
1131
|
+
|
|
1132
|
+
border_color = result[:success] ? "\e[32m" : "\e[31m" # Green or Red
|
|
1133
|
+
title_color = result[:success] ? "\e[1;32m" : "\e[1;31m"
|
|
1134
|
+
|
|
1135
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
|
|
1136
|
+
border_color: border_color,
|
|
1137
|
+
title_color: title_color,
|
|
1138
|
+
content_color: "\e[37m"
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
require 'io/console'
|
|
1142
|
+
IO.console.getch
|
|
1143
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# プロジェクト選択時の表示
|
|
1147
|
+
def show_project_selected
|
|
1148
|
+
# 選択完了メッセージを表示
|
|
1149
|
+
content_lines = [
|
|
1150
|
+
'',
|
|
1151
|
+
'Project selected!',
|
|
1152
|
+
'',
|
|
1153
|
+
'You can now press : to execute commands',
|
|
1154
|
+
'',
|
|
1155
|
+
'Press any key to continue...'
|
|
1156
|
+
]
|
|
1157
|
+
|
|
1158
|
+
width = 50
|
|
1159
|
+
height = 10
|
|
1160
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
1161
|
+
|
|
1162
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, 'Project Selected', content_lines, {
|
|
1163
|
+
border_color: "\e[32m", # Green
|
|
1164
|
+
title_color: "\e[1;32m", # Bold green
|
|
1165
|
+
content_color: "\e[37m" # White
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
require 'io/console'
|
|
1169
|
+
IO.console.getch
|
|
1170
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
1171
|
+
|
|
1172
|
+
# 画面を再描画
|
|
1173
|
+
refresh_display
|
|
1174
|
+
draw_screen
|
|
1175
|
+
end
|
|
683
1176
|
end
|
|
684
1177
|
end
|
|
685
1178
|
|
data/lib/rufio/version.rb
CHANGED
data/lib/rufio.rb
CHANGED
|
@@ -28,6 +28,11 @@ require_relative "rufio/plugin_manager"
|
|
|
28
28
|
require_relative "rufio/command_mode"
|
|
29
29
|
require_relative "rufio/command_mode_ui"
|
|
30
30
|
|
|
31
|
+
# プロジェクトモード
|
|
32
|
+
require_relative "rufio/project_mode"
|
|
33
|
+
require_relative "rufio/project_command"
|
|
34
|
+
require_relative "rufio/project_log"
|
|
35
|
+
|
|
31
36
|
module Rufio
|
|
32
37
|
class Error < StandardError; end
|
|
33
38
|
end
|