Background.purs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. module PureTabs.Background where
  2. import Browser.Runtime as Runtime
  3. import Browser.Tabs (Tab(..), TabId, WindowId, query, removeOne, activateTab, moveTab, createTab)
  4. import Browser.Tabs.OnActivated as OnActivated
  5. import Browser.Tabs.OnCreated as OnCreated
  6. import Browser.Tabs.OnMoved as OnMoved
  7. import Browser.Tabs.OnRemoved as OnRemoved
  8. import Browser.Tabs.OnUpdated as OnUpdated
  9. import Browser.Utils (Listener, mkListenerOne, mkListenerTwo, mkListenerUnit)
  10. import Control.Alt ((<#>))
  11. import Control.Alternative (empty, pure, (*>))
  12. import Control.Bind ((=<<), (>>=))
  13. import Control.Category (identity, (>>>))
  14. import Data.Array (catMaybes, deleteAt, foldl, fromFoldable, insertAt, mapWithIndex, (!!))
  15. import Data.Foldable (for_)
  16. import Data.Function (flip, (#))
  17. import Data.Lens (_Just, over, preview, set, view)
  18. import Data.Lens.At (at)
  19. import Data.Lens.Iso.Newtype (_Newtype)
  20. import Data.List (List, foldMap, foldr)
  21. import Data.Map as M
  22. import Data.Maybe (Maybe(..), maybe, maybe')
  23. import Data.Monoid ((<>))
  24. import Data.Newtype (unwrap)
  25. import Data.Show (show)
  26. import Data.Unit (unit)
  27. import Debug.Trace (traceM)
  28. import Effect (Effect)
  29. import Effect.Aff (Aff, launchAff_)
  30. import Effect.Class (liftEffect)
  31. import Effect.Console (log)
  32. import Effect.Exception.Unsafe (unsafeThrow)
  33. import Effect.Ref as Ref
  34. import Prelude (Unit, bind, ($), discard, (<<<))
  35. import PureTabs.Model (BackgroundEvent(..), GlobalState, SidebarEvent(..), _active, _index, _port, _portFromWindow, _portFromWindowId, _positions, _tabFromTabIdAndWindow, _tabFromWindow, _tabs, _windowIdToWindow, _windows, tabsToGlobalState)
  36. type Ports
  37. = Ref.Ref (List Runtime.Port)
  38. main :: Effect Unit
  39. main = do
  40. log "starting background"
  41. launchAff_ runMain
  42. where
  43. runMain :: Aff Unit
  44. runMain = do
  45. allTabs <- query
  46. liftEffect $ initializeBackground =<< (Ref.new $ tabsToGlobalState allTabs)
  47. initializeBackground :: Ref.Ref GlobalState -> Effect Unit
  48. initializeBackground ref = do
  49. OnCreated.addListener $ onTabCreated ref
  50. (mkListenerTwo $ onTabDeleted ref) >>= OnRemoved.addListener
  51. OnActivated.addListener $ onTabActived ref
  52. OnUpdated.addListener $ onTabUpdated ref
  53. (mkListenerTwo $ onTabMoved ref) >>= OnMoved.addListener
  54. (mkListenerOne $ onConnect ref) >>= Runtime.onConnectAddListener
  55. onTabCreated :: (Ref.Ref GlobalState) -> Tab -> Effect Unit
  56. onTabCreated stateRef tab' = do
  57. state <-
  58. Ref.modify
  59. ( set (_tabFromWindow tab') (Just tab')
  60. *> over (_positions >>> _windowIdToWindow tab.windowId)
  61. -- TODO: throw an error here instead. Encapsulate the manipulations of
  62. -- the position array to make sure we always perform valid operation
  63. -- and otherwise throw an error or recover from it.
  64. (\p -> maybe p identity (insertAt tab.index tab.id p))
  65. )
  66. stateRef
  67. log $ "tabId: " <> (show tab.id) <> " windowId " <> show tab.windowId
  68. case (preview (_portFromWindow tab') state) of
  69. Nothing -> pure unit
  70. Just port -> do
  71. _ <- Runtime.postMessageJson port $ BgTabCreated tab'
  72. log $ "tab " <> (show tab.id) <> " created: " <> tab.title
  73. where
  74. tab = unwrap tab'
  75. onTabUpdated :: (Ref.Ref GlobalState) -> TabId -> OnUpdated.ChangeInfo -> Tab -> Effect Unit
  76. onTabUpdated stateRef tid cinfo tab' = do
  77. state <- Ref.modify (set (_tabFromWindow tab') (Just tab')) stateRef
  78. case (preview (_portFromWindow tab') state) of
  79. Nothing -> pure unit
  80. Just port -> Runtime.postMessageJson port $ BgTabUpdated tid cinfo tab'
  81. onTabMoved :: (Ref.Ref GlobalState) -> TabId -> OnMoved.MoveInfo -> Effect Unit
  82. onTabMoved ref tid minfo = do
  83. s <- Ref.modify (updateState minfo) ref
  84. case (preview (_portFromWindowId minfo.windowId) s) of
  85. Nothing -> pure unit
  86. Just port -> Runtime.postMessageJson port $ BgTabMoved tid minfo.fromIndex minfo.toIndex
  87. where
  88. updateState :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  89. updateState minfo' state =
  90. let
  91. newState = updatePositions minfo' state
  92. newPositions :: Array TabId
  93. newPositions = view ((_windowIdToWindow minfo'.windowId) <<< _positions) newState
  94. in
  95. over ((_windowIdToWindow minfo'.windowId) <<< _tabs) (updateTabsIndex newPositions) newState
  96. updatePositions :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  97. updatePositions minfo' = over ((_windowIdToWindow minfo'.windowId) <<< _positions) $ unsafeUpdatePositions minfo'
  98. updateTabsIndex :: Array TabId -> M.Map TabId Tab -> M.Map TabId Tab
  99. updateTabsIndex positions tabs =
  100. let
  101. modifyFuncs :: Array (M.Map TabId Tab -> M.Map TabId Tab)
  102. modifyFuncs = mapWithIndex (\idx tid' -> set (at tid' <<< _Just <<< _Newtype <<< _index) idx) positions
  103. in
  104. foldl (#) tabs modifyFuncs
  105. unsafeUpdatePositions :: OnMoved.MoveInfo -> Array TabId -> Array TabId
  106. unsafeUpdatePositions minfo' =
  107. (moveElement minfo'.fromIndex minfo'.toIndex)
  108. -- the indexes should exist, we need to revisit the code if it doesn't
  109. >>> (maybe' (\_ -> unsafeThrow "invalid indexes") identity)
  110. moveElement :: forall a. Int -> Int -> Array a -> Maybe (Array a)
  111. moveElement from to arr = do
  112. tab <- arr !! from
  113. deleteAt from arr >>= insertAt to tab
  114. onTabActived :: (Ref.Ref GlobalState) -> OnActivated.ActiveInfo -> Effect Unit
  115. onTabActived stateRef (OnActivated.ActiveInfo aInfo) = do
  116. log $ "activated " <> show aInfo.tabId
  117. state <- Ref.modify (updateGlobalState aInfo.previousTabId aInfo.tabId) stateRef
  118. case (preview (_portFromWindowId aInfo.windowId) state) of
  119. Nothing -> pure unit
  120. Just port -> Runtime.postMessageJson port $ BgTabActived aInfo.previousTabId aInfo.tabId
  121. where
  122. updateGlobalState :: (Maybe TabId) -> TabId -> GlobalState -> GlobalState
  123. updateGlobalState prev new state =
  124. let
  125. -- TODO: we have the windowId, we can directly get the tab from that
  126. -- without using _tabFromTabIdAndWindow that goes through all the windows.
  127. prevTab = prev >>= _tabFromTabIdAndWindow state
  128. prevTabF :: GlobalState -> GlobalState
  129. prevTabF = maybe identity (\t -> set (_activeTab t) false) prevTab
  130. newTab = _tabFromTabIdAndWindow state new
  131. newTabF :: GlobalState -> GlobalState
  132. newTabF = maybe identity (\t -> set (_activeTab t) true) newTab
  133. _activeTab t = (_tabFromWindow t) <<< _Just <<< _Newtype <<< _active
  134. in
  135. (prevTabF >>> newTabF) state
  136. onTabDeleted :: (Ref.Ref GlobalState) -> TabId -> OnRemoved.RemoveInfo -> Effect Unit
  137. onTabDeleted stateRef tabId info = do
  138. state <- Ref.read stateRef
  139. let
  140. allTabs = _tabFromTabIdAndWindow state tabId
  141. deleteTabState :: Tab -> GlobalState -> GlobalState
  142. deleteTabState t = set (_tabFromWindow t) Nothing
  143. deletePositionState :: Tab -> GlobalState -> GlobalState
  144. deletePositionState (Tab t) =
  145. over
  146. (_positions >>> _windowIdToWindow t.windowId)
  147. (\p -> maybe p identity (deleteAt t.index p))
  148. newState = foldr (\t -> deleteTabState t >>> deletePositionState t) state allTabs
  149. Ref.write newState stateRef
  150. for_ allTabs \t -> do
  151. let
  152. port = preview (_portFromWindow t) state
  153. maybe (pure unit) ((flip Runtime.postMessageJson) (BgTabDeleted tabId)) port
  154. onConnect :: (Ref.Ref GlobalState) -> Runtime.Port -> Effect Unit
  155. onConnect stateRef' port = do
  156. -- create a temporary listener ref that will only be held until the sidebar has sent its current window
  157. listenerRef <- Ref.new Nothing
  158. initialListener <-
  159. Runtime.onMessageJsonAddListener port $ windowListener
  160. $ onNewWindowId port stateRef' listenerRef
  161. -- XXX: is it possible a message arrive *before* this is executed ?
  162. -- theoretically yes, and this means this way of doing is unsafe, but it's
  163. -- difficult for a handler to remove itself otherwise.
  164. Ref.write (Just initialListener) listenerRef
  165. where
  166. windowListener :: (WindowId -> Effect Unit) -> SidebarEvent -> Effect Unit
  167. windowListener callback msg = case msg of
  168. SbHasWindowId winId -> log ("bg: created winId " <> show winId) *> callback winId
  169. _ -> pure unit
  170. -- | Initialize the data and the listeners of a new window, and send the current window state.
  171. onNewWindowId ::
  172. forall a.
  173. Runtime.Port ->
  174. (Ref.Ref GlobalState) ->
  175. (Ref.Ref (Maybe (Listener a))) ->
  176. WindowId -> Effect Unit
  177. onNewWindowId port stateRef listenerRef winId = do
  178. -- initial state of the current window
  179. r <- initWindowState port stateRef winId
  180. -- remove the previous onMessage listener
  181. ogListener <- Ref.read listenerRef
  182. foldMap (\l -> Runtime.onMessageRemoveListener port l) ogListener
  183. Ref.write Nothing listenerRef
  184. -- send initial tabs
  185. maybe (pure unit)
  186. ( \w ->
  187. Runtime.postMessageJson port
  188. $ BgInitialTabList
  189. $ fromFoldable
  190. $ w.positions
  191. <#> (flip M.lookup w.tabs)
  192. # catMaybes
  193. )
  194. (M.lookup winId r.windows)
  195. -- add the new onMessage listener
  196. sidebarListener <- Runtime.onMessageJsonAddListener port $ manageSidebar stateRef port
  197. onDisconnectListener <- mkListenerUnit $ onDisconnect stateRef winId sidebarListener
  198. Runtime.portOnDisconnect port onDisconnectListener
  199. -- | Set the port of a new window connecting. If the window doesn't exist, initialize a new data
  200. initWindowState :: Runtime.Port -> (Ref.Ref GlobalState) -> WindowId -> Effect GlobalState
  201. initWindowState port ref winId =
  202. (flip Ref.modify) ref
  203. $ over (_windows <<< (at winId))
  204. ( case _ of
  205. Nothing -> Just $ { tabs: M.empty, port: Just port, positions: empty }
  206. Just win -> Just $ set _port (Just port) win
  207. )
  208. -- TODO don't pass the full ref, but only a set of function to manipulate/access
  209. -- the data required
  210. manageSidebar :: (Ref.Ref GlobalState) -> Runtime.Port -> SidebarEvent -> Effect Unit
  211. manageSidebar stateRef port (SbDeleteTab tabId) = launchAff_ $ removeOne tabId
  212. manageSidebar stateRef port (SbActivateTab tabId) = launchAff_ $ activateTab tabId
  213. manageSidebar stateRef port (SbMoveTab tabId newIndex) = moveTab tabId {index: newIndex}
  214. manageSidebar stateRef port (SbCreateTab winId) = createTab {windowId: winId}
  215. manageSidebar stateRef port msg = pure unit
  216. onDisconnect :: forall a. (Ref.Ref GlobalState) -> WindowId -> Listener a -> Effect Unit
  217. onDisconnect stateRef winId listener = Ref.modify_ (set (_windows <<< (at winId) <<< _Just <<< _port) Nothing) stateRef