From a1e67c63872d8282ef27c9c9e151ad6392e5f423 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 08:33:44 +0200 Subject: [PATCH 1/7] Refactor to use text instead of string --- src/Data/Buuka.hs | 6 ++++-- src/Data/Query.hs | 31 ++++++++++++++++--------------- src/Operations/Format.hs | 17 ++++++++++------- src/Operations/Insert.hs | 5 ++++- src/Operations/List.hs | 8 ++++++-- src/Operations/Query.hs | 10 +++++++--- test/Test/Data/Buuka.hs | 11 ++++++----- 7 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/Data/Buuka.hs b/src/Data/Buuka.hs index 62d1f62..5a6a578 100644 --- a/src/Data/Buuka.hs +++ b/src/Data/Buuka.hs @@ -23,6 +23,8 @@ import Data.Hashable import GHC.Generics (Generic) +import Data.Text + import Control.Monad.Reader import Control.Monad.State @@ -30,13 +32,13 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as B -newtype URL = URL String +newtype URL = URL Text deriving stock (Show, Eq, Generic, Ord) deriving newtype (ToJSON, FromJSON, FromJSONKey, ToJSONKey, Hashable) data BuukaEntry = BuukaEntry { url :: URL - , title :: Maybe String + , title :: Maybe Text } deriving stock (Show, Eq, Generic) deriving anyclass (ToJSON, FromJSON, Hashable) diff --git a/src/Data/Query.hs b/src/Data/Query.hs index 138ed12..70003ba 100644 --- a/src/Data/Query.hs +++ b/src/Data/Query.hs @@ -20,36 +20,37 @@ module Data.Query import Data.Buuka (BuukaEntry(..), URL(..)) -import Data.List - (isPrefixOf, isSuffixOf) - import Text.Regex.TDFA ((=~)) import Data.Functor.Foldable (Fix(..), cata) +import Data.Text + (Text) +import qualified Data.Text as T + data Field a where - Url :: Field String - Title :: Field String + Url :: Field Text + Title :: Field Text data QueryF f where - StartsWith :: Field String -> String -> QueryF f - EndsWith :: Field String -> String -> QueryF f - Regex :: Field String -> String -> QueryF f + StartsWith :: Field Text -> Text -> QueryF f + EndsWith :: Field Text -> Text -> QueryF f + Regex :: Field Text -> Text -> QueryF f And :: f -> f -> QueryF f deriving instance Functor QueryF type Query = Fix QueryF -startsWith :: Field String -> String -> Query +startsWith :: Field Text -> Text -> Query startsWith field x = Fix (StartsWith field x) -endsWith :: Field String -> String -> Query +endsWith :: Field Text -> Text -> Query endsWith field x = Fix (EndsWith field x) -regex :: Field String -> String -> Query +regex :: Field Text -> Text -> Query regex field x = Fix (Regex field x) (.&&.) :: Query -> Query -> Query @@ -57,10 +58,10 @@ a .&&. b = Fix (And a b) evaluate :: QueryF (BuukaEntry -> Bool) -> (BuukaEntry -> Bool) evaluate = \case - StartsWith Url x -> \BuukaEntry{url=URL u} -> x `isPrefixOf` u - EndsWith Url x -> \BuukaEntry{url=URL u} -> x `isSuffixOf` u - StartsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `isPrefixOf`) t - EndsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `isSuffixOf`) t + StartsWith Url x -> \BuukaEntry{url=URL u} -> x `T.isPrefixOf` u + EndsWith Url x -> \BuukaEntry{url=URL u} -> x `T.isSuffixOf` u + StartsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `T.isPrefixOf`) t + EndsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `T.isSuffixOf`) t Regex Url x -> \BuukaEntry{url=URL u} -> u =~ x Regex Title x -> \BuukaEntry{title=t} -> maybe False (=~ x) t And a b -> \e -> a e && b e diff --git a/src/Operations/Format.hs b/src/Operations/Format.hs index b155c77..3c294c9 100644 --- a/src/Operations/Format.hs +++ b/src/Operations/Format.hs @@ -26,23 +26,26 @@ import Web.Hashids import Control.Lens import Data.Text.Strict.Lens - (unpacked, utf8) + (utf8) +import Data.Text + (Text) +import qualified Data.Text as T -- | Format the entries formatEntries :: Buuka -- ^ The full set of entries, for the context -> [BuukaEntry] -- ^ The list of entries to be formatted - -> [String] + -> [Text] formatEntries buuka xs = let formatted = zipWith formatEntry [1..] xs - indexWidth = getMax . foldMap (Max . length . fst) $ formatted - in fmap (\(idx,x) -> idx <> replicate (indexWidth - length idx) ' ' <> ". " <> x) formatted + indexWidth = getMax . foldMap (Max . T.length . fst) $ formatted + in fmap (\(idx,x) -> idx <> T.replicate (indexWidth - T.length idx) " " <> ". " <> x) formatted where ctx = mkContext buuka mkContext :: Buuka -> HashidsContext mkContext = hashidsSimple . B.fingerprint - formatEntry :: Int -> BuukaEntry -> (String, String) + formatEntry :: Int -> BuukaEntry -> (Text, Text) formatEntry n = \case - BuukaEntry{title=Just t} -> (encode ctx n ^. utf8 . unpacked, t) - BuukaEntry{url=URL u} -> (encode ctx n ^. utf8 . unpacked, u) + BuukaEntry{title=Just t} -> (encode ctx n ^. utf8, t) + BuukaEntry{url=URL u} -> (encode ctx n ^. utf8, u) diff --git a/src/Operations/Insert.hs b/src/Operations/Insert.hs index 4f840d2..aa97a6b 100644 --- a/src/Operations/Insert.hs +++ b/src/Operations/Insert.hs @@ -7,7 +7,10 @@ import Control.Monad.State import qualified Data.Buuka as B -insert :: String -> Maybe String -> BuukaM () +import Data.Text + (Text) + +insert :: Text -> Maybe Text -> BuukaM () insert url title = buukaU (modify (B.insert entry)) where entry = B.BuukaEntry{ B.url = B.URL url, B.title = title } diff --git a/src/Operations/List.hs b/src/Operations/List.hs index d786655..7db1f0b 100644 --- a/src/Operations/List.hs +++ b/src/Operations/List.hs @@ -14,10 +14,14 @@ import qualified Data.Buuka as B import Operations.Format +import Data.Text + (Text) +import qualified Data.Text.IO as T + list :: BuukaM () list = - buukaQ (asks go) >>= traverse_ (liftIO . putStrLn) + buukaQ (asks go) >>= traverse_ (liftIO . T.putStrLn) where - go :: Buuka -> [String] + go :: Buuka -> [Text] go b = formatEntries b (B.elements b) diff --git a/src/Operations/Query.hs b/src/Operations/Query.hs index 0ed3a5e..d871181 100644 --- a/src/Operations/Query.hs +++ b/src/Operations/Query.hs @@ -14,10 +14,14 @@ import qualified Data.Buuka as B import Operations.Format -query :: Field String -> String -> BuukaM () +import Data.Text + (Text) +import qualified Data.Text.IO as T + +query :: Field Text -> Text -> BuukaM () query field q = - buukaQ (asks go) >>= traverse_ (liftIO . putStrLn) + buukaQ (asks go) >>= traverse_ (liftIO . T.putStrLn) where - go :: Buuka -> [String] + go :: Buuka -> [Text] go b = formatEntries b (filter (predicate (regex field q)) . B.elements $ b) diff --git a/test/Test/Data/Buuka.hs b/test/Test/Data/Buuka.hs index 5e65b10..8149767 100644 --- a/test/Test/Data/Buuka.hs +++ b/test/Test/Data/Buuka.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE OverloadedStrings #-} module Test.Data.Buuka where import Hedgehog @@ -8,26 +9,26 @@ import Test.Tasty.Hedgehog import qualified Data.Aeson as A -import Data.List - (intercalate) import qualified Data.Foldable as F +import qualified Data.Text as T + import Data.Buuka genUrl :: Gen URL -genUrl = URL . concat <$> sequence go +genUrl = URL . T.concat <$> sequence go where go = [ Gen.element protocols, Gen.element domains, Gen.element tlds, pure "/", genPath ] protocols = ["http://", "https://"] domains = ["example", "foo", "bar"] tlds = ["com", "fi", "org", "net", "info"] - genPath = intercalate "/" <$> Gen.list (Range.linear 0 10) (Gen.element paths) + genPath = T.intercalate "/" <$> Gen.list (Range.linear 0 10) (Gen.element paths) paths = ["foo", "bar", "asd", "xyzzy"] genBuukaEntry :: Gen BuukaEntry genBuukaEntry = BuukaEntry <$> genUrl <*> genTitle where - genTitle = Gen.maybe (Gen.string (Range.linear 0 10) Gen.unicode) + genTitle = Gen.maybe (Gen.text (Range.linear 0 10) Gen.unicode) genBuuka :: Gen Buuka genBuuka = F.foldl' (flip insert) mempty <$> Gen.list (Range.linear 0 10) genBuukaEntry -- 2.47.0 From 25ecac21fab2fc3d89e557b08f90183093a8ede1 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 09:00:40 +0200 Subject: [PATCH 2/7] List the firefox places.sqlite --- buuka.cabal | 4 +++ default.nix | 16 ++++++----- src/Operations/Import/Firefox.hs | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/Operations/Import/Firefox.hs diff --git a/buuka.cabal b/buuka.cabal index 8b64432..c4d48b2 100644 --- a/buuka.cabal +++ b/buuka.cabal @@ -40,6 +40,7 @@ library , Operations.Insert , Operations.List , Operations.Query + , Operations.Import.Firefox , Operations , Data.Environment , Data.Buuka @@ -61,6 +62,9 @@ library , lens , hashable , regex-tdfa + , sqlite-simple + , conduit + , conduit-extra hs-source-dirs: src executable buuka diff --git a/default.nix b/default.nix index 795a5dd..ece3a3d 100644 --- a/default.nix +++ b/default.nix @@ -1,8 +1,9 @@ -{ mkDerivation, aeson, base, bytestring, containers -, deriving-compat, exceptions, filepath, hashable, hashids -, hedgehog, hedgehog-corpus, lens, mtl, optparse-applicative -, regex-tdfa, stdenv, tasty, tasty-hedgehog, tasty-hunit, text -, transformers, unliftio, vector, yaml +{ mkDerivation, aeson, base, bytestring, conduit, conduit-extra +, containers, deriving-compat, exceptions, filepath, hashable +, hashids, hedgehog, hedgehog-corpus, lens, mtl +, optparse-applicative, regex-tdfa, sqlite-simple, stdenv, tasty +, tasty-hedgehog, tasty-hunit, text, transformers, unliftio, vector +, yaml }: mkDerivation { pname = "buuka"; @@ -11,8 +12,9 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson base bytestring containers exceptions filepath hashable - hashids lens mtl regex-tdfa text transformers unliftio vector yaml + aeson base bytestring conduit conduit-extra containers exceptions + filepath hashable hashids lens mtl regex-tdfa sqlite-simple text + transformers unliftio vector yaml ]; executableHaskellDepends = [ base optparse-applicative unliftio ]; testHaskellDepends = [ diff --git a/src/Operations/Import/Firefox.hs b/src/Operations/Import/Firefox.hs new file mode 100644 index 0000000..9671aa7 --- /dev/null +++ b/src/Operations/Import/Firefox.hs @@ -0,0 +1,47 @@ +{-| +Module : Operations.Import.Firefox +Description : Imports from firefox +Copyright : (c) Mats Rauhala, 2020 +License : BSD-3-Clause +Maintainer : mats.rauhala@iki.fi +Stability : experimental +Portability : POSIX + +Imports from firefox. Firefox needs to be closed when doing the import +-} +module Operations.Import.Firefox where + +import Conduit +import qualified Data.Conduit.Combinators as C + +import System.FilePath + (takeFileName, ()) + +import Control.Exception + (Exception) + +import System.Environment + (lookupEnv) + +import GHC.Stack + +-- select p.title, p.url from moz_bookmarks b join moz_places p on b.fk = p.id +-- +-- select keyword from moz_keywords where place_id = ? + +newtype ImportException + = HomeNotFound CallStack + deriving stock (Show) + deriving anyclass (Exception) + +stores + :: MonadResource m + => MonadThrow m + => MonadIO m + => HasCallStack + => ConduitT i FilePath m () +stores = liftIO (lookupEnv "HOME") >>= maybe (throwM (HomeNotFound callStack)) listStores + where + listStores home = + sourceDirectoryDeep False (home ".mozilla/firefox") + .| C.filter (\p -> takeFileName p == "places.sqlite") -- 2.47.0 From b1f3760e06266da69c19b3dc05ea5750b9f30bab Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 09:18:04 +0200 Subject: [PATCH 3/7] Query bookmarks from firefox N+1 queries :/ --- src/Operations/Import/Firefox.hs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Operations/Import/Firefox.hs b/src/Operations/Import/Firefox.hs index 9671aa7..b36b970 100644 --- a/src/Operations/Import/Firefox.hs +++ b/src/Operations/Import/Firefox.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} {-| Module : Operations.Import.Firefox Description : Imports from firefox @@ -14,6 +16,9 @@ module Operations.Import.Firefox where import Conduit import qualified Data.Conduit.Combinators as C +import Data.Text + (Text) + import System.FilePath (takeFileName, ()) @@ -25,6 +30,14 @@ import System.Environment import GHC.Stack +import Control.Lens + (makeLenses) + +import qualified Database.SQLite.Simple as SQL + +import Data.Traversable + (for) + -- select p.title, p.url from moz_bookmarks b join moz_places p on b.fk = p.id -- -- select keyword from moz_keywords where place_id = ? @@ -34,6 +47,15 @@ newtype ImportException deriving stock (Show) deriving anyclass (Exception) +data Firefox + = Firefox { _url :: Text + , _title :: Text + , _keywords :: [Text] + } + deriving stock (Show, Eq) + +makeLenses ''Firefox + stores :: MonadResource m => MonadThrow m @@ -45,3 +67,10 @@ stores = liftIO (lookupEnv "HOME") >>= maybe (throwM (HomeNotFound callStack)) l listStores home = sourceDirectoryDeep False (home ".mozilla/firefox") .| C.filter (\p -> takeFileName p == "places.sqlite") + +bookmarks :: MonadIO m => FilePath -> m [Firefox] +bookmarks path = liftIO $ SQL.withConnection path $ \conn -> do + elems <- SQL.query_ conn "select p.id, p.title, p.url from moz_bookmarks b join moz_places p on b.fk = p.id" + for elems $ \(_id, _title, _url) -> do + _keywords <- fmap SQL.fromOnly <$> SQL.query conn "select keyword from moz_keywords where place_id = ?" (SQL.Only @Int _id) + pure Firefox{..} -- 2.47.0 From 01c591434ef19464d2b84a131dff37d93b554d4f Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 09:52:38 +0200 Subject: [PATCH 4/7] Some lenses and incomplete importer --- src/Data/Buuka.hs | 19 ++++++++++++++++-- src/Data/Query.hs | 16 ++++++++------- src/Operations/Format.hs | 4 ++-- src/Operations/Import/Firefox.hs | 34 +++++++++++++++++++++++++++++++- src/Operations/Insert.hs | 2 +- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/Data/Buuka.hs b/src/Data/Buuka.hs index 5a6a578..fc1d9c2 100644 --- a/src/Data/Buuka.hs +++ b/src/Data/Buuka.hs @@ -1,11 +1,16 @@ +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeFamilies #-} module Data.Buuka ( BuukaQ(..) , BuukaU(..) , BuukaEntry(..) + , url + , title , URL(..) + , _URL , Buuka + , _Buuka , insert , elements @@ -13,6 +18,8 @@ module Data.Buuka ) where +import Control.Lens (makeLenses, Iso', iso) + import Database.Migrations import Data.Aeson @@ -36,13 +43,18 @@ newtype URL = URL Text deriving stock (Show, Eq, Generic, Ord) deriving newtype (ToJSON, FromJSON, FromJSONKey, ToJSONKey, Hashable) +_URL :: Iso' URL Text +_URL = iso (\(URL t) -> t) URL + data BuukaEntry - = BuukaEntry { url :: URL - , title :: Maybe Text + = BuukaEntry { _url :: URL + , _title :: Maybe Text } deriving stock (Show, Eq, Generic) deriving anyclass (ToJSON, FromJSON, Hashable) +makeLenses ''BuukaEntry + instance SafeJSON BuukaEntry where type Version BuukaEntry = 0 @@ -50,6 +62,9 @@ newtype Buuka = Buuka [BuukaEntry] deriving stock (Show, Eq) deriving newtype (Semigroup, Monoid, FromJSON, ToJSON, Hashable) +_Buuka :: Iso' Buuka [BuukaEntry] +_Buuka = iso (\(Buuka b) -> b) Buuka + insert :: BuukaEntry -> Buuka -> Buuka insert e (Buuka b) = Buuka (e : b) diff --git a/src/Data/Query.hs b/src/Data/Query.hs index 70003ba..4425caa 100644 --- a/src/Data/Query.hs +++ b/src/Data/Query.hs @@ -18,7 +18,9 @@ module Data.Query where import Data.Buuka - (BuukaEntry(..), URL(..)) + (BuukaEntry, title, url, _URL) + +import Control.Lens import Text.Regex.TDFA ((=~)) @@ -58,12 +60,12 @@ a .&&. b = Fix (And a b) evaluate :: QueryF (BuukaEntry -> Bool) -> (BuukaEntry -> Bool) evaluate = \case - StartsWith Url x -> \BuukaEntry{url=URL u} -> x `T.isPrefixOf` u - EndsWith Url x -> \BuukaEntry{url=URL u} -> x `T.isSuffixOf` u - StartsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `T.isPrefixOf`) t - EndsWith Title x -> \BuukaEntry{title=t} -> maybe False (x `T.isSuffixOf`) t - Regex Url x -> \BuukaEntry{url=URL u} -> u =~ x - Regex Title x -> \BuukaEntry{title=t} -> maybe False (=~ x) t + StartsWith Url x -> \e -> x `T.isPrefixOf` (e ^. url . _URL) + EndsWith Url x -> \e -> x `T.isSuffixOf` (e ^. url . _URL) + StartsWith Title x -> \e -> maybe False (x `T.isPrefixOf`) $ e ^. title + EndsWith Title x -> \e -> maybe False (x `T.isSuffixOf`) $ e ^. title + Regex Url x -> \e -> (e ^. url . _URL) =~ x + Regex Title x -> \e -> maybe False (=~ x) $ e ^. title And a b -> \e -> a e && b e predicate :: Query -> BuukaEntry -> Bool diff --git a/src/Operations/Format.hs b/src/Operations/Format.hs index 3c294c9..f4524c4 100644 --- a/src/Operations/Format.hs +++ b/src/Operations/Format.hs @@ -47,5 +47,5 @@ formatEntries buuka xs = mkContext = hashidsSimple . B.fingerprint formatEntry :: Int -> BuukaEntry -> (Text, Text) formatEntry n = \case - BuukaEntry{title=Just t} -> (encode ctx n ^. utf8, t) - BuukaEntry{url=URL u} -> (encode ctx n ^. utf8, u) + BuukaEntry{_title=Just t} -> (encode ctx n ^. utf8, t) + BuukaEntry{_url=URL u} -> (encode ctx n ^. utf8, u) diff --git a/src/Operations/Import/Firefox.hs b/src/Operations/Import/Firefox.hs index b36b970..c938b73 100644 --- a/src/Operations/Import/Firefox.hs +++ b/src/Operations/Import/Firefox.hs @@ -13,6 +13,19 @@ Imports from firefox. Firefox needs to be closed when doing the import -} module Operations.Import.Firefox where +import Data.Monoid + (Endo(..)) + +import qualified Data.Foldable as F +import qualified Data.Set as S + +import Control.Monad.State + (modify) + +import Data.Buuka + (Buuka) +import qualified Data.Buuka as B + import Conduit import qualified Data.Conduit.Combinators as C @@ -31,13 +44,15 @@ import System.Environment import GHC.Stack import Control.Lens - (makeLenses) + (foldMapOf, folded, makeLenses, to, (&), (<>~), (^.)) import qualified Database.SQLite.Simple as SQL import Data.Traversable (for) +import Control.Monad.Buuka + -- select p.title, p.url from moz_bookmarks b join moz_places p on b.fk = p.id -- -- select keyword from moz_keywords where place_id = ? @@ -74,3 +89,20 @@ bookmarks path = liftIO $ SQL.withConnection path $ \conn -> do for elems $ \(_id, _title, _url) -> do _keywords <- fmap SQL.fromOnly <$> SQL.query conn "select keyword from moz_keywords where place_id = ?" (SQL.Only @Int _id) pure Firefox{..} + +data Update + = Update { _buuka :: !Buuka + , _seen :: !(S.Set Text) + } + deriving stock (Show) + +makeLenses ''Update + +importFirefox :: BuukaM () +importFirefox = do + fs <- (`appEndo` []) <$> runResourceT (runConduit $ stores .| C.mapM bookmarks .| C.foldMap (\f -> Endo (++ f))) + buukaU $ modify $ \oldState -> F.foldl' update (initialState oldState) fs ^. buuka + where + -- incomplete update + update acc f = acc & seen <>~ (f ^. url . to S.singleton) + initialState oldState = Update oldState (foldMapOf (B._Buuka . folded . B.url . B._URL) S.singleton oldState) diff --git a/src/Operations/Insert.hs b/src/Operations/Insert.hs index aa97a6b..84561d3 100644 --- a/src/Operations/Insert.hs +++ b/src/Operations/Insert.hs @@ -13,4 +13,4 @@ import Data.Text insert :: Text -> Maybe Text -> BuukaM () insert url title = buukaU (modify (B.insert entry)) where - entry = B.BuukaEntry{ B.url = B.URL url, B.title = title } + entry = B.BuukaEntry{ B._url = B.URL url, B._title = title } -- 2.47.0 From ae68414db30c33949187a7a7cf1ae4d123598e55 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 20:44:06 +0200 Subject: [PATCH 5/7] Insert to buuka --- src/Operations/Import/Firefox.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Operations/Import/Firefox.hs b/src/Operations/Import/Firefox.hs index c938b73..3a99da9 100644 --- a/src/Operations/Import/Firefox.hs +++ b/src/Operations/Import/Firefox.hs @@ -1,5 +1,6 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} {-| Module : Operations.Import.Firefox Description : Imports from firefox @@ -44,7 +45,7 @@ import System.Environment import GHC.Stack import Control.Lens - (foldMapOf, folded, makeLenses, to, (&), (<>~), (^.)) + (foldMapOf, folded, has, ix, makeLenses, to, (%~), (&), (<>~), (^.)) import qualified Database.SQLite.Simple as SQL @@ -103,6 +104,8 @@ importFirefox = do fs <- (`appEndo` []) <$> runResourceT (runConduit $ stores .| C.mapM bookmarks .| C.foldMap (\f -> Endo (++ f))) buukaU $ modify $ \oldState -> F.foldl' update (initialState oldState) fs ^. buuka where - -- incomplete update - update acc f = acc & seen <>~ (f ^. url . to S.singleton) + toEntry Firefox{..} = B.BuukaEntry{ B._url = B.URL _url, B._title = Just _title } + update acc f = acc + & seen <>~ (f ^. url . to S.singleton) + & if has (seen . ix (f ^. url)) acc then id else buuka %~ (B.insert (toEntry f)) initialState oldState = Update oldState (foldMapOf (B._Buuka . folded . B.url . B._URL) S.singleton oldState) -- 2.47.0 From e3d47d4d9d1e3ce410876f826f21aa5a663098e3 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 20:48:07 +0200 Subject: [PATCH 6/7] Improve readability --- src/Operations/Import/Firefox.hs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Operations/Import/Firefox.hs b/src/Operations/Import/Firefox.hs index 3a99da9..0c86d7a 100644 --- a/src/Operations/Import/Firefox.hs +++ b/src/Operations/Import/Firefox.hs @@ -12,7 +12,9 @@ Portability : POSIX Imports from firefox. Firefox needs to be closed when doing the import -} -module Operations.Import.Firefox where +module Operations.Import.Firefox + ( importFirefox ) + where import Data.Monoid (Endo(..)) @@ -45,7 +47,19 @@ import System.Environment import GHC.Stack import Control.Lens - (foldMapOf, folded, has, ix, makeLenses, to, (%~), (&), (<>~), (^.)) + ( Lens' + , foldMapOf + , folded + , has + , ix + , lens + , makeLenses + , to + , (%~) + , (&) + , (<>~) + , (^.) + ) import qualified Database.SQLite.Simple as SQL @@ -70,7 +84,8 @@ data Firefox } deriving stock (Show, Eq) -makeLenses ''Firefox +url :: Lens' Firefox Text +url = lens _url (\f u -> f{_url = u}) stores :: MonadResource m @@ -101,11 +116,17 @@ makeLenses ''Update importFirefox :: BuukaM () importFirefox = do + -- Collect all the imported bookmarks fs <- (`appEndo` []) <$> runResourceT (runConduit $ stores .| C.mapM bookmarks .| C.foldMap (\f -> Endo (++ f))) + -- Insert to the buuka store iff, the urls don't already exist in the store + -- The fold keeps track of a set of already seen entries. Every iteration + -- adds the current url to the known set of urls. Only if the url doesn't + -- exist in the set, will it be inserted to the store buukaU $ modify $ \oldState -> F.foldl' update (initialState oldState) fs ^. buuka where toEntry Firefox{..} = B.BuukaEntry{ B._url = B.URL _url, B._title = Just _title } update acc f = acc & seen <>~ (f ^. url . to S.singleton) & if has (seen . ix (f ^. url)) acc then id else buuka %~ (B.insert (toEntry f)) - initialState oldState = Update oldState (foldMapOf (B._Buuka . folded . B.url . B._URL) S.singleton oldState) + initialState oldState = Update oldState (initialUrls oldState) + initialUrls = foldMapOf (B._Buuka . folded . B.url . B._URL) S.singleton -- 2.47.0 From c05161eb73864b28b94dfc945fd2968626b9db62 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sun, 3 Jan 2021 22:05:22 +0200 Subject: [PATCH 7/7] Import firefox UX --- app/Main.hs | 1 + src/Operations.hs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/Main.hs b/app/Main.hs index cf3c54a..662dec2 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -27,6 +27,7 @@ commands = subparser ( command "insert" (info (insertOpts Operations.insert <**> helper) (progDesc "Insert a new bookmark")) <> command "list" (info (pure Operations.list <**> helper) (progDesc "List all the bookmarks")) <> command "query" (info (queryOpts Operations.query <**> helper) (progDesc "Query the bookmarks")) + <> command "import" (info (pure Operations.importFirefox <**> helper) (progDesc "Import")) ) where insertOpts f = diff --git a/src/Operations.hs b/src/Operations.hs index c0ed6d6..57f331a 100644 --- a/src/Operations.hs +++ b/src/Operations.hs @@ -2,6 +2,7 @@ module Operations ( module Operations.Insert , module Operations.List , module Operations.Query + , module Operations.Import.Firefox ) where @@ -11,3 +12,5 @@ import Operations.List (list) import Operations.Query (query) +import Operations.Import.Firefox + (importFirefox) -- 2.47.0