@@ -16,10 +16,15 @@ module readline |
| 16 | integer, parameter :: KEY_TAB = 9 | 16 | integer, parameter :: KEY_TAB = 9 |
| 17 | integer, parameter :: KEY_CTRL_C = 3 | 17 | integer, parameter :: KEY_CTRL_C = 3 |
| 18 | integer, parameter :: KEY_CTRL_D = 4 | 18 | integer, parameter :: KEY_CTRL_D = 4 |
| 19 | - integer, parameter :: KEY_CTRL_A = 1 ! Home | 19 | + integer, parameter :: KEY_CTRL_A = 1 ! Home (beginning of line) |
| 20 | - integer, parameter :: KEY_CTRL_E = 5 ! End | 20 | + integer, parameter :: KEY_CTRL_E = 5 ! End (end of line) |
| 21 | integer, parameter :: KEY_CTRL_K = 11 ! Kill to end of line | 21 | integer, parameter :: KEY_CTRL_K = 11 ! Kill to end of line |
| 22 | integer, parameter :: KEY_CTRL_L = 12 ! Clear screen | 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) |
| 23 | integer, parameter :: KEY_ESC = 27 | 28 | integer, parameter :: KEY_ESC = 27 |
| 24 | integer, parameter :: KEY_UP = 65 | 29 | integer, parameter :: KEY_UP = 65 |
| 25 | integer, parameter :: KEY_DOWN = 66 | 30 | integer, parameter :: KEY_DOWN = 66 |
@@ -34,9 +39,11 @@ module readline |
| 34 | type :: input_state_t | 39 | type :: input_state_t |
| 35 | character(len=MAX_LINE_LEN) :: buffer = '' | 40 | character(len=MAX_LINE_LEN) :: buffer = '' |
| 36 | character(len=MAX_LINE_LEN) :: original_buffer = '' ! Save original input during history navigation | 41 | character(len=MAX_LINE_LEN) :: original_buffer = '' ! Save original input during history navigation |
| | 42 | + character(len=MAX_LINE_LEN) :: kill_buffer = '' ! Kill ring buffer for cut/paste |
| 37 | integer :: length = 0 | 43 | integer :: length = 0 |
| 38 | integer :: cursor_pos = 0 ! 0-based position in buffer | 44 | integer :: cursor_pos = 0 ! 0-based position in buffer |
| 39 | integer :: history_pos = 0 ! Current position in history (0 = not browsing) | 45 | integer :: history_pos = 0 ! Current position in history (0 = not browsing) |
| | 46 | + integer :: kill_length = 0 ! Length of text in kill buffer |
| 40 | logical :: dirty = .false. ! Needs redraw | 47 | logical :: dirty = .false. ! Needs redraw |
| 41 | logical :: in_history = .false. ! Currently browsing history | 48 | logical :: in_history = .false. ! Currently browsing history |
| 42 | end type input_state_t | 49 | end type input_state_t |
@@ -80,9 +87,11 @@ contains |
| 80 | ! Initialize input state | 87 | ! Initialize input state |
| 81 | input_state%buffer = '' | 88 | input_state%buffer = '' |
| 82 | input_state%original_buffer = '' | 89 | input_state%original_buffer = '' |
| | 90 | + input_state%kill_buffer = '' |
| 83 | input_state%length = 0 | 91 | input_state%length = 0 |
| 84 | input_state%cursor_pos = 0 | 92 | input_state%cursor_pos = 0 |
| 85 | input_state%history_pos = 0 | 93 | input_state%history_pos = 0 |
| | 94 | + input_state%kill_length = 0 |
| 86 | input_state%dirty = .false. | 95 | input_state%dirty = .false. |
| 87 | input_state%in_history = .false. | 96 | input_state%in_history = .false. |
| 88 | | 97 | |
@@ -129,6 +138,42 @@ contains |
| 129 | ! Escape sequence - try to read more | 138 | ! Escape sequence - try to read more |
| 130 | call handle_escape_sequence(input_state, done) | 139 | call handle_escape_sequence(input_state, done) |
| 131 | | 140 | |
| | 141 | + case(KEY_CTRL_A) |
| | 142 | + ! Home - move to beginning of line |
| | 143 | + call handle_home(input_state) |
| | 144 | + |
| | 145 | + case(KEY_CTRL_E) |
| | 146 | + ! End - move to end of line |
| | 147 | + call handle_end(input_state) |
| | 148 | + |
| | 149 | + case(KEY_CTRL_F) |
| | 150 | + ! Forward character (same as right arrow) |
| | 151 | + call handle_cursor_right(input_state) |
| | 152 | + |
| | 153 | + case(KEY_CTRL_B) |
| | 154 | + ! Backward character (same as left arrow) |
| | 155 | + call handle_cursor_left(input_state) |
| | 156 | + |
| | 157 | + case(KEY_CTRL_K) |
| | 158 | + ! Kill to end of line |
| | 159 | + call handle_kill_to_end(input_state) |
| | 160 | + |
| | 161 | + case(KEY_CTRL_U) |
| | 162 | + ! Kill entire line |
| | 163 | + call handle_kill_line(input_state) |
| | 164 | + |
| | 165 | + case(KEY_CTRL_W) |
| | 166 | + ! Kill previous word |
| | 167 | + call handle_kill_word(input_state) |
| | 168 | + |
| | 169 | + case(KEY_CTRL_Y) |
| | 170 | + ! Yank (paste) killed text |
| | 171 | + call handle_yank(input_state) |
| | 172 | + |
| | 173 | + case(KEY_CTRL_L) |
| | 174 | + ! Clear screen and redraw |
| | 175 | + call handle_clear_screen(input_state) |
| | 176 | + |
| 132 | case(32:126) | 177 | case(32:126) |
| 133 | ! Regular printable characters | 178 | ! Regular printable characters |
| 134 | call insert_char(input_state, ch) | 179 | call insert_char(input_state, ch) |
@@ -1017,4 +1062,150 @@ contains |
| 1017 | flush(output_unit) | 1062 | flush(output_unit) |
| 1018 | end subroutine | 1063 | end subroutine |
| 1019 | | 1064 | |
| | 1065 | + ! Advanced line editing functions for Phase 5 |
| | 1066 | + subroutine handle_home(input_state) |
| | 1067 | + type(input_state_t), intent(inout) :: input_state |
| | 1068 | + |
| | 1069 | + ! Move cursor to beginning of line |
| | 1070 | + if (input_state%cursor_pos > 0) then |
| | 1071 | + do while (input_state%cursor_pos > 0) |
| | 1072 | + write(output_unit, '(a)', advance='no') ESC_CURSOR_LEFT |
| | 1073 | + input_state%cursor_pos = input_state%cursor_pos - 1 |
| | 1074 | + end do |
| | 1075 | + flush(output_unit) |
| | 1076 | + end if |
| | 1077 | + end subroutine |
| | 1078 | + |
| | 1079 | + subroutine handle_end(input_state) |
| | 1080 | + type(input_state_t), intent(inout) :: input_state |
| | 1081 | + |
| | 1082 | + ! Move cursor to end of line |
| | 1083 | + do while (input_state%cursor_pos < input_state%length) |
| | 1084 | + write(output_unit, '(a)', advance='no') ESC_CURSOR_RIGHT |
| | 1085 | + input_state%cursor_pos = input_state%cursor_pos + 1 |
| | 1086 | + end do |
| | 1087 | + flush(output_unit) |
| | 1088 | + end subroutine |
| | 1089 | + |
| | 1090 | + subroutine handle_kill_to_end(input_state) |
| | 1091 | + type(input_state_t), intent(inout) :: input_state |
| | 1092 | + |
| | 1093 | + ! Save text from cursor to end of line in kill buffer |
| | 1094 | + if (input_state%cursor_pos < input_state%length) then |
| | 1095 | + input_state%kill_buffer = input_state%buffer(input_state%cursor_pos+1:input_state%length) |
| | 1096 | + input_state%kill_length = input_state%length - input_state%cursor_pos |
| | 1097 | + |
| | 1098 | + ! Clear from cursor to end of line |
| | 1099 | + input_state%length = input_state%cursor_pos |
| | 1100 | + input_state%dirty = .true. |
| | 1101 | + else |
| | 1102 | + ! Nothing to kill |
| | 1103 | + input_state%kill_length = 0 |
| | 1104 | + end if |
| | 1105 | + end subroutine |
| | 1106 | + |
| | 1107 | + subroutine handle_kill_line(input_state) |
| | 1108 | + type(input_state_t), intent(inout) :: input_state |
| | 1109 | + |
| | 1110 | + ! Save entire line in kill buffer |
| | 1111 | + if (input_state%length > 0) then |
| | 1112 | + input_state%kill_buffer = input_state%buffer(:input_state%length) |
| | 1113 | + input_state%kill_length = input_state%length |
| | 1114 | + |
| | 1115 | + ! Clear the line |
| | 1116 | + input_state%buffer = '' |
| | 1117 | + input_state%length = 0 |
| | 1118 | + input_state%cursor_pos = 0 |
| | 1119 | + input_state%dirty = .true. |
| | 1120 | + else |
| | 1121 | + input_state%kill_length = 0 |
| | 1122 | + end if |
| | 1123 | + end subroutine |
| | 1124 | + |
| | 1125 | + subroutine handle_kill_word(input_state) |
| | 1126 | + type(input_state_t), intent(inout) :: input_state |
| | 1127 | + integer :: word_start, i |
| | 1128 | + |
| | 1129 | + if (input_state%cursor_pos == 0) then |
| | 1130 | + input_state%kill_length = 0 |
| | 1131 | + return |
| | 1132 | + end if |
| | 1133 | + |
| | 1134 | + ! Find start of current word (skip trailing spaces first) |
| | 1135 | + word_start = input_state%cursor_pos |
| | 1136 | + |
| | 1137 | + ! Skip any trailing whitespace |
| | 1138 | + do while (word_start > 0 .and. input_state%buffer(word_start:word_start) == ' ') |
| | 1139 | + word_start = word_start - 1 |
| | 1140 | + end do |
| | 1141 | + |
| | 1142 | + ! Find beginning of word (non-space characters) |
| | 1143 | + do while (word_start > 0 .and. input_state%buffer(word_start:word_start) /= ' ') |
| | 1144 | + word_start = word_start - 1 |
| | 1145 | + end do |
| | 1146 | + |
| | 1147 | + ! word_start is now at space before word, or 0 if at beginning |
| | 1148 | + if (word_start < input_state%cursor_pos) then |
| | 1149 | + ! Save killed text |
| | 1150 | + input_state%kill_buffer = input_state%buffer(word_start+1:input_state%cursor_pos) |
| | 1151 | + input_state%kill_length = input_state%cursor_pos - word_start |
| | 1152 | + |
| | 1153 | + ! Shift remaining text left |
| | 1154 | + do i = word_start + 1, input_state%length - input_state%cursor_pos + word_start |
| | 1155 | + if (input_state%cursor_pos + i - word_start <= input_state%length) then |
| | 1156 | + input_state%buffer(i:i) = input_state%buffer(input_state%cursor_pos + i - word_start: & |
| | 1157 | + input_state%cursor_pos + i - word_start) |
| | 1158 | + else |
| | 1159 | + input_state%buffer(i:i) = ' ' |
| | 1160 | + end if |
| | 1161 | + end do |
| | 1162 | + |
| | 1163 | + ! Update length and cursor position |
| | 1164 | + input_state%length = input_state%length - (input_state%cursor_pos - word_start) |
| | 1165 | + input_state%cursor_pos = word_start |
| | 1166 | + input_state%dirty = .true. |
| | 1167 | + else |
| | 1168 | + input_state%kill_length = 0 |
| | 1169 | + end if |
| | 1170 | + end subroutine |
| | 1171 | + |
| | 1172 | + subroutine handle_yank(input_state) |
| | 1173 | + type(input_state_t), intent(inout) :: input_state |
| | 1174 | + integer :: i, insert_len |
| | 1175 | + |
| | 1176 | + if (input_state%kill_length == 0) return |
| | 1177 | + |
| | 1178 | + insert_len = min(input_state%kill_length, MAX_LINE_LEN - input_state%length) |
| | 1179 | + if (insert_len == 0) return |
| | 1180 | + |
| | 1181 | + ! Shift existing text right to make room |
| | 1182 | + do i = input_state%length, input_state%cursor_pos + 1, -1 |
| | 1183 | + if (i + insert_len <= MAX_LINE_LEN) then |
| | 1184 | + input_state%buffer(i + insert_len:i + insert_len) = input_state%buffer(i:i) |
| | 1185 | + end if |
| | 1186 | + end do |
| | 1187 | + |
| | 1188 | + ! Insert killed text at cursor position |
| | 1189 | + do i = 1, insert_len |
| | 1190 | + input_state%buffer(input_state%cursor_pos + i:input_state%cursor_pos + i) = & |
| | 1191 | + input_state%kill_buffer(i:i) |
| | 1192 | + end do |
| | 1193 | + |
| | 1194 | + ! Update length and cursor position |
| | 1195 | + input_state%length = input_state%length + insert_len |
| | 1196 | + input_state%cursor_pos = input_state%cursor_pos + insert_len |
| | 1197 | + input_state%dirty = .true. |
| | 1198 | + end subroutine |
| | 1199 | + |
| | 1200 | + subroutine handle_clear_screen(input_state) |
| | 1201 | + type(input_state_t), intent(inout) :: input_state |
| | 1202 | + |
| | 1203 | + ! Clear screen with ANSI escape sequence |
| | 1204 | + write(output_unit, '(a)', advance='no') char(27) // '[2J' // char(27) // '[H' |
| | 1205 | + flush(output_unit) |
| | 1206 | + |
| | 1207 | + ! Force redraw of current line |
| | 1208 | + input_state%dirty = .true. |
| | 1209 | + end subroutine |
| | 1210 | + |
| 1020 | end module readline | 1211 | end module readline |