  SPDX - FileCopyrightText : 2020    > 
  - 
  - SPDX - License - Identifier : MPL-2.0 
 
 -
 - SPDX-License-Identifier: MPL-2.0
 -}

# LANGUAGE LambdaCase    #

-----------------------------------------------------------------------------
-- |
--
-- Standard IO functions assume that the character encoding of the data
-- they read or write is the same as the one used by the current locale. In many
-- situtations this assumption is wrong, as tools work with files, and
  files nowadays are mostly UTF-8 encoded , regardless of the locale .
-- Therefore, it is almost always a good idea to switch the encoding of
  file handles to UTF-8 .
--
-- The same applies to standard input, output, and error handles. However,
-- there is an edge-case: if they are attached to a terminal, and the
  current locale encoding is not UTF-8 , using UTF-8 might actually be unsafe .
--
-- If you are developing an executable, in most cases, it is enough to
-- configure the environment accordingly on program start, see the
-- "Main.Utf8" for functions that help with this.
-- However, if you are a library author, you should avoid modifying the
-- global environment.
--
-- = Quick start
--
-- == Opening new files
--
-- If you need to open a text file, use @Utf8.@'withFile'
-- (or @Utf8.@'openFile'). These will not only open the file, but also
  set the handle ’s encoding to UTF-8 , regardless of the user ’s locale .
--
-- == Working with existing handles
--
-- Suppose you are creating a function which produces some text and writes
-- it to a file handle that is passed to it from the outside.
  Ask yourself this question : do I want to encode this text in UTF-8
-- or using the encoding from the user’s locale?
--
-- In many cases this question is easy to answer. For example, if your
  function produces  code , then you always want it in UTF-8 ,
  because that is what all other tools ( including GHC ) expect .
--
-- In some cases it is not that clear. What you can do then is consider
-- what the user is going to do with the data produced.
-- If it is, primarily, meant to be displayed on their screen and then
  forgotten , you do n’t need UTF-8 . On the other hand , if it is meant
-- to be saved somewhere and then used or edited by other tools, then
  you need UTF-8 .
--
-- If you decided that your function needs to try to switch the handle
  to UTF-8 , it is very easy to achieve :
--
-- @
  import qualified System . IO.Utf8 as Utf8
--
-- writeData :: 'IO.Handle' -> InputDataType -> IO ()
-- writeData hOut inData = Utf8.'withHandle' hOut $ do
--   {- ... write the data ...  -}
-- @
--
  If you decided that you do n’t need to try to switch it to UTF-8 ,
  replace @withHandle@ with ' withTerminalHandle ' to only make the
-- handle safe to write to without runtime errors.
module System.IO.Utf8
  ( withHandle
  , withTerminalHandle

  , setHandleEncoding
  , setTerminalHandleEncoding

  , openFile
  , withFile
  ) where

import Control.Exception.Safe (MonadMask, bracket)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Functor (void)
import GHC.IO.Encoding (mkTextEncoding, utf8)

import qualified System.IO as IO

import System.IO.Utf8.Internal (EncodingAction (..), chooseBestEnc)


type EncRestoreAction m = IO.Handle -> m ()

  | Set the best available UTF-8 - compatible encoding for the handle .
-- Returns the action that will restore the previous one.
--
-- If the handle is in binary mode, does nothing.
  If the handle is not attached to a terminal , sets UTF-8 .
-- Otherwise, keeps its current encoding, but augments it to transliterate
-- unsupported characters.
hSetBestUtf8Enc
  :: MonadIO m
  => (IO.Handle -> IO Bool)
  -> IO.Handle
  -> m (EncRestoreAction m)
hSetBestUtf8Enc hIsTerm h = liftIO $ do
    IO.hGetEncoding h >>= chooseBestEnc h hIsTerm >>= \case
      Keep -> pure (\_ -> pure ())
      ChangeFromTo enc newName -> do
        mkTextEncoding newName >>= IO.hSetEncoding h
        pure $ liftIO . flip IO.hSetEncoding enc


-- | Set handle encoding to the best possible.
--
-- See 'withHandle' for description and prefer it, if possible.
setHandleEncoding :: MonadIO m => IO.Handle -> m ()
setHandleEncoding = liftIO . void . hSetBestUtf8Enc IO.hIsTerminalDevice

-- | Temporarily set handle encoding to the best possible.
--
  “ Best possible ” means UTF-8 , unless the handle points to a terminal
