fortrangoingonforty/fuss / d1aed8a

Browse files

put git operations behind alt-g, needs redraw fixes

Authored by espadonne
SHA
d1aed8a98b6dd4313bdf7abeada36d95ef798ec1
Parents
3a355cb
Tree
d9b6432

3 changed files

StatusFile+-
M src/display_module.f90 34 13
M src/fuss_main.f90 201 123
M src/terminal_module.f90 11 1
src/display_module.f90modified
@@ -129,11 +129,11 @@ contains
129
     end subroutine print_tree_node
129
     end subroutine print_tree_node
130
 
130
 
131
     subroutine draw_interactive_tree(tree_root, items, n_items, selected, &
131
     subroutine draw_interactive_tree(tree_root, items, n_items, selected, &
132
-                                     repo_name, branch_name, viewport_offset, visible_items, top_padding)
132
+                                     repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
133
         type(tree_node), pointer, intent(in) :: tree_root
133
         type(tree_node), pointer, intent(in) :: tree_root
134
         integer, intent(in) :: n_items, selected
134
         integer, intent(in) :: n_items, selected
135
         type(selectable_item), intent(in) :: items(:)
135
         type(selectable_item), intent(in) :: items(:)
136
-        character(len=*), intent(in) :: repo_name, branch_name
136
+        character(len=*), intent(in) :: repo_name, branch_name, mode
137
         integer, intent(in) :: viewport_offset, visible_items, top_padding
137
         integer, intent(in) :: viewport_offset, visible_items, top_padding
138
         integer :: item_idx, viewport_end, i
138
         integer :: item_idx, viewport_end, i
139
         character(len=512) :: status_line
139
         character(len=512) :: status_line
@@ -143,12 +143,23 @@ contains
143
             print '(A)', ''
143
             print '(A)', ''
144
         end do
144
         end do
145
 
145
 
146
-        ! Display repo:branch info at top if available
146
+        ! Display repo:branch info at top with mode indicator
147
         if (len_trim(repo_name) > 0 .and. len_trim(branch_name) > 0) then
147
         if (len_trim(repo_name) > 0 .and. len_trim(branch_name) > 0) then
148
-            write(status_line, '(A,A,A,A,A,A,A)') &
148
+            if (mode == 'git') then
149
-                achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
149
+                ! Git mode: show in yellow/orange
150
-                ':', &
150
+                write(status_line, '(A,A,A,A,A,A,A,A,A,A,A)') &
151
-                achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m'
151
+                    achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
152
+                    ':', &
153
+                    achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m', &
154
+                    ' ', &
155
+                    achar(27) // '[1;33m[ GIT MODE ]', achar(27) // '[0m'
156
+            else
157
+                ! Normal mode
158
+                write(status_line, '(A,A,A,A,A,A,A)') &
159
+                    achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
160
+                    ':', &
161
+                    achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m'
162
+            end if
152
             print '(A)', trim(status_line)
163
             print '(A)', trim(status_line)
153
             print '(A)', ''
164
             print '(A)', ''
154
         end if
165
         end if
@@ -163,13 +174,23 @@ contains
163
         call print_interactive_node(tree_root, '', .true., .true., items, selected, &
174
         call print_interactive_node(tree_root, '', .true., .true., items, selected, &
164
                                     item_idx, viewport_offset, viewport_end)
175
                                     item_idx, viewport_offset, viewport_end)
165
 
176
 
166
-        ! Print help (two rows for better readability)
177
+        ! Print help (mode-dependent)
167
         print '(A)', ''
178
         print '(A)', ''
168
-        print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
179
+        if (mode == 'git') then
169
-                     achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
180
+            ! Git mode help - show in yellow tint
170
-                     achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
181
+            print '(A)', achar(27) // '[33mLegend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
171
-                     achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
182
+                         achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
172
-        print '(A)', 'Keys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | G:merge | O:reset | I:rebase | f:fetch | d:diff | c:view | w:blame | h:history | L:reflog | y:cherry-pick | v:revert | r:delete | l:pull | m:commit | M:amend | p:push | t:tag | s:status | q:quit'
183
+                         achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
184
+                         achar(27) // '[34m↓' // achar(27) // '[0m=incoming' // achar(27) // '[0m'
185
+            print '(A)', achar(27) // '[33mKeys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | G:merge | O:reset | I:rebase | f:fetch | d:diff | c:view | w:blame | h:history | L:reflog | y:cherry-pick | v:revert | r:delete | l:pull | m:commit | M:amend | p:push | t:tag | s:status | q:exit-mode | ESC:exit-mode' // achar(27) // '[0m'
186
+        else
187
+            ! Normal mode help
188
+            print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
189
+                         achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
190
+                         achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
191
+                         achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
192
+            print '(A)', 'Keys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | alt-g:git-mode | q:quit'
193
+        end if
173
 
194
 
174
         ! Don't free tree - it's owned by interactive_mode
195
         ! Don't free tree - it's owned by interactive_mode
175
     end subroutine draw_interactive_tree
196
     end subroutine draw_interactive_tree
src/fuss_main.f90modified
@@ -178,6 +178,7 @@ contains
178
         integer :: term_height, viewport_offset, visible_items, top_padding
178
         integer :: term_height, viewport_offset, visible_items, top_padding
179
         integer :: prev_selected, prev_viewport
179
         integer :: prev_selected, prev_viewport
180
         logical :: needs_full_redraw
180
         logical :: needs_full_redraw
181
+        character(len=10) :: mode  ! "normal" or "git" mode
181
         type(tree_node), pointer :: tree_root
182
         type(tree_node), pointer :: tree_root
182
 
183
 
183
         ! Initialize tree pointer
184
         ! Initialize tree pointer
@@ -243,6 +244,7 @@ contains
243
         selected = 1
244
         selected = 1
244
         viewport_offset = 1
245
         viewport_offset = 1
245
         running = .true.
246
         running = .true.
247
+        mode = 'normal'  ! Start in normal mode
246
 
248
 
247
         ! Partial redraw optimization: initialize tracking state
249
         ! Partial redraw optimization: initialize tracking state
248
         prev_selected = 0  ! Force initial draw
250
         prev_selected = 0  ! Force initial draw
@@ -271,14 +273,14 @@ contains
271
                 ! Full redraw needed: viewport scrolled or forced refresh
273
                 ! Full redraw needed: viewport scrolled or forced refresh
272
                 call clear_screen()
274
                 call clear_screen()
273
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
275
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
274
-                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
276
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
275
                 needs_full_redraw = .false.
277
                 needs_full_redraw = .false.
276
             else if (selected /= prev_selected) then
278
             else if (selected /= prev_selected) then
277
                 ! Only selection changed within same viewport - still need full redraw for now
279
                 ! Only selection changed within same viewport - still need full redraw for now
278
                 ! TODO: Could optimize this with partial line updates in the future
280
                 ! TODO: Could optimize this with partial line updates in the future
279
                 call clear_screen()
281
                 call clear_screen()
280
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
282
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
281
-                                           repo_name, branch_name, viewport_offset, visible_items, top_padding)
283
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
282
             end if
