Full Code of kencx/keyb for AI

master 27d84092f93c cached
61 files
155.7 KB
51.7k tokens
106 symbols
1 requests
Download .txt
Repository: kencx/keyb
Branch: master
Commit: 27d84092f93c
Files: 61
Total size: 155.7 KB

Directory structure:
gitextract_nio3xsob/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .goreleaser.yaml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── config/
│   ├── config.go
│   ├── config_test.go
│   ├── model.go
│   └── model_test.go
├── examples/
│   ├── Helix.yml
│   ├── ansi.yml
│   ├── awesomewm.yml
│   ├── bspwm.yml
│   ├── changefile
│   ├── config/
│   │   ├── README.md
│   │   └── default.yml
│   ├── curl.yml
│   ├── cut.yml
│   ├── date.yml
│   ├── dd.yml
│   ├── discord.yml
│   ├── du.yml
│   ├── dwm.yml
│   ├── find.yml
│   ├── firefox.yml
│   ├── irssi.yml
│   ├── kitty.yml
│   ├── less.yml
│   ├── ranger.yml
│   ├── screen.yml
│   ├── tar.yml
│   ├── tmux.yml
│   ├── unzip.yml
│   ├── vim.yml
│   ├── vscode.yml
│   ├── weechat.yml
│   ├── zip.yml
│   └── zsh.yml
├── go.mod
├── go.sum
├── main.go
├── output/
│   ├── output.go
│   └── output_test.go
├── testdata/
│   ├── testConfig.json
│   ├── testConfig.yml
│   ├── testConfigMinimal.json
│   ├── testConfigMinimal.yml
│   ├── testkeyb.json
│   └── testkeyb.yml
└── ui/
    ├── list/
    │   ├── keymap.go
    │   ├── list.go
    │   ├── list_test.go
    │   ├── update.go
    │   └── view.go
    ├── table/
    │   ├── row.go
    │   ├── table.go
    │   └── table_test.go
    └── ui.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "chore"
      include: "scope"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "chore"
      include: "scope"


================================================
FILE: .github/workflows/test.yml
================================================
name: CI

on:
  push:
    branches:
      - master
    tags:
      - "v*"
  pull_request:
    branches:
      - master

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout codebase
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: '1.26'

      - name: Verify dependencies
        run: go mod verify

      - name: Build application
        run: make build

      - name: Vet
        run: go vet ./...

      - name: Install staticcheck
        run: go install honnef.co/go/tools/cmd/staticcheck@latest

      - name: Run staticcheck
        run: staticcheck ./...

      - name: Run tests
        run: make test


  release:
    needs: [test]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout codebase
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: '1.26'

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v7
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
*.json
*.toml
*.ini
*.gitcommit

