├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── accountsof.clj ├── cljminimal.clj ├── depo.clj ├── install.clj ├── jrun.clj ├── keepbooks.clj ├── on-modify-log.clj ├── projectsof.clj ├── resumetask.clj ├── startnewtask.clj ├── stoptasks.clj └── taskinfo.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/ 2 | .lsp/ 3 | .nrepl-port 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | - [584d3d0](../../commit/584d3d04b3d9d2a9d1fdd79789e7c4908daa40be) - added [create-clj-minimal](../41de9d4fd0103c7b1cefa4b47439054353a59a91/create-clj-minimal) shell script 3 | - [4b8c492](../../commit/4b8c492ecd1725646dbff502a19a77cc73c52747) - added [stoptasks](../41de9d4fd0103c7b1cefa4b47439054353a59a91/stoptasks) shell script 4 | - [41de9d4](../../commit/41de9d4fd0103c7b1cefa4b47439054353a59a91) - added [starttasks](../41de9d4fd0103c7b1cefa4b47439054353a59a91/starttask) shell script 5 | - [8bd623e](../../commit/8bd623ef16068c4ed0ece1d1df32ed6bb0b210b8) - ported [create-clj-minimal](../41de9d4fd0103c7b1cefa4b47439054353a59a91/create-clj-minimal) to Clojure. It is now called [cljminimal](./cljminimal.clj) 6 | - [3a57c9a](../../commit/3a57c9abac263ceea7add9513b70868862b98d1d) - ported [starttasks](../41de9d4fd0103c7b1cefa4b47439054353a59a91/starttask) to Clojure. It is now called [startnewtask](./startnewtask.clj) 7 | - [e610b0b](../../commit/e610b0b5c82580de74f6ccb644e9e092f9f7e130) - ported [stoptasks](./stoptasks.clj) to Clojure. 8 | - [80e3f79](../../commit/80e3f792e56c5b620fa5ff1a6493c8b913188df7) - added [install](./install.clj) script 9 | - [2ec63e7](../../commit/2ec63e7e77a2adb9f3b2e22090f85a911868f238) - added [uninstall](../2ec63e7e77a2adb9f3b2e22090f85a911868f238/uninstall-some-utils.clj) script 10 | - [f1d42f7](../../commit/f1d42f7bc172d9ffdf51419d17b5d7792dabe70e) - removed the [uninstall](../2ec63e7e77a2adb9f3b2e22090f85a911868f238/uninstall-some-utils.clj) script in favor of a programmatically created uninstall script. Installing these scripts now automatically creates `uninstall-some-scripts`. 11 | - [a020b2a](../../commit/a020b2aba3fdbcc132e53df2b4859d5aab88e9f1) - added [keepbooks](./keepbooks.clj) 12 | - [2d46c23](../../commit/2d46c233a158950a3b2860f405a7dfb81484e06e) - fix [#1](../../issues/1) 13 | - [a7c0817](../../commit/a7c081747dc0ec4404f6a17dc3f9141316cdc534) - added [on-modify-log](./on-modify-log.clj) Taskwarrior hook, [resumetask](./resumetask.clj) and updated [install](./install.clj) script 14 | - [f2b0214](../../commit/f2b021434554a3491c5cf07aced3a33479e662d1) - added [taskinfo](./taskinfo.clj) 15 | - [e725018](../../commit/e7250185cc92cb0d2626b0048817ccd8a4e3cb5d) - added [jrun](./jrun.clj) 16 | - [fd7b165](../../commit/fd7b165136f06fcd8c018401942c008ba0a261da) - added [projectsof](./projectsof.clj) 17 | - [371c1ea](../../commit/371c1ea57bf5ebf3da98423552edba18d66f6957) - added clj to [projectsof](./projectsof.clj) and numbered output 18 | - [00520bf](../../commit/00520bf253a05fdcd2253c10b61be33b4c363cfa) - implemented feature [#3](../../issues/3) interactive entry to [keepbooks](./keepbooks.clj) 19 | - [f5ceb36](../../commit/f5ceb365150694ab86475a344da9c16946852b90) - [2.7.0](../../releases/tag/2.7.0) added [accountsof](./accountsof.clj) 20 | - [8cf25a2](../../commit/8cf25a240790e93bef0b53e40dcb4131e9677106) - [2.8.0](../../releases/tag/2.8.0) added [depo](./depo.clj) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Somē Cho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Somē's utility scripts 2 | 3 | Here are some utility scripts I wrote for myself. At first I wrote the scripts in a shell scripting language. But then I discovered [Babashka](https://github.com/babashka/babashka) and I love Clojure. I decided to port all the scripts to Clojure instead. You will need [Babashka](https://github.com/babashka/babashka) to run these scripts. These are helper tools for [Clj](https://clojure.org/guides/deps_and_cli), Java, [Ledger](https://github.com/ledger/ledger) and [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior). 4 | 5 | ### [Scripts](#scripts) included: 6 | 1. [accountsof](#accountsof) - outputs the name of all accounts used in a [Ledger](https://github.com/ledger/ledger) journal file 7 | 2. [cljminimal](#cljminimal) - creates an ultra barebones deps.edn [clj](https://clojure.org/guides/deps_and_cli) project for quick hacking 8 | 3. [depo](#depo) - adds dependencies to Clojure projects. Supports `deps.edn`,`project.clj`,`shadow-cljs.edn`. 9 | 4. [jrun](#jrun) - single file Java runner 10 | 5. [keepbooks](#keepbooks) - simple transaction entry helper for [Ledger](https://github.com/ledger/ledger) CLI accounting. Supports interactive entry. 11 | 6. [on-modify-log](#on-modify-log) - a [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) hook to log the latest modified task 12 | 7. [projectsof](#projectsof) - finds directories of certain project types 13 | 8. [resumetask](#resumetask) - resumes latest modified [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task 14 | 9. [startnewtask](#startnewtask) - creates and starts a new [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task 15 | 10. [stoptasks](#stoptasks) - stops all active [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) tasks 16 | 11. [taskinfo](#taskinfo) - prints the attribute of a [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task 17 | 18 | ## Installation 19 | You need to first [install Babashka](https://github.com/babashka/babashka#quickstart). 20 | ```sh 21 | git clone https://github.com/somecho/utility-scripts 22 | cd utility-scripts 23 | ./install.clj 24 | ``` 25 | This will copy all the scripts into `~/.local/bin`. Make sure `~/.local/bin` is in your path to call the scripts globally. 26 | 27 | ### Uninstalling 28 | To uninstall, simply call `uninstall-some-scripts` and all the scripts will be deleted from `~/.local/bin`. 29 | 30 | ## [accountsof](./accountsof.clj) 31 | Outputs the names of all the accounts used in a [Ledger](https://github.com/ledger/ledger) journal file. Example: `accountsof LEDGERFILE`. 32 | 33 | ## [cljminimal](./cljminimal.clj) 34 | A script to create an ultraminimal clj project with an empty deps.edn and a singular hello world main function. To use, simply call `cljminimal my-minimal-clj-project` and a project called `my-minimal-clj-project` will be created for you. Mainly used for quick hacking and throwaway prototyping. 35 | 36 | ## [depo](./depo.clj) 37 | Adds dependencies to Clojure projects. To use, run the script at the root of a project containing a `deps.edn`, `project.clj` or `shadow-cljs.edn` file. If multiple config files are present, the first config file in the order of `deps`,`project`,`shadow-cljs` will be selected. 38 | ### Usage 39 | ```sh 40 | depo add reagent 41 | # Added [reagent "1.2.0"] 42 | ``` 43 | If multiple config files are present, you can use `-f` to specify which file to add a dependency to. 44 | ```sh 45 | depo add reagent -f deps.edn 46 | # Added {reagent/reagent {:mvn/version 1.2.0}} 47 | ``` 48 | You can also specify a version. 49 | ```sh 50 | depo add reagent 1.1.0 51 | # Added [reagent "1.1.0"] 52 | ``` 53 | Depo can currently only search for dependencies from Clojars. For a more powerful alternative, checkout [neil](https://github.com/babashka/neil). 54 | 55 | ## [jrun](./jrun.clj) 56 | Compiles and runs a single java file. Mainly used for quick iteration of ideas. For example, you can run it in Vim with `:!jrun App.java` and see the output in a Vim buffer without leaving your current buffer. 57 | ### Usage 58 | ```sh 59 | jrun JAVAFILE 60 | ``` 61 | The `JAVAFILE` argument is glob-searched, so you can use `App.java` or `App` and it will still run. 62 | 63 | ## [keepbooks](./keepbooks.clj) 64 | A helper script to enter a simple transaction into a [Ledger](https://github.com/ledger/ledger) file. 65 | 66 | ### Single entry mode 67 | The script has the following format in single entry mode: 68 | ```sh 69 | keepbooks -f LEDGERFILE -d DATE PAYEE? ACCOUNT_TO_DEBIT ACCOUNT_TO_CREDIT AMOUNT CURRENCY 70 | ``` 71 | The `-d DATE` field is optional. If this flag is ommitted, the current date will be used. The `PAYEE` field is also optional. If the `PAYEE` is ommitted, no payee will be entered in the transaction. The other fields `ACCOUNT_TO_DEBIT`, `ACCOUNT_TO_CREDIT`, `AMOUNT` and `CURRENCY` are required fields. The ordering is strict. Upon entering a successful command, the ledger entry will be written into the ledger file provided and also printed out in the commandline. 72 | ```sh 73 | keepbooks -f 2023.ledger -d 2023/07/20 Sushi Bar Expenses:Restaurant Assets:Bank 30.00 EUR 74 | # prints out: 75 | # 2023/07/20 Sushi Bar 76 | # Expenses:Restaurant 30.00 EUR 77 | # Assets:Bank 78 | ``` 79 | 80 | ### Interactive mode 81 | `keepbooks` also supports interactive entry. Simply call `keepbooks -f LEDGERFILE` without any arguments and you will be prompted to enter your transaction. 82 | 83 | ## [on-modify-log](./on-modify-log.clj) 84 | A [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) hook to log latest modified task. This script is _not_ installed in `~/.local/bin`. Instead, it requires you to copy it to your Taskwarrior's hooks folder. This is usually `~/.task/hooks`. Every time a task is modified, it writed the UUID of the task in a file called `last-modified.data` in your Taskwarrior's `data.location`. **This hook is required for the [resumetask](#resumetask) script to work.** 85 | 86 | ## [projectsof](./projectsof.clj) 87 | Searches the current working directory for project directories of a certain type. For example, calling `projectsof java` will return all the directories which are java projects. **Requires [`rg`](https://github.com/BurntSushi/ripgrep) to run.** 88 | 89 | ### Flags 90 | - `-n` - displays numbered rows 91 | - `-i NUMBER` - outputs directory with line number `-i` 92 | 93 | ### Currently supported project types 94 | 1. Clojure/Clj 95 | 2. Java 96 | 97 | ## [resumetask](./resumetask.clj) 98 | Ever wanted to just restart the [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task you stopped right before a break? With this script, you can just pick up where you left off by calling `resumetask`. No more trying to figure what ID your task has! **This script requires the [on-modify-log](#on-modify-log) hook to work and the [taskinfo](#taskinfo) script to work.** 99 | 100 | ### Why use hooks? 101 | Some people suggest having a shell alias that starts a task and exports it as an environment variable. But since I use [Syncthing](https://github.com/syncthing/syncthing) to sync my tasks across devices, this will not work if I stopped a task on one device and want to resume it on another. By saving the last modified task's UUID in Taskwarrior's `data.location`, I can have the UUID synced as well. 102 | 103 | ## [startnewtask](./startnewtask.clj) 104 | Creates and immediately starts a [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task. Use this as you would `task add`. 105 | ```sh 106 | task add +admin +bookkeeping track finance # adds a task to Taskwarrior 107 | startnewtask +admin +bookkeeping track finance # adds and starts task 108 | ``` 109 | ## [stoptasks](./stoptasks.clj) 110 | Stops all active [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) tasks. Every tried `task stop` and gotten an error? Yeah, me too. Now you can stop all active tasks with a single `stoptasks`. 111 | ## [taskinfo](./taskinfo.clj) 112 | Prints the attribute of a [Taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) task. Commands follow this format: 113 | ```sh 114 | taskinfo TASKID TASKATTRIBUTE 115 | # example: taskinfo 40 description 116 | ``` 117 | The [resumetask](#resumetask) script depends on this script. 118 | -------------------------------------------------------------------------------- /accountsof.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :refer [sh]] 3 | '[clojure.string :as str]) 4 | 5 | (defn get-accounts [ledger-file] 6 | (-> (sh "ledger" "-f" ledger-file "bal" "--flat") 7 | (:out) 8 | (str/split #"\n") 9 | (as-> entries (map #(str/split % #" ") entries)) 10 | (as-> entry-arrays (map #(filter not-empty %) entry-arrays)) 11 | (as-> clean-arrays (filter #(>= (count %) 3) clean-arrays)) 12 | (as-> entry-arrays (map #(nthrest % 2) entry-arrays)) 13 | (as-> accounts (map #(str/join " " %) accounts)))) 14 | 15 | (let [ledger-file (first *command-line-args*)] 16 | (->> (get-accounts ledger-file) 17 | (mapv println))) 18 | 19 | 20 | -------------------------------------------------------------------------------- /cljminimal.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.io :as io] 3 | '[clojure.string :as str]) 4 | 5 | (def args *command-line-args*) 6 | (when-not args 7 | (println "You need a name for your project!") 8 | (System/exit 1)) 9 | 10 | (def project-name (first args)) 11 | (when (.exists (io/file project-name)) 12 | (println "This directory already exists. Aborting.") 13 | (System/exit 1)) 14 | (when (str/includes? project-name "/") 15 | (println "Project name cannot include \"/\" or be a directory path.") 16 | (System/exit 1)) 17 | 18 | (io/make-parents (str project-name "/src/core.clj")) 19 | (spit (str project-name "/deps.edn") '{}) 20 | (let [core-path (str project-name "/src/core.clj")] 21 | (spit core-path '(ns core)) 22 | (spit core-path '(defn -main [opts] (println "Hello, world!")) :append true)) 23 | 24 | (doall (map println [(str project-name "was created") 25 | "try this:" 26 | (str "cd " project-name) 27 | "clj -X core/-main"])) 28 | -------------------------------------------------------------------------------- /depo.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[babashka.cli :as cli]) 3 | (require '[babashka.curl :as curl]) 4 | (require '[clojure.data.xml :as xml]) 5 | (require '[clojure.tools.logging :as log]) 6 | (require '[clojure.java.io :as io]) 7 | (require '[clojure.pprint :refer [pprint]]) 8 | (require '[clojure.string :as str]) 9 | 10 | (defn in? [coll elm] (some #(= elm %) coll)) 11 | 12 | (defn format-lib [lib] 13 | (if (.contains lib "/") 14 | lib 15 | (str lib "/" lib))) 16 | 17 | (defn format-url [artifact] 18 | (str "https://repo.clojars.org/" artifact "/maven-metadata.xml")) 19 | 20 | (defn parse-body 21 | "parses the XML metadata returned by clojars and returns a list of versions" 22 | [body] 23 | (-> (xml/parse-str body) 24 | (:content) 25 | (as-> content 26 | (filter #(= :versioning (:tag %)) content)) 27 | (first) 28 | (:content) 29 | (as-> content 30 | (filter #(= :versions (:tag %)) content)) 31 | (first) 32 | (:content) 33 | (as-> versions 34 | (filter map? versions)) 35 | (as-> version-maps 36 | (map #(first (:content %)) version-maps)) 37 | (vec))) 38 | 39 | (defn find-versions [lib] 40 | (parse-body (:body (curl/get (format-url lib))))) 41 | 42 | (defn validate-version [version versions] 43 | (if-not (in? versions version) 44 | (do (log/error (str "Version " version " not found")) 45 | (System/exit 1)) 46 | version)) 47 | 48 | (defn validate-file [file] 49 | (if-not (.exists (io/file file)) 50 | (do (log/error (str "Config file: " file " not found")) 51 | (System/exit 1)) 52 | file)) 53 | 54 | (defn get-config [] 55 | (cond (.exists (io/file "deps.edn")) "deps.edn" 56 | (.exists (io/file "project.clj")) "project.clj" 57 | (.exists (io/file "shadow-cljs.edn")) "shadow-cljs.edn")) 58 | 59 | (defn add-deps-edn [file artifact version] 60 | (let [edn (read-string (slurp file)) 61 | new-edn (assoc-in edn [:deps (symbol artifact)] {:mvn/version version})] 62 | (binding [*print-namespace-maps* false] 63 | (pprint new-edn (io/writer file)) 64 | (println "Added" {artifact {:mvn/version version}})))) 65 | 66 | (defn shorten [artifact] 67 | (let [[group name] (str/split artifact #"/")] 68 | (if (= group name) name artifact))) 69 | 70 | (defn add-project-clj [file artifact version] 71 | (let [config (read-string (slurp file)) 72 | [deps-index deps] (loop [clj (map-indexed #(vec [%1 %2]) config)] 73 | (if (= :dependencies (second (first clj))) 74 | (second clj) 75 | (recur (rest clj)))) 76 | new-deps (vec (distinct (conj deps [(symbol artifact) version]))) 77 | new-config (concat 78 | (take deps-index config) 79 | `[~new-deps] 80 | (drop (inc deps-index) config))] 81 | (binding [*print-namespace-maps* false] 82 | (pprint new-config (io/writer file)) 83 | (println "Added" [artifact (str "\"" version "\"")])))) 84 | 85 | (defn add-shadow-edn [file artifact version] 86 | (let [edn (read-string (slurp file)) 87 | artifact (shorten artifact) 88 | deps (if (:dependencies edn) (:dependencies edn) []) 89 | new-deps (vec (distinct (conj deps [(symbol artifact) version]))) 90 | new-edn (assoc edn :dependencies new-deps)] 91 | (binding [*print-namespace-maps* false] 92 | (pprint new-edn (io/writer file))) 93 | (println "Added" [artifact (str "\"" version "\"")]))) 94 | 95 | (defn add-dependencies [file artifact version] 96 | (let [fav [file artifact version]] 97 | (case file 98 | "deps.edn" (apply add-deps-edn fav) 99 | "project.clj" (apply add-project-clj fav) 100 | "shadow-cljs.edn" (apply add-shadow-edn fav)))) 101 | 102 | (defn add 103 | [{:keys [:opts] 104 | {:keys [lib version file]} :opts}] 105 | (let [artifact (format-lib lib) 106 | versions (find-versions artifact) 107 | version (if version 108 | (validate-version version versions) 109 | (last versions)) 110 | file (if file (validate-file file) (get-config))] 111 | (add-dependencies file artifact version))) 112 | 113 | (def table 114 | [{:cmds ["add"] :fn add :args->opts [:lib :version] :alias {:f :file}}]) 115 | 116 | (cli/dispatch table *command-line-args*) 117 | -------------------------------------------------------------------------------- /install.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :refer [sh]] 3 | '[clojure.string :refer [split includes?]]) 4 | 5 | (defn drop-extension [filename] 6 | (first (split filename #"\."))) 7 | 8 | (defn create-uninstall-script [scripts path filename] 9 | (spit (str path filename) "#!/usr/bin/env bb\n") 10 | (spit (str path filename) 11 | `(do 12 | (def a# ~scripts) 13 | (doseq [b# a#] 14 | (clojure.java.io/delete-file (str ~path b#))) 15 | (clojure.java.io/delete-file (str ~path ~filename))) :append true)) 16 | 17 | (let [files (-> (sh "ls") 18 | (:out) 19 | (split #"\n")) 20 | cljfiles (filter #(includes? % ".clj") files) 21 | scripts (filter #(and (not= % "install.clj") 22 | (not= % "on-modify-log.clj")) cljfiles) 23 | extensionless (vec (map drop-extension scripts)) 24 | home (System/getProperty "user.home") 25 | path "/.local/bin/" 26 | uninstall "uninstall-some-scripts"] 27 | (doseq [script scripts] 28 | (sh "cp" script (str home path (drop-extension script)))) 29 | (create-uninstall-script extensionless (str home path) uninstall) 30 | (sh "chmod" "+x" (str home path uninstall)) 31 | (println "The following scripts have been copied to '~/.local/bin':\n") 32 | (doseq [script scripts] 33 | (println (drop-extension script))) 34 | (println) 35 | (println "To use them please ensure that '~/.local/bin` is in your PATH.") 36 | (println "i.e. export PATH=~/.local/bin:$PATH\n") 37 | (println "To uninstall call the" uninstall "script")) 38 | -------------------------------------------------------------------------------- /jrun.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :as sh] 3 | '[babashka.fs :as fs]) 4 | 5 | (fs/glob "." "*App*") 6 | (defn matchfile 7 | [s] 8 | (-> (fs/glob "." (str "*" s "*")) 9 | (first) 10 | (str))) 11 | 12 | (let [file (matchfile (first *command-line-args*))] 13 | (sh/sh "javac" file) 14 | (-> (sh/sh "java" (first (fs/split-ext file))) 15 | (:out) 16 | (println))) 17 | 18 | -------------------------------------------------------------------------------- /keepbooks.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[babashka.cli :as cli] 3 | '[clojure.string :as str]) 4 | 5 | (def spec {:alias {:f :filepath 6 | :b :batch 7 | :d :date} 8 | :require [:filepath]}) 9 | 10 | ; Mode checkers 11 | (defn single-entry? [args] 12 | (and (not-empty (:args args)) 13 | (not (contains? (:opts args) :batch)))) 14 | 15 | (defn interactive? [args] 16 | (and (not (contains? (:opts args) :batch)) 17 | (empty? (:args args)))) 18 | 19 | (defn batch? [args] 20 | (contains? (:opts args) :batch)) 21 | 22 | ; Argument checkers 23 | (defn string-is-number? [s] 24 | (try 25 | (number? (Float/parseFloat s)) 26 | (catch Exception _ false))) 27 | 28 | (defn valid-amount? [amt] 29 | (string-is-number? amt)) 30 | 31 | (defn valid-date? [date] 32 | (let [components (str/split date #"/")] 33 | (and (= (count components) 3) 34 | (every? true? (map string-is-number? components))))) 35 | 36 | ; Validators 37 | (defn validate-string [s] 38 | (when (str/includes? s ";") 39 | (throw (Exception. (str s " contains invalid character ';'"))))) 40 | 41 | (defn validate-date [date] 42 | (validate-string date) 43 | (when-not (valid-date? date) 44 | (throw (Exception. (str date " is an invalid date"))))) 45 | 46 | (defn validate-amount [amt] 47 | (validate-string amt) 48 | (when-not (valid-amount? amt) 49 | (throw (Exception. (str amt " is an invalid amount"))))) 50 | 51 | (defn validate-txn [txn] 52 | (validate-date (:date txn)) 53 | (mapv validate-string (:payee txn)) 54 | (validate-string (:debit txn)) 55 | (validate-string (:credit txn)) 56 | (validate-amount (:amount txn)) 57 | (validate-string (:currency txn))) 58 | 59 | ; transaction builder 60 | (defn today 61 | "Creates a string in a format compatible with ledger" 62 | [] 63 | (let [date (java.util.Date.) 64 | year (+ 1900 (.getYear date)) 65 | month (inc (.getMonth date)) 66 | day (.getDate date)] 67 | (str/join "/" [year month day]))) 68 | 69 | (defn format-txn [account amount currency] 70 | (let [account-length (count account) 71 | amount-length (count amount) 72 | currency-length (count currency) 73 | whitespace-length (- 50 (+ account-length amount-length currency-length))] 74 | (str " " account (str/join "" (repeat whitespace-length " ")) amount " " currency))) 75 | 76 | (defn build-txn [parsed-args] 77 | (let [args (:args parsed-args) 78 | num-args (count args) 79 | date (if (:date (:opts parsed-args)) 80 | (:date (:opts parsed-args)) 81 | (today)) 82 | payee (drop-last 4 args) 83 | debit (nth args (- num-args 4)) 84 | credit (nth args (- num-args 3)) 85 | amount (nth args (- num-args 2)) 86 | currency (nth args (- num-args 1))] 87 | {:date date 88 | :payee payee 89 | :debit debit 90 | :credit credit 91 | :amount amount 92 | :currency currency})) 93 | 94 | (defn build-entry [txn] 95 | (let [payee (str/join " " (:payee txn)) 96 | line1 (str/join " " [(:date txn) payee]) 97 | line2 (format-txn (:debit txn) (:amount txn) (:currency txn)) 98 | line3 (str " " (:credit txn))] 99 | (str/join "\n" ["" line1 line2 line3 ""]))) 100 | 101 | (defn single-entry-txn [parsed-args] 102 | (when (< (count (:args parsed-args)) 4) 103 | (throw (Exception. "Not enough arguments to make an entry."))) 104 | (let [path (:filepath (:opts parsed-args)) 105 | txn (build-txn parsed-args) 106 | entry (build-entry txn)] 107 | (validate-txn txn) 108 | (spit path entry :append true) 109 | (println entry))) 110 | 111 | (defn prompt 112 | "Reads user input and validates it. If input is invalid, 113 | prompts again." 114 | [validate-fn validatee error-msg prompt-fn] 115 | (try (validate-fn @validatee) 116 | (catch Exception _ (do 117 | (println error-msg) 118 | (reset! validatee (prompt-fn)))))) 119 | (defn prompt-date [] 120 | (println "Date (leave blank for today):") 121 | (let [date (atom nil)] 122 | (reset! date (read-line)) 123 | (if (empty? @date) 124 | (reset! date (today)) 125 | (prompt validate-date date "Invalid date\n" prompt-date)) 126 | @date)) 127 | 128 | (defn prompt-payee [] 129 | (println "Payee:") 130 | (let [payee (atom nil)] 131 | (reset! payee (read-line)) 132 | (prompt validate-string payee "Field cannot contain ; character" prompt-payee) 133 | @payee)) 134 | 135 | (defn prompt-account [account-msg] 136 | (println account-msg) 137 | (let [acct (atom nil)] 138 | (reset! acct (read-line)) 139 | (prompt validate-string 140 | acct 141 | "Field cannot contain ; character" 142 | #(prompt-account "Account to debit:")) 143 | @acct)) 144 | 145 | (defn prompt-amount [] 146 | (println "Ammount (without currency):") 147 | (let [amt (atom nil)] 148 | (reset! amt (read-line)) 149 | (prompt validate-amount amt "Invalid amount" prompt-amount) 150 | @amt)) 151 | 152 | (defn prompt-currency [] 153 | (println "Currency:") 154 | (let [currency (atom nil)] 155 | (reset! currency (read-line)) 156 | (prompt validate-string 157 | currency 158 | "Field cannot contain ; character" 159 | prompt-currency) 160 | @currency)) 161 | 162 | (defn affirmative? [s] 163 | (case s 164 | "" true 165 | "y" true 166 | "yes" true 167 | false)) 168 | 169 | (defn interactive-txn [parsed-args] 170 | (let [date (prompt-date) 171 | payee (prompt-payee) 172 | debit (prompt-account "Account to debit:") 173 | credit (prompt-account "Account to credit:") 174 | amount (prompt-amount) 175 | currency (prompt-currency) 176 | path (:filepath (:opts parsed-args)) 177 | entry (build-entry {:date date 178 | :payee (str/split payee #" ") 179 | :debit debit 180 | :credit credit 181 | :amount amount 182 | :currency currency})] 183 | (spit path entry :append true) 184 | (println entry "\n") 185 | (println "Make another entry? (y/n)") 186 | (when (affirmative? (str/lower-case (read-line))) 187 | (interactive-txn parsed-args)))) 188 | 189 | ; entry point 190 | (let [parsed (cli/parse-args *command-line-args* spec)] 191 | (cond 192 | (single-entry? parsed) (single-entry-txn parsed) 193 | (batch? parsed) (println "batch-entry coming soon!") 194 | (interactive? parsed) (interactive-txn parsed))) 195 | -------------------------------------------------------------------------------- /on-modify-log.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[cheshire.core :as json] 3 | '[babashka.fs :as fs]) 4 | 5 | (let [oldtask (json/parse-string (read-line) true) 6 | newtask (json/parse-string (read-line) true) 7 | uuid (:uuid oldtask) 8 | data-path (str (fs/path (fs/parent *file*) (fs/path "last-modified.data")))] 9 | (spit data-path uuid) 10 | (println (json/generate-string newtask))) 11 | -------------------------------------------------------------------------------- /projectsof.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :refer [sh]] 3 | '[clojure.string :as str] 4 | '[babashka.fs :as fs] 5 | '[babashka.cli :as cli]) 6 | 7 | (def spec {:alias {:n :numbered 8 | :i :id} 9 | :coerce {:numbered :boolean} 10 | :args->opts [:project-type] 11 | :validate {:id {:pred number? 12 | :ex-msg (fn [m] "-g --goto has to be a valid number")}}}) 13 | 14 | (defn ripgrep [types] 15 | (let [type-adds (map #(vec ["--type-add" 16 | (str (:alias %) ":" (:glob %))]) types) 17 | t (map #(str "-t" (:alias %)) types) 18 | args (concat ["rg"] (flatten type-adds) t ["--files"])] 19 | (-> (apply sh args) 20 | (:out) 21 | (str/split #"\n") 22 | (as-> paths (map #(str (fs/cwd) "/" %) paths)) 23 | (as-> fullpaths (map fs/parent fullpaths)) 24 | (as-> pathobjs (map str pathobjs)) 25 | (sort)))) 26 | 27 | (defn find-java-projects [] 28 | (ripgrep [{:alias "settings" :glob "settings.gradle"} 29 | {:alias "pom" :glob "pom.xml"}])) 30 | 31 | (defn find-clj-projects [] 32 | (ripgrep [{:alias "deps" :glob "deps.edn"} 33 | {:alias "lein" :glob "project.cl"}])) 34 | 35 | (defn assign [project-type] 36 | (case project-type 37 | "clj" (find-clj-projects) 38 | "clojure" (find-clj-projects) 39 | "java" (find-java-projects) 40 | (println "Project type not supported"))) 41 | 42 | (try (sh "rg") (catch Exception _ (println "rg is not installed"))) 43 | 44 | (let [parsed (cli/parse-opts *command-line-args* spec) 45 | numbered (:numbered parsed) 46 | id (:id parsed) 47 | project-type (:project-type parsed)] 48 | (-> (assign project-type) 49 | (as-> paths (if numbered 50 | (map-indexed #(str %1 ": " %2) paths) 51 | paths)) 52 | (as-> paths (if id 53 | (println (nth paths id)) 54 | (mapv println paths))))) 55 | -------------------------------------------------------------------------------- /resumetask.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.string :as str] 3 | '[clojure.java.io :as io] 4 | '[clojure.java.shell :as sh]) 5 | 6 | (defn get-taskwarrior-data-location [] 7 | (let [home (System/getProperty "user.home") 8 | taskrc (-> (try (slurp (str home "/.taskrc")) 9 | (catch Exception _ 10 | (slurp (str home "/.config/task/taskrc")))) 11 | (as-> rc (str/split rc #"\n"))) 12 | ;On NixOS or using Homemanager, the taskrc may be 13 | ;in another place. The following lines tries to find 14 | ;that place. 15 | config (-> taskrc 16 | (as-> lines 17 | (filter #(str/includes? % "include") lines)) 18 | (as-> lines 19 | (map #(drop 8 %) lines)) 20 | (as-> lines 21 | (map #(str/join "" %) lines)) 22 | (as-> paths 23 | (map #(slurp %) paths)) 24 | (as-> lines 25 | (map #(str/split % #"\n") lines)) 26 | (flatten) 27 | (concat taskrc)) 28 | datalocation (-> config 29 | (as-> c 30 | (filter #(str/includes? % "data.location") c)) 31 | (first) 32 | (as-> line (drop 14 line)) 33 | (as-> ch (str/join "" ch)) 34 | (str/replace "~" home))] 35 | datalocation)) 36 | 37 | (defn validate-task 38 | "Validate if task is not complete" 39 | [uuid] 40 | (if (-> (sh/sh "taskinfo" uuid "status") 41 | (:out) 42 | (str/replace "\n" "") 43 | (= "completed")) 44 | (do (println "Task is already completed.") 45 | (System/exit 1)) 46 | uuid)) 47 | 48 | (let [data-path (get-taskwarrior-data-location) 49 | modified-path (str data-path "/last-modified.data")] 50 | (when-not (.exists (io/file modified-path)) 51 | (println "No last modified data. Have you installed and used the hook?") 52 | (System/exit 1)) 53 | (-> (slurp modified-path) 54 | (validate-task) 55 | (as-> id (sh/sh "task" id "start")) 56 | (:out) 57 | (println))) 58 | -------------------------------------------------------------------------------- /startnewtask.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :as sh] 3 | '[clojure.string :as str]) 4 | 5 | (defn get-task-id 6 | [stdout] 7 | (-> stdout 8 | (last) 9 | (drop-last) 10 | (drop-last) 11 | (as-> chars (str/join "" chars)))) 12 | 13 | (defn add-task [args] 14 | (apply sh/sh (conj args "add" "task"))) 15 | 16 | (let [result (add-task *command-line-args*)] 17 | (when (not= (:exit result) 0) 18 | (println (:err result)) 19 | (System/exit 1)) 20 | (->> (str/split (:out result) #" ") 21 | (get-task-id) 22 | (sh/sh "task" "start"))) 23 | -------------------------------------------------------------------------------- /stoptasks.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :refer [sh]] 3 | '[clojure.string :refer [split]]) 4 | 5 | (defn also [args f & {:keys [skip] 6 | :or {skip false}}] 7 | (if skip 8 | (f) 9 | (f args)) 10 | args) 11 | 12 | (-> (sh "task" "active") 13 | (:out) 14 | (split #"\n") 15 | (nthrest 3) 16 | (drop-last) 17 | (as-> cols (map #(split % #" ") cols)) 18 | (as-> cols (map first cols)) 19 | (as-> cols (filter not-empty cols)) 20 | (also (fn [ids] (doall (map #(println "Task" % "is active") ids)))) 21 | (also #(println "stopping tasks...") :skip true) 22 | (as-> ids (map #(sh "task" "stop" %) ids)) 23 | (doall)) 24 | 25 | (println "Tasks stopped") 26 | -------------------------------------------------------------------------------- /taskinfo.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[clojure.java.shell :as sh] 3 | '[cheshire.core :as json]) 4 | 5 | (when (< (count *command-line-args*) 2) 6 | (println "You need atleast two arguments.")) 7 | 8 | (defn get-task-attribute 9 | [id attr] 10 | (-> (sh/sh "task" id "export") 11 | (:out) 12 | (json/parse-string) 13 | (first) 14 | (get attr))) 15 | 16 | (let [[id attr] *command-line-args*] 17 | (println (get-task-attribute id attr))) 18 | --------------------------------------------------------------------------------