284
             end if
283
 
285
 
284
             ! Update tracking state
286
             ! Update tracking state
@@ -288,6 +290,30 @@ contains
288
             ! Read key
290
             ! Read key
289
             call read_key(key)
291
             call read_key(key)
290
 
292
 
293
+            ! Check for alt-g to toggle git mode
294
+            ! alt-g is encoded as achar(1 + ichar('g') - ichar('a')) = achar(7)
295
+            if (key == achar(7)) then
296
+                ! Toggle between normal and git mode
297
+                if (mode == 'normal') then
298
+                    mode = 'git'
299
+                else
300
+                    mode = 'normal'
301
+                end if
302
+                needs_full_redraw = .true.
303
+                cycle  ! Skip rest of key handling
304
+            end if
305
+
306
+            ! Handle ESC key - exit git mode if active
307
+            if (key == achar(27)) then
308
+                if (mode == 'git') then
309
+                    mode = 'normal'
310
+                    needs_full_redraw = .true.
311
+                    cycle
312
+                end if
313
+                ! In normal mode, ESC does nothing for now
314
+                cycle
315
+            end if
316
+
291
             ! Handle input
317
             ! Handle input
292
             select case (key)
318
             select case (key)
293
             case ('j', 'B')  ! j or down arrow - navigate to next sibling (skip nested items)
319
             case ('j', 'B')  ! j or down arrow - navigate to next sibling (skip nested items)
@@ -309,24 +335,27 @@ contains
309
                     ! Force full redraw after tree structure change
335
                     ! Force full redraw after tree structure change
310
                     needs_full_redraw = .true.
336
                     needs_full_redraw = .true.
311
                 end if
337
                 end if
338
+            ! Git operations - only available in git mode
312
             case ('a')  ! Stage file or directory (lowercase to avoid conflict with arrow A)
