GlobalState.purs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. module PureTabs.Model.GlobalState (
  2. ExtWindow
  3. , GlobalState
  4. , _active
  5. , _id
  6. , _index
  7. , _port
  8. , _portFromWindow
  9. , _portFromWindowId
  10. , _positions
  11. , _tabFromTabIdAndWindow
  12. , _tabFromWindow
  13. , _tabId
  14. , _tabs
  15. , _tabWindowId
  16. , _windowIdToWindow
  17. , _windowIdToTabIdToTab
  18. , _windows
  19. , emptyWindow
  20. , initialGlobalState
  21. , initialTabListToGlobalState
  22. , addEmptyWindow
  23. , deleteWindow
  24. , createTab
  25. , updateTab
  26. , activateTab
  27. , moveTab
  28. , deleteTab
  29. , detachTab
  30. , attachTab
  31. , sendToTabPort
  32. , sendToWindowPort
  33. , tabFromWinIdAndTabId
  34. , initializeWindowState
  35. ) where
  36. import Browser.Runtime (Port, postMessageJson)
  37. import Browser.Tabs (Tab(..), TabId, WindowId, showTabId)
  38. import Control.Alt ((<|>))
  39. import Control.Bind (join, bind, (>>=))
  40. import Control.Category (identity, (<<<), (>>>))
  41. import Control.Plus (empty) as A
  42. import Data.Array (sortBy, singleton, fromFoldable, insertAt, deleteAt, mapWithIndex, foldl, filter, (!!)) as A
  43. import Data.Eq ((==), (/=))
  44. import Data.Function (const, on, ($))
  45. import Data.Functor (map, (<#>), (<$>))
  46. import Data.Lens (Lens', Traversal', _Just, over, preview, set, view)
  47. import Data.Lens.At (at)
  48. import Data.Lens.Iso.Newtype (_Newtype)
  49. import Data.Lens.Record (prop)
  50. import Data.List (List, groupBy, head) as L
  51. import Data.List.NonEmpty (NonEmptyList, head) as NEL
  52. import Data.Map as M
  53. import Data.Maybe (Maybe(..), fromMaybe, maybe, maybe')
  54. import Data.Monoid ((<>))
  55. import Data.Ord (compare)
  56. import Data.Show (show)
  57. import Data.Symbol (SProxy(..))
  58. import Data.Tuple (Tuple(..))
  59. import Data.Unit (Unit)
  60. import Effect (Effect)
  61. import Effect.Console (error)
  62. import Effect.Exception.Unsafe (unsafeThrow)
  63. import Prelude ((#))
  64. import PureTabs.Model.Events (BackgroundEvent)
  65. type GlobalState
  66. = { windows :: M.Map WindowId ExtWindow
  67. , detached :: Maybe Tab
  68. }
  69. initialGlobalState :: GlobalState
  70. initialGlobalState =
  71. { windows: M.empty
  72. , detached: Nothing
  73. }
  74. type ExtWindow
  75. = { positions :: Array TabId
  76. , tabs :: M.Map TabId Tab
  77. , port :: Maybe Port
  78. }
  79. emptyWindow :: ExtWindow
  80. emptyWindow =
  81. { positions: A.empty
  82. , tabs: M.empty
  83. , port: Nothing
  84. }
  85. _tabs :: forall a r. Lens' { tabs :: a | r } a
  86. _tabs = prop (SProxy :: _ "tabs")
  87. _port :: forall a r. Lens' { port :: a | r } a
  88. _port = prop (SProxy :: _ "port")
  89. _windows :: forall a r. Lens' { windows :: a | r } a
  90. _windows = prop (SProxy :: _ "windows")
  91. _title :: forall a r. Lens' { title :: a | r } a
  92. _title = prop (SProxy :: _ "title")
  93. _index :: forall a r. Lens' { index :: a | r } a
  94. _index = prop (SProxy :: _ "index")
  95. _tabTitle :: Lens' Tab String
  96. _tabTitle = _Newtype <<< _title
  97. _id :: forall a r. Lens' { id :: a | r } a
  98. _id = prop (SProxy :: _ "id")
  99. _active :: forall a r. Lens' { active :: a | r } a
  100. _active = prop (SProxy :: _ "active")
  101. _tabId :: Lens' Tab TabId
  102. _tabId = _Newtype <<< _id
  103. _windowId :: forall a r. Lens' { windowId :: a | r } a
  104. _windowId = prop (SProxy :: _ "windowId")
  105. _positions :: forall a r. Lens' { positions :: a | r } a
  106. _positions = prop (SProxy :: _ "positions")
  107. _tabWindowId :: Lens' Tab WindowId
  108. _tabWindowId = _Newtype <<< _windowId
  109. _portFromWindow :: Tab -> Traversal' GlobalState Port
  110. _portFromWindow (Tab tab) = _portFromWindowId tab.windowId
  111. _portFromWindowId :: WindowId -> Traversal' GlobalState Port
  112. _portFromWindowId wid = _windowIdToWindow wid <<< _port <<< _Just
  113. _windowIdToWindow :: WindowId -> Traversal' GlobalState ExtWindow
  114. _windowIdToWindow wid = _windows <<< (at wid) <<< _Just
  115. _tabFromWindow :: Tab -> Traversal' GlobalState (Maybe Tab)
  116. _tabFromWindow (Tab tab) = _windowIdToWindow tab.windowId <<< _tabs <<< (at tab.id)
  117. _windowIdToTabIdToTab :: WindowId -> TabId -> Traversal' GlobalState (Maybe Tab)
  118. _windowIdToTabIdToTab wid tid = _windowIdToWindow wid <<< _tabs <<< (at tid)
  119. tabFromWinIdAndTabId :: WindowId -> TabId -> GlobalState -> Maybe Tab
  120. tabFromWinIdAndTabId winId tabId = join <<< preview (_windowIdToTabIdToTab winId tabId)
  121. _tabFromTabIdAndWindow :: GlobalState -> TabId -> Maybe Tab
  122. _tabFromTabIdAndWindow s tabId =
  123. let
  124. allWindows = M.values s.windows
  125. allTabs = map (view _tabs) allWindows
  126. matchingTabId = map (M.lookup tabId) allTabs
  127. in
  128. join $ L.head matchingTabId
  129. sendToTabPort :: Tab -> GlobalState -> BackgroundEvent -> Effect Unit
  130. sendToTabPort tab state msg =
  131. case (preview (_portFromWindow tab) state) of
  132. Just port -> postMessageJson port msg
  133. Nothing -> error $ "bg: no port found for tab id " <> (showTabId tab)
  134. sendToWindowPort :: WindowId -> GlobalState -> BackgroundEvent -> Effect Unit
  135. sendToWindowPort wid state event =
  136. case (preview (_portFromWindowId wid) state) of
  137. Just port -> postMessageJson port event
  138. Nothing -> error $ "bg: no port found for window id " <> (show wid)
  139. initialTabListToGlobalState :: L.List Tab -> GlobalState
  140. initialTabListToGlobalState tabs = { windows: windows, detached: Nothing }
  141. where
  142. groupedTabs = L.groupBy (\(Tab t1) (Tab t2) -> t1.windowId == t2.windowId) tabs
  143. tabsToWindow :: NEL.NonEmptyList Tab -> Tuple WindowId ExtWindow
  144. tabsToWindow tabs' =
  145. let
  146. windowId = (\(Tab t) -> t.windowId) $ NEL.head tabs'
  147. window =
  148. { tabs: M.fromFoldable $ tabs' <#> \(Tab t) -> Tuple t.id (Tab t)
  149. , port: Nothing
  150. , positions: (\(Tab t) -> t.id) <$> A.sortBy (compare `on` \(Tab t) -> t.index) (A.fromFoldable tabs')
  151. }
  152. in
  153. Tuple windowId window
  154. windows = M.fromFoldable $ (tabsToWindow <$> groupedTabs)
  155. addEmptyWindow :: WindowId -> GlobalState -> GlobalState
  156. addEmptyWindow winId = (over (_windows <<< at winId)) (_ <|> (Just emptyWindow))
  157. deleteWindow :: WindowId -> GlobalState -> GlobalState
  158. deleteWindow winId state = state { windows = M.delete winId state.windows }
  159. createTab :: Tab -> GlobalState -> GlobalState
  160. createTab (Tab t) s = s { windows = M.update updateWindow t.windowId windows }
  161. where
  162. windows = case M.lookup t.windowId s.windows of
  163. Nothing -> M.insert t.windowId emptyWindow s.windows
  164. Just _ -> s.windows
  165. updateWindow :: ExtWindow -> Maybe ExtWindow
  166. updateWindow win =
  167. -- this will delete the window if there is an issue with the position..
  168. -- not the best solution but we can't really recover from it anyway.
  169. (A.insertAt t.index t.id win.positions)
  170. <#> \newPos ->
  171. win
  172. { positions = newPos
  173. , tabs = M.insert t.id (Tab t) win.tabs
  174. }
  175. updateTab :: Tab -> GlobalState -> GlobalState
  176. updateTab tab =
  177. -- update by replacing the tab only if it already exists
  178. (over (_tabFromWindow tab) (map $ const tab))
  179. -- or update the currently detached tab
  180. >>> ( \s -> case s.detached of
  181. Just (Tab tab')
  182. | (view _tabId tab) == tab'.id -> s { detached = Just (Tab tab') }
  183. _ -> s
  184. )
  185. moveTab :: Int -> Int -> WindowId -> GlobalState -> GlobalState
  186. moveTab fromIndex toIndex windowId state =
  187. let
  188. -- Update the state by moving the tab at `fromIndex` to `toIndex`.
  189. newState = state # over ((_windowIdToWindow windowId) <<< _positions) unsafeUpdatePositions
  190. -- Get the new positions for each tab based on the move just done.
  191. newPositions = newState # view ((_windowIdToWindow windowId) <<< _positions)
  192. in
  193. -- Update the new positions for each tab
  194. newState # over ((_windowIdToWindow windowId) <<< _tabs) (updateTabsIndex newPositions)
  195. where
  196. -- | Move an element from `from` to `to` in array `arr`.
  197. moveElement :: forall a. Int -> Int -> Array a -> Maybe (Array a)
  198. moveElement from to arr = do
  199. tab <- arr A.!! from
  200. A.deleteAt from arr >>= A.insertAt to tab
  201. -- | Update the positions tabs
  202. unsafeUpdatePositions :: Array TabId -> Array TabId
  203. unsafeUpdatePositions =
  204. (moveElement fromIndex toIndex)
  205. -- The indexes should exist, we need to revisit the code if it doesn't
  206. >>> (maybe' (\_ -> unsafeThrow "bg: invalid indexes during moveTab") identity)
  207. -- | Update the index of the tab given the positions.
  208. -- | This is done by folding over a map of index update function applied to all tabs.
  209. updateTabsIndex :: Array TabId -> M.Map TabId Tab -> M.Map TabId Tab
  210. updateTabsIndex positions tabs =
  211. let
  212. modifyFuncs :: Array (M.Map TabId Tab -> M.Map TabId Tab)
  213. modifyFuncs = A.mapWithIndex (\idx tid' -> set (at tid' <<< _Just <<< _Newtype <<< _index) idx) positions
  214. in
  215. A.foldl (#) tabs modifyFuncs
  216. activateTab :: WindowId -> (Maybe TabId) -> TabId -> GlobalState -> GlobalState
  217. activateTab winId previousTabId newTabId state =
  218. let
  219. prevTab :: Maybe Tab
  220. prevTab = previousTabId >>= \ptid -> join $ preview (_windowIdToTabIdToTab winId ptid) state
  221. prevTabF :: GlobalState -> GlobalState
  222. prevTabF = maybe identity (\t -> set (_activeTab t) false) prevTab
  223. newTab = join $ preview (_windowIdToTabIdToTab winId newTabId) state
  224. newTabF :: GlobalState -> GlobalState
  225. newTabF = maybe identity (\t -> set (_activeTab t) true) newTab
  226. _activeTab t = (_tabFromWindow t) <<< _Just <<< _Newtype <<< _active
  227. in
  228. (prevTabF >>> newTabF) state
  229. deleteTab :: WindowId -> TabId -> GlobalState -> GlobalState
  230. deleteTab winId tabId =
  231. (set (_windowIdToTabIdToTab winId tabId) Nothing)
  232. >>> over (_windowIdToWindow winId <<< _positions) (A.filter ((/=) tabId))
  233. detachTab :: WindowId -> TabId -> GlobalState -> GlobalState
  234. detachTab winId tabId state =
  235. case preview (_windowIdToTabIdToTab winId tabId) state of
  236. Just (Just tab) -> do
  237. state # (deleteTab winId tabId) >>> \s -> s { detached = Just tab }
  238. -- XXX: We're losing the information that we couldn't fetch the tab.
  239. -- This shouldn't happen, but I don't see how to go around it. We don't
  240. -- have a (typed) proof that a given tab exists for a window id and a tab
  241. -- id, so let's just assume everything is well behaved.
  242. -- The other solution is to first do a read, then a write, and return an
  243. -- effect where we can throw.
  244. _ -> state
  245. attachTab :: WindowId -> TabId -> Int -> GlobalState -> GlobalState
  246. attachTab winId tabId newPosition state =
  247. case state.detached of
  248. Just (Tab tab) ->
  249. let
  250. newTab = Tab (tab { windowId = winId, index = newPosition })
  251. in
  252. state # (createTab newTab) >>> (_ { detached = Nothing})
  253. _ -> state
  254. -- | Set the port of a new window connecting. If the window doesn't exist,
  255. -- | initialize it with new data.
  256. initializeWindowState :: WindowId -> Port -> GlobalState -> GlobalState
  257. initializeWindowState winId port =
  258. over (_windows <<< (at winId)) (\win -> Just $ set _port (Just port) (fromMaybe emptyWindow win))