fortrangoingonforty/fuss / 1e3f0b6

Browse files

fuzzy jump on typing

Authored by espadonne
SHA
1e3f0b6322e8e5ee5fe6ee026165261fb3a467d9
Parents
5085e0c
Tree
623fcad

2 changed files

StatusFile+-
M src/display_module.f90 4 3
M src/fuss_main.f90 153 5
src/display_module.f90modified
@@ -129,12 +129,13 @@ 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, mode)
132
+                                     repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
133
+                                     search_buffer, search_length)
133
         type(tree_node), pointer, intent(in) :: tree_root
134
         type(tree_node), pointer, intent(in) :: tree_root
134
         integer, intent(in) :: n_items, selected
135
         integer, intent(in) :: n_items, selected
135
         type(selectable_item), intent(in) :: items(:)
136
         type(selectable_item), intent(in) :: items(:)
136
-        character(len=*), intent(in) :: repo_name, branch_name, mode
137
+        character(len=*), intent(in) :: repo_name, branch_name, mode, search_buffer
137
-        integer, intent(in) :: viewport_offset, visible_items, top_padding
138
+        integer, intent(in) :: viewport_offset, visible_items, top_padding, search_length
138
         integer :: item_idx, viewport_end, i
139
         integer :: item_idx, viewport_end, i
139
         character(len=512) :: status_line
140
         character(len=512) :: status_line
140
 
141
 
src/fuss_main.f90modified
@@ -179,6 +179,10 @@ contains
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
         character(len=10) :: mode  ! "normal" or "git" mode
182
+        ! Search state for fuzzy jump
183
+        character(len=32) :: search_buffer
184
+        integer :: search_length
185
+        real(8) :: last_search_time, current_time
182
         type(tree_node), pointer :: tree_root
186
         type(tree_node), pointer :: tree_root
183
 
187
 
184
         ! Initialize tree pointer
188
         ! Initialize tree pointer
@@ -246,6 +250,11 @@ contains
246
         running = .true.
250
         running = .true.
247
         mode = 'normal'  ! Start in normal mode
251
         mode = 'normal'  ! Start in normal mode
248
 
252
 
253
+        ! Initialize search state
254
+        search_buffer = ''
255
+        search_length = 0
256
+        last_search_time = 0.0d0
257
+
249
         ! Partial redraw optimization: initialize tracking state
258
         ! Partial redraw optimization: initialize tracking state
250
         prev_selected = 0  ! Force initial draw
259
         prev_selected = 0  ! Force initial draw
251
         prev_viewport = 0
260
         prev_viewport = 0
@@ -273,20 +282,32 @@ contains
273
                 ! Full redraw needed: viewport scrolled or forced refresh
282
                 ! Full redraw needed: viewport scrolled or forced refresh
274
                 call clear_screen()
283
                 call clear_screen()
275
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
284
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
276
-                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
285
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
286
+                                           search_buffer, search_length)
277
                 needs_full_redraw = .false.
287
                 needs_full_redraw = .false.
278
             else if (selected /= prev_selected) then
288
             else if (selected /= prev_selected) then
279
                 ! Only selection changed within same viewport - still need full redraw for now
289
                 ! Only selection changed within same viewport - still need full redraw for now
280
                 ! TODO: Could optimize this with partial line updates in the future
290
                 ! TODO: Could optimize this with partial line updates in the future
281
                 call clear_screen()
291
                 call clear_screen()
282
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
292
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
283
-                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
293
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
294
+                                           search_buffer, search_length)
284
             end if
295
             end if
285
 
296
 
286
             ! Update tracking state
297
             ! Update tracking state
287
             prev_selected = selected
298
             prev_selected = selected
288
             prev_viewport = viewport_offset
299
             prev_viewport = viewport_offset
289
 
300
 
301
+            ! Check search timeout (1 second)
302
+            if (search_length > 0) then
303
+                current_time = get_wall_time()
304
+                if (current_time - last_search_time > 1.0d0) then
305
+                    search_length = 0
306
+                    search_buffer = ''
307
+                    needs_full_redraw = .true.
308
+                end if
309
+            end if
310
+
290
             ! Read key
