@@ -59,6 +59,11 @@ program fortress |
| 59 | 59 | integer :: favorite_count = 0 |
| 60 | 60 | logical, dimension(MAX_FILES) :: current_is_favorite, parent_is_favorite |
| 61 | 61 | |
| 62 | + ! Rename mode state |
| 63 | + logical :: in_rename_mode = .false. |
| 64 | + character(len=MAX_PATH) :: rename_buffer = '' |
| 65 | + integer :: rename_cursor_pos = 0 |
| 66 | + |
| 62 | 67 | character(len=1) :: key |
| 63 | 68 | integer :: i, rows, cols, visible_height |
| 64 | 69 | logical :: is_shift_pressed, is_alt_pressed |
@@ -175,12 +180,95 @@ program fortress |
| 175 | 180 | has_clipboard, clipboard_is_cut, clipboard_source_name, clipboard_count, & |
| 176 | 181 | is_selected, selection_count, & |
| 177 | 182 | current_is_favorite, parent_is_favorite, & |
| 178 | | - search_buffer, search_length) |
| 183 | + search_buffer, search_length, & |
| 184 | + in_rename_mode, rename_buffer, rename_cursor_pos) |
| 179 | 185 | |
| 180 | 186 | ! Get input (with error handling for End-of-record after Enter key) |
| 181 | 187 | read(*, '(a1)', advance='no', iostat=i) key |
| 182 | | - ! Only cycle on End-of-record (negative iostat), which happens after pressing Enter |
| 183 | | - ! Don't skip on positive errors or when we successfully read a character |
| 188 | + |
| 189 | + ! Rename mode key handling - intercept ALL keys when in rename mode (BEFORE error handling) |
| 190 | + if (in_rename_mode) then |
| 191 | + ! In rename mode, handle read errors differently |
| 192 | + if (i < 0) then |
| 193 | + ! End-of-record in rename mode - treat as Enter |
| 194 | + key = achar(13) |
| 195 | + else if (i > 0) then |
| 196 | + ! Read error - ignore and continue |
| 197 | + cycle |
| 198 | + end if |
| 199 | + |
| 200 | + ! Handle ESC to cancel rename |
| 201 | + if (ichar(key) == 27) then |
| 202 | + in_rename_mode = .false. |
| 203 | + rename_buffer = '' |
| 204 | + rename_cursor_pos = 0 |
| 205 | + cycle |
| 206 | + ! Handle Enter to confirm rename |
| 207 | + else if (ichar(key) == 10 .or. ichar(key) == 13) then |
| 208 | + ! Execute rename if name changed |
| 209 | + if (len_trim(rename_buffer) > 0 .and. trim(rename_buffer) /= trim(current_files(selected))) then |
| 210 | + block |
| 211 | + character(len=MAX_PATH) :: old_path, new_path, old_name |
| 212 | + character(len=MAX_PATH*2) :: mv_cmd |
| 213 | + integer :: stat |
| 214 | + |
| 215 | + ! Store the old name for cursor tracking |
| 216 | + old_name = current_files(selected) |
| 217 | + old_path = join_path(current_dir, old_name) |
| 218 | + new_path = join_path(current_dir, trim(rename_buffer)) |
| 219 | + |
| 220 | + ! Use -f flag for case-only renames, double quotes for paths |
| 221 | + mv_cmd = 'mv -f "' // trim(old_path) // '" "' // trim(new_path) // '"' |
| 222 | + call execute_command_line(trim(mv_cmd), exitstat=stat, wait=.true.) |
| 223 | + |
| 224 | + ! After rename succeeds, find the renamed file in the refreshed list |
| 225 | + if (stat == 0) then |
| 226 | + temp_dir = new_path |
| 227 | + selected = -2 ! Signal to find this file in the next iteration |
| 228 | + end if |
| 229 | + end block |
| 230 | + end if |
| 231 | + in_rename_mode = .false. |
| 232 | + rename_buffer = '' |
| 233 | + rename_cursor_pos = 0 |
| 234 | + cycle |
| 235 | + ! Handle Backspace |
| 236 | + else if (ichar(key) == 127 .or. ichar(key) == 8) then |
| 237 | + if (rename_cursor_pos > 0) then |
| 238 | + if (rename_cursor_pos == len_trim(rename_buffer)) then |
| 239 | + ! Cursor at end - simple delete |
| 240 | + rename_buffer = rename_buffer(1:len_trim(rename_buffer)-1) |
| 241 | + else |
| 242 | + ! Cursor in middle - delete and shift left |
| 243 | + rename_buffer = rename_buffer(1:rename_cursor_pos-1) // & |
| 244 | + rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer)) |
| 245 | + end if |
| 246 | + rename_cursor_pos = rename_cursor_pos - 1 |
| 247 | + end if |
| 248 | + cycle |
| 249 | + ! Handle printable characters - insert at cursor position |
| 250 | + else if ((ichar(key) >= ichar('a') .and. ichar(key) <= ichar('z')) .or. & |
| 251 | + (ichar(key) >= ichar('A') .and. ichar(key) <= ichar('Z')) .or. & |
| 252 | + (ichar(key) >= ichar('0') .and. ichar(key) <= ichar('9')) .or. & |
| 253 | + key == '_' .or. key == '-' .or. key == '.' .or. key == ' ') then |
| 254 | + if (len_trim(rename_buffer) < MAX_PATH - 1) then |
| 255 | + if (rename_cursor_pos == len_trim(rename_buffer)) then |
| 256 | + ! Cursor at end - simple append |
| 257 | + rename_buffer = trim(rename_buffer) // key |
| 258 | + else |
| 259 | + ! Cursor in middle - insert and shift right |
| 260 | + rename_buffer = rename_buffer(1:rename_cursor_pos) // key // & |
| 261 | + rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer)) |
| 262 | + end if |
| 263 | + rename_cursor_pos = rename_cursor_pos + 1 |
| 264 | + end if |
| 265 | + cycle |
| 266 | + end if |
| 267 | + ! Ignore all other keys in rename mode |
| 268 | + cycle |
| 269 | + end if |
| 270 | + |
| 271 | + ! Handle read errors for normal mode |
| 184 | 272 | if (i < 0) cycle ! End-of-record - skip and try again |
| 185 | 273 | if (i > 0) cycle ! Other read errors - skip and try again |
| 186 | 274 | |
@@ -236,8 +324,138 @@ program fortress |
| 236 | 324 | |
| 237 | 325 | call read_key_with_modifiers(key, is_shift_pressed, is_alt_pressed) |
| 238 | 326 | |
| 239 | | - ! Handle Alt+key combinations - skip arrow processing and handle below |
| 240 | | - if (.not. is_alt_pressed) then |
| 327 | + ! Handle Alt+key combinations |
| 328 | + if (is_alt_pressed) then |
| 329 | + ! Process Alt keys here (key is now encoded as achar(1-26)) |
| 330 | + select case(ichar(key)) |
| 331 | + case(7) ! Alt+g - toggle git mode |
| 332 | + if (in_git_repo) then |
| 333 | + if (mode == 'normal') then |
| 334 | + mode = 'git' |
| 335 | + else |
| 336 | + mode = 'normal' |
| 337 | + end if |
| 338 | + end if |
| 339 | + case(14) ! Alt+n - enter rename mode |
| 340 | + if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 341 | + in_rename_mode = .true. |
| 342 | + rename_buffer = current_files(selected) |
| 343 | + rename_cursor_pos = len_trim(rename_buffer) |
| 344 | + end if |
| 345 | + case(22) ! Alt+v - view file |
| 346 | + if (.not. current_is_dir(selected)) then |
| 347 | + if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 348 | + call open_file_in_default_app(join_path(current_dir, current_files(selected))) |
| 349 | + end if |
| 350 | + end if |
| 351 | + case(19) ! Alt+s - fzf search |
| 352 | + call fzf_search(current_dir, temp_dir) |
| 353 | + if (len_trim(temp_dir) > 0) then |
| 354 | + parent_dir = get_parent_path(temp_dir) |
| 355 | + current_dir = parent_dir |
| 356 | + parent_dir = get_parent_path(current_dir) |
| 357 | + selected = -2 |
| 358 | + call detect_git_repo(current_dir, in_git_repo, repo_name, branch_name) |
| 359 | + end if |
| 360 | + case(3) ! Alt+c - cd on exit |
| 361 | + if (current_is_dir(selected)) then |
| 362 | + if (trim(current_files(selected)) == "..") then |
| 363 | + exit_dir = parent_dir |
| 364 | + else if (trim(current_files(selected)) == ".") then |
| 365 | + exit_dir = current_dir |
| 366 | + else |
| 367 | + exit_dir = join_path(current_dir, current_files(selected)) |
| 368 | + end if |
| 369 | + cd_on_exit = .true. |
| 370 | + running = .false. |
| 371 | + end if |
| 372 | + case(18) ! Alt+r - delete |
| 373 | + if (selection_count > 0) then |
| 374 | + call delete_multi_with_confirmation(current_dir, current_files, current_is_dir, & |
| 375 | + is_selected, selection_count, current_count) |
| 376 | + call clear_all_selections(is_selected, selection_count, in_selection_mode) |
| 377 | + else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 378 | + call delete_with_confirmation(current_dir, current_files(selected), current_is_dir(selected)) |
| 379 | + end if |
| 380 | + case(13) ! Alt+m - move mode |
| 381 | + if (move_mode) then |
| 382 | + call execute_move_file(move_source_path, current_dir, current_files(move_dest_selected), & |
| 383 | + current_is_dir(move_dest_selected)) |
| 384 | + move_mode = .false. |
| 385 | + else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 386 | + move_source_path = join_path(current_dir, current_files(selected)) |
| 387 | + move_source_name = current_files(selected) |
| 388 | + move_mode = .true. |
| 389 | + move_dest_selected = find_first_directory(current_files, current_is_dir, current_count) |
| 390 | + end if |
| 391 | + case(25) ! Alt+y - copy |
| 392 | + if (selection_count > 0) then |
| 393 | + clipboard_count = 0 |
| 394 | + do i = 1, current_count |
| 395 | + if (is_selected(i) .and. trim(current_files(i)) /= "." .and. & |
| 396 | + trim(current_files(i)) /= "..") then |
| 397 | + clipboard_count = clipboard_count + 1 |
| 398 | + clipboard_paths(clipboard_count) = join_path(current_dir, current_files(i)) |
| 399 | + clipboard_names(clipboard_count) = current_files(i) |
| 400 | + end if |
| 401 | + end do |
| 402 | + clipboard_is_cut = .false. |
| 403 | + has_clipboard = .true. |
| 404 | + call clear_all_selections(is_selected, selection_count, in_selection_mode) |
| 405 | + else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 406 | + clipboard_count = 1 |
| 407 | + clipboard_paths(1) = join_path(current_dir, current_files(selected)) |
| 408 | + clipboard_names(1) = current_files(selected) |
| 409 | + clipboard_source_path = clipboard_paths(1) |
| 410 | + clipboard_source_name = clipboard_names(1) |
| 411 | + clipboard_is_cut = .false. |
| 412 | + has_clipboard = .true. |
| 413 | + end if |
| 414 | + case(24) ! Alt+x - cut |
| 415 | + if (selection_count > 0) then |
| 416 | + clipboard_count = 0 |
| 417 | + do i = 1, current_count |
| 418 | + if (is_selected(i) .and. trim(current_files(i)) /= "." .and. & |
| 419 | + trim(current_files(i)) /= "..") then |
| 420 | + clipboard_count = clipboard_count + 1 |
| 421 | + clipboard_paths(clipboard_count) = join_path(current_dir, current_files(i)) |
| 422 | + clipboard_names(clipboard_count) = current_files(i) |
| 423 | + end if |
| 424 | + end do |
| 425 | + clipboard_is_cut = .true. |
| 426 | + has_clipboard = .true. |
| 427 | + call clear_all_selections(is_selected, selection_count, in_selection_mode) |
| 428 | + else if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 429 | + clipboard_count = 1 |
| 430 | + clipboard_paths(1) = join_path(current_dir, current_files(selected)) |
| 431 | + clipboard_names(1) = current_files(selected) |
| 432 | + clipboard_source_path = clipboard_paths(1) |
| 433 | + clipboard_source_name = clipboard_names(1) |
| 434 | + clipboard_is_cut = .true. |
| 435 | + has_clipboard = .true. |
| 436 | + end if |
| 437 | + case(16) ! Alt+p - paste |
| 438 | + if (has_clipboard) then |
| 439 | + if (clipboard_count > 1) then |
| 440 | + call execute_multi_paste(clipboard_paths, clipboard_names, clipboard_count, & |
| 441 | + clipboard_is_cut, current_dir, current_files(selected), & |
| 442 | + current_is_dir(selected)) |
| 443 | + else |
| 444 | + call execute_paste(clipboard_paths(1), clipboard_is_cut, current_dir, & |
| 445 | + current_files(selected), current_is_dir(selected)) |
| 446 | + end if |
| 447 | + if (clipboard_is_cut) then |
| 448 | + has_clipboard = .false. |
| 449 | + clipboard_count = 0 |
| 450 | + end if |
| 451 | + end if |
| 452 | + end select |
| 453 | + ! Alt key processed, skip rest of case(27) |
| 454 | + cycle |
| 455 | + end if |
| 456 | + |
| 457 | + ! Handle arrow keys (only if not Alt key) |
| 458 | + if (.true.) then |
| 241 | 459 | ! Process arrow keys normally |
| 242 | 460 | if (move_mode) then |
| 243 | 461 | ! In move mode, navigate directories only |
@@ -346,9 +564,12 @@ program fortress |
| 346 | 564 | mode = 'normal' |
| 347 | 565 | end if |
| 348 | 566 | end if |
| 349 | | - case(14) ! Alt+n - rename file/directory |
| 567 | + case(14) ! Alt+n - enter rename mode |
| 350 | 568 | if (trim(current_files(selected)) /= "." .and. trim(current_files(selected)) /= "..") then |
| 351 | | - call rename_file_prompt(current_dir, current_files(selected)) |
| 569 | + ! Enter rename mode - pre-fill with current filename |
| 570 | + in_rename_mode = .true. |
| 571 | + rename_buffer = current_files(selected) |
| 572 | + rename_cursor_pos = len_trim(rename_buffer) |
| 352 | 573 | end if |
| 353 | 574 | case(22) ! Alt+v - view file (always available) |
| 354 | 575 | if (.not. current_is_dir(selected)) then |