testdata/keyb*
!testdata/*.json
!testdata/*.yml
!testdata/*.yaml
keyb

dist/


================================================
FILE: .goreleaser.yaml
================================================
project_name: keyb
version: 2

before:
  hooks:
    - go mod tidy

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - "arm64"
      - "amd64"
      - "386"
    ldflags:
      - "-s -w -X main.version={{ .Tag }}"
archives:
  - name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
    formats: tar.gz
    format_overrides:
      - goos: windows
        formats: zip
checksum:
  name_template: 'checksums.txt'
changelog:
  disable: true


================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.

[v0.2.0]: https://github.com/kencx/keyb/compare/v0.1.0...v0.2.0
[v0.3.0]: https://github.com/kencx/keyb/compare/v0.2.0...v0.3.0
[v0.4.0]: https://github.com/kencx/keyb/compare/v0.3.0...v0.4.0
[v0.4.1]: https://github.com/kencx/keyb/compare/v0.4.0...v0.4.1
[v0.5.0]: https://github.com/kencx/keyb/compare/v0.4.1...v0.5.0
[v0.6.0]: https://github.com/kencx/keyb/compare/v0.5.0...v0.6.0
[v0.7.0]: https://github.com/kencx/keyb/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/kencx/keyb/compare/v0.7.0...v0.8.0

## [v0.8.0]

### Added
- Add support for json config and keyb files
- Add support to export keyb files to json and yaml

### Changed
- Update Go to v1.26.1
- Update bubbles to v1.0.0
- Update bubbletea to v1.3.10
- Update lipgloss to v1.1.0
- Bump goreleaser/goreleaser-action from 5 to 7
- Bump actions/checkout from 4 to 6
- Bump actions/setup-go from 4 to 6

## [v0.7.0]

### Fixed
- Exiting now clears the output

### Chore
- Replace deprecated `--rm-dist` with `--clean` flag in Goreleaser action

## [v0.6.0]

### Changed
- Bump goreleaser/goreleaser-action from 4 to 5 [#25](https://github.com/kencx/keyb/pull/25)
- Simplify config package
- Bump actions/checkout from 3 to 4 [#24](https://github.com/kencx/keyb/pull/24)
- Update Go to v1.21
- Update bubbletea to v0.24.2

### Fixed
- Fix environment variables not being expanded in the config's `settings.keyb_path`.

## [v0.5.0]
### Added
- Add ability to customize search bar cursor bindings.

### Changed
- Default configuration file is no longer automatically generated. `keyb` will
  default to reading any config file at `$XDG_CONFIG_HOME/keyb/config.yml`. If
  not present, the default configuration is used.

## [v0.4.1]
### Fixed
- Fix inability to delete characters in search bar [1385049](https://github.com/kencx/keyb/commit/138504964bad8f8827c5f5e9c1572298d4d5e102)

## [v0.4.0]
### BREAKING
- Use `XDG_CONFIG_HOME` environment variable in macOS if set [#18](https://github.com/kencx/keyb/pull/18)

### Added
- Add ability to move cursor in search mode
  [55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)
- Add ability to customize cursor movement keys in search mode
  [55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)

### Changed
- Update dependencies [#19](https://github.com/kencx/keyb/pull/19) and
  [3cba5b8](https://github.com/kencx/keyb/commit/3cba5b801acd617e9d1c37734582f3f15d2ec41b)

### Fixed
- Fix search bar cursor not blinking when focused [#20](https://github.com/kencx/keyb/pull/20)

## [v0.3.0]
### Added
- Add "add" subcommand to quick add keybinds [50b66d7](https://github.com/kencx/keyb/commit/50b66d7a78c4a08a9cb5ad5bd02d909b7b27ae53), [b9167474](https://github.com/kencx/keyb/commit/b9167474c9c5d12ed8ea0ca9630489fa7266bebe)

## [v0.2.0]
### Added
- Add counter styling [#2](https://github.com/kencx/keyb/pull/2)
- Add placeholder styling [#4](https://github.com/kencx/keyb/pull/4)
- Add ability to customize keyb key bindings [43ae9b8](https://github.com/kencx/keyb/commit/43ae9b83fbf5cae367ab74614fa42fce79817165)
- Add `sort_keys` option to sort keys alphabetically [#7](https://github.com/kencx/keyb/pull/7)
- Add ability to customize prompt location [019f6ca](https://github.com/kencx/keyb/commit/019f6cad03ada6507e6585e4f4403826dcd23212)
- Add `search_mode` option to start keyb in search mode [d6c53e1](https://github.com/kencx/keyb/commit/d6c53e1b908f05f6c0f7836068b4b6bbe1e8a451)

<!-- ### Changed -->
<!---->
<!-- ### Removed -->
<!---->
<!-- ### Fixed -->


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2022 kencx

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: Makefile
================================================
binary = keyb
version = $(shell git describe --tags)
ldflags = -ldflags "-s -w -X main.version=${version}"

.PHONY: help test clean snapshot build

default: help

help:
	@echo 'Usage:'
	@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'

## test: run tests
test:
	go test ./...

## clean: remove binaries, dist
clean:
	if [ -f ${binary} ]; then rm keyb; fi
	go clean
	rm -rf dist

## snapshot: generate unversioned snapshot release
snapshot:
	goreleaser release --snapshot

## build: build binary
build:
	go build ${ldflags} -o ${binary}

## install: install binary at ~/.local/bin
install:
	cp ${binary} ~/.local/bin/


================================================
FILE: README.md
================================================
# keyb

<p align="center">
	<img width="660" src="https://github.com/kencx/keyb/blob/master/assets/compressed.gif?raw=true">
</p>

<p align="center">Create and view your own custom hotkey cheatsheet in the terminal</p>

<p align="center">
	<img src="https://goreportcard.com/badge/github.com/kencx/keyb">
	<img src="https://github.com/kencx/keyb/actions/workflows/test.yml/badge.svg?branch=master">
</p>

### Features
- Lightweight and quick
- Fully customizable
- Fuzzy filtering
- Vim key bindings
- Export to stdout for fzf, rofi support

### Non-Features
keyb does **not** support:

- Auto detection of hotkeys
- Command selection

## Motivation

I had trouble remembering the various hotkeys that I sometimes use. It got
annoying to look them up so I resorted to writing them down on a paper
cheatsheet. Then, I thought: maybe there's a tool that does this better. I
didn't find one I liked so I built keyb.

I also wrote a blog [post](https://blog.cheo.dev/posts/keyb) about building keyb.

With keyb, I can list:

- Hotkeys that I occasionally forget or am new to
- Custom key combinations that I defined for my own workflow
- Short commands that I sometimes use

It is best used as a popup cheatsheet.

## Install
keyb supports Linux, MacOS and Windows.

### Compiled Binary
Download a compiled binary from the [releases](https://github.com/kencx/keyb/releases) page.

### Install with Go

```bash
$ go install github.com/kencx/keyb@latest
```

### AUR

Packages can be found in the AUR:
- [keyb-bin](https://aur.archlinux.org/packages/keyb-bin)
- [keyb](https://aur.archlinux.org/packages/keyb)

These are NOT maintained by me.

### Build from source

```bash
$ git clone https://github.com/kencx/keyb
$ cd keyb
$ make build
```

## Usage

```text
usage: keyb [options] <command>

Options:
  -p, --print     Print to stdout
  -e, --export    Export to file [yaml, json]
  -k, --key       Key bindings at custom path
  -c, --config    Config file at custom path
  -v, --version   Version info
  -h, --help      help for keyb

Commands:
  a, add          Add keybind to keyb file
```

### Search

- Enter search mode with `/` to perform fuzzy filtering on all rows
- Exit search mode again with `Esc`
- `Alt + d` clears the current filter

To perform filtering on section headings only, prefix the
search with `h:`. This will return all matching section headings with their
respective rows.

### Printing

keyb supports printing to stdout for use with other tools:

```bash
$ keyb -p | fzf
$ keyb -p | rofi -dmenu
```

### keyb File

keyb requires a `yaml` or `json` file with a list of hotkeys to work. A default
`yaml` file is generated in your system's config directory if no other file is
specified.

Hotkeys are classified into sections with a name and (optional) prefix field.
When displayed, sections are sorted by alphabetical order while the keys
themselves are arranged in their defined order.

```yaml
- name: bspwm
  keybinds:
    - name: terminal
      key: Super + Return
```

The prefix is a key combination that will be prepended to every hotkey in the
section. A key can choose to opt out by including a `ignore_prefix: true` field.
Prefixes are useful for applications with a common leading hotkey like tmux.

```yaml
- name: tmux
  prefix: ctrl + b
  keybinds:
    - name: Create new window
      key: c
    - name: Prev, next window
      key: Shift + {←, →}
      ignore_prefix: true
```

Refer to the `examples` for more examples.

>Multiline fields are not supported!

### Quick Add

```text
usage: keyb [-k file] add [app; name; key]

Options:
  -b, --binding  Key binding
  -p, --prefix   Ignore prefix
```

You can quick add bindings from the command line to a specified file. If `-k
file` is given and exists, the new keybind will be appended to the file.
Otherwise, `keyb_path` defined in `config.yml` will be used.

```bash
$ keyb add -b "kitty; open terminal; super + enter"
```

When adding a new keybind, the app name, keybind name and keybind must be
specified. It is separated by `;` and wrapped in quotes (to prevent parsing errors).

## Configuration

keyb can be customized with a config file at the default OS config
directory (i.e. `$XDG_CONFIG_HOME/keyb/config.yml`). If no such file exists, the
default configuration will be used.

See [config](examples/config/README.md) for all configuration options.

### Missing Colors

If you're missing colors, a workaround is to add the environment variable `CLICOLOR_FORCE=1` to
[force ANSI color support](https://pkg.go.dev/github.com/muesli/termenv#Output.EnvColorProfile).

## Roadmap

- [x] Ability to customize keyb hotkeys
- [x] `a, add` subcommand to quickly add a single hotkey entry from the CLI
- [ ] Export to additional file formats (`json, toml, conf/ini` etc.)
- [ ] Support multiple keyb files or directories

## Contributing

Contributing to keyb requires Go 1.26. Bug reports, feature requests and PRs are very welcome.

## Similar Tools

- [showkeys](https://github.com/adamharmansky/showkeys) offers a keybinding popup similar to awesomewm
- [cheat](https://github.com/cheat/cheat) is a CLI alternative to view cheatsheets for
  commands and hotkeys for just about any topic
- Refer to [shortcut-pages](https://github.com/mt-empty/shortcut-pages), [cheat/cheatsheets](https://github.com/cheat/cheatsheets) for more cheatsheets

## License
[MIT](LICENSE)


================================================
FILE: config/config.go
================================================
package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v2"
)

const (
	defaultConfigDir  = "keyb"
	defaultConfigFile = "config.yml"
	defaultKeybFile   = "keyb.yml"
)

type Config struct {
	Settings `yaml:"settings" json:"settings"`
	Color    `yaml:"color" json:"color"`
	Keys     `yaml:"keys" json:"keys"`
}

type Settings struct {
	KeybPath       string `yaml:"keyb_path" json:"keyb_path"`
	Debug          bool
	Reverse        bool
	Mouse          bool
	SearchMode     bool `yaml:"search_mode" json:"search_mode"`
	SortKeys       bool `yaml:"sort_keys" json:"sort_keys"`
	Title          string
	Prompt         string
	PromptLocation string `yaml:"prompt_location" json:"prompt_location"`
	Placeholder    string
	PrefixSep      string `yaml:"prefix_sep" json:"prefix_sep"`
	SepWidth       int    `yaml:"sep_width" json:"sep_width"`
	Margin         int
	Padding        int
	BorderStyle    string `yaml:"border" json:"border"`
}

type Color struct {
	PromptColor   string `yaml:"prompt" json:"prompt"`
	CursorFg      string `yaml:"cursor_fg" json:"cursor_fg"`
	CursorBg      string `yaml:"cursor_bg" json:"cursor_bg"`
	FilterFg      string `yaml:"filter_fg" json:"filter_fg"`
	FilterBg      string `yaml:"filter_bg" json:"filter_bg"`
	CounterFg     string `yaml:"counter_fg" json:"counter_fg"`
	CounterBg     string `yaml:"counter_bg" json:"counter_bg"`
	PlaceholderFg string `yaml:"placeholder_fg" json:"placeholder_fg"`
	PlaceholderBg string `yaml:"placeholder_bg" json:"placeholder_bg"`
	BorderColor   string `yaml:"border_color" json:"border_color"`
}

type Keys struct {
	Quit                     string
	Up                       string
	Down                     string
	UpFocus                  string `yaml:"up_focus" json:"up_focus"`
	DownFocus                string `yaml:"down_focus" json:"down_focus"`
	HalfUp                   string `yaml:"half_up" json:"half_up"`
	HalfDown                 string `yaml:"half_down" json:"half_down"`
	FullUp                   string `yaml:"full_up" json:"full_up"`
	FullDown                 string `yaml:"full_bottom" json:"full_bottom"`
	GoToFirstLine            string `yaml:"first_line" json:"first_line"`
	GoToLastLine             string `yaml:"last_line" json:"last_line"`
	GoToTop                  string `yaml:"top" json:"top"`
	GoToMiddle               string `yaml:"middle" json:"middle"`
	GoToBottom               string `yaml:"bottom" json:"bottom"`
	Search                   string
	ClearSearch              string `yaml:"clear_search" json:"clear_search"`
	Normal                   string
	CursorWordForward        string `yaml:"cursor_word_forward" json:"cursor_word_forward"`
	CursorWordBackward       string `yaml:"cursor_word_backward" json:"cursor_word_backward"`
	CursorDeleteWordBackward string `yaml:"cursor_delete_word_backward" json:"cursor_delete_word_backward"`
	CursorDeleteWordForward  string `yaml:"cursor_delete_word_forward" json:"cursor_delete_word_forward"`
	CursorDeleteAfterCursor  string `yaml:"cursor_delete_after_cursor" json:"cursor_delete_after_cursor"`
	CursorDeleteBeforeCursor string `yaml:"cursor_delete_before_cursor" json:"cursor_delete_before_cursor"`
	CursorLineStart          string `yaml:"cursor_line_start" json:"cursor_line_start"`
	CursorLineEnd            string `yaml:"cursor_line_end" json:"cursor_line_end"`
	CursorPaste              string `yaml:"cursor_paste" json:"cursor_paste"`
}

var DefaultConfig = &Config{
	Settings: Settings{
		Debug:          false,
		Reverse:        false,
		Mouse:          true,
		SearchMode:     false,
		SortKeys:       false,
		Title:          "",
		Prompt:         "keys > ",
		PromptLocation: "top",
		Placeholder:    "...",
		PrefixSep:      ";",
		SepWidth:       4,
		Margin:         0,
		Padding:        1,
		BorderStyle:    "hidden",
	},
	Color: Color{
		FilterFg: "#FFA066",
	},
	Keys: Keys{
		Quit:                     "q, ctrl+c",
		Up:                       "k, up",
		Down:                     "j, down",
		UpFocus:                  "ctrl+k",
		DownFocus:                "ctrl+j",
		HalfUp:                   "ctrl+u",
		HalfDown:                 "ctrl+d",
		FullUp:                   "ctrl+b",
		FullDown:                 "ctrl+f",
		GoToFirstLine:            "g",
		GoToLastLine:             "G",
		GoToTop:                  "H",
		GoToMiddle:               "M",
		GoToBottom:               "L",
		Search:                   "/",
		ClearSearch:              "alt+d",
		Normal:                   "esc",
		CursorWordForward:        "alt+right, alt+f",
		CursorWordBackward:       "alt+left, alt+b",
		CursorDeleteWordBackward: "alt+backspace",
		CursorDeleteWordForward:  "alt+delete",
		CursorDeleteAfterCursor:  "alt+k",
		CursorDeleteBeforeCursor: "alt+u",
		CursorLineStart:          "home, ctrl+a",
		CursorLineEnd:            "end, ctrl+e",
		CursorPaste:              "ctrl+v",
	},
}

// Read configuration and keyb file from flags, default path.
func Parse(flagCPath, flagKPath string) (Apps, *Config, error) {
	xdgConfigDir, err := getXDGConfigDir()
	if err != nil {
		return nil, nil, err
	}

	basePath := filepath.Join(xdgConfigDir, defaultConfigDir)
	err = os.MkdirAll(basePath, 0744)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create config dir: %w", err)
	}

	config, err := UnmarshalConfig(flagCPath, basePath)
	if err != nil {
		return nil, nil, err
	}

	if flagKPath == "" {
		flagKPath = config.KeybPath
	}

	keys, err := UnmarshalKeyb(flagKPath, basePath)
	if err != nil {
		return nil, nil, err
	}
	return keys, config, nil
}

// Read config file and merge with default config
func UnmarshalConfig(configFile, basePath string) (*Config, error) {

	// set default config filepath
	if configFile == "" {
		configFile = filepath.Join(basePath, defaultConfigFile)
	}

	res := newDefaultConfig(basePath)
	configFile = os.ExpandEnv(configFile)

	file, err := os.ReadFile(configFile)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return res, nil
		} else {
			return nil, fmt.Errorf("failed to read config file \"%s\": %w", configFile, err)
		}
	}

	switch filepath.Ext(configFile) {
	case ".json":
		if err = json.Unmarshal(file, &res); err != nil {
			return nil, fmt.Errorf("failed to unmarshal config file \"%s\": %w", configFile, err)
		}
	case ".yaml", ".yml":
		if err = yaml.Unmarshal(file, &res); err != nil {
			return nil, fmt.Errorf("failed to unmarshal config file \"%s\": %w", configFile, err)
		}
	}

	return res, nil
}

func newDefaultConfig(basePath string) *Config {
	res := DefaultConfig
	res.KeybPath = filepath.Join(basePath, defaultKeybFile)
	return res
}

// Read keyb file or create default keyb file not exist
func UnmarshalKeyb(keybFile, basePath string) (Apps, error) {
	if keybFile == "" {
		keybFile = filepath.Join(basePath, defaultKeybFile)
	}

	keybFile = os.ExpandEnv(keybFile)
	file, err := os.ReadFile(keybFile)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {

			k := newDefaultKeyb(keybFile)
			data, err := yaml.Marshal(k)
			if err != nil {
				return nil, fmt.Errorf("failed to generate default keyb: %w", err)
			}

			if err := os.WriteFile(keybFile, data, 0644); err != nil {
				return nil, fmt.Errorf("failed to create keyb file: %w", err)
			}
			return k, nil

		} else {
			return nil, fmt.Errorf("failed to read keyb file: %w", err)
		}
	}

	var b Apps
	switch filepath.Ext(keybFile) {
	case ".json":
		if err = json.Unmarshal(file, &b); err != nil {
			return nil, fmt.Errorf("failed to unmarshal keyb file: %w", err)
		}
	case ".yaml", ".yml":
		if err := yaml.Unmarshal(file, &b); err != nil {
			return nil, fmt.Errorf("failed to unmarshal keyb file: %w", err)
		}
	}
	return b, nil
}

func newDefaultKeyb(path string) Apps {
	return Apps{{
		Name: "example",
		Keybinds: []KeyBind{{
			Name: "add your keys in",
			Key:  path,
		}},
	}}
}

// get user XDG_CONFIG_HOME directory
func getXDGConfigDir() (string, error) {
	val, ok := os.LookupEnv("XDG_CONFIG_HOME")
	if ok {
		return val, nil
	}

	path, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("user config directory not found: %w", err)
	}
	return path, nil
}


================================================
FILE: config/config_test.go
================================================
package config

import (
	"os"
	"path/filepath"
	"reflect"
	"testing"
)

const testBasePath = "../testdata"

func TestUnmarshalConfig(t *testing.T) {
	testConfig := &Config{
		Settings: Settings{
			KeybPath:       "./custom.yml",
			Debug:          true,
			Reverse:        true,
			Mouse:          false,
			SearchMode:     false,
			SortKeys:       true,
			Title:          "",
			Prompt:         "keys > ",
			PromptLocation: "bottom",
			Placeholder:    "...",
			PrefixSep:      ";",
			SepWidth:       4,
			Margin:         1,
			Padding:        1,
			BorderStyle:    "normal",
		},
		Color: Color{
			FilterFg: "#FFA066",
		},
		Keys: Keys{
			Quit:                     "q, ctrl+c",
			Up:                       "k, up",
			Down:                     "j, down",
			UpFocus:                  "alt+k",
			DownFocus:                "alt+j",
			HalfUp:                   "ctrl+u",
			HalfDown:                 "ctrl+d",
			FullUp:                   "ctrl+b",
			FullDown:                 "ctrl+f",
			GoToFirstLine:            "g",
			GoToLastLine:             "G",
			GoToTop:                  "H",
			GoToMiddle:               "M",
			GoToBottom:               "L",
			Search:                   "/",
			ClearSearch:              "alt+d",
			Normal:                   "esc",
			CursorWordForward:        "alt+right, alt+f",
			CursorWordBackward:       "alt+left, alt+b",
			CursorDeleteWordBackward: "alt+backspace",
			CursorDeleteWordForward:  "alt+delete",
			CursorDeleteAfterCursor:  "alt+k",
			CursorDeleteBeforeCursor: "alt+u",
			CursorLineStart:          "home, ctrl+a",
			CursorLineEnd:            "end, ctrl+e",
			CursorPaste:              "ctrl+v",
		},
	}

	configFileTests := []struct {
		name string
		file string
		want *Config
	}{
		{"full config yaml", "testConfig.yml", testConfig},
		{"full config json", "testConfig.json", testConfig},
		{"minimal config yaml", "testConfigMinimal.yml", newDefaultConfig(testBasePath)},
		{"minimal config json", "testConfigMinimal.json", newDefaultConfig(testBasePath)},
		{"config file absent", "testConfigAbsent.yml", newDefaultConfig(testBasePath)},
	}

	for _, tt := range configFileTests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := UnmarshalConfig(filepath.Join(testBasePath, tt.file), testBasePath)
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}

			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}

	t.Run("empty config file path", func(t *testing.T) {
		want := newDefaultConfig(testBasePath)
		got, err := UnmarshalConfig("", testBasePath)
		if err != nil {
			t.Fatalf("unexpected err: %v", err)
		}

		if !reflect.DeepEqual(got, want) {
			t.Errorf("got %v, want %v", got, want)
		}
	})
}

func TestUnmarshalKeyb(t *testing.T) {
	apps := Apps{{
		Name: "test",
		Keybinds: []KeyBind{{
			Name: "foo",
			Key:  "bar",
		}},
	}}

	keybFileTests := []struct {
		name string
		file string
		want Apps
	}{
		{"keyb file yaml", "testkeyb.yml", apps},
		{"keyb file json", "testkeyb.json", apps},
	}

	for _, tt := range keybFileTests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := UnmarshalKeyb(filepath.Join(testBasePath, tt.file), testBasePath)
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}

			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}

	t.Run("file absent", func(t *testing.T) {
		_, err := UnmarshalKeyb(filepath.Join(testBasePath, "temp.yml"), testBasePath)
		if err != nil {
			t.Fatalf("unexpected err: %v", err)
		}

		t.Cleanup(func() {
			err := os.Remove(filepath.Join(testBasePath, "temp.yml"))
			if err != nil {
				t.Fatal(err)
			}
		})
	})

	t.Run("empty filepath", func(t *testing.T) {
		_, err := UnmarshalKeyb("", testBasePath)
		if err != nil {
			t.Fatalf("unexpected err: %v", err)
		}

		t.Cleanup(func() {
			err := os.Remove(filepath.Join(testBasePath, defaultKeybFile))
			if err != nil {
				t.Fatal(err)
			}
		})
	})
}


================================================
FILE: config/model.go
================================================
package config

import (
	"fmt"
	"os"
	"strings"

	"gopkg.in/yaml.v2"
)

type App struct {
	Prefix   string    `yaml:"prefix,omitempty" json:"prefix,omitempty"`
	Name     string    `yaml:"name" json:"name"`
	Keybinds []KeyBind `yaml:"keybinds" json:"keybinds"`
}

type Apps []*App

func (a App) String() string {
	return fmt.Sprintf("App{name=%s,prefix=%s,keybinds=%v}", a.Name, a.Prefix, a.Keybinds)
}

type KeyBind struct {
	Name string `yaml:"name" json:"name"`
	Key  string `yaml:"key" json:"key"`

	// ignore prefix defaults to false
	// so user can choose to ignore prefix for a specific kb
	IgnorePrefix bool `yaml:"ignore_prefix,omitempty" json:"ignore_prefix,omitempty"`
}

func AddEntry(path, binding string, kbIgnorePrefix bool) error {
	xdgConfigDir, err := getXDGConfigDir()
	if err != nil {
		return err
	}

	// load existing struct from filepath
	apps, err := UnmarshalKeyb(path, xdgConfigDir)
	if err != nil {
		return err
	}

	if binding == "" {
		return fmt.Errorf("binding must be given in format [app; name; keybind]")
	}

	s := strings.Split(binding, ";")
	if len(s) < 3 {
		return fmt.Errorf("binding must be given in format [app; name; keybind]")
	}

	appName := strings.TrimSpace(s[0])
	name := strings.TrimSpace(s[1])
	key := strings.TrimSpace(s[2])

	apps.addOrUpdate(appName, name, key, kbIgnorePrefix)

	// rewrite file
	data, err := yaml.Marshal(apps)
	if err != nil {
		return fmt.Errorf("failed to marshal entry: %w", err)
	}

	path = os.ExpandEnv(path)
	if err := os.WriteFile(path, data, 0644); err != nil {
		return fmt.Errorf("failed to write keyb file: %w", err)
	}

	return nil
}

func (apps *Apps) addOrUpdate(appName string, name, key string, ignorePrefix bool) {
	newKeyBind := KeyBind{
		Name:         name,
		Key:          key,
		IgnorePrefix: ignorePrefix,
	}

	if !apps.exist(appName) {
		a := App{
			Name:     appName,
			Keybinds: []KeyBind{newKeyBind},
		}
		*apps = append(*apps, &a)

	} else {
		for _, app := range *apps {
			if appName == app.Name {
				app.Keybinds = append(app.Keybinds, newKeyBind)
			}
		}
	}
	// return apps
}

func (apps Apps) exist(appName string) bool {
	for _, app := range apps {
		if appName == app.Name {
			return true
		}
	}
	return false
}


================================================
FILE: config/model_test.go
================================================
package config

import (
	"reflect"
	"testing"
)

func TestAddOrUpdate(t *testing.T) {
	t.Run("update existing", func(t *testing.T) {
		apps := Apps{{
			Name: "test",
			Keybinds: []KeyBind{{
				Name: "foo",
				Key:  "bar",
			}},
		}}
		want := Apps{{
			Name: "test",
			Keybinds: []KeyBind{{
				Name: "foo",
				Key:  "bar",
			}, {
				Name: "addFoo",
				Key:  "addBar",
			}},
		}}
		apps.addOrUpdate("test", "addFoo", "addBar", false)

		if !reflect.DeepEqual(apps, want) {
			t.Errorf("got %v, want %v", apps, want)
		}
	})

	t.Run("add new", func(t *testing.T) {
		apps := Apps{{
			Name: "test",
			Keybinds: []KeyBind{{
				Name: "foo",
				Key:  "bar",
			}},
		}}
		want := Apps{
			{
				Name: "test",
				Keybinds: []KeyBind{{
					Name: "foo",
					Key:  "bar",
				}},
			}, {
				Name: "new",
				Keybinds: []KeyBind{{
					Name: "addFoo",
					Key:  "addBar",
				}},
			},
		}
		apps.addOrUpdate("new", "addFoo", "addBar", false)

		if !reflect.DeepEqual(apps, want) {
			t.Errorf("got %v, want %v", apps, want)
		}
	})
}


================================================
FILE: examples/Helix.yml
================================================
- name: Helix Normal mode (Movement)
  keybinds:
    - name: "Move left"
      key: "h, Left"
    - name: "Move down"
      key: "j, Down"
    - name: "Move up"
      key: "k, Up"
    - name: "Move right"
      key: "l, Right"
    - name: "Move next word start"
      key: "w"
    - name: "Move previous word start"
      key: "b"
    - name: "Move next word end"
      key: "e"
    - name: "Move next WORD end"
      key: "W"
    - name: "Move previous WORD start"
      key: "B"
    - name: "Move next WORD end"
      key: "E"
    - name: "Find 'till next char"
      key: "t"
    - name: "Find next char"
      key: "f"
    - name: "Find 'till previous char"
      key: "T"
    - name: "Find previous char"
      key: "F"
    - name: "Go to line number <n>"
      key: "G"
    - name: "Reapeat last motion (f, t ro m)"
      key: "Alt-."
    - name: "Move to the start of the line"
      key: "Home"
    - name: "Move to the end ofthe line"
      key: "End"
    - name: "Move page up"
      key: "Ctrl-b, PageUp"
    - name: "Move page down"
      key: "Ctrl-f, PageDown"
    - name: "Move half page up"
      key: "Ctrl-u"
    - name: "Move half page down"
      key: "Ctrf-d"
    - name: "Jump forward on the jumplist"
      key: "Ctrl-i"
    - name: "Jump backward on the jumplist"
      key: "Ctrl-o"
    - name: "Save the current selection to the jumplist"
      key: "Ctrl-s"
- name: Helix Normal mode (Changes)
  keybinds:
    - name: "Replace with a character"
      key: "r"
    - name: "Replace with yanked text"
      key: "R"
    - name: "Switch case of the selected text"
      key: "~"
    - name: "Set the selected text to lower case"
      key: "`"
    - name: "Set the selected text to upper case"
      key: "Alt-`"
    - name: "Insert before selection"
      key: "i"
    - name: "Insert after selection (append)"
      key: "a"
    - name: "Insert at the start of the line"
      key: "I"
    - name: "Insert at the end of the line"
      key: "A"
    - name: "Open new line below selection"
      key: "o"
    - name: "Open new line above selection"
      key: "O"
    - name: "Repeat last change"
      key: "."
    - name: "Undo change"
      key: "u"
    - name: "Redo change"
      key: "U"
    - name: "Move backward in history"
      key: "Alt-u"
    - name: "Move forward in history"
      key: "Alt-U"
    - name: "Yank selection"
      key: "y"
    - name: "Paste after selection"
      key: "p"
    - name: "Paste before selection"
      key: "P"
    - name: "Select a register to yank to or paste from"
      key: "<reg>"
    - name: "Indent selection"
      key: ">"
    - name: "Unindent selection"
      key: "<"
    - name: "Format selection (currently nonfunctional/disabled) (LSP)"
      key: "="
    - name: "Delete selection"
      key: "d"
    - name: "Delete selection, without yanking"
      key: "Alt-d"
    - name: "Change selection (delete and enter insert mode)"
      key: "c"
    - name: "Change selection (delete and enter insert mode, without yanking)"
      key: "Alt-c"
    - name: "Increment object (number) under cursor"
      key: "Ctrl-a"
    - name: "Decrement object (number) under cursor"
      key: "Ctrl-x"
    - name: "Start/stop macro recording to the selected register (experimental)"
      key: "Q"
    - name: "Play back a recorded macro from the selected register (experimental)"
      key: "q"
- name: Helix Normal mode (Changes Shell)
  keybinds:
    - name: "Pipe each selection through shell command, replacing with output"
      key: "|"
    - name: "Pipe each selection into shell command, ignoring output"
      key: "Alt-|"
    - name: "Run shell command, inserting output before each selection"
      key: "!"
    - name: "Run shell command, appending output after each selection"
      key: "Alt-!"
    - name: "Pipe each selection into shell command, keep selections where command returned 0"
      key: "$"
- name: Helix Normal mode (Selection manipulation)
  keybinds:
    - name: "Select all regex matches inside selections"
      key: "s"
    - name: "Split selection into subselections on regex matches"
      key: "S"
    - name: "Split selection on newlines"
      key: "Alt-s"
    - name: "Align selection in columns"
      key: "&"
    - name: "Trim whitespace from the selection"
      key: "_"
    - name: "Collapse selection onto a single cursor"
      key: ";"
    - name: "Flip selection cursor and anchor"
      key: "Alt-;"
    - name: "Ensures the selection is in forward direction"
      key: "Alt-:"
    - name: "Keep only the primary selection"
      key: ","
    - name: "Remove the primary selection"
      key: "Alt-,"
    - name: "Copy selection onto the next line (Add cursor below)"
      key: "C"
    - name: "Copy selection onto the previous line (Add cursor above)"
      key: "Alt-C"
    - name: "Rotate main selection backward"
      key: "("
    - name: "Rotate main selection forward"
      key: ")"
    - name: "Rotate selection contents backward"
      key: "Alt-("
    - name: "Rotate selection contents forward"
      key: "Alt-)"
    - name: "Select entire file"
      key: "%"
    - name: "Select current line, if already selected, extend to next line"
      key: "x"
    - name: "Extend selection to line bounds (line-wise selection)"
      key: "X"
    - name: "Shrink selection to line bounds (line-wise selection)"
      key: "Alt-x"
    - name: "Join lines inside selection"
      key: "J"
    - name: "Join lines inside selection and select space"
      key: "Alt-J"
    - name: "Keep selections matching the regex"
      key: "K"
    - name: "Remove selections matching the regex"
      key: "Alt-K"
    - name: "Comment/uncomment the selections"
      key: "Ctrl-c"
    - name: "Expand selection to parent syntax node (TS)"
      key: "Alt-o, Alt-up"
    - name: "Shrink syntax tree object selection (TS)"
      key: "Alt-i, Alt-down"
    - name: "Select previous sibling node in syntax tree (TS)"
      key: "Alt-p, Alt-left"
    - name: "Select next sibling node in syntax tree (TS)"
      key: "Alt-n, Alt-right"
- name: Helix Normal mode (Search)
  keybinds:
    - name: "Search for regex pattern"
      key: "/"
    - name: "Search for previous pattern"
      key: "?"
    - name: "Select next search match"
      key: "n"
    - name: "Select previous search match"
      key: "N"
    - name: "Use current selection as the search pattern"
      key: "*"
- name: Helix Minor modes
  keybinds:
    - name: "Enter select (extend) mode"
      key: "v"
    - name: "Enter goto mode"
      key: "g"
    - name: "Enter match mode"
      key: "m"
    - name: "Enter command mode"
      key: ":"
    - name: "Enter view mode"
      key: "z"
    - name: "Enter sticky view mode"
      key: "Z"
    - name: "Enter window mode"
      key: "Ctrl-w"
    - name: "Enter space mode"
      key: "Space"
- name: Helix View mode
  keybinds:
    - name: "Vertically center the line"
      key: "z, c"
    - name: "Align the line to the top of the screen"
      key: "t"
    - name: "Align the line to the bottom of the screen"
      key: "b"
    - name: "Align the line to the middle of the screen (horizontally)"
      key: "m"
    - name: "Scroll the view downwards"
      key: "j,down"
    - name: "Scroll the view upwards"
      key: "k, up"
    - name: "Move page down"
      key: "Ctrl-f, PageDown"
    - name: "Move page up"
      key: "Ctrl-b, PageUp"
    - name: "Move half page down"
      key: "Ctrl-d"
    - name: "Move half page up"
      key: "Ctrl-u"
- name: Helix Goto mode
  keybinds:
    - name: "Go to line number <n> else start of file"
      key: "g"
    - name: "Go to the end of the file"
      key: "e"
    - name: "Go to files in the selection"
      key: "f"
    - name: "Go to the start of the line"
      key: "h"
    - name: "Go to the end of the line"
      key: "l"
    - name: "Go to first non-whitespace character of the line"
      key: "s"
    - name: "Go to the top of the screen"
      key: "t"
    - name: "Go to the middle of the screen"
      key: "c"
    - name: "Go to the bottom of the screen"
      key: "b"
    - name: "Go to definition (LSP)"
      key: "d"
    - name: "Go to type definition (LSP)"
      key: "y"
    - name: "Go to references (LSP)"
      key: "r"
    - name: "Go to implementation (LSP)"
      key: "i"
    - name: "Go to the last accessed/alternate file"
      key: "a"
    - name: "Go to the last modified/alternate file"
      key: "m"
    - name: "Go to next buffer"
      key: "n"
    - name: "Go to previous buffer"
      key: "p"
    - name: "Go to last modification in current file"
      key: "."
- name: Helix Match mode
  keybinds:
    - name: "Goto matching bracket (TS)"
      key: "m"
    - name: "Surround current selection with <char>"
      key: "s <char>"
    - name: "Replace surround character <from> with <to>"
      key: "r <from><to>"
    - name: "Delete surround character <char>"
      key: "d <char>"
    - name: "Select around textobject"
      key: "a <object>"
    - name: "Select inside textobject"
      key: "i <object>"
- name: Helix Window mode
  keybinds:
    - name: "Switch to next window"
      key: "w, Ctrl-w"
    - name: "Vertical right split"
      key: "v, Ctrl-v"
    - name: "Horizontal bottom split"
      key: "s, Ctrl-s"
    - name: "Go to files in the selection in horizontal splits"
      key: "f"
    - name: "Go to files in the selection in vertical splits"
      key: "F"
    - name: "Move to left split"
      key: "h, Ctrl-h, Left"
    - name: "Move to split below"
      key: "j, Ctrl-j, Down"
    - name: "Move to split above"
      key: "k, Ctrl-k, Up"
    - name: "Move to right split"
      key: "l, Ctrl-l, Right"
    - name: "Close current window"
      key: "q, Ctrl-q"
    - name: "Only keep the current window, closing all the others"
      key: "o, Ctrl-o"
    - name: "Swap window to the left"
      key: "H"
    - name: "Swap window downwards"
      key: "J"
    - name: "Swap window upwards"
      key: "K"
    - name: "Swap window to the right"
      key: "L"
- name: Helix Space mode 
  keybinds:
    - name: "Open file picker"
      key: "f"
    - name: "Open file picker at current working directory"
      key: "F"
    - name: "Open buffer picker"
      key: "b"
    - name: "Open jumplist picker"
      key: "j"
    - name: "Show documentation for item under cursor in a popup (LSP)"
      key: "k"
    - name: "Open document symbol picker (LSP)"
      key: "s"
    - name: "Open workspace symbol picker (LSP)"
      key: "S"
    - name: "Open document diagnostics picker (LSP)"
      key: "d"
    - name: "Open workspace diagnostics picker (LSP)"
      key: "D"
    - name: "Rename symbol (LSP)"
      key: "r" 
    - name: "Apply code action (LSP)"
      key: "a"
    - name: "Open last fuzzy picker"
      key: "'"
    - name: "Enter window mode"
      key: "w"
    - name: "Paste system clipboard after selections"
      key: "p"
    - name: "Paste system clipboard before selections"
      key: "P"
    - name: "Join and yank selections to clipboard"
      key: "y"
    - name: "Yank main selection to clipboard"
      key: "Y"
    - name: "Replace selections by clipboard contents"
      key: "R"
    - name: "Global search in workspace folder"
      key: "/"
    - name: "Open command palette"
      key: "?"
- name: Helix Space mode (Popup)
  keybinds:
    - name: "Scroll up"
      key: "Ctrl-u"
    - name: "Scroll down"
      key: "Ctrl-d"
      
- name: Helix Unimpaired
  keybinds:
    - name: "Go to previous diagnostic (LSP)"
      key: "[d"
    - name: "Go to next diagnostic (LSP)"
      key: "]d"
    - name: "Go to first diagnostic in document (LSP)"
      key: "[D"
    - name: "Go to last diagnostic in document (LSP)"
      key: "]D"
    - name: "Go to next function (TS)"
      key: "]f"
    - name: "Go to previous function (TS)"
      key: "[f"
    - name: "Go to next type definition (TS)"
      key: "]t"
    - name: "Go to previous type definition (TS)"
      key: "[t"
    - name: "Go to next argument/parameter (TS)"
      key: "]a"
    - name: "Go to previous argument/parameter (TS)"
      key: "[a"
    - name: "Go to next comment (TS)"
      key: "]c"
    - name: "Go to previous comment (TS)"
      key: "[c"
    - name: "Go to next test (TS)"
      key: "]T"
    - name: "Go to previous test (TS)"
      key: "[T"
    - name: "Go to next paragraph"
      key: "]p"
    - name: "Go to previous paragraph"
      key: "[p"
    - name: "Go to next change"
      key: "]g"
    - name: "Go to previous change"
      key: "[g"
    - name: "Go to first change"
      key: "]G"
    - name: "Go to last change"
      key: "[G"
    - name: "Add newline above"
      key: "[Space"
    - name: "Add newline below"
      key: "]Space"
- name: Helix Insert mode
  keybinds:
    - name: "Switch to normal mode"
      key: "Escape"
    - name: "Commit undo checkpoint"
      key: "Ctrl-s"     
    - name: "Autocomplete"
      key: "Ctrl-x"
    - name: "Insert a register content"
      key: "Ctrl-r"
    - name: "Delete previous word"
      key: "Ctrl-w, Alt-Backspace"
    - name: "Delete next word"
      key: "Alt-d, Alt-Delete"
    - name: "Delete to start of line"
      key: "Ctrl-u"
    - name: "Delete to end of line"
      key: "Ctrl-k"
    - name: "Insert new line"
      key: "Ctrl-j, Enter"
    - name: "Delete previous char"
      key: "Backspace, Ctrl-h"
    - name: "Delete previous char"
      key: "Delete, Ctrl-d"
    - name: "Move to previous line"
      key: "Up"
    - name: "Move to next line"
      key: "Down"
    - name: "Backward a char"
      key: "Left"
    - name: "Forward a char"
      key: "Right"
    - name: "Move to line end"
      key: "End"
    - name: "Move to line start"
      key: "Home"
    - name: "Move one page up"
      key: "PageUp"
    - name: "Move one page down"
      key: "PageDown"
- name: Helix Picker
  keybinds:
    - name: "Next entry"
      key: "Tab, Down, Ctrl-n"
    - name: "Previous entry"
      key: "Shift-Tab, Up, Ctrl-p"
    - name: "Page up"
      key: "PageUp, Ctrl-u"
    - name: "Page down"
      key: "PageDown, Ctrl-d"
    - name: "Go to first entry"
      key: "Home"
    - name: "Go to last entry"
      key: "End"
    - name: "Filter options"
      key: "Ctrl-space"
    - name: "Open selected"
      key: "Enter"
    - name: "Open horizontally"
      key: "Ctrl-s"
    - name: "Open vertically"
      key: "Ctrl-v"
    - name: "Close picker"
      key: "Escape, Ctrl-c"
- name: Helix Prompt
  keybinds:
    - name: "Close prompt"
      key: "Escape, Ctrl-c"
    - name: "Backward a word"
      key: "Alt-b, Alt-Left"
    - name: "Backward a char"
      key: "Ctrl-b, Left"
    - name: "Forward a word"
      key: "Alt-f, Ctrl-Right"
    - name: "Forward a char"
      key: "Ctrl-f, Right"
    - name: "Move prompt end"
      key: "Ctrl-e, End"
    - name: "Move prompt start"
      key: "Ctrl-a, Home"
    - name: "Delete previous word"
      key: "Ctrl-w, Alt-Backspace, Ctrl-Backspace"
    - name: "Delete next word"
      key: "Alt-d, Alt-Delete, Ctrl-Delete"
    - name: "Delete to start of line"
      key: "Ctrl-u"
    - name: "Delete to end of line"
      key: "Ctrl-k"
    - name: "Delete previous char"
      key: "backspace, Ctrl-h"
    - name: "Delete next char"
      key: "delete, Ctrl-d"
    - name: "Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later"
      key: "Ctrl-s"
    - name: "Select previous history"
      key: "Ctrl-p, Up"
    - name: "Select next history"
      key: "Ctrl-n, Down"
    - name: "Insert the content of the register selected by following input char"
      key: "Ctrl-r"
    - name: "Select next completion item"
      key: "Tab"
    - name: "Select previous completion item"
      key: "BackTab"
    - name: "Open selected"
      key: "Enter"


================================================
FILE: examples/ansi.yml
================================================

# https://github.com/cheat/cheatsheets
- name: ansi
  keybinds:
    - name: 'Text Reset'
      key: '\e[0m'
    - name: 'Black'
      key: '\e[0;30m'
    - name: 'Red'
      key: '\e[0;31m'
    - name: 'Green'
      key: '\e[0;32m'
    - name: 'Yellow'
      key: '\e[0;33m'
    - name: 'Blue'
      key: '\e[0;34m'
    - name: 'Purple'
      key: '\e[0;35m'
    - name: 'Cyan'
      key: '\e[0;36m'
    - name: 'White'
      key: '\e[0;37m'
    - name: 'Bold Black'
      key: '\e[1;30m'
    - name: 'Bold Red'
      key: '\e[1;31m'
    - name: 'Bold Green'
      key: '\e[1;32m'
    - name: 'Bold Yellow'
      key: '\e[1;33m'
    - name: 'Bold Blue'
      key: '\e[1;34m'
    - name: 'Bold Purple'
      key: '\e[1;35m'
    - name: 'Bold Cyan'
      key: '\e[1;36m'
    - name: 'Bold White'
      key: '\e[1;37m'
    - name: 'Underline Black'
      key: '\e[4;30m'
    - name: 'Underline Red'
      key: '\e[4;31m'
    - name: 'Underline Green'
      key: '\e[4;32m'
    - name: 'Underline Yellow'
      key: '\e[4;33m'
    - name: 'Underline Blue'
      key: '\e[4;34m'
    - name: 'Underline Purple'
      key: '\e[4;35m'
    - name: 'Underline Cyan'
      key: '\e[4;36m'
    - name: 'Underline White'
      key: '\e[4;37m'
    - name: 'Background Black'
      key: '\e[40m'
    - name: 'Background Red'
      key: '\e[41m'
    - name: 'Background Green'
      key: '\e[42m'
    - name: 'Background Yellow'
      key: '\e[43m'
    - name: 'Background Blue'
      key: '\e[44m'
    - name: 'Background Purple'
      key: '\e[45m'
    - name: 'Background Cyan'
      key: '\e[46m'
    - name: 'Background White'
      key: '\e[47m'
    - name: 'High Intensity Black'
      key: '\e[0;90m'
    - name: 'High Intensity Red'
      key: '\e[0;91m'
    - name: 'High Intensity Green'
      key: '\e[0;92m'
    - name: 'High Intensity Yellow'
      key: '\e[0;93m'
    - name: 'High Intensity Blue'
      key: '\e[0;94m'
    - name: 'High Intensity Purple'
      key: '\e[0;95m'
    - name: 'High Intensity Cyan'
      key: '\e[0;96m'
    - name: 'High Intensity White'
      key: '\e[0;97m'
    - name: 'Bold High Intensity Black'
      key: '\e[1;90m'
    - name: 'Bold High Intensity Red'
      key: '\e[1;91m'
    - name: 'Bold High Intensity Green'
      key: '\e[1;92m'
    - name: 'Bold High Intensity Yellow'
      key: '\e[1;93m'
    - name: 'Bold High Intensity Blue'
      key: '\e[1;94m'
    - name: 'Bold High Intensity Purple'
      key: '\e[1;95m'
    - name: 'Bold High Intensity Cyan'
      key: '\e[1;96m'
    - name: 'Bold High Intensity White'
      key: '\e[1;97m'
    - name: 'High Intensity backgrounds Black'
      key: '\e[0;100m'
    - name: 'High Intensity backgrounds Red'
      key: '\e[0;101m'
    - name: 'High Intensity backgrounds Green'
      key: '\e[0;102m'
    - name: 'High Intensity backgrounds Yellow'
      key: '\e[0;103m'
    - name: 'High Intensity backgrounds Blue'
      key: '\e[0;104m'
    - name: 'High Intensity backgrounds Purple'
      key: '\e[0;105m'
    - name: 'High Intensity backgrounds Cyan'
      key: '\e[0;106m'
    - name: 'High Intensity backgrounds White'
      key: '\e[0;107m'


================================================
FILE: examples/awesomewm.yml
================================================
# Awesome WM Documentation
# source: https://awesomewm.org/doc/manpages/awesome.1.html

- name: awesomewm (window manager control)
  keybinds:
    - name: "Restart awesome"
      key: "[Mod4] [Ctrl] [r]"
    - name: "Quit awesome"
      key: "[Mod4] [Shift] [q]"
    - name: "Run prompt"
      key: "[Mod4] [r]"
    - name: "Run Lua code prompt"
      key: "[Mod4] [x]"
    - name: "Start terminal emulator"
      key: "[Mod4] [Return]"
    - name: "Open main menu"
      key: "[Mod4] [w]"

- name: awesomewm (clients)
  keybinds:
    - name: "Maximize client"
      key: "[Mod4] [m]"
    - name: "Minimize client"
      key: "[Mod4] [n]"
    - name: "Restore client"
      key: "[Mod4] [Ctrl] [n]"
    - name: "Set client fullscreen"
      key: "[Mod4] [f]"
    - name: "Kill focused client"
      key: "[Mod4] [Shift] [c]"
    - name: "Set client on-top"
      key: "[Mod4] [t]"

- name: awesomewm (mouse)
  keybinds:
    - name: "View tag"
      key: "[Button1] on tag name"
    - name: "Switch to previous tag"
      key: "[Button4] on tag name"
    - name: "Switch to next tag"
      key: "[Button5] on tag name"
    - name: "Switch to previous tag"
      key: "[Button4] on root window"
    - name: "Switch to next/previous layout"
      key: "([Button1] / [Button3]) / ([Button4] / [Button5]) on layout symbol"
    - name: "Tag client with this tag"
      key: "[Mod4] [Button1] on tag"
    - name: "Move window"
      key: "[Mod4] [Button1] on client"
    - name: "Toggle this tag for client"
      key: "[Mod4] [Button3] on tag"
    - name: "Resize window"
      key: "[Mod4] [Button3] on client"
    - name: "Add tag to current view"
      key: "[Button3] clicked on tag"

- name: awesomewm (navigation)
  keybinds:
    - name: "Focus next client"
      key: "[Mod4] [j]"
    - name: "Focus previous client"
      key: "[Mod4] [k]"
    - name: "Focus first urgent client"
      key: "[Mod4] [u]"
    - name: "View previous tag"
      key: "[Mod4] [Left]"
    - name: "View next tag"
      key: "[Mod4] [Right]"
    - name: "Switch to tag [1~9]"
      key: "[Mod4] [1~9]"
    - name: "Focus next screen"
      key: "[Mod4] [Ctrl] [j]"
    - name: "Focus previous screen"
      key: "[Mod4] [Ctrl] [k]"
    - name: "Focus previously selected tag set"
      key: "[Mod4] [Esc]"

- name: awesomewm (layout modification)
  keybinds:
    - name: "Switch client with next client"
      key: "[Mod4] [Shift] [j]"
    - name: "Switch client with previous client"
      key: "[Mod4] [Shift] [k]"
    - name: "Send client to next screen"
      key: "[Mod4] [o]"
    - name: "Decrease master width factor by 5%"
      key: "[Mod4] [h]"
    - name: "Increase master width factor by 5%"
      key: "[Mod4] [l]"
    - name: "Increase number of master windows by 1"
      key: "[Mod4] [Shift] [h]"
    - name: "Decrease number of master windows by 1"
      key: "[Mod4] [Shift] [l]"
    - name: "Increase number of columns for non-master windows by 1"
      key: "[Mod4] [Ctrl] [h]"
    - name: "Decrease number of columns for non-master windows by 1"
      key: "[Mod4] [Ctrl] [l]"
    - name: "Next Layout"
      key: "[Mod4] [Space]"
    - name: "Previous Layout"
      key: "[Mod4] [Shift] [Space]"
    - name: "Floating master"
      key: "[Mod4] [Ctrl] [Space]"
    - name: "Swap focused client with master"
      key: "[Mod4] [Ctrl] [Return]"
    - name: "Toggle tag view"
      key: "[Mod4] [Ctrl] [1~9]"
    - name: "Tag client with tag"
      key: "[Mod4] [Shift] [1~9]"
    - name: "Toggle tag on client"
      key: "[Mod4] [Shift] [Ctrl] [1~9]"


================================================
FILE: examples/bspwm.yml
================================================
# bspwm/sxhkd
# source: https://github.com/baskerville/bspwm/blob/master/examples/sxhkdrc

- name: bspwm
  keybinds:
    - name: terminal
      key: super + Return
    - name: program launcher
      key: super + space
    - name: reload sxhkd
      key: super + Escape
    - name: quit/escape bspwm
      key: super + alt + {q,r}

    - name: toggle tiled and monocle layout
      key: super + m
    - name: swap current node and biggest window
      key: super + g
    - name: send newest node to newest preselection
      key: super + y

    - name: set tiled state
      key: super + t
    - name: set pseudo tiled state
      key: super + shift + t
    - name: set floating state
      key: super + s
    - name: set fullscreen state
      key: super + f
    - name: set node to {marked, locked, sticky, private}
      key: super + ctrl + {m, x, y, z}

    - name: focus node in direction
      key: super + h,j,k,l
    - name: focus {parent, brother, first, second} node
      key: super + p, b, comma, period
    - name: focus {next, prev} window in current desktop
      key: super + {_, shift +} c
    - name: focus {next, prev} desktop
      key: super + bracket{left, right}
    - name: focus last node/desktop
      key: super + {grave, tab}
    - name: focus {older, newer} node in history
      key: super + {o, i}
    - name: focus or send to desktop
      key: super + {_, shift +} 1-9,0

    - name: preselect direction
      key: super + ctrl + {h,j,k,l}
    - name: preselect ratio
      key: super + ctrl + {1-9}
    - name: cancel preselection for focused node
      key: super + ctrl + space
    - name: cancel preselection for focused desktop
      key: super + ctrl + shift + space

    - name: expand window
      key: super + alt + {h,j,k,l}
    - name: contract window
      key: super + alt + shift + {h,j,k,l}
    - name: move floating window
      key: super + {Left,Down,Up,Right}


================================================
FILE: examples/changefile
================================================
#!/usr/bin/env bash
# A raw script to convert a cheat file from
# https://github.com/cheat/cheatsheets\
# to a markdown file for keyb cheat sheets.
FILENAME=${1//.yml/}
sed -E -i 's/^[-]//g' "$1"
sed -E -i '2s/^//' "$1"
sed -E -i '/./!d' "$1"
sed -E -i 's/"//g' "$1"
sed -E -i 's/^# (.*)/\ \ - name: "\1"/' "$1"
sed -E -i 's/^([a-zA-Z].*)/\ \ \ \ key: "\1"/' "$1"
sed -E -i "1s|^|# https://github.com/cheat/cheatsheets\n|" "$1"
sed -E -i "2s/^/- name: $FILENAME\n/" "$1"
sed -E -i "3s/^/\ \ keybinds:\n/" "$1"


================================================
FILE: examples/config/README.md
================================================
# Configuration

keyb will accept the following config in decreasing priority:

- `-c FILE` flag
- The default config path `$XDG_CONFIG_HOME/keyb/config.yml` (see note)
- The default configuration (see [default.yml](default.yml))

Note: If `$XDG_CONFIG_HOME` is set, it will be prioritized and used in Unix and Darwin
systems. Otherwise, keyb will fall back to the default OS config directory
defined as such:

- Unix: `$XDG_CONFIG_HOME/keyb/`,
- MacOS/Darwin: `$HOME/Library/Application Support/keyb/`,
- Windows: `%Appdata%\keyb\`

**Note**: `*.json` files are also supported.

## Options

| Option        | Default                  | Description |
| ------------- | ------------------------ | ----------- |
| `keyb_path`   | OS-dependent (see above) | keyb file path |
| `debug`       | `false`                  | Debug mode |
| `reverse`     | `false`                  | Swap the name and key columns |
| `mouse`       | `true`                   | Mouse enabled |
| `search_mode` | `false`                  | Start in search mode |
| `sort_keys`   | `false`                  | Sort keys alphabetically |
| `title`       | `""`                     | Title text |
| `prompt`      | `"keys > "`              | Search bar prompt text |
| `prompt_location` | `"top"`                | Location of search bar: `top, bottom` |
| `placeholder` | `"..."`                  | Search bar placeholder text |
| `prefix_sep`  | `";"`                    | Separator symbol between prefix and key |
| `sep_width`   | `4`                      | Separation width between columns |
| `margin`      | `0`                      | Space between window and border |
| `padding`     | `1`                      | Space between border and text |
| `border`      | `"hidden"`               | Border style: `normal, rounded, double, thick, hidden`|

### Color
Both ANSI and hex color codes are supported.

| Color Option     | Default    | Description |
| ---------------- | ---------- | ----------- |
| `prompt`         | -          | Prompt text color |
| `cursor_fg`      | -          | Cursor foreground |
| `cursor_bg`      | -          | Cursor background |
| `filter_fg`      | `"#FFA066"`| Filter matching text foreground |
| `filter_bg`      | -          | Filter matching text background |
| `counter_fg`     | -          | Counter foreground |
| `counter_bg`     | -          | Counter background |
| `placeholder_fg` | -          | Placeholder foreground |
| `placeholder_bg` | -          | Placeholder background |
| `border_color`   | -          | Border color |

If you are missing colors, see [Missing Colors](../../README.md#missing-colors).

### Hotkeys
Multiple keys may be set for a single binding, separated by commas.

| Hotkey                  | Default                    | Description      |
| ----------------------- | -------------------------- | ---------------- |
| `up`, `down`            | <kbd>j, k / Up, Down</kbd> | Move cursor      |
| `up_focus`, `down_focus`| <kbd>Ctrl + j, ctrl + k </kbd> | Move cursor in search mode |
| `half_up, half_down`    | <kbd>Ctrl + u, d</kbd>     | Move half window (also works in search mode) |
| `full_up, full_down`    | <kbd>Ctrl + b, f</kbd>     | Move full window (also works in search mode) |
| `top, middle, bottom`   | <kbd>H, M, L</kbd>         | Go to top, middle, bottom of screen |
| `first_line, last_line` | <kbd>g, G</kbd>            | Go to first, last line |
| `search`                | <kbd>/</kbd>               | Enter search mode      |
| `clear_search`          | <kbd>Alt + d</kbd>         | Clear current search (remains in search mode) |
| `normal`                | <kbd>Esc</kbd>             | Exit search mode |
| `quit`                  | <kbd>Ctrl + c, q</kbd>     | Quit		      |

These hotkeys configure the cursor behaviour in the search bar only:

| Hotkey                  | Default                     | Description      |
| ----------------------- | --------------------------- | ---------------- |
| `cursor_word_forward`     | <kbd>alt+right, alt+f</kbd> | Move forward by word |
| `cursor_word_backward`    | <kbd>alt+left, alt+b</kbd>  | Move backward by word |
| `cursor_delete_word_backward` | <kbd>alt+backspace</kbd> | Delete word backward |
| `cursor_delete_word_forward`  | <kbd>alt+delete</kbd>   | Delete word forward |
| `cursor_delete_after_cursor`  | <kbd>alt+k</kbd>        | Delete after cursor |
| `cursor_delete_before_cursor` | <kbd>alt+u</kbd>        | Delete before cursor |
| `cursor_line_start`       | <kbd>home, ctrl+a</kbd>     | Move cursor to start |
| `cursor_line_end`         | <kbd>end, ctrl+e</kbd>      | Move cursor to end |
| `cursor_paste`            | <kbd>ctrl+v</kbd>           | Paste into search bar|


================================================
FILE: examples/config/default.yml
================================================
settings:
  keyb_path: "$HOME/.config/keyb/keyb.yml"
  debug: false
  reverse: false
  mouse: true
  search_mode: false
  sort_keys: false
  title: ""
  prompt: 'keys > '
  prompt_location: "top"
  placeholder: '...'
  prefix_sep: ;
  sep_width: 4
  margin: 0
  padding: 1
  border: hidden
color:
  prompt: ""
  cursor_fg: ""
  cursor_bg: ""
  filter_fg: "#FFA066"
  filter_bg: ""
  border_color: ""
keys:
  quit: q, ctrl+c
  up: k, up
  down: j, down
  up_focus: ctrl+k
  down_focus: ctrl+j
  half_up: ctrl+u
  half_down: ctrl+d
  full_up: ctrl+b
  full_bottom: ctrl+f
  first_line: g
  last_line: G
  top: H
  middle: M
  bottom: L
  search: /
  clear_search: alt+d
  normal: esc
  cursor_word_forward: "alt+right, alt+f"
  cursor_word_backward: "alt+left, alt+b"
  cursor_delete_word_backward: "alt+backspace"
  cursor_delete_word_forward: "alt+delete"
  cursor_delete_after_cursor: "alt+k"
  cursor_delete_before_cursor: "alt+u"
  cursor_line_start: "home, ctrl+a"
  cursor_line_end: "end, ctrl+e"
  cursor_paste: "ctrl+v"


================================================
FILE: examples/curl.yml
================================================
# https://github.com/cheat/cheatsheets
- name: curl
  keybinds:
    - name: "To download a file"
      key: "curl <url>"
    - name: "To download and rename a file"
      key: "curl <url> -o <outfile>"
    - name: "To download multiple files"
      key: "curl -O <url> -O <url>"
    - name: "To download all sequentially numbered files (1-24)"
      key: "curl http://example.com/pic[1-24].jpg"
    - name: "To download a file and pass HTTP authentication"
      key: "curl -u <username>:<password> <url>"
    - name: "To download a file with a proxy"
      key: "curl -x <proxy-host>:<port> <url>"
    - name: "To download a file over FTP"
      key: "curl -u <username>:<password> -O ftp://example.com/pub/file.zip"
    - name: "To get an FTP directory listing"
      key: "curl ftp://username:password@example.com"
    - name: "To resume a previously failed download"
      key: "curl -C - -o <partial-file> <url>"
    - name: "To fetch only the HTTP headers from a response"
      key: "curl -I <url>"
    - name: "To fetch your external IP and network info as JSON"
      key: "curl http://ifconfig.me/all.json"
    - name: "To limit the rate of a download"
      key: "curl --limit-rate 1000B -O <outfile>"
    - name: "To get your global IP"
      key: "curl httpbin.org/ip "
    - name: "To get only the HTTP status code"
      key: "curl -o /dev/null -w '%{http_code}\n' -s -I URL"


================================================
FILE: examples/cut.yml
================================================
# Copied from cut --help
- name: cut
  keybinds:
    - name: "To cut out the third field of text or stdoutput that is delimited by a #"
      key: "cut -d# -f3"
    - name: "select only these bytes"
      key: "cut -b, --bytes=LIST <file>"
    - name: "select only these characters"
      key: "cut -c, --characters=LIST <file>"
    - name: "use DELIM instead of TAB for field delimiter"
      key: "cut -d, --delimiter=DELIM <file>"
    - name: "complement the set of selected bytes, characters or fields"
      key: "cut --complement <file>"
    - name: "do not print lines not containing delimiters"
      key: "cut -s, --only-delimited <file>"
    - name: "use STRING as the output delimiter the default is to use the input delimiter"
      key: "cut --output-delimiter=STRING <file>"
    - name: "line delimiter is NUL, not newline"
      key: "cut -z, --zero-terminated <file>"


================================================
FILE: examples/date.yml
================================================
# Copied from date --help
- name: date
  keybinds:
    - name: "To print the date in a format suitable for affixing to file names:"
      key: "date +%Y%m%d_%H%M%S"
    - name: "To show date in CET:"
      key: "TZ=CET date"
    - name: "To show the time on the west coast of the US (use tzselect(1) to find TZ):"
      key: "TZ='America/Los_Angeles' date"
    - name: "display time described by STRING, not 'now'"
      key: "date -d, --date=STRING"
    - name: "like --date; once for each line of DATEFILE"
      key: "date -f, --file=DATEFILE"
    - name: "output date/time in ISO 8601 format e.g. 006-08-14T02:34:56-06:00"
      key: "date -I[FMT], --iso-8601[=FMT]"
    - name: "output the available resolution of timestamps e.g. 0.000000001"
      key: "date --resolution"
    - name: "output date and time in RFC 5322 format. e.g. Mon, 14 Aug 2006 02:34:56 -0600"
      key: "date -R, --rfc-email"
    - name: "output date/time in RFC 3339 format e.g. 2006-08-14 02:34:56-06:00"
      key: "date --rfc-3339=FMT"
    - name: "display the last modification time of FILE"
      key: "date -r, --reference=FILE"
    - name: "set time described by STRING"
      key: "date -s, --set=STRING"
    - name: "print or set Coordinated Universal Time (UTC)"
      key: "date -u, --utc, --universal "
- name: date (FORMAT controls the output)
  keybinds:
    - name: "a literal %"
      key: "%%"
    - name: "locale's abbreviated weekday name (e.g., Sun)"
      key: "%a"
    - name: "locale's full weekday name (e.g., Sunday)"
      key: "%A"
    - name: "locale's abbreviated month name (e.g., Jan)"
      key: "%b"
    - name: "locale's full month name (e.g., January)"
      key: "%B"
    - name: "locale's date and time (e.g., Thu Mar  3 23:05:25 2005)"
      key: "%c"
    - name: "century; like %Y, except omit last two digits (e.g., 20)"
      key: "%C"
    - name: "day of month (e.g., 01)"
      key: "%d"
    - name: "date; same as %m/%d/%y"
      key: "%D"
    - name: "day of month, space padded; same as %_d"
      key: "%e"
    - name: "full date; like %+4Y-%m-%d"
      key: "%F"
    - name: "last two digits of year of ISO week number (see %G)"
      key: "%g"
    - name: "year of ISO week number (see %V); normally useful only with %V"
      key: "%G"
    - name: "same as %b"
      key: "%h"
    - name: "hour (00..23)"
      key: "%H"
    - name: "hour (01..12)"
      key: "%I"
    - name: "day of year (001..366)"
      key: "%j"
    - name: "hour, space padded ( 0..23); same as %_H"
      key: "%k"
    - name: "hour, space padded ( 1..12); same as %_I"
      key: "%l"
    - name: "month (01..12)"
      key: "%m"
    - name: "minute (00..59)"
      key: "%M"
    - name: "a newline"
      key: "%n"
    - name: "nanoseconds (000000000..999999999)"
      key: "%N"
    - name: "locale's equivalent of either AM or PM; blank if not known"
      key: "%p"
    - name: "like %p, but lower case"
      key: "%P"
    - name: "quarter of year (1..4)"
      key: "%q"
    - name: "locale's 12-hour clock time (e.g., 11:11:04 PM)"
      key: "%r"
    - name: "24-hour hour and minute; same as %H:%M"
      key: "%R"
    - name: "seconds since the Epoch (1970-01-01 00:00 UTC)"
      key: "%s"
    - name: "second (00..60)"
      key: "%S"
    - name: "a tab"
      key: "%t"
    - name: "time; same as %H:%M:%S"
      key: "%T"
    - name: "day of week (1..7); 1 is Monday"
      key: "%u"
    - name: "week number of year, with Sunday as first day of week (00..53)"
      key: "%U"
    - name: "ISO week number, with Monday as first day of week (01..53)"
      key: "%V"
    - name: "day of week (0..6); 0 is Sunday"
      key: "%w"
    - name: "week number of year, with Monday as first day of week (00..53)"
      key: "%W"
    - name: "locale's date representation (e.g., 12/31/99)"
      key: "%x"
    - name: "locale's time representation (e.g., 23:13:48)"
      key: "%X"
    - name: "last two digits of year (00..99)"
      key: "%y"
    - name: "year"
      key: "%Y"
    - name: "+hhmm numeric time zone (e.g., -0400)"
      key: "%z"
    - name: "z  +hh:mm numeric time zone (e.g., -04:00)"
      key: "%:"
    - name: ":z  +hh:mm:ss numeric time zone (e.g., -04:00:00)"
      key: "%:"
    - name: "::z  numeric time zone with : to necessary precision (e.g., -04, +05:30)"
      key: "%:"
    - name: "alphabetic time zone abbreviation (e.g., EDT)"
      key: "%Z"


================================================
FILE: examples/dd.yml
================================================
# https://github.com/cheat/cheatsheets
- name: dd
  keybinds:
    - name: "Backup disk partition with dd"
      key: "dd if=/dev/sda1 of=~/localdisk_sda1.img"
    - name: "Backup disk partition with dd and compress"
      key: "dd if=/dev/sda1 | gzip -c > ~/localdisk_sda1.img.gz"
    - name: "Restore disk partition with dd"
      key: "dd if=~/localdisk_sda1.img of=/dev/sda1"
    - name: "Restore disk partition with dd  with decompression"
      key: "gunzip -c ~/localdisk_sda1.img.gz | dd of=/dev/sda1"
    - name: "Every iteration, we read 512 Bytes."
      key: "dd if=/dev/urandom of=/tmp/test.txt count=2 bs=512"
    - name: "Create fixed size file with dd. Use sizes such as K, M and G with bs"
      key: "dd if=/dev/zero of=/root/test bs=1024 count=1"
    - name: "Convert text to lower case with dd and conv"
      key: "dd if=filetoconvert.txt of=convertedfile.txt conv=lcase"
    - name: "dd with the built-in `progress` functionality"
      key: "dd if=/dev/zero of=/dev/null bs=128M status=progress"
    - name: "dd with graphical return"
      key: "dcfldd if=/dev/zero of=/dev/null bs=500K"
    - name: "Create a 1MB file with zero allocated blocks:"
      key: "dd if=/dev/zero of=foo1 seek=1 bs=1M count=0"


================================================
FILE: examples/discord.yml
================================================
- name: discord (navigation)
  keybinds:
    - name: "Switch Servers"
      key: "[Ctrl] [Alt] [↑] or [↓]"
    - name: "Switch Channels"
      key: "[Alt] [↑] or [↓]"
    - name: "Switch Unread Channels"
      key: "[Alt] [Shift] [↑] or [↓]"
    - name: "Switch Unread & Mention Channels"
      key: "[Ctrl] [Shift] [Alt] [↑] or [↓]"
    - name: "Scroll Chat"
      key: "[Page Up] or [Page Down]"
    - name: "Jump to Oldest Unread Message"
      key: "[Shift] [Page Up]"
    - name: "Return To Connected Audio Channel"
      key: "[Ctrl] [Alt] [←]"
    - name: "Switch Last Channel/DMs"
      key: "[Ctrl] [Alt] [→]"

- name: discord (menus)
  keybinds:
    - name: "Pins Popout"
      key: "[Ctrl] [P]"
    - name: "Mentions Popout"
      key: "[Ctrl] [I]"
    - name: "Member List"
      key: "[Ctrl] [U]"
    - name: "Emoji Picker"
      key: "[Ctrl] [E]"
    - name: "Help"
      key: "[Ctrl] [Shift] [H]"
    - name: "Search"
      key: "[Ctrl] [F]"

- name: discord (creation)
  keybinds:
    - name: "Create/Join Server"
      key: "[Ctrl] [Shift] [N]"
    - name: "Find/Start DM"
      key: "[Ctrl] [K]"
    - name: "Create Group"
      key: "[Ctrl] [Shift] [T]"
    - name: "Start Call"
      key: "[Ctrl] [']"
    - name: "Focus Text Area"
      key: "Tab"
    - name: "Upload File"
      key: "[Ctrl] [Shift] [U]"

- name: discord (interaction)
  keybinds:
    - name: "Mark Server Read"
      key: "[Shift] [Esc]"
    - name: "Mark Channel Read"
      key: "Esc"
    - name: "Answer Call"
      key: "[Ctrl] [Enter]"
    - name: "Decline Call"
      key: "Esc"
    - name: "Mute"
      key: "[Ctrl] [Shift] [M]"
    - name: "Deafen"
      key: "[Ctrl] [Shift] [D]"


================================================
FILE: examples/du.yml
================================================
# https://github.com/cheat/cheatsheets
- name: du
  keybinds:
    - name: "To sort directories/files by size"
      key: "du -sk *| sort -rn"
    - name: "To show cumulative human-readable size"
      key: "du -sh"
    - name: "To show cumulative human-readable size and dereference symlinks"
      key: "du -shL"
    - name: "Show apparent size instead of disk usage (so sparse files will show greater than zero)"
      key: "du -h --apparent-size"
    - name: "To sort directories/files by size (human-readable)"
      key: "du -sh * | sort -rh "
    - name: "To list the 20 largest files and folders under the current working directory"
      key: "du -ma | sort -nr | head -n 20"


================================================
FILE: examples/dwm.yml
================================================
# https://github.com/cheat/cheatsheets
- name: dwm
  keybinds:
    - name: "launch terminal."
      key: "[Shift]+[Mod]+[Enter]"
    - name: "show/hide bar."
      key: "[Mod]+[b]"
    - name: "dmenu for running programs like the x#www#browser."
      key: "[Mod]+[p]"
    - name: "push acive window from stack to master, or pulls last used window from stack onto master."
      key: "[Mod]+[Enter]"
    - name: "focus on next/previous window in current tag."
      key: "[Mod] + [j / k]"
    - name: "increases / decreases master size."
      key: "[Mod] + [h / l]"
- name: dwm (Navigation)
  keybinds:
    - name: "moves your focus to tag 2."
      key: "[Mod]+[2]"
    - name: "move active window to the 2 tag."
      key: "[Shift]+[Mod]+[2]"
    - name: "increases / decreases number of windows on master"
      key: "[Mod] + [i / d]"
    - name: "move focus between screens (multi monitor setup)"
      key: "[Mod] + [, / .]"
    - name: "move active window to different screen."
      key: "[Shift]+[Mod]+[, / .]"
    - name: "view all windows on screen."
      key: "[Mod]+[0]"
    - name: "make focused window appear on all tags."
      key: "[Shift]+[Mod]+[0]"
    - name: "kill active window."
      key: "[Shift]+[Mod]+[c]"
    - name: "quit dwm cleanly."
      key: "[Shift]+[Mod]+[q]"
- name: dwm (Layout)
  keybinds:
    - name: "tiled mode. []="
      key: "[Mod]+[t]"
    - name: "floating mode. ><>"
      key: "[Mod]+[f]"
    - name: "monocle mode. [M] (single window fullscreen)"
      key: "[Mod]+[m]"
- name: dwm (Floating)
  keybinds:
    - name: "to resize the floating window."
      key: "[Mod]+[R M B]"
    - name: "to move the floating window around."
      key: "[Mod]+[L M B]"
    - name: "toggles to the previous layout mode."
      key: "[Mod]+[Space]"
    - name: "to make an individual window float."
      key: "[Mod]+[Shift]+[Space]"
    - name: "to make an individual window un#float."
      key: "[Mod]+[M M B]"


================================================
FILE: examples/find.yml
================================================
# https://github.com/cheat/cheatsheets
- name: find
  keybinds:
  - name: "To find files following symlinks (otherwise the symlinks are not followed)"
    key: "find -L . -type f"
  - name: "To find files by case-insensitive extension (ex: .jpg, .JPG, .jpG)"
    key: "find . -iname *.jpg"
  - name: "To find directories"
    key: "find . -type d"
  - name: "To find files"
    key: "find . -type f"
  - name: "To find files by octal permission"
    key: "find . -type f -perm 777"
  - name: "To find files with setuid bit set"
    key: "find . -xdev ( -perm -4000 ) -type f -print0 | xargs -0 ls -l"
  - name: "To find files newer than 1 day old and copy elsewhere (remove -p flag in xargs to not be asked)"
    key: "find . -type f -ctime -1 -print0 | xargs -0 -p cp -t <dir>"
  - name: "To find files with extension '.txt' and remove them"
    key: "find ./path/ -name '*.txt' -delete"
  - name: "To find files with tilde as postfix and remove them"
    key: "find ./path/ -name '*~' -delete"
  - name: "To find files with extension '.txt' and dump their contents"
    key: "find ./path/ -name '*.txt' -exec cat '{}' ;"
  - name: "To find files with extension '.txt' and look for a string into them"
    key: "find ./path/ -name '*.txt' | xargs grep 'string'"
  - name: "To find files with size bigger than 5 Mebibyte and sort them by size"
    key: "find . -size +5M -type f -print0 | xargs -0 ls -Ssh | sort -z"
  - name: "To find files modified more than 7 days ago and list file information"
    key: "find . -type f -mtime +7d -ls"
  - name: "To find symlinks owned by a user and list file information"
    key: "find . -type l -user <username-or-userid> -ls"
  - name: "To search for and delete empty directories"
    key: "find . -type d -empty -exec rmdir {} ;"
  - name: "To search for directories named build at a max depth of 2 directories"
    key: "find . -maxdepth 2 -name build -type d"
  - name: "To search all files who are not in .git directory"
    key: "find . ! -iwholename '*.git*' -type f"
  - name: "To find all files that have the same node (hard link) as MY_FILE_HERE"
    key: "find . -type f -samefile MY_FILE_HERE 2>/dev/null"
  - name: "To find all files in the current directory and modify their permissions"
    key: "find . -type f -exec chmod 644 {} ;"
  - name: "To find all files changed in last 2 days"
    key: "find . -type f -ctime -48h"
  - name: "Or created in last 2 days"
    key: "find . -type f -Btime -2"
  - name: "Or accessed in last 2 days"
    key: "find . -type f -atime -2"
  - name: "To find and rename (imperfect) all files and dirs that have a comma in the"
    key: "find . -name '*,*' | while read f; do echo mv $f ${f//,/};done"
  - name: "To find all broken links. Note -L returns a file unless it is a broken link"
    key: "find -L /usr/ports/packages -type l"
  - name: "To find and run multiple shell commands (without multiple execs)"
    key: "find . -type f -exec sh -c echo '{}'; cat '{}';"


================================================
FILE: examples/firefox.yml
================================================
- name: Firefox Navigation
  keybinds:
    - name: "Back"
      key: "[Alt] [←]"
    - name: "Forward"
      key: "[Alt] [→]"
    - name: "Home"
      key: "[Alt] [Home]"
    - name: "Open File"
      key: "[Ctrl] [O]"
    - name: "Reload"
      key: "F5"
    - name: "Reload (override cache)"
      key: "[Ctrl] [F5]"
    - name: "Stop"
      key: "Esc"
- name: Firefox Current Page
  keybinds:
    - name: "Go Down a Screen"
      key: "[Page Down]"
    - name: "Go Up a Screen"
      key: "[Page Up]"
    - name: "Go to Bottom of Page"
      key: "End"
    - name: "Go to Top of Page"
      key: "Home"
    - name: "Move to Next Frame"
      key: "F6"
    - name: "Move to Previous Frame"
      key: "[Shift] [F6]"
    - name: "Print"
      key: "[Ctrl] [P]"
    - name: "Save Focused Link"
      key: "[Alt] [Enter]"
    - name: "Save Page As"
      key: "[Ctrl] [S]"
    - name: "Zoom In"
      key: "[Ctrl] [+]"
    - name: "Zoom Out"
      key: "[Ctrl] [-]"
    - name: "Zoom Reset"
      key: "[Ctrl] [0]"
- name: Firefox Editing
  keybinds:
    - name: "Copy"
      key: "[Ctrl] [C]"
    - name: "Cut"
      key: "[Ctrl] [X]"
    - name: "Delete"
      key: "Del"
    - name: "Paste"
      key: "[Ctrl] [V]"
    - name: "Paste (as plain text)"
      key: "[Ctrl] [Shift] [V]"
    - name: "Redo"
      key: "[Ctrl] [Shift] [Z]"
    - name: "Select All"
      key: "[Ctrl] [A]"
    - name: "Undo"
      key: "[Ctrl] [Z]"
- name: Firefox Search
  keybinds:
    - name: "Find"
      key: "[Ctrl] [F]"
    - name: "Find Again"
      key: "[Ctrl] [G]"
    - name: "Find Previous"
      key: "[Shift] [F3]"
    - name: "Quick Find within link-text only"
      key: "'"
    - name: "Quick Find"
      key: "/"
    - name: "Close the Find or Quick Find bar (when focused)"
      key: "Esc"
    - name: "Focus Search bar"
      key: "[Ctrl] [K]"
    - name: "Focus Search bar (Alternative)"
      key: "[Ctrl] [J]"
    - name: "Quickly switch between search engines (when Search Bar is focused)"
      key: "[Ctrl] ( [↑] / [↓] )"
    - name: "View menu to switch, add or manage search engines (when Search Bar is focused)"
      key: "[Alt] ( [↑] / [↓] )"
- name: Firefox Windows & Tabs
  keybinds:
    - name: "Close Tab"
      key: "[Ctrl] [W]"
    - name: "Close Tab (Alternative)"
      key: "[Ctrl] [F4]"
    - name: "Close Window"
      key: "[Ctrl] [Shift] [W]"
    - name: "Move Tab in focus Left"
      key: "[Ctrl] [Shift] [Page Up]"
    - name: "Move Tab in focus Right"
      key: "[Ctrl] [Shift] [Page Down]"
    - name: "Move Tab in focus to start"
      key: "[Ctrl] [Home]"
    - name: "Move Tab in focus to end"
      key: "[Ctrl] [End]"
    - name: "New Tab"
      key: "[Ctrl] [T]"
    - name: "New Window"
      key: "[Ctrl] [N]"
    - name: "New Private Window"
      key: "[Ctrl] [Shift] [P]"
    - name: "Next Tab"
      key: "[Ctrl] [Tab]"
    - name: "Open Address in New Tab"
      key: "[Alt] [Enter]"
    - name: "Previous Tab"
      key: "[Ctrl] [Shift] [Tab]"
    - name: "Undo Close Tab"
      key: "[Ctrl] [Shift] [T]"
    - name: "Undo Close Window"
      key: "[Ctrl] [Shift] [N]"
    - name: "Select Tab 1 to 8"
      key: "[Ctrl] [1 to 8]"
    - name: "Select Last Tab"
      key: "[Alt] [9]"
    - name: "Tab Groups View"
      key: "[Ctrl] [Shift] [E]"
    - name: "Close Tab Groups View"
      key: "Esc"
    - name: "Next Tab Group (only for some keyboard layouts)"
      key: "[Ctrl] [`]"
    - name: "Previous Tab Group (only for some keyboard layouts)"
      key: "[Ctrl] [Shift] [`]"
- name: Firefox History
  keybinds:
    - name: "History sidebar"
      key: "[Ctrl] [H]"
    - name: "Library window (History)"
      key: "[Ctrl] [Shift] [H]"
    - name: "Clear Recent History"
      key: "[Ctrl] [Shift] [Del]"
- name: Firefox Bookmarks
  keybinds:
    - name: "Bookmark All Tabs"
      key: "[Ctrl] [Shift] [D]"
    - name: "Bookmark This Page"
      key: "[Ctrl] [D]"
    - name: "Bookmarks sidebar"
      key: "[Ctrl] [B]"
    - name: "Library window (Bookmarks)"
      key: "[Ctrl] [Shift] [B]"
- name: Firefox Tools
  keybinds:
    - name: "Downloads"
      key: "[Ctrl] [J]"
    - name: "Add-ons"
      key: "[Ctrl] [Shift] [A]"
    - name: "Toggle Developer Tools"
      key: "[Ctrl] [Shift] [I]"
    - name: "Toggle Developer Tools (Alternative)"
      key: "F12"
    - name: "Web Console"
      key: "[Ctrl] [Shift] [K]"
    - name: "Inspector"
      key: "[Ctrl] [Shift] [C]"
    - name: "Debugger"
      key: "[Ctrl] [Shift] [S]"
    - name: "Style Editor"
      key: "[Shift] [F7]"
    - name: "Profiler"
      key: "[Shift] [F5]"
    - name: "Network"
      key: "[Ctrl] [Shift] [Q]"
    - name: "Developer Toolbar"
      key: "[Shift] [F2]"
    - name: "Responsive Design View"
      key: "[Ctrl] [Shift] [M]"
    - name: "Scratchpad"
      key: "[Shift] [F4]"
    - name: "Page Source"
      key: "[Ctrl] [U]"
    - name: "Browser Console"
      key: "[Ctrl] [Shift] [J]"
- name: Firefox PDF Viewer
  keybinds:
    - name: "Next page"
      key: "→"
    - name: "Previous page"
      key: "←"
    - name: "Zoom in"
      key: "[Ctrl] [+]"
    - name: "Zoom out"
      key: "[Ctrl] [-]"
    - name: "Automatic Zoom"
      key: "[Ctrl] [0]"
    - name: "Rotate the document clockwise"
      key: "R"
    - name: "Rotate counterclockwise"
      key: "[Shift] [R]"
    - name: "Switch to Presentation Mode"
      key: "[Ctrl] [Alt] [P]"
    - name: "Toggle Hand Tool"
      key: "H"
    - name: "Focus the Page Number input box"
      key: "[Ctrl] [Alt] [G]"
- name: Firefox Miscellaneous
  keybinds:
    - name: "Complete .com Address"
      key: "[Ctrl] [Enter]"
    - name: "Complete .net Address"
      key: "[Shift] [Enter]"
    - name: "Complete .org Address"
      key: "[Ctrl] [Shift] [Enter]"
    - name: "Delete Selected Autocomplete Entry"
      key: "Del"
    - name: "Toggle Full Screen"
      key: "F11"
    - name: "Toggle Menu Bar activation (KDE) (showing it temporarily when hidden)"
      key: "Alt"
    - name: "Toggle Menu Bar activation (GNOME) (showing it temporarily when hidden)"
      key: "F10"
    - name: "Show/Hide Add-on Bar"
      key: "[Ctrl] [/]"
    - name: "Caret Browsing"
      key: "F7"
    - name: "Select Location Bar"
      key: "F6"
- name: Firefox Audio & Video Media
  keybinds:
    - name: "Toggle Play / Pause"
      key: "Space bar"
    - name: "Decrease volume"
      key: "↓"
    - name: "Increase volume"
      key: "↑"
    - name: "Mute audio"
      key: "[Ctrl] [↓]"
    - name: "Unmute audio"
      key: "[Ctrl] [↑]"
    - name: "Seek back 15 seconds"
      key: "←"
    - name: "Seek back 10 %"
      key: "[Ctrl] [←]"
    - name: "Seek forward 15 seconds"
      key: "→"
    - name: "Seek forward 10 %"
      key: "[Ctrl] [→]"
    - name: "Seek to the beginning"
      key: "Home"
    - name: "Seek to the end"
      key: "End"


================================================
FILE: examples/irssi.yml
================================================
# https://github.com/cheat/cheatsheets
- name: irssi
  keybinds:
  - name: "To connect to an IRC server:"
    key : "/connect <server>"
  - name: "To join a channel:"
    key: "/join #<channel>"
  - name: "To set a nickname:"
    key: "/nick <nickname>"
  - name: "To send a private message to a user:"
    key: "/msg <nickname>"
  - name: "To close the current channel window:"
    key: "/wc"
  - name: "To switch between channel windows:"
    key: "ALT+<number>, eg. ALT+1, ALT+2"
  - name: "To list the nicknames within the active channel:"
    key: "/names"
  - name: "To change the channel topic:"
    key: "/topic <description>"
  - name: "foo,#bar JOINS PARTS QUITS NICKS   # Quieten only channels `#foo`, `#bar`"
    key: "/ignore "
  - name: "Quieten all channels"
    key: "/ignore * JOINS PARTS QUITS NICKS"
  - name: "To save the current Irssi session config into the configuration file:"
    key: "/save"
  - name: "To quit Irssi:"
    key: "/exit"


================================================
FILE: examples/kitty.yml
================================================
# https://github.com/cheat/cheatsheets
- name: kitty
  keybinds:
    - name: "Line up"
      key: "ctrl+shift+up"
    - name: "Line down"
      key: "ctrl+shift+down"
    - name: "Page up"
      key: "ctrl+shift+page_up"
    - name: "Page down"
      key: "ctrl+shift+page_down"
    - name: "Top"
      key: "ctrl+shift+home"
    - name: "Bottom"
      key: "ctrl+shift+end"
    - name: "Previous shell prompt"
      key: "ctrl+shift+z"
    - name: "Next shell prompt"
      key: "ctrl+shift+x"
    - name: "Browse scrollback in less"
      key: "ctrl+shift+h"
    - name: "Browse last cmd output"
      key: "ctrl+shift+g"
    - name: "New tab"
      key: "ctrl+shift+t"
    - name: "Close tab"
      key: "ctrl+shift+q"
    - name: "Next tab"
      key: "ctrl+shift+right"
    - name: "Previous tab"
      key: "ctrl+shift+left"
    - name: "Next layout"
      key: "ctrl+shift+l"
    - name: "Move tab forward"
      key: "ctrl+shift+."
    - name: "Move tab backward"
      key: "ctrl+shift+,"
    - name: "Set tab title"
      key: "ctrl+shift+alt+t"
    - name: "Copy to clipboard"
      key: "ctrl+shift+c"
    - name: "Paste from clipboard"
      key: "ctrl+shift+v"
    - name: "Paste from selection"
      key: "ctrl+shift+s"
    - name: "Increase font size"
      key: "ctrl+shift+equal"
    - name: "Decrease font size"
      key: "ctrl+shift+minus"
    - name: "Restore font size"
      key: "ctrl+shift+backspace"
    - name: "New window"
      key: "ctrl+shift+enter"
    - name: "New OS window"
      key: "ctrl+shift+n"
    - name: "Close window"
      key: "ctrl+shift+w"
    - name: "Next window"
      key: "ctrl+shift+]"
    - name: "Previous window"
      key: "ctrl+shift+["
    - name: "Move window forward"
      key: "ctrl+shift+f"
    - name: "Move window backward"
      key: "ctrl+shift+b"
    - name: "Move window to top"
      key: "ctrl+shift+`"
    - name: "Visually focus window"
      key: "ctrl+shift+f7"
    - name: "Visually swap window"
      key: "ctrl+shift+f8"
    - name: "Focus specific window"
      key: "ctrl+shift+1, ctrl+shift+2 … ctrl+shift+0"
    - name: "Open URL"
      key: "ctrl+shift+e"
    - name: "Insert selected path"
      key: "ctrl+shift+p>f"
    - name: "Open selected path"
      key: "ctrl+shift+p>shft+f"
    - name: "Insert selected line"
      key: "ctrl+shift+p>l"
    - name: "Insert selected word"
      key: "ctrl+shift+p>w"
    - name: "Insert selected hash"
      key: "ctrl+shift+p>h"
    - name: "Open the selected file at the selected line"
      key: "ctrl+shift+p>n"
    - name: "Open the selected hyperlink"
      key: "ctrl+shift+p>y"


================================================
FILE: examples/less.yml
================================================
# https://github.com/cheat/cheatsheets
- name: less
  keybinds:
  - name: "Go to end of file:"
    key: "G"
  - name: "Go to start of file:"
    key: "g"
  - name: "To edit the current file in $EDITOR:"
    key: "v"
  - name: "Search in file:"
    key: "/<searchterm>"
  - name: "Next result:"
    key: "n"
  - name: "Previous result:"
    key: "N"
  - name: "Toggle search highlighting:"
    key: "Alt-u"
  - name: "Follow (tail -f) a file after opening it:"
    key: "F"
  - name: "Move to next file (if multiple files opened, eg. `file1 file2`):"
    key: ":n"
  - name: "Move to previous file:"
    key: ":p"
  - name: "To save the contents to a file:"
    key: "s <filename>"


================================================
FILE: examples/ranger.yml
================================================
- name: ranger
  keybinds:
    - name: "navigation and tabs"
      key: "g"
    - name: "':open_with' command"
      key: "r"
    - name: "yank(copy)"
      key: "y"
    - name: "paste"
      key: "p"
    - name: "cut/delete"
      key: "d"
    - name: "sort"
      key: "o"
    - name: "filter_stack"
      key: "."
    - name: "changing settings"
      key: "z"
    - name: "undo"
      key: "u"
    - name: "linemode"
      key: "M"
    - name: "+, -, ="
      key: "setting access rights to files"


================================================
FILE: examples/screen.yml
================================================
# https://github.com/cheat/cheatsheets
- name: screen
  keybinds:
  - name: "To start a new named screen session:"
    key: "screen -S <session-name>"
  - name: "To detach from the current session:"
    key: "Ctrl+A; d"
  - name: "To re-attach a detached session:"
    key: "screen -r <session-name>"
  - name: "To list all screen sessions:"
    key: "screen -ls"
  - name: "To quit a session:"
    key: "screen -XS <session-name> quit"


================================================
FILE: examples/tar.yml
================================================
# https://github.com/cheat/cheatsheets
- name: tar
  keybinds:
  - name: "To extract an uncompressed archive:"
    key: "tar -xvf /path/to/foo.tar"
  - name: "To extract a .tar in specified Directory:"
    key: "tar -xvf /path/to/foo.tar -C /path/to/destination/"
  - name: "To create an uncompressed archive:"
    key: "tar -cvf /path/to/foo.tar /path/to/foo/"
  - name: "To extract a .tgz or .tar.gz archive:"
    key: "tar -xzvf /path/to/foo.tgz"
  - name: "To create a .tgz or .tar.gz archive:"
    key: "tar -czvf /path/to/foo.tgz /path/to/foo/"
  - name: "To list the content of an .tgz or .tar.gz archive:"
    key: "tar -tzvf /path/to/foo.tgz"
  - name: "To extract a .tar.bz2 archive:"
    key: "tar -xjvf /path/to/foo.tar.bz2"
  - name: "To create a .tar.bz2 archive:"
    key: "tar -cjvf /path/to/foo.tar.bz2 /path/to/foo/"
  - name: "To list the content of an .tar.bz2 archive:"
    key: "tar -tjvf /path/to/foo.tar.bz2"


================================================
FILE: examples/tmux.yml
================================================
# tmux
# source: http://man.openbsd.org/OpenBSD-current/man1/tmux.1

- name: tmux (copy mode)
  prefix: "[Ctrl] [b]"
  keybinds:
    - name: "Enter copy mode"
      key: "["
    - name: "Bottom of history"
      key: "[M-<]"
    - name: "Top of history"
      key: "[M->]"
    - name: "Back to indentation"
      key: "[M-m]"
    - name: "Copy selection"
      key: "[M-w]"
    - name: "Paste selection"
      key: "[M-y]"
    - name: "Clear selection"
      key: "[Ctrl] [g]"
    - name: "Cursor to top line"
      key: "[M-R]"
    - name: "Cursor to middle line"
      key: "[M-r]"
    - name: "Cursor Up"
      key: "[↑]"
    - name: "Cursor Down"
      key: "[↓]"
    - name: "Cursor Left"
      key: "[←]"
    - name: "Cursor Right"
      key: "[→]"

- name: tmux (copy mode vi)
  prefix: "[Ctrl] [b]"
  keybinds:
      - name: "Enter copy mode"
        key: "["
      - name: "Bottom of history"
        key: "[G]"
      - name: "Top of history"
        key: "[g]"
      - name: "Copy selection"
        key: "[Enter]"
      - name: "Paste selection"
        key: "[p]"
      - name: "Cursor Up"
        key: "[k]"
      - name: "Cursor Down"
        key: "[j]"
      - name: "Cursor Left"
        key: "[h]"
      - name: "Cursor Right"
        key: "[l]"

- name: tmux (window control)
  prefix: "[Ctrl] [b]"
  keybinds:
      - name: "Create new window"
        key: "[c]"
      - name: "Detach from session"
        key: "[d]"
      - name: "Rename current window"
        key: "[,]"
      - name: "Close current window"
        key: "[&]"
      - name: "List windows"
        key: "[w]"
      - name: "Previous window"
        key: "[p]"
      - name: "Next window"
        key: "[n]"

- name: tmux (pane control)
  prefix: "[Ctrl] [b]"
  keybinds:
      - name: "Split pane horizontally"
        key: "[\"]"
      - name: "Split pane vertically"
        key: "[%]"
      - name: "Next pane"
        key: "[o]"
      - name: "Previous pane"
        key: "[;]"
      - name: "Show pane numbers"
        key: "[q]"
      - name: "Toggle pane zoom"
        key: "[z]"
      - name: "Convert pane into a window"
        key: "[!]"
      - name: "Kill current pane"
        key: "[x]"
      - name: "Swap panes"
        key: "[Ctrl] [O]"
      - name: "Display clock"
        key: "[t]"
      - name: "Transpose two letters (delete and paste)"
        key: "[q]"
      - name: "Move to the previous pane"
        key: "[{]"
      - name: "Move to the next pane"
        key: "[}]"
      - name: "Toggle between pane layouts"
        key: "[Space]"
      - name: "Resize pane (make taller)"
        key: "[↑]"
      - name: "Resize pane (make smaller)"
        key: "[↓]"
      - name: "Resize pane (make wider)"
        key: "[←]"
      - name: "Resize pane (make narrower)"
        key: "[→]"

- name: tmux (session control)
  keybinds:
      - name: "Start a new session"
        key: "tmux"
      - name: "Start a new session with the name chosen"
        key: "tmux new -s <session-name>"
      - name: "List all sessions"
        key: "tmux ls"
      - name: "Re-attach a detached session"
        key: "tmux attach -t <target-session>"
      - name: "Re-attach a detached session (and detach it from elsewhere)"
        key: "tmux attach -d -t <target-session>"
      - name: "Delete session"
        key: "tmux kill-session -t <target-session>"


================================================
FILE: examples/unzip.yml
================================================
# https://github.com/cheat/cheatsheets
- name: unzip
  keybinds:
  - name: "To extract an archive:"
    key: "unzip <archive>"
  - name: "To extract an archive to a specific directory:"
    key: "unzip <archive> -d <directory>"
  - name: "To test integrity of archive:"
    key: "unzip -tq <archive>"
  - name: "To list files and directories an archive:"
    key: "unzip -l <archive>"


================================================
FILE: examples/vim.yml
================================================
# vim
# source: http://vim.rtorr.com

- name: vim (tabs)
  keybinds:
    - name: "move to tab number #"
      key: "#gt"
    - name: "move the current split window into its own tab"
      key: "[Ctrl] [w], [t]"
    - name: "move current tab to the #th position (indexed from 0)"
      key: ":tabmove #"
    - name: "open a file in a new tab"
      key: "[:tabnew filename] / [:tabn filename]"
    - name: "close the current tab and all its windows"
      key: "[:tabclose] / [:tabc]"
    - name: "close all tabs except for the current one"
      key: "[:tabonly] / [:tabo]"
    - name: "move to the next tab"
      key: "[gt] / [:tabnext] / [:tabn]"
    - name: "move to the previous tab"
      key: "[gT] / [:tabprev] / [:tabp]"

- name: vim (registers)
  keybinds:
    - name: "view all current registers"
      key: "[:reg] / [:registers]"
    - name: "access register `r` as a variable"
      key: "echo @r"
    - name: "last search pattern register"
      key: "\"/"
    - name: "the black hole register"
      key: "\"_"
    - name: "last yank register"
      key: "\"0"
    - name: "last big delete register"
      key: "\"1"
    - name: "big delete register stack"
      key: "\"2-\"9"
    - name: "system clipboard"
      key: "\"+"
    - name: "small delete register"
      key: "\"-"
    - name: "named registers"
      key: "\"a-\"z"
    - name: "append registers"
      key: "\"A-\"Z"
    - name: "record into register `r`"
      key: "qr"

- name: vim (editing)
  keybinds:
    - name: "replace a single character"
      key: "r"
    - name: "join line below to the current one"
      key: "J"
    - name: "change (replace) entire line"
      key: "cc"
    - name: "change (replace) to the end of the word"
      key: "cw"
    - name: "change (replace) to the end of the line"
      key: "c$"
    - name: "delete character and substitute text"
      key: "s"
    - name: "delete line and substitute text (same as cc)"
      key: "S"
    - name: "transpose two letters (delete and paste)"
      key: "xp"
    - name: "undo"
      key: "u"
    - name: "redo"
      key: "[Ctrl] [r]"
    - name: "repeat last command"
      key: "."

- name: vim (exiting)
  keybinds:
    - name: "write (save) the file, but don't exit"
      key: ":w"
    - name: "write (save) and quit"
      key: ":wq"
    - name: "write (save) and quit"
      key: ":x"
    - name: "quit (fails if there are unsaved changes)"
      key: ":q"
    - name: "quit and throw away unsaved changes"
      key: ":q!"
    - name: "quit all buffers and windows"
      key: ":qa"
    - name: "write (save) current file, if modified, and quit"
      key: "ZZ"
    - name: "quit without checking for changes"
      key: "ZQ"

- name: vim (insert/appending text)
  keybinds:
    - name: "insert before the cursor"
      key: "i"
    - name: "insert at the beginning of the line"
      key: "I"
    - name: "insert (append) after the cursor"
      key: "a"
    - name: "insert (append) at the end of the line"
      key: "A"
    - name: "append (open) a new line below the current line"
      key: "o"
    - name: "append (open) a new line above the current line"
      key: "O"
    - name: "insert (append) at the end of the word"
      key: "ea"
    - name: "exit insert mode"
      key: "Esc"

- name: vim (cut and paste)
  keybinds:
    - name: "yank (copy) a line"
      key: "yy"
    - name: "yank (copy) 2 lines"
      key: "2yy"
    - name: "yank lines n through N"
      key: ":n,Ny"
    - name: "yank (copy) word"
      key: "yw"
    - name: "yank (copy) to end of line"
      key: "y$"
    - name: "put (paste) the clipboard after cursor"
      key: "p"
    - name: "put (paste) before cursor"
      key: "P"
    - name: "delete (cut) a line"
      key: "dd"
    - name: "delete (cut) 2 lines"
      key: "2dd"
    - name: "delete lines n through N"
      key: ":n,Nd"
    - name: "delete (cut) word"
      key: "dw"
    - name: "delete (cut) to the end of the line"
      key: "D"
    - name: "delete (cut) to the end of the line"
      key: "d$"
    - name: "delete (cut) character"
      key: "x"

- name: vim (visual mode)
  keybinds:
    - name: "start visual mode"
      key: "v"
    - name: "start linewise visual mode"
      key: "V"
    - name: "move to other end of marked area"
      key: "vo"
    - name: "start visual block mode"
      key: "[Ctrl] [v]"
    - name: "move to other corner of block"
      key: "vO"
    - name: "mark a word"
      key: "vaw"
    - name: "mark a block with ()"
      key: "vab"
    - name: "mark a block with {}"
      key: "vaB"
    - name: "mark inner block with ()"
      key: "vib"
    - name: "mark inner block with {}"
      key: "viB"
    - name: "exit visual mode"
      key: "Esc"

- name: vim (multiple files)
  keybinds:
    - name: "edit a file in a new buffer"
      key: ":e filename"
    - name: "open a blank file for editing"
      key: ":ene"
    - name: "go to the next buffer"
      key: "[:bnext] / [:bn]"
    - name: "go to the previous buffer"
      key: "[:bprev] / [:bp]"
    - name: "delete a buffer (close a file)"
      key: ":bd"
    - name: "new buffer in split window"
      key: ":sp filename"
    - name: "new buffer in vertically split window"
      key: ":vs filename"
    - name: "split window"
      key: "[Ctrl] [w], [s]"
    - name: "switch windows"
      key: "[Ctrl] [w], [w]"
    - name: "quit a window"
      key: "[Ctrl] [w], [q]"
    - name: "split window vertically"
      key: "[Ctrl] [w], [v]"
    - name: "move cursor to window left"
      key: "[Ctrl] [w], [h]"
    - name: "move cursor to window  right"
      key: "[Ctrl] [w], [l]"
    - name: "move cursor to window above"
      key: "[Ctrl] [w], [k]"
    - name: "move cursor to window below"
      key: "[Ctrl] [w], [j]"
    - name: "rotate windows clockwise"
      key: "[Ctrl] [w], [r]"
    - name: "move current window to a new tab"
      key: "[Ctrl] [w], [T]"
    - name: "close all windows except current window"
      key: ":on"
    - name: "maximize width of active window"
      key: "[Ctrl] [w], [|]"
    - name: "minimize width of active window"
      key: "[Ctrl] [w], [1], [|]"
    - name: "maximize height of active window"
      key: "[Ctrl] [w], [_]"
    - name: "minimize height of active window"
      key: "[Ctrl] [w], [1], [_]"
    - name: "equalize size of windows"
      key: "[Ctrl] [w], [=]"

- name: vim (cursor movement)
  keybinds:
    - name: "move cursor left"
      key: "h"
    - name: "move cursor down"
      key: "j"
    - name: "move cursor up"
      key: "k"
    - name: "move cursor right"
      key: "l"
    - name: "jump forwards to the start of a word"
      key: "w"
    - name: "jump forwards to the start of a word"
      key: "W"
    - name: "jump forwards to the end of a word"
      key: "e"
    - name: "jump forwards to the end of a word"
      key: "E"
    - name: "jump backwards to the start of a word"
      key: "b"
    - name: "jump backwards to the start of a word"
      key: "B"
    - name: "jump to the start of the line"
      key: "0"
    - name: "jump to the first non-blank character of the line"
      key: "^"
    - name: "jump to the end of the line"
      key: "$"
    - name: "go to the last line of the document"
      key: "G"
    - name: "go to line number n"
      key: "nG"
    - name: "go to line number n"
      key: ":n"
    - name: "To the position before the latest jump"
      key: "''"
    - name: "jump to next occurrence of character x"
      key: "fx"
    - name: "jump to one character before the character x"
      key: "tx"
    - name: "jump to next paragraph"
      key: "}"
    - name: "jump to previous paragraph"
      key: "{"
    - name: "jump to home (top) of screen"
      key: "H"
    - name: "jump to last line of screen"
      key: "L"
    - name: "jump to middle of screen"
      key: "M"
    - name: "jump to 3rd instance of character x forward from cursor on current line."
      key: "3, f, x"
    - name: "jump to 3rd instance of character x back from cursor on current line."
      key: "3, F, x"

- name: vim (screen movement)
  keybinds:
    - name: "move screen up by half page"
      key: "[Ctrl] [u]"
    - name: "move screen up by one page"
      key: "[Ctrl] [b]"
    - name: "move screen down by half page"
      key: "[Ctrl] [d]"
    - name: "move screen down by one page"
      key: "[Ctrl] [f]"
    - name: "center screen on cursor"
      key: "zz"
    - name: "align top of screen with cursor"
      key: "zt"
    - name: "align bottom of screen with cursor"
      key: "zb"

- name: vim (visual commands)
  keybinds:
    - name: "shift text right"
      key: ">"
    - name: "shift text left"
      key: "<"
    - name: "auto-indent current line"
      key: "=="
    - name: "shift current line left by shiftwidth"
      key: "<<"
    - name: "shift current line right by shiftwidth"
      key: ">>"
    - name: "yank (copy) marked text"
      key: "y"
    - name: "delete marked text"
      key: "d"
    - name: "switch case"
      key: "~"

- name: vim (search and replace)
  keybinds:
    - name: "search for word under cursor"
      key: "*"
    - name: "search for pattern"
      key: "/pattern"
    - name: "search backward for pattern"
      key: "?pattern"
    - name: "repeat search in same direction"
      key: "n"
    - name: "repeat search in opposite direction"
      key: "N"
    - name: "replace all"
      key: ":%s/old/new/g"
    - name: "replace all with confirmations"
      key: ":%s/old/new/gc"

- name: vim (extra commands)
  keybinds:
    - name: "run a compiler and enter quickfix mode"
      key: "[:mak] / [:make]"
    - name: "execute external shell command"
      key: ":!"
    - name: "read external program output into current file"
      key: "[:r] / [:read]"
    - name: "move lines x through y to z (delete + put)"
      key: ":x,ymz"
    - name: "yank lines x through y and put to z (yank + put)"
      key: ":x,ytz"
    - name: "current line (cursor location)"
      key: ":."
    - name: "last line (bottom of file)"
      key: ":$"
    - name: "first line (top of file)"
      key: ":0"
    - name: "list all open files"
      key: ":ls"
    - name: "create html representation of current working buffer"
      key: ":%TOhtml"

- name: vim (basic configuration)
  keybinds:
    - name: "show line numbers"
      key: ":set nu"
    - name: "lets you switch buffers without saving"
      key: ":set hid"
    - name: "highlight search matches"
      key: ":set hls"
    - name: "show commands as you type them"
      key: ":set sc"
    - name: "show line and column number of the cursor"
      key: ":set ru"

- name: vim (folding)
  keybinds:
    - name: "creates a fold from the cursor down # lines"
      key: "zf#j"
    - name: "string creates a fold from the cursor to string"
      key: "zf/"
    - name: "moves the cursor to the next fold"
      key: "zj"
    - name: "moves the cursor to the previous fold"
      key: "zk"
    - name: "toggle a fold under cursor"
      key: "za"
    - name: "opens a fold at the cursor"
      key: "zo"
    - name: "opens all folds at the cursor"
      key: "zO"
    - name: "closes a fold under cursor"
      key: "zc"
    - name: "increases the foldlevel by one"
      key: "zm"
    - name: "closes all open folds"
      key: "zM"
    - name: "decreases the foldlevel by one"
      key: "zr"
    - name: "open all folds"
      key: "zR"
    - name: "deletes the fold at the cursor"
      key: "zd"
    - name: "deletes all folds"
      key: "zE"
    - name: "move to start of open fold"
      key: "[z"
    - name: "move to end of open fold"
      key: "]z"

================================================
FILE: examples/vscode.yml
================================================
# Visual Studio Code
# source: https://code.visualstudio.com/docs/customization/keybinds

- name: vscode (basic editing)
  keybinds:
    - name: Cut line (empty selection)
      key: ctrl + x
    - name: "Copy line (empty selection)"
      key: "[Ctrl] [C]"
    - name: "Delete Line"
      key: "[Ctrl] [Shift] [K]"
    - name: "Insert Line Below"
      key: "[Ctrl] [Enter]"
    - name: "Insert Line Above"
      key: "[Ctrl] [Shift] [Enter]"
    - name: "Move Line Down"
      key: "[Alt] [Down]"
    - name: "Move Line Up"
      key: "[Alt] [Up]"
    - name: "Copy Line Down"
      key: "[Shift] [Alt] [Down]"
    - name: "Copy Line Up"
      key: "[Shift] [Alt] [Up]"
    - name: "Add Selection To Next Find Match"
      key: "[Ctrl] [D]"
    - name: "Move Last Selection To Next Find Match"
      key: "[Ctrl] [K], [Ctrl] [D]"
    - name: "Undo last cursor operation"
      key: "[Ctrl] [U]"
    - name: "Select all occurrences of current selection"
      key: "[Ctrl] [Shift] [L]"
    - name: "Select all occurrences of current word"
      key: "[Ctrl] [F2]"
    - name: "Select current line"
      key: "[Ctrl] [I]"
    - name: "Insert Cursor Below"
      key: "[Ctrl] [Alt] [Down]"
    - name: "Insert Cursor Above"
      key: "[Ctrl] [Alt] [Up]"
    - name: "Jump to matching bracket"
      key: "[Ctrl] [Shift] [\\]"
    - name: "Indent Line"
      key: "[Ctrl] {]}"
    - name: "Outdent Line"
      key: "[Ctrl] {[}"
    - name: "Go to Beginning of Line"
      key: "Home"
    - name: "Go to End of Line"
      key: "End"
    - name: "Go to End of File"
      key: "[Ctrl] [End]"
    - name: "Go to Beginning of File"
      key: "[Ctrl] [Home]"
    - name: "Scroll Line Down"
      key: "[Ctrl] [Down]"
    - name: "Scroll Line Up"
      key: "[Ctrl] [Up]"
    - name: "Scroll Page Down"
      key: "[Ctrl] [PageDown]"
    - name: "Scroll Page Up"
      key: "[Ctrl] [PageUp]"
    - name: "Fold (collapse) region"
      key: "[Ctrl] [Shift] {[}"
    - name: "Unfold (uncollapse) region"
      key: "[Ctrl] [Shift] {]}"
    - name: "Fold (collapse) all regions"
      key: "[Ctrl] [Shift] [Alt] {[}"
    - name: "Unfold (uncollapse) all regions"
      key: "[Ctrl] [Shift] [Alt] {]}"
    - name: "Add Line Comment"
      key: "[Ctrl] [K], [Ctrl] [C]"
    - name: "Remove Line Comment"
      key: "[Ctrl] [K], [Ctrl] [U]"
    - name: "Toggle Line Comment"
      key: "[Ctrl] [/]"
    - name: "Toggle Block Comment"
      key: "[Shift] [Alt] [A]"
    - name: "Find"
      key: "[Ctrl] [F]"
    - name: "Replace"
      key: "[Ctrl] [H]"
    - name: "Find Next"
      key: "F3"
    - name: "Find Previous"
      key: "[Shift] [F3]"
    - name: "Toggle Find Case Sensitive"
      key: "[Alt] [C]"
    - name: "Toggle Find Regex"
      key: "[Alt] [R]"
    - name: "Toggle Find Whole Word"
      key: "[Alt] [W]"
    - name: "Toggle Use of Tab Key for Setting Focus"
      key: "[Ctrl] [M]"

- name: vscode (rich language editing)
  keybinds:
    - name: "Trigger Suggest"
      key: "[Ctrl] [Space]"
    - name: "Trigger Parameter Hints"
      key: "[Ctrl] [Shift] [Space]"
    - name: "Format Code"
      key: "[Shift] [Alt] [F]"
    - name: "Go to Definition"
      key: "F12"
    - name: "Peek Definition"
      key: "[Alt] [F12]"
    - name: "Open Definition to the Side"
      key: "[Ctrl] [K], [F12]"
    - name: "Quick Fix"
      key: "[Ctrl] [.]"
    - name: "Show References"
      key: "[Shift] [F12]"
    - name: "Rename Symbol"
      key: "F2"
    - name: "Replace with Next Value"
      key: "[Ctrl] [Shift] [.]"
    - name: "Replace with Previous Value"
      key: "[Ctrl] [Shift] [,]"
    - name: "Expand AST Select"
      key: "[Shift] [Alt] [Right]"
    - name: "Shrink AST Select"
      key: "[Shift] [Alt] [Left]"
    - name: "Trim Trailing Whitespace"
      key: "[Ctrl] [Shift] [X]"
    - name: "Change Language Mode"
      key: "[Ctrl] [K], [M]"

- name: vscode (navigation)
  keybinds:
    - name: "Show All Symbols"
      key: "[Ctrl] [T]"
    - name: "Go to Line..."
      key: "[Ctrl] [G]"
    - name: "Go to File..., Quick Open"
      key: "[Ctrl] [P]"
    - name: "Go to Symbol..."
      key: "[Ctrl] [Shift] [O]"
    - name: "Show Errors and Warnings"
      key: "[Ctrl] [Shift] [M]"
    - name: "Go to Next Error or Warning"
      key: "F8"
    - name: "Go to Previous Error or Warning"
      key: "[Shift] [F8]"
    - name: "Show All Commands"
      key: "F1"
    - name: "Navigate History"
      key: "[Ctrl] [Tab]"
    - name: "Go Back"
      key: "[Alt] [Left]"
    - name: "Go Forward"
      key: "[Alt] [Right]"

- name: vscode (editor/window management)
  keybinds:
    - name: "New Window"
      key: "[Ctrl] [Shift] [N]"
    - name: "Close Window"
      key: "[Ctrl] [Shift] [W]"
    - name: "Close Editor"
      key: "[Ctrl] [F4]"
    - name: "Close Folder"
      key: "[Ctrl] [K], [F]"
    - name: "Cycle Between Opened Editors"
      key: "[Ctrl] [`]"
    - name: "Split Editor"
      key: "[Ctrl] [\\]"
    - name: "Focus into Left Hand Editor"
      key: "[Ctrl] [1]"
    - name: "Focus into Side Editor"
      key: "[Ctrl] [2]"
    - name: "Focus into Right Hand Editor"
      key: "[Ctrl] [3]"
    - name: "Focus into Next Editor on the Left"
      key: "[Ctrl] [Alt] [Left]"
    - name: "Focus into Next Editor on the Right"
      key: "[Ctrl] [Alt] [Right]"
    - name: "Move Active Editor Left"
      key: "[Ctrl] [K], [Left]"
    - name: "Move Active Editor Right"
      key: "[Ctrl] [K], [Right]"

- name: vscode (file management)
  keybinds:
    - name: "New File"
      key: "[Ctrl] [N]"
    - name: "Open File..."
      key: "[Ctrl] [O]"
    - name: "Save"
      key: "[Ctrl] [S]"
    - name: "Save As..."
      key: "[Ctrl] [Shift] [S]"
    - name: "Close File"
      key: "[Ctrl] [K], [W]"
    - name: "Close All Files"
      key: "[Ctrl] [K], [Ctrl] [W]"
    - name: "Close Other Files"
      key: "[Ctrl] [K], [Ctrl] [Shift] [W]"
    - name: "Add to Working Files"
      key: "[Ctrl] [K], [Enter]"
    - name: "Open Next Working File"
      key: "[Ctrl] [K], [Down]"
    - name: "Open Previous Working File"
      key: "[Ctrl] [K], [Up]"
    - name: "Copy Path of Active File"
      key: "[Ctrl] [K], [P]"
    - name: "Reveal Active File in Windows"
      key: "[Ctrl] [K], [R]"
    - name: "Show Opened File in New Window"
      key: "[Ctrl] [K], [O]"

- name: vscode (display)
  keybinds:
    - name: "Toggle Full Screen"
      key: "F11"
    - name: "Zoom in"
      key: "[Ctrl] [=]"
    - name: "Zoom out"
      key: "[Ctrl] [-]"
    - name: "Toggle Sidebar Visibility"
      key: "[Ctrl] [B]"
    - name: "Show Debug"
      key: "[Ctrl] [Shift] [D]"
    - name: "Show Explorer"
      key: "[Ctrl] [Shift] [E]"
    - name: "Show Git"
      key: "[Ctrl] [Shift] [G]"
    - name: "Show Search"
      key: "[Ctrl] [Shift] [F]"
    - name: "Toggle Search Details"
      key: "[Ctrl] [Shift] [J]"
    - name: "Open New Command Prompt"
      key: "[Ctrl] [Shift] [C]"
    - name: "Show Output"
      key: "[Ctrl] [Shift] [U]"
    - name: "Toggle Markdown Preview"
      key: "[Ctrl] [Shift] [V]"
    - name: "Open Preview to the Side"
      key: "[Ctrl] [K], [V]"

- name: vscode (debug)
  keybinds:
    - name: "Toggle Breakpoint"
      key: "F9"
    - name: "Continue"
      key: "F5"
    - name: "Pause"
      key: "F5"
    - name: "Step Into"
      key: "F11"
    - name: "Step Out"
      key: "[Shift] [F11]"
    - name: "Step Over"
      key: "F10"
    - name: "Stop"
      key: "[Shift] [F5]"
    - name: "Show Hover"
      key: "[Ctrl] [K], [Ctrl] [I]"

- name: vscode (tasks)
  keybinds:
    - name: "Run Build Task"
      key: "[Ctrl] [Shift] [B]"
    - name: "Run Test Task"
      key: "[Ctrl] [Shift] [T]"


================================================
FILE: examples/weechat.yml
================================================
# https://github.com/cheat/cheatsheets
- name: weechat
  keybinds:
  - name: "To set an unread marker on all windows:"
    key: "Ctrl-s Ctrl-u"
  - name: "To switch to the left buffer:"
    key: "Ctrl-p, Alt-left"
  - name: "To switch to the right buffer:"
    key: "Ctrl-n, Alt-right"
  - name: "To switch to the next buffer with activity:"
    key: "Alt-a"
  - name: "To switch buffers:"
    key: "Alt-0...9"
  - name: "To scroll buffer title:"
    key: "F9/F10"
  - name: "To scroll nick list:"
    key: "F11/F12"


================================================
FILE: examples/zip.yml
================================================
# https://github.com/cheat/cheatsheets
- name: zip
  keybinds:
    - name: "To create zip file"
      key: "zip archive.zip file1 directory/"
    - name: "To create zip file from directory"
      key: "zip -r archive.zip directory/"
    - name: "To create zip file with password"
      key: "zip -P password archive.zip file1"
    - name: "To join a split zip file (.zip, .z01, .z02, etc)"
      key: "zip -FF splitZipfile.zip --out joined.zip"
    - name: "To list, test and extract zip archives, see unzip"
      key: "cheat unzip"

================================================
FILE: examples/zsh.yml
================================================
# ZSH
# https://github.com/cheat/cheatsheets
- name: zsh (basic)
  keybinds:
    - name: "A plain old glob"
      key: "print -l *.txt"
    - name: "Show text files that end in a number from 1 to 10"
      key: "print -l **/*<1-10>.txt"
    - name: "Show text files that start with the letter a"
      key: "print -l **/[a]*.txt"
    - name: "Show text files that start with either ab or bc"
      key: "print -l **/(ab|bc)*.txt"
    - name: "Show text files that don't start with a lower or uppercase c"
      key: "print -l **/[^cC]*.txt"
    - name: "Show only directories"
      key: "print -l **/*(/)"
    - name: "Show only regular files"
      key: "print -l **/*(.)"
    - name: "Show empty files"
      key: "print -l **/*(L0)"
    - name: "Show files greater than 3 KB"
      key: "print -l **/*(Lk+3)"
    - name: "Show files modified in the last hour"
      key: "print -l **/*(mh-1)"
    - name: "Sort files from most to least recently modified and show the last 3"
      key: "print -l **/*(om[1,3])"
    - name: "show files, smaller than 2MB, modified in last hour, only first 3 files"
      key: "print -l **/*(.Lm-2mh-1om[1,3])"
    - name: "Show every directory that contain directory `.git`"
      key: "print -l **/*(e:'[[ -d $REPLY/.git ]]':)"
    - name: "Return the file name (t stands for tail)"
      key: "print -l *.txt(:t)"
    - name: "Return the file name without the extension (r stands for remove_extension)"
      key: "print -l *.txt(:t:r)"
    - name: "Return the extension"
      key: "print -l *.txt(:e)"
    - name: "Return the parent folder of the file (h stands for head)"
      key: "print -l *.txt(:h)"
    - name: "Return the parent folder of the parent"
      key: "print -l *.txt(:h:h)"
    - name: "Return the parent folder of the first file"
      key: "print -l *.txt([1]:h)"
    - name: "Split the file name at each underscore"
      key: "echo ${(s._.)file}"
    - name: "Join expansion flag, opposite of the split flag."
      key: "array=(a b c d)"
    - name: "Short if"
      key: "if [[ ... ]] command"
    - name: "Short for"
      key: "for i in word ... ; command"
    - name: "Short while"
      key: "while [[ ... ]] { command ... }"
    - name: "Short until"
      key: "until [[ ... ]] { command ... }"
    - name: "Use output of command, when using pipe is not possible Similar to <( ), but creates temporary file (instead of FD or FIFO), when program needs to seek in output."
      key: "=( command )"
