fortrangoingonforty/fuss / d14d0a1

Browse files

cleanup disk i/o

Authored by espadonne
SHA
d14d0a18726238325b8970eaade8a0e937cbbc4e
Parents
53c7a91
Tree
8f3fd7b

7 changed files

StatusFile+-
A src/cache_module.f90 98 0
M src/fuss_main.f90 87 33
C src/fuss_main.f90.bak 0 0
C src/fuss_main.f90.bak2 0 0
M src/git_module.f90 112 47
C src/git_module.f90.phase23 0 0
M src/terminal_module.f90 7 4
src/cache_module.f90added
@@ -0,0 +1,98 @@
1
+module cache_module
2
+    use types_module
3
+    implicit none
4
+
5
+    ! File cache type for storing git command results
6
+    type :: file_cache
7
+        real(8) :: timestamp
8
+        type(file_entry), allocatable :: files(:)
9
+        integer :: n_files
10
+        logical :: valid
11
+    end type
12
+
13
+    ! Global caches for dirty files and all files
14
+    type(file_cache) :: dirty_cache, all_cache
15
+
16
+    ! Cache time-to-live (500ms = 0.5 seconds)
17
+    ! After this time, cache is considered stale
18
+    real(8), parameter :: CACHE_TTL = 0.5d0
19
+
20
+contains
21
+
22
+    ! Check if a cache entry is still valid
23
+    function cache_valid(cache) result(valid)
24
+        type(file_cache), intent(in) :: cache
25
+        logical :: valid
26
+        real(8) :: current_time
27
+
28
+        if (.not. cache%valid) then
29
+            valid = .false.
30
+            return
31
+        end if
32
+
33
+        call cpu_time(current_time)
34
+        valid = (current_time - cache%timestamp) < CACHE_TTL
35
+    end function cache_valid
36
+
37
+    ! Invalidate a cache (mark as stale)
38
+    subroutine invalidate_cache(cache)
39
+        type(file_cache), intent(inout) :: cache
40
+        cache%valid = .false.
41
+    end subroutine invalidate_cache
42
+
43
+    ! Invalidate all caches (called after state-changing operations)
44
+    subroutine invalidate_all_caches()
45
+        call invalidate_cache(dirty_cache)
46
+        call invalidate_cache(all_cache)
47
+    end subroutine invalidate_all_caches
48
+
49
+    ! Update cache with new data
50
+    subroutine update_cache(cache, files, n_files)
51
+        type(file_cache), intent(inout) :: cache
52
+        type(file_entry), intent(in) :: files(:)
53
+        integer, intent(in) :: n_files
54
+        integer :: i
55
+
56
+        ! Free old data
57
+        if (allocated(cache%files)) deallocate(cache%files)
58
+
59
+        ! Allocate and copy new data
60
+        allocate(cache%files(n_files))
61
+        do i = 1, n_files
62
+            cache%files(i) = files(i)
63
+        end do
64
+
65
+        cache%n_files = n_files
66
+        call cpu_time(cache%timestamp)
67
+        cache%valid = .true.
68
+    end subroutine update_cache
69
+
70
+    ! Retrieve cached data
71
+    subroutine get_cached_files(cache, files, n_files)
72
+        type(file_cache), intent(in) :: cache
73
+        type(file_entry), allocatable, intent(out) :: files(:)
74
+        integer, intent(out) :: n_files
75
+        integer :: i
76
+
77
+        n_files = cache%n_files
78
+
79
+        if (allocated(files)) deallocate(files)
80
+        allocate(files(n_files))
81
+
82
+        do i = 1, n_files
83
+            files(i) = cache%files(i)
84
+        end do
85
+    end subroutine get_cached_files
86
+
87
+    ! Initialize caches (call at program start)
88
+    subroutine init_caches()
89
+        dirty_cache%valid = .false.
90
+        dirty_cache%timestamp = 0.0d0
91
+        dirty_cache%n_files = 0
92
+
93
+        all_cache%valid = .false.
94
+        all_cache%timestamp = 0.0d0
95
+        all_cache%n_files = 0
96
+    end subroutine init_caches
97
+
98
+end module cache_module
src/fuss_main.f90modified
@@ -5,12 +5,16 @@ program fuss
55
     use tree_module
66
     use display_module
77
     use terminal_module
8
+    use cache_module
89
     implicit none
910
 
1011
     ! Main program variables
1112
     logical :: show_all, interactive
1213
     character(len=:), allocatable :: root_path
1314
 
15
+    ! Initialize caches for performance optimization
16
+    call init_caches()
17
+
1418
     ! Parse command line arguments
1519
     call parse_arguments(show_all, interactive)
1620
 
@@ -124,9 +128,9 @@ contains
124128
         character(len=1024) :: buffer
125129
         integer :: status
126130
 
127
-        call execute_command_line('pwd > /tmp/fuss_pwd.txt', exitstat=status)
131
+        call execute_command_line('pwd > /tmp/fuss_tmp.txt', exitstat=status)
128132
 
129
-        open(unit=99, file='/tmp/fuss_pwd.txt', status='old', action='read')
133
+        open(unit=99, file='/tmp/fuss_tmp.txt', status='old', action='read')
130134
         read(99, '(A)') buffer
131135
         close(99, status='delete')
132136
 
@@ -173,6 +177,8 @@ contains
173177
         logical :: running, hide_dotfiles
174178
         character(len=256) :: repo_name, branch_name, term_program
175179
         integer :: term_height, viewport_offset, visible_items, top_padding
180
+        integer :: prev_selected, prev_viewport
181
+        logical :: needs_full_redraw
176182
         type(tree_node), pointer :: tree_root
177183
 
178184
         ! Initialize tree pointer
@@ -235,6 +241,11 @@ contains
235241
         viewport_offset = 1
236242
         running = .true.
237243
 
244
+        ! Partial redraw optimization: initialize tracking state
245
+        prev_selected = 0  ! Force initial draw
246
+        prev_viewport = 0
247
+        needs_full_redraw = .true.
248
+
238249
         ! Enter alternate screen buffer (preserves terminal content)
239250
         call enter_alternate_screen()
240251
 
@@ -252,10 +263,24 @@ contains
252263
                 viewport_offset = n_items - visible_items + 1
253264
             end if
254265
 
