diff options
-rw-r--r-- | Operations.hs | 22 | ||||
-rw-r--r-- | StackSet.hs | 101 | ||||
-rw-r--r-- | tests/Properties.hs | 123 |
3 files changed, 120 insertions, 126 deletions
diff --git a/Operations.hs b/Operations.hs index 678206e..37dbafe 100644 --- a/Operations.hs +++ b/Operations.hs @@ -65,11 +65,12 @@ shift n = withFocused hide >> windows (W.shift n) -- | view. Change the current workspace to workspace at offset n (0 indexed). view :: WorkspaceId -> X () -view n = withWorkspace $ \w -> when (n /= (W.tag (W.current w))) $ do +view n = withWorkspace $ \old -> when (n /= (W.tag (W.workspace (W.current old)))) $ do windows $ W.view n -- move in new workspace first, to avoid flicker + -- Hide the old workspace if it is no longer visible - oldWsNotVisible <- (not . M.member (W.tag . W.current $ w) . W.screens) `liftM` gets windowset - when oldWsNotVisible $ mapM_ hide (W.index w) + oldWsNotVisible <- liftM (notElem (W.current old)) (gets (W.visible . windowset)) + when oldWsNotVisible $ mapM_ hide (W.index old) clearEnterEvents -- better clear any events from the old workspace -- | Kill the currently focused client. If we do kill it, we'll get a @@ -114,12 +115,13 @@ refresh = do XConf { xineScreens = xinesc, display = d } <- ask -- for each workspace, layout the currently visible workspaces - flip mapM_ (M.assocs (W.screens ws)) $ \(n, scn) -> do - let this = W.view n ws + (`mapM_` (W.current ws : W.visible ws)) $ \w -> do + let n = W.tag (W.workspace w) + this = W.view n ws Just l = fmap fst $ M.lookup n fls -- now tile the windows on this workspace - rs <- doLayout l (genericIndex xinesc scn) (W.index this) - mapM_ (\(w,rect) -> io (tileWindow d w rect)) rs + rs <- doLayout l (genericIndex xinesc (W.screen w)) (W.index this) + mapM_ (\(win,rect) -> io (tileWindow d win rect)) rs -- and raise the focused window if there is one. whenJust (W.peek this) $ io . raiseWindow d @@ -178,8 +180,8 @@ setFocusX w = withWorkspace $ \ws -> do XConf { display = dpy , normalBorder = nbc, focusedBorder = fbc } <- ask -- clear mouse button grab and border on other windows - (`mapM_` (M.keys . W.screens $ ws)) $ \n -> do - (`mapM_` (W.index (W.view n ws))) $ \otherw -> do + (`mapM_` (W.current ws : W.visible ws)) $ \wk -> do + (`mapM_` (W.index (W.view (W.tag (W.workspace wk)) ws))) $ \otherw -> do setButtonGrab True otherw io $ setWindowBorder dpy otherw (color_pixel nbc) @@ -282,7 +284,7 @@ splitVerticallyBy f r = (\(a,b)->(mirrorRect a,mirrorRect b)) $ splitHorizontall layout :: ((Layout, [Layout]) -> (Layout, [Layout])) -> X () layout f = do modify $ \s -> - let n = W.tag . W.current . windowset $ s + let n = W.tag . W.workspace . W.current . windowset $ s (Just fl) = M.lookup n $ layouts s in s { layouts = M.insert n (f fl) (layouts s) } refresh diff --git a/StackSet.hs b/StackSet.hs index 9401bbc..747ee6a 100644 --- a/StackSet.hs +++ b/StackSet.hs @@ -75,13 +75,13 @@ -- 'delete'. -- module StackSet ( - StackSet(..), Workspace(..), Stack(..), + StackSet(..), Workspace(..), Screen(..), Stack(..), new, view, lookupWorkspace, peek, index, focusLeft, focusRight, focusWindow, member, findIndex, insertLeft, delete, swap, shift ) where -import qualified Data.Map as M import Data.Maybe (listToMaybe) +import qualified Data.List as L (delete,find,genericSplitAt) -- API changes from xmonad 0.1: @@ -103,15 +103,22 @@ import Data.Maybe (listToMaybe) ------------------------------------------------------------------------ -- --- A cursor into a non-empty list of workspaces. +-- A cursor into a non-empty list of workspaces. +-- We puncture the workspace list, producing a hole in the structure +-- used to track the currently focused workspace. The two other lists +-- that are produced are used to track those workspaces visible as +-- Xinerama screens, and those workspaces not visible anywhere. -- -data StackSet i a screen = - StackSet { size :: !i -- number of workspaces - , current :: !(Workspace i a) -- currently focused workspace - , prev :: [Workspace i a] -- workspaces to the left - , next :: [Workspace i a] -- workspaces to the right - , screens :: M.Map i screen -- a map of visible workspaces to their screens - } deriving (Show, Eq) +data StackSet i a sid = + StackSet { size :: !i -- number of workspaces + , current :: !(Screen i a sid) -- currently focused workspace + , visible :: [Screen i a sid] -- non-focused workspaces, visible in xinerama + , hidden :: [Workspace i a] -- workspaces not visible anywhere + } deriving (Show, Eq) + +-- Visible workspaces, and their Xinerama screens. +data Screen i a sid = Screen { workspace :: !(Workspace i a), screen :: !sid } + deriving (Show, Eq) -- -- A workspace is just a tag - its index - and a stack @@ -119,8 +126,6 @@ data StackSet i a screen = data Workspace i a = Workspace { tag :: !i, stack :: Stack a } deriving (Show, Eq) --- TODO an unmanaged floating layer would go in here somewhere (a 2nd stack?) - -- -- A stack is a cursor onto a (possibly empty) window list. -- The data structure tracks focus by construction, and @@ -149,40 +154,41 @@ data Stack a = Empty -- Xinerama: Virtual workspaces are assigned to physical screens, starting at 0. -- new :: (Integral i, Integral s) => i -> s -> StackSet i a s -new n m | n > 0 && m > 0 = StackSet n h [] ts xine +new n m | n > 0 && m > 0 = StackSet n cur visi unseen | otherwise = error "non-positive arguments to StackSet.new" - where (h:ts) = Workspace 0 Empty : [ Workspace i Empty | i <- [1 ..n-1]] - xine = M.fromList [ (fromIntegral s, s) | s <- [0 .. m-1] ] + + where (seen,unseen) = L.genericSplitAt m $ Workspace 0 Empty : [ Workspace i Empty | i <- [1 ..n-1]] + (cur:visi) = [ Screen i s | (i,s) <- zip seen [0..] ] + -- now zip up visibles with their screen id -- -- /O(w)/. Set focus to the workspace with index 'i'. -- If the index is out of range, return the original StackSet. -- -- Xinerama: If the workspace is not visible on any Xinerama screen, it --- is raised on the current screen. If it is already visible, focus is +-- becomes the current screen. If it is in the visible list, it becomes +-- current. + +-- is raised to the current screen. If it is already visible, focus is -- just moved. -- -view :: Integral i => i -> StackSet i a s -> StackSet i a s -view i s@(StackSet sz (Workspace n _) _ _ scrs) - | i >= 0 && i < sz - = setCurrent $ if M.member i scrs - then s -- already visisble. just set current. - else case M.lookup n scrs of -- TODO current should always be valid - Nothing -> error "xmonad:view: No physical screen" - Just sc -> s { screens = M.insert i sc (M.delete n scrs) } - | otherwise = s +view :: (Eq i, Eq a, Eq s, Integral i) => i -> StackSet i a s -> StackSet i a s +view i s + | i < 0 && i >= size s || i == tag (workspace (current s)) = s -- out of bounds or current + + | Just x <- L.find ((i==).tag.workspace) (visible s) + -- if it is visible, it is just raised + = s { current = x, visible = current s : L.delete x (visible s) } - -- actually moving focus is easy: - where setCurrent x = foldr traverse x [1..abs (i-n)] + | Just x <- L.find ((i==).tag) (hidden s) + -- if it was hidden, it is raised on the xine screen currently used + = s { current = Screen x (screen (current s)) + , hidden = workspace (current s) : L.delete x (hidden s) } - -- work out which direction to move - traverse _ = if signum (i-n) >= 0 then viewRight else viewLeft + | otherwise = error "Inconsistent StackSet: workspace not found" - -- /O(1)/. Move workspace focus left or right one node, a la Huet. - viewLeft (StackSet m t (l:ls) rs sc) = StackSet m l ls (t:rs) sc - viewLeft t = t - viewRight (StackSet m t ls (r:rs) sc) = StackSet m r (t:ls) rs sc - viewRight t = t + -- 'Catch'ing this might be hard. Relies on monotonically increasing + -- workspace tags defined in 'new' -- --------------------------------------------------------------------- -- Xinerama operations @@ -190,7 +196,7 @@ view i s@(StackSet sz (Workspace n _) _ _ scrs) -- | Find the tag of the workspace visible on Xinerama screen 'sc'. -- Nothing if screen is out of bounds. lookupWorkspace :: Eq s => s -> StackSet i a s -> Maybe i -lookupWorkspace sc w = listToMaybe [ i | (i,s) <- M.assocs (screens w), s == sc ] +lookupWorkspace sc w = listToMaybe [ tag i | Screen i s <- current w : visible w, s == sc ] -- --------------------------------------------------------------------- -- Operations on the current stack @@ -202,7 +208,7 @@ lookupWorkspace sc w = listToMaybe [ i | (i,s) <- M.assocs (screens w), s == sc -- returning the result. It is like 'maybe' for the focused workspace. -- with :: b -> (Stack a -> b) -> StackSet i a s -> b -with dflt f s = case stack (current s) of Empty -> dflt; v -> f v +with dflt f s = case stack (workspace (current s)) of Empty -> dflt; v -> f v -- TODO: ndm: a 'catch' proof here that 'f' only gets Node -- constructors, hence all 'f's are safe below? @@ -210,7 +216,8 @@ with dflt f s = case stack (current s) of Empty -> dflt; v -> f v -- Apply a function, and a default value for Empty, to modify the current stack. -- modify :: Stack a -> (Stack a -> Stack a) -> StackSet i a s -> StackSet i a s -modify d f s = s { current = (current s) { stack = with d f s } } +modify d f s = s { current = (current s) + { workspace = (workspace (current s)) { stack = with d f s }}} -- -- /O(1)/. Extract the focused element of the current stack. @@ -248,10 +255,10 @@ focusRight = modify Empty $ \c -> case c of Node t ls [] -> Node x [] (xs ++ [t]) where (x:xs) = reverse ls -- --- | /O(1) on current window, O(n) in general/. Focus the window 'w'. If the --- workspace 'w' is on is not visible, 'view' that workspace first. +-- | /O(1) on current window, O(n) in general/. Focus the window 'w', +-- and set its workspace as current. -- -focusWindow :: (Integral i, Eq a) => a -> StackSet i a s -> StackSet i a s +focusWindow :: (Integral i, Eq s, Eq a) => a -> StackSet i a s -> StackSet i a s focusWindow w s | Just w == peek s = s | otherwise = maybe s id $ do n <- findIndex w s @@ -270,7 +277,8 @@ member a s = maybe False (const True) (findIndex a s) -- Return Just the workspace index of the given window, or Nothing -- if the window is not in the StackSet. findIndex :: Eq a => a -> StackSet i a s -> Maybe i -findIndex a s = listToMaybe [ tag w | w <- current s : prev s ++ next s, has a (stack w) ] +findIndex a s = listToMaybe + [ tag w | w <- workspace (current s) : map workspace (visible s) ++ hidden s, has a (stack w) ] where has _ Empty = False has x (Node t l r) = x `elem` (t : l ++ r) @@ -314,9 +322,9 @@ insertLeft a s = if member a s then s else insert -- * deleting the master window resets it to the newly focused window -- * otherwise, delete doesn't affect the master. -- -delete :: (Integral i, Eq a) => a -> StackSet i a s -> StackSet i a s +delete :: (Integral i, Eq a, Eq s) => a -> StackSet i a s -> StackSet i a s delete w s | Just w == peek s = remove s -- common case. - | otherwise = maybe s (removeWindow . tag . current $ s) (findIndex w s) + | otherwise = maybe s (removeWindow.tag.workspace.current $ s) (findIndex w s) where -- find and remove window script removeWindow o n = foldr ($) s [view o,remove ,until ((Just w ==) . peek) focusLeft,view n] @@ -351,8 +359,9 @@ swap = modify Empty $ \c -> case c of -- workspace. The actual focused workspace doesn't change. If there is -- no element on the current stack, the original stackSet is returned. -- -shift :: (Eq a, Integral i) => i -> StackSet i a s -> StackSet i a s -shift n s = if and [n >= 0,n < size s,n /= tag (current s)] then maybe s go (peek s) else s - where go w = foldr ($) s [view (tag (current s)),insertLeft w,view n,delete w] +shift :: (Eq a, Eq s, Integral i) => i -> StackSet i a s -> StackSet i a s +shift n s = if and [n >= 0,n < size s,n /= tag (workspace (current s))] + then maybe s go (peek s) else s + where go w = foldr ($) s [view (tag (workspace (current s))),insertLeft w,view n,delete w] -- ^^ poor man's state monad :-) diff --git a/tests/Properties.hs b/tests/Properties.hs index 170bc36..7e10fe0 100644 --- a/tests/Properties.hs +++ b/tests/Properties.hs @@ -84,7 +84,7 @@ fromList (o,m,fs,xs) = type T = StackSet Int Char Int -- Useful operation, the non-local workspaces -hidden x = [ w | w <- prev x ++ next x ] -- the hidden workspaces +hidden_spaces x = map workspace (visible x) ++ hidden x -- Basic data invariants of the StackSet -- @@ -105,27 +105,23 @@ invariant (s :: T) = and [ noDuplicates -- all this xinerama stuff says we don't have the right structure - , currentIsVisible - , validScreens - , validWorkspaces - , inBounds +-- , validScreens +-- , validWorkspaces +-- , inBounds ] where ws = [ focus t : left t ++ right t - | w <- current s : prev s ++ next s, let t = stack w, t /= Empty ] + | w <- workspace (current s) : map workspace (visible s) ++ hidden s + , let t = stack w, t /= Empty ] noDuplicates = nub ws == ws - -- xinerama invariants: +-- validScreens = monotonic . sort . M. . (W.current s : W.visible : W$ s - currentIsVisible = M.member (tag (current s)) (screens s) +-- validWorkspaces = and [ w `elem` allworkspaces | w <- (M.keys . screens) s ] +-- where allworkspaces = map tag $ current s : prev s ++ next s - validScreens = monotonic . sort . M.elems . screens $ s - - validWorkspaces = and [ w `elem` allworkspaces | w <- (M.keys . screens) s ] - where allworkspaces = map tag $ current s : prev s ++ next s - - inBounds = and [ w >=0 && w < size s | (w,sc) <- M.assocs (screens s) ] +-- inBounds = and [ w >=0 && w < size s | (w,sc) <- M.assocs (screens s) ] monotonic [] = True monotonic (x:[]) = True @@ -149,7 +145,7 @@ prop_focusRight_I (n :: NonNegative Int) (x :: T) = prop_focus_I (n :: NonNegative Int) (x :: T) = case peek x of Nothing -> True - Just _ -> let w = focus . stack . current $ foldr (const focusLeft) x [1..n] + Just _ -> let w = focus . stack . workspace . current $ foldr (const focusLeft) x [1..n] in invariant $ focusWindow w x prop_insertLeft_I n (x :: T) = invariant $ insertLeft n x @@ -171,13 +167,14 @@ prop_shift_I (n :: NonNegative Int) (x :: T) = -- empty StackSets have no windows in them prop_empty (n :: Positive Int) (m :: Positive Int) = - all (== Empty) [ stack w | w <- current x : prev x ++ next x ] + all (== Empty) [ stack w | w <- workspace (current x) + : map workspace (visible x) ++ hidden x ] where x = new (fromIntegral n) (fromIntegral m) :: T -- empty StackSets always have focus on workspace 0 prop_empty_current (n :: Positive Int) - (m :: Positive Int) = tag (current x) == 0 + (m :: Positive Int) = tag (workspace $ current x) == 0 where x = new (fromIntegral n) (fromIntegral m) :: T -- no windows will be a member of an empty workspace @@ -189,7 +186,7 @@ prop_member_empty i (n :: Positive Int) (m :: Positive Int) -- view sets the current workspace to 'n' prop_view_current (x :: T) (n :: NonNegative Int) = i < size x ==> - tag (current (view i x)) == i + tag (workspace $ current (view i x)) == i where i = fromIntegral n @@ -199,14 +196,15 @@ prop_view_local (x :: T) (n :: NonNegative Int) = i < size x ==> workspaces x == workspaces (view i x) where workspaces a = sortBy (\s t -> tag s `compare` tag t) $ - current a : prev a ++ next a + workspace (current a) + : map workspace (visible a) ++ hidden a i = fromIntegral n -- view should result in a visible xinerama screen -prop_view_xinerama (x :: T) (n :: NonNegative Int) = i < size x ==> - M.member i (screens (view i x)) - where - i = fromIntegral n +-- prop_view_xinerama (x :: T) (n :: NonNegative Int) = i < size x ==> +-- M.member i (screens (view i x)) +-- where +-- i = fromIntegral n -- view is idempotent prop_view_idem (x :: T) r = @@ -214,21 +212,27 @@ prop_view_idem (x :: T) r = sz = size x in view i (view i x) == (view i x) --- view is reversible -prop_view_reversible r (x :: T) = view n (view i x) == x - where n = tag (current x) +-- view is reversible, though shuffles the order of hidden/visible +prop_view_reversible r (x :: T) = normal (view n (view i x)) == normal x + where n = tag (workspace $ current x) sz = size x i = fromIntegral $ r `mod` sz +-- normalise workspace list +normal s = s { hidden = sortBy g (hidden s), visible = sortBy f (visible s) } + where + f = \a b -> tag (workspace a) `compare` tag (workspace b) + g = \a b -> tag a `compare` tag b + -- --------------------------------------------------------------------- -- Xinerama -- every screen should yield a valid workspace -prop_lookupWorkspace (n :: NonNegative Int) (x :: T) = - s < M.size (screens x) ==> - fromJust (lookupWorkspace s x) `elem` (map tag $ current x : prev x ++ next x) - where - s = fromIntegral n +-- prop_lookupWorkspace (n :: NonNegative Int) (x :: T) = +-- s < M.size (screens x) ==> +-- fromJust (lookupWorkspace s x) `elem` (map tag $ current x : prev x ++ next x) +-- where +-- s = fromIntegral n -- --------------------------------------------------------------------- -- peek/index @@ -249,33 +253,12 @@ prop_index_length (x :: T) = Empty -> length (index x) == 0 Node {} -> length (index x) == length list where - it = stack . current $ x + it = stack . workspace . current $ x list = focus it : left it ++ right it -- --------------------------------------------------------------------- -- rotating focus -- --- Unfortunately, in the presence of wrapping of focus, we don't have a --- simple identity where focusLeft . focusRight == id, as the focus --- operations repartition the structure on wrapping. --- --- Note the issue with equality on Stacks given the wrapping semantics. --- --- [1,2,3] ++ [4] ++ [5] --- --- should be equivalent to: --- --- [] ++ [4] ++ [5,1,2,3] --- --- However, we can simply normalise the list, taking focus as the head, --- and the items should be the same. - --- So we normalise the stack on the current workspace. --- We normalise by moving everything to the 'left' of the focused item, --- to the right. --- normal (x :: T) = modify Empty (\c -> case c of --- Node t ls rs -> Node t [] (rs ++ reverse ls)) x -normal = id -- master/focus -- @@ -293,8 +276,8 @@ prop_focusWindow_master (n :: NonNegative Int) (x :: T) = in index (focusWindow (s !! i) x) == index x -- shifting focus is trivially reversible -prop_focus_left (x :: T) = normal (focusLeft (focusRight x)) == normal x -prop_focus_right (x :: T) = normal (focusRight (focusLeft x)) == normal x +prop_focus_left (x :: T) = (focusLeft (focusRight x)) == x +prop_focus_right (x :: T) = (focusRight (focusLeft x)) == x -- focusWindow actually leaves the window focused... prop_focusWindow_works (n :: NonNegative Int) (x :: T) = @@ -302,26 +285,26 @@ prop_focusWindow_works (n :: NonNegative Int) (x :: T) = Nothing -> True Just _ -> let s = index x i = fromIntegral n `mod` length s - in (focus . stack . current) (focusWindow (s !! i) x) == (s !! i) + in (focus . stack . workspace . current) (focusWindow (s !! i) x) == (s !! i) -- rotation through the height of a stack gets us back to the start -prop_focus_all_l (x :: T) = normal (foldr (const focusLeft) x [1..n]) == normal x +prop_focus_all_l (x :: T) = (foldr (const focusLeft) x [1..n]) == x where n = length (index x) -prop_focus_all_r (x :: T) = normal (foldr (const focusRight) x [1..n]) == normal x +prop_focus_all_r (x :: T) = (foldr (const focusRight) x [1..n]) == x where n = length (index x) -- prop_rotate_all (x :: T) = f (f x) == f x -- f x' = foldr (\_ y -> rotate GT y) x' [1..n] -- focus is local to the current workspace -prop_focus_local (x :: T) = hidden (focusRight x) == hidden x +prop_focus_local (x :: T) = hidden_spaces (focusRight x) == hidden_spaces x prop_focusWindow_local (n :: NonNegative Int) (x::T ) = case peek x of Nothing -> True Just _ -> let s = index x i = fromIntegral n `mod` length s - in hidden (focusWindow (s !! i) x) == hidden x + in hidden_spaces (focusWindow (s !! i) x) == hidden_spaces x -- --------------------------------------------------------------------- -- member/findIndex @@ -332,7 +315,7 @@ prop_focusWindow_local (n :: NonNegative Int) (x::T ) = -- prop_findIndex (x :: T) = and [ tag w == fromJust (findIndex i x) - | w <- current x : prev x ++ next x + | w <- workspace (current x) : map workspace (visible x) ++ hidden x , let t = stack w , t /= Empty , i <- focus (stack w) : left (stack w) ++ right (stack w) @@ -352,7 +335,7 @@ prop_insert_idem i (x :: T) = insertLeft i x == insertLeft i (insertLeft i x) prop_insert_duplicate i (x :: T) = member i x ==> insertLeft i x == x -- push shouldn't change anything but the current workspace -prop_insert_local (x :: T) i = not (member i x) ==> hidden x == hidden (insertLeft i x) +prop_insert_local (x :: T) i = not (member i x) ==> hidden_spaces x == hidden_spaces (insertLeft i x) -- Inserting a (unique) list of items into an empty stackset should -- result in the last inserted element having focus. @@ -402,7 +385,7 @@ prop_delete_insert (x :: T) = prop_delete_local (x :: T) = case peek x of Nothing -> True - Just i -> hidden x == hidden (delete i x) + Just i -> hidden_spaces x == hidden_spaces (delete i x) -- --------------------------------------------------------------------- -- swap: setting the master window @@ -414,10 +397,10 @@ prop_delete_local (x :: T) = prop_swap_focus (x :: T) = case peek x of Nothing -> True - Just f -> focus (stack (current (swap x))) == f + Just f -> focus (stack (workspace $ current (swap x))) == f -- swap is local -prop_swap_local (x :: T) = hidden x == hidden (swap x) +prop_swap_local (x :: T) = hidden_spaces x == hidden_spaces (swap x) -- TODO swap is reversible -- swap is reversible, but involves moving focus back the window with @@ -441,10 +424,10 @@ prop_swap_idempotent (x :: T) = swap (swap x) == swap x prop_shift_reversible (r :: Int) (x :: T) = let i = fromIntegral $ r `mod` sz sz = size y - n = tag (current y) + n = tag (workspace $ current y) in case peek y of Nothing -> True - Just _ -> (view n . shift n . view i . shift i) y == y + Just _ -> normal ((view n . shift n . view i . shift i) y) == normal y where y = swap x @@ -499,11 +482,11 @@ main = do ,("view : invariant" , mytest prop_view_I) ,("view sets current" , mytest prop_view_current) ,("view idempotent" , mytest prop_view_idem) - ,("view reviersible" , mytest prop_view_reversible) - ,("view / xinerama" , mytest prop_view_xinerama) + ,("view reversible" , mytest prop_view_reversible) +-- ,("view / xinerama" , mytest prop_view_xinerama) ,("view is local" , mytest prop_view_local) - ,("valid workspace xinerama", mytest prop_lookupWorkspace) +-- ,("valid workspace xinerama", mytest prop_lookupWorkspace) ,("peek/member " , mytest prop_member_peek) |