nix-tree 0.1.4 → 0.1.5
raw patch · 5 files changed
+219/−68 lines, 5 filesdep +clock
Dependencies added: clock
Files
- CHANGELOG.md +30/−0
- README.md +11/−3
- nix-tree.cabal +4/−2
- src/App.hs +166/−62
- src/StorePath.hs +8/−1
+ CHANGELOG.md view
@@ -0,0 +1,30 @@+# Changelog++## Unreleased++## 0.1.5++* feat: Add sort order toggle.+* fix: Search now does not contain false-positives.++## 0.1.4 - 2021-02-23++* feat: Make search case insensitive.+* fix: Can close the help modal now.++## 0.1.3.1 - 2021-01-07++* fix: Correctly pass --derivation flag to Nix >= 2.4++## 0.1.3.0 - 2020-12-22++* feat: Ability to work on .drv files.++## 0.1.2.0 - 2020-09-15++* feat: Add --version flag+* fix: Can search the letter 'q' without closing the search modal.++## 0.1.1.0 - 2020-08-23++* feat: Seach functionality.
README.md view
@@ -5,7 +5,7 @@ Interactively browse the dependency graph of your Nix derivations. -[](https://asciinema.org/a/ahDveBL1gs5t36z1myePtrKpR)+[](https://asciinema.org/a/cnilbmPXW51g97hdNJZcM5F6h) ## Installation @@ -33,11 +33,19 @@ q/Esc: : Quit / close modal w : Open why-depends mode / : Open search mode- i : Toggle modeline+ s : Change sort order ? : Show help ``` -[home-manager]: https://github.com/rycee/home-manager+### Glossary++* **NAR Size**: Size of the store path itself.+* **Closure size**: Total size of the store path and all its transitive dependencies.+* **Added size**: Size of the store path, and all its _unique_ transitive+ dependencies. In other words, the cost of having that store path on top+ of all other paths. See [issue #14] for a better explanation.++[issue #14]: https://github.com/utdemir/nix-tree/issues/14 ### Tips
nix-tree.cabal view
@@ -3,7 +3,7 @@ name: nix-tree synopsis: Interactively browse a Nix store paths dependencies description: A terminal curses application to browse a Nix store paths dependencies-version: 0.1.4+version: 0.1.5 homepage: https://github.com/utdemir/nix-tree license: BSD-3-Clause license-file: LICENSE@@ -12,7 +12,7 @@ copyright: Utku Demir category: Language.Nix build-type: Simple-extra-source-files: README.md+extra-source-files: README.md, CHANGELOG.md common common-options ghc-options: -threaded@@ -36,6 +36,7 @@ NamedFieldPuns RankNTypes ScopedTypeVariables+ NumericUnderscores other-modules: PathStats StorePath App@@ -50,6 +51,7 @@ , brick , bytestring , containers+ , clock , deepseq , directory , filepath
src/App.hs view
@@ -1,6 +1,7 @@ module App (run, helpText) where import qualified Brick as B+import qualified Brick.BChan as B import qualified Brick.Widgets.Border as B import qualified Brick.Widgets.Center as B import qualified Brick.Widgets.List as B@@ -11,8 +12,12 @@ import qualified Graphics.Vty as V import InvertedIndex import PathStats+import qualified System.Clock as Clock import qualified System.HrfSize as HRF +data Event+ = EventTick Clock.TimeSpec+ data Widgets = WidgetPrevPane | WidgetCurrPane@@ -26,6 +31,11 @@ | ModalWhyDepends (B.GenericList Widgets Seq (NonEmpty (Path s))) | ModalSearch Text Text (B.GenericList Widgets Seq (Path s)) +succCycle :: forall a. (Bounded a, Enum a) => a -> a+succCycle a+ | fromEnum a == fromEnum (maxBound @a) = minBound+ | otherwise = succ a+ data AppEnv s = AppEnv { aeActualStoreEnv :: StoreEnv s (PathStats s), aeInvertedIndex :: InvertedIndex,@@ -34,23 +44,42 @@ aeNextPane :: List s, aeParents :: [List s], aeOpenModal :: Maybe (Modal s),- aeShowInfoPane :: Bool+ aeSortOrder :: SortOrder,+ aeSortOrderLastChanged :: Clock.TimeSpec,+ aeCurrTime :: Clock.TimeSpec } type Path s = StorePath s (StoreName s) (PathStats s) type List s = B.GenericList Widgets Seq (Path s) -attrTerminal :: B.AttrName+data SortOrder+ = SortOrderAlphabetical+ | SortOrderClosureSize+ | SortOrderAddedSize+ deriving (Show, Eq, Enum, Bounded)++compareBySortOrder :: SortOrder -> Path s -> Path s -> Ordering+compareBySortOrder SortOrderAlphabetical = compare `on` T.toLower . storeNameToShortText . spName+compareBySortOrder SortOrderClosureSize = compare `on` Down . psTotalSize . spPayload+compareBySortOrder SortOrderAddedSize = compare `on` Down . psAddedSize . spPayload++attrTerminal, attrUnderlined :: B.AttrName attrTerminal = "terminal"+attrUnderlined = "underlined" run :: StoreEnv s (PathStats s) -> IO ()-run env = void . B.defaultMain app =<< appEnv- where- appEnv = do- let ii = iiFromList . toList . map (storeNameToText . spName) $ seAll env- _ <- forkIO $ evaluate (rnf ii)- return $+run env = do+ -- Create the inverted index, and start evaluating it in the background+ let ii = iiFromList . toList . map (storeNameToText . spName) $ seAll env+ _ <- forkIO $ evaluate (rnf ii)++ -- Initial state+ let getTime = Clock.getTime Clock.Monotonic+ currTime <- getTime+ let defaultSortOrder = SortOrderClosureSize++ let appEnv = AppEnv { aeActualStoreEnv = env,@@ -59,23 +88,47 @@ aePrevPane = B.list WidgetPrevPane S.empty 0, aeCurrPane =- B.list WidgetCurrPane (S.fromList . NE.toList $ seGetRoots env) 0,+ B.list+ WidgetCurrPane+ (S.fromList . sortBy (compareBySortOrder defaultSortOrder) . NE.toList $ seGetRoots env)+ 0, aeNextPane = B.list WidgetNextPane S.empty 0, aeParents = [], aeOpenModal = Nothing,- aeShowInfoPane =- True+ aeSortOrder =+ defaultSortOrder,+ aeSortOrderLastChanged =+ Clock.TimeSpec 0 0,+ aeCurrTime =+ currTime } & repopulateNextPane + -- Create a channel that's fed by current time+ chan <- B.newBChan 10+ void . forkIO $+ forever $ do+ threadDelay (100 * 100)+ t <- getTime+ _ <- B.writeBChanNonBlocking chan (EventTick t)+ return ()++ -- And run the application+ let mkVty = V.mkVty V.defaultConfig+ initialVty <- mkVty+ _ <- B.customMain initialVty mkVty (Just chan) app appEnv++ return ()+ renderList ::+ Maybe SortOrder -> Bool -> List s -> B.Widget Widgets-renderList isFocused =+renderList highlightSort isFocused = B.renderList ( \_ StorePath@@ -88,22 +141,35 @@ then B.withAttr attrTerminal else identity in color $- B.padRight B.Max (B.txt $ storeNameToShortText spName)- B.<+> B.padLeft- B.Max- ( B.txt $- prettySize psTotalSize- <> if not (null spRefs)- then- " ("- <> prettySize psAddedSize- <> ")"- else mempty- )+ B.hBox+ [ B.txt (storeNameToShortText spName)+ & underlineWhen SortOrderAlphabetical+ & B.padRight (B.Pad 1)+ & B.padRight B.Max,+ if null spRefs+ then+ B.txt (prettySize psTotalSize)+ & underlineWhen SortOrderClosureSize+ & underlineWhen SortOrderAddedSize+ else+ B.hBox+ [ B.txt (prettySize psTotalSize)+ & underlineWhen SortOrderClosureSize,+ B.txt " (",+ B.txt (prettySize psAddedSize)+ & underlineWhen SortOrderAddedSize,+ B.txt ")"+ ]+ ] ) isFocused+ where+ underlineWhen so =+ if Just so == highlightSort+ then B.withDefAttr attrUnderlined+ else identity -app :: B.App (AppEnv s) () Widgets+app :: B.App (AppEnv s) Event Widgets app = B.App { B.appDraw = \env@AppEnv {aeOpenModal} ->@@ -127,8 +193,13 @@ B.continue $ showWhyDepends s (B.VtyEvent (V.EvKey (V.KChar '/') []), Nothing) -> B.continue $ showAndUpdateSearch "" "" s- (B.VtyEvent (V.EvKey (V.KChar 'i') []), Nothing) ->- B.continue $ s {aeShowInfoPane = not (aeShowInfoPane s)}+ (B.VtyEvent (V.EvKey (V.KChar 's') []), Nothing) ->+ B.continue $+ s+ { aeSortOrder = succCycle (aeSortOrder s),+ aeSortOrderLastChanged = aeCurrTime s+ }+ & sortPanes (B.VtyEvent (V.EvKey k []), Nothing) | k `elem` [V.KChar 'h', V.KLeft] -> B.continue $ moveLeft s@@ -205,6 +276,9 @@ (B.VtyEvent (V.EvKey k []), Just ModalHelp) | k `elem` [V.KChar 'q', V.KEsc] -> B.continue s {aeOpenModal = Nothing}+ -- handle our events+ (B.AppEvent (EventTick t), Nothing) ->+ B.continue $ s {aeCurrTime = t} -- ignore otherwise _ -> B.continue s,@@ -213,7 +287,8 @@ B.attrMap (V.white `B.on` V.black) [ (B.listSelectedFocusedAttr, V.black `B.on` V.white),- (attrTerminal, B.fg V.blue)+ (attrTerminal, B.fg V.blue),+ (attrUnderlined, V.currentAttr `V.withStyle` V.underline) ] } where@@ -229,45 +304,50 @@ ) renderMainScreen :: AppEnv s -> B.Widget Widgets-renderMainScreen env@AppEnv {aePrevPane, aeCurrPane, aeNextPane, aeShowInfoPane} =+renderMainScreen env@AppEnv {aePrevPane, aeCurrPane, aeNextPane} = (B.joinBorders . B.border) ( B.hBox- [ renderList True aePrevPane,+ [ renderList Nothing True aePrevPane, B.vBorder,- renderList True aeCurrPane,+ renderList shouldHighlightSortOrder True aeCurrPane, B.vBorder,- renderList False aeNextPane+ renderList Nothing False aeNextPane ] )- B.<=> if aeShowInfoPane then renderInfoPane env else renderModeline env--renderModeline :: AppEnv s -> B.Widget Widgets-renderModeline env =- let selected = selectedPath env- in B.txt $- T.intercalate- " - "- [ T.pack $ storeNameToPath (spName selected),- "NAR Size: " <> prettySize (spSize selected),- "Closure Size: " <> prettySize (psTotalSize $ spPayload selected)- ]+ B.<=> renderInfoPane env+ where+ shouldHighlightSortOrder =+ let timePassed = Clock.diffTimeSpec (aeCurrTime env) (aeSortOrderLastChanged env)+ in if timePassed < Clock.TimeSpec 0 (500 * 1_000_000)+ then Just (aeSortOrder env)+ else Nothing renderInfoPane :: AppEnv s -> B.Widget Widgets renderInfoPane env = let selected = selectedPath env immediateParents = psImmediateParents $ spPayload selected- in B.txt $- T.intercalate- "\n"- [ T.pack $ storeNameToPath (spName selected),- "NAR Size: " <> prettySize (spSize selected),- "Closure Size: " <> prettySize (psTotalSize $ spPayload selected),+ in B.vBox+ [ ( let (f, s) = storeNameToSplitShortText (spName selected)+ in B.txt f B.<+> underlineWhen SortOrderAlphabetical (B.txt s)+ ),+ [ B.txt $ "NAR Size: " <> prettySize (spSize selected),+ underlineWhen SortOrderClosureSize . B.txt $ "Closure Size: " <> prettySize (psTotalSize $ spPayload selected),+ underlineWhen SortOrderAddedSize . B.txt $ "Added Size: " <> prettySize (psAddedSize $ spPayload selected)+ ]+ & intersperse (B.txt " | ")+ & B.hBox,+ B.txt $ if null immediateParents then "Immediate Parents: -" else "Immediate Parents (" <> T.pack (show $ length immediateParents) <> "): " <> T.intercalate ", " (map storeNameToShortText immediateParents)- ]+ ]+ where+ underlineWhen so =+ if so == aeSortOrder env+ then B.withAttr attrUnderlined+ else identity renderModal :: Text -> B.Widget a -> B.Widget a renderModal title widget =@@ -285,7 +365,7 @@ "q/Esc: : Quit / close modal", "w : Open why-depends mode", "/ : Open search mode",- "i : Toggle modeline",+ "s : Change sort order", "? : Show help" ] @@ -326,7 +406,7 @@ window = B.txt left B.<+> B.txt "|" B.<+> B.txt right B.<=> B.hBorder- B.<=> renderList True l+ B.<=> renderList Nothing True l showAndUpdateSearch :: Text -> Text -> AppEnv s -> AppEnv s showAndUpdateSearch left right env@AppEnv {aeActualStoreEnv, aeInvertedIndex} =@@ -368,12 +448,12 @@ & repopulateNextPane repopulateNextPane :: AppEnv s -> AppEnv s-repopulateNextPane env@AppEnv {aeActualStoreEnv, aeNextPane} =+repopulateNextPane env@AppEnv {aeActualStoreEnv, aeNextPane, aeSortOrder} = let ref = selectedPath env in env { aeNextPane = B.listReplace- ( S.sortOn (Down . psTotalSize . spPayload)+ ( S.sortBy (compareBySortOrder aeSortOrder) . S.fromList . map (seLookup aeActualStoreEnv) $ spRefs ref@@ -382,6 +462,24 @@ aeNextPane } +sortPane :: SortOrder -> List s -> List s+sortPane so l =+ let selected = B.listSelectedElement l+ elems =+ B.listElements l+ & S.sortBy (compareBySortOrder so)+ name = B.getName l+ in mkList so name elems (snd <$> selected)++sortPanes :: AppEnv s -> AppEnv s+sortPanes env@AppEnv {aeParents, aePrevPane, aeCurrPane, aeNextPane, aeSortOrder} =+ env+ { aeCurrPane = sortPane aeSortOrder aeCurrPane,+ aeNextPane = sortPane aeSortOrder aeNextPane,+ aeParents = sortPane aeSortOrder <$> aeParents,+ aePrevPane = sortPane aeSortOrder aePrevPane+ }+ selectedPath :: AppEnv s -> Path s selectedPath = NE.head . selectedPaths @@ -412,8 +510,7 @@ (NE.toList (seGetRoots aeActualStoreEnv), root) children & NE.reverse- & fmap- (\(possible, selected) -> mkList WidgetPrevPane possible selected)+ & fmap (\(possible, selected) -> mkList (aeSortOrder env) WidgetPrevPane (S.fromList possible) (Just selected)) & (<> (emptyPane :| [])) in case lists of (curr :| prevs) ->@@ -427,13 +524,20 @@ } & repopulateNextPane where- mkList name possible selected =- let contents = S.sortOn (Down . psTotalSize . spPayload) (S.fromList possible)- in B.list name contents 1- & B.listMoveTo- (fromMaybe (0) $ (((==) `on` spName) selected) `S.findIndexL` contents) emptyPane = B.list WidgetPrevPane S.empty 0++mkList ::+ SortOrder ->+ n ->+ Seq (Path s) ->+ Maybe (Path s) ->+ B.GenericList n Seq (Path s)+mkList sortOrder name possible selected =+ let contents = S.sortBy (compareBySortOrder sortOrder) possible+ in B.list name contents 1+ & B.listMoveTo+ (fromMaybe 0 $ selected >>= \s -> (((==) `on` spName) s) `S.findIndexL` contents) -- Utils
src/StorePath.hs view
@@ -3,6 +3,7 @@ storeNameToPath, storeNameToText, storeNameToShortText,+ storeNameToSplitShortText, StorePath (..), StoreEnv (..), withStoreEnv,@@ -44,7 +45,13 @@ storeNameToPath (StoreName sn) = "/nix/store/" <> toS sn storeNameToShortText :: StoreName a -> Text-storeNameToShortText = T.drop 1 . T.dropWhile (/= '-') . storeNameToText+storeNameToShortText = snd . storeNameToSplitShortText++storeNameToSplitShortText :: StoreName a -> (Text, Text)+storeNameToSplitShortText txt =+ case T.span (/= '-') . T.pack $ storeNameToPath txt of+ (f, s) | Just (c, s'') <- T.uncons s -> (T.snoc f c, s'')+ e -> e --------------------------------------------------------------------------------