Fortran · 24937 bytes Raw Blame History
1 module terminal_mod
2 use cell_mod
3 use screen_mod
4 use cursor_mod
5 use scrollback_mod
6 use wcwidth_mod, only: codepoint_width
7 implicit none
8 private
9
10 public :: terminal_t
11 public :: terminal_init, terminal_destroy, terminal_resize, terminal_reset
12 public :: terminal_put_char, terminal_newline, terminal_carriage_return
13 public :: terminal_tab, terminal_backspace
14 public :: terminal_scroll_up, terminal_scroll_down
15 public :: terminal_erase_display, terminal_erase_line
16 public :: terminal_cursor_move, terminal_cursor_up, terminal_cursor_down
17 public :: terminal_cursor_forward, terminal_cursor_backward
18 public :: terminal_save_cursor, terminal_restore_cursor
19 public :: terminal_set_scroll_region
20 public :: terminal_insert_lines, terminal_delete_lines
21 public :: terminal_insert_chars, terminal_delete_chars
22 public :: terminal_index, terminal_reverse_index
23 public :: terminal_switch_screen, terminal_active_screen
24 public :: terminal_scroll_view, terminal_get_scroll_offset, terminal_reset_scroll_view
25 public :: terminal_get_scrollback_count, terminal_get_scrollback_line
26 public :: terminal_queue_response, terminal_get_response, terminal_has_response
27 public :: terminal_set_title, terminal_get_title, terminal_has_title_changed
28
29 integer, parameter :: RESPONSE_BUFFER_SIZE = 256
30
31 type :: terminal_t
32 type(screen_t) :: screen ! Primary screen buffer
33 type(screen_t) :: alt_screen ! Alternate screen buffer
34 logical :: using_alt = .false. ! Which screen is active
35
36 type(cursor_t) :: cursor ! Current cursor state
37 type(cursor_t) :: saved_cursor ! Saved cursor (for DECSC/DECRC)
38
39 integer :: rows = 24 ! Terminal rows
40 integer :: cols = 80 ! Terminal columns
41 integer :: scroll_top = 1 ! Scroll region top
42 integer :: scroll_bottom = 24 ! Scroll region bottom
43
44 ! Scrollback buffer (for primary screen only)
45 type(scrollback_t) :: scrollback
46 integer :: scroll_offset = 0 ! View offset into scrollback (0 = live view)
47
48 ! Mode flags
49 logical :: mode_autowrap = .true. ! Auto-wrap at end of line
50 logical :: mode_origin = .false. ! Origin mode (cursor relative to scroll region)
51 logical :: mode_insert = .false. ! Insert mode
52 logical :: pending_wrap = .false. ! Deferred wrap: cursor at EOL, wrap on next char
53
54 ! Tab stops
55 logical, allocatable :: tabstops(:)
56
57 ! Response buffer for escape sequence replies (e.g., DA1)
58 character(len=RESPONSE_BUFFER_SIZE) :: response = ''
59 integer :: response_len = 0
60
61 ! Window title (set via OSC 0/1/2)
62 character(len=256) :: title = 'fortty'
63 logical :: title_changed = .false.
64 end type terminal_t
65
66 contains
67
68 ! Initialize terminal
69 subroutine terminal_init(term, rows, cols)
70 type(terminal_t), intent(inout) :: term
71 integer, intent(in) :: rows, cols
72 integer :: i
73
74 term%rows = rows
75 term%cols = cols
76 term%scroll_top = 1
77 term%scroll_bottom = rows
78 term%scroll_offset = 0
79
80 ! Initialize both screen buffers
81 call screen_init(term%screen, rows, cols)
82 call screen_init(term%alt_screen, rows, cols)
83
84 ! Initialize scrollback buffer
85 call scrollback_init(term%scrollback, cols)
86
87 ! Initialize cursor with default colors
88 term%cursor%row = 1
89 term%cursor%col = 1
90 term%cursor%fg = default_fg
91 term%cursor%bg = default_bg
92 term%cursor%attrs = 0
93
94 ! Initialize tab stops every 8 columns
95 allocate(term%tabstops(cols))
96 term%tabstops = .false.
97 do i = 1, cols, 8
98 term%tabstops(i) = .true.
99 end do
100 end subroutine terminal_init
101
102 ! Destroy terminal
103 subroutine terminal_destroy(term)
104 type(terminal_t), intent(inout) :: term
105
106 call screen_destroy(term%screen)
107 call screen_destroy(term%alt_screen)
108 call scrollback_destroy(term%scrollback)
109 if (allocated(term%tabstops)) deallocate(term%tabstops)
110 end subroutine terminal_destroy
111
112 ! Resize terminal
113 subroutine terminal_resize(term, rows, cols)
114 type(terminal_t), intent(inout) :: term
115 integer, intent(in) :: rows, cols
116 integer :: i
117
118 term%rows = rows
119 term%cols = cols
120 term%scroll_bottom = rows
121
122 call screen_resize(term%screen, rows, cols)
123 call screen_resize(term%alt_screen, rows, cols)
124
125 ! Resize tab stops
126 if (allocated(term%tabstops)) deallocate(term%tabstops)
127 allocate(term%tabstops(cols))
128 term%tabstops = .false.
129 do i = 1, cols, 8
130 term%tabstops(i) = .true.
131 end do
132
133 ! Clamp cursor to new bounds
134 term%cursor%row = min(term%cursor%row, rows)
135 term%cursor%col = min(term%cursor%col, cols)
136 end subroutine terminal_resize
137
138 ! Get pointer to active screen
139 function terminal_active_screen(term) result(scr)
140 type(terminal_t), intent(inout), target :: term
141 type(screen_t), pointer :: scr
142
143 if (term%using_alt) then
144 scr => term%alt_screen
145 else
146 scr => term%screen
147 end if
148 end function terminal_active_screen
149
150 ! Put a character at cursor position
151 subroutine terminal_put_char(term, codepoint)
152 type(terminal_t), intent(inout) :: term
153 integer, intent(in) :: codepoint
154 type(cell_t) :: cell, cont_cell
155 type(screen_t), pointer :: scr
156 integer :: width
157
158 scr => terminal_active_screen(term)
159
160 ! Handle control characters
161 select case (codepoint)
162 case (10) ! LF - Line Feed
163 call terminal_newline(term)
164 return
165 case (13) ! CR - Carriage Return
166 call terminal_carriage_return(term)
167 return
168 case (9) ! HT - Horizontal Tab
169 call terminal_tab(term)
170 return
171 case (8) ! BS - Backspace
172 call terminal_backspace(term)
173 return
174 case (7) ! BEL - Bell (ignore for now)
175 return
176 case (0:6, 14:26, 28:31) ! Other control chars - ignore (27=ESC handled in Phase 5)
177 return
178 end select
179
180 ! Determine character display width
181 width = codepoint_width(codepoint)
182
183 ! Skip zero-width characters (combining marks, etc.)
184 if (width == 0) return
185
186 ! Handle deferred wrap: if pending, execute wrap now before writing new char
187 if (term%pending_wrap) then
188 call terminal_newline(term)
189 term%cursor%col = 1
190 term%pending_wrap = .false.
191 end if
192
193 ! Check if wide character fits before line end
194 if (width == 2 .and. term%cursor%col + 1 > term%cols) then
195 if (term%mode_autowrap) then
196 call terminal_newline(term)
197 term%cursor%col = 1
198 else
199 ! Can't fit - don't draw
200 return
201 end if
202 end if
203
204 ! Printable character - create cell with current style
205 cell%codepoint = codepoint
206 cell%fg = term%cursor%fg
207 cell%bg = term%cursor%bg
208 cell%attrs = term%cursor%attrs
209 cell%width = width
210 cell%is_continuation = .false.
211
212 ! Place in buffer
213 call screen_set_cell(scr, term%cursor%row, term%cursor%col, cell)
214
215 ! For wide characters, write continuation cell
216 if (width == 2 .and. term%cursor%col < term%cols) then
217 cont_cell%codepoint = 0
218 cont_cell%fg = term%cursor%fg
219 cont_cell%bg = term%cursor%bg
220 cont_cell%attrs = term%cursor%attrs
221 cont_cell%width = 0
222 cont_cell%is_continuation = .true.
223 call screen_set_cell(scr, term%cursor%row, term%cursor%col + 1, cont_cell)
224 end if
225
226 ! Advance cursor by character width
227 term%cursor%col = term%cursor%col + width
228
229 ! Handle wrap at end of line - use deferred wrap
230 if (term%cursor%col > term%cols) then
231 if (term%mode_autowrap) then
232 ! Don't wrap yet - set pending flag, wrap on next char
233 term%cursor%col = term%cols ! Keep cursor at last column
234 term%pending_wrap = .true.
235 else
236 term%cursor%col = term%cols ! Stay at edge
237 end if
238 end if
239 end subroutine terminal_put_char
240
241 ! Move to next line
242 subroutine terminal_newline(term)
243 type(terminal_t), intent(inout) :: term
244
245 if (term%cursor%row >= term%scroll_bottom) then
246 ! At bottom of scroll region - scroll up
247 call terminal_scroll_up(term, 1)
248 else
249 ! Move down
250 term%cursor%row = term%cursor%row + 1
251 end if
252 end subroutine terminal_newline
253
254 ! Move to start of line
255 subroutine terminal_carriage_return(term)
256 type(terminal_t), intent(inout) :: term
257
258 term%cursor%col = 1
259 term%pending_wrap = .false. ! Cancel deferred wrap
260 end subroutine terminal_carriage_return
261
262 ! Move to next tab stop
263 subroutine terminal_tab(term)
264 type(terminal_t), intent(inout) :: term
265 integer :: col
266
267 term%pending_wrap = .false. ! Cancel deferred wrap
268
269 do col = term%cursor%col + 1, term%cols
270 if (term%tabstops(col)) then
271 term%cursor%col = col
272 return
273 end if
274 end do
275
276 ! No more tab stops - go to end
277 term%cursor%col = term%cols
278 end subroutine terminal_tab
279
280 ! Move cursor back one position
281 subroutine terminal_backspace(term)
282 type(terminal_t), intent(inout) :: term
283
284 term%pending_wrap = .false. ! Cancel deferred wrap
285 if (term%cursor%col > 1) then
286 term%cursor%col = term%cursor%col - 1
287 end if
288 end subroutine terminal_backspace
289
290 ! Scroll up n lines within scroll region
291 subroutine terminal_scroll_up(term, n)
292 type(terminal_t), intent(inout) :: term
293 integer, intent(in) :: n
294 type(screen_t), pointer :: scr
295 integer :: row, col, src_row, i
296
297 scr => terminal_active_screen(term)
298
299 ! Save lines to scrollback before they're lost (primary screen only)
300 ! Only save if scrolling from the very top of the screen
301 if (.not. term%using_alt .and. term%scroll_top == 1) then
302 do i = 1, min(n, term%scroll_bottom)
303 call scrollback_push_line(term%scrollback, scr%cells(i, :), term%cols)
304 end do
305 end if
306
307 ! Move lines up
308 do row = term%scroll_top, term%scroll_bottom - n
309 src_row = row + n
310 do col = 1, term%cols
311 scr%cells(row, col) = scr%cells(src_row, col)
312 end do
313 call screen_mark_dirty(scr, row)
314 end do
315
316 ! Clear new lines at bottom
317 do row = term%scroll_bottom - n + 1, term%scroll_bottom
318 call screen_clear_line(scr, row)
319 end do
320 end subroutine terminal_scroll_up
321
322 ! Scroll down n lines within scroll region
323 subroutine terminal_scroll_down(term, n)
324 type(terminal_t), intent(inout) :: term
325 integer, intent(in) :: n
326 type(screen_t), pointer :: scr
327 integer :: row, col, src_row
328
329 scr => terminal_active_screen(term)
330
331 ! Move lines down (iterate in reverse)
332 do row = term%scroll_bottom, term%scroll_top + n, -1
333 src_row = row - n
334 do col = 1, term%cols
335 scr%cells(row, col) = scr%cells(src_row, col)
336 end do
337 call screen_mark_dirty(scr, row)
338 end do
339
340 ! Clear new lines at top
341 do row = term%scroll_top, term%scroll_top + n - 1
342 call screen_clear_line(scr, row)
343 end do
344 end subroutine terminal_scroll_down
345
346 ! Erase in display (ED)
347 ! mode 0: cursor to end, mode 1: start to cursor, mode 2: entire screen
348 subroutine terminal_erase_display(term, mode)
349 type(terminal_t), intent(inout) :: term
350 integer, intent(in) :: mode
351 type(screen_t), pointer :: scr
352 integer :: row
353
354 scr => terminal_active_screen(term)
355
356 select case (mode)
357 case (0) ! Cursor to end of screen
358 ! Clear from cursor to end of current line
359 call screen_clear_region(scr, term%cursor%row, term%cursor%col, &
360 term%cursor%row, term%cols)
361 ! Clear remaining lines
362 do row = term%cursor%row + 1, term%rows
363 call screen_clear_line(scr, row)
364 end do
365
366 case (1) ! Start to cursor
367 ! Clear lines before cursor
368 do row = 1, term%cursor%row - 1
369 call screen_clear_line(scr, row)
370 end do
371 ! Clear from start of line to cursor
372 call screen_clear_region(scr, term%cursor%row, 1, &
373 term%cursor%row, term%cursor%col)
374
375 case (2, 3) ! Entire screen (3 also clears scrollback, but we don't have that yet)
376 call screen_clear(scr)
377 end select
378 end subroutine terminal_erase_display
379
380 ! Erase in line (EL)
381 ! mode 0: cursor to end, mode 1: start to cursor, mode 2: entire line
382 subroutine terminal_erase_line(term, mode)
383 type(terminal_t), intent(inout) :: term
384 integer, intent(in) :: mode
385 type(screen_t), pointer :: scr
386
387 scr => terminal_active_screen(term)
388
389 select case (mode)
390 case (0) ! Cursor to end of line
391 call screen_clear_region(scr, term%cursor%row, term%cursor%col, &
392 term%cursor%row, term%cols)
393
394 case (1) ! Start to cursor
395 call screen_clear_region(scr, term%cursor%row, 1, &
396 term%cursor%row, term%cursor%col)
397
398 case (2) ! Entire line
399 call screen_clear_line(scr, term%cursor%row)
400 end select
401 end subroutine terminal_erase_line
402
403 ! Move cursor to absolute position
404 subroutine terminal_cursor_move(term, row, col)
405 type(terminal_t), intent(inout) :: term
406 integer, intent(in) :: row, col
407
408 term%pending_wrap = .false. ! Cancel deferred wrap
409 term%cursor%row = max(1, min(row, term%rows))
410 term%cursor%col = max(1, min(col, term%cols))
411 end subroutine terminal_cursor_move
412
413 ! Move cursor up n rows
414 subroutine terminal_cursor_up(term, n)
415 type(terminal_t), intent(inout) :: term
416 integer, intent(in) :: n
417
418 term%pending_wrap = .false. ! Cancel deferred wrap
419 term%cursor%row = max(1, term%cursor%row - n)
420 end subroutine terminal_cursor_up
421
422 ! Move cursor down n rows
423 subroutine terminal_cursor_down(term, n)
424 type(terminal_t), intent(inout) :: term
425 integer, intent(in) :: n
426
427 term%pending_wrap = .false. ! Cancel deferred wrap
428 term%cursor%row = min(term%rows, term%cursor%row + n)
429 end subroutine terminal_cursor_down
430
431 ! Move cursor forward n columns
432 subroutine terminal_cursor_forward(term, n)
433 type(terminal_t), intent(inout) :: term
434 integer, intent(in) :: n
435
436 term%pending_wrap = .false. ! Cancel deferred wrap
437 term%cursor%col = min(term%cols, term%cursor%col + n)
438 end subroutine terminal_cursor_forward
439
440 ! Move cursor backward n columns
441 subroutine terminal_cursor_backward(term, n)
442 type(terminal_t), intent(inout) :: term
443 integer, intent(in) :: n
444
445 term%pending_wrap = .false. ! Cancel deferred wrap
446 term%cursor%col = max(1, term%cursor%col - n)
447 end subroutine terminal_cursor_backward
448
449 ! Switch between primary and alternate screen
450 subroutine terminal_switch_screen(term, use_alt)
451 type(terminal_t), intent(inout) :: term
452 logical, intent(in) :: use_alt
453
454 if (use_alt .and. .not. term%using_alt) then
455 ! Switching to alternate - save cursor
456 term%saved_cursor = term%cursor
457 term%using_alt = .true.
458 call screen_clear(term%alt_screen)
459 term%cursor%row = 1
460 term%cursor%col = 1
461 else if (.not. use_alt .and. term%using_alt) then
462 ! Switching back to primary - restore cursor
463 term%using_alt = .false.
464 term%cursor = term%saved_cursor
465 end if
466
467 ! Mark active screen as fully dirty
468 if (term%using_alt) then
469 call screen_mark_all_dirty(term%alt_screen)
470 else
471 call screen_mark_all_dirty(term%screen)
472 end if
473 end subroutine terminal_switch_screen
474
475 ! Save cursor position and attributes (DECSC)
476 subroutine terminal_save_cursor(term)
477 type(terminal_t), intent(inout) :: term
478
479 term%saved_cursor = term%cursor
480 end subroutine terminal_save_cursor
481
482 ! Restore cursor position and attributes (DECRC)
483 subroutine terminal_restore_cursor(term)
484 type(terminal_t), intent(inout) :: term
485
486 term%cursor = term%saved_cursor
487 ! Clamp to current screen bounds
488 term%cursor%row = max(1, min(term%cursor%row, term%rows))
489 term%cursor%col = max(1, min(term%cursor%col, term%cols))
490 end subroutine terminal_restore_cursor
491
492 ! Set scroll region (DECSTBM)
493 subroutine terminal_set_scroll_region(term, top, bottom)
494 type(terminal_t), intent(inout) :: term
495 integer, intent(in) :: top, bottom
496
497 if (top < bottom .and. top >= 1 .and. bottom <= term%rows) then
498 term%scroll_top = top
499 term%scroll_bottom = bottom
500 ! Move cursor to home position
501 if (term%mode_origin) then
502 term%cursor%row = top
503 else
504 term%cursor%row = 1
505 end if
506 term%cursor%col = 1
507 end if
508 end subroutine terminal_set_scroll_region
509
510 ! Insert n blank lines at cursor (IL)
511 subroutine terminal_insert_lines(term, n)
512 type(terminal_t), intent(inout) :: term
513 integer, intent(in) :: n
514 type(screen_t), pointer :: scr
515 integer :: row, col, src_row, actual_n
516
517 ! Only works within scroll region
518 if (term%cursor%row < term%scroll_top .or. term%cursor%row > term%scroll_bottom) return
519
520 scr => terminal_active_screen(term)
521 actual_n = min(n, term%scroll_bottom - term%cursor%row + 1)
522
523 ! Move lines down (iterate in reverse)
524 do row = term%scroll_bottom, term%cursor%row + actual_n, -1
525 src_row = row - actual_n
526 do col = 1, term%cols
527 scr%cells(row, col) = scr%cells(src_row, col)
528 end do
529 call screen_mark_dirty(scr, row)
530 end do
531
532 ! Clear new lines at cursor position
533 do row = term%cursor%row, term%cursor%row + actual_n - 1
534 call screen_clear_line(scr, row)
535 end do
536 end subroutine terminal_insert_lines
537
538 ! Delete n lines at cursor (DL)
539 subroutine terminal_delete_lines(term, n)
540 type(terminal_t), intent(inout) :: term
541 integer, intent(in) :: n
542 type(screen_t), pointer :: scr
543 integer :: row, col, src_row, actual_n
544
545 ! Only works within scroll region
546 if (term%cursor%row < term%scroll_top .or. term%cursor%row > term%scroll_bottom) return
547
548 scr => terminal_active_screen(term)
549 actual_n = min(n, term%scroll_bottom - term%cursor%row + 1)
550
551 ! Move lines up
552 do row = term%cursor%row, term%scroll_bottom - actual_n
553 src_row = row + actual_n
554 do col = 1, term%cols
555 scr%cells(row, col) = scr%cells(src_row, col)
556 end do
557 call screen_mark_dirty(scr, row)
558 end do
559
560 ! Clear lines at bottom of scroll region
561 do row = term%scroll_bottom - actual_n + 1, term%scroll_bottom
562 call screen_clear_line(scr, row)
563 end do
564 end subroutine terminal_delete_lines
565
566 ! Insert n blank characters at cursor (ICH)
567 subroutine terminal_insert_chars(term, n)
568 type(terminal_t), intent(inout) :: term
569 integer, intent(in) :: n
570 type(screen_t), pointer :: scr
571 integer :: col, src_col, actual_n
572
573 scr => terminal_active_screen(term)
574 actual_n = min(n, term%cols - term%cursor%col + 1)
575
576 ! Shift characters right
577 do col = term%cols, term%cursor%col + actual_n, -1
578 src_col = col - actual_n
579 scr%cells(term%cursor%row, col) = scr%cells(term%cursor%row, src_col)
580 end do
581
582 ! Clear inserted positions
583 do col = term%cursor%col, term%cursor%col + actual_n - 1
584 scr%cells(term%cursor%row, col) = cell_t(32, default_fg, default_bg, 0)
585 end do
586
587 call screen_mark_dirty(scr, term%cursor%row)
588 end subroutine terminal_insert_chars
589
590 ! Delete n characters at cursor (DCH)
591 subroutine terminal_delete_chars(term, n)
592 type(terminal_t), intent(inout) :: term
593 integer, intent(in) :: n
594 type(screen_t), pointer :: scr
595 integer :: col, src_col, actual_n
596
597 scr => terminal_active_screen(term)
598 actual_n = min(n, term%cols - term%cursor%col + 1)
599
600 ! Shift characters left
601 do col = term%cursor%col, term%cols - actual_n
602 src_col = col + actual_n
603 scr%cells(term%cursor%row, col) = scr%cells(term%cursor%row, src_col)
604 end do
605
606 ! Clear vacated positions at end
607 do col = term%cols - actual_n + 1, term%cols
608 scr%cells(term%cursor%row, col) = cell_t(32, default_fg, default_bg, 0)
609 end do
610
611 call screen_mark_dirty(scr, term%cursor%row)
612 end subroutine terminal_delete_chars
613
614 ! Index - move cursor down, scroll if at bottom (IND)
615 subroutine terminal_index(term)
616 type(terminal_t), intent(inout) :: term
617
618 if (term%cursor%row >= term%scroll_bottom) then
619 call terminal_scroll_up(term, 1)
620 else
621 term%cursor%row = term%cursor%row + 1
622 end if
623 end subroutine terminal_index
624
625 ! Reverse index - move cursor up, scroll if at top (RI)
626 subroutine terminal_reverse_index(term)
627 type(terminal_t), intent(inout) :: term
628
629 if (term%cursor%row <= term%scroll_top) then
630 call terminal_scroll_down(term, 1)
631 else
632 term%cursor%row = term%cursor%row - 1
633 end if
634 end subroutine terminal_reverse_index
635
636 ! Reset terminal to initial state (RIS)
637 subroutine terminal_reset(term)
638 type(terminal_t), intent(inout) :: term
639 integer :: i
640
641 ! Reset scroll region
642 term%scroll_top = 1
643 term%scroll_bottom = term%rows
644
645 ! Reset modes
646 term%mode_autowrap = .true.
647 term%mode_origin = .false.
648 term%mode_insert = .false.
649 term%pending_wrap = .false.
650
651 ! Reset cursor
652 term%cursor%row = 1
653 term%cursor%col = 1
654 term%cursor%fg = default_fg
655 term%cursor%bg = default_bg
656 term%cursor%attrs = 0
657 term%cursor%visible = .true.
658
659 ! Clear screen
660 call screen_clear(term%screen)
661 call screen_clear(term%alt_screen)
662
663 ! Switch to primary screen
664 term%using_alt = .false.
665
666 ! Reset tab stops
667 term%tabstops = .false.
668 do i = 1, term%cols, 8
669 term%tabstops(i) = .true.
670 end do
671 end subroutine terminal_reset
672
673 ! Scroll view into scrollback history
674 ! Positive delta = scroll up (back in history)
675 ! Negative delta = scroll down (toward present)
676 subroutine terminal_scroll_view(term, delta)
677 type(terminal_t), intent(inout) :: term
678 integer, intent(in) :: delta
679 integer :: max_offset
680
681 ! Don't scroll on alternate screen
682 if (term%using_alt) return
683
684 max_offset = scrollback_count(term%scrollback)
685 term%scroll_offset = term%scroll_offset + delta
686 term%scroll_offset = max(0, min(term%scroll_offset, max_offset))
687 end subroutine terminal_scroll_view
688
689 ! Get current scroll offset
690 function terminal_get_scroll_offset(term) result(offset)
691 type(terminal_t), intent(in) :: term
692 integer :: offset
693
694 offset = term%scroll_offset
695 end function terminal_get_scroll_offset
696
697 ! Reset scroll view to live (current) output
698 subroutine terminal_reset_scroll_view(term)
699 type(terminal_t), intent(inout) :: term
700
701 term%scroll_offset = 0
702 end subroutine terminal_reset_scroll_view
703
704 ! Get number of lines in scrollback
705 function terminal_get_scrollback_count(term) result(n)
706 type(terminal_t), intent(in) :: term
707 integer :: n
708
709 n = scrollback_count(term%scrollback)
710 end function terminal_get_scrollback_count
711
712 ! Get a line from scrollback
713 ! offset: 0 = most recent line, 1 = second most recent, etc.
714 subroutine terminal_get_scrollback_line(term, offset, line, cols)
715 type(terminal_t), intent(in) :: term
716 integer, intent(in) :: offset
717 type(cell_t), intent(out) :: line(:)
718 integer, intent(in) :: cols
719
720 call scrollback_get_line(term%scrollback, offset, line, cols)
721 end subroutine terminal_get_scrollback_line
722
723 ! Queue a response to be sent back to the PTY
724 subroutine terminal_queue_response(term, response)
725 type(terminal_t), intent(inout) :: term
726 character(len=*), intent(in) :: response
727 integer :: len_resp
728
729 len_resp = len_trim(response)
730 if (len_resp > 0 .and. len_resp <= RESPONSE_BUFFER_SIZE) then
731 term%response = response
732 term%response_len = len_resp
733 end if
734 end subroutine terminal_queue_response
735
736 ! Check if there's a pending response
737 function terminal_has_response(term) result(has)
738 type(terminal_t), intent(in) :: term
739 logical :: has
740
741 has = (term%response_len > 0)
742 end function terminal_has_response
743
744 ! Get and clear the pending response
745 subroutine terminal_get_response(term, response, length)
746 type(terminal_t), intent(inout) :: term
747 character(len=*), intent(out) :: response
748 integer, intent(out) :: length
749
750 response = term%response
751 length = term%response_len
752 term%response = ''
753 term%response_len = 0
754 end subroutine terminal_get_response
755
756 ! Set window title (from OSC 0/1/2)
757 subroutine terminal_set_title(term, title)
758 type(terminal_t), intent(inout) :: term
759 character(len=*), intent(in) :: title
760
761 term%title = title
762 term%title_changed = .true.
763 end subroutine terminal_set_title
764
765 ! Get window title
766 function terminal_get_title(term) result(title)
767 type(terminal_t), intent(in) :: term
768 character(len=256) :: title
769
770 title = term%title
771 end function terminal_get_title
772
773 ! Check if title has changed (and clear the flag)
774 function terminal_has_title_changed(term) result(changed)
775 type(terminal_t), intent(inout) :: term
776 logical :: changed
777
778 changed = term%title_changed
779 term%title_changed = .false.
780 end function terminal_has_title_changed
781
782 end module terminal_mod
783