| 1 | program fortty |
| 2 | use window_mod |
| 3 | use gl_bindings |
| 4 | use renderer_mod |
| 5 | use font_mod, only: font_find_monospace, font_find_for_codepoint |
| 6 | use pty_mod |
| 7 | use terminal_mod |
| 8 | use parser_mod |
| 9 | use screen_mod |
| 10 | use cell_mod, only: set_palette_color, set_default_colors |
| 11 | use config_mod |
| 12 | use glfw_bindings, only: glfwGetTime |
| 13 | use tab_manager_mod |
| 14 | use tab_bar_mod |
| 15 | use pane_mod |
| 16 | use layout_mod, only: DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN |
| 17 | use render_state_mod, only: render_state_init, render_state_update_blink, do_render |
| 18 | implicit none |
| 19 | |
| 20 | type(window_t) :: win |
| 21 | type(renderer_t), target :: ren |
| 22 | type(tab_manager_t), target :: tab_mgr |
| 23 | type(pane_t), pointer :: active_pane |
| 24 | type(terminal_t), pointer :: term |
| 25 | type(pty_t), pointer :: active_pty |
| 26 | type(config_t) :: cfg |
| 27 | integer :: win_width, win_height |
| 28 | integer :: prev_width, prev_height |
| 29 | integer :: term_rows, term_cols |
| 30 | integer :: new_rows, new_cols |
| 31 | character(len=256) :: font_path, fallback_path |
| 32 | character(len=4096) :: pty_buffer |
| 33 | character(len=256) :: response_buf |
| 34 | integer :: nbytes, i, j, k |
| 35 | integer :: response_len, tab_action, pane_action, tab_bar_height |
| 36 | integer :: cell_width, cell_height, ascender ! From font metrics |
| 37 | real(8) :: current_time, last_time, blink_timer |
| 38 | logical :: cursor_blink_visible, any_pty_alive |
| 39 | logical :: was_focused, is_focused |
| 40 | integer :: font_delta, new_font_size, base_font_size |
| 41 | character(len=256) :: font_path_saved, fallback_path_saved |
| 42 | |
| 43 | ! Load configuration (uses defaults if no config file found) |
| 44 | cfg = config_load('') |
| 45 | |
| 46 | ! Apply color palette from config |
| 47 | call set_default_colors(cfg%fg_color, cfg%bg_color) |
| 48 | do i = 0, 15 |
| 49 | call set_palette_color(i, cfg%palette(i)) |
| 50 | end do |
| 51 | |
| 52 | ! Window dimensions from config |
| 53 | win_width = cfg%window_width |
| 54 | win_height = cfg%window_height |
| 55 | |
| 56 | ! Create window with OpenGL context (enable transparency if opacity < 1.0) |
| 57 | win = window_create(win_width, win_height, "fortty", cfg%window_opacity < 1.0) |
| 58 | |
| 59 | ! Enable background blur if configured (macOS only) |
| 60 | if (cfg%window_blur) then |
| 61 | call window_set_blur(win, .true.) |
| 62 | end if |
| 63 | |
| 64 | ! Font path - use config if specified, otherwise fontconfig, then fallbacks |
| 65 | if (len_trim(cfg%font_path) > 0) then |
| 66 | font_path = cfg%font_path |
| 67 | print *, "Using configured font: ", trim(font_path) |
| 68 | else |
| 69 | font_path = font_find_monospace() |
| 70 | if (len_trim(font_path) > 0) then |
| 71 | print *, "Using system monospace font: ", trim(font_path) |
| 72 | end if |
| 73 | end if |
| 74 | |
| 75 | ! Create renderer with font (using font size from config) |
| 76 | ! Try configured/fontconfig path first, then fallback paths for various distros |
| 77 | if (len_trim(font_path) > 0) then |
| 78 | ren = renderer_create(trim(font_path), cfg%font_size) |
| 79 | end if |
| 80 | |
| 81 | if (.not. ren%initialized) then |
| 82 | call try_font_fallbacks(ren, cfg%font_size, font_path) |
| 83 | end if |
| 84 | |
| 85 | if (.not. ren%initialized) then |
| 86 | print *, "Error: Could not initialize renderer" |
| 87 | print *, "Please ensure a monospace font is installed, or set font_path in config" |
| 88 | print *, "Config location: ~/.config/fortty/fortty.toml" |
| 89 | call window_destroy(win) |
| 90 | stop 1 |
| 91 | end if |
| 92 | |
| 93 | ! Fix dangling pointer: atlas%font pointed to local var in renderer_create |
| 94 | ! After assignment, we need to update it to point to ren%font |
| 95 | ren%atlas%font => ren%font |
| 96 | |
| 97 | ! Save font path and base size for runtime font size changes |
| 98 | font_path_saved = font_path |
| 99 | base_font_size = cfg%font_size |
| 100 | fallback_path_saved = '' ! Will be set when fallback is loaded |
| 101 | |
| 102 | ! Load fallback font for missing glyphs (icons, symbols, etc.) |
| 103 | ! Use config fallback if specified, otherwise auto-detect via fontconfig |
| 104 | if (len_trim(cfg%font_fallback) > 0) then |
| 105 | call renderer_load_fallback_font(ren, trim(cfg%font_fallback)) |
| 106 | if (ren%font%has_fallback) then |
| 107 | print *, "Using configured fallback font: ", trim(cfg%font_fallback) |
| 108 | end if |
| 109 | end if |
| 110 | |
| 111 | ! If no configured fallback or it failed, try fontconfig auto-detection |
| 112 | ! First try to find a font with common Unicode symbols (chevron, arrows, etc.) |
| 113 | if (.not. ren%font%has_fallback) then |
| 114 | fallback_path = font_find_for_codepoint(int(z'276F')) ! Heavy right-pointing angle (❯) |
| 115 | if (len_trim(fallback_path) > 0) then |
| 116 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 117 | end if |
| 118 | end if |
| 119 | ! Then try Nerd Font-specific devicons |
| 120 | if (.not. ren%font%has_fallback) then |
| 121 | fallback_path = font_find_for_codepoint(int(z'E5FF')) ! Nerd Font devicon |
| 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 | if (.not. ren%font%has_fallback) then |
| 127 | fallback_path = font_find_for_codepoint(int(z'E0A0')) ! Powerline branch symbol |
| 128 | if (len_trim(fallback_path) > 0) then |
| 129 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 130 | end if |
| 131 | end if |
| 132 | if (.not. ren%font%has_fallback) then |
| 133 | fallback_path = font_find_for_codepoint(int(z'F07B')) ! folder icon (Font Awesome) |
| 134 | if (len_trim(fallback_path) > 0) then |
| 135 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 136 | end if |
| 137 | end if |
| 138 | |
| 139 | ! If fontconfig didn't work, try hardcoded paths (macOS) |
| 140 | if (.not. ren%font%has_fallback) then |
| 141 | fallback_path = "/System/Library/Fonts/Apple Symbols.ttf" |
| 142 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 143 | end if |
| 144 | if (.not. ren%font%has_fallback) then |
| 145 | fallback_path = "/Library/Fonts/MesloLGLDZNerdFontMono-Regular.ttf" |
| 146 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 147 | end if |
| 148 | if (.not. ren%font%has_fallback) then |
| 149 | fallback_path = "/Library/Fonts/MesloLGMNerdFontMono-Regular.ttf" |
| 150 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 151 | end if |
| 152 | ! Linux paths |
| 153 | if (.not. ren%font%has_fallback) then |
| 154 | fallback_path = "/usr/share/fonts/TTF/MesloLGLDZNerdFontMono-Regular.ttf" |
| 155 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 156 | end if |
| 157 | if (.not. ren%font%has_fallback) then |
| 158 | fallback_path = "/usr/share/fonts/TTF/MesloLGMNerdFontMono-Regular.ttf" |
| 159 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 160 | end if |
| 161 | ! Fall back to Noto Sans Symbols if no Nerd Font |
| 162 | if (.not. ren%font%has_fallback) then |
| 163 | fallback_path = "/usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf" |
| 164 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 165 | end if |
| 166 | if (.not. ren%font%has_fallback) then |
| 167 | fallback_path = "/usr/share/fonts/TTF/NotoSansSymbols2-Regular.ttf" |
| 168 | call renderer_load_fallback_font(ren, trim(fallback_path)) |
| 169 | end if |
| 170 | |
| 171 | if (ren%font%has_fallback) then |
| 172 | ! Save the successful fallback path for font size changes |
| 173 | if (len_trim(cfg%font_fallback) > 0) then |
| 174 | fallback_path_saved = cfg%font_fallback |
| 175 | else |
| 176 | fallback_path_saved = fallback_path |
| 177 | end if |
| 178 | print *, "Fallback font loaded: ", trim(fallback_path_saved) |
| 179 | else |
| 180 | print *, "Warning: No fallback font loaded - some icons may not display" |
| 181 | end if |
| 182 | |
| 183 | ! Set up projection matrix |
| 184 | call renderer_set_projection(ren, win_width, win_height) |
| 185 | |
| 186 | ! Get cell dimensions from font metrics |
| 187 | cell_width = ren%font%cell_width |
| 188 | cell_height = ren%font%cell_height |
| 189 | ascender = ren%font%ascender |
| 190 | if (cell_width < 1) cell_width = 10 ! Fallback |
| 191 | if (cell_height < 1) cell_height = 18 ! Fallback |
| 192 | if (ascender < 1) ascender = cell_height - 4 ! Fallback estimate |
| 193 | print *, "Font cell size:", cell_width, "x", cell_height, " ascender:", ascender |
| 194 | |
| 195 | ! Share cell dimensions with window module for mouse selection |
| 196 | call window_set_cell_size(cell_width, cell_height) |
| 197 | |
| 198 | ! Calculate terminal dimensions based on font metrics |
| 199 | ! Tab bar is hidden with only 1 tab, so use full height initially |
| 200 | term_cols = win_width / cell_width |
| 201 | term_rows = win_height / cell_height |
| 202 | prev_width = win_width |
| 203 | prev_height = win_height |
| 204 | |
| 205 | ! Initialize tab manager with first tab |
| 206 | call tab_manager_init(tab_mgr, term_rows, term_cols) |
| 207 | |
| 208 | if (.not. tab_manager_has_tabs(tab_mgr)) then |
| 209 | print *, "Error: Could not create initial tab" |
| 210 | call renderer_destroy(ren) |
| 211 | call window_destroy(win) |
| 212 | stop 1 |
| 213 | end if |
| 214 | |
| 215 | ! Apply cursor settings from config to first tab's first pane |
| 216 | tab_mgr%tabs(1)%panes(1)%term%cursor%style = cfg%cursor_style |
| 217 | tab_mgr%tabs(1)%panes(1)%term%cursor%blink = cfg%cursor_blink |
| 218 | |
| 219 | ! Get pointers to active tab's terminal and PTY |
| 220 | term => tab_manager_get_active_term(tab_mgr) |
| 221 | active_pty => tab_manager_get_active_pty(tab_mgr) |
| 222 | |
| 223 | ! Connect active PTY and terminal to window for keyboard input and scrollback |
| 224 | call window_set_pty(active_pty) |
| 225 | call window_set_terminal(term) |
| 226 | |
| 227 | ! Initialize render state module with pointers to program state |
| 228 | call render_state_init(win, ren, tab_mgr, cfg, cell_width, cell_height, ascender, & |
| 229 | win_width, win_height) |
| 230 | |
| 231 | ! Register render callback for live resize support on macOS |
| 232 | call window_set_render_callback(do_render) |
| 233 | |
| 234 | ! Initialize blink timer |
| 235 | last_time = glfwGetTime() |
| 236 | blink_timer = 0.0d0 |
| 237 | cursor_blink_visible = .true. |
| 238 | |
| 239 | ! Initialize focus tracking (assume focused initially to ensure first render) |
| 240 | was_focused = .true. |
| 241 | |
| 242 | ! Main event loop - exit when window closes or all tabs closed |
| 243 | any_pty_alive = tab_manager_has_tabs(tab_mgr) |
| 244 | do while (.not. window_should_close(win) .and. any_pty_alive) |
| 245 | ! Update blink timer |
| 246 | current_time = glfwGetTime() |
| 247 | blink_timer = blink_timer + (current_time - last_time) |
| 248 | last_time = current_time |
| 249 | if (blink_timer > 0.5d0) then |
| 250 | cursor_blink_visible = .not. cursor_blink_visible |
| 251 | blink_timer = 0.0d0 |
| 252 | end if |
| 253 | call render_state_update_blink(cursor_blink_visible) |
| 254 | |
| 255 | ! Poll events first - this ensures resize callbacks fire BEFORE we render |
| 256 | ! so viewport and projection updates happen in the same frame |
| 257 | call window_poll_events() |
| 258 | |
| 259 | ! Handle tab actions (Cmd/Ctrl+T, W, [, ], 1-9) |
| 260 | tab_action = window_get_tab_action() |
| 261 | if (tab_action /= 0) then |
| 262 | call window_clear_tab_action() |
| 263 | call handle_tab_action(tab_action) |
| 264 | end if |
| 265 | |
| 266 | ! Handle pane actions (Cmd/Ctrl+\, arrows, hjkl) |
| 267 | pane_action = window_get_pane_action() |
| 268 | if (pane_action /= 0) then |
| 269 | call window_clear_pane_action() |
| 270 | call handle_pane_action(pane_action) |
| 271 | end if |
| 272 | |
| 273 | ! Check for font size change request (Ctrl/Cmd +/-) |
| 274 | font_delta = window_get_font_delta() |
| 275 | if (font_delta /= 0) then |
| 276 | call window_clear_font_delta() |
| 277 | |
| 278 | if (font_delta == -999) then |
| 279 | ! Reset to default |
| 280 | new_font_size = base_font_size |
| 281 | else |
| 282 | new_font_size = ren%font%size_px + font_delta |
| 283 | end if |
| 284 | |
| 285 | ! Clamp to reasonable range (8px to 72px) |
| 286 | new_font_size = max(8, min(72, new_font_size)) |
| 287 | |
| 288 | if (new_font_size /= ren%font%size_px) then |
| 289 | ! Reload font with new size |
| 290 | call renderer_change_font_size(ren, trim(font_path_saved), new_font_size) |
| 291 | |
| 292 | ! Fix atlas font pointer after reload |
| 293 | ren%atlas%font => ren%font |
| 294 | |
| 295 | ! Reload fallback font (using saved path from startup) |
| 296 | if (len_trim(fallback_path_saved) > 0) then |
| 297 | call renderer_load_fallback_font(ren, trim(fallback_path_saved)) |
| 298 | end if |
| 299 | |
| 300 | ! Update cell dimensions from new font |
| 301 | cell_width = ren%font%cell_width |
| 302 | cell_height = ren%font%cell_height |
| 303 | ascender = ren%font%ascender |
| 304 | if (cell_width < 1) cell_width = 10 |
| 305 | if (cell_height < 1) cell_height = 18 |
| 306 | if (ascender < 1) ascender = cell_height - 4 |
| 307 | |
| 308 | ! Update window module's cell size for mouse coords |
| 309 | call window_set_cell_size(cell_width, cell_height) |
| 310 | |
| 311 | ! Recalculate terminal dimensions (account for tab bar if visible) |
| 312 | ! Tab bar is hidden when only 1 tab |
| 313 | if (tab_mgr%count > 1) then |
| 314 | tab_bar_height = tab_mgr%bar_height |
| 315 | else |
| 316 | tab_bar_height = 0 |
| 317 | end if |
| 318 | new_cols = win_width / cell_width |
| 319 | new_rows = (win_height - tab_bar_height) / cell_height |
| 320 | if (new_cols /= term_cols .or. new_rows /= term_rows) then |
| 321 | term_cols = new_cols |
| 322 | term_rows = new_rows |
| 323 | tab_mgr%term_rows = term_rows |
| 324 | tab_mgr%term_cols = term_cols |
| 325 | ! Resize all panes in all tabs |
| 326 | do i = 1, tab_mgr%count |
| 327 | do k = 1, tab_mgr%tabs(i)%pane_count |
| 328 | call pty_resize(tab_mgr%tabs(i)%panes(k)%pty, term_rows, term_cols) |
| 329 | call terminal_resize(tab_mgr%tabs(i)%panes(k)%term, term_rows, term_cols) |
| 330 | end do |
| 331 | end do |
| 332 | ! Recalculate layout for active tab |
| 333 | call tab_manager_recalculate_layout(tab_mgr, 0, tab_bar_height, & |
| 334 | win_width, win_height - tab_bar_height, & |
| 335 | cell_width, cell_height) |
| 336 | end if |
| 337 | |
| 338 | call window_set_font_size(new_font_size) |
| 339 | print *, "Font size:", new_font_size, "px (", term_cols, "x", term_rows, ")" |
| 340 | end if |
| 341 | end if |
| 342 | |
| 343 | ! Check for window resize |
| 344 | call window_get_size(win, win_width, win_height) |
| 345 | if (win_width /= prev_width .or. win_height /= prev_height) then |
| 346 | prev_width = win_width |
| 347 | prev_height = win_height |
| 348 | |
| 349 | ! Update projection matrix |
| 350 | call renderer_set_projection(ren, win_width, win_height) |
| 351 | |
| 352 | ! Calculate new terminal size and notify PTY and terminal (account for tab bar if visible) |
| 353 | if (tab_mgr%count > 1) then |
| 354 | tab_bar_height = tab_mgr%bar_height |
| 355 | else |
| 356 | tab_bar_height = 0 |
| 357 | end if |
| 358 | new_cols = win_width / cell_width |
| 359 | new_rows = (win_height - tab_bar_height) / cell_height |
| 360 | if (new_cols /= term_cols .or. new_rows /= term_rows) then |
| 361 | term_cols = new_cols |
| 362 | term_rows = new_rows |
| 363 | tab_mgr%term_rows = term_rows |
| 364 | tab_mgr%term_cols = term_cols |
| 365 | ! Resize all panes in all tabs |
| 366 | do i = 1, tab_mgr%count |
| 367 | do k = 1, tab_mgr%tabs(i)%pane_count |
| 368 | call pty_resize(tab_mgr%tabs(i)%panes(k)%pty, term_rows, term_cols) |
| 369 | call terminal_resize(tab_mgr%tabs(i)%panes(k)%term, term_rows, term_cols) |
| 370 | end do |
| 371 | end do |
| 372 | end if |
| 373 | ! Recalculate layout for active tab |
| 374 | call tab_manager_recalculate_layout(tab_mgr, 0, tab_bar_height, & |
| 375 | win_width, win_height - tab_bar_height, & |
| 376 | cell_width, cell_height) |
| 377 | end if |
| 378 | |
| 379 | ! Read from ALL PTYs (non-blocking) - keeps inactive panes responsive |
| 380 | any_pty_alive = .false. |
| 381 | do i = 1, tab_mgr%count |
| 382 | do k = 1, tab_mgr%tabs(i)%pane_count |
| 383 | if (tab_mgr%tabs(i)%panes(k)%pty%active) then |
| 384 | any_pty_alive = .true. |
| 385 | nbytes = pty_read(tab_mgr%tabs(i)%panes(k)%pty, pty_buffer, 4096) |
| 386 | if (nbytes > 0) then |
| 387 | ! Process each byte through this pane's parser |
| 388 | do j = 1, nbytes |
| 389 | call parser_process_byte(tab_mgr%tabs(i)%panes(k)%parser, & |
| 390 | tab_mgr%tabs(i)%panes(k)%term, & |
| 391 | ichar(pty_buffer(j:j))) |
| 392 | end do |
| 393 | end if |
| 394 | |
| 395 | ! Check for terminal responses and send to this pane's PTY |
| 396 | if (terminal_has_response(tab_mgr%tabs(i)%panes(k)%term)) then |
| 397 | call terminal_get_response(tab_mgr%tabs(i)%panes(k)%term, response_buf, response_len) |
| 398 | if (response_len > 0) then |
| 399 | call pty_write(tab_mgr%tabs(i)%panes(k)%pty, response_buf, response_len) |
| 400 | end if |
| 401 | end if |
| 402 | end if |
| 403 | end do |
| 404 | end do |
| 405 | |
| 406 | ! Update window pointers to current active tab |
| 407 | term => tab_manager_get_active_term(tab_mgr) |
| 408 | active_pty => tab_manager_get_active_pty(tab_mgr) |
| 409 | if (associated(term) .and. associated(active_pty)) then |
| 410 | call window_set_pty(active_pty) |
| 411 | call window_set_terminal(term) |
| 412 | end if |
| 413 | |
| 414 | ! Check for window title changes (from OSC 0/1/2) on active tab |
| 415 | if (associated(term)) then |
| 416 | if (terminal_has_title_changed(term)) then |
| 417 | call window_set_title(win, terminal_get_title(term)) |
| 418 | ! Also update the tab title |
| 419 | tab_mgr%tabs(tab_mgr%active_index)%title = terminal_get_title(term) |
| 420 | end if |
| 421 | end if |
| 422 | |
| 423 | ! Render and swap - but only if window has focus or just regained focus |
| 424 | ! On Wayland, glfwSwapBuffers blocks waiting for frame callbacks when |
| 425 | ! the window is on an inactive workspace, causing compositor timeout. |
| 426 | ! Skip rendering when unfocused to keep event loop responsive. |
| 427 | is_focused = window_is_focused(win) |
| 428 | if (is_focused .or. was_focused) then |
| 429 | call do_render() |
| 430 | end if |
| 431 | was_focused = is_focused |
| 432 | end do |
| 433 | |
| 434 | ! Cleanup |
| 435 | call tab_manager_destroy(tab_mgr) |
| 436 | call renderer_destroy(ren) |
| 437 | call window_destroy(win) |
| 438 | |
| 439 | contains |
| 440 | |
| 441 | ! Try multiple font fallback paths for different Linux distributions |
| 442 | subroutine try_font_fallbacks(ren, font_size, found_path) |
| 443 | type(renderer_t), intent(inout) :: ren |
| 444 | integer, intent(in) :: font_size |
| 445 | character(len=*), intent(out) :: found_path |
| 446 | character(len=256) :: fallback_paths(12) |
| 447 | integer :: i |
| 448 | |
| 449 | ! Fallback paths for various Linux distributions |
| 450 | ! DejaVu Sans Mono - widely available |
| 451 | fallback_paths(1) = "/usr/share/fonts/TTF/DejaVuSansMono.ttf" ! Arch |
| 452 | fallback_paths(2) = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" ! Debian/Ubuntu |
| 453 | fallback_paths(3) = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf" ! Fedora |
| 454 | ! Liberation Mono - common alternative |
| 455 | fallback_paths(4) = "/usr/share/fonts/TTF/LiberationMono-Regular.ttf" ! Arch |
| 456 | fallback_paths(5) = "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf" ! Debian |
| 457 | fallback_paths(6) = "/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf" ! Fedora |
| 458 | ! Noto Sans Mono |
| 459 | fallback_paths(7) = "/usr/share/fonts/noto/NotoSansMono-Regular.ttf" ! Common |
| 460 | fallback_paths(8) = "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf" ! Debian |
| 461 | ! FreeMono (GNU FreeFont) |
| 462 | fallback_paths(9) = "/usr/share/fonts/TTF/FreeMono.ttf" ! Arch |
| 463 | fallback_paths(10) = "/usr/share/fonts/truetype/freefont/FreeMono.ttf" ! Debian |
| 464 | ! Hack font (popular terminal font) |
| 465 | fallback_paths(11) = "/usr/share/fonts/TTF/Hack-Regular.ttf" ! Arch |
| 466 | fallback_paths(12) = "/usr/share/fonts/truetype/hack/Hack-Regular.ttf" ! Debian |
| 467 | |
| 468 | found_path = '' |
| 469 | do i = 1, size(fallback_paths) |
| 470 | if (len_trim(fallback_paths(i)) == 0) cycle |
| 471 | ren = renderer_create(trim(fallback_paths(i)), font_size) |
| 472 | if (ren%initialized) then |
| 473 | found_path = fallback_paths(i) |
| 474 | print *, "Using fallback font: ", trim(found_path) |
| 475 | return |
| 476 | end if |
| 477 | end do |
| 478 | end subroutine try_font_fallbacks |
| 479 | |
| 480 | ! Handle tab action signals from keyboard |
| 481 | subroutine handle_tab_action(action) |
| 482 | use window_mod, only: TAB_ACTION_NEW, TAB_ACTION_CLOSE, TAB_ACTION_NEXT, TAB_ACTION_PREV |
| 483 | integer, intent(in) :: action |
| 484 | integer :: target_tab, old_count, effective_bar_height, new_term_rows, ii, kk |
| 485 | logical :: should_close_tab |
| 486 | |
| 487 | old_count = tab_mgr%count |
| 488 | |
| 489 | select case (action) |
| 490 | case (TAB_ACTION_NEW) |
| 491 | ! Create new tab |
| 492 | call tab_manager_add(tab_mgr) |
| 493 | ! Apply cursor settings from config to new tab's first pane |
| 494 | if (tab_mgr%count > 0) then |
| 495 | tab_mgr%tabs(tab_mgr%count)%panes(1)%term%cursor%style = cfg%cursor_style |
| 496 | tab_mgr%tabs(tab_mgr%count)%panes(1)%term%cursor%blink = cfg%cursor_blink |
| 497 | end if |
| 498 | |
| 499 | case (TAB_ACTION_CLOSE) |
| 500 | ! Context-aware close: close pane if multiple, otherwise close tab |
| 501 | if (tab_mgr%active_index >= 1 .and. tab_mgr%active_index <= tab_mgr%count) then |
| 502 | if (tab_mgr%tabs(tab_mgr%active_index)%pane_count > 1) then |
| 503 | ! Multiple panes - close just the active pane |
| 504 | call tab_manager_close_pane(tab_mgr, should_close_tab) |
| 505 | ! Recalculate layout after pane removal |
| 506 | if (tab_mgr%count > 1) then |
| 507 | effective_bar_height = tab_mgr%bar_height |
| 508 | else |
| 509 | effective_bar_height = 0 |
| 510 | end if |
| 511 | call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, & |
| 512 | win_width, win_height - effective_bar_height, & |
| 513 | cell_width, cell_height) |
| 514 | else |
| 515 | ! Single pane - close the tab |
| 516 | call tab_manager_close(tab_mgr, tab_mgr%active_index) |
| 517 | end if |
| 518 | end if |
| 519 | |
| 520 | case (TAB_ACTION_NEXT) |
| 521 | ! Switch to next tab |
| 522 | call tab_manager_next(tab_mgr) |
| 523 | |
| 524 | case (TAB_ACTION_PREV) |
| 525 | ! Switch to previous tab |
| 526 | call tab_manager_prev(tab_mgr) |
| 527 | |
| 528 | case default |
| 529 | ! Check for goto tab 1-9 (actions 10-18) |
| 530 | if (action >= 10 .and. action <= 18) then |
| 531 | target_tab = action - 9 ! 10 -> tab 1, 11 -> tab 2, etc. |
| 532 | if (target_tab <= tab_mgr%count) then |
| 533 | call tab_manager_switch(tab_mgr, target_tab) |
| 534 | end if |
| 535 | ! Check for close tab 1-9 (actions 20-28) |
| 536 | else if (action >= 20 .and. action <= 28) then |
| 537 | target_tab = action - 19 ! 20 -> close tab 1, 21 -> close tab 2, etc. |
| 538 | if (target_tab <= tab_mgr%count) then |
| 539 | call tab_manager_close(tab_mgr, target_tab) |
| 540 | end if |
| 541 | end if |
| 542 | end select |
| 543 | |
| 544 | ! Calculate effective bar height based on current tab count |
| 545 | if (tab_mgr%count > 1) then |
| 546 | effective_bar_height = tab_mgr%bar_height |
| 547 | else |
| 548 | effective_bar_height = 0 |
| 549 | end if |
| 550 | |
| 551 | ! Check if tab bar visibility changed (1 <-> 2+ tabs) |
| 552 | ! If so, resize all terminals to account for new available height |
| 553 | if ((old_count == 1 .and. tab_mgr%count > 1) .or. & |
| 554 | (old_count > 1 .and. tab_mgr%count == 1)) then |
| 555 | new_term_rows = (win_height - effective_bar_height) / cell_height |
| 556 | if (new_term_rows /= term_rows) then |
| 557 | term_rows = new_term_rows |
| 558 | tab_mgr%term_rows = term_rows |
| 559 | do ii = 1, tab_mgr%count |
| 560 | do kk = 1, tab_mgr%tabs(ii)%pane_count |
| 561 | call pty_resize(tab_mgr%tabs(ii)%panes(kk)%pty, term_rows, term_cols) |
| 562 | call terminal_resize(tab_mgr%tabs(ii)%panes(kk)%term, term_rows, term_cols) |
| 563 | end do |
| 564 | end do |
| 565 | end if |
| 566 | end if |
| 567 | |
| 568 | ! Always recalculate layout for active tab after any tab action |
| 569 | ! This ensures the pane has correct y-offset when tab bar is visible |
| 570 | call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, & |
| 571 | win_width, win_height - effective_bar_height, & |
| 572 | cell_width, cell_height) |
| 573 | |
| 574 | ! Update pointers after tab change |
| 575 | term => tab_manager_get_active_term(tab_mgr) |
| 576 | active_pty => tab_manager_get_active_pty(tab_mgr) |
| 577 | end subroutine handle_tab_action |
| 578 | |
| 579 | ! Handle pane action signals from keyboard |
| 580 | subroutine handle_pane_action(action) |
| 581 | use window_mod, only: PANE_ACTION_SPLIT_V, PANE_ACTION_SPLIT_H, & |
| 582 | PANE_ACTION_NAV_LEFT, PANE_ACTION_NAV_RIGHT, & |
| 583 | PANE_ACTION_NAV_UP, PANE_ACTION_NAV_DOWN |
| 584 | integer, intent(in) :: action |
| 585 | integer :: effective_bar_height |
| 586 | |
| 587 | ! Calculate effective tab bar height |
| 588 | if (tab_mgr%count > 1) then |
| 589 | effective_bar_height = tab_mgr%bar_height |
| 590 | else |
| 591 | effective_bar_height = 0 |
| 592 | end if |
| 593 | |
| 594 | select case (action) |
| 595 | case (PANE_ACTION_SPLIT_V) |
| 596 | ! Split vertically (side-by-side) |
| 597 | call tab_manager_split_pane_v(tab_mgr) |
| 598 | ! Apply cursor settings from config to new pane |
| 599 | active_pane => tab_manager_get_active_pane(tab_mgr) |
| 600 | if (associated(active_pane)) then |
| 601 | active_pane%term%cursor%style = cfg%cursor_style |
| 602 | active_pane%term%cursor%blink = cfg%cursor_blink |
| 603 | end if |
| 604 | ! Recalculate layout |
| 605 | call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, & |
| 606 | win_width, win_height - effective_bar_height, & |
| 607 | cell_width, cell_height) |
| 608 | |
| 609 | case (PANE_ACTION_SPLIT_H) |
| 610 | ! Split horizontally (stacked) |
| 611 | call tab_manager_split_pane_h(tab_mgr) |
| 612 | ! Apply cursor settings from config to new pane |
| 613 | active_pane => tab_manager_get_active_pane(tab_mgr) |
| 614 | if (associated(active_pane)) then |
| 615 | active_pane%term%cursor%style = cfg%cursor_style |
| 616 | active_pane%term%cursor%blink = cfg%cursor_blink |
| 617 | end if |
| 618 | ! Recalculate layout |
| 619 | call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, & |
| 620 | win_width, win_height - effective_bar_height, & |
| 621 | cell_width, cell_height) |
| 622 | |
| 623 | case (PANE_ACTION_NAV_LEFT) |
| 624 | call tab_manager_navigate_pane(tab_mgr, DIR_LEFT) |
| 625 | |
| 626 | case (PANE_ACTION_NAV_RIGHT) |
| 627 | call tab_manager_navigate_pane(tab_mgr, DIR_RIGHT) |
| 628 | |
| 629 | case (PANE_ACTION_NAV_UP) |
| 630 | call tab_manager_navigate_pane(tab_mgr, DIR_UP) |
| 631 | |
| 632 | case (PANE_ACTION_NAV_DOWN) |
| 633 | call tab_manager_navigate_pane(tab_mgr, DIR_DOWN) |
| 634 | end select |
| 635 | |
| 636 | ! Update pointers after pane change |
| 637 | term => tab_manager_get_active_term(tab_mgr) |
| 638 | active_pty => tab_manager_get_active_pty(tab_mgr) |
| 639 | end subroutine handle_pane_action |
| 640 | |
| 641 | end program fortty |
| 642 |