Background.purs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. module PureTabs.Background where
  2. import Browser.Runtime as Runtime
  3. import Browser.Tabs (Tab, TabId, WindowId, query, removeOne, activateTab)
  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. traceM allTabs
  47. liftEffect
  48. $ do
  49. state <- Ref.new $ tabsToGlobalState allTabs
  50. readState <- Ref.read state
  51. traceM readState
  52. initializeBackground state
  53. log "all listener initialized"
  54. initializeBackground :: Ref.Ref GlobalState -> Effect Unit
  55. initializeBackground ref = do
  56. OnCreated.addListener $ onTabCreated ref
  57. (mkListenerTwo $ onTabDeleted ref) >>= OnRemoved.addListener
  58. OnActivated.addListener $ onTabActived ref
  59. OnUpdated.addListener $ onTabUpdated ref
  60. (mkListenerTwo $ onTabMoved ref) >>= OnMoved.addListener
  61. (mkListenerOne $ onConnect ref) >>= Runtime.onConnectAddListener
  62. onTabCreated :: (Ref.Ref GlobalState) -> Tab -> Effect Unit
  63. onTabCreated stateRef tab' = do
  64. state <- Ref.modify (set (_tabFromWindow tab') (Just tab')) stateRef
  65. log $ "tabId: " <> (show tab.id) <> " windowId " <> show tab.windowId
  66. case (preview (_portFromWindow tab') state) of
  67. Nothing -> pure unit
  68. Just port -> do
  69. _ <- Runtime.postMessageJson port $ BgTabCreated tab'
  70. log $ "tab " <> (show tab.id) <> " created: " <> tab.title
  71. where
  72. tab = unwrap tab'
  73. onTabUpdated :: (Ref.Ref GlobalState) -> TabId -> OnUpdated.ChangeInfo -> Tab -> Effect Unit
  74. onTabUpdated stateRef tid cinfo tab' = do
  75. state <- Ref.modify (set (_tabFromWindow tab') (Just tab')) stateRef
  76. case (preview (_portFromWindow tab') state) of
  77. Nothing -> pure unit
  78. Just port -> Runtime.postMessageJson port $ BgTabUpdated tid cinfo tab'
  79. onTabMoved :: (Ref.Ref GlobalState) -> TabId -> OnMoved.MoveInfo -> Effect Unit
  80. onTabMoved ref tid minfo = do
  81. s <- Ref.modify (updateState minfo) ref
  82. case (preview (_portFromWindowId minfo.windowId) s) of
  83. Nothing -> pure unit
  84. Just port -> Runtime.postMessageJson port $ BgTabMoved tid minfo.fromIndex minfo.toIndex
  85. where
  86. updateState :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  87. updateState minfo' state =
  88. let
  89. newState = updatePositions minfo' state
  90. newPositions :: Array TabId
  91. newPositions = view ((_windowIdToWindow minfo'.windowId) <<< _positions) newState
  92. in
  93. over ((_windowIdToWindow minfo'.windowId) <<< _tabs) (updateTabsIndex newPositions) newState
  94. updatePositions :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  95. updatePositions minfo' = over ((_windowIdToWindow minfo'.windowId) <<< _positions) $ unsafeUpdatePositions minfo'
  96. updateTabsIndex :: Array TabId -> M.Map TabId Tab -> M.Map TabId Tab
  97. updateTabsIndex positions tabs =
  98. let
  99. modifyFuncs :: Array (M.Map TabId Tab -> M.Map TabId Tab)
  100. modifyFuncs = mapWithIndex (\idx tid' -> set (at tid' <<< _Just <<< _Newtype <<< _index) idx) positions
  101. in
  102. foldl (#) tabs modifyFuncs
  103. unsafeUpdatePositions :: OnMoved.MoveInfo -> Array TabId -> Array TabId
  104. unsafeUpdatePositions minfo' =
  105. (moveElement minfo'.fromIndex minfo'.toIndex)
  106. -- the indexes should exist, we need to revisit the code if it doesn't
  107. >>> (maybe' (\_ -> unsafeThrow "invalid indexes") identity)
  108. moveElement :: forall a. Int -> Int -> Array a -> Maybe (Array a)
  109. moveElement from to arr = do
  110. tab <- arr !! from
  111. deleteAt from arr >>= insertAt to tab
  112. onTabActived :: (Ref.Ref GlobalState) -> OnActivated.ActiveInfo -> Effect Unit
  113. onTabActived stateRef (OnActivated.ActiveInfo aInfo) = do
  114. log $ "activated " <> show aInfo.tabId
  115. state <- Ref.modify (updateGlobalState aInfo.previousTabId aInfo.tabId) stateRef
  116. case (preview (_portFromWindowId aInfo.windowId) state) of
  117. Nothing -> pure unit
  118. Just port -> Runtime.postMessageJson port $ BgTabActived aInfo.previousTabId aInfo.tabId
  119. where
  120. updateGlobalState :: (Maybe TabId) -> TabId -> GlobalState -> GlobalState
  121. updateGlobalState prev new state =
  122. let
  123. -- TODO: we have the windowId, we can directly get the tab from that
  124. -- without using _tabFromTabIdAndWindow that goes through all the windows.
  125. prevTab = prev >>= _tabFromTabIdAndWindow state
  126. prevTabF :: GlobalState -> GlobalState
  127. prevTabF = maybe identity (\t -> set (_activeTab t) false) prevTab
  128. newTab = _tabFromTabIdAndWindow state new
  129. newTabF :: GlobalState -> GlobalState
  130. newTabF = maybe identity (\t -> set (_activeTab t) true) newTab
  131. _activeTab t = (_tabFromWindow t) <<< _Just <<< _Newtype <<< _active
  132. in
  133. (prevTabF >>> newTabF) state
  134. onTabDeleted :: (Ref.Ref GlobalState) -> TabId -> OnRemoved.RemoveInfo -> Effect Unit
  135. onTabDeleted stateRef tabId info = do
  136. state <- Ref.read stateRef
  137. let
  138. allTabs = _tabFromTabIdAndWindow state tabId
  139. newState = foldr (\t -> set (_tabFromWindow t) Nothing) state allTabs
  140. Ref.write newState stateRef
  141. for_ allTabs \t -> do
  142. let
  143. port = preview (_portFromWindow t) state
  144. maybe (pure unit) ((flip Runtime.postMessageJson) (BgTabDeleted tabId)) port
  145. onConnect :: (Ref.Ref GlobalState) -> Runtime.Port -> Effect Unit
  146. onConnect stateRef' port = do
  147. -- create a temporary listener ref that will only be held until the sidebar has sent its current window
  148. listenerRef <- Ref.new Nothing
  149. initialListener <-
  150. Runtime.onMessageJsonAddListener port $ windowListener
  151. $ onNewWindowId port stateRef' listenerRef
  152. -- XXX: is it possible a message arrive *before* this is executed ?
  153. -- theoretically yes, and this means this way of doing is unsafe, but it's
  154. -- difficult for a handler to remove itself otherwise.
  155. Ref.write (Just initialListener) listenerRef
  156. where
  157. windowListener :: (WindowId -> Effect Unit) -> SidebarEvent -> Effect Unit
  158. windowListener callback msg = case msg of
  159. SbHasWindowId winId -> log ("bg: created winId " <> show winId) *> callback winId
  160. _ -> pure unit
  161. -- | Initialize the data and the listeners of a new window, and send the current window state.
  162. onNewWindowId ::
  163. forall a.
  164. Runtime.Port ->
  165. (Ref.Ref GlobalState) ->
  166. (Ref.Ref (Maybe (Listener a))) ->
  167. WindowId -> Effect Unit
  168. onNewWindowId port stateRef listenerRef winId = do
  169. -- initial state of the current window
  170. r <- initWindowState port stateRef winId
  171. -- remove the previous onMessage listener
  172. ogListener <- Ref.read listenerRef
  173. foldMap (\l -> Runtime.onMessageRemoveListener port l) ogListener
  174. Ref.write Nothing listenerRef
  175. -- send initial tabs
  176. maybe (pure unit)
  177. ( \w ->
  178. Runtime.postMessageJson port
  179. $ BgInitialTabList
  180. $ fromFoldable
  181. $ w.positions
  182. <#> (flip M.lookup w.tabs)
  183. # catMaybes
  184. )
  185. (M.lookup winId r.windows)
  186. -- add the new onMessage listener
  187. sidebarListener <- Runtime.onMessageJsonAddListener port $ manageSidebar stateRef port
  188. onDisconnectListener <- mkListenerUnit $ onDisconnect stateRef winId sidebarListener
  189. Runtime.portOnDisconnect port onDisconnectListener
  190. -- | Set the port of a new window connecting. If the window doesn't exist, initialize a new data
  191. initWindowState :: Runtime.Port -> (Ref.Ref GlobalState) -> WindowId -> Effect GlobalState
  192. initWindowState port ref winId =
  193. (flip Ref.modify) ref
  194. $ over (_windows <<< (at winId))
  195. ( case _ of
  196. Nothing -> Just $ { tabs: M.empty, port: Just port, positions: empty }
  197. Just win -> Just $ set _port (Just port) win
  198. )
  199. -- TODO don't pass the full ref, but only a set of function to manipulate/access
  200. -- the data required
  201. manageSidebar :: (Ref.Ref GlobalState) -> Runtime.Port -> SidebarEvent -> Effect Unit
  202. manageSidebar stateRef port (SbTabDeleted tabId) = launchAff_ $ removeOne tabId
  203. manageSidebar stateRef port (SbTabActived tabId) = launchAff_ $ activateTab tabId
  204. manageSidebar stateRef port msg = pure unit
  205. onDisconnect :: forall a. (Ref.Ref GlobalState) -> WindowId -> Listener a -> Effect Unit
  206. onDisconnect stateRef winId listener = Ref.modify_ (set (_windows <<< (at winId) <<< _Just <<< _port) Nothing) stateRef