Fortran · 53687 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: readline
3 ! Purpose: Advanced input handling with command history and line editing
4 ! ==============================================================================
5 module readline
6 use shell_types
7 use system_interface
8 use iso_fortran_env, only: input_unit, output_unit, error_unit
9 use iso_c_binding
10 implicit none
11
12 ! Constants for special keys
13 integer, parameter :: KEY_ENTER = 10
14 integer, parameter :: KEY_BACKSPACE = 127
15 integer, parameter :: KEY_DELETE = 127 ! Same as backspace on most terminals
16 integer, parameter :: KEY_TAB = 9
17 integer, parameter :: KEY_CTRL_C = 3
18 integer, parameter :: KEY_CTRL_D = 4
19 integer, parameter :: KEY_CTRL_A = 1 ! Home (beginning of line)
20 integer, parameter :: KEY_CTRL_E = 5 ! End (end of line)
21 integer, parameter :: KEY_CTRL_K = 11 ! Kill to end of line
22 integer, parameter :: KEY_CTRL_L = 12 ! Clear screen
23 integer, parameter :: KEY_CTRL_W = 23 ! Kill previous word
24 integer, parameter :: KEY_CTRL_U = 21 ! Kill entire line
25 integer, parameter :: KEY_CTRL_Y = 25 ! Yank (paste) killed text
26 integer, parameter :: KEY_CTRL_F = 6 ! Forward character (same as right arrow)
27 integer, parameter :: KEY_CTRL_B = 2 ! Backward character (same as left arrow)
28 integer, parameter :: KEY_ESC = 27
29 integer, parameter :: KEY_UP = 65
30 integer, parameter :: KEY_DOWN = 66
31 integer, parameter :: KEY_RIGHT = 67
32 integer, parameter :: KEY_LEFT = 68
33
34 ! History and line management
35 integer, parameter :: MAX_HISTORY = 1000
36 integer, parameter :: MAX_LINE_LEN = 1024
37
38 ! Input state management
39 ! Editing mode constants
40 integer, parameter :: EDITING_MODE_EMACS = 1
41 integer, parameter :: EDITING_MODE_VI = 2
42 integer, parameter :: VI_MODE_INSERT = 1
43 integer, parameter :: VI_MODE_COMMAND = 2
44
45 type :: input_state_t
46 character(len=MAX_LINE_LEN) :: buffer = ''
47 character(len=MAX_LINE_LEN) :: original_buffer = '' ! Save original input during history navigation
48 character(len=MAX_LINE_LEN) :: kill_buffer = '' ! Kill ring buffer for cut/paste
49 integer :: length = 0
50 integer :: cursor_pos = 0 ! 0-based position in buffer
51 integer :: history_pos = 0 ! Current position in history (0 = not browsing)
52 integer :: kill_length = 0 ! Length of text in kill buffer
53 logical :: dirty = .false. ! Needs redraw
54 logical :: in_history = .false. ! Currently browsing history
55
56 ! Editing mode support
57 integer :: editing_mode = EDITING_MODE_EMACS
58 integer :: vi_mode = VI_MODE_INSERT
59 character(len=MAX_LINE_LEN) :: vi_command_buffer = ''
60 integer :: vi_command_count = 0
61 logical :: vi_repeat_pending = .false.
62 end type input_state_t
63
64 type :: history_t
65 character(len=MAX_LINE_LEN) :: lines(MAX_HISTORY)
66 integer :: count = 0
67 integer :: current = 0 ! Current position in history navigation
68 end type history_t
69
70 type(history_t), save :: command_history
71
72 contains
73
74 ! Enhanced readline with character-by-character input processing
75 subroutine readline_enhanced(prompt, line, iostat)
76 character(len=*), intent(in) :: prompt
77 character(len=*), intent(out) :: line
78 integer, intent(out) :: iostat
79
80 type(input_state_t) :: input_state
81 type(termios_t) :: original_termios
82 character :: ch
83 logical :: success, done, raw_enabled
84 integer :: char_code
85
86 iostat = 0
87 done = .false.
88 raw_enabled = .false.
89
90 ! Try to enable raw mode (only works in interactive mode)
91 success = enable_raw_mode(original_termios)
92 if (success) then
93 raw_enabled = .true.
94 end if
95
96 ! Print prompt
97 write(output_unit, '(a)', advance='no') prompt
98 flush(output_unit)
99
100 ! Initialize input state
101 input_state%buffer = ''
102 input_state%original_buffer = ''
103 input_state%kill_buffer = ''
104 input_state%length = 0
105 input_state%cursor_pos = 0
106 input_state%history_pos = 0
107 input_state%kill_length = 0
108 input_state%dirty = .false.
109 input_state%in_history = .false.
110
111 if (raw_enabled) then
112 ! Enhanced input processing
113 do while (.not. done)
114 success = read_single_char(ch)
115 if (.not. success) then
116 iostat = -1
117 exit
118 end if
119
120 char_code = iachar(ch)
121
122 select case(char_code)
123 case(KEY_ENTER)
124 ! Enter - finish input
125 write(output_unit, '()') ! New line
126 done = .true.
127
128 case(KEY_CTRL_D)
129 ! Ctrl+D - EOF
130 if (input_state%length == 0) then
131 iostat = -1
132 done = .true.
133 end if
134
135 case(KEY_CTRL_C)
136 ! Ctrl+C - cancel input
137 write(output_unit, '(a)') '^C'
138 input_state%buffer = ''
139 input_state%length = 0
140 done = .true.
141
142 case(KEY_BACKSPACE)
143 ! Backspace
144 call handle_backspace(input_state)
145
146 case(KEY_TAB)
147 ! Tab completion (placeholder)
148 call handle_tab_completion(input_state)
149
150 case(KEY_ESC)
151 ! Escape sequence - try to read more
152 call handle_escape_sequence(input_state, done)
153
154 case(KEY_CTRL_A)
155 ! Home - move to beginning of line
156 call handle_home(input_state)
157
158 case(KEY_CTRL_E)
159 ! End - move to end of line
160 call handle_end(input_state)
161
162 case(KEY_CTRL_F)
163 ! Forward character (same as right arrow)
164 call handle_cursor_right(input_state)
165
166 case(KEY_CTRL_B)
167 ! Backward character (same as left arrow)
168 call handle_cursor_left(input_state)
169
170 case(KEY_CTRL_K)
171 ! Kill to end of line
172 call handle_kill_to_end(input_state)
173
174 case(KEY_CTRL_U)
175 ! Kill entire line
176 call handle_kill_line(input_state)
177
178 case(KEY_CTRL_W)
179 ! Kill previous word
180 call handle_kill_word(input_state)
181
182 case(KEY_CTRL_Y)
183 ! Yank (paste) killed text
184 call handle_yank(input_state)
185
186 case(KEY_CTRL_L)
187 ! Clear screen and redraw
188 call handle_clear_screen(input_state)
189
190 case(32:126)
191 ! Regular printable characters
192 call insert_char(input_state, ch)
193
194 case default
195 ! Ignore other control characters for now
196 end select
197
198 ! Redraw line if needed
199 if (input_state%dirty) then
200 call redraw_line(prompt, input_state)
201 input_state%dirty = .false.
202 end if
203 end do
204
205 ! Restore terminal
206 if (.not. restore_terminal(original_termios)) then
207 ! Warning but don't fail
208 end if
209 else
210 ! Fallback to line-based input
211 read(input_unit, '(a)', iostat=iostat) input_state%buffer
212 if (iostat == 0) input_state%length = len_trim(input_state%buffer)
213 end if
214
215 ! Return the result
216 if (iostat == 0) then
217 line = input_state%buffer(:input_state%length)
218 ! write(error_unit, '(a,a,a,i0)') 'DEBUG: Got line: "', trim(line), '", length: ', input_state%length
219 if (input_state%length > 0) then
220 call add_to_history(line)
221 end if
222 else
223 line = ''
224 ! write(error_unit, '(a)') 'DEBUG: iostat not 0, no line returned'
225 end if
226 end subroutine
227
228 ! Simple fallback readline - uses standard input for now
229 ! This is a placeholder for a full readline implementation
230 subroutine readline_simple(prompt, line, iostat)
231 character(len=*), intent(in) :: prompt
232 character(len=*), intent(out) :: line
233 integer, intent(out) :: iostat
234
235 ! Print prompt
236 write(output_unit, '(a)', advance='no') prompt
237 flush(output_unit)
238
239 ! Read line using standard input (no special key handling yet)
240 read(input_unit, '(a)', iostat=iostat) line
241
242 ! Add to history if successful and non-empty
243 if (iostat == 0 .and. len_trim(line) > 0) then
244 call add_to_history(line)
245 end if
246 end subroutine
247
248 ! Enhanced readline with tab completion support
249 ! Note: This is a simplified version that detects tab in the input
250 subroutine readline_with_completion(prompt, line, iostat)
251 character(len=*), intent(in) :: prompt
252 character(len=*), intent(out) :: line
253 integer, intent(out) :: iostat
254
255 character(len=MAX_LINE_LEN) :: temp_line
256 character(len=MAX_LINE_LEN) :: completions(50)
257 integer :: num_completions, tab_pos
258
259 ! Print prompt
260 write(output_unit, '(a)', advance='no') prompt
261 flush(output_unit)
262
263 ! Read line using standard input
264 read(input_unit, '(a)', iostat=iostat) temp_line
265
266 if (iostat /= 0) then
267 line = ''
268 return
269 end if
270
271 ! Check for tab character in input (simplified detection)
272 tab_pos = index(temp_line, char(KEY_TAB))
273 if (tab_pos > 0) then
274 ! Extract partial input before tab
275 if (tab_pos == 1) then
276 temp_line = ''
277 else
278 temp_line = temp_line(:tab_pos-1)
279 end if
280
281 ! Perform tab completion
282 call tab_complete(temp_line, completions, num_completions)
283
284 if (num_completions > 0) then
285 if (num_completions == 1) then
286 ! Single completion - auto-complete
287 line = trim(temp_line) // trim(completions(1))
288 write(output_unit, '(a)') trim(line)
289 else
290 ! Multiple completions - show options
291 call show_completions(completions, num_completions)
292 line = temp_line
293 end if
294 else
295 line = temp_line
296 end if
297 else
298 line = temp_line
299 end if
300
301 ! Add to history if non-empty
302 if (len_trim(line) > 0) then
303 call add_to_history(line)
304 end if
305 end subroutine
306
307 subroutine add_to_history(line)
308 character(len=*), intent(in) :: line
309 integer :: i
310
311 ! Debug: Show what we're trying to add
312 ! write(error_unit, '(a,a)') 'DEBUG: Adding to history: "', trim(line) // '"'
313
314 ! Don't add duplicate consecutive commands
315 if (command_history%count > 0) then
316 if (trim(command_history%lines(command_history%count)) == trim(line)) then
317 return
318 end if
319 end if
320
321 ! Shift history if at max capacity
322 if (command_history%count >= MAX_HISTORY) then
323 do i = 1, MAX_HISTORY - 1
324 command_history%lines(i) = command_history%lines(i + 1)
325 end do
326 command_history%count = MAX_HISTORY - 1
327 end if
328
329 ! Add new command
330 command_history%count = command_history%count + 1
331 command_history%lines(command_history%count) = line
332
333 ! Reset current position
334 command_history%current = command_history%count + 1
335
336 ! Debug: Show history count
337 ! write(error_unit, '(a,i0)') 'DEBUG: History count now: ', command_history%count
338 end subroutine
339
340 subroutine get_history_line(index, line, found)
341 integer, intent(in) :: index
342 character(len=*), intent(out) :: line
343 logical, intent(out) :: found
344
345 if (index >= 1 .and. index <= command_history%count) then
346 line = command_history%lines(index)
347 found = .true.
348 else
349 line = ''
350 found = .false.
351 end if
352 end subroutine
353
354 function get_history_count() result(count)
355 integer :: count
356 count = command_history%count
357 end function
358
359 ! Show command history (for 'history' builtin)
360 subroutine show_history()
361 integer :: i
362
363 if (command_history%count == 0) then
364 write(output_unit, '(a)') 'No commands in history.'
365 else
366 do i = 1, command_history%count
367 write(output_unit, '(i4,2x,a)') i, trim(command_history%lines(i))
368 end do
369 end if
370 end subroutine
371
372 ! Clear history
373 subroutine clear_history()
374 command_history%count = 0
375 command_history%current = 0
376 end subroutine
377
378 ! History expansion functions
379 function expand_history(input_line) result(expanded_line)
380 character(len=*), intent(in) :: input_line
381 character(len=len(input_line)) :: expanded_line
382
383 character(len=len(input_line)) :: work_line
384 integer :: pos, expansion_start, expansion_end
385 character(len=256) :: expansion, replacement
386 logical :: found_expansion
387
388 work_line = input_line
389 expanded_line = ''
390 pos = 1
391
392 do while (pos <= len_trim(work_line))
393 if (work_line(pos:pos) == '!' .and. pos < len_trim(work_line)) then
394 ! Found potential history expansion
395 expansion_start = pos
396 expansion_end = find_history_expansion_end(work_line, pos)
397
398 if (expansion_end > expansion_start) then
399 expansion = work_line(expansion_start:expansion_end)
400 call process_history_expansion(expansion, replacement, found_expansion)
401
402 if (found_expansion) then
403 expanded_line = trim(expanded_line) // trim(replacement)
404 pos = expansion_end + 1
405 else
406 expanded_line = trim(expanded_line) // '!'
407 pos = pos + 1
408 end if
409 else
410 expanded_line = trim(expanded_line) // '!'
411 pos = pos + 1
412 end if
413 else
414 expanded_line = trim(expanded_line) // work_line(pos:pos)
415 pos = pos + 1
416 end if
417 end do
418 end function
419
420 function find_history_expansion_end(line, start_pos) result(end_pos)
421 character(len=*), intent(in) :: line
422 integer, intent(in) :: start_pos
423 integer :: end_pos
424
425 integer :: pos
426 character :: ch
427
428 pos = start_pos + 1 ! Skip the '!'
429 end_pos = start_pos
430
431 if (pos > len_trim(line)) return
432
433 ch = line(pos:pos)
434
435 if (ch == '!') then
436 ! !! expansion
437 end_pos = pos
438 else if (ch >= '0' .and. ch <= '9') then
439 ! !n expansion (number)
440 do while (pos <= len_trim(line) .and. line(pos:pos) >= '0' .and. line(pos:pos) <= '9')
441 end_pos = pos
442 pos = pos + 1
443 end do
444 else if (ch == '-') then
445 ! !-n expansion (negative number)
446 pos = pos + 1
447 if (pos <= len_trim(line) .and. line(pos:pos) >= '0' .and. line(pos:pos) <= '9') then
448 do while (pos <= len_trim(line) .and. line(pos:pos) >= '0' .and. line(pos:pos) <= '9')
449 end_pos = pos
450 pos = pos + 1
451 end do
452 end if
453 else if ((ch >= 'a' .and. ch <= 'z') .or. (ch >= 'A' .and. ch <= 'Z') .or. ch == '_') then
454 ! !string expansion
455 do while (pos <= len_trim(line) .and. &
456 ((line(pos:pos) >= 'a' .and. line(pos:pos) <= 'z') .or. &
457 (line(pos:pos) >= 'A' .and. line(pos:pos) <= 'Z') .or. &
458 (line(pos:pos) >= '0' .and. line(pos:pos) <= '9') .or. &
459 line(pos:pos) == '_' .or. line(pos:pos) == '-'))
460 end_pos = pos
461 pos = pos + 1
462 end do
463 end if
464 end function
465
466 subroutine process_history_expansion(expansion, replacement, found)
467 character(len=*), intent(in) :: expansion
468 character(len=*), intent(out) :: replacement
469 logical, intent(out) :: found
470
471 character(len=256) :: search_pattern
472 integer :: history_num, i, search_len
473
474 replacement = ''
475 found = .false.
476
477 if (len_trim(expansion) < 2) return
478
479 select case (expansion(2:2))
480 case ('!')
481 ! !! - last command
482 if (command_history%count > 0) then
483 replacement = command_history%lines(command_history%count)
484 found = .true.
485 end if
486
487 case ('0':'9')
488 ! !n - command number n
489 read(expansion(2:), *, iostat=i) history_num
490 if (i == 0 .and. history_num >= 1 .and. history_num <= command_history%count) then
491 replacement = command_history%lines(history_num)
492 found = .true.
493 end if
494
495 case ('-')
496 ! !-n - n commands back
497 if (len_trim(expansion) > 2) then
498 read(expansion(3:), *, iostat=i) history_num
499 if (i == 0 .and. history_num > 0) then
500 history_num = command_history%count - history_num + 1
501 if (history_num >= 1 .and. history_num <= command_history%count) then
502 replacement = command_history%lines(history_num)
503 found = .true.
504 end if
505 end if
506 end if
507
508 case default
509 ! !string - last command starting with string
510 search_pattern = expansion(2:)
511 search_len = len_trim(search_pattern)
512
513 if (search_len > 0) then
514 ! Search backwards through history
515 do i = command_history%count, 1, -1
516 if (len_trim(command_history%lines(i)) >= search_len) then
517 if (command_history%lines(i)(1:search_len) == search_pattern) then
518 replacement = command_history%lines(i)
519 found = .true.
520 exit
521 end if
522 end if
523 end do
524 end if
525 end select
526 end subroutine
527
528 function needs_history_expansion(line) result(needs_expansion)
529 character(len=*), intent(in) :: line
530 logical :: needs_expansion
531
532 integer :: pos
533
534 needs_expansion = .false.
535 pos = index(line, '!')
536
537 do while (pos > 0 .and. pos < len_trim(line))
538 ! Check if this ! is the start of a history expansion
539 if (pos == 1 .or. line(pos-1:pos-1) == ' ' .or. line(pos-1:pos-1) == char(9)) then
540 ! Check what follows the !
541 if (line(pos+1:pos+1) == '!' .or. &
542 (line(pos+1:pos+1) >= '0' .and. line(pos+1:pos+1) <= '9') .or. &
543 line(pos+1:pos+1) == '-' .or. &
544 (line(pos+1:pos+1) >= 'a' .and. line(pos+1:pos+1) <= 'z') .or. &
545 (line(pos+1:pos+1) >= 'A' .and. line(pos+1:pos+1) <= 'Z')) then
546 needs_expansion = .true.
547 return
548 end if
549 end if
550
551 ! Look for next !
552 pos = index(line(pos+1:), '!')
553 if (pos > 0) pos = pos + len_trim(line(1:pos))
554 end do
555 end function
556
557 ! Editing mode control functions
558 subroutine set_editing_mode(input_state, mode)
559 type(input_state_t), intent(inout) :: input_state
560 integer, intent(in) :: mode
561
562 if (mode == EDITING_MODE_EMACS .or. mode == EDITING_MODE_VI) then
563 input_state%editing_mode = mode
564 if (mode == EDITING_MODE_VI) then
565 input_state%vi_mode = VI_MODE_INSERT
566 end if
567 end if
568 end subroutine
569
570 subroutine handle_vi_mode_switch(input_state, key)
571 type(input_state_t), intent(inout) :: input_state
572 integer, intent(in) :: key
573
574 if (input_state%editing_mode /= EDITING_MODE_VI) return
575
576 select case (input_state%vi_mode)
577 case (VI_MODE_INSERT)
578 if (key == KEY_ESC) then
579 input_state%vi_mode = VI_MODE_COMMAND
580 ! Move cursor back one position in command mode
581 if (input_state%cursor_pos > 0) then
582 input_state%cursor_pos = input_state%cursor_pos - 1
583 end if
584 input_state%dirty = .true.
585 end if
586
587 case (VI_MODE_COMMAND)
588 select case (key)
589 case (ichar('i'))
590 ! Insert mode
591 input_state%vi_mode = VI_MODE_INSERT
592 case (ichar('a'))
593 ! Append mode
594 input_state%vi_mode = VI_MODE_INSERT
595 if (input_state%cursor_pos < input_state%length) then
596 input_state%cursor_pos = input_state%cursor_pos + 1
597 end if
598 case (ichar('I'))
599 ! Insert at beginning
600 input_state%vi_mode = VI_MODE_INSERT
601 input_state%cursor_pos = 0
602 case (ichar('A'))
603 ! Append at end
604 input_state%vi_mode = VI_MODE_INSERT
605 input_state%cursor_pos = input_state%length
606 case (ichar('o'))
607 ! Open new line below (simplified)
608 input_state%vi_mode = VI_MODE_INSERT
609 input_state%cursor_pos = input_state%length
610 case (ichar('O'))
611 ! Open new line above (simplified)
612 input_state%vi_mode = VI_MODE_INSERT
613 input_state%cursor_pos = 0
614 end select
615 input_state%dirty = .true.
616 end select
617 end subroutine
618
619 subroutine handle_vi_command_mode(input_state, key)
620 type(input_state_t), intent(inout) :: input_state
621 integer, intent(in) :: key
622
623 if (input_state%editing_mode /= EDITING_MODE_VI .or. input_state%vi_mode /= VI_MODE_COMMAND) return
624
625 select case (key)
626 ! Navigation
627 case (ichar('h'))
628 ! Move left
629 if (input_state%cursor_pos > 0) then
630 input_state%cursor_pos = input_state%cursor_pos - 1
631 input_state%dirty = .true.
632 end if
633 case (ichar('l'))
634 ! Move right
635 if (input_state%cursor_pos < input_state%length - 1) then
636 input_state%cursor_pos = input_state%cursor_pos + 1
637 input_state%dirty = .true.
638 end if
639 case (ichar('j'))
640 ! Move down (history down)
641 call handle_history_down(input_state)
642 case (ichar('k'))
643 ! Move up (history up)
644 call handle_history_up(input_state)
645 case (ichar('0'))
646 ! Beginning of line
647 input_state%cursor_pos = 0
648 input_state%dirty = .true.
649 case (ichar('$'))
650 ! End of line
651 input_state%cursor_pos = input_state%length
652 input_state%dirty = .true.
653 case (ichar('w'))
654 ! Next word
655 call move_to_next_word(input_state)
656 case (ichar('b'))
657 ! Previous word
658 call move_to_previous_word(input_state)
659
660 ! Deletion
661 case (ichar('x'))
662 ! Delete character at cursor
663 call delete_char_at_cursor(input_state)
664 case (ichar('X'))
665 ! Delete character before cursor
666 if (input_state%cursor_pos > 0) then
667 input_state%cursor_pos = input_state%cursor_pos - 1
668 call delete_char_at_cursor(input_state)
669 end if
670 case (ichar('d'))
671 ! Delete (simplified - would need more complex handling)
672 call handle_vi_delete_command(input_state)
673
674 ! Undo/Redo (simplified)
675 case (ichar('u'))
676 ! Undo (simplified)
677 input_state%buffer = input_state%original_buffer
678 input_state%length = len_trim(input_state%original_buffer)
679 input_state%cursor_pos = min(input_state%cursor_pos, input_state%length)
680 input_state%dirty = .true.
681 end select
682 end subroutine
683
684 subroutine handle_vi_delete_command(input_state)
685 type(input_state_t), intent(inout) :: input_state
686
687 ! Simplified delete command - just delete current character
688 call delete_char_at_cursor(input_state)
689 end subroutine
690
691 subroutine move_to_next_word(input_state)
692 type(input_state_t), intent(inout) :: input_state
693 integer :: pos
694
695 pos = input_state%cursor_pos + 1
696
697 ! Skip current word
698 do while (pos <= input_state%length .and. input_state%buffer(pos:pos) /= ' ')
699 pos = pos + 1
700 end do
701
702 ! Skip spaces
703 do while (pos <= input_state%length .and. input_state%buffer(pos:pos) == ' ')
704 pos = pos + 1
705 end do
706
707 input_state%cursor_pos = min(pos - 1, input_state%length)
708 input_state%dirty = .true.
709 end subroutine
710
711 subroutine move_to_previous_word(input_state)
712 type(input_state_t), intent(inout) :: input_state
713 integer :: pos
714
715 if (input_state%cursor_pos <= 0) return
716
717 pos = input_state%cursor_pos - 1
718
719 ! Skip spaces
720 do while (pos > 0 .and. input_state%buffer(pos:pos) == ' ')
721 pos = pos - 1
722 end do
723
724 ! Find beginning of word
725 do while (pos > 0 .and. input_state%buffer(pos:pos) /= ' ')
726 pos = pos - 1
727 end do
728
729 if (input_state%buffer(pos:pos) == ' ') pos = pos + 1
730
731 input_state%cursor_pos = pos
732 input_state%dirty = .true.
733 end subroutine
734
735 subroutine delete_char_at_cursor(input_state)
736 type(input_state_t), intent(inout) :: input_state
737 integer :: i
738
739 if (input_state%cursor_pos >= input_state%length) return
740
741 ! Shift characters left
742 do i = input_state%cursor_pos + 1, input_state%length - 1
743 input_state%buffer(i:i) = input_state%buffer(i+1:i+1)
744 end do
745
746 input_state%length = input_state%length - 1
747 input_state%buffer(input_state%length+1:input_state%length+1) = ' '
748 input_state%dirty = .true.
749 end subroutine
750
751 function get_editing_mode_name(input_state) result(mode_name)
752 type(input_state_t), intent(in) :: input_state
753 character(len=16) :: mode_name
754
755 select case (input_state%editing_mode)
756 case (EDITING_MODE_EMACS)
757 mode_name = 'emacs'
758 case (EDITING_MODE_VI)
759 if (input_state%vi_mode == VI_MODE_INSERT) then
760 mode_name = 'vi-insert'
761 else
762 mode_name = 'vi-command'
763 end if
764 case default
765 mode_name = 'unknown'
766 end select
767 end function
768
769 ! Basic tab completion - simplified implementation
770 subroutine tab_complete(partial_input, completions, num_completions)
771 character(len=*), intent(in) :: partial_input
772 character(len=MAX_LINE_LEN), intent(out) :: completions(50) ! Max 50 completions
773 integer, intent(out) :: num_completions
774
775 character(len=MAX_LINE_LEN) :: last_word, dir_path, file_pattern
776 integer :: last_space_pos, i
777
778 num_completions = 0
779
780 ! Find the last word to complete
781 last_space_pos = 0
782 do i = len_trim(partial_input), 1, -1
783 if (partial_input(i:i) == ' ') then
784 last_space_pos = i
785 exit
786 end if
787 end do
788
789 if (last_space_pos == 0) then
790 last_word = trim(partial_input)
791 else
792 last_word = trim(partial_input(last_space_pos+1:))
793 end if
794
795 ! If it's the first word, complete commands
796 if (last_space_pos == 0) then
797 call complete_commands(last_word, completions, num_completions)
798 else
799 ! Otherwise, complete files/directories
800 call complete_files(last_word, completions, num_completions)
801 end if
802 end subroutine
803
804 ! Enhanced tab completion with real filesystem integration
805 subroutine enhanced_tab_complete(partial_input, completions, num_completions)
806 character(len=*), intent(in) :: partial_input
807 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
808 integer, intent(out) :: num_completions
809
810 character(len=MAX_LINE_LEN) :: last_word, prefix_part
811 integer :: last_space_pos, i
812 logical :: is_command
813
814 num_completions = 0
815
816 ! Find the last word to complete
817 last_space_pos = 0
818 do i = len_trim(partial_input), 1, -1
819 if (partial_input(i:i) == ' ') then
820 last_space_pos = i
821 exit
822 end if
823 end do
824
825 if (last_space_pos == 0) then
826 last_word = trim(partial_input)
827 prefix_part = ''
828 is_command = .true.
829 else
830 last_word = trim(partial_input(last_space_pos+1:))
831 prefix_part = partial_input(:last_space_pos)
832 is_command = .false.
833 end if
834
835 if (is_command) then
836 ! Complete commands (builtins + PATH executables)
837 call complete_commands_enhanced(last_word, completions, num_completions)
838
839 ! Add prefix back to completions
840 do i = 1, num_completions
841 completions(i) = trim(completions(i))
842 end do
843 else
844 ! Complete files and directories
845 call complete_files_enhanced(last_word, completions, num_completions)
846
847 ! Don't add prefix to completions - they are for display only
848 ! The prefix will be added when constructing the completed line
849 end if
850 end subroutine
851
852 subroutine complete_commands(prefix, completions, num_completions)
853 character(len=*), intent(in) :: prefix
854 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
855 integer, intent(out) :: num_completions
856
857 character(len=50), parameter :: builtin_commands(19) = [ &
858 'cd ', 'echo ', 'exit ', 'export ', &
859 'pwd ', 'jobs ', 'fg ', 'bg ', &
860 'history ', 'source ', 'test ', 'if ', &
861 'kill ', 'wait ', 'trap ', 'config ', &
862 'alias ', 'unalias ', 'help ' &
863 ]
864 integer :: i, prefix_len
865
866 num_completions = 0
867 prefix_len = len_trim(prefix)
868
869 ! Complete builtin commands
870 do i = 1, size(builtin_commands)
871 if (prefix_len == 0 .or. &
872 index(trim(builtin_commands(i)), prefix(1:prefix_len)) == 1) then
873 num_completions = num_completions + 1
874 if (num_completions <= 50) then
875 completions(num_completions) = trim(builtin_commands(i))
876 end if
877 end if
878 end do
879
880 ! TODO: Add external command completion from PATH
881 end subroutine
882
883 subroutine complete_files(prefix, completions, num_completions)
884 character(len=*), intent(in) :: prefix
885 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
886 integer, intent(out) :: num_completions
887
888 character(len=MAX_LINE_LEN) :: dir_path, file_pattern, current_dir
889 integer :: last_slash_pos, i
890
891 num_completions = 0
892
893 ! Extract directory path and filename pattern
894 last_slash_pos = 0
895 do i = len_trim(prefix), 1, -1
896 if (prefix(i:i) == '/') then
897 last_slash_pos = i
898 exit
899 end if
900 end do
901
902 if (last_slash_pos > 0) then
903 dir_path = prefix(:last_slash_pos-1)
904 file_pattern = prefix(last_slash_pos+1:)
905 if (len_trim(dir_path) == 0) dir_path = '/'
906 else
907 dir_path = '.'
908 file_pattern = trim(prefix)
909 end if
910
911 ! Add common directory completions
912 if (len_trim(file_pattern) == 0 .or. file_pattern(1:1) == '.') then
913 if (num_completions < 50) then
914 num_completions = num_completions + 1
915 if (trim(dir_path) == '.') then
916 completions(num_completions) = './'
917 else
918 completions(num_completions) = trim(dir_path) // '/./'
919 end if
920 end if
921
922 if (len_trim(file_pattern) == 0 .or. index(file_pattern, '..') == 1) then
923 if (num_completions < 50) then
924 num_completions = num_completions + 1
925 if (trim(dir_path) == '.') then
926 completions(num_completions) = '../'
927 else
928 completions(num_completions) = trim(dir_path) // '/../'
929 end if
930 end if
931 end if
932 end if
933
934 ! Add some common file extensions for demonstration
935 if (len_trim(file_pattern) == 0) then
936 if (num_completions < 47) then
937 completions(num_completions + 1) = 'Makefile'
938 completions(num_completions + 2) = 'README'
939 completions(num_completions + 3) = 'LICENSE'
940 num_completions = num_completions + 3
941 end if
942 end if
943 end subroutine
944
945 ! Enhanced command completion with PATH executable scanning
946 subroutine complete_commands_enhanced(prefix, completions, num_completions)
947 character(len=*), intent(in) :: prefix
948 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
949 integer, intent(out) :: num_completions
950
951 character(len=50), parameter :: builtin_commands(20) = [ &
952 'cd ', 'echo ', 'exit ', 'export ', &
953 'pwd ', 'jobs ', 'fg ', 'bg ', &
954 'history ', 'source ', 'test ', 'if ', &
955 'kill ', 'wait ', 'trap ', 'config ', &
956 'alias ', 'unalias ', 'help ', 'rawtest ' &
957 ]
958 integer :: i, prefix_len
959
960 num_completions = 0
961 prefix_len = len_trim(prefix)
962
963 ! Complete builtin commands
964 do i = 1, size(builtin_commands)
965 if (prefix_len == 0 .or. &
966 index(trim(builtin_commands(i)), prefix(1:prefix_len)) == 1) then
967 num_completions = num_completions + 1
968 if (num_completions <= 50) then
969 completions(num_completions) = trim(builtin_commands(i))
970 end if
971 end if
972 end do
973
974 ! Add common system commands
975 call add_system_commands(prefix, completions, num_completions)
976 end subroutine
977
978 subroutine add_system_commands(prefix, completions, num_completions)
979 character(len=*), intent(in) :: prefix
980 character(len=MAX_LINE_LEN), intent(inout) :: completions(50)
981 integer, intent(inout) :: num_completions
982
983 character(len=50), parameter :: common_commands(15) = [ &
984 'ls ', 'cat ', 'grep ', 'find ', &
985 'sort ', 'head ', 'tail ', 'wc ', &
986 'cp ', 'mv ', 'rm ', 'mkdir ', &
987 'rmdir ', 'chmod ', 'which ' &
988 ]
989 integer :: i, prefix_len
990
991 prefix_len = len_trim(prefix)
992
993 do i = 1, size(common_commands)
994 if (num_completions >= 50) exit
995 if (prefix_len == 0 .or. &
996 index(trim(common_commands(i)), prefix(1:prefix_len)) == 1) then
997 num_completions = num_completions + 1
998 completions(num_completions) = trim(common_commands(i))
999 end if
1000 end do
1001 end subroutine
1002
1003 ! Enhanced file completion with real filesystem access
1004 subroutine complete_files_enhanced(prefix, completions, num_completions)
1005 character(len=*), intent(in) :: prefix
1006 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
1007 integer, intent(out) :: num_completions
1008
1009 character(len=MAX_LINE_LEN) :: dir_path, file_pattern
1010 integer :: last_slash_pos, i
1011
1012 num_completions = 0
1013
1014 ! Extract directory path and filename pattern
1015 last_slash_pos = 0
1016 do i = len_trim(prefix), 1, -1
1017 if (prefix(i:i) == '/') then
1018 last_slash_pos = i
1019 exit
1020 end if
1021 end do
1022
1023 if (last_slash_pos > 0) then
1024 dir_path = prefix(:last_slash_pos-1)
1025 file_pattern = prefix(last_slash_pos+1:)
1026 if (len_trim(dir_path) == 0) dir_path = '/'
1027 else
1028 dir_path = '.'
1029 file_pattern = trim(prefix)
1030 end if
1031
1032 ! Add directory navigation options
1033 if (len_trim(file_pattern) == 0 .or. file_pattern(1:1) == '.') then
1034 ! Current directory
1035 if (num_completions < 50) then
1036 num_completions = num_completions + 1
1037 if (trim(dir_path) == '.') then
1038 completions(num_completions) = './'
1039 else
1040 completions(num_completions) = trim(dir_path) // '/./'
1041 end if
1042 end if
1043
1044 ! Parent directory
1045 if (len_trim(file_pattern) == 0 .or. index(file_pattern, '..') == 1) then
1046 if (num_completions < 50) then
1047 num_completions = num_completions + 1
1048 if (trim(dir_path) == '.') then
1049 completions(num_completions) = '../'
1050 else
1051 completions(num_completions) = trim(dir_path) // '/../'
1052 end if
1053 end if
1054 end if
1055 end if
1056
1057 ! Get actual filesystem entries
1058 call scan_directory(dir_path, file_pattern, completions, num_completions)
1059 end subroutine
1060
1061 ! Scan directory for matching files and directories
1062 subroutine scan_directory(dir_path, pattern, completions, num_completions)
1063 character(len=*), intent(in) :: dir_path, pattern
1064 character(len=MAX_LINE_LEN), intent(inout) :: completions(50)
1065 integer, intent(inout) :: num_completions
1066
1067 character(len=MAX_LINE_LEN) :: ls_command, ls_output
1068 character(len=MAX_LINE_LEN) :: entries(100) ! Temp storage for directory entries
1069 integer :: num_entries, i, pattern_len
1070
1071 pattern_len = len_trim(pattern)
1072
1073 ! Use ls command to get directory listing
1074 ls_command = 'ls -1a "' // trim(dir_path) // '" 2>/dev/null'
1075 ls_output = execute_and_capture(ls_command)
1076
1077 ! Parse ls output into individual entries
1078 call parse_ls_output(ls_output, entries, num_entries)
1079
1080 ! Filter entries by pattern and add to completions
1081 do i = 1, num_entries
1082 if (num_completions >= 50) exit
1083
1084 ! Skip . and .. unless explicitly requested
1085 if (trim(entries(i)) == '.' .or. trim(entries(i)) == '..') then
1086 if (pattern_len == 0 .or. (pattern_len > 0 .and. pattern(1:1) /= '.')) then
1087 cycle
1088 end if
1089 end if
1090
1091 ! Check if entry matches pattern
1092 if (pattern_len == 0 .or. index(entries(i), pattern(1:pattern_len)) == 1) then
1093 num_completions = num_completions + 1
1094 if (trim(dir_path) == '.') then
1095 completions(num_completions) = trim(entries(i))
1096 else
1097 completions(num_completions) = trim(dir_path) // '/' // trim(entries(i))
1098 end if
1099 end if
1100 end do
1101 end subroutine
1102
1103 ! Parse ls output into individual entries
1104 subroutine parse_ls_output(output, entries, num_entries)
1105 character(len=*), intent(in) :: output
1106 character(len=MAX_LINE_LEN), intent(out) :: entries(100)
1107 integer, intent(out) :: num_entries
1108
1109 integer :: pos, start, output_len
1110
1111 num_entries = 0
1112 pos = 1
1113 output_len = len_trim(output)
1114
1115 do while (pos <= output_len .and. num_entries < 100)
1116 ! Skip whitespace
1117 do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
1118 pos = pos + 1
1119 end do
1120
1121 if (pos > output_len) exit
1122
1123 start = pos
1124
1125 ! Find end of entry (newline or space)
1126 do while (pos <= output_len .and. output(pos:pos) /= char(10) .and. output(pos:pos) /= ' ')
1127 pos = pos + 1
1128 end do
1129
1130 if (pos > start) then
1131 num_entries = num_entries + 1
1132 entries(num_entries) = output(start:pos-1)
1133 end if
1134
1135 pos = pos + 1
1136 end do
1137 end subroutine
1138
1139 subroutine show_completions(completions, num_completions)
1140 character(len=MAX_LINE_LEN), intent(in) :: completions(50)
1141 integer, intent(in) :: num_completions
1142 integer :: i
1143
1144 if (num_completions > 1) then
1145 write(output_unit, '(a)') ''
1146 do i = 1, num_completions
1147 write(output_unit, '(a)', advance='no') trim(completions(i)) // ' '
1148 if (mod(i, 8) == 0) write(output_unit, '(a)') '' ! New line every 8 items
1149 end do
1150 write(output_unit, '(a)') ''
1151 end if
1152 end subroutine
1153
1154 ! Find common prefix among completions
1155 function get_common_prefix(completions, num_completions) result(prefix)
1156 character(len=MAX_LINE_LEN), intent(in) :: completions(50)
1157 integer, intent(in) :: num_completions
1158 character(len=MAX_LINE_LEN) :: prefix
1159
1160 integer :: i, j, min_len, common_len
1161 logical :: matches
1162
1163 prefix = ''
1164 if (num_completions == 0) return
1165
1166 if (num_completions == 1) then
1167 prefix = trim(completions(1))
1168 return
1169 end if
1170
1171 ! Find minimum length
1172 min_len = len_trim(completions(1))
1173 do i = 2, num_completions
1174 min_len = min(min_len, len_trim(completions(i)))
1175 end do
1176
1177 ! Find common prefix length
1178 common_len = 0
1179 do j = 1, min_len
1180 matches = .true.
1181 do i = 2, num_completions
1182 if (completions(1)(j:j) /= completions(i)(j:j)) then
1183 matches = .false.
1184 exit
1185 end if
1186 end do
1187
1188 if (matches) then
1189 common_len = j
1190 else
1191 exit
1192 end if
1193 end do
1194
1195 if (common_len > 0) then
1196 prefix = completions(1)(:common_len)
1197 end if
1198 end function
1199
1200 ! Enhanced tab completion that handles partial completion
1201 subroutine smart_tab_complete(partial_input, completions, num_completions, completed_line, completed)
1202 character(len=*), intent(in) :: partial_input
1203 character(len=MAX_LINE_LEN), intent(out) :: completions(50)
1204 integer, intent(out) :: num_completions
1205 character(len=*), intent(out) :: completed_line
1206 logical, intent(out) :: completed
1207
1208 character(len=MAX_LINE_LEN) :: common_prefix, prefix_part
1209 integer :: last_space_pos, i
1210
1211 completed = .false.
1212 completed_line = partial_input
1213
1214 ! Find the prefix (command and any earlier arguments)
1215 last_space_pos = 0
1216 do i = len_trim(partial_input), 1, -1
1217 if (partial_input(i:i) == ' ') then
1218 last_space_pos = i
1219 exit
1220 end if
1221 end do
1222
1223 if (last_space_pos > 0) then
1224 prefix_part = partial_input(:last_space_pos)
1225 else
1226 prefix_part = ''
1227 end if
1228
1229 call enhanced_tab_complete(partial_input, completions, num_completions)
1230
1231 if (num_completions == 0) then
1232 ! No completions found
1233 return
1234 else if (num_completions == 1) then
1235 ! Single completion - add prefix back (preserve spacing)
1236 if (last_space_pos > 0) then
1237 completed_line = prefix_part(:last_space_pos) // trim(completions(1))
1238 else
1239 completed_line = trim(completions(1))
1240 end if
1241 completed = .true.
1242 else
1243 ! Multiple completions - try common prefix
1244 common_prefix = get_common_prefix(completions, num_completions)
1245
1246 if (len_trim(common_prefix) > 0) then
1247 if (last_space_pos > 0) then
1248 completed_line = prefix_part(:last_space_pos) // trim(common_prefix)
1249 else
1250 completed_line = trim(common_prefix)
1251 end if
1252 completed = .true.
1253 end if
1254 end if
1255 end subroutine
1256
1257 ! Helper functions for enhanced readline
1258 subroutine insert_char(input_state, ch)
1259 type(input_state_t), intent(inout) :: input_state
1260 character, intent(in) :: ch
1261 integer :: i
1262
1263 ! Check if we have room
1264 if (input_state%length >= MAX_LINE_LEN) return
1265
1266 ! If we're browsing history, exit history mode when typing
1267 if (input_state%in_history) then
1268 input_state%in_history = .false.
1269 input_state%history_pos = 0
1270 end if
1271
1272 ! If cursor is at end, simple append
1273 if (input_state%cursor_pos >= input_state%length) then
1274 input_state%length = input_state%length + 1
1275 input_state%buffer(input_state%length:input_state%length) = ch
1276 input_state%cursor_pos = input_state%length
1277 write(output_unit, '(a)', advance='no') ch
1278 flush(output_unit)
1279 else
1280 ! Insert in middle - shift characters right
1281 do i = input_state%length, input_state%cursor_pos + 1, -1
1282 input_state%buffer(i+1:i+1) = input_state%buffer(i:i)
1283 end do
1284 input_state%cursor_pos = input_state%cursor_pos + 1
1285 input_state%buffer(input_state%cursor_pos:input_state%cursor_pos) = ch
1286 input_state%length = input_state%length + 1
1287 input_state%dirty = .true.
1288 end if
1289 end subroutine
1290
1291 subroutine handle_backspace(input_state)
1292 type(input_state_t), intent(inout) :: input_state
1293 integer :: i
1294
1295 if (input_state%cursor_pos <= 0) return
1296
1297 ! If we're browsing history, exit history mode when editing
1298 if (input_state%in_history) then
1299 input_state%in_history = .false.
1300 input_state%history_pos = 0
1301 end if
1302
1303 ! If cursor is at end, simple deletion
1304 if (input_state%cursor_pos >= input_state%length) then
1305 input_state%length = input_state%length - 1
1306 input_state%cursor_pos = input_state%cursor_pos - 1
1307 input_state%buffer(input_state%length+1:input_state%length+1) = ' '
1308 write(output_unit, '(a)', advance='no') char(8) // ' ' // char(8) ! Backspace, space, backspace
1309 flush(output_unit)
1310 else
1311 ! Delete in middle - shift characters left
1312 do i = input_state%cursor_pos, input_state%length - 1
1313 input_state%buffer(i:i) = input_state%buffer(i+1:i+1)
1314 end do
1315 input_state%cursor_pos = input_state%cursor_pos - 1
1316 input_state%length = input_state%length - 1
1317 input_state%buffer(input_state%length+1:input_state%length+1) = ' '
1318 input_state%dirty = .true.
1319 end if
1320 end subroutine
1321
1322 subroutine handle_tab_completion(input_state)
1323 type(input_state_t), intent(inout) :: input_state
1324 character(len=MAX_LINE_LEN) :: partial_input
1325 character(len=MAX_LINE_LEN) :: completions(50)
1326 character(len=MAX_LINE_LEN) :: completed_line
1327 integer :: num_completions
1328 logical :: completed
1329
1330 ! Exit history mode if we're browsing
1331 if (input_state%in_history) then
1332 input_state%in_history = .false.
1333 input_state%history_pos = 0
1334 end if
1335
1336 ! Get the current buffer content
1337 partial_input = input_state%buffer(:input_state%length)
1338
1339 ! Attempt smart completion
1340 call smart_tab_complete(partial_input, completions, num_completions, completed_line, completed)
1341
1342 if (completed) then
1343 ! Update the input buffer with completion
1344 input_state%buffer = completed_line
1345 input_state%length = len_trim(completed_line)
1346 input_state%cursor_pos = input_state%length
1347 input_state%dirty = .true.
1348
1349 if (num_completions > 1) then
1350 ! Show available options
1351 write(output_unit, '()') ! New line
1352 call show_completions(completions, num_completions)
1353 input_state%dirty = .true.
1354 end if
1355 else
1356 ! No completions found, just add spaces (old behavior)
1357 call insert_char(input_state, ' ')
1358 call insert_char(input_state, ' ')
1359 end if
1360 end subroutine
1361
1362 subroutine handle_escape_sequence(input_state, done)
1363 type(input_state_t), intent(inout) :: input_state
1364 logical, intent(inout) :: done
1365 character :: ch1, ch2
1366 logical :: success
1367
1368 ! Try to read the next character
1369 success = read_single_char(ch1)
1370 if (.not. success) return
1371
1372 if (ch1 == '[') then
1373 ! ANSI escape sequence
1374 success = read_single_char(ch2)
1375 if (.not. success) return
1376
1377 select case(ch2)
1378 case('A') ! Up arrow
1379 call handle_history_up(input_state)
1380 case('B') ! Down arrow
1381 call handle_history_down(input_state)
1382 case('C') ! Right arrow
1383 call handle_cursor_right(input_state)
1384 case('D') ! Left arrow
1385 call handle_cursor_left(input_state)
1386 case default
1387 ! Unknown escape sequence
1388 continue
1389 end select
1390 end if
1391 end subroutine
1392
1393 subroutine handle_cursor_left(input_state)
1394 type(input_state_t), intent(inout) :: input_state
1395
1396 if (input_state%cursor_pos > 0) then
1397 input_state%cursor_pos = input_state%cursor_pos - 1
1398 write(output_unit, '(a)', advance='no') ESC_CURSOR_LEFT
1399 flush(output_unit)
1400 end if
1401 end subroutine
1402
1403 subroutine handle_cursor_right(input_state)
1404 type(input_state_t), intent(inout) :: input_state
1405
1406 if (input_state%cursor_pos < input_state%length) then
1407 input_state%cursor_pos = input_state%cursor_pos + 1
1408 write(output_unit, '(a)', advance='no') ESC_CURSOR_RIGHT
1409 flush(output_unit)
1410 end if
1411 end subroutine
1412
1413 subroutine handle_history_up(input_state)
1414 type(input_state_t), intent(inout) :: input_state
1415 character(len=MAX_LINE_LEN) :: history_line
1416 logical :: found
1417
1418 ! If not currently browsing history, save the current input
1419 if (.not. input_state%in_history) then
1420 input_state%original_buffer = input_state%buffer
1421 input_state%history_pos = command_history%count + 1
1422 input_state%in_history = .true.
1423 end if
1424
1425 ! Move up in history
1426 if (input_state%history_pos > 1) then
1427 input_state%history_pos = input_state%history_pos - 1
1428 call get_history_line(input_state%history_pos, history_line, found)
1429
1430 if (found) then
1431 input_state%buffer = history_line
1432 input_state%length = len_trim(history_line)
1433 input_state%cursor_pos = input_state%length
1434 input_state%dirty = .true.
1435 end if
1436 end if
1437 end subroutine
1438
1439 subroutine handle_history_down(input_state)
1440 type(input_state_t), intent(inout) :: input_state
1441 character(len=MAX_LINE_LEN) :: history_line
1442 logical :: found
1443
1444 ! Only navigate down if we're currently in history
1445 if (.not. input_state%in_history) return
1446
1447 ! Move down in history
1448 if (input_state%history_pos < command_history%count) then
1449 input_state%history_pos = input_state%history_pos + 1
1450 call get_history_line(input_state%history_pos, history_line, found)
1451
1452 if (found) then
1453 input_state%buffer = history_line
1454 input_state%length = len_trim(history_line)
1455 input_state%cursor_pos = input_state%length
1456 input_state%dirty = .true.
1457 end if
1458 else if (input_state%history_pos <= command_history%count) then
1459 ! Reached the end of history, restore original input
1460 input_state%buffer = input_state%original_buffer
1461 input_state%length = len_trim(input_state%original_buffer)
1462 input_state%cursor_pos = input_state%length
1463 input_state%history_pos = command_history%count + 1
1464 input_state%in_history = .false.
1465 input_state%dirty = .true.
1466 end if
1467 end subroutine
1468
1469 subroutine redraw_line(prompt, input_state)
1470 character(len=*), intent(in) :: prompt
1471 type(input_state_t), intent(in) :: input_state
1472 integer :: i
1473
1474 ! Move to beginning of line and clear it
1475 write(output_unit, '(a)', advance='no') ESC_MOVE_BOL // ESC_CLEAR_LINE
1476
1477 ! Redraw prompt and current buffer
1478 write(output_unit, '(a)', advance='no') prompt
1479 if (input_state%length > 0) then
1480 write(output_unit, '(a)', advance='no') input_state%buffer(:input_state%length)
1481 end if
1482
1483 ! Position cursor correctly
1484 do i = input_state%length, input_state%cursor_pos + 1, -1
1485 write(output_unit, '(a)', advance='no') ESC_CURSOR_LEFT
1486 end do
1487
1488 flush(output_unit)
1489 end subroutine
1490
1491 ! Advanced line editing functions for Phase 5
1492 subroutine handle_home(input_state)
1493 type(input_state_t), intent(inout) :: input_state
1494
1495 ! Move cursor to beginning of line
1496 if (input_state%cursor_pos > 0) then
1497 do while (input_state%cursor_pos > 0)
1498 write(output_unit, '(a)', advance='no') ESC_CURSOR_LEFT
1499 input_state%cursor_pos = input_state%cursor_pos - 1
1500 end do
1501 flush(output_unit)
1502 end if
1503 end subroutine
1504
1505 subroutine handle_end(input_state)
1506 type(input_state_t), intent(inout) :: input_state
1507
1508 ! Move cursor to end of line
1509 do while (input_state%cursor_pos < input_state%length)
1510 write(output_unit, '(a)', advance='no') ESC_CURSOR_RIGHT
1511 input_state%cursor_pos = input_state%cursor_pos + 1
1512 end do
1513 flush(output_unit)
1514 end subroutine
1515
1516 subroutine handle_kill_to_end(input_state)
1517 type(input_state_t), intent(inout) :: input_state
1518
1519 ! Save text from cursor to end of line in kill buffer
1520 if (input_state%cursor_pos < input_state%length) then
1521 input_state%kill_buffer = input_state%buffer(input_state%cursor_pos+1:input_state%length)
1522 input_state%kill_length = input_state%length - input_state%cursor_pos
1523
1524 ! Clear from cursor to end of line
1525 input_state%length = input_state%cursor_pos
1526 input_state%dirty = .true.
1527 else
1528 ! Nothing to kill
1529 input_state%kill_length = 0
1530 end if
1531 end subroutine
1532
1533 subroutine handle_kill_line(input_state)
1534 type(input_state_t), intent(inout) :: input_state
1535
1536 ! Save entire line in kill buffer
1537 if (input_state%length > 0) then
1538 input_state%kill_buffer = input_state%buffer(:input_state%length)
1539 input_state%kill_length = input_state%length
1540
1541 ! Clear the line
1542 input_state%buffer = ''
1543 input_state%length = 0
1544 input_state%cursor_pos = 0
1545 input_state%dirty = .true.
1546 else
1547 input_state%kill_length = 0
1548 end if
1549 end subroutine
1550
1551 subroutine handle_kill_word(input_state)
1552 type(input_state_t), intent(inout) :: input_state
1553 integer :: word_start, i
1554
1555 if (input_state%cursor_pos == 0) then
1556 input_state%kill_length = 0
1557 return
1558 end if
1559
1560 ! Find start of current word (skip trailing spaces first)
1561 word_start = input_state%cursor_pos
1562
1563 ! Skip any trailing whitespace
1564 do while (word_start > 0 .and. input_state%buffer(word_start:word_start) == ' ')
1565 word_start = word_start - 1
1566 end do
1567
1568 ! Find beginning of word (non-space characters)
1569 do while (word_start > 0 .and. input_state%buffer(word_start:word_start) /= ' ')
1570 word_start = word_start - 1
1571 end do
1572
1573 ! word_start is now at space before word, or 0 if at beginning
1574 if (word_start < input_state%cursor_pos) then
1575 ! Save killed text
1576 input_state%kill_buffer = input_state%buffer(word_start+1:input_state%cursor_pos)
1577 input_state%kill_length = input_state%cursor_pos - word_start
1578
1579 ! Shift remaining text left
1580 do i = word_start + 1, input_state%length - input_state%cursor_pos + word_start
1581 if (input_state%cursor_pos + i - word_start <= input_state%length) then
1582 input_state%buffer(i:i) = input_state%buffer(input_state%cursor_pos + i - word_start: &
1583 input_state%cursor_pos + i - word_start)
1584 else
1585 input_state%buffer(i:i) = ' '
1586 end if
1587 end do
1588
1589 ! Update length and cursor position
1590 input_state%length = input_state%length - (input_state%cursor_pos - word_start)
1591 input_state%cursor_pos = word_start
1592 input_state%dirty = .true.
1593 else
1594 input_state%kill_length = 0
1595 end if
1596 end subroutine
1597
1598 subroutine handle_yank(input_state)
1599 type(input_state_t), intent(inout) :: input_state
1600 integer :: i, insert_len
1601
1602 if (input_state%kill_length == 0) return
1603
1604 insert_len = min(input_state%kill_length, MAX_LINE_LEN - input_state%length)
1605 if (insert_len == 0) return
1606
1607 ! Shift existing text right to make room
1608 do i = input_state%length, input_state%cursor_pos + 1, -1
1609 if (i + insert_len <= MAX_LINE_LEN) then
1610 input_state%buffer(i + insert_len:i + insert_len) = input_state%buffer(i:i)
1611 end if
1612 end do
1613
1614 ! Insert killed text at cursor position
1615 do i = 1, insert_len
1616 input_state%buffer(input_state%cursor_pos + i:input_state%cursor_pos + i) = &
1617 input_state%kill_buffer(i:i)
1618 end do
1619
1620 ! Update length and cursor position
1621 input_state%length = input_state%length + insert_len
1622 input_state%cursor_pos = input_state%cursor_pos + insert_len
1623 input_state%dirty = .true.
1624 end subroutine
1625
1626 subroutine handle_clear_screen(input_state)
1627 type(input_state_t), intent(inout) :: input_state
1628
1629 ! Clear screen with ANSI escape sequence
1630 write(output_unit, '(a)', advance='no') char(27) // '[2J' // char(27) // '[H'
1631 flush(output_unit)
1632
1633 ! Force redraw of current line
1634 input_state%dirty = .true.
1635 end subroutine
1636
1637 end module readline