255
-            ! Clear screen and redraw
256
-            call clear_screen()
257
-            call draw_interactive_tree(tree_root, items, n_items, selected, &
258
-                                       repo_name, branch_name, viewport_offset, visible_items, top_padding)
266
+            ! Conditional redraw for performance optimization
267
+            if (needs_full_redraw .or. viewport_offset /= prev_viewport) then
268
+                ! Full redraw needed: viewport scrolled or forced refresh
269
+                call clear_screen()
270
+                call draw_interactive_tree(tree_root, items, n_items, selected, &
271
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
272
+                needs_full_redraw = .false.
273
+            else if (selected /= prev_selected) then
274
+                ! Only selection changed within same viewport - still need full redraw for now
275
+                ! TODO: Could optimize this with partial line updates in the future
276
+                call clear_screen()
277
+                call draw_interactive_tree(tree_root, items, n_items, selected, &
278
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
279
+            end if
280
+
281
+            ! Update tracking state
282
+            prev_selected = selected
283
+            prev_viewport = viewport_offset
259284
 
260285
             ! Read key
261286
             call read_key(key)
@@ -278,6 +303,8 @@ contains
278303
                     call rebuild_item_list_from_tree(tree_root, items, n_items, hide_dotfiles)
279304
                     ! Adjust selection if needed
280305
                     if (selected > n_items .and. n_items > 0) selected = n_items
306
+                    ! Force full redraw after tree structure change
307
+                    needs_full_redraw = .true.
281308
                 end if
282309
             case ('a')  ! Stage file or directory (lowercase to avoid conflict with arrow A)
283310
                 ! Check if it's a directory - stage all files in it
@@ -285,62 +312,72 @@ contains
285312
                     call git_stage_directory(items(selected)%path)
286313
                     ! Refresh files after staging directory
287314
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
288
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
315
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
316
+                    needs_full_redraw = .true.
289317
                 ! Otherwise it's a file - stage individual file
290318
                 else if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
291319
                     call git_add_file(items(selected)%path)
292320
                     ! Refresh files after git add
293321
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
294
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
322
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
323
+                    needs_full_redraw = .true.
295324
                 end if
296325
             case ('u')  ! Unstage file (lowercase)
297326
                 if (items(selected)%is_file .and. items(selected)%is_staged) then
298327
                     call git_unstage_file(items(selected)%path)
299328
                     ! Refresh files after git unstage
300329
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
301
-                                            hide_dotfiles, selected, running)
330
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
331
+                    needs_full_redraw = .true.
302332
                 end if
303333
             case ('S')  ! Stage all (Shift+S to avoid conflict with up arrow 'A')
304334
                 call git_stage_all()
305335
                 ! Refresh files after staging all
306336
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
307
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
337
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
338
+                    needs_full_redraw = .true.
308339
             case ('U')  ! Unstage all (Shift+U)
309340
                 call git_unstage_all()
310341
                 ! Refresh files after unstaging all
311342
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
312
-                                        hide_dotfiles, selected, running)
343
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
344
+                    needs_full_redraw = .true.
313345
             case ('m')  ! Commit (lowercase)
314346
                 call commit_prompt()
315347
                 ! Refresh files after commit
316348
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
317
-                                        hide_dotfiles, selected, running)
349
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
350
+                    needs_full_redraw = .true.
318351
             case ('M')  ! Amend last commit (Shift+m)
319352
                 call amend_commit_prompt()
320353
                 ! Refresh files after amend commit
321354
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
322
-                                        hide_dotfiles, selected, running)
355
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
356
+                    needs_full_redraw = .true.
323357
             case ('s')  ! Show git status (lowercase)
324358
                 call show_status_view()
325359
             case ('p')  ! Push (lowercase)
326360
                 call push_prompt()
327361
                 ! Refresh files after push
328362
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
329
-                                        hide_dotfiles, selected, running)
363
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
364
+                    needs_full_redraw = .true.
330365
             case ('t')  ! Tag (lowercase)
331366
                 call tag_prompt()
332367
             case ('b')  ! Switch branch
333368
                 call branch_switch_prompt()
334369
                 ! Refresh files after branch switch
335370
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
336
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
371
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
372
+                    needs_full_redraw = .true.
337373
                 ! Update branch name display
338374
                 call get_repo_info(repo_name, branch_name)
339375
             case ('n')  ! Create new branch
340376
                 call branch_create_prompt()
341377
                 ! Refresh files after branch creation
342378
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
343
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
379
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
380
+                    needs_full_redraw = .true.
344381
                 ! Update branch name display
345382
                 call get_repo_info(repo_name, branch_name)
346383
             case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
@@ -350,7 +387,8 @@ contains
350387
                 call git_fetch()
351388
                 ! Refresh files after fetch and include files with incoming changes
352389
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
353
-                                        hide_dotfiles, selected, running, include_incoming=.true.)
390
+                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
391
+                    needs_full_redraw = .true.
354392
             case ('d')  ! Git diff with less
355393
                 if (items(selected)%is_file) then
356394
                     call git_diff_file(items(selected)%path, items(selected)%has_incoming)
@@ -368,42 +406,49 @@ contains
368406
                     call delete_prompt(items(selected)%path, items(selected)%is_untracked)
369407
                     ! Refresh files after delete
370408
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
371
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
409
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
410
+                    needs_full_redraw = .true.
372411
                 end if
373412
             case ('x', 'X')  ! Discard changes
374413
                 if (items(selected)%is_file .and. (items(selected)%is_staged .or. items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
375414
                     call discard_prompt(items(selected)%path, items(selected)%is_staged, items(selected)%is_untracked)
376415
                     ! Refresh files after discard
377416
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
378
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
417
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
418
+                    needs_full_redraw = .true.
379419
                 end if
380420
             case ('l')  ! Git pull
381421
                 call git_pull()
382422
                 ! Refresh files after pull (incoming indicators will automatically clear)
383423
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
384
-                                        hide_dotfiles, selected, running, include_incoming=.true.)
424
+                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
425
+                    needs_full_redraw = .true.
385426
                 ! Note: After successful pull, git diff will show no upstream differences
386427
                 ! so has_incoming will be .false. for all files automatically
387428
             case ('z')  ! Stash push (save changes)
388429
                 call stash_push_prompt()
389430
                 ! Refresh files after stash
390431
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
391
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
432
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
433
+                    needs_full_redraw = .true.
392434
             case ('Z')  ! Stash pop/apply (restore changes)
393435
                 call stash_pop_apply_prompt()
394436
                 ! Refresh files after stash pop/apply
395437
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
396
-                                        hide_dotfiles, selected, running)
438
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
439
+                    needs_full_redraw = .true.
397440
             case ('y')  ! Cherry-pick (yank commit)
398441
                 call cherry_pick_prompt()
399442
                 ! Refresh files after cherry-pick
400443
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
401
-                                        hide_dotfiles, selected, running)
444
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
445
+                    needs_full_redraw = .true.
402446
             case ('v')  ! Revert commit
403447
                 call revert_commit_prompt()
404448
                 ! Refresh files after revert
405449
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
406
-                                        hide_dotfiles, selected, running)
450
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
451
+                    needs_full_redraw = .true.
407452
             case ('h')  ! Show commit history
408453
                 call history_browser_prompt()
409454
                 ! No refresh needed - read-only
@@ -414,19 +459,22 @@ contains
414459
                 call merge_branch_prompt()
415460
                 ! Refresh files after merge
416461
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
417
-                                        hide_dotfiles, selected, running)
462
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
463
+                    needs_full_redraw = .true.
418464
                 ! Update branch name display in case we merged
419465
                 call get_repo_info(repo_name, branch_name)
420466
             case ('O')  ! Reset (Shift+o - "Oh no, undo!")
421467
                 call reset_prompt()
422468
                 ! Refresh files after reset
423469
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
424
-                                        hide_dotfiles, selected, running)
470
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
471
+                    needs_full_redraw = .true.
425472
             case ('I')  ! Interactive rebase (Shift+i)
426473
                 call rebase_prompt()
427474
                 ! Refresh files after rebase
428475
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
429
-                                        hide_dotfiles, selected, running)
476
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
477
+                    needs_full_redraw = .true.
430478
             case ('.')  ! Toggle hiding dotfiles and gitignored files
431479
                 hide_dotfiles = .not. hide_dotfiles
432480
                 ! Rebuild item list with new filter
@@ -434,6 +482,8 @@ contains
434482
                 ! Adjust selection and visible_items for new item count
435483
                 if (selected > n_items .and. n_items > 0) selected = n_items
436484
                 if (n_items > 0 .and. selected < 1) selected = 1
485
+                ! Force full redraw after filter change
486
+                needs_full_redraw = .true.
437487
                 ! Recalculate visible_items in case n_items changed
438488
                 visible_items = term_height - 6
439489
                 if (visible_items < 3) visible_items = 3
@@ -1211,18 +1261,19 @@ contains
12111261
     end subroutine branch_delete_prompt
12121262
 
12131263
     subroutine refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
1214
-                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming)
1264
+                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming, force_refresh)
12151265
         ! Centralized helper to refresh file list and rebuild tree
12161266
         ! Consolidates the pattern repeated 20+ times in the codebase
