@@ -16,10 +16,15 @@ module readline |
| 16 | 16 | integer, parameter :: KEY_TAB = 9 |
| 17 | 17 | integer, parameter :: KEY_CTRL_C = 3 |
| 18 | 18 | integer, parameter :: KEY_CTRL_D = 4 |
| 19 | | - integer, parameter :: KEY_CTRL_A = 1 ! Home |
| 20 | | - integer, parameter :: KEY_CTRL_E = 5 ! End |
| 19 | + integer, parameter :: KEY_CTRL_A = 1 ! Home (beginning of line) |
| 20 | + integer, parameter :: KEY_CTRL_E = 5 ! End (end of line) |
| 21 | 21 | integer, parameter :: KEY_CTRL_K = 11 ! Kill to end of line |
| 22 | 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 | 28 | integer, parameter :: KEY_ESC = 27 |
| 24 | 29 | integer, parameter :: KEY_UP = 65 |
| 25 | 30 | integer, parameter :: KEY_DOWN = 66 |
@@ -34,9 +39,11 @@ module readline |
| 34 | 39 | type :: input_state_t |
| 35 | 40 | character(len=MAX_LINE_LEN) :: buffer = '' |
| 36 | 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 | 43 | integer :: length = 0 |
| 38 | 44 | integer :: cursor_pos = 0 ! 0-based position in buffer |
| 39 | 45 | integer :: history_pos = 0 ! Current position in history (0 = not browsing) |
| 46 | + integer :: kill_length = 0 ! Length of text in kill buffer |
| 40 | 47 | logical :: dirty = .false. ! Needs redraw |
| 41 | 48 | logical :: in_history = .false. ! Currently browsing history |
| 42 | 49 | end type input_state_t |
@@ -80,9 +87,11 @@ contains |
| 80 | 87 | ! Initialize input state |
| 81 | 88 | input_state%buffer = '' |
| 82 | 89 | input_state%original_buffer = '' |
| 90 | + input_state%kill_buffer = '' |
| 83 | 91 | input_state%length = 0 |
| 84 | 92 | input_state%cursor_pos = 0 |
| 85 | 93 | input_state%history_pos = 0 |
| 94 | + input_state%kill_length = 0 |
| 86 | 95 | input_state%dirty = .false. |
| 87 | 96 | input_state%in_history = .false. |
| 88 | 97 | |
@@ -129,6 +138,42 @@ contains |
| 129 | 138 | ! Escape sequence - try to read more |
| 130 | 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 | 177 | case(32:126) |
| 133 | 178 | ! Regular printable characters |
| 134 | 179 | call insert_char(input_state, ch) |
@@ -1017,4 +1062,150 @@ contains |
| 1017 | 1062 | flush(output_unit) |
| 1018 | 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 | 1211 | end module readline |