fortrangoingonforty/fuss / 1baaaa4

Browse files

alternate screen buffer, cleanup handlers, extracted refresh logic

Authored by espadonne
SHA
1baaaa4739fb020e271f928f03507a65193d4c9f
Parents
5abd8ce
Tree
82b815b

2 changed files

StatusFile+-
M src/fuss_main.f90 106 185
M src/terminal_module.f90 14 1
src/fuss_main.f90modified
@@ -24,6 +24,9 @@ program fuss
2424
         call build_and_display_tree(show_all)
2525
     end if
2626
 
27
+    ! Ensure terminal is always restored (safety cleanup)
28
+    call cleanup_terminal()
29
+
2730
 contains
2831
 
2932
     subroutine parse_arguments(show_all, interactive)
@@ -154,6 +157,13 @@ contains
154157
         end if
155158
     end subroutine build_and_display_tree
156159
 
160
+    subroutine cleanup_terminal()
161
+        ! Emergency cleanup - restores terminal to normal state
162
+        ! Call this before any exit or when calling external programs
163
+        call disable_raw_mode()
164
+        call exit_alternate_screen()
165
+    end subroutine cleanup_terminal
166
+
157167
     subroutine interactive_mode(show_all)
158168
         logical, intent(in) :: show_all
159169
         type(file_entry), allocatable :: files(:)
@@ -190,14 +200,12 @@ contains
190200
         ! Initialize hide_dotfiles before first use
191201
         hide_dotfiles = .false.
192202
 
193
-        ! Get files
203
+        ! Get files and mark incoming changes
194204
         if (show_all) then
195205
             call get_all_files(files, n_files)
196206
         else
197207
             call get_dirty_files(files, n_files)
198208
         end if
199
-
200
-        ! Mark files with incoming changes
201209
         call mark_incoming_changes(files, n_files)
202210
 
203211
         if (n_files == 0) then
@@ -227,6 +235,9 @@ contains
227235
         viewport_offset = 1
228236
         running = .true.
229237
 
238
+        ! Enter alternate screen buffer (preserves terminal content)
239
+        call enter_alternate_screen()
240
+
230241
         ! Enable raw terminal mode
231242
         call enable_raw_mode()
232243
 
@@ -273,128 +284,63 @@ contains
273284
                 if (.not. items(selected)%is_file) then
274285
                     call git_stage_directory(items(selected)%path)
275286
                     ! Refresh files after staging directory
276
-                    if (show_all) then
277
-                        call get_all_files(files, n_files)
278
-                    else
279
-                        call get_dirty_files(files, n_files)
280
-                    end if
281
-                    call mark_incoming_changes(files, n_files)
282
-                    call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
283
-                    if (selected > n_items .and. n_items > 0) selected = n_items
284
-                    if (n_items == 0) running = .false.
287
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
288
+                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
285289
                 ! Otherwise it's a file - stage individual file
286290
                 else if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
287291
                     call git_add_file(items(selected)%path)
288292
                     ! Refresh files after git add
289
-                    if (show_all) then
290
-                        call get_all_files(files, n_files)
291
-                    else
292
-                        call get_dirty_files(files, n_files)
293
-                    end if
294
-                    call mark_incoming_changes(files, n_files)
295
-                    call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
296
-                    if (selected > n_items .and. n_items > 0) selected = n_items
297
-                    if (n_items == 0) running = .false.
293
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
294
+                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
298295
                 end if
299296
             case ('u')  ! Unstage file (lowercase)
300297
                 if (items(selected)%is_file .and. items(selected)%is_staged) then
301298
                     call git_unstage_file(items(selected)%path)
302299
                     ! Refresh files after git unstage
303
-                    if (show_all) then
304
-                        call get_all_files(files, n_files)
305
-                    else
306
-                        call get_dirty_files(files, n_files)
307
-                    end if
308
-                    call mark_incoming_changes(files, n_files)
309
-                    call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
310
-                    if (selected > n_items .and. n_items > 0) selected = n_items
300
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
301
+                                            hide_dotfiles, selected, running)
311302
                 end if
312303
             case ('S')  ! Stage all (Shift+S to avoid conflict with up arrow 'A')
313304
                 call git_stage_all()
314305
                 ! Refresh files after staging all
315
-                if (show_all) then
316
-                    call get_all_files(files, n_files)
317
-                else
318
-                    call get_dirty_files(files, n_files)
319
-                end if
320
-                call mark_incoming_changes(files, n_files)
321
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
322
-                if (selected > n_items .and. n_items > 0) selected = n_items
323
-                if (n_items == 0) running = .false.
306
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
307
+                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
324308
             case ('U')  ! Unstage all (Shift+U)
325309
                 call git_unstage_all()
326310
                 ! Refresh files after unstaging all
