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