packages feed

crackNum-4.0: src/CrackNum/GUI.hs

---------------------------------------------------------------------------
-- |
-- Module      :  CrackNum.GUI
-- Copyright   :  (c) Levent Erkok
-- License     :  BSD3
-- Maintainer  :  erkokl@gmail.com
-- Stability   :  experimental
--
-- Locating and launching the graphical interface
-----------------------------------------------------------------------------

{-# OPTIONS_GHC -Wall -Werror #-}

module CrackNum.GUI(
     launchGUI
   ) where

import System.Directory   (findExecutable, doesFileExist)
import System.Environment (lookupEnv, getExecutablePath)
import System.Exit        (ExitCode(..))
import System.FilePath    (takeDirectory, (</>))
import System.Process     (rawSystem, spawnProcess)
import qualified System.Info as Info

import Paths_crackNum (getDataFileName)

import CrackNum.Utils (die)

-- | Where the Tcl/Tk GUI script lives relative to the package root; also its
-- location within the installed data-directory. (See Data-files in the cabal file.)
tclRelPath :: FilePath
tclRelPath = "GUI/tclGUI/crackNum.tcl"

-- | The Windows GUI executable. Note the name: Windows file systems are
-- case-insensitive, so calling it CrackNum.exe would make it the very same file as
-- the crackNum.exe it drives, and the two cannot share a directory.
winGUIExe :: FilePath
winGUIExe = "CrackNumGUI.exe"

-- | Where a file would sit if it were next to the running executable.
--
-- NB. getExecutablePath resolves symlinks, so this finds the file even when the
-- binary is reached through a link from elsewhere on the PATH.
besideExe :: FilePath -> IO FilePath
besideExe f = do exe <- getExecutablePath
                 pure (takeDirectory exe </> f)

-- | The first candidate that exists; if none do, die with the given message.
search :: [FilePath] -> [String] -> IO FilePath
search []     onFail = die onFail
search (c:cs) onFail = do ok <- doesFileExist c
                          if ok then pure c else search cs onFail

-- | Locate the Tcl/Tk GUI script. Normally it is installed together with the
-- binary, so this just works; we look in four places, in order:
--
--   1. $CRACKNUM_TCL, if set: an explicit override, mirroring $CRACKNUM_GUI on macOS.
--   2. The PATH, so a source checkout can shadow the installed copy while hacking.
--   3. Next to the executable itself. This is what makes a relocatable binary
--      distribution work: in one the data-directory below was baked in on the
--      build machine, and names a path that does not exist on the user's.
--   4. The copy cabal installed in our data-directory.
locateTcl :: IO FilePath
locateTcl = do mbEnv <- lookupEnv "CRACKNUM_TCL"
               case mbEnv of
                 Just p  -> do ok <- doesFileExist p
                               if ok
                                  then pure p
                                  else die [ "The CRACKNUM_TCL environment variable is set, but does not name a file:"
                                           , ""
                                           , "    " ++ p
                                           ]
                 Nothing -> do beside    <- besideExe "crackNum.tcl"
                               installed <- getDataFileName tclRelPath
                               mbPath    <- findExecutable "crackNum.tcl"
                               case mbPath of
                                 Just p  -> pure p
                                 Nothing -> search [beside, installed] (noTcl beside installed)
  where noTcl beside installed =
             [ "Cannot find the CrackNum GUI script (crackNum.tcl)."
             , ""
             , "Looked in:"
             , "  $CRACKNUM_TCL                 (not set)"
             , "  crackNum.tcl on your PATH     (not found)"
             , "  " ++ beside
             , "  " ++ installed
             , ""
             , "This script is normally installed along with crackNum, so seeing this"
             , "means the installed copy is missing or the binary has been moved."
             , ""
             , "If you have a source checkout, point at it directly:"
             , ""
             , "    export CRACKNUM_TCL=/path/to/crackNum/" ++ tclRelPath
             , ""
             , "Otherwise, get a copy of the sources with either of:"
             , ""
             , "    cabal get crackNum"
             , "    git clone http://github.com/LeventErkok/crackNum.git"
             ]

-- | Locate the Windows GUI executable, mirroring 'locateTcl'. We look in three
-- places, in order:
--
--   1. $CRACKNUM_GUI, if set: an explicit override, as on macOS.
--   2. Next to the executable itself, which is how the binary zip is laid out.
--   3. The PATH, for an installation that puts the two elsewhere.
locateWinGUI :: IO FilePath
locateWinGUI = do mbEnv <- lookupEnv "CRACKNUM_GUI"
                  case mbEnv of
                    Just p  -> do ok <- doesFileExist p
                                  if ok
                                     then pure p
                                     else die [ "The CRACKNUM_GUI environment variable is set, but does not name a file:"
                                              , ""
                                              , "    " ++ p
                                              ]
                    Nothing -> do beside <- besideExe winGUIExe
                                  ok     <- doesFileExist beside
                                  if ok
                                     then pure beside
                                     else do mbPath <- findExecutable winGUIExe
                                             case mbPath of
                                               Just p  -> pure p
                                               Nothing -> die (noGUI beside)
  where noGUI beside =
             [ "Cannot find the CrackNum GUI (" ++ winGUIExe ++ ")."
             , ""
             , "Looked in:"
             , "  %CRACKNUM_GUI%                (not set)"
             , "  " ++ beside
             , "  " ++ winGUIExe ++ " on your PATH  (not found)"
             , ""
             , "The GUI ships in the same zip as crackNum.exe, so seeing this means the"
             , "copy next to the binary is missing or the binary has been moved."
             , ""
             , "Get the Windows bundle from:"
             , ""
             , "    https://github.com/LeventErkok/crackNum/releases"
             , ""
             , "Or point at a copy directly:"
             , ""
             , "    $env:CRACKNUM_GUI = \"C:\\path\\to\\" ++ winGUIExe ++ "\""
             ]

-- | Launch the graphical interface, forwarding all remaining arguments
-- (format flags, rounding mode, and/or the value to crack) so the GUI can preselect
-- them. The GUI itself calls back into this executable to do the actual cracking.
--
-- On macOS the GUI is a Swift/AppKit app; CRACKNUM_GUI can override the .app bundle
-- location. On Linux the GUI is a Tcl/Tk script; 'wish' is located via PATH, and
-- 'crackNum.tcl' via 'locateTcl'. On Windows it is a WinForms app located by
-- 'locateWinGUI', with CRACKNUM_GUI overriding as it does on macOS.
launchGUI :: [String] -> IO ()
launchGUI vals
  | Info.os == "darwin"
  = do mbApp <- lookupEnv "CRACKNUM_GUI"
       let args = case mbApp of
                    Just p  -> ["-n", p,                "--args"] ++ vals
                    Nothing -> ["-n", "-a", "CrackNum", "--args"] ++ vals
       ec <- rawSystem "open" args
       case ec of
         ExitSuccess   -> pure ()
         ExitFailure _ -> die [ "Unable to launch the CrackNum GUI application."
                              , ""
                              , "The CrackNum GUI app does not seem to be installed. To install it,"
                              , "get the crackNum sources and build the GUI (macOS 13+, Swift toolchain):"
                              , ""
                              , "    git clone http://github.com/LeventErkok/crackNum.git"
                              , "    cd crackNum/GUI/swiftGUI"
                              , "    make install       # builds and copies CrackNum.app into /Applications"
                              , ""
                              , "Then re-run: crackNum --gui" ++ (if null vals then "" else ' ' : unwords vals)
                              ]
  | Info.os == "linux"
  = do mbWish <- findExecutable "wish"
       wish   <- case mbWish of
                   Just w  -> pure w
                   Nothing -> die [ "Cannot find 'wish' on your PATH."
                                  , "Install Tcl/Tk to get wish, e.g.:"
                                  , ""
                                  , "    nix profile install nixpkgs#tk"
                                  , "    sudo apt install tk       # Debian/Ubuntu"
                                  , "    sudo dnf install tk       # RHEL/Fedora"
                                  ]
       tcl    <- locateTcl
       ec     <- rawSystem wish (tcl : vals)
       case ec of
         ExitSuccess   -> pure ()
         ExitFailure _ -> die [ "Unable to launch the CrackNum GUI."
                              , ""
                              , "Tried: " ++ wish ++ " " ++ tcl
                              ]
  | Info.os == "mingw32"
  = do gui <- locateWinGUI
       -- Spawned rather than run through rawSystem: the GUI outlives this process,
       -- matching what 'open -n' gives us on macOS. Waiting would pin the console
       -- for as long as the window stayed open.
       _   <- spawnProcess gui vals
       pure ()
  | True
  = die [ "The --gui option is not supported on this platform (" ++ Info.os ++ ")."
        , "Use crackNum directly from the command line."
        ]