diff --git a/miscellaneous/suckless-conf/.envrc b/miscellaneous/suckless-conf/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/miscellaneous/suckless-conf/.envrc @@ -0,0 +1 @@ +use flake diff --git a/miscellaneous/suckless-conf/.fixme/config b/miscellaneous/suckless-conf/.fixme/config new file mode 100644 index 00000000..0c2d92c8 --- /dev/null +++ b/miscellaneous/suckless-conf/.fixme/config @@ -0,0 +1,78 @@ + + +fixme-comments "--" + +fixme-prefix FIXME: bugs issues +fixme-prefix TODO: bugs issues +fixme-prefix REVIEW: review +fixme-prefix PR: pr + +fixme-files **/*.hs docs/devlog.md + +fixme-files docs/pep*.txt +fixme-files docs/drafts/**/*.txt +fixme-files docs/pr/**/*.txt +fixme-files docs/todo/**/*.txt + +fixme-files-ignore .direnv/** dist-newstyle/** + +fixme-id-show-len 10 + + +fixme-attribs assigned workflow resolution cat scope + +fixme-value-set workflow new backlog wip test fixed done + +fixme-value-set cat bug feat refactor + +fixme-value-set scope mvp-0 mvp-1 backlog + + +[ fixme-report all json + (render builtin:microstache report-wip.tpl) + (post builtin:columns | 10 8 8 10 12 _) +; (query ~workflow:backlog) +] + + +[ fixme-report open json + (render builtin:microstache report-wip.tpl) + (post builtin:columns | 10 8 8 10 12 _) + (query ~workflow:backlog) + (query ~workflow:done) + (query ~workflow:test) +] + +[ fixme-report backlog json + (render builtin:microstache report-wip.tpl) + (post builtin:columns | 10 8 8 10 12 _) + (query workflow:backlog) +] + + +[ fixme-report wip json + (render builtin:microstache report-wip.tpl) + (post builtin:columns | 10 8 8 10 12 _) + (query ?workflow:test) + (query ?workflow:wip) + (query ?workflow:fixed) +] + + +[ fixme-report review json + (render builtin:microstache report-wip.tpl) + (post builtin:columns | 10 8 8 10 12 _) + (query tag:REVIEW:) +] + +[ fixme-report wip-json json +] + +fixme-log-macro backlog (fixme-set "workflow" "backlog" "$1") +fixme-log-macro test (fixme-set "workflow" "test" "$1") +fixme-log-macro wip (fixme-set "workflow" "wip" "$1") +fixme-log-macro done (fixme-set "workflow" "done" "$1") +fixme-log-macro assign (fixme-set "assigned" "$1" "$2") + +fixme-log-macro scope (fixme-set "scope" "$1" "$2") + diff --git a/miscellaneous/suckless-conf/.fixme/log b/miscellaneous/suckless-conf/.fixme/log new file mode 100644 index 00000000..5a545a38 --- /dev/null +++ b/miscellaneous/suckless-conf/.fixme/log @@ -0,0 +1,5 @@ + +;; This is a log file. All fixmies status updates go here + +fixme-set "workflow" "done" "9QfPgLHLSw" +(fixme-set "assigned" "voidlizard" "9XmyXek1Y6") \ No newline at end of file diff --git a/miscellaneous/suckless-conf/.fixme/report-wip.tpl b/miscellaneous/suckless-conf/.fixme/report-wip.tpl new file mode 100644 index 00000000..093b991e --- /dev/null +++ b/miscellaneous/suckless-conf/.fixme/report-wip.tpl @@ -0,0 +1,3 @@ +{{#items}} +{{&id}}|{{&tag}}|{{&scope}}|[{{&workflow}}]|{{&assigned}}|{{&title}} +{{/items}} diff --git a/miscellaneous/suckless-conf/.gitignore b/miscellaneous/suckless-conf/.gitignore new file mode 100644 index 00000000..9694907a --- /dev/null +++ b/miscellaneous/suckless-conf/.gitignore @@ -0,0 +1,3 @@ +dist-newstyle/ +.direnv/ +.fixme/state.db diff --git a/miscellaneous/suckless-conf/CHANGELOG.md b/miscellaneous/suckless-conf/CHANGELOG.md new file mode 100644 index 00000000..abb8dc61 --- /dev/null +++ b/miscellaneous/suckless-conf/CHANGELOG.md @@ -0,0 +1,7 @@ +# Revision history for suckless-conf + +## 0.1.0.0 -- YYYY-mm-dd + +* Test + +* First version. Released on an unsuspecting world. diff --git a/miscellaneous/suckless-conf/LICENSE b/miscellaneous/suckless-conf/LICENSE new file mode 100644 index 00000000..3086ee5d --- /dev/null +++ b/miscellaneous/suckless-conf/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2023, Dmitry Zuikov + +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 Dmitry Zuikov 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. diff --git a/miscellaneous/suckless-conf/doc/devlog b/miscellaneous/suckless-conf/doc/devlog new file mode 100644 index 00000000..cd266d07 --- /dev/null +++ b/miscellaneous/suckless-conf/doc/devlog @@ -0,0 +1,77 @@ +## 2023-07-12 + +PR: key-value-utilities + branch: key-value-utilities + commit: cb4ee37f455b8e001fd5688106b2da1b31885dc4 + Добавлены утилиты для работы с ключами и значениями. + +## 2023-02-09 + +TODO: implement-regression-tests + + 54377068aac95cbfd8c69177a101c434feecff41 + + +## 2023-02-07 + +Для fixme нам нужны конфиги. Стандартые конфиги это, в основном, треш, +поэтому будем делать новые. Как всегда. + +Конфиги будут sexp, при этом, будут уметь притворяться не sexp. + +поэтому: + +``` +atom term* eol +``` + +единичная инструкция. + +эквивалентна + +``` +( atom term* ) +``` + +выражение конфига: + +``` + +(atom term* ) + +``` + +### term + +``` +term ::= string | number | atom | bool +``` + +Про bool это не точно. + + +### Пример конфига: + +``` +fixme-comments // # ; -- + +; FIXME могут быть в блоках комментариев, +; а могут и нет. + +fixme-prefix FIXME: bug issue + +; комментарий. ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ +; Префикс Категории для префикса + + +``` + +Как биндить термы на целевой язык? В нашем случае Haskell. + + +FIXME: хорошо бы тут сразу поддержать wisp. + Но что бы его поддержать, надо его понять. + + + + diff --git a/miscellaneous/suckless-conf/flake.lock b/miscellaneous/suckless-conf/flake.lock new file mode 100644 index 00000000..9202d627 --- /dev/null +++ b/miscellaneous/suckless-conf/flake.lock @@ -0,0 +1,130 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "fuzzy": { + "inputs": { + "haskell-flake-utils": "haskell-flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1727197542, + "narHash": "sha256-BF9Xd2fa8L5Xju9NTaoUjmzUEJfrRMMKULYQieBjbKo=", + "ref": "refs/heads/master", + "rev": "a834b152e29d632c816eefe117036e5d9330bd03", + "revCount": 43, + "type": "git", + "url": "http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA" + }, + "original": { + "type": "git", + "url": "http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA" + } + }, + "haskell-flake-utils": { + "inputs": { + "flake-utils": "flake-utils" + }, + "locked": { + "lastModified": 1707809372, + "narHash": "sha256-wfTL9PlCSOqSSyU4eenFFI7pHrV21gba4GEILnI4nAU=", + "owner": "ivanovs-4", + "repo": "haskell-flake-utils", + "rev": "3cbdc5d6093e8b4464ae64097e0c8c61e4414ff2", + "type": "github" + }, + "original": { + "owner": "ivanovs-4", + "repo": "haskell-flake-utils", + "type": "github" + } + }, + "haskell-flake-utils_2": { + "inputs": { + "flake-utils": "flake-utils_2" + }, + "locked": { + "lastModified": 1707809372, + "narHash": "sha256-wfTL9PlCSOqSSyU4eenFFI7pHrV21gba4GEILnI4nAU=", + "owner": "ivanovs-4", + "repo": "haskell-flake-utils", + "rev": "3cbdc5d6093e8b4464ae64097e0c8c61e4414ff2", + "type": "github" + }, + "original": { + "owner": "ivanovs-4", + "repo": "haskell-flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1727089097, + "narHash": "sha256-ZMHMThPsthhUREwDebXw7GX45bJnBCVbfnH1g5iuSPc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "568bfef547c14ca438c56a0bece08b8bb2b71a9c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1727089097, + "narHash": "sha256-ZMHMThPsthhUREwDebXw7GX45bJnBCVbfnH1g5iuSPc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "568bfef547c14ca438c56a0bece08b8bb2b71a9c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fuzzy": "fuzzy", + "haskell-flake-utils": "haskell-flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/miscellaneous/suckless-conf/flake.nix b/miscellaneous/suckless-conf/flake.nix new file mode 100644 index 00000000..73256ac7 --- /dev/null +++ b/miscellaneous/suckless-conf/flake.nix @@ -0,0 +1,69 @@ +{ +description = "suckless-cong: sexp based configs"; + +inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + haskell-flake-utils.url = "github:ivanovs-4/haskell-flake-utils"; + + fuzzy.url = + # "git+http://git.hbs2/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA?ref=sexp-parser&rev=bd3a38904864d5cc333974e7b029412607b46871"; + "git+http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA"; + +}; + +outputs = { self, fuzzy, nixpkgs, haskell-flake-utils, ... }@inputs: + + + haskell-flake-utils.lib.simpleCabal2flake { + + inherit self nixpkgs; + # systems = [ "x86_64-linux" ]; + + # wtf = import fetcher-flake.out.outPath; + # project-b = import fuzzy.out.outPath; + + name = "suckless-conf"; + + ## Optional parameters follow + + # nixpkgs config + # config = { }; + + # Add another haskell flakes as requirements + # haskellFlakes = [ inputs.another-simple-haskell-flake ]; + + # Use this to load other flakes overlays to supplement nixpkgs + # preOverlays = [ ]; + + # Pass either a function or a file + # preOverlay = ./overlay.nix; + + # Override haskell packages + # hpPreOverrides = { pkgs }: new: old: + # with pkgs.haskell.lib; with haskell-flake-utils.lib; + # tunePackages pkgs old { + # some-haskellPackages-package = [ dontHaddock ]; + # } // { + # some-cabal-pkg = ((jailbreakUnbreak pkgs) (dontCheck (old.callCabal2nix "some-cabal-pkg" inputs.some-cabal-pkg {}))); + # }; + + # Arguments for callCabal2nix + # cabal2nixArgs = {pkgs}: { + # }; + + # Maps to the devShell output. Pass in a shell.nix file or function + + haskellFlakes = with inputs; [ + fuzzy + ]; + + # Additional build intputs of the default shell + shellExtBuildInputs = {pkgs}: with pkgs; [ + haskellPackages.haskell-language-server + ]; + + # Wether to build hoogle in the default shell + # shellWithHoogle = true; + + }; +} diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless.hs new file mode 100644 index 00000000..24a240e7 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless.hs @@ -0,0 +1,12 @@ +module Data.Config.Suckless + ( module Data.Config.Suckless.Syntax + , module Data.Config.Suckless.Parse + , module Data.Config.Suckless.KeyValue + ) + where + +import Data.Config.Suckless.Syntax +import Data.Config.Suckless.Parse +import Data.Config.Suckless.KeyValue + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/KeyValue.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/KeyValue.hs new file mode 100644 index 00000000..447e0551 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/KeyValue.hs @@ -0,0 +1,111 @@ +{-# Language PatternSynonyms #-} +{-# Language UndecidableInstances #-} +{-# Language AllowAmbiguousTypes #-} +module Data.Config.Suckless.KeyValue where + +import Data.Config.Suckless.Syntax + +import Data.String (IsString(..)) +import Data.Set qualified as Set +import Data.Set (Set) +import Data.Maybe +import Data.Scientific +import Data.Aeson +import Prettyprinter +import Control.Monad.Reader +import Control.Monad.Identity +import Safe + +import Debug.Trace + +class HasCfgKey a b where + -- type family CfgValue a :: Type + key :: Id + +class Monad m => HasCfgValue a b m where + cfgValue :: m b + +class Monad m => HasConf m where + getConf :: m [Syntax C] + +pattern Key :: forall {c}. Id -> [Syntax c] -> [Syntax c] +pattern Key n ns <- SymbolVal n : ns + +instance {-# OVERLAPPABLE #-} (Monad m) => HasConf (ReaderT [Syntax C] m) where + getConf = ask + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Integer)) => HasCfgValue a (Maybe Integer) m where + cfgValue = lastMay . val <$> getConf + where + val syn = [ e + | ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Maybe Integer) + ] + + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Int)) => HasCfgValue a (Maybe Int) m where + cfgValue = lastMay . val <$> getConf @m + where + val syn = [ fromIntegral e + | ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Maybe Int) + ] + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Scientific)) => HasCfgValue a (Maybe Scientific) m where + cfgValue = lastMay . val <$> getConf + where + val syn = [ e + | ListVal (Key s [LitScientificVal e]) <- syn, s == key @a @(Maybe Scientific) + ] + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Bool)) => HasCfgValue a (Maybe Bool) m where + cfgValue = lastMay . val <$> getConf + where + val syn = [ e + | ListVal (Key s [LitBoolVal e]) <- syn, s == key @a @(Maybe Bool) + ] + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Value)) => HasCfgValue a (Maybe Value) m where + cfgValue = lastMay . val <$> getConf + where + val syn = [ toJSON v + | ListVal (Key s [v@ListVal{}]) <- syn, s == key @a @(Maybe Value) + ] + +instance {-# OVERLAPPABLE #-} (HasConf m, IsString b, HasCfgKey a (Maybe b)) => HasCfgValue a (Maybe b) m where + cfgValue = lastMay . val <$> getConf + where + val syn = [ fromString (show $ pretty e) + | ListVal (Key s [LitStrVal e]) <- syn, s == key @a @(Maybe b) + ] + + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Integer)) => HasCfgValue a (Set Integer) m where + cfgValue = Set.fromList . val <$> getConf + where + val syn = [ e + | ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Set Integer) + ] + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Scientific)) => HasCfgValue a (Set Scientific) m where + cfgValue = Set.fromList . val <$> getConf + where + val syn = [ e + | ListVal (Key s [LitScientificVal e]) <- syn, s == key @a @(Set Scientific) + ] + + +instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Value)) => HasCfgValue a (Set Value) m where + cfgValue = Set.fromList . val <$> getConf + where + val syn = [ toJSON v + | ListVal (Key s [v@ListVal{}]) <- syn, s == key @a @(Set Value) + ] + + +instance {-# OVERLAPPABLE #-} (HasConf m, Ord b, IsString b, HasCfgKey a (Set b)) => HasCfgValue a (Set b) m where + cfgValue = Set.fromList . val <$> getConf + where + val syn = [ fromString (show $ pretty e) + | ListVal (Key s [LitStrVal e]) <- syn, s == key @a @(Set b) + ] + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse.hs new file mode 100644 index 00000000..6b932524 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse.hs @@ -0,0 +1,8 @@ +module Data.Config.Suckless.Parse + ( module Data.Config.Suckless.Parse.Fuzzy + ) where + +import Data.Config.Suckless.Parse.Fuzzy + + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse/Fuzzy.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse/Fuzzy.hs new file mode 100644 index 00000000..03078aeb --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Parse/Fuzzy.hs @@ -0,0 +1,40 @@ +module Data.Config.Suckless.Parse.Fuzzy + ( ParseSExp(..) + ) where + +import Data.Config.Suckless.Syntax +import Data.Text.Fuzzy.SExp qualified as P +import Data.Text.Fuzzy.SExp (C0(..),SExpParseError,ForMicroSexp(..)) + +import Data.Functor +import Data.Text as Text +import Control.Monad.Except +import Control.Monad.Identity + +class ParseSExp what where + parseTop :: what -> Either SExpParseError [Syntax C] + parseSyntax :: what -> Either SExpParseError (Syntax C) + +instance ParseSExp Text where + parseTop what = runIdentity (runExceptT (P.parseTop what)) <&> fmap toSyntax + parseSyntax txt = runIdentity (runExceptT (P.parseSexp txt)) <&> toSyntax + +instance ParseSExp String where + parseTop what = runIdentity (runExceptT (P.parseTop (Text.pack what))) <&> fmap toSyntax + parseSyntax txt = runIdentity (runExceptT (P.parseSexp (Text.pack txt))) <&> toSyntax + +toSyntax :: P.MicroSexp C0 -> Syntax C +toSyntax = \case + P.List_ co a -> List (toContext co) (fmap toSyntax a) + P.Symbol_ co a -> Symbol (toContext co) (Id a) + P.String_ co a -> Literal (toContext co) (LitStr a) + P.Boolean_ co a -> Literal (toContext co) (LitBool a) + P.Number_ co v -> case v of + P.NumInteger n -> Literal (toContext co) (LitInt n) + P.NumDouble n -> Literal (toContext co) (LitScientific (realToFrac n)) + +toContext :: C0 -> Context C +toContext (C0 what) = SimpleContext (fromIntegral <$> what) + + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script.hs new file mode 100644 index 00000000..cbf1d511 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script.hs @@ -0,0 +1,50 @@ +{-# Language UndecidableInstances #-} +{-# Language PatternSynonyms #-} +module Data.Config.Suckless.Script + ( module Exported + , module Data.Config.Suckless.Script + ) where + +import Data.Config.Suckless as Exported +import Data.Config.Suckless.Script.Internal as Exported + +import Control.Monad +import Control.Monad.Reader +import Data.HashMap.Strict qualified as HM +import Prettyprinter +import Prettyprinter.Render.Terminal +import Data.List qualified as List +import Data.Text qualified as Text +import UnliftIO + + +{- HLINT ignore "Functor law" -} + +helpList :: MonadUnliftIO m => Bool -> Maybe String -> RunM c m () +helpList hasDoc p = do + + let match = maybe (const True) (Text.isPrefixOf . Text.pack) p + + d <- ask >>= readTVarIO + let ks = [k | Id k <- List.sort (HM.keys d) + , match k + , not hasDoc || docDefined (HM.lookup (Id k) d) + ] + + display_ $ vcat (fmap pretty ks) + + where + docDefined (Just (Bind (Just w) _)) = True + docDefined _ = False + +helpEntry :: MonadUnliftIO m => Id -> RunM c m () +helpEntry what = do + man <- ask >>= readTVarIO + <&> HM.lookup what + <&> maybe mzero bindMan + + liftIO $ hPutDoc stdout (pretty man) + +pattern HelpEntryBound :: forall {c}. Id -> [Syntax c] +pattern HelpEntryBound what <- [ListVal (SymbolVal "builtin:lambda" : SymbolVal what : _ )] + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/File.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/File.hs new file mode 100644 index 00000000..e6fa848d --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/File.hs @@ -0,0 +1,85 @@ +{-# Language MultiWayIf #-} +module Data.Config.Suckless.Script.File where + +import Data.Config.Suckless +import Data.Config.Suckless.Script.Internal + +import Control.Monad +import Control.Monad.Trans.Class +import Control.Monad.Trans.Cont +import Data.Maybe +import Data.Either +import Data.Foldable +import System.Directory +import System.FilePath +import System.FilePattern +import Data.HashSet qualified as HS + +import Lens.Micro.Platform +import UnliftIO +import Control.Concurrent.STM qualified as STM +import Streaming.Prelude qualified as S + +-- FIXME: skip-symlink +glob :: forall m . MonadIO m + => [FilePattern] -- ^ search patterns + -> [FilePattern] -- ^ ignore patterns + -> FilePath -- ^ directory + -> (FilePath -> m Bool) -- ^ file action + -> m () + +glob pat ignore dir action = do + q <- newTQueueIO + void $ liftIO (async $ go q dir >> atomically (writeTQueue q Nothing)) + fix $ \next -> do + atomically (readTQueue q) >>= \case + Nothing -> pure () + Just x -> do + r <- action x + when r next + + where + + matches p f = or [ i ?== f | i <- p ] + skip p = or [ i ?== p | i <- ignore ] + + go q f = do + + isD <- doesDirectoryExist f + + if not isD then do + isF <- doesFileExist f + when (isF && matches pat f && not (skip f)) do + atomically $ writeTQueue q (Just f) + else do + co' <- (try @_ @IOError $ listDirectory f) + <&> fromRight mempty + + forConcurrently_ co' $ \x -> do + let p = normalise (f x) + unless (skip p) (go q p) + +entries :: forall c m . ( IsContext c + , Exception (BadFormException c) + , MonadUnliftIO m) + => MakeDictM c m () +entries = do + entry $ bindMatch "glob" $ \syn -> do + + (p,i,d) <- case syn of + [] -> pure (["*"], [], ".") + + [StringLike d, StringLike i, StringLike e] -> do + pure ([i], [e], d) + + [StringLike d, ListVal (StringLikeList i), ListVal (StringLikeList e)] -> do + pure (i, e, d) + + _ -> throwIO (BadFormException @c nil) + + r <- S.toList_ $ glob p i d $ \fn -> do + S.yield (mkStr @c fn) -- do + pure True + + pure (mkList r) + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/Internal.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/Internal.hs new file mode 100644 index 00000000..cb6f83a5 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Script/Internal.hs @@ -0,0 +1,969 @@ +{-# Language AllowAmbiguousTypes #-} +{-# Language UndecidableInstances #-} +{-# Language PatternSynonyms #-} +{-# Language ViewPatterns #-} +module Data.Config.Suckless.Script.Internal + ( module Data.Config.Suckless.Script.Internal + , module Export + ) where + +import Data.Config.Suckless + +import Control.Applicative +import Control.Monad +import Control.Monad.Identity +import Control.Monad.Reader +import Control.Monad.Writer +import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as BS8 +import Data.Data +import Data.Function as Export +import Data.Functor as Export +import Data.Hashable +import Data.HashMap.Strict (HashMap) +import Data.HashMap.Strict qualified as HM +import Data.Kind +import Data.List (isPrefixOf) +import Data.List qualified as List +import Data.Maybe +import Data.String +import Data.Text.IO qualified as TIO +import Data.Text qualified as Text +import Data.Text (Text) +import Data.Time.Clock.POSIX +import GHC.Generics hiding (C) +import Prettyprinter +import Prettyprinter.Render.Terminal +import Safe +import Streaming.Prelude qualified as S +import System.Environment +import Text.InterpolatedString.Perl6 (qc) +import UnliftIO + +-- TODO: move-to-suckless-conf + +data ManApplyArg = ManApplyArg Text Text + deriving stock (Eq,Show,Data,Generic) + +newtype ManApply = ManApply [ ManApplyArg ] + deriving stock (Eq,Show,Data,Generic) + deriving newtype (Semigroup,Monoid) + +data ManSynopsis = + ManSynopsis ManApply + deriving stock (Eq,Show,Data,Generic) + +data ManDesc = ManDescRaw Text + deriving stock (Eq,Show,Data,Generic) + +data ManRetVal = ManRetVal + deriving stock (Eq,Show,Data,Generic) + +newtype ManName a = ManName Id + deriving stock (Eq,Show,Data,Generic) + deriving newtype (IsString,Pretty) + +newtype ManBrief = ManBrief Text + deriving stock (Eq,Show,Data,Generic) + deriving newtype (Pretty,IsString) + +data ManReturns = ManReturns Text Text + deriving stock (Eq,Show,Data,Generic) + +newtype ManExamples = + ManExamples Text + deriving stock (Eq,Show,Data,Generic) + deriving newtype (Pretty,IsString,Monoid,Semigroup) + +class ManNameOf a ann where + manNameOf :: a -> ManName ann + +data Man a = + Man + { manName :: Maybe (ManName a) + , manHidden :: Bool + , manBrief :: Maybe ManBrief + , manSynopsis :: [ManSynopsis] + , manDesc :: Maybe ManDesc + , manReturns :: Maybe ManReturns + , manExamples :: [ManExamples] + } + deriving stock (Eq,Show,Generic) + +instance Monoid (Man a) where + mempty = Man Nothing False Nothing mempty Nothing Nothing mempty + +instance Semigroup (Man a) where + (<>) a b = Man (manName b <|> manName a) + (manHidden b || manHidden a) + (manBrief b <|> manBrief a) + (manSynopsis a <> manSynopsis b) + (manDesc b <|> manDesc a) + (manReturns b <|> manReturns a) + (manExamples a <> manExamples b) + +instance ManNameOf Id a where + manNameOf = ManName + + +instance Pretty ManDesc where + pretty = \case + ManDescRaw t -> pretty t + +instance IsString ManDesc where + fromString s = ManDescRaw (Text.pack s) + +instance Pretty (Man a) where + pretty e = "NAME" + <> line + <> indent 8 (pretty (manName e) <> fmtBrief e) + <> line + <> fmtSynopsis + <> fmtDescription + <> retval + <> fmtExamples + where + fmtBrief a = case manBrief a of + Nothing -> mempty + Just x -> " - " <> pretty x + + retval = case manReturns e of + Nothing -> mempty + Just (ManReturns t s) -> + line <> "RETURN VALUE" <> line + <> indent 8 ( + if not (Text.null s) then + (pretty t <> hsep ["","-",""] <> pretty s) <> line + else pretty t ) + + fmtDescription = line + <> "DESCRIPTION" <> line + <> indent 8 ( case manDesc e of + Nothing -> pretty (manBrief e) + Just x -> pretty x) + <> line + + fmtSynopsis = case manSynopsis e of + [] -> mempty + _ -> + line + <> "SYNOPSIS" + <> line + <> vcat (fmap synEntry (manSynopsis e)) + <> line + + fmtExamples = case manExamples e of + [] -> mempty + es -> line + <> "EXAMPLES" + <> line + <> indent 8 ( vcat (fmap pretty es) ) + + synEntry (ManSynopsis (ManApply [])) = + indent 8 ( parens (pretty (manName e)) ) <> line + + synEntry (ManSynopsis (ManApply xs)) = do + indent 8 do + parens (pretty (manName e) <+> + hsep [ pretty n | ManApplyArg t n <- xs ] ) + <> line + <> line + <> vcat [ pretty n <+> ":" <+> pretty t | ManApplyArg t n <- xs ] + +stringLike :: Syntax c -> Maybe String +stringLike = \case + LitStrVal s -> Just $ Text.unpack s + SymbolVal (Id s) -> Just $ Text.unpack s + _ -> Nothing + +stringLikeList :: [Syntax c] -> [String] +stringLikeList syn = [ stringLike s | s <- syn ] & takeWhile isJust & catMaybes + +blobLike :: Syntax c -> Maybe ByteString +blobLike = \case + LitStrVal s -> Just $ BS8.pack (Text.unpack s) + ListVal [SymbolVal "blob", LitStrVal s] -> Just $ BS8.pack (Text.unpack s) + _ -> Nothing + +pattern BlobLike :: forall {c} . ByteString -> Syntax c +pattern BlobLike s <- (blobLike -> Just s) + +class Display a where + display :: MonadIO m => a -> m () + +instance {-# OVERLAPPABLE #-} Pretty w => Display w where + display = liftIO . print . pretty + +instance IsContext c => Display (Syntax c) where + display = \case + LitStrVal s -> liftIO $ TIO.putStr s + -- ListVal [SymbolVal "small-encrypted-block", LitStrVal txt] -> do + -- let s = Text.unpack txt & BS8.pack & toBase58 & AsBase58 & pretty + -- liftIO $ print $ parens $ "small-encrypted-block" <+> parens ("blob" <+> dquotes s) + -- ListVal [SymbolVal "blob", LitStrVal txt] -> do + -- let s = Text.unpack txt & BS8.pack & toBase58 & AsBase58 & pretty + -- liftIO $ print $ parens $ "blob:base58" <+> dquotes s + x -> liftIO $ putStr (show $ pretty x) + +instance Display Text where + display = liftIO . TIO.putStr + +instance Display String where + display = liftIO . putStr + +display_ :: (MonadIO m, Show a) => a -> m () +display_ = liftIO . print + +{- HLINT ignore "Functor law" -} + +class IsContext c => MkSym c a where + mkSym :: a -> Syntax c + +instance IsContext c => MkSym c String where + mkSym s = Symbol noContext (Id $ Text.pack s) + +instance IsContext c => MkSym c Text where + mkSym s = Symbol noContext (Id s) + +instance IsContext c => MkSym c Id where + mkSym = Symbol noContext + +class IsContext c => MkStr c s where + mkStr :: s -> Syntax c + +instance IsContext c => MkStr c String where + mkStr s = Literal noContext $ LitStr (Text.pack s) + +instance IsContext c => MkStr c Text where + mkStr s = Literal noContext $ LitStr s + +mkBool :: forall c . IsContext c => Bool -> Syntax c +mkBool v = Literal noContext (LitBool v) + + +class IsContext c => MkForm c a where + mkForm :: a-> [Syntax c] -> Syntax c + +instance (IsContext c, MkSym c s) => MkForm c s where + mkForm s sy = List noContext ( mkSym @c s : sy ) + +mkList :: forall c. IsContext c => [Syntax c] -> Syntax c +mkList = List noContext + +isFalse :: forall c . IsContext c => Syntax c -> Bool +isFalse = \case + Literal _ (LitBool False) -> True + ListVal [] -> True + _ -> False + +eatNil :: Monad m => (Syntax c -> m a) -> Syntax c -> m () +eatNil f = \case + Nil -> pure () + x -> void $ f x + +class IsContext c => MkInt c s where + mkInt :: s -> Syntax c + +instance (Integral i, IsContext c) => MkInt c i where + mkInt n = Literal noContext $ LitInt (fromIntegral n) + +class OptionalVal c b where + optional :: b -> Syntax c -> b + +instance IsContext c => OptionalVal c Int where + optional d = \case + LitIntVal x -> fromIntegral x + _ -> d + +hasKey :: IsContext c => Id -> [Syntax c] -> Maybe (Syntax c) +hasKey k ss = headMay [ e | ListVal [SymbolVal z, e] <- ss, z == k] + + +pattern Lambda :: forall {c}. [Id] -> Syntax c -> Syntax c +pattern Lambda a e <- ListVal [SymbolVal "lambda", LambdaArgs a, e] + +pattern LambdaArgs :: [Id] -> Syntax c +pattern LambdaArgs a <- (lambdaArgList -> Just a) + + +lambdaArgList :: Syntax c -> Maybe [Id] + +lambdaArgList (ListVal a) = sequence argz + where + argz = flip fmap a \case + (SymbolVal x) -> Just x + _ -> Nothing + +lambdaArgList _ = Nothing + + +pattern PairList :: [Syntax c] -> [Syntax c] +pattern PairList es <- (pairList -> es) + +pairList :: [Syntax c ] -> [Syntax c] +pairList syn = [ isPair s | s <- syn ] & takeWhile isJust & catMaybes + +optlist :: IsContext c => [Syntax c] -> [(Id, Syntax c)] +optlist = reverse . go [] + where + go acc ( SymbolVal i : b : rest ) = go ((i, b) : acc) rest + go acc [ SymbolVal i ] = (i, nil) : acc + go acc _ = acc + + +isPair :: Syntax c -> Maybe (Syntax c) +isPair = \case + e@(ListVal [_,_]) -> Just e + _ -> Nothing + +data BindAction c ( m :: Type -> Type) = + BindLambda { fromLambda :: [Syntax c] -> RunM c m (Syntax c) } + | BindValue (Syntax c) + +data Bind c ( m :: Type -> Type) = Bind + { bindMan :: Maybe (Man AnsiStyle) + , bindAction :: BindAction c m + } deriving (Generic) + +deriving newtype instance Hashable Id + +newtype NameNotBoundException = + NameNotBound Id + deriving stock Show + deriving newtype (Generic,Typeable) + +newtype NotLambda = NotLambda Id + deriving stock Show + deriving newtype (Generic,Typeable) + +instance Exception NotLambda + +data BadFormException c = BadFormException (Syntax c) + | ArityMismatch (Syntax c) + +newtype TypeCheckError c = TypeCheckError (Syntax c) + +instance Exception (TypeCheckError C) + +newtype BadValueException = BadValueException String + deriving stock Show + deriving newtype (Generic,Typeable) + +instance Exception NameNotBoundException + +instance IsContext c => Show (BadFormException c) where + show (BadFormException sy) = show $ "BadFormException" <+> pretty sy + show (ArityMismatch sy) = show $ "ArityMismatch" <+> pretty sy + +instance IsContext c => Show (TypeCheckError c) where + show (TypeCheckError sy) = show $ "TypeCheckError" <+> pretty sy + +instance Exception (BadFormException C) + +instance Exception BadValueException + +type Dict c m = HashMap Id (Bind c m) + +newtype RunM c m a = RunM { fromRunM :: ReaderT (TVar (Dict c m)) m a } + deriving newtype ( Applicative + , Functor + , Monad + , MonadIO + , MonadUnliftIO + , MonadReader (TVar (Dict c m)) + ) + +instance MonadTrans (RunM c) where + lift = RunM . lift + +newtype MakeDictM c m a = MakeDictM { fromMakeDict :: Writer (Dict c m) a } + deriving newtype ( Applicative + , Functor + , Monad + , MonadWriter (Dict c m) + ) + +makeDict :: (IsContext c, Monad m) => MakeDictM c m () -> Dict c m +makeDict w = execWriter ( fromMakeDict w ) + +entry :: Dict c m -> MakeDictM c m () +entry = tell + +hide :: MakeDictM c m () +hide = pure () + +desc :: Doc ann -> MakeDictM c m () -> MakeDictM c m () +desc txt = censor (HM.map setDesc) + where + w0 = mempty { manDesc = Just (ManDescRaw $ Text.pack $ show txt) } + setDesc (Bind w x) = Bind (Just (maybe w0 (<> w0) w)) x + +brief :: ManBrief -> MakeDictM c m () -> MakeDictM c m () +brief txt = censor (HM.map setBrief) + where + w0 = mempty { manBrief = Just txt } + setBrief (Bind w x) = Bind (Just (maybe w0 (<> w0) w)) x + +returns :: Text -> Text -> MakeDictM c m () -> MakeDictM c m () +returns tp txt = censor (HM.map setReturns) + where + w0 = mempty { manReturns = Just (ManReturns tp txt) } + setReturns (Bind w x) = Bind (Just (maybe w0 (<>w0) w)) x + + +addSynopsis :: ManSynopsis -> Bind c m -> Bind c m +addSynopsis synopsis (Bind w x) = Bind (Just updatedMan) x + where + updatedMan = case w of + Nothing -> mempty { manSynopsis = [synopsis] } + Just man -> man { manSynopsis = manSynopsis man <> [synopsis] } + +noArgs :: MakeDictM c m () -> MakeDictM c m () +noArgs = censor (HM.map (addSynopsis (ManSynopsis (ManApply [])))) + +arg :: Text -> Text -> ManApplyArg +arg = ManApplyArg + + +args :: [ManApplyArg] -> MakeDictM c m () -> MakeDictM c m () +args argList = censor (HM.map (addSynopsis (ManSynopsis (ManApply argList)))) + +opt :: Doc a -> Doc a -> Doc a +opt n d = n <+> "-" <+> d + +examples :: ManExamples -> MakeDictM c m () -> MakeDictM c m () +examples (ManExamples s) = censor (HM.map setExamples ) + where + ex = ManExamples (Text.unlines $ Text.strip <$> Text.lines (Text.strip s)) + ex0 = mempty { manExamples = [ex] } + setExamples (Bind w x) = Bind (Just (maybe ex0 (<>ex0) w)) x + +splitForms :: [String] -> [[String]] +splitForms s0 = runIdentity $ S.toList_ (go mempty s0) + where + go acc ( "then" : rest ) = emit acc >> go mempty rest + go acc ( "and" : rest ) = emit acc >> go mempty rest + go acc ( x : rest ) | isPrefixOf "-" x = go ( x : acc ) rest + go acc ( x : rest ) | isPrefixOf "--" x = go ( x : acc ) rest + go acc ( x : rest ) = go ( x : acc ) rest + go acc [] = emit acc + + emit = S.yield . reverse + +applyLambda :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) + => [Id] + -> Syntax c + -> [Syntax c] + -> RunM c m (Syntax c) +applyLambda decl body args = do + + when (length decl /= length args) do + throwIO (ArityMismatch @c nil) + + ev <- mapM eval args + tv <- ask + d0 <- readTVarIO tv + + forM_ (zip decl ev) $ \(n,v) -> do + bind n v + + e <- eval body + + atomically $ writeTVar tv d0 + pure e + +apply_ :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) + => Syntax c + -> [Syntax c] + -> RunM c m (Syntax c) + +apply_ s args = case s of + ListVal [SymbolVal "builtin:lambda", SymbolVal n] -> apply n args + SymbolVal what -> apply what args + Lambda d body -> applyLambda d body args + e -> throwIO $ BadFormException @c s + +apply :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) + => Id + -> [Syntax c] + -> RunM c m (Syntax c) +apply name args' = do + -- notice $ red "APPLY" <+> pretty name + what <- ask >>= readTVarIO <&> HM.lookup name + + case bindAction <$> what of + Just (BindLambda e) -> mapM eval args' >>= e + + Just (BindValue (Lambda argz body) ) -> do + applyLambda argz body args' + + Just (BindValue _) -> do + throwIO (NotLambda name) + + Nothing -> throwIO (NameNotBound name) + +bind :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) + => Id + -> Syntax c + -> RunM c m () +bind name expr = do + t <- ask + + what <- case expr of + ListVal [SymbolVal "builtin:lambda", SymbolVal n] -> do + m <- readTVarIO t + HM.lookup n m & maybe (throwIO (NameNotBound n)) pure + + e -> pure $ Bind mzero (BindValue e) + + atomically do + modifyTVar t (HM.insert name what) + +bindBuiltins :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) + => Dict c m + -> RunM c m () + +bindBuiltins dict = do + t <- ask + atomically do + modifyTVar t (<> dict) + +eval :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) => Syntax c -> RunM c m (Syntax c) +eval syn = handle (handleForm syn) $ do + + dict <- ask >>= readTVarIO + + case syn of + + ListVal [ w, SymbolVal ".", b] -> do + pure $ mkList [w, b] + + ListVal [ SymbolVal "quot", ListVal b] -> do + pure $ mkList b + + ListVal [SymbolVal "define", SymbolVal what, e] -> do + ev <- eval e + bind what ev>> pure nil + + ListVal [SymbolVal "lambda", arglist, body] -> do + pure $ mkForm @c "lambda" [ arglist, body ] + + ListVal [SymbolVal "define", LambdaArgs (name : args), e] -> do + bind name ( mkForm @c "lambda" [ mkList [ mkSym s | s <- args], e ] ) + pure nil + + ListVal [SymbolVal "false?", e'] -> do + e <- eval e' + pure $ if isFalse e then mkBool True else mkBool False + + ListVal [SymbolVal "if", w, e1, e2] -> do + what <- eval w + if isFalse what then eval e2 else eval e1 + + ListVal (SymbolVal "begin" : what) -> do + evalTop what + + e@(ListVal (SymbolVal "blob" : what)) -> do + pure e + -- evalTop what + + lc@(ListVal (Lambda decl body : args)) -> do + applyLambda decl body args + + ListVal (SymbolVal name : args') -> do + apply name args' + + SymbolVal (Id s) | Text.isPrefixOf ":" s -> do + pure (mkSym @c (Text.drop 1 s)) + + SymbolVal name | HM.member name dict -> do + let what = HM.lookup name dict + & maybe (BindValue (mkSym name)) bindAction + + case what of + BindValue e -> pure e + BindLambda e -> pure $ mkForm "builtin:lambda" [mkSym name] + + e@(SymbolVal name) | not (HM.member name dict) -> do + pure e + + e@Literal{} -> pure e + + e -> throwIO $ BadFormException @c e + + where + handleForm syn = \case + (BadFormException _ :: BadFormException c) -> do + throwIO (BadFormException syn) + (ArityMismatch s :: BadFormException c) -> do + throwIO (ArityMismatch syn) + +runM :: forall c m a. ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) => Dict c m -> RunM c m a -> m a +runM d m = do + tvd <- newTVarIO d + runReaderT (fromRunM m) tvd + +run :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c) + ) => Dict c m -> [Syntax c] -> m (Syntax c) +run d sy = do + tvd <- newTVarIO d + lastDef nil <$> runReaderT (fromRunM (mapM eval sy)) tvd + +evalTop :: forall c m . ( IsContext c + , MonadUnliftIO m + , Exception (BadFormException c)) + => [Syntax c] + -> RunM c m (Syntax c) +evalTop syn = lastDef nil <$> mapM eval syn + +bindMatch :: Id -> ([Syntax c] -> RunM c m (Syntax c)) -> Dict c m +bindMatch n fn = HM.singleton n (Bind man (BindLambda fn)) + where + man = Just $ mempty { manName = Just (manNameOf n) } + +bindValue :: Id -> Syntax c -> Dict c m +bindValue n e = HM.singleton n (Bind mzero (BindValue e)) + +nil :: forall c . IsContext c => Syntax c +nil = List noContext [] + +nil_ :: (IsContext c, MonadIO m) => (a -> RunM c m b) -> a -> RunM c m (Syntax c) +nil_ m w = m w >> pure (List noContext []) + +fixContext :: (IsContext c1, IsContext c2) => Syntax c1 -> Syntax c2 +fixContext = go + where + go = \case + List _ xs -> List noContext (fmap go xs) + Symbol _ w -> Symbol noContext w + Literal _ l -> Literal noContext l + + +fmt :: Syntax c -> Doc ann +fmt = \case + LitStrVal x -> pretty $ Text.unpack x + x -> pretty x + +internalEntries :: forall c m . (IsContext c, Exception (BadFormException c), MonadUnliftIO m) => MakeDictM c m () +internalEntries = do + + entry $ bindValue "false" (mkBool False) + entry $ bindValue "true" (mkBool True) + entry $ bindValue "chr:semi" (mkStr ";") + entry $ bindValue "chr:tilda" (mkStr "~") + entry $ bindValue "chr:colon" (mkStr ":") + entry $ bindValue "chr:comma" (mkStr ",") + entry $ bindValue "chr:q" (mkStr "'") + entry $ bindValue "chr:minus" (mkStr "-") + entry $ bindValue "chr:dq" (mkStr "\"") + entry $ bindValue "chr:lf" (mkStr "\n") + entry $ bindValue "chr:cr" (mkStr "\r") + entry $ bindValue "chr:tab" (mkStr "\t") + entry $ bindValue "chr:space" (mkStr " ") + + brief "concatenates list of string-like elements into a string" + $ args [arg "list" "(list ...)"] + $ args [arg "..." "..."] + $ returns "string" "" + $ examples [qc| + (concat a b c d) + abcd|] + $ examples [qc| + (concat 1 2 3 4 5) + 12345|] + + $ entry $ bindMatch "concat" $ \syn -> do + + case syn of + [ListVal xs] -> do + pure $ mkStr ( show $ hcat (fmap fmt xs) ) + + xs -> do + pure $ mkStr ( show $ hcat (fmap fmt xs) ) + + brief "creates a list of elements" + $ args [arg "..." "..."] + $ returns "list" "" + $ examples [qc| +(list 1 2 3 fuu bar "baz") +(1 2 3 fuu bar "baz") + |] + $ entry $ bindMatch "list" $ \case + es -> do + pure $ mkList es + + entry $ bindMatch "dict" $ \case + (pairList -> es@(_:_)) -> do + pure $ mkForm "dict" es + [a, b] -> do + pure $ mkForm "dict" [ mkList [a, b] ] + _ -> throwIO (BadFormException @C nil) + + brief "creates a dict from a linear list of string-like items" + $ args [arg "list-of-terms" "..."] + $ desc ( "macro; syntax sugar" <> line + <> "useful for creating function args" <> line + <> "leftover records are skipped" + ) + $ returns "dict" "" + $ examples [qc| +[kw a 1 b 2 c 3] +(dict (a 1) (b 2) (c 3)) + +[kw a] +(dict (a ())) + +[kw a b] +(dict (a b)) + +[kw 1 2 3] +(dict) + +[kw a b c] +(dict (a b) (c ())) + |] + $ entry $ bindMatch "kw" $ \syn -> do + let wat = [ mkList @c [mkSym i, e] | (i,e) <- optlist syn ] + pure $ mkForm "dict" wat + + entry $ bindMatch "iterate" $ nil_ $ \syn -> do + case syn of + [ListVal (SymbolVal "builtin:lambda" : SymbolVal fn : _), ListVal rs] -> do + mapM_ (apply @c fn . List.singleton) rs + + [Lambda decl body, ListVal args] -> do + mapM_ (applyLambda decl body . List.singleton) args + + _ -> do + throwIO (BadFormException @C nil) + + entry $ bindMatch "repeat" $ nil_ $ \case + [LitIntVal n, Lambda [] b] -> do + replicateM_ (fromIntegral n) (applyLambda [] b []) + + [LitIntVal n, e@(ListVal _)] -> do + replicateM_ (fromIntegral n) (eval e) + + z -> + throwIO (BadFormException @C nil) + + entry $ bindMatch "map" $ \syn -> do + case syn of + [ListVal (SymbolVal "builtin:lambda" : SymbolVal fn : _), ListVal rs] -> do + mapM (apply @c fn . List.singleton) rs + <&> mkList + + [Lambda decl body, ListVal args] -> do + mapM (applyLambda decl body . List.singleton) args + <&> mkList + + _ -> do + throwIO (BadFormException @C nil) + + entry $ bindMatch "head" $ \case + [ ListVal es ] -> pure (head es) + _ -> throwIO (TypeCheckError @C nil) + + brief "get tail of list" + $ args [arg "list" "list"] + $ desc "nil if the list is empty; error if not list" + $ examples [qc| + (tail [list 1 2 3]) + (2 3) + (tail [list]) + |] + $ entry $ bindMatch "tail" $ \case + [] -> pure nil + [ListVal []] -> pure nil + [ListVal es] -> pure $ mkList (tail es) + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "lookup" $ \case + [s, ListVal (SymbolVal "dict" : es) ] -> do + let val = headDef nil [ v | ListVal [k, v] <- es, k == s ] + pure val + + [StringLike s, ListVal [] ] -> do + pure nil + + _ -> throwIO (BadFormException @c nil) + + brief "returns current unix time" + $ returns "int" "current unix time in seconds" + $ noArgs + $ entry $ bindMatch "now" $ \case + [] -> mkInt . round <$> liftIO getPOSIXTime + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "display" $ nil_ \case + [ sy ] -> display sy + ss -> display (mkList ss) + + brief "prints new line character to stdout" + $ entry $ bindMatch "newline" $ nil_ $ \case + [] -> liftIO (putStrLn "") + _ -> throwIO (BadFormException @c nil) + + brief "prints a list of terms to stdout" + $ entry $ bindMatch "print" $ nil_ $ \case + [ sy ] -> display sy + ss -> mapM_ display ss + + entry $ bindMatch "println" $ nil_ $ \case + [ sy ] -> display sy >> liftIO (putStrLn "") + ss -> mapM_ display ss >> liftIO (putStrLn "") + + entry $ bindMatch "str:read-stdin" $ \case + [] -> liftIO getContents <&> mkStr @c + + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "str:put" $ nil_ $ \case + [LitStrVal s] -> liftIO $ TIO.putStr s + _ -> throwIO (BadFormException @c nil) + + brief "reads file as a string" do + entry $ bindMatch "str:read-file" $ \case + [StringLike fn] -> liftIO (TIO.readFile fn) <&> mkStr + + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "str:save" $ nil_ \case + [StringLike fn, StringLike what] -> + liftIO (writeFile fn what) + + _ -> throwIO (BadFormException @c nil) + + entry $ bindValue "space" $ mkStr " " + + entry $ bindMatch "parse-top" $ \case + + [SymbolVal w, LitStrVal s] -> do + pure $ parseTop s & either (const nil) (mkForm w . fmap fixContext) + + [LitStrVal s] -> do + pure $ parseTop s & either (const nil) (mkList . fmap fixContext) + + _ -> throwIO (BadFormException @c nil) + + let atomFrom = \case + [StringLike s] -> pure (mkSym s) + [e] -> pure (mkSym $ show $ pretty e) + _ -> throwIO (BadFormException @c nil) + + brief "type of argument" + $ args [arg "term" "term"] + $ returns "symbol" "type" + $ entry $ bindMatch "type" \case + [ListVal _] -> pure $ mkSym "list" + [SymbolVal _] -> pure $ mkSym "symbol" + [LitStrVal _] -> pure $ mkSym "string" + [LitIntVal _] -> pure $ mkSym "int" + [LitScientificVal _] -> pure $ mkSym "float" + [LitBoolVal _] -> pure $ mkSym "bool" + _ -> throwIO (BadFormException @c nil) + + brief "creates a symbol from argument" + $ args [arg "any-term" "term"] + $ returns "symbol" "" + do + entry $ bindMatch "sym" atomFrom + entry $ bindMatch "atom" atomFrom + + brief "compares two terms" $ + args [arg "term" "a", arg "term" "b"] $ + returns "boolean" "#t if terms are equal, otherwise #f" $ + entry $ bindMatch "eq?" $ \case + [a, b] -> do + pure $ if a == b then mkBool True else mkBool False + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "length" $ \case + [ListVal es] -> pure $ mkInt (length es) + [StringLike es] -> pure $ mkInt (length es) + _ -> pure $ mkInt 0 + + entry $ bindMatch "nil?" $ \case + [ListVal []] -> pure $ mkBool True + _ -> pure $ mkBool False + + entry $ bindMatch "not" $ \case + [w] -> do + pure $ if isFalse w then mkBool True else mkBool False + _ -> throwIO (BadFormException @c nil) + + brief "get system environment" + $ args [] + $ args [ arg "string" "string" ] + $ returns "env" "single var or dict of all vars" + $ examples [qc| + (env HOME) + /home/user + + (env) + (dict + (HOME "/home/user") ... (CC "gcc") ...) + |] + $ entry $ bindMatch "env" $ \case + [] -> do + s <- liftIO getEnvironment + pure $ mkForm "dict" [ mkList [mkSym @c a, mkStr b] | (a,b) <- s ] + + [StringLike s] -> do + liftIO (lookupEnv s) + <&> maybe nil mkStr + _ -> throwIO (BadFormException @c nil) + + -- FIXME: we-need-opaque-type + entry $ bindMatch "blob:read-stdin" $ \case + [] -> do + blob <- liftIO BS8.getContents <&> BS8.unpack + pure (mkForm "blob" [mkStr @c blob]) + + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "blob:read-file" $ \case + [StringLike fn] -> do + blob <- liftIO (BS8.readFile fn) <&> BS8.unpack + pure (mkForm "blob" [mkStr @c blob]) + + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "blob:save" $ nil_ $ \case + [StringLike fn, ListVal [SymbolVal "blob", LitStrVal t]] -> do + let s = Text.unpack t & BS8.pack + liftIO $ BS8.writeFile fn s + + _ -> throwIO (BadFormException @c nil) + + entry $ bindMatch "blob:put" $ nil_ $ \case + [ListVal [SymbolVal "blob", LitStrVal t]] -> do + let s = Text.unpack t & BS8.pack + liftIO $ BS8.putStr s + + _ -> throwIO (BadFormException @c nil) + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Syntax.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Syntax.hs new file mode 100644 index 00000000..f87e4189 --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Syntax.hs @@ -0,0 +1,211 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE PatternSynonyms #-} +{-# Language ViewPatterns #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Data.Config.Suckless.Syntax + ( Syntax(..) + , Id(..) + , Literal(..) + , HasContext + , C(..) + , Context(..) + , IsContext(..) + , IsLiteral(..) + , pattern SymbolVal + , pattern ListVal + , pattern LitIntVal + , pattern LitStrVal + , pattern LitBoolVal + , pattern LitScientificVal + , pattern StringLike + , pattern StringLikeList + , pattern Nil + ) + where + +import Data.Data +import Data.Kind +import Data.String +import Data.Text (Text) +import Data.Scientific +import GHC.Generics (Generic(..)) +import Data.Maybe +-- import GHC.Generics( Fixity(..) ) +-- import Data.Data as Data +import Data.Aeson +import Data.Aeson.Key +import Data.Aeson.KeyMap qualified as Aeson +import Data.Vector qualified as V +import Data.Traversable (forM) +import Data.Text qualified as Text +import Data.Function + +import Prettyprinter + +pattern SymbolVal :: Id -> Syntax c +pattern SymbolVal v <- Symbol _ v + +-- pattern LitVal :: forall {c}. Id -> Li +pattern LitIntVal :: Integer -> Syntax c +pattern LitIntVal v <- Literal _ (LitInt v) + +pattern LitScientificVal :: Scientific -> Syntax c +pattern LitScientificVal v <- Literal _ (LitScientific v) + +pattern LitStrVal :: Text -> Syntax c +pattern LitStrVal v <- Literal _ (LitStr v) + +pattern LitBoolVal :: Bool -> Syntax c +pattern LitBoolVal v <- Literal _ (LitBool v) + +pattern ListVal :: [Syntax c] -> Syntax c +pattern ListVal v <- List _ v + + +stringLike :: Syntax c -> Maybe String +stringLike = \case + LitStrVal s -> Just $ Text.unpack s + SymbolVal (Id s) -> Just $ Text.unpack s + _ -> Nothing + +stringLikeList :: [Syntax c] -> [String] +stringLikeList syn = [ stringLike s | s <- syn ] & takeWhile isJust & catMaybes + + +pattern StringLike :: forall {c} . String -> Syntax c +pattern StringLike e <- (stringLike -> Just e) + +pattern StringLikeList :: forall {c} . [String] -> [Syntax c] +pattern StringLikeList e <- (stringLikeList -> e) + + +pattern Nil :: forall {c} . Syntax c +pattern Nil <- ListVal [] + + + +data family Context c :: Type + +class IsContext c where + noContext :: Context c + +data instance Context () = EmptyContext + +instance IsContext () where + noContext = EmptyContext + +class HasContext c a where + +class IsLiteral a where + mkLit :: a -> Literal + +newtype Id = + Id Text + deriving newtype (IsString,Pretty) + deriving stock (Data,Generic,Show,Eq,Ord) + +data Literal = + LitStr Text + | LitInt Integer + | LitScientific Scientific + | LitBool Bool + deriving stock (Eq,Ord,Data,Generic,Show) + +instance IsLiteral Text where + mkLit = LitStr + +instance IsLiteral Bool where + mkLit = LitBool + +instance IsLiteral Integer where + mkLit = LitInt + +data C = C + deriving stock (Eq,Ord,Show,Data,Typeable,Generic) + +-- simple, yet sufficient context +-- Integer may be offset, maybe line number, +-- token number, whatever +-- it's up to parser to use this context for +-- error printing, etc +newtype instance (Context C) = + SimpleContext { fromSimpleContext :: Maybe Integer } + deriving stock (Eq,Ord,Show,Data,Typeable,Generic) + +instance IsContext C where + noContext = SimpleContext Nothing + +data Syntax c + = List (Context c) [Syntax c] + | Symbol (Context c) Id + | Literal (Context c) Literal + deriving stock (Generic,Typeable) + + +instance Eq (Syntax c) where + (==) (Literal _ a) (Literal _ b) = a == b + (==) (Symbol _ a) (Symbol _ b) = a == b + (==) (List _ a) (List _ b) = a == b + (==) _ _ = False + +deriving instance (Data c, Data (Context c)) => Data (Syntax c) + +instance Pretty (Syntax c) where + pretty (Literal _ ast) = pretty ast + pretty (Symbol _ s) = pretty s + pretty (List _ (x:xs)) = parens $ align $ sep ( fmap pretty (x:xs) ) + pretty (List _ []) = parens mempty + +instance Pretty Literal where + pretty = \case + LitStr s -> dquotes (pretty s) + LitInt i -> pretty i + LitScientific v -> viaShow v + + LitBool b | b -> "#t" + | otherwise -> "#f" + + +instance ToJSON Literal where + toJSON (LitStr s) = String s + toJSON (LitInt i) = Number (fromInteger i) + toJSON (LitScientific s) = Number s + toJSON (LitBool b) = Bool b + +instance ToJSON (Syntax c) where + toJSON (Symbol _ (Id "#nil")) = Null + toJSON (Symbol _ (Id s)) = String s + toJSON (Literal _ l) = toJSON l + toJSON (List _ items) = + case items of + (Symbol _ "object" : rest) -> + object $ mapMaybe pairToKeyValue rest + _ -> Array . V.fromList $ fmap toJSON items + + where + pairToKeyValue :: Syntax c -> Maybe (Key, Value) + pairToKeyValue (List _ [SymbolVal (Id k), SymbolVal ":", v]) = Just (fromText k .= toJSON v) + pairToKeyValue (List _ [LitStrVal k, SymbolVal ":", v]) = Just (fromText k .= toJSON v) + pairToKeyValue _ = Nothing + + +instance IsContext c => FromJSON (Syntax c) where + parseJSON (String t) = pure $ Literal noContext (LitStr t) + parseJSON (Number n) + | isInteger n = pure $ Literal noContext (LitInt (floor n)) + | otherwise = pure $ Literal noContext (LitScientific n) + parseJSON (Bool b) = pure $ Literal noContext (LitBool b) + parseJSON (Array a) = List noContext <$> mapM parseJSON (V.toList a) + parseJSON (Object o) = do + pairs <- forM (Aeson.toList o) $ \(key, value) -> do + valueSyntax <- parseJSON value + pure $ List noContext [ Symbol noContext (Id (toText key)) + , Symbol noContext ":" + , valueSyntax + ] + pure $ List noContext (Symbol noContext (Id "object") : pairs) + parseJSON _ = fail "Cannot parse JSON to Syntax" + + diff --git a/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Types.hs b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Types.hs new file mode 100644 index 00000000..8efe212c --- /dev/null +++ b/miscellaneous/suckless-conf/lib/Data/Config/Suckless/Types.hs @@ -0,0 +1 @@ +module Data.Config.Suckless.Types where \ No newline at end of file diff --git a/miscellaneous/suckless-conf/suckless-conf.cabal b/miscellaneous/suckless-conf/suckless-conf.cabal new file mode 100644 index 00000000..8011ace2 --- /dev/null +++ b/miscellaneous/suckless-conf/suckless-conf.cabal @@ -0,0 +1,143 @@ +cabal-version: 3.0 +name: suckless-conf +version: 0.1.2.9 +-- synopsis: +-- description: +license: BSD-3-Clause +license-file: LICENSE +author: Dmitry Zuikov +maintainer: dzuikov@gmail.com +-- copyright: +category: Text +build-type: Simple +extra-doc-files: CHANGELOG.md +-- extra-source-files: + +common shared-properties + ghc-options: + -Wall + -- -fno-warn-unused-matches + -- -fno-warn-unused-do-bind + -- -Werror=missing-methods + -- -Werror=incomplete-patterns + -- -fno-warn-unused-binds + -- -threaded + -- -rtsopts + -- "-with-rtsopts=-N4 -A64m -AL256m -I0" + + + default-language: Haskell2010 + + default-extensions: + ApplicativeDo + , BangPatterns + , BlockArguments + , ConstraintKinds + , DataKinds + , DeriveDataTypeable + , DeriveGeneric + , DerivingStrategies + , DerivingVia + , ExtendedDefaultRules + , FlexibleContexts + , FlexibleInstances + , GADTs + , GeneralizedNewtypeDeriving + , ImportQualifiedPost + , LambdaCase + , MultiParamTypeClasses + , OverloadedStrings + , QuasiQuotes + , ScopedTypeVariables + , StandaloneDeriving + , TupleSections + , TypeApplications + , TypeFamilies + , ImportQualifiedPost + + + +library + import: shared-properties + + exposed-modules: + Data.Config.Suckless + , Data.Config.Suckless.Syntax + , Data.Config.Suckless.Parse + , Data.Config.Suckless.KeyValue + , Data.Config.Suckless.Script + , Data.Config.Suckless.Script.File + + other-modules: + Data.Config.Suckless.Types + , Data.Config.Suckless.Parse.Fuzzy + , Data.Config.Suckless.Script.Internal + + -- other-extensions: + build-depends: base + , aeson + , bytestring + , containers + , directory + , filepath + , filepattern + , fuzzy-parse >= 0.1.3.1 + , hashable + , interpolatedstring-perl6 + , microlens-platform + , mtl + , prettyprinter + , prettyprinter-ansi-terminal + , safe + , scientific + , streaming + , stm + , text + , time + , transformers + , unliftio + , unordered-containers + , vector + + hs-source-dirs: lib + default-language: Haskell2010 + +test-suite spec + import: shared-properties + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Data.Config.Suckless.KeyValueSpec + Data.Config.Suckless.AesonSpec + + hs-source-dirs: + test + ghc-options: + -Wall + -threaded + -rtsopts + -with-rtsopts=-N + build-tool-depends: + hspec-discover:hspec-discover + build-depends: base + , hspec + , aeson + , scientific + , suckless-conf + , fuzzy-parse >= 0.1.3.1 + , containers + , mtl + , text + , prettyprinter + , interpolatedstring-perl6 + , tasty-hunit + + default-language: Haskell2010 + default-extensions: + DerivingStrategies + , FlexibleInstances + , MultiParamTypeClasses + , OverloadedStrings + , ScopedTypeVariables + , TypeApplications + diff --git a/miscellaneous/suckless-conf/t/fixme-config-1 b/miscellaneous/suckless-conf/t/fixme-config-1 new file mode 100644 index 00000000..847bfb21 --- /dev/null +++ b/miscellaneous/suckless-conf/t/fixme-config-1 @@ -0,0 +1,23 @@ + +;; fixme config file + +fixme-comments // # -- + +fixme-prefix FIXME: bugs issues +fixme-prefix TODO: bugs issues + +fixme-files **/*.hs + +fixme-files doc/devlog + +fixme-files-ignore .direnv/** dist-newstyle/** + +fixme-id-show-len 10 + +; fixme-tag-prefix # + +fixme-list-full-row-pref "## " +fixme-list-full-row-suff "\n\n;;;" + + + diff --git a/miscellaneous/suckless-conf/t/fixme-log b/miscellaneous/suckless-conf/t/fixme-log new file mode 100644 index 00000000..05e0e0bc --- /dev/null +++ b/miscellaneous/suckless-conf/t/fixme-log @@ -0,0 +1,4 @@ +; fixme log file + +fixme-merged BbjfCj H4epFBNr2i + diff --git a/miscellaneous/suckless-conf/t/just-comment b/miscellaneous/suckless-conf/t/just-comment new file mode 100644 index 00000000..6e0d2e18 --- /dev/null +++ b/miscellaneous/suckless-conf/t/just-comment @@ -0,0 +1 @@ +; blah-blah diff --git a/miscellaneous/suckless-conf/t/key-value-test-config b/miscellaneous/suckless-conf/t/key-value-test-config new file mode 100644 index 00000000..2e8f7a99 --- /dev/null +++ b/miscellaneous/suckless-conf/t/key-value-test-config @@ -0,0 +1,34 @@ +; comment +foo "a" +bar "a" +bar "b" + +int1 122 +int2 0 +int3 -22 +int4 0xFAFA +int5 0b11111111 +int6 -0xFAFA + +(jopa-kita) + +(sci1 1e9) +(sci2 0.003) +(sci3 -0.001) +(sci4 -2e11) +(sci5 -2e-3) + +(wtf1 .001) + +some-object {object ( key : 42) } + +{another-object + (object + ( key1 : 42 ) + ( key2 : #f ) + ( key3 : [ 1 2 3 4 ] ) + ) +} + + + diff --git a/miscellaneous/suckless-conf/test/Data/Config/Suckless/AesonSpec.hs b/miscellaneous/suckless-conf/test/Data/Config/Suckless/AesonSpec.hs new file mode 100644 index 00000000..d6239168 --- /dev/null +++ b/miscellaneous/suckless-conf/test/Data/Config/Suckless/AesonSpec.hs @@ -0,0 +1,212 @@ +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE KindSignatures #-} + + +{-# OPTIONS_GHC -Wno-orphans #-} +module Data.Config.Suckless.AesonSpec (spec) where + +import Data.Config.Suckless.KeyValue +import Data.Config.Suckless.Parse +import Data.Config.Suckless.Syntax +import Data.Functor +import Data.Function +import Data.Scientific +import Data.Text (Text) +import Data.Text.IO qualified as Text + +import GHC.Generics hiding (C) +import Text.InterpolatedString.Perl6 (qc,q) +import Data.Aeson +import Data.Maybe +import Control.Monad.Reader +import Test.Hspec +import Test.Tasty.HUnit +import Prettyprinter + + +readConfig :: Text -> IO [Syntax C] +readConfig s = do + pure $ parseTop s & either mempty id + -- print $ pretty f + -- pure f + +data SomeData = + SomeData + { someDataKey1 :: Int + , someDataKey2 :: String + , someDataKey3 :: [Scientific] + } + deriving stock (Generic,Show,Eq) + +instance ToJSON SomeData +instance FromJSON SomeData + +data Port +data Users + +instance HasCfgKey Port (Maybe Int) + where key = "port" + +instance HasCfgKey Users [Value] + where key = "basic-users" + +spec :: Spec +spec = do + describe "toJSON" $ do + + it "reads int" $ do + c <- readConfig [qc|1|] <&> toJSON + c `shouldBe` toJSON [[1::Int]] + + it "reads scientific" $ do + c <- readConfig [qc|1.00|] <&> toJSON + c `shouldBe` toJSON [[1.00 :: Scientific]] + + it "reads bool" $ do + t <- readConfig [qc|#t|] <&> toJSON . head + t `shouldBe` toJSON [Bool True] + f <- readConfig [qc|#f|] <&> toJSON . head + f `shouldBe` toJSON [Bool False] + + it "reads string" $ do + s <- readConfig [qc|"somestring"|] <&> toJSON + s `shouldBe` toJSON [["somestring" :: String]] + + it "reads array" $ do + s <- readConfig [qc|(1 2 3 4)|] <&> toJSON . head + print s + s `shouldBe` toJSON [1::Int,2,3,4] + + it "reads simple object" $ do + s <- readConfig [qc| + (object + (key1 : 22) + (key2 : #f) + (key3 : [1 2 3 4]) + (key4 : (object (o1 : "bebe")) ) + ("fafa" : "fifa") + (none : #nil) + ) + |] <&> toJSON . head + + let s1 = decode @Value [q| + { + "key1": 22, + "key2": false, + "key3": [1, 2, 3, 4], + "key4": { + "o1": "bebe" + }, + "fafa" : "fifa", + "none" : null + } + + |] + + print s + print s1 + Just s `shouldBe` s1 + + + it "serializes object to syntax" $ do + let some = SomeData 1 "some-data" [1, 2, 3, 4, 5, 10] + + let someSyn = case fromJSON @(Syntax ()) (toJSON some) of + Success syn -> Just syn + _ -> Nothing + + print $ pretty someSyn + + let json = fromJust $ someSyn <&> toJSON + + let someObject = fromJSON @SomeData json + + print someObject + someObject `shouldBe` Success some + + it "read-real-config" do + let cfg = [q| + +port 3000 + +hbs2-url "http://localhost:5001" + +default-token-name "LCOIN" + +hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/hbs2.key" + +; old test thermoland reflog +hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/termoland-reflog-GX8gmPi2cAxxgnaKmLmR5iViup1BNkwpdCCub3snLT1y.key" + +; new test thermoland reflog +hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/termoland-reflog-AdowWzo4iW1JejHFRnPnxQWNot8uL5sciFup6RHx2gZG.key" + + + +hbs2-keyring "/home/hbs2/keys/lcoin-belorusskaya-JAiAjKzfWfTGXjuSf4GXaj44cWfDQ8vifxoQU3tq5hn7.key" +hbs2-keyring "/home/hbs2/keys/lcoin-krymskaya-CEDBX2niVK3YL7WxzLR3xj8iUNHa9GU2EfXUqDU7fSGK.key" +hbs2-keyring "/home/hbs2/keys/lcoin-ushakova-GyTXGiCUJu81CMXYZzs7RhHu4vxJnLYgT3n2neXG5uaY.key" +hbs2-keyring "/home/hbs2/keys/lcoin-zelenopark-4fFvFGzQRp2WSXtDHepeJvMtCfQdSASq9qmsELWRJDgv.key" + + + +jwk-path "/home/hbs2/lcoin-adapter/secrets/jwk/public_key.jwk" + +jwk-path "/home/hbs2/lcoin-adapter/secrets/jwk/public-key-2023-11-03.jwk" + +lcoin-rate 5 + +db-path "/home/hbs2/.local/share/lcoin-adapter/state.db" + +registration-bonus 500 + +log-file "/home/hbs2/lcoin-adapter/log.txt" + +; qblf-socket "/tmp/qblf.socket" + +qblf-treasure "64zvWqGUf57WmGCTFWrVaNEqXikUocGyKFtg5mhyWCiB" + +reports-ignore-key "DyKWNLvpRSsTsJfVxTciqxnCJ6UhF4Mf6WoMw5qkftG4" +reports-ignore-key "3MjGvpffawUijHxbbsaF9J6wt4YReRdArUCTfHo1RhSm" + + +;; v2 +db-journal "/tmp/lcoin-adapter-journal.sqlite" +db-cache "/tmp/lcoin-adapter-cache-db.sqlite" +hbs2-store "/tmp/hbs2-store" + +treasure "64zvWqGUf57WmGCTFWrVaNEqXikUocGyKFtg5mhyWCiB" + +keybox "http://localhost:8034/" +dev-env false +(jwk-keys ( + "/home/hbs2/lcoin-adapter/secrets/jwk/public_key.jwk" + "/home/hbs2/lcoin-adapter/secrets/jwk/public-key-2023-11-03.jwk" + )) + +(basic-users ( + (object (name "mobile") (pass "mobile-pass")) + (object (name "termo") (pass "termo-pass")) + )) + +(client-creator "BYVqWJdn18Q3AjmJBPw2yusZ5ouNmgiRydWQgBEh684J") +(client-creator-keyring "/home/hbs2/keys/journal/client-creator_BYVqWJdn18Q3AjmJBPw2yusZ5ouNmgiRydWQgBEh684J.key") + +(coin-minter "4Gnno5yXUbT5dwfphKtDW7dWeq4uBvassSdbVvB3y67p") +(coin-minter-keyring "/home/hbs2/keys/journal/coin-minter_4Gnno5yXUbT5dwfphKtDW7dWeq4uBvassSdbVvB3y67p.key") + |] :: Text + + + let what = parseTop cfg & either (error.show) id + + let pno = runReader (cfgValue @Port @(Maybe Int)) what + -- what + + assertEqual "pno" pno (Just 3000) + + + diff --git a/miscellaneous/suckless-conf/test/Data/Config/Suckless/KeyValueSpec.hs b/miscellaneous/suckless-conf/test/Data/Config/Suckless/KeyValueSpec.hs new file mode 100644 index 00000000..be1543ff --- /dev/null +++ b/miscellaneous/suckless-conf/test/Data/Config/Suckless/KeyValueSpec.hs @@ -0,0 +1,184 @@ +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Data.Config.Suckless.KeyValueSpec (spec) where + +import Data.Function +import Control.Monad.IO.Class +import Control.Monad.Reader +import Data.Config.Suckless.KeyValue +import Data.Config.Suckless.Parse +import Data.Config.Suckless.Syntax +import Data.Functor +import Data.Scientific +import Data.Text.IO qualified as Text +import Data.Set (Set) +import qualified Data.Set as Set +import Prettyprinter +import Data.Aeson +import Text.InterpolatedString.Perl6 (qc,q) +import Test.Hspec + + +data FirstKey + +data SecondKey + +data ThirdKey + +data Int1 +data Int2 +data Int3 +data Int4 +data Int5 +data Int6 + +data Sci1 +data Sci2 +data Sci3 +data Sci4 +data Sci5 + +data O1 +data O2 + +instance HasCfgKey FirstKey (Maybe String) where + key = "foo" + +instance HasCfgKey SecondKey (Set String) where + key = "bar" + +instance HasCfgKey ThirdKey (Maybe String) where + key = "baz" + +instance HasCfgKey Int1 b where + key = "int1" + +instance HasCfgKey Int2 b where + key = "int2" + +instance HasCfgKey Int3 b where + key = "int3" + +instance HasCfgKey Int4 b where + key = "int4" + +instance HasCfgKey Int5 b where + key = "int5" + +instance HasCfgKey Int6 b where + key = "int6" + +instance HasCfgKey Sci1 b where + key = "sci1" + +instance HasCfgKey Sci2 b where + key = "sci2" + +instance HasCfgKey Sci3 b where + key = "sci3" + +instance HasCfgKey Sci4 b where + key = "sci4" + +instance HasCfgKey Sci5 b where + key = "sci5" + +instance HasCfgKey O1 b where + key = "some-object" + +instance HasCfgKey O2 b where + key = "another-object" + +instance HasConf IO where + getConf = liftIO readConfig + +readConfig :: IO [Syntax C] +readConfig = do + let configFilePath = "t/key-value-test-config" + f <- Text.readFile configFilePath <&> parseTop <&> either mempty id + print $ pretty f + pure f + +spec :: Spec +spec = do + describe "config parsing" $ do + + it "reads string" $ do + firstValue <- cfgValue @FirstKey @(Maybe String) + firstValue `shouldBe` Just "a" + + it "reads a set of strings" $ do + secondValue <- cfgValue @SecondKey @(Set String) + secondValue `shouldBe` Set.fromList ["a", "b"] + + it "reads nothing" $ do + thridValue <- cfgValue @ThirdKey @(Maybe String) + thridValue `shouldBe` Nothing + + it "reads ints" $ do + x1 <- cfgValue @Int1 @(Maybe Integer) + x1 `shouldBe` Just 122 + + x2 <- cfgValue @Int2 + x2 `shouldBe` Just (0 :: Integer) + + x3 <- cfgValue @Int3 + x3 `shouldBe` Just (-22 :: Integer) + + x4 <- cfgValue @Int4 @(Maybe Integer) + x4 `shouldBe` Just 0xFAFA + + x5 <- cfgValue @Int5 @(Maybe Integer) + x5 `shouldBe` Just 255 + + x6 <- cfgValue @Int6 @(Maybe Integer) + x6 `shouldBe` Just (-0xFAFA) + + it "reads scientifics" $ do + x1 <- cfgValue @Sci1 @(Maybe Scientific) + x1 `shouldBe` Just 1e9 + + x2 <- cfgValue @Sci2 @(Maybe Scientific) + x2 `shouldBe` Just 0.003 + + -- x3 <- cfgValue @Sci3 @(Maybe Scientific) + -- x3 `shouldBe` Just (-0.001) + + x4 <- cfgValue @Sci4 @(Maybe Scientific) + x4 `shouldBe` Just (-2e11) + + x5 <- cfgValue @Sci5 @(Maybe Scientific) + x5 `shouldBe` Just (-2e-3) + + it "reads objects" $ do + o1 <- cfgValue @O1 @(Maybe Value) + let wtf1 = [q|{ "key" : 42 }|] + o1 `shouldBe` decode wtf1 + let wtf2 = [q| + { "key1" : 42 + , "key2" : false + , "key3" : [ 1, 2, 3, 4] + } + |] + o2 <- cfgValue @O2 @(Maybe Value) + o2 `shouldBe` decode wtf2 + + + it "works in Reader" $ do + + let cfg = [qc| +int1 123 + +|] + let conf = parseTop cfg & either mempty id + + let v = runReader (cfgValue @Int1 @(Maybe Int)) conf + + v `shouldBe` Just 123 + + + diff --git a/miscellaneous/suckless-conf/test/Spec.hs b/miscellaneous/suckless-conf/test/Spec.hs new file mode 100644 index 00000000..52ef578f --- /dev/null +++ b/miscellaneous/suckless-conf/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file