fortplot_system_viewer.f90 Source File


Source Code

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