1267
+        ! Now with caching support for performance optimization
12171268
         logical, intent(in) :: show_all, hide_dotfiles
12181269
         type(file_entry), allocatable, intent(inout) :: files(:)
12191270
         integer, intent(inout) :: n_files, n_items, selected
12201271
         type(tree_node), pointer, intent(inout) :: tree_root
12211272
         type(selectable_item), allocatable, intent(inout) :: items(:)
12221273
         logical, intent(inout), optional :: running
1223
-        logical, intent(in), optional :: exit_if_empty, include_incoming
1274
+        logical, intent(in), optional :: exit_if_empty, include_incoming, force_refresh
12241275
 
1225
-        logical :: do_exit_if_empty, do_include_incoming
1276
+        logical :: do_exit_if_empty, do_include_incoming, do_force_refresh
12261277
 
12271278
         ! Handle optional parameters
12281279
         do_exit_if_empty = .false.
@@ -1231,12 +1282,15 @@ contains
12311282
         do_include_incoming = .false.
12321283
         if (present(include_incoming)) do_include_incoming = include_incoming
12331284
 
1234
-        ! Get files based on mode
1285
+        do_force_refresh = .false.
1286
+        if (present(force_refresh)) do_force_refresh = force_refresh
1287
+
1288
+        ! Get files based on mode (with caching support)
12351289
         if (show_all) then
1236
-            call get_all_files(files, n_files)
1290
+            call get_all_files(files, n_files, force_refresh=do_force_refresh)
12371291
             call mark_incoming_changes(files, n_files)
12381292
         else
1239
-            call get_dirty_files(files, n_files)
1293
+            call get_dirty_files(files, n_files, force_refresh=do_force_refresh)
12401294
             if (do_include_incoming) then
12411295
                 ! For fetch/pull: also include files with only incoming changes
12421296
                 call add_incoming_files(files, n_files)
src/fuss_main.f90 → src/fuss_main.f90.bakcopied (98% similarity)
@@ -5,12 +5,16 @@ program fuss
55
     use tree_module
66
     use display_module
77
     use terminal_module
8
+    use cache_module
89
     implicit none
910
 
1011
     ! Main program variables
1112
     logical :: show_all, interactive
1213
     character(len=:), allocatable :: root_path
1314
 
15
+    ! Initialize caches for performance optimization
16
+    call init_caches()
17
+
1418
     ! Parse command line arguments
1519
     call parse_arguments(show_all, interactive)
1620
 
@@ -1211,18 +1215,19 @@ contains
12111215
     end subroutine branch_delete_prompt
12121216
 
12131217
     subroutine refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
1214
-                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming)
1218
+                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming, force_refresh)
12151219
         ! Centralized helper to refresh file list and rebuild tree
12161220
         ! Consolidates the pattern repeated 20+ times in the codebase
1221
+        ! Now with caching support for performance optimization
12171222
         logical, intent(in) :: show_all, hide_dotfiles
12181223
         type(file_entry), allocatable, intent(inout) :: files(:)
12191224
         integer, intent(inout) :: n_files, n_items, selected
12201225
         type(tree_node), pointer, intent(inout) :: tree_root
12211226
         type(selectable_item), allocatable, intent(inout) :: items(:)
12221227
         logical, intent(inout), optional :: running
1223
-        logical, intent(in), optional :: exit_if_empty, include_incoming
1228
+        logical, intent(in), optional :: exit_if_empty, include_incoming, force_refresh
12241229
 
1225
-        logical :: do_exit_if_empty, do_include_incoming
1230
+        logical :: do_exit_if_empty, do_include_incoming, do_force_refresh
12261231
 
12271232
         ! Handle optional parameters
12281233
         do_exit_if_empty = .false.
@@ -1231,12 +1236,15 @@ contains
12311236
         do_include_incoming = .false.
12321237
         if (present(include_incoming)) do_include_incoming = include_incoming
12331238
 
1234
-        ! Get files based on mode
1239
+        do_force_refresh = .false.
1240
+        if (present(force_refresh)) do_force_refresh = force_refresh
1241
+
1242
+        ! Get files based on mode (with caching support)
12351243
         if (show_all) then
1236
-            call get_all_files(files, n_files)
1244
+            call get_all_files(files, n_files, force_refresh=do_force_refresh)
12371245
             call mark_incoming_changes(files, n_files)
12381246
         else
1239
-            call get_dirty_files(files, n_files)
1247
+            call get_dirty_files(files, n_files, force_refresh=do_force_refresh)
12401248
             if (do_include_incoming) then
12411249
                 ! For fetch/pull: also include files with only incoming changes
12421250
                 call add_incoming_files(files, n_files)
src/fuss_main.f90 → src/fuss_main.f90.bak2copied (94% similarity)
@@ -5,12 +5,16 @@ program fuss
55
     use tree_module
66
     use display_module
77
     use terminal_module
8
+    use cache_module
89
     implicit none
910
 
1011
     ! Main program variables
1112
     logical :: show_all, interactive
1213
     character(len=:), allocatable :: root_path
1314
 
15
+    ! Initialize caches for performance optimization
16
+    call init_caches()
17
+
1418
     ! Parse command line arguments
1519
     call parse_arguments(show_all, interactive)
1620
 
@@ -235,6 +239,13 @@ contains
235239
         viewport_offset = 1
236240
         running = .true.
237241
 
242
+        ! Partial redraw optimization: track previous state
243
+        integer :: prev_selected, prev_viewport
244
+        logical :: needs_full_redraw
245
+        prev_selected = 0  ! Force initial draw
246
+        prev_viewport = 0
247
+        needs_full_redraw = .true.
248
+
238249
         ! Enter alternate screen buffer (preserves terminal content)
239250
         call enter_alternate_screen()
240251
 
@@ -252,10 +263,24 @@ contains
252263
                 viewport_offset = n_items - visible_items + 1
253264
             end if
254265
 
255
-            ! Clear screen and redraw
256
-            call clear_screen()
257
-            call draw_interactive_tree(tree_root, items, n_items, selected, &
258
-                                       repo_name, branch_name, viewport_offset, visible_items, top_padding)
266
+            ! Conditional redraw for performance optimization
267
+            if (needs_full_redraw .or. viewport_offset /= prev_viewport) then
268
+                ! Full redraw needed: viewport scrolled or forced refresh
269
+                call clear_screen()
270
+                call draw_interactive_tree(tree_root, items, n_items, selected, &
271
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
272
+                needs_full_redraw = .false.
273
+            else if (selected /= prev_selected) then
274
+                ! Only selection changed within same viewport - still need full redraw for now
275
+                ! TODO: Could optimize this with partial line updates in the future
276
+                call clear_screen()
277
+                call draw_interactive_tree(tree_root, items, n_items, selected, &
278
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
279
+            end if
280
+
281
+            ! Update tracking state
282
+            prev_selected = selected
283
+            prev_viewport = viewport_offset
259284
 
260285
             ! Read key
261286
             call read_key(key)
@@ -278,6 +303,8 @@ contains
278303
                     call rebuild_item_list_from_tree(tree_root, items, n_items, hide_dotfiles)
279304
                     ! Adjust selection if needed
280305
                     if (selected > n_items .and. n_items > 0) selected = n_items
306
+                    ! Force full redraw after tree structure change
307
+                    needs_full_redraw = .true.
281308
                 end if
282309
             case ('a')  ! Stage file or directory (lowercase to avoid conflict with arrow A)
283310
                 ! Check if it's a directory - stage all files in it
@@ -285,62 +312,63 @@ contains
285312
                     call git_stage_directory(items(selected)%path)
286313
                     ! Refresh files after staging directory
287314
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
288
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
315
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
316
+                    needs_full_redraw = .true.
289317
                 ! Otherwise it's a file - stage individual file
290318
                 else if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
291319
                     call git_add_file(items(selected)%path)
292320
                     ! Refresh files after git add
293321
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
294
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
322
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
295323
                 end if
296324
             case ('u')  ! Unstage file (lowercase)
297325
                 if (items(selected)%is_file .and. items(selected)%is_staged) then
298326
                     call git_unstage_file(items(selected)%path)
299327
                     ! Refresh files after git unstage
300328
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
301
-                                            hide_dotfiles, selected, running)
329
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
302330
                 end if
303331
             case ('S')  ! Stage all (Shift+S to avoid conflict with up arrow 'A')
304332
                 call git_stage_all()
305333
                 ! Refresh files after staging all
306334
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
307
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
335
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
308336
             case ('U')  ! Unstage all (Shift+U)
309337
                 call git_unstage_all()
310338
                 ! Refresh files after unstaging all
311339
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
312
-                                        hide_dotfiles, selected, running)
340
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
313341
             case ('m')  ! Commit (lowercase)
