Background.purs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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.OnAttached as OnAttached
  6. import Browser.Tabs.OnCreated as OnCreated
  7. import Browser.Tabs.OnDetached as OnDetached
  8. import Browser.Tabs.OnMoved as OnMoved
  9. import Browser.Tabs.OnRemoved as OnRemoved
  10. import Browser.Tabs.OnUpdated as OnUpdated
  11. import Browser.Utils (Listener, mkListenerOne, mkListenerTwo, mkListenerUnit)
  12. import Browser.Windows (Window)
  13. import Browser.Windows.OnCreated as WinOnCreated
  14. import Browser.Windows.OnRemoved as WinOnRemoved
  15. import Control.Alt (map, (<#>), (<$>), (<|>))
  16. import Control.Alternative (empty, pure, (*>))
  17. import Control.Bind ((=<<), (>>=))
  18. import Control.Category (identity, (>>>))
  19. import Data.Array (catMaybes, deleteAt, filter, foldl, fromFoldable, insertAt, mapWithIndex, (!!))
  20. import Data.Eq ((/=), (==))
  21. import Data.Foldable (for_)
  22. import Data.Function (const, flip, (#))
  23. import Data.Lens (_Just, over, preview, set, view)
  24. import Data.Lens.At (at)
  25. import Data.Lens.Iso.Newtype (_Newtype)
  26. import Data.List (List, foldMap, foldr)
  27. import Data.Map as M
  28. import Data.Maybe (Maybe(..), maybe, maybe')
  29. import Data.Monoid ((<>))
  30. import Data.Newtype (unwrap)
  31. import Data.Show (show)
  32. import Data.Unit (unit)
  33. import Debug.Trace (traceM)
  34. import Effect (Effect)
  35. import Effect.Aff (Aff, launchAff_)
  36. import Effect.Class (liftEffect)
  37. import Effect.Console (log)
  38. import Effect.Exception (throw)
  39. import Effect.Exception.Unsafe (unsafeThrow)
  40. import Effect.Ref as Ref
  41. import Prelude (Unit, bind, ($), discard, (<<<))
  42. import PureTabs.Model (BackgroundEvent(..), ExtWindow, GlobalState, SidebarEvent(..), _active, _index, _port, _portFromWindow, _portFromWindowId, _positions, _tabFromTabIdAndWindow, _tabFromWindow, _tabs, _windowIdToWindow, _windows, _windowIdToTabIdToTab, emptyWindow, tabsToGlobalState)
  43. type Ports
  44. = Ref.Ref (List Runtime.Port)
  45. main :: Effect Unit
  46. main = do
  47. log "starting background"
  48. launchAff_ runMain
  49. where
  50. runMain :: Aff Unit
  51. runMain = do
  52. allTabs <- query
  53. liftEffect $ initializeBackground =<< (Ref.new $ tabsToGlobalState allTabs)
  54. initializeBackground :: Ref.Ref GlobalState -> Effect Unit
  55. initializeBackground ref = do
  56. (mkListenerOne $ onWindowCreated ref) >>= WinOnCreated.addListener
  57. (mkListenerOne $ onWindowRemoved ref) >>= WinOnRemoved.addListener
  58. onTabCreated ref # OnCreated.addListener
  59. (mkListenerTwo $ onTabDeleted ref) >>= OnRemoved.addListener
  60. onTabActived ref # OnActivated.addListener
  61. onTabUpdated ref # OnUpdated.addListener
  62. (mkListenerTwo $ onTabDetached ref) >>= OnDetached.addListener
  63. (mkListenerTwo $ onTabAttached ref) >>= OnAttached.addListener
  64. (mkListenerTwo $ onTabMoved ref) >>= OnMoved.addListener
  65. (mkListenerOne $ onConnect ref) >>= Runtime.onConnectAddListener
  66. onWindowCreated :: (Ref.Ref GlobalState) -> Window -> Effect Unit
  67. onWindowCreated ref { id: winId } =
  68. (log $ "bg: created window " <> (show winId))
  69. *> (ref # Ref.modify_ (over (_windows <<< at winId) (_ <|> (Just emptyWindow))))
  70. onWindowRemoved :: (Ref.Ref GlobalState) -> WindowId -> Effect Unit
  71. onWindowRemoved ref winId =
  72. (log $ "bg: deleted window " <> (show winId))
  73. *> (ref # Ref.modify_ \s -> s { windows = M.delete winId s.windows })
  74. onTabCreated :: (Ref.Ref GlobalState) -> Tab -> Effect Unit
  75. onTabCreated stateRef (Tab tab) = do
  76. log $ "bg: created tab " <> show tab.id
  77. state <-
  78. Ref.modify (insertTab (Tab tab)) stateRef
  79. case (preview (_portFromWindow (Tab tab)) state) of
  80. Nothing -> pure unit
  81. Just port -> Runtime.postMessageJson port $ BgTabCreated (Tab tab)
  82. where
  83. -- | insert a tab, creating the window and updating the position
  84. insertTab :: Tab -> GlobalState -> GlobalState
  85. insertTab (Tab t) s =
  86. let
  87. windows = case M.lookup t.windowId s.windows of
  88. Nothing -> M.insert t.windowId emptyWindow s.windows
  89. Just _ -> s.windows
  90. in
  91. s { windows = M.update updateWindow t.windowId windows }
  92. where
  93. updateWindow :: ExtWindow -> Maybe ExtWindow
  94. updateWindow win =
  95. -- this will delete the window if there is an issue with the position..
  96. -- not the best solution but we can't really recover from it anyway.
  97. (insertAt t.index t.id win.positions)
  98. <#> \newPos ->
  99. win
  100. { positions = newPos
  101. , tabs = M.insert t.id (Tab t) win.tabs
  102. }
  103. onTabUpdated :: (Ref.Ref GlobalState) -> TabId -> OnUpdated.ChangeInfo -> Tab -> Effect Unit
  104. onTabUpdated stateRef tid cinfo tab' = do
  105. log $ "bg: updated tab " <> show tid
  106. state <- Ref.modify (updateTab tab') stateRef
  107. case (preview (_portFromWindow tab') state) of
  108. Nothing -> pure unit
  109. Just port -> Runtime.postMessageJson port $ BgTabUpdated tid cinfo tab'
  110. where
  111. updateTab :: Tab -> GlobalState -> GlobalState
  112. updateTab (Tab t) =
  113. -- update by replacing the tab only if it already exists
  114. (over (_tabFromWindow (Tab t)) (map $ const (Tab t)))
  115. -- or update the currently detached tab
  116. >>> ( \s -> case s.detached of
  117. Just (Tab t')
  118. | t.id == t'.id -> s { detached = Just (Tab t') }
  119. _ -> s
  120. )
  121. onTabMoved :: (Ref.Ref GlobalState) -> TabId -> OnMoved.MoveInfo -> Effect Unit
  122. onTabMoved ref tid minfo = do
  123. log $ "bg: moved tab " <> show tid
  124. s <- Ref.modify (updateState minfo) ref
  125. case (preview (_portFromWindowId minfo.windowId) s) of
  126. Nothing -> pure unit
  127. Just port -> Runtime.postMessageJson port $ BgTabMoved tid minfo.fromIndex minfo.toIndex
  128. where
  129. updateState :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  130. updateState minfo' state =
  131. let
  132. newState = updatePositions minfo' state
  133. newPositions :: Array TabId
  134. newPositions = view ((_windowIdToWindow minfo'.windowId) <<< _positions) newState
  135. in
  136. over ((_windowIdToWindow minfo'.windowId) <<< _tabs) (updateTabsIndex newPositions) newState
  137. updatePositions :: OnMoved.MoveInfo -> GlobalState -> GlobalState
  138. updatePositions minfo' = over ((_windowIdToWindow minfo'.windowId) <<< _positions) $ unsafeUpdatePositions minfo'
  139. -- | given a move info, update the positions tabs
  140. unsafeUpdatePositions :: OnMoved.MoveInfo -> Array TabId -> Array TabId
  141. unsafeUpdatePositions minfo' =
  142. (moveElement minfo'.fromIndex minfo'.toIndex)
  143. -- the indexes should exist, we need to revisit the code if it doesn't
  144. >>> (maybe' (\_ -> unsafeThrow "invalid indexes") identity)
  145. moveElement :: forall a. Int -> Int -> Array a -> Maybe (Array a)
  146. moveElement from to arr = do
  147. tab <- arr !! from
  148. deleteAt from arr >>= insertAt to tab
  149. -- | update the index of the tab given the positions
  150. updateTabsIndex :: Array TabId -> M.Map TabId Tab -> M.Map TabId Tab
  151. updateTabsIndex positions tabs =
  152. let
  153. modifyFuncs :: Array (M.Map TabId Tab -> M.Map TabId Tab)
  154. modifyFuncs = mapWithIndex (\idx tid' -> set (at tid' <<< _Just <<< _Newtype <<< _index) idx) positions
  155. in
  156. foldl (#) tabs modifyFuncs
  157. onTabActived :: (Ref.Ref GlobalState) -> OnActivated.ActiveInfo -> Effect Unit
  158. onTabActived stateRef (OnActivated.ActiveInfo aInfo) = do
  159. log $ "bg: activated tab " <> show aInfo.tabId
  160. state <- Ref.modify (updateGlobalState aInfo.previousTabId aInfo.tabId) stateRef
  161. case (preview (_portFromWindowId aInfo.windowId) state) of
  162. Nothing -> pure unit
  163. Just port -> Runtime.postMessageJson port $ BgTabActived aInfo.previousTabId aInfo.tabId
  164. where
  165. updateGlobalState :: (Maybe TabId) -> TabId -> GlobalState -> GlobalState
  166. updateGlobalState prev new state =
  167. let
  168. -- TODO: we have the windowId, we can directly get the tab from that
  169. -- without using _tabFromTabIdAndWindow that goes through all the windows.
  170. prevTab = prev >>= _tabFromTabIdAndWindow state
  171. prevTabF :: GlobalState -> GlobalState
  172. prevTabF = maybe identity (\t -> set (_activeTab t) false) prevTab
  173. newTab = _tabFromTabIdAndWindow state new
  174. newTabF :: GlobalState -> GlobalState
  175. newTabF = maybe identity (\t -> set (_activeTab t) true) newTab
  176. _activeTab t = (_tabFromWindow t) <<< _Just <<< _Newtype <<< _active
  177. in
  178. (prevTabF >>> newTabF) state
  179. stateDeleteTab :: WindowId -> TabId -> GlobalState -> GlobalState
  180. stateDeleteTab wid tid =
  181. ( (set (_windowIdToTabIdToTab wid tid) Nothing)
  182. >>> over (_windowIdToWindow wid <<< _positions) (filter ((/=) tid))
  183. )
  184. deleteTab :: (Ref.Ref GlobalState) -> WindowId -> TabId -> Effect Unit
  185. deleteTab stateRef wid tid = do
  186. log $ "bg: deleted tab " <> show tid
  187. state <- Ref.modify (stateDeleteTab wid tid) stateRef
  188. let
  189. port = preview (_portFromWindowId wid) state
  190. maybe (pure unit) (\p -> Runtime.postMessageJson p (BgTabDeleted tid)) port
  191. onTabDeleted :: (Ref.Ref GlobalState) -> TabId -> OnRemoved.RemoveInfo -> Effect Unit
  192. onTabDeleted stateRef tabId info = deleteTab stateRef info.windowId tabId
  193. onTabDetached :: (Ref.Ref GlobalState) -> TabId -> OnDetached.DetachInfo -> Effect Unit
  194. onTabDetached stateRef tabId info = do
  195. log $ "bg: detached tab " <> show tabId
  196. oldState <- Ref.read stateRef
  197. case preview (_windowIdToTabIdToTab info.oldWindowId tabId) oldState of
  198. Just (Just tab) -> do
  199. deleteTab stateRef info.oldWindowId tabId
  200. Ref.modify_ (_ { detached = Just tab }) stateRef
  201. _ -> throw $ "tab " <> (show tabId) <> " not found, shouldn't happen"
  202. onTabAttached :: (Ref.Ref GlobalState) -> TabId -> OnAttached.AttachInfo -> Effect Unit
  203. onTabAttached stateRef tid info = do
  204. log $ "bg: attached tab " <> show tid
  205. state <- Ref.read stateRef
  206. case state.detached of
  207. Just (Tab tab) ->
  208. let
  209. newTab = Tab (tab { windowId = info.newWindowId, index = info.newPosition })
  210. in
  211. onTabCreated stateRef newTab
  212. *> Ref.modify_ (_ { detached = Nothing }) stateRef
  213. _ -> throw $ "tab " <> (show tid) <> " doesn't exist in the state, this shouldn't happen"
  214. onConnect :: (Ref.Ref GlobalState) -> Runtime.Port -> Effect Unit
  215. onConnect stateRef port = do
  216. -- create a temporary listener ref that will only be held until the sidebar has sent its current window
  217. listenerRef <- Ref.new Nothing
  218. initialListener <-
  219. Runtime.onMessageJsonAddListener port $ windowListener
  220. $ onNewWindowId port stateRef listenerRef
  221. -- XXX: is it possible a message arrive *before* this is executed ?
  222. -- theoretically yes, and this means this way of doing is unsafe, but it's
  223. -- difficult for a handler to remove itself otherwise.
  224. Ref.write (Just initialListener) listenerRef
  225. where
  226. windowListener :: (WindowId -> Effect Unit) -> SidebarEvent -> Effect Unit
  227. windowListener callback msg = case msg of
  228. SbHasWindowId winId -> log ("bg: created winId " <> show winId) *> callback winId
  229. _ -> pure unit
  230. -- | Initialize the data and the listeners of a new window, and send the current window state.
  231. onNewWindowId ::
  232. forall a.
  233. Runtime.Port ->
  234. (Ref.Ref GlobalState) ->
  235. (Ref.Ref (Maybe (Listener a))) ->
  236. WindowId -> Effect Unit
  237. onNewWindowId port stateRef listenerRef winId = do
  238. -- initial state of the current window
  239. initWindowState port stateRef winId
  240. -- remove the previous onMessage listener
  241. ogListener <- Ref.read listenerRef
  242. foldMap (\l -> Runtime.onMessageRemoveListener port l) ogListener
  243. Ref.write Nothing listenerRef
  244. -- send initial tabs
  245. latestState <- Ref.read stateRef
  246. maybe (pure unit)
  247. ( \w -> do
  248. Runtime.postMessageJson port
  249. $ BgInitialTabList
  250. $ fromFoldable
  251. $ w.positions
  252. <#> (flip M.lookup w.tabs)
  253. # catMaybes
  254. )
  255. (M.lookup winId latestState.windows)
  256. -- add the new onMessage listener
  257. sidebarListener <- Runtime.onMessageJsonAddListener port $ manageSidebar stateRef port
  258. onDisconnectListener <- mkListenerUnit $ onDisconnect stateRef winId sidebarListener
  259. Runtime.portOnDisconnect port onDisconnectListener
  260. -- | Set the port of a new window connecting. If the window doesn't exist, initialize a new data
  261. initWindowState :: Runtime.Port -> (Ref.Ref GlobalState) -> WindowId -> Effect Unit
  262. initWindowState port ref winId =
  263. (flip Ref.modify_) ref
  264. $ over (_windows <<< (at winId))
  265. ( case _ of
  266. Nothing -> Just $ { tabs: M.empty, port: Just port, positions: empty }
  267. Just win -> Just $ set _port (Just port) win
  268. )
  269. -- TODO don't pass the full ref, but only a set of function to manipulate/access
  270. -- the data required
  271. manageSidebar :: (Ref.Ref GlobalState) -> Runtime.Port -> SidebarEvent -> Effect Unit
  272. manageSidebar stateRef port (SbDeleteTab tabId) = launchAff_ $ removeOne tabId
  273. manageSidebar stateRef port (SbActivateTab tabId) = launchAff_ $ activateTab tabId
  274. manageSidebar stateRef port (SbMoveTab tabId newIndex) = moveTab tabId { index: newIndex }
  275. manageSidebar stateRef port (SbCreateTab winId) = createTab { windowId: winId }
  276. manageSidebar stateRef port msg = pure unit
  277. onDisconnect :: forall a. (Ref.Ref GlobalState) -> WindowId -> Listener a -> Effect Unit
  278. onDisconnect stateRef winId listener = Ref.modify_ (set (_windows <<< (at winId) <<< _Just <<< _port) Nothing) stateRef