rufio 0.20.0 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b83ee6642477115c1a4590c5e0dd5194277eb5cc893e66fa8f59467a8768bec
4
- data.tar.gz: 674b4c809669a17fcfc94a068e13a1524977cda466b610a75cd94f85995cbfbb
3
+ metadata.gz: 2273fb7a1731d8d60e7a617d9055bdff0f7f5cd92f85eaa36d91dd266cf01773
4
+ data.tar.gz: e599d21775fd19631fca769972977f3a98771afae998b2523e16cf84d2b30d12
5
5
  SHA512:
6
- metadata.gz: 8e72520a40a2315b963d1c9a04f0071f35732696bbbf56d83a8d990025fb334fb6c834ec73bbe97b18fe27122c878771fc9710c33eda3293aa0dd0478946cd1e
7
- data.tar.gz: 26089800c3bb70e4b63d698ffdb8c87c5f4827229769bddef6e18c76cfa78984e9dbbacdf7bf14de2c201396c390b714b89ec74a170fda157ea5a7297c7b0293
6
+ metadata.gz: 7c66f9c83af723f5a993bfdec6e68b801320394e0912d94431986747e36f86cceea8afc0fa63fc345f2f1a045c1eb8753fb9e1cc238762a49e4f4d5f162f8cb4
7
+ data.tar.gz: 36c12657770e86c3f36054a735761db1e0207bbf70b2a89588b988f33fffc42ed2c00e00ace7595a7f9cc93ed95c3ad1097bbafb732db14d488727417b859345
@@ -0,0 +1,175 @@
1
+ # Changelog v0.21.0 - Copy Feature & Code Refactoring
2
+
3
+ ## [0.21.0] - 2025-12-28
4
+
5
+ ### 🎨 Enhanced - File Operations & Code Quality
6
+
7
+ Complete implementation of copy functionality with floating dialog UI, cross-directory selection improvements, and major code refactoring for better maintainability.
8
+
9
+ ### Added
10
+
11
+ #### Copy Feature
12
+ - **`c` key**: Copy selected files to current directory
13
+ - Uses floating confirmation dialog (green border)
14
+ - Shows source and destination paths
15
+ - Supports cross-directory operations
16
+ - Silent completion (no result dialog)
17
+ - **`SelectionManager#source_directory`**: Track directory where files were selected
18
+ - Enables selecting files in one directory and copying/moving to another
19
+ - Automatically set on first selection
20
+ - Cleared when all selections are removed
21
+ - **Copy confirmation dialog**: Green-bordered floating window
22
+ - Displays source and destination paths with smart truncation
23
+ - Consistent with move/delete confirmation UI
24
+ - `[Y]es` / `[N]o` / `ESC` to cancel
25
+
26
+ #### Multiple Selection Delete
27
+ - **Enhanced `x` key**: Delete multiple selected files
28
+ - Checks for selected items first
29
+ - Falls back to single file deletion if no selection
30
+ - Uses floating confirmation dialog showing source path
31
+ - Red border for warning (dangerous operation)
32
+
33
+ #### UI Improvements
34
+ - **Cross-directory selection display**: Selection marks only show in source directory
35
+ - `is_selected?` method checks current directory matches source directory
36
+ - Prevents confusion when navigating to different directories
37
+ - Selection internally maintained for operations
38
+
39
+ ### Changed
40
+
41
+ #### File Operations
42
+ - **Move operation** (`m` key): Uses source directory from SelectionManager
43
+ - Select files → navigate to destination → press `m` to move
44
+ - Blue-bordered confirmation dialog with paths
45
+ - Silent completion
46
+ - **Delete operation** (`x` key):
47
+ - Multiple selection support added
48
+ - Shows source path in confirmation
49
+ - Red-bordered warning dialog
50
+ - **All file operations** now use consistent floating dialog pattern:
51
+ - Red: Delete (dangerous)
52
+ - Blue: Move (informational)
53
+ - Green: Copy (safe)
54
+
55
+ #### Code Quality
56
+ - **terminal_ui.rb refactored**: Reduced from 1139 to 1048 lines (-91 lines)
57
+ - Removed duplicate text utility methods
58
+ - Uses `TextUtils` module for all text operations
59
+ - Better separation of concerns
60
+ - **TextUtils module enhanced**: Added `wrap_preview_lines` method
61
+ - Centralized text display width calculation
62
+ - Multi-byte character support (Japanese, etc.)
63
+ - Consistent truncation and wrapping logic
64
+ - **Method call updates**: All `display_width`, `truncate_to_width` calls use `TextUtils.` prefix
65
+ - Proper module method invocation
66
+ - Fixed runtime errors from refactoring
67
+
68
+ #### Keybind Handler
69
+ - **Removed duplicate method definition**: `show_delete_confirmation`
70
+ - Fixed warning about method redefinition
71
+ - Single source of truth for delete confirmation
72
+ - **Unified confirmation methods**:
73
+ - `show_move_confirmation(count, source_path, dest_path)`
74
+ - `show_copy_confirmation(count, source_path, dest_path)`
75
+ - `show_delete_confirmation(count, source_path)`
76
+
77
+ ### Technical Changes
78
+
79
+ #### SelectionManager Enhancement
80
+ - **`@source_directory` tracking**: Remember where files were selected
81
+ - **`toggle_selection` updated**: Accepts `current_directory` parameter
82
+ - Sets source directory on first selection
83
+ - Maintains source directory for subsequent selections
84
+ - Clears source when all items deselected
85
+ - **Cross-directory operation support**:
86
+ - Select in `/path/A`
87
+ - Navigate to `/path/B`
88
+ - Execute move/copy/delete from original location
89
+
90
+ #### TextUtils Module
91
+ - **New method**: `wrap_preview_lines(lines, max_width)`
92
+ - Intelligent line wrapping for preview pane
93
+ - Preserves empty lines
94
+ - Character-by-character width calculation
95
+ - **Improved display width calculation**: Handles full-width and half-width characters
96
+ - **Smart truncation**: Adds ellipsis when space permits
97
+
98
+ #### KeybindHandler Updates
99
+ - **`move_selected_to_current`**: Uses `@selection_manager.source_directory`
100
+ - **`copy_selected_to_current`**: New method following same pattern as move
101
+ - **`delete_current_file_with_confirmation`**: Checks for selected items first
102
+ - **`is_selected?(entry_name)`**: Validates current directory matches source
103
+ - **Path display helper**: `shorten_path(path, max_length)` for dialog display
104
+
105
+ ### Bug Fixes
106
+
107
+ - **Fixed undefined method error**: `display_width` in terminal_ui.rb
108
+ - Changed to `TextUtils.display_width`
109
+ - Fixed all text utility method calls
110
+ - **Fixed method redefinition warning**: Removed duplicate `show_delete_confirmation`
111
+ - **Fixed cross-directory selection display**: Selection marks only show in source directory
112
+ - Prevents showing same filename selected in different directories
113
+ - Internal selection state maintained correctly
114
+
115
+ ### Documentation
116
+
117
+ - **README.md updated**: Added copy operation documentation
118
+ - Updated File Operations table with Copy row
119
+ - Updated Operation Workflow with `c` key
120
+ - Added copy to File Operations section
121
+ - **README_EN.md updated**: Same updates for English documentation
122
+
123
+ ### Key Bindings Summary
124
+
125
+ #### File Selection & Operations
126
+ - `SPACE`: Select/deselect files and directories
127
+ - `m`: Move selected items to current directory (blue dialog)
128
+ - `c`: Copy selected items to current directory (green dialog)
129
+ - `x`: Delete selected items (red dialog)
130
+
131
+ #### Operation Workflow
132
+ ```
133
+ 1. SPACE → Select files/directories (multiple selection possible)
134
+ 2. Choose operation key:
135
+ - m → Move to current directory
136
+ - c → Copy to current directory
137
+ - x → Delete
138
+ 3. Floating Dialog → Confirm with Y/N, ESC to cancel
139
+ ```
140
+
141
+ #### Cross-Directory Operations
142
+ ```
143
+ 1. Navigate to source directory
144
+ 2. SPACE → Select files
145
+ 3. Navigate to destination directory (selection marks hidden)
146
+ 4. m/c → Move or copy from source to current
147
+ OR navigate back to source
148
+ 5. x → Delete selected files
149
+ ```
150
+
151
+ ### UI Color Scheme
152
+
153
+ - **Copy dialog**: Green border (`\e[32m`) - safe operation
154
+ - **Move dialog**: Blue border (`\e[34m`) - informational
155
+ - **Delete dialog**: Red border (`\e[31m`) - warning/danger
156
+ - **Selection mark**: `✓` with green background (`\e[42m\e[30m`)
157
+ - **Selection visibility**: Only in source directory
158
+
159
+ ### Code Metrics
160
+
161
+ - **Lines of code reduction**: terminal_ui.rb: 1139 → 1048 lines (-8%)
162
+ - **Test coverage**: 309 runs, 1407 assertions, 0 failures
163
+ - **Warnings fixed**: Method redefinition warning eliminated
164
+ - **Module organization**: Better separation with TextUtils module
165
+
166
+ ### Migration Notes
167
+
168
+ - **No breaking changes**: All existing functionality preserved
169
+ - **New feature**: `c` key for copy operation
170
+ - **Behavior improvement**: Selection display now directory-aware
171
+ - **Code quality**: Internal refactoring with no user-facing changes
172
+
173
+ ---
174
+
175
+ **Full Changelog**: [v0.20.0...v0.21.0](https://github.com/masisz/rufio/compare/v0.20.0...v0.21.0)
data/README.md CHANGED
@@ -16,7 +16,6 @@ rufioは、Yaziにインスパイアされたターミナル上で動作する
16
16
  - **コマンドモード**: Tab補完とフローティングウィンドウを備えた強力なコマンドシステム
17
17
  - **ファイルプレビュー**: テキストファイルの内容をその場で確認
18
18
  - **ファイル選択・操作**: 複数ファイルの選択、移動、コピー、削除が可能
19
- - **ベースディレクトリ操作**: 起動ディレクトリへの一括ファイル移動・コピー
20
19
  - **リアルタイムフィルター**: sキーでファイル名の絞り込み表示
21
20
  - **高度な検索機能**: fzfとrgaを使った強力な検索
22
21
  - **マルチプラットフォーム**: macOS、Linux、Windowsで動作
@@ -87,8 +86,8 @@ rufio --help # ヘルプメッセージを表示
87
86
  | キー | 機能 |
88
87
  | ------- | ---------------------------------------------------- |
89
88
  | `Space` | ファイル・ディレクトリを選択・選択解除(複数選択可) |
90
- | `m` | 選択したアイテムをベースディレクトリに移動 |
91
- | `p` | 選択したアイテムをベースディレクトリにコピー |
89
+ | `m` | 選択したアイテムをカレントディレクトリに移動 |
90
+ | `c` | 選択したアイテムをカレントディレクトリにコピー |
92
91
  | `x` | 選択したアイテムを削除 |
93
92
 
94
93
  #### リアルタイムフィルター機能
@@ -144,17 +143,14 @@ rufio --help # ヘルプメッセージを表示
144
143
  - **選択・選択解除**: `Space`キーでファイルやディレクトリを選択・選択解除
145
144
  - **複数選択**: 複数のファイルやディレクトリを同時に選択可能
146
145
  - **視覚的表示**: 選択されたアイテムは✓マークと緑色の背景で表示
147
- - **選択状態の表示**: 画面上部2行目に選択中のアイテム数が表示
148
146
 
149
- #### ベースディレクトリ操作
150
-
151
- rufio起動時のディレクトリが**ベースディレクトリ**として設定され、選択したファイルの移動・コピー先となります。
147
+ #### ファイル操作
152
148
 
153
- | 操作 | キー | 機能 |
154
- | -------- | ---- | ---------------------------------------- |
155
- | **移動** | `m` | 選択したアイテムをベースディレクトリに移動 |
156
- | **コピー** | `p` | 選択したアイテムをベースディレクトリにコピー |
157
- | **削除** | `x` | 選択したアイテムを削除 |
149
+ | 操作 | キー | 機能 |
150
+ | ---------- | ---- | ---------------------------------------- |
151
+ | **移動** | `m` | 選択したアイテムをカレントディレクトリに移動 |
152
+ | **コピー** | `c` | 選択したアイテムをカレントディレクトリにコピー |
153
+ | **削除** | `x` | 選択したアイテムを削除 |
158
154
 
159
155
  #### 削除操作の詳細
160
156
 
@@ -171,11 +167,11 @@ rufio起動時のディレクトリが**ベースディレクトリ**として
171
167
  ```
172
168
  1. Space → ファイル・ディレクトリを選択(複数選択可)
173
169
  2. 操作キーを選択:
174
- - m → ベースディレクトリに移動
175
- - pベースディレクトリにコピー
170
+ - m → カレントディレクトリに移動
171
+ - cカレントディレクトリにコピー
176
172
  - x → 削除
177
173
  3. フローティングダイアログ → Y/Nで確認、ESCでキャンセル
178
- 4. 結果表示フローティングウィンドウで操作結果を確認
174
+ 4. 操作完了選択解除、画面更新
179
175
  ```
180
176
 
181
177
  #### 安全機能
data/README_EN.md CHANGED
@@ -14,8 +14,7 @@ rufio is a terminal-based file manager inspired by Yazi. It's implemented in Rub
14
14
  - **Intuitive Operation**: Vim-like key bindings
15
15
  - **Plugin System**: Extensible plugin architecture
16
16
  - **File Preview**: View text file contents on the fly
17
- - **File Selection & Operations**: Select multiple files, move, copy, and delete
18
- - **Base Directory Operations**: Batch file operations to startup directory
17
+ - **File Selection & Operations**: Select multiple files, move, and delete
19
18
  - **Real-time Filter**: Filter files by name using s key
20
19
  - **Advanced Search**: Powerful search using fzf and rga
21
20
  - **Multi-platform**: Runs on macOS, Linux, and Windows
@@ -85,8 +84,8 @@ rufio --help # Show help message
85
84
  | Key | Function |
86
85
  | ------- | ------------------------------------------- |
87
86
  | `Space` | Select/deselect files and directories |
88
- | `m` | Move selected items to base directory |
89
- | `p` | Copy selected items to base directory |
87
+ | `m` | Move selected items to current directory |
88
+ | `c` | Copy selected items to current directory |
90
89
  | `x` | Delete selected items |
91
90
 
92
91
  #### Real-time Filter
@@ -132,16 +131,13 @@ rufio --help # Show help message
132
131
  - **Select/Deselect**: Use `Space` key to select or deselect files and directories
133
132
  - **Multiple Selection**: Select multiple files and directories simultaneously
134
133
  - **Visual Display**: Selected items are marked with ✓ and highlighted in green
135
- - **Selection Counter**: The number of selected items is shown on the second line of the screen
136
134
 
137
- #### Base Directory Operations
138
-
139
- The directory where rufio starts becomes the **base directory**, which serves as the destination for move and copy operations.
135
+ #### File Operations
140
136
 
141
137
  | Operation | Key | Function |
142
138
  | --------- | --- | --------------------------------------- |
143
- | **Move** | `m` | Move selected items to base directory |
144
- | **Copy** | `p` | Copy selected items to base directory |
139
+ | **Move** | `m` | Move selected items to current directory |
140
+ | **Copy** | `c` | Copy selected items to current directory |
145
141
  | **Delete** | `x` | Delete selected items |
146
142
 
147
143
  #### Delete Operation Details
@@ -159,8 +155,8 @@ The directory where rufio starts becomes the **base directory**, which serves as
159
155
  ```
160
156
  1. Space → Select files/directories (multiple selection possible)
161
157
  2. Choose operation key:
162
- - m → Move to base directory
163
- - p → Copy to base directory
158
+ - m → Move to current directory
159
+ - c → Copy to current directory
164
160
  - x → Delete
165
161
  3. Floating Dialog → Confirm with Y/N, ESC to cancel
166
162
  4. Result Display → Review operation results in floating window
@@ -15,7 +15,6 @@ module Rufio
15
15
  # 各コンポーネントを初期化
16
16
  directory_listing = DirectoryListing.new(@start_directory)
17
17
  keybind_handler = KeybindHandler.new
18
- keybind_handler.set_base_directory(@start_directory)
19
18
  file_preview = FilePreview.new
20
19
  terminal_ui = TerminalUI.new
21
20
 
@@ -51,9 +51,6 @@ module Rufio
51
51
  @project_log = ProjectLog.new(log_dir)
52
52
  @in_project_mode = false
53
53
  @in_log_mode = false
54
-
55
- # Legacy fields for backward compatibility
56
- @base_directory = nil
57
54
  end
58
55
 
59
56
  def set_directory_listing(directory_listing)
@@ -65,15 +62,18 @@ module Rufio
65
62
  @terminal_ui = terminal_ui
66
63
  end
67
64
 
68
- def set_base_directory(base_dir)
69
- @base_directory = File.expand_path(base_dir)
70
- end
71
-
72
65
  def selected_items
73
66
  @selection_manager.selected_items
74
67
  end
75
68
 
76
69
  def is_selected?(entry_name)
70
+ # Only show as selected if we're in the same directory where selection happened
71
+ current_path = @directory_listing&.current_path || Dir.pwd
72
+ source_path = @selection_manager.source_directory
73
+
74
+ # If no source directory or different directory, nothing is selected
75
+ return false if source_path.nil? || current_path != source_path
76
+
77
77
  @selection_manager.selected?(entry_name)
78
78
  end
79
79
 
@@ -141,10 +141,10 @@ module Rufio
141
141
  create_file
142
142
  when 'A' # A
143
143
  create_directory
144
- when 'm' # m - move selected files to base directory
145
- move_selected_to_base
146
- when 'C' # C - copy selected files to base directory
147
- copy_selected_to_base
144
+ when 'm' # m - move selected files to current directory
145
+ move_selected_to_current
146
+ when 'c' # c - copy selected files to current directory
147
+ copy_selected_to_current
148
148
  when 'x' # x - delete selected files
149
149
  delete_selected_files
150
150
  when 'p' # p - project mode
@@ -483,11 +483,29 @@ module Rufio
483
483
  end
484
484
 
485
485
  def delete_current_file_with_confirmation
486
+ current_path = @directory_listing&.current_path || Dir.pwd
487
+
488
+ # Check if there are selected items
489
+ if !@selection_manager.empty?
490
+ # Delete multiple selected items
491
+ source_path = @selection_manager.source_directory || current_path
492
+
493
+ if show_delete_confirmation(@selection_manager.count, source_path)
494
+ result = @file_operations.delete(@selection_manager.selected_items, source_path)
495
+ @selection_manager.clear
496
+ @directory_listing.refresh if @directory_listing
497
+ @terminal_ui&.refresh_display
498
+ return result.success
499
+ else
500
+ return false
501
+ end
502
+ end
503
+
504
+ # Single file deletion (current item)
486
505
  current_item = current_entry()
487
506
  return false unless current_item
488
507
 
489
508
  current_name = current_item[:name]
490
- current_path = @directory_listing&.current_path || Dir.pwd
491
509
  is_directory = current_item[:type] == :directory
492
510
 
493
511
  # 確認ダイアログを表示
@@ -554,19 +572,24 @@ module Rufio
554
572
  entry = current_entry
555
573
  return false unless entry
556
574
 
557
- @selection_manager.toggle_selection(entry)
575
+ current_path = @directory_listing&.current_path || Dir.pwd
576
+ @selection_manager.toggle_selection(entry, current_path)
558
577
  true
559
578
  end
560
579
 
561
- def move_selected_to_base
562
- return false if @selection_manager.empty? || @base_directory.nil?
563
580
 
564
- if show_confirmation_dialog('Move', @selection_manager.count)
565
- current_path = @directory_listing&.current_path || Dir.pwd
566
- result = @file_operations.move(@selection_manager.selected_items, current_path, @base_directory)
581
+ def move_selected_to_current
582
+ return false if @selection_manager.empty?
583
+
584
+ current_path = @directory_listing&.current_path || Dir.pwd
585
+ source_path = @selection_manager.source_directory || current_path
586
+
587
+ # Move selected files/directories from source directory to current directory
588
+ # This allows moving files even after navigating to a different directory
589
+ if show_move_confirmation(@selection_manager.count, source_path, current_path)
590
+ result = @file_operations.move(@selection_manager.selected_items, source_path, current_path)
567
591
 
568
- # Show result and refresh
569
- show_operation_result(result)
592
+ # Clear selection and refresh
570
593
  @selection_manager.clear
571
594
  @directory_listing.refresh if @directory_listing
572
595
  true
@@ -575,15 +598,18 @@ module Rufio
575
598
  end
576
599
  end
577
600
 
578
- def copy_selected_to_base
579
- return false if @selection_manager.empty? || @base_directory.nil?
601
+ def copy_selected_to_current
602
+ return false if @selection_manager.empty?
580
603
 
581
- if show_confirmation_dialog('Copy', @selection_manager.count)
582
- current_path = @directory_listing&.current_path || Dir.pwd
583
- result = @file_operations.copy(@selection_manager.selected_items, current_path, @base_directory)
604
+ current_path = @directory_listing&.current_path || Dir.pwd
605
+ source_path = @selection_manager.source_directory || current_path
606
+
607
+ # Copy selected files/directories from source directory to current directory
608
+ # This allows copying files even after navigating to a different directory
609
+ if show_copy_confirmation(@selection_manager.count, source_path, current_path)
610
+ result = @file_operations.copy(@selection_manager.selected_items, source_path, current_path)
584
611
 
585
- # Show result and refresh
586
- show_operation_result(result)
612
+ # Clear selection and refresh
587
613
  @selection_manager.clear
588
614
  @directory_listing.refresh if @directory_listing
589
615
  true
@@ -627,17 +653,31 @@ module Rufio
627
653
  end
628
654
  end
629
655
 
630
- def show_delete_confirmation(count)
631
- show_floating_delete_confirmation(count)
656
+ def show_move_confirmation(count, source_path, dest_path)
657
+ show_floating_move_confirmation(count, source_path, dest_path)
658
+ end
659
+
660
+ def show_copy_confirmation(count, source_path, dest_path)
661
+ show_floating_copy_confirmation(count, source_path, dest_path)
632
662
  end
633
663
 
634
- def show_floating_delete_confirmation(count)
664
+ def show_delete_confirmation(count, source_path)
665
+ show_floating_delete_confirmation(count, source_path)
666
+ end
667
+
668
+ def show_floating_delete_confirmation(count, source_path)
635
669
  # コンテンツの準備
636
670
  title = 'Delete Confirmation'
671
+
672
+ # パスの短縮表示
673
+ source_display = shorten_path(source_path, 35)
674
+
637
675
  content_lines = [
638
676
  '',
639
677
  "Delete #{count} item(s)?",
640
678
  '',
679
+ "From: #{source_display}",
680
+ '',
641
681
  ' [Y]es - Delete',
642
682
  ' [N]o - Cancel',
643
683
  ''
@@ -685,6 +725,128 @@ module Rufio
685
725
  end
686
726
  end
687
727
 
728
+ def show_floating_move_confirmation(count, source_path, dest_path)
729
+ # コンテンツの準備
730
+ title = 'Move Confirmation'
731
+
732
+ # パスの短縮表示
733
+ source_display = shorten_path(source_path, 35)
734
+ dest_display = shorten_path(dest_path, 35)
735
+
736
+ content_lines = [
737
+ '',
738
+ "Move #{count} item(s)?",
739
+ '',
740
+ "From: #{source_display}",
741
+ "To: #{dest_display}",
742
+ '',
743
+ ' [Y]es - Move',
744
+ ' [N]o - Cancel',
745
+ ''
746
+ ]
747
+
748
+ # ダイアログのサイズ設定
749
+ dialog_width = CONFIRMATION_DIALOG_WIDTH
750
+ dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
751
+
752
+ # ダイアログの位置を中央に設定
753
+ x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
754
+
755
+ # ダイアログの描画(移動は青色で表示)
756
+ @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
757
+ border_color: "\e[34m", # 青色(情報)
758
+ title_color: "\e[1;34m", # 太字青色
759
+ content_color: "\e[37m" # 白色
760
+ })
761
+
762
+ # キー入力待機
763
+ loop do
764
+ input = STDIN.getch.downcase
765
+
766
+ case input
767
+ when 'y'
768
+ # ダイアログをクリア
769
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
770
+ @terminal_ui&.refresh_display # 画面を再描画
771
+ return true
772
+ when 'n', "\e", "\x03" # n, ESC, Ctrl+C
773
+ # ダイアログをクリア
774
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
775
+ @terminal_ui&.refresh_display # 画面を再描画
776
+ return false
777
+ when 'q' # qキーでもキャンセル
778
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
779
+ @terminal_ui&.refresh_display
780
+ return false
781
+ end
782
+ # 無効なキー入力の場合は再度ループ
783
+ end
784
+ end
785
+
786
+ def show_floating_copy_confirmation(count, source_path, dest_path)
787
+ # コンテンツの準備
788
+ title = 'Copy Confirmation'
789
+
790
+ # パスの短縮表示
791
+ source_display = shorten_path(source_path, 35)
792
+ dest_display = shorten_path(dest_path, 35)
793
+
794
+ content_lines = [
795
+ '',
796
+ "Copy #{count} item(s)?",
797
+ '',
798
+ "From: #{source_display}",
799
+ "To: #{dest_display}",
800
+ '',
801
+ ' [Y]es - Copy',
802
+ ' [N]o - Cancel',
803
+ ''
804
+ ]
805
+
806
+ # ダイアログのサイズ設定
807
+ dialog_width = CONFIRMATION_DIALOG_WIDTH
808
+ dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
809
+
810
+ # ダイアログの位置を中央に設定
811
+ x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
812
+
813
+ # ダイアログの描画(コピーは緑色で表示)
814
+ @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
815
+ border_color: "\e[32m", # 緑色(安全な操作)
816
+ title_color: "\e[1;32m", # 太字緑色
817
+ content_color: "\e[37m" # 白色
818
+ })
819
+
820
+ # キー入力待機
821
+ loop do
822
+ input = STDIN.getch.downcase
823
+
824
+ case input
825
+ when 'y'
826
+ # ダイアログをクリア
827
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
828
+ @terminal_ui&.refresh_display # 画面を再描画
829
+ return true
830
+ when 'n', "\e", "\x03" # n, ESC, Ctrl+C
831
+ # ダイアログをクリア
832
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
833
+ @terminal_ui&.refresh_display # 画面を再描画
834
+ return false
835
+ when 'q' # qキーでもキャンセル
836
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
837
+ @terminal_ui&.refresh_display
838
+ return false
839
+ end
840
+ # 無効なキー入力の場合は再度ループ
841
+ end
842
+ end
843
+
844
+ # パスを指定した長さに短縮
845
+ def shorten_path(path, max_length)
846
+ return path if path.length <= max_length
847
+ "...#{path[-(max_length - 3)..-1]}"
848
+ end
849
+
688
850
  def perform_delete_operation(items)
689
851
  Logger.debug('Starting delete operation', context: { items: items, count: items.length })
690
852
 
@@ -3,20 +3,28 @@
3
3
  module Rufio
4
4
  # Manages selected items (files/directories) for bulk operations
5
5
  class SelectionManager
6
+ attr_reader :source_directory
7
+
6
8
  def initialize
7
9
  @selected_items = []
10
+ @source_directory = nil
8
11
  end
9
12
 
10
13
  # Toggle selection for an entry
11
14
  # @param entry [Hash] Entry with :name key
15
+ # @param current_directory [String, nil] Current directory path (optional)
12
16
  # @return [Boolean] true if now selected, false if unselected
13
- def toggle_selection(entry)
17
+ def toggle_selection(entry, current_directory = nil)
14
18
  return false unless entry
15
19
 
16
20
  if @selected_items.include?(entry[:name])
17
21
  @selected_items.delete(entry[:name])
22
+ # Clear source_directory if no items are selected
23
+ @source_directory = nil if @selected_items.empty?
18
24
  false
19
25
  else
26
+ # Set source directory on first selection
27
+ @source_directory = current_directory if @selected_items.empty? && current_directory
20
28
  @selected_items << entry[:name]
21
29
  true
22
30
  end
@@ -38,6 +46,7 @@ module Rufio
38
46
  # Clear all selections
39
47
  def clear
40
48
  @selected_items.clear
49
+ @source_directory = nil
41
50
  end
42
51
 
43
52
  # Check if any items are selected
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'io/console'
4
+ require_relative 'text_utils'
4
5
 
5
6
  module Rufio
6
7
  class TerminalUI
7
8
  # Layout constants
8
- HEADER_HEIGHT = 2 # Header占有行数
9
+ HEADER_HEIGHT = 1 # Header占有行数
9
10
  FOOTER_HEIGHT = 1 # Footer占有行数
10
- HEADER_FOOTER_MARGIN = 4 # Header + Footer分のマージン
11
+ HEADER_FOOTER_MARGIN = 3 # Header + Footer分のマージン
11
12
 
12
13
  # Panel layout ratios
13
14
  LEFT_PANEL_RATIO = 0.5 # 左パネルの幅比率
@@ -17,8 +18,6 @@ module Rufio
17
18
  DEFAULT_SCREEN_WIDTH = 80 # デフォルト画面幅
18
19
  DEFAULT_SCREEN_HEIGHT = 24 # デフォルト画面高さ
19
20
  HEADER_PADDING = 2 # ヘッダーのパディング
20
- BASE_INFO_RESERVED_WIDTH = 20 # ベースディレクトリ表示の予約幅
21
- BASE_INFO_MIN_WIDTH = 10 # ベースディレクトリ表示の最小幅
22
21
  FILTER_TEXT_RESERVED = 15 # フィルタテキスト表示の予約幅
23
22
 
24
23
  # File display constants
@@ -31,7 +30,7 @@ module Rufio
31
30
  GIGABYTE = MEGABYTE * 1024
32
31
 
33
32
  # Line offsets
34
- CONTENT_START_LINE = 3 # コンテンツ開始行(ヘッダー2行スキップ)
33
+ CONTENT_START_LINE = 2 # コンテンツ開始行(ヘッダー1行スキップ)
35
34
 
36
35
  def initialize
37
36
  console = IO.console
@@ -125,9 +124,8 @@ module Rufio
125
124
  return
126
125
  end
127
126
 
128
- # header (2 lines)
127
+ # header (1 line)
129
128
  draw_header
130
- draw_base_directory_info
131
129
 
132
130
  # main content (left: directory list, right: preview)
133
131
  entries = get_display_entries
@@ -184,42 +182,6 @@ module Rufio
184
182
  puts "\e[7m#{header.ljust(@screen_width)}\e[0m" # reverse display
185
183
  end
186
184
 
187
- def draw_base_directory_info
188
- # 強制的に表示 - デバッグ用に安全チェックを緩和
189
- if @keybind_handler && @keybind_handler.instance_variable_get(:@base_directory)
190
- base_dir = @keybind_handler.instance_variable_get(:@base_directory)
191
- selected_count = @keybind_handler.selected_items.length
192
- base_info = "📋 Base Directory: #{base_dir}"
193
-
194
- # 選択されたアイテム数を表示
195
- if selected_count > 0
196
- base_info += " | Selected: #{selected_count} item(s)"
197
- end
198
- else
199
- # keybind_handlerがない場合、またはbase_directoryが設定されていない場合
200
- base_info = "📋 Base Directory: #{Dir.pwd}"
201
- end
202
-
203
- # 長すぎる場合は省略
204
- if base_info.length > @screen_width - HEADER_PADDING
205
- if base_info.include?(" | Selected:")
206
- selected_part = base_info.split(" | Selected:").last
207
- available_length = @screen_width - BASE_INFO_RESERVED_WIDTH - " | Selected:#{selected_part}".length
208
- else
209
- available_length = @screen_width - BASE_INFO_RESERVED_WIDTH
210
- end
211
-
212
- if available_length > BASE_INFO_MIN_WIDTH
213
- # パスの最後の部分を表示
214
- dir_part = base_info.split(": ").last.split(" | ").first
215
- short_base_dir = "...#{dir_part[-available_length..-1]}"
216
- base_info = base_info.gsub(dir_part, short_base_dir)
217
- end
218
- end
219
-
220
- # 2行目に確実に表示
221
- print "\e[2;1H\e[44m\e[37m#{base_info.ljust(@screen_width)}\e[0m"
222
- end
223
185
 
224
186
 
225
187
  def draw_directory_list(entries, width, height)
@@ -352,7 +314,7 @@ module Rufio
352
314
  elsif selected_entry && selected_entry[:type] == 'file' && i >= 2
353
315
  # ファイルプレビュー(折り返し対応)
354
316
  preview_content = get_preview_content(selected_entry)
355
- wrapped_lines = wrap_preview_lines(preview_content, safe_width - 1) # スペース分を除く
317
+ wrapped_lines = TextUtils.wrap_preview_lines(preview_content, safe_width - 1) # スペース分を除く
356
318
  display_line_index = i - 2
357
319
 
358
320
  if display_line_index < wrapped_lines.length
@@ -370,16 +332,16 @@ module Rufio
370
332
  if safe_width <= 0
371
333
  # 表示スペースがない場合は何も出力しない
372
334
  next
373
- elsif display_width(content_to_print) > safe_width
335
+ elsif TextUtils.display_width(content_to_print) > safe_width
374
336
  # 表示幅ベースで切り詰める
375
- content_to_print = truncate_to_width(content_to_print, safe_width)
337
+ content_to_print = TextUtils.truncate_to_width(content_to_print, safe_width)
376
338
  end
377
339
 
378
340
  # 出力(パディングなし、はみ出し防止のため)
379
341
  print content_to_print
380
342
 
381
343
  # 残りのスペースを埋める(ただし安全な範囲内のみ)
382
- remaining_space = safe_width - display_width(content_to_print)
344
+ remaining_space = safe_width - TextUtils.display_width(content_to_print)
383
345
  print ' ' * remaining_space if remaining_space > 0
384
346
  end
385
347
  end
@@ -402,98 +364,6 @@ module Rufio
402
364
  ["(#{ConfigLoader.message('file.preview_error')})"]
403
365
  end
404
366
 
405
- def wrap_preview_lines(lines, max_width)
406
- return [] if lines.empty? || max_width <= 0
407
-
408
- wrapped_lines = []
409
-
410
- lines.each do |line|
411
- if display_width(line) <= max_width
412
- # 短い行はそのまま追加
413
- wrapped_lines << line
414
- else
415
- # 長い行は折り返し
416
- remaining_line = line
417
- while display_width(remaining_line) > max_width
418
- # 単語境界で折り返すことを試みる
419
- break_point = find_break_point(remaining_line, max_width)
420
- wrapped_lines << remaining_line[0...break_point]
421
- remaining_line = remaining_line[break_point..-1]
422
- end
423
- # 残りの部分を追加
424
- wrapped_lines << remaining_line if remaining_line.length > 0
425
- end
426
- end
427
-
428
- wrapped_lines
429
- end
430
-
431
- def display_width(string)
432
- # 文字列の表示幅を計算する
433
- # 日本語文字(全角)は幅2、ASCII文字(半角)は幅1として計算
434
- width = 0
435
- string.each_char do |char|
436
- # 全角文字の判定
437
- width += if char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/)
438
- 2
439
- else
440
- 1
441
- end
442
- end
443
- width
444
- end
445
-
446
- def truncate_to_width(string, max_width)
447
- # 表示幅を指定して文字列を切り詰める
448
- return string if display_width(string) <= max_width
449
-
450
- current_width = 0
451
- result = ''
452
-
453
- string.each_char do |char|
454
- char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
455
-
456
- if current_width + char_width > max_width
457
- # "..."を追加できるかチェック
458
- result += '...' if max_width >= 3 && current_width <= max_width - 3
459
- break
460
- end
461
-
462
- result += char
463
- current_width += char_width
464
- end
465
-
466
- result
467
- end
468
-
469
- def find_break_point(line, max_width)
470
- # 最大幅以内で適切な折り返し位置を見つける
471
- return line.length if display_width(line) <= max_width
472
-
473
- # 文字ごとに幅を計算しながら適切な位置を探す
474
- current_width = 0
475
- best_break_point = 0
476
- space_break_point = nil
477
- punct_break_point = nil
478
-
479
- line.each_char.with_index do |char, index|
480
- char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
481
-
482
- break if current_width + char_width > max_width
483
-
484
- current_width += char_width
485
- best_break_point = index + 1
486
-
487
- # スペースで区切れる位置を記録
488
- space_break_point = index + 1 if char == ' ' && current_width > max_width * 0.5
489
-
490
- # 日本語の句読点で区切れる位置を記録
491
- punct_break_point = index + 1 if char.match?(/[、。,.!?]/) && current_width > max_width * 0.5
492
- end
493
-
494
- # 最適な折り返し位置を選択
495
- space_break_point || punct_break_point || best_break_point
496
- end
497
367
 
498
368
  def get_display_entries
499
369
  if @keybind_handler.filter_active?
@@ -104,5 +104,54 @@ module Rufio
104
104
 
105
105
  space_break_point || punct_break_point || best_break_point
106
106
  end
107
+
108
+ # Wrap preview lines to fit within max_width
109
+ # @param lines [Array<String>] Lines to wrap
110
+ # @param max_width [Integer] Maximum width for each line
111
+ # @return [Array<String>] Wrapped lines
112
+ def wrap_preview_lines(lines, max_width)
113
+ return lines if max_width <= 0
114
+
115
+ wrapped = []
116
+ lines.each do |line|
117
+ # Remove trailing whitespace
118
+ line = line.rstrip
119
+
120
+ # If line is empty, keep it
121
+ if line.empty?
122
+ wrapped << ''
123
+ next
124
+ end
125
+
126
+ # If line fits within max_width, keep it as is
127
+ if display_width(line) <= max_width
128
+ wrapped << line
129
+ next
130
+ end
131
+
132
+ # Split long lines
133
+ current_line = []
134
+ current_width = 0
135
+
136
+ line.each_char do |char|
137
+ char_width = display_width(char)
138
+
139
+ if current_width + char_width > max_width
140
+ # Start a new line
141
+ wrapped << current_line.join
142
+ current_line = [char]
143
+ current_width = char_width
144
+ else
145
+ current_line << char
146
+ current_width += char_width
147
+ end
148
+ end
149
+
150
+ # Add remaining characters
151
+ wrapped << current_line.join unless current_line.empty?
152
+ end
153
+
154
+ wrapped
155
+ end
107
156
  end
108
157
  end
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.20.0'
4
+ VERSION = '0.21.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
@@ -119,6 +119,7 @@ files:
119
119
  - CHANGELOG.md
120
120
  - CHANGELOG_v0.10.0.md
121
121
  - CHANGELOG_v0.20.0.md
122
+ - CHANGELOG_v0.21.0.md
122
123
  - CHANGELOG_v0.4.0.md
123
124
  - CHANGELOG_v0.5.0.md
124
125
  - CHANGELOG_v0.6.0.md