GlobalState.purs 11 KB

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