Run commands easily with kwt

Part 2: defining menus, actions, and params with YAML.

bettercallshao
bettercallshao
Run commands easily with kwt

In part 2, I will explain how YAMLs are used to configure kwt. Please install kwt by following the guide and read part-1 if interested.

What are menus, actions, and params?

The structures are defined in menu.go. Note the actual config files are YAML even though the code references JSON.

// Param struct: a parameter for a action
type Param struct {
	Name  string `json:"name" binding:"required"`
	Help  string `json:"help"`
	Value string `json:"value"`
}

// Action struct: a thing a user can do
type Action struct {
	Name     string  `json:"name" binding:"required"`
	Help     string  `json:"help"`
	Template string  `json:"template" binding:"required"`
	Params   []Param `json:"params"`
}

// Menu struct: a list of actions
type Menu struct {
	Name    string   `json:"name" binding:"required"`
	Version string   `json:"version"`
	Help    string   `json:"help"`
	Actions []Action `json:"actions" binding:"required"`
	Hash    string   `json:"hash"`
}

Actions & params

The core concept is an action, which represents an command. It consists of

  • Name: the unique key for this action.
  • Help: the help message for this action (optional).
  • Template: the Go template for this action, renderable with input parameters.
  • Params: list of input parameter definitions (optional).

A param defines a input parameter for an action, and is relatively simple. It consists of a name, a help message, and an optional default value. A fully defined action allows a user to render a templated command by applying parameter values, so that kwt can execute it.

find-vim

developer-demo.yaml

name: find-vim
help: Find the first file with given name in directory and edit it with vim
template: vim $(find . -name '{{.target}}' | head -1)
params:
- name: target
  help: Target file name / directory
  value: README.md

In this example, there is one param called target, and with it the placeholder {{.target}} in template can be filled at run-time (after kwt is called and before the rendered command executes) to produce an executable command. If target is Apple.java, the template renders to.

vim $(find . -name 'Apple.java' | head -1)

The default target is README.md, so if no target is provided, the template renders to.

vim $(find . -name 'README.md' | head -1)

Kwt executes the rendered command, which will consequently open a vim.

uuid

python-demo.yaml

name: uuid
help: Generate a UUID
template: python -c 'import uuid; print(str(uuid.uuid4()))'

An action is not required to have params, if no params are defined, the template just renders to itself and gets executed. The benefit is the action is a predefined command and has a name.

forex-rate

python-demo.yaml

name: forex-rate
help: Print forex rates
template: |
  python3 -u -c "
  import urllib.request
  import urllib.parse
  import json
  url = 'https://api.exchangeratesapi.io/latest?base={{.base}}&symbols={{.symbols}}'
  r = json.load(urllib.request.urlopen(url))
  date = r['date']
  rates = r['rates']
  print(f'On {date}')
  print('\n'.join(
    f'1 {{.base}} can buy {rates[symbol]:.2f} {symbol}'
    for symbol in rates
  ))
  "
params:
- name: base
  help: Base currency
  value: USD
- name: symbols
  help: Comma separated currency symbol list
  value: CAD,GBP

The template can be multi-line. As long as the rendered can be executed as a bash or Windows BAT script, it should work (cmd.go will actually write the content to a file then call shell to execute it). I tried going crazy and wrote long Python scripts, it generally works, unless you need to use ", which is already used to quote the script. One can also go crazy with Go templating with if-else, range, etc.

A menu is a YAML file, it represents a collection of actions that are managed and distributed together. A menu has a list of actions, and a name and a help much like an action. It also has an optional version which the author can use to convey the evolution of a menu. A hash (checksum) is defined in code for other purposes, and kwt does not expect the YAML to carry its own checksum. Examples are in kwt-menus.

How do I use menus?

To use a menu, ingest it into your system with ingest command from kwt. It takes either a local file or a HTTP link, and overrides the existing menu with the same name.

kwt i -s ~/Download/python-demo.yaml
kwt i -s https://raw.githubusercontent.com/bettercallshao/kwt-menus/master/python-demo.yaml

Read and follow help, or read guide.

kwt -h

Where are the YAMLs?

Kwt ingests menus to and reads menus from $HOME/.kwt/menus/. It tries to load every file in this directory as a menu YAML. If a file fails to load, it is ignored with no warnings (should probably add some), which unfortunately happens when menus have bad syntax. Environment variable KUT_HOME can be used to override the directory path to $KUT_HOME/menus.

How to develop menus?

To write new menus.

  1. Copy an existing menu to a new file under $HOME/.kwt/menus/.
  2. Make sure the name defined inside the YAML matches the new file name. Otherwise it's confusing and untested.
  3. Modify the new menu and you should see it in kwt -h.
  4. If you can't find your menu in kwt, it probably has a YAML syntax error.
  5. Hack away!
  6. Optionally give the menu a version.

How to distribute menus?

Since I have been the only user, this has not been thought through. The current guide is that the author should publish a menu as a publicly available text file (e.g. raw github file) and ask the user to ingest it with kwt i. If menu distribution becomes a thing, we should probably do something similar to Homebrew taps.