327
-                if (show_all) then
328
-                    call get_all_files(files, n_files)
329
-                else
330
-                    call get_dirty_files(files, n_files)
331
-                end if
332
-                call mark_incoming_changes(files, n_files)
333
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
334
-                if (selected > n_items .and. n_items > 0) selected = n_items
311
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
312
+                                        hide_dotfiles, selected, running)
335313
             case ('m')  ! Commit (lowercase)
336314
                 call commit_prompt()
337315
                 ! Refresh files after commit
338
-                if (show_all) then
339
-                    call get_all_files(files, n_files)
340
-                else
341
-                    call get_dirty_files(files, n_files)
342
-                end if
343
-                call mark_incoming_changes(files, n_files)
344
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
345
-                if (selected > n_items .and. n_items > 0) selected = n_items
316
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
317
+                                        hide_dotfiles, selected, running)
346318
             case ('M')  ! Amend last commit (Shift+m)
347319
                 call amend_commit_prompt()
348320
                 ! Refresh files after amend commit
349
-                if (show_all) then
350
-                    call get_all_files(files, n_files)
351
-                else
352
-                    call get_dirty_files(files, n_files)
353
-                end if
354
-                call mark_incoming_changes(files, n_files)
355
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
356
-                if (selected > n_items .and. n_items > 0) selected = n_items
321
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
322
+                                        hide_dotfiles, selected, running)
357323
             case ('s')  ! Show git status (lowercase)
358324
                 call show_status_view()
359325
             case ('p')  ! Push (lowercase)
360326
                 call push_prompt()
361327
                 ! Refresh files after push
362
-                if (show_all) then
363
-                    call get_all_files(files, n_files)
364
-                else
365
-                    call get_dirty_files(files, n_files)
366
-                end if
367
-                call mark_incoming_changes(files, n_files)
368
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
369
-                if (selected > n_items .and. n_items > 0) selected = n_items
328
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
329
+                                        hide_dotfiles, selected, running)
370330
             case ('t')  ! Tag (lowercase)
371331
                 call tag_prompt()
372332
             case ('b')  ! Switch branch
373333
                 call branch_switch_prompt()
374334
                 ! Refresh files after branch switch
375
-                if (show_all) then
376
-                    call get_all_files(files, n_files)
377
-                else
378
-                    call get_dirty_files(files, n_files)
379
-                end if
380
-                call mark_incoming_changes(files, n_files)
381
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
382
-                if (selected > n_items .and. n_items > 0) selected = n_items
383
-                if (n_items == 0) running = .false.
335
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
336
+                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
384337
                 ! Update branch name display
385338
                 call get_repo_info(repo_name, branch_name)
386339
             case ('n')  ! Create new branch
387340
                 call branch_create_prompt()
388341
                 ! Refresh files after branch creation
389
-                if (show_all) then
390
-                    call get_all_files(files, n_files)
391
-                else
392
-                    call get_dirty_files(files, n_files)
393
-                end if
394
-                call mark_incoming_changes(files, n_files)
395
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
396
-                if (selected > n_items .and. n_items > 0) selected = n_items
397
-                if (n_items == 0) running = .false.
342
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
343
+                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
398344
                 ! Update branch name display
399345
                 call get_repo_info(repo_name, branch_name)
400346
             case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
@@ -403,16 +349,8 @@ contains
403349
             case ('f')  ! Git fetch
404350
                 call git_fetch()
405351
                 ! Refresh files after fetch and include files with incoming changes
406
-                if (show_all) then
407
-                    call get_all_files(files, n_files)
408
-                    call mark_incoming_changes(files, n_files)
409
-                else
410
-                    ! In non-all mode, add files that only have incoming changes
411
-                    call get_dirty_files(files, n_files)
412
-                    call add_incoming_files(files, n_files)
413
-                end if
414
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
415
-                if (selected > n_items .and. n_items > 0) selected = n_items
352
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
353
+                                        hide_dotfiles, selected, running, include_incoming=.true.)
416354
             case ('d')  ! Git diff with less
417355
                 if (items(selected)%is_file) then
418356
                     call git_diff_file(items(selected)%path, items(selected)%has_incoming)
@@ -429,89 +367,43 @@ contains
429367
                 if (items(selected)%is_file) then
430368
                     call delete_prompt(items(selected)%path, items(selected)%is_untracked)
431369
                     ! Refresh files after delete
432
-                    if (show_all) then
433
-                        call get_all_files(files, n_files)
434
-                    else
435
-                        call get_dirty_files(files, n_files)
436
-                    end if
437
-                    call mark_incoming_changes(files, n_files)
438
-                    call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
439
-                    if (selected > n_items .and. n_items > 0) selected = n_items
440
-                    if (n_items == 0) running = .false.
370
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
371
+                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
441372
                 end if
442373
             case ('x', 'X')  ! Discard changes
