taffybar-7.2.0: src/System/Taffybar/Information/Workspaces/Model.hs
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Information.Workspaces.Model
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- Backend-agnostic workspace/window model shared by workspace-related widgets.
module System.Taffybar.Information.Workspaces.Model
( WorkspaceBackend (..),
WorkspaceSnapshot (..),
WorkspaceEventBatch (..),
WorkspaceEvent (..),
WindowEvent (..),
diffWorkspaceSnapshots,
WorkspaceViewState (..),
WorkspaceIdentity (..),
WorkspaceInfo (..),
WindowIdentity (..),
WindowInfo (..),
WorkspacesRefreshTarget (..),
bumpWorkspaceRefreshTarget,
preserveWorkspaceUpdateRevisions,
)
where
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Data.Word (Word64)
data WorkspaceBackend
= WorkspaceBackendEWMH
| WorkspaceBackendHyprland
deriving (Eq, Show)
-- | Snapshot of workspace state produced by a backend provider.
data WorkspaceSnapshot = WorkspaceSnapshot
{ snapshotBackend :: WorkspaceBackend,
snapshotRevision :: Word64,
-- | Whether window-level data was complete in this snapshot.
snapshotWindowDataComplete :: Bool,
snapshotWorkspaces :: [WorkspaceInfo]
}
deriving (Eq, Show)
-- | Delta events corresponding to a snapshot revision.
--
-- Consumers that want incremental updates can subscribe to these events,
-- while snapshot consumers keep using 'WorkspaceSnapshot'.
data WorkspaceEventBatch = WorkspaceEventBatch
{ eventBatchBackend :: WorkspaceBackend,
eventBatchRevision :: Word64,
eventBatchWindowDataComplete :: Bool,
eventBatchEvents :: [WorkspaceEvent]
}
deriving (Eq, Show)
data WorkspaceEvent
= WorkspaceOrderChanged [WorkspaceIdentity]
| WorkspaceAdded WorkspaceInfo
| WorkspaceRemoved WorkspaceIdentity
| WorkspaceChanged WorkspaceInfo
| WorkspaceWindowsChanged WorkspaceIdentity [WindowEvent]
deriving (Eq, Show)
data WindowEvent
= WindowAdded WindowInfo
| WindowRemoved WindowIdentity
| WindowChanged WindowInfo
deriving (Eq, Show)
-- | Visibility/occupancy state that controls workspace presentation.
data WorkspaceViewState
= WorkspaceActive
| WorkspaceVisible
| WorkspaceHidden
| WorkspaceEmpty
deriving (Eq, Show)
-- | Backend-neutral workspace identity.
data WorkspaceIdentity = WorkspaceIdentity
{ workspaceNumericId :: Maybe Int,
workspaceName :: Text
}
deriving (Eq, Ord, Show)
-- | Backend-neutral workspace information.
data WorkspaceInfo = WorkspaceInfo
{ workspaceIdentity :: WorkspaceIdentity,
workspaceUpdateRevision :: Word64,
workspaceState :: WorkspaceViewState,
workspaceHasUrgentWindow :: Bool,
workspaceIsSpecial :: Bool,
workspaceWindows :: [WindowInfo]
}
deriving (Eq, Show)
data WindowIdentity
= X11WindowIdentity Word64
| HyprlandWindowIdentity Text
deriving (Eq, Ord, Show)
-- | Backend-neutral window information.
data WindowInfo = WindowInfo
{ windowIdentity :: WindowIdentity,
windowUpdateRevision :: Word64,
windowTitle :: Text,
-- | Ordered class/app-id hints used for icon lookup.
windowClassHints :: [Text],
-- | Top-left position in global coordinates when available.
windowPosition :: Maybe (Int, Int),
windowUrgent :: Bool,
windowActive :: Bool,
windowMinimized :: Bool,
windowPinned :: Bool
}
deriving (Eq, Show)
data WorkspacesRefreshTarget
= RefreshAllWorkspaces
| RefreshWorkspace WorkspaceIdentity
| RefreshWindow WindowIdentity
deriving (Eq, Show)
bumpWorkspaceRefreshTarget :: WorkspacesRefreshTarget -> WorkspaceSnapshot -> WorkspaceSnapshot
bumpWorkspaceRefreshTarget target snapshot =
snapshot {snapshotWorkspaces = map bumpWorkspaceForTarget (snapshotWorkspaces snapshot)}
where
bumpWorkspaceRevision workspace =
workspace {workspaceUpdateRevision = workspaceUpdateRevision workspace + 1}
bumpWindowRevision window =
window {windowUpdateRevision = windowUpdateRevision window + 1}
bumpWorkspaceAndWindows workspace =
bumpWorkspaceRevision
workspace
{ workspaceWindows = map bumpWindowRevision (workspaceWindows workspace)
}
bumpWindowForTarget targetWindow window
| windowIdentity window == targetWindow = bumpWindowRevision window
| otherwise = window
bumpWorkspaceForTarget workspace =
case target of
RefreshAllWorkspaces -> bumpWorkspaceAndWindows workspace
RefreshWorkspace targetWorkspace
| workspaceIdentity workspace == targetWorkspace -> bumpWorkspaceAndWindows workspace
| otherwise -> workspace
RefreshWindow targetWindow ->
workspace {workspaceWindows = map (bumpWindowForTarget targetWindow) (workspaceWindows workspace)}
preserveWorkspaceUpdateRevisions :: WorkspaceSnapshot -> WorkspaceSnapshot -> WorkspaceSnapshot
preserveWorkspaceUpdateRevisions previous next =
next {snapshotWorkspaces = map preserveWorkspaceRevision (snapshotWorkspaces next)}
where
previousWorkspaces =
M.fromList
[ (workspaceIdentity workspace, workspace)
| workspace <- snapshotWorkspaces previous
]
previousWindows =
M.fromList
[ (windowIdentity window, window)
| workspace <- snapshotWorkspaces previous,
window <- workspaceWindows workspace
]
preserveWindowRevision window =
case M.lookup (windowIdentity window) previousWindows of
Just previousWindow ->
window {windowUpdateRevision = windowUpdateRevision previousWindow}
Nothing -> window
preserveWorkspaceRevision workspace =
let withWindows =
workspace
{ workspaceWindows = map preserveWindowRevision (workspaceWindows workspace)
}
in case M.lookup (workspaceIdentity workspace) previousWorkspaces of
Just previousWorkspace ->
withWindows
{ workspaceUpdateRevision = workspaceUpdateRevision previousWorkspace
}
Nothing -> withWindows
-- | Produce incremental events that describe the change from an old snapshot to
-- a new one.
diffWorkspaceSnapshots :: WorkspaceSnapshot -> WorkspaceSnapshot -> [WorkspaceEvent]
diffWorkspaceSnapshots previous next =
orderEvent ++ removedEvents ++ addedEvents ++ changedEvents
where
oldWorkspaces = snapshotWorkspaces previous
newWorkspaces = snapshotWorkspaces next
oldOrder = map workspaceIdentity oldWorkspaces
newOrder = map workspaceIdentity newWorkspaces
orderEvent =
[WorkspaceOrderChanged newOrder | oldOrder /= newOrder]
oldMap = M.fromList [(workspaceIdentity ws, ws) | ws <- oldWorkspaces]
newMap = M.fromList [(workspaceIdentity ws, ws) | ws <- newWorkspaces]
removedEvents =
[WorkspaceRemoved wsId | wsId <- M.keys oldMap, M.notMember wsId newMap]
addedEvents =
[WorkspaceAdded ws | (wsId, ws) <- M.toList newMap, M.notMember wsId oldMap]
changedEvents =
concatMap mkChangedEvents sharedIds
sharedIds = M.keys $ M.intersection oldMap newMap
mkChangedEvents wsId =
case (M.lookup wsId oldMap, M.lookup wsId newMap) of
(Just oldWs, Just newWs) ->
let oldWithoutWindows = oldWs {workspaceWindows = []}
newWithoutWindows = newWs {workspaceWindows = []}
metadataEvents =
[WorkspaceChanged newWs | oldWithoutWindows /= newWithoutWindows]
windowEvents = diffWorkspaceWindows oldWs newWs
windowsChangedEvents =
[ WorkspaceWindowsChanged wsId windowEvents
| not (null windowEvents)
]
in metadataEvents ++ windowsChangedEvents
_ -> []
diffWorkspaceWindows :: WorkspaceInfo -> WorkspaceInfo -> [WindowEvent]
diffWorkspaceWindows oldWs newWs =
removedEvents ++ addedEvents ++ changedEvents
where
oldWindows = workspaceWindows oldWs
newWindows = workspaceWindows newWs
oldMap = M.fromList [(windowIdentity w, w) | w <- oldWindows]
newMap = M.fromList [(windowIdentity w, w) | w <- newWindows]
removedEvents =
[WindowRemoved wId | wId <- M.keys oldMap, M.notMember wId newMap]
addedEvents =
[WindowAdded w | (wId, w) <- M.toList newMap, M.notMember wId oldMap]
changedEvents =
[ WindowChanged newW
| wId <- M.keys $ M.intersection oldMap newMap,
Just oldW <- [M.lookup wId oldMap],
Just newW <- [M.lookup wId newMap],
oldW /= newW
]