Background.purs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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)
  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 (map, (=<<), (>>=))
  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(..), fromMaybe, 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 (sequence, traverse_)
  33. import Data.Tuple (Tuple(..))
  34. import Data.Unit (unit)
  35. import Effect (Effect)
  36. import Effect.Aff (Aff, launchAff_)
  37. import Effect.Class (liftEffect)
  38. import Effect.Console (log)
  39. import Effect.Ref as Ref
  40. import Prelude (Unit, bind, discard, pure, ($), (<$>), (<<<))
  41. import PureTabs.Browser.Sessions (getTabValue, removeTabValue, setTabValue)
  42. import PureTabs.Model.BackgroundEvent (BackgroundEvent(..))
  43. import PureTabs.Model.GlobalState as GS
  44. import PureTabs.Model.Group (GroupId(..))
  45. import PureTabs.Model.GroupMapping (GroupData, createGroup, deleteGroup, moveGroup, renameGroup, retrieveGroups, updateGroupsMapping)
  46. import PureTabs.Model.SidebarEvent (SidebarEvent(..))
  47. import PureTabs.Model.TabWithGroup (TabWithGroup(..))
  48. type Ports
  49. = Ref.Ref (List Runtime.Port)
  50. type StateRef = Ref.Ref GS.GlobalState
  51. main :: Effect Unit
  52. main = do
  53. log "[bg] starting"
  54. launchAff_ do
  55. allTabs <- BT.browserQuery {}
  56. groups <- M.fromFoldable <$> setWindowsGroups allTabs
  57. setTabsGroups groups allTabs
  58. liftEffect $ log "[bg] done initializing groups"
  59. liftEffect $ initializeBackground =<< (Ref.new $ GS.initialTabsToGlobalState allTabs)
  60. where
  61. -- | For each window found, set a default group if it doesn't exist
  62. setWindowsGroups :: Array Tab -> Aff (Array (Tuple WindowId (Array GroupData)))
  63. setWindowsGroups tabs = sequence $ tabs #
  64. map (unwrap >>> _.windowId)
  65. >>> Set.fromFoldable
  66. >>> A.fromFoldable
  67. -- Retrieve the groups for each existing window, and if they don't exist, create a group
  68. >>> map \winId -> retrieveGroups winId >>=
  69. case _ of
  70. [] -> updateGroupsMapping winId (createGroup (GroupId 0) "main")
  71. *> retrieveGroups winId >>= \groups' -> pure $ Tuple winId groups'
  72. groups' -> pure $ Tuple winId groups'
  73. -- | For each tab, set a default tab if it doesn't exist
  74. setTabsGroups :: M.Map WindowId (Array GroupData) -> Array Tab -> Aff Unit
  75. setTabsGroups winToGroups tabs =
  76. let
  77. defaultGroupIdPerWin = winToGroups # map (A.head >>> maybe (GroupId 0) (unwrap >>> _.groupId))
  78. defaultGroup winId = fromMaybe (GroupId 0) $ M.lookup winId defaultGroupIdPerWin
  79. in
  80. tabs # traverse_ \(Tab t) -> (getTabValue t.id "groupId" :: Aff (Maybe GroupId)) >>=
  81. case _ of
  82. Nothing -> setTabValue t.id "groupId" $ defaultGroup t.windowId
  83. _ -> pure unit
  84. initializeBackground :: Ref.Ref GS.GlobalState -> Effect Unit
  85. initializeBackground ref = do
  86. (mkListenerOne $ onConnect ref) >>= Runtime.onConnectAddListener
  87. (mkListenerOne $ onWindowCreated ref) >>= WinOnCreated.addListener
  88. (mkListenerOne $ onWindowRemoved ref) >>= WinOnRemoved.addListener
  89. onTabCreated ref # OnCreated.addListener
  90. (mkListenerTwo $ onTabDeleted ref) >>= OnRemoved.addListener
  91. onTabActived ref # OnActivated.addListener
  92. onTabUpdated ref # OnUpdated.addListener
  93. (mkListenerTwo $ onTabDetached ref) >>= OnDetached.addListener
  94. (mkListenerTwo $ onTabAttached ref) >>= OnAttached.addListener
  95. (mkListenerTwo $ onTabMoved ref) >>= OnMoved.addListener
  96. onWindowCreated :: StateRef -> Window -> Effect Unit
  97. onWindowCreated ref { id: winId } = do
  98. log $ "bg: created window " <> (show winId)
  99. ref # Ref.modify_ (GS.addEmptyWindow winId)
  100. onWindowRemoved :: StateRef -> WindowId -> Effect Unit
  101. onWindowRemoved ref winId = do
  102. log $ "bg: deleted window " <> (show winId)
  103. ref # Ref.modify_ (GS.deleteWindow winId)
  104. onTabCreated :: StateRef -> Tab -> Effect Unit
  105. onTabCreated stateRef tab = do
  106. log $ "bg: created tab " <> (BT.showTabId tab)
  107. state <- Ref.modify (GS.createTab tab) stateRef
  108. GS.sendToTabPort tab state $ BgTabCreated tab
  109. onTabUpdated :: StateRef -> TabId -> OnUpdated.ChangeInfo -> Tab -> Effect Unit
  110. onTabUpdated stateRef tid cinfo tab = do
  111. log $ "bg: updated tab " <> show tid
  112. state <- Ref.modify (GS.updateTab tab) stateRef
  113. GS.sendToTabPort tab state $ BgTabUpdated tid cinfo tab
  114. onTabMoved :: StateRef -> TabId -> OnMoved.MoveInfo -> Effect Unit
  115. onTabMoved ref tid minfo = do
  116. log $ "bg: moved tab " <> show tid
  117. state <- Ref.modify (GS.moveTab minfo.fromIndex minfo.toIndex minfo.windowId) ref
  118. GS.sendToWindowPort minfo.windowId state $ BgTabMoved tid minfo.fromIndex minfo.toIndex
  119. onTabActived :: StateRef -> OnActivated.ActiveInfo -> Effect Unit
  120. onTabActived stateRef (OnActivated.ActiveInfo aInfo) = do
  121. log $ "bg: activated tab " <> show aInfo.tabId
  122. state <- Ref.modify (GS.activateTab aInfo.windowId aInfo.previousTabId aInfo.tabId) stateRef
  123. GS.sendToWindowPort aInfo.windowId state $ BgTabActivated aInfo.previousTabId aInfo.tabId
  124. onTabDeleted :: StateRef -> TabId -> OnRemoved.RemoveInfo -> Effect Unit
  125. onTabDeleted stateRef tabId info = do
  126. log $ "bg: deleted tab " <> show tabId
  127. state <- Ref.modify (GS.deleteTab info.windowId tabId) stateRef
  128. GS.sendToWindowPort info.windowId state $ BgTabDeleted tabId
  129. onTabDetached :: StateRef -> TabId -> OnDetached.DetachInfo -> Effect Unit
  130. onTabDetached stateRef tabId info = do
  131. log $ "bg: detached tab " <> show tabId
  132. state <- Ref.modify (GS.detachTab info.oldWindowId tabId) stateRef
  133. GS.sendToWindowPort info.oldWindowId state $ BgTabDetached tabId
  134. onTabAttached :: StateRef -> TabId -> OnAttached.AttachInfo -> Effect Unit
  135. onTabAttached stateRef tid info = do
  136. log $ "bg: attached tab " <> show tid
  137. state <- Ref.modify (GS.attachTab info.newWindowId tid info.newPosition) stateRef
  138. case GS.tabFromWinIdAndTabId info.newWindowId tid state of
  139. Just newTab -> GS.sendToWindowPort info.newWindowId state $ BgTabAttached newTab
  140. Nothing -> pure unit
  141. onConnect :: StateRef -> Runtime.Port -> Effect Unit
  142. onConnect stateRef port = do
  143. -- Create a temporary listener ref that will only be held until the sidebar has sent its current window
  144. listenerRef <- Ref.new Nothing
  145. initialListener <-
  146. Runtime.onMessageJsonAddListener port $ windowListener
  147. $ onNewWindowId port stateRef listenerRef
  148. -- XXX: Is it possible a message arrives *before* this is executed ?
  149. -- Theoretically yes, and this means this way of doing is unsafe, but it's
  150. -- difficult for a handler to remove itself otherwise.
  151. Ref.write (Just initialListener) listenerRef
  152. where
  153. windowListener :: (WindowId -> Effect Unit) -> SidebarEvent -> Effect Unit
  154. windowListener callback msg = case msg of
  155. SbHasWindowId winId -> log ("bg: created winId " <> show winId) *> callback winId
  156. _ -> pure unit
  157. -- | Initialize the data and the listeners of a new window, and send the current window state.
  158. onNewWindowId
  159. :: forall a.
  160. Runtime.Port
  161. -> StateRef
  162. -> (Ref.Ref (Maybe (Listener a)))
  163. -> WindowId
  164. -> Effect Unit
  165. onNewWindowId port stateRef listenerRef winId = do
  166. -- Initial state of the current window
  167. Ref.modify_ (GS.initializeWindowState winId port) stateRef
  168. -- Remove the previous onMessage listener
  169. ogListener <- Ref.read listenerRef
  170. foldMap (\l -> Runtime.onMessageRemoveListener port l) ogListener
  171. Ref.write Nothing listenerRef
  172. -- Send initial tabs
  173. latestState <- Ref.read stateRef
  174. M.lookup winId latestState.windows # foldMap \w ->
  175. let
  176. tabs = A.fromFoldable
  177. $ w.positions
  178. <#> (flip M.lookup w.tabs)
  179. # A.catMaybes
  180. tabsWithGid =
  181. tabs <#> \tab@(Tab t)->
  182. getTabValue t.id "groupId" <#> \gid -> TabWithGroup tab gid
  183. in
  184. launchAff_ do
  185. tabsWithGroup <- sequence tabsWithGid
  186. groups <- retrieveGroups winId
  187. liftEffect $ Runtime.postMessageJson port $ BgInitialTabList groups tabsWithGroup
  188. -- Add the new onMessage listener
  189. sidebarListener <- Runtime.onMessageJsonAddListener port $ manageSidebar stateRef winId port
  190. onDisconnectListener <- mkListenerUnit $ onDisconnect stateRef winId sidebarListener
  191. Runtime.portOnDisconnect port onDisconnectListener
  192. manageSidebar :: StateRef -> WindowId -> Runtime.Port -> SidebarEvent -> Effect Unit
  193. manageSidebar ref winId port = case _ of
  194. SbDeleteTab tabId -> launchAff_ $ BT.browserRemoveOne tabId
  195. SbActivateTab tabId -> launchAff_ $ BT.browserActivateTab tabId
  196. SbMoveTab tabId newIndex -> BT.browserMoveTab tabId { index: newIndex }
  197. SbCreateTab tid' -> case tid' of
  198. Nothing -> BT.browserCreateTab { windowId: winId }
  199. Just tid ->
  200. Ref.read ref <#> view (GS._positions >>> GS._windowIdToWindow winId)
  201. >>= \positions -> case A.elemIndex tid positions of
  202. Nothing -> BT.browserCreateTab { windowId: winId }
  203. Just idx -> BT.browserCreateTab { windowId: winId, index: idx + 1 }
  204. SbSelectedGroup tabIds -> do
  205. state <- Ref.read ref
  206. let
  207. allTabIds = M.keys $ view ((GS._windowIdToWindow winId) <<< GS._tabs) state
  208. tabIdsToHide = A.fromFoldable $ Set.difference allTabIds (Set.fromFoldable tabIds)
  209. BT.browserHideTabs tabIdsToHide
  210. BT.browserShowTabs tabIds
  211. SbDeletedGroup gid tabIds -> launchAff_ do
  212. BT.browserRemove tabIds
  213. activeTab <- BT.browserQuery { windowId: unwrap winId, active: true }
  214. let activeTabId = activeTab # A.head >>> (<$>) (unwrap >>> _.id)
  215. liftEffect $ Runtime.postMessageJson port $ BgGroupDeleted gid activeTabId
  216. updateGroupsMapping winId $ deleteGroup gid
  217. SbChangeTabGroup tid Nothing -> launchAff_ $ removeTabValue tid "groupId"
  218. SbChangeTabGroup tid (Just gid) -> launchAff_ $ setTabValue tid "groupId" gid
  219. SbCreatedGroup gid name -> launchAff_ $ updateGroupsMapping winId $ createGroup gid name
  220. SbRenamedGroup gid name -> launchAff_ $ updateGroupsMapping winId $ renameGroup gid name
  221. SbMovedGroup gid pos -> launchAff_ $ updateGroupsMapping winId $ moveGroup gid pos
  222. SbDetacheTab -> pure unit
  223. SbHasWindowId winId' -> pure unit
  224. onDisconnect :: forall a. StateRef -> WindowId -> Listener a -> Effect Unit
  225. onDisconnect stateRef winId listener = Ref.modify_ (set (GS._windows <<< (at winId) <<< _Just <<< GS._port) Nothing) stateRef