443374
                 if (items(selected)%is_file .and. (items(selected)%is_staged .or. items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
444375
                     call discard_prompt(items(selected)%path, items(selected)%is_staged, items(selected)%is_untracked)
445376
                     ! Refresh files after discard
446
-                    if (show_all) then
447
-                        call get_all_files(files, n_files)
448
-                    else
449
-                        call get_dirty_files(files, n_files)
450
-                    end if
451
-                    call mark_incoming_changes(files, n_files)
452
-                    call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
453
-                    if (selected > n_items .and. n_items > 0) selected = n_items
454
-                    if (n_items == 0) running = .false.
377
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
378
+                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
455379
                 end if
456380
             case ('l')  ! Git pull
457381
                 call git_pull()
458382
                 ! Refresh files after pull (incoming indicators will automatically clear)
459
-                if (show_all) then
460
-                    call get_all_files(files, n_files)
461
-                    call mark_incoming_changes(files, n_files)
462
-                else
463
-                    call get_dirty_files(files, n_files)
464
-                    call add_incoming_files(files, n_files)
465
-                end if
466
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
467
-                if (selected > n_items .and. n_items > 0) selected = n_items
383
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
384
+                                        hide_dotfiles, selected, running, include_incoming=.true.)
468385
                 ! Note: After successful pull, git diff will show no upstream differences
469386
                 ! so has_incoming will be .false. for all files automatically
470387
             case ('z')  ! Stash push (save changes)
471388
                 call stash_push_prompt()
472389
                 ! Refresh files after stash
473
-                if (show_all) then
474
-                    call get_all_files(files, n_files)
475
-                else
476
-                    call get_dirty_files(files, n_files)
477
-                end if
478
-                call mark_incoming_changes(files, n_files)
479
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
480
-                if (selected > n_items .and. n_items > 0) selected = n_items
481
-                if (n_items == 0) running = .false.
390
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
391
+                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
482392
             case ('Z')  ! Stash pop/apply (restore changes)
483393
                 call stash_pop_apply_prompt()
484394
                 ! Refresh files after stash pop/apply
485
-                if (show_all) then
486
-                    call get_all_files(files, n_files)
487
-                else
488
-                    call get_dirty_files(files, n_files)
489
-                end if
490
-                call mark_incoming_changes(files, n_files)
491
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
492
-                if (selected > n_items .and. n_items > 0) selected = n_items
395
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
396
+                                        hide_dotfiles, selected, running)
493397
             case ('y')  ! Cherry-pick (yank commit)
494398
                 call cherry_pick_prompt()
495399
                 ! Refresh files after cherry-pick
496
-                if (show_all) then
497
-                    call get_all_files(files, n_files)
498
-                else
499
-                    call get_dirty_files(files, n_files)
500
-                end if
501
-                call mark_incoming_changes(files, n_files)
502
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
503
-                if (selected > n_items .and. n_items > 0) selected = n_items
400
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
401
+                                        hide_dotfiles, selected, running)
504402
             case ('v')  ! Revert commit
505403
                 call revert_commit_prompt()
506404
                 ! Refresh files after revert
507
-                if (show_all) then
508
-                    call get_all_files(files, n_files)
509
-                else
510
-                    call get_dirty_files(files, n_files)
511
-                end if
512
-                call mark_incoming_changes(files, n_files)
513
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
514
-                if (selected > n_items .and. n_items > 0) selected = n_items
405
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
406
+                                        hide_dotfiles, selected, running)
515407
             case ('h')  ! Show commit history
516408
                 call history_browser_prompt()
517409
                 ! No refresh needed - read-only
@@ -521,38 +413,20 @@ contains
521413
             case ('G')  ! Merge branch (Shift+g)
522414
                 call merge_branch_prompt()
523415
                 ! Refresh files after merge
524
-                if (show_all) then
525
-                    call get_all_files(files, n_files)
526
-                else
527
-                    call get_dirty_files(files, n_files)
528
-                end if
529
-                call mark_incoming_changes(files, n_files)
530
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
531
-                if (selected > n_items .and. n_items > 0) selected = n_items
416
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
417
+                                        hide_dotfiles, selected, running)
532418
                 ! Update branch name display in case we merged
533419
                 call get_repo_info(repo_name, branch_name)
534420
             case ('O')  ! Reset (Shift+o - "Oh no, undo!")
535421
                 call reset_prompt()
536422
                 ! Refresh files after reset
537
-                if (show_all) then
538
-                    call get_all_files(files, n_files)
539
-                else
540
-                    call get_dirty_files(files, n_files)
541
-                end if
542
-                call mark_incoming_changes(files, n_files)
543
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
544
-                if (selected > n_items .and. n_items > 0) selected = n_items
423
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
424
+                                        hide_dotfiles, selected, running)
545425
             case ('I')  ! Interactive rebase (Shift+i)