- name: zsh (Parameter expansion)
  keybinds:
    - name: "store a glob in a variable"
      key: "files=(*.txt)"
    - name: "store a glob in a variable"
      key: "print -l $files"
    - name: "this is the syntax we saw before"
      key: "print -l $files(:h)"
    - name: "don't mix the two, or you'll get an error"
      key: "print -l ${files(:h)}"
    - name: "the :u modifier makes the text uppercase"
      key: "print -l ${files:u}"
- name: zsh (:s modifier)
  keybinds:
    - name: "path/aaaBCd"
      key: "echo ${variable:s/bc/BC/}"
    - name: "path/aaaBCd"
      key: "echo ${variable:s_bc_BC_}"
    - name: "path.aaabcd (escaping the slash /)"
      key: "echo ${variable:s///./}"
    - name: "path.aaabcd (slightly more readable)"
      key: "echo ${variable:s_/_._}"
    - name: "path/aaabcd (only first A is replaced)"
      key: "echo ${variable:s/a/A/}"
    - name: "path/AAAbcd (all A is replaced)"
      key: "echo ${variable:gs/a/A/}"


================================================
FILE: go.mod
================================================
module github.com/kencx/keyb

go 1.26.1

require (
	github.com/charmbracelet/bubbles v1.0.0
	github.com/charmbracelet/bubbletea v1.3.10
	github.com/charmbracelet/lipgloss v1.1.0
	github.com/juju/ansiterm v1.0.0
	github.com/muesli/reflow v0.3.0
	github.com/sahilm/fuzzy v0.1.1
	gopkg.in/yaml.v2 v2.4.0
)

