Skip to content

Commit

Permalink
Move command output to placeholders
Browse files Browse the repository at this point in the history
A couple of things:

 - Remove command output format, handle in placeholders instead.
 - Add support for a "column" format (requires titles).

Ignore the output of commands in each `nixon` command definition, but
rather interpret the output for each placeholder where the command is
used. This allows extracting data differently from commands based on how
the selection will be used.
  • Loading branch information
myme committed May 19, 2024
2 parents adb9b7e + f1ae2a3 commit 4dc52cf
Show file tree
Hide file tree
Showing 17 changed files with 244 additions and 174 deletions.
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

0 comments on commit 4dc52cf

Please sign in to comment.