-- device, in which case the encoding will be left the same, but tweaked
-- to approximate unencodable characters.
--
-- This function is safe to call on handles open in binary mode and it will
-- do nothing on them.
--
-- To sum up:
--
--   * If the handle is in binary mode, do nothing.
--   * If the handle points to a terminal device, act like 'withTerminalHandle'.
    * For regular files always choose UTF-8 , of course .
withHandle :: (MonadIO m, MonadMask m) => IO.Handle -> m r -> m r
withHandle h = bracket (hSetBestUtf8Enc IO.hIsTerminalDevice h) ($ h) . const

-- | Make a handle safe to write any text to.
--
-- See 'withTerminalHandle' for description and prefer it, if possible.
setTerminalHandleEncoding :: MonadIO m => IO.Handle -> m ()
setTerminalHandleEncoding = liftIO . void . hSetBestUtf8Enc (const $ pure True)

-- | Temporarily make a handle safe to write any text to.
--
  If the handle is not using UTF-8 , adjust the encoding to remain the same
-- as before, but approximate unencodable characters. When the action is done,
-- restore it back to the previous one.
--
-- Use this function only if you are sure you want to treat this handle as
-- a terminal (that is, you will be using it to interact with the user
-- and to write user-visible messages, rather than something that can
-- be reasonably expected to go to a file).
--
-- This function is safe to call on handles open in binary mode and it will
-- do nothing on them.
withTerminalHandle :: (MonadIO m, MonadMask m) => IO.Handle -> m r -> m r
withTerminalHandle h = bracket (hSetBestUtf8Enc (const $ pure True) h) ($ h) . const


  | Like ' System . IO.openFile ' , but sets the file encoding to UTF-8 , regardless
-- of the current locale.
openFile :: MonadIO m => IO.FilePath -> IO.IOMode -> m IO.Handle
openFile path mode = liftIO $ do
  h <- IO.openFile path mode
  IO.hSetEncoding h utf8
  pure h

  | Like ' System . IO.withFile ' , but sets the file encoding to UTF-8 , regardless
-- of the current locale.
withFile
  :: (MonadIO m, MonadMask m)
  => IO.FilePath -> IO.IOMode -> (IO.Handle -> m r) -> m r
withFile path mode = bracket (openFile path mode) (liftIO . IO.hClose)

---------------------------------------------------------------------------
 |

 Standard IO functions assume that the character encoding of the data
 they read or write is the same as the one used by the current locale. In many
 situtations this assumption is wrong, as tools work with files, and
 Therefore, it is almost always a good idea to switch the encoding of

 The same applies to standard input, output, and error handles. However,
 there is an edge-case: if they are attached to a terminal, and the

 If you are developing an executable, in most cases, it is enough to
 configure the environment accordingly on program start, see the
 "Main.Utf8" for functions that help with this.
 However, if you are a library author, you should avoid modifying the
 global environment.

 = Quick start

 == Opening new files

 If you need to open a text file, use @Utf8.@'withFile'
 (or @Utf8.@'openFile'). These will not only open the file, but also

 == Working with existing handles

 Suppose you are creating a function which produces some text and writes
 it to a file handle that is passed to it from the outside.
 or using the encoding from the user’s locale?

 In many cases this question is easy to answer. For example, if your

 In some cases it is not that clear. What you can do then is consider
 what the user is going to do with the data produced.
 If it is, primarily, meant to be displayed on their screen and then
 to be saved somewhere and then used or edited by other tools, then

 If you decided that your function needs to try to switch the handle

 @

 writeData :: 'IO.Handle' -> InputDataType -> IO ()
 writeData hOut inData = Utf8.'withHandle' hOut $ do
   {- ... write the data ...  -}
 @

 handle safe to write to without runtime errors.
 Returns the action that will restore the previous one.

 If the handle is in binary mode, does nothing.
 Otherwise, keeps its current encoding, but augments it to transliterate
 unsupported characters.
 | Set handle encoding to the best possible.

 See 'withHandle' for description and prefer it, if possible.
 | Temporarily set handle encoding to the best possible.

 device, in which case the encoding will be left the same, but tweaked
 to approximate unencodable characters.

 This function is safe to call on handles open in binary mode and it will
 do nothing on them.

 To sum up:

   * If the handle is in binary mode, do nothing.
   * If the handle points to a terminal device, act like 'withTerminalHandle'.
 | Make a handle safe to write any text to.

 See 'withTerminalHandle' for description and prefer it, if possible.
 | Temporarily make a handle safe to write any text to.

 as before, but approximate unencodable characters. When the action is done,
 restore it back to the previous one.

 Use this function only if you are sure you want to treat this handle as
 a terminal (that is, you will be using it to interact with the user
 and to write user-visible messages, rather than something that can
 be reasonably expected to go to a file).

 This function is safe to call on handles open in binary mode and it will
 do nothing on them.
 of the current locale.
 of the current locale.
  SPDX - FileCopyrightText : 2020    > 
  - 
  - SPDX - License - Identifier : MPL-2.0 
 
 -
 - SPDX-License-Identifier: MPL-2.0
 -}

