This commit is contained in:
Nathan Dwarshuis 2020-03-31 23:15:43 -04:00
commit 4f1f1b3a7f
6 changed files with 316 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.stack-work/
stack.yaml.lock
TAGS
*~

30
LICENSE Normal file
View File

@ -0,0 +1,30 @@
Copyright Nathan Dwarshuis (c) 2020
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Nathan Dwarshuis nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
README.md Normal file
View File

@ -0,0 +1 @@
# xman-hs

178
app/Main.hs Normal file
View File

@ -0,0 +1,178 @@
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main where
import Control.Monad (forM_, forever, void, when)
import Control.Monad.Reader
import Control.Monad.State
import Data.List (any)
import Data.Maybe (isJust)
import Foreign.C.String (castCCharToChar)
import Foreign.C.Types (CLong)
import Graphics.X11.Types
import Graphics.X11.Xlib.Atom
import Graphics.X11.Xlib.Display
import Graphics.X11.Xlib.Event
import Graphics.X11.Xlib.Extras
import Graphics.X11.Xlib.Misc
import Graphics.X11.Xlib.Types
import Text.Regex.TDFA
import System.Environment
import System.Posix.IO
import System.Posix.Signals
import System.Process
type WindowTitle = String
-- I wonder where this idea came from...
newtype XMan a = XMan (ReaderT XMConf (StateT XMState IO) a) deriving
(Functor, Monad, MonadIO, MonadState XMState, MonadReader XMConf)
instance Applicative XMan where
pure = return
(<*>) = ap
data XMState = XMState
{ currentTitle :: Maybe WindowTitle
, xcapeProcess :: Maybe ProcessHandle
}
data XMConf = XMConf
{ display :: Display
, theRoot :: Window
, netActiveWindow :: Atom
, netWMName :: Atom
, regexps :: [String]
, xcapeKeys :: String
}
io :: MonadIO m => IO a -> m a
io = liftIO
runXMan :: XMConf -> XMState -> XMan a -> IO (a, XMState)
runXMan c s (XMan a) = runStateT (runReaderT a c) s
parse :: [String] -> IO ()
parse [_] = usage
parse (x:rs) = initXMan x rs
parse _ = usage
usage :: IO ()
usage = putStrLn "xman XCAPE_KEYS REGEXP [[REGEXP]...]"
main :: IO ()
main = getArgs >>= parse
initXMan :: String -> [String] -> IO ()
initXMan x r = do
void $ installHandler sigCHLD Ignore Nothing
dpy <- openDisplay ""
root <- rootWindow dpy $ defaultScreen dpy
naw <- internAtom dpy "_NET_ACTIVE_WINDOW" False
nwn <- internAtom dpy "_NET_WM_NAME" False
let cf = XMConf
{ display = dpy
, theRoot = root
, netActiveWindow = naw
, netWMName = nwn
, regexps = r
, xcapeKeys = x
}
st = XMState { currentTitle = Nothing, xcapeProcess = Nothing }
allocaSetWindowAttributes $ \a -> do
set_event_mask a propertyChangeMask
changeWindowAttributes dpy root cWEventMask a
void $ allocaXEvent $ \e ->
runXMan cf st $ do
updateXCape
forever $ handle =<< io (nextEvent dpy e >> getEvent e)
updateXCape :: XMan ()
updateXCape = do
dpy <- asks display
atom <- asks netActiveWindow
root <- asks theRoot
prop <- io $ getWindowProperty32 dpy atom root
case prop of
Just [aw] -> getTitle aw >>= updateTitle >> startOrKillXCape
_ -> return ()
handle :: Event -> XMan ()
handle PropertyEvent { ev_atom = a } = do
atom <- asks netActiveWindow
when (a == atom) updateXCape
handle _ = return ()
getTitle :: CLong -> XMan (Maybe WindowTitle)
getTitle winID = do
nwn <- asks netWMName
-- try getting _NET_WM_NAME first before trying legacy WM_NAME
doMaybe [nwn, wM_NAME] $ getTitle' winID
where
doMaybe (x:xs) f = f x >>= (\r -> if isJust r then return r else doMaybe xs f)
doMaybe [] _ = return Nothing
getTitle' :: CLong -> Atom -> XMan (Maybe WindowTitle)
getTitle' winID atom = do
dpy <- asks display
title' <- io $ permitBadWindow $ getWindowProperty8 dpy atom
$ fromIntegral winID
return $ fmap (fmap castCCharToChar) title'
permitBadWindow :: IO a -> IO a
permitBadWindow action = do
handler <- mkXErrorHandler $ \_ e ->
getErrorEvent e >>= handleError >> return 0
original <- _xSetErrorHandler handler
res <- action
void $ _xSetErrorHandler original
return res
where
-- totally ignore BadWindow errors
-- TODO also ignore badvalue errors?
handleError ErrorEvent { ev_error_code = t }
| fromIntegral t == badWindow = return ()
-- anything not a BadWindow is an unexpected error
handleError _ = print "actual error"
updateTitle :: Maybe WindowTitle -> XMan ()
updateTitle newTitle = modify (\s -> s { currentTitle = newTitle } )
startOrKillXCape :: XMan ()
startOrKillXCape = do
title <- gets currentTitle
case title of
Just t -> asks regexps >>= \r ->
if any (t =~) r then stopXCape else startXCape
Nothing -> startXCape
startXCape :: XMan ()
startXCape = do
pID <- gets xcapeProcess
unless (isJust pID) $ do
x <- asks xcapeKeys
h <- io $ runXcape x
modify $ \s -> s { xcapeProcess = Just h }
io $ print "started xcape"
stopXCape :: XMan ()
stopXCape = do
pID <- gets xcapeProcess
forM_ pID $ \p -> do
io $ terminateProcess p
modify $ \s -> s { xcapeProcess = Nothing }
io $ print "stopped xcape"
runXcape :: String -> IO ProcessHandle
runXcape keys = do
dn <- fmap UseHandle $ fdToHandle
=<< openFd "/dev/null" ReadOnly Nothing defaultFileFlags
let cp = proc "xcape" $ ["-d", "-t", "500", "-e"] ++ [keys]
(_, _, _, h) <- createProcess $ cp { std_err = dn, std_out = dn }
return h

