Fortran · 61911 bytes Raw Blame History
1 program fortress
2 use iso_fortran_env, only: output_unit
3 use terminal_control
4 use filesystem_ops
5 use git_ops
6 use ui_display
7 implicit none
8
9 ! State variables
10 character(len=MAX_PATH) :: current_dir, parent_dir, temp_dir, exit_dir
11 character(len=MAX_PATH), dimension(MAX_FILES) :: current_files, parent_files
12 logical, dimension(MAX_FILES) :: current_is_dir, parent_is_dir
13 logical, dimension(MAX_FILES) :: current_is_exec, parent_is_exec
14 logical, dimension(MAX_FILES) :: current_is_staged, current_is_unstaged, current_is_untracked
15 logical, dimension(MAX_FILES) :: parent_is_staged, parent_is_unstaged, parent_is_untracked
16 logical, dimension(MAX_FILES) :: current_has_incoming
17 integer :: current_count, parent_count
18 integer :: selected = 1, parent_selected = -1
19 integer :: scroll_offset = 0, parent_scroll_offset = 0
20 character(len=256) :: repo_name, branch_name
21 logical :: in_git_repo = .false., running = .true., cd_on_exit = .false.
22 logical :: show_dotfiles = .true.
23
24 ! Move mode state
25 logical :: move_mode = .false.
26 character(len=MAX_PATH) :: move_source_path
27 character(len=MAX_PATH) :: move_source_name
28 integer :: move_dest_selected = 1
29
30 ! Clipboard state
31 logical :: has_clipboard = .false.
32 logical :: clipboard_is_cut = .false. ! true = cut, false = copy
33 character(len=MAX_PATH) :: clipboard_source_path
34 character(len=MAX_PATH) :: clipboard_source_name
35
36 ! Multi-select state
37 logical, dimension(MAX_FILES) :: is_selected
38 integer :: selection_count = 0
39 logical :: in_selection_mode = .false.
40 integer :: selection_anchor = -1 ! For shift+arrow block selection
41 logical :: has_disjoint_selection = .false. ! True if non-contiguous items selected
42
43 ! Multi-clipboard for batch operations
44 character(len=MAX_PATH), dimension(MAX_FILES) :: clipboard_paths
45 character(len=MAX_PATH), dimension(MAX_FILES) :: clipboard_names
46 integer :: clipboard_count = 0
47
48 ! Favorites/bookmarks state
49 character(len=MAX_PATH), dimension(10) :: favorite_dirs
50 integer :: favorite_count = 0
51 logical, dimension(MAX_FILES) :: current_is_favorite, parent_is_favorite
52
53 character(len=1) :: key
54 integer :: i, rows, cols, visible_height
55 logical :: is_shift_pressed
56
57 ! Initialize
58 current_dir = get_pwd()
59 parent_dir = get_parent_path(current_dir)
60 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
61 call load_favorites(favorite_dirs, favorite_count)
62 call setup_raw_mode()
63
64 ! Main loop
65 do while (running)
66 ! Get files
67 call get_file_list(current_dir, current_files, current_is_dir, current_is_exec, current_count)
68 call get_file_list(parent_dir, parent_files, parent_is_dir, parent_is_exec, parent_count)
69
70 ! Filter dotfiles if needed
71 if (.not. show_dotfiles) then
72 call filter_dotfiles(current_files, current_is_dir, current_is_exec, current_count)
73 call filter_dotfiles(parent_files, parent_is_dir, parent_is_exec, parent_count)
74 end if
75
76 ! Initialize git arrays and selection - only for actual file counts
77 do i = 1, current_count
78 current_is_staged(i) = .false.
79 current_is_unstaged(i) = .false.
80 current_is_untracked(i) = .false.
81 current_has_incoming(i) = .false.
82 ! Keep selections if still in same directory, clear otherwise
83 if (i > MAX_FILES) then
84 is_selected(i) = .false.
85 end if
86 end do
87 do i = 1, parent_count
88 parent_is_staged(i) = .false.
89 parent_is_unstaged(i) = .false.
90 parent_is_untracked(i) = .false.
91 end do
92
93 ! Get git status if in a repo
94 if (in_git_repo) then
95 call get_git_status(current_dir, current_files, current_is_dir, current_count, &
96 current_is_staged, current_is_unstaged, current_is_untracked)
97 call mark_incoming_changes(current_dir, current_files, current_count, current_has_incoming)
98 end if
99
100 ! Mark favorited directories
101 call mark_favorites_in_lists(current_dir, current_files, current_count, current_is_dir, &
102 favorite_dirs, favorite_count, current_is_favorite)
103 call mark_favorites_in_lists(parent_dir, parent_files, parent_count, parent_is_dir, &
104 favorite_dirs, favorite_count, parent_is_favorite)
105
106 ! Get terminal size
107 call get_term_size(rows, cols)
108 visible_height = rows - 6 ! Account for 2 pre-spacing + header + 2 post-spacing + footer
109
110 ! Handle navigation signals from previous iteration
111 if (selected == -1) then
112 selected = find_in_parent(temp_dir, current_files, current_count)
113 scroll_offset = max(0, selected - visible_height / 2)
114 else if (selected == -2) then
115 selected = find_file_in_list(temp_dir, current_files, current_count)
116 scroll_offset = max(0, selected - visible_height / 2)
117 end if
118
119 ! Handle move mode destination cursor
120 if (move_mode .and. move_dest_selected == -1) then
121 move_dest_selected = find_in_parent(temp_dir, current_files, current_count)
122 end if
123
124 ! Bounds check
125 if (current_count > 0) then
126 selected = max(1, min(selected, current_count))
127 if (move_mode) then
128 move_dest_selected = max(1, min(move_dest_selected, current_count))
129 end if
130 else
131 selected = 1
132 if (move_mode) move_dest_selected = 1
133 end if
134
135 ! Find current dir in parent
136 parent_selected = find_in_parent(current_dir, parent_files, parent_count)
137
138 ! Adjust scroll to keep cursor visible
139 if (move_mode) then
140 ! In move mode, track the destination cursor
141 if (move_dest_selected < scroll_offset + 1) scroll_offset = max(0, move_dest_selected - 1)
142 if (move_dest_selected > scroll_offset + visible_height) scroll_offset = move_dest_selected - visible_height
143 scroll_offset = max(0, min(scroll_offset, max(0, current_count - visible_height)))
144 else
145 ! Normal mode, track the selection cursor
146 if (selected < scroll_offset + 1) scroll_offset = max(0, selected - 1)
147 if (selected > scroll_offset + visible_height) scroll_offset = selected - visible_height
148 scroll_offset = max(0, min(scroll_offset, max(0, current_count - visible_height)))
149 end if
150
151 if (parent_selected > 0) then
152 if (parent_selected < parent_scroll_offset + 1) parent_scroll_offset = max(0, parent_selected - 1)
153 if (parent_selected > parent_scroll_offset + visible_height) parent_scroll_offset = parent_selected - visible_height
154 parent_scroll_offset = max(0, min(parent_scroll_offset, max(0, parent_count - visible_height)))
155 end if
156
157 ! Draw
158 write(output_unit, '(a)', advance='no') CLEAR
159 call draw_interface(rows, cols, current_dir, current_files, current_is_dir, current_is_exec, &
160 current_is_staged, current_is_unstaged, current_is_untracked, current_has_incoming, &
161 current_count, parent_files, parent_is_dir, parent_is_exec, parent_count, &
162 selected, parent_selected, scroll_offset, parent_scroll_offset, &
163 in_git_repo, repo_name, branch_name, &
164 move_mode, move_source_name, move_dest_selected, &
165 has_clipboard, clipboard_is_cut, clipboard_source_name, clipboard_count, &
166 is_selected, selection_count, &
167 current_is_favorite, parent_is_favorite)
168
169 ! Get input (with error handling for End-of-record after Enter key)
170 read(*, '(a1)', advance='no', iostat=i) key
171 ! Only cycle on End-of-record (negative iostat), which happens after pressing Enter
172 ! Don't skip on positive errors or when we successfully read a character
173 if (i < 0) cycle ! End-of-record - skip and try again
174 if (i > 0) cycle ! Other read errors - skip and try again
175
176 ! Handle input
177 select case(ichar(key))
178 case(27) ! ESC - arrow keys or Shift+arrow keys
179 call read_arrow_key_with_shift(key, is_shift_pressed)
180
181 if (move_mode) then
182 ! In move mode, navigate directories only
183 select case(key)
184 case('A') ! Up - jump to previous directory
185 move_dest_selected = find_prev_directory(current_files, current_is_dir, current_count, move_dest_selected)
186 case('B') ! Down - jump to next directory
187 move_dest_selected = find_next_directory(current_files, current_is_dir, current_count, move_dest_selected)
188 case('C') ! Right - enter directory
189 if (current_is_dir(move_dest_selected)) then
190 if (trim(current_files(move_dest_selected)) == "..") then
191 ! Don't descend into ..
192 else if (trim(current_files(move_dest_selected)) /= ".") then
193 ! Descend into directory
194 parent_dir = current_dir
195 current_dir = join_path(current_dir, current_files(move_dest_selected))
196 move_dest_selected = find_first_directory(current_files, current_is_dir, current_count)
197 scroll_offset = 0
198 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
199 end if
200 end if
201 case('D') ! Left - go to parent
202 if (current_dir /= "/") then
203 temp_dir = current_dir
204 current_dir = parent_dir
205 parent_dir = get_parent_path(current_dir)
206 move_dest_selected = -1 ! Will be set to parent dir position
207 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
208 end if
209 end select
210 else
211 ! Normal navigation
212 select case(key)
213 case('A') ! Up
214 if (is_shift_pressed .and. .not. has_disjoint_selection) then
215 ! Shift+Up: Start or extend block selection upward
216 if (selection_anchor == -1) then
217 ! Start new block selection
218 selection_anchor = selected
219 call clear_all_selections(is_selected, selection_count, in_selection_mode)
220 end if
221 if (selected > 1) selected = selected - 1
222 ! Select range from anchor to current
223 call select_range(is_selected, selection_count, selection_anchor, selected, &
224 current_files, current_count)
225 else
226 ! Normal up movement - clear anchor
227 if (selected > 1) selected = selected - 1
228 selection_anchor = -1
229 end if
230 case('B') ! Down
231 if (is_shift_pressed .and. .not. has_disjoint_selection) then
232 ! Shift+Down: Start or extend block selection downward
233 if (selection_anchor == -1) then
234 ! Start new block selection
235 selection_anchor = selected
236 call clear_all_selections(is_selected, selection_count, in_selection_mode)
237 end if
238 if (selected < current_count .and. current_count > 0) selected = selected + 1
239 ! Select range from anchor to current
240 call select_range(is_selected, selection_count, selection_anchor, selected, &
241 current_files, current_count)
242 else
243 ! Normal down movement - clear anchor
244 if (selected < current_count .and. current_count > 0) selected = selected + 1
245 selection_anchor = -1
246 end if
247 case('C') ! Right - enter directory
248 if (current_is_dir(selected)) then
249 if (trim(current_files(selected)) == "..") then
250 temp_dir = current_dir
251 current_dir = parent_dir
252 parent_dir = get_parent_path(current_dir)
253 selected = -1
254 selection_anchor = -1
255 call clear_all_selections(is_selected, selection_count, in_selection_mode)
256 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
257 else if (trim(current_files(selected)) /= ".") then
258 parent_dir = current_dir
259 current_dir = join_path(current_dir, current_files(selected))
260 selected = 1
261 scroll_offset = 0
262 selection_anchor = -1
263 call clear_all_selections(is_selected, selection_count, in_selection_mode)
264 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
265 end if
266 end if
267 case('D') ! Left - go back
268 if (current_dir /= "/") then
269 temp_dir = current_dir
270 current_dir = parent_dir
271 parent_dir = get_parent_path(current_dir)
272 selected = -1
273 selection_anchor = -1
274 call clear_all_selections(is_selected, selection_count, in_selection_mode)
275 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
276 end if
277 end select
278 end if
279 case(113, 81) ! 'q' or 'Q' - exit move mode or quit
280 if (move_mode) then
281 move_mode = .false.
282 else
283 running = .false.
284 end if
285 case(99, 67) ! 'c' or 'C' - cd to directory on exit
286 if (current_is_dir(selected)) then
287 if (trim(current_files(selected)) == "..") then
288 exit_dir = parent_dir
289 else if (trim(current_files(selected)) == ".") then
290 exit_dir = current_dir
291 else
292 exit_dir = join_path(current_dir, current_files(selected))
293 end if
294 cd_on_exit = .true.
295 running = .false.
296 end if
297 case(83, 115) ! 'S' or 's' - fzf search (moved from 'f')
298 call fzf_search(current_dir, temp_dir)
299 if (len_trim(temp_dir) > 0) then
300 parent_dir = get_parent_path(temp_dir)
301 current_dir = parent_dir
302 parent_dir = get_parent_path(current_dir)
303 selected = -2
304 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
305 end if
306 case(65, 97) ! 'A' or 'a' - git add (batch stage directories)
307 if (in_git_repo) then
308 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
309 call git_add_file(current_dir, current_files(selected))
310 end if
311 end if
312 case(85, 117) ! 'U' or 'u' - git unstage (batch unstage directories)
313 if (in_git_repo) then
314 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
315 if (current_is_staged(selected)) then
316 call git_unstage_file(current_dir, current_files(selected))
317 end if
318 end if
319 end if
320 case(77, 109) ! 'M' or 'm' - git commit
321 if (in_git_repo) then
322 call git_commit_prompt(current_dir, repo_name)
323 end if
324 case(72, 104) ! 'H' or 'h' - git push (h for "push to remote Host")
325 if (in_git_repo) then
326 call git_push_prompt(current_dir, repo_name)
327 end if
328 case(84, 116) ! 'T' or 't' - git tag
329 if (in_git_repo) then
330 call git_tag_prompt(current_dir, repo_name)
331 end if
332 case(70, 102) ! 'F' or 'f' - git fetch
333 if (in_git_repo) then
334 call git_fetch_prompt(current_dir, repo_name)
335 end if
336 case(76, 108) ! 'L' or 'l' - git pull
337 if (in_git_repo) then
338 call git_pull_prompt(current_dir, repo_name)
339 end if
340 case(79, 111) ! 'O' or 'o' - open file
341 if (.not. current_is_dir(selected)) then
342 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
343 call open_file_in_default_app(join_path(current_dir, current_files(selected)))
344 end if
345 end if
346 case(78, 110) ! 'N' or 'n' - rename file/directory
347 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
348 call rename_file_prompt(current_dir, current_files(selected))
349 end if
350 case(68, 100) ! 'D' or 'd' - show git diff
351 if (in_git_repo .and. .not. current_is_dir(selected)) then
352 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
353 if (current_is_staged(selected) .or. current_is_unstaged(selected)) then
354 call show_git_diff_fullscreen(current_dir, current_files(selected), &
355 current_is_staged(selected), current_is_unstaged(selected))
356 end if
357 end if
358 end if
359 case(82, 114) ! 'R' or 'r' - delete/remove with confirmation
360 if (selection_count > 0) then
361 ! Delete multiple selections
362 call delete_multi_with_confirmation(current_dir, current_files, current_is_dir, &
363 is_selected, selection_count, current_count)
364 ! Clear selections after delete
365 call clear_all_selections(is_selected, selection_count, in_selection_mode)
366 else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
367 call delete_with_confirmation(current_dir, current_files(selected), current_is_dir(selected))
368 end if
369 case(46) ! '.' - toggle dotfiles visibility
370 show_dotfiles = .not. show_dotfiles
371 ! Reset selection to avoid going out of bounds
372 selected = 1
373 scroll_offset = 0
374 case(126) ! '~' - go to home directory
375 call get_environment_variable("HOME", temp_dir)
376 if (len_trim(temp_dir) > 0) then
377 current_dir = temp_dir
378 parent_dir = get_parent_path(current_dir)
379 selected = 1
380 scroll_offset = 0
381 selection_anchor = -1
382 call clear_all_selections(is_selected, selection_count, in_selection_mode)
383 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
384 end if
385 case(47) ! '/' - go to root directory
386 current_dir = "/"
387 parent_dir = "/"
388 selected = 1
389 scroll_offset = 0
390 selection_anchor = -1
391 call clear_all_selections(is_selected, selection_count, in_selection_mode)
392 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
393 case(42) ! '*' - toggle favorite on current directory
394 if (current_is_dir(selected) .and. trim(current_files(selected)) /= "." .and. &
395 trim(current_files(selected)) /= "..") then
396 ! Build full path for the directory
397 temp_dir = join_path(current_dir, current_files(selected))
398
399 ! Check if already favorited
400 if (is_dir_favorited(temp_dir, favorite_dirs, favorite_count)) then
401 ! Remove from favorites
402 call toggle_favorite(temp_dir, favorite_dirs, favorite_count)
403 call save_favorites(favorite_dirs, favorite_count)
404 else
405 ! Try to add
406 if (favorite_count < 10) then
407 ! Room available - add it
408 call toggle_favorite(temp_dir, favorite_dirs, favorite_count)
409 call save_favorites(favorite_dirs, favorite_count)
410 else
411 ! Full - prompt for replacement
412 call add_favorite_with_replacement(temp_dir, favorite_dirs, favorite_count)
413 call save_favorites(favorite_dirs, favorite_count)
414 end if
415 end if
416 end if
417 case(56) ! '8' - open favorites picker
418 call open_favorites_picker(favorite_dirs, favorite_count, temp_dir)
419 if (len_trim(temp_dir) > 0) then
420 ! Navigate to selected favorite
421 parent_dir = get_parent_path(temp_dir)
422 current_dir = temp_dir
423 selected = 1
424 scroll_offset = 0
425 selection_anchor = -1
426 call clear_all_selections(is_selected, selection_count, in_selection_mode)
427 call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name)
428 end if
429 case(32) ! Space - toggle selection on current item
430 if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
431 if (is_selected(selected)) then
432 is_selected(selected) = .false.
433 selection_count = selection_count - 1
434 else
435 is_selected(selected) = .true.
436 selection_count = selection_count + 1
437 in_selection_mode = .true.
438 end if
439 ! Clear the selection anchor when toggling individual items
440 selection_anchor = -1
441 ! Check if we have disjoint selections
442 call check_disjoint_selection(is_selected, current_count, has_disjoint_selection)
443 end if
444 case(69, 101) ! 'E' or 'e' - exit/clear multi-select mode
445 if (selection_count > 0) then
446 call clear_all_selections(is_selected, selection_count, in_selection_mode)
447 selection_anchor = -1
448 end if
449 case(86, 118) ! 'V' or 'v' - enter move mode OR confirm move
450 if (move_mode) then
451 ! Confirm move - execute the move to the white-highlighted directory
452 call execute_move_file(move_source_path, current_dir, current_files(move_dest_selected), &
453 current_is_dir(move_dest_selected))
454 move_mode = .false.
455 else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
456 ! Enter move mode - store source file or directory
457 move_source_path = join_path(current_dir, current_files(selected))
458 move_source_name = current_files(selected)
459 move_mode = .true.
460 ! Find first directory for destination cursor
461 move_dest_selected = find_first_directory(current_files, current_is_dir, current_count)
462 end if
463 case(89, 121) ! 'Y' or 'y' - yank/copy to clipboard
464 if (selection_count > 0) then
465 ! Copy multiple selections
466 clipboard_count = 0
467 do i = 1, current_count
468 if (is_selected(i) .and. trim(current_files(i)) /= "." .and. &
469 trim(current_files(i)) /= "..") then
470 clipboard_count = clipboard_count + 1
471 clipboard_paths(clipboard_count) = join_path(current_dir, current_files(i))
472 clipboard_names(clipboard_count) = current_files(i)
473 end if
474 end do
475 clipboard_is_cut = .false.
476 has_clipboard = .true.
477 ! Clear selections after copy
478 call clear_all_selections(is_selected, selection_count, in_selection_mode)
479 else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
480 ! Single item copy
481 clipboard_count = 1
482 clipboard_paths(1) = join_path(current_dir, current_files(selected))
483 clipboard_names(1) = current_files(selected)
484 clipboard_source_path = clipboard_paths(1) ! For backward compatibility
485 clipboard_source_name = clipboard_names(1)
486 clipboard_is_cut = .false.
487 has_clipboard = .true.
488 end if
489 case(88, 120) ! 'X' or 'x' - cut to clipboard
490 if (selection_count > 0) then
491 ! Cut multiple selections
492 clipboard_count = 0
493 do i = 1, current_count
494 if (is_selected(i) .and. trim(current_files(i)) /= "." .and. &
495 trim(current_files(i)) /= "..") then
496 clipboard_count = clipboard_count + 1
497 clipboard_paths(clipboard_count) = join_path(current_dir, current_files(i))
498 clipboard_names(clipboard_count) = current_files(i)
499 end if
500 end do
501 clipboard_is_cut = .true.
502 has_clipboard = .true.
503 ! Clear selections after cut
504 call clear_all_selections(is_selected, selection_count, in_selection_mode)
505 else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then
506 ! Single item cut
507 clipboard_count = 1
508 clipboard_paths(1) = join_path(current_dir, current_files(selected))
509 clipboard_names(1) = current_files(selected)
510 clipboard_source_path = clipboard_paths(1) ! For backward compatibility
511 clipboard_source_name = clipboard_names(1)
512 clipboard_is_cut = .true.
513 has_clipboard = .true.
514 end if
515 case(80, 112) ! 'P' or 'p' - paste from clipboard
516 if (has_clipboard) then
517 if (clipboard_count > 1) then
518 ! Paste multiple items
519 call execute_multi_paste(clipboard_paths, clipboard_names, clipboard_count, &
520 clipboard_is_cut, current_dir, current_files(selected), &
521 current_is_dir(selected))
522 else
523 ! Single item paste (backward compatibility)
524 call execute_paste(clipboard_paths(1), clipboard_is_cut, current_dir, &
525 current_files(selected), current_is_dir(selected))
526 end if
527 ! Clear clipboard after cut operation
528 if (clipboard_is_cut) then
529 has_clipboard = .false.
530 clipboard_count = 0
531 end if
532 end if
533 end select
534 end do
535
536 ! Cleanup
537 call restore_terminal()
538 write(output_unit, '(a)', advance='no') CLEAR
539
540 if (cd_on_exit) then
541 call write_exit_dir(exit_dir)
542 else
543 write(output_unit, '(a)') "Thanks for using FORTRESS!"
544 end if
545
546 contains
547
548 subroutine filter_dotfiles(files, is_dir, is_exec, count)
549 character(len=*), dimension(*), intent(inout) :: files
550 logical, dimension(*), intent(inout) :: is_dir, is_exec
551 integer, intent(inout) :: count
552 character(len=MAX_PATH), dimension(MAX_FILES) :: temp_files
553 logical, dimension(MAX_FILES) :: temp_is_dir, temp_is_exec
554 integer :: i, new_count
555
556 new_count = 0
557 do i = 1, count
558 ! Always keep "." and "..", filter other dotfiles
559 if (trim(files(i)) == "." .or. trim(files(i)) == ".." .or. files(i)(1:1) /= '.') then
560 new_count = new_count + 1
561 temp_files(new_count) = files(i)
562 temp_is_dir(new_count) = is_dir(i)
563 temp_is_exec(new_count) = is_exec(i)
564 end if
565 end do
566
567 ! Copy back
568 do i = 1, new_count
569 files(i) = temp_files(i)
570 is_dir(i) = temp_is_dir(i)
571 is_exec(i) = temp_is_exec(i)
572 end do
573 count = new_count
574 end subroutine filter_dotfiles
575
576 function find_first_directory(files, is_dir, count) result(idx)
577 character(len=*), dimension(*), intent(in) :: files
578 logical, dimension(*), intent(in) :: is_dir
579 integer, intent(in) :: count
580 integer :: idx, i
581
582 ! Find first directory (including . and ..)
583 do i = 1, count
584 if (is_dir(i)) then
585 idx = i
586 return
587 end if
588 end do
589
590 ! If no directory found, default to first item
591 idx = 1
592 end function find_first_directory
593
594 function find_next_directory(files, is_dir, count, current) result(idx)
595 character(len=*), dimension(*), intent(in) :: files
596 logical, dimension(*), intent(in) :: is_dir
597 integer, intent(in) :: count, current
598 integer :: idx, i
599
600 ! Search forward from current position (including . and ..)
601 do i = current + 1, count
602 if (is_dir(i)) then
603 idx = i
604 return
605 end if
606 end do
607
608 ! No directory found forward, stay at current
609 idx = current
610 end function find_next_directory
611
612 function find_prev_directory(files, is_dir, count, current) result(idx)
613 character(len=*), dimension(*), intent(in) :: files
614 logical, dimension(*), intent(in) :: is_dir
615 integer, intent(in) :: count, current
616 integer :: idx, i
617
618 ! Search backward from current position (including . and ..)
619 do i = current - 1, 1, -1
620 if (is_dir(i)) then
621 idx = i
622 return
623 end if
624 end do
625
626 ! No directory found backward, stay at current
627 idx = current
628 end function find_prev_directory
629
630 subroutine execute_move_file(source_path, dest_dir, dest_name, is_dest_dir)
631 use iso_fortran_env, only: output_unit
632 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD
633 character(len=*), intent(in) :: source_path, dest_dir, dest_name
634 logical, intent(in) :: is_dest_dir
635 character(len=MAX_PATH*2) :: dest_path, mv_cmd
636 integer :: stat
637
638 ! Build destination path
639 if (is_dest_dir) then
640 if (trim(dest_name) == ".") then
641 ! Move to current directory
642 dest_path = dest_dir
643 else if (trim(dest_name) == "..") then
644 ! Move to parent directory
645 dest_path = get_parent_path(dest_dir)
646 else
647 ! Move into the selected directory
648 dest_path = join_path(dest_dir, dest_name)
649 end if
650 else
651 ! Not a directory - shouldn't happen due to our navigation, but handle it
652 dest_path = dest_dir
653 end if
654
655 ! Execute move command (mv will move file into dest_path directory)
656 mv_cmd = "mv '" // trim(source_path) // "' '" // trim(dest_path) // "'"
657 call execute_command_line(trim(mv_cmd), exitstat=stat, wait=.true.)
658
659 ! Show result briefly (no user input to avoid terminal state issues)
660 write(output_unit, '(a)', advance='no') CLEAR
661 write(output_unit, '(a)') BOLD // "Move Result" // RESET
662 write(output_unit, *)
663 if (stat == 0) then
664 write(output_unit, '(a)') GREEN // "✓ Moved successfully!" // RESET
665 write(output_unit, '(a)') " From: " // trim(source_path)
666 write(output_unit, '(a)') " To: " // trim(dest_path)
667 else
668 write(output_unit, '(a)') RED // "✗ Move failed" // RESET
669 write(output_unit, '(a)') " (destination may already exist or be invalid)"
670 end if
671 write(output_unit, *)
672
673 ! Brief pause to let user see the result (use Fortran sleep to avoid stdin issues)
674 call sleep(2)
675 end subroutine execute_move_file
676
677 subroutine execute_paste(source_path, is_cut, dest_dir, dest_name, dest_is_dir)
678 use iso_fortran_env, only: output_unit
679 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD, YELLOW
680 character(len=*), intent(in) :: source_path, dest_dir, dest_name
681 logical, intent(in) :: is_cut, dest_is_dir
682 character(len=MAX_PATH*2) :: dest_path, cmd, final_dest, base_name, extension
683 character(len=MAX_PATH*2) :: test_path
684 character(len=10) :: suffix_str
685 integer :: stat, suffix_num, ext_pos, name_len, ios
686
687 ! Determine destination directory based on cursor position
688 if (dest_is_dir) then
689 if (trim(dest_name) == ".") then
690 ! Paste into current directory
691 dest_path = dest_dir
692 else if (trim(dest_name) == "..") then
693 ! Paste into parent directory
694 dest_path = get_parent_path(dest_dir)
695 else
696 ! Paste into the selected directory
697 dest_path = join_path(dest_dir, dest_name)
698 end if
699 else
700 ! Cursor is on a file - paste into current directory (next to the file)
701 dest_path = dest_dir
702 end if
703
704 ! Extract the source filename from source_path
705 stat = index(source_path, "/", back=.true.)
706 if (stat > 0) then
707 base_name = source_path(stat+1:)
708 else
709 base_name = source_path
710 end if
711
712 ! Build initial destination (directory + filename)
713 final_dest = join_path(dest_path, base_name)
714
715 ! Check if destination exists and find available suffix if needed
716 call execute_command_line("test -e '" // trim(final_dest) // "'", exitstat=stat, wait=.true.)
717 if (stat == 0) then
718 ! Destination exists - find next available suffix (for both copy and cut)
719 ! Split filename into name and extension
720 ext_pos = index(base_name, ".", back=.true.)
721 if (ext_pos > 1) then
722 ! Has extension
723 extension = base_name(ext_pos:)
724 name_len = ext_pos - 1
725 else
726 ! No extension
727 extension = ""
728 name_len = len_trim(base_name)
729 end if
730
731 ! Find next available suffix number
732 suffix_num = 1
733 do while (suffix_num < 1000) ! Safety limit
734 ! Build the test path with suffix using concatenation
735 write(suffix_str, '(i0)') suffix_num
736
737 if (len_trim(extension) > 0) then
738 ! With extension: filename-N.ext
739 test_path = trim(dest_path) // "/" // base_name(1:name_len) // "-" // &
740 trim(suffix_str) // trim(extension)
741 else
742 ! Without extension: filename-N
743 test_path = trim(dest_path) // "/" // trim(base_name) // "-" // trim(suffix_str)
744 end if
745
746 ! Check if this suffixed name exists
747 call execute_command_line("test -e '" // trim(test_path) // "'", exitstat=stat, wait=.true.)
748 if (stat /= 0) then
749 ! This name is available!
750 final_dest = test_path
751 exit
752 end if
753 suffix_num = suffix_num + 1
754 end do
755 end if
756
757 ! Execute copy or move command
758 if (is_cut) then
759 ! Cut = move
760 cmd = "mv '" // trim(source_path) // "' '" // trim(final_dest) // "'"
761 else
762 ! Copy recursively (works for both files and directories)
763 cmd = "cp -r '" // trim(source_path) // "' '" // trim(final_dest) // "'"
764 end if
765 call execute_command_line(trim(cmd), exitstat=stat, wait=.true.)
766
767 ! Show result briefly
768 write(output_unit, '(a)', advance='no') CLEAR
769 if (is_cut) then
770 write(output_unit, '(a)') BOLD // "Cut Result" // RESET
771 else
772 write(output_unit, '(a)') BOLD // "Copy Result" // RESET
773 end if
774 write(output_unit, *)
775 if (stat == 0) then
776 if (is_cut) then
777 write(output_unit, '(a)') GREEN // "✓ Cut and pasted successfully!" // RESET
778 else
779 write(output_unit, '(a)') GREEN // "✓ Copied successfully!" // RESET
780 end if
781 write(output_unit, '(a)') " From: " // trim(source_path)
782 write(output_unit, '(a)') " To: " // trim(final_dest)
783 else
784 if (is_cut) then
785 write(output_unit, '(a)') RED // "✗ Cut failed" // RESET
786 else
787 write(output_unit, '(a)') RED // "✗ Copy failed" // RESET
788 end if
789 write(output_unit, '(a)') " (destination may already exist or be invalid)"
790 end if
791 write(output_unit, *)
792
793 ! Brief pause to let user see the result
794 call sleep(2)
795 end subroutine execute_paste
796
797 subroutine delete_with_confirmation(dir, filename, is_dir)
798 use iso_fortran_env, only: output_unit
799 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD, YELLOW
800 character(len=*), intent(in) :: dir, filename
801 logical, intent(in) :: is_dir
802 character(len=MAX_PATH*2) :: full_path, rm_cmd
803 character(len=1) :: response
804 integer :: stat, ios
805
806 ! Build full path
807 full_path = join_path(dir, filename)
808
809 ! Clear screen and show confirmation prompt
810 write(output_unit, '(a)', advance='no') CLEAR
811 write(output_unit, '(a)') BOLD // "Delete Confirmation" // RESET
812 write(output_unit, *)
813 if (is_dir) then
814 write(output_unit, '(a)') YELLOW // "WARNING: You are about to delete a directory!" // RESET
815 write(output_unit, '(a)') "Directory: " // trim(filename)
816 else
817 write(output_unit, '(a)') "File: " // trim(filename)
818 end if
819 write(output_unit, '(a)') "Path: " // trim(full_path)
820 write(output_unit, *)
821 write(output_unit, '(a)', advance='no') RED // "Are you sure? (y/N): " // RESET
822
823 ! Read single character immediately (no need to wait for Enter)
824 read(*, '(a1)', advance='no', iostat=ios) response
825
826 if (ios == 0 .and. (response == 'y' .or. response == 'Y')) then
827 ! User confirmed - proceed with deletion
828 if (is_dir) then
829 ! Delete directory recursively
830 rm_cmd = "rm -rf '" // trim(full_path) // "'"
831 else
832 ! Delete file
833 rm_cmd = "rm -f '" // trim(full_path) // "'"
834 end if
835
836 call execute_command_line(trim(rm_cmd), exitstat=stat, wait=.true.)
837
838 ! Show result
839 write(output_unit, *)
840 write(output_unit, *)
841 if (stat == 0) then
842 write(output_unit, '(a)') GREEN // "✓ Deleted successfully!" // RESET
843 else
844 write(output_unit, '(a)') RED // "✗ Delete failed" // RESET
845 end if
846 write(output_unit, *)
847 write(output_unit, '(a)') "Press any key to continue..."
848
849 ! Wait for keypress
850 read(*, '(a1)', advance='no', iostat=ios) response
851 else
852 ! User cancelled
853 write(output_unit, *)
854 write(output_unit, *)
855 write(output_unit, '(a)') "Delete cancelled."
856 write(output_unit, *)
857 write(output_unit, '(a)') "Press any key to continue..."
858
859 ! Wait for keypress
860 read(*, '(a1)', advance='no', iostat=ios) response
861 end if
862 end subroutine delete_with_confirmation
863
864 subroutine clear_all_selections(is_selected, selection_count, in_selection_mode)
865 logical, dimension(*), intent(inout) :: is_selected
866 integer, intent(inout) :: selection_count
867 logical, intent(inout) :: in_selection_mode
868 integer :: i
869
870 ! Clear all selections
871 do i = 1, MAX_FILES
872 is_selected(i) = .false.
873 end do
874 selection_count = 0
875 in_selection_mode = .false.
876 end subroutine clear_all_selections
877
878 subroutine check_disjoint_selection(is_selected, count, has_disjoint)
879 logical, dimension(*), intent(in) :: is_selected
880 integer, intent(in) :: count
881 logical, intent(out) :: has_disjoint
882 integer :: i, first_selected, last_selected
883
884 has_disjoint = .false.
885 first_selected = -1
886 last_selected = -1
887
888 ! Find first and last selected items
889 do i = 1, count
890 if (is_selected(i)) then
891 if (first_selected == -1) first_selected = i
892 last_selected = i
893 end if
894 end do
895
896 ! If we have a range, check if all items in range are selected
897 if (first_selected > 0 .and. last_selected > first_selected) then
898 do i = first_selected + 1, last_selected - 1
899 if (.not. is_selected(i)) then
900 has_disjoint = .true.
901 exit
902 end if
903 end do
904 end if
905 end subroutine check_disjoint_selection
906
907 subroutine select_range(is_selected, selection_count, anchor, cursor, files, count)
908 logical, dimension(*), intent(inout) :: is_selected
909 integer, intent(inout) :: selection_count
910 integer, intent(in) :: anchor, cursor, count
911 character(len=*), dimension(*), intent(in) :: files
912 integer :: i, range_start, range_end
913
914 ! Determine range boundaries
915 range_start = min(anchor, cursor)
916 range_end = max(anchor, cursor)
917
918 ! Clear all selections first
919 do i = 1, count
920 is_selected(i) = .false.
921 end do
922
923 ! Select the range, skipping "." and ".."
924 selection_count = 0
925 do i = range_start, range_end
926 if (i >= 1 .and. i <= count) then
927 ! Skip special directories "." and ".."
928 if (trim(files(i)) /= "." .and. trim(files(i)) /= "..") then
929 is_selected(i) = .true.
930 selection_count = selection_count + 1
931 end if
932 end if
933 end do
934 end subroutine select_range
935
936 subroutine delete_multi_with_confirmation(dir, files, is_dir, is_selected, selection_count, count)
937 use iso_fortran_env, only: output_unit
938 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD, YELLOW
939 character(len=*), intent(in) :: dir
940 character(len=*), dimension(*), intent(in) :: files
941 logical, dimension(*), intent(in) :: is_dir, is_selected
942 integer, intent(in) :: selection_count, count
943 character(len=MAX_PATH*2) :: full_path, rm_cmd
944 character(len=1) :: response
945 integer :: stat, ios, i, deleted_count
946
947 ! Clear screen and show confirmation prompt
948 write(output_unit, '(a)', advance='no') CLEAR
949 write(output_unit, '(a)') BOLD // "Delete Multiple Items" // RESET
950 write(output_unit, *)
951 write(output_unit, '(a)') YELLOW // "WARNING: You are about to delete " // &
952 trim(adjustl(itoa(selection_count))) // " items!" // RESET
953 write(output_unit, *)
954 write(output_unit, '(a)') "Selected items:"
955
956 ! List selected items (up to 10)
957 i = 0
958 do stat = 1, count
959 if (is_selected(stat)) then
960 i = i + 1
961 if (i <= 10) then
962 if (is_dir(stat)) then
963 write(output_unit, '(a)') " [DIR] " // trim(files(stat))
964 else
965 write(output_unit, '(a)') " [FILE] " // trim(files(stat))
966 end if
967 else if (i == 11) then
968 write(output_unit, '(a)') " ... and " // &
969 trim(adjustl(itoa(selection_count - 10))) // " more"
970 exit
971 end if
972 end if
973 end do
974
975 write(output_unit, *)
976 write(output_unit, '(a)', advance='no') RED // "Delete all selected items? (y/N): " // RESET
977
978 ! Read single character immediately
979 read(*, '(a1)', advance='no', iostat=ios) response
980
981 if (ios == 0 .and. (response == 'y' .or. response == 'Y')) then
982 ! User confirmed - proceed with deletion
983 deleted_count = 0
984
985 do i = 1, count
986 if (is_selected(i) .and. trim(files(i)) /= "." .and. trim(files(i)) /= "..") then
987 full_path = join_path(dir, files(i))
988
989 if (is_dir(i)) then
990 rm_cmd = "rm -rf '" // trim(full_path) // "'"
991 else
992 rm_cmd = "rm -f '" // trim(full_path) // "'"
993 end if
994
995 call execute_command_line(trim(rm_cmd), exitstat=stat, wait=.true.)
996 if (stat == 0) deleted_count = deleted_count + 1
997 end if
998 end do
999
1000 ! Show result
1001 write(output_unit, *)
1002 write(output_unit, *)
1003 if (deleted_count == selection_count) then
1004 write(output_unit, '(a)') GREEN // "✓ All items deleted successfully!" // RESET
1005 else if (deleted_count > 0) then
1006 write(output_unit, '(a)') YELLOW // "⚠ Deleted " // &
1007 trim(adjustl(itoa(deleted_count))) // " of " // &
1008 trim(adjustl(itoa(selection_count))) // " items" // RESET
1009 else
1010 write(output_unit, '(a)') RED // "✗ Delete failed" // RESET
1011 end if
1012 write(output_unit, *)
1013 write(output_unit, '(a)') "Press any key to continue..."
1014
1015 ! Wait for keypress
1016 read(*, '(a1)', advance='no', iostat=ios) response
1017 else
1018 ! User cancelled
1019 write(output_unit, *)
1020 write(output_unit, *)
1021 write(output_unit, '(a)') "Delete cancelled."
1022 write(output_unit, *)
1023 write(output_unit, '(a)') "Press any key to continue..."
1024
1025 ! Wait for keypress
1026 read(*, '(a1)', advance='no', iostat=ios) response
1027 end if
1028 end subroutine delete_multi_with_confirmation
1029
1030 function itoa(n) result(str)
1031 integer, intent(in) :: n
1032 character(len=10) :: str
1033 write(str, '(i0)') n
1034 end function itoa
1035
1036 subroutine execute_multi_paste(paths, names, count, is_cut, dest_dir, dest_name, dest_is_dir)
1037 use iso_fortran_env, only: output_unit
1038 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD, YELLOW
1039 character(len=*), dimension(*), intent(in) :: paths, names
1040 integer, intent(in) :: count
1041 logical, intent(in) :: is_cut, dest_is_dir
1042 character(len=*), intent(in) :: dest_dir, dest_name
1043 character(len=MAX_PATH*2) :: dest_path, cmd, final_dest
1044 integer :: i, stat, success_count
1045 character(len=1) :: response
1046
1047 ! Determine destination directory
1048 if (dest_is_dir) then
1049 if (trim(dest_name) == ".") then
1050 dest_path = dest_dir
1051 else if (trim(dest_name) == "..") then
1052 dest_path = get_parent_path(dest_dir)
1053 else
1054 dest_path = join_path(dest_dir, dest_name)
1055 end if
1056 else
1057 dest_path = dest_dir
1058 end if
1059
1060 ! Show operation preview
1061 write(output_unit, '(a)', advance='no') CLEAR
1062 if (is_cut) then
1063 write(output_unit, '(a)') BOLD // "Multi-Cut Operation" // RESET
1064 else
1065 write(output_unit, '(a)') BOLD // "Multi-Copy Operation" // RESET
1066 end if
1067 write(output_unit, *)
1068 write(output_unit, '(a)') "Pasting " // trim(adjustl(itoa(count))) // " items to:"
1069 write(output_unit, '(a)') " " // trim(dest_path)
1070 write(output_unit, *)
1071
1072 success_count = 0
1073
1074 ! Process each item
1075 do i = 1, count
1076 ! Generate unique destination name if needed
1077 call get_unique_dest_name(dest_path, names(i), final_dest)
1078
1079 ! Execute operation
1080 if (is_cut) then
1081 cmd = "mv '" // trim(paths(i)) // "' '" // trim(final_dest) // "'"
1082 else
1083 cmd = "cp -r '" // trim(paths(i)) // "' '" // trim(final_dest) // "'"
1084 end if
1085
1086 call execute_command_line(trim(cmd), exitstat=stat, wait=.true.)
1087
1088 if (stat == 0) then
1089 success_count = success_count + 1
1090 write(output_unit, '(a)') GREEN // " ✓ " // RESET // trim(names(i))
1091 else
1092 write(output_unit, '(a)') RED // " ✗ " // RESET // trim(names(i))
1093 end if
1094 end do
1095
1096 ! Show summary
1097 write(output_unit, *)
1098 if (success_count == count) then
1099 write(output_unit, '(a)') GREEN // "✓ All items processed successfully!" // RESET
1100 else if (success_count > 0) then
1101 write(output_unit, '(a)') YELLOW // "⚠ Processed " // &
1102 trim(adjustl(itoa(success_count))) // " of " // &
1103 trim(adjustl(itoa(count))) // " items" // RESET
1104 else
1105 write(output_unit, '(a)') RED // "✗ Operation failed" // RESET
1106 end if
1107 write(output_unit, *)
1108 write(output_unit, '(a)') "Press any key to continue..."
1109
1110 ! Wait for keypress
1111 read(*, '(a1)', advance='no', iostat=stat) response
1112 end subroutine execute_multi_paste
1113
1114 subroutine get_unique_dest_name(dest_dir, base_name, unique_name)
1115 character(len=*), intent(in) :: dest_dir, base_name
1116 character(len=*), intent(out) :: unique_name
1117 character(len=MAX_PATH*2) :: test_path
1118 character(len=256) :: name_part, extension
1119 character(len=10) :: suffix_str
1120 integer :: ext_pos, suffix_num, stat
1121
1122 ! Initial destination
1123 unique_name = join_path(dest_dir, base_name)
1124
1125 ! Check if exists
1126 call execute_command_line("test -e '" // trim(unique_name) // "'", exitstat=stat, wait=.true.)
1127 if (stat /= 0) return ! Doesn't exist, we're good
1128
1129 ! Split name and extension
1130 ext_pos = index(base_name, ".", back=.true.)
1131 if (ext_pos > 1) then
1132 name_part = base_name(1:ext_pos-1)
1133 extension = base_name(ext_pos:)
1134 else
1135 name_part = base_name
1136 extension = ""
1137 end if
1138
1139 ! Find available suffix
1140 do suffix_num = 1, 999
1141 write(suffix_str, '(i0)') suffix_num
1142 if (len_trim(extension) > 0) then
1143 test_path = trim(dest_dir) // "/" // trim(name_part) // "-" // &
1144 trim(suffix_str) // trim(extension)
1145 else
1146 test_path = trim(dest_dir) // "/" // trim(name_part) // "-" // &
1147 trim(suffix_str)
1148 end if
1149
1150 call execute_command_line("test -e '" // trim(test_path) // "'", exitstat=stat, wait=.true.)
1151 if (stat /= 0) then
1152 unique_name = test_path
1153 exit
1154 end if
1155 end do
1156 end subroutine get_unique_dest_name
1157
1158 subroutine load_favorites(favs, count)
1159 character(len=MAX_PATH), dimension(10), intent(out) :: favs
1160 integer, intent(out) :: count
1161 character(len=MAX_PATH) :: favorites_file
1162 integer :: unit, ios
1163
1164 count = 0
1165 call get_environment_variable("HOME", favorites_file)
1166 favorites_file = trim(favorites_file) // "/.fortress_favorites"
1167
1168 open(newunit=unit, file=favorites_file, status='old', action='read', iostat=ios)
1169 if (ios /= 0) return ! File doesn't exist yet
1170
1171 do while (count < 10)
1172 read(unit, '(a)', iostat=ios) favs(count + 1)
1173 if (ios /= 0) exit
1174 if (len_trim(favs(count + 1)) > 0) count = count + 1
1175 end do
1176
1177 close(unit)
1178 end subroutine load_favorites
1179
1180 subroutine save_favorites(favs, count)
1181 character(len=MAX_PATH), dimension(10), intent(in) :: favs
1182 integer, intent(in) :: count
1183 character(len=MAX_PATH) :: favorites_file
1184 integer :: unit, ios, i
1185
1186 call get_environment_variable("HOME", favorites_file)
1187 favorites_file = trim(favorites_file) // "/.fortress_favorites"
1188
1189 open(newunit=unit, file=favorites_file, status='replace', action='write', iostat=ios)
1190 if (ios /= 0) return
1191
1192 do i = 1, count
1193 write(unit, '(a)') trim(favs(i))
1194 end do
1195
1196 close(unit)
1197 end subroutine save_favorites
1198
1199 function is_dir_favorited(dir_path, favs, count) result(is_fav)
1200 character(len=*), intent(in) :: dir_path
1201 character(len=MAX_PATH), dimension(10), intent(in) :: favs
1202 integer, intent(in) :: count
1203 logical :: is_fav
1204 integer :: i
1205
1206 is_fav = .false.
1207 do i = 1, count
1208 if (trim(favs(i)) == trim(dir_path)) then
1209 is_fav = .true.
1210 return
1211 end if
1212 end do
1213 end function is_dir_favorited
1214
1215 subroutine toggle_favorite(dir_path, favs, count)
1216 character(len=*), intent(in) :: dir_path
1217 character(len=MAX_PATH), dimension(10), intent(inout) :: favs
1218 integer, intent(inout) :: count
1219 integer :: i, j
1220 logical :: found
1221
1222 ! Check if already favorited
1223 found = .false.
1224 do i = 1, count
1225 if (trim(favs(i)) == trim(dir_path)) then
1226 ! Found - remove it
1227 found = .true.
1228 ! Shift remaining favorites down
1229 do j = i, count - 1
1230 favs(j) = favs(j + 1)
1231 end do
1232 favs(count) = ""
1233 count = count - 1
1234 exit
1235 end if
1236 end do
1237
1238 if (.not. found) then
1239 ! Not found - add it (if we have space)
1240 if (count < 10) then
1241 count = count + 1
1242 favs(count) = dir_path
1243 end if
1244 end if
1245 end subroutine toggle_favorite
1246
1247 subroutine add_favorite_with_replacement(dir_path, favs, count)
1248 use iso_fortran_env, only: output_unit
1249 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD
1250 character(len=*), intent(in) :: dir_path
1251 character(len=MAX_PATH), dimension(10), intent(inout) :: favs
1252 integer, intent(in) :: count
1253 character(len=MAX_PATH) :: temp_file, selected_fav
1254 integer :: unit, ios, stat, i
1255
1256 ! Clear screen and show prompt
1257 write(output_unit, '(a)', advance='no') CLEAR
1258 write(output_unit, '(a)') BOLD // "Favorites Full (10/10)" // RESET
1259 write(output_unit, *)
1260 write(output_unit, '(a)') "Select a favorite to replace:"
1261 write(output_unit, *)
1262
1263 ! Restore terminal for fzf
1264 call execute_command_line("stty sane 2>/dev/null")
1265
1266 ! Create temp file with current favorites
1267 call get_environment_variable("HOME", temp_file)
1268 temp_file = trim(temp_file) // "/.fortress_fav_temp"
1269
1270 open(newunit=unit, file=temp_file, status='replace', action='write', iostat=ios)
1271 if (ios == 0) then
1272 do i = 1, count
1273 write(unit, '(a)') trim(favs(i))
1274 end do
1275 close(unit)
1276 end if
1277
1278 ! Use fzf to select which favorite to replace
1279 call get_environment_variable("HOME", temp_file)
1280 temp_file = trim(temp_file) // "/.fortress_fav_select"
1281 call execute_command_line("cat ~/.fortress_fav_temp | fzf --height=10 --prompt='Replace: ' > " // &
1282 trim(temp_file) // " 2>/dev/null", exitstat=stat, wait=.true.)
1283
1284 if (stat == 0) then
1285 ! Read selected favorite
1286 open(newunit=unit, file=temp_file, status='old', action='read', iostat=ios)
1287 if (ios == 0) then
1288 read(unit, '(a)', iostat=ios) selected_fav
1289 close(unit)
1290
1291 if (ios == 0 .and. len_trim(selected_fav) > 0) then
1292 ! Replace the selected favorite with the new one
1293 do i = 1, count
1294 if (trim(favs(i)) == trim(selected_fav)) then
1295 favs(i) = dir_path
1296 write(output_unit, '(a)') GREEN // "✓ Replaced: " // trim(selected_fav) // RESET
1297 write(output_unit, '(a)') " With: " // trim(dir_path)
1298 call execute_command_line("sleep 1")
1299 exit
1300 end if
1301 end do
1302 end if
1303 end if
1304 else
1305 write(output_unit, '(a)') RED // "Cancelled." // RESET
1306 call execute_command_line("sleep 1")
1307 end if
1308
1309 ! Cleanup temp files
1310 call execute_command_line("rm -f ~/.fortress_fav_temp ~/.fortress_fav_select 2>/dev/null")
1311
1312 ! Re-enable raw mode
1313 call setup_raw_mode()
1314 end subroutine add_favorite_with_replacement
1315
1316 subroutine mark_favorites_in_lists(current_dir, files, count, is_dir, favs, fav_count, is_fav)
1317 character(len=*), intent(in) :: current_dir
1318 character(len=*), dimension(*), intent(in) :: files
1319 integer, intent(in) :: count
1320 logical, dimension(*), intent(in) :: is_dir
1321 character(len=MAX_PATH), dimension(10), intent(in) :: favs
1322 integer, intent(in) :: fav_count
1323 logical, dimension(*), intent(out) :: is_fav
1324 character(len=MAX_PATH) :: full_path
1325 integer :: i
1326
1327 ! Initialize all to false
1328 do i = 1, count
1329 is_fav(i) = .false.
1330 end do
1331
1332 ! Mark directories that are in favorites
1333 do i = 1, count
1334 if (is_dir(i) .and. trim(files(i)) /= "." .and. trim(files(i)) /= "..") then
1335 ! Build full path and check if favorited
1336 full_path = join_path(current_dir, files(i))
1337 is_fav(i) = is_dir_favorited(full_path, favs, fav_count)
1338 end if
1339 end do
1340 end subroutine mark_favorites_in_lists
1341
1342 subroutine open_favorites_picker(favs, count, selected_dir)
1343 use iso_fortran_env, only: output_unit
1344 use terminal_control, only: CLEAR, GREEN, RED, RESET, BOLD
1345 character(len=MAX_PATH), dimension(10), intent(in) :: favs
1346 integer, intent(in) :: count
1347 character(len=MAX_PATH), intent(out) :: selected_dir
1348 character(len=MAX_PATH) :: temp_file
1349 integer :: unit, ios, stat, i
1350
1351 selected_dir = ""
1352
1353 if (count == 0) then
1354 ! No favorites yet
1355 write(output_unit, '(a)', advance='no') CLEAR
1356 write(output_unit, '(a)') RED // "No favorites yet!" // RESET
1357 write(output_unit, *)
1358 write(output_unit, '(a)') "Press '*' on a directory to add it to favorites."
1359 call execute_command_line("sleep 2")
1360 return
1361 end if
1362
1363 ! Restore terminal for fzf
1364 call execute_command_line("stty sane 2>/dev/null")
1365
1366 ! Create temp file with favorites
1367 call get_environment_variable("HOME", temp_file)
1368 temp_file = trim(temp_file) // "/.fortress_fav_picker"
1369
1370 open(newunit=unit, file=temp_file, status='replace', action='write', iostat=ios)
1371 if (ios == 0) then
1372 do i = 1, count
1373 write(unit, '(a)') trim(favs(i))
1374 end do
1375 close(unit)
1376 end if
1377
1378 ! Use fzf to select favorite
1379 call get_environment_variable("HOME", temp_file)
1380 temp_file = trim(temp_file) // "/.fortress_fav_selected"
1381 call execute_command_line("cat ~/.fortress_fav_picker | fzf --height=10 --prompt='Jump to: ' > " // &
1382 trim(temp_file) // " 2>/dev/null", exitstat=stat, wait=.true.)
1383
1384 if (stat == 0) then
1385 ! Read selected directory
1386 open(newunit=unit, file=temp_file, status='old', action='read', iostat=ios)
1387 if (ios == 0) then
1388 read(unit, '(a)', iostat=ios) selected_dir
1389 close(unit)
1390 end if
1391 end if
1392
1393 ! Cleanup temp files
1394 call execute_command_line("rm -f ~/.fortress_fav_picker ~/.fortress_fav_selected 2>/dev/null")
1395
1396 ! Re-enable raw mode
1397 call setup_raw_mode()
1398 end subroutine open_favorites_picker
1399
1400 end program fortress
1401