# LANGUAGE LambdaCase    #

  files nowadays are mostly UTF-8 encoded , regardless of the locale .
  file handles to UTF-8 .
  current locale encoding is not UTF-8 , using UTF-8 might actually be unsafe .
  set the handle ’s encoding to UTF-8 , regardless of the user ’s locale .
  Ask yourself this question : do I want to encode this text in UTF-8
  function produces  code , then you always want it in UTF-8 ,
  because that is what all other tools ( including GHC ) expect .
  forgotten , you do n’t need UTF-8 . On the other hand , if it is meant
  you need UTF-8 .
  to UTF-8 , it is very easy to achieve :
  import qualified System . IO.Utf8 as Utf8
  If you decided that you do n’t need to try to switch it to UTF-8 ,
  replace @withHandle@ with ' withTerminalHandle ' to only make the
module System.IO.Utf8
  ( withHandle
  , withTerminalHandle

  , setHandleEncoding
  , setTerminalHandleEncoding

  , openFile
  , withFile
  ) where

import Control.Exception.Safe (MonadMask, bracket)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Functor (void)
import GHC.IO.Encoding (mkTextEncoding, utf8)

import qualified System.IO as IO

import System.IO.Utf8.Internal (EncodingAction (..), chooseBestEnc)


type EncRestoreAction m = IO.Handle -> m ()

  | Set the best available UTF-8 - compatible encoding for the handle .
  If the handle is not attached to a terminal , sets UTF-8 .
hSetBestUtf8Enc
  :: MonadIO m
  => (IO.Handle -> IO Bool)
  -> IO.Handle
  -> m (EncRestoreAction m)
hSetBestUtf8Enc hIsTerm h = liftIO $ do
    IO.hGetEncoding h >>= chooseBestEnc h hIsTerm >>= \case
      Keep -> pure (\_ -> pure ())
      ChangeFromTo enc newName -> do
        mkTextEncoding newName >>= IO.hSetEncoding h
        pure $ liftIO . flip IO.hSetEncoding enc


setHandleEncoding :: MonadIO m => IO.Handle -> m ()
setHandleEncoding = liftIO . void . hSetBestUtf8Enc IO.hIsTerminalDevice

  “ Best possible ” means UTF-8 , unless the handle points to a terminal
    * For regular files always choose UTF-8 , of course .
withHandle :: (MonadIO m, MonadMask m) => IO.Handle -> m r -> m r
withHandle h = bracket (hSetBestUtf8Enc IO.hIsTerminalDevice h) ($ h) . const

setTerminalHandleEncoding :: MonadIO m => IO.Handle -> m ()
setTerminalHandleEncoding = liftIO . void . hSetBestUtf8Enc (const $ pure True)

  If the handle is not using UTF-8 , adjust the encoding to remain the same
withTerminalHandle :: (MonadIO m, MonadMask m) => IO.Handle -> m r -> m r
withTerminalHandle h = bracket (hSetBestUtf8Enc (const $ pure True) h) ($ h) . const


  | Like ' System . IO.openFile ' , but sets the file encoding to UTF-8 , regardless
openFile :: MonadIO m => IO.FilePath -> IO.IOMode -> m IO.Handle
openFile path mode = liftIO $ do
  h <- IO.openFile path mode
  IO.hSetEncoding h utf8
  pure h

  | Like ' System . IO.withFile ' , but sets the file encoding to UTF-8 , regardless
withFile
  :: (MonadIO m, MonadMask m)
  => IO.FilePath -> IO.IOMode -> (IO.Handle -> m r) -> m r
withFile path mode = bracket (openFile path mode) (liftIO . IO.hClose)
