| 1 | program fortty |
| 2 | use window_mod |
| 3 | use selection_mod, only: selection_contains |
| 4 | use gl_bindings |
| 5 | use renderer_mod |
| 6 | use font_mod, only: font_find_monospace, font_find_for_codepoint |
| 7 | use pty_mod |
| 8 | use terminal_mod |
| 9 | use parser_mod |
| 10 | use screen_mod |
| 11 | use cell_mod, only: cell_t, set_palette_color, set_default_colors |
| 12 | use config_mod |
| 13 | use cursor_mod, only: CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR |
| 14 | use glfw_bindings, only: glfwGetTime |
| 15 | implicit none |
| 16 | |
| 17 | type(window_t) :: win |
| 18 | type(renderer_t), target :: ren |
| 19 | type(pty_t) :: pty |
| 20 | type(terminal_t) :: term |
| 21 | type(parser_t) :: parser |
| 22 | type(screen_t), pointer :: scr |
| 23 | type(cell_t) :: cell |
| 24 | type(config_t) :: cfg |
| 25 | integer :: win_width, win_height |
| 26 | integer :: prev_width, prev_height |
| 27 | integer :: term_rows, term_cols |
| 28 | integer :: new_rows, new_cols |
| 29 | character(len=256) :: font_path, fallback_path |
| 30 | character(len=4096) :: pty_buffer |
| 31 | character(len=256) :: response_buf |
| 32 | integer :: nbytes, i, row, col, scroll_offset, sb_offset, screen_row |
| 33 | integer :: response_len |
| 34 | real :: x, y, r, g, b, bg_r, bg_g, bg_b |
| 35 | type(cell_t), allocatable :: sb_line(:) |
| 36 | integer :: cell_width, cell_height ! From font metrics |
| 37 | real(8) :: current_time, last_time, blink_timer |
| 38 | logical :: cursor_blink_visible |
| 39 | type(selection_t) :: sel |
| 40 | |
| 41 | ! Load configuration (uses defaults if no config file found) |
| 42 | cfg = config_load('') |
| 43 | |
| 44 | ! Apply color palette from config |
| 45 | call set_default_colors(cfg%fg_color, cfg%bg_color) |
| 46 | do i = 0, 15 |
| 47 | call set_palette_color(i, cfg%palette(i)) |
| 48 | end do |
| 49 | |
| 50 | ! Window dimensions from config |
| 51 | win_width = cfg%window_width |
| 52 | win_height = cfg%window_height |
| 53 | |
| 54 | ! Create window with OpenGL context |
| 55 | win = window_create(win_width, win_height, "fortty") |
| 56 | |
| 57 | ! Font path - use config if specified, otherwise fontconfig, then fallbacks |
| 58 | if (len_trim(cfg%font_path) > 0) then |
| 59 | font_path = cfg%font_path |
| 60 | print *, "Using configured font: ", trim(font_path) |
| 61 | else |
| 62 | font_path = font_find_monospace() |
| 63 | if (len_trim(font_path) == 0) then |
| 64 | ! Fontconfig not available or failed - try common system locations |
| 65 | font_path = "/usr/share/fonts/TTF/DejaVuSansMono.ttf" |
| 66 | else |
| 67 | print *, "Using system monospace font: ", trim(font_path) |
| 68 | end if |
| 69 | end if |
| 70 | |
| 71 | ! Create renderer with font (using font size from config) |
| 72 | ren = renderer_create(trim(font_path), cfg%font_size) |
| 73 | if (.not. ren%initialized) then |
| 74 | print *, "Warning: Could not load font, trying alternate path..." |
| 75 | font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" |
| 76 | ren = renderer_create(trim(font_path), cfg%font_size) |
| 77 | end if |
| 78 | |
| 79 | if (.not. ren%initialized) then |
| 80 | print *, "Error: Could not initialize renderer" |
| 81 | print *, "Please ensure a monospace font is installed" |
| 82 | call window_destroy(win) |
| 83 | stop 1 |
| 84 | end if |
| 85 | |
| 86 | ! Fix dangling pointer: atlas%font pointed to local var in renderer_create |
| 87 | ! After assignment, we need to update it to point to ren%font |
| 88 | ren%atlas%font => ren%font |
| 89 | |
| 90 | ! Load fallback font for missing glyphs (icons, symbols, etc.) |
| 91 | ! Use config fallback if specified, otherwise auto-detect via fontconfig |
| 92 | if (len_trim(cfg%font_fallback) > 0) then |
| 93 | call renderer_load_fallback_font(ren, trim(cfg%font_fallback)) |
| 94 | if (ren%font%has_fallback) then |
| 95 | print *, "Using configured fallback font: ", trim(cfg%font_fallback) |
| 96 | end if |
| 97 | end if |
| 98 | |
| 99 | ! If no configured fallback or it failed, try fontconfig auto-detection |
| 100 | ! First try to find a font with common Unicode symbols (chevron, arrows, etc.) |
| 101 | if (.not. ren%font%has_fallback) then |
| 102 | fallback_path = font_find_for_codepoint(int(z'276F')) ! Heavy right-pointing angle (❯) |
| 103 | if (len_trim(fallback_path) > 0) then |
| 104 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 105 | end if |
| 106 | end if |
| 107 | ! Then try Nerd Font-specific devicons |
| 108 | if (.not. ren%font%has_fallback) then |
| 109 | fallback_path = font_find_for_codepoint(int(z'E5FF')) ! Nerd Font devicon |
| 110 | if (len_trim(fallback_path) > 0) then |
| 111 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 112 | end if |
| 113 | end if |
| 114 | if (.not. ren%font%has_fallback) then |
| 115 | fallback_path = font_find_for_codepoint(int(z'E0A0')) ! Powerline branch symbol |
| 116 | if (len_trim(fallback_path) > 0) then |
| 117 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 118 | end if |
| 119 | end if |
| 120 | if (.not. ren%font%has_fallback) then |
| 121 | fallback_path = font_find_for_codepoint(int(z'F07B')) ! folder icon (Font Awesome) |
| 122 | if (len_trim(fallback_path) > 0) then |
| 123 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 124 | end if |
| 125 | end if |
| 126 | |
| 127 | ! If fontconfig didn't work, try hardcoded paths (macOS) |
| 128 | if (.not. ren%font%has_fallback) then |
| 129 | fallback_path = "/System/Library/Fonts/Apple Symbols.ttf" |
| 130 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 131 | end if |
| 132 | if (.not. ren%font%has_fallback) then |
| 133 | fallback_path = "/Library/Fonts/MesloLGLDZNerdFontMono-Regular.ttf" |
| 134 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 135 | end if |
| 136 | if (.not. ren%font%has_fallback) then |
| 137 | fallback_path = "/Library/Fonts/MesloLGMNerdFontMono-Regular.ttf" |
| 138 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 139 | end if |
| 140 | ! Linux paths |
| 141 | if (.not. ren%font%has_fallback) then |
| 142 | fallback_path = "/usr/share/fonts/TTF/MesloLGLDZNerdFontMono-Regular.ttf" |
| 143 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 144 | end if |
| 145 | if (.not. ren%font%has_fallback) then |
| 146 | fallback_path = "/usr/share/fonts/TTF/MesloLGMNerdFontMono-Regular.ttf" |
| 147 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 148 | end if |
| 149 | ! Fall back to Noto Sans Symbols if no Nerd Font |
| 150 | if (.not. ren%font%has_fallback) then |
| 151 | fallback_path = "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf" |
| 152 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 153 | end if |
| 154 | if (.not. ren%font%has_fallback) then |
| 155 | fallback_path = "/usr/share/fonts/TTF/NotoSansSymbols2-Regular.ttf" |
| 156 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 157 | end if |
| 158 | |
| 159 | if (ren%font%has_fallback) then |
| 160 | print *, "Fallback font loaded: ", trim(fallback_path) |
| 161 | else |
| 162 | print *, "Warning: No fallback font loaded - some icons may not display" |
| 163 | end if |
| 164 | |
| 165 | ! Set up projection matrix |
| 166 | call renderer_set_projection(ren, win_width, win_height) |
| 167 | |
| 168 | ! Get cell dimensions from font metrics |
| 169 | cell_width = ren%font%cell_width |
| 170 | cell_height = ren%font%cell_height |
| 171 | if (cell_width < 1) cell_width = 10 ! Fallback |
| 172 | if (cell_height < 1) cell_height = 18 ! Fallback |
| 173 | print *, "Font cell size:", cell_width, "x", cell_height |
| 174 | |
| 175 | ! Share cell dimensions with window module for mouse selection |
| 176 | call window_set_cell_size(cell_width, cell_height) |
| 177 | |
| 178 | ! Calculate terminal dimensions based on font metrics |
| 179 | term_cols = win_width / cell_width |
| 180 | term_rows = win_height / cell_height |
| 181 | prev_width = win_width |
| 182 | prev_height = win_height |
| 183 | |
| 184 | ! Initialize terminal state and parser |
| 185 | call terminal_init(term, term_rows, term_cols) |
| 186 | call parser_init(parser) |
| 187 | |
| 188 | ! Open PTY with shell |
| 189 | pty = pty_open("", term_rows, term_cols) ! Empty string = use $SHELL |
| 190 | |
| 191 | if (.not. pty%active) then |
| 192 | print *, "Error: Could not open PTY" |
| 193 | call terminal_destroy(term) |
| 194 | call renderer_destroy(ren) |
| 195 | call window_destroy(win) |
| 196 | stop 1 |
| 197 | end if |
| 198 | |
| 199 | ! Connect PTY and terminal to window for keyboard input and scrollback |
| 200 | call window_set_pty(pty) |
| 201 | call window_set_terminal(term) |
| 202 | |
| 203 | ! Initialize blink timer |
| 204 | last_time = glfwGetTime() |
| 205 | blink_timer = 0.0d0 |
| 206 | cursor_blink_visible = .true. |
| 207 | |
| 208 | ! Main event loop |
| 209 | do while (.not. window_should_close(win) .and. pty_is_alive(pty)) |
| 210 | ! Update blink timer |
| 211 | current_time = glfwGetTime() |
| 212 | blink_timer = blink_timer + (current_time - last_time) |
| 213 | last_time = current_time |
| 214 | if (blink_timer > 0.5d0) then |
| 215 | cursor_blink_visible = .not. cursor_blink_visible |
| 216 | blink_timer = 0.0d0 |
| 217 | end if |
| 218 | |
| 219 | ! Check for window resize |
| 220 | call window_get_size(win, win_width, win_height) |
| 221 | if (win_width /= prev_width .or. win_height /= prev_height) then |
| 222 | prev_width = win_width |
| 223 | prev_height = win_height |
| 224 | |
| 225 | ! Update projection matrix |
| 226 | call renderer_set_projection(ren, win_width, win_height) |
| 227 | |
| 228 | ! Calculate new terminal size and notify PTY and terminal |
| 229 | new_cols = win_width / cell_width |
| 230 | new_rows = win_height / cell_height |
| 231 | if (new_cols /= term_cols .or. new_rows /= term_rows) then |
| 232 | term_cols = new_cols |
| 233 | term_rows = new_rows |
| 234 | call pty_resize(pty, term_rows, term_cols) |
| 235 | call terminal_resize(term, term_rows, term_cols) |
| 236 | end if |
| 237 | end if |
| 238 | |
| 239 | ! Read from PTY (non-blocking) |
| 240 | nbytes = pty_read(pty, pty_buffer, 4096) |
| 241 | if (nbytes > 0) then |
| 242 | ! Process each byte through escape sequence parser |
| 243 | do i = 1, nbytes |
| 244 | call parser_process_byte(parser, term, ichar(pty_buffer(i:i))) |
| 245 | end do |
| 246 | end if |
| 247 | |
| 248 | ! Check for terminal responses (DA1, DSR, etc.) and send to PTY |
| 249 | if (terminal_has_response(term)) then |
| 250 | call terminal_get_response(term, response_buf, response_len) |
| 251 | if (response_len > 0) then |
| 252 | call pty_write(pty, response_buf, response_len) |
| 253 | end if |
| 254 | end if |
| 255 | |
| 256 | ! Check for window title changes (from OSC 0/1/2) |
| 257 | if (terminal_has_title_changed(term)) then |
| 258 | call window_set_title(win, terminal_get_title(term)) |
| 259 | end if |
| 260 | |
| 261 | ! Clear screen with background color from config |
| 262 | bg_r = real(cfg%bg_color%r) / 255.0 |
| 263 | bg_g = real(cfg%bg_color%g) / 255.0 |
| 264 | bg_b = real(cfg%bg_color%b) / 255.0 |
| 265 | call glClearColor(bg_r, bg_g, bg_b, 1.0) |
| 266 | call glClear(GL_COLOR_BUFFER_BIT) |
| 267 | |
| 268 | ! Render terminal buffer |
| 269 | call renderer_begin(ren) |
| 270 | |
| 271 | scr => terminal_active_screen(term) |
| 272 | scroll_offset = terminal_get_scroll_offset(term) |
| 273 | |
| 274 | ! Get current selection for highlighting |
| 275 | sel = window_get_selection() |
| 276 | |
| 277 | ! Allocate/resize scrollback line buffer if needed |
| 278 | if (.not. allocated(sb_line)) then |
| 279 | allocate(sb_line(term_cols)) |
| 280 | else if (size(sb_line) /= term_cols) then |
| 281 | deallocate(sb_line) |
| 282 | allocate(sb_line(term_cols)) |
| 283 | end if |
| 284 | |
| 285 | do row = 1, scr%rows |
| 286 | ! Cell top-left y coordinate (for rectangles like selection/cursor) |
| 287 | ! Row 1 starts at y=0, row 2 at y=cell_height, etc. |
| 288 | y = real(row - 1) * cell_height |
| 289 | |
| 290 | ! Determine if this row shows scrollback or screen content |
| 291 | sb_offset = scroll_offset - row + 1 |
| 292 | |
| 293 | if (sb_offset > 0 .and. sb_offset <= terminal_get_scrollback_count(term)) then |
| 294 | ! This row shows scrollback content |
| 295 | call terminal_get_scrollback_line(term, sb_offset - 1, sb_line, term_cols) |
| 296 | do col = 1, min(term_cols, scr%cols) |
| 297 | cell = sb_line(col) |
| 298 | |
| 299 | ! Skip continuation cells (2nd half of wide chars) |
| 300 | if (cell%is_continuation) cycle |
| 301 | |
| 302 | x = real(col - 1) * cell_width |
| 303 | |
| 304 | ! Draw selection background if selected (for all cells including spaces) |
| 305 | if (selection_contains(sel, row, col)) then |
| 306 | call renderer_draw_rect(ren, x, y, real(cell_width), real(cell_height), & |
| 307 | 0.3, 0.3, 0.6, 1.0) |
| 308 | end if |
| 309 | |
| 310 | ! Only render cells with actual text content |
| 311 | if (cell%codepoint /= 32 .and. cell%codepoint /= 0) then |
| 312 | r = real(cell%fg%r) / 255.0 |
| 313 | g = real(cell%fg%g) / 255.0 |
| 314 | b = real(cell%fg%b) / 255.0 |
| 315 | ! Text baseline is at cell bottom; add cell_height to position correctly |
| 316 | call renderer_draw_char(ren, x, y + real(cell_height), cell%codepoint, r, g, b, 1.0) |
| 317 | end if |
| 318 | end do |
| 319 | else |
| 320 | ! This row shows screen content |
| 321 | screen_row = row - scroll_offset |
| 322 | if (screen_row >= 1 .and. screen_row <= scr%rows) then |
| 323 | do col = 1, scr%cols |
| 324 | cell = screen_get_cell(scr, screen_row, col) |
| 325 | |
| 326 | ! Skip continuation cells (2nd half of wide chars) |
| 327 | if (cell%is_continuation) cycle |
| 328 | |
| 329 | x = real(col - 1) * cell_width |
| 330 | |
| 331 | ! Draw selection background if selected (for all cells including spaces) |
| 332 | if (selection_contains(sel, row, col)) then |
| 333 | call renderer_draw_rect(ren, x, y, real(cell_width), real(cell_height), & |
| 334 | 0.3, 0.3, 0.6, 1.0) |
| 335 | end if |
| 336 | |
| 337 | ! Only render cells with actual text content |
| 338 | if (cell%codepoint /= 32 .and. cell%codepoint /= 0) then |
| 339 | r = real(cell%fg%r) / 255.0 |
| 340 | g = real(cell%fg%g) / 255.0 |
| 341 | b = real(cell%fg%b) / 255.0 |
| 342 | ! Text baseline is at cell bottom; add cell_height to position correctly |
| 343 | call renderer_draw_char(ren, x, y + real(cell_height), cell%codepoint, r, g, b, 1.0) |
| 344 | end if |
| 345 | end do |
| 346 | end if |
| 347 | end if |
| 348 | end do |
| 349 | |
| 350 | ! Draw cursor if visible (only when not scrolled back) |
| 351 | if (term%cursor%visible .and. scroll_offset == 0) then |
| 352 | ! Check blink state - only hide cursor if blink is enabled and in off phase |
| 353 | if (.not. term%cursor%blink .or. cursor_blink_visible) then |
| 354 | x = real(term%cursor%col - 1) * cell_width |
| 355 | y = real(term%cursor%row - 1) * cell_height |
| 356 | |
| 357 | select case (term%cursor%style) |
| 358 | case (CURSOR_BLOCK) |
| 359 | ! Filled block cursor |
| 360 | call renderer_draw_rect(ren, x, y, real(cell_width), real(cell_height), & |
| 361 | 0.7, 0.7, 0.7, 0.8) |
| 362 | case (CURSOR_UNDERLINE) |
| 363 | ! Underline at bottom of cell |
| 364 | call renderer_draw_rect(ren, x, y + real(cell_height) - 2.0, & |
| 365 | real(cell_width), 2.0, 0.7, 0.7, 0.7, 1.0) |
| 366 | case (CURSOR_BAR) |
| 367 | ! Vertical bar at left of cell |
| 368 | call renderer_draw_rect(ren, x, y, 2.0, real(cell_height), 0.7, 0.7, 0.7, 1.0) |
| 369 | case default |
| 370 | ! Fallback to block |
| 371 | call renderer_draw_rect(ren, x, y, real(cell_width), real(cell_height), & |
| 372 | 0.7, 0.7, 0.7, 0.8) |
| 373 | end select |
| 374 | end if |
| 375 | end if |
| 376 | |
| 377 | call renderer_flush(ren) |
| 378 | |
| 379 | ! Swap buffers and poll events |
| 380 | call window_swap_buffers(win) |
| 381 | call window_poll_events() |
| 382 | end do |
| 383 | |
| 384 | ! Cleanup |
| 385 | if (allocated(sb_line)) deallocate(sb_line) |
| 386 | call pty_close(pty) |
| 387 | call terminal_destroy(term) |
| 388 | call renderer_destroy(ren) |
| 389 | call window_destroy(win) |
| 390 | |
| 391 | end program fortty |
| 392 |