66
stack.yaml Normal file
View File

@ -0,0 +1,66 @@
# This file was automatically generated by 'stack init'
#
# Some commonly used options have been documented as comments in this file.
# For advanced use and comprehensive documentation of the format, please see:
# https://docs.haskellstack.org/en/stable/yaml_configuration/
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# resolver: lts-3.5
# resolver: nightly-2015-09-21
# resolver: ghc-7.10.2
#
# The location of a snapshot can be provided as a file or url. Stack assumes
# a snapshot provided as a file might change, whereas a url resource does not.
#
# resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-14.12
# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# subdirs:
# - auto-update
# - wai
packages:
- .
# Dependency packages to be pulled from upstream that are not in the resolver.
# These entries can reference officially published versions as well as
# forks / in-progress versions pinned to a git hash. For example:
#
# extra-deps:
# - acme-missiles-0.3
# - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#
# extra-deps: []
# Override default flag values for local packages and extra-deps
# flags: {}
# Extra package databases containing global packages
# extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
#
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: ">=2.1"
#
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
#
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
#
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

37
xman-hs.cabal Normal file
View File

@ -0,0 +1,37 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.31.2.
--
-- see: https://github.com/sol/hpack
--
-- hash: 32a585abcde8be89acd003c83fd878360a2fa7ed9a02418cbcbf6539cfd85401
name: xman-hs
version: 0.1.0.0
description: Please see the README on GitHub at <https://github.com/ndwarshuis/xman-hs#readme>
homepage: https://github.com/ndwarshuis/xman-hs#readme
bug-reports: https://github.com/ndwarshuis/xman-hs/issues
author: Nathan Dwarshuis
maintainer: ndwar@yavin4.ch
copyright: 2020 Nathan Dwarshuis
license: BSD3
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
executable xman-hs
main-is: Main.hs
hs-source-dirs:
app
ghc-options: -Wall -Werror -fno-warn-missing-signatures -threaded -rtsopts -with-rtsopts=-N
build-depends:
base >=4.7 && <5
, X11
, mtl >= 2.2.2
, unix >= 2.7.2.2
, regex-tdfa >= 1.2.3.2
, process >= 1.6.5.0
default-language: Haskell2010