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

Improve watch output, and simplify nix configuration

This commit is contained in:
Malo Bourgon 2021-01-25 10:39:15 -08:00
parent 8d9cc82acc
commit b98c0e7624
15 changed files with 307 additions and 113 deletions

View file

@ -14,8 +14,6 @@ jobs:
with: with:
install_url: https://github.com/numtide/nix-flakes-installer/releases/download/nix-2.4pre20201221_9fab14a/install install_url: https://github.com/numtide/nix-flakes-installer/releases/download/nix-2.4pre20201221_9fab14a/install
extra_nix_config: | 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 experimental-features = nix-command flakes
- name: Setup Cachix - name: Setup Cachix

2
.gitignore vendored
View file

@ -2,3 +2,5 @@
*~ *~
.envrc .envrc
result result
result-doc
.direnv/

View file

@ -1,11 +1,15 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where module Main where
import Control.Monad ( join )
import Data.Coerce ( coerce )
import qualified Data.Set as S
import Options.Applicative
import Defaults 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
main :: IO () main :: IO ()
@ -23,9 +27,9 @@ commands = hsubparser
( command "watch" ( command "watch"
(info (info
( watch . S.fromList <$> some ( watch . S.fromList <$> some
(Domain <$> strArgument (DomainName <$> strArgument
( metavar "DOMAIN..." ( 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" <> help "Domain(s) that will be watched"
) )
) )
@ -39,7 +43,7 @@ commands = hsubparser
) )
<> command "list-domains" <> command "list-domains"
(info (info
(pure $ S.toList <$> domains >>= mapM_ (putStrLn . coerce)) (pure $ domains >>= traverse_ (putStrLn . T.unpack . coerce) . S.toList)
(progDesc "List all domains") (progDesc "List all domains")
) )
) )

View file

@ -1,5 +1,20 @@
{ {
"nodes": { "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-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -31,21 +46,6 @@
"type": "github" "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1610575970, "lastModified": 1610575970,
@ -61,12 +61,30 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"devshell": "devshell",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"haskellNix": "haskellNix", "nixpkgs": "nixpkgs",
"nixpkgs": "nixpkgs" "plist-source": "plist-source"
} }
} }
}, },

View file

@ -3,38 +3,52 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; 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-compat = { url = "github:edolstra/flake-compat"; flake = false; };
flake-utils.url = "github:numtide/flake-utils"; 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 flake-utils.lib.eachSystem [ "x86_64-darwin" ] (system: let
pkgs = import nixpkgs { inherit system; overlays = [ haskellNix.overlay ]; }; pkgs = nixpkgs.legacyPackages.${system};
name = "prefmanager"; compiler = pkgs.haskell.packages.ghc8103;
compiler = "ghc884"; hlib = pkgs.haskell.lib;
project = pkgs.haskell-nix.project' { plist = hlib.markUnbroken (hlib.overrideSrc compiler.plist { src = plist-source; });
inherit name; prefmanager = compiler.callPackage ./prefmanager.nix { inherit plist; };
src = self; mkShell = devshell.legacyPackages.${system}.mkShell;
compiler-nix-name = compiler;
};
components = project.hsPkgs.${name}.components;
in rec { in rec {
# Built by `nix build .` # Built by `nix build .`
defaultPackage = components.exes.${name}; defaultPackage = prefmanager;
packages.${name} = defaultPackage; packages.prefmanager = defaultPackage;
# Run `prefmanager` with `nix run .` # Run `prefmanager` with `nix run .`
defaultApp = { type = "app"; program = components.exes.${name}.exePath; }; defaultApp = { type = "app"; program = "${prefmanager}/bin/prefmanager"; };
apps.${name} = defaultApp; apps.prefmanager = defaultApp;
# This is used by `nix develop .` # # This is used by `nix develop .`
devShell = project.shellFor { devShell = mkShell {
buildInputs = [ name = "prefmanager";
pkgs.haskell.packages.${compiler}.haskell-language-server packages = [
pkgs.haskell.packages.${compiler}.implicit-hie compiler.haskell-language-server
compiler.implicit-hie
pkgs.cabal2nix
pkgs.stack 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";
}
];
}; };
} }
); );

13
hie.yaml Normal file
View file

@ -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"

View file