314342
                 call commit_prompt()
315343
                 ! Refresh files after commit
316344
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
317
-                                        hide_dotfiles, selected, running)
345
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
318346
             case ('M')  ! Amend last commit (Shift+m)
319347
                 call amend_commit_prompt()
320348
                 ! Refresh files after amend commit
321349
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
322
-                                        hide_dotfiles, selected, running)
350
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
323351
             case ('s')  ! Show git status (lowercase)
324352
                 call show_status_view()
325353
             case ('p')  ! Push (lowercase)
326354
                 call push_prompt()
327355
                 ! Refresh files after push
328356
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
329
-                                        hide_dotfiles, selected, running)
357
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
330358
             case ('t')  ! Tag (lowercase)
331359
                 call tag_prompt()
332360
             case ('b')  ! Switch branch
333361
                 call branch_switch_prompt()
334362
                 ! Refresh files after branch switch
335363
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
336
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
364
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
337365
                 ! Update branch name display
338366
                 call get_repo_info(repo_name, branch_name)
339367
             case ('n')  ! Create new branch
340368
                 call branch_create_prompt()
341369
                 ! Refresh files after branch creation
342370
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
343
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
371
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
344372
                 ! Update branch name display
345373
                 call get_repo_info(repo_name, branch_name)
346374
             case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
@@ -350,7 +378,7 @@ contains
350378
                 call git_fetch()
351379
                 ! Refresh files after fetch and include files with incoming changes
352380
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
353
-                                        hide_dotfiles, selected, running, include_incoming=.true.)
381
+                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
354382
             case ('d')  ! Git diff with less
355383
                 if (items(selected)%is_file) then
356384
                     call git_diff_file(items(selected)%path, items(selected)%has_incoming)
@@ -368,42 +396,42 @@ contains
368396
                     call delete_prompt(items(selected)%path, items(selected)%is_untracked)
369397
                     ! Refresh files after delete
370398
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
371
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
399
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
372400
                 end if
373401
             case ('x', 'X')  ! Discard changes
374402
                 if (items(selected)%is_file .and. (items(selected)%is_staged .or. items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
375403
                     call discard_prompt(items(selected)%path, items(selected)%is_staged, items(selected)%is_untracked)
376404
                     ! Refresh files after discard
377405
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
378
-                                            hide_dotfiles, selected, running, exit_if_empty=.true.)
406
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
379407
                 end if
380408
             case ('l')  ! Git pull
381409
                 call git_pull()
382410
                 ! Refresh files after pull (incoming indicators will automatically clear)
383411
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
384
-                                        hide_dotfiles, selected, running, include_incoming=.true.)
412
+                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
385413
                 ! Note: After successful pull, git diff will show no upstream differences
386414
                 ! so has_incoming will be .false. for all files automatically
387415
             case ('z')  ! Stash push (save changes)
388416
                 call stash_push_prompt()
389417
                 ! Refresh files after stash
390418
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
391
-                                        hide_dotfiles, selected, running, exit_if_empty=.true.)
419
+                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
392420
             case ('Z')  ! Stash pop/apply (restore changes)
393421
                 call stash_pop_apply_prompt()
394422
                 ! Refresh files after stash pop/apply
395423
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
396
-                                        hide_dotfiles, selected, running)
424
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
397425
             case ('y')  ! Cherry-pick (yank commit)
398426
                 call cherry_pick_prompt()
399427
                 ! Refresh files after cherry-pick
400428
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
401
-                                        hide_dotfiles, selected, running)
429
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
402430
             case ('v')  ! Revert commit
403431
                 call revert_commit_prompt()
404432
                 ! Refresh files after revert
405433
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
406
-                                        hide_dotfiles, selected, running)
434
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
407435
             case ('h')  ! Show commit history
408436
                 call history_browser_prompt()
409437
                 ! No refresh needed - read-only
@@ -414,19 +442,19 @@ contains
414442
                 call merge_branch_prompt()
415443
                 ! Refresh files after merge
416444
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
417
-                                        hide_dotfiles, selected, running)
445
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
418446
                 ! Update branch name display in case we merged
419447
                 call get_repo_info(repo_name, branch_name)
420448
             case ('O')  ! Reset (Shift+o - "Oh no, undo!")
421449
                 call reset_prompt()
422450
                 ! Refresh files after reset
423451
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
424
-                                        hide_dotfiles, selected, running)
452
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
425453
             case ('I')  ! Interactive rebase (Shift+i)
426454
                 call rebase_prompt()
427455
                 ! Refresh files after rebase
428456
                 call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
429
-                                        hide_dotfiles, selected, running)
457
+                                        hide_dotfiles, selected, running, force_refresh=.true.)
430458
             case ('.')  ! Toggle hiding dotfiles and gitignored files
431459
                 hide_dotfiles = .not. hide_dotfiles
432460
                 ! Rebuild item list with new filter
@@ -1211,18 +1239,19 @@ contains
12111239
     end subroutine branch_delete_prompt
12121240
 
12131241
     subroutine refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
1214
-                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming)
1242
+                                    hide_dotfiles, selected, running, exit_if_empty, include_incoming, force_refresh)
12151243
         ! Centralized helper to refresh file list and rebuild tree
12161244
         ! Consolidates the pattern repeated 20+ times in the codebase
1245
+        ! Now with caching support for performance optimization
12171246
         logical, intent(in) :: show_all, hide_dotfiles
12181247
         type(file_entry), allocatable, intent(inout) :: files(:)
12191248
         integer, intent(inout) :: n_files, n_items, selected
12201249
         type(tree_node), pointer, intent(inout) :: tree_root
12211250
         type(selectable_item), allocatable, intent(inout) :: items(:)
12221251
         logical, intent(inout), optional :: running
1223
-        logical, intent(in), optional :: exit_if_empty, include_incoming
1252
+        logical, intent(in), optional :: exit_if_empty, include_incoming, force_refresh
12241253
 
1225
-        logical :: do_exit_if_empty, do_include_incoming
1254
+        logical :: do_exit_if_empty, do_include_incoming, do_force_refresh
12261255
 
12271256
         ! Handle optional parameters
12281257
         do_exit_if_empty = .false.
@@ -1231,12 +1260,15 @@ contains
12311260
         do_include_incoming = .false.
12321261
         if (present(include_incoming)) do_include_incoming = include_incoming
12331262
 
1234
-        ! Get files based on mode
1263
+        do_force_refresh = .false.
1264
+        if (present(force_refresh)) do_force_refresh = force_refresh
1265
+
1266
+        ! Get files based on mode (with caching support)
12351267
         if (show_all) then
