[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*\"\n  pull_request:\n    branches:\n      - master\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout codebase\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n\n      - name: Verify dependencies\n        run: go mod verify\n\n      - name: Build application\n        run: make build\n\n      - name: Vet\n        run: go vet ./...\n\n      - name: Install staticcheck\n        run: go install honnef.co/go/tools/cmd/staticcheck@latest\n\n      - name: Run staticcheck\n        run: staticcheck ./...\n\n      - name: Run tests\n        run: make test\n\n\n  release:\n    needs: [test]\n    if: startsWith(github.ref, 'refs/tags/v')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout codebase\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.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",
    "content": "project_name: keyb\nversion: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - windows\n      - darwin\n    goarch:\n      - \"arm64\"\n      - \"amd64\"\n      - \"386\"\n    ldflags:\n      - \"-s -w -X main.version={{ .Tag }}\"\narchives:\n  - name_template: \"{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}\"\n    formats: tar.gz\n    format_overrides:\n      - goos: windows\n        formats: zip\nchecksum:\n  name_template: 'checksums.txt'\nchangelog:\n  disable: true\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\n[v0.2.0]: https://github.com/kencx/keyb/compare/v0.1.0...v0.2.0\n[v0.3.0]: https://github.com/kencx/keyb/compare/v0.2.0...v0.3.0\n[v0.4.0]: https://github.com/kencx/keyb/compare/v0.3.0...v0.4.0\n[v0.4.1]: https://github.com/kencx/keyb/compare/v0.4.0...v0.4.1\n[v0.5.0]: https://github.com/kencx/keyb/compare/v0.4.1...v0.5.0\n[v0.6.0]: https://github.com/kencx/keyb/compare/v0.5.0...v0.6.0\n[v0.7.0]: https://github.com/kencx/keyb/compare/v0.6.0...v0.7.0\n[v0.8.0]: https://github.com/kencx/keyb/compare/v0.7.0...v0.8.0\n\n## [v0.8.0]\n\n### Added\n- Add support for json config and keyb files\n- Add support to export keyb files to json and yaml\n\n### Changed\n- Update Go to v1.26.1\n- Update bubbles to v1.0.0\n- Update bubbletea to v1.3.10\n- Update lipgloss to v1.1.0\n- Bump goreleaser/goreleaser-action from 5 to 7\n- Bump actions/checkout from 4 to 6\n- Bump actions/setup-go from 4 to 6\n\n## [v0.7.0]\n\n### Fixed\n- Exiting now clears the output\n\n### Chore\n- Replace deprecated `--rm-dist` with `--clean` flag in Goreleaser action\n\n## [v0.6.0]\n\n### Changed\n- Bump goreleaser/goreleaser-action from 4 to 5 [#25](https://github.com/kencx/keyb/pull/25)\n- Simplify config package\n- Bump actions/checkout from 3 to 4 [#24](https://github.com/kencx/keyb/pull/24)\n- Update Go to v1.21\n- Update bubbletea to v0.24.2\n\n### Fixed\n- Fix environment variables not being expanded in the config's `settings.keyb_path`.\n\n## [v0.5.0]\n### Added\n- Add ability to customize search bar cursor bindings.\n\n### Changed\n- Default configuration file is no longer automatically generated. `keyb` will\n  default to reading any config file at `$XDG_CONFIG_HOME/keyb/config.yml`. If\n  not present, the default configuration is used.\n\n## [v0.4.1]\n### Fixed\n- Fix inability to delete characters in search bar [1385049](https://github.com/kencx/keyb/commit/138504964bad8f8827c5f5e9c1572298d4d5e102)\n\n## [v0.4.0]\n### BREAKING\n- Use `XDG_CONFIG_HOME` environment variable in macOS if set [#18](https://github.com/kencx/keyb/pull/18)\n\n### Added\n- Add ability to move cursor in search mode\n  [55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)\n- Add ability to customize cursor movement keys in search mode\n  [55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)\n\n### Changed\n- Update dependencies [#19](https://github.com/kencx/keyb/pull/19) and\n  [3cba5b8](https://github.com/kencx/keyb/commit/3cba5b801acd617e9d1c37734582f3f15d2ec41b)\n\n### Fixed\n- Fix search bar cursor not blinking when focused [#20](https://github.com/kencx/keyb/pull/20)\n\n## [v0.3.0]\n### Added\n- Add \"add\" subcommand to quick add keybinds [50b66d7](https://github.com/kencx/keyb/commit/50b66d7a78c4a08a9cb5ad5bd02d909b7b27ae53), [b9167474](https://github.com/kencx/keyb/commit/b9167474c9c5d12ed8ea0ca9630489fa7266bebe)\n\n## [v0.2.0]\n### Added\n- Add counter styling [#2](https://github.com/kencx/keyb/pull/2)\n- Add placeholder styling [#4](https://github.com/kencx/keyb/pull/4)\n- Add ability to customize keyb key bindings [43ae9b8](https://github.com/kencx/keyb/commit/43ae9b83fbf5cae367ab74614fa42fce79817165)\n- Add `sort_keys` option to sort keys alphabetically [#7](https://github.com/kencx/keyb/pull/7)\n- Add ability to customize prompt location [019f6ca](https://github.com/kencx/keyb/commit/019f6cad03ada6507e6585e4f4403826dcd23212)\n- Add `search_mode` option to start keyb in search mode [d6c53e1](https://github.com/kencx/keyb/commit/d6c53e1b908f05f6c0f7836068b4b6bbe1e8a451)\n\n<!-- ### Changed -->\n<!---->\n<!-- ### Removed -->\n<!---->\n<!-- ### Fixed -->\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 kencx\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "Makefile",
    "content": "binary = keyb\nversion = $(shell git describe --tags)\nldflags = -ldflags \"-s -w -X main.version=${version}\"\n\n.PHONY: help test clean snapshot build\n\ndefault: help\n\nhelp:\n\t@echo 'Usage:'\n\t@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'\n\n## test: run tests\ntest:\n\tgo test ./...\n\n## clean: remove binaries, dist\nclean:\n\tif [ -f ${binary} ]; then rm keyb; fi\n\tgo clean\n\trm -rf dist\n\n## snapshot: generate unversioned snapshot release\nsnapshot:\n\tgoreleaser release --snapshot\n\n## build: build binary\nbuild:\n\tgo build ${ldflags} -o ${binary}\n\n## install: install binary at ~/.local/bin\ninstall:\n\tcp ${binary} ~/.local/bin/\n"
  },
  {
    "path": "README.md",
    "content": "# keyb\n\n<p align=\"center\">\n\t<img width=\"660\" src=\"https://github.com/kencx/keyb/blob/master/assets/compressed.gif?raw=true\">\n</p>\n\n<p align=\"center\">Create and view your own custom hotkey cheatsheet in the terminal</p>\n\n<p align=\"center\">\n\t<img src=\"https://goreportcard.com/badge/github.com/kencx/keyb\">\n\t<img src=\"https://github.com/kencx/keyb/actions/workflows/test.yml/badge.svg?branch=master\">\n</p>\n\n### Features\n- Lightweight and quick\n- Fully customizable\n- Fuzzy filtering\n- Vim key bindings\n- Export to stdout for fzf, rofi support\n\n### Non-Features\nkeyb does **not** support:\n\n- Auto detection of hotkeys\n- Command selection\n\n## Motivation\n\nI had trouble remembering the various hotkeys that I sometimes use. It got\nannoying to look them up so I resorted to writing them down on a paper\ncheatsheet. Then, I thought: maybe there's a tool that does this better. I\ndidn't find one I liked so I built keyb.\n\nI also wrote a blog [post](https://blog.cheo.dev/posts/keyb) about building keyb.\n\nWith keyb, I can list:\n\n- Hotkeys that I occasionally forget or am new to\n- Custom key combinations that I defined for my own workflow\n- Short commands that I sometimes use\n\nIt is best used as a popup cheatsheet.\n\n## Install\nkeyb supports Linux, MacOS and Windows.\n\n### Compiled Binary\nDownload a compiled binary from the [releases](https://github.com/kencx/keyb/releases) page.\n\n### Install with Go\n\n```bash\n$ go install github.com/kencx/keyb@latest\n```\n\n### AUR\n\nPackages can be found in the AUR:\n- [keyb-bin](https://aur.archlinux.org/packages/keyb-bin)\n- [keyb](https://aur.archlinux.org/packages/keyb)\n\nThese are NOT maintained by me.\n\n### Build from source\n\n```bash\n$ git clone https://github.com/kencx/keyb\n$ cd keyb\n$ make build\n```\n\n## Usage\n\n```text\nusage: keyb [options] <command>\n\nOptions:\n  -p, --print     Print to stdout\n  -e, --export    Export to file [yaml, json]\n  -k, --key       Key bindings at custom path\n  -c, --config    Config file at custom path\n  -v, --version   Version info\n  -h, --help      help for keyb\n\nCommands:\n  a, add          Add keybind to keyb file\n```\n\n### Search\n\n- Enter search mode with `/` to perform fuzzy filtering on all rows\n- Exit search mode again with `Esc`\n- `Alt + d` clears the current filter\n\nTo perform filtering on section headings only, prefix the\nsearch with `h:`. This will return all matching section headings with their\nrespective rows.\n\n### Printing\n\nkeyb supports printing to stdout for use with other tools:\n\n```bash\n$ keyb -p | fzf\n$ keyb -p | rofi -dmenu\n```\n\n### keyb File\n\nkeyb requires a `yaml` or `json` file with a list of hotkeys to work. A default\n`yaml` file is generated in your system's config directory if no other file is\nspecified.\n\nHotkeys are classified into sections with a name and (optional) prefix field.\nWhen displayed, sections are sorted by alphabetical order while the keys\nthemselves are arranged in their defined order.\n\n```yaml\n- name: bspwm\n  keybinds:\n    - name: terminal\n      key: Super + Return\n```\n\nThe prefix is a key combination that will be prepended to every hotkey in the\nsection. A key can choose to opt out by including a `ignore_prefix: true` field.\nPrefixes are useful for applications with a common leading hotkey like tmux.\n\n```yaml\n- name: tmux\n  prefix: ctrl + b\n  keybinds:\n    - name: Create new window\n      key: c\n    - name: Prev, next window\n      key: Shift + {←, →}\n      ignore_prefix: true\n```\n\nRefer to the `examples` for more examples.\n\n>Multiline fields are not supported!\n\n### Quick Add\n\n```text\nusage: keyb [-k file] add [app; name; key]\n\nOptions:\n  -b, --binding  Key binding\n  -p, --prefix   Ignore prefix\n```\n\nYou can quick add bindings from the command line to a specified file. If `-k\nfile` is given and exists, the new keybind will be appended to the file.\nOtherwise, `keyb_path` defined in `config.yml` will be used.\n\n```bash\n$ keyb add -b \"kitty; open terminal; super + enter\"\n```\n\nWhen adding a new keybind, the app name, keybind name and keybind must be\nspecified. It is separated by `;` and wrapped in quotes (to prevent parsing errors).\n\n## Configuration\n\nkeyb can be customized with a config file at the default OS config\ndirectory (i.e. `$XDG_CONFIG_HOME/keyb/config.yml`). If no such file exists, the\ndefault configuration will be used.\n\nSee [config](examples/config/README.md) for all configuration options.\n\n### Missing Colors\n\nIf you're missing colors, a workaround is to add the environment variable `CLICOLOR_FORCE=1` to\n[force ANSI color support](https://pkg.go.dev/github.com/muesli/termenv#Output.EnvColorProfile).\n\n## Roadmap\n\n- [x] Ability to customize keyb hotkeys\n- [x] `a, add` subcommand to quickly add a single hotkey entry from the CLI\n- [ ] Export to additional file formats (`json, toml, conf/ini` etc.)\n- [ ] Support multiple keyb files or directories\n\n## Contributing\n\nContributing to keyb requires Go 1.26. Bug reports, feature requests and PRs are very welcome.\n\n## Similar Tools\n\n- [showkeys](https://github.com/adamharmansky/showkeys) offers a keybinding popup similar to awesomewm\n- [cheat](https://github.com/cheat/cheat) is a CLI alternative to view cheatsheets for\n  commands and hotkeys for just about any topic\n- Refer to [shortcut-pages](https://github.com/mt-empty/shortcut-pages), [cheat/cheatsheets](https://github.com/cheat/cheatsheets) for more cheatsheets\n\n## License\n[MIT](LICENSE)\n"
  },
  {
    "path": "config/config.go",
    "content": "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\tdefaultConfigDir  = \"keyb\"\n\tdefaultConfigFile = \"config.yml\"\n\tdefaultKeybFile   = \"keyb.yml\"\n)\n\ntype Config struct {\n\tSettings `yaml:\"settings\" json:\"settings\"`\n\tColor    `yaml:\"color\" json:\"color\"`\n\tKeys     `yaml:\"keys\" json:\"keys\"`\n}\n\ntype Settings struct {\n\tKeybPath       string `yaml:\"keyb_path\" json:\"keyb_path\"`\n\tDebug          bool\n\tReverse        bool\n\tMouse          bool\n\tSearchMode     bool `yaml:\"search_mode\" json:\"search_mode\"`\n\tSortKeys       bool `yaml:\"sort_keys\" json:\"sort_keys\"`\n\tTitle          string\n\tPrompt         string\n\tPromptLocation string `yaml:\"prompt_location\" json:\"prompt_location\"`\n\tPlaceholder    string\n\tPrefixSep      string `yaml:\"prefix_sep\" json:\"prefix_sep\"`\n\tSepWidth       int    `yaml:\"sep_width\" json:\"sep_width\"`\n\tMargin         int\n\tPadding        int\n\tBorderStyle    string `yaml:\"border\" json:\"border\"`\n}\n\ntype Color struct {\n\tPromptColor   string `yaml:\"prompt\" json:\"prompt\"`\n\tCursorFg      string `yaml:\"cursor_fg\" json:\"cursor_fg\"`\n\tCursorBg      string `yaml:\"cursor_bg\" json:\"cursor_bg\"`\n\tFilterFg      string `yaml:\"filter_fg\" json:\"filter_fg\"`\n\tFilterBg      string `yaml:\"filter_bg\" json:\"filter_bg\"`\n\tCounterFg     string `yaml:\"counter_fg\" json:\"counter_fg\"`\n\tCounterBg     string `yaml:\"counter_bg\" json:\"counter_bg\"`\n\tPlaceholderFg string `yaml:\"placeholder_fg\" json:\"placeholder_fg\"`\n\tPlaceholderBg string `yaml:\"placeholder_bg\" json:\"placeholder_bg\"`\n\tBorderColor   string `yaml:\"border_color\" json:\"border_color\"`\n}\n\ntype Keys struct {\n\tQuit                     string\n\tUp                       string\n\tDown                     string\n\tUpFocus                  string `yaml:\"up_focus\" json:\"up_focus\"`\n\tDownFocus                string `yaml:\"down_focus\" json:\"down_focus\"`\n\tHalfUp                   string `yaml:\"half_up\" json:\"half_up\"`\n\tHalfDown                 string `yaml:\"half_down\" json:\"half_down\"`\n\tFullUp                   string `yaml:\"full_up\" json:\"full_up\"`\n\tFullDown                 string `yaml:\"full_bottom\" json:\"full_bottom\"`\n\tGoToFirstLine            string `yaml:\"first_line\" json:\"first_line\"`\n\tGoToLastLine             string `yaml:\"last_line\" json:\"last_line\"`\n\tGoToTop                  string `yaml:\"top\" json:\"top\"`\n\tGoToMiddle               string `yaml:\"middle\" json:\"middle\"`\n\tGoToBottom               string `yaml:\"bottom\" json:\"bottom\"`\n\tSearch                   string\n\tClearSearch              string `yaml:\"clear_search\" json:\"clear_search\"`\n\tNormal                   string\n\tCursorWordForward        string `yaml:\"cursor_word_forward\" json:\"cursor_word_forward\"`\n\tCursorWordBackward       string `yaml:\"cursor_word_backward\" json:\"cursor_word_backward\"`\n\tCursorDeleteWordBackward string `yaml:\"cursor_delete_word_backward\" json:\"cursor_delete_word_backward\"`\n\tCursorDeleteWordForward  string `yaml:\"cursor_delete_word_forward\" json:\"cursor_delete_word_forward\"`\n\tCursorDeleteAfterCursor  string `yaml:\"cursor_delete_after_cursor\" json:\"cursor_delete_after_cursor\"`\n\tCursorDeleteBeforeCursor string `yaml:\"cursor_delete_before_cursor\" json:\"cursor_delete_before_cursor\"`\n\tCursorLineStart          string `yaml:\"cursor_line_start\" json:\"cursor_line_start\"`\n\tCursorLineEnd            string `yaml:\"cursor_line_end\" json:\"cursor_line_end\"`\n\tCursorPaste              string `yaml:\"cursor_paste\" json:\"cursor_paste\"`\n}\n\nvar DefaultConfig = &Config{\n\tSettings: Settings{\n\t\tDebug:          false,\n\t\tReverse:        false,\n\t\tMouse:          true,\n\t\tSearchMode:     false,\n\t\tSortKeys:       false,\n\t\tTitle:          \"\",\n\t\tPrompt:         \"keys > \",\n\t\tPromptLocation: \"top\",\n\t\tPlaceholder:    \"...\",\n\t\tPrefixSep:      \";\",\n\t\tSepWidth:       4,\n\t\tMargin:         0,\n\t\tPadding:        1,\n\t\tBorderStyle:    \"hidden\",\n\t},\n\tColor: Color{\n\t\tFilterFg: \"#FFA066\",\n\t},\n\tKeys: Keys{\n\t\tQuit:                     \"q, ctrl+c\",\n\t\tUp:                       \"k, up\",\n\t\tDown:                     \"j, down\",\n\t\tUpFocus:                  \"ctrl+k\",\n\t\tDownFocus:                \"ctrl+j\",\n\t\tHalfUp:                   \"ctrl+u\",\n\t\tHalfDown:                 \"ctrl+d\",\n\t\tFullUp:                   \"ctrl+b\",\n\t\tFullDown:                 \"ctrl+f\",\n\t\tGoToFirstLine:            \"g\",\n\t\tGoToLastLine:             \"G\",\n\t\tGoToTop:                  \"H\",\n\t\tGoToMiddle:               \"M\",\n\t\tGoToBottom:               \"L\",\n\t\tSearch:                   \"/\",\n\t\tClearSearch:              \"alt+d\",\n\t\tNormal:                   \"esc\",\n\t\tCursorWordForward:        \"alt+right, alt+f\",\n\t\tCursorWordBackward:       \"alt+left, alt+b\",\n\t\tCursorDeleteWordBackward: \"alt+backspace\",\n\t\tCursorDeleteWordForward:  \"alt+delete\",\n\t\tCursorDeleteAfterCursor:  \"alt+k\",\n\t\tCursorDeleteBeforeCursor: \"alt+u\",\n\t\tCursorLineStart:          \"home, ctrl+a\",\n\t\tCursorLineEnd:            \"end, ctrl+e\",\n\t\tCursorPaste:              \"ctrl+v\",\n\t},\n}\n\n// Read configuration and keyb file from flags, default path.\nfunc Parse(flagCPath, flagKPath string) (Apps, *Config, error) {\n\txdgConfigDir, err := getXDGConfigDir()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tbasePath := filepath.Join(xdgConfigDir, defaultConfigDir)\n\terr = os.MkdirAll(basePath, 0744)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create config dir: %w\", err)\n\t}\n\n\tconfig, err := UnmarshalConfig(flagCPath, basePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif flagKPath == \"\" {\n\t\tflagKPath = config.KeybPath\n\t}\n\n\tkeys, err := UnmarshalKeyb(flagKPath, basePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn keys, config, nil\n}\n\n// Read config file and merge with default config\nfunc UnmarshalConfig(configFile, basePath string) (*Config, error) {\n\n\t// set default config filepath\n\tif configFile == \"\" {\n\t\tconfigFile = filepath.Join(basePath, defaultConfigFile)\n\t}\n\n\tres := newDefaultConfig(basePath)\n\tconfigFile = os.ExpandEnv(configFile)\n\n\tfile, err := os.ReadFile(configFile)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn res, nil\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"failed to read config file \\\"%s\\\": %w\", configFile, err)\n\t\t}\n\t}\n\n\tswitch filepath.Ext(configFile) {\n\tcase \".json\":\n\t\tif err = json.Unmarshal(file, &res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal config file \\\"%s\\\": %w\", configFile, err)\n\t\t}\n\tcase \".yaml\", \".yml\":\n\t\tif err = yaml.Unmarshal(file, &res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal config file \\\"%s\\\": %w\", configFile, err)\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc newDefaultConfig(basePath string) *Config {\n\tres := DefaultConfig\n\tres.KeybPath = filepath.Join(basePath, defaultKeybFile)\n\treturn res\n}\n\n// Read keyb file or create default keyb file not exist\nfunc UnmarshalKeyb(keybFile, basePath string) (Apps, error) {\n\tif keybFile == \"\" {\n\t\tkeybFile = filepath.Join(basePath, defaultKeybFile)\n\t}\n\n\tkeybFile = os.ExpandEnv(keybFile)\n\tfile, err := os.ReadFile(keybFile)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\n\t\t\tk := newDefaultKeyb(keybFile)\n\t\t\tdata, err := yaml.Marshal(k)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to generate default keyb: %w\", err)\n\t\t\t}\n\n\t\t\tif err := os.WriteFile(keybFile, data, 0644); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create keyb file: %w\", err)\n\t\t\t}\n\t\t\treturn k, nil\n\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"failed to read keyb file: %w\", err)\n\t\t}\n\t}\n\n\tvar b Apps\n\tswitch filepath.Ext(keybFile) {\n\tcase \".json\":\n\t\tif err = json.Unmarshal(file, &b); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal keyb file: %w\", err)\n\t\t}\n\tcase \".yaml\", \".yml\":\n\t\tif err := yaml.Unmarshal(file, &b); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal keyb file: %w\", err)\n\t\t}\n\t}\n\treturn b, nil\n}\n\nfunc newDefaultKeyb(path string) Apps {\n\treturn Apps{{\n\t\tName: \"example\",\n\t\tKeybinds: []KeyBind{{\n\t\t\tName: \"add your keys in\",\n\t\t\tKey:  path,\n\t\t}},\n\t}}\n}\n\n// get user XDG_CONFIG_HOME directory\nfunc getXDGConfigDir() (string, error) {\n\tval, ok := os.LookupEnv(\"XDG_CONFIG_HOME\")\n\tif ok {\n\t\treturn val, nil\n\t}\n\n\tpath, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"user config directory not found: %w\", err)\n\t}\n\treturn path, nil\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nconst testBasePath = \"../testdata\"\n\nfunc TestUnmarshalConfig(t *testing.T) {\n\ttestConfig := &Config{\n\t\tSettings: Settings{\n\t\t\tKeybPath:       \"./custom.yml\",\n\t\t\tDebug:          true,\n\t\t\tReverse:        true,\n\t\t\tMouse:          false,\n\t\t\tSearchMode:     false,\n\t\t\tSortKeys:       true,\n\t\t\tTitle:          \"\",\n\t\t\tPrompt:         \"keys > \",\n\t\t\tPromptLocation: \"bottom\",\n\t\t\tPlaceholder:    \"...\",\n\t\t\tPrefixSep:      \";\",\n\t\t\tSepWidth:       4,\n\t\t\tMargin:         1,\n\t\t\tPadding:        1,\n\t\t\tBorderStyle:    \"normal\",\n\t\t},\n\t\tColor: Color{\n\t\t\tFilterFg: \"#FFA066\",\n\t\t},\n\t\tKeys: Keys{\n\t\t\tQuit:                     \"q, ctrl+c\",\n\t\t\tUp:                       \"k, up\",\n\t\t\tDown:                     \"j, down\",\n\t\t\tUpFocus:                  \"alt+k\",\n\t\t\tDownFocus:                \"alt+j\",\n\t\t\tHalfUp:                   \"ctrl+u\",\n\t\t\tHalfDown:                 \"ctrl+d\",\n\t\t\tFullUp:                   \"ctrl+b\",\n\t\t\tFullDown:                 \"ctrl+f\",\n\t\t\tGoToFirstLine:            \"g\",\n\t\t\tGoToLastLine:             \"G\",\n\t\t\tGoToTop:                  \"H\",\n\t\t\tGoToMiddle:               \"M\",\n\t\t\tGoToBottom:               \"L\",\n\t\t\tSearch:                   \"/\",\n\t\t\tClearSearch:              \"alt+d\",\n\t\t\tNormal:                   \"esc\",\n\t\t\tCursorWordForward:        \"alt+right, alt+f\",\n\t\t\tCursorWordBackward:       \"alt+left, alt+b\",\n\t\t\tCursorDeleteWordBackward: \"alt+backspace\",\n\t\t\tCursorDeleteWordForward:  \"alt+delete\",\n\t\t\tCursorDeleteAfterCursor:  \"alt+k\",\n\t\t\tCursorDeleteBeforeCursor: \"alt+u\",\n\t\t\tCursorLineStart:          \"home, ctrl+a\",\n\t\t\tCursorLineEnd:            \"end, ctrl+e\",\n\t\t\tCursorPaste:              \"ctrl+v\",\n\t\t},\n\t}\n\n\tconfigFileTests := []struct {\n\t\tname string\n\t\tfile string\n\t\twant *Config\n\t}{\n\t\t{\"full config yaml\", \"testConfig.yml\", testConfig},\n\t\t{\"full config json\", \"testConfig.json\", testConfig},\n\t\t{\"minimal config yaml\", \"testConfigMinimal.yml\", newDefaultConfig(testBasePath)},\n\t\t{\"minimal config json\", \"testConfigMinimal.json\", newDefaultConfig(testBasePath)},\n\t\t{\"config file absent\", \"testConfigAbsent.yml\", newDefaultConfig(testBasePath)},\n\t}\n\n\tfor _, tt := range configFileTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := UnmarshalConfig(filepath.Join(testBasePath, tt.file), testBasePath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"got %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"empty config file path\", func(t *testing.T) {\n\t\twant := newDefaultConfig(testBasePath)\n\t\tgot, err := UnmarshalConfig(\"\", testBasePath)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t\t}\n\n\t\tif !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"got %v, want %v\", got, want)\n\t\t}\n\t})\n}\n\nfunc TestUnmarshalKeyb(t *testing.T) {\n\tapps := Apps{{\n\t\tName: \"test\",\n\t\tKeybinds: []KeyBind{{\n\t\t\tName: \"foo\",\n\t\t\tKey:  \"bar\",\n\t\t}},\n\t}}\n\n\tkeybFileTests := []struct {\n\t\tname string\n\t\tfile string\n\t\twant Apps\n\t}{\n\t\t{\"keyb file yaml\", \"testkeyb.yml\", apps},\n\t\t{\"keyb file json\", \"testkeyb.json\", apps},\n\t}\n\n\tfor _, tt := range keybFileTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := UnmarshalKeyb(filepath.Join(testBasePath, tt.file), testBasePath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"got %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"file absent\", func(t *testing.T) {\n\t\t_, err := UnmarshalKeyb(filepath.Join(testBasePath, \"temp.yml\"), testBasePath)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t\t}\n\n\t\tt.Cleanup(func() {\n\t\t\terr := os.Remove(filepath.Join(testBasePath, \"temp.yml\"))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"empty filepath\", func(t *testing.T) {\n\t\t_, err := UnmarshalKeyb(\"\", testBasePath)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t\t}\n\n\t\tt.Cleanup(func() {\n\t\t\terr := os.Remove(filepath.Join(testBasePath, defaultKeybFile))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "config/model.go",
    "content": "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:\"prefix,omitempty\" json:\"prefix,omitempty\"`\n\tName     string    `yaml:\"name\" json:\"name\"`\n\tKeybinds []KeyBind `yaml:\"keybinds\" json:\"keybinds\"`\n}\n\ntype Apps []*App\n\nfunc (a App) String() string {\n\treturn fmt.Sprintf(\"App{name=%s,prefix=%s,keybinds=%v}\", a.Name, a.Prefix, a.Keybinds)\n}\n\ntype KeyBind struct {\n\tName string `yaml:\"name\" json:\"name\"`\n\tKey  string `yaml:\"key\" json:\"key\"`\n\n\t// ignore prefix defaults to false\n\t// so user can choose to ignore prefix for a specific kb\n\tIgnorePrefix bool `yaml:\"ignore_prefix,omitempty\" json:\"ignore_prefix,omitempty\"`\n}\n\nfunc AddEntry(path, binding string, kbIgnorePrefix bool) error {\n\txdgConfigDir, err := getXDGConfigDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// load existing struct from filepath\n\tapps, err := UnmarshalKeyb(path, xdgConfigDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif binding == \"\" {\n\t\treturn fmt.Errorf(\"binding must be given in format [app; name; keybind]\")\n\t}\n\n\ts := strings.Split(binding, \";\")\n\tif len(s) < 3 {\n\t\treturn fmt.Errorf(\"binding must be given in format [app; name; keybind]\")\n\t}\n\n\tappName := strings.TrimSpace(s[0])\n\tname := strings.TrimSpace(s[1])\n\tkey := strings.TrimSpace(s[2])\n\n\tapps.addOrUpdate(appName, name, key, kbIgnorePrefix)\n\n\t// rewrite file\n\tdata, err := yaml.Marshal(apps)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal entry: %w\", err)\n\t}\n\n\tpath = os.ExpandEnv(path)\n\tif err := os.WriteFile(path, data, 0644); err != nil {\n\t\treturn fmt.Errorf(\"failed to write keyb file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (apps *Apps) addOrUpdate(appName string, name, key string, ignorePrefix bool) {\n\tnewKeyBind := KeyBind{\n\t\tName:         name,\n\t\tKey:          key,\n\t\tIgnorePrefix: ignorePrefix,\n\t}\n\n\tif !apps.exist(appName) {\n\t\ta := App{\n\t\t\tName:     appName,\n\t\t\tKeybinds: []KeyBind{newKeyBind},\n\t\t}\n\t\t*apps = append(*apps, &a)\n\n\t} else {\n\t\tfor _, app := range *apps {\n\t\t\tif appName == app.Name {\n\t\t\t\tapp.Keybinds = append(app.Keybinds, newKeyBind)\n\t\t\t}\n\t\t}\n\t}\n\t// return apps\n}\n\nfunc (apps Apps) exist(appName string) bool {\n\tfor _, app := range apps {\n\t\tif appName == app.Name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "config/model_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestAddOrUpdate(t *testing.T) {\n\tt.Run(\"update existing\", func(t *testing.T) {\n\t\tapps := Apps{{\n\t\t\tName: \"test\",\n\t\t\tKeybinds: []KeyBind{{\n\t\t\t\tName: \"foo\",\n\t\t\t\tKey:  \"bar\",\n\t\t\t}},\n\t\t}}\n\t\twant := Apps{{\n\t\t\tName: \"test\",\n\t\t\tKeybinds: []KeyBind{{\n\t\t\t\tName: \"foo\",\n\t\t\t\tKey:  \"bar\",\n\t\t\t}, {\n\t\t\t\tName: \"addFoo\",\n\t\t\t\tKey:  \"addBar\",\n\t\t\t}},\n\t\t}}\n\t\tapps.addOrUpdate(\"test\", \"addFoo\", \"addBar\", false)\n\n\t\tif !reflect.DeepEqual(apps, want) {\n\t\t\tt.Errorf(\"got %v, want %v\", apps, want)\n\t\t}\n\t})\n\n\tt.Run(\"add new\", func(t *testing.T) {\n\t\tapps := Apps{{\n\t\t\tName: \"test\",\n\t\t\tKeybinds: []KeyBind{{\n\t\t\t\tName: \"foo\",\n\t\t\t\tKey:  \"bar\",\n\t\t\t}},\n\t\t}}\n\t\twant := Apps{\n\t\t\t{\n\t\t\t\tName: \"test\",\n\t\t\t\tKeybinds: []KeyBind{{\n\t\t\t\t\tName: \"foo\",\n\t\t\t\t\tKey:  \"bar\",\n\t\t\t\t}},\n\t\t\t}, {\n\t\t\t\tName: \"new\",\n\t\t\t\tKeybinds: []KeyBind{{\n\t\t\t\t\tName: \"addFoo\",\n\t\t\t\t\tKey:  \"addBar\",\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\t\tapps.addOrUpdate(\"new\", \"addFoo\", \"addBar\", false)\n\n\t\tif !reflect.DeepEqual(apps, want) {\n\t\t\tt.Errorf(\"got %v, want %v\", apps, want)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "examples/Helix.yml",
    "content": "- name: Helix Normal mode (Movement)\n  keybinds:\n    - name: \"Move left\"\n      key: \"h, Left\"\n    - name: \"Move down\"\n      key: \"j, Down\"\n    - name: \"Move up\"\n      key: \"k, Up\"\n    - name: \"Move right\"\n      key: \"l, Right\"\n    - name: \"Move next word start\"\n      key: \"w\"\n    - name: \"Move previous word start\"\n      key: \"b\"\n    - name: \"Move next word end\"\n      key: \"e\"\n    - name: \"Move next WORD end\"\n      key: \"W\"\n    - name: \"Move previous WORD start\"\n      key: \"B\"\n    - name: \"Move next WORD end\"\n      key: \"E\"\n    - name: \"Find 'till next char\"\n      key: \"t\"\n    - name: \"Find next char\"\n      key: \"f\"\n    - name: \"Find 'till previous char\"\n      key: \"T\"\n    - name: \"Find previous char\"\n      key: \"F\"\n    - name: \"Go to line number <n>\"\n      key: \"G\"\n    - name: \"Reapeat last motion (f, t ro m)\"\n      key: \"Alt-.\"\n    - name: \"Move to the start of the line\"\n      key: \"Home\"\n    - name: \"Move to the end ofthe line\"\n      key: \"End\"\n    - name: \"Move page up\"\n      key: \"Ctrl-b, PageUp\"\n    - name: \"Move page down\"\n      key: \"Ctrl-f, PageDown\"\n    - name: \"Move half page up\"\n      key: \"Ctrl-u\"\n    - name: \"Move half page down\"\n      key: \"Ctrf-d\"\n    - name: \"Jump forward on the jumplist\"\n      key: \"Ctrl-i\"\n    - name: \"Jump backward on the jumplist\"\n      key: \"Ctrl-o\"\n    - name: \"Save the current selection to the jumplist\"\n      key: \"Ctrl-s\"\n- name: Helix Normal mode (Changes)\n  keybinds:\n    - name: \"Replace with a character\"\n      key: \"r\"\n    - name: \"Replace with yanked text\"\n      key: \"R\"\n    - name: \"Switch case of the selected text\"\n      key: \"~\"\n    - name: \"Set the selected text to lower case\"\n      key: \"`\"\n    - name: \"Set the selected text to upper case\"\n      key: \"Alt-`\"\n    - name: \"Insert before selection\"\n      key: \"i\"\n    - name: \"Insert after selection (append)\"\n      key: \"a\"\n    - name: \"Insert at the start of the line\"\n      key: \"I\"\n    - name: \"Insert at the end of the line\"\n      key: \"A\"\n    - name: \"Open new line below selection\"\n      key: \"o\"\n    - name: \"Open new line above selection\"\n      key: \"O\"\n    - name: \"Repeat last change\"\n      key: \".\"\n    - name: \"Undo change\"\n      key: \"u\"\n    - name: \"Redo change\"\n      key: \"U\"\n    - name: \"Move backward in history\"\n      key: \"Alt-u\"\n    - name: \"Move forward in history\"\n      key: \"Alt-U\"\n    - name: \"Yank selection\"\n      key: \"y\"\n    - name: \"Paste after selection\"\n      key: \"p\"\n    - name: \"Paste before selection\"\n      key: \"P\"\n    - name: \"Select a register to yank to or paste from\"\n      key: \"<reg>\"\n    - name: \"Indent selection\"\n      key: \">\"\n    - name: \"Unindent selection\"\n      key: \"<\"\n    - name: \"Format selection (currently nonfunctional/disabled) (LSP)\"\n      key: \"=\"\n    - name: \"Delete selection\"\n      key: \"d\"\n    - name: \"Delete selection, without yanking\"\n      key: \"Alt-d\"\n    - name: \"Change selection (delete and enter insert mode)\"\n      key: \"c\"\n    - name: \"Change selection (delete and enter insert mode, without yanking)\"\n      key: \"Alt-c\"\n    - name: \"Increment object (number) under cursor\"\n      key: \"Ctrl-a\"\n    - name: \"Decrement object (number) under cursor\"\n      key: \"Ctrl-x\"\n    - name: \"Start/stop macro recording to the selected register (experimental)\"\n      key: \"Q\"\n    - name: \"Play back a recorded macro from the selected register (experimental)\"\n      key: \"q\"\n- name: Helix Normal mode (Changes Shell)\n  keybinds:\n    - name: \"Pipe each selection through shell command, replacing with output\"\n      key: \"|\"\n    - name: \"Pipe each selection into shell command, ignoring output\"\n      key: \"Alt-|\"\n    - name: \"Run shell command, inserting output before each selection\"\n      key: \"!\"\n    - name: \"Run shell command, appending output after each selection\"\n      key: \"Alt-!\"\n    - name: \"Pipe each selection into shell command, keep selections where command returned 0\"\n      key: \"$\"\n- name: Helix Normal mode (Selection manipulation)\n  keybinds:\n    - name: \"Select all regex matches inside selections\"\n      key: \"s\"\n    - name: \"Split selection into subselections on regex matches\"\n      key: \"S\"\n    - name: \"Split selection on newlines\"\n      key: \"Alt-s\"\n    - name: \"Align selection in columns\"\n      key: \"&\"\n    - name: \"Trim whitespace from the selection\"\n      key: \"_\"\n    - name: \"Collapse selection onto a single cursor\"\n      key: \";\"\n    - name: \"Flip selection cursor and anchor\"\n      key: \"Alt-;\"\n    - name: \"Ensures the selection is in forward direction\"\n      key: \"Alt-:\"\n    - name: \"Keep only the primary selection\"\n      key: \",\"\n    - name: \"Remove the primary selection\"\n      key: \"Alt-,\"\n    - name: \"Copy selection onto the next line (Add cursor below)\"\n      key: \"C\"\n    - name: \"Copy selection onto the previous line (Add cursor above)\"\n      key: \"Alt-C\"\n    - name: \"Rotate main selection backward\"\n      key: \"(\"\n    - name: \"Rotate main selection forward\"\n      key: \")\"\n    - name: \"Rotate selection contents backward\"\n      key: \"Alt-(\"\n    - name: \"Rotate selection contents forward\"\n      key: \"Alt-)\"\n    - name: \"Select entire file\"\n      key: \"%\"\n    - name: \"Select current line, if already selected, extend to next line\"\n      key: \"x\"\n    - name: \"Extend selection to line bounds (line-wise selection)\"\n      key: \"X\"\n    - name: \"Shrink selection to line bounds (line-wise selection)\"\n      key: \"Alt-x\"\n    - name: \"Join lines inside selection\"\n      key: \"J\"\n    - name: \"Join lines inside selection and select space\"\n      key: \"Alt-J\"\n    - name: \"Keep selections matching the regex\"\n      key: \"K\"\n    - name: \"Remove selections matching the regex\"\n      key: \"Alt-K\"\n    - name: \"Comment/uncomment the selections\"\n      key: \"Ctrl-c\"\n    - name: \"Expand selection to parent syntax node (TS)\"\n      key: \"Alt-o, Alt-up\"\n    - name: \"Shrink syntax tree object selection (TS)\"\n      key: \"Alt-i, Alt-down\"\n    - name: \"Select previous sibling node in syntax tree (TS)\"\n      key: \"Alt-p, Alt-left\"\n    - name: \"Select next sibling node in syntax tree (TS)\"\n      key: \"Alt-n, Alt-right\"\n- name: Helix Normal mode (Search)\n  keybinds:\n    - name: \"Search for regex pattern\"\n      key: \"/\"\n    - name: \"Search for previous pattern\"\n      key: \"?\"\n    - name: \"Select next search match\"\n      key: \"n\"\n    - name: \"Select previous search match\"\n      key: \"N\"\n    - name: \"Use current selection as the search pattern\"\n      key: \"*\"\n- name: Helix Minor modes\n  keybinds:\n    - name: \"Enter select (extend) mode\"\n      key: \"v\"\n    - name: \"Enter goto mode\"\n      key: \"g\"\n    - name: \"Enter match mode\"\n      key: \"m\"\n    - name: \"Enter command mode\"\n      key: \":\"\n    - name: \"Enter view mode\"\n      key: \"z\"\n    - name: \"Enter sticky view mode\"\n      key: \"Z\"\n    - name: \"Enter window mode\"\n      key: \"Ctrl-w\"\n    - name: \"Enter space mode\"\n      key: \"Space\"\n- name: Helix View mode\n  keybinds:\n    - name: \"Vertically center the line\"\n      key: \"z, c\"\n    - name: \"Align the line to the top of the screen\"\n      key: \"t\"\n    - name: \"Align the line to the bottom of the screen\"\n      key: \"b\"\n    - name: \"Align the line to the middle of the screen (horizontally)\"\n      key: \"m\"\n    - name: \"Scroll the view downwards\"\n      key: \"j,down\"\n    - name: \"Scroll the view upwards\"\n      key: \"k, up\"\n    - name: \"Move page down\"\n      key: \"Ctrl-f, PageDown\"\n    - name: \"Move page up\"\n      key: \"Ctrl-b, PageUp\"\n    - name: \"Move half page down\"\n      key: \"Ctrl-d\"\n    - name: \"Move half page up\"\n      key: \"Ctrl-u\"\n- name: Helix Goto mode\n  keybinds:\n    - name: \"Go to line number <n> else start of file\"\n      key: \"g\"\n    - name: \"Go to the end of the file\"\n      key: \"e\"\n    - name: \"Go to files in the selection\"\n      key: \"f\"\n    - name: \"Go to the start of the line\"\n      key: \"h\"\n    - name: \"Go to the end of the line\"\n      key: \"l\"\n    - name: \"Go to first non-whitespace character of the line\"\n      key: \"s\"\n    - name: \"Go to the top of the screen\"\n      key: \"t\"\n    - name: \"Go to the middle of the screen\"\n      key: \"c\"\n    - name: \"Go to the bottom of the screen\"\n      key: \"b\"\n    - name: \"Go to definition (LSP)\"\n      key: \"d\"\n    - name: \"Go to type definition (LSP)\"\n      key: \"y\"\n    - name: \"Go to references (LSP)\"\n      key: \"r\"\n    - name: \"Go to implementation (LSP)\"\n      key: \"i\"\n    - name: \"Go to the last accessed/alternate file\"\n      key: \"a\"\n    - name: \"Go to the last modified/alternate file\"\n      key: \"m\"\n    - name: \"Go to next buffer\"\n      key: \"n\"\n    - name: \"Go to previous buffer\"\n      key: \"p\"\n    - name: \"Go to last modification in current file\"\n      key: \".\"\n- name: Helix Match mode\n  keybinds:\n    - name: \"Goto matching bracket (TS)\"\n      key: \"m\"\n    - name: \"Surround current selection with <char>\"\n      key: \"s <char>\"\n    - name: \"Replace surround character <from> with <to>\"\n      key: \"r <from><to>\"\n    - name: \"Delete surround character <char>\"\n      key: \"d <char>\"\n    - name: \"Select around textobject\"\n      key: \"a <object>\"\n    - name: \"Select inside textobject\"\n      key: \"i <object>\"\n- name: Helix Window mode\n  keybinds:\n    - name: \"Switch to next window\"\n      key: \"w, Ctrl-w\"\n    - name: \"Vertical right split\"\n      key: \"v, Ctrl-v\"\n    - name: \"Horizontal bottom split\"\n      key: \"s, Ctrl-s\"\n    - name: \"Go to files in the selection in horizontal splits\"\n      key: \"f\"\n    - name: \"Go to files in the selection in vertical splits\"\n      key: \"F\"\n    - name: \"Move to left split\"\n      key: \"h, Ctrl-h, Left\"\n    - name: \"Move to split below\"\n      key: \"j, Ctrl-j, Down\"\n    - name: \"Move to split above\"\n      key: \"k, Ctrl-k, Up\"\n    - name: \"Move to right split\"\n      key: \"l, Ctrl-l, Right\"\n    - name: \"Close current window\"\n      key: \"q, Ctrl-q\"\n    - name: \"Only keep the current window, closing all the others\"\n      key: \"o, Ctrl-o\"\n    - name: \"Swap window to the left\"\n      key: \"H\"\n    - name: \"Swap window downwards\"\n      key: \"J\"\n    - name: \"Swap window upwards\"\n      key: \"K\"\n    - name: \"Swap window to the right\"\n      key: \"L\"\n- name: Helix Space mode \n  keybinds:\n    - name: \"Open file picker\"\n      key: \"f\"\n    - name: \"Open file picker at current working directory\"\n      key: \"F\"\n    - name: \"Open buffer picker\"\n      key: \"b\"\n    - name: \"Open jumplist picker\"\n      key: \"j\"\n    - name: \"Show documentation for item under cursor in a popup (LSP)\"\n      key: \"k\"\n    - name: \"Open document symbol picker (LSP)\"\n      key: \"s\"\n    - name: \"Open workspace symbol picker (LSP)\"\n      key: \"S\"\n    - name: \"Open document diagnostics picker (LSP)\"\n      key: \"d\"\n    - name: \"Open workspace diagnostics picker (LSP)\"\n      key: \"D\"\n    - name: \"Rename symbol (LSP)\"\n      key: \"r\" \n    - name: \"Apply code action (LSP)\"\n      key: \"a\"\n    - name: \"Open last fuzzy picker\"\n      key: \"'\"\n    - name: \"Enter window mode\"\n      key: \"w\"\n    - name: \"Paste system clipboard after selections\"\n      key: \"p\"\n    - name: \"Paste system clipboard before selections\"\n      key: \"P\"\n    - name: \"Join and yank selections to clipboard\"\n      key: \"y\"\n    - name: \"Yank main selection to clipboard\"\n      key: \"Y\"\n    - name: \"Replace selections by clipboard contents\"\n      key: \"R\"\n    - name: \"Global search in workspace folder\"\n      key: \"/\"\n    - name: \"Open command palette\"\n      key: \"?\"\n- name: Helix Space mode (Popup)\n  keybinds:\n    - name: \"Scroll up\"\n      key: \"Ctrl-u\"\n    - name: \"Scroll down\"\n      key: \"Ctrl-d\"\n      \n- name: Helix Unimpaired\n  keybinds:\n    - name: \"Go to previous diagnostic (LSP)\"\n      key: \"[d\"\n    - name: \"Go to next diagnostic (LSP)\"\n      key: \"]d\"\n    - name: \"Go to first diagnostic in document (LSP)\"\n      key: \"[D\"\n    - name: \"Go to last diagnostic in document (LSP)\"\n      key: \"]D\"\n    - name: \"Go to next function (TS)\"\n      key: \"]f\"\n    - name: \"Go to previous function (TS)\"\n      key: \"[f\"\n    - name: \"Go to next type definition (TS)\"\n      key: \"]t\"\n    - name: \"Go to previous type definition (TS)\"\n      key: \"[t\"\n    - name: \"Go to next argument/parameter (TS)\"\n      key: \"]a\"\n    - name: \"Go to previous argument/parameter (TS)\"\n      key: \"[a\"\n    - name: \"Go to next comment (TS)\"\n      key: \"]c\"\n    - name: \"Go to previous comment (TS)\"\n      key: \"[c\"\n    - name: \"Go to next test (TS)\"\n      key: \"]T\"\n    - name: \"Go to previous test (TS)\"\n      key: \"[T\"\n    - name: \"Go to next paragraph\"\n      key: \"]p\"\n    - name: \"Go to previous paragraph\"\n      key: \"[p\"\n    - name: \"Go to next change\"\n      key: \"]g\"\n    - name: \"Go to previous change\"\n      key: \"[g\"\n    - name: \"Go to first change\"\n      key: \"]G\"\n    - name: \"Go to last change\"\n      key: \"[G\"\n    - name: \"Add newline above\"\n      key: \"[Space\"\n    - name: \"Add newline below\"\n      key: \"]Space\"\n- name: Helix Insert mode\n  keybinds:\n    - name: \"Switch to normal mode\"\n      key: \"Escape\"\n    - name: \"Commit undo checkpoint\"\n      key: \"Ctrl-s\"     \n    - name: \"Autocomplete\"\n      key: \"Ctrl-x\"\n    - name: \"Insert a register content\"\n      key: \"Ctrl-r\"\n    - name: \"Delete previous word\"\n      key: \"Ctrl-w, Alt-Backspace\"\n    - name: \"Delete next word\"\n      key: \"Alt-d, Alt-Delete\"\n    - name: \"Delete to start of line\"\n      key: \"Ctrl-u\"\n    - name: \"Delete to end of line\"\n      key: \"Ctrl-k\"\n    - name: \"Insert new line\"\n      key: \"Ctrl-j, Enter\"\n    - name: \"Delete previous char\"\n      key: \"Backspace, Ctrl-h\"\n    - name: \"Delete previous char\"\n      key: \"Delete, Ctrl-d\"\n    - name: \"Move to previous line\"\n      key: \"Up\"\n    - name: \"Move to next line\"\n      key: \"Down\"\n    - name: \"Backward a char\"\n      key: \"Left\"\n    - name: \"Forward a char\"\n      key: \"Right\"\n    - name: \"Move to line end\"\n      key: \"End\"\n    - name: \"Move to line start\"\n      key: \"Home\"\n    - name: \"Move one page up\"\n      key: \"PageUp\"\n    - name: \"Move one page down\"\n      key: \"PageDown\"\n- name: Helix Picker\n  keybinds:\n    - name: \"Next entry\"\n      key: \"Tab, Down, Ctrl-n\"\n    - name: \"Previous entry\"\n      key: \"Shift-Tab, Up, Ctrl-p\"\n    - name: \"Page up\"\n      key: \"PageUp, Ctrl-u\"\n    - name: \"Page down\"\n      key: \"PageDown, Ctrl-d\"\n    - name: \"Go to first entry\"\n      key: \"Home\"\n    - name: \"Go to last entry\"\n      key: \"End\"\n    - name: \"Filter options\"\n      key: \"Ctrl-space\"\n    - name: \"Open selected\"\n      key: \"Enter\"\n    - name: \"Open horizontally\"\n      key: \"Ctrl-s\"\n    - name: \"Open vertically\"\n      key: \"Ctrl-v\"\n    - name: \"Close picker\"\n      key: \"Escape, Ctrl-c\"\n- name: Helix Prompt\n  keybinds:\n    - name: \"Close prompt\"\n      key: \"Escape, Ctrl-c\"\n    - name: \"Backward a word\"\n      key: \"Alt-b, Alt-Left\"\n    - name: \"Backward a char\"\n      key: \"Ctrl-b, Left\"\n    - name: \"Forward a word\"\n      key: \"Alt-f, Ctrl-Right\"\n    - name: \"Forward a char\"\n      key: \"Ctrl-f, Right\"\n    - name: \"Move prompt end\"\n      key: \"Ctrl-e, End\"\n    - name: \"Move prompt start\"\n      key: \"Ctrl-a, Home\"\n    - name: \"Delete previous word\"\n      key: \"Ctrl-w, Alt-Backspace, Ctrl-Backspace\"\n    - name: \"Delete next word\"\n      key: \"Alt-d, Alt-Delete, Ctrl-Delete\"\n    - name: \"Delete to start of line\"\n      key: \"Ctrl-u\"\n    - name: \"Delete to end of line\"\n      key: \"Ctrl-k\"\n    - name: \"Delete previous char\"\n      key: \"backspace, Ctrl-h\"\n    - name: \"Delete next char\"\n      key: \"delete, Ctrl-d\"\n    - name: \"Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later\"\n      key: \"Ctrl-s\"\n    - name: \"Select previous history\"\n      key: \"Ctrl-p, Up\"\n    - name: \"Select next history\"\n      key: \"Ctrl-n, Down\"\n    - name: \"Insert the content of the register selected by following input char\"\n      key: \"Ctrl-r\"\n    - name: \"Select next completion item\"\n      key: \"Tab\"\n    - name: \"Select previous completion item\"\n      key: \"BackTab\"\n    - name: \"Open selected\"\n      key: \"Enter\"\n"
  },
  {
    "path": "examples/ansi.yml",
    "content": "\n# https://github.com/cheat/cheatsheets\n- name: ansi\n  keybinds:\n    - name: 'Text Reset'\n      key: '\\e[0m'\n    - name: 'Black'\n      key: '\\e[0;30m'\n    - name: 'Red'\n      key: '\\e[0;31m'\n    - name: 'Green'\n      key: '\\e[0;32m'\n    - name: 'Yellow'\n      key: '\\e[0;33m'\n    - name: 'Blue'\n      key: '\\e[0;34m'\n    - name: 'Purple'\n      key: '\\e[0;35m'\n    - name: 'Cyan'\n      key: '\\e[0;36m'\n    - name: 'White'\n      key: '\\e[0;37m'\n    - name: 'Bold Black'\n      key: '\\e[1;30m'\n    - name: 'Bold Red'\n      key: '\\e[1;31m'\n    - name: 'Bold Green'\n      key: '\\e[1;32m'\n    - name: 'Bold Yellow'\n      key: '\\e[1;33m'\n    - name: 'Bold Blue'\n      key: '\\e[1;34m'\n    - name: 'Bold Purple'\n      key: '\\e[1;35m'\n    - name: 'Bold Cyan'\n      key: '\\e[1;36m'\n    - name: 'Bold White'\n      key: '\\e[1;37m'\n    - name: 'Underline Black'\n      key: '\\e[4;30m'\n    - name: 'Underline Red'\n      key: '\\e[4;31m'\n    - name: 'Underline Green'\n      key: '\\e[4;32m'\n    - name: 'Underline Yellow'\n      key: '\\e[4;33m'\n    - name: 'Underline Blue'\n      key: '\\e[4;34m'\n    - name: 'Underline Purple'\n      key: '\\e[4;35m'\n    - name: 'Underline Cyan'\n      key: '\\e[4;36m'\n    - name: 'Underline White'\n      key: '\\e[4;37m'\n    - name: 'Background Black'\n      key: '\\e[40m'\n    - name: 'Background Red'\n      key: '\\e[41m'\n    - name: 'Background Green'\n      key: '\\e[42m'\n    - name: 'Background Yellow'\n      key: '\\e[43m'\n    - name: 'Background Blue'\n      key: '\\e[44m'\n    - name: 'Background Purple'\n      key: '\\e[45m'\n    - name: 'Background Cyan'\n      key: '\\e[46m'\n    - name: 'Background White'\n      key: '\\e[47m'\n    - name: 'High Intensity Black'\n      key: '\\e[0;90m'\n    - name: 'High Intensity Red'\n      key: '\\e[0;91m'\n    - name: 'High Intensity Green'\n      key: '\\e[0;92m'\n    - name: 'High Intensity Yellow'\n      key: '\\e[0;93m'\n    - name: 'High Intensity Blue'\n      key: '\\e[0;94m'\n    - name: 'High Intensity Purple'\n      key: '\\e[0;95m'\n    - name: 'High Intensity Cyan'\n      key: '\\e[0;96m'\n    - name: 'High Intensity White'\n      key: '\\e[0;97m'\n    - name: 'Bold High Intensity Black'\n      key: '\\e[1;90m'\n    - name: 'Bold High Intensity Red'\n      key: '\\e[1;91m'\n    - name: 'Bold High Intensity Green'\n      key: '\\e[1;92m'\n    - name: 'Bold High Intensity Yellow'\n      key: '\\e[1;93m'\n    - name: 'Bold High Intensity Blue'\n      key: '\\e[1;94m'\n    - name: 'Bold High Intensity Purple'\n      key: '\\e[1;95m'\n    - name: 'Bold High Intensity Cyan'\n      key: '\\e[1;96m'\n    - name: 'Bold High Intensity White'\n      key: '\\e[1;97m'\n    - name: 'High Intensity backgrounds Black'\n      key: '\\e[0;100m'\n    - name: 'High Intensity backgrounds Red'\n      key: '\\e[0;101m'\n    - name: 'High Intensity backgrounds Green'\n      key: '\\e[0;102m'\n    - name: 'High Intensity backgrounds Yellow'\n      key: '\\e[0;103m'\n    - name: 'High Intensity backgrounds Blue'\n      key: '\\e[0;104m'\n    - name: 'High Intensity backgrounds Purple'\n      key: '\\e[0;105m'\n    - name: 'High Intensity backgrounds Cyan'\n      key: '\\e[0;106m'\n    - name: 'High Intensity backgrounds White'\n      key: '\\e[0;107m'\n"
  },
  {
    "path": "examples/awesomewm.yml",
    "content": "# Awesome WM Documentation\n# source: https://awesomewm.org/doc/manpages/awesome.1.html\n\n- name: awesomewm (window manager control)\n  keybinds:\n    - name: \"Restart awesome\"\n      key: \"[Mod4] [Ctrl] [r]\"\n    - name: \"Quit awesome\"\n      key: \"[Mod4] [Shift] [q]\"\n    - name: \"Run prompt\"\n      key: \"[Mod4] [r]\"\n    - name: \"Run Lua code prompt\"\n      key: \"[Mod4] [x]\"\n    - name: \"Start terminal emulator\"\n      key: \"[Mod4] [Return]\"\n    - name: \"Open main menu\"\n      key: \"[Mod4] [w]\"\n\n- name: awesomewm (clients)\n  keybinds:\n    - name: \"Maximize client\"\n      key: \"[Mod4] [m]\"\n    - name: \"Minimize client\"\n      key: \"[Mod4] [n]\"\n    - name: \"Restore client\"\n      key: \"[Mod4] [Ctrl] [n]\"\n    - name: \"Set client fullscreen\"\n      key: \"[Mod4] [f]\"\n    - name: \"Kill focused client\"\n      key: \"[Mod4] [Shift] [c]\"\n    - name: \"Set client on-top\"\n      key: \"[Mod4] [t]\"\n\n- name: awesomewm (mouse)\n  keybinds:\n    - name: \"View tag\"\n      key: \"[Button1] on tag name\"\n    - name: \"Switch to previous tag\"\n      key: \"[Button4] on tag name\"\n    - name: \"Switch to next tag\"\n      key: \"[Button5] on tag name\"\n    - name: \"Switch to previous tag\"\n      key: \"[Button4] on root window\"\n    - name: \"Switch to next/previous layout\"\n      key: \"([Button1] / [Button3]) / ([Button4] / [Button5]) on layout symbol\"\n    - name: \"Tag client with this tag\"\n      key: \"[Mod4] [Button1] on tag\"\n    - name: \"Move window\"\n      key: \"[Mod4] [Button1] on client\"\n    - name: \"Toggle this tag for client\"\n      key: \"[Mod4] [Button3] on tag\"\n    - name: \"Resize window\"\n      key: \"[Mod4] [Button3] on client\"\n    - name: \"Add tag to current view\"\n      key: \"[Button3] clicked on tag\"\n\n- name: awesomewm (navigation)\n  keybinds:\n    - name: \"Focus next client\"\n      key: \"[Mod4] [j]\"\n    - name: \"Focus previous client\"\n      key: \"[Mod4] [k]\"\n    - name: \"Focus first urgent client\"\n      key: \"[Mod4] [u]\"\n    - name: \"View previous tag\"\n      key: \"[Mod4] [Left]\"\n    - name: \"View next tag\"\n      key: \"[Mod4] [Right]\"\n    - name: \"Switch to tag [1~9]\"\n      key: \"[Mod4] [1~9]\"\n    - name: \"Focus next screen\"\n      key: \"[Mod4] [Ctrl] [j]\"\n    - name: \"Focus previous screen\"\n      key: \"[Mod4] [Ctrl] [k]\"\n    - name: \"Focus previously selected tag set\"\n      key: \"[Mod4] [Esc]\"\n\n- name: awesomewm (layout modification)\n  keybinds:\n    - name: \"Switch client with next client\"\n      key: \"[Mod4] [Shift] [j]\"\n    - name: \"Switch client with previous client\"\n      key: \"[Mod4] [Shift] [k]\"\n    - name: \"Send client to next screen\"\n      key: \"[Mod4] [o]\"\n    - name: \"Decrease master width factor by 5%\"\n      key: \"[Mod4] [h]\"\n    - name: \"Increase master width factor by 5%\"\n      key: \"[Mod4] [l]\"\n    - name: \"Increase number of master windows by 1\"\n      key: \"[Mod4] [Shift] [h]\"\n    - name: \"Decrease number of master windows by 1\"\n      key: \"[Mod4] [Shift] [l]\"\n    - name: \"Increase number of columns for non-master windows by 1\"\n      key: \"[Mod4] [Ctrl] [h]\"\n    - name: \"Decrease number of columns for non-master windows by 1\"\n      key: \"[Mod4] [Ctrl] [l]\"\n    - name: \"Next Layout\"\n      key: \"[Mod4] [Space]\"\n    - name: \"Previous Layout\"\n      key: \"[Mod4] [Shift] [Space]\"\n    - name: \"Floating master\"\n      key: \"[Mod4] [Ctrl] [Space]\"\n    - name: \"Swap focused client with master\"\n      key: \"[Mod4] [Ctrl] [Return]\"\n    - name: \"Toggle tag view\"\n      key: \"[Mod4] [Ctrl] [1~9]\"\n    - name: \"Tag client with tag\"\n      key: \"[Mod4] [Shift] [1~9]\"\n    - name: \"Toggle tag on client\"\n      key: \"[Mod4] [Shift] [Ctrl] [1~9]\"\n"
  },
  {
    "path": "examples/bspwm.yml",
    "content": "# bspwm/sxhkd\n# source: https://github.com/baskerville/bspwm/blob/master/examples/sxhkdrc\n\n- name: bspwm\n  keybinds:\n    - name: terminal\n      key: super + Return\n    - name: program launcher\n      key: super + space\n    - name: reload sxhkd\n      key: super + Escape\n    - name: quit/escape bspwm\n      key: super + alt + {q,r}\n\n    - name: toggle tiled and monocle layout\n      key: super + m\n    - name: swap current node and biggest window\n      key: super + g\n    - name: send newest node to newest preselection\n      key: super + y\n\n    - name: set tiled state\n      key: super + t\n    - name: set pseudo tiled state\n      key: super + shift + t\n    - name: set floating state\n      key: super + s\n    - name: set fullscreen state\n      key: super + f\n    - name: set node to {marked, locked, sticky, private}\n      key: super + ctrl + {m, x, y, z}\n\n    - name: focus node in direction\n      key: super + h,j,k,l\n    - name: focus {parent, brother, first, second} node\n      key: super + p, b, comma, period\n    - name: focus {next, prev} window in current desktop\n      key: super + {_, shift +} c\n    - name: focus {next, prev} desktop\n      key: super + bracket{left, right}\n    - name: focus last node/desktop\n      key: super + {grave, tab}\n    - name: focus {older, newer} node in history\n      key: super + {o, i}\n    - name: focus or send to desktop\n      key: super + {_, shift +} 1-9,0\n\n    - name: preselect direction\n      key: super + ctrl + {h,j,k,l}\n    - name: preselect ratio\n      key: super + ctrl + {1-9}\n    - name: cancel preselection for focused node\n      key: super + ctrl + space\n    - name: cancel preselection for focused desktop\n      key: super + ctrl + shift + space\n\n    - name: expand window\n      key: super + alt + {h,j,k,l}\n    - name: contract window\n      key: super + alt + shift + {h,j,k,l}\n    - name: move floating window\n      key: super + {Left,Down,Up,Right}\n"
  },
  {
    "path": "examples/changefile",
    "content": "#!/usr/bin/env bash\n# A raw script to convert a cheat file from\n# https://github.com/cheat/cheatsheets\\\n# to a markdown file for keyb cheat sheets.\nFILENAME=${1//.yml/}\nsed -E -i 's/^[-]//g' \"$1\"\nsed -E -i '2s/^//' \"$1\"\nsed -E -i '/./!d' \"$1\"\nsed -E -i 's/\"//g' \"$1\"\nsed -E -i 's/^# (.*)/\\ \\ - name: \"\\1\"/' \"$1\"\nsed -E -i 's/^([a-zA-Z].*)/\\ \\ \\ \\ key: \"\\1\"/' \"$1\"\nsed -E -i \"1s|^|# https://github.com/cheat/cheatsheets\\n|\" \"$1\"\nsed -E -i \"2s/^/- name: $FILENAME\\n/\" \"$1\"\nsed -E -i \"3s/^/\\ \\ keybinds:\\n/\" \"$1\"\n"
  },
  {
    "path": "examples/config/README.md",
    "content": "# Configuration\n\nkeyb will accept the following config in decreasing priority:\n\n- `-c FILE` flag\n- The default config path `$XDG_CONFIG_HOME/keyb/config.yml` (see note)\n- The default configuration (see [default.yml](default.yml))\n\nNote: If `$XDG_CONFIG_HOME` is set, it will be prioritized and used in Unix and Darwin\nsystems. Otherwise, keyb will fall back to the default OS config directory\ndefined as such:\n\n- Unix: `$XDG_CONFIG_HOME/keyb/`,\n- MacOS/Darwin: `$HOME/Library/Application Support/keyb/`,\n- Windows: `%Appdata%\\keyb\\`\n\n**Note**: `*.json` files are also supported.\n\n## Options\n\n| Option        | Default                  | Description |\n| ------------- | ------------------------ | ----------- |\n| `keyb_path`   | OS-dependent (see above) | keyb file path |\n| `debug`       | `false`                  | Debug mode |\n| `reverse`     | `false`                  | Swap the name and key columns |\n| `mouse`       | `true`                   | Mouse enabled |\n| `search_mode` | `false`                  | Start in search mode |\n| `sort_keys`   | `false`                  | Sort keys alphabetically |\n| `title`       | `\"\"`                     | Title text |\n| `prompt`      | `\"keys > \"`              | Search bar prompt text |\n| `prompt_location` | `\"top\"`                | Location of search bar: `top, bottom` |\n| `placeholder` | `\"...\"`                  | Search bar placeholder text |\n| `prefix_sep`  | `\";\"`                    | Separator symbol between prefix and key |\n| `sep_width`   | `4`                      | Separation width between columns |\n| `margin`      | `0`                      | Space between window and border |\n| `padding`     | `1`                      | Space between border and text |\n| `border`      | `\"hidden\"`               | Border style: `normal, rounded, double, thick, hidden`|\n\n### Color\nBoth ANSI and hex color codes are supported.\n\n| Color Option     | Default    | Description |\n| ---------------- | ---------- | ----------- |\n| `prompt`         | -          | Prompt text color |\n| `cursor_fg`      | -          | Cursor foreground |\n| `cursor_bg`      | -          | Cursor background |\n| `filter_fg`      | `\"#FFA066\"`| Filter matching text foreground |\n| `filter_bg`      | -          | Filter matching text background |\n| `counter_fg`     | -          | Counter foreground |\n| `counter_bg`     | -          | Counter background |\n| `placeholder_fg` | -          | Placeholder foreground |\n| `placeholder_bg` | -          | Placeholder background |\n| `border_color`   | -          | Border color |\n\nIf you are missing colors, see [Missing Colors](../../README.md#missing-colors).\n\n### Hotkeys\nMultiple keys may be set for a single binding, separated by commas.\n\n| Hotkey                  | Default                    | Description      |\n| ----------------------- | -------------------------- | ---------------- |\n| `up`, `down`            | <kbd>j, k / Up, Down</kbd> | Move cursor      |\n| `up_focus`, `down_focus`| <kbd>Ctrl + j, ctrl + k </kbd> | Move cursor in search mode |\n| `half_up, half_down`    | <kbd>Ctrl + u, d</kbd>     | Move half window (also works in search mode) |\n| `full_up, full_down`    | <kbd>Ctrl + b, f</kbd>     | Move full window (also works in search mode) |\n| `top, middle, bottom`   | <kbd>H, M, L</kbd>         | Go to top, middle, bottom of screen |\n| `first_line, last_line` | <kbd>g, G</kbd>            | Go to first, last line |\n| `search`                | <kbd>/</kbd>               | Enter search mode      |\n| `clear_search`          | <kbd>Alt + d</kbd>         | Clear current search (remains in search mode) |\n| `normal`                | <kbd>Esc</kbd>             | Exit search mode |\n| `quit`                  | <kbd>Ctrl + c, q</kbd>     | Quit\t\t      |\n\nThese hotkeys configure the cursor behaviour in the search bar only:\n\n| Hotkey                  | Default                     | Description      |\n| ----------------------- | --------------------------- | ---------------- |\n| `cursor_word_forward`     | <kbd>alt+right, alt+f</kbd> | Move forward by word |\n| `cursor_word_backward`    | <kbd>alt+left, alt+b</kbd>  | Move backward by word |\n| `cursor_delete_word_backward` | <kbd>alt+backspace</kbd> | Delete word backward |\n| `cursor_delete_word_forward`  | <kbd>alt+delete</kbd>   | Delete word forward |\n| `cursor_delete_after_cursor`  | <kbd>alt+k</kbd>        | Delete after cursor |\n| `cursor_delete_before_cursor` | <kbd>alt+u</kbd>        | Delete before cursor |\n| `cursor_line_start`       | <kbd>home, ctrl+a</kbd>     | Move cursor to start |\n| `cursor_line_end`         | <kbd>end, ctrl+e</kbd>      | Move cursor to end |\n| `cursor_paste`            | <kbd>ctrl+v</kbd>           | Paste into search bar|\n"
  },
  {
    "path": "examples/config/default.yml",
    "content": "settings:\n  keyb_path: \"$HOME/.config/keyb/keyb.yml\"\n  debug: false\n  reverse: false\n  mouse: true\n  search_mode: false\n  sort_keys: false\n  title: \"\"\n  prompt: 'keys > '\n  prompt_location: \"top\"\n  placeholder: '...'\n  prefix_sep: ;\n  sep_width: 4\n  margin: 0\n  padding: 1\n  border: hidden\ncolor:\n  prompt: \"\"\n  cursor_fg: \"\"\n  cursor_bg: \"\"\n  filter_fg: \"#FFA066\"\n  filter_bg: \"\"\n  border_color: \"\"\nkeys:\n  quit: q, ctrl+c\n  up: k, up\n  down: j, down\n  up_focus: ctrl+k\n  down_focus: ctrl+j\n  half_up: ctrl+u\n  half_down: ctrl+d\n  full_up: ctrl+b\n  full_bottom: ctrl+f\n  first_line: g\n  last_line: G\n  top: H\n  middle: M\n  bottom: L\n  search: /\n  clear_search: alt+d\n  normal: esc\n  cursor_word_forward: \"alt+right, alt+f\"\n  cursor_word_backward: \"alt+left, alt+b\"\n  cursor_delete_word_backward: \"alt+backspace\"\n  cursor_delete_word_forward: \"alt+delete\"\n  cursor_delete_after_cursor: \"alt+k\"\n  cursor_delete_before_cursor: \"alt+u\"\n  cursor_line_start: \"home, ctrl+a\"\n  cursor_line_end: \"end, ctrl+e\"\n  cursor_paste: \"ctrl+v\"\n"
  },
  {
    "path": "examples/curl.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: curl\n  keybinds:\n    - name: \"To download a file\"\n      key: \"curl <url>\"\n    - name: \"To download and rename a file\"\n      key: \"curl <url> -o <outfile>\"\n    - name: \"To download multiple files\"\n      key: \"curl -O <url> -O <url>\"\n    - name: \"To download all sequentially numbered files (1-24)\"\n      key: \"curl http://example.com/pic[1-24].jpg\"\n    - name: \"To download a file and pass HTTP authentication\"\n      key: \"curl -u <username>:<password> <url>\"\n    - name: \"To download a file with a proxy\"\n      key: \"curl -x <proxy-host>:<port> <url>\"\n    - name: \"To download a file over FTP\"\n      key: \"curl -u <username>:<password> -O ftp://example.com/pub/file.zip\"\n    - name: \"To get an FTP directory listing\"\n      key: \"curl ftp://username:password@example.com\"\n    - name: \"To resume a previously failed download\"\n      key: \"curl -C - -o <partial-file> <url>\"\n    - name: \"To fetch only the HTTP headers from a response\"\n      key: \"curl -I <url>\"\n    - name: \"To fetch your external IP and network info as JSON\"\n      key: \"curl http://ifconfig.me/all.json\"\n    - name: \"To limit the rate of a download\"\n      key: \"curl --limit-rate 1000B -O <outfile>\"\n    - name: \"To get your global IP\"\n      key: \"curl httpbin.org/ip \"\n    - name: \"To get only the HTTP status code\"\n      key: \"curl -o /dev/null -w '%{http_code}\\n' -s -I URL\"\n"
  },
  {
    "path": "examples/cut.yml",
    "content": "# Copied from cut --help\n- name: cut\n  keybinds:\n    - name: \"To cut out the third field of text or stdoutput that is delimited by a #\"\n      key: \"cut -d# -f3\"\n    - name: \"select only these bytes\"\n      key: \"cut -b, --bytes=LIST <file>\"\n    - name: \"select only these characters\"\n      key: \"cut -c, --characters=LIST <file>\"\n    - name: \"use DELIM instead of TAB for field delimiter\"\n      key: \"cut -d, --delimiter=DELIM <file>\"\n    - name: \"complement the set of selected bytes, characters or fields\"\n      key: \"cut --complement <file>\"\n    - name: \"do not print lines not containing delimiters\"\n      key: \"cut -s, --only-delimited <file>\"\n    - name: \"use STRING as the output delimiter the default is to use the input delimiter\"\n      key: \"cut --output-delimiter=STRING <file>\"\n    - name: \"line delimiter is NUL, not newline\"\n      key: \"cut -z, --zero-terminated <file>\"\n"
  },
  {
    "path": "examples/date.yml",
    "content": "# Copied from date --help\n- name: date\n  keybinds:\n    - name: \"To print the date in a format suitable for affixing to file names:\"\n      key: \"date +%Y%m%d_%H%M%S\"\n    - name: \"To show date in CET:\"\n      key: \"TZ=CET date\"\n    - name: \"To show the time on the west coast of the US (use tzselect(1) to find TZ):\"\n      key: \"TZ='America/Los_Angeles' date\"\n    - name: \"display time described by STRING, not 'now'\"\n      key: \"date -d, --date=STRING\"\n    - name: \"like --date; once for each line of DATEFILE\"\n      key: \"date -f, --file=DATEFILE\"\n    - name: \"output date/time in ISO 8601 format e.g. 006-08-14T02:34:56-06:00\"\n      key: \"date -I[FMT], --iso-8601[=FMT]\"\n    - name: \"output the available resolution of timestamps e.g. 0.000000001\"\n      key: \"date --resolution\"\n    - name: \"output date and time in RFC 5322 format. e.g. Mon, 14 Aug 2006 02:34:56 -0600\"\n      key: \"date -R, --rfc-email\"\n    - name: \"output date/time in RFC 3339 format e.g. 2006-08-14 02:34:56-06:00\"\n      key: \"date --rfc-3339=FMT\"\n    - name: \"display the last modification time of FILE\"\n      key: \"date -r, --reference=FILE\"\n    - name: \"set time described by STRING\"\n      key: \"date -s, --set=STRING\"\n    - name: \"print or set Coordinated Universal Time (UTC)\"\n      key: \"date -u, --utc, --universal \"\n- name: date (FORMAT controls the output)\n  keybinds:\n    - name: \"a literal %\"\n      key: \"%%\"\n    - name: \"locale's abbreviated weekday name (e.g., Sun)\"\n      key: \"%a\"\n    - name: \"locale's full weekday name (e.g., Sunday)\"\n      key: \"%A\"\n    - name: \"locale's abbreviated month name (e.g., Jan)\"\n      key: \"%b\"\n    - name: \"locale's full month name (e.g., January)\"\n      key: \"%B\"\n    - name: \"locale's date and time (e.g., Thu Mar  3 23:05:25 2005)\"\n      key: \"%c\"\n    - name: \"century; like %Y, except omit last two digits (e.g., 20)\"\n      key: \"%C\"\n    - name: \"day of month (e.g., 01)\"\n      key: \"%d\"\n    - name: \"date; same as %m/%d/%y\"\n      key: \"%D\"\n    - name: \"day of month, space padded; same as %_d\"\n      key: \"%e\"\n    - name: \"full date; like %+4Y-%m-%d\"\n      key: \"%F\"\n    - name: \"last two digits of year of ISO week number (see %G)\"\n      key: \"%g\"\n    - name: \"year of ISO week number (see %V); normally useful only with %V\"\n      key: \"%G\"\n    - name: \"same as %b\"\n      key: \"%h\"\n    - name: \"hour (00..23)\"\n      key: \"%H\"\n    - name: \"hour (01..12)\"\n      key: \"%I\"\n    - name: \"day of year (001..366)\"\n      key: \"%j\"\n    - name: \"hour, space padded ( 0..23); same as %_H\"\n      key: \"%k\"\n    - name: \"hour, space padded ( 1..12); same as %_I\"\n      key: \"%l\"\n    - name: \"month (01..12)\"\n      key: \"%m\"\n    - name: \"minute (00..59)\"\n      key: \"%M\"\n    - name: \"a newline\"\n      key: \"%n\"\n    - name: \"nanoseconds (000000000..999999999)\"\n      key: \"%N\"\n    - name: \"locale's equivalent of either AM or PM; blank if not known\"\n      key: \"%p\"\n    - name: \"like %p, but lower case\"\n      key: \"%P\"\n    - name: \"quarter of year (1..4)\"\n      key: \"%q\"\n    - name: \"locale's 12-hour clock time (e.g., 11:11:04 PM)\"\n      key: \"%r\"\n    - name: \"24-hour hour and minute; same as %H:%M\"\n      key: \"%R\"\n    - name: \"seconds since the Epoch (1970-01-01 00:00 UTC)\"\n      key: \"%s\"\n    - name: \"second (00..60)\"\n      key: \"%S\"\n    - name: \"a tab\"\n      key: \"%t\"\n    - name: \"time; same as %H:%M:%S\"\n      key: \"%T\"\n    - name: \"day of week (1..7); 1 is Monday\"\n      key: \"%u\"\n    - name: \"week number of year, with Sunday as first day of week (00..53)\"\n      key: \"%U\"\n    - name: \"ISO week number, with Monday as first day of week (01..53)\"\n      key: \"%V\"\n    - name: \"day of week (0..6); 0 is Sunday\"\n      key: \"%w\"\n    - name: \"week number of year, with Monday as first day of week (00..53)\"\n      key: \"%W\"\n    - name: \"locale's date representation (e.g., 12/31/99)\"\n      key: \"%x\"\n    - name: \"locale's time representation (e.g., 23:13:48)\"\n      key: \"%X\"\n    - name: \"last two digits of year (00..99)\"\n      key: \"%y\"\n    - name: \"year\"\n      key: \"%Y\"\n    - name: \"+hhmm numeric time zone (e.g., -0400)\"\n      key: \"%z\"\n    - name: \"z  +hh:mm numeric time zone (e.g., -04:00)\"\n      key: \"%:\"\n    - name: \":z  +hh:mm:ss numeric time zone (e.g., -04:00:00)\"\n      key: \"%:\"\n    - name: \"::z  numeric time zone with : to necessary precision (e.g., -04, +05:30)\"\n      key: \"%:\"\n    - name: \"alphabetic time zone abbreviation (e.g., EDT)\"\n      key: \"%Z\"\n"
  },
  {
    "path": "examples/dd.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: dd\n  keybinds:\n    - name: \"Backup disk partition with dd\"\n      key: \"dd if=/dev/sda1 of=~/localdisk_sda1.img\"\n    - name: \"Backup disk partition with dd and compress\"\n      key: \"dd if=/dev/sda1 | gzip -c > ~/localdisk_sda1.img.gz\"\n    - name: \"Restore disk partition with dd\"\n      key: \"dd if=~/localdisk_sda1.img of=/dev/sda1\"\n    - name: \"Restore disk partition with dd  with decompression\"\n      key: \"gunzip -c ~/localdisk_sda1.img.gz | dd of=/dev/sda1\"\n    - name: \"Every iteration, we read 512 Bytes.\"\n      key: \"dd if=/dev/urandom of=/tmp/test.txt count=2 bs=512\"\n    - name: \"Create fixed size file with dd. Use sizes such as K, M and G with bs\"\n      key: \"dd if=/dev/zero of=/root/test bs=1024 count=1\"\n    - name: \"Convert text to lower case with dd and conv\"\n      key: \"dd if=filetoconvert.txt of=convertedfile.txt conv=lcase\"\n    - name: \"dd with the built-in `progress` functionality\"\n      key: \"dd if=/dev/zero of=/dev/null bs=128M status=progress\"\n    - name: \"dd with graphical return\"\n      key: \"dcfldd if=/dev/zero of=/dev/null bs=500K\"\n    - name: \"Create a 1MB file with zero allocated blocks:\"\n      key: \"dd if=/dev/zero of=foo1 seek=1 bs=1M count=0\"\n"
  },
  {
    "path": "examples/discord.yml",
    "content": "- name: discord (navigation)\n  keybinds:\n    - name: \"Switch Servers\"\n      key: \"[Ctrl] [Alt] [↑] or [↓]\"\n    - name: \"Switch Channels\"\n      key: \"[Alt] [↑] or [↓]\"\n    - name: \"Switch Unread Channels\"\n      key: \"[Alt] [Shift] [↑] or [↓]\"\n    - name: \"Switch Unread & Mention Channels\"\n      key: \"[Ctrl] [Shift] [Alt] [↑] or [↓]\"\n    - name: \"Scroll Chat\"\n      key: \"[Page Up] or [Page Down]\"\n    - name: \"Jump to Oldest Unread Message\"\n      key: \"[Shift] [Page Up]\"\n    - name: \"Return To Connected Audio Channel\"\n      key: \"[Ctrl] [Alt] [←]\"\n    - name: \"Switch Last Channel/DMs\"\n      key: \"[Ctrl] [Alt] [→]\"\n\n- name: discord (menus)\n  keybinds:\n    - name: \"Pins Popout\"\n      key: \"[Ctrl] [P]\"\n    - name: \"Mentions Popout\"\n      key: \"[Ctrl] [I]\"\n    - name: \"Member List\"\n      key: \"[Ctrl] [U]\"\n    - name: \"Emoji Picker\"\n      key: \"[Ctrl] [E]\"\n    - name: \"Help\"\n      key: \"[Ctrl] [Shift] [H]\"\n    - name: \"Search\"\n      key: \"[Ctrl] [F]\"\n\n- name: discord (creation)\n  keybinds:\n    - name: \"Create/Join Server\"\n      key: \"[Ctrl] [Shift] [N]\"\n    - name: \"Find/Start DM\"\n      key: \"[Ctrl] [K]\"\n    - name: \"Create Group\"\n      key: \"[Ctrl] [Shift] [T]\"\n    - name: \"Start Call\"\n      key: \"[Ctrl] [']\"\n    - name: \"Focus Text Area\"\n      key: \"Tab\"\n    - name: \"Upload File\"\n      key: \"[Ctrl] [Shift] [U]\"\n\n- name: discord (interaction)\n  keybinds:\n    - name: \"Mark Server Read\"\n      key: \"[Shift] [Esc]\"\n    - name: \"Mark Channel Read\"\n      key: \"Esc\"\n    - name: \"Answer Call\"\n      key: \"[Ctrl] [Enter]\"\n    - name: \"Decline Call\"\n      key: \"Esc\"\n    - name: \"Mute\"\n      key: \"[Ctrl] [Shift] [M]\"\n    - name: \"Deafen\"\n      key: \"[Ctrl] [Shift] [D]\"\n"
  },
  {
    "path": "examples/du.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: du\n  keybinds:\n    - name: \"To sort directories/files by size\"\n      key: \"du -sk *| sort -rn\"\n    - name: \"To show cumulative human-readable size\"\n      key: \"du -sh\"\n    - name: \"To show cumulative human-readable size and dereference symlinks\"\n      key: \"du -shL\"\n    - name: \"Show apparent size instead of disk usage (so sparse files will show greater than zero)\"\n      key: \"du -h --apparent-size\"\n    - name: \"To sort directories/files by size (human-readable)\"\n      key: \"du -sh * | sort -rh \"\n    - name: \"To list the 20 largest files and folders under the current working directory\"\n      key: \"du -ma | sort -nr | head -n 20\"\n"
  },
  {
    "path": "examples/dwm.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: dwm\n  keybinds:\n    - name: \"launch terminal.\"\n      key: \"[Shift]+[Mod]+[Enter]\"\n    - name: \"show/hide bar.\"\n      key: \"[Mod]+[b]\"\n    - name: \"dmenu for running programs like the x#www#browser.\"\n      key: \"[Mod]+[p]\"\n    - name: \"push acive window from stack to master, or pulls last used window from stack onto master.\"\n      key: \"[Mod]+[Enter]\"\n    - name: \"focus on next/previous window in current tag.\"\n      key: \"[Mod] + [j / k]\"\n    - name: \"increases / decreases master size.\"\n      key: \"[Mod] + [h / l]\"\n- name: dwm (Navigation)\n  keybinds:\n    - name: \"moves your focus to tag 2.\"\n      key: \"[Mod]+[2]\"\n    - name: \"move active window to the 2 tag.\"\n      key: \"[Shift]+[Mod]+[2]\"\n    - name: \"increases / decreases number of windows on master\"\n      key: \"[Mod] + [i / d]\"\n    - name: \"move focus between screens (multi monitor setup)\"\n      key: \"[Mod] + [, / .]\"\n    - name: \"move active window to different screen.\"\n      key: \"[Shift]+[Mod]+[, / .]\"\n    - name: \"view all windows on screen.\"\n      key: \"[Mod]+[0]\"\n    - name: \"make focused window appear on all tags.\"\n      key: \"[Shift]+[Mod]+[0]\"\n    - name: \"kill active window.\"\n      key: \"[Shift]+[Mod]+[c]\"\n    - name: \"quit dwm cleanly.\"\n      key: \"[Shift]+[Mod]+[q]\"\n- name: dwm (Layout)\n  keybinds:\n    - name: \"tiled mode. []=\"\n      key: \"[Mod]+[t]\"\n    - name: \"floating mode. ><>\"\n      key: \"[Mod]+[f]\"\n    - name: \"monocle mode. [M] (single window fullscreen)\"\n      key: \"[Mod]+[m]\"\n- name: dwm (Floating)\n  keybinds:\n    - name: \"to resize the floating window.\"\n      key: \"[Mod]+[R M B]\"\n    - name: \"to move the floating window around.\"\n      key: \"[Mod]+[L M B]\"\n    - name: \"toggles to the previous layout mode.\"\n      key: \"[Mod]+[Space]\"\n    - name: \"to make an individual window float.\"\n      key: \"[Mod]+[Shift]+[Space]\"\n    - name: \"to make an individual window un#float.\"\n      key: \"[Mod]+[M M B]\"\n"
  },
  {
    "path": "examples/find.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: find\n  keybinds:\n  - name: \"To find files following symlinks (otherwise the symlinks are not followed)\"\n    key: \"find -L . -type f\"\n  - name: \"To find files by case-insensitive extension (ex: .jpg, .JPG, .jpG)\"\n    key: \"find . -iname *.jpg\"\n  - name: \"To find directories\"\n    key: \"find . -type d\"\n  - name: \"To find files\"\n    key: \"find . -type f\"\n  - name: \"To find files by octal permission\"\n    key: \"find . -type f -perm 777\"\n  - name: \"To find files with setuid bit set\"\n    key: \"find . -xdev ( -perm -4000 ) -type f -print0 | xargs -0 ls -l\"\n  - name: \"To find files newer than 1 day old and copy elsewhere (remove -p flag in xargs to not be asked)\"\n    key: \"find . -type f -ctime -1 -print0 | xargs -0 -p cp -t <dir>\"\n  - name: \"To find files with extension '.txt' and remove them\"\n    key: \"find ./path/ -name '*.txt' -delete\"\n  - name: \"To find files with tilde as postfix and remove them\"\n    key: \"find ./path/ -name '*~' -delete\"\n  - name: \"To find files with extension '.txt' and dump their contents\"\n    key: \"find ./path/ -name '*.txt' -exec cat '{}' ;\"\n  - name: \"To find files with extension '.txt' and look for a string into them\"\n    key: \"find ./path/ -name '*.txt' | xargs grep 'string'\"\n  - name: \"To find files with size bigger than 5 Mebibyte and sort them by size\"\n    key: \"find . -size +5M -type f -print0 | xargs -0 ls -Ssh | sort -z\"\n  - name: \"To find files modified more than 7 days ago and list file information\"\n    key: \"find . -type f -mtime +7d -ls\"\n  - name: \"To find symlinks owned by a user and list file information\"\n    key: \"find . -type l -user <username-or-userid> -ls\"\n  - name: \"To search for and delete empty directories\"\n    key: \"find . -type d -empty -exec rmdir {} ;\"\n  - name: \"To search for directories named build at a max depth of 2 directories\"\n    key: \"find . -maxdepth 2 -name build -type d\"\n  - name: \"To search all files who are not in .git directory\"\n    key: \"find . ! -iwholename '*.git*' -type f\"\n  - name: \"To find all files that have the same node (hard link) as MY_FILE_HERE\"\n    key: \"find . -type f -samefile MY_FILE_HERE 2>/dev/null\"\n  - name: \"To find all files in the current directory and modify their permissions\"\n    key: \"find . -type f -exec chmod 644 {} ;\"\n  - name: \"To find all files changed in last 2 days\"\n    key: \"find . -type f -ctime -48h\"\n  - name: \"Or created in last 2 days\"\n    key: \"find . -type f -Btime -2\"\n  - name: \"Or accessed in last 2 days\"\n    key: \"find . -type f -atime -2\"\n  - name: \"To find and rename (imperfect) all files and dirs that have a comma in the\"\n    key: \"find . -name '*,*' | while read f; do echo mv $f ${f//,/};done\"\n  - name: \"To find all broken links. Note -L returns a file unless it is a broken link\"\n    key: \"find -L /usr/ports/packages -type l\"\n  - name: \"To find and run multiple shell commands (without multiple execs)\"\n    key: \"find . -type f -exec sh -c echo '{}'; cat '{}';\"\n"
  },
  {
    "path": "examples/firefox.yml",
    "content": "- name: Firefox Navigation\n  keybinds:\n    - name: \"Back\"\n      key: \"[Alt] [←]\"\n    - name: \"Forward\"\n      key: \"[Alt] [→]\"\n    - name: \"Home\"\n      key: \"[Alt] [Home]\"\n    - name: \"Open File\"\n      key: \"[Ctrl] [O]\"\n    - name: \"Reload\"\n      key: \"F5\"\n    - name: \"Reload (override cache)\"\n      key: \"[Ctrl] [F5]\"\n    - name: \"Stop\"\n      key: \"Esc\"\n- name: Firefox Current Page\n  keybinds:\n    - name: \"Go Down a Screen\"\n      key: \"[Page Down]\"\n    - name: \"Go Up a Screen\"\n      key: \"[Page Up]\"\n    - name: \"Go to Bottom of Page\"\n      key: \"End\"\n    - name: \"Go to Top of Page\"\n      key: \"Home\"\n    - name: \"Move to Next Frame\"\n      key: \"F6\"\n    - name: \"Move to Previous Frame\"\n      key: \"[Shift] [F6]\"\n    - name: \"Print\"\n      key: \"[Ctrl] [P]\"\n    - name: \"Save Focused Link\"\n      key: \"[Alt] [Enter]\"\n    - name: \"Save Page As\"\n      key: \"[Ctrl] [S]\"\n    - name: \"Zoom In\"\n      key: \"[Ctrl] [+]\"\n    - name: \"Zoom Out\"\n      key: \"[Ctrl] [-]\"\n    - name: \"Zoom Reset\"\n      key: \"[Ctrl] [0]\"\n- name: Firefox Editing\n  keybinds:\n    - name: \"Copy\"\n      key: \"[Ctrl] [C]\"\n    - name: \"Cut\"\n      key: \"[Ctrl] [X]\"\n    - name: \"Delete\"\n      key: \"Del\"\n    - name: \"Paste\"\n      key: \"[Ctrl] [V]\"\n    - name: \"Paste (as plain text)\"\n      key: \"[Ctrl] [Shift] [V]\"\n    - name: \"Redo\"\n      key: \"[Ctrl] [Shift] [Z]\"\n    - name: \"Select All\"\n      key: \"[Ctrl] [A]\"\n    - name: \"Undo\"\n      key: \"[Ctrl] [Z]\"\n- name: Firefox Search\n  keybinds:\n    - name: \"Find\"\n      key: \"[Ctrl] [F]\"\n    - name: \"Find Again\"\n      key: \"[Ctrl] [G]\"\n    - name: \"Find Previous\"\n      key: \"[Shift] [F3]\"\n    - name: \"Quick Find within link-text only\"\n      key: \"'\"\n    - name: \"Quick Find\"\n      key: \"/\"\n    - name: \"Close the Find or Quick Find bar (when focused)\"\n      key: \"Esc\"\n    - name: \"Focus Search bar\"\n      key: \"[Ctrl] [K]\"\n    - name: \"Focus Search bar (Alternative)\"\n      key: \"[Ctrl] [J]\"\n    - name: \"Quickly switch between search engines (when Search Bar is focused)\"\n      key: \"[Ctrl] ( [↑] / [↓] )\"\n    - name: \"View menu to switch, add or manage search engines (when Search Bar is focused)\"\n      key: \"[Alt] ( [↑] / [↓] )\"\n- name: Firefox Windows & Tabs\n  keybinds:\n    - name: \"Close Tab\"\n      key: \"[Ctrl] [W]\"\n    - name: \"Close Tab (Alternative)\"\n      key: \"[Ctrl] [F4]\"\n    - name: \"Close Window\"\n      key: \"[Ctrl] [Shift] [W]\"\n    - name: \"Move Tab in focus Left\"\n      key: \"[Ctrl] [Shift] [Page Up]\"\n    - name: \"Move Tab in focus Right\"\n      key: \"[Ctrl] [Shift] [Page Down]\"\n    - name: \"Move Tab in focus to start\"\n      key: \"[Ctrl] [Home]\"\n    - name: \"Move Tab in focus to end\"\n      key: \"[Ctrl] [End]\"\n    - name: \"New Tab\"\n      key: \"[Ctrl] [T]\"\n    - name: \"New Window\"\n      key: \"[Ctrl] [N]\"\n    - name: \"New Private Window\"\n      key: \"[Ctrl] [Shift] [P]\"\n    - name: \"Next Tab\"\n      key: \"[Ctrl] [Tab]\"\n    - name: \"Open Address in New Tab\"\n      key: \"[Alt] [Enter]\"\n    - name: \"Previous Tab\"\n      key: \"[Ctrl] [Shift] [Tab]\"\n    - name: \"Undo Close Tab\"\n      key: \"[Ctrl] [Shift] [T]\"\n    - name: \"Undo Close Window\"\n      key: \"[Ctrl] [Shift] [N]\"\n    - name: \"Select Tab 1 to 8\"\n      key: \"[Ctrl] [1 to 8]\"\n    - name: \"Select Last Tab\"\n      key: \"[Alt] [9]\"\n    - name: \"Tab Groups View\"\n      key: \"[Ctrl] [Shift] [E]\"\n    - name: \"Close Tab Groups View\"\n      key: \"Esc\"\n    - name: \"Next Tab Group (only for some keyboard layouts)\"\n      key: \"[Ctrl] [`]\"\n    - name: \"Previous Tab Group (only for some keyboard layouts)\"\n      key: \"[Ctrl] [Shift] [`]\"\n- name: Firefox History\n  keybinds:\n    - name: \"History sidebar\"\n      key: \"[Ctrl] [H]\"\n    - name: \"Library window (History)\"\n      key: \"[Ctrl] [Shift] [H]\"\n    - name: \"Clear Recent History\"\n      key: \"[Ctrl] [Shift] [Del]\"\n- name: Firefox Bookmarks\n  keybinds:\n    - name: \"Bookmark All Tabs\"\n      key: \"[Ctrl] [Shift] [D]\"\n    - name: \"Bookmark This Page\"\n      key: \"[Ctrl] [D]\"\n    - name: \"Bookmarks sidebar\"\n      key: \"[Ctrl] [B]\"\n    - name: \"Library window (Bookmarks)\"\n      key: \"[Ctrl] [Shift] [B]\"\n- name: Firefox Tools\n  keybinds:\n    - name: \"Downloads\"\n      key: \"[Ctrl] [J]\"\n    - name: \"Add-ons\"\n      key: \"[Ctrl] [Shift] [A]\"\n    - name: \"Toggle Developer Tools\"\n      key: \"[Ctrl] [Shift] [I]\"\n    - name: \"Toggle Developer Tools (Alternative)\"\n      key: \"F12\"\n    - name: \"Web Console\"\n      key: \"[Ctrl] [Shift] [K]\"\n    - name: \"Inspector\"\n      key: \"[Ctrl] [Shift] [C]\"\n    - name: \"Debugger\"\n      key: \"[Ctrl] [Shift] [S]\"\n    - name: \"Style Editor\"\n      key: \"[Shift] [F7]\"\n    - name: \"Profiler\"\n      key: \"[Shift] [F5]\"\n    - name: \"Network\"\n      key: \"[Ctrl] [Shift] [Q]\"\n    - name: \"Developer Toolbar\"\n      key: \"[Shift] [F2]\"\n    - name: \"Responsive Design View\"\n      key: \"[Ctrl] [Shift] [M]\"\n    - name: \"Scratchpad\"\n      key: \"[Shift] [F4]\"\n    - name: \"Page Source\"\n      key: \"[Ctrl] [U]\"\n    - name: \"Browser Console\"\n      key: \"[Ctrl] [Shift] [J]\"\n- name: Firefox PDF Viewer\n  keybinds:\n    - name: \"Next page\"\n      key: \"→\"\n    - name: \"Previous page\"\n      key: \"←\"\n    - name: \"Zoom in\"\n      key: \"[Ctrl] [+]\"\n    - name: \"Zoom out\"\n      key: \"[Ctrl] [-]\"\n    - name: \"Automatic Zoom\"\n      key: \"[Ctrl] [0]\"\n    - name: \"Rotate the document clockwise\"\n      key: \"R\"\n    - name: \"Rotate counterclockwise\"\n      key: \"[Shift] [R]\"\n    - name: \"Switch to Presentation Mode\"\n      key: \"[Ctrl] [Alt] [P]\"\n    - name: \"Toggle Hand Tool\"\n      key: \"H\"\n    - name: \"Focus the Page Number input box\"\n      key: \"[Ctrl] [Alt] [G]\"\n- name: Firefox Miscellaneous\n  keybinds:\n    - name: \"Complete .com Address\"\n      key: \"[Ctrl] [Enter]\"\n    - name: \"Complete .net Address\"\n      key: \"[Shift] [Enter]\"\n    - name: \"Complete .org Address\"\n      key: \"[Ctrl] [Shift] [Enter]\"\n    - name: \"Delete Selected Autocomplete Entry\"\n      key: \"Del\"\n    - name: \"Toggle Full Screen\"\n      key: \"F11\"\n    - name: \"Toggle Menu Bar activation (KDE) (showing it temporarily when hidden)\"\n      key: \"Alt\"\n    - name: \"Toggle Menu Bar activation (GNOME) (showing it temporarily when hidden)\"\n      key: \"F10\"\n    - name: \"Show/Hide Add-on Bar\"\n      key: \"[Ctrl] [/]\"\n    - name: \"Caret Browsing\"\n      key: \"F7\"\n    - name: \"Select Location Bar\"\n      key: \"F6\"\n- name: Firefox Audio & Video Media\n  keybinds:\n    - name: \"Toggle Play / Pause\"\n      key: \"Space bar\"\n    - name: \"Decrease volume\"\n      key: \"↓\"\n    - name: \"Increase volume\"\n      key: \"↑\"\n    - name: \"Mute audio\"\n      key: \"[Ctrl] [↓]\"\n    - name: \"Unmute audio\"\n      key: \"[Ctrl] [↑]\"\n    - name: \"Seek back 15 seconds\"\n      key: \"←\"\n    - name: \"Seek back 10 %\"\n      key: \"[Ctrl] [←]\"\n    - name: \"Seek forward 15 seconds\"\n      key: \"→\"\n    - name: \"Seek forward 10 %\"\n      key: \"[Ctrl] [→]\"\n    - name: \"Seek to the beginning\"\n      key: \"Home\"\n    - name: \"Seek to the end\"\n      key: \"End\"\n"
  },
  {
    "path": "examples/irssi.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: irssi\n  keybinds:\n  - name: \"To connect to an IRC server:\"\n    key : \"/connect <server>\"\n  - name: \"To join a channel:\"\n    key: \"/join #<channel>\"\n  - name: \"To set a nickname:\"\n    key: \"/nick <nickname>\"\n  - name: \"To send a private message to a user:\"\n    key: \"/msg <nickname>\"\n  - name: \"To close the current channel window:\"\n    key: \"/wc\"\n  - name: \"To switch between channel windows:\"\n    key: \"ALT+<number>, eg. ALT+1, ALT+2\"\n  - name: \"To list the nicknames within the active channel:\"\n    key: \"/names\"\n  - name: \"To change the channel topic:\"\n    key: \"/topic <description>\"\n  - name: \"foo,#bar JOINS PARTS QUITS NICKS   # Quieten only channels `#foo`, `#bar`\"\n    key: \"/ignore \"\n  - name: \"Quieten all channels\"\n    key: \"/ignore * JOINS PARTS QUITS NICKS\"\n  - name: \"To save the current Irssi session config into the configuration file:\"\n    key: \"/save\"\n  - name: \"To quit Irssi:\"\n    key: \"/exit\"\n"
  },
  {
    "path": "examples/kitty.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: kitty\n  keybinds:\n    - name: \"Line up\"\n      key: \"ctrl+shift+up\"\n    - name: \"Line down\"\n      key: \"ctrl+shift+down\"\n    - name: \"Page up\"\n      key: \"ctrl+shift+page_up\"\n    - name: \"Page down\"\n      key: \"ctrl+shift+page_down\"\n    - name: \"Top\"\n      key: \"ctrl+shift+home\"\n    - name: \"Bottom\"\n      key: \"ctrl+shift+end\"\n    - name: \"Previous shell prompt\"\n      key: \"ctrl+shift+z\"\n    - name: \"Next shell prompt\"\n      key: \"ctrl+shift+x\"\n    - name: \"Browse scrollback in less\"\n      key: \"ctrl+shift+h\"\n    - name: \"Browse last cmd output\"\n      key: \"ctrl+shift+g\"\n    - name: \"New tab\"\n      key: \"ctrl+shift+t\"\n    - name: \"Close tab\"\n      key: \"ctrl+shift+q\"\n    - name: \"Next tab\"\n      key: \"ctrl+shift+right\"\n    - name: \"Previous tab\"\n      key: \"ctrl+shift+left\"\n    - name: \"Next layout\"\n      key: \"ctrl+shift+l\"\n    - name: \"Move tab forward\"\n      key: \"ctrl+shift+.\"\n    - name: \"Move tab backward\"\n      key: \"ctrl+shift+,\"\n    - name: \"Set tab title\"\n      key: \"ctrl+shift+alt+t\"\n    - name: \"Copy to clipboard\"\n      key: \"ctrl+shift+c\"\n    - name: \"Paste from clipboard\"\n      key: \"ctrl+shift+v\"\n    - name: \"Paste from selection\"\n      key: \"ctrl+shift+s\"\n    - name: \"Increase font size\"\n      key: \"ctrl+shift+equal\"\n    - name: \"Decrease font size\"\n      key: \"ctrl+shift+minus\"\n    - name: \"Restore font size\"\n      key: \"ctrl+shift+backspace\"\n    - name: \"New window\"\n      key: \"ctrl+shift+enter\"\n    - name: \"New OS window\"\n      key: \"ctrl+shift+n\"\n    - name: \"Close window\"\n      key: \"ctrl+shift+w\"\n    - name: \"Next window\"\n      key: \"ctrl+shift+]\"\n    - name: \"Previous window\"\n      key: \"ctrl+shift+[\"\n    - name: \"Move window forward\"\n      key: \"ctrl+shift+f\"\n    - name: \"Move window backward\"\n      key: \"ctrl+shift+b\"\n    - name: \"Move window to top\"\n      key: \"ctrl+shift+`\"\n    - name: \"Visually focus window\"\n      key: \"ctrl+shift+f7\"\n    - name: \"Visually swap window\"\n      key: \"ctrl+shift+f8\"\n    - name: \"Focus specific window\"\n      key: \"ctrl+shift+1, ctrl+shift+2 … ctrl+shift+0\"\n    - name: \"Open URL\"\n      key: \"ctrl+shift+e\"\n    - name: \"Insert selected path\"\n      key: \"ctrl+shift+p>f\"\n    - name: \"Open selected path\"\n      key: \"ctrl+shift+p>shft+f\"\n    - name: \"Insert selected line\"\n      key: \"ctrl+shift+p>l\"\n    - name: \"Insert selected word\"\n      key: \"ctrl+shift+p>w\"\n    - name: \"Insert selected hash\"\n      key: \"ctrl+shift+p>h\"\n    - name: \"Open the selected file at the selected line\"\n      key: \"ctrl+shift+p>n\"\n    - name: \"Open the selected hyperlink\"\n      key: \"ctrl+shift+p>y\"\n"
  },
  {
    "path": "examples/less.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: less\n  keybinds:\n  - name: \"Go to end of file:\"\n    key: \"G\"\n  - name: \"Go to start of file:\"\n    key: \"g\"\n  - name: \"To edit the current file in $EDITOR:\"\n    key: \"v\"\n  - name: \"Search in file:\"\n    key: \"/<searchterm>\"\n  - name: \"Next result:\"\n    key: \"n\"\n  - name: \"Previous result:\"\n    key: \"N\"\n  - name: \"Toggle search highlighting:\"\n    key: \"Alt-u\"\n  - name: \"Follow (tail -f) a file after opening it:\"\n    key: \"F\"\n  - name: \"Move to next file (if multiple files opened, eg. `file1 file2`):\"\n    key: \":n\"\n  - name: \"Move to previous file:\"\n    key: \":p\"\n  - name: \"To save the contents to a file:\"\n    key: \"s <filename>\"\n"
  },
  {
    "path": "examples/ranger.yml",
    "content": "- name: ranger\n  keybinds:\n    - name: \"navigation and tabs\"\n      key: \"g\"\n    - name: \"':open_with' command\"\n      key: \"r\"\n    - name: \"yank(copy)\"\n      key: \"y\"\n    - name: \"paste\"\n      key: \"p\"\n    - name: \"cut/delete\"\n      key: \"d\"\n    - name: \"sort\"\n      key: \"o\"\n    - name: \"filter_stack\"\n      key: \".\"\n    - name: \"changing settings\"\n      key: \"z\"\n    - name: \"undo\"\n      key: \"u\"\n    - name: \"linemode\"\n      key: \"M\"\n    - name: \"+, -, =\"\n      key: \"setting access rights to files\"\n"
  },
  {
    "path": "examples/screen.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: screen\n  keybinds:\n  - name: \"To start a new named screen session:\"\n    key: \"screen -S <session-name>\"\n  - name: \"To detach from the current session:\"\n    key: \"Ctrl+A; d\"\n  - name: \"To re-attach a detached session:\"\n    key: \"screen -r <session-name>\"\n  - name: \"To list all screen sessions:\"\n    key: \"screen -ls\"\n  - name: \"To quit a session:\"\n    key: \"screen -XS <session-name> quit\"\n"
  },
  {
    "path": "examples/tar.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: tar\n  keybinds:\n  - name: \"To extract an uncompressed archive:\"\n    key: \"tar -xvf /path/to/foo.tar\"\n  - name: \"To extract a .tar in specified Directory:\"\n    key: \"tar -xvf /path/to/foo.tar -C /path/to/destination/\"\n  - name: \"To create an uncompressed archive:\"\n    key: \"tar -cvf /path/to/foo.tar /path/to/foo/\"\n  - name: \"To extract a .tgz or .tar.gz archive:\"\n    key: \"tar -xzvf /path/to/foo.tgz\"\n  - name: \"To create a .tgz or .tar.gz archive:\"\n    key: \"tar -czvf /path/to/foo.tgz /path/to/foo/\"\n  - name: \"To list the content of an .tgz or .tar.gz archive:\"\n    key: \"tar -tzvf /path/to/foo.tgz\"\n  - name: \"To extract a .tar.bz2 archive:\"\n    key: \"tar -xjvf /path/to/foo.tar.bz2\"\n  - name: \"To create a .tar.bz2 archive:\"\n    key: \"tar -cjvf /path/to/foo.tar.bz2 /path/to/foo/\"\n  - name: \"To list the content of an .tar.bz2 archive:\"\n    key: \"tar -tjvf /path/to/foo.tar.bz2\"\n"
  },
  {
    "path": "examples/tmux.yml",
    "content": "# tmux\n# source: http://man.openbsd.org/OpenBSD-current/man1/tmux.1\n\n- name: tmux (copy mode)\n  prefix: \"[Ctrl] [b]\"\n  keybinds:\n    - name: \"Enter copy mode\"\n      key: \"[\"\n    - name: \"Bottom of history\"\n      key: \"[M-<]\"\n    - name: \"Top of history\"\n      key: \"[M->]\"\n    - name: \"Back to indentation\"\n      key: \"[M-m]\"\n    - name: \"Copy selection\"\n      key: \"[M-w]\"\n    - name: \"Paste selection\"\n      key: \"[M-y]\"\n    - name: \"Clear selection\"\n      key: \"[Ctrl] [g]\"\n    - name: \"Cursor to top line\"\n      key: \"[M-R]\"\n    - name: \"Cursor to middle line\"\n      key: \"[M-r]\"\n    - name: \"Cursor Up\"\n      key: \"[↑]\"\n    - name: \"Cursor Down\"\n      key: \"[↓]\"\n    - name: \"Cursor Left\"\n      key: \"[←]\"\n    - name: \"Cursor Right\"\n      key: \"[→]\"\n\n- name: tmux (copy mode vi)\n  prefix: \"[Ctrl] [b]\"\n  keybinds:\n      - name: \"Enter copy mode\"\n        key: \"[\"\n      - name: \"Bottom of history\"\n        key: \"[G]\"\n      - name: \"Top of history\"\n        key: \"[g]\"\n      - name: \"Copy selection\"\n        key: \"[Enter]\"\n      - name: \"Paste selection\"\n        key: \"[p]\"\n      - name: \"Cursor Up\"\n        key: \"[k]\"\n      - name: \"Cursor Down\"\n        key: \"[j]\"\n      - name: \"Cursor Left\"\n        key: \"[h]\"\n      - name: \"Cursor Right\"\n        key: \"[l]\"\n\n- name: tmux (window control)\n  prefix: \"[Ctrl] [b]\"\n  keybinds:\n      - name: \"Create new window\"\n        key: \"[c]\"\n      - name: \"Detach from session\"\n        key: \"[d]\"\n      - name: \"Rename current window\"\n        key: \"[,]\"\n      - name: \"Close current window\"\n        key: \"[&]\"\n      - name: \"List windows\"\n        key: \"[w]\"\n      - name: \"Previous window\"\n        key: \"[p]\"\n      - name: \"Next window\"\n        key: \"[n]\"\n\n- name: tmux (pane control)\n  prefix: \"[Ctrl] [b]\"\n  keybinds:\n      - name: \"Split pane horizontally\"\n        key: \"[\\\"]\"\n      - name: \"Split pane vertically\"\n        key: \"[%]\"\n      - name: \"Next pane\"\n        key: \"[o]\"\n      - name: \"Previous pane\"\n        key: \"[;]\"\n      - name: \"Show pane numbers\"\n        key: \"[q]\"\n      - name: \"Toggle pane zoom\"\n        key: \"[z]\"\n      - name: \"Convert pane into a window\"\n        key: \"[!]\"\n      - name: \"Kill current pane\"\n        key: \"[x]\"\n      - name: \"Swap panes\"\n        key: \"[Ctrl] [O]\"\n      - name: \"Display clock\"\n        key: \"[t]\"\n      - name: \"Transpose two letters (delete and paste)\"\n        key: \"[q]\"\n      - name: \"Move to the previous pane\"\n        key: \"[{]\"\n      - name: \"Move to the next pane\"\n        key: \"[}]\"\n      - name: \"Toggle between pane layouts\"\n        key: \"[Space]\"\n      - name: \"Resize pane (make taller)\"\n        key: \"[↑]\"\n      - name: \"Resize pane (make smaller)\"\n        key: \"[↓]\"\n      - name: \"Resize pane (make wider)\"\n        key: \"[←]\"\n      - name: \"Resize pane (make narrower)\"\n        key: \"[→]\"\n\n- name: tmux (session control)\n  keybinds:\n      - name: \"Start a new session\"\n        key: \"tmux\"\n      - name: \"Start a new session with the name chosen\"\n        key: \"tmux new -s <session-name>\"\n      - name: \"List all sessions\"\n        key: \"tmux ls\"\n      - name: \"Re-attach a detached session\"\n        key: \"tmux attach -t <target-session>\"\n      - name: \"Re-attach a detached session (and detach it from elsewhere)\"\n        key: \"tmux attach -d -t <target-session>\"\n      - name: \"Delete session\"\n        key: \"tmux kill-session -t <target-session>\"\n"
  },
  {
    "path": "examples/unzip.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: unzip\n  keybinds:\n  - name: \"To extract an archive:\"\n    key: \"unzip <archive>\"\n  - name: \"To extract an archive to a specific directory:\"\n    key: \"unzip <archive> -d <directory>\"\n  - name: \"To test integrity of archive:\"\n    key: \"unzip -tq <archive>\"\n  - name: \"To list files and directories an archive:\"\n    key: \"unzip -l <archive>\"\n"
  },
  {
    "path": "examples/vim.yml",
    "content": "# vim\n# source: http://vim.rtorr.com\n\n- name: vim (tabs)\n  keybinds:\n    - name: \"move to tab number #\"\n      key: \"#gt\"\n    - name: \"move the current split window into its own tab\"\n      key: \"[Ctrl] [w], [t]\"\n    - name: \"move current tab to the #th position (indexed from 0)\"\n      key: \":tabmove #\"\n    - name: \"open a file in a new tab\"\n      key: \"[:tabnew filename] / [:tabn filename]\"\n    - name: \"close the current tab and all its windows\"\n      key: \"[:tabclose] / [:tabc]\"\n    - name: \"close all tabs except for the current one\"\n      key: \"[:tabonly] / [:tabo]\"\n    - name: \"move to the next tab\"\n      key: \"[gt] / [:tabnext] / [:tabn]\"\n    - name: \"move to the previous tab\"\n      key: \"[gT] / [:tabprev] / [:tabp]\"\n\n- name: vim (registers)\n  keybinds:\n    - name: \"view all current registers\"\n      key: \"[:reg] / [:registers]\"\n    - name: \"access register `r` as a variable\"\n      key: \"echo @r\"\n    - name: \"last search pattern register\"\n      key: \"\\\"/\"\n    - name: \"the black hole register\"\n      key: \"\\\"_\"\n    - name: \"last yank register\"\n      key: \"\\\"0\"\n    - name: \"last big delete register\"\n      key: \"\\\"1\"\n    - name: \"big delete register stack\"\n      key: \"\\\"2-\\\"9\"\n    - name: \"system clipboard\"\n      key: \"\\\"+\"\n    - name: \"small delete register\"\n      key: \"\\\"-\"\n    - name: \"named registers\"\n      key: \"\\\"a-\\\"z\"\n    - name: \"append registers\"\n      key: \"\\\"A-\\\"Z\"\n    - name: \"record into register `r`\"\n      key: \"qr\"\n\n- name: vim (editing)\n  keybinds:\n    - name: \"replace a single character\"\n      key: \"r\"\n    - name: \"join line below to the current one\"\n      key: \"J\"\n    - name: \"change (replace) entire line\"\n      key: \"cc\"\n    - name: \"change (replace) to the end of the word\"\n      key: \"cw\"\n    - name: \"change (replace) to the end of the line\"\n      key: \"c$\"\n    - name: \"delete character and substitute text\"\n      key: \"s\"\n    - name: \"delete line and substitute text (same as cc)\"\n      key: \"S\"\n    - name: \"transpose two letters (delete and paste)\"\n      key: \"xp\"\n    - name: \"undo\"\n      key: \"u\"\n    - name: \"redo\"\n      key: \"[Ctrl] [r]\"\n    - name: \"repeat last command\"\n      key: \".\"\n\n- name: vim (exiting)\n  keybinds:\n    - name: \"write (save) the file, but don't exit\"\n      key: \":w\"\n    - name: \"write (save) and quit\"\n      key: \":wq\"\n    - name: \"write (save) and quit\"\n      key: \":x\"\n    - name: \"quit (fails if there are unsaved changes)\"\n      key: \":q\"\n    - name: \"quit and throw away unsaved changes\"\n      key: \":q!\"\n    - name: \"quit all buffers and windows\"\n      key: \":qa\"\n    - name: \"write (save) current file, if modified, and quit\"\n      key: \"ZZ\"\n    - name: \"quit without checking for changes\"\n      key: \"ZQ\"\n\n- name: vim (insert/appending text)\n  keybinds:\n    - name: \"insert before the cursor\"\n      key: \"i\"\n    - name: \"insert at the beginning of the line\"\n      key: \"I\"\n    - name: \"insert (append) after the cursor\"\n      key: \"a\"\n    - name: \"insert (append) at the end of the line\"\n      key: \"A\"\n    - name: \"append (open) a new line below the current line\"\n      key: \"o\"\n    - name: \"append (open) a new line above the current line\"\n      key: \"O\"\n    - name: \"insert (append) at the end of the word\"\n      key: \"ea\"\n    - name: \"exit insert mode\"\n      key: \"Esc\"\n\n- name: vim (cut and paste)\n  keybinds:\n    - name: \"yank (copy) a line\"\n      key: \"yy\"\n    - name: \"yank (copy) 2 lines\"\n      key: \"2yy\"\n    - name: \"yank lines n through N\"\n      key: \":n,Ny\"\n    - name: \"yank (copy) word\"\n      key: \"yw\"\n    - name: \"yank (copy) to end of line\"\n      key: \"y$\"\n    - name: \"put (paste) the clipboard after cursor\"\n      key: \"p\"\n    - name: \"put (paste) before cursor\"\n      key: \"P\"\n    - name: \"delete (cut) a line\"\n      key: \"dd\"\n    - name: \"delete (cut) 2 lines\"\n      key: \"2dd\"\n    - name: \"delete lines n through N\"\n      key: \":n,Nd\"\n    - name: \"delete (cut) word\"\n      key: \"dw\"\n    - name: \"delete (cut) to the end of the line\"\n      key: \"D\"\n    - name: \"delete (cut) to the end of the line\"\n      key: \"d$\"\n    - name: \"delete (cut) character\"\n      key: \"x\"\n\n- name: vim (visual mode)\n  keybinds:\n    - name: \"start visual mode\"\n      key: \"v\"\n    - name: \"start linewise visual mode\"\n      key: \"V\"\n    - name: \"move to other end of marked area\"\n      key: \"vo\"\n    - name: \"start visual block mode\"\n      key: \"[Ctrl] [v]\"\n    - name: \"move to other corner of block\"\n      key: \"vO\"\n    - name: \"mark a word\"\n      key: \"vaw\"\n    - name: \"mark a block with ()\"\n      key: \"vab\"\n    - name: \"mark a block with {}\"\n      key: \"vaB\"\n    - name: \"mark inner block with ()\"\n      key: \"vib\"\n    - name: \"mark inner block with {}\"\n      key: \"viB\"\n    - name: \"exit visual mode\"\n      key: \"Esc\"\n\n- name: vim (multiple files)\n  keybinds:\n    - name: \"edit a file in a new buffer\"\n      key: \":e filename\"\n    - name: \"open a blank file for editing\"\n      key: \":ene\"\n    - name: \"go to the next buffer\"\n      key: \"[:bnext] / [:bn]\"\n    - name: \"go to the previous buffer\"\n      key: \"[:bprev] / [:bp]\"\n    - name: \"delete a buffer (close a file)\"\n      key: \":bd\"\n    - name: \"new buffer in split window\"\n      key: \":sp filename\"\n    - name: \"new buffer in vertically split window\"\n      key: \":vs filename\"\n    - name: \"split window\"\n      key: \"[Ctrl] [w], [s]\"\n    - name: \"switch windows\"\n      key: \"[Ctrl] [w], [w]\"\n    - name: \"quit a window\"\n      key: \"[Ctrl] [w], [q]\"\n    - name: \"split window vertically\"\n      key: \"[Ctrl] [w], [v]\"\n    - name: \"move cursor to window left\"\n      key: \"[Ctrl] [w], [h]\"\n    - name: \"move cursor to window  right\"\n      key: \"[Ctrl] [w], [l]\"\n    - name: \"move cursor to window above\"\n      key: \"[Ctrl] [w], [k]\"\n    - name: \"move cursor to window below\"\n      key: \"[Ctrl] [w], [j]\"\n    - name: \"rotate windows clockwise\"\n      key: \"[Ctrl] [w], [r]\"\n    - name: \"move current window to a new tab\"\n      key: \"[Ctrl] [w], [T]\"\n    - name: \"close all windows except current window\"\n      key: \":on\"\n    - name: \"maximize width of active window\"\n      key: \"[Ctrl] [w], [|]\"\n    - name: \"minimize width of active window\"\n      key: \"[Ctrl] [w], [1], [|]\"\n    - name: \"maximize height of active window\"\n      key: \"[Ctrl] [w], [_]\"\n    - name: \"minimize height of active window\"\n      key: \"[Ctrl] [w], [1], [_]\"\n    - name: \"equalize size of windows\"\n      key: \"[Ctrl] [w], [=]\"\n\n- name: vim (cursor movement)\n  keybinds:\n    - name: \"move cursor left\"\n      key: \"h\"\n    - name: \"move cursor down\"\n      key: \"j\"\n    - name: \"move cursor up\"\n      key: \"k\"\n    - name: \"move cursor right\"\n      key: \"l\"\n    - name: \"jump forwards to the start of a word\"\n      key: \"w\"\n    - name: \"jump forwards to the start of a word\"\n      key: \"W\"\n    - name: \"jump forwards to the end of a word\"\n      key: \"e\"\n    - name: \"jump forwards to the end of a word\"\n      key: \"E\"\n    - name: \"jump backwards to the start of a word\"\n      key: \"b\"\n    - name: \"jump backwards to the start of a word\"\n      key: \"B\"\n    - name: \"jump to the start of the line\"\n      key: \"0\"\n    - name: \"jump to the first non-blank character of the line\"\n      key: \"^\"\n    - name: \"jump to the end of the line\"\n      key: \"$\"\n    - name: \"go to the last line of the document\"\n      key: \"G\"\n    - name: \"go to line number n\"\n      key: \"nG\"\n    - name: \"go to line number n\"\n      key: \":n\"\n    - name: \"To the position before the latest jump\"\n      key: \"''\"\n    - name: \"jump to next occurrence of character x\"\n      key: \"fx\"\n    - name: \"jump to one character before the character x\"\n      key: \"tx\"\n    - name: \"jump to next paragraph\"\n      key: \"}\"\n    - name: \"jump to previous paragraph\"\n      key: \"{\"\n    - name: \"jump to home (top) of screen\"\n      key: \"H\"\n    - name: \"jump to last line of screen\"\n      key: \"L\"\n    - name: \"jump to middle of screen\"\n      key: \"M\"\n    - name: \"jump to 3rd instance of character x forward from cursor on current line.\"\n      key: \"3, f, x\"\n    - name: \"jump to 3rd instance of character x back from cursor on current line.\"\n      key: \"3, F, x\"\n\n- name: vim (screen movement)\n  keybinds:\n    - name: \"move screen up by half page\"\n      key: \"[Ctrl] [u]\"\n    - name: \"move screen up by one page\"\n      key: \"[Ctrl] [b]\"\n    - name: \"move screen down by half page\"\n      key: \"[Ctrl] [d]\"\n    - name: \"move screen down by one page\"\n      key: \"[Ctrl] [f]\"\n    - name: \"center screen on cursor\"\n      key: \"zz\"\n    - name: \"align top of screen with cursor\"\n      key: \"zt\"\n    - name: \"align bottom of screen with cursor\"\n      key: \"zb\"\n\n- name: vim (visual commands)\n  keybinds:\n    - name: \"shift text right\"\n      key: \">\"\n    - name: \"shift text left\"\n      key: \"<\"\n    - name: \"auto-indent current line\"\n      key: \"==\"\n    - name: \"shift current line left by shiftwidth\"\n      key: \"<<\"\n    - name: \"shift current line right by shiftwidth\"\n      key: \">>\"\n    - name: \"yank (copy) marked text\"\n      key: \"y\"\n    - name: \"delete marked text\"\n      key: \"d\"\n    - name: \"switch case\"\n      key: \"~\"\n\n- name: vim (search and replace)\n  keybinds:\n    - name: \"search for word under cursor\"\n      key: \"*\"\n    - name: \"search for pattern\"\n      key: \"/pattern\"\n    - name: \"search backward for pattern\"\n      key: \"?pattern\"\n    - name: \"repeat search in same direction\"\n      key: \"n\"\n    - name: \"repeat search in opposite direction\"\n      key: \"N\"\n    - name: \"replace all\"\n      key: \":%s/old/new/g\"\n    - name: \"replace all with confirmations\"\n      key: \":%s/old/new/gc\"\n\n- name: vim (extra commands)\n  keybinds:\n    - name: \"run a compiler and enter quickfix mode\"\n      key: \"[:mak] / [:make]\"\n    - name: \"execute external shell command\"\n      key: \":!\"\n    - name: \"read external program output into current file\"\n      key: \"[:r] / [:read]\"\n    - name: \"move lines x through y to z (delete + put)\"\n      key: \":x,ymz\"\n    - name: \"yank lines x through y and put to z (yank + put)\"\n      key: \":x,ytz\"\n    - name: \"current line (cursor location)\"\n      key: \":.\"\n    - name: \"last line (bottom of file)\"\n      key: \":$\"\n    - name: \"first line (top of file)\"\n      key: \":0\"\n    - name: \"list all open files\"\n      key: \":ls\"\n    - name: \"create html representation of current working buffer\"\n      key: \":%TOhtml\"\n\n- name: vim (basic configuration)\n  keybinds:\n    - name: \"show line numbers\"\n      key: \":set nu\"\n    - name: \"lets you switch buffers without saving\"\n      key: \":set hid\"\n    - name: \"highlight search matches\"\n      key: \":set hls\"\n    - name: \"show commands as you type them\"\n      key: \":set sc\"\n    - name: \"show line and column number of the cursor\"\n      key: \":set ru\"\n\n- name: vim (folding)\n  keybinds:\n    - name: \"creates a fold from the cursor down # lines\"\n      key: \"zf#j\"\n    - name: \"string creates a fold from the cursor to string\"\n      key: \"zf/\"\n    - name: \"moves the cursor to the next fold\"\n      key: \"zj\"\n    - name: \"moves the cursor to the previous fold\"\n      key: \"zk\"\n    - name: \"toggle a fold under cursor\"\n      key: \"za\"\n    - name: \"opens a fold at the cursor\"\n      key: \"zo\"\n    - name: \"opens all folds at the cursor\"\n      key: \"zO\"\n    - name: \"closes a fold under cursor\"\n      key: \"zc\"\n    - name: \"increases the foldlevel by one\"\n      key: \"zm\"\n    - name: \"closes all open folds\"\n      key: \"zM\"\n    - name: \"decreases the foldlevel by one\"\n      key: \"zr\"\n    - name: \"open all folds\"\n      key: \"zR\"\n    - name: \"deletes the fold at the cursor\"\n      key: \"zd\"\n    - name: \"deletes all folds\"\n      key: \"zE\"\n    - name: \"move to start of open fold\"\n      key: \"[z\"\n    - name: \"move to end of open fold\"\n      key: \"]z\""
  },
  {
    "path": "examples/vscode.yml",
    "content": "# Visual Studio Code\n# source: https://code.visualstudio.com/docs/customization/keybinds\n\n- name: vscode (basic editing)\n  keybinds:\n    - name: Cut line (empty selection)\n      key: ctrl + x\n    - name: \"Copy line (empty selection)\"\n      key: \"[Ctrl] [C]\"\n    - name: \"Delete Line\"\n      key: \"[Ctrl] [Shift] [K]\"\n    - name: \"Insert Line Below\"\n      key: \"[Ctrl] [Enter]\"\n    - name: \"Insert Line Above\"\n      key: \"[Ctrl] [Shift] [Enter]\"\n    - name: \"Move Line Down\"\n      key: \"[Alt] [Down]\"\n    - name: \"Move Line Up\"\n      key: \"[Alt] [Up]\"\n    - name: \"Copy Line Down\"\n      key: \"[Shift] [Alt] [Down]\"\n    - name: \"Copy Line Up\"\n      key: \"[Shift] [Alt] [Up]\"\n    - name: \"Add Selection To Next Find Match\"\n      key: \"[Ctrl] [D]\"\n    - name: \"Move Last Selection To Next Find Match\"\n      key: \"[Ctrl] [K], [Ctrl] [D]\"\n    - name: \"Undo last cursor operation\"\n      key: \"[Ctrl] [U]\"\n    - name: \"Select all occurrences of current selection\"\n      key: \"[Ctrl] [Shift] [L]\"\n    - name: \"Select all occurrences of current word\"\n      key: \"[Ctrl] [F2]\"\n    - name: \"Select current line\"\n      key: \"[Ctrl] [I]\"\n    - name: \"Insert Cursor Below\"\n      key: \"[Ctrl] [Alt] [Down]\"\n    - name: \"Insert Cursor Above\"\n      key: \"[Ctrl] [Alt] [Up]\"\n    - name: \"Jump to matching bracket\"\n      key: \"[Ctrl] [Shift] [\\\\]\"\n    - name: \"Indent Line\"\n      key: \"[Ctrl] {]}\"\n    - name: \"Outdent Line\"\n      key: \"[Ctrl] {[}\"\n    - name: \"Go to Beginning of Line\"\n      key: \"Home\"\n    - name: \"Go to End of Line\"\n      key: \"End\"\n    - name: \"Go to End of File\"\n      key: \"[Ctrl] [End]\"\n    - name: \"Go to Beginning of File\"\n      key: \"[Ctrl] [Home]\"\n    - name: \"Scroll Line Down\"\n      key: \"[Ctrl] [Down]\"\n    - name: \"Scroll Line Up\"\n      key: \"[Ctrl] [Up]\"\n    - name: \"Scroll Page Down\"\n      key: \"[Ctrl] [PageDown]\"\n    - name: \"Scroll Page Up\"\n      key: \"[Ctrl] [PageUp]\"\n    - name: \"Fold (collapse) region\"\n      key: \"[Ctrl] [Shift] {[}\"\n    - name: \"Unfold (uncollapse) region\"\n      key: \"[Ctrl] [Shift] {]}\"\n    - name: \"Fold (collapse) all regions\"\n      key: \"[Ctrl] [Shift] [Alt] {[}\"\n    - name: \"Unfold (uncollapse) all regions\"\n      key: \"[Ctrl] [Shift] [Alt] {]}\"\n    - name: \"Add Line Comment\"\n      key: \"[Ctrl] [K], [Ctrl] [C]\"\n    - name: \"Remove Line Comment\"\n      key: \"[Ctrl] [K], [Ctrl] [U]\"\n    - name: \"Toggle Line Comment\"\n      key: \"[Ctrl] [/]\"\n    - name: \"Toggle Block Comment\"\n      key: \"[Shift] [Alt] [A]\"\n    - name: \"Find\"\n      key: \"[Ctrl] [F]\"\n    - name: \"Replace\"\n      key: \"[Ctrl] [H]\"\n    - name: \"Find Next\"\n      key: \"F3\"\n    - name: \"Find Previous\"\n      key: \"[Shift] [F3]\"\n    - name: \"Toggle Find Case Sensitive\"\n      key: \"[Alt] [C]\"\n    - name: \"Toggle Find Regex\"\n      key: \"[Alt] [R]\"\n    - name: \"Toggle Find Whole Word\"\n      key: \"[Alt] [W]\"\n    - name: \"Toggle Use of Tab Key for Setting Focus\"\n      key: \"[Ctrl] [M]\"\n\n- name: vscode (rich language editing)\n  keybinds:\n    - name: \"Trigger Suggest\"\n      key: \"[Ctrl] [Space]\"\n    - name: \"Trigger Parameter Hints\"\n      key: \"[Ctrl] [Shift] [Space]\"\n    - name: \"Format Code\"\n      key: \"[Shift] [Alt] [F]\"\n    - name: \"Go to Definition\"\n      key: \"F12\"\n    - name: \"Peek Definition\"\n      key: \"[Alt] [F12]\"\n    - name: \"Open Definition to the Side\"\n      key: \"[Ctrl] [K], [F12]\"\n    - name: \"Quick Fix\"\n      key: \"[Ctrl] [.]\"\n    - name: \"Show References\"\n      key: \"[Shift] [F12]\"\n    - name: \"Rename Symbol\"\n      key: \"F2\"\n    - name: \"Replace with Next Value\"\n      key: \"[Ctrl] [Shift] [.]\"\n    - name: \"Replace with Previous Value\"\n      key: \"[Ctrl] [Shift] [,]\"\n    - name: \"Expand AST Select\"\n      key: \"[Shift] [Alt] [Right]\"\n    - name: \"Shrink AST Select\"\n      key: \"[Shift] [Alt] [Left]\"\n    - name: \"Trim Trailing Whitespace\"\n      key: \"[Ctrl] [Shift] [X]\"\n    - name: \"Change Language Mode\"\n      key: \"[Ctrl] [K], [M]\"\n\n- name: vscode (navigation)\n  keybinds:\n    - name: \"Show All Symbols\"\n      key: \"[Ctrl] [T]\"\n    - name: \"Go to Line...\"\n      key: \"[Ctrl] [G]\"\n    - name: \"Go to File..., Quick Open\"\n      key: \"[Ctrl] [P]\"\n    - name: \"Go to Symbol...\"\n      key: \"[Ctrl] [Shift] [O]\"\n    - name: \"Show Errors and Warnings\"\n      key: \"[Ctrl] [Shift] [M]\"\n    - name: \"Go to Next Error or Warning\"\n      key: \"F8\"\n    - name: \"Go to Previous Error or Warning\"\n      key: \"[Shift] [F8]\"\n    - name: \"Show All Commands\"\n      key: \"F1\"\n    - name: \"Navigate History\"\n      key: \"[Ctrl] [Tab]\"\n    - name: \"Go Back\"\n      key: \"[Alt] [Left]\"\n    - name: \"Go Forward\"\n      key: \"[Alt] [Right]\"\n\n- name: vscode (editor/window management)\n  keybinds:\n    - name: \"New Window\"\n      key: \"[Ctrl] [Shift] [N]\"\n    - name: \"Close Window\"\n      key: \"[Ctrl] [Shift] [W]\"\n    - name: \"Close Editor\"\n      key: \"[Ctrl] [F4]\"\n    - name: \"Close Folder\"\n      key: \"[Ctrl] [K], [F]\"\n    - name: \"Cycle Between Opened Editors\"\n      key: \"[Ctrl] [`]\"\n    - name: \"Split Editor\"\n      key: \"[Ctrl] [\\\\]\"\n    - name: \"Focus into Left Hand Editor\"\n      key: \"[Ctrl] [1]\"\n    - name: \"Focus into Side Editor\"\n      key: \"[Ctrl] [2]\"\n    - name: \"Focus into Right Hand Editor\"\n      key: \"[Ctrl] [3]\"\n    - name: \"Focus into Next Editor on the Left\"\n      key: \"[Ctrl] [Alt] [Left]\"\n    - name: \"Focus into Next Editor on the Right\"\n      key: \"[Ctrl] [Alt] [Right]\"\n    - name: \"Move Active Editor Left\"\n      key: \"[Ctrl] [K], [Left]\"\n    - name: \"Move Active Editor Right\"\n      key: \"[Ctrl] [K], [Right]\"\n\n- name: vscode (file management)\n  keybinds:\n    - name: \"New File\"\n      key: \"[Ctrl] [N]\"\n    - name: \"Open File...\"\n      key: \"[Ctrl] [O]\"\n    - name: \"Save\"\n      key: \"[Ctrl] [S]\"\n    - name: \"Save As...\"\n      key: \"[Ctrl] [Shift] [S]\"\n    - name: \"Close File\"\n      key: \"[Ctrl] [K], [W]\"\n    - name: \"Close All Files\"\n      key: \"[Ctrl] [K], [Ctrl] [W]\"\n    - name: \"Close Other Files\"\n      key: \"[Ctrl] [K], [Ctrl] [Shift] [W]\"\n    - name: \"Add to Working Files\"\n      key: \"[Ctrl] [K], [Enter]\"\n    - name: \"Open Next Working File\"\n      key: \"[Ctrl] [K], [Down]\"\n    - name: \"Open Previous Working File\"\n      key: \"[Ctrl] [K], [Up]\"\n    - name: \"Copy Path of Active File\"\n      key: \"[Ctrl] [K], [P]\"\n    - name: \"Reveal Active File in Windows\"\n      key: \"[Ctrl] [K], [R]\"\n    - name: \"Show Opened File in New Window\"\n      key: \"[Ctrl] [K], [O]\"\n\n- name: vscode (display)\n  keybinds:\n    - name: \"Toggle Full Screen\"\n      key: \"F11\"\n    - name: \"Zoom in\"\n      key: \"[Ctrl] [=]\"\n    - name: \"Zoom out\"\n      key: \"[Ctrl] [-]\"\n    - name: \"Toggle Sidebar Visibility\"\n      key: \"[Ctrl] [B]\"\n    - name: \"Show Debug\"\n      key: \"[Ctrl] [Shift] [D]\"\n    - name: \"Show Explorer\"\n      key: \"[Ctrl] [Shift] [E]\"\n    - name: \"Show Git\"\n      key: \"[Ctrl] [Shift] [G]\"\n    - name: \"Show Search\"\n      key: \"[Ctrl] [Shift] [F]\"\n    - name: \"Toggle Search Details\"\n      key: \"[Ctrl] [Shift] [J]\"\n    - name: \"Open New Command Prompt\"\n      key: \"[Ctrl] [Shift] [C]\"\n    - name: \"Show Output\"\n      key: \"[Ctrl] [Shift] [U]\"\n    - name: \"Toggle Markdown Preview\"\n      key: \"[Ctrl] [Shift] [V]\"\n    - name: \"Open Preview to the Side\"\n      key: \"[Ctrl] [K], [V]\"\n\n- name: vscode (debug)\n  keybinds:\n    - name: \"Toggle Breakpoint\"\n      key: \"F9\"\n    - name: \"Continue\"\n      key: \"F5\"\n    - name: \"Pause\"\n      key: \"F5\"\n    - name: \"Step Into\"\n      key: \"F11\"\n    - name: \"Step Out\"\n      key: \"[Shift] [F11]\"\n    - name: \"Step Over\"\n      key: \"F10\"\n    - name: \"Stop\"\n      key: \"[Shift] [F5]\"\n    - name: \"Show Hover\"\n      key: \"[Ctrl] [K], [Ctrl] [I]\"\n\n- name: vscode (tasks)\n  keybinds:\n    - name: \"Run Build Task\"\n      key: \"[Ctrl] [Shift] [B]\"\n    - name: \"Run Test Task\"\n      key: \"[Ctrl] [Shift] [T]\"\n"
  },
  {
    "path": "examples/weechat.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: weechat\n  keybinds:\n  - name: \"To set an unread marker on all windows:\"\n    key: \"Ctrl-s Ctrl-u\"\n  - name: \"To switch to the left buffer:\"\n    key: \"Ctrl-p, Alt-left\"\n  - name: \"To switch to the right buffer:\"\n    key: \"Ctrl-n, Alt-right\"\n  - name: \"To switch to the next buffer with activity:\"\n    key: \"Alt-a\"\n  - name: \"To switch buffers:\"\n    key: \"Alt-0...9\"\n  - name: \"To scroll buffer title:\"\n    key: \"F9/F10\"\n  - name: \"To scroll nick list:\"\n    key: \"F11/F12\"\n"
  },
  {
    "path": "examples/zip.yml",
    "content": "# https://github.com/cheat/cheatsheets\n- name: zip\n  keybinds:\n    - name: \"To create zip file\"\n      key: \"zip archive.zip file1 directory/\"\n    - name: \"To create zip file from directory\"\n      key: \"zip -r archive.zip directory/\"\n    - name: \"To create zip file with password\"\n      key: \"zip -P password archive.zip file1\"\n    - name: \"To join a split zip file (.zip, .z01, .z02, etc)\"\n      key: \"zip -FF splitZipfile.zip --out joined.zip\"\n    - name: \"To list, test and extract zip archives, see unzip\"\n      key: \"cheat unzip\""
  },
  {
    "path": "examples/zsh.yml",
    "content": "# ZSH\n# https://github.com/cheat/cheatsheets\n- name: zsh (basic)\n  keybinds:\n    - name: \"A plain old glob\"\n      key: \"print -l *.txt\"\n    - name: \"Show text files that end in a number from 1 to 10\"\n      key: \"print -l **/*<1-10>.txt\"\n    - name: \"Show text files that start with the letter a\"\n      key: \"print -l **/[a]*.txt\"\n    - name: \"Show text files that start with either ab or bc\"\n      key: \"print -l **/(ab|bc)*.txt\"\n    - name: \"Show text files that don't start with a lower or uppercase c\"\n      key: \"print -l **/[^cC]*.txt\"\n    - name: \"Show only directories\"\n      key: \"print -l **/*(/)\"\n    - name: \"Show only regular files\"\n      key: \"print -l **/*(.)\"\n    - name: \"Show empty files\"\n      key: \"print -l **/*(L0)\"\n    - name: \"Show files greater than 3 KB\"\n      key: \"print -l **/*(Lk+3)\"\n    - name: \"Show files modified in the last hour\"\n      key: \"print -l **/*(mh-1)\"\n    - name: \"Sort files from most to least recently modified and show the last 3\"\n      key: \"print -l **/*(om[1,3])\"\n    - name: \"show files, smaller than 2MB, modified in last hour, only first 3 files\"\n      key: \"print -l **/*(.Lm-2mh-1om[1,3])\"\n    - name: \"Show every directory that contain directory `.git`\"\n      key: \"print -l **/*(e:'[[ -d $REPLY/.git ]]':)\"\n    - name: \"Return the file name (t stands for tail)\"\n      key: \"print -l *.txt(:t)\"\n    - name: \"Return the file name without the extension (r stands for remove_extension)\"\n      key: \"print -l *.txt(:t:r)\"\n    - name: \"Return the extension\"\n      key: \"print -l *.txt(:e)\"\n    - name: \"Return the parent folder of the file (h stands for head)\"\n      key: \"print -l *.txt(:h)\"\n    - name: \"Return the parent folder of the parent\"\n      key: \"print -l *.txt(:h:h)\"\n    - name: \"Return the parent folder of the first file\"\n      key: \"print -l *.txt([1]:h)\"\n    - name: \"Split the file name at each underscore\"\n      key: \"echo ${(s._.)file}\"\n    - name: \"Join expansion flag, opposite of the split flag.\"\n      key: \"array=(a b c d)\"\n    - name: \"Short if\"\n      key: \"if [[ ... ]] command\"\n    - name: \"Short for\"\n      key: \"for i in word ... ; command\"\n    - name: \"Short while\"\n      key: \"while [[ ... ]] { command ... }\"\n    - name: \"Short until\"\n      key: \"until [[ ... ]] { command ... }\"\n    - 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.\"\n      key: \"=( command )\"\n- name: zsh (Parameter expansion)\n  keybinds:\n    - name: \"store a glob in a variable\"\n      key: \"files=(*.txt)\"\n    - name: \"store a glob in a variable\"\n      key: \"print -l $files\"\n    - name: \"this is the syntax we saw before\"\n      key: \"print -l $files(:h)\"\n    - name: \"don't mix the two, or you'll get an error\"\n      key: \"print -l ${files(:h)}\"\n    - name: \"the :u modifier makes the text uppercase\"\n      key: \"print -l ${files:u}\"\n- name: zsh (:s modifier)\n  keybinds:\n    - name: \"path/aaaBCd\"\n      key: \"echo ${variable:s/bc/BC/}\"\n    - name: \"path/aaaBCd\"\n      key: \"echo ${variable:s_bc_BC_}\"\n    - name: \"path.aaabcd (escaping the slash /)\"\n      key: \"echo ${variable:s///./}\"\n    - name: \"path.aaabcd (slightly more readable)\"\n      key: \"echo ${variable:s_/_._}\"\n    - name: \"path/aaabcd (only first A is replaced)\"\n      key: \"echo ${variable:s/a/A/}\"\n    - name: \"path/AAAbcd (all A is replaced)\"\n      key: \"echo ${variable:gs/a/A/}\"\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/kencx/keyb\n\ngo 1.26.1\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v1.0.0\n\tgithub.com/charmbracelet/bubbletea v1.3.10\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/juju/ansiterm v1.0.0\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/sahilm/fuzzy v0.1.1\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.3 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.6 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/lunixbochs/vtclean v1.0.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.21 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=\ngithub.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=\ngithub.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg=\ngithub.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=\ngithub.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=\ngithub.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=\ngithub.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\n"
  },
  {
    "path": "main.go",
    "content": "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/config\"\n\t\"github.com/kencx/keyb/output\"\n\t\"github.com/kencx/keyb/ui\"\n)\n\nconst (\n\thelp = `usage: keyb [options] <command>\n\n  Options:\n    -p, --print\t    Print to stdout\n    -e, --export    Export to file [yaml, json]\n    -k, --key       Key bindings at custom path\n    -c, --config    Config file at custom path\n    -v, --version   Version info\n    -h, --help\t    Show help\n\n  Commands:\n    a, add          Add keybind to keyb file\n`\n\n\taddHelp = `usage: keyb [-k file] add [app; name; key]\n\n  Options:\n    -k, --key      Key bindings file at custom path\n    -b, --binding  Key binding\n    -p, --prefix   Ignore prefix\n`\n)\n\nvar version string\n\nfunc main() {\n\tlog.SetPrefix(\"keyb: \")\n\tlog.SetFlags(log.Lshortfile)\n\n\tvar (\n\t\tstdout     bool\n\t\texportFile string\n\t\tkeybFile   string\n\t\tconfigFile string\n\n\t\taddBind   string\n\t\taddPrefix bool\n\t)\n\n\tshortVersion := flag.Bool(\"v\", false, \"version information\")\n\tlongVersion := flag.Bool(\"version\", false, \"version information\")\n\n\tflag.BoolVar(&stdout, \"p\", false, \"print to stdout\")\n\tflag.BoolVar(&stdout, \"print\", false, \"print to stdout\")\n\n\tflag.StringVar(&exportFile, \"e\", \"\", \"export to file\")\n\tflag.StringVar(&exportFile, \"export\", \"\", \"export to file\")\n\n\tflag.StringVar(&keybFile, \"k\", \"\", \"keybindings file\")\n\tflag.StringVar(&keybFile, \"key\", \"\", \"keybindings file\")\n\n\tflag.StringVar(&configFile, \"c\", \"\", \"config file\")\n\tflag.StringVar(&configFile, \"config\", \"\", \"config file\")\n\n\taddCmd := flag.NewFlagSet(\"add\", flag.ExitOnError)\n\taddCmd.StringVar(&addBind, \"b\", \"\", \"keybind\")\n\taddCmd.StringVar(&addBind, \"binding\", \"\", \"keybind\")\n\taddCmd.BoolVar(&addPrefix, \"p\", false, \"prefix\")\n\taddCmd.BoolVar(&addPrefix, \"prefix\", false, \"prefix\")\n\n\tflag.Usage = func() { os.Stdout.Write([]byte(help)) }\n\tflag.Parse()\n\n\tif *shortVersion || *longVersion {\n\t\tfmt.Println(version)\n\t\tos.Exit(0)\n\t}\n\n\tkeys, cfg, err := config.Parse(configFile, keybFile)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\targs := flag.Args()\n\tif len(args) > 1 {\n\t\tswitch args[0] {\n\t\tcase \"add\", \"a\":\n\t\t\taddCmd.Usage = func() { os.Stdout.Write([]byte(addHelp)) }\n\t\t\taddCmd.Parse(args[1:])\n\n\t\t\tvar addFile string\n\t\t\tif keybFile != \"\" {\n\t\t\t\t// use flag -k path\n\t\t\t\taddFile = keybFile\n\t\t\t} else {\n\t\t\t\t// use default path in config\n\t\t\t\taddFile = cfg.KeybPath\n\t\t\t}\n\t\t\tif err := config.AddEntry(addFile, addBind, addPrefix); err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\tfmt.Printf(\"%s added to %s\", addBind, addFile)\n\t\t\tos.Exit(0)\n\t\tdefault:\n\t\t\tfmt.Print(help)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tm := ui.NewModel(keys, cfg)\n\n\tif stdout {\n\t\tif err := output.ToStdout(m); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tos.Exit(0)\n\t}\n\tif exportFile != \"\" {\n\t\tif err := output.ToFile(m, exportFile); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tos.Exit(0)\n\t}\n\n\tif err := start(m); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc start(m *ui.Model) error {\n\n\tp := tea.NewProgram(m, tea.WithMouseCellMotion(), tea.WithAltScreen())\n\n\tif _, err := p.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "output/output.go",
    "content": "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\"\n)\n\nfunc ToFile(m *ui.Model, path string) error {\n\tvar (\n\t\toutput []byte\n\t\terr    error\n\t)\n\n\tpath = os.ExpandEnv(path)\n\text := filepath.Ext(path)\n\n\tswitch ext {\n\tcase \".json\":\n\t\toutput, err = json.Marshal(m.Apps)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal to json: %w\", err)\n\t\t}\n\tcase \".yml\", \".yaml\":\n\t\toutput, err = yaml.Marshal(m.Apps)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal to yaml: %w\", err)\n\t\t}\n\tdefault:\n\t\toutput = []byte(m.List.UnstyledString())\n\t}\n\tif err := os.WriteFile(path, output, 0664); err != nil {\n\t\treturn fmt.Errorf(\"failed to write to file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc ToStdout(m *ui.Model) error {\n\toutput := m.List.UnstyledString()\n\n\t_, err := os.Stdout.Write([]byte(output))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write to stdout: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "output/output_test.go",
    "content": "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.com/kencx/keyb/ui\"\n\t\"github.com/kencx/keyb/ui/list\"\n\t\"github.com/kencx/keyb/ui/table\"\n)\n\nvar (\n\ttestTable  = table.New([]*table.Row{table.NewHeading(\"foo\"), {Text: \"bar\"}})\n\ttestConfig = &config.Config{}\n\ttestApps   = &config.Apps{\n\t\t&config.App{\n\t\t\tName:   \"foo\",\n\t\t\tPrefix: \"bar\",\n\t\t\tKeybinds: []config.KeyBind{\n\t\t\t\t{\n\t\t\t\t\tName: \"key foo\",\n\t\t\t\t\tKey:  \"key bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tm = &ui.Model{List: list.New(testTable, testConfig), Apps: testApps}\n)\n\nfunc TestToJson(t *testing.T) {\n\ttempDir := t.TempDir()\n\tpath := filepath.Join(tempDir, \"test.json\")\n\n\terr := ToFile(m, path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgot, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twant := []byte(`[{\"prefix\":\"bar\",\"name\":\"foo\",\"keybinds\":[{\"name\":\"key foo\",\"key\":\"key bar\"}]}]`)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %s, want %s\", got, want)\n\t}\n}\n\nfunc TestToStdout(t *testing.T) {\n\trescueStdout := os.Stdout\n\tr, w, _ := os.Pipe()\n\tos.Stdout = w\n\n\terr := ToStdout(m)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw.Close()\n\n\tgot, _ := io.ReadAll(r)\n\tos.Stdout = rescueStdout\n\n\twant := \"foo      \\nbar     \"\n\tif string(got) != want {\n\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t}\n}\n"
  },
  {
    "path": "testdata/testConfig.json",
    "content": "{\n  \"settings\": {\n    \"keyb_path\": \"./custom.yml\",\n    \"debug\": true,\n    \"reverse\": true,\n    \"mouse\": false,\n    \"search_mode\": false,\n    \"sort_keys\": true,\n    \"title\": \"\",\n    \"prompt\": \"keys > \",\n    \"prompt_location\": \"bottom\",\n    \"placeholder\": \"...\",\n    \"prefix_sep\": \";\",\n    \"sep_width\": 4,\n    \"margin\": 1,\n    \"padding\": 1,\n    \"border\": \"normal\"\n  },\n  \"color\": {\n    \"prompt\": \"\",\n    \"cursor_fg\": \"\",\n    \"cursor_bg\": \"\",\n    \"filter_fg\": \"#FFA066\",\n    \"filter_bg\": \"\",\n    \"border_color\": \"\"\n  },\n  \"keys\": {\n    \"quit\": \"q, ctrl+c\",\n    \"up\": \"k, up\",\n    \"down\": \"j, down\",\n    \"up_focus\": \"alt+k\",\n    \"down_focus\": \"alt+j\",\n    \"half_up\": \"ctrl+u\",\n    \"half_down\": \"ctrl+d\",\n    \"full_up\": \"ctrl+b\",\n    \"full_bottom\": \"ctrl+f\",\n    \"first_line\": \"g\",\n    \"last_line\": \"G\",\n    \"top\": \"H\",\n    \"middle\": \"M\",\n    \"bottom\": \"L\",\n    \"search\": \"/\",\n    \"clear_search\": \"alt+d\",\n    \"normal\": \"esc\",\n    \"cursor_word_forward\": \"alt+right, alt+f\",\n    \"cursor_word_backward\": \"alt+left, alt+b\",\n    \"cursor_delete_word_backward\": \"alt+backspace\",\n    \"cursor_delete_word_forward\": \"alt+delete\",\n    \"cursor_delete_after_cursor\": \"alt+k\",\n    \"cursor_delete_before_cursor\": \"alt+u\",\n    \"cursor_line_start\": \"home, ctrl+a\",\n    \"cursor_line_end\": \"end, ctrl+e\",\n    \"cursor_paste\": \"ctrl+v\"\n  }\n}\n"
  },
  {
    "path": "testdata/testConfig.yml",
    "content": "settings:\n  keyb_path: \"./custom.yml\"\n  debug: true\n  reverse: true\n  mouse: false\n  search_mode: false\n  sort_keys: true\n  title: \"\"\n  prompt: 'keys > '\n  prompt_location: \"bottom\"\n  placeholder: '...'\n  prefix_sep: ;\n  sep_width: 4\n  margin: 1\n  padding: 1\n  border: normal\ncolor:\n  prompt: \"\"\n  cursor_fg: \"\"\n  cursor_bg: \"\"\n  filter_fg: \"#FFA066\"\n  filter_bg: \"\"\n  border_color: \"\"\nkeys:\n  quit: q, ctrl+c\n  up: k, up\n  down: j, down\n  up_focus: alt+k\n  down_focus: alt+j\n  half_up: ctrl+u\n  half_down: ctrl+d\n  full_up: ctrl+b\n  full_bottom: ctrl+f\n  first_line: g\n  last_line: G\n  top: H\n  middle: M\n  bottom: L\n  search: /\n  clear_search: alt+d\n  normal: esc\n  cursor_word_forward: \"alt+right, alt+f\"\n  cursor_word_backward: \"alt+left, alt+b\"\n  cursor_delete_word_backward: \"alt+backspace\"\n  cursor_delete_word_forward: \"alt+delete\"\n  cursor_delete_after_cursor: \"alt+k\"\n  cursor_delete_before_cursor: \"alt+u\"\n  cursor_line_start: \"home, ctrl+a\"\n  cursor_line_end: \"end, ctrl+e\"\n  cursor_paste: \"ctrl+v\"\n"
  },
  {
    "path": "testdata/testConfigMinimal.json",
    "content": "{\n  \"settings\": {}\n}\n"
  },
  {
    "path": "testdata/testConfigMinimal.yml",
    "content": "---\nsettings:\n"
  },
  {
    "path": "testdata/testkeyb.json",
    "content": "[\n  {\n    \"name\": \"test\",\n    \"keybinds\": [\n      {\n        \"name\": \"foo\",\n        \"key\": \"bar\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "testdata/testkeyb.yml",
    "content": "- name: test\n  keybinds:\n  - name: foo\n    key: bar\n"
  },
  {
    "path": "ui/list/keymap.go",
    "content": "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 KeyMap struct {\n\tQuit key.Binding\n\n\tUp            key.Binding\n\tDown          key.Binding\n\tHalfUp        key.Binding\n\tHalfDown      key.Binding\n\tFullUp        key.Binding\n\tFullDown      key.Binding\n\tUpFocus       key.Binding\n\tDownFocus     key.Binding\n\tGoToFirstLine key.Binding\n\tGoToLastLine  key.Binding\n\tGoToTop       key.Binding\n\tGoToMiddle    key.Binding\n\tGoToBottom    key.Binding\n\n\tCenterCursor key.Binding\n\n\tSearch      key.Binding\n\tClearSearch key.Binding\n\tNormal      key.Binding\n\n\tTextInputKeyMap\n}\n\ntype TextInputKeyMap struct {\n\tCharacterForward        key.Binding\n\tCharacterBackward       key.Binding\n\tWordForward             key.Binding\n\tWordBackward            key.Binding\n\tDeleteWordBackward      key.Binding\n\tDeleteWordForward       key.Binding\n\tDeleteAfterCursor       key.Binding\n\tDeleteBeforeCursor      key.Binding\n\tDeleteCharacterBackward key.Binding\n\tDeleteCharacterForward  key.Binding\n\tLineStart               key.Binding\n\tLineEnd                 key.Binding\n\tPaste                   key.Binding\n\tAcceptSuggestion        key.Binding\n\tNextSuggestion          key.Binding\n\tPrevSuggestion          key.Binding\n}\n\nfunc CreateKeyMap(keys config.Keys) KeyMap {\n\treturn KeyMap{\n\t\tQuit:          SetKey(keys.Quit),\n\t\tUp:            SetKey(keys.Up),\n\t\tDown:          SetKey(keys.Down),\n\t\tHalfUp:        SetKey(keys.HalfUp),\n\t\tHalfDown:      SetKey(keys.HalfDown),\n\t\tFullUp:        SetKey(keys.FullUp),\n\t\tFullDown:      SetKey(keys.FullDown),\n\t\tUpFocus:       SetKey(keys.UpFocus),\n\t\tDownFocus:     SetKey(keys.DownFocus),\n\t\tGoToFirstLine: SetKey(keys.GoToFirstLine),\n\t\tGoToLastLine:  SetKey(keys.GoToLastLine),\n\t\tGoToTop:       SetKey(keys.GoToTop),\n\t\tGoToMiddle:    SetKey(keys.GoToMiddle),\n\t\tGoToBottom:    SetKey(keys.GoToBottom),\n\n\t\tSearch:      SetKey(keys.Search),\n\t\tClearSearch: SetKey(keys.ClearSearch),\n\t\tNormal:      SetKey(keys.Normal),\n\n\t\tTextInputKeyMap: TextInputKeyMap{\n\t\t\tCharacterForward:        SetKey(\"right\"),\n\t\t\tCharacterBackward:       SetKey(\"left\"),\n\t\t\tWordForward:             SetKey(keys.CursorWordForward),\n\t\t\tWordBackward:            SetKey(keys.CursorWordBackward),\n\t\t\tDeleteWordBackward:      SetKey(keys.CursorDeleteWordBackward),\n\t\t\tDeleteWordForward:       SetKey(keys.CursorDeleteWordForward),\n\t\t\tDeleteAfterCursor:       SetKey(keys.CursorDeleteAfterCursor),\n\t\t\tDeleteBeforeCursor:      SetKey(keys.CursorDeleteBeforeCursor),\n\t\t\tDeleteCharacterBackward: SetKey(\"backspace\"),\n\t\t\tDeleteCharacterForward:  SetKey(\"delete\"),\n\t\t\tLineStart:               SetKey(keys.CursorLineStart),\n\t\t\tLineEnd:                 SetKey(keys.CursorLineEnd),\n\t\t\tPaste:                   SetKey(keys.CursorPaste),\n\t\t\tAcceptSuggestion:        SetKey(\"tab\"),\n\t\t\tNextSuggestion:          SetKey(\"ctrl+n\"),\n\t\t\tPrevSuggestion:          SetKey(\"ctrl+p\"),\n\t\t},\n\t}\n}\n\nfunc SetKey(s string) key.Binding {\n\treturn key.NewBinding(\n\t\tkey.WithKeys(splitAndTrim(s, \",\")...),\n\t)\n}\n\nfunc splitAndTrim(s, sep string) []string {\n\tsl := strings.Split(s, sep)\n\tfor i := range sl {\n\t\tsl[i] = strings.TrimSpace(sl[i])\n\t}\n\treturn sl\n}\n"
  },
  {
    "path": "ui/list/list.go",
    "content": "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/bubbles/cursor\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype filterState int\n\nconst (\n\tunfiltered filterState = iota\n\tfiltering\n)\n\ntype Model struct {\n\tkeys     KeyMap\n\tviewport viewport.Model\n\ttable    *table.Model\n\n\tsearchBar         textinput.Model\n\tsearch            bool\n\tstartInSearchMode bool\n\n\tfilterState    filterState\n\tfilteredTable  *table.Model\n\tcurrentHeading string\n\n\ttitle   string\n\tdebug   bool\n\tcursor  int\n\tmaxRows int // max number of rows regardless of filterState\n\n\tmargin         int\n\tpadding        int\n\tscrollOffset   int\n\tborder         lipgloss.Style\n\tcounterStyle   lipgloss.Style\n\tpromptLocation string\n}\n\nfunc New(t *table.Model, c *config.Config) Model {\n\tkeyMap := CreateKeyMap(c.Keys)\n\n\tm := Model{\n\t\tkeys: keyMap,\n\t\tviewport: viewport.Model{\n\t\t\tYOffset:           0,\n\t\t\tMouseWheelDelta:   3,\n\t\t\tMouseWheelEnabled: c.Mouse,\n\t\t},\n\t\ttable: t,\n\n\t\tsearchBar: textinput.Model{\n\t\t\tPrompt:           c.Prompt,\n\t\t\tPromptStyle:      lipgloss.NewStyle().Foreground(lipgloss.Color(c.PromptColor)),\n\t\t\tPlaceholder:      c.Placeholder,\n\t\t\tPlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\t\tEchoCharacter:    '*',\n\t\t\tCharLimit:        0,\n\t\t\tCursor:           cursor.New(),\n\t\t\tKeyMap:           textinput.KeyMap(keyMap.TextInputKeyMap),\n\t\t\tShowSuggestions:  false,\n\t\t},\n\t\tstartInSearchMode: c.SearchMode,\n\n\t\tfilteredTable: table.NewEmpty(t.LineCount),\n\n\t\ttitle:   c.Title,\n\t\tdebug:   c.Debug,\n\t\tcursor:  0,\n\t\tmaxRows: t.LineCount,\n\n\t\tmargin:         c.Margin,\n\t\tpadding:        c.Padding,\n\t\tscrollOffset:   5,\n\t\tcounterStyle:   lipgloss.NewStyle().Faint(true).Margin(0, 1),\n\t\tpromptLocation: c.PromptLocation,\n\t}\n\n\tm.table.SepWidth = c.SepWidth\n\tm.filteredTable.SepWidth = c.SepWidth\n\tm.scrollOffset += (m.margin * 2) + (m.padding * 2)\n\tm.style(c)\n\n\tif m.startInSearchMode {\n\t\tm.startSearch()\n\t}\n\n\treturn m\n}\n\nfunc (m *Model) style(c *config.Config) {\n\tif c.PlaceholderFg != \"\" || c.PlaceholderBg != \"\" {\n\t\tm.searchBar.PlaceholderStyle = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(c.PlaceholderFg)).\n\t\t\tBackground(lipgloss.Color(c.PlaceholderBg))\n\t}\n\n\tif c.CounterFg != \"\" || c.CounterBg != \"\" {\n\t\tm.counterStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(c.CounterFg)).Background(lipgloss.Color(c.CounterBg)).Margin(0, 1)\n\t}\n\n\tvar b lipgloss.Border\n\tswitch c.BorderStyle {\n\tcase \"normal\":\n\t\tb = lipgloss.NormalBorder()\n\tcase \"rounded\":\n\t\tb = lipgloss.RoundedBorder()\n\tcase \"double\":\n\t\tb = lipgloss.DoubleBorder()\n\tcase \"thick\":\n\t\tb = lipgloss.ThickBorder()\n\tdefault:\n\t\tb = lipgloss.HiddenBorder()\n\t}\n\tm.border = lipgloss.NewStyle().BorderStyle(b).BorderForeground(lipgloss.Color(c.BorderColor))\n\n\t// row specific config\n\tif !m.table.Empty() {\n\t\tcursor := lipgloss.NewStyle().Bold(true).\n\t\t\tForeground(lipgloss.Color(c.CursorFg)).\n\t\t\tBackground(lipgloss.Color(c.CursorBg))\n\n\t\ts := table.RowStyles{\n\t\t\tNormal:          lipgloss.NewStyle().Margin(0, 2).TabWidth(lipgloss.NoTabConversion),\n\t\t\tHeading:         lipgloss.NewStyle().Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion),\n\t\t\tSelected:        cursor.Margin(0, 2).TabWidth(lipgloss.NoTabConversion),\n\t\t\tSelectedHeading: cursor.Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion),\n\t\t\tFiltered: lipgloss.NewStyle().\n\t\t\t\tForeground(lipgloss.Color(c.FilterFg)).\n\t\t\t\tBackground(lipgloss.Color(c.FilterBg)).\n\t\t\t\tTabWidth(lipgloss.NoTabConversion),\n\t\t}\n\n\t\tfor _, row := range m.table.Rows {\n\t\t\trow.PrefixSep = c.PrefixSep\n\t\t\trow.Reversed = c.Reverse\n\t\t\trow.Styles = s\n\t\t}\n\t}\n}\n\nfunc (m *Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m *Model) Resize(width, height int) {\n\tm.viewport.Width = width\n\tm.viewport.Height = height - m.scrollOffset\n}\n\n// Resets list to unfiltered state\nfunc (m *Model) Reset() {\n\tm.filteredTable.Reset()\n\tm.filterState = unfiltered\n\tm.currentHeading = \"\"\n\tm.cursorToBeginning()\n\tm.visibleRows()\n}\n\n// Sets items to be shown. All items are shown unless filtered\nfunc (m *Model) visibleRows() {\n\tif !m.filteredTable.Empty() {\n\t\tm.SyncContent(m.filteredTable)\n\t} else {\n\t\tm.SyncContent(m.table)\n\t}\n}\n\n// Sync content by updating cursor and visible rows\nfunc (m *Model) SyncContent(table *table.Model) {\n\tif table.Empty() {\n\t\tm.viewport.SetContent(\"\")\n\t\treturn\n\t}\n\n\tif m.cursor > table.LineCount {\n\t\tm.cursor = table.LineCount - 1\n\t}\n\n\tfor i, row := range table.Rows {\n\t\tif i == m.cursor {\n\t\t\trow.IsSelected = true\n\t\t\tm.currentHeading = row.Heading\n\t\t} else {\n\t\t\trow.IsSelected = false\n\t\t}\n\t}\n\tm.viewport.SetContent(table.Render())\n\tm.maxRows = table.LineCount\n}\n\nfunc (m *Model) UnstyledString() string {\n\treturn m.table.GetAlignedRows()\n}\n\nfunc (m *Model) searchMode() bool {\n\treturn m.search && m.searchBar.Focused()\n}\n\nfunc (m *Model) startSearch() tea.Cmd {\n\tm.search = true\n\tm.filterState = filtering\n\tm.searchBar.Focus()\n\treturn textinput.Blink\n}\n\nfunc (m *Model) cursorToBeginning() {\n\tm.cursor = 0\n}\n\nfunc (m *Model) cursorToEnd() {\n\tm.cursor = m.maxRows - 1\n}\n\nfunc (m *Model) cursorToViewTop() {\n\tm.cursor = m.viewport.YOffset + 3\n}\n\nfunc (m *Model) cursorToViewMiddle() {\n\tm.cursor = (m.viewport.YOffset + m.viewport.Height) / 2\n}\n\nfunc (m *Model) cursorToViewBottom() {\n\tm.cursor = m.viewport.YOffset + m.viewport.Height - 3\n}\n\nfunc (m *Model) cursorPastViewTop() bool {\n\treturn m.cursor < m.viewport.YOffset\n}\n\nfunc (m *Model) cursorPastViewBottom() bool {\n\treturn m.cursor > m.viewport.YOffset+m.viewport.Height-1\n}\n\nfunc (m *Model) cursorPastBeginning() bool {\n\treturn m.cursor < 0\n}\n\nfunc (m *Model) cursorPastEnd() bool {\n\treturn m.cursor > m.maxRows-1\n}\n"
  },
  {
    "path": "ui/list/list_test.go",
    "content": "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 = []*table.Row{\n\t\ttable.NewHeading(\"fooTable\"),\n\t\t{Text: \"foo\"},\n\t\t{Text: \"bar\"},\n\t\t{Text: \"baz\"},\n\t}\n\ttestTable  = table.New(testRows)\n\ttestConfig = &config.Config{\n\t\tSettings: config.Settings{\n\t\t\tTitle:       \"foo\",\n\t\t\tDebug:       true,\n\t\t\tReverse:     true,\n\t\t\tMouse:       true,\n\t\t\tPrompt:      \"prompt\",\n\t\t\tPlaceholder: \"placeholder\",\n\t\t\tPrefixSep:   \"$\",\n\t\t\tSepWidth:    4,\n\t\t}}\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Run(\"populated\", func(t *testing.T) {\n\t\ttm := New(testTable, testConfig)\n\n\t\tassertEqual(t, tm.table.LineCount, 4)\n\t\tassertEqual(t, tm.filterState, unfiltered)\n\t\tassertEqual(t, tm.filteredTable.LineCount, 0)\n\n\t\tassertEqual(t, tm.title, \"foo\")\n\t\tassertEqual(t, tm.debug, true)\n\t\tassertEqual(t, tm.table.Rows[0].Reversed, true)\n\t\tassertEqual(t, tm.viewport.MouseWheelEnabled, true)\n\t\tassertEqual(t, tm.searchBar.Prompt, \"prompt\")\n\t\tassertEqual(t, tm.searchBar.Placeholder, \"placeholder\")\n\t\tassertEqual(t, tm.table.Rows[0].PrefixSep, \"$\")\n\t\tassertEqual(t, tm.table.SepWidth, 4)\n\t})\n\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\ttm := New(table.New([]*table.Row{table.EmptyRow(), table.EmptyRow()}), testConfig)\n\n\t\tassertEqual(t, tm.title, \"foo\")\n\t\tassertEqual(t, tm.table.LineCount, 0)\n\t\tassertEqual(t, tm.filterState, unfiltered)\n\t\tassertEqual(t, tm.filteredTable.LineCount, 0)\n\t})\n\n}\n\nfunc TestReset(t *testing.T) {\n\n\ttm := New(testTable, testConfig)\n\ttm.filterState = filtering\n\ttm.searchBar.SetValue(\"searching...\")\n\ttm.cursor = 10\n\n\ttm.Reset()\n\n\tassertEqual(t, tm.filteredTable.Render(), \"\")\n\tassertEqual(t, tm.searchBar.Value(), \"searching...\")\n\tassertEqual(t, tm.filterState, unfiltered)\n\tassertEqual(t, tm.cursor, 0)\n\tassertEqual(t, tm.maxRows, tm.table.LineCount)\n}\n\nfunc assertEqual[T comparable](t *testing.T, got, want T) {\n\tif got != want {\n\t\tt.Errorf(\"got %#v, want %#v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "ui/list/update.go",
    "content": "package list\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/kencx/keyb/ui/table\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\n\tvar cmds []tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\n\t\t// to play nice with borders and truncation,\n\t\t// <2 results in border exceeding max width\n\t\tm.viewport.Width = msg.Width - max(2, (m.padding*2+m.margin*2))\n\t\tm.viewport.Height = msg.Height - m.scrollOffset\n\n\t\tm.table.MaxWidth = m.viewport.Width - m.padding*2\n\t\tm.filteredTable.MaxWidth = m.viewport.Width - m.padding*2\n\n\t\tif m.cursorPastViewBottom() {\n\t\t\tm.cursor = m.viewport.YOffset + m.viewport.Height - 1\n\t\t}\n\n\tcase tea.MouseMsg:\n\t\tif !m.viewport.MouseWheelEnabled {\n\t\t\tbreak\n\t\t}\n\t\tswitch msg.Button {\n\t\tcase tea.MouseButtonWheelUp:\n\t\t\tm.cursor -= m.viewport.MouseWheelDelta\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.ScrollUp(m.viewport.MouseWheelDelta)\n\t\t\t}\n\t\tcase tea.MouseButtonWheelDown:\n\t\t\tm.cursor += m.viewport.MouseWheelDelta\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.ScrollDown(m.viewport.MouseWheelDelta)\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch {\n\tcase m.searchMode():\n\t\tcmds = append(cmds, m.handleSearch(msg))\n\tdefault:\n\t\tcmds = append(cmds, m.handleNormal(msg))\n\t}\n\n\t// cursor loop around\n\tif m.cursorPastBeginning() {\n\t\tm.cursorToEnd()\n\t\tm.viewport.GotoBottom()\n\t} else if m.cursorPastEnd() {\n\t\tm.cursorToBeginning()\n\t\tm.viewport.GotoTop()\n\t}\n\n\tm.visibleRows()\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *Model) handleNormal(msg tea.Msg) tea.Cmd {\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.keys.Quit):\n\t\t\treturn tea.Quit\n\n\t\tcase key.Matches(msg, m.keys.Search):\n\t\t\treturn m.startSearch()\n\n\t\tcase key.Matches(msg, m.keys.ClearSearch):\n\t\t\tm.searchBar.Reset()\n\t\t\treturn m.startSearch()\n\n\t\tcase key.Matches(msg, m.keys.Up):\n\t\t\tm.cursor--\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.ScrollUp(1)\n\t\t\t}\n\t\tcase key.Matches(msg, m.keys.Down):\n\t\t\tm.cursor++\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.ScrollDown(1)\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.UpFocus):\n\t\t\tm.cursor--\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.ScrollUp(1)\n\t\t\t}\n\t\tcase key.Matches(msg, m.keys.DownFocus):\n\t\t\tm.cursor++\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.ScrollDown(1)\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.HalfUp):\n\t\t\tm.cursor -= m.viewport.Height / 2\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.HalfPageUp()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastBeginning() {\n\t\t\t\tm.cursorToBeginning()\n\t\t\t\tm.viewport.GotoTop()\n\t\t\t}\n\t\tcase key.Matches(msg, m.keys.HalfDown):\n\t\t\tm.cursor += m.viewport.Height / 2\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.HalfPageDown()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastEnd() {\n\t\t\t\tm.cursorToEnd()\n\t\t\t\tm.viewport.GotoBottom()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.FullUp):\n\t\t\tm.cursor -= m.viewport.Height\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.PageUp()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastBeginning() {\n\t\t\t\tm.cursorToBeginning()\n\t\t\t\tm.viewport.GotoTop()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.FullDown):\n\t\t\tm.cursor += m.viewport.Height\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.PageDown()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastEnd() {\n\t\t\t\tm.cursorToEnd()\n\t\t\t\tm.viewport.GotoBottom()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.GoToFirstLine):\n\t\t\tm.cursorToBeginning()\n\t\t\tm.viewport.GotoTop()\n\t\tcase key.Matches(msg, m.keys.GoToLastLine):\n\t\t\tm.cursorToEnd()\n\t\t\tm.viewport.GotoBottom()\n\n\t\tcase key.Matches(msg, m.keys.GoToTop):\n\t\t\tm.cursorToViewTop()\n\n\t\tcase key.Matches(msg, m.keys.GoToMiddle):\n\t\t\tm.cursorToViewMiddle()\n\n\t\tcase key.Matches(msg, m.keys.GoToBottom):\n\t\t\tm.cursorToViewBottom()\n\n\t\t\t// case key.Matches(msg, m.keys.CenterCursor):\n\t\t\t// \tmiddle := m.viewport.Height / 2\n\t\t\t// \tdiff := m.cursor - middle\n\t\t\t// \tif diff > 0 {\n\t\t\t// \t\tm.viewport.LineDown(diff)\n\t\t\t// \t} else {\n\t\t\t// \t\tm.viewport.LineUp(diff)\n\t\t\t// \t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *Model) handleSearch(msg tea.Msg) tea.Cmd {\n\n\tvar (\n\t\tcmds []tea.Cmd\n\t\tcmd  tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch {\n\t\tcase msg.String() == \"ctrl+c\":\n\t\t\treturn tea.Quit\n\n\t\tcase key.Matches(msg, m.keys.ClearSearch):\n\t\t\tm.searchBar.Reset()\n\t\t\treturn m.startSearch()\n\n\t\t\t// scrolling in search mode\n\t\tcase key.Matches(msg, m.keys.UpFocus):\n\t\t\tm.cursor--\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.ScrollUp(1)\n\t\t\t}\n\t\t\treturn nil\n\t\tcase key.Matches(msg, m.keys.DownFocus):\n\t\t\tm.cursor++\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.ScrollDown(1)\n\t\t\t}\n\t\t\treturn nil\n\n\t\tcase key.Matches(msg, m.keys.HalfUp):\n\t\t\tm.cursor -= m.viewport.Height / 2\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.HalfPageUp()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastBeginning() {\n\t\t\t\tm.cursorToBeginning()\n\t\t\t\tm.viewport.GotoTop()\n\t\t\t}\n\t\tcase key.Matches(msg, m.keys.HalfDown):\n\t\t\tm.cursor += m.viewport.Height / 2\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.HalfPageDown()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastEnd() {\n\t\t\t\tm.cursorToEnd()\n\t\t\t\tm.viewport.GotoBottom()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.FullUp):\n\t\t\tm.cursor -= m.viewport.Height\n\t\t\tif m.cursorPastViewTop() {\n\t\t\t\tm.viewport.PageUp()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastBeginning() {\n\t\t\t\tm.cursorToBeginning()\n\t\t\t\tm.viewport.GotoTop()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.FullDown):\n\t\t\tm.cursor += m.viewport.Height\n\t\t\tif m.cursorPastViewBottom() {\n\t\t\t\tm.viewport.PageDown()\n\t\t\t}\n\n\t\t\t// don't loop around\n\t\t\tif m.cursorPastEnd() {\n\t\t\t\tm.cursorToEnd()\n\t\t\t\tm.viewport.GotoBottom()\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.keys.Normal):\n\t\t\tm.search = false\n\t\t\tm.searchBar.Blur()\n\n\t\t\tif m.filteredTable.Empty() {\n\t\t\t\tm.filterState = unfiltered\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// filter with search input\n\tm.searchBar, cmd = m.searchBar.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tprefix := \"h:\"\n\tif strings.HasPrefix(m.searchBar.Value(), prefix) {\n\t\tmatchHeadings(m, prefix)\n\t} else {\n\t\tmatchRows(m)\n\t}\n\n\t// reset if search input is empty regardless of filterState\n\tif m.searchBar.Value() == \"\" {\n\t\tm.Reset()\n\n\t\t// remain in filtering state\n\t\t// until user explicitly returns to Normal mode\n\t\tm.filterState = filtering\n\t}\n\treturn tea.Batch(cmds...)\n}\n\nfunc filter(term string, target []string) fuzzy.Matches {\n\tmatches := fuzzy.Find(term, target)\n\tsort.Stable(matches)\n\treturn matches\n}\n\nfunc matchHeadings(m *Model, prefix string) {\n\tvar matches fuzzy.Matches\n\tvalue := strings.TrimSpace(strings.TrimPrefix(m.searchBar.Value(), prefix))\n\tif !m.table.Empty() {\n\t\tmatches = filter(value, m.table.GetPlainHeadings())\n\t}\n\n\t// present new filtered rows\n\tm.filteredTable.Reset()\n\tif len(matches) == 0 {\n\t\tm.filteredTable.AppendRow(table.EmptyRow())\n\n\t} else {\n\t\tvar hlMatches []*table.Row\n\t\t// get non-pointers as filtering is ephemeral\n\t\theadings := m.table.GetCopyOfHeadings()\n\n\t\tfor _, match := range matches {\n\t\t\theading := headings[match.Index]\n\t\t\theading.IsFiltered = true\n\t\t\theading.MatchedIndex = match.MatchedIndexes\n\n\t\t\thlMatches = append(hlMatches, &heading)\n\t\t\thlMatches = append(hlMatches, m.table.GetAllRowsofHeading(heading.Text)...)\n\t\t}\n\t\tm.filteredTable.AppendRows(hlMatches...)\n\t}\n}\n\nfunc matchRows(m *Model) {\n\tvar matches fuzzy.Matches\n\tif !m.table.Empty() {\n\t\tmatches = filter(m.searchBar.Value(), m.table.GetPlainRowsWithoutHeadings())\n\t}\n\n\t// present new filtered rows\n\tm.filteredTable.Reset()\n\tif len(matches) == 0 {\n\t\tm.filteredTable.AppendRow(table.EmptyRow())\n\n\t} else {\n\t\tvar hlMatches []*table.Row\n\t\t// get non-pointers as filtering is ephemeral\n\t\trows := m.table.GetCopyOfRowsWithoutHeadings()\n\n\t\tfor _, match := range matches {\n\t\t\trow := rows[match.Index]\n\t\t\trow.IsFiltered = true\n\t\t\trow.MatchedIndex = match.MatchedIndexes\n\t\t\thlMatches = append(hlMatches, &row)\n\t\t}\n\t\tm.filteredTable.AppendRows(hlMatches...)\n\t}\n}\n"
  },
  {
    "path": "ui/list/view.go",
    "content": "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() {\n\t\tm.viewport.SetContent(\"\\nNo key bindings found\")\n\t}\n\n\tcounter := formCounter(m)\n\n\tvar view string\n\tif m.promptLocation == \"bottom\" {\n\t\tview = lipgloss.JoinVertical(\n\t\t\tlipgloss.Left,\n\t\t\tm.viewport.View(),\n\t\t\tcounter,\n\t\t\tm.searchBar.View(),\n\t\t)\n\t} else {\n\t\tview = lipgloss.JoinVertical(\n\t\t\tlipgloss.Left,\n\t\t\tm.searchBar.View(),\n\t\t\tcounter,\n\t\t\tm.viewport.View(),\n\t\t)\n\t}\n\n\tstyle := m.border.\n\t\tMargin(m.margin).\n\t\tPadding(m.padding).\n\t\tWidth(m.viewport.Width)\n\treturn style.Render(view)\n}\n\nfunc formCounter(m *Model) string {\n\tvar counter string\n\n\tif m.filterState == filtering && m.searchBar.Value() != \"\" {\n\t\tcounter = fmt.Sprintf(\"%d/%d %s\", m.filteredTable.LineCount, m.table.LineCount, m.currentHeading)\n\t} else {\n\t\tcounter = fmt.Sprintf(\"%d/%d %s\", m.table.LineCount, m.table.LineCount, m.currentHeading)\n\t}\n\n\tif m.debug {\n\t\tcounter = fmt.Sprintf(\"%s\\tLine: %d YOffset: %d Height: %d\",\n\t\t\tcounter, m.cursor, m.viewport.YOffset, m.viewport.Height)\n\t}\n\treturn m.counterStyle.Render(counter)\n}\n"
  },
  {
    "path": "ui/table/row.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype Row struct {\n\tText      string\n\tKey       string\n\tPrefix    string\n\tPrefixSep string\n\n\t// default false unless prefix defined\n\tShowPrefix bool\n\t// only used to show row's corresponding heading during filtering\n\tHeading string\n\n\tMatchedIndex []int\n\tStyles       RowStyles\n\n\tIsHeading  bool\n\tIsSelected bool\n\tIsFiltered bool\n\tReversed   bool\n}\n\ntype RowStyles struct {\n\tNormal          lipgloss.Style\n\tHeading         lipgloss.Style\n\tSelected        lipgloss.Style\n\tSelectedHeading lipgloss.Style\n\tFiltered        lipgloss.Style\n}\n\nfunc NewHeading(text string) *Row {\n\treturn &Row{\n\t\tText:      text,\n\t\tIsHeading: true,\n\t}\n}\n\nfunc NewRow(text, key, prefix, heading string) *Row {\n\tr := &Row{\n\t\tText:    text,\n\t\tKey:     key,\n\t\tPrefix:  prefix,\n\t\tHeading: heading,\n\t}\n\tr.ShowPrefix = r.Prefix != \"\"\n\treturn r\n}\n\nfunc EmptyRow() *Row {\n\treturn &Row{}\n}\n\nfunc (r *Row) String() string {\n\tif r.Text == \"\" && r.Key == \"\" {\n\t\treturn \"\"\n\t}\n\n\tif r.IsHeading {\n\t\treturn fmt.Sprintf(\"%s\\t \", r.Text)\n\t}\n\n\tif r.Reversed {\n\t\treturn r.ReverseString()\n\t}\n\n\tif !r.ShowPrefix {\n\t\treturn fmt.Sprintf(\"%s\\t%s\", r.Text, r.Key)\n\t}\n\n\tkey := fmt.Sprintf(\"%s %s %s\", r.Prefix, r.PrefixSep, r.Key)\n\treturn fmt.Sprintf(\"%s\\t%s\", r.Text, key)\n}\n\nfunc (r *Row) ReverseString() string {\n\tif !r.ShowPrefix {\n\t\treturn fmt.Sprintf(\"%s\\t%s\", r.Key, r.Text)\n\t}\n\n\tkey := fmt.Sprintf(\"%s %s %s\", r.Prefix, r.PrefixSep, r.Key)\n\treturn fmt.Sprintf(\"%s\\t%s\", key, r.Text)\n}\n\nfunc (r *Row) Render() string {\n\ts := r.Styles\n\n\tif r.IsSelected {\n\t\tif r.IsFiltered {\n\t\t\t// Inline to remove margins, paddings and borders from styledrunes\n\t\t\tunmatched := s.Selected.Inline(true)\n\t\t\tmatched := s.Filtered.Inherit(unmatched)\n\t\t\tstr := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched)\n\n\t\t\tif r.IsHeading {\n\t\t\t\treturn s.SelectedHeading.Render(str)\n\t\t\t}\n\t\t\treturn s.Selected.Render(str)\n\t\t}\n\n\t\tif r.IsHeading {\n\t\t\treturn s.SelectedHeading.Render(r.String())\n\t\t}\n\t\treturn s.Selected.Render(r.String())\n\t}\n\n\tif r.IsFiltered {\n\t\tunmatched := s.Normal.Inline(true)\n\t\tmatched := s.Filtered.Inherit(unmatched)\n\t\tstr := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched)\n\n\t\tif r.IsHeading {\n\t\t\treturn s.Heading.Render(str)\n\t\t}\n\t\treturn s.Normal.Render(str)\n\t}\n\n\tif r.IsHeading {\n\t\treturn s.Heading.Render(r.String())\n\t}\n\treturn s.Normal.Render(r.String())\n}\n"
  },
  {
    "path": "ui/table/table.go",
    "content": "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\ntype Model struct {\n\tRows      []*Row\n\tLineCount int\n\tSepWidth  int\n\tMaxWidth  int // prevents line wrapping\n}\n\nfunc New(rows []*Row) *Model {\n\tt := &Model{\n\t\tRows: rows,\n\t}\n\n\tfor _, row := range rows {\n\t\tif row.String() != \"\" {\n\t\t\tt.LineCount += 1\n\t\t}\n\t}\n\treturn t\n}\n\nfunc NewEmpty(n int) *Model {\n\treturn &Model{\n\t\tRows:      make([]*Row, 1, max(2, n)),\n\t\tLineCount: 0,\n\t}\n}\n\nfunc (t *Model) Empty() bool {\n\treturn t.LineCount <= 0\n}\n\nfunc (t *Model) AppendRow(row *Row) {\n\tt.Rows = append(t.Rows, row)\n\tt.LineCount += 1\n}\n\nfunc (t *Model) AppendRows(rows ...*Row) {\n\tt.Rows = append(t.Rows, rows...)\n\tt.LineCount += len(rows)\n}\n\nfunc (t *Model) Join(table *Model) {\n\tt.Rows = append(t.Rows, table.Rows...)\n\tt.LineCount += table.LineCount\n}\n\nfunc (t *Model) Reset() {\n\tt.Rows = nil\n\tt.LineCount = 0\n}\n\n// Align and style rows\nfunc (t *Model) Render() string {\n\tvar sb strings.Builder\n\ttw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0)\n\n\tfor _, row := range t.Rows {\n\t\tif row != nil && row.String() != \"\" {\n\t\t\tfmt.Fprintln(tw, row.Render())\n\t\t}\n\t}\n\ttw.Flush()\n\n\tif sb.String() != \"\" {\n\t\tsl := strings.Split(strings.TrimSuffix(sb.String(), \"\\n\"), \"\\n\")\n\t\tsb.Reset()\n\n\t\t// Unable to truncate while aligning due to nature of tabwriter\n\t\tfor _, row := range sl {\n\t\t\tif t.MaxWidth > 0 {\n\t\t\t\tfmt.Fprintln(&sb, truncate.StringWithTail(row, uint(t.MaxWidth), \"...\"))\n\t\t\t} else {\n\t\t\t\tfmt.Fprintln(&sb, row)\n\t\t\t}\n\t\t}\n\t}\n\treturn sb.String()\n}\n\n// Helper functions for retrieving specific rows in a table\n// Aligned but unstyled rows\nfunc (t *Model) GetAlignedRows() string {\n\tvar sb strings.Builder\n\ttw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0)\n\n\tfor _, row := range t.Rows {\n\t\tif row != nil && row.String() != \"\" {\n\t\t\tfmt.Fprintln(tw, row.String())\n\t\t}\n\t}\n\n\ttw.Flush()\n\treturn strings.TrimSuffix(sb.String(), \"\\n\")\n}\n\nfunc (t *Model) GetPlainHeadings() []string {\n\tvar res []string\n\tfor _, r := range t.Rows {\n\t\tif r.IsHeading {\n\t\t\tres = append(res, r.String())\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (t *Model) GetPlainRowsWithoutHeadings() []string {\n\tvar res []string\n\tfor _, r := range t.Rows {\n\t\tif !r.IsHeading {\n\t\t\tres = append(res, r.String())\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (t *Model) GetCopyOfHeadings() []Row {\n\tvar res []Row\n\tfor _, r := range t.Rows {\n\t\tif r.IsHeading {\n\t\t\tres = append(res, *r)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (t *Model) GetCopyOfRowsWithoutHeadings() []Row {\n\tvar res []Row\n\tfor _, r := range t.Rows {\n\t\tif !r.IsHeading {\n\t\t\tres = append(res, *r)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (t *Model) GetAllRowsofHeading(heading string) []*Row {\n\tvar res []*Row\n\tfor _, r := range t.Rows {\n\t\tif !r.IsHeading && r.Heading == heading {\n\t\t\tres = append(res, r)\n\t\t}\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "ui/table/table_test.go",
    "content": "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{Text: \"baz\", Key: \"foo\"},\n\t}\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Run(\"populated\", func(t *testing.T) {\n\t\ttt := New(testRows)\n\n\t\tassertEqual(t, tt.LineCount, 3)\n\t})\n\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\ttt := NewEmpty(5)\n\n\t\tassertEqual(t, len(tt.Rows), 1)\n\t\tassertEqual(t, cap(tt.Rows), 5)\n\t\tassertEqual(t, tt.LineCount, 0)\n\t\tassertEqual(t, tt.Render(), \"\")\n\t})\n}\n\nfunc TestAppend(t *testing.T) {\n\n\trow1 := NewRow(\"baz\", \"\", \"\", \"\")\n\trow2 := NewRow(\"foobar\", \"\", \"\", \"\")\n\n\tt.Run(\"appendRow\", func(t *testing.T) {\n\t\ttt := New(testRows)\n\t\toriginalCount := tt.LineCount\n\n\t\ttt.AppendRow(row1)\n\t\tassertEqual(t, tt.LineCount, originalCount+1)\n\t})\n\n\tt.Run(\"appendRows\", func(t *testing.T) {\n\t\ttt := New(testRows)\n\t\toriginalCount := tt.LineCount\n\n\t\ttt.AppendRows(row1, row2)\n\t\tassertEqual(t, tt.LineCount, originalCount+2)\n\t})\n}\n\nfunc TestJoin(t *testing.T) {\n\tt1 := New(testRows)\n\tt1Count := t1.LineCount\n\tt2 := New([]*Row{{Text: \"baz\"}})\n\tt2Count := t2.LineCount\n\tt1.Join(t2)\n\n\tassertEqual(t, t1.LineCount, t1Count+t2Count)\n}\n\nfunc TestReset(t *testing.T) {\n\ttt := New(testRows)\n\ttt.Reset()\n\n\tassertEqual(t, tt.LineCount, 0)\n\tassertEqual(t, tt.Render(), \"\")\n}\n\nfunc assertEqual[T comparable](t *testing.T, got, want T) {\n\tif got != want {\n\t\tt.Errorf(\"got %#v, want %#v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "ui/ui.go",
    "content": "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/kencx/keyb/ui/table\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\ntype Model struct {\n\tList list.Model\n\tApps *config.Apps\n}\n\nfunc NewModel(a config.Apps, config *config.Config) *Model {\n\n\ttable := createParentTable(a, config.SortKeys)\n\treturn &Model{\n\t\tList: list.New(table, config),\n\t\tApps: &a,\n\t}\n}\n\nfunc createParentTable(a config.Apps, sortKeys bool) *table.Model {\n\n\tif len(a) <= 0 {\n\t\tt := table.NewEmpty(1)\n\t\treturn t\n\t}\n\n\tsort.Slice(a, func(i, j int) bool {\n\t\treturn a[i].Name < a[j].Name\n\t})\n\n\tparent := appToTable(a[0].Name, *a[0], sortKeys)\n\n\tif len(a) > 1 {\n\t\tfor _, k := range a[1:] {\n\t\t\tchild := appToTable(k.Name, *k, sortKeys)\n\t\t\tparent.Join(child)\n\t\t}\n\t}\n\treturn parent\n}\n\nfunc appToTable(heading string, app config.App, sortKeys bool) *table.Model {\n\tvar rows []*table.Row\n\n\th := table.NewHeading(heading)\n\trows = append(rows, h)\n\n\tif sortKeys {\n\t\tsort.Slice(app.Keybinds, func(i, j int) bool {\n\t\t\treturn strings.ToLower(app.Keybinds[i].Name) < strings.ToLower(app.Keybinds[j].Name)\n\t\t})\n\t}\n\n\t// convert Keybind to Row\n\tfor _, kb := range app.Keybinds {\n\t\trow := table.NewRow(kb.Name, kb.Key, app.Prefix, heading)\n\n\t\t// KeyBind's ignore prefix defaults to false\n\t\t// so user can choose to ignore prefix for a specific kb\n\t\tif kb.IgnorePrefix {\n\t\t\t// Row's show prefix defaults to false\n\t\t\t// so prefix is not shown unless defined\n\t\t\trow.ShowPrefix = false\n\t\t}\n\t\trows = append(rows, row)\n\t}\n\treturn table.New(rows)\n}\n\nfunc (m *Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tm.List.Resize(msg.Width, msg.Height)\n\t}\n\n\tm.List, cmd = m.List.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *Model) View() string {\n\treturn m.List.View()\n}\n"
  }
]