module Rahm.Desktop.History ( zipperDbgPrint, pushZipper, getZipper, zipperBack, zipperForward, historyHook, nextLocation, jumpToLastLocation, lastLocation, historyBack, historyForward, ) where import Control.Monad (forM_, when) import Data.Default (Default (..)) import Data.Foldable (find, toList) import Data.Map (Map) import qualified Data.Map as Map import Data.Maybe (catMaybes, fromMaybe, isJust) import Data.Sequence (Seq (..)) import qualified Data.Sequence as Seq (length, (!?)) import Rahm.Desktop.Common (Location (Location, locationWindow), focusLocation, getCurrentScreen, getCurrentWorkspace, locationWorkspace) import Rahm.Desktop.Hooks.WindowChange import Rahm.Desktop.Logger import Text.Printf (printf) import XMonad (ExtensionClass (extensionType, initialValue), ScreenId, StateExtension (..), Window, X, withWindowSet) import XMonad.StackSet import qualified XMonad.Util.ExtensibleState as XS ( get, modify, put, ) data BoundedSeqZipper a = BoundedSeqZipper Int (Seq a) (Seq a) deriving (Eq, Show, Ord, Read) instance Functor BoundedSeqZipper where fmap fn (BoundedSeqZipper i h t) = BoundedSeqZipper i (fmap fn h) (fmap fn t) popSeqZipper :: BoundedSeqZipper a -> BoundedSeqZipper a popSeqZipper (BoundedSeqZipper n h (c :<| t)) = BoundedSeqZipper n h t popSeqZipper a = a zipperDbgPrint :: BoundedSeqZipper Location -> String zipperDbgPrint (BoundedSeqZipper _ h (c :<| t)) = concat $ map (printf " %s " . showLoc) (toList h) ++ [printf "[%s]" (showLoc c)] ++ map (printf " %s " . showLoc) (toList t) where showLoc :: Location -> String showLoc (Location workspaceId windowId) = printf "%s@%s" (maybe "nil" show windowId) workspaceId zipperDbgPrint _ = "" pushZipper :: a -> BoundedSeqZipper a -> BoundedSeqZipper a pushZipper e (BoundedSeqZipper maxSize _ (tail :|> _)) | maxSize <= Seq.length tail = BoundedSeqZipper maxSize mempty (e :<| tail) pushZipper e (BoundedSeqZipper maxSize _ tail) = BoundedSeqZipper maxSize mempty (e :<| tail) getZipper :: BoundedSeqZipper a -> Maybe a getZipper (BoundedSeqZipper _ _ (e :<| _)) = Just e getZipper _ = Nothing emptyZipper :: BoundedSeqZipper a emptyZipper = BoundedSeqZipper 1000 mempty mempty zipperBack :: BoundedSeqZipper a -> BoundedSeqZipper a zipperBack (BoundedSeqZipper s h (e :<| t)) = BoundedSeqZipper s (e :<| h) t zipperBack b = b zipperForward :: BoundedSeqZipper a -> BoundedSeqZipper a zipperForward (BoundedSeqZipper s (e :<| h) t) = BoundedSeqZipper s h (e :<| t) zipperForward b = b newtype History = History { historyByScreenId :: Map ScreenId (BoundedSeqZipper Location) } deriving (Read, Show) instance Default History where def = History mempty instance ExtensionClass History where initialValue = def extensionType = PersistentExtension getCurrentZipper :: ScreenId -> History -> BoundedSeqZipper Location getCurrentZipper screenId (History byScreenId) = fromMaybe emptyZipper (Map.lookup screenId byScreenId) historyBack :: X () historyBack = historyDo zipperBack historyForward :: X () historyForward = historyDo zipperForward jumpToLastLocation :: X () jumpToLastLocation = do logs Trace "Jumping to Last Location." mapM_ focusLocation =<< lastLocation -- Get the last location for the current screen. If lastLocation :: X (Maybe Location) lastLocation = do screenId <- getCurrentScreen ret <- getZipper . getCurrentZipper screenId <$> XS.get XS.modify $ \(History byScreen) -> History (Map.adjust popSeqZipper screenId byScreen) t <- withWindowSet $ \ws -> return $ flip findTag ws =<< (locationWindow =<< ret) -- The last location should not return the last location. if t == Just "*" || (locationWorkspace <$> ret) == Just "*" then lastLocation else return ret nextLocation :: X (Maybe Location) nextLocation = do screenId <- getCurrentScreen getZipper . zipperForward . getCurrentZipper screenId <$> XS.get dbgLogHistory :: X () dbgLogHistory = do (History byScreen) <- XS.get logs Trace "History State: \n" forM_ (Map.toList byScreen) $ \(screenId, hist) -> logs Trace "%s -> %s\n" (show screenId) (zipperDbgPrint hist) data ScreenDiff = ScreenDiff { scrId :: ScreenId, oldLocation :: Location, newLocation :: Location } historyHook :: StackChangeHook -- History hook where the 'from' location workspace does not match the 'to' -- location workspace. historyHook = StackChangeHook $ \lastWindowSet currentWindowSet -> do (History hist) <- XS.get forM_ (getScreenDiffs lastWindowSet currentWindowSet) $ -- Read as "the screen went from to " \(ScreenDiff sid oloc nloc) -> let (ows, nws) = (locationWorkspace oloc, locationWorkspace nloc) in -- The goal here is to preserve history in as intuitive a way as possible -- When the stackset changes, for each screen that changed in the last -- windowchange, one of 2 situations are possibel: -- -- 1. The workspace on the screen was swapped with an already visible -- screen -- -- 2. The workspace on the screen was swapped with a hidden workspace. -- -- In the case of 1, we want to treat it as if the screen was -- "reseated" to a different monitor, preserving the history for that -- screen on its new screen. -- -- In case of 2, we want to add the old workspace to the history of the -- screen that changed. case () of () | nws `visibleIn` lastWindowSet, (Just oscr) <- screenOf nws lastWindowSet -> -- The last workspace was on a different screen. Swap the current -- screen's history with the history from the last screen the -- workspace was on. XS.modify $ \(History byScreen) -> History ( Map.alter (const $ Map.lookup oscr hist) sid byScreen ) -- The new workspace was not originally visible, add to history () | not (nws `visibleIn` lastWindowSet) -> XS.modify $ \(History byScreen) -> History ( Map.alter (Just . pushZipper oloc . fromMaybe emptyZipper) sid byScreen ) -- This is typically not a possible case. It's only possible when a -- screen is unplugged. If that's the case, do nothing. _ -> return () dbgLogHistory where -- Returns a list of "screen diffs", which are a record of which screens -- changed and how they changed. getScreenDiffs os ns = catMaybes $ Map.elems $ Map.intersectionWithKey ( \screenId (Screen ow@(Workspace ot _ _) _ _) (Screen nw@(Workspace nt _ _) _ _) -> case () of () | ot == nt -> Nothing _ -> Just (ScreenDiff screenId (wsToLoc ow) (wsToLoc nw)) ) (screenMap os) (screenMap ns) wsToLoc (Workspace t _ (fmap focus -> win)) = Location t win screenMap (StackSet ocur ovis _ _) = Map.fromList $ map (\s -> (screen s, s)) (ocur : ovis) mapSwap k1 k2 map = Map.alter (const $ Map.lookup k2 map) k1 $ Map.alter (const $ Map.lookup k1 map) k2 map visibleIn wsId ws = isJust $ screenOf wsId ws screenOf wsId ws@(StackSet cur vis _ _) = screen <$> find ((== wsId) . tag . workspace) (cur : vis) getWindowsetData ws@(StackSet cur vis hidden floating) = (screen cur, tag $ workspace cur, focus <$> stack (workspace cur)) historyDo :: (forall a. BoundedSeqZipper a -> BoundedSeqZipper a) -> X () historyDo f = do screenId <- getCurrentScreen History byScreenId <- XS.get let mz' = Map.lookup screenId byScreenId case mz' of Nothing -> return () Just z -> do let z' = f z mapM_ focusLocation (getZipper z') XS.put (History (Map.insert screenId z' byScreenId))