1236
-            call get_all_files(files, n_files)
1268
+            call get_all_files(files, n_files, force_refresh=do_force_refresh)
12371269
             call mark_incoming_changes(files, n_files)
12381270
         else
1239
-            call get_dirty_files(files, n_files)
1271
+            call get_dirty_files(files, n_files, force_refresh=do_force_refresh)
12401272
             if (do_include_incoming) then
12411273
                 ! For fetch/pull: also include files with only incoming changes
12421274
                 call add_incoming_files(files, n_files)
src/git_module.f90modified
@@ -1,8 +1,13 @@
11
 module git_module
22
     use iso_fortran_env, only: error_unit
33
     use types_module
4
+    use cache_module
45
     implicit none
56
 
7
+    ! Shared temp file for reducing disk I/O and clutter
8
+    ! Reused across operations, deleted after each read
9
+    character(len=*), parameter :: FUSS_TEMP = '/tmp/fuss_tmp.txt'
10
+
611
 contains
712
 
813
     ! ========== Performance Optimization: Binary Search Functions ==========
@@ -198,7 +203,8 @@ contains
198203
         end if
199204
     end function unquote_filename
200205
 
201
-    subroutine get_dirty_files(files, n_files)
206
+    ! Internal implementation - called directly for actual git operations
207
+    subroutine get_dirty_files_impl(files, n_files)
202208
         type(file_entry), allocatable, intent(out) :: files(:)
203209
         integer, intent(out) :: n_files
204210
         integer :: iostat, unit_num, status_code
@@ -280,9 +286,10 @@ contains
280286
             call quicksort_files(files, 1, n_files)
281287
         end if
282288
         deallocate(temp_files)
283
-    end subroutine get_dirty_files
289
+    end subroutine get_dirty_files_impl
284290
 
285
-    subroutine get_all_files(files, n_files)
291
+    ! Internal implementation - called directly for actual file operations
292
+    subroutine get_all_files_impl(files, n_files)
286293
         type(file_entry), allocatable, intent(out) :: files(:)
287294
         integer, intent(out) :: n_files
288295
         integer :: iostat, unit_num, status_code, i
@@ -290,8 +297,8 @@ contains
290297
         type(file_entry), allocatable :: dirty_files(:), temp_files(:)
291298
         integer :: n_dirty, max_files
292299
 
293
-        ! First get dirty files
294
-        call get_dirty_files(dirty_files, n_dirty)
300
+        ! First get dirty files (call impl directly to avoid double caching)
301
+        call get_dirty_files_impl(dirty_files, n_dirty)
295302
 
296303
         ! Get all files using find
297304
         call execute_command_line('find . -type f ! -path "*/\.git/*" > /tmp/fuss_all_files.txt', exitstat=status_code)
@@ -369,8 +376,66 @@ contains
369376
         end if
370377
         deallocate(temp_files)
371378
         if (allocated(dirty_files)) deallocate(dirty_files)
379
+    end subroutine get_all_files_impl
380
+
381
+    ! ========== Cached Wrappers for File Retrieval ==========
382
+
383
+    ! Get dirty files with caching support
384
+    subroutine get_dirty_files(files, n_files, force_refresh)
385
+        type(file_entry), allocatable, intent(out) :: files(:)
386
+        integer, intent(out) :: n_files
387
+        logical, intent(in), optional :: force_refresh
388
+        logical :: do_refresh
389
+
390
+        do_refresh = .false.
391
+        if (present(force_refresh)) do_refresh = force_refresh
392
+
393
+        ! Force refresh if requested
394
+        if (do_refresh) then
395
+            call invalidate_cache(dirty_cache)
396
+        end if
397
+
398
+        ! Check cache validity
399
+        if (cache_valid(dirty_cache)) then
400
+            ! Use cached results
401
+            call get_cached_files(dirty_cache, files, n_files)
402
+        else
403
+            ! Call actual implementation
404
+            call get_dirty_files_impl(files, n_files)
405
+            ! Update cache with fresh data
406
+            call update_cache(dirty_cache, files, n_files)
407
+        end if
408
+    end subroutine get_dirty_files
409
+
410
+    ! Get all files with caching support
411
+    subroutine get_all_files(files, n_files, force_refresh)
412
+        type(file_entry), allocatable, intent(out) :: files(:)
413
+        integer, intent(out) :: n_files
414
+        logical, intent(in), optional :: force_refresh
415
+        logical :: do_refresh
416
+
417
+        do_refresh = .false.
418
+        if (present(force_refresh)) do_refresh = force_refresh
419
+
420
+        ! Force refresh if requested
421
+        if (do_refresh) then
422
+            call invalidate_cache(all_cache)
423
+        end if
424
+
425
+        ! Check cache validity
426
+        if (cache_valid(all_cache)) then
427
+            ! Use cached results
428
+            call get_cached_files(all_cache, files, n_files)
429
+        else
430
+            ! Call actual implementation
431
+            call get_all_files_impl(files, n_files)
432
+            ! Update cache with fresh data
433
+            call update_cache(all_cache, files, n_files)
434
+        end if
372435
     end subroutine get_all_files
373436
 
437
+    ! ========== End Cached Wrappers ==========
438
+
374439
     subroutine resize_array(array, new_size)
375440
         type(file_entry), allocatable, intent(inout) :: array(:)
376441
         integer, intent(in) :: new_size
@@ -656,12 +721,12 @@ contains
656721
         message = ''
657722
 
658723
         ! Get the last commit message using git log