339
             case ('a')  ! Stage file or directory (lowercase to avoid conflict with arrow A)
313
-                ! Check if it's a directory - stage all files in it
340
+                if (mode == 'git') then
314
-                if (.not. items(selected)%is_file) then
341
+                    ! Check if it's a directory - stage all files in it
315
-                    call git_stage_directory(items(selected)%path)
342
+                    if (.not. items(selected)%is_file) then
316
-                    ! Refresh files after staging directory
343
+                        call git_stage_directory(items(selected)%path)
317
-                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
344
+                        ! Refresh files after staging directory
318
-                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
345
+                        call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
319
-                    needs_full_redraw = .true.
346
+                                                hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
320
-                ! Otherwise it's a file - stage individual file
347
+                        needs_full_redraw = .true.
321
-                else if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
348
+                    ! Otherwise it's a file - stage individual file
322
-                    call git_add_file(items(selected)%path)
349
+                    else if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
323
-                    ! Refresh files after git add
350
+                        call git_add_file(items(selected)%path)
324
-                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
351
+                        ! Refresh files after git add
325
-                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
352
+                        call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
326
-                    needs_full_redraw = .true.
353
+                                                hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
354
+                        needs_full_redraw = .true.
355
+                    end if
327
                 end if
356
                 end if
328
             case ('u')  ! Unstage file (lowercase)
357
             case ('u')  ! Unstage file (lowercase)
329
-                if (items(selected)%is_file .and. items(selected)%is_staged) then
358
+                if (mode == 'git' .and. items(selected)%is_file .and. items(selected)%is_staged) then
330
                     call git_unstage_file(items(selected)%path)
359
                     call git_unstage_file(items(selected)%path)
331
                     ! Refresh files after git unstage
360
                     ! Refresh files after git unstage
332
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
361
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -334,84 +363,106 @@ contains
334
                     needs_full_redraw = .true.
363
                     needs_full_redraw = .true.
335
                 end if
364
                 end if
336
             case ('S')  ! Stage all (Shift+S to avoid conflict with up arrow 'A')
365
             case ('S')  ! Stage all (Shift+S to avoid conflict with up arrow 'A')
337
-                call git_stage_all()
366
+                if (mode == 'git') then
338
-                ! Refresh files after staging all
367
+                    call git_stage_all()
339
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
368
+                    ! Refresh files after staging all
340
-                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
369
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
341
-                    needs_full_redraw = .true.
370
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
371
+                        needs_full_redraw = .true.
372
+                end if
342
             case ('U')  ! Unstage all (Shift+U)
373
             case ('U')  ! Unstage all (Shift+U)
343
-                call git_unstage_all()
374
+                if (mode == 'git') then
344
-                ! Refresh files after unstaging all
375
+                    call git_unstage_all()
345
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
376
+                    ! Refresh files after unstaging all
346
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
377
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
347
-                    needs_full_redraw = .true.
378
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
379
+                        needs_full_redraw = .true.
380
+                end if
348
             case ('m')  ! Commit (lowercase)
381
             case ('m')  ! Commit (lowercase)
349
-                call commit_prompt()
382
+                if (mode == 'git') then
350
-                ! Refresh files after commit
383
+                    call commit_prompt()
351
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
384
+                    ! Refresh files after commit
352
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
385
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
353
-                    needs_full_redraw = .true.
386
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
387
+                        needs_full_redraw = .true.
388
+                end if
354
             case ('M')  ! Amend last commit (Shift+m)
389
             case ('M')  ! Amend last commit (Shift+m)
355
-                call amend_commit_prompt()
390
+                if (mode == 'git') then
356
-                ! Refresh files after amend commit
391
+                    call amend_commit_prompt()
357
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
392
+                    ! Refresh files after amend commit
358
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
393
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
359
-                    needs_full_redraw = .true.
394
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
395
+                        needs_full_redraw = .true.
396
+                end if
360
             case ('s')  ! Show git status (lowercase)
397
             case ('s')  ! Show git status (lowercase)
361
-                call show_status_view()
398
+                if (mode == 'git') then
362
-                needs_full_redraw = .true.
399
+                    call show_status_view()
363
-            case ('p')  ! Push (lowercase)
364
-                call push_prompt()
365
-                ! Refresh files after push
366
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
367
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
368
                     needs_full_redraw = .true.
400
                     needs_full_redraw = .true.
