FORTRAT-F90 is a terminal application that fetches the AT Protocol lexicon schema. Written in Fortran
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix render stability, node labels, inspect panel word-wrap

FormerLab 9e874291 1f47abc4

+137 -86
+5 -5
README.md
··· 25 25 26 26 ## Why? 27 27 28 - [Fortransky](https://github.com/FormerLab/fortransky) — our FORTRAN Bluesky client — shipped a few days before Atmosphere 2026. The AT Proto crowd found out that someone had written a Bluesky client in a language from 1957. This was considered funny. We considered it a mandate :) 28 + [Fortransky](https://github.com/FormerLab/fortransky) — our FORTRAN 77 Bluesky client — shipped a few days before Atmosphere 2026. The AT Proto crowd found out that someone had written a Bluesky client in a language from 1957. This was considered funny. We considered it a mandate :) 29 29 30 - FORTRAT-F90 is the follow-up, our inhouse AT Ptoto lexicon explorer now open sourced. If Fortransky proved you *could* post to Bluesky from Fortran, FORTRAT-F90 proves you can also explore the entire AT Protocol schema from Fortran while watching a force simulation settle in real time. 30 + FORTRAT-F90 is the follow-up, our in-house AT Proto lexicon explorer, now open sourced. If Fortransky proved you *could* post to Bluesky from Fortran, FORTRAT-F90 proves you can also explore the entire AT Protocol schema from Fortran while watching a force simulation settle in real time. 31 31 32 32 The name is FORTRAN + AT Protocol. FORTRAN identifiers were limited to 6 characters. We used 6. We showed restraint. 33 33 ··· 39 39 40 40 FORTRAT-F90 fetches all of them, builds a graph where nodes are lexicon types and edges are references between them, and renders it. The dense blue-green cluster in the middle is `app.bsky.*`. The orbiting yellow nodes are `com.atproto.*`. The purple ones are `tools.ozone.*`. The force simulation pulls related types together and pushes unrelated ones apart, so the layout roughly reflects the actual structure of the protocol. 41 41 42 - It updates every 150ms. The nodes drift. 42 + Nodes show their actual lexicon name — `post`, `like`, `follow`, `getProfile`. The simulation settles after a few seconds and the graph holds still. 43 43 44 44 --- 45 45 ··· 106 106 107 107 ## Recommended terminal size 108 108 109 - 120×40 minimum. The wider the better. The graph breathes at 200+ columns 109 + 120×40 minimum. The wider the better. The graph breathes at 200+ columns. 110 110 111 111 Works in cool-retro-term (phosphor green preset, obviously) and in any modern terminal. The screenshots from cool-retro-term looked better. The screenshots from a normal terminal are more readable. Both are valid choices and we will not judge you. 112 112 ··· 114 114 115 115 ## Relation to Fortransky 116 116 117 - [Fortransky](https://github.com/FormerLab/fortransky) is a FORTRAN Bluesky client. It posts. It reads timelines. It has a Rust decoder for the firehose and an x86-64 assembly decoder for fun. It is arguably the most over-engineered Bluesky client in existence... 117 + [Fortransky](https://github.com/FormerLab/fortransky) is a FORTRAN 77 Bluesky client. It posts. It reads timelines. It has a Rust decoder for the firehose and an x86-64 assembly decoder for fun. It is arguably the most over-engineered Bluesky client in existence... 118 118 119 119 FORTRAT-F90 is Fortran 2018. It uses allocatable strings, `iso_c_binding`, and modern modules. By comparison it is practically contemporary software. 120 120
+31 -14
src/main.f90
··· 9 9 10 10 type(lex_graph_t) :: graph 11 11 type(app_state_t) :: state 12 - integer :: key, cols, rows 12 + integer :: key, cols, rows, frame_skip 13 + logical :: dirty 13 14 14 15 ! ── Init state ── 15 16 state%groups = .true. ! all ns groups visible ··· 43 44 44 45 state%mode = MODE_GRAPH 45 46 state%status_msg = 'READY' 47 + frame_skip = 0 46 48 47 49 ! ── Main event loop ── 48 50 do while (.true.) 51 + dirty = .false. 49 52 50 - ! ── Simulation tick ── 51 - call sim_tick(graph, state%graph_w, state%graph_h) 53 + ! ── Simulation tick (only while alpha > 0) ── 54 + if (alpha > 0.0d0) then 55 + call sim_tick(graph, state%graph_w, state%graph_h) 56 + dirty = .true. 57 + end if 52 58 53 59 ! ── Read input (non-blocking) ── 54 60 key = tui_read_key() 55 - if (key /= 0) call handle_key(key, graph, state) 61 + if (key /= 0) then 62 + call handle_key(key, graph, state) 63 + dirty = .true. 64 + end if 56 65 57 - ! ── Render ── 58 - call render_clear(state%term_w, state%term_h) 59 - call render_ruler(state%term_w, state%term_h) 60 - call render_header(state%term_w, state%term_h, state%graph_w + 1) 61 - call render_graph_pane(graph, state, state%graph_w, state%term_h, 2) 62 - call render_inspect_pane(graph, state, state%graph_w + 2, state%term_w, state%term_h) 63 - call render_status(state, graph, state%term_w, state%term_h) 64 - call render_flush(state%term_w, state%term_h) 66 + ! ── Render only when something changed, or every 8 frames as keepalive ── 67 + frame_skip = frame_skip + 1 68 + if (dirty .or. frame_skip >= 8) then 69 + frame_skip = 0 70 + call render_clear(state%term_w, state%term_h) 71 + call render_ruler(state%term_w, state%term_h) 72 + call render_header(state%term_w, state%term_h, state%graph_w + 1) 73 + call render_graph_pane(graph, state, state%graph_w, state%term_h, 2) 74 + call render_inspect_pane(graph, state, state%graph_w + 2, state%term_w, state%term_h) 75 + call render_status(state, graph, state%term_w, state%term_h) 76 + call render_flush(state%term_w, state%term_h) 77 + end if 65 78 66 - ! ── Frame sleep 200ms ── 67 - call usleep_f(200000) 79 + ! ── Frame sleep: faster while settling, slower when idle ── 80 + if (alpha > 0.0d0) then 81 + call usleep_f(100000) ! 100ms while sim running (~10fps) 82 + else 83 + call usleep_f(50000) ! 50ms when idle — responsive to keys 84 + end if 68 85 end do 69 86 70 87 contains
+86 -64
src/render.f90
··· 83 83 end subroutine 84 84 85 85 ! Draw a node sigil at position 86 - subroutine render_node(cx, cy, sigil, color, selected, w, h) 86 + subroutine render_node(cx, cy, lbl, color, selected, w, h) 87 87 integer, intent(in) :: cx, cy, w, h 88 - character(len=3), intent(in) :: sigil 89 - character(len=*), intent(in) :: color 88 + character(len=*), intent(in) :: lbl, color 90 89 logical, intent(in) :: selected 91 - character(len=5) :: label 92 - integer :: i 93 - 90 + integer :: i, llen, start_col 91 + character(len=12) :: display 92 + llen = min(len_trim(lbl), 10) ! cap at 10 chars 94 93 if (selected) then 95 - label = '['//sigil//']' 94 + display = '['//lbl(1:llen)//']' 95 + llen = llen + 2 96 96 else 97 - label = ' '//sigil//' ' 97 + display = lbl(1:llen) 98 98 end if 99 - 100 - do i = 1, 5 101 - call render_set(cx - 2 + i, cy, label(i:i), color, selected, w, h) 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) 102 103 end do 103 104 end subroutine 104 105 ··· 114 115 end do 115 116 end subroutine 116 117 117 - ! Flush frame to terminal — clear screen then write only non-space cells 118 + ! Flush frame — build entire output as one buffer, write atomically (no flicker) 118 119 subroutine render_flush(w, h) 119 - use fortrat_tui, only: fortrat_write_at, fortrat_flush, fortrat_clear_screen 120 + use fortrat_tui, only: fortrat_write_buf, fortrat_flush 120 121 use iso_c_binding 121 122 integer, intent(in) :: w, h 122 - integer :: c, r, clen 123 - character(len=512) :: cell_str 124 - integer(c_int) :: ci, ri 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 125 135 126 - ! Clear screen once per frame 127 - call fortrat_clear_screen() 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 128 139 129 - ! Write only non-space cells individually — each as ESC[row;colH + color + char 140 + ! Write all cells — including spaces — so previous frame is fully overwritten 130 141 do r = 1, h 131 - do c = 1, w 132 - if (cur_frame(c,r)%ch == ' ' .and. len_trim(cur_frame(c,r)%color) == 0) cycle 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) 133 146 134 - ! Build: color + bold + char 135 - clen = 0 147 + do c = 1, w 148 + ! Color 136 149 if (len_trim(cur_frame(c,r)%color) > 0) then 137 - block 138 - integer :: cl 139 - cl = len_trim(cur_frame(c,r)%color) 140 - cell_str(1:cl) = cur_frame(c,r)%color(1:cl) 141 - clen = cl 142 - end block 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) 143 154 end if 155 + ! Bold 144 156 if (cur_frame(c,r)%bold) then 145 - cell_str(clen+1:clen+4) = char(27)//'[1m' 146 - clen = clen + 4 157 + call fbuf_append(fbuf, fpos, char(27)//'[1m', 4) 147 158 end if 148 - cell_str(clen+1:clen+1) = cur_frame(c,r)%ch 149 - clen = clen + 1 150 - ! Append reset 151 - cell_str(clen+1:clen+4) = char(27)//'[0m' 152 - clen = clen + 4 153 - 154 - ri = int(r, c_int) 155 - ci = int(c, c_int) 156 - call fortrat_write_at(ri, ci, cell_str, int(clen, c_int)) 159 + ! Character 160 + call fbuf_append(fbuf, fpos, cur_frame(c,r)%ch, 1) 157 161 prv_frame(c,r) = cur_frame(c,r) 162 + if (fpos > fbuf_size - 64) exit 158 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 159 167 end do 160 168 169 + ! Single write call — atomic, no flicker 170 + call fortrat_write_buf(fbuf, int(fpos, c_int)) 161 171 call fortrat_flush() 172 + deallocate(fbuf) 162 173 first_frame = .false. 163 174 end subroutine 164 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 + 165 188 ! ── Main graph pane render ── 166 189 subroutine render_graph_pane(graph, state, w, h, row_off) 167 190 type(lex_graph_t), intent(in) :: graph 168 191 type(app_state_t), intent(in) :: state 169 192 integer, intent(in) :: w, h, row_off 170 193 integer :: i, cx, cy, vis_idx 171 - character(len=3) :: sigil 172 194 character(len=16) :: col 173 195 logical :: selected, dimmed 174 196 integer :: visible_nodes(MAX_NODES), n_vis 175 - 176 197 ! Build visible node index list 177 198 n_vis = 0 178 199 do i = 1, graph%n_nodes ··· 203 224 vis_idx = vis_idx + 1 204 225 cx = nint(graph%nodes(i)%x) 205 226 cy = nint(graph%nodes(i)%y) + row_off 206 - sigil = KIND_SIGIL(graph%nodes(i)%kind)(1:3) 207 227 col = ns_color(graph%nodes(i)%ns_group) 208 228 selected = (vis_idx == state%cursor_idx .or. i == state%selected_idx) 209 229 dimmed = len_trim(state%search_query) > 0 .and. & 210 230 index(graph%nodes(i)%id, trim(state%search_query)) == 0 211 231 if (dimmed) col = GREEN_DIM 212 - call render_node(cx, cy, sigil, col, selected, w, h) 232 + call render_node(cx, cy, trim(graph%nodes(i)%label), col, selected, w, h) 213 233 end do 214 234 end subroutine 215 235 ··· 218 238 type(lex_graph_t), intent(in) :: graph 219 239 type(app_state_t), intent(in) :: state 220 240 integer, intent(in) :: col_off, w, h 221 - integer :: row, i, idx, vis_idx, n_out 241 + integer :: row, i, idx, vis_idx, n_out, pane_w 222 242 character(len=ID_LEN) :: out_ids(64) 223 243 character(len=3) :: sigil 224 - character(len=w) :: divider 225 244 226 - row = 2 ! start below pane header 245 + pane_w = w - col_off 246 + row = 2 227 247 228 - ! Find actual node index from cursor 229 248 idx = 0 230 249 vis_idx = 0 231 250 do i = 1, graph%n_nodes ··· 233 252 vis_idx = vis_idx + 1 234 253 if (vis_idx == state%cursor_idx) then; idx = i; exit; end if 235 254 end do 236 - 237 255 if (state%selected_idx > 0) idx = state%selected_idx 238 256 239 257 if (idx == 0) then ··· 250 268 sigil = KIND_SIGIL(graph%nodes(idx)%kind)(1:3) 251 269 call render_text(col_off, row, 'SUBROUTINE INSPECT('//sigil//')', GREEN_BR, .true., w, h) 252 270 row = row + 1 253 - 254 - divider = repeat('-', w - col_off) 255 - call render_text(col_off, row, divider, GREEN_DIM, .false., w, h) 271 + call render_text(col_off, row, repeat('-', pane_w), GREEN_DIM, .false., w, h) 256 272 row = row + 1 257 - 258 273 call render_text(col_off, row, trim(graph%nodes(idx)%id), GREEN_BR, .true., w, h) 259 274 row = row + 2 260 275 ··· 270 285 end if 271 286 row = row + 1 272 287 273 - ! Description 274 - if (len_trim(graph%nodes(idx)%doc) > 0) then 288 + ! Description with word-wrap 289 + if (len_trim(graph%nodes(idx)%doc) > 0 .and. row < h - 3) then 275 290 block 276 - integer :: doc_len, max_len 277 - doc_len = len_trim(graph%nodes(idx)%doc) 278 - max_len = min(doc_len, w - col_off - 6) 279 - call render_text(col_off, row, & 280 - 'C '//graph%nodes(idx)%doc(1:max_len), & 281 - GREEN_DIM, .false., w, h) 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 282 306 end block 283 - row = row + 2 284 307 end if 285 308 286 309 ! Fields 287 - if (graph%nodes(idx)%n_fields > 0) then 310 + if (graph%nodes(idx)%n_fields > 0 .and. row < h - 3) then 288 311 call render_text(col_off, row, 'C FIELDS', GREEN_DIM, .false., w, h) 289 312 row = row + 1 290 313 do i = 1, min(graph%nodes(idx)%n_fields, 8) ··· 319 342 end if 320 343 end if 321 344 322 - ! Footer 323 345 call render_text(col_off, h-1, 'END SUBROUTINE INSPECT', GREEN_DIM, .false., w, h) 324 346 end subroutine 325 347
+4 -3
src/simulate.f90
··· 12 12 real(real64), parameter :: ALPHA_DECAY = 0.02d0 13 13 real(real64), parameter :: VELOCITY_DEC = 0.6d0 ! velocity decay per tick 14 14 15 - real(real64), save :: alpha = 1.0d0 15 + real(real64), save, public :: alpha = 1.0d0 16 16 17 17 contains 18 18 ··· 134 134 135 135 ! ── Cool down ── 136 136 alpha = alpha * (1.0d0 - ALPHA_DECAY) 137 - if (alpha < 0.001d0) alpha = 0.001d0 137 + if (alpha < 0.0005d0) alpha = 0.0d0 ! fully settled 138 138 end subroutine 139 139 140 140 ! Pre-warm: run N ticks with high alpha to settle initial layout ··· 146 146 do i = 1, n_ticks 147 147 call sim_tick(graph, w, h) 148 148 end do 149 - alpha = 0.3d0 ! keep some heat for live animation 149 + ! Leave a little heat so it finishes settling live, but not too much 150 + if (alpha > 0.05d0) alpha = 0.05d0 150 151 end subroutine 151 152 152 153 end module fortrat_simulate
+6
src/tui.f90
··· 22 22 integer(c_int), intent(out) :: cols, rows 23 23 end subroutine 24 24 25 + subroutine fortrat_write_buf(buf, len) bind(c, name='fortrat_write_buf') 26 + import c_int, c_char 27 + integer(c_int), value :: len 28 + character(kind=c_char), intent(in):: buf(len) 29 + end subroutine 30 + 25 31 subroutine fortrat_clear_screen() bind(c, name='fortrat_clear_screen') 26 32 end subroutine 27 33
+5
src/tui_helper.c
··· 56 56 xwrite(STDOUT_FILENO, str, len); 57 57 } 58 58 59 + /* Write an entire pre-built frame buffer in one shot */ 60 + void fortrat_write_buf(const char *buf, int len) { 61 + xwrite(STDOUT_FILENO, buf, len); 62 + } 63 + 59 64 void fortrat_flush(void) { 60 65 fsync(STDOUT_FILENO); 61 66 }