659
-        call execute_command_line('git log -1 --pretty=%B > /tmp/fuss_last_commit.txt 2>/dev/null', exitstat=status)
724
+        call execute_command_line('git log -1 --pretty=%B > ' // FUSS_TEMP // ' 2>/dev/null', exitstat=status)
660725
 
661726
         if (status /= 0) return
662727
 
663728
         ! Read the commit message
664
-        open(newunit=unit_num, file='/tmp/fuss_last_commit.txt', status='old', action='read', iostat=iostat)
729
+        open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
665730
         if (iostat /= 0) return
666731
 
667732
         ! Read first line (single-line commit message)
@@ -767,7 +832,7 @@ contains
767832
             end if
768833
 
769834
             ! Origin exists - get current branch name and push to origin
770
-            call execute_command_line('git rev-parse --abbrev-ref HEAD > /tmp/fuss_current_branch.txt 2>&1', &
835
+            call execute_command_line('git rev-parse --abbrev-ref HEAD > ' // FUSS_TEMP // ' 2>&1', &
771836
                                       exitstat=status)
772837
 
773838
             if (status /= 0) then
@@ -777,7 +842,7 @@ contains
777842
             end if
778843
 
779844
             ! Read current branch name
780
-            open(newunit=unit_num, file='/tmp/fuss_current_branch.txt', status='old', action='read', iostat=iostat)
845
+            open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
781846
             if (iostat /= 0) then
782847
                 print '(A)', achar(27) // '[31m✗ Could not read branch name' // achar(27) // '[0m'
783848
                 print '(A)', 'Press any key to continue...'
@@ -844,11 +909,11 @@ contains
844909
         branch_name = ''
845910
 
846911
         ! Get repo name (basename of repo root)
847
-        call execute_command_line('git rev-parse --show-toplevel 2>/dev/null | xargs basename > /tmp/fuss_repo.txt', &
912
+        call execute_command_line('git rev-parse --show-toplevel 2>/dev/null | xargs basename > ' // FUSS_TEMP // '', &
848913
                                   exitstat=status_code)
849914
 
850915
         if (status_code == 0) then
851
-            open(newunit=unit_num, file='/tmp/fuss_repo.txt', status='old', action='read', iostat=iostat)
916
+            open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
852917
             if (iostat == 0) then
853918
                 read(unit_num, '(A)', iostat=iostat) repo_name
854919
                 close(unit_num, status='delete')
@@ -856,11 +921,11 @@ contains
856921
         end if
857922
 
858923
         ! Get current branch name
859
-        call execute_command_line('git rev-parse --abbrev-ref HEAD 2>/dev/null > /tmp/fuss_branch.txt', &
924
+        call execute_command_line('git rev-parse --abbrev-ref HEAD 2>/dev/null > ' // FUSS_TEMP // '', &
860925
                                   exitstat=status_code)
861926
 
862927
         if (status_code == 0) then
863
-            open(newunit=unit_num, file='/tmp/fuss_branch.txt', status='old', action='read', iostat=iostat)
928
+            open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
864929
             if (iostat == 0) then
865930
                 read(unit_num, '(A)', iostat=iostat) branch_name
866931
                 close(unit_num, status='delete')
@@ -886,7 +951,7 @@ contains
886951
         ! Use fzf to select remote branch
887952
         call execute_command_line('git branch -r | grep -v HEAD | sed "s/^  //" | ' // &
888953
                                   'fzf --height=10 --border=rounded --border-label=" ESC to cancel " ' // &
889
-                                  '--prompt="Select upstream: " > /tmp/fuss_upstream.txt', &
954
+                                  '--prompt="Select upstream: " > ' // FUSS_TEMP // '', &
890955
                                   exitstat=status_code)
891956
 
892957
         if (status_code /= 0) then
@@ -899,7 +964,7 @@ contains
899964
         end if
900965
 
901966
         ! Read selected branch
902
-        open(unit=99, file='/tmp/fuss_upstream.txt', status='old', action='read', iostat=status_code)
967
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status_code)
903968
         if (status_code == 0) then
904969
             read(99, '(A)', iostat=status_code) selected_branch
905970
             close(99, status='delete')
@@ -1334,11 +1399,11 @@ contains
13341399
                                   'fzf --height=15 --border=rounded --border-label=" ESC to cancel " ' // &
13351400
                                   '--prompt="Source branch: " ' // &
13361401
                                   '--preview="git log --oneline --graph --color=always {} | head -20" ' // &
1337
-                                  '--preview-window=right:50% > /tmp/fuss_branch_select.txt', &
1402
+                                  '--preview-window=right:50% > ' // FUSS_TEMP // '', &
13381403
                                   exitstat=status_code)
13391404
 
13401405
         if (status_code /= 0) then
1341
-            call execute_command_line('rm -f /tmp/fuss_branch_select.txt', exitstat=status)
1406
+            call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status)
13421407
             print '(A)', ''
13431408
             print '(A)', 'Operation cancelled.'
13441409
             print '(A)', ''
@@ -1349,7 +1414,7 @@ contains
13491414
         end if
13501415
 
13511416
         ! Read selected branch
1352
-        open(unit=99, file='/tmp/fuss_branch_select.txt', status='old', action='read')
1417
+        open(unit=99, file=FUSS_TEMP, status='old', action='read')
13531418
         read(99, '(A)', iostat=status) selected_branch
13541419
         close(99, status='delete')
13551420
 
@@ -1372,11 +1437,11 @@ contains
13721437
                                   'fzf --height=20 --border=rounded --border-label=" ESC to cancel " ' // &
13731438
                                   '--prompt="Commit: " ' // &
13741439
                                   '--preview="git show --color=always {1}" ' // &
1375
-                                  '--preview-window=right:60% > /tmp/fuss_commit_select.txt'
1440
+                                  '--preview-window=right:60% > ' // FUSS_TEMP // ''
13761441
         call execute_command_line(trim(command), exitstat=status_code)
13771442
 
13781443
         if (status_code /= 0) then
1379
-            call execute_command_line('rm -f /tmp/fuss_commit_select.txt', exitstat=status)
1444
+            call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status)
13801445
             print '(A)', ''
13811446
             print '(A)', 'Operation cancelled.'
13821447
             print '(A)', ''
@@ -1387,7 +1452,7 @@ contains
13871452
         end if
13881453
 
13891454
         ! Read selected commit (first 7 chars is the hash)
1390
-        open(unit=99, file='/tmp/fuss_commit_select.txt', status='old', action='read')
1455
+        open(unit=99, file=FUSS_TEMP, status='old', action='read')
13911456
         read(99, '(A)', iostat=status) selected_commit
13921457
         close(99, status='delete')
13931458
 
@@ -1448,11 +1513,11 @@ contains
14481513
                                   'fzf --height=20 --border=rounded --border-label=" ESC to cancel " ' // &
14491514
                                   '--prompt="Commit to revert: " ' // &
14501515
                                   '--preview="git show --color=always {1}" ' // &
1451
-                                  '--preview-window=right:60% > /tmp/fuss_revert_select.txt', &
1516
+                                  '--preview-window=right:60% > ' // FUSS_TEMP // '', &
14521517
                                   exitstat=status_code)
14531518
 
14541519
         if (status_code /= 0) then
1455
-            call execute_command_line('rm -f /tmp/fuss_revert_select.txt', exitstat=status)
1520
+            call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status)
14561521
             print '(A)', ''
14571522
             print '(A)', 'Operation cancelled.'
14581523
             print '(A)', ''
@@ -1463,7 +1528,7 @@ contains
14631528
         end if
14641529
 
14651530
         ! Read selected commit hash
1466
-        open(unit=99, file='/tmp/fuss_revert_select.txt', status='old', action='read')
1531
+        open(unit=99, file=FUSS_TEMP, status='old', action='read')
14671532
         read(99, '(A)', iostat=status) selected_commit
14681533
         close(99, status='delete')
14691534
 
@@ -1558,11 +1623,11 @@ contains
15581623
                                   'fzf --height=15 --border=rounded --border-label=" ESC to cancel " ' // &
15591624
                                   '--prompt="Branch to merge: " ' // &
15601625
                                   '--preview="echo Commits to merge:; git log --oneline --color=always HEAD..{} | head -20" ' // &
1561
-                                  '--preview-window=right:50% > /tmp/fuss_merge_select.txt', &
1626
+                                  '--preview-window=right:50% > ' // FUSS_TEMP // '', &
15621627
                                   exitstat=status_code)
15631628
 
15641629
         if (status_code /= 0) then
1565
-            call execute_command_line('rm -f /tmp/fuss_merge_select.txt', exitstat=status)
1630
+            call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status)
15661631
             print '(A)', ''
15671632
             print '(A)', 'Operation cancelled.'
15681633
             print '(A)', ''
@@ -1573,7 +1638,7 @@ contains
15731638
         end if
15741639
 
15751640
         ! Read selected branch
1576
-        open(unit=99, file='/tmp/fuss_merge_select.txt', status='old', action='read')
1641
+        open(unit=99, file=FUSS_TEMP, status='old', action='read')
15771642
         read(99, '(A)', iostat=status) selected_branch
15781643
         close(99, status='delete')
15791644
 
@@ -1656,7 +1721,7 @@ contains
16561721
                                   'fzf --height=15 --border=rounded --border-label=" ESC to cancel " ' // &
16571722
                                   '--prompt="Switch to branch: " ' // &
16581723
                                   '--preview="git log --oneline --graph --color=always {}" ' // &
1659
-                                  '--preview-window=right:50% > /tmp/fuss_branch_select.txt', &
1724
+                                  '--preview-window=right:50% > ' // FUSS_TEMP // '', &
16601725
                                   exitstat=status_code)
16611726
 
16621727
         ! Re-enable cbreak mode first
@@ -1671,7 +1736,7 @@ contains
16711736
         end if
16721737
 
16731738
         ! Read selected branch
1674
-        open(unit=99, file='/tmp/fuss_branch_select.txt', status='old', action='read', iostat=status_code)
1739
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status_code)
16751740
         if (status_code /= 0) then
