summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-06-22 12:54:49 +0200
committerHannes Schulze <haschu0103@outlook.de>2025-06-22 12:54:49 +0200
commit731edfef3f416fc763a7fbbd26d482b77b2b4068 (patch)
tree78ce6c5739fd588c6ff1ed92d4a2a0f38b6760a2 /app
parent93289029b938864d1111c4285d64f3f0a1e5735d (diff)
refactorhaskell
Diffstat (limited to 'app')
-rw-r--r--app/Foobar/Application.hs39
-rw-r--r--app/Foobar/Models/Config.hs99
-rw-r--r--app/Foobar/Services/Coalescing.hs46
-rw-r--r--app/Foobar/Services/Config.hs178
-rw-r--r--app/foobar.cabal3
5 files changed, 219 insertions, 146 deletions
diff --git a/app/Foobar/Application.hs b/app/Foobar/Application.hs
index 94c8182..8034d11 100644
--- a/app/Foobar/Application.hs
+++ b/app/Foobar/Application.hs
@@ -7,56 +7,45 @@ module Foobar.Application
import Data.Maybe (fromJust)
import Data.GI.Base
-import Data.Text
import GHC.Generics (Generic)
-import Foobar.Services.Config (initConfiguration, loadConfiguration, observeConfiguration)
import Declarative.Gtk
+import Foobar.Services.Config
+import Foobar.Models.Config
import qualified GI.Gtk as Gtk
import qualified GI.Gdk as Gdk
import qualified GI.Gio as Gio
import qualified GI.Foobar as FB
-mkApplication :: IO Gtk.Application
-mkApplication = new Gtk.Application [ #applicationId := "com.github.hannesschulze.foobar-hs"
- , On #activate (onActivate ?self) ]
-
newtype AppState
- = AppState { stateText :: Text }
+ = AppState { config :: Config }
deriving (Generic)
-data Message = MsgQuit | MsgDoSomething
+data Message = MsgConfigChanged
-initialState :: AppState
-initialState = AppState { stateText = "Do something" }
+mkApplication :: IO Gtk.Application
+mkApplication = new Gtk.Application [ #applicationId := "com.github.hannesschulze.foobar-hs"
+ , On #activate (onActivate ?self) ]
appView :: View AppState Message Gtk.Widget
-appView state emit = widget DeclarativeBox [ #children := children
- , #orientation := Gtk.OrientationVertical
- , #spacing := 12]
- where children = [ widget Gtk.Button [ #label :<~ state.stateText
- , On #clicked (emit MsgDoSomething) ]
- , widget Gtk.Button [ #label := "Quit"
- , On #clicked (emit MsgQuit) ] ]
+appView state _ = widget Gtk.Label [ #label :<~ lbl ]
+ where lbl = fmap ("Stylesheet: " <>) state.config.general.stylesheet
appHandler :: Handler Message AppState
-appHandler MsgQuit _ = pure ()
-appHandler MsgDoSomething s = s.stateText <~ "Did something!"
+appHandler MsgConfigChanged s = do newConfig <- loadConfig
+ s.config <~ newConfig
onActivate :: Gtk.Application -> IO ()
onActivate app = do
- conf <- initConfiguration
- putStrLn $ "Initial config: " ++ show conf
- observeConfiguration $ do
- conf' <- loadConfiguration
- putStrLn $ "Updated config: " ++ show conf'
+ initialState <- AppState <$> initConfig
provider <- new Gtk.CssProvider []
provider.loadFromResource "/foobar/styles/default.css"
disp <- Gdk.displayGetDefault
Gtk.styleContextAddProviderForDisplay (fromJust disp)
provider
(fromIntegral Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
- view <- mvh initialState appView appHandler
+ (view, emit, _) <- fullMVH initialState appView appHandler
+ observeConfig (emit MsgConfigChanged)
window <- new Gtk.ApplicationWindow [ #application := app
, #title := "Test"
, #child := view ]
diff --git a/app/Foobar/Models/Config.hs b/app/Foobar/Models/Config.hs
index a21049a..7df0936 100644
--- a/app/Foobar/Models/Config.hs
+++ b/app/Foobar/Models/Config.hs
@@ -18,11 +18,11 @@ module Foobar.Models.Config
, Action(..)
, -- * Panel Configuration
PanelItem(..)
- , PanelItemConfiguration(..)
- , PanelIconConfiguration(..)
- , PanelClockConfiguration(..)
- , PanelWorkspacesConfiguration(..)
- , PanelStatusConfiguration(..)
+ , PanelItemConfig(..)
+ , PanelIconConfig(..)
+ , PanelClockConfig(..)
+ , PanelWorkspacesConfig(..)
+ , PanelStatusConfig(..)
, PanelStatusItem(..)
, PanelItemPosition(..)
, -- * Control Center Configuration
@@ -40,14 +40,14 @@ data Config
, controlCenter :: !ControlCenterConfig -- ^ The control center configuration.
, notifications :: !NotificationConfig -- ^ The notification configuration.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | General application settings not specific to a single component.
data GeneralConfig
= GeneralConfig { stylesheet :: !Text -- ^ URI to the CSS stylesheet to use -- this may be a resource that's bundled
-- with Foobar or a "file:" URI.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | Configuration for the panel shown at the edge of a screen.
data PanelConfig
@@ -60,7 +60,7 @@ data PanelConfig
, multiMonitor :: !Bool -- ^ Flag to enable the panel on all monitors.
, items :: ![PanelItem] -- ^ Items configured to be displayed in the panel.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | Configuration for the application launcher.
data LauncherConfig
@@ -68,7 +68,7 @@ data LauncherConfig
, position :: !Int -- ^ Offset from the top of the screen.
, maxHeight :: !Int -- ^ Maximum allowed height for the launcher before scrolling is enabled.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | Configuration for the control center.
data ControlCenterConfig
@@ -86,7 +86,7 @@ data ControlCenterConfig
, rows :: ![ControlCenterRow] -- ^ Ordered list of rows to display in the controls
-- section.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | Configuration for notifications and the notification area shown in the corner of the screen.
data NotificationConfig
@@ -97,7 +97,7 @@ data NotificationConfig
-- negative).
, timeFormat :: !Text -- ^ The time format string as used by g_date_time_format.
}
- deriving (Show)
+ deriving (Show, Eq)
-- | An edge of the screen.
data ScreenEdge
@@ -122,55 +122,54 @@ data Action
-- | Configuration for an item in the panel, including a general item configuration and an item-specific configuration.
data PanelItem
- = PanelItemIcon !PanelItemConfiguration !PanelIconConfiguration
- | PanelItemClock !PanelItemConfiguration !PanelClockConfiguration
- | PanelItemWorkspaces !PanelItemConfiguration !PanelWorkspacesConfiguration
- | PanelItemStatus !PanelItemConfiguration !PanelStatusConfiguration
- deriving (Show)
+ = PanelItemIcon !PanelItemConfig !PanelIconConfig
+ | PanelItemClock !PanelItemConfig !PanelClockConfig
+ | PanelItemWorkspaces !PanelItemConfig !PanelWorkspacesConfig
+ | PanelItemStatus !PanelItemConfig !PanelStatusConfig
+ deriving (Show, Eq)
-- | General configuration for items in the panel.
-data PanelItemConfiguration
- = PanelItemConfiguration { name :: !Text -- ^ Name of the panel item (this is derived from the
- -- section name, which has the form "panel.[name]").
- , position :: !PanelItemPosition -- ^ Position where the item should be placed item
- -- within the panel.
- }
- deriving (Show)
+data PanelItemConfig
+ = PanelItemConfig { name :: !Text -- ^ Name of the panel item (this is derived from the section
+ -- name, which has the form "panel.[name]").
+ , position :: !PanelItemPosition -- ^ Position where the item should be placed item within the
+ -- panel.
+ }
+ deriving (Show, Eq)
-- | Configuration for icon items in the panel.
-data PanelIconConfiguration
- = PanelIconConfiguration { iconName :: !Text -- ^ The GTK icon to use for the item.
- , action :: !Action -- ^ Action invoked when the user clicks the item.
- }
- deriving (Show)
+data PanelIconConfig
+ = PanelIconConfig { iconName :: !Text -- ^ The GTK icon to use for the item.
+ , action :: !Action -- ^ Action invoked when the user clicks the item.
+ }
+ deriving (Show, Eq)
-- | Configuration for clock items in the panel.
-data PanelClockConfiguration
- = PanelClockConfiguration { format :: !Text -- ^ The time format string as used by g_date_time_format.
- , action :: !Action -- ^ Action invoked when the user clicks the item.
- }
- deriving (Show)
+data PanelClockConfig
+ = PanelClockConfig { format :: !Text -- ^ The time format string as used by g_date_time_format.
+ , action :: !Action -- ^ Action invoked when the user clicks the item.
+ }
+ deriving (Show, Eq)
-- | Configuration for workspace items in the panel.
-data PanelWorkspacesConfiguration
- = PanelWorkspacesConfiguration { buttonSize :: !Int -- ^ Size of each workspace button.
- , spacing :: !Int -- ^ Inner spacing between the workspace buttons.
- }
- deriving (Show)
+data PanelWorkspacesConfig
+ = PanelWorkspacesConfig { buttonSize :: !Int -- ^ Size of each workspace button.
+ , spacing :: !Int -- ^ Inner spacing between the workspace buttons.
+ }
+ deriving (Show, Eq)
-- | Configuration for status items in the panel.
-data PanelStatusConfiguration
- = PanelStatusConfiguration { items :: ![PanelStatusItem] -- ^ Ordered list of status items to display in
- -- the item.
- , spacing :: !Int -- ^ Inner spacing between the status items.
- , showLabels :: !Bool -- ^ Indicates whether text labels should be shown
- -- next to the status icons.
- , enableScrolling :: !Bool -- ^ If set to true, some settings like volume or
- -- brightness can be adjusted by scrolling while
- -- hovering over the status item.
- , action :: !Action -- ^ Action invoked when the user clicks the item.
- }
- deriving (Show)
+data PanelStatusConfig
+ = PanelStatusConfig { items :: ![PanelStatusItem] -- ^ Ordered list of status items to display in the item.
+ , spacing :: !Int -- ^ Inner spacing between the status items.
+ , showLabels :: !Bool -- ^ Indicates whether text labels should be shown next
+ -- to the status icons.
+ , enableScrolling :: !Bool -- ^ If set to true, some settings like volume or
+ -- brightness can be adjusted by scrolling while
+ -- hovering over the status item.
+ , action :: !Action -- ^ Action invoked when the user clicks the item.
+ }
+ deriving (Show, Eq)
-- | An item to display in a status panel item.
data PanelStatusItem
diff --git a/app/Foobar/Services/Coalescing.hs b/app/Foobar/Services/Coalescing.hs
new file mode 100644
index 0000000..c44c5eb
--- /dev/null
+++ b/app/Foobar/Services/Coalescing.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedRecordDot, DuplicateRecordFields #-}
+
+-- |
+-- Module : Foobar.Services.Coalescing
+-- Description: Coalesce multiple events using a timeout.
+
+module Foobar.Services.Coalescing
+ ( EventScheduler
+ , mkEventScheduler
+ , scheduleEvent
+ ) where
+
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.Word (Word32)
+
+import qualified GI.GLib as GLib
+
+-- | Management structure for coalescing events.
+data EventScheduler
+ = EventScheduler { delay :: !Int
+ , sourceId :: !(IORef (Maybe Word32)) }
+
+-- | Create a new event scheduler for coalescing events.
+mkEventScheduler
+ :: Int -- ^ Delay for scheduling events in milliseconds.
+ -> IO EventScheduler -- ^ The event scheduler structure.
+mkEventScheduler delay' = EventScheduler delay' <$> newIORef Nothing
+
+-- | Schedule an event.
+--
+-- If there is already an event scheduled, the two are coalesced.
+scheduleEvent
+ :: EventScheduler -- ^ The event scheduler.
+ -> IO () -- ^ The event to be scheduled.
+ -> IO ()
+scheduleEvent s event = do
+ existingId <- readIORef s.sourceId
+ let sourceFunc :: GLib.SourceFunc
+ sourceFunc = do
+ event
+ writeIORef s.sourceId Nothing
+ pure GLib.SOURCE_REMOVE
+ case existingId of
+ Just _ -> pure ()
+ Nothing -> do newId <- GLib.timeoutAdd GLib.PRIORITY_DEFAULT (fromIntegral s.delay) sourceFunc
+ writeIORef s.sourceId (Just newId)
diff --git a/app/Foobar/Services/Config.hs b/app/Foobar/Services/Config.hs
index ed1a3ca..d2d3a75 100644
--- a/app/Foobar/Services/Config.hs
+++ b/app/Foobar/Services/Config.hs
@@ -1,14 +1,15 @@
-{-# LANGUAGE OverloadedStrings, OverloadedRecordDot, OverloadedLabels, DuplicateRecordFields, ScopedTypeVariables, FlexibleInstances, UndecidableInstances #-}
+{-# LANGUAGE OverloadedStrings, OverloadedRecordDot, OverloadedLabels, DuplicateRecordFields, ScopedTypeVariables,
+ FlexibleInstances, UndecidableInstances #-}
-- |
-- Module : Foobar.Services.Config
-- Description: Retrieval of data in the configuration file.
module Foobar.Services.Config
- ( observeConfiguration
- , loadConfiguration
- , initConfiguration
- , defaultConfiguration
+ ( observeConfig
+ , loadConfig
+ , initConfig
+ , defaultConfig
) where
import Data.Text hiding (filter, elem)
@@ -22,28 +23,24 @@ import Foreign.C (CString, CChar, peekCString, withCString)
import Foreign.Marshal.Alloc (free)
import Foreign (Ptr, nullPtr)
import Foobar.Models.Config
+import Foobar.Services.Coalescing
import qualified GI.GLib as GLib
import qualified GI.Gio as Gio
-foreign import ccall "realpath" cRealpath :: CString -> CString -> IO (Ptr CChar)
-
-getRealPath :: FilePath -> IO FilePath
-getRealPath p = withCString p $ \cPath -> do
- cRes <- cRealpath cPath nullPtr
- res <- if cRes /= nullPtr then peekCString cRes else pure p
- free cRes
- pure res
-
data MonitoringData
= MonitoringData { actualPath :: !FilePath
, actualMonitor :: !Gio.FileMonitor
, actualHandlerId :: !SignalHandlerId }
-observeConfiguration :: IO () -> IO ()
-observeConfiguration handler = do
+-- | Observe the config file for changes.
+observeConfig
+ :: IO () -- ^ Callback invoked whenever the config changes.
+ -> IO ()
+observeConfig handler = do
path <- getConfigPath
actualData <- newIORef (Nothing :: Maybe MonitoringData)
+ scheduler <- mkEventScheduler 250
let createMonitor :: FilePath -> IO (Maybe Gio.FileMonitor)
createMonitor p = do
file <- Gio.fileNewForPath p
@@ -53,8 +50,8 @@ observeConfiguration handler = do
Right m' -> Just m'
let actualMonitorHandler :: Gio.File -> Maybe Gio.File -> Gio.FileMonitorEvent -> IO ()
actualMonitorHandler _ _ eventType = case eventType of
- Gio.FileMonitorEventCreated -> handler
- Gio.FileMonitorEventChanged -> handler
+ Gio.FileMonitorEventCreated -> scheduleEvent scheduler handler
+ Gio.FileMonitorEventChanged -> scheduleEvent scheduler handler
_ -> pure ()
let clearActualMonitor :: IO ()
clearActualMonitor = do
@@ -76,9 +73,9 @@ observeConfiguration handler = do
realPath <- getRealPath path
let actualPath' = if realPath == path then Nothing else Just realPath
actualData' <- readIORef actualData
- when ((actualPath <$> actualData') == actualPath') $
- do clearActualMonitor
- setActualMonitor actualPath'
+ when ((actualPath <$> actualData') /= actualPath') $ do
+ clearActualMonitor
+ setActualMonitor actualPath'
let monitorHandler :: Gio.File -> Maybe Gio.File -> Gio.FileMonitorEvent -> IO ()
monitorHandler file otherFile eventType = do
updateActualMonitor
@@ -91,28 +88,31 @@ observeConfiguration handler = do
Nothing -> pure ()
-loadConfiguration :: IO Config
-loadConfiguration = do
+-- | Load the config file, falling back to a default config if it is invalid.
+loadConfig :: IO Config
+loadConfig = do
path <- getConfigPath
f <- GLib.keyFileNew
let process = do convertError (throw . ValidationError) $ f.loadFromFile path []
loadSection f ""
let handleError (ValidationError msg) = do putStrLn $ "Could not read config: " ++ unpack msg
- pure defaultConfiguration
+ pure defaultConfig
catch process handleError
-initConfiguration :: IO Config
-initConfiguration = do
+-- | Create the config file if it doesn't exist, otherwise read it.
+initConfig :: IO Config
+initConfig = do
path <- getConfigPath
exists <- GLib.fileTest path [GLib.FileTestExists]
- if exists then loadConfiguration
+ if exists then loadConfig
else do f <- GLib.keyFileNew
- storeSection f "" defaultConfiguration
+ storeSection f "" defaultConfig
void (try $ f.saveToFile (pack path) :: IO (Either GError ()))
- pure defaultConfiguration
+ pure defaultConfig
-defaultConfiguration :: Config
-defaultConfiguration =
+-- | The default configuration.
+defaultConfig :: Config
+defaultConfig =
Config { general = GeneralConfig { stylesheet = "resource:///foobar/styles/default.css" }
, panel = PanelConfig { position = ScreenEdgeLeft
, margin = 16
@@ -138,25 +138,25 @@ defaultConfiguration =
, spacing = 16
, closeButtonInset = -6
, timeFormat = "%H:%M" } }
- where panelItems = [ PanelItemIcon (PanelItemConfiguration { name = "launcher"
- , position = PanelItemPositionStart })
- (PanelIconConfiguration { iconName = "fluent-grid-dots-symbolic"
- , action = ActionLauncher })
- , PanelItemWorkspaces (PanelItemConfiguration { name = "workspaces"
- , position = PanelItemPositionStart })
- (PanelWorkspacesConfiguration { buttonSize = 20
- , spacing = 6 })
- , PanelItemClock (PanelItemConfiguration { name = "clock"
- , position = PanelItemPositionCenter })
- (PanelClockConfiguration { format = "%H\n%M"
- , action = ActionNone })
- , PanelItemStatus (PanelItemConfiguration { name = "status"
- , position = PanelItemPositionEnd })
- (PanelStatusConfiguration { items = statusItems
- , spacing = 6
- , showLabels = False
- , enableScrolling = False
- , action = ActionControlCenter }) ]
+ where panelItems = [ PanelItemIcon (PanelItemConfig { name = "launcher"
+ , position = PanelItemPositionStart })
+ (PanelIconConfig { iconName = "fluent-grid-dots-symbolic"
+ , action = ActionLauncher })
+ , PanelItemWorkspaces (PanelItemConfig { name = "workspaces"
+ , position = PanelItemPositionStart })
+ (PanelWorkspacesConfig { buttonSize = 20
+ , spacing = 6 })
+ , PanelItemClock (PanelItemConfig { name = "clock"
+ , position = PanelItemPositionCenter })
+ (PanelClockConfig { format = "%H\n%M"
+ , action = ActionNone })
+ , PanelItemStatus (PanelItemConfig { name = "status"
+ , position = PanelItemPositionEnd })
+ (PanelStatusConfig { items = statusItems
+ , spacing = 6
+ , showLabels = False
+ , enableScrolling = False
+ , action = ActionControlCenter }) ]
statusItems = [ PanelStatusItemBattery
, PanelStatusItemBrightness
, PanelStatusItemAudio
@@ -167,11 +167,14 @@ defaultConfiguration =
, ControlCenterRowAudioOutput
, ControlCenterRowBrightness ]
+-- | Config file path.
getConfigPath :: IO FilePath
getConfigPath = do
dir <- GLib.getUserConfigDir
pure (dir </> "foobar.conf")
+-- Validation errors
+
newtype ValidationError = ValidationError Text
deriving (Show)
@@ -182,6 +185,8 @@ instance Exception ValidationError
throwValidationError :: Text -> Text -> Text -> IO a
throwValidationError s k description = throw $ ValidationError $ s <> ":" <> k <> ": " <> description
+-- Key File Serialization
+
class KeyFileSection a where
loadSection :: GLib.KeyFile -> Text -> IO a
storeSection :: GLib.KeyFile -> Text -> a -> IO ()
@@ -287,41 +292,43 @@ instance KeyFileSection PanelItem where
storeSection f s x
storeSection f s y
-instance KeyFileSection PanelItemConfiguration where
- loadSection f s = PanelItemConfiguration n <$> loadValue f s "position" []
+instance KeyFileSection PanelItemConfig where
+ loadSection f s = PanelItemConfig n <$> loadValue f s "position" []
where n = Data.Text.drop 1 $ snd $ breakOn "." s
storeSection f s c = do storeValue f s "position" c.position
-instance KeyFileSection PanelIconConfiguration where
- loadSection f s = PanelIconConfiguration <$> loadValue f s "icon-name" []
- <*> loadValue f s "action" []
+instance KeyFileSection PanelIconConfig where
+ loadSection f s = PanelIconConfig <$> loadValue f s "icon-name" []
+ <*> loadValue f s "action" []
storeSection f s c = do storeValue f s "icon-name" c.iconName
storeValue f s "action" c.action
-instance KeyFileSection PanelClockConfiguration where
- loadSection f s = PanelClockConfiguration <$> loadValue f s "format" []
- <*> loadValue f s "action" []
+instance KeyFileSection PanelClockConfig where
+ loadSection f s = PanelClockConfig <$> loadValue f s "format" []
+ <*> loadValue f s "action" []
storeSection f s c = do storeValue f s "format" c.format
storeValue f s "action" c.action
-instance KeyFileSection PanelWorkspacesConfiguration where
- loadSection f s = PanelWorkspacesConfiguration <$> loadValue f s "button-size" [ validateNonNegative, validateNonZero ]
- <*> loadValue f s "spacing" [ validateNonNegative ]
+instance KeyFileSection PanelWorkspacesConfig where
+ loadSection f s = PanelWorkspacesConfig <$> loadValue f s "button-size" [ validateNonNegative, validateNonZero ]
+ <*> loadValue f s "spacing" [ validateNonNegative ]
storeSection f s c = do storeValue f s "button-size" c.buttonSize
storeValue f s "spacing" c.spacing
-instance KeyFileSection PanelStatusConfiguration where
- loadSection f s = PanelStatusConfiguration <$> loadValue f s "items" [ validateDistinct ]
- <*> loadValue f s "spacing" [ validateNonNegative ]
- <*> loadValue f s "show-labels" []
- <*> loadValue f s "enable-scrolling" []
- <*> loadValue f s "action" []
+instance KeyFileSection PanelStatusConfig where
+ loadSection f s = PanelStatusConfig <$> loadValue f s "items" [ validateDistinct ]
+ <*> loadValue f s "spacing" [ validateNonNegative ]
+ <*> loadValue f s "show-labels" []
+ <*> loadValue f s "enable-scrolling" []
+ <*> loadValue f s "action" []
storeSection f s c = do storeValue f s "items" c.items
storeValue f s "spacing" c.spacing
storeValue f s "show-labels" c.showLabels
storeValue f s "enable-scrolling" c.enableScrolling
storeValue f s "action" c.action
+-- Options
+
class (Bounded o, Enum o, Eq o) => Option o where
readOption :: Text -> Maybe o
showOption :: o -> Text
@@ -450,10 +457,22 @@ instance {-# OVERLAPS #-} Option o => KeyFileValue o where
validate s k validators val
storeValue f s k val = f.setString s k (showOption val)
-convertError :: (Text -> IO a) -> IO a -> IO a
+-- Helpers
+
+-- | Convert GErrors into other exceptions.
+convertError
+ :: (Text -> IO a) -- ^ Conversion function which throws an exception with a given message.
+ -> IO a -- ^ Operation possibly throwing an exception.
+ -> IO a -- ^ The same operation throwing the converted exception.
convertError t a = catch a (gerrorMessage >=> t)
-parseOption :: forall o. Option o => Text -> Text -> Text -> IO o
+-- | Parse an option string, otherwise throw a matching validation error.
+parseOption
+ :: forall o. Option o
+ => Text -- ^ Section name.
+ -> Text -- ^ Key name.
+ -> Text -- ^ Option value string.
+ -> IO o -- ^ The parsed option.
parseOption s k str = case readOption str of
Just o -> pure o
Nothing -> let values = [minBound..] :: [o] in
@@ -461,10 +480,18 @@ parseOption s k str = case readOption str of
let msg = "Invalid enum value \"" <> str <> "\" (allowed values: " <> allowed <> ")" in
throwValidationError s k msg
-validate :: Text -> Text -> [Validator a] -> a -> IO a
+-- | Apply validators to an input, throwing a matching validation error if one validator fails.
+validate
+ :: Text -- ^ Section name.
+ -> Text -- ^ Key name.
+ -> [Validator a] -- ^ List of validators.
+ -> a -- ^ The input.
+ -> IO a -- ^ The validated input.
validate s k validators val = val <$ forM_ validators (singleValidate val)
where singleValidate val' v = v val' >>= mapM_ (throwValidationError s k)
+-- Validators
+
validateFileURL :: Validator Text
validateFileURL url = do
f <- Gio.fileNewForUri url
@@ -484,3 +511,14 @@ validateDistinct :: Option o => Validator [o]
validateDistinct (x:xs) = if x `elem` xs then pure $ Just $ "Duplicate value \"" <> showOption x <> "\" not allowed."
else validateDistinct xs
validateDistinct [] = pure Nothing
+
+-- C Wrappers
+
+foreign import ccall "realpath" cRealpath :: CString -> CString -> IO (Ptr CChar)
+
+getRealPath :: FilePath -> IO FilePath
+getRealPath p = withCString p $ \cPath -> do
+ cRes <- cRealpath cPath nullPtr
+ res <- if cRes /= nullPtr then peekCString cRes else pure p
+ free cRes
+ pure res
diff --git a/app/foobar.cabal b/app/foobar.cabal
index c48349c..f4841cf 100644
--- a/app/foobar.cabal
+++ b/app/foobar.cabal
@@ -25,5 +25,6 @@ executable foobar
directory
other-modules: Foobar.Application,
Foobar.Models.Config,
- Foobar.Services.Config
+ Foobar.Services.Config,
+ Foobar.Services.Coalescing
default-language: Haskell2010