@@ -7,11 +7,13 @@ module Unison.CommandLine.FuzzySelect
7
7
isFZFInstalled ,
8
8
fzfPathEnvVar ,
9
9
Options (.. ),
10
+ FuzzySelections (.. ),
10
11
defaultOptions ,
11
12
)
12
13
where
13
14
14
- import Control.Monad.Except (runExceptT , throwError )
15
+ import Control.Monad.Except (throwError )
16
+ import Control.Monad.Trans.Except
15
17
import Data.Set qualified as Set
16
18
import Data.Text qualified as Text
17
19
import Data.Text.IO qualified as Text
@@ -20,6 +22,7 @@ import System.Environment (lookupEnv)
20
22
import System.IO (BufferMode (NoBuffering ), hPutStrLn , stderr )
21
23
import System.IO.Unsafe (unsafePerformIO )
22
24
import Unison.Prelude
25
+ import Unison.Util.Monoid qualified as Monoid
23
26
import UnliftIO qualified
24
27
import UnliftIO.Directory (findExecutable )
25
28
import UnliftIO.Exception (bracket )
@@ -57,28 +60,33 @@ defaultOptions =
57
60
}
58
61
59
62
-- | Convert options into command-line args for fzf
60
- optsToArgs :: Options -> [String ]
61
- optsToArgs opts =
63
+ optsToArgs :: Options -> Bool -> [String ]
64
+ optsToArgs opts useNumberings =
62
65
defaultArgs <> case opts of
63
66
Options {allowMultiSelect = True } -> [" -m" ]
64
67
_ -> []
65
68
where
66
69
defaultArgs =
67
- -- Don 't show or match on the first column of input.
70
+ -- When using numberings, don 't show or match on the first column of input.
68
71
-- This allows us to prepend each line with a number, and use that number to determine
69
72
-- which values from the input list were selected.
70
- [ " --with-nth" ,
71
- " 2.." ,
72
- -- Use only half the screen (it's nice to see what you were working on when searching)
73
- " --height=50%" ,
74
- -- But if 50% of the screen is too small, ensure show at least 10 results.
75
- " --min-height=10"
76
- ]
73
+ Monoid. whenM
74
+ useNumberings
75
+ [" --with-nth" , " 2.." ]
76
+ <> [ -- Use only half the screen (it's nice to see what you were working on when searching)
77
+ " --height=50%" ,
78
+ -- But if 50% of the screen is too small, ensure show at least 10 results.
79
+ " --min-height=10"
80
+ ]
81
+
82
+ data FuzzySelections a where
83
+ SelectFromChoices :: (a -> Text ) -> [a ] -> FuzzySelections a
84
+ SelectFiles :: FuzzySelections Text
77
85
78
86
-- | Allows prompting the user to interactively fuzzy-select a result from a list of options, currently shells out to `fzf` under the hood.
79
87
-- If fzf is missing, or an error (other than ctrl-c) occurred, returns Nothing.
80
- fuzzySelect :: forall a . Options -> ( a -> Text ) -> [ a ] -> IO (Maybe [a ])
81
- fuzzySelect opts intoSearchText choices =
88
+ fuzzySelect :: forall a . Options -> FuzzySelections a -> IO (Maybe [a ])
89
+ fuzzySelect opts selections =
82
90
UnliftIO. handleAny handleException
83
91
. handleError
84
92
. restoreBuffering
@@ -88,12 +96,36 @@ fuzzySelect opts intoSearchText choices =
88
96
liftIO fzfExecutable >>= \ case
89
97
Nothing -> throwError " I couldn't find the `fzf` executable on your path, consider installing `fzf` to enable fuzzy searching."
90
98
Just fzfPath -> pure fzfPath
91
- let fzfArgs :: [String ] =
92
- optsToArgs opts
93
- let numberedChoices :: [(Int , a )] =
94
- zip [0 .. ] choices
95
- let searchTexts :: [Text ] =
96
- (\ (n, ch) -> tShow (n) <> " " <> intoSearchText ch) <$> numberedChoices
99
+ case selections of
100
+ SelectFromChoices intoSearchText choices -> do
101
+ let fzfArgs :: [String ] =
102
+ optsToArgs opts True
103
+ let numberedChoices :: [(Int , a )] =
104
+ zip [0 .. ] choices
105
+ let searchTexts :: [Text ] =
106
+ (\ (n, ch) -> tShow (n) <> " " <> intoSearchText ch) <$> numberedChoices
107
+
108
+ result <- fzfWithChoices fzfPath fzfArgs searchTexts
109
+ -- Since we prefixed every search term with its number earlier, we know each result
110
+ -- is prefixed with a number, we need to parse it and use it to select the matching
111
+ -- value from our input list.
112
+ pure $ case result of
113
+ Left _ -> Nothing
114
+ Right selections ->
115
+ selections
116
+ & mapMaybe (readMaybe @ Int . Text. unpack . Text. takeWhile (/= ' ' ))
117
+ & Set. fromList
118
+ & ( \ selectedNumbers ->
119
+ numberedChoices
120
+ & mapMaybe (\ (n, a) -> if n `Set.member` selectedNumbers then Just a else Nothing )
121
+ )
122
+ & Just
123
+ SelectFiles -> do
124
+ let fzfArgs :: [String ] = optsToArgs opts False
125
+ eitherToMaybe <$> fzfFileSelector fzfPath fzfArgs
126
+ where
127
+ fzfWithChoices :: FilePath -> [String ] -> [Text ] -> ExceptT Text IO (Either SomeException [Text ])
128
+ fzfWithChoices fzfPath fzfArgs searchTexts = do
97
129
let fzfProc :: Proc. CreateProcess =
98
130
(Proc. proc fzfPath fzfArgs)
99
131
{ Proc. std_in = Proc. CreatePipe ,
@@ -104,28 +136,27 @@ fuzzySelect opts intoSearchText choices =
104
136
-- Generally no-buffering is helpful for highly interactive processes.
105
137
hSetBuffering stdin NoBuffering
106
138
hSetBuffering stdin' NoBuffering
107
- result <- liftIO . UnliftIO. tryAny $ do
139
+ liftIO . UnliftIO. tryAny $ do
108
140
-- Dump the search terms into fzf's stdin
109
141
traverse_ (Text. hPutStrLn stdin') searchTexts
110
142
-- Wire up the interactive terminal to fzf now that the inputs have been loaded.
111
143
hDuplicateTo stdin stdin'
112
144
void $ Proc. waitForProcess procHandle
113
145
Text. lines <$> liftIO (Text. hGetContents stdout')
114
- -- Since we prefixed every search term with its number earlier, we know each result
115
- -- is prefixed with a number, we need to parse it and use it to select the matching
116
- -- value from our input list.
117
- pure $ case result of
118
- Left _ -> Nothing
119
- Right selections ->
120
- selections
121
- & mapMaybe (readMaybe @ Int . Text. unpack . Text. takeWhile (/= ' ' ))
122
- & Set. fromList
123
- & ( \ selectedNumbers ->
124
- numberedChoices
125
- & mapMaybe (\ (n, a) -> if n `Set.member` selectedNumbers then Just a else Nothing )
126
- )
127
- & Just
128
- where
146
+ fzfFileSelector :: FilePath -> [String ] -> ExceptT Text IO (Either SomeException [Text ])
147
+ fzfFileSelector fzfPath fzfArgs = do
148
+ let fzfProc :: Proc. CreateProcess =
149
+ (Proc. proc fzfPath fzfArgs)
150
+ { Proc. std_in = Proc. Inherit ,
151
+ Proc. std_out = Proc. CreatePipe ,
152
+ Proc. delegate_ctlc = True
153
+ }
154
+ (_stdin', Just stdout', _, procHandle) <- Proc. createProcess fzfProc
155
+ -- Generally no-buffering is helpful for highly interactive processes.
156
+ hSetBuffering stdin NoBuffering
157
+ liftIO . UnliftIO. tryAny $ do
158
+ void $ Proc. waitForProcess procHandle
159
+ Text. lines <$> liftIO (Text. hGetContents stdout')
129
160
handleException :: SomeException -> IO (Maybe [a ])
130
161
handleException err = traceShowM err *> hPutStrLn stderr " Oops, something went wrong. No input selected." *> pure Nothing
131
162
handleError :: IO (Either Text (Maybe [a ])) -> IO (Maybe [a ])
0 commit comments