16761741
             print '(A)', 'Branch switch cancelled.'
16771742
             print '(A)', ''
@@ -1743,7 +1808,7 @@ contains
17431808
         success = .false.
17441809
 
17451810
         ! Get current branch name to prevent deleting it
1746
-        call execute_command_line('git rev-parse --abbrev-ref HEAD > /tmp/fuss_current_branch.txt 2>&1', &
1811
+        call execute_command_line('git rev-parse --abbrev-ref HEAD > ' // FUSS_TEMP // ' 2>&1', &
17471812
                                   exitstat=status_code)
17481813
 
17491814
         if (status_code /= 0) then
@@ -1751,7 +1816,7 @@ contains
17511816
             return
17521817
         end if
17531818
 
1754
-        open(unit=99, file='/tmp/fuss_current_branch.txt', status='old', action='read', iostat=status_code)
1819
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status_code)
17551820
         if (status_code == 0) then
17561821
             read(99, '(A)', iostat=status_code) current_branch
17571822
             close(99, status='delete')
@@ -1764,7 +1829,7 @@ contains
17641829
 
17651830
         ! Use fzf to select branch to delete (exclude current branch)
17661831
         write(command, '(A,A,A)') 'git branch | grep -v "^* " | sed "s/^  //" | grep -v "^', trim(current_branch), &
1767
-                                  '$" | fzf --height=15 --border=rounded --border-label=" ESC to cancel " --prompt="Delete branch: " > /tmp/fuss_branch_delete.txt'
1832
+                                  '$" | fzf --height=15 --border=rounded --border-label=" ESC to cancel " --prompt="Delete branch: " > ' // FUSS_TEMP // ''
17681833
         call execute_command_line(trim(command), exitstat=status_code)
17691834
 
17701835
         ! Re-enable cbreak mode
@@ -1779,7 +1844,7 @@ contains
17791844
         end if
17801845
 
17811846
         ! Read selected branch
1782
-        open(unit=99, file='/tmp/fuss_branch_delete.txt', status='old', action='read', iostat=status_code)
1847
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status_code)
17831848
         if (status_code /= 0) then
17841849
             print '(A)', 'Branch deletion cancelled.'
17851850
             print '(A)', ''
@@ -1895,7 +1960,7 @@ contains
18951960
         success = .false.
18961961
 
18971962
         ! Check if there are any stashes
1898
-        call execute_command_line('git stash list > /tmp/fuss_stash_check.txt 2>&1', exitstat=status_code)
1963
+        call execute_command_line('git stash list > ' // FUSS_TEMP // ' 2>&1', exitstat=status_code)
18991964
         if (status_code /= 0) then
19001965
             print '(A)', achar(27) // '[33mNo stashes available' // achar(27) // '[0m'
19011966
             print '(A)', 'Press any key to continue...'
@@ -1903,15 +1968,15 @@ contains
19031968
         end if
19041969
 
19051970
         ! Check if stash list is empty
1906
-        call execute_command_line('test -s /tmp/fuss_stash_check.txt', exitstat=status_code)
1971
+        call execute_command_line('test -s ' // FUSS_TEMP // '', exitstat=status_code)
19071972
         if (status_code /= 0) then
19081973
             print '(A)', achar(27) // '[33mNo stashes available' // achar(27) // '[0m'
19091974
             print '(A)', 'Press any key to continue...'
1910
-            call execute_command_line('rm -f /tmp/fuss_stash_check.txt', exitstat=status_code)
1975
+            call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status_code)
19111976
             return
19121977
         end if
19131978
 
1914
-        call execute_command_line('rm -f /tmp/fuss_stash_check.txt', exitstat=status_code)
1979
+        call execute_command_line('rm -f ' // FUSS_TEMP // '', exitstat=status_code)
19151980
 
19161981
         ! Restore terminal for fzf
19171982
         call execute_command_line('stty sane < /dev/tty', exitstat=status_code)
@@ -1921,7 +1986,7 @@ contains
19211986
                                   'fzf --height=15 --border=rounded --border-label=" ESC to cancel " ' // &
19221987
                                   '--prompt="Select stash: " ' // &
19231988
                                   '--preview="git stash show -p {1}" --preview-window=right:50% ' // &
1924
-                                  '> /tmp/fuss_stash_select.txt', &
1989
+                                  '> ' // FUSS_TEMP // '', &
19251990
                                   exitstat=status_code)
19261991
 
19271992
         ! Re-enable cbreak mode
@@ -1936,7 +2001,7 @@ contains
19362001
         end if
19372002
 
19382003
         ! Read selected stash
1939
-        open(unit=99, file='/tmp/fuss_stash_select.txt', status='old', action='read', iostat=status_code)
2004
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status_code)
19402005
         if (status_code /= 0) then
19412006
             print '(A)', 'Stash operation cancelled.'
19422007
             print '(A)', ''
@@ -2065,7 +2130,7 @@ contains
20652130
                                   'fzf --height=15 --border=rounded --border-label=" ESC to cancel " ' // &
20662131
                                   '--prompt="Reset to: " ' // &
20672132
                                   '--preview="git show --stat --color=always {1}" ' // &
2068
-                                  '--preview-window=right:60% > /tmp/fuss_reset_commit.txt', &
2133
+                                  '--preview-window=right:60% > ' // FUSS_TEMP // '', &
20692134
                                   exitstat=status_code)
20702135
 
20712136
         if (status_code /= 0) then
@@ -2078,7 +2143,7 @@ contains
20782143
         end if
20792144
 
20802145
         ! Read selected commit
2081
-        open(unit=99, file='/tmp/fuss_reset_commit.txt', status='old', action='read', iostat=status)
2146
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status)
20822147
         if (status /= 0) then
20832148
             print '(A)', achar(27) // '[31m✗ Failed to read selection' // achar(27) // '[0m'
20842149
             call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status)
@@ -2103,11 +2168,11 @@ contains
21032168
         print '(A)', 'Enter choice (1/2/3) or ESC to cancel: '
21042169
 
21052170
         ! Get mode choice
2106
-        call execute_command_line('read -n 1 choice < /dev/tty; echo $choice > /tmp/fuss_reset_mode.txt', &
2171
+        call execute_command_line('read -n 1 choice < /dev/tty; echo $choice > ' // FUSS_TEMP // '', &
21072172
                                   exitstat=status)
21082173
 
21092174
         ! Read mode choice
2110
-        open(unit=99, file='/tmp/fuss_reset_mode.txt', status='old', action='read', iostat=status)
2175
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status)
21112176
         if (status /= 0) then
21122177
             print '(A)', 'Reset cancelled.'
21132178
             call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status)
@@ -2130,11 +2195,11 @@ contains
21302195
                 print '(A)', achar(27) // '[1;31mThis operation CANNOT be undone!' // achar(27) // '[0m'
21312196
                 print '(A)', ''
21322197
                 print '(A)', 'Type "yes" to confirm hard reset: '
2133
-                call execute_command_line('read conf < /dev/tty; echo $conf > /tmp/fuss_reset_confirm.txt', &
2198
+                call execute_command_line('read conf < /dev/tty; echo $conf > ' // FUSS_TEMP // '', &
21342199
                                           exitstat=status)
21352200
 
21362201
                 ! Read confirmation
2137
-                open(unit=99, file='/tmp/fuss_reset_confirm.txt', status='old', action='read', iostat=status)
2202
+                open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status)
21382203
                 if (status == 0) then
21392204
                     read(99, '(A)', iostat=status) confirmation
21402205
                     close(99)
@@ -2248,7 +2313,7 @@ contains
22482313
                                   '--preview="git log --oneline --color=always {1}~1..HEAD | head -20" ' // &
