@@ -183,6 +183,11 @@ contains |
| 183 | 183 | character(len=32) :: search_buffer |
| 184 | 184 | integer :: search_length |
| 185 | 185 | integer(8) :: last_search_tick, current_tick, clock_rate |
| 186 | + ! Rename state for inline renaming |
| 187 | + logical :: in_rename_mode |
| 188 | + character(len=256) :: rename_buffer |
| 189 | + character(len=256) :: rename_original_name |
| 190 | + integer :: rename_cursor_pos |
| 186 | 191 | type(tree_node), pointer :: tree_root |
| 187 | 192 | |
| 188 | 193 | ! Initialize tree pointer |
@@ -256,6 +261,12 @@ contains |
| 256 | 261 | last_search_tick = 0 |
| 257 | 262 | call system_clock(count_rate=clock_rate) |
| 258 | 263 | |
| 264 | + ! Initialize rename state |
| 265 | + in_rename_mode = .false. |
| 266 | + rename_buffer = '' |
| 267 | + rename_original_name = '' |
| 268 | + rename_cursor_pos = 0 |
| 269 | + |
| 259 | 270 | ! Partial redraw optimization: initialize tracking state |
| 260 | 271 | prev_selected = 0 ! Force initial draw |
| 261 | 272 | prev_viewport = 0 |
@@ -283,14 +294,16 @@ contains |
| 283 | 294 | ! Full redraw needed: viewport scrolled or forced refresh |
| 284 | 295 | call clear_screen() |
| 285 | 296 | call draw_interactive_tree(tree_root, items, n_items, selected, & |
| 286 | | - repo_name, branch_name, viewport_offset, visible_items, top_padding, mode) |
| 297 | + repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, & |
| 298 | + in_rename_mode, rename_buffer, rename_cursor_pos) |
| 287 | 299 | needs_full_redraw = .false. |
| 288 | 300 | else if (selected /= prev_selected) then |
| 289 | 301 | ! Only selection changed within same viewport - still need full redraw for now |
| 290 | 302 | ! TODO: Could optimize this with partial line updates in the future |
| 291 | 303 | call clear_screen() |
| 292 | 304 | call draw_interactive_tree(tree_root, items, n_items, selected, & |
| 293 | | - repo_name, branch_name, viewport_offset, visible_items, top_padding, mode) |
| 305 | + repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, & |
| 306 | + in_rename_mode, rename_buffer, rename_cursor_pos) |
| 294 | 307 | end if |
| 295 | 308 | |
| 296 | 309 | ! Update tracking state |
@@ -340,7 +353,8 @@ contains |
| 340 | 353 | call execute_command_line('stty sane < /dev/tty') |
| 341 | 354 | call clear_screen() |
| 342 | 355 | call draw_interactive_tree(tree_root, items, n_items, selected, & |
| 343 | | - repo_name, branch_name, viewport_offset, visible_items, top_padding, mode) |
| 356 | + repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, & |
| 357 | + in_rename_mode, rename_buffer, rename_cursor_pos) |
| 344 | 358 | ! Restore cbreak mode |
| 345 | 359 | call enable_raw_mode() |
| 346 | 360 | cycle ! Skip rest of key handling |
@@ -364,15 +378,38 @@ contains |
| 364 | 378 | cycle |
| 365 | 379 | end if |
| 366 | 380 | |
| 367 | | - ! Handle ESC key - exit git mode or clear search |
| 381 | + ! Check for alt-n to enter rename mode (available in both modes) |
| 382 | + ! alt-n is encoded as achar(1 + ichar('n') - ichar('a')) = achar(14) |
| 383 | + if (key == achar(14) .and. .not. in_rename_mode) then |
| 384 | + ! Enter rename mode |
| 385 | + if (associated(items(selected)%node)) then |
| 386 | + in_rename_mode = .true. |
| 387 | + rename_original_name = items(selected)%node%name |
| 388 | + rename_buffer = items(selected)%node%name |
| 389 | + rename_cursor_pos = len_trim(rename_buffer) |
| 390 | + needs_full_redraw = .true. |
| 391 | + end if |
| 392 | + cycle |
| 393 | + end if |
| 394 | + |
| 395 | + ! Handle ESC key - exit rename mode, git mode, or clear search |
| 368 | 396 | if (key == achar(27)) then |
| 369 | | - if (mode == 'git') then |
| 397 | + if (in_rename_mode) then |
| 398 | + ! Cancel rename - restore original name |
| 399 | + in_rename_mode = .false. |
| 400 | + rename_buffer = '' |
| 401 | + rename_original_name = '' |
| 402 | + rename_cursor_pos = 0 |
| 403 | + needs_full_redraw = .true. |
| 404 | + cycle |
| 405 | + else if (mode == 'git') then |
| 370 | 406 | mode = 'normal' |
| 371 | 407 | ! Temporarily restore terminal to flush output properly |
| 372 | 408 | call execute_command_line('stty sane < /dev/tty') |
| 373 | 409 | call clear_screen() |
| 374 | 410 | call draw_interactive_tree(tree_root, items, n_items, selected, & |
| 375 | | - repo_name, branch_name, viewport_offset, visible_items, top_padding, mode) |
| 411 | + repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, & |
| 412 | + in_rename_mode, rename_buffer, rename_cursor_pos) |
| 376 | 413 | ! Restore cbreak mode |
| 377 | 414 | call enable_raw_mode() |
| 378 | 415 | cycle |
@@ -387,6 +424,87 @@ contains |
| 387 | 424 | cycle |
| 388 | 425 | end if |
| 389 | 426 | |
| 427 | + ! Rename mode key handling - intercept all keys when in rename mode |
| 428 | + if (in_rename_mode) then |
| 429 | + ! DEBUG: Log all keys in rename mode |
| 430 | + open(99, file='/tmp/fuss_debug.log', position='append') |
| 431 | + write(99, '(A,I0,A,I0)') 'RENAME MODE: key code=', ichar(key), ' decimal=', ichar(key) |
| 432 | + close(99) |
| 433 | + |
| 434 | + ! Handle Enter (multiple possible codes) or Tab to confirm rename |
| 435 | + ! achar(10) = LF, achar(13) = CR, achar(9) = Tab, achar(0) = null |
| 436 | + if (key == achar(10) .or. key == achar(13) .or. key == achar(9) .or. & |
| 437 | + key == achar(0) .or. ichar(key) == 10 .or. ichar(key) == 13) then |
| 438 | + ! Enter or Tab - execute rename |
| 439 | + open(99, file='/tmp/fuss_debug.log', position='append') |
| 440 | + write(99, '(A)') 'RENAME MODE: ENTER/TAB detected - executing rename' |
| 441 | + close(99) |
| 442 | + call execute_rename(items(selected)%path, trim(rename_buffer)) |
| 443 | + in_rename_mode = .false. |
| 444 | + rename_buffer = '' |
| 445 | + rename_original_name = '' |
| 446 | + rename_cursor_pos = 0 |
| 447 | + ! Force refresh to show renamed file |
| 448 | + call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, & |
| 449 | + hide_dotfiles, selected, running, force_refresh=.true.) |
| 450 | + needs_full_redraw = .true. |
| 451 | + cycle |
| 452 | + else if (key == achar(127) .or. key == achar(8)) then |
| 453 | + ! Backspace - delete character before cursor |
| 454 | + if (rename_cursor_pos > 0) then |
| 455 | + ! Delete character before cursor |
| 456 | + if (rename_cursor_pos == len_trim(rename_buffer)) then |
| 457 | + ! Cursor at end - simple delete |
| 458 | + rename_buffer = rename_buffer(1:len_trim(rename_buffer)-1) |
| 459 | + else |
| 460 | + ! Cursor in middle - delete and shift left |
| 461 | + rename_buffer = rename_buffer(1:rename_cursor_pos-1) // & |
| 462 | + rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer)) |
| 463 | + end if |
| 464 | + rename_cursor_pos = rename_cursor_pos - 1 |
| 465 | + needs_full_redraw = .true. |
| 466 | + end if |
| 467 | + cycle |
| 468 | + else if (key == 'C') then |
| 469 | + ! Right arrow - move cursor right |
| 470 | + if (rename_cursor_pos < len_trim(rename_buffer)) then |
| 471 | + rename_cursor_pos = rename_cursor_pos + 1 |
| 472 | + needs_full_redraw = .true. |
| 473 | + end if |
| 474 | + cycle |
| 475 | + else if (key == 'D') then |
| 476 | + ! Left arrow - move cursor left |
| 477 | + if (rename_cursor_pos > 0) then |
| 478 | + rename_cursor_pos = rename_cursor_pos - 1 |
| 479 | + needs_full_redraw = .true. |
| 480 | + end if |
| 481 | + cycle |
| 482 | + else if (key == 'A' .or. key == 'B') then |
| 483 | + ! Up/Down arrows - ignore in rename mode |
| 484 | + cycle |
| 485 | + else if ((key >= 'a' .and. key <= 'z') .or. & |
| 486 | + (key >= 'A' .and. key <= 'Z') .or. & |
| 487 | + (key >= '0' .and. key <= '9') .or. & |
| 488 | + key == '_' .or. key == '-' .or. key == '.' .or. key == ' ') then |
| 489 | + ! Printable character - insert at cursor position |
| 490 | + if (len_trim(rename_buffer) < 255) then |
| 491 | + if (rename_cursor_pos == len_trim(rename_buffer)) then |
| 492 | + ! Cursor at end - simple append |
| 493 | + rename_buffer = trim(rename_buffer) // key |
| 494 | + else |
| 495 | + ! Cursor in middle - insert and shift right |
| 496 | + rename_buffer = rename_buffer(1:rename_cursor_pos) // key // & |
| 497 | + rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer)) |
| 498 | + end if |
| 499 | + rename_cursor_pos = rename_cursor_pos + 1 |
| 500 | + needs_full_redraw = .true. |
| 501 | + end if |
| 502 | + cycle |
| 503 | + end if |
| 504 | + ! Ignore all other keys in rename mode |
| 505 | + cycle |
| 506 | + end if |
| 507 | + |
| 390 | 508 | ! Fuzzy search in normal mode - handle any printable character |
| 391 | 509 | ! Exclude A, B, C, D since those are arrow key codes after escape sequence processing |
| 392 | 510 | if (mode == 'normal') then |
@@ -1736,4 +1854,64 @@ contains |
| 1736 | 1854 | end do |
| 1737 | 1855 | end subroutine to_lowercase |
| 1738 | 1856 | |
| 1857 | + subroutine execute_rename(old_path, new_name) |
| 1858 | + ! Execute file/directory rename |
| 1859 | + character(len=*), intent(in) :: old_path, new_name |
| 1860 | + character(len=1024) :: dirname, new_path, command |
| 1861 | + integer :: status, last_slash |
| 1862 | + logical :: file_exists |
| 1863 | + |
| 1864 | + ! Validate new name |
| 1865 | + if (len_trim(new_name) == 0) then |
| 1866 | + call show_message_and_wait('Error: Filename cannot be empty!') |
| 1867 | + return |
| 1868 | + end if |
| 1869 | + |
| 1870 | + ! Get directory name from old path |
| 1871 | + last_slash = index(old_path, '/', back=.true.) |
| 1872 | + if (last_slash > 0) then |
| 1873 | + dirname = old_path(1:last_slash) |
| 1874 | + else |
| 1875 | + dirname = './' |
| 1876 | + end if |
| 1877 | + |
| 1878 | + ! Build new full path |
| 1879 | + write(new_path, '(A,A)') trim(dirname), trim(new_name) |
| 1880 | + |
| 1881 | + ! Check if new path already exists (and it's not the same file) |
| 1882 | + if (trim(new_path) /= trim(old_path)) then |
| 1883 | + inquire(file=trim(new_path), exist=file_exists) |
| 1884 | + if (file_exists) then |
| 1885 | + call show_message_and_wait('Error: A file with that name already exists!') |
| 1886 | + return |
| 1887 | + end if |
| 1888 | + end if |
| 1889 | + |
| 1890 | + ! If it's the same name, do nothing |
| 1891 | + if (trim(new_path) == trim(old_path)) then |
| 1892 | + return |
| 1893 | + end if |
| 1894 | + |
| 1895 | + ! Execute rename using mv command |
| 1896 | + write(command, '(A,A,A,A,A)') 'mv "', trim(old_path), '" "', trim(new_path), '" 2>/dev/null' |
| 1897 | + call execute_command_line(trim(command), exitstat=status) |
| 1898 | + |
| 1899 | + if (status /= 0) then |
| 1900 | + call show_message_and_wait('Error: Rename failed! Check permissions.') |
| 1901 | + return |
| 1902 | + end if |
| 1903 | + |
| 1904 | + end subroutine execute_rename |
| 1905 | + |
| 1906 | + subroutine show_message_and_wait(message) |
| 1907 | + ! Show a message and wait for user to press a key |
| 1908 | + character(len=*), intent(in) :: message |
| 1909 | + character(len=1) :: key |
| 1910 | + |
| 1911 | + print '(A)', '' |
| 1912 | + print '(A)', trim(message) |
| 1913 | + print '(A)', 'Press any key to continue...' |
| 1914 | + call wait_for_key(key) |
| 1915 | + end subroutine show_message_and_wait |
| 1916 | + |
| 1739 | 1917 | end program fuss |