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
+            if (mode == 'git') then
149
+                ! Git mode: show in yellow/orange
150
+                write(status_line, '(A,A,A,A,A,A,A,A,A,A,A)') &
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
148
                 write(status_line, '(A,A,A,A,A,A,A)') &
158
                 write(status_line, '(A,A,A,A,A,A,A)') &
149
                     achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
159
                     achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
150
                     ':', &
160
                     ':', &
151
                     achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m'
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)', ''
179
+        if (mode == 'git') then
180
+            ! Git mode help - show in yellow tint
181
+            print '(A)', achar(27) // '[33mLegend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
182
+                         achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
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
168
             print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
188
             print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
169
                          achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
189
                          achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
170
                          achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
190
                          achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
171
                          achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
191
                          achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
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'
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,7 +335,9 @@ 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)
340
+                if (mode == 'git') then
313
                     ! Check if it's a directory - stage all files in it
341
                     ! Check if it's a directory - stage all files in it
314
                     if (.not. items(selected)%is_file) then
342
                     if (.not. items(selected)%is_file) then
315
                         call git_stage_directory(items(selected)%path)
343
                         call git_stage_directory(items(selected)%path)
@@ -325,8 +353,9 @@ contains
325
                                                 hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
353
                                                 hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
326
                         needs_full_redraw = .true.
354
                         needs_full_redraw = .true.
327
                     end if
355
                     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,42 +363,57 @@ 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')
366
+                if (mode == 'git') then
337
                     call git_stage_all()
367
                     call git_stage_all()
338
                     ! Refresh files after staging all
368
                     ! Refresh files after staging all
339
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
369
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
340
                                             hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
370
                                             hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
341
                         needs_full_redraw = .true.
371
                         needs_full_redraw = .true.
372
+                end if
342
             case ('U')  ! Unstage all (Shift+U)
373
             case ('U')  ! Unstage all (Shift+U)
374
+                if (mode == 'git') then
343
                     call git_unstage_all()
375
                     call git_unstage_all()
344
                     ! Refresh files after unstaging all
376
                     ! Refresh files after unstaging all
345
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
377
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
346
                                             hide_dotfiles, selected, running, force_refresh=.true.)
378
                                             hide_dotfiles, selected, running, force_refresh=.true.)
347
                         needs_full_redraw = .true.
379
                         needs_full_redraw = .true.
380
+                end if
348
             case ('m')  ! Commit (lowercase)
381
             case ('m')  ! Commit (lowercase)
382
+                if (mode == 'git') then
349
                     call commit_prompt()
383
                     call commit_prompt()
350
                     ! Refresh files after commit
384
                     ! Refresh files after commit
351
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
385
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
352
                                             hide_dotfiles, selected, running, force_refresh=.true.)
386
                                             hide_dotfiles, selected, running, force_refresh=.true.)
353
                         needs_full_redraw = .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)
390
+                if (mode == 'git') then
355
                     call amend_commit_prompt()
391
                     call amend_commit_prompt()
356
                     ! Refresh files after amend commit
392
                     ! Refresh files after amend commit
357
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
393
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
358
                                             hide_dotfiles, selected, running, force_refresh=.true.)
394
                                             hide_dotfiles, selected, running, force_refresh=.true.)
359
                         needs_full_redraw = .true.
395
                         needs_full_redraw = .true.
396
+                end if
360
             case ('s')  ! Show git status (lowercase)
397
             case ('s')  ! Show git status (lowercase)
398
+                if (mode == 'git') then
361
                     call show_status_view()
399
                     call show_status_view()
362
                     needs_full_redraw = .true.
400
                     needs_full_redraw = .true.
401
+                end if
363
             case ('p')  ! Push (lowercase)
402
             case ('p')  ! Push (lowercase)
403
+                if (mode == 'git') then
364
                     call push_prompt()
404
                     call push_prompt()
365
                     ! Refresh files after push
405
                     ! Refresh files after push
366
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
406
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
367
                                             hide_dotfiles, selected, running, force_refresh=.true.)
407
                                             hide_dotfiles, selected, running, force_refresh=.true.)
368
                         needs_full_redraw = .true.
408
                         needs_full_redraw = .true.
409
+                end if
369
             case ('t')  ! Tag (lowercase)
410
             case ('t')  ! Tag (lowercase)
411
+                if (mode == 'git') then
370
                     call tag_prompt()
412
                     call tag_prompt()
371
                     needs_full_redraw = .true.
413
                     needs_full_redraw = .true.
414
+                end if
372
             case ('b')  ! Switch branch
415
             case ('b')  ! Switch branch
416
+                if (mode == 'git') then
373
                     call branch_switch_prompt()
417
                     call branch_switch_prompt()
374
                     ! Refresh files after branch switch
418
                     ! Refresh files after branch switch
375
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
419
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -377,7 +421,9 @@ contains
377
                         needs_full_redraw = .true.
421
                         needs_full_redraw = .true.
378
                     ! Update branch name display
422
                     ! Update branch name display
379
                     call get_repo_info(repo_name, branch_name)
423
                     call get_repo_info(repo_name, branch_name)
424
+                end if
380
             case ('n')  ! Create new branch
425
             case ('n')  ! Create new branch
426
+                if (mode == 'git') then
381
                     call branch_create_prompt()
427
                     call branch_create_prompt()
382
                     ! Refresh files after branch creation
428
                     ! Refresh files after branch creation
383
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
429
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -385,33 +431,38 @@ contains
385
                         needs_full_redraw = .true.
431
                         needs_full_redraw = .true.
386
                     ! Update branch name display
432
                     ! Update branch name display
387
                     call get_repo_info(repo_name, branch_name)
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)
436
+                if (mode == 'git') then
389
                     call branch_delete_prompt()
