diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index e7896ba..bd30e28 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -14,8 +14,6 @@ jobs:
with:
install_url: https://github.com/numtide/nix-flakes-installer/releases/download/nix-2.4pre20201221_9fab14a/install
extra_nix_config: |
- trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= iohk.cachix.org-1:DpRUyj7h7V830dp/i6Nti+NEO2/nhblbov/8MW7Rqoo=
- substituters = https://cache.nixos.org https://hydra.iohk.io https://iohk.cachix.org
experimental-features = nix-command flakes
- name: Setup Cachix
diff --git a/.gitignore b/.gitignore
index af87ed4..48813b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
*~
.envrc
result
+result-doc
+.direnv/
diff --git a/app/Main.hs b/app/Main.hs
index 16073f2..072651b 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,11 +1,15 @@
+{-# LANGUAGE OverloadedStrings #-}
module Main where
-import Control.Monad ( join )
-import Data.Coerce ( coerce )
-import qualified Data.Set as S
-import Options.Applicative
-
import Defaults
+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
-- | Main
main :: IO ()
@@ -23,9 +27,9 @@ commands = hsubparser
( command "watch"
(info
( watch . S.fromList <$> some
- (Domain <$> strArgument
+ (DomainName <$> strArgument
( metavar "DOMAIN..."
- <> completer (listIOCompleter $ fmap coerce . S.toList <$> domains)
+ <> completer (listIOCompleter $ fmap (T.unpack . coerce) . S.toList <$> domains)
<> help "Domain(s) that will be watched"
)
)
@@ -39,7 +43,7 @@ commands = hsubparser
)
<> command "list-domains"
(info
- (pure $ S.toList <$> domains >>= mapM_ (putStrLn . coerce))
+ (pure $ domains >>= traverse_ (putStrLn . T.unpack . coerce) . S.toList)
(progDesc "List all domains")
)
)
diff --git a/flake.lock b/flake.lock
index bd00009..3063a83 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,5 +1,20 @@
{
"nodes": {
+ "devshell": {
+ "locked": {
+ "lastModified": 1611223133,
+ "narHash": "sha256-oRipYhMDsu2bUMHXBN5Q06nZhKSYqux0SsBcq+iKyYc=",
+ "owner": "numtide",
+ "repo": "devshell",
+ "rev": "4d217ab9f63da23bc43351c68c2d7dc4fb5d1077",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "devshell",
+ "type": "github"
+ }
+ },
"flake-compat": {
"flake": false,
"locked": {
@@ -31,21 +46,6 @@
"type": "github"
}
},
- "haskellNix": {
- "locked": {
- "lastModified": 1610500541,
- "narHash": "sha256-XedfI5fERgPeIbJTMkd6AH1lJGXv28CxBYMEjzAfrJc=",
- "owner": "input-output-hk",
- "repo": "haskell.nix",
- "rev": "faaca6735566d8707ffcc812ec0da964cded27f6",
- "type": "github"
- },
- "original": {
- "owner": "input-output-hk",
- "repo": "haskell.nix",
- "type": "github"
- }
- },
"nixpkgs": {
"locked": {
"lastModified": 1610575970,
@@ -61,12 +61,30 @@
"type": "github"
}
},
+ "plist-source": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1611283854,
+ "narHash": "sha256-7ubuVH6P2uYeCq82/ALd6E5szjBBqEtcO61u+5qRCt0=",
+ "owner": "malob",
+ "repo": "plist",
+ "rev": "5e22be0933bf9a100868b944fa7a8b1236b99255",
+ "type": "github"
+ },
+ "original": {
+ "owner": "malob",
+ "ref": "monadfail",
+ "repo": "plist",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
+ "devshell": "devshell",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
- "haskellNix": "haskellNix",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "plist-source": "plist-source"
}
}
},
diff --git a/flake.nix b/flake.nix
index 7286f7c..a9ef210 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,38 +3,52 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
- haskellNix.url = "github:input-output-hk/haskell.nix";
+ devshell.url = "github:numtide/devshell/";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
flake-utils.url = "github:numtide/flake-utils";
+ plist-source = { url = "github:malob/plist/monadfail"; flake = false; };
};
- outputs = { self, nixpkgs, haskellNix, flake-utils, ... }:
+ outputs = { self, devshell, nixpkgs, flake-utils, plist-source, ... }:
flake-utils.lib.eachSystem [ "x86_64-darwin" ] (system: let
- pkgs = import nixpkgs { inherit system; overlays = [ haskellNix.overlay ]; };
- name = "prefmanager";
- compiler = "ghc884";
- project = pkgs.haskell-nix.project' {
- inherit name;
- src = self;
- compiler-nix-name = compiler;
- };
- components = project.hsPkgs.${name}.components;
+ pkgs = nixpkgs.legacyPackages.${system};
+ compiler = pkgs.haskell.packages.ghc8103;
+ hlib = pkgs.haskell.lib;
+ plist = hlib.markUnbroken (hlib.overrideSrc compiler.plist { src = plist-source; });
+ prefmanager = compiler.callPackage ./prefmanager.nix { inherit plist; };
+ mkShell = devshell.legacyPackages.${system}.mkShell;
in rec {
# Built by `nix build .`
- defaultPackage = components.exes.${name};
- packages.${name} = defaultPackage;
+ defaultPackage = prefmanager;
+ packages.prefmanager = defaultPackage;
# Run `prefmanager` with `nix run .`
- defaultApp = { type = "app"; program = components.exes.${name}.exePath; };
- apps.${name} = defaultApp;
+ defaultApp = { type = "app"; program = "${prefmanager}/bin/prefmanager"; };
+ apps.prefmanager = defaultApp;
- # This is used by `nix develop .`
- devShell = project.shellFor {
- buildInputs = [
- pkgs.haskell.packages.${compiler}.haskell-language-server
- pkgs.haskell.packages.${compiler}.implicit-hie
+ # # This is used by `nix develop .`
+ devShell = mkShell {
+ name = "prefmanager";
+ packages = [
+ compiler.haskell-language-server
+ compiler.implicit-hie
+ pkgs.cabal2nix
pkgs.stack
];
+ commands = [
+ {
+ help = "Regenerate hie.yaml (run from project root)";
+ name = "hie";
+ category = "project";
+ command = "gen-hie > hie.yaml";
+ }
+ {
+ help = "Update prefmanager.nix (run from project root)";
+ name = "2nix";
+ category = "project";
+ command = "cabal2nix --hpack . > prefmanager.nix";
+ }
+ ];
};
}
);
diff --git a/hie.yaml b/hie.yaml
new file mode 100644
index 0000000..f01f210
--- /dev/null
+++ b/hie.yaml
@@ -0,0 +1,13 @@
+cradle:
+ stack:
+ - path: "./src"
+ component: "prefmanager:lib"
+
+ - path: "./app/Main.hs"
+ component: "prefmanager:exe:prefmanager"
+
+ - path: "./app/Paths_prefmanager.hs"
+ component: "prefmanager:exe:prefmanager"
+
+ - path: "./test"
+ component: "prefmanager:test:prefmanager-test"
diff --git a/package.yaml b/package.yaml
index 517ea13..e452d14 100644
--- a/package.yaml
+++ b/package.yaml
@@ -17,12 +17,14 @@ dependencies:
- base >= 4.7 && < 5
- async
- containers
-- diffmap
- hxt
- optparse-applicative
+- patience
- plist
-- pretty-show
+- prettyprinter
+- prettyprinter-ansi-terminal
- process
+- text
library:
source-dirs: src
diff --git a/prefmanager.cabal b/prefmanager.cabal
index b2e525f..d74a6e3 100644
--- a/prefmanager.cabal
+++ b/prefmanager.cabal
@@ -1,10 +1,10 @@
cabal-version: 1.12
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.3.
--
-- see: https://github.com/sol/hpack
--
--- hash: 1af3ad98cee7d7fc142970a5965d8baebd3604edff043fae4c2b5d15e4cde1dc
+-- hash: e47b42f89876fbc6a9f6ac24c1b66d161577d67abc0267f021a78a23c1ca8389
name: prefmanager
version: 0.1.0.0
@@ -29,6 +29,8 @@ source-repository head
library
exposed-modules:
Defaults
+ Defaults.Pretty
+ Defaults.Types
other-modules:
Paths_prefmanager
hs-source-dirs:
@@ -37,12 +39,14 @@ library
async
, base >=4.7 && <5
, containers
- , diffmap
, hxt
, optparse-applicative
+ , patience
, plist
- , pretty-show
+ , prettyprinter
+ , prettyprinter-ansi-terminal
, process
+ , text
default-language: Haskell2010
executable prefmanager
@@ -56,13 +60,15 @@ executable prefmanager
async
, base >=4.7 && <5
, containers
- , diffmap
, hxt
, optparse-applicative
+ , patience
, plist
, prefmanager
- , pretty-show
+ , prettyprinter
+ , prettyprinter-ansi-terminal
, process
+ , text
default-language: Haskell2010
test-suite prefmanager-test
@@ -77,11 +83,13 @@ test-suite prefmanager-test
async
, base >=4.7 && <5
, containers
- , diffmap
, hxt
, optparse-applicative
+ , patience
, plist
, prefmanager
- , pretty-show
+ , prettyprinter
+ , prettyprinter-ansi-terminal
, process
+ , text
default-language: Haskell2010
diff --git a/prefmanager.nix b/prefmanager.nix
new file mode 100644
index 0000000..752a5f9
--- /dev/null
+++ b/prefmanager.nix
@@ -0,0 +1,28 @@
+{ mkDerivation, async, base, containers, hpack, hxt
+, optparse-applicative, patience, plist, prettyprinter
+, prettyprinter-ansi-terminal, process, stdenv, text
+}:
+mkDerivation {
+ pname = "prefmanager";
+ version = "0.1.0.0";
+ src = ./.;
+ isLibrary = true;
+ isExecutable = true;
+ libraryHaskellDepends = [
+ async base containers hxt optparse-applicative patience plist
+ prettyprinter prettyprinter-ansi-terminal process text
+ ];
+ libraryToolDepends = [ hpack ];
+ executableHaskellDepends = [
+ async base containers hxt optparse-applicative patience plist
+ prettyprinter prettyprinter-ansi-terminal process text
+ ];
+ testHaskellDepends = [
+ async base containers hxt optparse-applicative patience plist
+ prettyprinter prettyprinter-ansi-terminal process text
+ ];
+ prePatch = "hpack";
+ homepage = "https://github.com/malob/prefmanager#readme";
+ description = "A CLI utility for managing macOS preferences";
+ license = stdenv.lib.licenses.bsd3;
+}
diff --git a/src/Defaults.hs b/src/Defaults.hs
index 8f6aa6b..d8b125d 100644
--- a/src/Defaults.hs
+++ b/src/Defaults.hs
@@ -1,62 +1,70 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
module Defaults where
-import Control.Concurrent.Async ( mapConcurrently )
-import Data.Coerce ( coerce )
-import Data.List ( delete )
-import Data.Map ( Map(..) )
-import qualified Data.Map as M
-import Data.Map.Delta
-import Data.Maybe
-import Data.Set ( Set(..) )
-import qualified Data.Set as S
-import Text.Show.Pretty
-import Text.XML.Plist
-import Text.XML.HXT.Core
-import System.Process ( shell
- , readCreateProcess
- )
+import Defaults.Pretty (prettyDomainDiffs)
+import Defaults.Types (DomainDiff(..), Domains, Domain, DomainName(..))
-newtype Domain = Domain String deriving (Eq, Ord, Show)
+import Control.Concurrent.Async (mapConcurrently)
+import Data.Coerce (coerce)
+import Data.List (delete)
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Maybe (fromJust)
+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 System.IO (hFlush, stdout)
+import System.Process (shell, readCreateProcess)
+import Text.XML.HXT.Core (no, withSubstDTDEntities, withValidate)
+import Text.XML.Plist (PlObject, fromPlDict, readPlistFromString)
-- | Convenience function for running macOS @defaults@ command.
-defaultsCmd :: String -> IO String
-defaultsCmd s = readCreateProcess (shell $ "defaults " <> s) ""
+defaultsCmd :: Text -> IO Text
+defaultsCmd (T.unpack -> s) = T.pack <$> readCreateProcess (shell $ "defaults " <> s) ""
-- | Convenience function for parsing Plist strings
-parsePlist :: String -> IO PlObject
-parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no]
+parsePlist :: Text -> IO PlObject
+parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no] . T.unpack
-- | Gets list of domains by running @defaults domains@ and adds @NSGlobalDomain@ to the 'Set'.
-domains :: IO (Set Domain)
+domains :: IO (Set DomainName)
domains
= S.fromList
- . (Domain "NSGlobalDomain" :)
- . fmap (Domain . delete ',')
- . words
+ . coerce
+ . ("NSGlobalDomain" :)
+ . T.splitOn ", "
+ . T.stripEnd
<$> defaultsCmd "domains"
-- | Runs @defaults export [domain] -@ and parses the output.
-export :: Domain -> IO (Map String PlObject)
+export :: DomainName -> IO Domain
export (coerce -> d)
- = M.fromList
+ = coerce
+ . M.fromList
. fromJust
. fromPlDict
- <$> (defaultsCmd ("export " <> d <> " -") >>= parsePlist)
+ <$> (defaultsCmd ("export '" <> d <> "' -") >>= parsePlist)
-- | Runs 'export' on the 'Set' of provided domains
-exports :: Set Domain -> IO (Map Domain (Map String PlObject))
+exports :: Set DomainName -> IO Domains
exports = (M.fromList <$>) . mapConcurrently (\d -> (d,) <$> export d) . S.toList
--- | Watches a 'Set' of domains for changes.
-watch :: Set Domain -> IO ()
-watch ds = exports ds >>= (putStrLn "Watching..." >>) . go where
+diffDomain :: Domain -> Domain -> DomainDiff
+diffDomain old new = coerce $ M.filter (not . isSame) $ diff old new
+
+-- | Watches a 'Set' of domains and prints any changes.
+watch :: Set DomainName -> IO ()
+watch ds = exports ds >>= (putStrLn "Watching..." >>) . go where
+ go :: Domains -> IO ()
go old = do
new <- exports ds
- let plDiffs
- = M.filter (not . null)
- . toDelta
- . (\(DeltaUnit o n) -> diff o n)
- <$> toDelta (diff old new)
- (if null plDiffs then pure () else pPrint plDiffs) *> go new
+ let domainDiffs = uncurry diffDomain <$> toDelta (diff old new)
+ (if null domainDiffs then pure () else putDoc $ prettyDomainDiffs domainDiffs)
+ *> hFlush stdout
+ *> go new
diff --git a/src/Defaults/Pretty.hs b/src/Defaults/Pretty.hs
new file mode 100644
index 0000000..a36571d
--- /dev/null
+++ b/src/Defaults/Pretty.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Defaults.Pretty where
+
+import Defaults.Types (DomainDiff(..), DomainName(..), Key)
+
+import Data.Bool (bool)
+import Data.Coerce (coerce)
+import Data.Map (Map, foldrWithKey)
+import Patience.Map (Delta(..))
+import Prettyprinter
+import Prettyprinter.Render.Terminal
+import Text.XML.Plist (PlObject(..))
+
+prettyDomainDiffs :: Map DomainName DomainDiff -> Doc AnsiStyle
+prettyDomainDiffs = foldrWithKey go emptyDoc where
+ go :: DomainName -> DomainDiff -> Doc AnsiStyle -> Doc AnsiStyle
+ go (DomainName name) diff doc
+ = doc
+ <> hardline
+ <> hardline
+ <> annotate (bold <> italicized) (pretty name)
+ <> hardline
+ <> indent 2 (prettyDomainDiff diff)
+
+prettyDomainDiff :: DomainDiff -> Doc AnsiStyle
+prettyDomainDiff = foldrWithKey go emptyDoc . coerce where
+ go :: Key -> Delta PlObject -> Doc AnsiStyle -> Doc AnsiStyle
+ go key delta doc
+ = doc
+ <> hardline
+ <> case delta of
+ Delta old new
+ -> pretty key <+> "(Value changed)"
+ <> hardline
+ <> indent 2 (red (pretty old) <> hardline <> green (pretty new))
+ New x
+ -> green
+ $ pretty key <+> "(Key added)"
+ <> hardline
+ <> indent 2 (pretty x)
+ Old x
+ -> red
+ $ pretty key <+> "(Key removed)"
+ <> hardline
+ <> indent 2 (pretty x)
+ Same x
+ -> pretty key <+> "(No change)"
+ <> hardline
+ <> indent 2 (pretty x)
+ red = annotate $ colorDull Red
+ green = annotate $ colorDull Green
+
+instance Pretty PlObject where
+ pretty = \case
+ PlString x -> tag "string" $ pretty x
+ PlBool x -> bool "" "" x
+ PlInteger x -> tag "integer" $ pretty x
+ PlReal x -> tag "real" $ pretty x
+ PlArray x -> tag "array" $ concatWith ((<>) . (<> hardline)) (fmap pretty x)
+ PlDict x -> tag "dict" $ concatWith ((<>) . (<> hardline)) (fmap prettyDict x)
+ PlData x -> tag "data" "[binary data]"
+ PlDate x -> tag "date" $ pretty x
+
+ where
+ prettyDict (k, o) = tag "key" (pretty k) <> hardline <> pretty o
+ tag t s = group $ flatAlt
+ (open <> hardline <> indent 4 s <> hardline <> close)
+ (open <> s <> close)
+ where
+ open = angles t
+ close = angles ("/" <> t)
+
diff --git a/src/Defaults/Types.hs b/src/Defaults/Types.hs
new file mode 100644
index 0000000..77a72d2
--- /dev/null
+++ b/src/Defaults/Types.hs
@@ -0,0 +1,20 @@
+module Defaults.Types where
+
+import Data.Map (Map)
+import Data.Text (Text)
+import Patience.Map (Delta)
+import Text.XML.Plist (PlObject)
+
+-- | Name of a domain, e.g., @NSGlobalDomain@, @com.apple.finder@, etc.
+newtype DomainName = DomainName Text deriving (Eq, Ord, Show)
+
+type Key = String
+
+-- | Representation of the settings of a domain.
+type Domain = Map Key PlObject
+
+-- | Map of domains.
+type Domains = Map DomainName Domain
+
+-- | Map representing the change of the values of keys of a domain.
+newtype DomainDiff = DomainDiff (Map Key (Delta PlObject)) deriving (Eq, Ord, Show)
diff --git a/stack.nix b/stack.nix
new file mode 100644
index 0000000..93b1bbd
--- /dev/null
+++ b/stack.nix
@@ -0,0 +1,6 @@
+{ ghc }:
+with import (import ./default.nix).inputs.nixpkgs {};
+haskell.lib.buildStackProject {
+ inherit ghc;
+ name = "prefmanager";
+}
diff --git a/stack.yaml b/stack.yaml
index ba5b767..5f93096 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,14 +1,14 @@
-# GHC 8.8.4
-resolver: lts-16.17
+# GHC 8.10.3
+resolver: nightly-2021-01-20
packages:
- .
extra-deps:
-- diffmap-0.1.0.0
+- patience-0.3
# Use fork of plist package that supports MonadFail
- github: malob/plist
- commit: 4103af23b364c101e44da115367800a2f90cc7c3
- # nix-sha256: 126c52498c89gd015p40hg671dg58j8xq8ggdqfywnkw2c8cjxxr
+ commit: 5e22be0933bf9a100868b944fa7a8b1236b99255
-allow-newer: true
+nix:
+ shell-file: ./stack.nix
diff --git a/stack.yaml.lock b/stack.yaml.lock
index b1e1990..443f943 100644
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -5,26 +5,26 @@
packages:
- completed:
- hackage: diffmap-0.1.0.0@sha256:27ea8c315b5dbfb243b2c3b61eab2164534e1d02887a05392dda65995cd36c3e,727
+ hackage: patience-0.3@sha256:fe3a2c5b1ac4d3425bad3d1ee0b6bb529b6e74ab41151f8526f26fc8dfa1206b,1252
pantry-tree:
- size: 264
- sha256: 76ea9b9ad91d113addadf58cb19e289e84347ab667aa5c7feed0a1ccfa4ed613
+ size: 368
+ sha256: d2ca47451a599605c641b7c353588d7e1ba3d6036c8a52040b7d1b5b1af8959b
original:
- hackage: diffmap-0.1.0.0
+ hackage: patience-0.3
- completed:
- size: 4384
- url: https://github.com/malob/plist/archive/4103af23b364c101e44da115367800a2f90cc7c3.tar.gz
+ size: 4351
+ url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz
name: plist
version: 0.0.6
- sha256: fcc0a7f0dcdb81d379329f5be2af2a566431ed66cbe37e9a1f4101e49b80d4c6
+ sha256: 7943778b77d008b8e529035d8a4d42c537ede9efe3d24900eb0d855b4567a892
pantry-tree:
size: 512
- sha256: 0ede20c4e670e906987a171721f05c3a3337ed936d5ff84e6fe77c13e2c7fbdf
+ sha256: 4872e51ef6ab654c99e06f286635fe1c977e165bc38f77c6bfb402e9fbff0559
original:
- url: https://github.com/malob/plist/archive/4103af23b364c101e44da115367800a2f90cc7c3.tar.gz
+ url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz
snapshots:
- completed:
- size: 532386
- url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/17.yaml
- sha256: d3ee1ae797cf63189c95cf27f00700304946c5cb3c1e6a82001cd6584a221e1b
- original: lts-16.17
+ size: 563098
+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2021/1/20.yaml
+ sha256: 97841a28b47a29e798aefda350785c31823f64f31c43ba057fc6c707dfb7ead3
+ original: nightly-2021-01-20