1
0
Fork 0
mirror of https://github.com/malob/prefmanager.git synced 2024-12-14 11:57:49 +00:00

Add keys command, and refactor with Relude

This commit is contained in:
Malo Bourgon 2021-01-27 18:22:20 -08:00
parent 56d271b02e
commit d70f1df876
12 changed files with 141 additions and 136 deletions

View file

@ -1,6 +1,6 @@
# macOS Preferences Manager # macOS Preferences Manager
This project is a WIP, currently only watching functionality has been implemented. This project is a WIP.
## Usage ## Usage
@ -9,7 +9,7 @@ Currently available commands:
``` ```
> prefmanager --help > prefmanager --help
macOS Preferences Manager - a utility for working with macOS preferences macOS Preferences Manager - a utility for working with macOS preferences.
Usage: prefmanager COMMAND Usage: prefmanager COMMAND
@ -17,8 +17,9 @@ Available options:
-h,--help Show this help text -h,--help Show this help text
Available commands: Available commands:
watch Watch domain(s) for changes watch Watch domain(s) for changes.
domains List all domains domains List all domains.
keys List the current keys in a domain.
``` ```
Watch functionality: Watch functionality:

View file

@ -4,11 +4,6 @@ module Main where
import Defaults import Defaults
import Defaults.Types (DomainName(..)) import Defaults.Types (DomainName(..))
import Control.Monad (join)
import Data.Coerce (coerce)
import Data.Foldable (traverse_)
import qualified Data.Set as S
import qualified Data.Text as T
import Options.Applicative import Options.Applicative
-- | Main -- | Main
@ -19,31 +14,39 @@ main = join $ execParser opts
opts :: ParserInfo (IO ()) opts :: ParserInfo (IO ())
opts = info opts = info
(commands <**> helper) (commands <**> helper)
(fullDesc <> header "macOS Preferences Manager - a utility for working with macOS preferences") (fullDesc <> header "macOS Preferences Manager - a utility for working with macOS preferences.")
-- | App CLI commands -- | App CLI commands
commands :: Parser (IO ()) commands :: Parser (IO ())
commands = hsubparser commands = hsubparser
( command "watch" ( command "watch"
(info (info
( watch . S.fromList <$> some ( watch . fromList <$> some
(DomainName <$> strArgument (DomainName <$> strArgument
( metavar "DOMAIN..." ( metavar "DOMAIN..."
<> completer (listIOCompleter $ fmap (T.unpack . coerce) . S.toList <$> domains) <> help "Domain(s) that will be watched."
<> help "Domain(s) that will be watched"
) )
) )
<|> flag' (watch =<< domains) <|> flag' (watch =<< domains)
( long "all" ( long "all"
<> short 'a' <> short 'a'
<> help "Watch all domains including NSGlobalDomain" <> help "Watch all domains including NSGlobalDomain."
) )
) )
$ progDesc "Watch domain(s) for changes" $ progDesc "Watch domain(s) for changes."
) )
<> command "domains" <> command "domains"
(info (info
(pure $ domains >>= traverse_ (putStrLn . T.unpack . coerce) . S.toList) (pure printDomains)
(progDesc "List all domains") (progDesc "List all domains.")
)
<> command "keys"
(info
(printKeys . DomainName <$> strArgument
( metavar "DOMAIN"
<> help "A domain for which to list keys."
)
)
$ progDesc "List the current keys in a domain."
) )
) )

View file

