Repository: kencx/keyb
Branch: master
Commit: 27d84092f93c
Files: 61
Total size: 155.7 KB
Directory structure:
gitextract_nio3xsob/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .goreleaser.yaml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── config/
│ ├── config.go
│ ├── config_test.go
│ ├── model.go
│ └── model_test.go
├── examples/
│ ├── Helix.yml
│ ├── ansi.yml
│ ├── awesomewm.yml
│ ├── bspwm.yml
│ ├── changefile
│ ├── config/
│ │ ├── README.md
│ │ └── default.yml
│ ├── curl.yml
│ ├── cut.yml
│ ├── date.yml
│ ├── dd.yml
│ ├── discord.yml
│ ├── du.yml
│ ├── dwm.yml
│ ├── find.yml
│ ├── firefox.yml
│ ├── irssi.yml
│ ├── kitty.yml
│ ├── less.yml
│ ├── ranger.yml
│ ├── screen.yml
│ ├── tar.yml
│ ├── tmux.yml
│ ├── unzip.yml
│ ├── vim.yml
│ ├── vscode.yml
│ ├── weechat.yml
│ ├── zip.yml
│ └── zsh.yml
├── go.mod
├── go.sum
├── main.go
├── output/
│ ├── output.go
│ └── output_test.go
├── testdata/
│ ├── testConfig.json
│ ├── testConfig.yml
│ ├── testConfigMinimal.json
│ ├── testConfigMinimal.yml
│ ├── testkeyb.json
│ └── testkeyb.yml
└── ui/
├── list/
│ ├── keymap.go
│ ├── list.go
│ ├── list_test.go
│ ├── update.go
│ └── view.go
├── table/
│ ├── row.go
│ ├── table.go
│ └── table_test.go
└── ui.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
include: "scope"
================================================
FILE: .github/workflows/test.yml
================================================
name: CI
on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.26'
- name: Verify dependencies
run: go mod verify
- name: Build application
run: make build
- name: Vet
run: go vet ./...
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- name: Run staticcheck
run: staticcheck ./...
- name: Run tests
run: make test
release:
needs: [test]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
*.json
*.toml
*.ini
*.gitcommit
testdata/keyb*
!testdata/*.json
!testdata/*.yml
!testdata/*.yaml
keyb
dist/
================================================
FILE: .goreleaser.yaml
================================================
project_name: keyb
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- "arm64"
- "amd64"
- "386"
ldflags:
- "-s -w -X main.version={{ .Tag }}"
archives:
- name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
formats: tar.gz
format_overrides:
- goos: windows
formats: zip
checksum:
name_template: 'checksums.txt'
changelog:
disable: true
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
[v0.2.0]: https://github.com/kencx/keyb/compare/v0.1.0...v0.2.0
[v0.3.0]: https://github.com/kencx/keyb/compare/v0.2.0...v0.3.0
[v0.4.0]: https://github.com/kencx/keyb/compare/v0.3.0...v0.4.0
[v0.4.1]: https://github.com/kencx/keyb/compare/v0.4.0...v0.4.1
[v0.5.0]: https://github.com/kencx/keyb/compare/v0.4.1...v0.5.0
[v0.6.0]: https://github.com/kencx/keyb/compare/v0.5.0...v0.6.0
[v0.7.0]: https://github.com/kencx/keyb/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/kencx/keyb/compare/v0.7.0...v0.8.0
## [v0.8.0]
### Added
- Add support for json config and keyb files
- Add support to export keyb files to json and yaml
### Changed
- Update Go to v1.26.1
- Update bubbles to v1.0.0
- Update bubbletea to v1.3.10
- Update lipgloss to v1.1.0
- Bump goreleaser/goreleaser-action from 5 to 7
- Bump actions/checkout from 4 to 6
- Bump actions/setup-go from 4 to 6
## [v0.7.0]
### Fixed
- Exiting now clears the output
### Chore
- Replace deprecated `--rm-dist` with `--clean` flag in Goreleaser action
## [v0.6.0]
### Changed
- Bump goreleaser/goreleaser-action from 4 to 5 [#25](https://github.com/kencx/keyb/pull/25)
- Simplify config package
- Bump actions/checkout from 3 to 4 [#24](https://github.com/kencx/keyb/pull/24)
- Update Go to v1.21
- Update bubbletea to v0.24.2
### Fixed
- Fix environment variables not being expanded in the config's `settings.keyb_path`.
## [v0.5.0]
### Added
- Add ability to customize search bar cursor bindings.
### Changed
- Default configuration file is no longer automatically generated. `keyb` will
default to reading any config file at `$XDG_CONFIG_HOME/keyb/config.yml`. If
not present, the default configuration is used.
## [v0.4.1]
### Fixed
- Fix inability to delete characters in search bar [1385049](https://github.com/kencx/keyb/commit/138504964bad8f8827c5f5e9c1572298d4d5e102)
## [v0.4.0]
### BREAKING
- Use `XDG_CONFIG_HOME` environment variable in macOS if set [#18](https://github.com/kencx/keyb/pull/18)
### Added
- Add ability to move cursor in search mode
[55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)
- Add ability to customize cursor movement keys in search mode
[55dd7ad](https://github.com/kencx/keyb/commit/55dd7adead29316d3952e7c19bb5b15546394668)
### Changed
- Update dependencies [#19](https://github.com/kencx/keyb/pull/19) and
[3cba5b8](https://github.com/kencx/keyb/commit/3cba5b801acd617e9d1c37734582f3f15d2ec41b)
### Fixed
- Fix search bar cursor not blinking when focused [#20](https://github.com/kencx/keyb/pull/20)
## [v0.3.0]
### Added
- Add "add" subcommand to quick add keybinds [50b66d7](https://github.com/kencx/keyb/commit/50b66d7a78c4a08a9cb5ad5bd02d909b7b27ae53), [b9167474](https://github.com/kencx/keyb/commit/b9167474c9c5d12ed8ea0ca9630489fa7266bebe)
## [v0.2.0]
### Added
- Add counter styling [#2](https://github.com/kencx/keyb/pull/2)
- Add placeholder styling [#4](https://github.com/kencx/keyb/pull/4)
- Add ability to customize keyb key bindings [43ae9b8](https://github.com/kencx/keyb/commit/43ae9b83fbf5cae367ab74614fa42fce79817165)
- Add `sort_keys` option to sort keys alphabetically [#7](https://github.com/kencx/keyb/pull/7)
- Add ability to customize prompt location [019f6ca](https://github.com/kencx/keyb/commit/019f6cad03ada6507e6585e4f4403826dcd23212)
- Add `search_mode` option to start keyb in search mode [d6c53e1](https://github.com/kencx/keyb/commit/d6c53e1b908f05f6c0f7836068b4b6bbe1e8a451)
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2022 kencx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
binary = keyb
version = $(shell git describe --tags)
ldflags = -ldflags "-s -w -X main.version=${version}"
.PHONY: help test clean snapshot build
default: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
## test: run tests
test:
go test ./...
## clean: remove binaries, dist
clean:
if [ -f ${binary} ]; then rm keyb; fi
go clean
rm -rf dist
## snapshot: generate unversioned snapshot release
snapshot:
goreleaser release --snapshot
## build: build binary
build:
go build ${ldflags} -o ${binary}
## install: install binary at ~/.local/bin
install:
cp ${binary} ~/.local/bin/
================================================
FILE: README.md
================================================
# keyb
Create and view your own custom hotkey cheatsheet in the terminal
### Features
- Lightweight and quick
- Fully customizable
- Fuzzy filtering
- Vim key bindings
- Export to stdout for fzf, rofi support
### Non-Features
keyb does **not** support:
- Auto detection of hotkeys
- Command selection
## Motivation
I had trouble remembering the various hotkeys that I sometimes use. It got
annoying to look them up so I resorted to writing them down on a paper
cheatsheet. Then, I thought: maybe there's a tool that does this better. I
didn't find one I liked so I built keyb.
I also wrote a blog [post](https://blog.cheo.dev/posts/keyb) about building keyb.
With keyb, I can list:
- Hotkeys that I occasionally forget or am new to
- Custom key combinations that I defined for my own workflow
- Short commands that I sometimes use
It is best used as a popup cheatsheet.
## Install
keyb supports Linux, MacOS and Windows.
### Compiled Binary
Download a compiled binary from the [releases](https://github.com/kencx/keyb/releases) page.
### Install with Go
```bash
$ go install github.com/kencx/keyb@latest
```
### AUR
Packages can be found in the AUR:
- [keyb-bin](https://aur.archlinux.org/packages/keyb-bin)
- [keyb](https://aur.archlinux.org/packages/keyb)
These are NOT maintained by me.
### Build from source
```bash
$ git clone https://github.com/kencx/keyb
$ cd keyb
$ make build
```
## Usage
```text
usage: keyb [options]
Options:
-p, --print Print to stdout
-e, --export Export to file [yaml, json]
-k, --key Key bindings at custom path
-c, --config Config file at custom path
-v, --version Version info
-h, --help help for keyb
Commands:
a, add Add keybind to keyb file
```
### Search
- Enter search mode with `/` to perform fuzzy filtering on all rows
- Exit search mode again with `Esc`
- `Alt + d` clears the current filter
To perform filtering on section headings only, prefix the
search with `h:`. This will return all matching section headings with their
respective rows.
### Printing
keyb supports printing to stdout for use with other tools:
```bash
$ keyb -p | fzf
$ keyb -p | rofi -dmenu
```
### keyb File
keyb requires a `yaml` or `json` file with a list of hotkeys to work. A default
`yaml` file is generated in your system's config directory if no other file is
specified.
Hotkeys are classified into sections with a name and (optional) prefix field.
When displayed, sections are sorted by alphabetical order while the keys
themselves are arranged in their defined order.
```yaml
- name: bspwm
keybinds:
- name: terminal
key: Super + Return
```
The prefix is a key combination that will be prepended to every hotkey in the
section. A key can choose to opt out by including a `ignore_prefix: true` field.
Prefixes are useful for applications with a common leading hotkey like tmux.
```yaml
- name: tmux
prefix: ctrl + b
keybinds:
- name: Create new window
key: c
- name: Prev, next window
key: Shift + {←, →}
ignore_prefix: true
```
Refer to the `examples` for more examples.
>Multiline fields are not supported!
### Quick Add
```text
usage: keyb [-k file] add [app; name; key]
Options:
-b, --binding Key binding
-p, --prefix Ignore prefix
```
You can quick add bindings from the command line to a specified file. If `-k
file` is given and exists, the new keybind will be appended to the file.
Otherwise, `keyb_path` defined in `config.yml` will be used.
```bash
$ keyb add -b "kitty; open terminal; super + enter"
```
When adding a new keybind, the app name, keybind name and keybind must be
specified. It is separated by `;` and wrapped in quotes (to prevent parsing errors).
## Configuration
keyb can be customized with a config file at the default OS config
directory (i.e. `$XDG_CONFIG_HOME/keyb/config.yml`). If no such file exists, the
default configuration will be used.
See [config](examples/config/README.md) for all configuration options.
### Missing Colors
If you're missing colors, a workaround is to add the environment variable `CLICOLOR_FORCE=1` to
[force ANSI color support](https://pkg.go.dev/github.com/muesli/termenv#Output.EnvColorProfile).
## Roadmap
- [x] Ability to customize keyb hotkeys
- [x] `a, add` subcommand to quickly add a single hotkey entry from the CLI
- [ ] Export to additional file formats (`json, toml, conf/ini` etc.)
- [ ] Support multiple keyb files or directories
## Contributing
Contributing to keyb requires Go 1.26. Bug reports, feature requests and PRs are very welcome.
## Similar Tools
- [showkeys](https://github.com/adamharmansky/showkeys) offers a keybinding popup similar to awesomewm
- [cheat](https://github.com/cheat/cheat) is a CLI alternative to view cheatsheets for
commands and hotkeys for just about any topic
- Refer to [shortcut-pages](https://github.com/mt-empty/shortcut-pages), [cheat/cheatsheets](https://github.com/cheat/cheatsheets) for more cheatsheets
## License
[MIT](LICENSE)
================================================
FILE: config/config.go
================================================
package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
const (
defaultConfigDir = "keyb"
defaultConfigFile = "config.yml"
defaultKeybFile = "keyb.yml"
)
type Config struct {
Settings `yaml:"settings" json:"settings"`
Color `yaml:"color" json:"color"`
Keys `yaml:"keys" json:"keys"`
}
type Settings struct {
KeybPath string `yaml:"keyb_path" json:"keyb_path"`
Debug bool
Reverse bool
Mouse bool
SearchMode bool `yaml:"search_mode" json:"search_mode"`
SortKeys bool `yaml:"sort_keys" json:"sort_keys"`
Title string
Prompt string
PromptLocation string `yaml:"prompt_location" json:"prompt_location"`
Placeholder string
PrefixSep string `yaml:"prefix_sep" json:"prefix_sep"`
SepWidth int `yaml:"sep_width" json:"sep_width"`
Margin int
Padding int
BorderStyle string `yaml:"border" json:"border"`
}
type Color struct {
PromptColor string `yaml:"prompt" json:"prompt"`
CursorFg string `yaml:"cursor_fg" json:"cursor_fg"`
CursorBg string `yaml:"cursor_bg" json:"cursor_bg"`
FilterFg string `yaml:"filter_fg" json:"filter_fg"`
FilterBg string `yaml:"filter_bg" json:"filter_bg"`
CounterFg string `yaml:"counter_fg" json:"counter_fg"`
CounterBg string `yaml:"counter_bg" json:"counter_bg"`
PlaceholderFg string `yaml:"placeholder_fg" json:"placeholder_fg"`
PlaceholderBg string `yaml:"placeholder_bg" json:"placeholder_bg"`
BorderColor string `yaml:"border_color" json:"border_color"`
}
type Keys struct {
Quit string
Up string
Down string
UpFocus string `yaml:"up_focus" json:"up_focus"`
DownFocus string `yaml:"down_focus" json:"down_focus"`
HalfUp string `yaml:"half_up" json:"half_up"`
HalfDown string `yaml:"half_down" json:"half_down"`
FullUp string `yaml:"full_up" json:"full_up"`
FullDown string `yaml:"full_bottom" json:"full_bottom"`
GoToFirstLine string `yaml:"first_line" json:"first_line"`
GoToLastLine string `yaml:"last_line" json:"last_line"`
GoToTop string `yaml:"top" json:"top"`
GoToMiddle string `yaml:"middle" json:"middle"`
GoToBottom string `yaml:"bottom" json:"bottom"`
Search string
ClearSearch string `yaml:"clear_search" json:"clear_search"`
Normal string
CursorWordForward string `yaml:"cursor_word_forward" json:"cursor_word_forward"`
CursorWordBackward string `yaml:"cursor_word_backward" json:"cursor_word_backward"`
CursorDeleteWordBackward string `yaml:"cursor_delete_word_backward" json:"cursor_delete_word_backward"`
CursorDeleteWordForward string `yaml:"cursor_delete_word_forward" json:"cursor_delete_word_forward"`
CursorDeleteAfterCursor string `yaml:"cursor_delete_after_cursor" json:"cursor_delete_after_cursor"`
CursorDeleteBeforeCursor string `yaml:"cursor_delete_before_cursor" json:"cursor_delete_before_cursor"`
CursorLineStart string `yaml:"cursor_line_start" json:"cursor_line_start"`
CursorLineEnd string `yaml:"cursor_line_end" json:"cursor_line_end"`
CursorPaste string `yaml:"cursor_paste" json:"cursor_paste"`
}
var DefaultConfig = &Config{
Settings: Settings{
Debug: false,
Reverse: false,
Mouse: true,
SearchMode: false,
SortKeys: false,
Title: "",
Prompt: "keys > ",
PromptLocation: "top",
Placeholder: "...",
PrefixSep: ";",
SepWidth: 4,
Margin: 0,
Padding: 1,
BorderStyle: "hidden",
},
Color: Color{
FilterFg: "#FFA066",
},
Keys: Keys{
Quit: "q, ctrl+c",
Up: "k, up",
Down: "j, down",
UpFocus: "ctrl+k",
DownFocus: "ctrl+j",
HalfUp: "ctrl+u",
HalfDown: "ctrl+d",
FullUp: "ctrl+b",
FullDown: "ctrl+f",
GoToFirstLine: "g",
GoToLastLine: "G",
GoToTop: "H",
GoToMiddle: "M",
GoToBottom: "L",
Search: "/",
ClearSearch: "alt+d",
Normal: "esc",
CursorWordForward: "alt+right, alt+f",
CursorWordBackward: "alt+left, alt+b",
CursorDeleteWordBackward: "alt+backspace",
CursorDeleteWordForward: "alt+delete",
CursorDeleteAfterCursor: "alt+k",
CursorDeleteBeforeCursor: "alt+u",
CursorLineStart: "home, ctrl+a",
CursorLineEnd: "end, ctrl+e",
CursorPaste: "ctrl+v",
},
}
// Read configuration and keyb file from flags, default path.
func Parse(flagCPath, flagKPath string) (Apps, *Config, error) {
xdgConfigDir, err := getXDGConfigDir()
if err != nil {
return nil, nil, err
}
basePath := filepath.Join(xdgConfigDir, defaultConfigDir)
err = os.MkdirAll(basePath, 0744)
if err != nil {
return nil, nil, fmt.Errorf("failed to create config dir: %w", err)
}
config, err := UnmarshalConfig(flagCPath, basePath)
if err != nil {
return nil, nil, err
}
if flagKPath == "" {
flagKPath = config.KeybPath
}
keys, err := UnmarshalKeyb(flagKPath, basePath)
if err != nil {
return nil, nil, err
}
return keys, config, nil
}
// Read config file and merge with default config
func UnmarshalConfig(configFile, basePath string) (*Config, error) {
// set default config filepath
if configFile == "" {
configFile = filepath.Join(basePath, defaultConfigFile)
}
res := newDefaultConfig(basePath)
configFile = os.ExpandEnv(configFile)
file, err := os.ReadFile(configFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return res, nil
} else {
return nil, fmt.Errorf("failed to read config file \"%s\": %w", configFile, err)
}
}
switch filepath.Ext(configFile) {
case ".json":
if err = json.Unmarshal(file, &res); err != nil {
return nil, fmt.Errorf("failed to unmarshal config file \"%s\": %w", configFile, err)
}
case ".yaml", ".yml":
if err = yaml.Unmarshal(file, &res); err != nil {
return nil, fmt.Errorf("failed to unmarshal config file \"%s\": %w", configFile, err)
}
}
return res, nil
}
func newDefaultConfig(basePath string) *Config {
res := DefaultConfig
res.KeybPath = filepath.Join(basePath, defaultKeybFile)
return res
}
// Read keyb file or create default keyb file not exist
func UnmarshalKeyb(keybFile, basePath string) (Apps, error) {
if keybFile == "" {
keybFile = filepath.Join(basePath, defaultKeybFile)
}
keybFile = os.ExpandEnv(keybFile)
file, err := os.ReadFile(keybFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
k := newDefaultKeyb(keybFile)
data, err := yaml.Marshal(k)
if err != nil {
return nil, fmt.Errorf("failed to generate default keyb: %w", err)
}
if err := os.WriteFile(keybFile, data, 0644); err != nil {
return nil, fmt.Errorf("failed to create keyb file: %w", err)
}
return k, nil
} else {
return nil, fmt.Errorf("failed to read keyb file: %w", err)
}
}
var b Apps
switch filepath.Ext(keybFile) {
case ".json":
if err = json.Unmarshal(file, &b); err != nil {
return nil, fmt.Errorf("failed to unmarshal keyb file: %w", err)
}
case ".yaml", ".yml":
if err := yaml.Unmarshal(file, &b); err != nil {
return nil, fmt.Errorf("failed to unmarshal keyb file: %w", err)
}
}
return b, nil
}
func newDefaultKeyb(path string) Apps {
return Apps{{
Name: "example",
Keybinds: []KeyBind{{
Name: "add your keys in",
Key: path,
}},
}}
}
// get user XDG_CONFIG_HOME directory
func getXDGConfigDir() (string, error) {
val, ok := os.LookupEnv("XDG_CONFIG_HOME")
if ok {
return val, nil
}
path, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("user config directory not found: %w", err)
}
return path, nil
}
================================================
FILE: config/config_test.go
================================================
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
)
const testBasePath = "../testdata"
func TestUnmarshalConfig(t *testing.T) {
testConfig := &Config{
Settings: Settings{
KeybPath: "./custom.yml",
Debug: true,
Reverse: true,
Mouse: false,
SearchMode: false,
SortKeys: true,
Title: "",
Prompt: "keys > ",
PromptLocation: "bottom",
Placeholder: "...",
PrefixSep: ";",
SepWidth: 4,
Margin: 1,
Padding: 1,
BorderStyle: "normal",
},
Color: Color{
FilterFg: "#FFA066",
},
Keys: Keys{
Quit: "q, ctrl+c",
Up: "k, up",
Down: "j, down",
UpFocus: "alt+k",
DownFocus: "alt+j",
HalfUp: "ctrl+u",
HalfDown: "ctrl+d",
FullUp: "ctrl+b",
FullDown: "ctrl+f",
GoToFirstLine: "g",
GoToLastLine: "G",
GoToTop: "H",
GoToMiddle: "M",
GoToBottom: "L",
Search: "/",
ClearSearch: "alt+d",
Normal: "esc",
CursorWordForward: "alt+right, alt+f",
CursorWordBackward: "alt+left, alt+b",
CursorDeleteWordBackward: "alt+backspace",
CursorDeleteWordForward: "alt+delete",
CursorDeleteAfterCursor: "alt+k",
CursorDeleteBeforeCursor: "alt+u",
CursorLineStart: "home, ctrl+a",
CursorLineEnd: "end, ctrl+e",
CursorPaste: "ctrl+v",
},
}
configFileTests := []struct {
name string
file string
want *Config
}{
{"full config yaml", "testConfig.yml", testConfig},
{"full config json", "testConfig.json", testConfig},
{"minimal config yaml", "testConfigMinimal.yml", newDefaultConfig(testBasePath)},
{"minimal config json", "testConfigMinimal.json", newDefaultConfig(testBasePath)},
{"config file absent", "testConfigAbsent.yml", newDefaultConfig(testBasePath)},
}
for _, tt := range configFileTests {
t.Run(tt.name, func(t *testing.T) {
got, err := UnmarshalConfig(filepath.Join(testBasePath, tt.file), testBasePath)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
t.Run("empty config file path", func(t *testing.T) {
want := newDefaultConfig(testBasePath)
got, err := UnmarshalConfig("", testBasePath)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
}
func TestUnmarshalKeyb(t *testing.T) {
apps := Apps{{
Name: "test",
Keybinds: []KeyBind{{
Name: "foo",
Key: "bar",
}},
}}
keybFileTests := []struct {
name string
file string
want Apps
}{
{"keyb file yaml", "testkeyb.yml", apps},
{"keyb file json", "testkeyb.json", apps},
}
for _, tt := range keybFileTests {
t.Run(tt.name, func(t *testing.T) {
got, err := UnmarshalKeyb(filepath.Join(testBasePath, tt.file), testBasePath)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
t.Run("file absent", func(t *testing.T) {
_, err := UnmarshalKeyb(filepath.Join(testBasePath, "temp.yml"), testBasePath)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
t.Cleanup(func() {
err := os.Remove(filepath.Join(testBasePath, "temp.yml"))
if err != nil {
t.Fatal(err)
}
})
})
t.Run("empty filepath", func(t *testing.T) {
_, err := UnmarshalKeyb("", testBasePath)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
t.Cleanup(func() {
err := os.Remove(filepath.Join(testBasePath, defaultKeybFile))
if err != nil {
t.Fatal(err)
}
})
})
}
================================================
FILE: config/model.go
================================================
package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v2"
)
type App struct {
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
Name string `yaml:"name" json:"name"`
Keybinds []KeyBind `yaml:"keybinds" json:"keybinds"`
}
type Apps []*App
func (a App) String() string {
return fmt.Sprintf("App{name=%s,prefix=%s,keybinds=%v}", a.Name, a.Prefix, a.Keybinds)
}
type KeyBind struct {
Name string `yaml:"name" json:"name"`
Key string `yaml:"key" json:"key"`
// ignore prefix defaults to false
// so user can choose to ignore prefix for a specific kb
IgnorePrefix bool `yaml:"ignore_prefix,omitempty" json:"ignore_prefix,omitempty"`
}
func AddEntry(path, binding string, kbIgnorePrefix bool) error {
xdgConfigDir, err := getXDGConfigDir()
if err != nil {
return err
}
// load existing struct from filepath
apps, err := UnmarshalKeyb(path, xdgConfigDir)
if err != nil {
return err
}
if binding == "" {
return fmt.Errorf("binding must be given in format [app; name; keybind]")
}
s := strings.Split(binding, ";")
if len(s) < 3 {
return fmt.Errorf("binding must be given in format [app; name; keybind]")
}
appName := strings.TrimSpace(s[0])
name := strings.TrimSpace(s[1])
key := strings.TrimSpace(s[2])
apps.addOrUpdate(appName, name, key, kbIgnorePrefix)
// rewrite file
data, err := yaml.Marshal(apps)
if err != nil {
return fmt.Errorf("failed to marshal entry: %w", err)
}
path = os.ExpandEnv(path)
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("failed to write keyb file: %w", err)
}
return nil
}
func (apps *Apps) addOrUpdate(appName string, name, key string, ignorePrefix bool) {
newKeyBind := KeyBind{
Name: name,
Key: key,
IgnorePrefix: ignorePrefix,
}
if !apps.exist(appName) {
a := App{
Name: appName,
Keybinds: []KeyBind{newKeyBind},
}
*apps = append(*apps, &a)
} else {
for _, app := range *apps {
if appName == app.Name {
app.Keybinds = append(app.Keybinds, newKeyBind)
}
}
}
// return apps
}
func (apps Apps) exist(appName string) bool {
for _, app := range apps {
if appName == app.Name {
return true
}
}
return false
}
================================================
FILE: config/model_test.go
================================================
package config
import (
"reflect"
"testing"
)
func TestAddOrUpdate(t *testing.T) {
t.Run("update existing", func(t *testing.T) {
apps := Apps{{
Name: "test",
Keybinds: []KeyBind{{
Name: "foo",
Key: "bar",
}},
}}
want := Apps{{
Name: "test",
Keybinds: []KeyBind{{
Name: "foo",
Key: "bar",
}, {
Name: "addFoo",
Key: "addBar",
}},
}}
apps.addOrUpdate("test", "addFoo", "addBar", false)
if !reflect.DeepEqual(apps, want) {
t.Errorf("got %v, want %v", apps, want)
}
})
t.Run("add new", func(t *testing.T) {
apps := Apps{{
Name: "test",
Keybinds: []KeyBind{{
Name: "foo",
Key: "bar",
}},
}}
want := Apps{
{
Name: "test",
Keybinds: []KeyBind{{
Name: "foo",
Key: "bar",
}},
}, {
Name: "new",
Keybinds: []KeyBind{{
Name: "addFoo",
Key: "addBar",
}},
},
}
apps.addOrUpdate("new", "addFoo", "addBar", false)
if !reflect.DeepEqual(apps, want) {
t.Errorf("got %v, want %v", apps, want)
}
})
}
================================================
FILE: examples/Helix.yml
================================================
- name: Helix Normal mode (Movement)
keybinds:
- name: "Move left"
key: "h, Left"
- name: "Move down"
key: "j, Down"
- name: "Move up"
key: "k, Up"
- name: "Move right"
key: "l, Right"
- name: "Move next word start"
key: "w"
- name: "Move previous word start"
key: "b"
- name: "Move next word end"
key: "e"
- name: "Move next WORD end"
key: "W"
- name: "Move previous WORD start"
key: "B"
- name: "Move next WORD end"
key: "E"
- name: "Find 'till next char"
key: "t"
- name: "Find next char"
key: "f"
- name: "Find 'till previous char"
key: "T"
- name: "Find previous char"
key: "F"
- name: "Go to line number "
key: "G"
- name: "Reapeat last motion (f, t ro m)"
key: "Alt-."
- name: "Move to the start of the line"
key: "Home"
- name: "Move to the end ofthe line"
key: "End"
- name: "Move page up"
key: "Ctrl-b, PageUp"
- name: "Move page down"
key: "Ctrl-f, PageDown"
- name: "Move half page up"
key: "Ctrl-u"
- name: "Move half page down"
key: "Ctrf-d"
- name: "Jump forward on the jumplist"
key: "Ctrl-i"
- name: "Jump backward on the jumplist"
key: "Ctrl-o"
- name: "Save the current selection to the jumplist"
key: "Ctrl-s"
- name: Helix Normal mode (Changes)
keybinds:
- name: "Replace with a character"
key: "r"
- name: "Replace with yanked text"
key: "R"
- name: "Switch case of the selected text"
key: "~"
- name: "Set the selected text to lower case"
key: "`"
- name: "Set the selected text to upper case"
key: "Alt-`"
- name: "Insert before selection"
key: "i"
- name: "Insert after selection (append)"
key: "a"
- name: "Insert at the start of the line"
key: "I"
- name: "Insert at the end of the line"
key: "A"
- name: "Open new line below selection"
key: "o"
- name: "Open new line above selection"
key: "O"
- name: "Repeat last change"
key: "."
- name: "Undo change"
key: "u"
- name: "Redo change"
key: "U"
- name: "Move backward in history"
key: "Alt-u"
- name: "Move forward in history"
key: "Alt-U"
- name: "Yank selection"
key: "y"
- name: "Paste after selection"
key: "p"
- name: "Paste before selection"
key: "P"
- name: "Select a register to yank to or paste from"
key: ""
- name: "Indent selection"
key: ">"
- name: "Unindent selection"
key: "<"
- name: "Format selection (currently nonfunctional/disabled) (LSP)"
key: "="
- name: "Delete selection"
key: "d"
- name: "Delete selection, without yanking"
key: "Alt-d"
- name: "Change selection (delete and enter insert mode)"
key: "c"
- name: "Change selection (delete and enter insert mode, without yanking)"
key: "Alt-c"
- name: "Increment object (number) under cursor"
key: "Ctrl-a"
- name: "Decrement object (number) under cursor"
key: "Ctrl-x"
- name: "Start/stop macro recording to the selected register (experimental)"
key: "Q"
- name: "Play back a recorded macro from the selected register (experimental)"
key: "q"
- name: Helix Normal mode (Changes Shell)
keybinds:
- name: "Pipe each selection through shell command, replacing with output"
key: "|"
- name: "Pipe each selection into shell command, ignoring output"
key: "Alt-|"
- name: "Run shell command, inserting output before each selection"
key: "!"
- name: "Run shell command, appending output after each selection"
key: "Alt-!"
- name: "Pipe each selection into shell command, keep selections where command returned 0"
key: "$"
- name: Helix Normal mode (Selection manipulation)
keybinds:
- name: "Select all regex matches inside selections"
key: "s"
- name: "Split selection into subselections on regex matches"
key: "S"
- name: "Split selection on newlines"
key: "Alt-s"
- name: "Align selection in columns"
key: "&"
- name: "Trim whitespace from the selection"
key: "_"
- name: "Collapse selection onto a single cursor"
key: ";"
- name: "Flip selection cursor and anchor"
key: "Alt-;"
- name: "Ensures the selection is in forward direction"
key: "Alt-:"
- name: "Keep only the primary selection"
key: ","
- name: "Remove the primary selection"
key: "Alt-,"
- name: "Copy selection onto the next line (Add cursor below)"
key: "C"
- name: "Copy selection onto the previous line (Add cursor above)"
key: "Alt-C"
- name: "Rotate main selection backward"
key: "("
- name: "Rotate main selection forward"
key: ")"
- name: "Rotate selection contents backward"
key: "Alt-("
- name: "Rotate selection contents forward"
key: "Alt-)"
- name: "Select entire file"
key: "%"
- name: "Select current line, if already selected, extend to next line"
key: "x"
- name: "Extend selection to line bounds (line-wise selection)"
key: "X"
- name: "Shrink selection to line bounds (line-wise selection)"
key: "Alt-x"
- name: "Join lines inside selection"
key: "J"
- name: "Join lines inside selection and select space"
key: "Alt-J"
- name: "Keep selections matching the regex"
key: "K"
- name: "Remove selections matching the regex"
key: "Alt-K"
- name: "Comment/uncomment the selections"
key: "Ctrl-c"
- name: "Expand selection to parent syntax node (TS)"
key: "Alt-o, Alt-up"
- name: "Shrink syntax tree object selection (TS)"
key: "Alt-i, Alt-down"
- name: "Select previous sibling node in syntax tree (TS)"
key: "Alt-p, Alt-left"
- name: "Select next sibling node in syntax tree (TS)"
key: "Alt-n, Alt-right"
- name: Helix Normal mode (Search)
keybinds:
- name: "Search for regex pattern"
key: "/"
- name: "Search for previous pattern"
key: "?"
- name: "Select next search match"
key: "n"
- name: "Select previous search match"
key: "N"
- name: "Use current selection as the search pattern"
key: "*"
- name: Helix Minor modes
keybinds:
- name: "Enter select (extend) mode"
key: "v"
- name: "Enter goto mode"
key: "g"
- name: "Enter match mode"
key: "m"
- name: "Enter command mode"
key: ":"
- name: "Enter view mode"
key: "z"
- name: "Enter sticky view mode"
key: "Z"
- name: "Enter window mode"
key: "Ctrl-w"
- name: "Enter space mode"
key: "Space"
- name: Helix View mode
keybinds:
- name: "Vertically center the line"
key: "z, c"
- name: "Align the line to the top of the screen"
key: "t"
- name: "Align the line to the bottom of the screen"
key: "b"
- name: "Align the line to the middle of the screen (horizontally)"
key: "m"
- name: "Scroll the view downwards"
key: "j,down"
- name: "Scroll the view upwards"
key: "k, up"
- name: "Move page down"
key: "Ctrl-f, PageDown"
- name: "Move page up"
key: "Ctrl-b, PageUp"
- name: "Move half page down"
key: "Ctrl-d"
- name: "Move half page up"
key: "Ctrl-u"
- name: Helix Goto mode
keybinds:
- name: "Go to line number else start of file"
key: "g"
- name: "Go to the end of the file"
key: "e"
- name: "Go to files in the selection"
key: "f"
- name: "Go to the start of the line"
key: "h"
- name: "Go to the end of the line"
key: "l"
- name: "Go to first non-whitespace character of the line"
key: "s"
- name: "Go to the top of the screen"
key: "t"
- name: "Go to the middle of the screen"
key: "c"
- name: "Go to the bottom of the screen"
key: "b"
- name: "Go to definition (LSP)"
key: "d"
- name: "Go to type definition (LSP)"
key: "y"
- name: "Go to references (LSP)"
key: "r"
- name: "Go to implementation (LSP)"
key: "i"
- name: "Go to the last accessed/alternate file"
key: "a"
- name: "Go to the last modified/alternate file"
key: "m"
- name: "Go to next buffer"
key: "n"
- name: "Go to previous buffer"
key: "p"
- name: "Go to last modification in current file"
key: "."
- name: Helix Match mode
keybinds:
- name: "Goto matching bracket (TS)"
key: "m"
- name: "Surround current selection with "
key: "s "
- name: "Replace surround character with "
key: "r "
- name: "Delete surround character "
key: "d "
- name: "Select around textobject"
key: "a