Optimize input handling and cursor movement performance
- SHA
7e7fe234f3f90540f3a90fa9c9005d658ef9dc76- Parents
-
fb634e0 - Tree
afe941e
7e7fe23
7e7fe234f3f90540f3a90fa9c9005d658ef9dc76fb634e0
afe941e| Status | File | + | - |
|---|---|---|---|
| A |
CLAUDE.md
|
149 | 0 |
| M |
app/main.f90
|
27 | 20 |
| M |
src/commands/command_handler_module.f90
|
7 | 0 |
| M |
src/terminal/input_handler_module.f90
|
28 | 28 |
| M |
src/terminal/raw_mode_module.f90
|
28 | 0 |
| M |
src/terminal/renderer_module.f90
|
33 | 0 |
| M |
src/terminal/terminal_io_module.f90
|
16 | 1 |
| M |
src/terminal/termios_wrapper.c
|
119 | 10 |
CLAUDE.mdadded@@ -0,0 +1,149 @@ | |||
| 1 | +# CLAUDE.md | ||
| 2 | + | ||
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
| 4 | + | ||
| 5 | +## Project Overview | ||
| 6 | + | ||
| 7 | +Facsimile (`fac`) is a terminal text editor written in modern Fortran with VSCode-style keybindings. It uses a gap buffer for text storage and pure ANSI escape sequences for terminal rendering. | ||
| 8 | + | ||
| 9 | +## Build Commands | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +# Standard build (recommended) | ||
| 13 | +make | ||
| 14 | + | ||
| 15 | +# Clean and rebuild | ||
| 16 | +make clean && make | ||
| 17 | + | ||
| 18 | +# Development build with comprehensive warnings | ||
| 19 | +make dev | ||
| 20 | + | ||
| 21 | +# Debug build with runtime checks | ||
| 22 | +make debug | ||
| 23 | + | ||
| 24 | +# Show compiler info | ||
| 25 | +make info | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +The Makefile auto-detects the platform: | ||
| 29 | +- **macOS arm64**: Uses gfortran-15 or flang-new from Homebrew | ||
| 30 | +- **macOS Intel/Linux**: Uses standard gfortran | ||
| 31 | + | ||
| 32 | +## Version Management | ||
| 33 | + | ||
| 34 | +```bash | ||
| 35 | +# Check current version | ||
| 36 | +make version | ||
| 37 | + | ||
| 38 | +# Bump versions | ||
| 39 | +make bump-patch # 0.9.1 -> 0.9.2 | ||
| 40 | +make bump-minor # 0.9.1 -> 0.10.0 | ||
| 41 | +make bump-major # 0.9.1 -> 1.0.0 | ||
| 42 | + | ||
| 43 | +# Full release build with checklist | ||
| 44 | +make release | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +The `VERSION` file is the single source of truth. The Makefile auto-generates `src/version_module.f90`. | ||
| 48 | + | ||
| 49 | +## Architecture | ||
| 50 | + | ||
| 51 | +### Core Data Flow | ||
| 52 | + | ||
| 53 | +``` | ||
| 54 | +Input → input_handler_module → command_handler_module → buffer operations → renderer_module → Terminal | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +### Key Modules | ||
| 58 | + | ||
| 59 | +- **`src/buffer/text_buffer_module.f90`**: Gap buffer implementation for text storage. All operations maintain gap position for efficient insertions. | ||
| 60 | + | ||
| 61 | +- **`src/editor_state_module.f90`**: Central state management. Contains `editor_state_t` with tabs, panes, cursors, LSP state, and UI panels. This is the "god object" that gets passed around. | ||
| 62 | + | ||
| 63 | +- **`src/terminal/input_handler_module.f90`**: Raw keyboard input processing. Handles escape sequences, mouse events, and key combinations. | ||
| 64 | + | ||
| 65 | +- **`src/terminal/renderer_module.f90`**: Screen rendering with ANSI escape sequences. Handles syntax highlighting, status bar, and split panes. | ||
| 66 | + | ||
| 67 | +- **`src/commands/command_handler_module.f90`**: Main command dispatch. Maps key inputs to editor actions (~7000 lines). | ||
| 68 | + | ||
| 69 | +- **`src/terminal/termios_wrapper.c`**: C wrapper for terminal raw mode via termios. | ||
| 70 | + | ||
| 71 | +### Module Dependency Order | ||
| 72 | + | ||
| 73 | +The Makefile's `SOURCES` list defines the required compilation order. Fortran modules must be compiled before modules that `use` them. The `.NOTPARALLEL` directive enforces sequential builds. | ||
| 74 | + | ||
| 75 | +### UTF-8 Handling | ||
| 76 | + | ||
| 77 | +All cursor positions use CHARACTER indices, not byte indices. The `utf8_module` provides conversion functions: | ||
| 78 | +- `utf8_char_count()` - count characters in string | ||
| 79 | +- `buffer_byte_to_char_col()` - convert byte position to character column | ||
| 80 | +- `buffer_char_to_byte_col()` - convert character column to byte position | ||
| 81 | + | ||
| 82 | +### Pane/Tab Architecture | ||
| 83 | + | ||
| 84 | +``` | ||
| 85 | +editor_state_t | ||
| 86 | + └── tabs[] (multiple open files) | ||
| 87 | + └── panes[] (split views of same file) | ||
| 88 | + ├── buffer (text content) | ||
| 89 | + ├── cursors[] (multiple cursor support) | ||
| 90 | + └── viewport (scroll position) | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +### LSP Integration | ||
| 94 | + | ||
| 95 | +Located in `src/lsp/`. Communicates with language servers via JSON-RPC over stdio: | ||
| 96 | +- `lsp_server_manager_module.f90` - Server lifecycle management | ||
| 97 | +- `json_module.f90` - JSON parsing/generation | ||
| 98 | +- `lsp_client_module.f90` - Request/response handling | ||
| 99 | + | ||
| 100 | +## Fortran-Specific Constraints | ||
| 101 | + | ||
| 102 | +### Line Length Limit | ||
| 103 | +Fortran has a 132-character line limit. Unicode characters (like box-drawing `═`) count as multiple bytes. Long lines must be split using `&` continuation: | ||
| 104 | + | ||
| 105 | +```fortran | ||
| 106 | +! Bad - will fail compilation | ||
| 107 | +line = '═══════════════════════════════════════════════════════════════════' | ||
| 108 | + | ||
| 109 | +! Good - split across lines | ||
| 110 | +line = '═══════════════════════' // & | ||
| 111 | + '═══════════════════════' // & | ||
| 112 | + '═══════════════════════' | ||
| 113 | +``` | ||
| 114 | + | ||
| 115 | +### Module Files | ||
| 116 | +Compilation generates `.mod` files in the root directory. These are binary module interfaces, not source files. | ||
| 117 | + | ||
| 118 | +## Distribution | ||
| 119 | + | ||
| 120 | +The project is distributed via three channels: | ||
| 121 | +- **Homebrew**: `homebrew-facsimile` repo with `facsimile.rb` formula | ||
| 122 | +- **AUR**: Arch User Repository PKGBUILD | ||
| 123 | +- **RPM**: Spec file at `~/rpmbuild/SPECS/facsimile.spec` | ||
| 124 | + | ||
| 125 | +When releasing, update all three with the new version and SHA256 hash from the GitHub release tarball. | ||
| 126 | + | ||
| 127 | +## Testing | ||
| 128 | + | ||
| 129 | +```bash | ||
| 130 | +# LSP module tests | ||
| 131 | +make test-lsp | ||
| 132 | + | ||
| 133 | +# Test LSP with editor | ||
| 134 | +make test-lsp-editor | ||
| 135 | + | ||
| 136 | +# Manual key testing | ||
| 137 | +./keytest # Basic key codes | ||
| 138 | +./keytest_fac # With fac's termios settings | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +## Running | ||
| 142 | + | ||
| 143 | +```bash | ||
| 144 | +./fac [filename] # Open file | ||
| 145 | +./fac --version # Show version | ||
| 146 | +./fac --help # Show help | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +Key bindings follow VSCode conventions: Ctrl-S save, Ctrl-Q quit, Ctrl-B file tree, Ctrl-F search, Ctrl-Z undo. | ||
app/main.f90modified@@ -8,7 +8,7 @@ program facsimile | |||
| 8 | use renderer_module | 8 | use renderer_module |
| 9 | use command_handler_module, only: handle_key_command, init_command_handler, cleanup_command_handler, & | 9 | use command_handler_module, only: handle_key_command, init_command_handler, cleanup_command_handler, & |
| 10 | save_initial_state_for_undo, search_pattern, match_case_sensitive, & | 10 | save_initial_state_for_undo, search_pattern, match_case_sensitive, & |
| 11 | - g_lsp_modified_buffer, g_lsp_ui_changed | 11 | + g_lsp_modified_buffer, g_lsp_ui_changed, g_cursor_only_move |
| 12 | use workspace_module | 12 | use workspace_module |
| 13 | use backup_module | 13 | use backup_module |
| 14 | use save_prompt_module | 14 | use save_prompt_module |
@@ -434,27 +434,30 @@ program facsimile | |||
| 434 | call handle_key_command(key_input, editor, buffer, should_quit) | 434 | call handle_key_command(key_input, editor, buffer, should_quit) |
| 435 | 435 | ||
| 436 | ! Sync back to active pane and other instances | 436 | ! Sync back to active pane and other instances |
| 437 | - if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then | 437 | + ! Skip buffer sync for cursor-only moves (buffer content unchanged) |
| 438 | - if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & | 438 | + if (.not. g_cursor_only_move) then |
| 439 | - size(editor%tabs(editor%active_tab_index)%panes) > 0) then | 439 | + if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then |
| 440 | - ! Get active pane index | 440 | + if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & |
| 441 | - status = editor%tabs(editor%active_tab_index)%active_pane_index | 441 | + size(editor%tabs(editor%active_tab_index)%panes) > 0) then |
| 442 | - if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then | 442 | + ! Get active pane index |
| 443 | - ! Copy main buffer back to active pane's buffer | 443 | + status = editor%tabs(editor%active_tab_index)%active_pane_index |
| 444 | - call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer) | 444 | + if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then |
| 445 | - | 445 | + ! Copy main buffer back to active pane's buffer |
| 446 | - ! Sync to all instances of this file | 446 | + call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer) |
| 447 | - if (allocated(editor%tabs(editor%active_tab_index)%panes(status)%filename)) then | 447 | + |
| 448 | - call sync_buffer_to_all_instances(editor, & | 448 | + ! Sync to all instances of this file |
| 449 | - editor%tabs(editor%active_tab_index)%panes(status)%filename, buffer) | 449 | + if (allocated(editor%tabs(editor%active_tab_index)%panes(status)%filename)) then |
| 450 | + call sync_buffer_to_all_instances(editor, & | ||
| 451 | + editor%tabs(editor%active_tab_index)%panes(status)%filename, buffer) | ||
| 452 | + end if | ||
| 450 | end if | 453 | end if |
| 451 | - end if | ||
| 452 | 454 | ||
| 453 | - ! Also update tab buffer for backwards compatibility | 455 | + ! Also update tab buffer for backwards compatibility |
| 454 | - call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer) | 456 | + call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer) |
| 455 | 457 | ||
| 456 | - ! Sync modified flag from buffer to tab | 458 | + ! Sync modified flag from buffer to tab |
| 457 | - editor%tabs(editor%active_tab_index)%modified = buffer%modified | 459 | + editor%tabs(editor%active_tab_index)%modified = buffer%modified |
| 460 | + end if | ||
| 458 | end if | 461 | end if |
| 459 | end if | 462 | end if |
| 460 | 463 | ||
@@ -462,7 +465,11 @@ program facsimile | |||
| 462 | running = .false. | 465 | running = .false. |
| 463 | else | 466 | else |
| 464 | ! Re-render screen after each command | 467 | ! Re-render screen after each command |
| 465 | - if (editor%fuss_mode_active) then | 468 | + ! Use fast path for cursor-only movements |
| 469 | + if (g_cursor_only_move) then | ||
| 470 | + call render_cursor_only(buffer, editor, allocated(search_pattern), match_case_sensitive) | ||
| 471 | + g_cursor_only_move = .false. | ||
| 472 | + else if (editor%fuss_mode_active) then | ||
| 466 | call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive) | 473 | call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive) |
| 467 | else | 474 | else |
| 468 | call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) | 475 | call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) |
src/commands/command_handler_module.f90modified@@ -79,11 +79,14 @@ module command_handler_module | |||
| 79 | public :: search_pattern, match_case_sensitive ! Exposed for status bar hint | 79 | public :: search_pattern, match_case_sensitive ! Exposed for status bar hint |
| 80 | public :: g_lsp_modified_buffer ! Flag for immediate render after LSP edits | 80 | public :: g_lsp_modified_buffer ! Flag for immediate render after LSP edits |
| 81 | public :: g_lsp_ui_changed ! Flag for immediate render after LSP UI changes | 81 | public :: g_lsp_ui_changed ! Flag for immediate render after LSP UI changes |
| 82 | + public :: g_cursor_only_move ! Flag for cursor-only moves (skip full re-render) | ||
| 82 | 83 | ||
| 83 | ! Flag to track if LSP modified the buffer (for immediate rendering) | 84 | ! Flag to track if LSP modified the buffer (for immediate rendering) |
| 84 | logical :: g_lsp_modified_buffer = .false. | 85 | logical :: g_lsp_modified_buffer = .false. |
| 85 | ! Flag to track if LSP changed UI panels (for immediate rendering) | 86 | ! Flag to track if LSP changed UI panels (for immediate rendering) |
| 86 | logical :: g_lsp_ui_changed = .false. | 87 | logical :: g_lsp_ui_changed = .false. |
| 88 | + ! Flag for cursor-only movements (can skip full re-render) | ||
| 89 | + logical :: g_cursor_only_move = .false. | ||
| 87 | 90 | ||
| 88 | type(yank_stack_t) :: yank_stack | 91 | type(yank_stack_t) :: yank_stack |
| 89 | type(undo_stack_t) :: undo_stack | 92 | type(undo_stack_t) :: undo_stack |
@@ -416,6 +419,7 @@ contains | |||
| 416 | end if | 419 | end if |
| 417 | call sync_editor_to_pane(editor) | 420 | call sync_editor_to_pane(editor) |
| 418 | call update_viewport(editor) | 421 | call update_viewport(editor) |
| 422 | + g_cursor_only_move = .true. | ||
| 419 | 423 | ||
| 420 | case('down') | 424 | case('down') |
| 421 | ! If completion popup is visible, navigate it instead | 425 | ! If completion popup is visible, navigate it instead |
@@ -438,6 +442,7 @@ contains | |||
| 438 | end if | 442 | end if |
| 439 | call sync_editor_to_pane(editor) | 443 | call sync_editor_to_pane(editor) |
| 440 | call update_viewport(editor) | 444 | call update_viewport(editor) |
| 445 | + g_cursor_only_move = .true. | ||
| 441 | 446 | ||
| 442 | case('left') | 447 | case('left') |
| 443 | ! Hide hover tooltip on movement | 448 | ! Hide hover tooltip on movement |
@@ -457,6 +462,7 @@ contains | |||
| 457 | end if | 462 | end if |
| 458 | call sync_editor_to_pane(editor) | 463 | call sync_editor_to_pane(editor) |
| 459 | call update_viewport(editor) | 464 | call update_viewport(editor) |
| 465 | + g_cursor_only_move = .true. | ||
| 460 | 466 | ||
| 461 | case('right') | 467 | case('right') |
| 462 | ! Hide hover tooltip on movement | 468 | ! Hide hover tooltip on movement |
@@ -476,6 +482,7 @@ contains | |||
| 476 | end if | 482 | end if |
| 477 | call sync_editor_to_pane(editor) | 483 | call sync_editor_to_pane(editor) |
| 478 | call update_viewport(editor) | 484 | call update_viewport(editor) |
| 485 | + g_cursor_only_move = .true. | ||
| 479 | 486 | ||
| 480 | ! Selection with shift+motion | 487 | ! Selection with shift+motion |
| 481 | case('shift-up') | 488 | case('shift-up') |
src/terminal/input_handler_module.f90modified@@ -1,6 +1,6 @@ | |||
| 1 | module input_handler_module | 1 | module input_handler_module |
| 2 | use iso_fortran_env, only: input_unit, int8, error_unit | 2 | use iso_fortran_env, only: input_unit, int8, error_unit |
| 3 | - use terminal_io_module, only: terminal_read_char | 3 | + use terminal_io_module, only: terminal_read_char, terminal_read_char_escape |
| 4 | implicit none | 4 | implicit none |
| 5 | private | 5 | private |
| 6 | 6 | ||
@@ -43,7 +43,7 @@ contains | |||
| 43 | key_str = '' | 43 | key_str = '' |
| 44 | status = -1 | 44 | status = -1 |
| 45 | 45 | ||
| 46 | - ! Read single character using raw mode function | 46 | + ! Read single character using raw mode function (50ms timeout when idle) |
| 47 | char_code = terminal_read_char() | 47 | char_code = terminal_read_char() |
| 48 | 48 | ||
| 49 | if (char_code < 0) then | 49 | if (char_code < 0) then |
@@ -90,14 +90,14 @@ contains | |||
| 90 | 90 | ||
| 91 | key_str = 'esc' | 91 | key_str = 'esc' |
| 92 | 92 | ||
| 93 | - ! Try to read next character (with timeout) | 93 | + ! Try to read next character (with fast 5ms timeout for escape sequences) |
| 94 | - char_code = terminal_read_char() | 94 | + char_code = terminal_read_char_escape() |
| 95 | if (char_code < 0) return | 95 | if (char_code < 0) return |
| 96 | ch1 = achar(char_code) | 96 | ch1 = achar(char_code) |
| 97 | 97 | ||
| 98 | if (ch1 == '[') then | 98 | if (ch1 == '[') then |
| 99 | ! CSI sequence (or Alt+[ if no valid sequence follows) | 99 | ! CSI sequence (or Alt+[ if no valid sequence follows) |
| 100 | - char_code = terminal_read_char() | 100 | + char_code = terminal_read_char_escape() |
| 101 | if (char_code < 0) then | 101 | if (char_code < 0) then |
| 102 | ! Timeout - no character follows, this is Alt+[ | 102 | ! Timeout - no character follows, this is Alt+[ |
| 103 | key_str = 'alt-[' | 103 | key_str = 'alt-[' |
@@ -123,7 +123,7 @@ contains | |||
| 123 | key_str = 'shift-tab' | 123 | key_str = 'shift-tab' |
| 124 | case('3') | 124 | case('3') |
| 125 | ! Could be delete or Alt+Delete | 125 | ! Could be delete or Alt+Delete |
| 126 | - char_code = terminal_read_char() | 126 | + char_code = terminal_read_char_escape() |
| 127 | if (char_code >= 0) then | 127 | if (char_code >= 0) then |
| 128 | ch3 = achar(char_code) | 128 | ch3 = achar(char_code) |
| 129 | if (ch3 == '~') then | 129 | if (ch3 == '~') then |
@@ -131,11 +131,11 @@ contains | |||
| 131 | else if (ch3 == ';') then | 131 | else if (ch3 == ';') then |
| 132 | ! Modified delete: ESC [ 3 ; modifier ~ | 132 | ! Modified delete: ESC [ 3 ; modifier ~ |
| 133 | ! Read the modifier | 133 | ! Read the modifier |
| 134 | - char_code = terminal_read_char() | 134 | + char_code = terminal_read_char_escape() |
| 135 | if (char_code >= 0) then | 135 | if (char_code >= 0) then |
| 136 | modifier_ch = achar(char_code) | 136 | modifier_ch = achar(char_code) |
| 137 | ! Read the terminating ~ | 137 | ! Read the terminating ~ |
| 138 | - char_code = terminal_read_char() | 138 | + char_code = terminal_read_char_escape() |
| 139 | if (char_code >= 0 .and. achar(char_code) == '~') then | 139 | if (char_code >= 0 .and. achar(char_code) == '~') then |
| 140 | ! Check modifier: 3 = Alt | 140 | ! Check modifier: 3 = Alt |
| 141 | if (modifier_ch == '3') then | 141 | if (modifier_ch == '3') then |
@@ -149,7 +149,7 @@ contains | |||
| 149 | end if | 149 | end if |
| 150 | case('5') | 150 | case('5') |
| 151 | ! Could be page up | 151 | ! Could be page up |
| 152 | - char_code = terminal_read_char() | 152 | + char_code = terminal_read_char_escape() |
| 153 | if (char_code >= 0) then | 153 | if (char_code >= 0) then |
| 154 | ch3 = achar(char_code) | 154 | ch3 = achar(char_code) |
| 155 | ios = 0 | 155 | ios = 0 |
@@ -164,7 +164,7 @@ contains | |||
| 164 | end if | 164 | end if |
| 165 | case('6') | 165 | case('6') |
| 166 | ! Could be page down | 166 | ! Could be page down |
| 167 | - char_code = terminal_read_char() | 167 | + char_code = terminal_read_char_escape() |
| 168 | if (char_code >= 0) then | 168 | if (char_code >= 0) then |
| 169 | ch3 = achar(char_code) | 169 | ch3 = achar(char_code) |
| 170 | ios = 0 | 170 | ios = 0 |
@@ -180,7 +180,7 @@ contains | |||
| 180 | case('1') | 180 | case('1') |
| 181 | ! Could be function key (F1-F9) or modified arrow/home/end | 181 | ! Could be function key (F1-F9) or modified arrow/home/end |
| 182 | ! Check next character | 182 | ! Check next character |
| 183 | - char_code = terminal_read_char() | 183 | + char_code = terminal_read_char_escape() |
| 184 | if (char_code >= 0) then | 184 | if (char_code >= 0) then |
| 185 | ch3 = achar(char_code) | 185 | ch3 = achar(char_code) |
| 186 | if (ch3 == '~') then | 186 | if (ch3 == '~') then |
@@ -188,14 +188,14 @@ contains | |||
| 188 | key_str = 'f1' | 188 | key_str = 'f1' |
| 189 | else if (ch3 == '0') then | 189 | else if (ch3 == '0') then |
| 190 | ! F10 might be ESC [ 2 1 ~, check for tilde | 190 | ! F10 might be ESC [ 2 1 ~, check for tilde |
| 191 | - char_code = terminal_read_char() | 191 | + char_code = terminal_read_char_escape() |
| 192 | if (char_code >= 0 .and. achar(char_code) == '~') then | 192 | if (char_code >= 0 .and. achar(char_code) == '~') then |
| 193 | key_str = 'f10' | 193 | key_str = 'f10' |
| 194 | end if | 194 | end if |
| 195 | else if (ch3 == '1' .or. ch3 == '2' .or. ch3 == '3' .or. ch3 == '4' .or. & | 195 | else if (ch3 == '1' .or. ch3 == '2' .or. ch3 == '3' .or. ch3 == '4' .or. & |
| 196 | ch3 == '5' .or. ch3 == '7' .or. ch3 == '8' .or. ch3 == '9') then | 196 | ch3 == '5' .or. ch3 == '7' .or. ch3 == '8' .or. ch3 == '9') then |
| 197 | ! Function keys F1-F8: ESC [ 1 X ~ or ESC [ 1 X ; modifier ~ | 197 | ! Function keys F1-F8: ESC [ 1 X ~ or ESC [ 1 X ; modifier ~ |
| 198 | - char_code = terminal_read_char() | 198 | + char_code = terminal_read_char_escape() |
| 199 | if (char_code >= 0) then | 199 | if (char_code >= 0) then |
| 200 | ch = achar(char_code) | 200 | ch = achar(char_code) |
| 201 | if (ch == '~') then | 201 | if (ch == '~') then |
@@ -230,12 +230,12 @@ contains | |||
| 230 | end if | 230 | end if |
| 231 | case('2') | 231 | case('2') |
| 232 | ! Could be F9-F12 or alternate modified keys | 232 | ! Could be F9-F12 or alternate modified keys |
| 233 | - char_code = terminal_read_char() | 233 | + char_code = terminal_read_char_escape() |
| 234 | if (char_code >= 0) then | 234 | if (char_code >= 0) then |
| 235 | ch3 = achar(char_code) | 235 | ch3 = achar(char_code) |
| 236 | if (ch3 == '0' .or. ch3 == '1' .or. ch3 == '3' .or. ch3 == '4') then | 236 | if (ch3 == '0' .or. ch3 == '1' .or. ch3 == '3' .or. ch3 == '4') then |
| 237 | ! Function keys F9-F12: ESC [ 2 X ~ or ESC [ 2 X ; modifier ~ | 237 | ! Function keys F9-F12: ESC [ 2 X ~ or ESC [ 2 X ; modifier ~ |
| 238 | - char_code = terminal_read_char() | 238 | + char_code = terminal_read_char_escape() |
| 239 | if (char_code >= 0) then | 239 | if (char_code >= 0) then |
| 240 | ch = achar(char_code) | 240 | ch = achar(char_code) |
| 241 | if (ch == '~') then | 241 | if (ch == '~') then |
@@ -257,7 +257,7 @@ contains | |||
| 257 | end if | 257 | end if |
| 258 | else if (ch3 == ';') then | 258 | else if (ch3 == ';') then |
| 259 | ! ESC [ 2 ; A format (shift+arrow) | 259 | ! ESC [ 2 ; A format (shift+arrow) |
| 260 | - char_code = terminal_read_char() | 260 | + char_code = terminal_read_char_escape() |
| 261 | if (char_code >= 0) then | 261 | if (char_code >= 0) then |
| 262 | ch = achar(char_code) | 262 | ch = achar(char_code) |
| 263 | key_str = 'shift-' | 263 | key_str = 'shift-' |
@@ -297,7 +297,7 @@ contains | |||
| 297 | end select | 297 | end select |
| 298 | else if (ch1 == 'O') then | 298 | else if (ch1 == 'O') then |
| 299 | ! SS3 sequence (e.g., function keys F1-F4) | 299 | ! SS3 sequence (e.g., function keys F1-F4) |
| 300 | - char_code = terminal_read_char() | 300 | + char_code = terminal_read_char_escape() |
| 301 | if (char_code < 0) then | 301 | if (char_code < 0) then |
| 302 | ! Timeout - this is just Alt+O | 302 | ! Timeout - this is just Alt+O |
| 303 | key_str = 'alt-o' | 303 | key_str = 'alt-o' |
@@ -319,12 +319,12 @@ contains | |||
| 319 | end select | 319 | end select |
| 320 | else if (ch1 == achar(27)) then | 320 | else if (ch1 == achar(27)) then |
| 321 | ! ESC ESC - likely Alt+something | 321 | ! ESC ESC - likely Alt+something |
| 322 | - char_code = terminal_read_char() | 322 | + char_code = terminal_read_char_escape() |
| 323 | if (char_code >= 0) then | 323 | if (char_code >= 0) then |
| 324 | ch2 = achar(char_code) | 324 | ch2 = achar(char_code) |
| 325 | if (ch2 == '[') then | 325 | if (ch2 == '[') then |
| 326 | ! ESC ESC [ - Alt+arrow keys or Alt+modified keys | 326 | ! ESC ESC [ - Alt+arrow keys or Alt+modified keys |
| 327 | - char_code = terminal_read_char() | 327 | + char_code = terminal_read_char_escape() |
| 328 | if (char_code >= 0) then | 328 | if (char_code >= 0) then |
| 329 | ch3 = achar(char_code) | 329 | ch3 = achar(char_code) |
| 330 | select case(ch3) | 330 | select case(ch3) |
@@ -338,7 +338,7 @@ contains | |||
| 338 | key_str = 'alt-left' | 338 | key_str = 'alt-left' |
| 339 | case('3') | 339 | case('3') |
| 340 | ! Could be Alt+Delete (ESC ESC [ 3 ~) | 340 | ! Could be Alt+Delete (ESC ESC [ 3 ~) |
| 341 | - char_code = terminal_read_char() | 341 | + char_code = terminal_read_char_escape() |
| 342 | if (char_code >= 0 .and. achar(char_code) == '~') then | 342 | if (char_code >= 0 .and. achar(char_code) == '~') then |
| 343 | key_str = 'alt-delete' | 343 | key_str = 'alt-delete' |
| 344 | end if | 344 | end if |
@@ -406,7 +406,7 @@ contains | |||
| 406 | read_count = read_count + 1 | 406 | read_count = read_count + 1 |
| 407 | if (read_count > 20) exit ! Safety limit | 407 | if (read_count > 20) exit ! Safety limit |
| 408 | 408 | ||
| 409 | - char_code = terminal_read_char() | 409 | + char_code = terminal_read_char_escape() |
| 410 | if (char_code >= 0) then | 410 | if (char_code >= 0) then |
| 411 | ch = achar(char_code) | 411 | ch = achar(char_code) |
| 412 | ios = 0 | 412 | ios = 0 |
@@ -506,14 +506,14 @@ contains | |||
| 506 | read(modifier_char, '(i1)') modifier | 506 | read(modifier_char, '(i1)') modifier |
| 507 | 507 | ||
| 508 | ! Read the next character - might be the key or a semicolon | 508 | ! Read the next character - might be the key or a semicolon |
| 509 | - char_code = terminal_read_char() | 509 | + char_code = terminal_read_char_escape() |
| 510 | if (char_code < 0) return | 510 | if (char_code < 0) return |
| 511 | ch = achar(char_code) | 511 | ch = achar(char_code) |
| 512 | 512 | ||
| 513 | ! Check if there's a semicolon (ESC [ 2 ; A format) or direct key (ESC [ 2 A) | 513 | ! Check if there's a semicolon (ESC [ 2 ; A format) or direct key (ESC [ 2 A) |
| 514 | if (ch == ';') then | 514 | if (ch == ';') then |
| 515 | ! Read the actual key | 515 | ! Read the actual key |
| 516 | - char_code = terminal_read_char() | 516 | + char_code = terminal_read_char_escape() |
| 517 | if (char_code < 0) return | 517 | if (char_code < 0) return |
| 518 | ch = achar(char_code) | 518 | ch = achar(char_code) |
| 519 | end if | 519 | end if |
@@ -574,7 +574,7 @@ contains | |||
| 574 | read_count = read_count + 1 | 574 | read_count = read_count + 1 |
| 575 | if (read_count > 20) exit | 575 | if (read_count > 20) exit |
| 576 | 576 | ||
| 577 | - char_code = terminal_read_char() | 577 | + char_code = terminal_read_char_escape() |
| 578 | if (char_code < 0) exit | 578 | if (char_code < 0) exit |
| 579 | 579 | ||
| 580 | ch = achar(char_code) | 580 | ch = achar(char_code) |
@@ -641,7 +641,7 @@ contains | |||
| 641 | 641 | ||
| 642 | ! Read modifier sequence (already past the semicolon) | 642 | ! Read modifier sequence (already past the semicolon) |
| 643 | do | 643 | do |
| 644 | - char_code = terminal_read_char() | 644 | + char_code = terminal_read_char_escape() |
| 645 | if (char_code >= 0) then | 645 | if (char_code >= 0) then |
| 646 | ch = achar(char_code) | 646 | ch = achar(char_code) |
| 647 | ios = 0 | 647 | ios = 0 |
@@ -738,12 +738,12 @@ contains | |||
| 738 | end if | 738 | end if |
| 739 | 739 | ||
| 740 | ! Read modifier (should be a digit 2-8) | 740 | ! Read modifier (should be a digit 2-8) |
| 741 | - char_code = terminal_read_char() | 741 | + char_code = terminal_read_char_escape() |
| 742 | if (char_code < 0) return | 742 | if (char_code < 0) return |
| 743 | modifier_ch = achar(char_code) | 743 | modifier_ch = achar(char_code) |
| 744 | 744 | ||
| 745 | ! Read terminating ~ | 745 | ! Read terminating ~ |
| 746 | - char_code = terminal_read_char() | 746 | + char_code = terminal_read_char_escape() |
| 747 | if (char_code < 0 .or. achar(char_code) /= '~') return | 747 | if (char_code < 0 .or. achar(char_code) /= '~') return |
| 748 | 748 | ||
| 749 | ! Parse modifier: 2=Shift, 3=Alt, 4=Alt+Shift, 5=Ctrl, 6=Ctrl+Shift, 7=Alt+Ctrl, 8=Alt+Shift | 749 | ! Parse modifier: 2=Shift, 3=Alt, 4=Alt+Shift, 5=Ctrl, 6=Ctrl+Shift, 7=Alt+Ctrl, 8=Alt+Shift |
@@ -784,7 +784,7 @@ contains | |||
| 784 | 784 | ||
| 785 | ! Read until 'M' (press) or 'm' (release) | 785 | ! Read until 'M' (press) or 'm' (release) |
| 786 | do | 786 | do |
| 787 | - char_code = terminal_read_char() | 787 | + char_code = terminal_read_char_escape() |
| 788 | if (char_code >= 0) then | 788 | if (char_code >= 0) then |
| 789 | ch = achar(char_code) | 789 | ch = achar(char_code) |
| 790 | ios = 0 | 790 | ios = 0 |
src/terminal/raw_mode_module.f90modified@@ -4,6 +4,7 @@ module raw_mode_module | |||
| 4 | private | 4 | private |
| 5 | 5 | ||
| 6 | public :: enable_raw_mode, disable_raw_mode, input_available, read_char_timeout | 6 | public :: enable_raw_mode, disable_raw_mode, input_available, read_char_timeout |
| 7 | + public :: read_char_escape, input_available_count | ||
| 7 | 8 | ||
| 8 | ! C function interfaces | 9 | ! C function interfaces |
| 9 | interface | 10 | interface |
@@ -22,10 +23,20 @@ module raw_mode_module | |||
| 22 | integer(c_int) :: c_input_available | 23 | integer(c_int) :: c_input_available |
| 23 | end function c_input_available | 24 | end function c_input_available |
| 24 | 25 | ||
| 26 | + function c_input_available_count() bind(C, name="input_available_count") | ||
| 27 | + import :: c_int | ||
| 28 | + integer(c_int) :: c_input_available_count | ||
| 29 | + end function c_input_available_count | ||
| 30 | + | ||
| 25 | function c_read_char_timeout() bind(C, name="read_char_timeout") | 31 | function c_read_char_timeout() bind(C, name="read_char_timeout") |
| 26 | import :: c_int | 32 | import :: c_int |
| 27 | integer(c_int) :: c_read_char_timeout | 33 | integer(c_int) :: c_read_char_timeout |
| 28 | end function c_read_char_timeout | 34 | end function c_read_char_timeout |
| 35 | + | ||
| 36 | + function c_read_char_escape() bind(C, name="read_char_escape") | ||
| 37 | + import :: c_int | ||
| 38 | + integer(c_int) :: c_read_char_escape | ||
| 39 | + end function c_read_char_escape | ||
| 29 | end interface | 40 | end interface |
| 30 | 41 | ||
| 31 | contains | 42 | contains |
@@ -54,6 +65,14 @@ contains | |||
| 54 | available = (result > 0) | 65 | available = (result > 0) |
| 55 | end function input_available | 66 | end function input_available |
| 56 | 67 | ||
| 68 | + function input_available_count() result(count) | ||
| 69 | + integer :: count | ||
| 70 | + integer(c_int) :: result | ||
| 71 | + | ||
| 72 | + result = c_input_available_count() | ||
| 73 | + count = result | ||
| 74 | + end function input_available_count | ||
| 75 | + | ||
| 57 | function read_char_timeout() result(ch) | 76 | function read_char_timeout() result(ch) |
| 58 | integer :: ch | 77 | integer :: ch |
| 59 | integer(c_int) :: result | 78 | integer(c_int) :: result |
@@ -62,4 +81,13 @@ contains | |||
| 62 | ch = result | 81 | ch = result |
| 63 | end function read_char_timeout | 82 | end function read_char_timeout |
| 64 | 83 | ||
| 84 | + ! Fast read for escape sequences (5ms timeout instead of 50ms) | ||
| 85 | + function read_char_escape() result(ch) | ||
| 86 | + integer :: ch | ||
| 87 | + integer(c_int) :: result | ||
| 88 | + | ||
| 89 | + result = c_read_char_escape() | ||
| 90 | + ch = result | ||
| 91 | + end function read_char_escape | ||
| 92 | + | ||
| 65 | end module raw_mode_module | 93 | end module raw_mode_module |
src/terminal/renderer_module.f90modified@@ -27,6 +27,7 @@ module renderer_module | |||
| 27 | public :: render_screen_with_tree, render_screen_with_lsp_panel | 27 | public :: render_screen_with_tree, render_screen_with_lsp_panel |
| 28 | public :: tree_state | 28 | public :: tree_state |
| 29 | public :: update_syntax_highlighter | 29 | public :: update_syntax_highlighter |
| 30 | + public :: render_cursor_only ! Fast path for cursor-only updates | ||
| 30 | 31 | ||
| 31 | ! Configuration | 32 | ! Configuration |
| 32 | logical :: show_line_numbers = .true. | 33 | logical :: show_line_numbers = .true. |
@@ -773,6 +774,38 @@ contains | |||
| 773 | end if | 774 | end if |
| 774 | end subroutine render_cursor | 775 | end subroutine render_cursor |
| 775 | 776 | ||
| 777 | + ! Fast path for cursor-only updates - just update cursor and status bar | ||
| 778 | + ! Use this when only cursor position changed, not buffer content | ||
| 779 | + subroutine render_cursor_only(buffer, editor, match_mode_active, match_case_sens) | ||
| 780 | + type(buffer_t), intent(in) :: buffer | ||
| 781 | + type(editor_state_t), intent(inout) :: editor | ||
| 782 | + logical, intent(in), optional :: match_mode_active | ||
| 783 | + logical, intent(in), optional :: match_case_sens | ||
| 784 | + | ||
| 785 | + ! Just render the status bar and position cursor | ||
| 786 | + call render_status_bar(editor, buffer, match_mode_active, match_case_sens) | ||
| 787 | + | ||
| 788 | + ! Handle panes vs single buffer | ||
| 789 | + if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0 .and. & | ||
| 790 | + editor%active_tab_index <= size(editor%tabs)) then | ||
| 791 | + if (allocated(editor%tabs(editor%active_tab_index)%panes)) then | ||
| 792 | + if (editor%fuss_mode_active) then | ||
| 793 | + call render_cursor_for_panes_with_tree(editor, 31, editor%screen_cols - 30) | ||
| 794 | + else | ||
| 795 | + call render_cursor_for_panes(editor) | ||
| 796 | + end if | ||
| 797 | + return | ||
| 798 | + end if | ||
| 799 | + end if | ||
| 800 | + | ||
| 801 | + ! Single buffer mode | ||
| 802 | + if (editor%fuss_mode_active) then | ||
| 803 | + call render_cursor_in_pane(editor, 31, editor%screen_cols - 30) | ||
| 804 | + else | ||
| 805 | + call render_cursor(editor, buffer) | ||
| 806 | + end if | ||
| 807 | + end subroutine render_cursor_only | ||
| 808 | + | ||
| 776 | subroutine update_viewport(editor) | 809 | subroutine update_viewport(editor) |
| 777 | use editor_state_module, only: pane_t | 810 | use editor_state_module, only: pane_t |
| 778 | type(editor_state_t), intent(inout) :: editor | 811 | type(editor_state_t), intent(inout) :: editor |
src/terminal/terminal_io_module.f90modified@@ -4,7 +4,9 @@ module terminal_io_module | |||
| 4 | use raw_mode_module, only: raw_enable_raw_mode => enable_raw_mode, & | 4 | use raw_mode_module, only: raw_enable_raw_mode => enable_raw_mode, & |
| 5 | raw_disable_raw_mode => disable_raw_mode, & | 5 | raw_disable_raw_mode => disable_raw_mode, & |
| 6 | raw_input_available => input_available, & | 6 | raw_input_available => input_available, & |
| 7 | - raw_read_char_timeout => read_char_timeout | 7 | + raw_read_char_timeout => read_char_timeout, & |
| 8 | + raw_read_char_escape => read_char_escape, & | ||
| 9 | + raw_input_available_count => input_available_count | ||
| 8 | implicit none | 10 | implicit none |
| 9 | private | 11 | private |
| 10 | 12 | ||
@@ -13,6 +15,7 @@ module terminal_io_module | |||
| 13 | public :: terminal_get_size, terminal_enable_raw_mode, terminal_disable_raw_mode | 15 | public :: terminal_get_size, terminal_enable_raw_mode, terminal_disable_raw_mode |
| 14 | public :: terminal_write, terminal_enable_mouse, terminal_disable_mouse | 16 | public :: terminal_write, terminal_enable_mouse, terminal_disable_mouse |
| 15 | public :: terminal_input_available, terminal_read_char | 17 | public :: terminal_input_available, terminal_read_char |
| 18 | + public :: terminal_read_char_escape, terminal_input_available_count | ||
| 16 | 19 | ||
| 17 | ! ANSI escape codes | 20 | ! ANSI escape codes |
| 18 | character(len=*), parameter :: ESC = char(27) | 21 | character(len=*), parameter :: ESC = char(27) |
@@ -118,6 +121,18 @@ contains | |||
| 118 | ch = raw_read_char_timeout() | 121 | ch = raw_read_char_timeout() |
| 119 | end function terminal_read_char | 122 | end function terminal_read_char |
| 120 | 123 | ||
| 124 | + ! Fast read for escape sequences (5ms timeout) | ||
| 125 | + function terminal_read_char_escape() result(ch) | ||
| 126 | + integer :: ch | ||
| 127 | + ch = raw_read_char_escape() | ||
| 128 | + end function terminal_read_char_escape | ||
| 129 | + | ||
| 130 | + ! Get count of available input bytes | ||
| 131 | + function terminal_input_available_count() result(count) | ||
| 132 | + integer :: count | ||
| 133 | + count = raw_input_available_count() | ||
| 134 | + end function terminal_input_available_count | ||
| 135 | + | ||
| 121 | subroutine terminal_write(text) | 136 | subroutine terminal_write(text) |
| 122 | character(len=*), intent(in) :: text | 137 | character(len=*), intent(in) :: text |
| 123 | write(output_unit, '(a)', advance='no') text | 138 | write(output_unit, '(a)', advance='no') text |
src/terminal/termios_wrapper.cmodified@@ -5,10 +5,18 @@ | |||
| 5 | #include <stdio.h> | 5 | #include <stdio.h> |
| 6 | #include <errno.h> | 6 | #include <errno.h> |
| 7 | #include <sys/ioctl.h> | 7 | #include <sys/ioctl.h> |
| 8 | +#include <sys/select.h> | ||
| 9 | +#include <string.h> | ||
| 8 | 10 | ||
| 9 | static struct termios orig_termios; | 11 | static struct termios orig_termios; |
| 10 | static int raw_mode_enabled = 0; | 12 | static int raw_mode_enabled = 0; |
| 11 | 13 | ||
| 14 | +// Input buffer for batching reads | ||
| 15 | +#define INPUT_BUFFER_SIZE 256 | ||
| 16 | +static unsigned char input_buffer[INPUT_BUFFER_SIZE]; | ||
| 17 | +static int buffer_start = 0; | ||
| 18 | +static int buffer_end = 0; | ||
| 19 | + | ||
| 12 | // Enable raw mode - returns 0 on success, -1 on failure | 20 | // Enable raw mode - returns 0 on success, -1 on failure |
| 13 | int enable_raw_mode(void) { | 21 | int enable_raw_mode(void) { |
| 14 | if (raw_mode_enabled) return 0; | 22 | if (raw_mode_enabled) return 0; |
@@ -31,15 +39,17 @@ int enable_raw_mode(void) { | |||
| 31 | // Local flags: disable canonical mode, echo, signals, extended input processing | 39 | // Local flags: disable canonical mode, echo, signals, extended input processing |
| 32 | raw.c_lflag &= ~(tcflag_t)(ECHO | ICANON | ISIG | IEXTEN); | 40 | raw.c_lflag &= ~(tcflag_t)(ECHO | ICANON | ISIG | IEXTEN); |
| 33 | 41 | ||
| 34 | - // Control characters: minimum bytes and timeout for read() | 42 | + // Control characters: non-blocking reads |
| 35 | - raw.c_cc[VMIN] = 0; // Return each byte, or zero for timeout | 43 | + // We'll use select() for timeout management instead of VTIME |
| 36 | - raw.c_cc[VTIME] = 1; // 100ms timeout (unit is 1/10 second) | 44 | + raw.c_cc[VMIN] = 0; // Don't block |
| 45 | + raw.c_cc[VTIME] = 0; // No timeout - we use select() instead | ||
| 37 | 46 | ||
| 38 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { | 47 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { |
| 39 | return -1; | 48 | return -1; |
| 40 | } | 49 | } |
| 41 | 50 | ||
| 42 | raw_mode_enabled = 1; | 51 | raw_mode_enabled = 1; |
| 52 | + buffer_start = buffer_end = 0; | ||
| 43 | return 0; | 53 | return 0; |
| 44 | } | 54 | } |
| 45 | 55 | ||
@@ -52,11 +62,18 @@ int disable_raw_mode(void) { | |||
| 52 | } | 62 | } |
| 53 | 63 | ||
| 54 | raw_mode_enabled = 0; | 64 | raw_mode_enabled = 0; |
| 65 | + buffer_start = buffer_end = 0; | ||
| 55 | return 0; | 66 | return 0; |
| 56 | } | 67 | } |
| 57 | 68 | ||
| 58 | // Check if input is available (non-blocking) | 69 | // Check if input is available (non-blocking) |
| 59 | int input_available(void) { | 70 | int input_available(void) { |
| 71 | + // First check our buffer | ||
| 72 | + if (buffer_start < buffer_end) { | ||
| 73 | + return 1; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + // Then check stdin | ||
| 60 | int nread; | 77 | int nread; |
| 61 | if (ioctl(STDIN_FILENO, FIONREAD, &nread) == -1) { | 78 | if (ioctl(STDIN_FILENO, FIONREAD, &nread) == -1) { |
| 62 | return 0; | 79 | return 0; |
@@ -64,13 +81,105 @@ int input_available(void) { | |||
| 64 | return nread > 0; | 81 | return nread > 0; |
| 65 | } | 82 | } |
| 66 | 83 | ||
| 67 | -// Read a single character (with timeout) | 84 | +// Get count of available input bytes |
| 85 | +int input_available_count(void) { | ||
| 86 | + int buffered = buffer_end - buffer_start; | ||
| 87 | + int pending = 0; | ||
| 88 | + if (ioctl(STDIN_FILENO, FIONREAD, &pending) == -1) { | ||
| 89 | + pending = 0; | ||
| 90 | + } | ||
| 91 | + return buffered + pending; | ||
| 92 | +} | ||
| 93 | + | ||
| 94 | +// Fill the input buffer with all available data | ||
| 95 | +static void fill_input_buffer(void) { | ||
| 96 | + // Shift remaining data to start of buffer | ||
| 97 | + if (buffer_start > 0 && buffer_start < buffer_end) { | ||
| 98 | + memmove(input_buffer, input_buffer + buffer_start, buffer_end - buffer_start); | ||
| 99 | + buffer_end -= buffer_start; | ||
| 100 | + buffer_start = 0; | ||
| 101 | + } else if (buffer_start >= buffer_end) { | ||
| 102 | + buffer_start = buffer_end = 0; | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + // Read all available data into buffer | ||
| 106 | + int space = INPUT_BUFFER_SIZE - buffer_end; | ||
| 107 | + if (space > 0) { | ||
| 108 | + ssize_t nread = read(STDIN_FILENO, input_buffer + buffer_end, space); | ||
| 109 | + if (nread > 0) { | ||
| 110 | + buffer_end += nread; | ||
| 111 | + } | ||
| 112 | + } | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +// Read a single character with smart timeout | ||
| 116 | +// - If data is buffered or available, return immediately | ||
| 117 | +// - Otherwise wait up to timeout_ms for input | ||
| 118 | +// - Use short timeout (5ms) for escape sequence continuation | ||
| 119 | +// - Use longer timeout (50ms) for initial wait when idle | ||
| 68 | int read_char_timeout(void) { | 120 | int read_char_timeout(void) { |
| 69 | - char c; | 121 | + // Return from buffer if available |
| 70 | - ssize_t nread = read(STDIN_FILENO, &c, 1); | 122 | + if (buffer_start < buffer_end) { |
| 71 | - if (nread == 1) { | 123 | + return input_buffer[buffer_start++]; |
| 72 | - return (unsigned char)c; | 124 | + } |
| 73 | - } else { | 125 | + |
| 74 | - return -1; // No input or error | 126 | + // Try to fill buffer |
| 127 | + fill_input_buffer(); | ||
| 128 | + if (buffer_start < buffer_end) { | ||
| 129 | + return input_buffer[buffer_start++]; | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + // No data available - wait with select() | ||
| 133 | + // Use 50ms timeout for responsive feel without busy-waiting | ||
| 134 | + fd_set readfds; | ||
| 135 | + struct timeval tv; | ||
| 136 | + | ||
| 137 | + FD_ZERO(&readfds); | ||
| 138 | + FD_SET(STDIN_FILENO, &readfds); | ||
| 139 | + tv.tv_sec = 0; | ||
| 140 | + tv.tv_usec = 50000; // 50ms - good balance of responsiveness and CPU usage | ||
| 141 | + | ||
| 142 | + int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); | ||
| 143 | + if (ret > 0) { | ||
| 144 | + fill_input_buffer(); | ||
| 145 | + if (buffer_start < buffer_end) { | ||
| 146 | + return input_buffer[buffer_start++]; | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + return -1; // No input | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +// Read a character with very short timeout (for escape sequences) | ||
| 154 | +// This is used when we've already seen ESC and are looking for the rest | ||
| 155 | +int read_char_escape(void) { | ||
| 156 | + // Return from buffer if available | ||
| 157 | + if (buffer_start < buffer_end) { | ||
| 158 | + return input_buffer[buffer_start++]; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + // Try immediate read first | ||
| 162 | + fill_input_buffer(); | ||
| 163 | + if (buffer_start < buffer_end) { | ||
| 164 | + return input_buffer[buffer_start++]; | ||
| 75 | } | 165 | } |
| 166 | + | ||
| 167 | + // Short wait for escape sequence continuation (5ms) | ||
| 168 | + fd_set readfds; | ||
| 169 | + struct timeval tv; | ||
| 170 | + | ||
| 171 | + FD_ZERO(&readfds); | ||
| 172 | + FD_SET(STDIN_FILENO, &readfds); | ||
| 173 | + tv.tv_sec = 0; | ||
| 174 | + tv.tv_usec = 5000; // 5ms - fast escape sequence detection | ||
| 175 | + | ||
| 176 | + int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); | ||
| 177 | + if (ret > 0) { | ||
| 178 | + fill_input_buffer(); | ||
| 179 | + if (buffer_start < buffer_end) { | ||
| 180 | + return input_buffer[buffer_start++]; | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + return -1; | ||
| 76 | } | 185 | } |