22492314
                                   '--preview-window=right:60% ' // &
22502315
                                   '--preview-label=" Commits that will be rebased " ' // &
2251
-                                  '> /tmp/fuss_rebase_base.txt', &
2316
+                                  '> ' // FUSS_TEMP // '', &
22522317
                                   exitstat=status_code)
22532318
 
22542319
         if (status_code /= 0) then
@@ -2261,7 +2326,7 @@ contains
22612326
         end if
22622327
 
22632328
         ! Read selected commit
2264
-        open(unit=99, file='/tmp/fuss_rebase_base.txt', status='old', action='read', iostat=status)
2329
+        open(unit=99, file=FUSS_TEMP, status='old', action='read', iostat=status)
22652330
         if (status /= 0) then
22662331
             print '(A)', achar(27) // '[31m✗ Failed to read selection' // achar(27) // '[0m'
22672332
             call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status)
src/git_module.f90 → src/git_module.f90.phase23copied (97% similarity)
@@ -1,8 +1,13 @@
11
 module git_module
22
     use iso_fortran_env, only: error_unit
33
     use types_module
4
+    use cache_module
45
     implicit none
56
 
7
+    ! Shared temp file for reducing disk I/O and clutter
8
+    ! Reused across operations, deleted after each read
9
+    character(len=*), parameter :: FUSS_TEMP = '/tmp/fuss_tmp.txt'
10
+
611
 contains
712
 
813
     ! ========== Performance Optimization: Binary Search Functions ==========
@@ -198,7 +203,8 @@ contains
198203
         end if
199204
     end function unquote_filename
200205
 
201
-    subroutine get_dirty_files(files, n_files)
206
+    ! Internal implementation - called directly for actual git operations
207
+    subroutine get_dirty_files_impl(files, n_files)
202208
         type(file_entry), allocatable, intent(out) :: files(:)
203209
         integer, intent(out) :: n_files
204210
         integer :: iostat, unit_num, status_code
@@ -280,9 +286,10 @@ contains
280286
             call quicksort_files(files, 1, n_files)
281287
         end if
282288
         deallocate(temp_files)
283
-    end subroutine get_dirty_files
289
+    end subroutine get_dirty_files_impl
284290
 
285
-    subroutine get_all_files(files, n_files)
291
+    ! Internal implementation - called directly for actual file operations
292
+    subroutine get_all_files_impl(files, n_files)
286293
         type(file_entry), allocatable, intent(out) :: files(:)
287294
         integer, intent(out) :: n_files
288295
         integer :: iostat, unit_num, status_code, i
@@ -290,8 +297,8 @@ contains
290297
         type(file_entry), allocatable :: dirty_files(:), temp_files(:)
291298
         integer :: n_dirty, max_files
292299
 
293
-        ! First get dirty files
294
-        call get_dirty_files(dirty_files, n_dirty)
300
+        ! First get dirty files (call impl directly to avoid double caching)
301
+        call get_dirty_files_impl(dirty_files, n_dirty)
295302
 
296303
         ! Get all files using find
297304
         call execute_command_line('find . -type f ! -path "*/\.git/*" > /tmp/fuss_all_files.txt', exitstat=status_code)
@@ -369,8 +376,66 @@ contains
369376
         end if
370377
         deallocate(temp_files)
371378
         if (allocated(dirty_files)) deallocate(dirty_files)
379
+    end subroutine get_all_files_impl
380
+
381
+    ! ========== Cached Wrappers for File Retrieval ==========
382
+
383
+    ! Get dirty files with caching support
384
+    subroutine get_dirty_files(files, n_files, force_refresh)
385
+        type(file_entry), allocatable, intent(out) :: files(:)
386
+        integer, intent(out) :: n_files
387
+        logical, intent(in), optional :: force_refresh
388
+        logical :: do_refresh
389
+
390
+        do_refresh = .false.
391
+        if (present(force_refresh)) do_refresh = force_refresh
392
+
393
+        ! Force refresh if requested
394
+        if (do_refresh) then
395
+            call invalidate_cache(dirty_cache)
396
+        end if
397
+
398
+        ! Check cache validity
399
+        if (cache_valid(dirty_cache)) then
400
+            ! Use cached results
401
+            call get_cached_files(dirty_cache, files, n_files)
402
+        else
403
+            ! Call actual implementation
404
+            call get_dirty_files_impl(files, n_files)
405
+            ! Update cache with fresh data
406
+            call update_cache(dirty_cache, files, n_files)
407
+        end if
408
+    end subroutine get_dirty_files
409
+
410
+    ! Get all files with caching support
411
+    subroutine get_all_files(files, n_files, force_refresh)
412
+        type(file_entry), allocatable, intent(out) :: files(:)
413
+        integer, intent(out) :: n_files
414
+        logical, intent(in), optional :: force_refresh
415
+        logical :: do_refresh
416
+
417
+        do_refresh = .false.
418
+        if (present(force_refresh)) do_refresh = force_refresh
419
+
420
+        ! Force refresh if requested
421
+        if (do_refresh) then
422
+            call invalidate_cache(all_cache)
423
+        end if
424
+
425
+        ! Check cache validity
426
+        if (cache_valid(all_cache)) then
427
+            ! Use cached results
428
+            call get_cached_files(all_cache, files, n_files)
429
+        else
430
+            ! Call actual implementation
431
+            call get_all_files_impl(files, n_files)
432
+            ! Update cache with fresh data
433
+            call update_cache(all_cache, files, n_files)
434
+        end if
372435
     end subroutine get_all_files
373436
 
437
+    ! ========== End Cached Wrappers ==========
438
+
374439
     subroutine resize_array(array, new_size)
375440
         type(file_entry), allocatable, intent(inout) :: array(:)
376441
         integer, intent(in) :: new_size
src/terminal_module.f90modified
@@ -1,6 +1,9 @@
11
 module terminal_module
22
     implicit none
33
 
4
+    ! Shared temp file for reducing disk I/O
5
+    character(len=*), parameter :: FUSS_TEMP = '/tmp/fuss_tmp.txt'
6
+
47
 contains
58
 
69
     subroutine enter_alternate_screen()
@@ -85,11 +88,11 @@ contains
8588
         height = 24  ! Default fallback
8689
 
8790
         ! Try method 1: Use stty size to get terminal dimensions
88
-        call execute_command_line('stty size < /dev/tty 2>/dev/null | cut -d" " -f1 > /tmp/fuss_term_height.txt', &
91
+        call execute_command_line('stty size < /dev/tty 2>/dev/null | cut -d" " -f1 > ' // FUSS_TEMP // '', &
8992
                                   exitstat=status)
9093
 
9194
         if (status == 0) then
92
-            open(newunit=unit_num, file='/tmp/fuss_term_height.txt', status='old', action='read', iostat=iostat)
95
+            open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
9396
             if (iostat == 0) then
9497
                 read(unit_num, *, iostat=iostat) height
9598
                 close(unit_num, status='delete')
@@ -99,10 +102,10 @@ contains
99102
         end if
100103
 
101104
         ! Try method 2: tput lines
102
-        call execute_command_line('tput lines < /dev/tty > /tmp/fuss_term_height.txt 2>/dev/null', exitstat=status)
105
+        call execute_command_line('tput lines < /dev/tty > ' // FUSS_TEMP // ' 2>/dev/null', exitstat=status)
103106
 
104107
         if (status == 0) then
105
-            open(newunit=unit_num, file='/tmp/fuss_term_height.txt', status='old', action='read', iostat=iostat)
108
+            open(newunit=unit_num, file=FUSS_TEMP, status='old', action='read', iostat=iostat)
106109
             if (iostat == 0) then
107110
                 read(unit_num, *, iostat=iostat) height
108111
                 close(unit_num, status='delete')