require (
	github.com/atotto/clipboard v0.1.4 // indirect
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
	github.com/charmbracelet/colorprofile v0.4.3 // indirect
	github.com/charmbracelet/x/ansi v0.11.6 // indirect
	github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
	github.com/charmbracelet/x/term v0.2.2 // indirect
	github.com/clipperhouse/displaywidth v0.11.0 // indirect
	github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
	github.com/lunixbochs/vtclean v1.0.0 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mattn/go-localereader v0.0.1 // indirect
	github.com/mattn/go-runewidth v0.0.21 // indirect
	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
	github.com/muesli/cancelreader v0.2.2 // indirect
	github.com/muesli/termenv v0.16.0 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
	golang.org/x/sys v0.42.0 // indirect
	golang.org/x/text v0.35.0 // indirect
)


================================================
FILE: go.sum
================================================
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg=
github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=


================================================
FILE: main.go
================================================
package main

import (
	"flag"
	"fmt"
	"log"
	"os"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/kencx/keyb/config"
	"github.com/kencx/keyb/output"
	"github.com/kencx/keyb/ui"
)

const (
	help = `usage: keyb [options] <command>

  Options:
    -p, --print	    Print to stdout
    -e, --export    Export to file [yaml, json]
    -k, --key       Key bindings at custom path
    -c, --config    Config file at custom path
    -v, --version   Version info
    -h, --help	    Show help

  Commands:
    a, add          Add keybind to keyb file
`

	addHelp = `usage: keyb [-k file] add [app; name; key]

  Options:
    -k, --key      Key bindings file at custom path
    -b, --binding  Key binding
    -p, --prefix   Ignore prefix
`
)

