Fortran · 22855 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 ! Fontconfig not available or failed - try common system locations
72 font_path = "/usr/share/fonts/TTF/DejaVuSansMono.ttf"
73 else
74 print *, "Using system monospace font: ", trim(font_path)
75 end if
76 end if
77
78 ! Create renderer with font (using font size from config)
79 ren = renderer_create(trim(font_path), cfg%font_size)
80 if (.not. ren%initialized) then
81 print *, "Warning: Could not load font, trying alternate path..."
82 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
83 ren = renderer_create(trim(font_path), cfg%font_size)
84 end if
85
86 if (.not. ren%initialized) then
87 print *, "Error: Could not initialize renderer"
88 print *, "Please ensure a monospace font is installed"
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 ! Handle tab action signals from keyboard
442 subroutine handle_tab_action(action)
443 use window_mod, only: TAB_ACTION_NEW, TAB_ACTION_CLOSE, TAB_ACTION_NEXT, TAB_ACTION_PREV
444 integer, intent(in) :: action
445 integer :: target_tab, old_count, effective_bar_height, new_term_rows, ii, kk
446 logical :: should_close_tab
447
448 old_count = tab_mgr%count
449
450 select case (action)
451 case (TAB_ACTION_NEW)
452 ! Create new tab
453 call tab_manager_add(tab_mgr)
454 ! Apply cursor settings from config to new tab's first pane
455 if (tab_mgr%count > 0) then
456 tab_mgr%tabs(tab_mgr%count)%panes(1)%term%cursor%style = cfg%cursor_style
457 tab_mgr%tabs(tab_mgr%count)%panes(1)%term%cursor%blink = cfg%cursor_blink
458 end if
459
460 case (TAB_ACTION_CLOSE)
461 ! Context-aware close: close pane if multiple, otherwise close tab
462 if (tab_mgr%active_index >= 1 .and. tab_mgr%active_index <= tab_mgr%count) then
463 if (tab_mgr%tabs(tab_mgr%active_index)%pane_count > 1) then
464 ! Multiple panes - close just the active pane
465 call tab_manager_close_pane(tab_mgr, should_close_tab)
466 ! Recalculate layout after pane removal
467 if (tab_mgr%count > 1) then
468 effective_bar_height = tab_mgr%bar_height
469 else
470 effective_bar_height = 0
471 end if
472 call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, &
473 win_width, win_height - effective_bar_height, &
474 cell_width, cell_height)
475 else
476 ! Single pane - close the tab
477 call tab_manager_close(tab_mgr, tab_mgr%active_index)
478 end if
479 end if
480
481 case (TAB_ACTION_NEXT)
482 ! Switch to next tab
483 call tab_manager_next(tab_mgr)
484
485 case (TAB_ACTION_PREV)
486 ! Switch to previous tab
487 call tab_manager_prev(tab_mgr)
488
489 case default
490 ! Check for goto tab 1-9 (actions 10-18)
491 if (action >= 10 .and. action <= 18) then
492 target_tab = action - 9 ! 10 -> tab 1, 11 -> tab 2, etc.
493 if (target_tab <= tab_mgr%count) then
494 call tab_manager_switch(tab_mgr, target_tab)
495 end if
496 end if
497 end select
498
499 ! Check if tab bar visibility changed (1 <-> 2+ tabs)
500 ! If so, resize all terminals to account for new available height
501 if ((old_count == 1 .and. tab_mgr%count > 1) .or. &
502 (old_count > 1 .and. tab_mgr%count == 1)) then
503 if (tab_mgr%count > 1) then
504 effective_bar_height = tab_mgr%bar_height
505 else
506 effective_bar_height = 0
507 end if
508 new_term_rows = (win_height - effective_bar_height) / cell_height
509 if (new_term_rows /= term_rows) then
510 term_rows = new_term_rows
511 tab_mgr%term_rows = term_rows
512 do ii = 1, tab_mgr%count
513 do kk = 1, tab_mgr%tabs(ii)%pane_count
514 call pty_resize(tab_mgr%tabs(ii)%panes(kk)%pty, term_rows, term_cols)
515 call terminal_resize(tab_mgr%tabs(ii)%panes(kk)%term, term_rows, term_cols)
516 end do
517 end do
518 end if
519 ! Recalculate layout for active tab
520 call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, &
521 win_width, win_height - effective_bar_height, &
522 cell_width, cell_height)
523 end if
524
525 ! Update pointers after tab change
526 term => tab_manager_get_active_term(tab_mgr)
527 active_pty => tab_manager_get_active_pty(tab_mgr)
528 end subroutine handle_tab_action
529
530 ! Handle pane action signals from keyboard
531 subroutine handle_pane_action(action)
532 use window_mod, only: PANE_ACTION_SPLIT_V, PANE_ACTION_SPLIT_H, &
533 PANE_ACTION_NAV_LEFT, PANE_ACTION_NAV_RIGHT, &
534 PANE_ACTION_NAV_UP, PANE_ACTION_NAV_DOWN
535 integer, intent(in) :: action
536 integer :: effective_bar_height
537
538 ! Calculate effective tab bar height
539 if (tab_mgr%count > 1) then
540 effective_bar_height = tab_mgr%bar_height
541 else
542 effective_bar_height = 0
543 end if
544
545 select case (action)
546 case (PANE_ACTION_SPLIT_V)
547 ! Split vertically (side-by-side)
548 call tab_manager_split_pane_v(tab_mgr)
549 ! Apply cursor settings from config to new pane
550 active_pane => tab_manager_get_active_pane(tab_mgr)
551 if (associated(active_pane)) then
552 active_pane%term%cursor%style = cfg%cursor_style
553 active_pane%term%cursor%blink = cfg%cursor_blink
554 end if
555 ! Recalculate layout
556 call tab_manager_recalculate_layout(tab_mgr, 0, effective_bar_height, &
557 win_width, win_height - effective_bar_height, &
558 cell_width, cell_height)
559
560 case (PANE_ACTION_SPLIT_H)
561 ! Split horizontally (stacked)
562 call tab_manager_split_pane_h(tab_mgr)
563 ! Apply cursor settings from config to new pane
564 active_pane => tab_manager_get_active_pane(tab_mgr)
565 if (associated(active_pane)) then
566 active_pane%term%cursor%style = cfg%cursor_style
567 active_pane%term%cursor%blink = cfg%cursor_blink
568 end if
569 ! Recalculate layout
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 case (PANE_ACTION_NAV_LEFT)
575 call tab_manager_navigate_pane(tab_mgr, DIR_LEFT)
576
577 case (PANE_ACTION_NAV_RIGHT)
578 call tab_manager_navigate_pane(tab_mgr, DIR_RIGHT)
579
580 case (PANE_ACTION_NAV_UP)
581 call tab_manager_navigate_pane(tab_mgr, DIR_UP)
582
583 case (PANE_ACTION_NAV_DOWN)
584 call tab_manager_navigate_pane(tab_mgr, DIR_DOWN)
585 end select
586
587 ! Update pointers after pane change
588 term => tab_manager_get_active_term(tab_mgr)
589 active_pty => tab_manager_get_active_pty(tab_mgr)
590 end subroutine handle_pane_action
591
592 end program fortty
593