401
+                end if
402
+            case ('p')  ! Push (lowercase)
403
+                if (mode == 'git') then
404
+                    call push_prompt()
405
+                    ! Refresh files after push
406
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
407
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
408
+                        needs_full_redraw = .true.
409
+                end if
369
             case ('t')  ! Tag (lowercase)
410
             case ('t')  ! Tag (lowercase)
370
-                call tag_prompt()
411
+                if (mode == 'git') then
371
-                needs_full_redraw = .true.
412
+                    call tag_prompt()
372
-            case ('b')  ! Switch branch
373
-                call branch_switch_prompt()
374
-                ! Refresh files after branch switch
375
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
376
-                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
377
                     needs_full_redraw = .true.
413
                     needs_full_redraw = .true.
378
-                ! Update branch name display
414
+                end if
379
-                call get_repo_info(repo_name, branch_name)
415
+            case ('b')  ! Switch branch
416
+                if (mode == 'git') then
417
+                    call branch_switch_prompt()
418
+                    ! Refresh files after branch switch
419
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
420
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
421
+                        needs_full_redraw = .true.
422
+                    ! Update branch name display
423
+                    call get_repo_info(repo_name, branch_name)
424
+                end if
380
             case ('n')  ! Create new branch
425
             case ('n')  ! Create new branch
381
-                call branch_create_prompt()
426
+                if (mode == 'git') then
382
-                ! Refresh files after branch creation
427
+                    call branch_create_prompt()
383
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
428
+                    ! Refresh files after branch creation
384
-                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
429
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
385
-                    needs_full_redraw = .true.
430
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
386
-                ! Update branch name display
431
+                        needs_full_redraw = .true.
387
-                call get_repo_info(repo_name, branch_name)
432
+                    ! Update branch name display
433
+                    call get_repo_info(repo_name, branch_name)
434
+                end if
388
             case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
435
             case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
389
-                call branch_delete_prompt()
436
+                if (mode == 'git') then
390
-                needs_full_redraw = .true.
437
+                    call branch_delete_prompt()
391
-                ! No need to refresh files or update branch name (stays on current branch)
392
-            case ('f')  ! Git fetch
393
-                call git_fetch()
394
-                ! Refresh files after fetch and include files with incoming changes
395
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
396
-                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
397
                     needs_full_redraw = .true.
438
                     needs_full_redraw = .true.
439
+                    ! No need to refresh files or update branch name (stays on current branch)
440
+                end if
441
+            case ('f')  ! Git fetch
442
+                if (mode == 'git') then
443
+                    call git_fetch()
444
+                    ! Refresh files after fetch and include files with incoming changes
445
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
446
+                                            hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
447
+                        needs_full_redraw = .true.
448
+                end if
398
             case ('d')  ! Git diff with less
449
             case ('d')  ! Git diff with less
399
-                if (items(selected)%is_file) then
450
+                if (mode == 'git' .and. items(selected)%is_file) then
400
                     call git_diff_file(items(selected)%path, items(selected)%has_incoming)
451
                     call git_diff_file(items(selected)%path, items(selected)%has_incoming)
401
                     needs_full_redraw = .true.
452
                     needs_full_redraw = .true.
402
                 end if
453
                 end if
403
             case ('c')  ! View file contents (cat/bat/less)
454
             case ('c')  ! View file contents (cat/bat/less)
404
-                if (items(selected)%is_file) then
455
+                if (mode == 'git' .and. items(selected)%is_file) then
405
                     call view_file(items(selected)%path)
456
                     call view_file(items(selected)%path)
406
                     needs_full_redraw = .true.
457
                     needs_full_redraw = .true.
407
                 end if
458
                 end if
408
             case ('w')  ! Git blame (who changed this line)
459
             case ('w')  ! Git blame (who changed this line)
409
-                if (items(selected)%is_file) then
460
+                if (mode == 'git' .and. items(selected)%is_file) then
410
                     call blame_prompt(items(selected)%path)
461
                     call blame_prompt(items(selected)%path)
411
                     needs_full_redraw = .true.
462
                     needs_full_redraw = .true.
412
                 end if
463
                 end if
413
             case ('r')  ! Remove/delete file
464
             case ('r')  ! Remove/delete file
414
-                if (items(selected)%is_file) then
465
+                if (mode == 'git' .and. items(selected)%is_file) then
415
                     call delete_prompt(items(selected)%path, items(selected)%is_untracked)
466
                     call delete_prompt(items(selected)%path, items(selected)%is_untracked)
416
                     ! Refresh files after delete
467
                     ! Refresh files after delete
417
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
468
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -419,7 +470,7 @@ contains
419
                     needs_full_redraw = .true.