var version string

func main() {
	log.SetPrefix("keyb: ")
	log.SetFlags(log.Lshortfile)

	var (
		stdout     bool
		exportFile string
		keybFile   string
		configFile string

		addBind   string
		addPrefix bool
	)

	shortVersion := flag.Bool("v", false, "version information")
	longVersion := flag.Bool("version", false, "version information")

	flag.BoolVar(&stdout, "p", false, "print to stdout")
	flag.BoolVar(&stdout, "print", false, "print to stdout")

	flag.StringVar(&exportFile, "e", "", "export to file")
	flag.StringVar(&exportFile, "export", "", "export to file")

	flag.StringVar(&keybFile, "k", "", "keybindings file")
	flag.StringVar(&keybFile, "key", "", "keybindings file")

	flag.StringVar(&configFile, "c", "", "config file")
	flag.StringVar(&configFile, "config", "", "config file")

	addCmd := flag.NewFlagSet("add", flag.ExitOnError)
	addCmd.StringVar(&addBind, "b", "", "keybind")
	addCmd.StringVar(&addBind, "binding", "", "keybind")
	addCmd.BoolVar(&addPrefix, "p", false, "prefix")
	addCmd.BoolVar(&addPrefix, "prefix", false, "prefix")

	flag.Usage = func() { os.Stdout.Write([]byte(help)) }
	flag.Parse()

	if *shortVersion || *longVersion {
		fmt.Println(version)
		os.Exit(0)
	}

	keys, cfg, err := config.Parse(configFile, keybFile)
	if err != nil {
		log.Fatal(err)
	}

	args := flag.Args()
	if len(args) > 1 {
		switch args[0] {
		case "add", "a":
			addCmd.Usage = func() { os.Stdout.Write([]byte(addHelp)) }
			addCmd.Parse(args[1:])

			var addFile string
			if keybFile != "" {
				// use flag -k path
				addFile = keybFile
			} else {
				// use default path in config
				addFile = cfg.KeybPath
			}
			if err := config.AddEntry(addFile, addBind, addPrefix); err != nil {
				log.Fatal(err)
			}
			fmt.Printf("%s added to %s", addBind, addFile)
			os.Exit(0)
		default:
			fmt.Print(help)
			os.Exit(1)
		}
	}

	m := ui.NewModel(keys, cfg)

	if stdout {
		if err := output.ToStdout(m); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}
	if exportFile != "" {
		if err := output.ToFile(m, exportFile); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}

	if err := start(m); err != nil {
		log.Fatal(err)
	}
}

