{-# LANGUAGE ScopedTypeVariables, GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances, OverlappingInstances #-}
module XMonad.Actions.GridSelect (
GSConfig(..),
def,
defaultGSConfig,
TwoDPosition,
buildDefaultGSConfig,
gridselect,
gridselectWindow,
withSelectedWindow,
bringSelected,
goToSelected,
gridselectWorkspace,
gridselectWorkspace',
spawnSelected,
runSelectedAction,
HasColorizer(defaultColorizer),
fromClassName,
stringColorizer,
colorRangeFromClassName,
TwoD,
makeXEventhandler,
shadowWithKeymap,
defaultNavigation,
substringSearch,
navNSearch,
setPos,
move,
moveNext, movePrev,
select,
cancel,
transformSearchString,
Rearranger,
noRearranger,
searchStringRearrangerGenerator,
TwoDState,
) where
import Data.Maybe
import Data.Bits
import Data.Char
import Data.Ord (comparing)
import Control.Applicative
import Control.Monad.State
import Control.Arrow
import Data.List as L
import qualified Data.Map as M
import XMonad hiding (liftX)
import XMonad.Util.Font
import XMonad.Prompt (mkUnmanagedWindow)
import XMonad.StackSet as W
import XMonad.Layout.Decoration
import XMonad.Util.NamedWindows
import XMonad.Actions.WindowBringer (bringWindow)
import Text.Printf
import System.Random (mkStdGen, genRange, next)
import Data.Word (Word8)
data GSConfig a = GSConfig {
gs_cellheight :: Integer,
gs_cellwidth :: Integer,
gs_cellpadding :: Integer,
gs_colorizer :: a -> Bool -> X (String, String),
gs_font :: String,
gs_navigate :: TwoD a (Maybe a),
gs_rearranger :: Rearranger a,
gs_originFractX :: Double,
gs_originFractY :: Double,
gs_bordercolor :: String
}
class HasColorizer a where
defaultColorizer :: a -> Bool -> X (String, String)
instance HasColorizer Window where
defaultColorizer = fromClassName
instance HasColorizer String where
defaultColorizer = stringColorizer
instance HasColorizer a where
defaultColorizer _ isFg =
let getColor = if isFg then focusedBorderColor else normalBorderColor
in asks $ flip (,) "black" . getColor . config
instance HasColorizer a => Default (GSConfig a) where
def = buildDefaultGSConfig defaultColorizer
{-# DEPRECATED defaultGSConfig "Use def (from Data.Default, and re-exported from XMonad.Actions.GridSelect) instead." #-}
defaultGSConfig :: HasColorizer a => GSConfig a
defaultGSConfig = def
type TwoDPosition = (Integer, Integer)
type TwoDElementMap a = [(TwoDPosition,(String,a))]
data TwoDState a = TwoDState { td_curpos :: TwoDPosition
, td_availSlots :: [TwoDPosition]
, td_elements :: [(String,a)]
, td_gsconfig :: GSConfig a
, td_font :: XMonadFont
, td_paneX :: Integer
, td_paneY :: Integer
, td_drawingWin :: Window
, td_searchString :: String
, td_elementmap :: TwoDElementMap a
}
generateElementmap :: TwoDState a -> X (TwoDElementMap a)
generateElementmap s = do
rearrangedElements <- rearranger searchString sortedElements
return $ zip positions rearrangedElements
where
TwoDState {td_availSlots = positions,
td_gsconfig = gsconfig,
td_searchString = searchString} = s
GSConfig {gs_rearranger = rearranger} = gsconfig
filteredElements = L.filter ((searchString `isInfixOfI`) . fst) (td_elements s)
sortedElements = orderElementmap searchString filteredElements
needle `isInfixOfI` haystack = (upper needle) `isInfixOf` (upper haystack)
upper = map toUpper
orderElementmap :: String -> [(String,a)] -> [(String,a)]
orderElementmap searchString elements = if not $ null searchString then sortedElements else elements
where
upper = map toUpper
calcScore element = ( length $ takeWhile (not . isPrefixOf (upper searchString)) (tails . upper . fst $ element)
, element)
compareScore = comparing (\(score, (str,_)) -> (score, str))
sortedElements = map snd . sortBy compareScore $ map calcScore elements
newtype TwoD a b = TwoD { unTwoD :: StateT (TwoDState a) X b }
deriving (Monad,Functor,MonadState (TwoDState a))
instance Applicative (TwoD a) where
(<*>) = ap
pure = return
liftX :: X a1 -> TwoD a a1
liftX = TwoD . lift
evalTwoD :: TwoD a1 a -> TwoDState a1 -> X a
evalTwoD m s = flip evalStateT s $ unTwoD m
diamondLayer :: (Enum a, Num a, Eq a) => a -> [(a, a)]
diamondLayer 0 = [(0,0)]
diamondLayer n =
let tr = [ (x,n-x) | x <- [0..n-1] ]
r = tr ++ (map (\(x,y) -> (y,-x)) tr)
in r ++ (map (negate *** negate) r)
diamond :: (Enum a, Num a, Eq a) => [(a, a)]
diamond = concatMap diamondLayer [0..]
diamondRestrict :: Integer -> Integer -> Integer -> Integer -> [(Integer, Integer)]
diamondRestrict x y originX originY =
L.filter (\(x',y') -> abs x' <= x && abs y' <= y) .
map (\(x', y') -> (x' + fromInteger originX, y' + fromInteger originY)) .
take 1000 $ diamond
findInElementMap :: (Eq a) => a -> [(a, b)] -> Maybe (a, b)
findInElementMap pos = find ((== pos) . fst)
drawWinBox :: Window -> XMonadFont -> (String, String) -> String -> Integer -> Integer -> String -> Integer -> Integer -> Integer -> X ()
drawWinBox win font (fg,bg) bc ch cw text x y cp =
withDisplay $ \dpy -> do
gc <- liftIO $ createGC dpy win
bordergc <- liftIO $ createGC dpy win
liftIO $ do
Just fgcolor <- initColor dpy fg
Just bgcolor <- initColor dpy bg
Just bordercolor <- initColor dpy bc
setForeground dpy gc fgcolor
setBackground dpy gc bgcolor
setForeground dpy bordergc bordercolor
fillRectangle dpy win gc (fromInteger x) (fromInteger y) (fromInteger cw) (fromInteger ch)
drawRectangle dpy win bordergc (fromInteger x) (fromInteger y) (fromInteger cw) (fromInteger ch)
stext <- shrinkWhile (shrinkIt shrinkText)
(\n -> do size <- liftIO $ textWidthXMF dpy font n
return $ size > (fromInteger (cw-(2*cp))))
text
(asc,desc) <- liftIO $ textExtentsXMF font stext
let offset = ((ch - fromIntegral (asc + desc)) `div` 2) + fromIntegral asc
printStringXMF dpy win font gc bg fg (fromInteger (x+cp)) (fromInteger (y+offset)) stext
liftIO $ freeGC dpy gc
liftIO $ freeGC dpy bordergc
updateAllElements :: TwoD a ()
updateAllElements =
do
s <- get
updateElements (td_elementmap s)
grayoutElements :: Int -> TwoD a ()
grayoutElements skip =
do
s <- get
updateElementsWithColorizer grayOnly $ drop skip (td_elementmap s)
where grayOnly _ _ = return ("#808080", "#808080")
updateElements :: TwoDElementMap a -> TwoD a ()
updateElements elementmap = do
s <- get
updateElementsWithColorizer (gs_colorizer (td_gsconfig s)) elementmap
updateElementsWithColorizer :: (a -> Bool -> X (String, String)) -> TwoDElementMap a -> TwoD a ()
updateElementsWithColorizer colorizer elementmap = do
TwoDState { td_curpos = curpos,
td_drawingWin = win,
td_gsconfig = gsconfig,
td_font = font,
td_paneX = paneX,
td_paneY = paneY} <- get
let cellwidth = gs_cellwidth gsconfig
cellheight = gs_cellheight gsconfig
paneX' = div (paneX-cellwidth) 2
paneY' = div (paneY-cellheight) 2
updateElement (pos@(x,y),(text, element)) = liftX $ do
colors <- colorizer element (pos == curpos)
drawWinBox win font
colors
(gs_bordercolor gsconfig)
cellheight
cellwidth
text
(paneX'+x*cellwidth)
(paneY'+y*cellheight)
(gs_cellpadding gsconfig)
mapM_ updateElement elementmap
stdHandle :: Event -> TwoD a (Maybe a) -> TwoD a (Maybe a)
stdHandle (ButtonEvent { ev_event_type = t, ev_x = x, ev_y = y }) contEventloop
| t == buttonRelease = do
s @ TwoDState { td_paneX = px, td_paneY = py,
td_gsconfig = (GSConfig ch cw _ _ _ _ _ _ _ _) } <- get
let gridX = (fi x - (px - cw) `div` 2) `div` cw
gridY = (fi y - (py - ch) `div` 2) `div` ch
case lookup (gridX,gridY) (td_elementmap s) of
Just (_,el) -> return (Just el)
Nothing -> contEventloop
| otherwise = contEventloop
stdHandle (ExposeEvent { }) contEventloop = updateAllElements >> contEventloop
stdHandle _ contEventloop = contEventloop
makeXEventhandler :: ((KeySym, String, KeyMask) -> TwoD a (Maybe a)) -> TwoD a (Maybe a)
makeXEventhandler keyhandler = fix $ \me -> join $ liftX $ withDisplay $ \d -> liftIO $ allocaXEvent $ \e -> do
maskEvent d (exposureMask .|. keyPressMask .|. buttonReleaseMask) e
ev <- getEvent e
if ev_event_type ev == keyPress
then do
(ks,s) <- lookupString $ asKeyEvent e
return $ do
mask <- liftX $ cleanMask (ev_state ev)
keyhandler (fromMaybe xK_VoidSymbol ks, s, mask)
else
return $ stdHandle ev me
shadowWithKeymap :: M.Map (KeyMask, KeySym) a -> ((KeySym, String, KeyMask) -> a) -> (KeySym, String, KeyMask) -> a
shadowWithKeymap keymap dflt keyEvent@(ks,_,m') = fromMaybe (dflt keyEvent) (M.lookup (m',ks) keymap)
select :: TwoD a (Maybe a)
select = do
s <- get
return $ fmap (snd . snd) $ findInElementMap (td_curpos s) (td_elementmap s)
cancel :: TwoD a (Maybe a)
cancel = return Nothing
setPos :: (Integer, Integer) -> TwoD a ()
setPos newPos = do
s <- get
let elmap = td_elementmap s
newSelectedEl = findInElementMap newPos (td_elementmap s)
oldPos = td_curpos s
when (isJust newSelectedEl && newPos /= oldPos) $ do
put s { td_curpos = newPos }
updateElements (catMaybes [(findInElementMap oldPos elmap), newSelectedEl])
move :: (Integer, Integer) -> TwoD a ()
move (dx,dy) = do
s <- get
let (x,y) = td_curpos s
newPos = (x+dx,y+dy)
setPos newPos
moveNext :: TwoD a ()
moveNext = do
position <- gets td_curpos
elems <- gets td_elementmap
let n = length elems
m = case findIndex (\p -> fst p == position) elems of
Nothing -> Nothing
Just k | k == n-1 -> Just 0
| otherwise -> Just (k+1)
whenJust m $ \i ->
setPos (fst $ elems !! i)
movePrev :: TwoD a ()
movePrev = do
position <- gets td_curpos
elems <- gets td_elementmap
let n = length elems
m = case findIndex (\p -> fst p == position) elems of
Nothing -> Nothing
Just 0 -> Just (n-1)
Just k -> Just (k-1)
whenJust m $ \i ->
setPos (fst $ elems !! i)
transformSearchString :: (String -> String) -> TwoD a ()
transformSearchString f = do
s <- get
let oldSearchString = td_searchString s
newSearchString = f oldSearchString
when (newSearchString /= oldSearchString) $ do
let s' = s { td_searchString = newSearchString }
m <- liftX $ generateElementmap s'
let s'' = s' { td_elementmap = m }
oldLen = length $ td_elementmap s
newLen = length $ td_elementmap s''
when (newLen < oldLen) $ grayoutElements newLen
put s''
updateAllElements
defaultNavigation :: TwoD a (Maybe a)
defaultNavigation = makeXEventhandler $ shadowWithKeymap navKeyMap navDefaultHandler
where navKeyMap = M.fromList [
((0,xK_Escape) , cancel)
,((0,xK_Return) , select)
,((0,xK_slash) , substringSearch defaultNavigation)
,((0,xK_Left) , move (-1,0) >> defaultNavigation)
,((0,xK_h) , move (-1,0) >> defaultNavigation)
,((0,xK_Right) , move (1,0) >> defaultNavigation)
,((0,xK_l) , move (1,0) >> defaultNavigation)
,((0,xK_Down) , move (0,1) >> defaultNavigation)
,((0,xK_j) , move (0,1) >> defaultNavigation)
,((0,xK_Up) , move (0,-1) >> defaultNavigation)
,((0,xK_k) , move (0,-1) >> defaultNavigation)
,((0,xK_Tab) , moveNext >> defaultNavigation)
,((0,xK_n) , moveNext >> defaultNavigation)
,((shiftMask,xK_Tab), movePrev >> defaultNavigation)
,((0,xK_p) , movePrev >> defaultNavigation)
]
navDefaultHandler = const defaultNavigation
navNSearch :: TwoD a (Maybe a)
navNSearch = makeXEventhandler $ shadowWithKeymap navNSearchKeyMap navNSearchDefaultHandler
where navNSearchKeyMap = M.fromList [
((0,xK_Escape) , cancel)
,((0,xK_Return) , select)
,((0,xK_Left) , move (-1,0) >> navNSearch)
,((0,xK_Right) , move (1,0) >> navNSearch)
,((0,xK_Down) , move (0,1) >> navNSearch)
,((0,xK_Up) , move (0,-1) >> navNSearch)
,((0,xK_Tab) , moveNext >> navNSearch)
,((shiftMask,xK_Tab), movePrev >> navNSearch)
,((0,xK_BackSpace), transformSearchString (\s -> if (s == "") then "" else init s) >> navNSearch)
]
navNSearchDefaultHandler (_,s,_) = do
transformSearchString (++ s)
navNSearch
substringSearch :: TwoD a (Maybe a) -> TwoD a (Maybe a)
substringSearch returnNavigation = fix $ \me ->
let searchKeyMap = M.fromList [
((0,xK_Escape) , transformSearchString (const "") >> returnNavigation)
,((0,xK_Return) , returnNavigation)
,((0,xK_BackSpace), transformSearchString (\s -> if (s == "") then "" else init s) >> me)
]
searchDefaultHandler (_,s,_) = do
transformSearchString (++ s)
me
in makeXEventhandler $ shadowWithKeymap searchKeyMap searchDefaultHandler
hsv2rgb :: Fractional a => (Integer,a,a) -> (a,a,a)
hsv2rgb (h,s,v) =
let hi = (div h 60) `mod` 6 :: Integer
f = (((fromInteger h)/60) - (fromInteger hi)) :: Fractional a => a
q = v * (1-f)
p = v * (1-s)
t = v * (1-(1-f)*s)
in case hi of
0 -> (v,t,p)
1 -> (q,v,p)
2 -> (p,v,t)
3 -> (p,q,v)
4 -> (t,p,v)
5 -> (v,p,q)
_ -> error "The world is ending. x mod a >= a."
stringColorizer :: String -> Bool -> X (String, String)
stringColorizer s active =
let seed x = toInteger (sum $ map ((*x).fromEnum) s) :: Integer
(r,g,b) = hsv2rgb ((seed 83) `mod` 360,
(fromInteger ((seed 191) `mod` 1000))/2500+0.4,
(fromInteger ((seed 121) `mod` 1000))/2500+0.4)
in if active
then return ("#faff69", "black")
else return ("#" ++ concat (map (twodigitHex.(round :: Double -> Word8).(*256)) [r, g, b] ), "white")
fromClassName :: Window -> Bool -> X (String, String)
fromClassName w active = runQuery className w >>= flip defaultColorizer active
twodigitHex :: Word8 -> String
twodigitHex a = printf "%02x" a
colorRangeFromClassName :: (Word8, Word8, Word8)
-> (Word8, Word8, Word8)
-> (Word8, Word8, Word8)
-> (Word8, Word8, Word8)
-> (Word8, Word8, Word8)
-> Window -> Bool -> X (String, String)
colorRangeFromClassName startC endC activeC inactiveT activeT w active =
do classname <- runQuery className w
if active
then return (rgbToHex activeC, rgbToHex activeT)
else return (rgbToHex $ mix startC endC
$ stringToRatio classname, rgbToHex inactiveT)
where rgbToHex :: (Word8, Word8, Word8) -> String
rgbToHex (r, g, b) = '#':twodigitHex r
++twodigitHex g++twodigitHex b
mix :: (Word8, Word8, Word8) -> (Word8, Word8, Word8)
-> Double -> (Word8, Word8, Word8)
mix (r1, g1, b1) (r2, g2, b2) r = (mix' r1 r2, mix' g1 g2, mix' b1 b2)
where mix' a b = truncate $ (fi a * r) + (fi b * (1 - r))
stringToRatio :: String -> Double
stringToRatio "" = 0
stringToRatio s = let gen = mkStdGen $ sum $ map fromEnum s
range = (\(a, b) -> b - a) $ genRange gen
randomInt = foldr1 combine $ replicate 20 next
combine f1 f2 g = let (_, g') = f1 g in f2 g'
in fi (fst $ randomInt gen) / fi range
gridselect :: GSConfig a -> [(String,a)] -> X (Maybe a)
gridselect _ [] = return Nothing
gridselect gsconfig elements =
withDisplay $ \dpy -> do
rootw <- asks theRoot
scr <- gets $ screenRect . W.screenDetail . W.current . windowset
win <- liftIO $ mkUnmanagedWindow dpy (defaultScreenOfDisplay dpy) rootw
(rect_x scr) (rect_y scr) (rect_width scr) (rect_height scr)
liftIO $ mapWindow dpy win
liftIO $ selectInput dpy win (exposureMask .|. keyPressMask .|. buttonReleaseMask)
status <- io $ grabKeyboard dpy win True grabModeAsync grabModeAsync currentTime
io $ grabPointer dpy win True buttonReleaseMask grabModeAsync grabModeAsync none none currentTime
font <- initXMF (gs_font gsconfig)
let screenWidth = toInteger $ rect_width scr
screenHeight = toInteger $ rect_height scr
selectedElement <- if (status == grabSuccess) then do
let restriction ss cs = (fromInteger ss/fromInteger (cs gsconfig)-1)/2 :: Double
restrictX = floor $ restriction screenWidth gs_cellwidth
restrictY = floor $ restriction screenHeight gs_cellheight
originPosX = floor $ ((gs_originFractX gsconfig) - (1/2)) * 2 * fromIntegral restrictX
originPosY = floor $ ((gs_originFractY gsconfig) - (1/2)) * 2 * fromIntegral restrictY
coords = diamondRestrict restrictX restrictY originPosX originPosY
s = TwoDState { td_curpos = (head coords),
td_availSlots = coords,
td_elements = elements,
td_gsconfig = gsconfig,
td_font = font,
td_paneX = screenWidth,
td_paneY = screenHeight,
td_drawingWin = win,
td_searchString = "",
td_elementmap = [] }
m <- generateElementmap s
evalTwoD (updateAllElements >> (gs_navigate gsconfig))
(s { td_elementmap = m })
else
return Nothing
liftIO $ do
unmapWindow dpy win
destroyWindow dpy win
ungrabPointer dpy currentTime
sync dpy False
releaseXMF font
return selectedElement
gridselectWindow :: GSConfig Window -> X (Maybe Window)
gridselectWindow gsconf = windowMap >>= gridselect gsconf
withSelectedWindow :: (Window -> X ()) -> GSConfig Window -> X ()
withSelectedWindow callback conf = do
mbWindow <- gridselectWindow conf
case mbWindow of
Just w -> callback w
Nothing -> return ()
windowMap :: X [(String,Window)]
windowMap = do
ws <- gets windowset
wins <- mapM keyValuePair (W.allWindows ws)
return wins
where keyValuePair w = flip (,) w `fmap` decorateName' w
decorateName' :: Window -> X String
decorateName' w = do
fmap show $ getName w
buildDefaultGSConfig :: (a -> Bool -> X (String,String)) -> GSConfig a
buildDefaultGSConfig col = GSConfig 50 130 10 col "xft:Sans-8" defaultNavigation noRearranger (1/2) (1/2) "white"
bringSelected :: GSConfig Window -> X ()
bringSelected = withSelectedWindow $ \w -> do
windows (bringWindow w)
XMonad.focus w
windows W.shiftMaster
goToSelected :: GSConfig Window -> X ()
goToSelected = withSelectedWindow $ windows . W.focusWindow
spawnSelected :: GSConfig String -> [String] -> X ()
spawnSelected conf lst = gridselect conf (zip lst lst) >>= flip whenJust spawn
runSelectedAction :: GSConfig (X ()) -> [(String, X ())] -> X ()
runSelectedAction conf actions = do
selectedActionM <- gridselect conf actions
case selectedActionM of
Just selectedAction -> selectedAction
Nothing -> return ()
gridselectWorkspace :: GSConfig WorkspaceId ->
(WorkspaceId -> WindowSet -> WindowSet) -> X ()
gridselectWorkspace conf viewFunc = gridselectWorkspace' conf (windows . viewFunc)
gridselectWorkspace' :: GSConfig WorkspaceId -> (WorkspaceId -> X ()) -> X ()
gridselectWorkspace' conf func = withWindowSet $ \ws -> do
let wss = map W.tag $ W.hidden ws ++ map W.workspace (W.current ws : W.visible ws)
gridselect conf (zip wss wss) >>= flip whenJust func
type Rearranger a = String -> [(String, a)] -> X [(String, a)]
noRearranger :: Rearranger a
noRearranger _ = return
searchStringRearrangerGenerator :: (String -> a) -> Rearranger a
searchStringRearrangerGenerator f =
let r "" xs = return $ xs
r s xs | s `elem` map fst xs = return $ xs
| otherwise = return $ xs ++ [(s, f s)]
in r