packages feed

crackNum-3.30: 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)
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"

-- | 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
                               installed <- getDataFileName tclRelPath
                               mbPath    <- findExecutable "crackNum.tcl"
                               case mbPath of
                                 Just p  -> pure p
                                 Nothing -> search [beside, installed] (noTcl beside installed)
  where -- NB. getExecutablePath resolves symlinks, so this finds the script even
        -- when the binary is reached through a link from elsewhere on the PATH.
        besideExe = do exe <- getExecutablePath
                       pure (takeDirectory exe </> "crackNum.tcl")

        search []     onFail = die onFail
        search (c:cs) onFail = do ok <- doesFileExist c
                                  if ok then pure c else search cs onFail

        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"
             ]

-- | 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'.
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
                              ]
  | True
  = die [ "The --gui option is not supported on this platform (" ++ Info.os ++ ")."
        , "Use crackNum directly from the command line."
        ]