module fortplot_system_viewer !! System viewer launching for show() functionality !! Handles platform-specific viewer launching and graphical session detection use, intrinsic :: iso_c_binding, only: c_int use fortplot_logging, only: log_info, log_error implicit none private public :: launch_system_viewer public :: has_graphical_session public :: get_temp_filename public :: cleanup_temp_file interface function c_getpid() bind(C, name="getpid") result(pid) import :: c_int integer(c_int) :: pid end function c_getpid end interface !! Memoised OS detection: see get_os_type. logical :: os_type_cached = .false. character(len=32) :: cached_os_type = '' contains function has_graphical_session() result(has_display) !! Check if a graphical session is available !! Returns .true. if X11, Wayland, macOS GUI, or Windows GUI available logical :: has_display character(len=256) :: display_var, wayland_var, session_type, ssh_var character(len=32) :: os_type has_display = .false. os_type = get_os_type() if (trim(os_type) == 'windows') then has_display = .true. return end if if (trim(os_type) == 'darwin') then call get_environment_variable('SSH_CONNECTION', ssh_var) has_display = len_trim(ssh_var) == 0 return end if call get_environment_variable('DISPLAY', display_var) if (len_trim(display_var) > 0) then has_display = .true. return end if call get_environment_variable('WAYLAND_DISPLAY', wayland_var) if (len_trim(wayland_var) > 0) then has_display = .true. return end if call get_environment_variable('XDG_SESSION_TYPE', session_type) if (trim(session_type) == 'wayland' .or. trim(session_type) == 'x11') then has_display = .true. return end if end function has_graphical_session subroutine launch_system_viewer(filename, success) !! Launch system viewer for file !! Uses xdg-open on Linux, open on macOS, start on Windows character(len=*), intent(in) :: filename logical, intent(out) :: success character(len=1024) :: command integer :: stat success = .false. command = get_viewer_command(filename) if (len_trim(command) == 0) then call log_error("No suitable viewer command found for platform") return end if call log_info("Launching viewer: "//trim(command)) call execute_command_line(trim(command), exitstat=stat) if (stat == 0) then success = .true. else call log_error("Failed to launch viewer (exit status "// & trim(int_to_str(stat))//")") end if end subroutine launch_system_viewer function get_viewer_command(filename) result(command) !! Get platform-specific viewer command character(len=*), intent(in) :: filename character(len=1024) :: command character(len=32) :: os_type command = '' os_type = get_os_type() select case (trim(os_type)) case ('linux') command = 'xdg-open "'//trim(filename)//'" >/dev/null 2>&1 &' case ('darwin') command = 'open "'//trim(filename)//'"' case ('windows') command = 'start "" "'//trim(filename)//'"' case default command = '' end select end function get_viewer_command function get_os_type() result(os_type) !! Detect operating system type. !! !! Cached: the answer cannot change while the process runs, and the !! probe below shells out and touches the filesystem. Every savefig on !! the PNG, PDF and SVG paths reaches here through get_temp_filename, !! so repeating the probe was wasteful and, under a parallel test run, !! a source of intermittent failures. character(len=32) :: os_type character(len=256) :: ostype_var, uname_s character(len=256) :: probe_path integer :: stat, uname_unit, ios if (os_type_cached) then os_type = cached_os_type return end if os_type = 'linux' call get_environment_variable('OS', ostype_var) if (index(ostype_var, 'Windows') > 0) then os_type = 'windows' call cache_os_type(os_type) return end if ! Per-process probe path. A fixed /tmp/fortplot_uname.txt meant ! concurrent processes overwrote and deleted each other's file, so the ! probe failed at random and the result silently fell back below. write (probe_path, '(A,I0,A)') '/tmp/fortplot_uname_', & int(c_getpid()), '.txt' call execute_command_line('uname -s > "'//trim(probe_path)//'"', & exitstat=stat) if (stat == 0) then open (newunit=uname_unit, file=trim(probe_path), status='old', & iostat=ios) if (ios == 0) then read (uname_unit, '(A)', iostat=ios) uname_s close (uname_unit, status='delete') if (index(uname_s, 'Darwin') > 0) then os_type = 'darwin' call cache_os_type(os_type) return else if (index(uname_s, 'Linux') > 0) then os_type = 'linux' call cache_os_type(os_type) return end if end if end if call get_environment_variable('OSTYPE', ostype_var) if (index(ostype_var, 'darwin') > 0 .or. index(ostype_var, 'Darwin') > 0) then os_type = 'darwin' else if (index(ostype_var, 'linux') > 0 .or. index(ostype_var, & 'Linux') > 0) then os_type = 'linux' else if (index(ostype_var, 'win') > 0 .or. index(ostype_var, 'msys') > 0) then os_type = 'windows' else os_type = 'linux' end if call cache_os_type(os_type) end function get_os_type subroutine cache_os_type(os_type) character(len=*), intent(in) :: os_type cached_os_type = os_type os_type_cached = .true. end subroutine cache_os_type function get_temp_dir() result(tempdir) !! Get platform-specific temporary directory character(len=256) :: tempdir character(len=256) :: temp_env character(len=32) :: os_type os_type = get_os_type() if (trim(os_type) == 'windows') then call get_environment_variable('TEMP', temp_env) if (len_trim(temp_env) > 0) then tempdir = trim(temp_env) return end if call get_environment_variable('TMP', temp_env) if (len_trim(temp_env) > 0) then tempdir = trim(temp_env) return end if tempdir = 'C:\Temp' else tempdir = '/tmp' end if end function get_temp_dir subroutine get_temp_filename(extension, filename) !! Generate unique temporary filename with proper platform handling character(len=*), intent(in) :: extension character(len=*), intent(out) :: filename character(len=256) :: tempdir character(len=1) :: sep character(len=32) :: os_type integer(c_int) :: pid integer :: clk_count, clk_rate, clk_max tempdir = get_temp_dir() os_type = get_os_type() pid = c_getpid() call system_clock(clk_count, clk_rate, clk_max) if (trim(os_type) == 'windows') then sep = '\' else sep = '/' end if write (filename, '(A,A,A,I0,A,I0,A)') trim(tempdir), sep, 'fortplot_show_', & int(pid), '_', clk_count, trim(extension) end subroutine get_temp_filename subroutine cleanup_temp_file(filename) !! Remove temporary file (best effort, no error on failure) character(len=*), intent(in) :: filename integer :: stat logical :: exists inquire (file=trim(filename), exist=exists) if (exists) then open (newunit=stat, file=trim(filename), status='old', iostat=stat) if (stat == 0) then close (stat, status='delete', iostat=stat) end if end if end subroutine cleanup_temp_file function int_to_str(i) result(str) !! Convert integer to string integer, intent(in) :: i character(len=32) :: str write (str, '(I0)') i end function int_to_str end module fortplot_system_viewer