func start(m *ui.Model) error {

	p := tea.NewProgram(m, tea.WithMouseCellMotion(), tea.WithAltScreen())

	if _, err := p.Run(); err != nil {
		return fmt.Errorf("failed to start: %w", err)
	}
	return nil
}


================================================
FILE: output/output.go
================================================
package output

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"

	"github.com/kencx/keyb/ui"
	"gopkg.in/yaml.v2"
)

func ToFile(m *ui.Model, path string) error {
	var (
		output []byte
		err    error
	)

	path = os.ExpandEnv(path)
	ext := filepath.Ext(path)

	switch ext {
	case ".json":
		output, err = json.Marshal(m.Apps)
		if err != nil {
			return fmt.Errorf("failed to marshal to json: %w", err)
		}
	case ".yml", ".yaml":
		output, err = yaml.Marshal(m.Apps)
		if err != nil {
			return fmt.Errorf("failed to marshal to yaml: %w", err)
		}
	default:
		output = []byte(m.List.UnstyledString())
	}
	if err := os.WriteFile(path, output, 0664); err != nil {
		return fmt.Errorf("failed to write to file: %w", err)
	}

	return nil
}

func ToStdout(m *ui.Model) error {
	output := m.List.UnstyledString()

	_, err := os.Stdout.Write([]byte(output))
	if err != nil {
		return fmt.Errorf("failed to write to stdout: %w", err)
	}
	return nil
}


================================================
FILE: output/output_test.go
================================================
package output

import (
	"io"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/kencx/keyb/config"
	"github.com/kencx/keyb/ui"
	"github.com/kencx/keyb/ui/list"
	"github.com/kencx/keyb/ui/table"
)

var (
	testTable  = table.New([]*table.Row{table.NewHeading("foo"), {Text: "bar"}})
	testConfig = &config.Config{}
	testApps   = &config.Apps{
		&config.App{
			Name:   "foo",
			Prefix: "bar",
			Keybinds: []config.KeyBind{
				{
					Name: "key foo",
					Key:  "key bar",
				},
			},
		},
	}
	m = &ui.Model{List: list.New(testTable, testConfig), Apps: testApps}
)

func TestToJson(t *testing.T) {
	tempDir := t.TempDir()
	path := filepath.Join(tempDir, "test.json")

	err := ToFile(m, path)
	if err != nil {
		t.Fatal(err)
	}

	got, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}

	want := []byte(`[{"prefix":"bar","name":"foo","keybinds":[{"name":"key foo","key":"key bar"}]}]`)
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("got %s, want %s", got, want)
	}
}

func TestToStdout(t *testing.T) {
	rescueStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	err := ToStdout(m)
	if err != nil {
		t.Fatal(err)
	}
	w.Close()

	got, _ := io.ReadAll(r)
	os.Stdout = rescueStdout

	want := "foo      \nbar     "
	if string(got) != want {
		t.Errorf("got %q, want %q", got, want)
	}
}


================================================
FILE: testdata/testConfig.json
================================================
{
  "settings": {
    "keyb_path": "./custom.yml",
    "debug": true,
    "reverse": true,
    "mouse": false,
    "search_mode": false,
    "sort_keys": true,
    "title": "",
    "prompt": "keys > ",
    "prompt_location": "bottom",
    "placeholder": "...",
    "prefix_sep": ";",
    "sep_width": 4,
    "margin": 1,
    "padding": 1,
    "border": "normal"
  },
  "color": {
    "prompt": "",
    "cursor_fg": "",
    "cursor_bg": "",
    "filter_fg": "#FFA066",
    "filter_bg": "",
    "border_color": ""
  },
  "keys": {
    "quit": "q, ctrl+c",
    "up": "k, up",
    "down": "j, down",
    "up_focus": "alt+k",
    "down_focus": "alt+j",
    "half_up": "ctrl+u",
    "half_down": "ctrl+d",
    "full_up": "ctrl+b",
    "full_bottom": "ctrl+f",
    "first_line": "g",
    "last_line": "G",
    "top": "H",
    "middle": "M",
    "bottom": "L",
    "search": "/",
    "clear_search": "alt+d",
    "normal": "esc",
    "cursor_word_forward": "alt+right, alt+f",
    "cursor_word_backward": "alt+left, alt+b",
    "cursor_delete_word_backward": "alt+backspace",
    "cursor_delete_word_forward": "alt+delete",
    "cursor_delete_after_cursor": "alt+k",
    "cursor_delete_before_cursor": "alt+u",
    "cursor_line_start": "home, ctrl+a",
    "cursor_line_end": "end, ctrl+e",
    "cursor_paste": "ctrl+v"
  }
}


================================================
FILE: testdata/testConfig.yml
================================================
settings:
  keyb_path: "./custom.yml"
  debug: true
  reverse: true
  mouse: false
  search_mode: false
  sort_keys: true
  title: ""
  prompt: 'keys > '
  prompt_location: "bottom"
  placeholder: '...'
  prefix_sep: ;
  sep_width: 4
  margin: 1
  padding: 1
  border: normal
color:
  prompt: ""
  cursor_fg: ""
  cursor_bg: ""
  filter_fg: "#FFA066"
  filter_bg: ""
  border_color: ""
keys:
  quit: q, ctrl+c
  up: k, up
  down: j, down
  up_focus: alt+k
  down_focus: alt+j
  half_up: ctrl+u
  half_down: ctrl+d
  full_up: ctrl+b
  full_bottom: ctrl+f
  first_line: g
  last_line: G
  top: H
  middle: M
  bottom: L
  search: /
  clear_search: alt+d
  normal: esc
  cursor_word_forward: "alt+right, alt+f"
  cursor_word_backward: "alt+left, alt+b"
  cursor_delete_word_backward: "alt+backspace"
  cursor_delete_word_forward: "alt+delete"
  cursor_delete_after_cursor: "alt+k"
  cursor_delete_before_cursor: "alt+u"
  cursor_line_start: "home, ctrl+a"
  cursor_line_end: "end, ctrl+e"
  cursor_paste: "ctrl+v"


================================================
FILE: testdata/testConfigMinimal.json
================================================
{
  "settings": {}
}


================================================
FILE: testdata/testConfigMinimal.yml
================================================
---
settings:


================================================
FILE: testdata/testkeyb.json
================================================
[
  {
    "name": "test",
    "keybinds": [
      {
        "name": "foo",
        "key": "bar"
      }
    ]
  }
]


================================================
FILE: testdata/testkeyb.yml
================================================
- name: test
  keybinds:
  - name: foo
    key: bar


================================================
FILE: ui/list/keymap.go
================================================
package list

import (
	"strings"

	"github.com/charmbracelet/bubbles/key"
	"github.com/kencx/keyb/config"
)

type KeyMap struct {
	Quit key.Binding

	Up            key.Binding
	Down          key.Binding
	HalfUp        key.Binding
	HalfDown      key.Binding
	FullUp        key.Binding
	FullDown      key.Binding
	UpFocus       key.Binding
	DownFocus     key.Binding
	GoToFirstLine key.Binding
	GoToLastLine  key.Binding
	GoToTop       key.Binding
	GoToMiddle    key.Binding
	GoToBottom    key.Binding

	CenterCursor key.Binding

	Search      key.Binding
	ClearSearch key.Binding
	Normal      key.Binding

	TextInputKeyMap
}

type TextInputKeyMap struct {
	CharacterForward        key.Binding
	CharacterBackward       key.Binding
	WordForward             key.Binding
	WordBackward            key.Binding
	DeleteWordBackward      key.Binding
	DeleteWordForward       key.Binding
	DeleteAfterCursor       key.Binding
	DeleteBeforeCursor      key.Binding
	DeleteCharacterBackward key.Binding
	DeleteCharacterForward  key.Binding
	LineStart               key.Binding
	LineEnd                 key.Binding
	Paste                   key.Binding
	AcceptSuggestion        key.Binding
	NextSuggestion          key.Binding
	PrevSuggestion          key.Binding
}

func CreateKeyMap(keys config.Keys) KeyMap {
	return KeyMap{
		Quit:          SetKey(keys.Quit),
		Up:            SetKey(keys.Up),
		Down:          SetKey(keys.Down),
		HalfUp:        SetKey(keys.HalfUp),
		HalfDown:      SetKey(keys.HalfDown),
		FullUp:        SetKey(keys.FullUp),
		FullDown:      SetKey(keys.FullDown),
		UpFocus:       SetKey(keys.UpFocus),
		DownFocus:     SetKey(keys.DownFocus),
		GoToFirstLine: SetKey(keys.GoToFirstLine),
		GoToLastLine:  SetKey(keys.GoToLastLine),
		GoToTop:       SetKey(keys.GoToTop),
		GoToMiddle:    SetKey(keys.GoToMiddle),
		GoToBottom:    SetKey(keys.GoToBottom),

		Search:      SetKey(keys.Search),
		ClearSearch: SetKey(keys.ClearSearch),
		Normal:      SetKey(keys.Normal),

		TextInputKeyMap: TextInputKeyMap{
			CharacterForward:        SetKey("right"),
			CharacterBackward:       SetKey("left"),
			WordForward:             SetKey(keys.CursorWordForward),
			WordBackward:            SetKey(keys.CursorWordBackward),
			DeleteWordBackward:      SetKey(keys.CursorDeleteWordBackward),
			DeleteWordForward:       SetKey(keys.CursorDeleteWordForward),
			DeleteAfterCursor:       SetKey(keys.CursorDeleteAfterCursor),
			DeleteBeforeCursor:      SetKey(keys.CursorDeleteBeforeCursor),
			DeleteCharacterBackward: SetKey("backspace"),
			DeleteCharacterForward:  SetKey("delete"),
			LineStart:               SetKey(keys.CursorLineStart),
			LineEnd:                 SetKey(keys.CursorLineEnd),
			Paste:                   SetKey(keys.CursorPaste),
			AcceptSuggestion:        SetKey("tab"),
			NextSuggestion:          SetKey("ctrl+n"),
			PrevSuggestion:          SetKey("ctrl+p"),
		},
	}
}

func SetKey(s string) key.Binding {
	return key.NewBinding(
		key.WithKeys(splitAndTrim(s, ",")...),
	)
}

func splitAndTrim(s, sep string) []string {
	sl := strings.Split(s, sep)
	for i := range sl {
		sl[i] = strings.TrimSpace(sl[i])
	}
	return sl
}


================================================
FILE: ui/list/list.go
================================================
package list

import (
	"github.com/kencx/keyb/config"
	"github.com/kencx/keyb/ui/table"

	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type filterState int

const (
	unfiltered filterState = iota
	filtering
)

type Model struct {
	keys     KeyMap
	viewport viewport.Model
	table    *table.Model

	searchBar         textinput.Model
	search            bool
	startInSearchMode bool

	filterState    filterState
	filteredTable  *table.Model
	currentHeading string

	title   string
	debug   bool
	cursor  int
	maxRows int // max number of rows regardless of filterState

	margin         int
	padding        int
	scrollOffset   int
	border         lipgloss.Style
	counterStyle   lipgloss.Style
	promptLocation string
}

func New(t *table.Model, c *config.Config) Model {
	keyMap := CreateKeyMap(c.Keys)

	m := Model{
		keys: keyMap,
		viewport: viewport.Model{
			YOffset:           0,
			MouseWheelDelta:   3,
			MouseWheelEnabled: c.Mouse,
		},
		table: t,

		searchBar: textinput.Model{
			Prompt:           c.Prompt,
			PromptStyle:      lipgloss.NewStyle().Foreground(lipgloss.Color(c.PromptColor)),
			Placeholder:      c.Placeholder,
			PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
			EchoCharacter:    '*',
			CharLimit:        0,
			Cursor:           cursor.New(),
			KeyMap:           textinput.KeyMap(keyMap.TextInputKeyMap),
			ShowSuggestions:  false,
		},
		startInSearchMode: c.SearchMode,

		filteredTable: table.NewEmpty(t.LineCount),

		title:   c.Title,
		debug:   c.Debug,
		cursor:  0,
		maxRows: t.LineCount,

		margin:         c.Margin,
		padding:        c.Padding,
		scrollOffset:   5,
		counterStyle:   lipgloss.NewStyle().Faint(true).Margin(0, 1),
		promptLocation: c.PromptLocation,
	}

	m.table.SepWidth = c.SepWidth
	m.filteredTable.SepWidth = c.SepWidth
	m.scrollOffset += (m.margin * 2) + (m.padding * 2)
	m.style(c)

	if m.startInSearchMode {
		m.startSearch()
	}

	return m
}

func (m *Model) style(c *config.Config) {
	if c.PlaceholderFg != "" || c.PlaceholderBg != "" {
		m.searchBar.PlaceholderStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color(c.PlaceholderFg)).
			Background(lipgloss.Color(c.PlaceholderBg))
	}

	if c.CounterFg != "" || c.CounterBg != "" {
		m.counterStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(c.CounterFg)).Background(lipgloss.Color(c.CounterBg)).Margin(0, 1)
	}

	var b lipgloss.Border
	switch c.BorderStyle {
	case "normal":
		b = lipgloss.NormalBorder()
	case "rounded":
		b = lipgloss.RoundedBorder()
	case "double":
		b = lipgloss.DoubleBorder()
	case "thick":
		b = lipgloss.ThickBorder()
	default:
		b = lipgloss.HiddenBorder()
	}
	m.border = lipgloss.NewStyle().BorderStyle(b).BorderForeground(lipgloss.Color(c.BorderColor))

	// row specific config
	if !m.table.Empty() {
		cursor := lipgloss.NewStyle().Bold(true).
			Foreground(lipgloss.Color(c.CursorFg)).
			Background(lipgloss.Color(c.CursorBg))

		s := table.RowStyles{
			Normal:          lipgloss.NewStyle().Margin(0, 2).TabWidth(lipgloss.NoTabConversion),
			Heading:         lipgloss.NewStyle().Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion),
			Selected:        cursor.Margin(0, 2).TabWidth(lipgloss.NoTabConversion),
			SelectedHeading: cursor.Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion),
			Filtered: lipgloss.NewStyle().
				Foreground(lipgloss.Color(c.FilterFg)).
				Background(lipgloss.Color(c.FilterBg)).
				TabWidth(lipgloss.NoTabConversion),
		}

		for _, row := range m.table.Rows {
			row.PrefixSep = c.PrefixSep
			row.Reversed = c.Reverse
			row.Styles = s
		}
	}
}

