packages feed

taffybar-4.1.2: src/System/Taffybar/Information/KeyboardState.hs

{-# LANGUAGE OverloadedStrings #-}

--------------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.Information.KeyboardState
-- Copyright   : (c) Ivan Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan Malison <IvanMalison@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- Provides information about keyboard lock states (Caps Lock, Num Lock,
-- Scroll Lock) by reading LED brightness values from sysfs.
--
--------------------------------------------------------------------------------

module System.Taffybar.Information.KeyboardState
  ( KeyboardState(..)
  , getKeyboardState
  , defaultKeyboardState
  , findLedPath
  , defaultLedBasePath
  ) where

import Control.Exception (catch, SomeException)
import Data.List (find)
import System.Directory (listDirectory, doesFileExist)
import System.FilePath ((</>))

-- | Represents the state of keyboard lock keys.
data KeyboardState = KeyboardState
  { capsLock :: Bool
  , numLock :: Bool
  , scrollLock :: Bool
  } deriving (Eq, Show)

-- | Default keyboard state with all locks off.
defaultKeyboardState :: KeyboardState
defaultKeyboardState = KeyboardState False False False

-- | Default base path for LED sysfs entries.
defaultLedBasePath :: FilePath
defaultLedBasePath = "/sys/class/leds"

-- | Find the LED path for a given lock type by scanning sysfs.
-- Returns the full path to the brightness file, or Nothing if not found.
findLedPath :: FilePath -> String -> IO (Maybe FilePath)
findLedPath basePath lockType = do
  entries <- listDirectory basePath `catch` \(_ :: SomeException) -> return []
  let matchingEntry = find (matchesLockType lockType) entries
  case matchingEntry of
    Nothing -> return Nothing
    Just entry -> do
      let brightnessPath = basePath </> entry </> "brightness"
      exists <- doesFileExist brightnessPath
      return $ if exists then Just brightnessPath else Nothing

-- | Check if an LED entry name matches a lock type.
-- Matches patterns like "input15::capslock" for lock type "capslock".
matchesLockType :: String -> String -> Bool
matchesLockType lockType entry =
  ("::" ++ lockType) `isSuffixOf` entry || (":" ++ lockType) `isSuffixOf` entry
  where
    isSuffixOf :: String -> String -> Bool
    isSuffixOf suffix str = drop (length str - length suffix) str == suffix

-- | Read the brightness value from a sysfs LED file.
-- Returns True if the LED is on (brightness > 0), False otherwise.
readLedState :: FilePath -> IO Bool
readLedState path = do
  content <- readFile path `catch` \(_ :: SomeException) -> return "0"
  let value = reads (filter (/= '\n') content) :: [(Int, String)]
  case value of
    [(n, _)] -> return (n > 0)
    _ -> return False

-- | Get the current keyboard state by reading LED brightness from sysfs.
-- Uses the default LED base path ("/sys/class/leds").
-- Returns 'defaultKeyboardState' if LED files cannot be found.
getKeyboardState :: IO KeyboardState
getKeyboardState = getKeyboardStateFromPath defaultLedBasePath

-- | Get the keyboard state from a custom sysfs path.
getKeyboardStateFromPath :: FilePath -> IO KeyboardState
getKeyboardStateFromPath basePath = do
  capsPath <- findLedPath basePath "capslock"
  numPath <- findLedPath basePath "numlock"
  scrollPath <- findLedPath basePath "scrolllock"

  capsState <- maybe (return False) readLedState capsPath
  numState <- maybe (return False) readLedState numPath
  scrollState <- maybe (return False) readLedState scrollPath

  return KeyboardState
    { capsLock = capsState
    , numLock = numState
    , scrollLock = scrollState
    }