@ -32,8 +32,10 @@
packages = [ packages = [
compiler.haskell-language-server compiler.haskell-language-server
compiler.implicit-hie compiler.implicit-hie
compiler.weeder
pkgs.cabal2nix pkgs.cabal2nix
pkgs.stack pkgs.stack
pkgs.hlint
]; ];
commands = [ commands = [
{ {

View file

@ -14,20 +14,22 @@ extra-source-files:
description: Please see the README on GitHub at <https://github.com/malob/prefmanager#readme> description: Please see the README on GitHub at <https://github.com/malob/prefmanager#readme>
dependencies: dependencies:
- base >= 4.7 && < 5 - base-noprelude
- async
- containers
- hxt
- optparse-applicative
- patience
- plist
- prettyprinter
- prettyprinter-ansi-terminal
- process
- text
library: library:
source-dirs: src source-dirs: src
dependencies:
- relude
- ansi-terminal
- async
- containers
- hxt
- patience
- plist
- prettyprinter
- prettyprinter-ansi-terminal
- process
- text
executables: executables:
prefmanager: prefmanager:
@ -39,6 +41,7 @@ executables:
- -with-rtsopts=-N - -with-rtsopts=-N
dependencies: dependencies:
- prefmanager - prefmanager
- optparse-applicative
tests: tests:
prefmanager-test: prefmanager-test:

View file

@ -4,7 +4,7 @@ cabal-version: 1.12
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
-- --
-- hash: e47b42f89876fbc6a9f6ac24c1b66d161577d67abc0267f021a78a23c1ca8389 -- hash: 1a8cff5464ff5ded24a399492c6be76412d42e1e21780bb40565735f140afab2
name: prefmanager name: prefmanager
version: 0.1.0.0 version: 0.1.0.0
@ -31,21 +31,23 @@ library
Defaults Defaults
Defaults.Pretty Defaults.Pretty
Defaults.Types Defaults.Types
Prelude
other-modules: other-modules:
Paths_prefmanager Paths_prefmanager
hs-source-dirs: hs-source-dirs:
src src
build-depends: build-depends:
async ansi-terminal
, base >=4.7 && <5 , async
, base-noprelude
, containers , containers
, hxt , hxt
, optparse-applicative
, patience , patience
, plist , plist
, prettyprinter , prettyprinter
, prettyprinter-ansi-terminal , prettyprinter-ansi-terminal
, process , process
, relude
, text , text
default-language: Haskell2010 default-language: Haskell2010
@ -57,18 +59,9 @@ executable prefmanager
app app
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: build-depends:
async base-noprelude
, base >=4.7 && <5
, containers
, hxt
, optparse-applicative , optparse-applicative
, patience
, plist
, prefmanager , prefmanager
, prettyprinter
, prettyprinter-ansi-terminal
, process
, text
default-language: Haskell2010 default-language: Haskell2010
test-suite prefmanager-test test-suite prefmanager-test
@ -80,16 +73,6 @@ test-suite prefmanager-test
test test
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: build-depends:
async base-noprelude
, base >=4.7 && <5
, containers
, hxt
, optparse-applicative
, patience
, plist
, prefmanager , prefmanager
, prettyprinter
, prettyprinter-ansi-terminal
, process
, text
default-language: Haskell2010 default-language: Haskell2010

View file

@ -1,6 +1,6 @@
{ mkDerivation, async, base, containers, hpack, hxt { mkDerivation, ansi-terminal, async, base-noprelude, containers
, optparse-applicative, patience, plist, prettyprinter , hpack, hxt, optparse-applicative, patience, plist, prettyprinter
, prettyprinter-ansi-terminal, process, stdenv, text , prettyprinter-ansi-terminal, process, relude, stdenv, text
}: }:
mkDerivation { mkDerivation {
pname = "prefmanager"; pname = "prefmanager";
@ -9,18 +9,12 @@ mkDerivation {
isLibrary = true; isLibrary = true;
isExecutable = true; isExecutable = true;
libraryHaskellDepends = [ libraryHaskellDepends = [
async base containers hxt optparse-applicative patience plist ansi-terminal async base-noprelude containers hxt patience plist
prettyprinter prettyprinter-ansi-terminal process text prettyprinter prettyprinter-ansi-terminal process relude text
]; ];
libraryToolDepends = [ hpack ]; libraryToolDepends = [ hpack ];
executableHaskellDepends = [ executableHaskellDepends = [ base-noprelude optparse-applicative ];
async base containers hxt optparse-applicative patience plist testHaskellDepends = [ base-noprelude ];
prettyprinter prettyprinter-ansi-terminal process text
];
testHaskellDepends = [
async base containers hxt optparse-applicative patience plist
prettyprinter prettyprinter-ansi-terminal process text
];
prePatch = "hpack"; prePatch = "hpack";
homepage = "https://github.com/malob/prefmanager#readme"; homepage = "https://github.com/malob/prefmanager#readme";
description = "A CLI utility for managing macOS preferences"; description = "A CLI utility for managing macOS preferences";

View file

@ -1,70 +1,81 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Defaults where module Defaults where
import Defaults.Pretty (prettyDomainDiffs) import Defaults.Pretty (prettyDomainDiffs)
import Defaults.Types (DomainDiff(..), Domains, Domain, DomainName(..)) import Defaults.Types (DomainDiff(..), Domains(..), Domain(..), DomainName(..), Key)
import Relude.Extra (un, wrap, traverseToSnd, keys)
import Control.Concurrent.Async (mapConcurrently) import Control.Concurrent.Async (mapConcurrently)
import Data.Coerce (coerce)
import Data.List (delete) import Data.List (delete)
import Data.Map (Map) import qualified Data.Map.Strict as M
import qualified Data.Map as M import Data.Text (stripEnd, splitOn)
import Data.Maybe (fromJust) import Patience.Map (diff, isSame, toDelta)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Patience.Map
import Prettyprinter.Render.Terminal (putDoc) import Prettyprinter.Render.Terminal (putDoc)
import System.IO (hFlush, stdout) import System.Console.ANSI (clearLine, setCursorColumn)
import System.IO (hFlush)
import System.Process (shell, readCreateProcess) import System.Process (shell, readCreateProcess)
import Text.XML.HXT.Core (no, withSubstDTDEntities, withValidate) import Text.XML.HXT.Core (no, withSubstDTDEntities, withValidate)
import Text.XML.Plist (PlObject, fromPlDict, readPlistFromString) import Text.XML.Plist (PlObject, fromPlDict, readPlistFromString)
-- | Convenience function for running macOS @defaults@ command. -- | Convenience function for running macOS @defaults@ command.
defaultsCmd :: Text -> IO Text defaultsCmd :: Text -> IO Text
defaultsCmd (T.unpack -> s) = T.pack <$> readCreateProcess (shell $ "defaults " <> s) "" defaultsCmd (toString -> t) = toText <$> readCreateProcess (shell $ "/usr/bin/defaults " <> t) ""
-- | Convenience function for parsing Plist strings -- | Convenience function for parsing Plist strings
parsePlist :: Text -> IO PlObject parsePlist :: Text -> IO PlObject
parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no] . T.unpack parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no] . toString
-- | Gets list of domains by running @defaults domains@ and adds @NSGlobalDomain@ to the 'Set'. -- | Gets list of domains by running @defaults domains@ and adds @NSGlobalDomain@ to the 'Set'.
domains :: IO (Set DomainName) domains :: IO (Set DomainName)
domains domains
= S.fromList = fromList
. coerce . wrap
. ("NSGlobalDomain" :) . ("NSGlobalDomain" :)
. T.splitOn ", " . splitOn ", "
. T.stripEnd . stripEnd
<$> defaultsCmd "domains" <$> defaultsCmd "domains"
-- | Runs @defaults export [domain] -@ and parses the output. -- | Runs @defaults export [domain] -@ and parses the output.
export :: DomainName -> IO Domain export :: DomainName -> IO Domain
export (coerce -> d) export d
= coerce = wrap
. M.fromList . fromList @(Map _ _)
. fromJust . maybeToMonoid
. fromPlDict . fromPlDict
<$> (defaultsCmd ("export '" <> d <> "' -") >>= parsePlist) <$> (defaultsCmd ("export '" <> un d <> "' -") >>= parsePlist)
-- | Runs 'export' on the 'Set' of provided domains -- | Runs 'export' on the 'Set' of provided domains
exports :: Set DomainName -> IO Domains exports :: Set DomainName -> IO Domains
exports = (M.fromList <$>) . mapConcurrently (\d -> (d,) <$> export d) . S.toList exports = wrap . fmap (fromList @(Map _ _)) . mapConcurrently (traverseToSnd export) . toList
diffDomain :: Domain -> Domain -> DomainDiff diffDomain :: Domain -> Domain -> DomainDiff
diffDomain old new = coerce $ M.filter (not . isSame) $ diff old new diffDomain (Domain old) (Domain new) = wrap $ M.filter (not . isSame) $ diff old new
-- | Watches a 'Set' of domains and prints any changes. -- | Watches a 'Set' of domains and prints any changes.
watch :: Set DomainName -> IO () watch :: Set DomainName -> IO ()
watch ds = exports ds >>= (putStrLn "Watching..." >>) . go where watch ds = exports ds >>= (putStrLn "Watching..." *>) . go 0 where
go :: Domains -> IO () go :: Int -> Domains -> IO ()
go old = do go count old = do
putText "Processing domains"
if count > 0 then putText $ " (" <> show count <> " loops with no changes)" else pass
hFlush stdout
new <- exports ds new <- exports ds
let domainDiffs = uncurry diffDomain <$> toDelta (diff old new) let domainDiffs = uncurry diffDomain <$> toDelta (diff (un old) (un new))
(if null domainDiffs then pure () else putDoc $ prettyDomainDiffs domainDiffs) clearLine
*> hFlush stdout setCursorColumn 0
*> go new if null domainDiffs
then go (count + 1) new
else do
putDoc $ prettyDomainDiffs domainDiffs
go 0 new
-- | Print the keys of a given domain
printKeys :: DomainName -> IO ()
printKeys = traverse_ putStrLn . keys @(Map _ PlObject) . un <=< export
-- | Print the list of available domains
printDomains :: IO ()
printDomains = traverse_ (putTextLn . un) =<< domains

View file

@ -4,50 +4,47 @@ module Defaults.Pretty where
import Defaults.Types (DomainDiff(..), DomainName(..), Key) import Defaults.Types (DomainDiff(..), DomainName(..), Key)
import Data.Bool (bool) import Prelude hiding (group)
import Data.Coerce (coerce) import Relude.Extra (un)
import Data.Map (Map, foldrWithKey)
import Data.Map.Strict (foldMapWithKey)
import Patience.Map (Delta(..)) import Patience.Map (Delta(..))
import Prettyprinter import Prettyprinter
import Prettyprinter.Render.Terminal import Prettyprinter.Render.Terminal
import Text.XML.Plist (PlObject(..)) import Text.XML.Plist (PlObject(..))
prettyDomainDiffs :: Map DomainName DomainDiff -> Doc AnsiStyle prettyDomainDiffs :: Map DomainName DomainDiff -> Doc AnsiStyle
prettyDomainDiffs = foldrWithKey go emptyDoc where prettyDomainDiffs = foldMapWithKey go where
go :: DomainName -> DomainDiff -> Doc AnsiStyle -> Doc AnsiStyle go :: DomainName -> DomainDiff -> Doc AnsiStyle
go (DomainName name) diff doc go (DomainName name) diff
= doc = annotate (bold <> italicized) (pretty name)
<> hardline <> hardline
<> hardline <> hardline
<> annotate (bold <> italicized) (pretty name)
<> hardline
<> indent 2 (prettyDomainDiff diff) <> indent 2 (prettyDomainDiff diff)
<> hardline
prettyDomainDiff :: DomainDiff -> Doc AnsiStyle prettyDomainDiff :: DomainDiff -> Doc AnsiStyle
prettyDomainDiff = foldrWithKey go emptyDoc . coerce where prettyDomainDiff = foldMapWithKey go . un where
go :: Key -> Delta PlObject -> Doc AnsiStyle -> Doc AnsiStyle go :: Key -> Delta PlObject -> Doc AnsiStyle
go key delta doc go key = (<> hardline <> hardline) . \case
= doc Delta old new
<> hardline -> pretty key <+> "(Value changed)"
<> case delta of <> hardline
Delta old new <> indent 2 (red (pretty old) <> hardline <> green (pretty new))
-> pretty key <+> "(Value changed)" New x
<> hardline -> green
<> indent 2 (red (pretty old) <> hardline <> green (pretty new)) $ pretty key <+> "(Key added)"
New x <> hardline
-> green <> indent 2 (pretty x)
$ pretty key <+> "(Key added)" Old x
<> hardline -> red
<> indent 2 (pretty x) $ pretty key <+> "(Key removed)"
Old x <> hardline
-> red <> indent 2 (pretty x)
$ pretty key <+> "(Key removed)" Same x
<> hardline -> pretty key <+> "(No change)"
<> indent 2 (pretty x) <> hardline
Same x <> indent 2 (pretty x)
-> pretty key <+> "(No change)"
<> hardline
<> indent 2 (pretty x)
red = annotate $ colorDull Red red = annotate $ colorDull Red
green = annotate $ colorDull Green green = annotate $ colorDull Green

View file

@ -1,7 +1,5 @@
module Defaults.Types where module Defaults.Types where
import Data.Map (Map)
import Data.Text (Text)
import Patience.Map (Delta) import Patience.Map (Delta)
import Text.XML.Plist (PlObject) import Text.XML.Plist (PlObject)
@ -11,10 +9,10 @@ newtype DomainName = DomainName Text deriving (Eq, Ord, Show)
type Key = String type Key = String
-- | Representation of the settings of a domain. -- | Representation of the settings of a domain.
type Domain = Map Key PlObject newtype Domain = Domain (Map Key PlObject) deriving (Eq, Ord, Show)
-- | Map of domains. -- | Map of domains.
type Domains = Map DomainName Domain newtype Domains = Domains (Map DomainName Domain) deriving (Eq, Ord, Show)
-- | Map representing the change of the values of keys of a domain. -- | Map representing the change of the values of keys of a domain.
newtype DomainDiff = DomainDiff (Map Key (Delta PlObject)) deriving (Eq, Ord, Show) newtype DomainDiff = DomainDiff (Map Key (Delta PlObject)) deriving (Eq, Ord, Show)

3
src/Prelude.hs Normal file
View file

@ -0,0 +1,3 @@
module Prelude (module Relude) where
import Relude

View file

@ -6,9 +6,12 @@ packages:
extra-deps: extra-deps:
- patience-0.3 - patience-0.3
- base-noprelude-4.13.0.0
# Use fork of plist package that supports MonadFail # Use fork of plist package that supports MonadFail
- github: malob/plist - github: malob/plist
commit: 5e22be0933bf9a100868b944fa7a8b1236b99255 commit: 5e22be0933bf9a100868b944fa7a8b1236b99255
allow-newer: true
nix: nix:
shell-file: ./stack.nix shell-file: ./stack.nix

View file

@ -11,6 +11,13 @@ packages:
sha256: d2ca47451a599605c641b7c353588d7e1ba3d6036c8a52040b7d1b5b1af8959b sha256: d2ca47451a599605c641b7c353588d7e1ba3d6036c8a52040b7d1b5b1af8959b
original: original:
hackage: patience-0.3 hackage: patience-0.3
- completed:
hackage: base-noprelude-4.13.0.0@sha256:3cccbfda38e1422ca5cc436d58858ba51ff9114d2ed87915a6569be11e4e5a90,6842
pantry-tree:
size: 112
sha256: 90db92c8401880187ce642c5345407bcbd9546ea235524dd445cab2566ee3db1
original:
hackage: base-noprelude-4.13.0.0
- completed: - completed:
size: 4351 size: 4351
url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz