diff --git a/CHANGELOG.md b/CHANGELOG.md index a6cdd4c..18b7c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +* feat: Ability to yank selected store path to clipboard (shortcut: 'y') + ## 0.1.6 - 2021-03-12 * feat: Support non standard Nix store locations diff --git a/nix-tree.cabal b/nix-tree.cabal index 8ae81ba..8af5ee9 100644 --- a/nix-tree.cabal +++ b/nix-tree.cabal @@ -41,6 +41,7 @@ common common-options StorePath App InvertedIndex + Clipboard Paths_nix_tree autogen-modules: Paths_nix_tree mixins: base hiding (Prelude) diff --git a/src/App.hs b/src/App.hs index a43ab05..d41650f 100644 --- a/src/App.hs +++ b/src/App.hs @@ -5,6 +5,7 @@ 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 +import qualified Clipboard import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import qualified Data.Sequence as S @@ -28,8 +29,10 @@ data Widgets | WidgetWhyDependsViewport deriving (Show, Eq, Ord) +data Notice = Notice Text Text + data Modal s - = ModalHelp + = ModalNotice Notice | ModalWhyDepends (B.GenericList Widgets Seq (NonEmpty (Path s))) | ModalSearch Text Text (B.GenericList Widgets Seq (Path s)) @@ -177,9 +180,9 @@ app = { B.appDraw = \env@AppEnv {aeOpenModal} -> [ case aeOpenModal of Nothing -> B.emptyWidget - Just ModalHelp -> renderHelpModal Just (ModalWhyDepends l) -> renderWhyDependsModal l - Just (ModalSearch l r xs) -> renderSearchModal l r xs, + Just (ModalSearch l r xs) -> renderSearchModal l r xs + Just (ModalNotice notice) -> renderNotice notice, renderMainScreen env ], B.appChooseCursor = \_ -> const Nothing, @@ -190,12 +193,17 @@ app = | k `elem` [V.KChar 'q', V.KEsc] -> B.halt s (B.VtyEvent (V.EvKey (V.KChar '?') []), Nothing) -> - B.continue s {aeOpenModal = Just ModalHelp} + B.continue s {aeOpenModal = Just (ModalNotice helpNotice)} (B.VtyEvent (V.EvKey (V.KChar 'w') []), Nothing) -> do B.hScrollToBeginning (B.viewportScroll WidgetWhyDependsViewport) B.continue $ showWhyDepends s (B.VtyEvent (V.EvKey (V.KChar '/') []), Nothing) -> B.continue $ showAndUpdateSearch "" "" s + (B.VtyEvent (V.EvKey (V.KChar 'y') []), Nothing) -> do + liftIO (yankToClipboard $ spName (selectedPath s)) + >>= \case + Right () -> B.continue s + Left n -> B.continue s {aeOpenModal = Just (ModalNotice n)} (B.VtyEvent (V.EvKey (V.KChar 's') []), Nothing) -> B.continue $ s @@ -283,8 +291,8 @@ app = selectPath (shortestPathTo (aeActualStoreEnv s) (spName path)) closed - -- help modal - (B.VtyEvent (V.EvKey k []), Just ModalHelp) + -- notices + (B.VtyEvent (V.EvKey k []), Just (ModalNotice _)) | k `elem` [V.KChar 'q', V.KEsc] -> B.continue s {aeOpenModal = Nothing} -- handle our events @@ -314,6 +322,20 @@ app = ] ) +yankToClipboard :: StoreName s -> IO (Either Notice ()) +yankToClipboard p = + Clipboard.copy (toS $ storeNameToPath p) + <&> \case + Right () -> Right () + Left errs -> + Left $ + Notice + "Error" + ( T.intercalate "\n" $ + "Cannot copy to clipboard: " : + map (" " <>) errs + ) + renderMainScreen :: AppEnv s -> B.Widget Widgets renderMainScreen env@AppEnv {aePrevPane, aeCurrPane, aeNextPane} = (B.joinBorders . B.border) @@ -373,15 +395,19 @@ helpText = T.intercalate "\n" [ "hjkl/Arrow Keys : Navigate", - "q/Esc: : Quit / close modal", "w : Open why-depends mode", "/ : Open search mode", "s : Change sort order", - "? : Show help" + "y : Yank selected path to clipboard", + "? : Show help", + "q/Esc: : Quit / close modal" ] -renderHelpModal :: B.Widget a -renderHelpModal = renderModal "Help" (B.txt helpText) +helpNotice :: Notice +helpNotice = Notice "Help" helpText + +renderNotice :: Notice -> B.Widget a +renderNotice (Notice title txt) = renderModal title (B.txt txt) renderWhyDependsModal :: B.GenericList Widgets Seq (NonEmpty (Path s)) -> diff --git a/src/Clipboard.hs b/src/Clipboard.hs new file mode 100644 index 0000000..2286e27 --- /dev/null +++ b/src/Clipboard.hs @@ -0,0 +1,51 @@ +module Clipboard + ( copy, + ) +where + +import qualified Data.ByteString.Lazy as BL +import qualified System.Process.Typed as P + +cmds :: [(Text, [Text])] +cmds = + [ ("xsel", ["-i", "-b"]), + ("xclip", ["-selection", "clipboard"]), + ("wl-copy", []), + ("pbcopy", []) + ] + +runCmd :: Text -> (Text, [Text]) -> IO (Either Text ()) +runCmd txt (cmd, args) = + P.proc (toS cmd) (map toS args) + & P.setStdin (P.byteStringInput $ toUtf8Lazy txt) + & P.readProcess + & try + <&> \case + (Right (ExitSuccess, _, _)) -> Right () + (Right (ExitFailure e, out, err)) -> + Left $ + "Running " <> show (cmd, args) <> " " + <> "failed with exit code " + <> show e + <> ", " + <> "stdout: " + <> decodeUtf8 (BL.toStrict out) + <> ", " + <> "stderr: " + <> decodeUtf8 (BL.toStrict err) + <> "." + (Left (ex :: SomeException)) -> + Left $ + "Running " <> show (cmd, args) <> " " + <> "failed with exception: " + <> show ex + <> "." + +copy :: Text -> IO (Either [Text] ()) +copy txt = go cmds [] + where + go [] errs = return $ Left errs + go (x : xs) errs = + runCmd txt x >>= \case + Right () -> return $ Right () + Left err -> go xs (err : errs)