module Util.Rhombus ( defaultRhombusConfig , MatchMethod(..) , rhombus , RhombusConfig(..) , RhombusState(..) ) where import Control.Monad ( forM_, zipWithM_ ) import Data.Char import Data.List import Data.Ord import Data.Map ( fromList ) import Data.Maybe ( isJust, fromJust ) import XMonad import XMonad.StackSet hiding ( filter ) import XMonad.Util.Font import XMonad.Util.Image ( drawIcon ) import XMonad.Util.XUtils import Util.Debunk import Util.Submap import Util.XUtils import Util.Font data MatchMethod = MatchInfix | MatchPrefix data RhombusConfig = RhombusConfig { rc_font :: String , rc_cellwidth :: Dimension , rc_margin :: Dimension , rc_matchmethod :: MatchMethod , rc_wrap :: Bool , rc_colors :: Bool -> Bool -> Bool -> (String, String, String) , rc_paint :: RhombusConfig -> Display -> Pixmap -> GC -> String -> Rectangle -> Bool -> Bool -> Bool -> X () } -- TODO currently xft is broken defaultRhombusConfig = RhombusConfig "xft:Sans-8" 100 0 MatchInfix True stupidColors noPaint where stupidColors _ _ _ = ("red", "magenta", "yellow") noPaint _ _ _ _ _ _ _ _ _ = return () data RhombusState = RhombusState { rs_window :: Window , rs_search :: String , rs_font :: XMonadFont , rs_focus :: (Position, Position) , rs_strings :: [String] } reachableCoords :: RhombusState -> [(Position, Position)] reachableCoords RhombusState{rs_strings=xs} = take (length xs) wave matchingReachableCoords :: RhombusConfig -> RhombusState -> [(Position, Position)] matchingReachableCoords rc rs = snd $ unzip $ filter (isXOf (rc_matchmethod rc) (rs_search rs) . fst) $ zip (rs_strings rs) (reachableCoords rs) match :: MatchMethod -> String -> [String] -> Maybe String match m s ws = do let cands = filter (isXOf m s) ws if length cands == 1 then Just $ head cands else Nothing rhombus :: RhombusConfig -> (String -> X ()) -> [String] -> X () rhombus rc viewFunc as = withGrabbedKeyboard $ do rs <- newRhombus rc as --redraw rc rs showWindow (rs_window rs) rhombusMode viewFunc rc rs rhombusMode :: (String -> X ()) -> RhombusConfig -> RhombusState -> X () rhombusMode viewFunc rc rs = case match (rc_matchmethod rc) (rs_search rs) (init $ rs_strings rs) of Nothing -> redraw rc rs >> submapString def keys Just i -> removeRhombus rs >> viewFunc i where def (ch:[]) | isPrint ch = incSearchPushChar ch rs >>= rhombusMode viewFunc rc def _ = failbeep >> rhombusMode viewFunc rc rs keys = fromList $ [ ((0 , xK_BackSpace ), incSearchPopChar rs >>= rhombusMode viewFunc rc) , ((0 , xK_Escape ), removeRhombus rs) , ((0 , xK_Menu ), removeRhombus rs) , ((0 , xK_Left ), goto rc (-1, 0) rs >>= rhombusMode viewFunc rc) , ((0 , xK_Right ), goto rc ( 1, 0) rs >>= rhombusMode viewFunc rc) , ((0 , xK_Up ), goto rc ( 0,-1) rs >>= rhombusMode viewFunc rc) , ((0 , xK_Down ), goto rc ( 0, 1) rs >>= rhombusMode viewFunc rc) , ((0 , xK_Tab ), gotoNextMatch rc rs >>= rhombusMode viewFunc rc) , ((_S , xK_Tab ), gotoPrevMatch rc rs >>= rhombusMode viewFunc rc) , ((0 , xK_Return ), removeRhombus rs >> return (selectFocused rs) >>= viewFunc) ] _S = shiftMask -- TODO make failbeep configurable failbeep = spawn "beep -l 100 -f 500" goto :: RhombusConfig -> (Position, Position) -> RhombusState -> X RhombusState goto RhombusConfig{rc_wrap=True} xy rs = maybe (failbeep >> return rs) return $ wrapFocus xy rs goto RhombusConfig{rc_wrap=False} xy rs = maybe (failbeep >> return rs) return $ moveFocus xy rs moveFocus :: (Position, Position) -> RhombusState -> Maybe RhombusState moveFocus (dx, dy) rs@RhombusState{rs_focus=(x,y)} = do let focus' = (x + dx, y + dy) if elem focus' (reachableCoords rs) then Just rs { rs_focus = focus' } else Nothing wrapFocus :: (Position, Position) -> RhombusState -> Maybe RhombusState wrapFocus (0, dy) rs@RhombusState{rs_focus=focus} = do let column = sortBy (comparing snd) $ filter ((==) (fst focus) . fst) (reachableCoords rs) i <- elemIndex focus column return rs { rs_focus = column `modIndex` (i + fromIntegral dy) } wrapFocus (dx, 0) rs@RhombusState{rs_focus=focus} = do let column = sortBy (comparing fst) $ filter ((==) (snd focus) . snd) (reachableCoords rs) i <- elemIndex focus column return rs { rs_focus = column `modIndex` (i + fromIntegral dx) } wrapFocus _ _ = Nothing gotoPrevMatch :: RhombusConfig -> RhombusState -> X RhombusState gotoPrevMatch rc rs@RhombusState{rs_focus=focus} = do case reverse (matchingReachableCoords rc rs) of [] -> failbeep >> return rs xs -> return rs { rs_focus = maybe (head xs) (modIndex xs . (+1)) (focus `elemIndex` xs) } gotoNextMatch :: RhombusConfig -> RhombusState -> X RhombusState gotoNextMatch rc rs@RhombusState{rs_focus=focus} = do case matchingReachableCoords rc rs of [] -> failbeep >> return rs xs -> return rs { rs_focus = maybe (head xs) (modIndex xs . (+1)) (focus `elemIndex` xs) } selectFocused :: RhombusState -> String selectFocused rs = -- TODO the rhombus must never "focus" something inexistent fromJust $ lookup (rs_focus rs) $ zip wave (rs_strings rs) incSearchPushChar :: Char -> RhombusState -> X RhombusState incSearchPushChar c rs = return rs { rs_search = rs_search rs ++ [c] } incSearchPopChar :: RhombusState -> X RhombusState -- only rubout if we have at least one char incSearchPopChar rs@RhombusState{rs_search=xs@(_:_)} = return rs { rs_search = init xs } incSearchPopChar rs = return rs redraw :: RhombusConfig -> RhombusState -> X () redraw rc rs = do ss <- gets windowset let Screen _ _ (SD (Rectangle _ _ s_width s_height)) = current ss -- TODO this let is duplicated in newRhombus let scale x = x * cell_w `div` s_width -- TODO use bw cell_w = rc_cellwidth rc cell_h = scale s_height -- txy is the top-left corner of the first (center) cell -- XXX div and (-) are not distributive -- we could round $ (s_* - cell_*) / 2, though... tx = fi $ s_width `div` 2 - cell_w `div` 2 ty = fi $ s_height `div` 2 - cell_h `div` 2 margin = rc_margin rc -- dxy are the outer cell dimensions (i.e. including the border) dx = fi $ cell_w + 2 + margin dy = fi $ cell_h + 2 + margin paint = rc_paint rc xmf = rs_font rs tags = rs_strings rs --currentTag = last tags withDisplay $ \ d -> do -- XXX we cannot use withPixmapAndGC because rc_paint is an X monad p <- io $ createPixmap d (rs_window rs) s_width s_height (defaultDepthOfScreen $ defaultScreenOfDisplay d) g <- io $ createGC d p -- TODO fixme color_black <- stringToPixel d "black" forZipWithM_ tags (reachableCoords rs) $ \ tag oxy@(ox, oy) -> do let focus = oxy == rs_focus rs match = isXOf (rc_matchmethod rc) (rs_search rs) tag current = tag == last tags (_b_color, _bg_color, _fg_color) = rc_colors rc focus match current --cell_x = (ox * dx) + x - fi (cell_w `div` 2) --cell_y = (oy * dy) + y - fi (cell_h `div` 2) cell_x = (ox * dx) + tx + 1 cell_y = (oy * dy) + ty + 1 b_color <- stringToPixel d _b_color bg_color <- stringToPixel d _bg_color fg_color <- stringToPixel d _fg_color -- draw background io $ setForeground d g bg_color io $ fillRectangle d p g cell_x cell_y cell_w cell_h -- draw border io $ setForeground d g b_color io $ drawLines d p g [ Point (cell_x - 1) (cell_y - 1) , Point (fi cell_w + 1) 0 , Point 0 (fi cell_h + 1) , Point (-(fi cell_w + 1)) 0 , Point 0 (-(fi cell_h + 1)) ] coordModePrevious -- custom draw paint rc d p g tag (Rectangle cell_x cell_y cell_w cell_h) focus match current -- paint text -- TODO custom paint text? -- TODO withCopyArea io $ withPixmapAndGC d p s_width s_height (defaultDepthOfScreen $ defaultScreenOfDisplay d) $ \ f_pm f_gc -> do withPixmapAndGC d f_pm s_width s_height 1 $ \ clip_mask clip_gc -> do setForeground d clip_gc 0 setBackground d clip_gc 0 fillRectangle d clip_mask clip_gc 0 0 s_width s_height setForeground d clip_gc 1 let r = Rectangle cell_x cell_y cell_w cell_h printStringCentered d clip_mask xmf clip_gc r tag setForeground d f_gc fg_color setBackground d f_gc color_black -- TODO printStringCentered d f_pm xmf f_gc r tag setClipMask d f_gc clip_mask copyArea d f_pm p f_gc 0 0 s_width s_height 0 0 io $ copyArea d p (rs_window rs) g 0 0 s_width s_height 0 0 io $ freePixmap d p io $ freeGC d g newRhombus :: RhombusConfig -> [String] -> X RhombusState newRhombus rc tags = do ss <- gets windowset let Screen _ _ (SD (Rectangle _ _ s_width s_height)) = current ss (_, def_win_bg, _) = rc_colors rc False True False -- TODO this let is duplicated in redraw let scale x = x * cell_w `div` s_width -- TODO use bw cell_w = rc_cellwidth rc cell_h = scale s_height -- TODO don't delete this let but use it instead of s_{width,height} -- (xcoords, ycoords) = unzip $ take (length tags) wave -- this is reachableCoords -- win_width = (maximum xcoords - minimum xcoords) * dx -- win_height = (maximum ycoords - minimum ycoords) * dy -- txy is the top-left corner of the first (center) cell -- XXX div and (-) are not distributive -- we could round $ (s_* - cell_*) / 2, though... tx = fi $ s_width `div` 2 - cell_w `div` 2 ty = fi $ s_height `div` 2 - cell_h `div` 2 margin = rc_margin rc -- dxy are the outer cell dimensions (i.e. including the border) dx = fi $ cell_w + 2 + margin dy = fi $ cell_h + 2 + margin fn <- initXMF (rc_font rc) win <- createNewWindow (Rectangle 0 0 s_width s_height) Nothing def_win_bg True withDisplay $ \ d -> io $ shapeWindow d win $ \ p g -> forZipWithM_ tags wave $ \ _ (ox, oy) -> fillRectangle d p g (tx + ox * dx) (ty + oy * dy) (fi cell_w + 2) (fi cell_h + 2) return $ RhombusState win "" fn (0,0) tags removeRhombus :: RhombusState -> X () removeRhombus (RhombusState w _ fn _ _) = do deleteWindow w releaseXMF fn wave :: [(Position, Position)] wave = zip (0:(concat $ map (\i -> [0..i]++[i-1,i-2..1] ++ [0,-1..(-i)]++[-i,-i+1..(-1)]) [1..])) (concat $ map (\i -> [0..i]++[i-1,i-2..1] ++ [0,-1..(-i)]++[-i+1,-i+2..(-1)]) [1..]) where wave1 = 0:(concat $ map (\i -> [0..i]++[i-1,i-2..1] ++ [0,-1..(-i)]++[-i,-i+1..(-1)]) [1..]) wave2 = concat $ map (\i -> [0..i]++[i-1,i-2..1] ++ [0,-1..(-i)]++[-i+1,-i+2..(-1)]) [1..] commonPrefix (x:xs) (y:ys) | x == y = x:commonPrefix xs ys commonPrefix _ _ = [] isXOf :: MatchMethod -> String -> String -> Bool isXOf MatchInfix = isInfixOf isXOf MatchPrefix = isPrefixOf findXIndex :: (Eq a) => MatchMethod -> [a] -> [a] -> Maybe Int findXIndex MatchInfix = findInfixIndex findXIndex MatchPrefix = findPrefixIndex findInfixIndex :: (Eq a) => [a] -> [a] -> Maybe Int findInfixIndex needle haystack = (\x -> if null x then Nothing else Just (fst $ head x)) . dropWhile (\(_,x) -> not $ isPrefixOf needle x) $ zip [0..] (tails haystack) findPrefixIndex :: (Eq a) => [a] -> [a] -> Maybe Int findPrefixIndex needle haystack = if isPrefixOf needle haystack then Just 0 else Nothing modIndex :: Integral i => [a] -> i -> a modIndex xs i = xs `genericIndex` (i `mod` genericLength xs) forZipWithM_ a b f = zipWithM_ f a b withGrabbedKeyboard f = do XConf { theRoot = root, display = d } <- ask catchX (io (grabKeyboard d root False grabModeAsync grabModeAsync currentTime) >> f) (return ()) io $ ungrabKeyboard d currentTime