Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move command output format handling to placeholders #6

Merged
merged 7 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Install dependencies
run: |
cabal update
cabal install hlint
cabal install --overwrite-policy=always hlint
cabal build --only-dependencies --enable-tests --enable-benchmarks

- name: Hlint
Expand Down
15 changes: 8 additions & 7 deletions extra/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ x-terminal-emulator
emacs
```

### `json-greetings` {.json}
### `json-greetings`

```json
[
Expand All @@ -70,7 +70,7 @@ emacs

Select from one or more greetings in a JSON format.

```bash <{json-greetings | multi}
```bash <{json-greetings | json | multi}
cat
```

Expand All @@ -79,15 +79,16 @@ cat
Use `nmcli` to list available networks.

```bash
nmcli -t connection | cut -d':' -f1
nmcli connection
```

### `network-connect`

Use the `networks` placeholder to select a network to connect to.

```bash ${networks}
nmcli connection up "$1"
```bash ${networks | cols 1}
# nmcli connection up "$1"
echo "$@"
```

### `pd`
Expand Down Expand Up @@ -115,7 +116,7 @@ nix-shell

## npm stuff {type="npm"}

### `npm-scripts` {.json}
### `npm-scripts`

List all `npm` scripts in a `package.json`.

Expand All @@ -127,7 +128,7 @@ jq '.scripts | to_entries | map({ title: (.key + " → " + .value), value: .key

Run a `npm` script from `package.json`.

```bash ${npm-scripts}
```bash ${npm-scripts | json}
npm run "$1"
```

Expand Down
1 change: 1 addition & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tests:
- test
dependencies:
- base
- containers
- hspec
- nixon
- QuickCheck
Expand Down
2 changes: 1 addition & 1 deletion src/Nixon.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Nixon.Language (Language (..), fromFilePath)
import Nixon.Logging (log_error, log_info)
import Nixon.Prelude
import Nixon.Process (run)
import Nixon.Project (Project, project_path, inspectProjects)
import Nixon.Project (Project, inspectProjects, project_path)
import qualified Nixon.Project as P
import Nixon.Select (Candidate (..), Selection (..), SelectionType (..))
import qualified Nixon.Select as Select
Expand Down
4 changes: 2 additions & 2 deletions src/Nixon/Backend/Fzf.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import Nixon.Project
( Project (projectDir, projectName),
project_path,
)
import Nixon.Select (Candidate, Selection (..), SelectionType (..), withProcessSelection)
import Nixon.Select (Candidate, Selection (..), SelectionType (..))
import qualified Nixon.Select as Select
import Nixon.Utils
( implode_home,
Expand Down Expand Up @@ -77,7 +77,7 @@ fzfBackend cfg =
in Backend
{ projectSelector = fzfProjects . fzf_opts,
commandSelector = fzfProjectCommand fzf_opts',
selector = withProcessSelection (fzf . fzf_opts)
selector = fzf . fzf_opts
}

data FzfOpts = FzfOpts
Expand Down
4 changes: 2 additions & 2 deletions src/Nixon/Backend/Rofi.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import qualified Nixon.Config.Types as Config
import Nixon.Prelude
import Nixon.Process (arg, arg_fmt, build_args, flag)
import Nixon.Project (Project (projectDir, projectName))
import Nixon.Select (Candidate, Selection (..), SelectionType (..), withProcessSelection)
import Nixon.Select (Candidate, Selection (..), SelectionType (..))
import qualified Nixon.Select as Select
import Nixon.Utils (implode_home, shell_to_list, toLines, (<<?))
import Turtle
Expand Down Expand Up @@ -56,7 +56,7 @@ rofiBackend cfg =
in Backend
{ projectSelector = rofiProjects . rofi_opts,
commandSelector = const $ rofiProjectCommand rofi_opts',
selector = withProcessSelection (rofi . rofi_opts)
selector = rofi . rofi_opts
}

-- | Data type for command line options to rofi
Expand Down
9 changes: 0 additions & 9 deletions src/Nixon/Command.hs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
module Nixon.Command
( Command (..),
CommandLocation (..),
CommandOutput (..),
Language (..),
empty,
is_bg_command,
(<!),
description,
bg,
json,
show_command,
show_command_with_description,
)
Expand All @@ -32,7 +30,6 @@ data Command = Command
cmdIsBg :: Bool,
-- | Command should be hidden from selection
cmdIsHidden :: Bool,
cmdOutput :: CommandOutput,
-- | Command location in configuration
cmdLocation :: Maybe CommandLocation
}
Expand All @@ -58,12 +55,9 @@ empty =
cmdPlaceholders = [],
cmdIsBg = False,
cmdIsHidden = False,
cmdOutput = Lines,
cmdLocation = Nothing
}

data CommandOutput = Lines | JSON deriving (Eq, Show)

show_command :: Command -> Text
show_command cmd = T.unwords $ cmdName cmd : map (format ("${" % s % "}") . P.name) (cmdPlaceholders cmd)

Expand All @@ -83,8 +77,5 @@ description d cmd = cmd {cmdDesc = Just d}
bg :: Bool -> Command -> Command
bg g cmd = cmd {cmdIsBg = g}

json :: Bool -> Command -> Command
json j cmd = cmd {cmdOutput = if j then JSON else Lines}

is_bg_command :: Command -> Bool
is_bg_command _ = False
16 changes: 14 additions & 2 deletions src/Nixon/Command/Placeholder.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Nixon.Command.Placeholder
( Placeholder (..),
PlaceholderType (..),
PlaceholderFormat (..),
PlaceholderType (..)
)
where

Expand All @@ -9,14 +10,25 @@ import Nixon.Prelude
data PlaceholderType = Arg | EnvVar {_envName :: Text} | Stdin
deriving (Eq, Show)

data PlaceholderFormat
= -- | Interpret output as columns and extract the specified columns
Columns [Int]
| -- | Interpret output as fields and extract the specified fields
Fields [Int]
| -- | Interpret output as plain lines
Lines
| -- | Parse output as JSON
JSON
deriving (Eq, Show)

-- | Placeholders for environment variables
data Placeholder = Placeholder
{ -- | Type of placeholder
type_ :: PlaceholderType,
-- | The command it's referencing
name :: Text,
-- | The field numbers to extract
fields :: [Integer],
format :: PlaceholderFormat,
-- | If the placeholder can select multiple
multiple :: Bool,
-- | Pre-expanded value of the placeholder
Expand Down
59 changes: 35 additions & 24 deletions src/Nixon/Command/Run.hs
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
{-# LANGUAGE OverloadedRecordDot #-}

module Nixon.Command.Run
( resolveCmd,
resolveEnv,
runCmd,
)
where

import Control.Arrow ((&&&))
import Control.Monad (foldM)
import Data.Aeson (eitherDecodeStrict)
import Data.Foldable (find)
import qualified Data.Text as T
import Nixon.Command (Command, CommandOutput (..))
import Nixon.Command (Command)
import qualified Nixon.Command as Cmd
import Nixon.Command.Find (findProjectCommands)
import qualified Nixon.Command.Placeholder as Cmd
import qualified Nixon.Command.Placeholder as P
import Nixon.Evaluator (evaluate, getEvaluator)
import Nixon.Format (parseColumns, pickColumns, pickFields)
import Nixon.Prelude
import Nixon.Process (run_with_output)
import qualified Nixon.Process
import Nixon.Project (Project)
import qualified Nixon.Project as Project
import Nixon.Select (Selection (..), Selector, selector_fields, selector_multiple)
import Nixon.Select (Selection (..), Selector, selector_format, selector_multiple)
import qualified Nixon.Select as Select
import Nixon.Types (Nixon)
import Nixon.Utils (toLines)
import Nixon.Utils (shell_to_list, toLines)
import Turtle (Shell, cd, format, fp, select, stream)
import qualified Turtle.Bytes as BS
import Turtle.Line (lineToText)
Expand All @@ -33,54 +37,54 @@ runCmd selector project cmd args = do
let projectPath = Project.project_path project
project_selector select_opts shell' =
cd projectPath
>> selector (select_opts <> Select.title (Cmd.show_command cmd)) shell'
>> selector (select_opts `Select.title` Cmd.show_command cmd) shell'
(stdin, args', env') <- resolveEnv project project_selector cmd args
let pwd = Cmd.cmdPwd cmd <|> Just projectPath
evaluate cmd args' pwd env' (toLines <$> stdin)

-- | Resolve all command placeholders to either stdin input, positional arguments or env vars.
resolveEnv :: Project -> Selector Nixon -> Command -> [Text] -> Nixon (Maybe (Shell Text), [Text], Nixon.Process.Env)
resolveEnv project selector cmd args = do
let mappedArgs = zipArgs (Cmd.cmdPlaceholders cmd) args
let mappedArgs = zipArgs cmd.cmdPlaceholders args
(stdin, args', envs) <- resolveEnv' project selector mappedArgs
pure (stdin, args', nixonEnvs ++ envs)
where
nixonEnvs = [("nixon_project_path", format fp (Project.project_path project))]

-- | Zip placeholders with arguments, filling in missing placeholders with overflow arguments.
zipArgs :: [Cmd.Placeholder] -> [Text] -> [(Cmd.Placeholder, Select.SelectorOpts)]
zipArgs [] args' = map ((, Select.defaults) . argOverflow) args'
zipArgs :: [P.Placeholder] -> [Text] -> [(P.Placeholder, Select.SelectorOpts)]
zipArgs [] args' = map ((,Select.defaults) . argOverflow) args'
where
argOverflow = Cmd.Placeholder Cmd.Arg "arg" [] False . pure
zipArgs placeholders [] = map (, Select.defaults) placeholders
argOverflow = P.Placeholder P.Arg "arg" P.Lines False . pure
zipArgs placeholders [] = map (,Select.defaults) placeholders
zipArgs (p : ps) (a : as) = (p, Select.search a) : zipArgs ps as

-- | Resolve all command placeholders to either stdin input, positional arguments or env vars.
resolveEnv' :: Project -> Selector Nixon -> [(Cmd.Placeholder, Select.SelectorOpts)] -> Nixon (Maybe (Shell Text), [Text], Nixon.Process.Env)
resolveEnv' :: Project -> Selector Nixon -> [(P.Placeholder, Select.SelectorOpts)] -> Nixon (Maybe (Shell Text), [Text], Nixon.Process.Env)
resolveEnv' project selector = foldM resolveEach (Nothing, [], [])
where
resolveEach (stdin, args', envs) (Cmd.Placeholder envType cmdName fields multiple value, select_opts) = do
resolveEach (stdin, args', envs) (P.Placeholder envType cmdName format' multiple value, select_opts) = do
resolved <- case value of
[] -> do
cmd' <- assertCommand cmdName
let select_opts' =
select_opts
{ selector_fields = fields,
{ selector_format = format',
selector_multiple = Just multiple
}
resolveCmd project selector cmd' select_opts'
_ -> pure value
case envType of
-- Standard inputs are concatenated
Cmd.Stdin ->
P.Stdin ->
let stdinCombined = Just $ case stdin of
Nothing -> select resolved
Just prev -> prev <|> select resolved
in pure (stdinCombined, args', envs)
-- Each line counts as one positional argument
Cmd.Arg -> pure (stdin, args' <> resolved, envs)
P.Arg -> pure (stdin, args' <> resolved, envs)
-- Environment variables are concatenated into space-separated line
Cmd.EnvVar name -> pure (stdin, args', envs <> [(name, T.unwords resolved)])
P.EnvVar name -> pure (stdin, args', envs <> [(name, T.unwords resolved)])

assertCommand cmd_name = do
cmd' <- find ((==) cmd_name . Cmd.cmdName) <$> findProjectCommands project
Expand All @@ -93,14 +97,21 @@ resolveCmd project selector cmd select_opts = do
let projectPath = Just (Project.project_path project)
linesEval <- getEvaluator (run_with_output stream) cmd args projectPath env' (toLines <$> stdin)
jsonEval <- getEvaluator (run_with_output BS.stream) cmd args projectPath env' (BS.fromUTF8 <$> stdin)
selection <- selector select_opts $ do
case Cmd.cmdOutput cmd of
Lines -> Select.Identity . lineToText <$> linesEval
JSON -> do
output <- BS.strict jsonEval
case eitherDecodeStrict output :: Either String [Select.Candidate] of
Left err -> error err
Right candidates -> select candidates
selection <- selector select_opts $ case select_opts.selector_format of
P.Columns cols -> do
let parseColumns' = map T.unwords . pickColumns cols . parseColumns
(title, value) <- (drop 1 &&& parseColumns') . map lineToText <$> shell_to_list linesEval
select $ zipWith Select.WithTitle title value
P.Fields fields -> do
let parseFields' = T.unwords . pickFields fields . T.words
(title, value) <- (id &&& map parseFields') . map lineToText <$> shell_to_list linesEval
select $ zipWith Select.WithTitle title value
P.Lines -> Select.Identity . lineToText <$> linesEval
P.JSON -> do
output <- BS.strict jsonEval
case eitherDecodeStrict output :: Either String [Select.Candidate] of
Left err -> error err
Right candidates -> select candidates
case selection of
Selection _ result -> pure result
_ -> error "Argument expansion aborted"
Loading
Loading