437
                     call branch_delete_prompt()
390
                     needs_full_redraw = .true.
438
                     needs_full_redraw = .true.
391
                     ! No need to refresh files or update branch name (stays on current branch)
439
                     ! No need to refresh files or update branch name (stays on current branch)
440
+                end if
392
             case ('f')  ! Git fetch
441
             case ('f')  ! Git fetch
442
+                if (mode == 'git') then
393
                     call git_fetch()
443
                     call git_fetch()
394
                     ! Refresh files after fetch and include files with incoming changes
444
                     ! 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, &
445
                     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.)
446
                                             hide_dotfiles, selected, running, include_incoming=.true., force_refresh=.true.)
397
                         needs_full_redraw = .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,6 +478,7 @@ 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
481
+                if (mode == 'git') then
430
                     call git_pull()
482
                     call git_pull()
431
                     ! Refresh files after pull (incoming indicators will automatically clear)
483
                     ! Refresh files after pull (incoming indicators will automatically clear)
432
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
484
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -434,37 +486,51 @@ contains
434
                         needs_full_redraw = .true.
486
                         needs_full_redraw = .true.
435
                     ! Note: After successful pull, git diff will show no upstream differences
487
                     ! Note: After successful pull, git diff will show no upstream differences
436
                     ! so has_incoming will be .false. for all files automatically
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)
491
+                if (mode == 'git') then
438
                     call stash_push_prompt()
492
                     call stash_push_prompt()
439
                     ! Refresh files after stash
493
                     ! Refresh files after stash
440
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
494
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
441
                                             hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
495
                                             hide_dotfiles, selected, running, exit_if_empty=.true., force_refresh=.true.)
442
                         needs_full_redraw = .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)
499
+                if (mode == 'git') then
444
                     call stash_pop_apply_prompt()
500
                     call stash_pop_apply_prompt()
445
                     ! Refresh files after stash pop/apply
501
                     ! Refresh files after stash pop/apply
446
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
502
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
447
                                             hide_dotfiles, selected, running, force_refresh=.true.)
503
                                             hide_dotfiles, selected, running, force_refresh=.true.)
448
                         needs_full_redraw = .true.
504
                         needs_full_redraw = .true.
505
+                end if
449
             case ('y')  ! Cherry-pick (yank commit)
506
             case ('y')  ! Cherry-pick (yank commit)
507
+                if (mode == 'git') then
450
                     call cherry_pick_prompt()
508
                     call cherry_pick_prompt()
451
                     ! Refresh files after cherry-pick
509
                     ! Refresh files after cherry-pick
452
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
510
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
453
                                             hide_dotfiles, selected, running, force_refresh=.true.)
511
                                             hide_dotfiles, selected, running, force_refresh=.true.)
454
                         needs_full_redraw = .true.
512
                         needs_full_redraw = .true.
513
+                end if
455
             case ('v')  ! Revert commit
514
             case ('v')  ! Revert commit
515
+                if (mode == 'git') then
456
                     call revert_commit_prompt()
516
                     call revert_commit_prompt()
457
                     ! Refresh files after revert
517
                     ! Refresh files after revert
458
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
518
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
459
                                             hide_dotfiles, selected, running, force_refresh=.true.)
519
                                             hide_dotfiles, selected, running, force_refresh=.true.)
460
                         needs_full_redraw = .true.
520
                         needs_full_redraw = .true.
521
+                end if
461
             case ('h')  ! Show commit history
522
             case ('h')  ! Show commit history
523
+                if (mode == 'git') then
462
                     call history_browser_prompt()
524
                     call history_browser_prompt()
463
                     needs_full_redraw = .true.
525
                     needs_full_redraw = .true.
526
+                end if
464
             case ('L')  ! Show reflog (Shift+l)
527
             case ('L')  ! Show reflog (Shift+l)
528
+                if (mode == 'git') then
465
                     call reflog_browser_prompt()
529
                     call reflog_browser_prompt()
466
                     needs_full_redraw = .true.
530
                     needs_full_redraw = .true.
531
+                end if
467
             case ('G')  ! Merge branch (Shift+g)
532
             case ('G')  ! Merge branch (Shift+g)
533
+                if (mode == 'git') then
468
                     call merge_branch_prompt()
534
                     call merge_branch_prompt()
469
                     ! Refresh files after merge
535
                     ! Refresh files after merge
470
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
536
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
@@ -472,18 +538,23 @@ contains
472
                         needs_full_redraw = .true.
538
                         needs_full_redraw = .true.
473
                     ! Update branch name display in case we merged
539
                     ! Update branch name display in case we merged
474
                     call get_repo_info(repo_name, branch_name)
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!")
543
+                if (mode == 'git') then
476
                     call reset_prompt()
544
                     call reset_prompt()
477
                     ! Refresh files after reset
545
                     ! Refresh files after reset
478
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
546
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
479
                                             hide_dotfiles, selected, running, force_refresh=.true.)
547
                                             hide_dotfiles, selected, running, force_refresh=.true.)
480
                         needs_full_redraw = .true.
548
                         needs_full_redraw = .true.
549
+                end if
481
             case ('I')  ! Interactive rebase (Shift+i)
550
             case ('I')  ! Interactive rebase (Shift+i)
551
+                if (mode == 'git') then
482
                     call rebase_prompt()
552
                     call rebase_prompt()
483
                     ! Refresh files after rebase
553
                     ! Refresh files after rebase
484
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
554
                     call refresh_and_rebuild(show_all, files, n_files, items, n_items, tree_root, &
485
                                             hide_dotfiles, selected, running, force_refresh=.true.)
555
                                             hide_dotfiles, selected, running, force_refresh=.true.)
486
                         needs_full_redraw = .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
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
501
                     running = .false.
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