FORTRAT-F90 is a terminal application that fetches the AT Protocol lexicon schema. Written in Fortran
at main 451 lines 16 kB view raw
1module fortrat_render 2 use iso_fortran_env, only: real64, output_unit 3 use fortrat_types 4 implicit none 5 6 integer, parameter :: MAX_COLS = 300 7 integer, parameter :: MAX_ROWS = 100 8 9 type :: cell_t 10 character(len=1) :: ch = ' ' 11 character(len=16):: color = '' 12 logical :: bold = .false. 13 end type 14 15 type(cell_t), save :: cur_frame(MAX_COLS, MAX_ROWS) 16 type(cell_t), save :: prv_frame(MAX_COLS, MAX_ROWS) 17 logical, save :: first_frame = .true. 18 19contains 20 21 subroutine render_reset_frame() 22 first_frame = .true. 23 end subroutine 24 25 subroutine render_clear(w, h) 26 integer, intent(in) :: w, h 27 integer :: c, r 28 do r = 1, h 29 do c = 1, w 30 cur_frame(c,r)%ch = ' ' 31 cur_frame(c,r)%color = '' 32 cur_frame(c,r)%bold = .false. 33 end do 34 end do 35 end subroutine 36 37 subroutine render_set(c, r, ch, color, bold, w, h) 38 integer, intent(in) :: c, r, w, h 39 character(len=1), intent(in) :: ch 40 character(len=*), intent(in) :: color 41 logical, intent(in) :: bold 42 if (c < 1 .or. c > w .or. r < 1 .or. r > h) return 43 ! Don't overwrite bold (node) cells with edge chars 44 if (cur_frame(c,r)%bold .and. .not. bold) return 45 cur_frame(c,r)%ch = ch 46 cur_frame(c,r)%color = trim(color) 47 cur_frame(c,r)%bold = bold 48 end subroutine 49 50 ! Bresenham line — draw edge between two node positions 51 subroutine render_edge(x0, y0, x1, y1, color, w, h) 52 integer, intent(in) :: x0, y0, x1, y1, w, h 53 character(len=*), intent(in) :: color 54 integer :: cx, cy, dx, dy, sx, sy, err, e2 55 character(len=1) :: ch 56 57 dx = abs(x1-x0); sx = merge(1, -1, x0 < x1) 58 dy = -abs(y1-y0); sy = merge(1, -1, y0 < y1) 59 err = dx + dy 60 cx = x0; cy = y0 61 62 do 63 ! Pick character based on local slope 64 if (dx == 0 .or. abs(dy) > abs(dx)*2) then 65 ch = '|' 66 else if (dy == 0 .or. abs(dx) > abs(dy)*2) then 67 ch = '-' 68 else if ((sx > 0 .and. sy > 0) .or. (sx < 0 .and. sy < 0)) then 69 ch = '\' 70 else 71 ch = '/' 72 end if 73 ! Skip cells too close to endpoints (node area) 74 if (.not. (cx == x0 .and. cy == y0) .and. & 75 .not. (cx == x1 .and. cy == y1)) then 76 call render_set(cx, cy, ch, color, .false., w, h) 77 end if 78 if (cx == x1 .and. cy == y1) exit 79 e2 = 2 * err 80 if (e2 >= dy) then; err = err + dy; cx = cx + sx; end if 81 if (e2 <= dx) then; err = err + dx; cy = cy + sy; end if 82 end do 83 end subroutine 84 85 ! Draw a node sigil at position 86 subroutine render_node(cx, cy, lbl, color, selected, w, h) 87 integer, intent(in) :: cx, cy, w, h 88 character(len=*), intent(in) :: lbl, color 89 logical, intent(in) :: selected 90 integer :: i, llen, start_col 91 character(len=12) :: display 92 llen = min(len_trim(lbl), 10) ! cap at 10 chars 93 if (selected) then 94 display = '['//lbl(1:llen)//']' 95 llen = llen + 2 96 else 97 display = lbl(1:llen) 98 end if 99 ! Centre the label on cx 100 start_col = cx - llen/2 101 do i = 1, llen 102 call render_set(start_col + i - 1, cy, display(i:i), color, selected, w, h) 103 end do 104 end subroutine 105 106 ! Draw text string at position (truncated to fit) 107 subroutine render_text(c, r, text, color, bold, w, h) 108 integer, intent(in) :: c, r, w, h 109 character(len=*), intent(in) :: text, color 110 logical, intent(in) :: bold 111 integer :: i, len_t 112 len_t = min(len_trim(text), w - c + 1) 113 do i = 1, len_t 114 call render_set(c + i - 1, r, text(i:i), color, bold, w, h) 115 end do 116 end subroutine 117 118 ! Flush frame — build entire output as one buffer, write atomically (no flicker) 119 subroutine render_flush(w, h) 120 use fortrat_tui, only: fortrat_write_buf, fortrat_flush 121 use iso_c_binding 122 integer, intent(in) :: w, h 123 ! Frame buffer: enough for clear + all cells 124 ! Each cell: ~20 bytes (cursor pos + color + char + reset) 125 ! Max cells: 300*100 = 30000 * 20 = 600KB — use allocatable 126 character(len=1), allocatable :: fbuf(:) 127 integer :: fbuf_size, fpos 128 integer :: c, r, cl 129 character(len=32) :: pos_seq 130 integer :: pos_len 131 132 fbuf_size = 9 + w * h * 16 ! home seq + per cell: color+bold+char+reset ~16 bytes 133 allocate(fbuf(fbuf_size)) 134 fpos = 0 135 136 ! Start with: hide cursor, home cursor only (no clear — prevents scroll) 137 call fbuf_append(fbuf, fpos, char(27)//'[?25l', 6) ! hide cursor 138 call fbuf_append(fbuf, fpos, char(27)//'[H', 3) ! cursor home row 1 col 1 139 140 ! Write all cells — including spaces — so previous frame is fully overwritten 141 do r = 1, h 142 ! Move to start of row 143 write(pos_seq, '(a,i0,a,i0,a)') char(27)//'[', r, ';1H' 144 pos_len = len_trim(pos_seq) 145 call fbuf_append(fbuf, fpos, pos_seq(1:pos_len), pos_len) 146 147 do c = 1, w 148 ! Color 149 if (len_trim(cur_frame(c,r)%color) > 0) then 150 cl = len_trim(cur_frame(c,r)%color) 151 call fbuf_append(fbuf, fpos, cur_frame(c,r)%color(1:cl), cl) 152 else 153 call fbuf_append(fbuf, fpos, char(27)//'[0m', 4) 154 end if 155 ! Bold 156 if (cur_frame(c,r)%bold) then 157 call fbuf_append(fbuf, fpos, char(27)//'[1m', 4) 158 end if 159 ! Character 160 call fbuf_append(fbuf, fpos, cur_frame(c,r)%ch, 1) 161 prv_frame(c,r) = cur_frame(c,r) 162 if (fpos > fbuf_size - 64) exit 163 end do 164 ! Reset at end of row 165 call fbuf_append(fbuf, fpos, char(27)//'[0m', 4) 166 if (fpos > fbuf_size - 64) exit 167 end do 168 169 ! Single write call — atomic, no flicker 170 call fortrat_write_buf(fbuf, int(fpos, c_int)) 171 call fortrat_flush() 172 deallocate(fbuf) 173 first_frame = .false. 174 end subroutine 175 176 subroutine fbuf_append(buf, pos, str, n) 177 character(len=1), intent(inout) :: buf(:) 178 integer, intent(inout) :: pos 179 character(len=*), intent(in) :: str 180 integer, intent(in) :: n 181 integer :: i 182 do i = 1, n 183 pos = pos + 1 184 if (pos <= size(buf)) buf(pos) = str(i:i) 185 end do 186 end subroutine 187 188 ! ── Main graph pane render ── 189 subroutine render_graph_pane(graph, state, w, h, row_off) 190 type(lex_graph_t), intent(in) :: graph 191 type(app_state_t), intent(in) :: state 192 integer, intent(in) :: w, h, row_off 193 integer :: i, cx, cy, vis_idx 194 character(len=16) :: col 195 logical :: selected, dimmed 196 integer :: visible_nodes(MAX_NODES), n_vis 197 ! Build visible node index list 198 n_vis = 0 199 do i = 1, graph%n_nodes 200 if (graph%nodes(i)%active) then 201 n_vis = n_vis + 1 202 visible_nodes(n_vis) = i 203 end if 204 end do 205 206 ! Draw edges first 207 do i = 1, graph%n_edges 208 if (graph%edges(i)%src == 0) cycle 209 if (.not. graph%nodes(graph%edges(i)%src)%active) cycle 210 if (.not. graph%nodes(graph%edges(i)%tgt)%active) cycle 211 cx = nint(graph%nodes(graph%edges(i)%src)%x) 212 cy = nint(graph%nodes(graph%edges(i)%src)%y) + row_off 213 call render_edge( & 214 cx, cy, & 215 nint(graph%nodes(graph%edges(i)%tgt)%x), & 216 nint(graph%nodes(graph%edges(i)%tgt)%y) + row_off, & 217 GREEN_DIM, w, h) 218 end do 219 220 ! Draw nodes 221 vis_idx = 0 222 do i = 1, graph%n_nodes 223 if (.not. graph%nodes(i)%active) cycle 224 vis_idx = vis_idx + 1 225 cx = nint(graph%nodes(i)%x) 226 cy = nint(graph%nodes(i)%y) + row_off 227 col = ns_color(graph%nodes(i)%ns_group) 228 selected = (vis_idx == state%cursor_idx .or. i == state%selected_idx) 229 dimmed = len_trim(state%search_query) > 0 .and. & 230 index(graph%nodes(i)%id, trim(state%search_query)) == 0 231 if (dimmed) col = GREEN_DIM 232 call render_node(cx, cy, trim(graph%nodes(i)%label), col, selected, w, h) 233 end do 234 end subroutine 235 236 ! ── Inspect pane ── 237 subroutine render_inspect_pane(graph, state, col_off, w, h) 238 type(lex_graph_t), intent(in) :: graph 239 type(app_state_t), intent(in) :: state 240 integer, intent(in) :: col_off, w, h 241 integer :: row, i, idx, vis_idx, n_out, pane_w 242 character(len=ID_LEN) :: out_ids(64) 243 character(len=3) :: sigil 244 245 pane_w = w - col_off 246 row = 2 247 248 idx = 0 249 vis_idx = 0 250 do i = 1, graph%n_nodes 251 if (.not. graph%nodes(i)%active) cycle 252 vis_idx = vis_idx + 1 253 if (vis_idx == state%cursor_idx) then; idx = i; exit; end if 254 end do 255 if (state%selected_idx > 0) idx = state%selected_idx 256 257 if (idx == 0) then 258 call render_text(col_off, row, 'C NO NODE SELECTED', GREEN_DIM, .false., w, h) 259 call render_text(col_off, row+2, ' HJKL : navigate', GREEN_DIM, .false., w, h) 260 call render_text(col_off, row+3, ' ENTER : inspect', GREEN_DIM, .false., w, h) 261 call render_text(col_off, row+4, ' / : search', GREEN_DIM, .false., w, h) 262 call render_text(col_off, row+5, ' TAB : toggle ns',GREEN_DIM, .false., w, h) 263 call render_text(col_off, row+6, ' C : community',GREEN_DIM, .false., w, h) 264 call render_text(col_off, row+7, ' Q : quit', GREEN_DIM, .false., w, h) 265 return 266 end if 267 268 sigil = KIND_SIGIL(graph%nodes(idx)%kind)(1:3) 269 call render_text(col_off, row, 'SUBROUTINE INSPECT('//sigil//')', GREEN_BR, .true., w, h) 270 row = row + 1 271 call render_text(col_off, row, repeat('-', pane_w), GREEN_DIM, .false., w, h) 272 row = row + 1 273 call render_text(col_off, row, trim(graph%nodes(idx)%id), GREEN_BR, .true., w, h) 274 row = row + 2 275 276 call render_text(col_off, row, 'COMMON /LEXDATA/', GREEN_DIM, .false., w, h) 277 row = row + 1 278 call render_text(col_off, row, ' KIND '//sigil, GREEN, .false., w, h) 279 row = row + 1 280 call render_text(col_off, row, ' NS_GROUP '//ns_name(graph%nodes(idx)%ns_group), GREEN, .false., w, h) 281 row = row + 1 282 if (graph%nodes(idx)%ns_group == NS_COMMUNITY) then 283 call render_text(col_off, row, ' ORIGIN EXTERNAL', YELLOW, .false., w, h) 284 row = row + 1 285 end if 286 row = row + 1 287 288 ! Description with word-wrap 289 if (len_trim(graph%nodes(idx)%doc) > 0 .and. row < h - 3) then 290 block 291 integer :: dlen, lw, pos, npos 292 character(len=DOC_LEN) :: doc 293 doc = trim(graph%nodes(idx)%doc) 294 dlen = len_trim(doc) 295 lw = pane_w - 6 296 pos = 1 297 call render_text(col_off, row, 'C DESCRIPTION', GREEN_DIM, .false., w, h) 298 row = row + 1 299 do while (pos <= dlen .and. row < h - 3) 300 npos = min(pos + lw - 1, dlen) 301 call render_text(col_off, row, 'C '//doc(pos:npos), GREEN_DIM, .false., w, h) 302 pos = npos + 1 303 row = row + 1 304 end do 305 row = row + 1 306 end block 307 end if 308 309 ! Fields 310 if (graph%nodes(idx)%n_fields > 0 .and. row < h - 3) then 311 call render_text(col_off, row, 'C FIELDS', GREEN_DIM, .false., w, h) 312 row = row + 1 313 do i = 1, min(graph%nodes(idx)%n_fields, 8) 314 if (graph%nodes(idx)%fields(i)%required) then 315 call render_text(col_off, row, ' '//trim(graph%nodes(idx)%fields(i)%name), GREEN_BR, .false., w, h) 316 else 317 call render_text(col_off, row, 'C '//trim(graph%nodes(idx)%fields(i)%name), GREEN, .false., w, h) 318 end if 319 row = row + 1 320 if (row >= h - 2) exit 321 end do 322 row = row + 1 323 end if 324 325 ! Outbound refs 326 if (row < h - 4) then 327 n_out = 0 328 do i = 1, graph%n_edges 329 if (graph%edges(i)%src == idx) then 330 n_out = n_out + 1 331 if (n_out <= 4) out_ids(n_out) = graph%nodes(graph%edges(i)%tgt)%id 332 end if 333 end do 334 if (n_out > 0) then 335 call render_text(col_off, row, 'C CALL/REF', GREEN_DIM, .false., w, h) 336 row = row + 1 337 do i = 1, min(n_out, 4) 338 call render_text(col_off, row, ' CALL '//trim(out_ids(i)), GREEN, .false., w, h) 339 row = row + 1 340 if (row >= h - 2) exit 341 end do 342 end if 343 end if 344 345 call render_text(col_off, h-1, 'END SUBROUTINE INSPECT', GREEN_DIM, .false., w, h) 346 end subroutine 347 348 ! ── Column ruler ── 349 subroutine render_ruler(w, h) 350 integer, intent(in) :: w, h 351 character(len=80) :: ruler 352 ruler = 'C23456789012345678901234567890123456789012345678901234567890123456789072' 353 call render_text(1, 1, ruler(1:min(w,72)), GREEN_DIM, .false., w, h) 354 end subroutine 355 356 ! ── Pane header bar ── 357 subroutine render_header(w, h, divider_col) 358 integer, intent(in) :: w, h, divider_col 359 integer :: i 360 character(len=w) :: line 361 character(len=32) :: left_hdr, right_hdr 362 363 line = repeat('-', w) 364 left_hdr = '-[FORTRAT:GRAPH]' 365 right_hdr = '-[INSPECT]' 366 line(1:len_trim(left_hdr)) = left_hdr 367 line(divider_col:divider_col) = '+' 368 line(divider_col+1:divider_col+len_trim(right_hdr)) = right_hdr(1:len_trim(right_hdr)) 369 370 call render_text(1, 2, line, GREEN_DIM, .false., w, h) 371 372 ! Vertical divider 373 do i = 3, h - 1 374 call render_set(divider_col, i, '|', GREEN_DIM, .false., w, h) 375 end do 376 end subroutine 377 378 ! ── Status bar ── 379 subroutine render_status(state, graph, w, h) 380 type(app_state_t), intent(in) :: state 381 type(lex_graph_t), intent(in) :: graph 382 integer, intent(in) :: w, h 383 character(len=w) :: bar 384 character(len=32) :: left_part, right_part, mode_str 385 character(len=16) :: n_str, e_str 386 integer :: n_vis, n_edg, i 387 388 select case(state%mode) 389 case(MODE_GRAPH); mode_str = '[GRAPH]' 390 case(MODE_SEARCH); mode_str = '[SEARCH]' 391 case(MODE_INSPECT); mode_str = '[INSPECT]' 392 case(MODE_LOADING); mode_str = '[LOADING]' 393 case default; mode_str = '[?]' 394 end select 395 396 n_vis = 0 397 do i = 1, graph%n_nodes 398 if (graph%nodes(i)%active) n_vis = n_vis + 1 399 end do 400 n_edg = graph%n_edges 401 402 write(n_str, '(i0)') n_vis 403 write(e_str, '(i0)') n_edg 404 405 left_part = ' FORTRAT '//trim(mode_str)//' '//trim(state%status_msg) 406 right_part = ' N='//trim(n_str)//' E='//trim(e_str)//' ' 407 408 bar = repeat(' ', w) 409 bar(1:min(len_trim(left_part),w)) = left_part(1:min(len_trim(left_part),w)) 410 bar(w-len_trim(right_part)+1:w) = right_part 411 412 call render_text(1, h, bar, GREEN, .true., w, h) 413 end subroutine 414 415 ! ── Loading screen ── 416 subroutine render_loading(state, w, h) 417 type(app_state_t), intent(in) :: state 418 integer, intent(in) :: w, h 419 integer :: r 420 421 r = 2 422 call render_text(1, r, 'C23456789012345678901234567890123456789012345678901234567890123456789072', GREEN_DIM, .false., w, h) 423 r = r + 2 424 call render_text(7, r, 'FORTRAT V1.0 (FORMERLAB 2026)', GREEN_BR, .true., w, h) 425 r = r + 1 426 call render_text(7, r, 'COPYRIGHT (C) FORMERLAB. ALL RIGHTS RESERVED.', GREEN_DIM, .false., w, h) 427 r = r + 2 428 call render_text(7, r, 'IMPLICIT NONE', GREEN, .false., w, h); r=r+1 429 call render_text(7, r, 'CHARACTER*(256) QUERY', GREEN, .false., w, h); r=r+1 430 call render_text(7, r, 'INTEGER N_NODES, N_EDGES', GREEN, .false., w, h); r=r+1 431 call render_text(7, r, 'LOGICAL COMMUNITY_FLAG', GREEN, .false., w, h); r=r+1 432 call render_text(7, r, 'DATA COMMUNITY_FLAG /.FALSE./', GREEN, .false., w, h); r=r+2 433 call render_text(7, r, 'CALL FETCH_LEXICONS(ATPROTO_REPO, GRAPH)', GREEN, .false., w, h); r=r+2 434 call render_text(7, r, 'C STATUS:', GREEN_DIM, .false., w, h); r=r+1 435 call render_text(7, r, '>> '//trim(state%progress), GREEN_BR, .false., w, h) 436 end subroutine 437 438 ! Helper 439 function ns_name(ns) result(s) 440 integer, intent(in) :: ns 441 character(len=20) :: s 442 select case(ns) 443 case(NS_APP_BSKY); s = 'app.bsky' 444 case(NS_COM_ATPROTO); s = 'com.atproto' 445 case(NS_CHAT_BSKY); s = 'chat.bsky' 446 case(NS_TOOLS_OZONE); s = 'tools.ozone' 447 case default; s = 'community' 448 end select 449 end function 450 451end module fortrat_render