func (m *Model) Init() tea.Cmd {
	return nil
}

func (m *Model) Resize(width, height int) {
	m.viewport.Width = width
	m.viewport.Height = height - m.scrollOffset
}

// Resets list to unfiltered state
func (m *Model) Reset() {
	m.filteredTable.Reset()
	m.filterState = unfiltered
	m.currentHeading = ""
	m.cursorToBeginning()
	m.visibleRows()
}

// Sets items to be shown. All items are shown unless filtered
func (m *Model) visibleRows() {
	if !m.filteredTable.Empty() {
		m.SyncContent(m.filteredTable)
	} else {
		m.SyncContent(m.table)
	}
}

// Sync content by updating cursor and visible rows
func (m *Model) SyncContent(table *table.Model) {
	if table.Empty() {
		m.viewport.SetContent("")
		return
	}

	if m.cursor > table.LineCount {
		m.cursor = table.LineCount - 1
	}

	for i, row := range table.Rows {
		if i == m.cursor {
			row.IsSelected = true
			m.currentHeading = row.Heading
		} else {
			row.IsSelected = false
		}
	}
	m.viewport.SetContent(table.Render())
	m.maxRows = table.LineCount
}

func (m *Model) UnstyledString() string {
	return m.table.GetAlignedRows()
}

func (m *Model) searchMode() bool {
	return m.search && m.searchBar.Focused()
}

func (m *Model) startSearch() tea.Cmd {
	m.search = true
	m.filterState = filtering
	m.searchBar.Focus()
	return textinput.Blink
}

func (m *Model) cursorToBeginning() {
	m.cursor = 0
}

func (m *Model) cursorToEnd() {
	m.cursor = m.maxRows - 1
}

func (m *Model) cursorToViewTop() {
	m.cursor = m.viewport.YOffset + 3
}

func (m *Model) cursorToViewMiddle() {
	m.cursor = (m.viewport.YOffset + m.viewport.Height) / 2
}

func (m *Model) cursorToViewBottom() {
	m.cursor = m.viewport.YOffset + m.viewport.Height - 3
}

func (m *Model) cursorPastViewTop() bool {
	return m.cursor < m.viewport.YOffset
}

func (m *Model) cursorPastViewBottom() bool {
	return m.cursor > m.viewport.YOffset+m.viewport.Height-1
}

func (m *Model) cursorPastBeginning() bool {
	return m.cursor < 0
}

func (m *Model) cursorPastEnd() bool {
	return m.cursor > m.maxRows-1
}


================================================
FILE: ui/list/list_test.go
================================================
package list

import (
	"testing"

	"github.com/kencx/keyb/config"
	"github.com/kencx/keyb/ui/table"
)

var (
	testRows = []*table.Row{
		table.NewHeading("fooTable"),
		{Text: "foo"},
		{Text: "bar"},
		{Text: "baz"},
	}
	testTable  = table.New(testRows)
	testConfig = &config.Config{
		Settings: config.Settings{
			Title:       "foo",
			Debug:       true,
			Reverse:     true,
			Mouse:       true,
			Prompt:      "prompt",
			Placeholder: "placeholder",
			PrefixSep:   "$",
			SepWidth:    4,
		}}
)

func TestNew(t *testing.T) {
	t.Run("populated", func(t *testing.T) {
		tm := New(testTable, testConfig)

		assertEqual(t, tm.table.LineCount, 4)
		assertEqual(t, tm.filterState, unfiltered)
		assertEqual(t, tm.filteredTable.LineCount, 0)

		assertEqual(t, tm.title, "foo")
		assertEqual(t, tm.debug, true)
		assertEqual(t, tm.table.Rows[0].Reversed, true)
		assertEqual(t, tm.viewport.MouseWheelEnabled, true)
		assertEqual(t, tm.searchBar.Prompt, "prompt")
		assertEqual(t, tm.searchBar.Placeholder, "placeholder")
		assertEqual(t, tm.table.Rows[0].PrefixSep, "$")
		assertEqual(t, tm.table.SepWidth, 4)
	})

	t.Run("empty", func(t *testing.T) {
		tm := New(table.New([]*table.Row{table.EmptyRow(), table.EmptyRow()}), testConfig)

		assertEqual(t, tm.title, "foo")
		assertEqual(t, tm.table.LineCount, 0)
		assertEqual(t, tm.filterState, unfiltered)
		assertEqual(t, tm.filteredTable.LineCount, 0)
	})

}

func TestReset(t *testing.T) {

	tm := New(testTable, testConfig)
	tm.filterState = filtering
	tm.searchBar.SetValue("searching...")
	tm.cursor = 10

	tm.Reset()

	assertEqual(t, tm.filteredTable.Render(), "")
	assertEqual(t, tm.searchBar.Value(), "searching...")
	assertEqual(t, tm.filterState, unfiltered)
	assertEqual(t, tm.cursor, 0)
	assertEqual(t, tm.maxRows, tm.table.LineCount)
}

func assertEqual[T comparable](t *testing.T, got, want T) {
	if got != want {
		t.Errorf("got %#v, want %#v", got, want)
	}
}


================================================
FILE: ui/list/update.go
================================================
package list

import (
	"sort"
	"strings"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/kencx/keyb/ui/table"
	"github.com/sahilm/fuzzy"
)

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {

	var cmds []tea.Cmd

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:

		// to play nice with borders and truncation,
		// <2 results in border exceeding max width
		m.viewport.Width = msg.Width - max(2, (m.padding*2+m.margin*2))
		m.viewport.Height = msg.Height - m.scrollOffset

		m.table.MaxWidth = m.viewport.Width - m.padding*2
		m.filteredTable.MaxWidth = m.viewport.Width - m.padding*2

		if m.cursorPastViewBottom() {
			m.cursor = m.viewport.YOffset + m.viewport.Height - 1
		}

	case tea.MouseMsg:
		if !m.viewport.MouseWheelEnabled {
			break
		}
		switch msg.Button {
		case tea.MouseButtonWheelUp:
			m.cursor -= m.viewport.MouseWheelDelta
			if m.cursorPastViewTop() {
				m.viewport.ScrollUp(m.viewport.MouseWheelDelta)
			}
		case tea.MouseButtonWheelDown:
			m.cursor += m.viewport.MouseWheelDelta
			if m.cursorPastViewBottom() {
				m.viewport.ScrollDown(m.viewport.MouseWheelDelta)
			}
		}
	}

	switch {
	case m.searchMode():
		cmds = append(cmds, m.handleSearch(msg))
	default:
		cmds = append(cmds, m.handleNormal(msg))
	}

	// cursor loop around
	if m.cursorPastBeginning() {
		m.cursorToEnd()
		m.viewport.GotoBottom()
	} else if m.cursorPastEnd() {
		m.cursorToBeginning()
		m.viewport.GotoTop()
	}

	m.visibleRows()
	return m, tea.Batch(cmds...)
}

func (m *Model) handleNormal(msg tea.Msg) tea.Cmd {

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.keys.Quit):
			return tea.Quit

		case key.Matches(msg, m.keys.Search):
			return m.startSearch()

		case key.Matches(msg, m.keys.ClearSearch):
			m.searchBar.Reset()
			return m.startSearch()

		case key.Matches(msg, m.keys.Up):
			m.cursor--
			if m.cursorPastViewTop() {
				m.viewport.ScrollUp(1)
			}
		case key.Matches(msg, m.keys.Down):
			m.cursor++
			if m.cursorPastViewBottom() {
				m.viewport.ScrollDown(1)
			}

		case key.Matches(msg, m.keys.UpFocus):
			m.cursor--
			if m.cursorPastViewTop() {
				m.viewport.ScrollUp(1)
			}
		case key.Matches(msg, m.keys.DownFocus):
			m.cursor++
			if m.cursorPastViewBottom() {
				m.viewport.ScrollDown(1)
			}

		case key.Matches(msg, m.keys.HalfUp):
			m.cursor -= m.viewport.Height / 2
			if m.cursorPastViewTop() {
				m.viewport.HalfPageUp()
			}

			// don't loop around
			if m.cursorPastBeginning() {
				m.cursorToBeginning()
				m.viewport.GotoTop()
			}
		case key.Matches(msg, m.keys.HalfDown):
			m.cursor += m.viewport.Height / 2
			if m.cursorPastViewBottom() {
				m.viewport.HalfPageDown()
			}

			// don't loop around
			if m.cursorPastEnd() {
				m.cursorToEnd()
				m.viewport.GotoBottom()
			}

		case key.Matches(msg, m.keys.FullUp):
			m.cursor -= m.viewport.Height
			if m.cursorPastViewTop() {
				m.viewport.PageUp()
			}

			// don't loop around
			if m.cursorPastBeginning() {
				m.cursorToBeginning()
				m.viewport.GotoTop()
			}

		case key.Matches(msg, m.keys.FullDown):
			m.cursor += m.viewport.Height
			if m.cursorPastViewBottom() {
				m.viewport.PageDown()
			}

			// don't loop around
			if m.cursorPastEnd() {
				m.cursorToEnd()
				m.viewport.GotoBottom()
			}

		case key.Matches(msg, m.keys.GoToFirstLine):
			m.cursorToBeginning()
			m.viewport.GotoTop()
		case key.Matches(msg, m.keys.GoToLastLine):
			m.cursorToEnd()
			m.viewport.GotoBottom()

		case key.Matches(msg, m.keys.GoToTop):
			m.cursorToViewTop()

		case key.Matches(msg, m.keys.GoToMiddle):
			m.cursorToViewMiddle()

		case key.Matches(msg, m.keys.GoToBottom):
			m.cursorToViewBottom()

			// case key.Matches(msg, m.keys.CenterCursor):
			// 	middle := m.viewport.Height / 2
			// 	diff := m.cursor - middle
			// 	if diff > 0 {
			// 		m.viewport.LineDown(diff)
			// 	} else {
			// 		m.viewport.LineUp(diff)
			// 	}
		}
	}
	return nil
}

func (m *Model) handleSearch(msg tea.Msg) tea.Cmd {

	var (
		cmds []tea.Cmd
		cmd  tea.Cmd
	)

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case msg.String() == "ctrl+c":
			return tea.Quit

		case key.Matches(msg, m.keys.ClearSearch):
			m.searchBar.Reset()
			return m.startSearch()

			// scrolling in search mode
		case key.Matches(msg, m.keys.UpFocus):
			m.cursor--
			if m.cursorPastViewTop() {
				m.viewport.ScrollUp(1)
			}
			return nil
		case key.Matches(msg, m.keys.DownFocus):
			m.cursor++
			if m.cursorPastViewBottom() {
				m.viewport.ScrollDown(1)
			}
			return nil

		case key.Matches(msg, m.keys.HalfUp):
			m.cursor -= m.viewport.Height / 2
			if m.cursorPastViewTop() {
				m.viewport.HalfPageUp()
			}

			// don't loop around
			if m.cursorPastBeginning() {
				m.cursorToBeginning()
				m.viewport.GotoTop()
			}
		case key.Matches(msg, m.keys.HalfDown):
			m.cursor += m.viewport.Height / 2
			if m.cursorPastViewBottom() {
				m.viewport.HalfPageDown()
			}

			// don't loop around
			if m.cursorPastEnd() {
				m.cursorToEnd()
				m.viewport.GotoBottom()
			}

		case key.Matches(msg, m.keys.FullUp):
			m.cursor -= m.viewport.Height
			if m.cursorPastViewTop() {
				m.viewport.PageUp()
			}

			// don't loop around
			if m.cursorPastBeginning() {
				m.cursorToBeginning()
				m.viewport.GotoTop()
			}

		case key.Matches(msg, m.keys.FullDown):
			m.cursor += m.viewport.Height
			if m.cursorPastViewBottom() {
				m.viewport.PageDown()
			}

			// don't loop around
			if m.cursorPastEnd() {
				m.cursorToEnd()
				m.viewport.GotoBottom()
			}

		case key.Matches(msg, m.keys.Normal):
			m.search = false
			m.searchBar.Blur()

			if m.filteredTable.Empty() {
				m.filterState = unfiltered
			}
			return nil
		}
	}

	// filter with search input
	m.searchBar, cmd = m.searchBar.Update(msg)
	cmds = append(cmds, cmd)

	prefix := "h:"
	if strings.HasPrefix(m.searchBar.Value(), prefix) {
		matchHeadings(m, prefix)
	} else {
		matchRows(m)
	}

	// reset if search input is empty regardless of filterState
	if m.searchBar.Value() == "" {
		m.Reset()

		// remain in filtering state
		// until user explicitly returns to Normal mode
		m.filterState = filtering
	}
	return tea.Batch(cmds...)
}

func filter(term string, target []string) fuzzy.Matches {
	matches := fuzzy.Find(term, target)
	sort.Stable(matches)
	return matches
}

func matchHeadings(m *Model, prefix string) {
	var matches fuzzy.Matches
	value := strings.TrimSpace(strings.TrimPrefix(m.searchBar.Value(), prefix))
	if !m.table.Empty() {
		matches = filter(value, m.table.GetPlainHeadings())
	}

	// present new filtered rows
	m.filteredTable.Reset()
	if len(matches) == 0 {
		m.filteredTable.AppendRow(table.EmptyRow())

	} else {
		var hlMatches []*table.Row
		// get non-pointers as filtering is ephemeral
		headings := m.table.GetCopyOfHeadings()

		for _, match := range matches {
			heading := headings[match.Index]
			heading.IsFiltered = true
			heading.MatchedIndex = match.MatchedIndexes

			hlMatches = append(hlMatches, &heading)
			hlMatches = append(hlMatches, m.table.GetAllRowsofHeading(heading.Text)...)
		}
		m.filteredTable.AppendRows(hlMatches...)
	}
}

func matchRows(m *Model) {
	var matches fuzzy.Matches
	if !m.table.Empty() {
		matches = filter(m.searchBar.Value(), m.table.GetPlainRowsWithoutHeadings())
	}

	// present new filtered rows
	m.filteredTable.Reset()
	if len(matches) == 0 {
		m.filteredTable.AppendRow(table.EmptyRow())

	} else {
		var hlMatches []*table.Row
		// get non-pointers as filtering is ephemeral
		rows := m.table.GetCopyOfRowsWithoutHeadings()

		for _, match := range matches {
			row := rows[match.Index]
			row.IsFiltered = true
			row.MatchedIndex = match.MatchedIndexes
			hlMatches = append(hlMatches, &row)
		}
		m.filteredTable.AppendRows(hlMatches...)
	}
}


================================================
FILE: ui/list/view.go
================================================
package list

import (
	"fmt"

	"github.com/charmbracelet/lipgloss"
)

func (m *Model) View() string {
	if m.table.Empty() {
		m.viewport.SetContent("\nNo key bindings found")
	}

	counter := formCounter(m)

	var view string
	if m.promptLocation == "bottom" {
		view = lipgloss.JoinVertical(
			lipgloss.Left,
			m.viewport.View(),
			counter,
			m.searchBar.View(),
		)
	} else {
		view = lipgloss.JoinVertical(
			lipgloss.Left,
			m.searchBar.View(),
			counter,
			m.viewport.View(),
		)
	}

	style := m.border.
		Margin(m.margin).
		Padding(m.padding).
		Width(m.viewport.Width)
	return style.Render(view)
}

func formCounter(m *Model) string {
	var counter string

	if m.filterState == filtering && m.searchBar.Value() != "" {
		counter = fmt.Sprintf("%d/%d %s", m.filteredTable.LineCount, m.table.LineCount, m.currentHeading)
	} else {
		counter = fmt.Sprintf("%d/%d %s", m.table.LineCount, m.table.LineCount, m.currentHeading)
	}

	if m.debug {
		counter = fmt.Sprintf("%s\tLine: %d YOffset: %d Height: %d",
			counter, m.cursor, m.viewport.YOffset, m.viewport.Height)
	}
	return m.counterStyle.Render(counter)
}


================================================
FILE: ui/table/row.go
================================================
package table

import (
	"fmt"

	"github.com/charmbracelet/lipgloss"
)

type Row struct {
	Text      string
	Key       string
	Prefix    string
	PrefixSep string

	// default false unless prefix defined
	ShowPrefix bool
	// only used to show row's corresponding heading during filtering
	Heading string

	MatchedIndex []int
	Styles       RowStyles

	IsHeading  bool
	IsSelected bool
	IsFiltered bool
	Reversed   bool
}

type RowStyles struct {
	Normal          lipgloss.Style
	Heading         lipgloss.Style
	Selected        lipgloss.Style
	SelectedHeading lipgloss.Style
	Filtered        lipgloss.Style
}

func NewHeading(text string) *Row {
	return &Row{
		Text:      text,
		IsHeading: true,
	}
}

func NewRow(text, key, prefix, heading string) *Row {
	r := &Row{
		Text:    text,
		Key:     key,
		Prefix:  prefix,
		Heading: heading,
	}
	r.ShowPrefix = r.Prefix != ""
	return r
}

func EmptyRow() *Row {
	return &Row{}
}

func (r *Row) String() string {
	if r.Text == "" && r.Key == "" {
		return ""
	}

	if r.IsHeading {
		return fmt.Sprintf("%s\t ", r.Text)
	}

	if r.Reversed {
		return r.ReverseString()
	}

	if !r.ShowPrefix {
		return fmt.Sprintf("%s\t%s", r.Text, r.Key)
	}

	key := fmt.Sprintf("%s %s %s", r.Prefix, r.PrefixSep, r.Key)
	return fmt.Sprintf("%s\t%s", r.Text, key)
}

func (r *Row) ReverseString() string {
	if !r.ShowPrefix {
		return fmt.Sprintf("%s\t%s", r.Key, r.Text)
	}

	key := fmt.Sprintf("%s %s %s", r.Prefix, r.PrefixSep, r.Key)
	return fmt.Sprintf("%s\t%s", key, r.Text)
}

func (r *Row) Render() string {
	s := r.Styles

	if r.IsSelected {
		if r.IsFiltered {
			// Inline to remove margins, paddings and borders from styledrunes
			unmatched := s.Selected.Inline(true)
			matched := s.Filtered.Inherit(unmatched)
			str := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched)

			if r.IsHeading {
				return s.SelectedHeading.Render(str)
			}
			return s.Selected.Render(str)
		}

		if r.IsHeading {
			return s.SelectedHeading.Render(r.String())
		}
		return s.Selected.Render(r.String())
	}

	if r.IsFiltered {
		unmatched := s.Normal.Inline(true)
		matched := s.Filtered.Inherit(unmatched)
		str := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched)

		if r.IsHeading {
			return s.Heading.Render(str)
		}
		return s.Normal.Render(str)
	}

	if r.IsHeading {
		return s.Heading.Render(r.String())
	}
	return s.Normal.Render(r.String())
}


================================================
FILE: ui/table/table.go
================================================
package table

import (
	"fmt"
	"strings"

	"github.com/juju/ansiterm/tabwriter"
	"github.com/muesli/reflow/truncate"
)

type Model struct {
	Rows      []*Row
	LineCount int
	SepWidth  int
	MaxWidth  int // prevents line wrapping
}

func New(rows []*Row) *Model {
	t := &Model{
		Rows: rows,
	}

	for _, row := range rows {
		if row.String() != "" {
			t.LineCount += 1
		}
	}
	return t
}

func NewEmpty(n int) *Model {
	return &Model{
		Rows:      make([]*Row, 1, max(2, n)),
		LineCount: 0,
	}
}

func (t *Model) Empty() bool {
	return t.LineCount <= 0
}

func (t *Model) AppendRow(row *Row) {
	t.Rows = append(t.Rows, row)
	t.LineCount += 1
}

func (t *Model) AppendRows(rows ...*Row) {
	t.Rows = append(t.Rows, rows...)
	t.LineCount += len(rows)
}

func (t *Model) Join(table *Model) {
	t.Rows = append(t.Rows, table.Rows...)
	t.LineCount += table.LineCount
}

func (t *Model) Reset() {
	t.Rows = nil
	t.LineCount = 0
}

// Align and style rows
func (t *Model) Render() string {
	var sb strings.Builder
	tw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0)

	for _, row := range t.Rows {
		if row != nil && row.String() != "" {
			fmt.Fprintln(tw, row.Render())
		}
	}
	tw.Flush()

	if sb.String() != "" {
		sl := strings.Split(strings.TrimSuffix(sb.String(), "\n"), "\n")
		sb.Reset()

		// Unable to truncate while aligning due to nature of tabwriter
		for _, row := range sl {
			if t.MaxWidth > 0 {
				fmt.Fprintln(&sb, truncate.StringWithTail(row, uint(t.MaxWidth), "..."))
			} else {
				fmt.Fprintln(&sb, row)
			}
		}
	}
	return sb.String()
}

// Helper functions for retrieving specific rows in a table
// Aligned but unstyled rows
func (t *Model) GetAlignedRows() string {
	var sb strings.Builder
	tw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0)

	for _, row := range t.Rows {
		if row != nil && row.String() != "" {
			fmt.Fprintln(tw, row.String())
		}
	}

	tw.Flush()
	return strings.TrimSuffix(sb.String(), "\n")
}

func (t *Model) GetPlainHeadings() []string {
	var res []string
	for _, r := range t.Rows {
		if r.IsHeading {
			res = append(res, r.String())
		}
	}
	return res
}

func (t *Model) GetPlainRowsWithoutHeadings() []string {
	var res []string
	for _, r := range t.Rows {
		if !r.IsHeading {
			res = append(res, r.String())
		}
	}
	return res
}

func (t *Model) GetCopyOfHeadings() []Row {
	var res []Row
	for _, r := range t.Rows {
		if r.IsHeading {
			res = append(res, *r)
		}
	}
	return res
}

func (t *Model) GetCopyOfRowsWithoutHeadings() []Row {
	var res []Row
	for _, r := range t.Rows {
		if !r.IsHeading {
			res = append(res, *r)
		}
	}
	return res
}

func (t *Model) GetAllRowsofHeading(heading string) []*Row {
	var res []*Row
	for _, r := range t.Rows {
		if !r.IsHeading && r.Heading == heading {
			res = append(res, r)
		}
	}
	return res
}


================================================
FILE: ui/table/table_test.go
================================================
package table

import (
	"testing"
)

var (
	testRows = []*Row{
		NewHeading("heading"),
		{Text: "foo", Key: "bar"},
		{Text: "baz", Key: "foo"},
	}
)

func TestNew(t *testing.T) {
	t.Run("populated", func(t *testing.T) {
		tt := New(testRows)

		assertEqual(t, tt.LineCount, 3)
	})

	t.Run("empty", func(t *testing.T) {
		tt := NewEmpty(5)

		assertEqual(t, len(tt.Rows), 1)
		assertEqual(t, cap(tt.Rows), 5)
		assertEqual(t, tt.LineCount, 0)
		assertEqual(t, tt.Render(), "")
	})
}

func TestAppend(t *testing.T) {

	row1 := NewRow("baz", "", "", "")
	row2 := NewRow("foobar", "", "", "")

	t.Run("appendRow", func(t *testing.T) {
		tt := New(testRows)
		originalCount := tt.LineCount

		tt.AppendRow(row1)
		assertEqual(t, tt.LineCount, originalCount+1)
	})

	t.Run("appendRows", func(t *testing.T) {
		tt := New(testRows)
		originalCount := tt.LineCount

		tt.AppendRows(row1, row2)
		assertEqual(t, tt.LineCount, originalCount+2)
	})
}

func TestJoin(t *testing.T) {
	t1 := New(testRows)
	t1Count := t1.LineCount
	t2 := New([]*Row{{Text: "baz"}})
	t2Count := t2.LineCount
	t1.Join(t2)

	assertEqual(t, t1.LineCount, t1Count+t2Count)
}

func TestReset(t *testing.T) {
	tt := New(testRows)
	tt.Reset()

	assertEqual(t, tt.LineCount, 0)
	assertEqual(t, tt.Render(), "")
}

func assertEqual[T comparable](t *testing.T, got, want T) {
	if got != want {
		t.Errorf("got %#v, want %#v", got, want)
	}
}


================================================
FILE: ui/ui.go
================================================
package ui