311
             ! Read key
291
             call read_key(key)
312
             call read_key(key)
292
 
313
 
@@ -303,13 +324,14 @@ contains
303
                 call execute_command_line('stty sane < /dev/tty')
324
                 call execute_command_line('stty sane < /dev/tty')
304
                 call clear_screen()
325
                 call clear_screen()
305
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
326
                 call draw_interactive_tree(tree_root, items, n_items, selected, &
306
-                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
327
+                                           repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
328
+                                           search_buffer, search_length)
307
                 ! Restore cbreak mode
329
                 ! Restore cbreak mode
308
                 call enable_raw_mode()
330
                 call enable_raw_mode()
309
                 cycle  ! Skip rest of key handling
331
                 cycle  ! Skip rest of key handling
310
             end if
332
             end if
311
 
333
 
312
-            ! Handle ESC key - exit git mode if active
334
+            ! Handle ESC key - exit git mode or clear search
313
             if (key == achar(27)) then
335
             if (key == achar(27)) then
314
                 if (mode == 'git') then
336
                 if (mode == 'git') then
315
                     mode = 'normal'
337
                     mode = 'normal'
@@ -317,10 +339,17 @@ contains
317
                     call execute_command_line('stty sane < /dev/tty')
339
                     call execute_command_line('stty sane < /dev/tty')
318
                     call clear_screen()
340
                     call clear_screen()
319
                     call draw_interactive_tree(tree_root, items, n_items, selected, &
341
                     call draw_interactive_tree(tree_root, items, n_items, selected, &
320
-                                               repo_name, branch_name, viewport_offset, visible_items, top_padding, mode)
342
+                                               repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
343
+                                               search_buffer, search_length)
321
                     ! Restore cbreak mode
344
                     ! Restore cbreak mode
322
                     call enable_raw_mode()
345
                     call enable_raw_mode()
323
                     cycle
346
                     cycle
347
+                else if (search_length > 0) then
348
+                    ! Clear search in normal mode
349
+                    search_length = 0
350
+                    search_buffer = ''
351
+                    needs_full_redraw = .true.
352
+                    cycle
324
                 end if
353
                 end if
325
                 ! In normal mode, ESC does nothing for now
354
                 ! In normal mode, ESC does nothing for now
326
                 cycle
355
                 cycle
@@ -589,6 +618,39 @@ contains
589
                     ! In normal mode: q quits the application
618
                     ! In normal mode: q quits the application
590
                     running = .false.
619
                     running = .false.
591
                 end if
620
                 end if
621
+            case default  ! Handle fuzzy search in normal mode
622
+                if (mode == 'normal') then
623
+                    ! Check if it's a printable letter/number for search
624
+                    if ((key >= 'a' .and. key <= 'z') .or. (key >= 'A' .and. key <= 'Z') .or. &
625
+                        (key >= '0' .and. key <= '9') .or. key == '_' .or. key == '-' .or. key == '.') then
626
+                        ! Add to search buffer
627
+                        if (search_length < 32) then
628
+                            search_length = search_length + 1
629
+                            search_buffer(search_length:search_length) = key
630
+                            last_search_time = get_wall_time()
631
+
632
+                            ! Find first matching item and jump immediately
633
+                            call fuzzy_jump_to_match(items, n_items, search_buffer(1:search_length), selected)
634
+
635
+                            ! Redraw will happen at top of loop
636
+                            needs_full_redraw = .true.
637
+                        end if
638
+                    else if (key == achar(127) .or. key == achar(8)) then
639
+                        ! Backspace - remove last character
640
+                        if (search_length > 0) then
641
+                            search_length = search_length - 1
642
+                            last_search_time = get_wall_time()
643
+
644
+                            ! Re-search with shorter pattern
645
+                            if (search_length > 0) then
646
+                                call fuzzy_jump_to_match(items, n_items, search_buffer(1:search_length), selected)
647
+                            end if
648
+
649
+                            ! Redraw will happen at top of loop
650
+                            needs_full_redraw = .true.
651
+                        end if
652
+                    end if
653
+                end if
592
             end select
