Skip to content

Commit 05dad31

Browse files
authored
Store history in XDG data directory (#253)
- fixes #136 - changes the history file format to simple text line per `REPLEntry` - refactors out a `REPLHistory` type and its pure functions - adds tests for REPL logic (move to different previous entry,...)
1 parent 8deb10f commit 05dad31

File tree

6 files changed

+308
-94
lines changed

6 files changed

+308
-94
lines changed

src/Swarm/TUI/Controller.hs

+28-29
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module Swarm.TUI.Controller (
3131
handleREPLEvent,
3232
validateREPLForm,
3333
adjReplHistIndex,
34+
TimeDir (..),
3435

3536
-- ** World panel
3637
handleWorldEvent,
@@ -49,9 +50,10 @@ import Control.Monad.State
4950
import Data.Bits
5051
import Data.Either (isRight)
5152
import Data.Int (Int64)
52-
import Data.Maybe (fromMaybe, isJust)
53+
import Data.Maybe (fromMaybe, isJust, mapMaybe)
5354
import qualified Data.Set as S
5455
import qualified Data.Text as T
56+
import qualified Data.Text.IO as T
5557
import Linear
5658
import System.Clock
5759
import Witch (into)
@@ -150,16 +152,12 @@ toggleModal s modal = do
150152
-- the updated REPL history to a @.swarm_history@ file.
151153
shutdown :: AppState -> EventM Name (Next AppState)
152154
shutdown s = do
153-
let s' = s & uiState . uiReplHistory . traverse %~ markOld
154-
hist = filter isEntry (s' ^. uiState . uiReplHistory)
155-
liftIO $ writeFile ".swarm_history" (show hist)
155+
let hist = mapMaybe getREPLEntry $ getLatestREPLHistoryItems maxBound history
156+
liftIO $ (`T.appendFile` T.unlines hist) =<< getSwarmHistoryPath True
157+
let s' = s & uiState . uiReplHistory %~ restartREPLHistory
156158
halt s'
157159
where
158-
markOld (REPLEntry _ d e) = REPLEntry False d e
159-
markOld r = r
160-
161-
isEntry REPLEntry {} = True
162-
isEntry _ = False
160+
history = s ^. uiState . uiReplHistory
163161

164162
------------------------------------------------------------
165163
-- Handling Frame events
@@ -320,7 +318,7 @@ updateUI = do
320318
-- result as a REPL output, with its type, and reset the replStatus.
321319
REPLWorking pty (Just v) -> do
322320
let out = T.intercalate " " [into (prettyValue v), ":", prettyText (stripCmd pty)]
323-
uiState . uiReplHistory %= (REPLOutput out :)
321+
uiState . uiReplHistory %= addREPLItem (REPLOutput out)
324322
gameState . replStatus .= REPLDone
325323
pure True
326324

@@ -403,8 +401,7 @@ handleREPLEvent s (VtyEvent (V.EvKey V.KEnter [])) =
403401
s
404402
& uiState . uiReplForm %~ updateFormState ""
405403
& uiState . uiReplType .~ Nothing
406-
& uiState . uiReplHistory %~ prependReplEntry
407-
& uiState . uiReplHistIdx .~ (-1)
404+
& uiState . uiReplHistory %~ addREPLItem (REPLEntry entry)
408405
& uiState . uiError .~ Nothing
409406
& gameState . replStatus .~ REPLWorking ty Nothing
410407
& gameState . robotMap . ix "base" . machine .~ initMachine t topValCtx topStore
@@ -422,13 +419,10 @@ handleREPLEvent s (VtyEvent (V.EvKey V.KEnter [])) =
422419
topStore =
423420
fromMaybe emptyStore $
424421
s ^? gameState . robotMap . at "base" . _Just . robotContext . defStore
425-
prependReplEntry replHistory
426-
| firstReplEntry replHistory == Just entry = REPLEntry True True entry : replHistory
427-
| otherwise = REPLEntry True False entry : replHistory
428422
handleREPLEvent s (VtyEvent (V.EvKey V.KUp [])) =
429-
continue $ s & adjReplHistIndex (+)
423+
continue $ s & adjReplHistIndex Older
430424
handleREPLEvent s (VtyEvent (V.EvKey V.KDown [])) =
431-
continue $ s & adjReplHistIndex (-)
425+
continue $ s & adjReplHistIndex Newer
432426
handleREPLEvent s ev = do
433427
f' <- handleFormEvent ev (s ^. uiState . uiReplForm)
434428
continue $ validateREPLForm (s & uiState . uiReplForm .~ f')
@@ -450,21 +444,26 @@ validateREPLForm s =
450444
validate = setFieldValid (isRight result) REPLInput
451445

452446
-- | Update our current position in the REPL history.
453-
adjReplHistIndex :: (Int -> Int -> Int) -> AppState -> AppState
454-
adjReplHistIndex (+/-) s =
455-
s & uiState . uiReplHistIdx .~ newIndex
456-
& (if curIndex == -1 then saveLastEntry else id)
457-
& (if newIndex /= curIndex then uiState . uiReplForm %~ updateFormState newEntry else id)
447+
adjReplHistIndex :: TimeDir -> AppState -> AppState
448+
adjReplHistIndex d s =
449+
ns
450+
& (if replIndexIsAtInput (s ^. repl) then saveLastEntry else id)
451+
& (if oldEntry /= newEntry then showNewEntry else id)
458452
& validateREPLForm
459453
where
454+
-- new AppState after moving the repl index
455+
ns = s & repl %~ moveReplHistIndex d oldEntry
456+
457+
repl :: Lens' AppState REPLHistory
458+
repl = uiState . uiReplHistory
459+
460+
replLast = s ^. uiState . uiReplLast
460461
saveLastEntry = uiState . uiReplLast .~ formState (s ^. uiState . uiReplForm)
461-
entries = [e | REPLEntry _ False e <- s ^. uiState . uiReplHistory]
462-
curIndex = s ^. uiState . uiReplHistIdx
463-
histLen = length entries
464-
newIndex = min (histLen - 1) (max (-1) (curIndex +/- 1))
465-
newEntry
466-
| newIndex == -1 = s ^. uiState . uiReplLast
467-
| otherwise = entries !! newIndex
462+
showNewEntry = uiState . uiReplForm %~ updateFormState newEntry
463+
-- get REPL data
464+
getCurrEntry = fromMaybe replLast . getCurrentItemText . view repl
465+
oldEntry = getCurrEntry s
466+
newEntry = getCurrEntry ns
468467

469468
------------------------------------------------------------
470469
-- World events

src/Swarm/TUI/Model.hs

+142-41
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,36 @@ module Swarm.TUI.Model (
2020
Modal (..),
2121

2222
-- * UI state
23+
24+
-- ** REPL
2325
REPLHistItem (..),
24-
firstReplEntry,
26+
replItemText,
27+
isREPLEntry,
28+
getREPLEntry,
29+
REPLHistory,
30+
replIndex,
31+
replLength,
32+
newREPLHistory,
33+
addREPLItem,
34+
restartREPLHistory,
35+
getLatestREPLHistoryItems,
36+
moveReplHistIndex,
37+
getCurrentItemText,
38+
replIndexIsAtInput,
39+
TimeDir (..),
40+
41+
-- ** Inventory
2542
InventoryListEntry (..),
2643
_Separator,
2744
_InventoryEntry,
2845
_InstalledEntry,
29-
UIState,
3046

31-
-- ** Fields
47+
-- ** UI Model
48+
UIState,
3249
uiFocusRing,
3350
uiReplForm,
3451
uiReplType,
3552
uiReplHistory,
36-
uiReplHistIdx,
3753
uiReplLast,
3854
uiInventory,
3955
uiMoreInfoTop,
@@ -78,19 +94,22 @@ module Swarm.TUI.Model (
7894
import Control.Lens
7995
import Control.Monad.Except
8096
import Control.Monad.State
97+
import Data.Bits (FiniteBits (finiteBitSize))
98+
import Data.Foldable (toList)
8199
import Data.List (findIndex, sortOn)
82-
import Data.Maybe (fromMaybe)
100+
import Data.Maybe (fromMaybe, isJust)
101+
import Data.Sequence (Seq)
102+
import qualified Data.Sequence as Seq
83103
import Data.Text (Text)
104+
import qualified Data.Text as T
84105
import qualified Data.Vector as V
85106
import System.Clock
86-
import Text.Read (readMaybe)
87107

88108
import Brick
89109
import Brick.Focus
90110
import Brick.Forms
91111
import qualified Brick.Widgets.List as BL
92112

93-
import Data.Bits (FiniteBits (finiteBitSize))
94113
import Swarm.Game.Entity as E
95114
import Swarm.Game.Robot
96115
import Swarm.Game.State
@@ -138,39 +157,126 @@ data Name
138157
infoScroll :: ViewportScroll Name
139158
infoScroll = viewportScroll InfoViewport
140159

141-
data Modal
142-
= HelpModal
143-
deriving (Eq, Show)
144-
145160
------------------------------------------------------------
146-
-- UI state
161+
-- REPL History
147162
------------------------------------------------------------
148163

149164
-- | An item in the REPL history.
150165
data REPLHistItem
151-
= -- | Something entered by the user. The first
152-
-- @Bool@ indicates whether it is
153-
-- something entered this session (it
154-
-- will be @False@ for entries that were
155-
-- loaded from the history file). This is
156-
-- so we know which ones to append to the
157-
-- history file on shutdown.
158-
-- The second @Bool@ indicates whether it
159-
-- is a duplicate of the preceding item (it
160-
-- will be @True@ for duplicate entries).
161-
-- This is so we can ignore it when scrolling
162-
-- through the REPL history in the REPL window.
163-
REPLEntry Bool Bool Text
166+
= -- | Something entered by the user.
167+
REPLEntry Text
164168
| -- | A response printed by the system.
165169
REPLOutput Text
166170
deriving (Eq, Ord, Show, Read)
167171

168-
-- | Given a REPL history return @Just@ the most recent @Text@
169-
-- entered by the user or @Nothing@ if there is none.
170-
firstReplEntry :: [REPLHistItem] -> Maybe Text
171-
firstReplEntry ((REPLEntry _ _ entry) : _) = Just entry
172-
firstReplEntry (_ : rest) = firstReplEntry rest
173-
firstReplEntry [] = Nothing
172+
-- | Useful helper function to only get user input text.
173+
getREPLEntry :: REPLHistItem -> Maybe Text
174+
getREPLEntry = \case
175+
REPLEntry t -> Just t
176+
_ -> Nothing
177+
178+
-- | Useful helper function to filter out REPL output.
179+
isREPLEntry :: REPLHistItem -> Bool
180+
isREPLEntry = isJust . getREPLEntry
181+
182+
-- | Get the text of REPL input/output.
183+
replItemText :: REPLHistItem -> Text
184+
replItemText = \case
185+
REPLEntry t -> t
186+
REPLOutput t -> t
187+
188+
-- | History of the REPL with indices (0 is first entry) to the current
189+
-- line and to the first entry since loading saved history.
190+
-- We also (ab)use the length of the REPL as the index of current
191+
-- input line, since that number is one past the index of last entry.
192+
data REPLHistory = REPLHistory
193+
{ _replSeq :: Seq REPLHistItem
194+
, _replIndex :: Int
195+
, _replStart :: Int
196+
}
197+
198+
makeLensesWith (lensRules & generateSignatures .~ False) ''REPLHistory
199+
200+
-- | Sequence of REPL inputs and outputs, oldest entry is leftmost.
201+
replSeq :: Lens' REPLHistory (Seq REPLHistItem)
202+
203+
-- | The current index in the REPL history (if the user is going back
204+
-- through the history using up/down keys).
205+
replIndex :: Lens' REPLHistory Int
206+
207+
-- | The index of the first entry since loading saved history.
208+
--
209+
-- It will be set on load and reset on save (happens during exit).
210+
replStart :: Lens' REPLHistory Int
211+
212+
-- | Create new REPL history (i.e. from loaded history file lines).
213+
newREPLHistory :: [REPLHistItem] -> REPLHistory
214+
newREPLHistory xs =
215+
let s = Seq.fromList xs
216+
in REPLHistory
217+
{ _replSeq = s
218+
, _replStart = length s
219+
, _replIndex = length s
220+
}
221+
222+
-- | Point the start of REPL history after current last line. See 'replStart'.
223+
restartREPLHistory :: REPLHistory -> REPLHistory
224+
restartREPLHistory h = h & replStart .~ replLength h
225+
226+
-- | Current number lines of the REPL history - (ab)used as index of input buffer.
227+
replLength :: REPLHistory -> Int
228+
replLength = length . _replSeq
229+
230+
-- | Add new REPL input - the index must have been pointing one past
231+
-- the last element already, so we increment it to keep it that way.
232+
addREPLItem :: REPLHistItem -> REPLHistory -> REPLHistory
233+
addREPLItem t h =
234+
h
235+
& replSeq %~ (|> t)
236+
& replIndex .~ 1 + replLength h
237+
238+
-- | Get the latest N items in history, starting with the oldest one.
239+
--
240+
-- This is used to show previous REPL lines in UI, so we need the items
241+
-- sorted in the order they were entered and will be drawn top to bottom.
242+
getLatestREPLHistoryItems :: Int -> REPLHistory -> [REPLHistItem]
243+
getLatestREPLHistoryItems n h = toList latestN
244+
where
245+
latestN = Seq.drop oldestIndex $ h ^. replSeq
246+
oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n
247+
248+
data TimeDir = Newer | Older deriving (Eq, Ord, Show)
249+
250+
moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory
251+
moveReplHistIndex d lastEntered history = history & replIndex .~ newIndex
252+
where
253+
historyLen = replLength history
254+
curText = fromMaybe lastEntered $ getCurrentItemText history
255+
curIndex = history ^. replIndex
256+
entries = history ^. replSeq
257+
-- split repl at index
258+
(olderP, newer) = Seq.splitAt curIndex entries
259+
-- find first different entry in direction
260+
notSameEntry = \case
261+
REPLEntry t -> t /= curText
262+
_ -> False
263+
newIndex = case d of
264+
Newer -> maybe historyLen (curIndex +) $ Seq.findIndexL notSameEntry newer
265+
Older -> fromMaybe curIndex $ Seq.findIndexR notSameEntry olderP
266+
267+
getCurrentItemText :: REPLHistory -> Maybe Text
268+
getCurrentItemText history = replItemText <$> Seq.lookup (history ^. replIndex) (history ^. replSeq)
269+
270+
replIndexIsAtInput :: REPLHistory -> Bool
271+
replIndexIsAtInput repl = repl ^. replIndex == replLength repl
272+
273+
------------------------------------------------------------
274+
-- UI state
275+
------------------------------------------------------------
276+
277+
data Modal
278+
= HelpModal
279+
deriving (Eq, Show)
174280

175281
-- | An entry in the inventory list displayed in the info panel. We
176282
-- can either have an entity with a count in the robot's inventory,
@@ -192,8 +298,7 @@ data UIState = UIState
192298
, _uiReplForm :: Form Text AppEvent Name
193299
, _uiReplType :: Maybe Polytype
194300
, _uiReplLast :: Text
195-
, _uiReplHistory :: [REPLHistItem]
196-
, _uiReplHistIdx :: Int
301+
, _uiReplHistory :: REPLHistory
197302
, _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry)
198303
, _uiMoreInfoTop :: Bool
199304
, _uiMoreInfoBot :: Bool
@@ -238,11 +343,7 @@ uiReplLast :: Lens' UIState Text
238343

239344
-- | History of things the user has typed at the REPL, interleaved
240345
-- with outputs the system has generated.
241-
uiReplHistory :: Lens' UIState [REPLHistItem]
242-
243-
-- | The current index in the REPL history (if the user is going back
244-
-- through the history using up/down keys).
245-
uiReplHistIdx :: Lens' UIState Int
346+
uiReplHistory :: Lens' UIState REPLHistory
246347

247348
-- | The hash value of the focused robot entity (so we can tell if its
248349
-- inventory changed) along with a list of the items in the
@@ -340,15 +441,15 @@ initLgTicksPerSecond = 3 -- 2^3 = 8 ticks / second
340441
-- time.
341442
initUIState :: ExceptT Text IO UIState
342443
initUIState = liftIO $ do
343-
mhist <- (>>= readMaybe @[REPLHistItem]) <$> readFileMay ".swarm_history"
444+
historyT <- readFileMayT =<< getSwarmHistoryPath False
445+
let history = maybe [] (map REPLEntry . T.lines) historyT
344446
startTime <- getTime Monotonic
345447
return $
346448
UIState
347449
{ _uiFocusRing = initFocusRing
348450
, _uiReplForm = initReplForm
349451
, _uiReplType = Nothing
350-
, _uiReplHistory = mhist ? []
351-
, _uiReplHistIdx = -1
452+
, _uiReplHistory = newREPLHistory history
352453
, _uiReplLast = ""
353454
, _uiInventory = Nothing
354455
, _uiMoreInfoTop = False

0 commit comments

Comments
 (0)