@ -17,12 +17,14 @@ dependencies:
- base >= 4.7 && < 5 - base >= 4.7 && < 5
- async - async
- containers - containers
- diffmap
- hxt - hxt
- optparse-applicative - optparse-applicative
- patience
- plist - plist
- pretty-show - prettyprinter
- prettyprinter-ansi-terminal
- process - process
- text
library: library:
source-dirs: src source-dirs: src

View file

@ -1,10 +1,10 @@
cabal-version: 1.12 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 -- see: https://github.com/sol/hpack
-- --
-- hash: 1af3ad98cee7d7fc142970a5965d8baebd3604edff043fae4c2b5d15e4cde1dc -- hash: e47b42f89876fbc6a9f6ac24c1b66d161577d67abc0267f021a78a23c1ca8389
name: prefmanager name: prefmanager
version: 0.1.0.0 version: 0.1.0.0
@ -29,6 +29,8 @@ source-repository head
library library
exposed-modules: exposed-modules:
Defaults Defaults
Defaults.Pretty
Defaults.Types
other-modules: other-modules:
Paths_prefmanager Paths_prefmanager
hs-source-dirs: hs-source-dirs:
@ -37,12 +39,14 @@ library
async async
, base >=4.7 && <5 , base >=4.7 && <5
, containers , containers
, diffmap
, hxt , hxt
, optparse-applicative , optparse-applicative
, patience
, plist , plist
, pretty-show , prettyprinter
, prettyprinter-ansi-terminal
, process , process
, text
default-language: Haskell2010 default-language: Haskell2010
executable prefmanager executable prefmanager
@ -56,13 +60,15 @@ executable prefmanager
async async
, base >=4.7 && <5 , base >=4.7 && <5
, containers , containers
, diffmap
, hxt , hxt
, optparse-applicative , optparse-applicative
, patience
, plist , plist
, prefmanager , prefmanager
, pretty-show , prettyprinter
, prettyprinter-ansi-terminal
, process , process
, text
default-language: Haskell2010 default-language: Haskell2010
test-suite prefmanager-test test-suite prefmanager-test
@ -77,11 +83,13 @@ test-suite prefmanager-test
async async
, base >=4.7 && <5 , base >=4.7 && <5
, containers , containers
, diffmap
, hxt , hxt
, optparse-applicative , optparse-applicative
, patience
, plist , plist
, prefmanager , prefmanager
, pretty-show , prettyprinter
, prettyprinter-ansi-terminal
, process , process
, text
default-language: Haskell2010 default-language: Haskell2010

28
prefmanager.nix Normal file
View file

@ -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;
}

View file

@ -1,62 +1,70 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Defaults where module Defaults where
import Control.Concurrent.Async ( mapConcurrently ) import Defaults.Pretty (prettyDomainDiffs)
import Data.Coerce ( coerce ) import Defaults.Types (DomainDiff(..), Domains, Domain, DomainName(..))
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
)
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. -- | Convenience function for running macOS @defaults@ command.
defaultsCmd :: String -> IO String defaultsCmd :: Text -> IO Text
defaultsCmd s = readCreateProcess (shell $ "defaults " <> s) "" defaultsCmd (T.unpack -> s) = T.pack <$> readCreateProcess (shell $ "defaults " <> s) ""
-- | Convenience function for parsing Plist strings -- | Convenience function for parsing Plist strings
parsePlist :: String -> IO PlObject parsePlist :: Text -> IO PlObject
parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no] parsePlist = readPlistFromString [withValidate no, withSubstDTDEntities no] . T.unpack
-- | 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 Domain) domains :: IO (Set DomainName)
domains domains
= S.fromList = S.fromList
. (Domain "NSGlobalDomain" :) . coerce
. fmap (Domain . delete ',') . ("NSGlobalDomain" :)
. words . T.splitOn ", "
. T.stripEnd
<$> defaultsCmd "domains" <$> defaultsCmd "domains"
-- | Runs @defaults export [domain] -@ and parses the output. -- | Runs @defaults export [domain] -@ and parses the output.
export :: Domain -> IO (Map String PlObject) export :: DomainName -> IO Domain
export (coerce -> d) export (coerce -> d)
= M.fromList = coerce
. M.fromList
. fromJust . fromJust
. fromPlDict . fromPlDict
<$> (defaultsCmd ("export " <> d <> " -") >>= parsePlist) <$> (defaultsCmd ("export '" <> d <> "' -") >>= parsePlist)
-- | Runs 'export' on the 'Set' of provided domains -- | 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 exports = (M.fromList <$>) . mapConcurrently (\d -> (d,) <$> export d) . S.toList
-- | Watches a 'Set' of domains for changes. diffDomain :: Domain -> Domain -> DomainDiff
watch :: Set Domain -> IO () diffDomain old new = coerce $ M.filter (not . isSame) $ diff old new
watch ds = exports ds >>= (putStrLn "Watching..." >>) . go where
-- | 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 go old = do
new <- exports ds new <- exports ds
let plDiffs let domainDiffs = uncurry diffDomain <$> toDelta (diff old new)
= M.filter (not . null) (if null domainDiffs then pure () else putDoc $ prettyDomainDiffs domainDiffs)
. toDelta *> hFlush stdout
. (\(DeltaUnit o n) -> diff o n) *> go new
<$> toDelta (diff old new)
(if null plDiffs then pure () else pPrint plDiffs) *> go new

