packages feed

taffybar-5.2.0: src/System/Taffybar/Information/Udev.hs

{-# LANGUAGE ForeignFunctionInterface #-}

-- | Minimal libudev bindings used for event-driven backlight monitoring.
--
-- We only bind what we need for Waybar-style monitoring:
-- 1. Subscribe to udev netlink events for the "backlight" subsystem.
-- 2. Expose the monitor fd so callers can block (with a timeout) using
--    'GHC.Conc.threadWaitRead'.
-- 3. Drain pending udev events via 'udev_monitor_receive_device'.
module System.Taffybar.Information.Udev
  ( UdevBacklightMonitor,
    openBacklightMonitor,
    closeBacklightMonitor,
    backlightMonitorFd,
    drainBacklightMonitor,
  )
where

import Control.Exception (bracketOnError, throwIO)
import Control.Monad (void, when)
import Foreign hiding (void)
import Foreign.C
import System.Posix.Types (Fd (..))

data Udev

data UdevMonitor

data UdevDevice

foreign import ccall unsafe "udev_new"
  c_udev_new :: IO (Ptr Udev)

foreign import ccall unsafe "udev_unref"
  c_udev_unref :: Ptr Udev -> IO (Ptr Udev)

foreign import ccall unsafe "udev_monitor_new_from_netlink"
  c_udev_monitor_new_from_netlink :: Ptr Udev -> CString -> IO (Ptr UdevMonitor)

foreign import ccall unsafe "udev_monitor_unref"
  c_udev_monitor_unref :: Ptr UdevMonitor -> IO (Ptr UdevMonitor)

foreign import ccall unsafe "udev_monitor_filter_add_match_subsystem_devtype"
  c_udev_monitor_filter_add_match_subsystem_devtype ::
    Ptr UdevMonitor ->
    CString ->
    CString -> -- nullable
    IO CInt

foreign import ccall unsafe "udev_monitor_enable_receiving"
  c_udev_monitor_enable_receiving :: Ptr UdevMonitor -> IO CInt

foreign import ccall unsafe "udev_monitor_get_fd"
  c_udev_monitor_get_fd :: Ptr UdevMonitor -> IO CInt

foreign import ccall unsafe "udev_monitor_receive_device"
  c_udev_monitor_receive_device :: Ptr UdevMonitor -> IO (Ptr UdevDevice)

foreign import ccall unsafe "udev_device_unref"
  c_udev_device_unref :: Ptr UdevDevice -> IO (Ptr UdevDevice)

data UdevBacklightMonitor = UdevBacklightMonitor
  { monitorUdev :: Ptr Udev,
    monitorHandle :: Ptr UdevMonitor,
    monitorFd :: Fd
  }

openBacklightMonitor :: IO UdevBacklightMonitor
openBacklightMonitor = do
  udev <- c_udev_new
  whenNull udev "udev_new failed"
  bracketOnError (pure udev) (void . c_udev_unref) $ \udev' ->
    withCString "udev" $ \netlinkName -> do
      mon <- c_udev_monitor_new_from_netlink udev' netlinkName
      whenNull mon "udev_monitor_new_from_netlink failed"
      bracketOnError (pure mon) (void . c_udev_monitor_unref) $ \mon' -> do
        withCString "backlight" $ \subsystem -> do
          rc <- c_udev_monitor_filter_add_match_subsystem_devtype mon' subsystem nullPtr
          whenNeg rc "udev_monitor_filter_add_match_subsystem_devtype failed"
        rc2 <- c_udev_monitor_enable_receiving mon'
        whenNeg rc2 "udev_monitor_enable_receiving failed"
        fdC <- c_udev_monitor_get_fd mon'
        whenNeg fdC "udev_monitor_get_fd failed"
        pure $
          UdevBacklightMonitor
            { monitorUdev = udev',
              monitorHandle = mon',
              monitorFd = Fd fdC
            }
  where
    whenNull p msg = when (p == nullPtr) $ throwIO (userError msg)
    whenNeg rc msg = when (rc < 0) $ throwIO (userError msg)

closeBacklightMonitor :: UdevBacklightMonitor -> IO ()
closeBacklightMonitor mon = do
  void $ c_udev_monitor_unref (monitorHandle mon)
  void $ c_udev_unref (monitorUdev mon)

backlightMonitorFd :: UdevBacklightMonitor -> Fd
backlightMonitorFd = monitorFd

-- | Drain pending udev events from the monitor.
--
-- Call this after the monitor fd becomes readable.
drainBacklightMonitor :: UdevBacklightMonitor -> IO ()
drainBacklightMonitor mon = do
  -- Only consume a single event per wake; if multiple are queued, the fd will
  -- stay readable and we'll be woken again.
  dev <- c_udev_monitor_receive_device (monitorHandle mon)
  if dev == nullPtr
    then pure ()
    else void $ c_udev_device_unref dev