From 5cbf172547c9d65dd38279300167be93853986e4 Mon Sep 17 00:00:00 2001 From: Joakim Lindeng Engeset Date: Fri, 7 Jul 2023 18:15:00 +0200 Subject: [PATCH] Extract fzf args from map to arg for thread-last - To allow for threading (thread-last) of fzf input args, move the args from `in`-key in opt map to a single arg - Validate args with spec --- README.md | 20 +++++++++++++- src/fzf/core.clj | 36 +++++++++++++++++--------- src/fzf/impl.clj | 59 +++++++++++++++++++++--------------------- test/fzf/impl_test.clj | 16 +++++++----- 4 files changed, 82 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index a95e33d..1834e9f 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,30 @@ Add the following to your `bb.edn` or `deps.edn`: #### Passing data to stdin +> :bulb: Since `v0.0.2`, input-arguments have been moved from the `in`-key in the options map to its own argument to facilitate threading. + +To provide input arguments to fzf via STDIN instead of using the default file selection command, the arguments can be passed as a vector of strings. + ```clojure -(fzf {:in ["one" "two" "three"]}) +(fzf ["one" "two" "three"]) ;; user selects an item "two" ``` +Threading + +```clojure +(->> ["quick" "brown" "fox"] fzf) ;; => fox +``` + +Threading w/options + +```clojure +(->> ["quick" "brown" "fox"] + (map upper-case) + (fzf {:multi true})) ;; => ["QUICK" "FOX"] +``` + ### Available options [fzf](https://github.com/junegunn/fzf) supports a slew of options, so I have only included the ones I frequently use myself. diff --git a/src/fzf/core.clj b/src/fzf/core.clj index dbb162c..925af3f 100644 --- a/src/fzf/core.clj +++ b/src/fzf/core.clj @@ -1,13 +1,12 @@ (ns fzf.core "Public interface to the fzf-wrapper" - (:require [fzf.impl :as i] - [clojure.spec.alpha :as s])) + (:require [clojure.spec.alpha :as s] + [fzf.impl :as i])) (defn fzf "Public interface to fzf. - Options map (all keys are optional): - - in: Vec of args passed as input to fzf as stdin + `opts`: Options map (all keys are optional) - dir: String indicating the startup-dir of the fzf-command - multi: Bool, toggles multi-select in fzf. If true, fzf returns a vector instead of string - preview: String, preview-command for the currently selected item @@ -20,20 +19,33 @@ - tac: Bool, reverse the order of the input - case-insensitive: Bool, toggle case-insensitive search (default: smart-case) - exact: Bool, toggle exact search (default: fuzzy) + + `args`: Input arguments to fzf (optional, list of strings) Examples: (fzf) ;; => \"myfile.txt\" - (fzf :multi true - :in [\"one\" \"two\" \"three\"] - :reverse true) ;; => [\"one\" \"two\"] - + (->> [\"quick\" \"brown\" \"fox\"] + (map clojure.string/upper-case) + fzf) ;; => \"FOX\" + + + (fzf {:multi true + :reverse true} + [\"one \" \"two \" \"three \"]) ;; => [\"one\" \"two\"] + + Returns: - on success with :multi = false (default): the selected item as string - on success with :multi = true: the selected item(s) as vector of strings - on error or ctrl-c: nil" - ([] (i/fzf)) - ([opts] - {:pre [(s/valid? :fzf/opts opts)]} - (i/fzf opts))) + ([] (fzf {} [])) + ([opts-or-args] + (if (map? opts-or-args) + (fzf opts-or-args []) + (fzf {} opts-or-args))) + ([opts args] + {:pre [(s/and (s/valid? :fzf/opts opts) + (s/valid? :fzf/args args))]} + (i/fzf opts args))) diff --git a/src/fzf/impl.clj b/src/fzf/impl.clj index 11961b7..ddddc32 100644 --- a/src/fzf/impl.clj +++ b/src/fzf/impl.clj @@ -1,39 +1,38 @@ (ns fzf.impl "Implementation of fzf-wrapper" - (:require [babashka.process :as p] - [babashka.fs :as fs] + (:require [babashka.fs :as fs] + [babashka.process :as p] [clojure.string :as str])) (defn parse-opts "Parse fzf and process options" - ([] (parse-opts {})) - ([opts] - (let [{:keys [in dir multi preview tac case-insensitive exact reverse height] - {:keys [header-str header-lines header-first]} :header} opts] - {:cmd (cond-> ["fzf"] - multi (conj "--multi") - reverse (conj "--reverse") - tac (conj "--tac") - case-insensitive (conj "-i") - exact (conj "--exact") - preview (conj "--preview" preview) - header-str (conj "--header" header-str) - header-lines (conj "--header-lines" header-lines) - header-first (conj "--header-first") - height (conj "--height" height)) - :opts (cond-> {:in :inherit - :out :string - :err :inherit} - in (assoc :in (str/join "\n" in)) - dir (assoc :dir (-> dir fs/expand-home str)))}))) + [opts args] + (let [{:keys [dir multi preview tac case-insensitive exact reverse height] + {:keys [header-str header-lines header-first]} :header} opts] + {:cmd (cond-> ["fzf"] + multi (conj "--multi") + reverse (conj "--reverse") + tac (conj "--tac") + case-insensitive (conj "-i") + exact (conj "--exact") + preview (conj "--preview" preview) + header-str (conj "--header" header-str) + header-lines (conj "--header-lines" header-lines) + header-first (conj "--header-first") + height (conj "--height" height)) + :opts (cond-> {:in :inherit + :out :string + :err :inherit} + (not-empty args) (assoc :in (str/join "\n" args)) + dir (assoc :dir (-> dir fs/expand-home str)))})) (defn fzf "Internal interface to fzf" - ([] (fzf {})) - ([opts] (let [multi (:multi opts) - {:keys [cmd opts]} (parse-opts opts) - {:keys [out exit]} @(p/process cmd opts)] - (if (zero? exit) - (cond-> (str/trim out) - multi str/split-lines) - nil)))) + [opts args] + (let [multi (:multi opts) + {:keys [cmd opts]} (parse-opts opts args) + {:keys [out exit]} @(p/process cmd opts)] + (if (zero? exit) + (cond-> (str/trim out) + multi str/split-lines) + nil))) diff --git a/test/fzf/impl_test.clj b/test/fzf/impl_test.clj index c3770dd..e91c5da 100644 --- a/test/fzf/impl_test.clj +++ b/test/fzf/impl_test.clj @@ -5,7 +5,7 @@ (t/deftest parse-opts-test (t/testing "no options produce expected defaults" (t/is (= {:cmd ["fzf"], :opts {:in :inherit, :out :string, :err :inherit}} - (i/parse-opts)))) + (i/parse-opts {} [])))) (t/testing "adding fzf flags produce correct command string" (t/is (= ["fzf" "--multi" "--reverse" "--tac" "-i" "--exact" "--preview" "echo {}" "--header" "header-text" "--header-lines" 2 "--header-first" "--height" "10%"] (:cmd (i/parse-opts {:multi true @@ -17,14 +17,18 @@ :header {:header-str "header-text" :header-lines 2 :header-first true} - :height "10%"}))))) + :height "10%"} + []))))) (t/testing "providing input-arguments maps them to process stdin" (t/is (= {:in "one\ntwo\nthree", :out :string, :err :inherit} - (:opts (i/parse-opts {:in ["one" "two" "three"]})))) + (:opts (i/parse-opts {} ["one" "two" "three"])))) (t/is (= {:in "1\n2\n3", :out :string, :err :inherit} - (:opts (i/parse-opts {:in [1 2 3]})))) + (:opts (i/parse-opts {} [1 2 3])))) (t/is (= {:in ":one\n:two\n:three", :out :string, :err :inherit} - (:opts (i/parse-opts {:in [:one :two :three]}))))) + (:opts (i/parse-opts {} [:one :two :three]))))) + (t/testing "providing no input-arguments should retain default proc options" + (t/is (= {:in :inherit, :out :string, :err :inherit} + (:opts (i/parse-opts {} []))))) (t/testing "option for startup-dir to correctly mapped to process options" (t/is (= {:in :inherit, :out :string, :err :inherit, :dir "/tmp"} - (:opts (i/parse-opts {:dir "/tmp"})))))) + (:opts (i/parse-opts {:dir "/tmp"} []))))))