546426
                 call rebase_prompt()
547427
                 ! Refresh files after rebase
548
-                if (show_all) then
549
-                    call get_all_files(files, n_files)
550
-                else
551
-                    call get_dirty_files(files, n_files)
552
-                end if
553
-                call mark_incoming_changes(files, n_files)
554
-                call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
555
-                if (selected > n_items .and. n_items > 0) selected = n_items
428
+                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
429
+                                        hide_dotfiles, selected, running)
556430
             case ('.')  ! Toggle hiding dotfiles and gitignored files
557431
                 hide_dotfiles = .not. hide_dotfiles
558432
                 ! Rebuild item list with new filter
@@ -569,15 +443,15 @@ contains
569443
             end select
570444
         end do
571445
 
572
-        ! Restore terminal
573
-        call disable_raw_mode()
446
+        ! Restore terminal to normal state
447
+        call cleanup_terminal()
574448
 
575449
         ! Free the tree
576450
         if (associated(tree_root)) then
577451
             call free_tree(tree_root)
578452
         end if
579453
 
580
-        ! Final display
454
+        ! Final display (now in normal terminal buffer)
581455
         call clear_screen()
582456
         call build_and_display_tree(show_all)
583457
     end subroutine interactive_mode
@@ -1336,4 +1210,51 @@ contains
13361210
         call read_key(key)
13371211
     end subroutine branch_delete_prompt
13381212
 
1213
+    subroutine refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
1214
+                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming)
1215
+        ! Centralized helper to refresh file list and rebuild tree
1216
+        ! Consolidates the pattern repeated 20+ times in the codebase
1217
+        logical, intent(in) :: show_all, hide_dotfiles
1218
+        type(file_entry), allocatable, intent(inout) :: files(:)
1219
+        integer, intent(inout) :: n_files, n_items, selected
1220
+        type(tree_node), pointer, intent(inout) :: tree_root
1221
+        type(selectable_item), allocatable, intent(inout) :: items(:)
1222
+        logical, intent(inout), optional :: running
1223
+        logical, intent(in), optional :: exit_if_empty, include_incoming
1224
+
1225
+        logical :: do_exit_if_empty, do_include_incoming
1226
+
1227
+        ! Handle optional parameters
1228
+        do_exit_if_empty = .false.
1229
+        if (present(exit_if_empty)) do_exit_if_empty = exit_if_empty
1230
+
1231
+        do_include_incoming = .false.
1232
+        if (present(include_incoming)) do_include_incoming = include_incoming
1233
+
1234
+        ! Get files based on mode
1235
+        if (show_all) then
1236
+            call get_all_files(files, n_files)
1237
+            call mark_incoming_changes(files, n_files)
1238
+        else
1239
+            call get_dirty_files(files, n_files)
1240
+            if (do_include_incoming) then
1241
+                ! For fetch/pull: also include files with only incoming changes
1242
+                call add_incoming_files(files, n_files)
1243
+            else
1244
+                call mark_incoming_changes(files, n_files)
1245
+            end if
1246
+        end if
1247
+
1248
+        ! Rebuild tree and flatten to items
1249
+        call build_item_list(files, n_files, items, n_items, tree_root, hide_dotfiles)
1250
+
1251
+        ! Adjust selection if needed
1252
+        if (selected > n_items .and. n_items > 0) selected = n_items
1253
+
1254
+        ! Exit if no items and exit_if_empty is set
1255
+        if (do_exit_if_empty .and. n_items == 0) then
1256
+            if (present(running)) running = .false.
1257
+        end if
1258
+    end subroutine refresh_and_rebuild
1259
+
13391260
 end program fuss
src/terminal_module.f90modified
@@ -3,9 +3,22 @@ module terminal_module
33
 
44
 contains
55
 
6
+    subroutine enter_alternate_screen()
7
+        ! Switch to alternate screen buffer (like vim, less, htop)
8
+        ! This preserves the main terminal content
9
+        print '(A)', achar(27) // '[?1049h'
10
+    end subroutine enter_alternate_screen
11
+
12
+    subroutine exit_alternate_screen()
13
+        ! Return to main screen buffer
14
+        ! Restores terminal to state before enter_alternate_screen()
15
+        print '(A)', achar(27) // '[?1049l'
16
+    end subroutine exit_alternate_screen
17
+
618
     subroutine clear_screen()
719
         ! ANSI escape code to clear screen and move cursor to top
8
-        print '(A)', achar(27) // '[2J' // achar(27) // '[H'
20
+        ! In alternate screen buffer, we just need to home cursor and clear
21
+        print '(A)', achar(27) // '[H' // achar(27) // '[2J'
922
     end subroutine clear_screen
1023
 
1124
     subroutine enable_raw_mode()