73
src/Defaults/Pretty.hs Normal file
View file

@ -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 "<false/>" "<true/>" 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)

20
src/Defaults/Types.hs Normal file
View file

@ -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)

6
stack.nix Normal file
View file

@ -0,0 +1,6 @@
{ ghc }:
with import (import ./default.nix).inputs.nixpkgs {};
haskell.lib.buildStackProject {
inherit ghc;
name = "prefmanager";
}

View file

@ -1,14 +1,14 @@
# GHC 8.8.4 # GHC 8.10.3
resolver: lts-16.17 resolver: nightly-2021-01-20
packages: packages:
- . - .
extra-deps: extra-deps:
- diffmap-0.1.0.0 - patience-0.3
# Use fork of plist package that supports MonadFail # Use fork of plist package that supports MonadFail
- github: malob/plist - github: malob/plist
commit: 4103af23b364c101e44da115367800a2f90cc7c3 commit: 5e22be0933bf9a100868b944fa7a8b1236b99255
# nix-sha256: 126c52498c89gd015p40hg671dg58j8xq8ggdqfywnkw2c8cjxxr
allow-newer: true nix:
shell-file: ./stack.nix

View file

@ -5,26 +5,26 @@
packages: packages:
- completed: - completed:
hackage: diffmap-0.1.0.0@sha256:27ea8c315b5dbfb243b2c3b61eab2164534e1d02887a05392dda65995cd36c3e,727 hackage: patience-0.3@sha256:fe3a2c5b1ac4d3425bad3d1ee0b6bb529b6e74ab41151f8526f26fc8dfa1206b,1252
pantry-tree: pantry-tree:
size: 264 size: 368
sha256: 76ea9b9ad91d113addadf58cb19e289e84347ab667aa5c7feed0a1ccfa4ed613 sha256: d2ca47451a599605c641b7c353588d7e1ba3d6036c8a52040b7d1b5b1af8959b
original: original:
hackage: diffmap-0.1.0.0 hackage: patience-0.3
- completed: - completed:
size: 4384 size: 4351
url: https://github.com/malob/plist/archive/4103af23b364c101e44da115367800a2f90cc7c3.tar.gz url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz
name: plist name: plist
version: 0.0.6 version: 0.0.6
sha256: fcc0a7f0dcdb81d379329f5be2af2a566431ed66cbe37e9a1f4101e49b80d4c6 sha256: 7943778b77d008b8e529035d8a4d42c537ede9efe3d24900eb0d855b4567a892
pantry-tree: pantry-tree:
size: 512 size: 512
sha256: 0ede20c4e670e906987a171721f05c3a3337ed936d5ff84e6fe77c13e2c7fbdf sha256: 4872e51ef6ab654c99e06f286635fe1c977e165bc38f77c6bfb402e9fbff0559
original: original:
url: https://github.com/malob/plist/archive/4103af23b364c101e44da115367800a2f90cc7c3.tar.gz url: https://github.com/malob/plist/archive/5e22be0933bf9a100868b944fa7a8b1236b99255.tar.gz
snapshots: snapshots:
- completed: - completed:
size: 532386 size: 563098
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/17.yaml url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2021/1/20.yaml
sha256: d3ee1ae797cf63189c95cf27f00700304946c5cb3c1e6a82001cd6584a221e1b sha256: 97841a28b47a29e798aefda350785c31823f64f31c43ba057fc6c707dfb7ead3
original: lts-16.17 original: nightly-2021-01-20