Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: XMonad.Layout.Inspect #2

Open
wants to merge 4 commits into
base: layout-inspect
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions XMonad/Layout/Inspect.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@

-- |
-- Module : XMonad.Layout.Inspect
-- Description : Inspect layout data.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps s/data/state (here and in other places)? Data sounds very static, but the main point seems to be that we want access to runtime information

-- Copyright : (c) 2020 Tomáš Janoušek <[email protected]>
-- License : BSD3
--
-- Maintainer : Tomáš Janoušek <[email protected]>
-- Stability : experimental
-- Portability : unknown
--
-- TODO
--
-- Inspect a workspace's layout data. Useful for accessing data contained in
-- layout modifiers.

module XMonad.Layout.Inspect (
-- * Usage
-- $usage
Expand All @@ -36,23 +38,58 @@ import XMonad
import qualified XMonad.StackSet as W

-- $usage
-- This module provides a way for layout authors to make workspace layout data
-- accessible to end-users.
--
-- Best explained by example, suppose you've written a layout modifier:
--
-- > newtype Foo a = Foo String deriving (Read, Show)
-- > instance LayoutModifier Foo a
-- > foo :: LayoutClass l a => String -> l a -> ModifiedLayout Foo l a
-- > foo = ModifiedLayout . Foo
--
-- To allow users to inspect the string contained in a given workspace's layout,
-- first import this module:
--
-- > import XMonad.Layout.Inspect
--
-- Then define instances for 'InspectResult' and 'InspectLayout', and implement
-- getFoo in terms of 'inspectWorkspace':
--
-- > data GetFoo = GetFoo
-- > type instance InspectResult GetFoo = Alt Maybe String
-- >
-- > instance InspectLayout GetFoo Foo a where
-- > inspectLayout GetFoo (Foo s) = pure s
-- >
-- > getFoo :: (LayoutClass l Window, InspectLayout GetFoo l Window)
-- > => l Window -> WindowSpace -> Maybe String
-- > getFoo l = getAlt . inspectWorkspace l GetFoo
--
-- An end-user can then call getFoo by passing it the appropriate layout and a
-- given workspace:
--
-- TODO: help for users
-- > xFoo :: WindowSpace -> X ()
-- > xFoo wsp = do
-- > l <- asks (layoutHook . config)
-- > case getFoo l wsp of
-- > Nothing -> pure ()
-- > Just s -> xmessage s
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liskin @mislavzanic I encountered some unexpected behavior while testing this. I captured my test setup here, and the most relevant part is this section.

In brief, I created a simple layout modifier:

newtype Foo a = Foo String deriving (Read, Show)
instance LayoutModifier Foo a

foo :: LayoutClass l a => String -> l a -> ModifiedLayout Foo l a
foo = ModifiedLayout . Foo

data GetFoo = GetFoo
type instance InspectResult GetFoo = Alt Maybe String

instance InspectLayout GetFoo Foo a where
  inspectLayout GetFoo (Foo s) = pure s

getFoo :: (LayoutClass l Window, InspectLayout GetFoo l Window)
       => l Window -> WindowSpace -> Maybe String
getFoo l = getAlt . inspectWorkspace l GetFoo

and used it in a layoutHook:

myLayoutHook = foo "test" (Tall nmaster delta ratio)
  where
     nmaster = 1
     ratio   = 1/2
     delta   = 3/100

main = xmonad $ def { keys = myKeys, layoutHook = myLayoutHook }

If I pass myLayoutHook as the first argument, getFoo returns Just "test" as expected. E.g.

do
  wsp <- withWindowSet (pure . W.workspace . W.current)
  getFoo myLayoutHook wsp
  -- => Just "test"

However, if I retrieve the layout hook by other means, such as asks, then getFoo returns Nothing:

do
  wsp <- withWindowSet (pure . W.workspace . W.current)
  l <- asks (layoutHook . config)
  getFoo l wsp
  -- => Nothing

Any idea what I'm missing?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layoutHook you pull out of the config record at runtime is wrapped in the Layout existential. However, the actual purpose of the l a argument to inspectWorkspace and co. is to unwrap that existential by casting its contents to the supplied type, so giving it the wrapped layout doesn't help at all; the cast should fail with a call to error.

The OVERLAPPABLE instance for InspectLayout is then selected, narrowly preventing the error from blowing up in your face, relegating it to a Nothing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That explains it, thanks 👍

I can update the usage documentation accordingly (see below). I'd be interested to know if there's another way to call this correctly.

diff --git a/XMonad/Layout/Inspect.hs b/XMonad/Layout/Inspect.hs
index 18b93eb8..ddea14a0 100644
--- a/XMonad/Layout/Inspect.hs
+++ b/XMonad/Layout/Inspect.hs
@@ -69,10 +69,11 @@ import qualified XMonad.StackSet as W
 -- An end-user can then call getFoo by passing it the appropriate layout and a
 -- given workspace:
 --
+-- > myLayoutHook = ...
+-- >
 -- > xFoo :: WindowSpace -> X ()
 -- > xFoo wsp = do
--- >   l <- asks (layoutHook . config)
--- >   case getFoo l wsp of
+-- >   case getFoo myLayoutHook wsp of
 -- >     Nothing -> pure ()
 -- >     Just s -> xmessage s

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, as myLayoutHook is AFAIK the only place where the actual layout type (not wrapped in an existential) is present

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's present in the XConfig as well, so we could possibly add something like an inspectableLayout :: Proxy (l Window) → XConfig l → XConfig l and then have people do

main = xmonad $ inspectableLayout $ \il -> def{ layoutHook = myLayoutHook,  }

That might be marginally less error-prone, as some XConfig combinators like withEasySB change the layout, we'd just need to tell people that inspectableLayout needs to be the very last. But they'd need to pass the il as a parameter every time they need it outside of main

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I really think you'd want to be capturing the layout type immediately before xmonad eats it. I suggest something like:

-- > main = xmonadIL \i@Inspect{current,tag,workspace} ->
-- >   ...
xmonadIL
  :: (LayoutClass l Window, Read (l Window))
  => (Inspect l -> XConfig l) -> IO ()
xmonadIL = xmonad . inspectableLayout

-- > main = xmonad $ inspectableLayout \i@Inspect{current,tag,workspace} ->
-- >   ...
inspectableLayout
  :: forall l. Typeable l
  => (Inspect l -> XConfig l) -> XConfig l
inspectableLayout k = k (makeInspect (Proxy @l))

data Inspect l = Inspect
  { current   :: forall i. InspectLayout i l Window
              => i                -> X (       InspectResult i )
  , tag       :: forall i. InspectLayout i l Window
              => i -> WorkspaceId -> X (Maybe (InspectResult i))
  , workspace :: forall i. InspectLayout i l Window
              => i -> WindowSpace ->           InspectResult i
  }

makeInspect :: Typeable l => proxy l -> Inspect l
makeInspect p = Inspect
  { current   = inspectCurrentP   p
  , tag       = inspectTagP       p
  , workspace = inspectWorkspaceP p
  }

where

inspectCurrentP :: (Typeable l, InspectLayout i l Window)
                => proxy l -> i -> X (InspectResult i)
inspectCurrentP p i = gets (inspectWorkspaceP p i . w)
    where w = W.workspace . W.current . windowset

inspectTagP :: (Typeable l, InspectLayout i l Window)
            => proxy l -> i -> WorkspaceId
            -> X (Maybe (InspectResult i))
inspectTagP p i t = gets (fmap (inspectWorkspaceP p i) . mw)
    where mw = find ((t==) . W.tag) . W.workspaces . windowset

inspectWorkspaceP :: (Typeable l, InspectLayout i l Window)
                  => proxy l -> i -> WindowSpace -> InspectResult i
inspectWorkspaceP p i = inspectLayout i . asLayout p . W.layout

asLayout :: (Typeable l, Typeable a) => proxy l -> Layout a -> l a
asLayout _ (Layout l) = cast' l

and

inspectCurrent :: forall l i. (Typeable l, InspectLayout i l Window)
               => l Window -> i -> X (InspectResult i)
inspectCurrent _ = inspectCurrentP (Proxy @l)

inspectTag :: forall l i. (Typeable l, InspectLayout i l Window)
           => l Window -> i -> WorkspaceId
           -> X (Maybe (InspectResult i))
inspectTag _ = inspectTagP (Proxy @l)

inspectWorkspace :: forall l i. (Typeable l, InspectLayout i l Window)
                 => l Window -> i -> WindowSpace -> InspectResult i
inspectWorkspace _ = inspectWorkspaceP (Proxy @l)

It can't really be helped that i needs to be passed around to be used outside of main; this is the only place we know the actual layout type. The alternative is to tell users to write their configs like:

main = xmonad myConfig

myConfig = ewmh . ... $ def
  { ...
  }

and use e.g. inspectCurrentP myConfig. Of if IO is necessary:

main = makeMyConfig >>= xmonad

makeMyConfig :: IO (XConfig L)
makeMyConfig = do
  ...

using inspectCurrentP (Compose makeMyConfig).


-- | TODO
-- | Inspect the layout of the currently focused workspace.
inspectCurrent :: (LayoutClass l Window, InspectLayout i l Window)
=> l Window -> i -> X (InspectResult i)
inspectCurrent l i = gets (inspectWorkspace l i . w)
where w = W.workspace . W.current . windowset

-- | TODO
-- | Inspect the layout of the workspace with a specified tag.
inspectTag :: (LayoutClass l Window, InspectLayout i l Window)
=> l Window -> i -> WorkspaceId
-> X (Maybe (InspectResult i))
inspectTag l i t = gets (fmap (inspectWorkspace l i) . mw)
where mw = find ((t==) . W.tag) . W.workspaces . windowset

-- | TODO
-- | Inspect the layout of a specified workspace.
inspectWorkspace :: (LayoutClass l Window, InspectLayout i l Window)
=> l Window -> i -> WindowSpace -> InspectResult i
inspectWorkspace l i = inspectLayout i . asLayout l . W.layout
Expand All @@ -69,7 +106,12 @@ cast' x | Just HRefl <- ta `eqTypeRep` tb = x

type family InspectResult i

-- | TODO: for layout/modifier authors
-- | A typeclass defining 'inspectLayout' over monoidal @InspectResult@ types.
-- An overlappable instance provides sensible default behavior, returning
-- 'mempty'. (Specific @InspectResult@ instances may override this by defining
-- their own instance of 'InspectLayout'.) Additionally, an instance is provided
-- for layouts using 'Choose', provided that all the layouts in the 'Choose' are
-- capable of being inspected.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should perhaps be documentation of the respective instances? Although I guess the Choose one could be omitted entirely, as the dependency graph is right there in the type signature

class Monoid (InspectResult i) => InspectLayout i l a where
inspectLayout :: i -> l a -> InspectResult i

Expand Down