| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- module PureTabs.Model.GlobalState (
- ExtWindow
- , GlobalState
- , _active
- , _id
- , _index
- , _port
- , _portFromWindow
- , _portFromWindowId
- , _positions
- , _tabFromTabIdAndWindow
- , _tabFromWindow
- , _tabId
- , _tabIndex
- , _tabs
- , _tabWindowId
- , _windowIdToWindow
- , _windowIdToTabIdToTab
- , _windows
- , emptyWindow
- , initialGlobalState
- , initialTabListToGlobalState
- , addEmptyWindow
- , deleteWindow
- , createTab
- , updateTab
- , activateTab
- , moveTab
- , deleteTab
- , detachTab
- , attachTab
- , sendToTabPort
- , sendToWindowPort
- , tabFromWinIdAndTabId
- , initializeWindowState
- ) where
- import Browser.Runtime (Port, postMessageJson)
- import Browser.Tabs (Tab(..), TabId, WindowId, showTabId)
- import Control.Alt ((<|>))
- import Control.Bind (join, bind, (>>=))
- import Control.Category (identity, (<<<), (>>>))
- import Control.Plus (empty) as A
- import Data.Array (deleteAt, filter, foldl, fromFoldable, insertAt, mapWithIndex, sortBy, (!!)) as A
- import Data.Eq ((==), (/=))
- import Data.Function (const, on, ($))
- import Data.Functor (map, (<#>), (<$>))
- import Data.Lens (Lens', Traversal', _Just, over, preview, set, view)
- import Data.Lens.At (at)
- import Data.Lens.Iso.Newtype (_Newtype)
- import Data.Lens.Record (prop)
- import Data.List (List, groupBy, head) as L
- import Data.List.NonEmpty (NonEmptyList, head) as NEL
- import Data.Map as M
- import Data.Maybe (Maybe(..), fromMaybe, maybe, maybe')
- import Data.Monoid ((<>))
- import Data.Ord (compare)
- import Data.Show (show)
- import Data.Symbol (SProxy(..))
- import Data.Tuple (Tuple(..))
- import Data.Unit (Unit)
- import Effect (Effect)
- import Effect.Console (error)
- import Effect.Exception.Unsafe (unsafeThrow)
- import Prelude ((#))
- import PureTabs.Model.Events (BackgroundEvent)
- type GlobalState
- = { windows :: M.Map WindowId ExtWindow
- , detached :: Maybe Tab
- }
- initialGlobalState :: GlobalState
- initialGlobalState =
- { windows: M.empty
- , detached: Nothing
- }
- type ExtWindow
- = { positions :: Array TabId
- , tabs :: M.Map TabId Tab
- , port :: Maybe Port
- }
- emptyWindow :: ExtWindow
- emptyWindow =
- { positions: A.empty
- , tabs: M.empty
- , port: Nothing
- }
- _tabs :: forall a r. Lens' { tabs :: a | r } a
- _tabs = prop (SProxy :: _ "tabs")
- _port :: forall a r. Lens' { port :: a | r } a
- _port = prop (SProxy :: _ "port")
- _windows :: forall a r. Lens' { windows :: a | r } a
- _windows = prop (SProxy :: _ "windows")
- _title :: forall a r. Lens' { title :: a | r } a
- _title = prop (SProxy :: _ "title")
- _tabTitle :: Lens' Tab String
- _tabTitle = _Newtype <<< _title
- _index :: forall a r. Lens' { index :: a | r } a
- _index = prop (SProxy :: _ "index")
- _tabIndex :: Lens' Tab Int
- _tabIndex = _Newtype <<< _index
- _id :: forall a r. Lens' { id :: a | r } a
- _id = prop (SProxy :: _ "id")
- _tabId :: Lens' Tab TabId
- _tabId = _Newtype <<< _id
- _active :: forall a r. Lens' { active :: a | r } a
- _active = prop (SProxy :: _ "active")
- _windowId :: forall a r. Lens' { windowId :: a | r } a
- _windowId = prop (SProxy :: _ "windowId")
- _positions :: forall a r. Lens' { positions :: a | r } a
- _positions = prop (SProxy :: _ "positions")
- _tabWindowId :: Lens' Tab WindowId
- _tabWindowId = _Newtype <<< _windowId
- _portFromWindow :: Tab -> Traversal' GlobalState Port
- _portFromWindow (Tab tab) = _portFromWindowId tab.windowId
- _portFromWindowId :: WindowId -> Traversal' GlobalState Port
- _portFromWindowId wid = _windowIdToWindow wid <<< _port <<< _Just
- _windowIdToWindow :: WindowId -> Traversal' GlobalState ExtWindow
- _windowIdToWindow wid = _windows <<< (at wid) <<< _Just
- _tabFromWindow :: Tab -> Traversal' GlobalState (Maybe Tab)
- _tabFromWindow (Tab tab) = _windowIdToWindow tab.windowId <<< _tabs <<< (at tab.id)
- _windowIdToTabIdToTab :: WindowId -> TabId -> Traversal' GlobalState (Maybe Tab)
- _windowIdToTabIdToTab wid tid = _windowIdToWindow wid <<< _tabs <<< (at tid)
- tabFromWinIdAndTabId :: WindowId -> TabId -> GlobalState -> Maybe Tab
- tabFromWinIdAndTabId winId tabId = join <<< preview (_windowIdToTabIdToTab winId tabId)
- _tabFromTabIdAndWindow :: GlobalState -> TabId -> Maybe Tab
- _tabFromTabIdAndWindow s tabId =
- let
- allWindows = M.values s.windows
- allTabs = map (view _tabs) allWindows
- matchingTabId = map (M.lookup tabId) allTabs
- in
- join $ L.head matchingTabId
- sendToTabPort :: Tab -> GlobalState -> BackgroundEvent -> Effect Unit
- sendToTabPort tab state msg =
- case (preview (_portFromWindow tab) state) of
- Just port -> postMessageJson port msg
- Nothing -> error $ "bg: no port found for tab id " <> (showTabId tab)
- sendToWindowPort :: WindowId -> GlobalState -> BackgroundEvent -> Effect Unit
- sendToWindowPort wid state event =
- case (preview (_portFromWindowId wid) state) of
- Just port -> postMessageJson port event
- Nothing -> error $ "bg: no port found for window id " <> (show wid)
- initialTabListToGlobalState :: L.List Tab -> GlobalState
- initialTabListToGlobalState tabs = { windows: windows, detached: Nothing }
- where
- groupedTabs = L.groupBy (\(Tab t1) (Tab t2) -> t1.windowId == t2.windowId) tabs
- tabsToWindow :: NEL.NonEmptyList Tab -> Tuple WindowId ExtWindow
- tabsToWindow tabs' =
- let
- windowId = (\(Tab t) -> t.windowId) $ NEL.head tabs'
- window =
- { tabs: M.fromFoldable $ tabs' <#> \(Tab t) -> Tuple t.id (Tab t)
- , port: Nothing
- , positions: (\(Tab t) -> t.id) <$> A.sortBy (compare `on` \(Tab t) -> t.index) (A.fromFoldable tabs')
- }
- in
- Tuple windowId window
- windows = M.fromFoldable $ (tabsToWindow <$> groupedTabs)
- addEmptyWindow :: WindowId -> GlobalState -> GlobalState
- addEmptyWindow winId = (over (_windows <<< at winId)) (_ <|> (Just emptyWindow))
- deleteWindow :: WindowId -> GlobalState -> GlobalState
- deleteWindow winId state = state { windows = M.delete winId state.windows }
- createTab :: Tab -> GlobalState -> GlobalState
- createTab (Tab t) s = s { windows = M.update updateWindow t.windowId windows }
- where
- windows = case M.lookup t.windowId s.windows of
- Nothing -> M.insert t.windowId emptyWindow s.windows
- Just _ -> s.windows
- updateWindow :: ExtWindow -> Maybe ExtWindow
- updateWindow win =
- -- this will delete the window if there is an issue with the position..
- -- not the best solution but we can't really recover from it anyway.
- (A.insertAt t.index t.id win.positions)
- <#> \newPos ->
- win
- { positions = newPos
- , tabs = M.insert t.id (Tab t) win.tabs
- }
- updateTab :: Tab -> GlobalState -> GlobalState
- updateTab tab =
- -- update by replacing the tab only if it already exists
- (over (_tabFromWindow tab) (map $ const tab))
- -- or update the currently detached tab
- >>> ( \s -> case s.detached of
- Just (Tab tab')
- | (view _tabId tab) == tab'.id -> s { detached = Just (Tab tab') }
- _ -> s
- )
- moveTab :: Int -> Int -> WindowId -> GlobalState -> GlobalState
- moveTab fromIndex toIndex windowId state =
- let
- -- Update the state by moving the tab at `fromIndex` to `toIndex`.
- newState = state # over ((_windowIdToWindow windowId) <<< _positions) unsafeUpdatePositions
- -- Get the new positions for each tab based on the move just done.
- newPositions = newState # view ((_windowIdToWindow windowId) <<< _positions)
- in
- -- Update the new positions for each tab
- newState # over ((_windowIdToWindow windowId) <<< _tabs) (updateTabsIndex newPositions)
- where
- -- | Move an element from `from` to `to` in array `arr`.
- moveElement :: forall a. Int -> Int -> Array a -> Maybe (Array a)
- moveElement from to arr = do
- tab <- arr A.!! from
- A.deleteAt from arr >>= A.insertAt to tab
- -- | Update the positions tabs
- unsafeUpdatePositions :: Array TabId -> Array TabId
- unsafeUpdatePositions =
- (moveElement fromIndex toIndex)
- -- The indexes should exist, we need to revisit the code if it doesn't
- >>> (maybe' (\_ -> unsafeThrow "bg: invalid indexes during moveTab") identity)
- -- | Update the index of the tab given the positions.
- -- | This is done by folding over a map of index update function applied to all tabs.
- updateTabsIndex :: Array TabId -> M.Map TabId Tab -> M.Map TabId Tab
- updateTabsIndex positions tabs =
- let
- modifyFuncs :: Array (M.Map TabId Tab -> M.Map TabId Tab)
- modifyFuncs = A.mapWithIndex (\idx tid' -> set (at tid' <<< _Just <<< _Newtype <<< _index) idx) positions
- in
- A.foldl (#) tabs modifyFuncs
- activateTab :: WindowId -> (Maybe TabId) -> TabId -> GlobalState -> GlobalState
- activateTab winId previousTabId newTabId state =
- let
- prevTab :: Maybe Tab
- prevTab = previousTabId >>= \ptid -> join $ preview (_windowIdToTabIdToTab winId ptid) state
- prevTabF :: GlobalState -> GlobalState
- prevTabF = maybe identity (\t -> set (_activeTab t) false) prevTab
- newTab = join $ preview (_windowIdToTabIdToTab winId newTabId) state
- newTabF :: GlobalState -> GlobalState
- newTabF = maybe identity (\t -> set (_activeTab t) true) newTab
- _activeTab t = (_tabFromWindow t) <<< _Just <<< _Newtype <<< _active
- in
- (prevTabF >>> newTabF) state
- deleteTab :: WindowId -> TabId -> GlobalState -> GlobalState
- deleteTab winId tabId =
- (set (_windowIdToTabIdToTab winId tabId) Nothing)
- >>> over (_windowIdToWindow winId <<< _positions) (A.filter ((/=) tabId))
- detachTab :: WindowId -> TabId -> GlobalState -> GlobalState
- detachTab winId tabId state =
- case preview (_windowIdToTabIdToTab winId tabId) state of
- Just (Just tab) -> do
- state # (deleteTab winId tabId) >>> \s -> s { detached = Just tab }
- -- XXX: We're losing the information that we couldn't fetch the tab.
- -- This shouldn't happen, but I don't see how to go around it. We don't
- -- have a (typed) proof that a given tab exists for a window id and a tab
- -- id, so let's just assume everything is well behaved.
- -- The other solution is to first do a read, then a write, and return an
- -- effect where we can throw.
- _ -> state
- attachTab :: WindowId -> TabId -> Int -> GlobalState -> GlobalState
- attachTab winId tabId newPosition state =
- case state.detached of
- Just (Tab tab) ->
- let
- newTab = Tab (tab { windowId = winId, index = newPosition })
- in
- state # (createTab newTab) >>> (_ { detached = Nothing})
- _ -> state
- -- | Set the port of a new window connecting. If the window doesn't exist,
- -- | initialize it with new data.
- initializeWindowState :: WindowId -> Port -> GlobalState -> GlobalState
- initializeWindowState winId port =
- over (_windows <<< (at winId)) (\win -> Just $ set _port (Just port) (fromMaybe emptyWindow win))
|