Background.purs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. module PureTabs.Background where
  2. import Browser.Runtime as Runtime
  3. import Browser.Tabs (Tab(..), TabId, WindowId)
  4. import Browser.Tabs as BT
  5. import Browser.Tabs.OnActivated as OnActivated
  6. import Browser.Tabs.OnAttached as OnAttached
  7. import Browser.Tabs.OnCreated as OnCreated
  8. import Browser.Tabs.OnDetached as OnDetached
  9. import Browser.Tabs.OnMoved as OnMoved
  10. import Browser.Tabs.OnRemoved as OnRemoved
  11. import Browser.Tabs.OnUpdated as OnUpdated
  12. import Browser.Utils (Listener, mkListenerOne, mkListenerTwo, mkListenerUnit, unsafeLog)
  13. import Browser.Windows (Window)
  14. import Browser.Windows.OnCreated as WinOnCreated
  15. import Browser.Windows.OnRemoved as WinOnRemoved
  16. import Control.Alt ((<#>))
  17. import Control.Alternative ((*>))
  18. import Control.Bind ((=<<), (>>=))
  19. import Control.Category ((>>>))
  20. import Data.Array as A
  21. import Data.CommutativeRing ((+))
  22. import Data.Function (flip, (#))
  23. import Data.Lens (_Just, set, view)
  24. import Data.Lens.At (at)
  25. import Data.List (List, foldMap)
  26. import Data.Map as M
  27. import Data.Maybe (Maybe(..), maybe)
  28. import Data.Monoid ((<>))
  29. import Data.Newtype (unwrap)
  30. import Data.Set as Set
  31. import Data.Show (show)
  32. import Data.Traversable (traverse)
  33. import Data.Unit (unit)
  34. import Effect (Effect)
  35. import Effect.Aff (Aff, launchAff_)
  36. import Effect.Class (liftEffect)
  37. import Effect.Console (log)
  38. import Effect.Ref as Ref
  39. import Prelude (Unit, bind, discard, pure, ($), (<$>), (<<<))
  40. import PureTabs.Browser.Sessions (getTabValue, removeTabValue, setTabValue)
  41. import PureTabs.Model.BackgroundEvent (BackgroundEvent(..))
  42. import PureTabs.Model.GlobalState as GS
  43. import PureTabs.Model.Group (GroupId(..))
  44. import PureTabs.Model.GroupMapping (GroupData, createGroup, deleteGroup, moveGroup, renameGroup, retrieveGroups, updateGroupsMapping)
  45. import PureTabs.Model.SidebarEvent (SidebarEvent(..))
  46. import PureTabs.Model.TabWithGroup (TabWithGroup(..))
  47. type Ports
  48. = Ref.Ref (List Runtime.Port)
  49. type StateRef = Ref.Ref GS.GlobalState
  50. main :: Effect Unit
  51. main = do
  52. log "[bg] starting"
  53. launchAff_ do
  54. allTabs <- BT.browserQuery {}
  55. liftEffect $ initializeBackground =<< (Ref.new $ GS.initialTabsToGlobalState allTabs)
  56. initializeBackground :: Ref.Ref GS.GlobalState -> Effect Unit
  57. initializeBackground ref = do
  58. (mkListenerOne $ onConnect ref) >>= Runtime.onConnectAddListener
  59. (mkListenerOne $ onWindowCreated ref) >>= WinOnCreated.addListener
  60. (mkListenerOne $ onWindowRemoved ref) >>= WinOnRemoved.addListener
  61. onTabCreated ref # OnCreated.addListener
  62. (mkListenerTwo $ onTabDeleted ref) >>= OnRemoved.addListener
  63. onTabActived ref # OnActivated.addListener
  64. onTabUpdated ref # OnUpdated.addListener
  65. (mkListenerTwo $ onTabDetached ref) >>= OnDetached.addListener
  66. (mkListenerTwo $ onTabAttached ref) >>= OnAttached.addListener
  67. (mkListenerTwo $ onTabMoved ref) >>= OnMoved.addListener
  68. onWindowCreated :: StateRef -> Window -> Effect Unit
  69. onWindowCreated ref { id: winId } = do
  70. log $ "bg: created window " <> (show winId)
  71. ref # Ref.modify_ (GS.addEmptyWindow winId)
  72. onWindowRemoved :: StateRef -> WindowId -> Effect Unit
  73. onWindowRemoved ref winId = do
  74. log $ "bg: deleted window " <> (show winId)
  75. ref # Ref.modify_ (GS.deleteWindow winId)
  76. onTabCreated :: StateRef -> Tab -> Effect Unit
  77. onTabCreated stateRef tab = do
  78. log $ "bg: created tab " <> (BT.showTabId tab)
  79. state <- Ref.modify (GS.createTab tab) stateRef
  80. let Tab(t) = tab
  81. -- Attempt to detect session restore.
  82. -- If the tab we're opening already has a `groupId` value, it is either a
  83. -- restored tab from the current session, or a restored tab from a full
  84. -- session restore. If we found groups associated with the tab's window, we
  85. -- ask the sidebar to initiliaze them.
  86. --
  87. -- This solution ignores one use case (for which it will probably be buggy):
  88. -- opening a session on top of an already existing session. If the user
  89. -- starts creating groups, opening tab, and then restore a session, then it
  90. -- will probably break.
  91. -- An other issue with this solution is that the action is triggered in a
  92. -- fiber, delaying its action, and in particular allowing the tab creation
  93. -- event to be sent later than e.g. the tab activation event to the sidebar.
  94. -- A possible fix could be send the TabCreated event just as before, and then
  95. -- launch a fiber to ask the sidebar to switch the tab's group (and
  96. -- initialize the groups) in case it's needed.
  97. launchAff_ $
  98. (getTabValue t.id "groupId" :: Aff (Maybe GroupId)) >>=
  99. case _ of
  100. Nothing -> liftEffect $ GS.sendToTabPort tab state $ BgTabCreated tab Nothing
  101. Just gid -> do
  102. retrieveGroups t.windowId >>=
  103. case _ of
  104. [] -> pure unit
  105. groups -> liftEffect $ GS.sendToTabPort tab state $ BgInitializeGroups groups
  106. liftEffect $ GS.sendToTabPort tab state $ BgTabCreated tab (Just gid)
  107. onTabUpdated :: StateRef -> TabId -> OnUpdated.ChangeInfo -> Tab -> Effect Unit
  108. onTabUpdated stateRef tid cinfo tab = do
  109. log $ "bg: updated tab " <> show tid
  110. state <- Ref.modify (GS.updateTab tab) stateRef
  111. GS.sendToTabPort tab state $ BgTabUpdated tid cinfo tab
  112. onTabMoved :: StateRef -> TabId -> OnMoved.MoveInfo -> Effect Unit
  113. onTabMoved ref tid minfo = do
  114. log $ "bg: moved tab " <> show tid
  115. state <- Ref.modify (GS.moveTab minfo.fromIndex minfo.toIndex minfo.windowId) ref
  116. GS.sendToWindowPort minfo.windowId state $ BgTabMoved tid minfo.fromIndex minfo.toIndex
  117. onTabActived :: StateRef -> OnActivated.ActiveInfo -> Effect Unit
  118. onTabActived stateRef (OnActivated.ActiveInfo aInfo) = do
  119. log $ "bg: activated tab " <> show aInfo.tabId
  120. state <- Ref.modify (GS.activateTab aInfo.windowId aInfo.previousTabId aInfo.tabId) stateRef
  121. GS.sendToWindowPort aInfo.windowId state $ BgTabActivated aInfo.previousTabId aInfo.tabId
  122. onTabDeleted :: StateRef -> TabId -> OnRemoved.RemoveInfo -> Effect Unit
  123. onTabDeleted stateRef tabId info = do
  124. log $ "bg: deleted tab " <> show tabId
  125. state <- Ref.modify (GS.deleteTab info.windowId tabId) stateRef
  126. GS.sendToWindowPort info.windowId state $ BgTabDeleted tabId
  127. onTabDetached :: StateRef -> TabId -> OnDetached.DetachInfo -> Effect Unit
  128. onTabDetached stateRef tabId info = do
  129. log $ "bg: detached tab " <> show tabId
  130. state <- Ref.modify (GS.detachTab info.oldWindowId tabId) stateRef
  131. GS.sendToWindowPort info.oldWindowId state $ BgTabDetached tabId
  132. onTabAttached :: StateRef -> TabId -> OnAttached.AttachInfo -> Effect Unit
  133. onTabAttached stateRef tid info = do
  134. log $ "bg: attached tab " <> show tid
  135. state <- Ref.modify (GS.attachTab info.newWindowId tid info.newPosition) stateRef
  136. case GS.tabFromWinIdAndTabId info.newWindowId tid state of
  137. Just newTab -> GS.sendToWindowPort info.newWindowId state $ BgTabAttached newTab
  138. Nothing -> pure unit
  139. onConnect :: StateRef -> Runtime.Port -> Effect Unit
  140. onConnect stateRef port = do
  141. log "[bg] connection received"
  142. -- Create a temporary listener ref that will only be held until the sidebar has sent its current window
  143. listenerRef <- Ref.new Nothing
  144. initialListener <-
  145. Runtime.onMessageJsonAddListener port $ windowListener
  146. $ onNewWindowId port stateRef listenerRef
  147. -- XXX: Is it possible a message arrives *before* this is executed ?
  148. -- Theoretically yes, and this means this way of doing is unsafe, but it's
  149. -- difficult for a handler to remove itself otherwise.
  150. Ref.write (Just initialListener) listenerRef
  151. where
  152. windowListener :: (WindowId -> Effect Unit) -> SidebarEvent -> Effect Unit
  153. windowListener callback msg = case msg of
  154. SbHasWindowId winId -> log ("[bg] created winId " <> show winId) *> callback winId
  155. _ -> pure unit
  156. -- | Initialize the data and the listeners of a new window, and send the current window state.
  157. onNewWindowId
  158. :: forall a.
  159. Runtime.Port
  160. -> StateRef
  161. -> (Ref.Ref (Maybe (Listener a)))
  162. -> WindowId
  163. -> Effect Unit
  164. onNewWindowId port stateRef listenerRef winId = do
  165. -- Initial state of the current window
  166. Ref.modify_ (GS.initializeWindowState winId port) stateRef
  167. -- Remove the previous onMessage listener
  168. ogListener <- Ref.read listenerRef
  169. foldMap (\l -> Runtime.onMessageRemoveListener port l) ogListener
  170. Ref.write Nothing listenerRef
  171. -- Send initial tabs
  172. latestState <- Ref.read stateRef
  173. M.lookup winId latestState.windows # foldMap \w ->
  174. let
  175. tabs = A.fromFoldable
  176. $ w.positions
  177. <#> (flip M.lookup w.tabs)
  178. # A.catMaybes
  179. in
  180. launchAff_ do
  181. groups <- initialWindowGroups
  182. tabsWithGroup <- initialTabsGroups tabs groups
  183. liftEffect $ Runtime.postMessageJson port $ BgInitialTabList groups tabsWithGroup
  184. -- Add the new onMessage listener
  185. sidebarListener <- Runtime.onMessageJsonAddListener port $ manageSidebar stateRef winId port
  186. onDisconnectListener <- mkListenerUnit $ onDisconnect stateRef winId sidebarListener
  187. Runtime.portOnDisconnect port onDisconnectListener
  188. where
  189. -- | Set a default group if none exist.
  190. initialWindowGroups :: Aff (Array GroupData)
  191. initialWindowGroups =
  192. retrieveGroups winId >>=
  193. case _ of
  194. [] -> updateGroupsMapping winId (createGroup (GroupId 0) "main")
  195. *> retrieveGroups winId >>= \groups' -> pure groups'
  196. groups' -> pure groups'
  197. -- | For each tab, set a default tab if it doesn't exist
  198. initialTabsGroups :: Array Tab -> Array GroupData -> Aff (Array TabWithGroup)
  199. initialTabsGroups tabs groups =
  200. let
  201. defaultGroup = groups # (A.head >>> maybe (GroupId 0) (unwrap >>> _.groupId))
  202. in
  203. tabs # traverse \tab@(Tab t) -> (getTabValue t.id "groupId" :: Aff (Maybe GroupId)) >>=
  204. case _ of
  205. Nothing -> setTabValue t.id "groupId" defaultGroup *> pure (TabWithGroup tab defaultGroup)
  206. Just gid -> pure $ TabWithGroup tab gid
  207. manageSidebar :: StateRef -> WindowId -> Runtime.Port -> SidebarEvent -> Effect Unit
  208. manageSidebar ref winId port = case _ of
  209. SbDeleteTab tabId -> launchAff_ $ BT.browserRemoveOne tabId
  210. SbActivateTab tabId -> launchAff_ $ BT.browserActivateTab tabId
  211. SbMoveTab tabId newIndex -> BT.browserMoveTab tabId { index: newIndex }
  212. SbCreateTab tid' -> case tid' of
  213. Nothing -> BT.browserCreateTab { windowId: winId }
  214. Just tid ->
  215. Ref.read ref <#> view (GS._positions >>> GS._windowIdToWindow winId)
  216. >>= \positions -> case A.elemIndex tid positions of
  217. Nothing -> BT.browserCreateTab { windowId: winId }
  218. Just idx -> BT.browserCreateTab { windowId: winId, index: idx + 1 }
  219. SbSelectedGroup tabIds -> do
  220. state <- Ref.read ref
  221. let
  222. allTabIds = M.keys $ view ((GS._windowIdToWindow winId) <<< GS._tabs) state
  223. tabIdsToHide = A.fromFoldable $ Set.difference allTabIds (Set.fromFoldable tabIds)
  224. BT.browserHideTabs tabIdsToHide
  225. BT.browserShowTabs tabIds
  226. SbDeletedGroup gid tabIds -> launchAff_ do
  227. BT.browserRemove tabIds
  228. activeTab <- BT.browserQuery { windowId: unwrap winId, active: true }
  229. let activeTabId = activeTab # A.head >>> (<$>) (unwrap >>> _.id)
  230. liftEffect $ Runtime.postMessageJson port $ BgGroupDeleted gid activeTabId
  231. updateGroupsMapping winId $ deleteGroup gid
  232. SbChangeTabGroup tid Nothing -> launchAff_ $ removeTabValue tid "groupId"
  233. SbChangeTabGroup tid (Just gid) -> launchAff_ $ setTabValue tid "groupId" gid
  234. SbCreatedGroup gid name -> launchAff_ $ updateGroupsMapping winId $ createGroup gid name
  235. SbRenamedGroup gid name -> launchAff_ $ updateGroupsMapping winId $ renameGroup gid name
  236. SbMovedGroup gid pos -> launchAff_ $ updateGroupsMapping winId $ moveGroup gid pos
  237. SbDetacheTab -> pure unit
  238. SbHasWindowId winId' -> pure unit
  239. onDisconnect :: forall a. StateRef -> WindowId -> Listener a -> Effect Unit
  240. onDisconnect stateRef winId listener = Ref.modify_ (set (GS._windows <<< (at winId) <<< _Just <<< GS._port) Nothing) stateRef