654
             end select
593
         end do
655
         end do
594
 
656
 
@@ -1410,4 +1472,90 @@ contains
1410
         end if
1472
         end if
1411
     end subroutine refresh_and_rebuild
1473
     end subroutine refresh_and_rebuild
1412
 
1474
 
1475
+    subroutine fuzzy_jump_to_match(items, n_items, pattern, selected)
1476
+        ! Jump to first item that fuzzy matches the pattern
1477
+        type(selectable_item), intent(in) :: items(:)
1478
+        integer, intent(in) :: n_items
1479
+        character(len=*), intent(in) :: pattern
1480
+        integer, intent(inout) :: selected
1481
+        integer :: i
1482
+
1483
+        ! Search from current position forward
1484
+        do i = selected, n_items
1485
+            if (fuzzy_match(pattern, items(i)%path)) then
1486
+                selected = i
1487
+                return
1488
+            end if
1489
+        end do
1490
+
1491
+        ! Wrap around: search from beginning to current position
1492
+        do i = 1, selected - 1
1493
+            if (fuzzy_match(pattern, items(i)%path)) then
1494
+                selected = i
1495
+                return
1496
+            end if
1497
+        end do
1498
+
1499
+        ! No match found - stay at current position
1500
+    end subroutine fuzzy_jump_to_match
1501
+
1502
+    function fuzzy_match(pattern, text) result(matches)
1503
+        ! Fuzzy matching like fzf: pattern chars must appear in order in text
1504
+        ! Case-insensitive matching
1505
+        ! Returns .true. if all pattern chars found in sequence
1506
+        character(len=*), intent(in) :: pattern, text
1507
+        logical :: matches
1508
+        integer :: pattern_idx, text_idx
1509
+        character(len=1) :: pattern_char, text_char
1510
+
1511
+        matches = .false.
1512
+
1513
+        ! Empty pattern matches everything
1514
+        if (len_trim(pattern) == 0) then
1515
+            matches = .true.
1516
+            return
1517
+        end if
1518
+
1519
+        pattern_idx = 1
1520
+
1521
+        ! Scan through text looking for each pattern character in order
1522
+        do text_idx = 1, len_trim(text)
1523
+            if (pattern_idx > len_trim(pattern)) exit
1524
+
1525
+            ! Case-insensitive comparison
1526
+            pattern_char = pattern(pattern_idx:pattern_idx)
1527
+            text_char = text(text_idx:text_idx)
1528
+
1529
+            ! Convert to lowercase for comparison
1530
+            if (pattern_char >= 'A' .and. pattern_char <= 'Z') then
1531
+                pattern_char = achar(ichar(pattern_char) + 32)
1532
+            end if
1533
+            if (text_char >= 'A' .and. text_char <= 'Z') then
1534
+                text_char = achar(ichar(text_char) + 32)
1535
+            end if
1536
+
1537
+            if (pattern_char == text_char) then
1538
+                pattern_idx = pattern_idx + 1
1539
+            end if
1540
+        end do
1541
+
1542
+        ! Match succeeds if we found all pattern characters
1543
+        matches = (pattern_idx > len_trim(pattern))
1544
+    end function fuzzy_match
1545
+
1546
+    function get_wall_time() result(time_seconds)
1547
+        ! Get wall-clock time in seconds (for timeouts)
1548
+        ! Uses system date_and_time which provides millisecond precision
1549
+        real(8) :: time_seconds
1550
+        integer :: values(8)
1551
+
1552
+        call date_and_time(values=values)
1553
+
1554
+        ! Convert to seconds: hours*3600 + minutes*60 + seconds + milliseconds/1000
1555
+        time_seconds = real(values(5), 8) * 3600.0d0 + &
1556
+                      real(values(6), 8) * 60.0d0 + &
1557
+                      real(values(7), 8) + &
1558
+                      real(values(8), 8) / 1000.0d0
1559
+    end function get_wall_time
1560
+
1413
 end program fuss
1561
 end program fuss