470
                     needs_full_redraw = .true.
420
                 end if
471
                 end if
421
             case ('x', 'X')  ! Discard changes
472
             case ('x', 'X')  ! Discard changes
422
-                if (items(selected)%is_file .and. (items(selected)%is_staged .or. items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
473
+                if (mode == 'git' .and. items(selected)%is_file .and. (items(selected)%is_staged .or. items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
423
                     call discard_prompt(items(selected)%path, items(selected)%is_staged, items(selected)%is_untracked)
474
                     call discard_prompt(items(selected)%path, items(selected)%is_staged, items(selected)%is_untracked)
424
                     ! Refresh files after discard
475
                     ! Refresh files after discard
425
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
476
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -427,63 +478,83 @@ contains
427
                     needs_full_redraw = .true.
478
                     needs_full_redraw = .true.
428
                 end if
479
                 end if
429
             case ('l')  ! Git pull
480
             case ('l')  ! Git pull
430
-                call git_pull()
481
+                if (mode == 'git') then
431
-                ! Refresh files after pull (incoming indicators will automatically clear)
482
+                    call git_pull()
432
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
483
+                    ! Refresh files after pull (incoming indicators will automatically clear)
433
-                                        hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
484
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
434
-                    needs_full_redraw = .true.
485
+                                            hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
435
-                ! Note: After successful pull, git diff will show no upstream differences
486
+                        needs_full_redraw = .true.
436
-                ! so has_incoming will be .false. for all files automatically
487
+                    ! Note: After successful pull, git diff will show no upstream differences
488
+                    ! so has_incoming will be .false. for all files automatically
489
+                end if
437
             case ('z')  ! Stash push (save changes)
490
             case ('z')  ! Stash push (save changes)
438
-                call stash_push_prompt()
491
+                if (mode == 'git') then
439
-                ! Refresh files after stash
492
+                    call stash_push_prompt()
440
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
493
+                    ! Refresh files after stash
441
-                                        hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
494
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
442
-                    needs_full_redraw = .true.
495
+                                            hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
496
+                        needs_full_redraw = .true.
497
+                end if
443
             case ('Z')  ! Stash pop/apply (restore changes)
498
             case ('Z')  ! Stash pop/apply (restore changes)
444
-                call stash_pop_apply_prompt()
499
+                if (mode == 'git') then
445
-                ! Refresh files after stash pop/apply
500
+                    call stash_pop_apply_prompt()
446
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
501
+                    ! Refresh files after stash pop/apply
447
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
502
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
448
-                    needs_full_redraw = .true.
503
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
504
+                        needs_full_redraw = .true.
505
+                end if
449
             case ('y')  ! Cherry-pick (yank commit)
506
             case ('y')  ! Cherry-pick (yank commit)
450
-                call cherry_pick_prompt()
507
+                if (mode == 'git') then
451
-                ! Refresh files after cherry-pick
508
+                    call cherry_pick_prompt()
452
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
509
+                    ! Refresh files after cherry-pick
453
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
510
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
454
-                    needs_full_redraw = .true.
511
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
512
+                        needs_full_redraw = .true.
513
+                end if
455
             case ('v')  ! Revert commit
514
             case ('v')  ! Revert commit
456
-                call revert_commit_prompt()
515
+                if (mode == 'git') then
457
-                ! Refresh files after revert
516
+                    call revert_commit_prompt()
458
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
517
+                    ! Refresh files after revert
459
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
518
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
460
-                    needs_full_redraw = .true.
519
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
520
+                        needs_full_redraw = .true.
521
+                end if
461
             case ('h')  ! Show commit history
522
             case ('h')  ! Show commit history
462
-                call history_browser_prompt()
523
+                if (mode == 'git') then
463
-                needs_full_redraw = .true.
524
+                    call history_browser_prompt()
525
+                    needs_full_redraw = .true.
526
+                end if
464
             case ('L')  ! Show reflog (Shift+l)
527
             case ('L')  ! Show reflog (Shift+l)
465
-                call reflog_browser_prompt()
528
+                if (mode == 'git') then
466
-                needs_full_redraw = .true.
529
+                    call reflog_browser_prompt()
467
-            case ('G')  ! Merge branch (Shift+g)
468
-                call merge_branch_prompt()
469
-                ! Refresh files after merge
470
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
471
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
472
                     needs_full_redraw = .true.
530
                     needs_full_redraw = .true.
473
-                ! Update branch name display in case we merged
531
+                end if
474
-                call get_repo_info(repo_name, branch_name)
532
+            case ('G')  ! Merge branch (Shift+g)
533
+                if (mode == 'git') then
534
+                    call merge_branch_prompt()
535
+                    ! Refresh files after merge
536
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
537
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
538
+                        needs_full_redraw = .true.
539
+                    ! Update branch name display in case we merged
540
+                    call get_repo_info(repo_name, branch_name)
541
+                end if
475
             case ('O')  ! Reset (Shift+o - "Oh no, undo!")
542
             case ('O')  ! Reset (Shift+o - "Oh no, undo!")
476
-                call reset_prompt()
543
+                if (mode == 'git') then
477
-                ! Refresh files after reset
544
+                    call reset_prompt()
478
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
545
+                    ! Refresh files after reset
479
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
546
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
480
-                    needs_full_redraw = .true.
547
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
548
+                        needs_full_redraw = .true.
549
+                end if
481
             case ('I')  ! Interactive rebase (Shift+i)
550
             case ('I')  ! Interactive rebase (Shift+i)
482
-                call rebase_prompt()
551
+                if (mode == 'git') then
483
-                ! Refresh files after rebase
552
+                    call rebase_prompt()
484
-                call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
553
+                    ! Refresh files after rebase
485
-                                        hide_dotfiles, selected, running, force_refresh=.true.)
554
+                    call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
486
-                    needs_full_redraw = .true.
555
+                                            hide_dotfiles, selected, running, force_refresh=.true.)
556
+                        needs_full_redraw = .true.
557
+                end if
487
             case ('.')  ! Toggle hiding dotfiles and gitignored files
558
             case ('.')  ! Toggle hiding dotfiles and gitignored files
488
                 hide_dotfiles = .not. hide_dotfiles
559
                 hide_dotfiles = .not. hide_dotfiles
489
                 ! Rebuild item list with new filter
560
                 ! Rebuild item list with new filter
@@ -497,8 +568,15 @@ contains
497
                 visible_items = term_height - top_padding - 6
568
                 visible_items = term_height - top_padding - 6
498
                 if (visible_items < 3) visible_items = 3
569
                 if (visible_items < 3) visible_items = 3
499
                 if (visible_items > n_items) visible_items = n_items
570
                 if (visible_items > n_items) visible_items = n_items
500
-            case ('q', 'Q')  ! Quit
571
+            case ('q', 'Q')  ! Quit or exit git mode
501
-                running = .false.
572
+                if (mode == 'git') then
573
+                    ! In git mode: q exits to normal mode
574
+                    mode = 'normal'
575
+                    needs_full_redraw = .true.
576
+                else
577
+                    ! In normal mode: q quits the application
578
+                    running = .false.
579
+                end if
502
             end select
580
             end select
503
         end do
581
         end do
504
 
582
 
src/terminal_module.f90modified
@@ -67,11 +67,21 @@ contains
67
         ! Read one character
67
         ! Read one character
68
         read(tty_unit, '(A1)', iostat=iostat, advance='no') key
68
         read(tty_unit, '(A1)', iostat=iostat, advance='no') key
69
 
69
 
70
-        ! Check for escape sequence (arrow keys)
70
+        ! Check for escape sequence (arrow keys or alt-key combos)
71
         if (key == achar(27)) then
71
         if (key == achar(27)) then
72
             read(tty_unit, '(A2)', iostat=iostat, advance='no') escape_seq
72
             read(tty_unit, '(A2)', iostat=iostat, advance='no') escape_seq
73
             if (escape_seq(1:1) == '[') then
73
             if (escape_seq(1:1) == '[') then
74
+                ! Arrow key sequence: ESC[A/B/C/D
74
                 key = escape_seq(2:2)  ! Return A, B, C, or D
75
                 key = escape_seq(2:2)  ! Return A, B, C, or D
76
+            else if (escape_seq(1:1) >= 'a' .and. escape_seq(1:1) <= 'z') then
77
+                ! Alt-letter sequence: ESC followed by letter
78
+                ! Encode as ASCII control characters (1-26 for alt-a through alt-z)
79
+                ! This keeps us in valid ASCII range [0-127]
80
+                ! e.g., alt-g returns achar(7) = ASCII BEL
81
+                key = achar(1 + ichar(escape_seq(1:1)) - ichar('a'))
82
+            else if (iostat /= 0) then
83
+                ! Just ESC key alone (no following character or timeout)
84
+                key = achar(27)
75
             end if
85
             end if
76
         end if
86
         end if
77
 
87