Fortran · 25330 bytes Raw Blame History
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