import (
	"sort"
	"strings"

	"github.com/kencx/keyb/config"
	"github.com/kencx/keyb/ui/list"
	"github.com/kencx/keyb/ui/table"

	tea "github.com/charmbracelet/bubbletea"
)

type Model struct {
	List list.Model
	Apps *config.Apps
}

func NewModel(a config.Apps, config *config.Config) *Model {

	table := createParentTable(a, config.SortKeys)
	return &Model{
		List: list.New(table, config),
		Apps: &a,
	}
}

func createParentTable(a config.Apps, sortKeys bool) *table.Model {

	if len(a) <= 0 {
		t := table.NewEmpty(1)
		return t
	}

	sort.Slice(a, func(i, j int) bool {
		return a[i].Name < a[j].Name
	})

	parent := appToTable(a[0].Name, *a[0], sortKeys)

	if len(a) > 1 {
		for _, k := range a[1:] {
			child := appToTable(k.Name, *k, sortKeys)
			parent.Join(child)
		}
	}
	return parent
}

func appToTable(heading string, app config.App, sortKeys bool) *table.Model {
	var rows []*table.Row

	h := table.NewHeading(heading)
	rows = append(rows, h)

	if sortKeys {
		sort.Slice(app.Keybinds, func(i, j int) bool {
			return strings.ToLower(app.Keybinds[i].Name) < strings.ToLower(app.Keybinds[j].Name)
		})
	}

	// convert Keybind to Row
	for _, kb := range app.Keybinds {
		row := table.NewRow(kb.Name, kb.Key, app.Prefix, heading)

		// KeyBind's ignore prefix defaults to false
		// so user can choose to ignore prefix for a specific kb
		if kb.IgnorePrefix {
			// Row's show prefix defaults to false
			// so prefix is not shown unless defined
			row.ShowPrefix = false
		}
		rows = append(rows, row)
	}
	return table.New(rows)
}

func (m *Model) Init() tea.Cmd {
	return nil
}

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

	var (
		cmd  tea.Cmd
		cmds []tea.Cmd
	)

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.List.Resize(msg.Width, msg.Height)
	}

	m.List, cmd = m.List.Update(msg)
	cmds = append(cmds, cmd)

	return m, tea.Batch(cmds...)
}

func (m *Model) View() string {
	return m.List.View()
}
Download .txt
gitextract_nio3xsob/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .goreleaser.yaml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── config/
│   ├── config.go
│   ├── config_test.go
│   ├── model.go
│   └── model_test.go
├── examples/
│   ├── Helix.yml
│   ├── ansi.yml
│   ├── awesomewm.yml
│   ├── bspwm.yml
│   ├── changefile
│   ├── config/
│   │   ├── README.md
│   │   └── default.yml
│   ├── curl.yml
│   ├── cut.yml
│   ├── date.yml
│   ├── dd.yml
│   ├── discord.yml
│   ├── du.yml
│   ├── dwm.yml
│   ├── find.yml
│   ├── firefox.yml
│   ├── irssi.yml
│   ├── kitty.yml
│   ├── less.yml
│   ├── ranger.yml
│   ├── screen.yml
│   ├── tar.yml
│   ├── tmux.yml
│   ├── unzip.yml
│   ├── vim.yml
│   ├── vscode.yml
│   ├── weechat.yml
│   ├── zip.yml
│   └── zsh.yml
├── go.mod
├── go.sum
├── main.go
├── output/
│   ├── output.go
│   └── output_test.go
├── testdata/
│   ├── testConfig.json
│   ├── testConfig.yml
│   ├── testConfigMinimal.json
│   ├── testConfigMinimal.yml
│   ├── testkeyb.json
│   └── testkeyb.yml
└── ui/
    ├── list/
    │   ├── keymap.go
    │   ├── list.go
    │   ├── list_test.go
    │   ├── update.go
    │   └── view.go
    ├── table/
    │   ├── row.go
    │   ├── table.go
    │   └── table_test.go
    └── ui.go
Download .txt
SYMBOL INDEX (106 symbols across 16 files)

FILE: config/config.go
  constant defaultConfigDir (line 14) | defaultConfigDir  = "keyb"
  constant defaultConfigFile (line 15) | defaultConfigFile = "config.yml"
  constant defaultKeybFile (line 16) | defaultKeybFile   = "keyb.yml"
  type Config (line 19) | type Config struct
  type Settings (line 25) | type Settings struct
  type Color (line 43) | type Color struct
  type Keys (line 56) | type Keys struct
  function Parse (line 136) | func Parse(flagCPath, flagKPath string) (Apps, *Config, error) {
  function UnmarshalConfig (line 165) | func UnmarshalConfig(configFile, basePath string) (*Config, error) {
  function newDefaultConfig (line 198) | func newDefaultConfig(basePath string) *Config {
  function UnmarshalKeyb (line 205) | func UnmarshalKeyb(keybFile, basePath string) (Apps, error) {
  function newDefaultKeyb (line 245) | func newDefaultKeyb(path string) Apps {
  function getXDGConfigDir (line 256) | func getXDGConfigDir() (string, error) {

FILE: config/config_test.go
  constant testBasePath (line 10) | testBasePath = "../testdata"
  function TestUnmarshalConfig (line 12) | func TestUnmarshalConfig(t *testing.T) {
  function TestUnmarshalKeyb (line 102) | func TestUnmarshalKeyb(t *testing.T) {

FILE: config/model.go
  type App (line 11) | type App struct
    method String (line 19) | func (a App) String() string {
  type Apps (line 17) | type Apps
    method addOrUpdate (line 73) | func (apps *Apps) addOrUpdate(appName string, name, key string, ignore...
    method exist (line 97) | func (apps Apps) exist(appName string) bool {
  type KeyBind (line 23) | type KeyBind struct
  function AddEntry (line 32) | func AddEntry(path, binding string, kbIgnorePrefix bool) error {

FILE: config/model_test.go
  function TestAddOrUpdate (line 8) | func TestAddOrUpdate(t *testing.T) {

FILE: main.go
  constant help (line 16) | help = `usage: keyb [options] <command>
  constant addHelp (line 30) | addHelp = `usage: keyb [-k file] add [app; name; key]
  function main (line 41) | func main() {
  function start (line 135) | func start(m *ui.Model) error {

FILE: output/output.go
  function ToFile (line 13) | func ToFile(m *ui.Model, path string) error {
  function ToStdout (line 43) | func ToStdout(m *ui.Model) error {

FILE: output/output_test.go
  function TestToJson (line 34) | func TestToJson(t *testing.T) {
  function TestToStdout (line 54) | func TestToStdout(t *testing.T) {

FILE: ui/list/keymap.go
  type KeyMap (line 10) | type KeyMap struct
  type TextInputKeyMap (line 36) | type TextInputKeyMap struct
  function CreateKeyMap (line 55) | func CreateKeyMap(keys config.Keys) KeyMap {
  function SetKey (line 97) | func SetKey(s string) key.Binding {
  function splitAndTrim (line 103) | func splitAndTrim(s, sep string) []string {

FILE: ui/list/list.go
  type filterState (line 14) | type filterState
  constant unfiltered (line 17) | unfiltered filterState = iota
  constant filtering (line 18) | filtering
  type Model (line 21) | type Model struct
    method style (line 98) | func (m *Model) style(c *config.Config) {
    method Init (line 149) | func (m *Model) Init() tea.Cmd {
    method Resize (line 153) | func (m *Model) Resize(width, height int) {
    method Reset (line 159) | func (m *Model) Reset() {
    method visibleRows (line 168) | func (m *Model) visibleRows() {
    method SyncContent (line 177) | func (m *Model) SyncContent(table *table.Model) {
    method UnstyledString (line 199) | func (m *Model) UnstyledString() string {
    method searchMode (line 203) | func (m *Model) searchMode() bool {
    method startSearch (line 207) | func (m *Model) startSearch() tea.Cmd {
    method cursorToBeginning (line 214) | func (m *Model) cursorToBeginning() {
    method cursorToEnd (line 218) | func (m *Model) cursorToEnd() {
    method cursorToViewTop (line 222) | func (m *Model) cursorToViewTop() {
    method cursorToViewMiddle (line 226) | func (m *Model) cursorToViewMiddle() {
    method cursorToViewBottom (line 230) | func (m *Model) cursorToViewBottom() {
    method cursorPastViewTop (line 234) | func (m *Model) cursorPastViewTop() bool {
    method cursorPastViewBottom (line 238) | func (m *Model) cursorPastViewBottom() bool {
    method cursorPastBeginning (line 242) | func (m *Model) cursorPastBeginning() bool {
    method cursorPastEnd (line 246) | func (m *Model) cursorPastEnd() bool {
  function New (line 47) | func New(t *table.Model, c *config.Config) Model {

FILE: ui/list/list_test.go
  function TestNew (line 31) | func TestNew(t *testing.T) {
  function TestReset (line 60) | func TestReset(t *testing.T) {
  function assertEqual (line 76) | func assertEqual[T comparable](t *testing.T, got, want T) {

FILE: ui/list/update.go
  method Update (line 13) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  method handleNormal (line 70) | func (m *Model) handleNormal(msg tea.Msg) tea.Cmd {
  method handleSearch (line 183) | func (m *Model) handleSearch(msg tea.Msg) tea.Cmd {
  function filter (line 294) | func filter(term string, target []string) fuzzy.Matches {
  function matchHeadings (line 300) | func matchHeadings(m *Model, prefix string) {
  function matchRows (line 329) | func matchRows(m *Model) {

FILE: ui/list/view.go
  method View (line 9) | func (m *Model) View() string {
  function formCounter (line 40) | func formCounter(m *Model) string {

FILE: ui/table/row.go
  type Row (line 9) | type Row struct
    method String (line 59) | func (r *Row) String() string {
    method ReverseString (line 80) | func (r *Row) ReverseString() string {
    method Render (line 89) | func (r *Row) Render() string {
  type RowStyles (line 29) | type RowStyles struct
  function NewHeading (line 37) | func NewHeading(text string) *Row {
  function NewRow (line 44) | func NewRow(text, key, prefix, heading string) *Row {
  function EmptyRow (line 55) | func EmptyRow() *Row {

FILE: ui/table/table.go
  type Model (line 11) | type Model struct
    method Empty (line 38) | func (t *Model) Empty() bool {
    method AppendRow (line 42) | func (t *Model) AppendRow(row *Row) {
    method AppendRows (line 47) | func (t *Model) AppendRows(rows ...*Row) {
    method Join (line 52) | func (t *Model) Join(table *Model) {
    method Reset (line 57) | func (t *Model) Reset() {
    method Render (line 63) | func (t *Model) Render() string {
    method GetAlignedRows (line 92) | func (t *Model) GetAlignedRows() string {
    method GetPlainHeadings (line 106) | func (t *Model) GetPlainHeadings() []string {
    method GetPlainRowsWithoutHeadings (line 116) | func (t *Model) GetPlainRowsWithoutHeadings() []string {
    method GetCopyOfHeadings (line 126) | func (t *Model) GetCopyOfHeadings() []Row {
    method GetCopyOfRowsWithoutHeadings (line 136) | func (t *Model) GetCopyOfRowsWithoutHeadings() []Row {
    method GetAllRowsofHeading (line 146) | func (t *Model) GetAllRowsofHeading(heading string) []*Row {
  function New (line 18) | func New(rows []*Row) *Model {
  function NewEmpty (line 31) | func NewEmpty(n int) *Model {

FILE: ui/table/table_test.go
  function TestNew (line 15) | func TestNew(t *testing.T) {
  function TestAppend (line 32) | func TestAppend(t *testing.T) {
  function TestJoin (line 54) | func TestJoin(t *testing.T) {
  function TestReset (line 64) | func TestReset(t *testing.T) {
  function assertEqual (line 72) | func assertEqual[T comparable](t *testing.T, got, want T) {

FILE: ui/ui.go
  type Model (line 14) | type Model struct
    method Init (line 78) | func (m *Model) Init() tea.Cmd {
    method Update (line 82) | func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 100) | func (m *Model) View() string {
  function NewModel (line 19) | func NewModel(a config.Apps, config *config.Config) *Model {
  function createParentTable (line 28) | func createParentTable(a config.Apps, sortKeys bool) *table.Model {
  function appToTable (line 50) | func appToTable(heading string, app config.App, sortKeys bool) *table.Mo...
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (177K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 337,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1284,
    "preview": "name: CI\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*\"\n  pull_request:\n    branches:\n      - master\n\n"
  },
  {
    "path": ".gitignore",
    "chars": 110,
    "preview": "*.json\n*.toml\n*.ini\n*.gitcommit\n\ntestdata/keyb*\n!testdata/*.json\n!testdata/*.yml\n!testdata/*.yaml\nkeyb\n\ndist/\n"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 518,
    "preview": "project_name: keyb\nversion: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n    "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3635,
    "preview": "# Changelog\nAll notable changes to this project will be documented in this file.\n\n[v0.2.0]: https://github.com/kencx/key"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2022 kencx\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "Makefile",
    "chars": 641,
    "preview": "binary = keyb\nversion = $(shell git describe --tags)\nldflags = -ldflags \"-s -w -X main.version=${version}\"\n\n.PHONY: help"
  },
  {
    "path": "README.md",
    "chars": 5362,
    "preview": "# keyb\n\n<p align=\"center\">\n\t<img width=\"660\" src=\"https://github.com/kencx/keyb/blob/master/assets/compressed.gif?raw=tr"
  },
  {
    "path": "config/config.go",
    "chars": 8126,
    "preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\nconst (\n\tdefau"
  },
  {
    "path": "config/config_test.go",
    "chars": 3975,
    "preview": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nconst testBasePath = \"../testdata\"\n\nfunc TestUn"
  },
  {
    "path": "config/model.go",
    "chars": 2224,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype App struct {\n\tPrefix   string    `yaml:\"pr"
  },
  {
    "path": "config/model_test.go",
    "chars": 1046,
    "preview": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestAddOrUpdate(t *testing.T) {\n\tt.Run(\"update existing\", func(t "
  },
  {
    "path": "examples/Helix.yml",
    "chars": 15755,
    "preview": "- name: Helix Normal mode (Movement)\n  keybinds:\n    - name: \"Move left\"\n      key: \"h, Left\"\n    - name: \"Move down\"\n  "
  },
  {
    "path": "examples/ansi.yml",
    "chars": 3143,
    "preview": "\n# https://github.com/cheat/cheatsheets\n- name: ansi\n  keybinds:\n    - name: 'Text Reset'\n      key: '\\e[0m'\n    - name:"
  },
  {
    "path": "examples/awesomewm.yml",
    "chars": 3551,
    "preview": "# Awesome WM Documentation\n# source: https://awesomewm.org/doc/manpages/awesome.1.html\n\n- name: awesomewm (window manage"
  },
  {
    "path": "examples/bspwm.yml",
    "chars": 1911,
    "preview": "# bspwm/sxhkd\n# source: https://github.com/baskerville/bspwm/blob/master/examples/sxhkdrc\n\n- name: bspwm\n  keybinds:\n   "
  },
  {
    "path": "examples/changefile",
    "chars": 510,
    "preview": "#!/usr/bin/env bash\n# A raw script to convert a cheat file from\n# https://github.com/cheat/cheatsheets\\\n# to a markdown "
  },
  {
    "path": "examples/config/README.md",
    "chars": 4721,
    "preview": "# Configuration\n\nkeyb will accept the following config in decreasing priority:\n\n- `-c FILE` flag\n- The default config pa"
  },
  {
    "path": "examples/config/default.yml",
    "chars": 1027,
    "preview": "settings:\n  keyb_path: \"$HOME/.config/keyb/keyb.yml\"\n  debug: false\n  reverse: false\n  mouse: true\n  search_mode: false\n"
  },
  {
    "path": "examples/curl.yml",
    "chars": 1391,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: curl\n  keybinds:\n    - name: \"To download a file\"\n      key: \"curl <url>\""
  },
  {
    "path": "examples/cut.yml",
    "chars": 884,
    "preview": "# Copied from cut --help\n- name: cut\n  keybinds:\n    - name: \"To cut out the third field of text or stdoutput that is de"
  },
  {
    "path": "examples/date.yml",
    "chars": 4390,
    "preview": "# Copied from date --help\n- name: date\n  keybinds:\n    - name: \"To print the date in a format suitable for affixing to f"
  },
  {
    "path": "examples/dd.yml",
    "chars": 1229,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: dd\n  keybinds:\n    - name: \"Backup disk partition with dd\"\n      key: \"dd"
  },
  {
    "path": "examples/discord.yml",
    "chars": 1679,
    "preview": "- name: discord (navigation)\n  keybinds:\n    - name: \"Switch Servers\"\n      key: \"[Ctrl] [Alt] [↑] or [↓]\"\n    - name: \""
  },
  {
    "path": "examples/du.yml",
    "chars": 684,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: du\n  keybinds:\n    - name: \"To sort directories/files by size\"\n      key:"
  },
  {
    "path": "examples/dwm.yml",
    "chars": 1949,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: dwm\n  keybinds:\n    - name: \"launch terminal.\"\n      key: \"[Shift]+[Mod]+"
  },
  {
    "path": "examples/find.yml",
    "chars": 2962,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: find\n  keybinds:\n  - name: \"To find files following symlinks (otherwise t"
  },
  {
    "path": "examples/firefox.yml",
    "chars": 6841,
    "preview": "- name: Firefox Navigation\n  keybinds:\n    - name: \"Back\"\n      key: \"[Alt] [←]\"\n    - name: \"Forward\"\n      key: \"[Alt]"
  },
  {
    "path": "examples/irssi.yml",
    "chars": 962,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: irssi\n  keybinds:\n  - name: \"To connect to an IRC server:\"\n    key : \"/co"
  },
  {
    "path": "examples/kitty.yml",
    "chars": 2617,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: kitty\n  keybinds:\n    - name: \"Line up\"\n      key: \"ctrl+shift+up\"\n    - "
  },
  {
    "path": "examples/less.yml",
    "chars": 681,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: less\n  keybinds:\n  - name: \"Go to end of file:\"\n    key: \"G\"\n  - name: \"G"
  },
  {
    "path": "examples/ranger.yml",
    "chars": 502,
    "preview": "- name: ranger\n  keybinds:\n    - name: \"navigation and tabs\"\n      key: \"g\"\n    - name: \"':open_with' command\"\n      key"
  },
  {
    "path": "examples/screen.yml",
    "chars": 437,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: screen\n  keybinds:\n  - name: \"To start a new named screen session:\"\n    k"
  },
  {
    "path": "examples/tar.yml",
    "chars": 933,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: tar\n  keybinds:\n  - name: \"To extract an uncompressed archive:\"\n    key: "
  },
  {
    "path": "examples/tmux.yml",
    "chars": 3358,
    "preview": "# tmux\n# source: http://man.openbsd.org/OpenBSD-current/man1/tmux.1\n\n- name: tmux (copy mode)\n  prefix: \"[Ctrl] [b]\"\n  k"
  },
  {
    "path": "examples/unzip.yml",
    "chars": 385,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: unzip\n  keybinds:\n  - name: \"To extract an archive:\"\n    key: \"unzip <arc"
  },
  {
    "path": "examples/vim.yml",
    "chars": 11578,
    "preview": "# vim\n# source: http://vim.rtorr.com\n\n- name: vim (tabs)\n  keybinds:\n    - name: \"move to tab number #\"\n      key: \"#gt\""
  },
  {
    "path": "examples/vscode.yml",
    "chars": 7679,
    "preview": "# Visual Studio Code\n# source: https://code.visualstudio.com/docs/customization/keybinds\n\n- name: vscode (basic editing)"
  },
  {
    "path": "examples/weechat.yml",
    "chars": 517,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: weechat\n  keybinds:\n  - name: \"To set an unread marker on all windows:\"\n "
  },
  {
    "path": "examples/zip.yml",
    "chars": 533,
    "preview": "# https://github.com/cheat/cheatsheets\n- name: zip\n  keybinds:\n    - name: \"To create zip file\"\n      key: \"zip archive."
  },
  {
    "path": "examples/zsh.yml",
    "chars": 3426,
    "preview": "# ZSH\n# https://github.com/cheat/cheatsheets\n- name: zsh (basic)\n  keybinds:\n    - name: \"A plain old glob\"\n      key: \""
  },
  {
    "path": "go.mod",
    "chars": 1446,
    "preview": "module github.com/kencx/keyb\n\ngo 1.26.1\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v1.0.0\n\tgithub.com/charmbracelet/bu"
  },
  {
    "path": "go.sum",
    "chars": 7222,
    "preview": "github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go"
  },
  {
    "path": "main.go",
    "chars": 3099,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/kencx/keyb/co"
  },
  {
    "path": "output/output.go",
    "chars": 950,
    "preview": "package output\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/kencx/keyb/ui\"\n\t\"gopkg.in/yaml.v2\""
  },
  {
    "path": "output/output_test.go",
    "chars": 1308,
    "preview": "package output\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/kencx/keyb/config\"\n\t\"github.co"
  },
  {
    "path": "testdata/testConfig.json",
    "chars": 1317,
    "preview": "{\n  \"settings\": {\n    \"keyb_path\": \"./custom.yml\",\n    \"debug\": true,\n    \"reverse\": true,\n    \"mouse\": false,\n    \"sear"
  },
  {
    "path": "testdata/testConfig.yml",
    "chars": 1011,
    "preview": "settings:\n  keyb_path: \"./custom.yml\"\n  debug: true\n  reverse: true\n  mouse: false\n  search_mode: false\n  sort_keys: tru"
  },
  {
    "path": "testdata/testConfigMinimal.json",
    "chars": 21,
    "preview": "{\n  \"settings\": {}\n}\n"
  },
  {
    "path": "testdata/testConfigMinimal.yml",
    "chars": 14,
    "preview": "---\nsettings:\n"
  },
  {
    "path": "testdata/testkeyb.json",
    "chars": 116,
    "preview": "[\n  {\n    \"name\": \"test\",\n    \"keybinds\": [\n      {\n        \"name\": \"foo\",\n        \"key\": \"bar\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "testdata/testkeyb.yml",
    "chars": 52,
    "preview": "- name: test\n  keybinds:\n  - name: foo\n    key: bar\n"
  },
  {
    "path": "ui/list/keymap.go",
    "chars": 3153,
    "preview": "package list\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/kencx/keyb/config\"\n)\n\ntype KeyMa"
  },
  {
    "path": "ui/list/list.go",
    "chars": 5763,
    "preview": "package list\n\nimport (\n\t\"github.com/kencx/keyb/config\"\n\t\"github.com/kencx/keyb/ui/table\"\n\n\t\"github.com/charmbracelet/bub"
  },
  {
    "path": "ui/list/list_test.go",
    "chars": 1933,
    "preview": "package list\n\nimport (\n\t\"testing\"\n\n\t\"github.com/kencx/keyb/config\"\n\t\"github.com/kencx/keyb/ui/table\"\n)\n\nvar (\n\ttestRows "
  },
  {
    "path": "ui/list/update.go",
    "chars": 7807,
    "preview": "package list\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubble"
  },
  {
    "path": "ui/list/view.go",
    "chars": 1121,
    "preview": "package list\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nfunc (m *Model) View() string {\n\tif m.table.Empty"
  },
  {
    "path": "ui/table/row.go",
    "chars": 2412,
    "preview": "package table\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype Row struct {\n\tText      string\n\tKey       s"
  },
  {
    "path": "ui/table/table.go",
    "chars": 2805,
    "preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/juju/ansiterm/tabwriter\"\n\t\"github.com/muesli/reflow/truncate\"\n)\n"
  },
  {
    "path": "ui/table/table_test.go",
    "chars": 1402,
    "preview": "package table\n\nimport (\n\t\"testing\"\n)\n\nvar (\n\ttestRows = []*Row{\n\t\tNewHeading(\"heading\"),\n\t\t{Text: \"foo\", Key: \"bar\"},\n\t\t"
  },
  {
    "path": "ui/ui.go",
    "chars": 1952,
    "preview": "package ui\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/kencx/keyb/config\"\n\t\"github.com/kencx/keyb/ui/list\"\n\t\"github.com/k"
  }
]

About this extraction

This page contains the full source code of the kencx/keyb GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (155.7 KB), approximately 51.7k tokens, and a symbol index with 106 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!