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 " - name: "Select inside textobject" key: "i " - name: Helix Window mode keybinds: - name: "Switch to next window" key: "w, Ctrl-w" - name: "Vertical right split" key: "v, Ctrl-v" - name: "Horizontal bottom split" key: "s, Ctrl-s" - name: "Go to files in the selection in horizontal splits" key: "f" - name: "Go to files in the selection in vertical splits" key: "F" - name: "Move to left split" key: "h, Ctrl-h, Left" - name: "Move to split below" key: "j, Ctrl-j, Down" - name: "Move to split above" key: "k, Ctrl-k, Up" - name: "Move to right split" key: "l, Ctrl-l, Right" - name: "Close current window" key: "q, Ctrl-q" - name: "Only keep the current window, closing all the others" key: "o, Ctrl-o" - name: "Swap window to the left" key: "H" - name: "Swap window downwards" key: "J" - name: "Swap window upwards" key: "K" - name: "Swap window to the right" key: "L" - name: Helix Space mode keybinds: - name: "Open file picker" key: "f" - name: "Open file picker at current working directory" key: "F" - name: "Open buffer picker" key: "b" - name: "Open jumplist picker" key: "j" - name: "Show documentation for item under cursor in a popup (LSP)" key: "k" - name: "Open document symbol picker (LSP)" key: "s" - name: "Open workspace symbol picker (LSP)" key: "S" - name: "Open document diagnostics picker (LSP)" key: "d" - name: "Open workspace diagnostics picker (LSP)" key: "D" - name: "Rename symbol (LSP)" key: "r" - name: "Apply code action (LSP)" key: "a" - name: "Open last fuzzy picker" key: "'" - name: "Enter window mode" key: "w" - name: "Paste system clipboard after selections" key: "p" - name: "Paste system clipboard before selections" key: "P" - name: "Join and yank selections to clipboard" key: "y" - name: "Yank main selection to clipboard" key: "Y" - name: "Replace selections by clipboard contents" key: "R" - name: "Global search in workspace folder" key: "/" - name: "Open command palette" key: "?" - name: Helix Space mode (Popup) keybinds: - name: "Scroll up" key: "Ctrl-u" - name: "Scroll down" key: "Ctrl-d" - name: Helix Unimpaired keybinds: - name: "Go to previous diagnostic (LSP)" key: "[d" - name: "Go to next diagnostic (LSP)" key: "]d" - name: "Go to first diagnostic in document (LSP)" key: "[D" - name: "Go to last diagnostic in document (LSP)" key: "]D" - name: "Go to next function (TS)" key: "]f" - name: "Go to previous function (TS)" key: "[f" - name: "Go to next type definition (TS)" key: "]t" - name: "Go to previous type definition (TS)" key: "[t" - name: "Go to next argument/parameter (TS)" key: "]a" - name: "Go to previous argument/parameter (TS)" key: "[a" - name: "Go to next comment (TS)" key: "]c" - name: "Go to previous comment (TS)" key: "[c" - name: "Go to next test (TS)" key: "]T" - name: "Go to previous test (TS)" key: "[T" - name: "Go to next paragraph" key: "]p" - name: "Go to previous paragraph" key: "[p" - name: "Go to next change" key: "]g" - name: "Go to previous change" key: "[g" - name: "Go to first change" key: "]G" - name: "Go to last change" key: "[G" - name: "Add newline above" key: "[Space" - name: "Add newline below" key: "]Space" - name: Helix Insert mode keybinds: - name: "Switch to normal mode" key: "Escape" - name: "Commit undo checkpoint" key: "Ctrl-s" - name: "Autocomplete" key: "Ctrl-x" - name: "Insert a register content" key: "Ctrl-r" - name: "Delete previous word" key: "Ctrl-w, Alt-Backspace" - name: "Delete next word" key: "Alt-d, Alt-Delete" - name: "Delete to start of line" key: "Ctrl-u" - name: "Delete to end of line" key: "Ctrl-k" - name: "Insert new line" key: "Ctrl-j, Enter" - name: "Delete previous char" key: "Backspace, Ctrl-h" - name: "Delete previous char" key: "Delete, Ctrl-d" - name: "Move to previous line" key: "Up" - name: "Move to next line" key: "Down" - name: "Backward a char" key: "Left" - name: "Forward a char" key: "Right" - name: "Move to line end" key: "End" - name: "Move to line start" key: "Home" - name: "Move one page up" key: "PageUp" - name: "Move one page down" key: "PageDown" - name: Helix Picker keybinds: - name: "Next entry" key: "Tab, Down, Ctrl-n" - name: "Previous entry" key: "Shift-Tab, Up, Ctrl-p" - name: "Page up" key: "PageUp, Ctrl-u" - name: "Page down" key: "PageDown, Ctrl-d" - name: "Go to first entry" key: "Home" - name: "Go to last entry" key: "End" - name: "Filter options" key: "Ctrl-space" - name: "Open selected" key: "Enter" - name: "Open horizontally" key: "Ctrl-s" - name: "Open vertically" key: "Ctrl-v" - name: "Close picker" key: "Escape, Ctrl-c" - name: Helix Prompt keybinds: - name: "Close prompt" key: "Escape, Ctrl-c" - name: "Backward a word" key: "Alt-b, Alt-Left" - name: "Backward a char" key: "Ctrl-b, Left" - name: "Forward a word" key: "Alt-f, Ctrl-Right" - name: "Forward a char" key: "Ctrl-f, Right" - name: "Move prompt end" key: "Ctrl-e, End" - name: "Move prompt start" key: "Ctrl-a, Home" - name: "Delete previous word" key: "Ctrl-w, Alt-Backspace, Ctrl-Backspace" - name: "Delete next word" key: "Alt-d, Alt-Delete, Ctrl-Delete" - name: "Delete to start of line" key: "Ctrl-u" - name: "Delete to end of line" key: "Ctrl-k" - name: "Delete previous char" key: "backspace, Ctrl-h" - name: "Delete next char" key: "delete, Ctrl-d" - name: "Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later" key: "Ctrl-s" - name: "Select previous history" key: "Ctrl-p, Up" - name: "Select next history" key: "Ctrl-n, Down" - name: "Insert the content of the register selected by following input char" key: "Ctrl-r" - name: "Select next completion item" key: "Tab" - name: "Select previous completion item" key: "BackTab" - name: "Open selected" key: "Enter" ================================================ FILE: examples/ansi.yml ================================================ # https://github.com/cheat/cheatsheets - name: ansi keybinds: - name: 'Text Reset' key: '\e[0m' - name: 'Black' key: '\e[0;30m' - name: 'Red' key: '\e[0;31m' - name: 'Green' key: '\e[0;32m' - name: 'Yellow' key: '\e[0;33m' - name: 'Blue' key: '\e[0;34m' - name: 'Purple' key: '\e[0;35m' - name: 'Cyan' key: '\e[0;36m' - name: 'White' key: '\e[0;37m' - name: 'Bold Black' key: '\e[1;30m' - name: 'Bold Red' key: '\e[1;31m' - name: 'Bold Green' key: '\e[1;32m' - name: 'Bold Yellow' key: '\e[1;33m' - name: 'Bold Blue' key: '\e[1;34m' - name: 'Bold Purple' key: '\e[1;35m' - name: 'Bold Cyan' key: '\e[1;36m' - name: 'Bold White' key: '\e[1;37m' - name: 'Underline Black' key: '\e[4;30m' - name: 'Underline Red' key: '\e[4;31m' - name: 'Underline Green' key: '\e[4;32m' - name: 'Underline Yellow' key: '\e[4;33m' - name: 'Underline Blue' key: '\e[4;34m' - name: 'Underline Purple' key: '\e[4;35m' - name: 'Underline Cyan' key: '\e[4;36m' - name: 'Underline White' key: '\e[4;37m' - name: 'Background Black' key: '\e[40m' - name: 'Background Red' key: '\e[41m' - name: 'Background Green' key: '\e[42m' - name: 'Background Yellow' key: '\e[43m' - name: 'Background Blue' key: '\e[44m' - name: 'Background Purple' key: '\e[45m' - name: 'Background Cyan' key: '\e[46m' - name: 'Background White' key: '\e[47m' - name: 'High Intensity Black' key: '\e[0;90m' - name: 'High Intensity Red' key: '\e[0;91m' - name: 'High Intensity Green' key: '\e[0;92m' - name: 'High Intensity Yellow' key: '\e[0;93m' - name: 'High Intensity Blue' key: '\e[0;94m' - name: 'High Intensity Purple' key: '\e[0;95m' - name: 'High Intensity Cyan' key: '\e[0;96m' - name: 'High Intensity White' key: '\e[0;97m' - name: 'Bold High Intensity Black' key: '\e[1;90m' - name: 'Bold High Intensity Red' key: '\e[1;91m' - name: 'Bold High Intensity Green' key: '\e[1;92m' - name: 'Bold High Intensity Yellow' key: '\e[1;93m' - name: 'Bold High Intensity Blue' key: '\e[1;94m' - name: 'Bold High Intensity Purple' key: '\e[1;95m' - name: 'Bold High Intensity Cyan' key: '\e[1;96m' - name: 'Bold High Intensity White' key: '\e[1;97m' - name: 'High Intensity backgrounds Black' key: '\e[0;100m' - name: 'High Intensity backgrounds Red' key: '\e[0;101m' - name: 'High Intensity backgrounds Green' key: '\e[0;102m' - name: 'High Intensity backgrounds Yellow' key: '\e[0;103m' - name: 'High Intensity backgrounds Blue' key: '\e[0;104m' - name: 'High Intensity backgrounds Purple' key: '\e[0;105m' - name: 'High Intensity backgrounds Cyan' key: '\e[0;106m' - name: 'High Intensity backgrounds White' key: '\e[0;107m' ================================================ FILE: examples/awesomewm.yml ================================================ # Awesome WM Documentation # source: https://awesomewm.org/doc/manpages/awesome.1.html - name: awesomewm (window manager control) keybinds: - name: "Restart awesome" key: "[Mod4] [Ctrl] [r]" - name: "Quit awesome" key: "[Mod4] [Shift] [q]" - name: "Run prompt" key: "[Mod4] [r]" - name: "Run Lua code prompt" key: "[Mod4] [x]" - name: "Start terminal emulator" key: "[Mod4] [Return]" - name: "Open main menu" key: "[Mod4] [w]" - name: awesomewm (clients) keybinds: - name: "Maximize client" key: "[Mod4] [m]" - name: "Minimize client" key: "[Mod4] [n]" - name: "Restore client" key: "[Mod4] [Ctrl] [n]" - name: "Set client fullscreen" key: "[Mod4] [f]" - name: "Kill focused client" key: "[Mod4] [Shift] [c]" - name: "Set client on-top" key: "[Mod4] [t]" - name: awesomewm (mouse) keybinds: - name: "View tag" key: "[Button1] on tag name" - name: "Switch to previous tag" key: "[Button4] on tag name" - name: "Switch to next tag" key: "[Button5] on tag name" - name: "Switch to previous tag" key: "[Button4] on root window" - name: "Switch to next/previous layout" key: "([Button1] / [Button3]) / ([Button4] / [Button5]) on layout symbol" - name: "Tag client with this tag" key: "[Mod4] [Button1] on tag" - name: "Move window" key: "[Mod4] [Button1] on client" - name: "Toggle this tag for client" key: "[Mod4] [Button3] on tag" - name: "Resize window" key: "[Mod4] [Button3] on client" - name: "Add tag to current view" key: "[Button3] clicked on tag" - name: awesomewm (navigation) keybinds: - name: "Focus next client" key: "[Mod4] [j]" - name: "Focus previous client" key: "[Mod4] [k]" - name: "Focus first urgent client" key: "[Mod4] [u]" - name: "View previous tag" key: "[Mod4] [Left]" - name: "View next tag" key: "[Mod4] [Right]" - name: "Switch to tag [1~9]" key: "[Mod4] [1~9]" - name: "Focus next screen" key: "[Mod4] [Ctrl] [j]" - name: "Focus previous screen" key: "[Mod4] [Ctrl] [k]" - name: "Focus previously selected tag set" key: "[Mod4] [Esc]" - name: awesomewm (layout modification) keybinds: - name: "Switch client with next client" key: "[Mod4] [Shift] [j]" - name: "Switch client with previous client" key: "[Mod4] [Shift] [k]" - name: "Send client to next screen" key: "[Mod4] [o]" - name: "Decrease master width factor by 5%" key: "[Mod4] [h]" - name: "Increase master width factor by 5%" key: "[Mod4] [l]" - name: "Increase number of master windows by 1" key: "[Mod4] [Shift] [h]" - name: "Decrease number of master windows by 1" key: "[Mod4] [Shift] [l]" - name: "Increase number of columns for non-master windows by 1" key: "[Mod4] [Ctrl] [h]" - name: "Decrease number of columns for non-master windows by 1" key: "[Mod4] [Ctrl] [l]" - name: "Next Layout" key: "[Mod4] [Space]" - name: "Previous Layout" key: "[Mod4] [Shift] [Space]" - name: "Floating master" key: "[Mod4] [Ctrl] [Space]" - name: "Swap focused client with master" key: "[Mod4] [Ctrl] [Return]" - name: "Toggle tag view" key: "[Mod4] [Ctrl] [1~9]" - name: "Tag client with tag" key: "[Mod4] [Shift] [1~9]" - name: "Toggle tag on client" key: "[Mod4] [Shift] [Ctrl] [1~9]" ================================================ FILE: examples/bspwm.yml ================================================ # bspwm/sxhkd # source: https://github.com/baskerville/bspwm/blob/master/examples/sxhkdrc - name: bspwm keybinds: - name: terminal key: super + Return - name: program launcher key: super + space - name: reload sxhkd key: super + Escape - name: quit/escape bspwm key: super + alt + {q,r} - name: toggle tiled and monocle layout key: super + m - name: swap current node and biggest window key: super + g - name: send newest node to newest preselection key: super + y - name: set tiled state key: super + t - name: set pseudo tiled state key: super + shift + t - name: set floating state key: super + s - name: set fullscreen state key: super + f - name: set node to {marked, locked, sticky, private} key: super + ctrl + {m, x, y, z} - name: focus node in direction key: super + h,j,k,l - name: focus {parent, brother, first, second} node key: super + p, b, comma, period - name: focus {next, prev} window in current desktop key: super + {_, shift +} c - name: focus {next, prev} desktop key: super + bracket{left, right} - name: focus last node/desktop key: super + {grave, tab} - name: focus {older, newer} node in history key: super + {o, i} - name: focus or send to desktop key: super + {_, shift +} 1-9,0 - name: preselect direction key: super + ctrl + {h,j,k,l} - name: preselect ratio key: super + ctrl + {1-9} - name: cancel preselection for focused node key: super + ctrl + space - name: cancel preselection for focused desktop key: super + ctrl + shift + space - name: expand window key: super + alt + {h,j,k,l} - name: contract window key: super + alt + shift + {h,j,k,l} - name: move floating window key: super + {Left,Down,Up,Right} ================================================ FILE: examples/changefile ================================================ #!/usr/bin/env bash # A raw script to convert a cheat file from # https://github.com/cheat/cheatsheets\ # to a markdown file for keyb cheat sheets. FILENAME=${1//.yml/} sed -E -i 's/^[-]//g' "$1" sed -E -i '2s/^//' "$1" sed -E -i '/./!d' "$1" sed -E -i 's/"//g' "$1" sed -E -i 's/^# (.*)/\ \ - name: "\1"/' "$1" sed -E -i 's/^([a-zA-Z].*)/\ \ \ \ key: "\1"/' "$1" sed -E -i "1s|^|# https://github.com/cheat/cheatsheets\n|" "$1" sed -E -i "2s/^/- name: $FILENAME\n/" "$1" sed -E -i "3s/^/\ \ keybinds:\n/" "$1" ================================================ FILE: examples/config/README.md ================================================ # Configuration keyb will accept the following config in decreasing priority: - `-c FILE` flag - The default config path `$XDG_CONFIG_HOME/keyb/config.yml` (see note) - The default configuration (see [default.yml](default.yml)) Note: If `$XDG_CONFIG_HOME` is set, it will be prioritized and used in Unix and Darwin systems. Otherwise, keyb will fall back to the default OS config directory defined as such: - Unix: `$XDG_CONFIG_HOME/keyb/`, - MacOS/Darwin: `$HOME/Library/Application Support/keyb/`, - Windows: `%Appdata%\keyb\` **Note**: `*.json` files are also supported. ## Options | Option | Default | Description | | ------------- | ------------------------ | ----------- | | `keyb_path` | OS-dependent (see above) | keyb file path | | `debug` | `false` | Debug mode | | `reverse` | `false` | Swap the name and key columns | | `mouse` | `true` | Mouse enabled | | `search_mode` | `false` | Start in search mode | | `sort_keys` | `false` | Sort keys alphabetically | | `title` | `""` | Title text | | `prompt` | `"keys > "` | Search bar prompt text | | `prompt_location` | `"top"` | Location of search bar: `top, bottom` | | `placeholder` | `"..."` | Search bar placeholder text | | `prefix_sep` | `";"` | Separator symbol between prefix and key | | `sep_width` | `4` | Separation width between columns | | `margin` | `0` | Space between window and border | | `padding` | `1` | Space between border and text | | `border` | `"hidden"` | Border style: `normal, rounded, double, thick, hidden`| ### Color Both ANSI and hex color codes are supported. | Color Option | Default | Description | | ---------------- | ---------- | ----------- | | `prompt` | - | Prompt text color | | `cursor_fg` | - | Cursor foreground | | `cursor_bg` | - | Cursor background | | `filter_fg` | `"#FFA066"`| Filter matching text foreground | | `filter_bg` | - | Filter matching text background | | `counter_fg` | - | Counter foreground | | `counter_bg` | - | Counter background | | `placeholder_fg` | - | Placeholder foreground | | `placeholder_bg` | - | Placeholder background | | `border_color` | - | Border color | If you are missing colors, see [Missing Colors](../../README.md#missing-colors). ### Hotkeys Multiple keys may be set for a single binding, separated by commas. | Hotkey | Default | Description | | ----------------------- | -------------------------- | ---------------- | | `up`, `down` | j, k / Up, Down | Move cursor | | `up_focus`, `down_focus`| Ctrl + j, ctrl + k | Move cursor in search mode | | `half_up, half_down` | Ctrl + u, d | Move half window (also works in search mode) | | `full_up, full_down` | Ctrl + b, f | Move full window (also works in search mode) | | `top, middle, bottom` | H, M, L | Go to top, middle, bottom of screen | | `first_line, last_line` | g, G | Go to first, last line | | `search` | / | Enter search mode | | `clear_search` | Alt + d | Clear current search (remains in search mode) | | `normal` | Esc | Exit search mode | | `quit` | Ctrl + c, q | Quit | These hotkeys configure the cursor behaviour in the search bar only: | Hotkey | Default | Description | | ----------------------- | --------------------------- | ---------------- | | `cursor_word_forward` | alt+right, alt+f | Move forward by word | | `cursor_word_backward` | alt+left, alt+b | Move backward by word | | `cursor_delete_word_backward` | alt+backspace | Delete word backward | | `cursor_delete_word_forward` | alt+delete | Delete word forward | | `cursor_delete_after_cursor` | alt+k | Delete after cursor | | `cursor_delete_before_cursor` | alt+u | Delete before cursor | | `cursor_line_start` | home, ctrl+a | Move cursor to start | | `cursor_line_end` | end, ctrl+e | Move cursor to end | | `cursor_paste` | ctrl+v | Paste into search bar| ================================================ FILE: examples/config/default.yml ================================================ settings: keyb_path: "$HOME/.config/keyb/keyb.yml" debug: false reverse: false mouse: true search_mode: false sort_keys: false title: "" prompt: 'keys > ' prompt_location: "top" placeholder: '...' prefix_sep: ; sep_width: 4 margin: 0 padding: 1 border: hidden color: prompt: "" cursor_fg: "" cursor_bg: "" filter_fg: "#FFA066" filter_bg: "" border_color: "" keys: quit: q, ctrl+c up: k, up down: j, down up_focus: ctrl+k down_focus: ctrl+j half_up: ctrl+u half_down: ctrl+d full_up: ctrl+b full_bottom: ctrl+f first_line: g last_line: G top: H middle: M bottom: L search: / clear_search: alt+d normal: esc cursor_word_forward: "alt+right, alt+f" cursor_word_backward: "alt+left, alt+b" cursor_delete_word_backward: "alt+backspace" cursor_delete_word_forward: "alt+delete" cursor_delete_after_cursor: "alt+k" cursor_delete_before_cursor: "alt+u" cursor_line_start: "home, ctrl+a" cursor_line_end: "end, ctrl+e" cursor_paste: "ctrl+v" ================================================ FILE: examples/curl.yml ================================================ # https://github.com/cheat/cheatsheets - name: curl keybinds: - name: "To download a file" key: "curl " - name: "To download and rename a file" key: "curl -o " - name: "To download multiple files" key: "curl -O -O " - name: "To download all sequentially numbered files (1-24)" key: "curl http://example.com/pic[1-24].jpg" - name: "To download a file and pass HTTP authentication" key: "curl -u : " - name: "To download a file with a proxy" key: "curl -x : " - name: "To download a file over FTP" key: "curl -u : -O ftp://example.com/pub/file.zip" - name: "To get an FTP directory listing" key: "curl ftp://username:password@example.com" - name: "To resume a previously failed download" key: "curl -C - -o " - name: "To fetch only the HTTP headers from a response" key: "curl -I " - name: "To fetch your external IP and network info as JSON" key: "curl http://ifconfig.me/all.json" - name: "To limit the rate of a download" key: "curl --limit-rate 1000B -O " - name: "To get your global IP" key: "curl httpbin.org/ip " - name: "To get only the HTTP status code" key: "curl -o /dev/null -w '%{http_code}\n' -s -I URL" ================================================ FILE: examples/cut.yml ================================================ # Copied from cut --help - name: cut keybinds: - name: "To cut out the third field of text or stdoutput that is delimited by a #" key: "cut -d# -f3" - name: "select only these bytes" key: "cut -b, --bytes=LIST " - name: "select only these characters" key: "cut -c, --characters=LIST " - name: "use DELIM instead of TAB for field delimiter" key: "cut -d, --delimiter=DELIM " - name: "complement the set of selected bytes, characters or fields" key: "cut --complement " - name: "do not print lines not containing delimiters" key: "cut -s, --only-delimited " - name: "use STRING as the output delimiter the default is to use the input delimiter" key: "cut --output-delimiter=STRING " - name: "line delimiter is NUL, not newline" key: "cut -z, --zero-terminated " ================================================ FILE: examples/date.yml ================================================ # Copied from date --help - name: date keybinds: - name: "To print the date in a format suitable for affixing to file names:" key: "date +%Y%m%d_%H%M%S" - name: "To show date in CET:" key: "TZ=CET date" - name: "To show the time on the west coast of the US (use tzselect(1) to find TZ):" key: "TZ='America/Los_Angeles' date" - name: "display time described by STRING, not 'now'" key: "date -d, --date=STRING" - name: "like --date; once for each line of DATEFILE" key: "date -f, --file=DATEFILE" - name: "output date/time in ISO 8601 format e.g. 006-08-14T02:34:56-06:00" key: "date -I[FMT], --iso-8601[=FMT]" - name: "output the available resolution of timestamps e.g. 0.000000001" key: "date --resolution" - name: "output date and time in RFC 5322 format. e.g. Mon, 14 Aug 2006 02:34:56 -0600" key: "date -R, --rfc-email" - name: "output date/time in RFC 3339 format e.g. 2006-08-14 02:34:56-06:00" key: "date --rfc-3339=FMT" - name: "display the last modification time of FILE" key: "date -r, --reference=FILE" - name: "set time described by STRING" key: "date -s, --set=STRING" - name: "print or set Coordinated Universal Time (UTC)" key: "date -u, --utc, --universal " - name: date (FORMAT controls the output) keybinds: - name: "a literal %" key: "%%" - name: "locale's abbreviated weekday name (e.g., Sun)" key: "%a" - name: "locale's full weekday name (e.g., Sunday)" key: "%A" - name: "locale's abbreviated month name (e.g., Jan)" key: "%b" - name: "locale's full month name (e.g., January)" key: "%B" - name: "locale's date and time (e.g., Thu Mar 3 23:05:25 2005)" key: "%c" - name: "century; like %Y, except omit last two digits (e.g., 20)" key: "%C" - name: "day of month (e.g., 01)" key: "%d" - name: "date; same as %m/%d/%y" key: "%D" - name: "day of month, space padded; same as %_d" key: "%e" - name: "full date; like %+4Y-%m-%d" key: "%F" - name: "last two digits of year of ISO week number (see %G)" key: "%g" - name: "year of ISO week number (see %V); normally useful only with %V" key: "%G" - name: "same as %b" key: "%h" - name: "hour (00..23)" key: "%H" - name: "hour (01..12)" key: "%I" - name: "day of year (001..366)" key: "%j" - name: "hour, space padded ( 0..23); same as %_H" key: "%k" - name: "hour, space padded ( 1..12); same as %_I" key: "%l" - name: "month (01..12)" key: "%m" - name: "minute (00..59)" key: "%M" - name: "a newline" key: "%n" - name: "nanoseconds (000000000..999999999)" key: "%N" - name: "locale's equivalent of either AM or PM; blank if not known" key: "%p" - name: "like %p, but lower case" key: "%P" - name: "quarter of year (1..4)" key: "%q" - name: "locale's 12-hour clock time (e.g., 11:11:04 PM)" key: "%r" - name: "24-hour hour and minute; same as %H:%M" key: "%R" - name: "seconds since the Epoch (1970-01-01 00:00 UTC)" key: "%s" - name: "second (00..60)" key: "%S" - name: "a tab" key: "%t" - name: "time; same as %H:%M:%S" key: "%T" - name: "day of week (1..7); 1 is Monday" key: "%u" - name: "week number of year, with Sunday as first day of week (00..53)" key: "%U" - name: "ISO week number, with Monday as first day of week (01..53)" key: "%V" - name: "day of week (0..6); 0 is Sunday" key: "%w" - name: "week number of year, with Monday as first day of week (00..53)" key: "%W" - name: "locale's date representation (e.g., 12/31/99)" key: "%x" - name: "locale's time representation (e.g., 23:13:48)" key: "%X" - name: "last two digits of year (00..99)" key: "%y" - name: "year" key: "%Y" - name: "+hhmm numeric time zone (e.g., -0400)" key: "%z" - name: "z +hh:mm numeric time zone (e.g., -04:00)" key: "%:" - name: ":z +hh:mm:ss numeric time zone (e.g., -04:00:00)" key: "%:" - name: "::z numeric time zone with : to necessary precision (e.g., -04, +05:30)" key: "%:" - name: "alphabetic time zone abbreviation (e.g., EDT)" key: "%Z" ================================================ FILE: examples/dd.yml ================================================ # https://github.com/cheat/cheatsheets - name: dd keybinds: - name: "Backup disk partition with dd" key: "dd if=/dev/sda1 of=~/localdisk_sda1.img" - name: "Backup disk partition with dd and compress" key: "dd if=/dev/sda1 | gzip -c > ~/localdisk_sda1.img.gz" - name: "Restore disk partition with dd" key: "dd if=~/localdisk_sda1.img of=/dev/sda1" - name: "Restore disk partition with dd with decompression" key: "gunzip -c ~/localdisk_sda1.img.gz | dd of=/dev/sda1" - name: "Every iteration, we read 512 Bytes." key: "dd if=/dev/urandom of=/tmp/test.txt count=2 bs=512" - name: "Create fixed size file with dd. Use sizes such as K, M and G with bs" key: "dd if=/dev/zero of=/root/test bs=1024 count=1" - name: "Convert text to lower case with dd and conv" key: "dd if=filetoconvert.txt of=convertedfile.txt conv=lcase" - name: "dd with the built-in `progress` functionality" key: "dd if=/dev/zero of=/dev/null bs=128M status=progress" - name: "dd with graphical return" key: "dcfldd if=/dev/zero of=/dev/null bs=500K" - name: "Create a 1MB file with zero allocated blocks:" key: "dd if=/dev/zero of=foo1 seek=1 bs=1M count=0" ================================================ FILE: examples/discord.yml ================================================ - name: discord (navigation) keybinds: - name: "Switch Servers" key: "[Ctrl] [Alt] [↑] or [↓]" - name: "Switch Channels" key: "[Alt] [↑] or [↓]" - name: "Switch Unread Channels" key: "[Alt] [Shift] [↑] or [↓]" - name: "Switch Unread & Mention Channels" key: "[Ctrl] [Shift] [Alt] [↑] or [↓]" - name: "Scroll Chat" key: "[Page Up] or [Page Down]" - name: "Jump to Oldest Unread Message" key: "[Shift] [Page Up]" - name: "Return To Connected Audio Channel" key: "[Ctrl] [Alt] [←]" - name: "Switch Last Channel/DMs" key: "[Ctrl] [Alt] [→]" - name: discord (menus) keybinds: - name: "Pins Popout" key: "[Ctrl] [P]" - name: "Mentions Popout" key: "[Ctrl] [I]" - name: "Member List" key: "[Ctrl] [U]" - name: "Emoji Picker" key: "[Ctrl] [E]" - name: "Help" key: "[Ctrl] [Shift] [H]" - name: "Search" key: "[Ctrl] [F]" - name: discord (creation) keybinds: - name: "Create/Join Server" key: "[Ctrl] [Shift] [N]" - name: "Find/Start DM" key: "[Ctrl] [K]" - name: "Create Group" key: "[Ctrl] [Shift] [T]" - name: "Start Call" key: "[Ctrl] [']" - name: "Focus Text Area" key: "Tab" - name: "Upload File" key: "[Ctrl] [Shift] [U]" - name: discord (interaction) keybinds: - name: "Mark Server Read" key: "[Shift] [Esc]" - name: "Mark Channel Read" key: "Esc" - name: "Answer Call" key: "[Ctrl] [Enter]" - name: "Decline Call" key: "Esc" - name: "Mute" key: "[Ctrl] [Shift] [M]" - name: "Deafen" key: "[Ctrl] [Shift] [D]" ================================================ FILE: examples/du.yml ================================================ # https://github.com/cheat/cheatsheets - name: du keybinds: - name: "To sort directories/files by size" key: "du -sk *| sort -rn" - name: "To show cumulative human-readable size" key: "du -sh" - name: "To show cumulative human-readable size and dereference symlinks" key: "du -shL" - name: "Show apparent size instead of disk usage (so sparse files will show greater than zero)" key: "du -h --apparent-size" - name: "To sort directories/files by size (human-readable)" key: "du -sh * | sort -rh " - name: "To list the 20 largest files and folders under the current working directory" key: "du -ma | sort -nr | head -n 20" ================================================ FILE: examples/dwm.yml ================================================ # https://github.com/cheat/cheatsheets - name: dwm keybinds: - name: "launch terminal." key: "[Shift]+[Mod]+[Enter]" - name: "show/hide bar." key: "[Mod]+[b]" - name: "dmenu for running programs like the x#www#browser." key: "[Mod]+[p]" - name: "push acive window from stack to master, or pulls last used window from stack onto master." key: "[Mod]+[Enter]" - name: "focus on next/previous window in current tag." key: "[Mod] + [j / k]" - name: "increases / decreases master size." key: "[Mod] + [h / l]" - name: dwm (Navigation) keybinds: - name: "moves your focus to tag 2." key: "[Mod]+[2]" - name: "move active window to the 2 tag." key: "[Shift]+[Mod]+[2]" - name: "increases / decreases number of windows on master" key: "[Mod] + [i / d]" - name: "move focus between screens (multi monitor setup)" key: "[Mod] + [, / .]" - name: "move active window to different screen." key: "[Shift]+[Mod]+[, / .]" - name: "view all windows on screen." key: "[Mod]+[0]" - name: "make focused window appear on all tags." key: "[Shift]+[Mod]+[0]" - name: "kill active window." key: "[Shift]+[Mod]+[c]" - name: "quit dwm cleanly." key: "[Shift]+[Mod]+[q]" - name: dwm (Layout) keybinds: - name: "tiled mode. []=" key: "[Mod]+[t]" - name: "floating mode. ><>" key: "[Mod]+[f]" - name: "monocle mode. [M] (single window fullscreen)" key: "[Mod]+[m]" - name: dwm (Floating) keybinds: - name: "to resize the floating window." key: "[Mod]+[R M B]" - name: "to move the floating window around." key: "[Mod]+[L M B]" - name: "toggles to the previous layout mode." key: "[Mod]+[Space]" - name: "to make an individual window float." key: "[Mod]+[Shift]+[Space]" - name: "to make an individual window un#float." key: "[Mod]+[M M B]" ================================================ FILE: examples/find.yml ================================================ # https://github.com/cheat/cheatsheets - name: find keybinds: - name: "To find files following symlinks (otherwise the symlinks are not followed)" key: "find -L . -type f" - name: "To find files by case-insensitive extension (ex: .jpg, .JPG, .jpG)" key: "find . -iname *.jpg" - name: "To find directories" key: "find . -type d" - name: "To find files" key: "find . -type f" - name: "To find files by octal permission" key: "find . -type f -perm 777" - name: "To find files with setuid bit set" key: "find . -xdev ( -perm -4000 ) -type f -print0 | xargs -0 ls -l" - name: "To find files newer than 1 day old and copy elsewhere (remove -p flag in xargs to not be asked)" key: "find . -type f -ctime -1 -print0 | xargs -0 -p cp -t " - name: "To find files with extension '.txt' and remove them" key: "find ./path/ -name '*.txt' -delete" - name: "To find files with tilde as postfix and remove them" key: "find ./path/ -name '*~' -delete" - name: "To find files with extension '.txt' and dump their contents" key: "find ./path/ -name '*.txt' -exec cat '{}' ;" - name: "To find files with extension '.txt' and look for a string into them" key: "find ./path/ -name '*.txt' | xargs grep 'string'" - name: "To find files with size bigger than 5 Mebibyte and sort them by size" key: "find . -size +5M -type f -print0 | xargs -0 ls -Ssh | sort -z" - name: "To find files modified more than 7 days ago and list file information" key: "find . -type f -mtime +7d -ls" - name: "To find symlinks owned by a user and list file information" key: "find . -type l -user -ls" - name: "To search for and delete empty directories" key: "find . -type d -empty -exec rmdir {} ;" - name: "To search for directories named build at a max depth of 2 directories" key: "find . -maxdepth 2 -name build -type d" - name: "To search all files who are not in .git directory" key: "find . ! -iwholename '*.git*' -type f" - name: "To find all files that have the same node (hard link) as MY_FILE_HERE" key: "find . -type f -samefile MY_FILE_HERE 2>/dev/null" - name: "To find all files in the current directory and modify their permissions" key: "find . -type f -exec chmod 644 {} ;" - name: "To find all files changed in last 2 days" key: "find . -type f -ctime -48h" - name: "Or created in last 2 days" key: "find . -type f -Btime -2" - name: "Or accessed in last 2 days" key: "find . -type f -atime -2" - name: "To find and rename (imperfect) all files and dirs that have a comma in the" key: "find . -name '*,*' | while read f; do echo mv $f ${f//,/};done" - name: "To find all broken links. Note -L returns a file unless it is a broken link" key: "find -L /usr/ports/packages -type l" - name: "To find and run multiple shell commands (without multiple execs)" key: "find . -type f -exec sh -c echo '{}'; cat '{}';" ================================================ FILE: examples/firefox.yml ================================================ - name: Firefox Navigation keybinds: - name: "Back" key: "[Alt] [←]" - name: "Forward" key: "[Alt] [→]" - name: "Home" key: "[Alt] [Home]" - name: "Open File" key: "[Ctrl] [O]" - name: "Reload" key: "F5" - name: "Reload (override cache)" key: "[Ctrl] [F5]" - name: "Stop" key: "Esc" - name: Firefox Current Page keybinds: - name: "Go Down a Screen" key: "[Page Down]" - name: "Go Up a Screen" key: "[Page Up]" - name: "Go to Bottom of Page" key: "End" - name: "Go to Top of Page" key: "Home" - name: "Move to Next Frame" key: "F6" - name: "Move to Previous Frame" key: "[Shift] [F6]" - name: "Print" key: "[Ctrl] [P]" - name: "Save Focused Link" key: "[Alt] [Enter]" - name: "Save Page As" key: "[Ctrl] [S]" - name: "Zoom In" key: "[Ctrl] [+]" - name: "Zoom Out" key: "[Ctrl] [-]" - name: "Zoom Reset" key: "[Ctrl] [0]" - name: Firefox Editing keybinds: - name: "Copy" key: "[Ctrl] [C]" - name: "Cut" key: "[Ctrl] [X]" - name: "Delete" key: "Del" - name: "Paste" key: "[Ctrl] [V]" - name: "Paste (as plain text)" key: "[Ctrl] [Shift] [V]" - name: "Redo" key: "[Ctrl] [Shift] [Z]" - name: "Select All" key: "[Ctrl] [A]" - name: "Undo" key: "[Ctrl] [Z]" - name: Firefox Search keybinds: - name: "Find" key: "[Ctrl] [F]" - name: "Find Again" key: "[Ctrl] [G]" - name: "Find Previous" key: "[Shift] [F3]" - name: "Quick Find within link-text only" key: "'" - name: "Quick Find" key: "/" - name: "Close the Find or Quick Find bar (when focused)" key: "Esc" - name: "Focus Search bar" key: "[Ctrl] [K]" - name: "Focus Search bar (Alternative)" key: "[Ctrl] [J]" - name: "Quickly switch between search engines (when Search Bar is focused)" key: "[Ctrl] ( [↑] / [↓] )" - name: "View menu to switch, add or manage search engines (when Search Bar is focused)" key: "[Alt] ( [↑] / [↓] )" - name: Firefox Windows & Tabs keybinds: - name: "Close Tab" key: "[Ctrl] [W]" - name: "Close Tab (Alternative)" key: "[Ctrl] [F4]" - name: "Close Window" key: "[Ctrl] [Shift] [W]" - name: "Move Tab in focus Left" key: "[Ctrl] [Shift] [Page Up]" - name: "Move Tab in focus Right" key: "[Ctrl] [Shift] [Page Down]" - name: "Move Tab in focus to start" key: "[Ctrl] [Home]" - name: "Move Tab in focus to end" key: "[Ctrl] [End]" - name: "New Tab" key: "[Ctrl] [T]" - name: "New Window" key: "[Ctrl] [N]" - name: "New Private Window" key: "[Ctrl] [Shift] [P]" - name: "Next Tab" key: "[Ctrl] [Tab]" - name: "Open Address in New Tab" key: "[Alt] [Enter]" - name: "Previous Tab" key: "[Ctrl] [Shift] [Tab]" - name: "Undo Close Tab" key: "[Ctrl] [Shift] [T]" - name: "Undo Close Window" key: "[Ctrl] [Shift] [N]" - name: "Select Tab 1 to 8" key: "[Ctrl] [1 to 8]" - name: "Select Last Tab" key: "[Alt] [9]" - name: "Tab Groups View" key: "[Ctrl] [Shift] [E]" - name: "Close Tab Groups View" key: "Esc" - name: "Next Tab Group (only for some keyboard layouts)" key: "[Ctrl] [`]" - name: "Previous Tab Group (only for some keyboard layouts)" key: "[Ctrl] [Shift] [`]" - name: Firefox History keybinds: - name: "History sidebar" key: "[Ctrl] [H]" - name: "Library window (History)" key: "[Ctrl] [Shift] [H]" - name: "Clear Recent History" key: "[Ctrl] [Shift] [Del]" - name: Firefox Bookmarks keybinds: - name: "Bookmark All Tabs" key: "[Ctrl] [Shift] [D]" - name: "Bookmark This Page" key: "[Ctrl] [D]" - name: "Bookmarks sidebar" key: "[Ctrl] [B]" - name: "Library window (Bookmarks)" key: "[Ctrl] [Shift] [B]" - name: Firefox Tools keybinds: - name: "Downloads" key: "[Ctrl] [J]" - name: "Add-ons" key: "[Ctrl] [Shift] [A]" - name: "Toggle Developer Tools" key: "[Ctrl] [Shift] [I]" - name: "Toggle Developer Tools (Alternative)" key: "F12" - name: "Web Console" key: "[Ctrl] [Shift] [K]" - name: "Inspector" key: "[Ctrl] [Shift] [C]" - name: "Debugger" key: "[Ctrl] [Shift] [S]" - name: "Style Editor" key: "[Shift] [F7]" - name: "Profiler" key: "[Shift] [F5]" - name: "Network" key: "[Ctrl] [Shift] [Q]" - name: "Developer Toolbar" key: "[Shift] [F2]" - name: "Responsive Design View" key: "[Ctrl] [Shift] [M]" - name: "Scratchpad" key: "[Shift] [F4]" - name: "Page Source" key: "[Ctrl] [U]" - name: "Browser Console" key: "[Ctrl] [Shift] [J]" - name: Firefox PDF Viewer keybinds: - name: "Next page" key: "→" - name: "Previous page" key: "←" - name: "Zoom in" key: "[Ctrl] [+]" - name: "Zoom out" key: "[Ctrl] [-]" - name: "Automatic Zoom" key: "[Ctrl] [0]" - name: "Rotate the document clockwise" key: "R" - name: "Rotate counterclockwise" key: "[Shift] [R]" - name: "Switch to Presentation Mode" key: "[Ctrl] [Alt] [P]" - name: "Toggle Hand Tool" key: "H" - name: "Focus the Page Number input box" key: "[Ctrl] [Alt] [G]" - name: Firefox Miscellaneous keybinds: - name: "Complete .com Address" key: "[Ctrl] [Enter]" - name: "Complete .net Address" key: "[Shift] [Enter]" - name: "Complete .org Address" key: "[Ctrl] [Shift] [Enter]" - name: "Delete Selected Autocomplete Entry" key: "Del" - name: "Toggle Full Screen" key: "F11" - name: "Toggle Menu Bar activation (KDE) (showing it temporarily when hidden)" key: "Alt" - name: "Toggle Menu Bar activation (GNOME) (showing it temporarily when hidden)" key: "F10" - name: "Show/Hide Add-on Bar" key: "[Ctrl] [/]" - name: "Caret Browsing" key: "F7" - name: "Select Location Bar" key: "F6" - name: Firefox Audio & Video Media keybinds: - name: "Toggle Play / Pause" key: "Space bar" - name: "Decrease volume" key: "↓" - name: "Increase volume" key: "↑" - name: "Mute audio" key: "[Ctrl] [↓]" - name: "Unmute audio" key: "[Ctrl] [↑]" - name: "Seek back 15 seconds" key: "←" - name: "Seek back 10 %" key: "[Ctrl] [←]" - name: "Seek forward 15 seconds" key: "→" - name: "Seek forward 10 %" key: "[Ctrl] [→]" - name: "Seek to the beginning" key: "Home" - name: "Seek to the end" key: "End" ================================================ FILE: examples/irssi.yml ================================================ # https://github.com/cheat/cheatsheets - name: irssi keybinds: - name: "To connect to an IRC server:" key : "/connect " - name: "To join a channel:" key: "/join #" - name: "To set a nickname:" key: "/nick " - name: "To send a private message to a user:" key: "/msg " - name: "To close the current channel window:" key: "/wc" - name: "To switch between channel windows:" key: "ALT+, eg. ALT+1, ALT+2" - name: "To list the nicknames within the active channel:" key: "/names" - name: "To change the channel topic:" key: "/topic " - name: "foo,#bar JOINS PARTS QUITS NICKS # Quieten only channels `#foo`, `#bar`" key: "/ignore " - name: "Quieten all channels" key: "/ignore * JOINS PARTS QUITS NICKS" - name: "To save the current Irssi session config into the configuration file:" key: "/save" - name: "To quit Irssi:" key: "/exit" ================================================ FILE: examples/kitty.yml ================================================ # https://github.com/cheat/cheatsheets - name: kitty keybinds: - name: "Line up" key: "ctrl+shift+up" - name: "Line down" key: "ctrl+shift+down" - name: "Page up" key: "ctrl+shift+page_up" - name: "Page down" key: "ctrl+shift+page_down" - name: "Top" key: "ctrl+shift+home" - name: "Bottom" key: "ctrl+shift+end" - name: "Previous shell prompt" key: "ctrl+shift+z" - name: "Next shell prompt" key: "ctrl+shift+x" - name: "Browse scrollback in less" key: "ctrl+shift+h" - name: "Browse last cmd output" key: "ctrl+shift+g" - name: "New tab" key: "ctrl+shift+t" - name: "Close tab" key: "ctrl+shift+q" - name: "Next tab" key: "ctrl+shift+right" - name: "Previous tab" key: "ctrl+shift+left" - name: "Next layout" key: "ctrl+shift+l" - name: "Move tab forward" key: "ctrl+shift+." - name: "Move tab backward" key: "ctrl+shift+," - name: "Set tab title" key: "ctrl+shift+alt+t" - name: "Copy to clipboard" key: "ctrl+shift+c" - name: "Paste from clipboard" key: "ctrl+shift+v" - name: "Paste from selection" key: "ctrl+shift+s" - name: "Increase font size" key: "ctrl+shift+equal" - name: "Decrease font size" key: "ctrl+shift+minus" - name: "Restore font size" key: "ctrl+shift+backspace" - name: "New window" key: "ctrl+shift+enter" - name: "New OS window" key: "ctrl+shift+n" - name: "Close window" key: "ctrl+shift+w" - name: "Next window" key: "ctrl+shift+]" - name: "Previous window" key: "ctrl+shift+[" - name: "Move window forward" key: "ctrl+shift+f" - name: "Move window backward" key: "ctrl+shift+b" - name: "Move window to top" key: "ctrl+shift+`" - name: "Visually focus window" key: "ctrl+shift+f7" - name: "Visually swap window" key: "ctrl+shift+f8" - name: "Focus specific window" key: "ctrl+shift+1, ctrl+shift+2 … ctrl+shift+0" - name: "Open URL" key: "ctrl+shift+e" - name: "Insert selected path" key: "ctrl+shift+p>f" - name: "Open selected path" key: "ctrl+shift+p>shft+f" - name: "Insert selected line" key: "ctrl+shift+p>l" - name: "Insert selected word" key: "ctrl+shift+p>w" - name: "Insert selected hash" key: "ctrl+shift+p>h" - name: "Open the selected file at the selected line" key: "ctrl+shift+p>n" - name: "Open the selected hyperlink" key: "ctrl+shift+p>y" ================================================ FILE: examples/less.yml ================================================ # https://github.com/cheat/cheatsheets - name: less keybinds: - name: "Go to end of file:" key: "G" - name: "Go to start of file:" key: "g" - name: "To edit the current file in $EDITOR:" key: "v" - name: "Search in file:" key: "/" - name: "Next result:" key: "n" - name: "Previous result:" key: "N" - name: "Toggle search highlighting:" key: "Alt-u" - name: "Follow (tail -f) a file after opening it:" key: "F" - name: "Move to next file (if multiple files opened, eg. `file1 file2`):" key: ":n" - name: "Move to previous file:" key: ":p" - name: "To save the contents to a file:" key: "s " ================================================ FILE: examples/ranger.yml ================================================ - name: ranger keybinds: - name: "navigation and tabs" key: "g" - name: "':open_with' command" key: "r" - name: "yank(copy)" key: "y" - name: "paste" key: "p" - name: "cut/delete" key: "d" - name: "sort" key: "o" - name: "filter_stack" key: "." - name: "changing settings" key: "z" - name: "undo" key: "u" - name: "linemode" key: "M" - name: "+, -, =" key: "setting access rights to files" ================================================ FILE: examples/screen.yml ================================================ # https://github.com/cheat/cheatsheets - name: screen keybinds: - name: "To start a new named screen session:" key: "screen -S " - name: "To detach from the current session:" key: "Ctrl+A; d" - name: "To re-attach a detached session:" key: "screen -r " - name: "To list all screen sessions:" key: "screen -ls" - name: "To quit a session:" key: "screen -XS quit" ================================================ FILE: examples/tar.yml ================================================ # https://github.com/cheat/cheatsheets - name: tar keybinds: - name: "To extract an uncompressed archive:" key: "tar -xvf /path/to/foo.tar" - name: "To extract a .tar in specified Directory:" key: "tar -xvf /path/to/foo.tar -C /path/to/destination/" - name: "To create an uncompressed archive:" key: "tar -cvf /path/to/foo.tar /path/to/foo/" - name: "To extract a .tgz or .tar.gz archive:" key: "tar -xzvf /path/to/foo.tgz" - name: "To create a .tgz or .tar.gz archive:" key: "tar -czvf /path/to/foo.tgz /path/to/foo/" - name: "To list the content of an .tgz or .tar.gz archive:" key: "tar -tzvf /path/to/foo.tgz" - name: "To extract a .tar.bz2 archive:" key: "tar -xjvf /path/to/foo.tar.bz2" - name: "To create a .tar.bz2 archive:" key: "tar -cjvf /path/to/foo.tar.bz2 /path/to/foo/" - name: "To list the content of an .tar.bz2 archive:" key: "tar -tjvf /path/to/foo.tar.bz2" ================================================ FILE: examples/tmux.yml ================================================ # tmux # source: http://man.openbsd.org/OpenBSD-current/man1/tmux.1 - name: tmux (copy mode) prefix: "[Ctrl] [b]" keybinds: - name: "Enter copy mode" key: "[" - name: "Bottom of history" key: "[M-<]" - name: "Top of history" key: "[M->]" - name: "Back to indentation" key: "[M-m]" - name: "Copy selection" key: "[M-w]" - name: "Paste selection" key: "[M-y]" - name: "Clear selection" key: "[Ctrl] [g]" - name: "Cursor to top line" key: "[M-R]" - name: "Cursor to middle line" key: "[M-r]" - name: "Cursor Up" key: "[↑]" - name: "Cursor Down" key: "[↓]" - name: "Cursor Left" key: "[←]" - name: "Cursor Right" key: "[→]" - name: tmux (copy mode vi) prefix: "[Ctrl] [b]" keybinds: - name: "Enter copy mode" key: "[" - name: "Bottom of history" key: "[G]" - name: "Top of history" key: "[g]" - name: "Copy selection" key: "[Enter]" - name: "Paste selection" key: "[p]" - name: "Cursor Up" key: "[k]" - name: "Cursor Down" key: "[j]" - name: "Cursor Left" key: "[h]" - name: "Cursor Right" key: "[l]" - name: tmux (window control) prefix: "[Ctrl] [b]" keybinds: - name: "Create new window" key: "[c]" - name: "Detach from session" key: "[d]" - name: "Rename current window" key: "[,]" - name: "Close current window" key: "[&]" - name: "List windows" key: "[w]" - name: "Previous window" key: "[p]" - name: "Next window" key: "[n]" - name: tmux (pane control) prefix: "[Ctrl] [b]" keybinds: - name: "Split pane horizontally" key: "[\"]" - name: "Split pane vertically" key: "[%]" - name: "Next pane" key: "[o]" - name: "Previous pane" key: "[;]" - name: "Show pane numbers" key: "[q]" - name: "Toggle pane zoom" key: "[z]" - name: "Convert pane into a window" key: "[!]" - name: "Kill current pane" key: "[x]" - name: "Swap panes" key: "[Ctrl] [O]" - name: "Display clock" key: "[t]" - name: "Transpose two letters (delete and paste)" key: "[q]" - name: "Move to the previous pane" key: "[{]" - name: "Move to the next pane" key: "[}]" - name: "Toggle between pane layouts" key: "[Space]" - name: "Resize pane (make taller)" key: "[↑]" - name: "Resize pane (make smaller)" key: "[↓]" - name: "Resize pane (make wider)" key: "[←]" - name: "Resize pane (make narrower)" key: "[→]" - name: tmux (session control) keybinds: - name: "Start a new session" key: "tmux" - name: "Start a new session with the name chosen" key: "tmux new -s " - name: "List all sessions" key: "tmux ls" - name: "Re-attach a detached session" key: "tmux attach -t " - name: "Re-attach a detached session (and detach it from elsewhere)" key: "tmux attach -d -t " - name: "Delete session" key: "tmux kill-session -t " ================================================ FILE: examples/unzip.yml ================================================ # https://github.com/cheat/cheatsheets - name: unzip keybinds: - name: "To extract an archive:" key: "unzip " - name: "To extract an archive to a specific directory:" key: "unzip -d " - name: "To test integrity of archive:" key: "unzip -tq " - name: "To list files and directories an archive:" key: "unzip -l " ================================================ FILE: examples/vim.yml ================================================ # vim # source: http://vim.rtorr.com - name: vim (tabs) keybinds: - name: "move to tab number #" key: "#gt" - name: "move the current split window into its own tab" key: "[Ctrl] [w], [t]" - name: "move current tab to the #th position (indexed from 0)" key: ":tabmove #" - name: "open a file in a new tab" key: "[:tabnew filename] / [:tabn filename]" - name: "close the current tab and all its windows" key: "[:tabclose] / [:tabc]" - name: "close all tabs except for the current one" key: "[:tabonly] / [:tabo]" - name: "move to the next tab" key: "[gt] / [:tabnext] / [:tabn]" - name: "move to the previous tab" key: "[gT] / [:tabprev] / [:tabp]" - name: vim (registers) keybinds: - name: "view all current registers" key: "[:reg] / [:registers]" - name: "access register `r` as a variable" key: "echo @r" - name: "last search pattern register" key: "\"/" - name: "the black hole register" key: "\"_" - name: "last yank register" key: "\"0" - name: "last big delete register" key: "\"1" - name: "big delete register stack" key: "\"2-\"9" - name: "system clipboard" key: "\"+" - name: "small delete register" key: "\"-" - name: "named registers" key: "\"a-\"z" - name: "append registers" key: "\"A-\"Z" - name: "record into register `r`" key: "qr" - name: vim (editing) keybinds: - name: "replace a single character" key: "r" - name: "join line below to the current one" key: "J" - name: "change (replace) entire line" key: "cc" - name: "change (replace) to the end of the word" key: "cw" - name: "change (replace) to the end of the line" key: "c$" - name: "delete character and substitute text" key: "s" - name: "delete line and substitute text (same as cc)" key: "S" - name: "transpose two letters (delete and paste)" key: "xp" - name: "undo" key: "u" - name: "redo" key: "[Ctrl] [r]" - name: "repeat last command" key: "." - name: vim (exiting) keybinds: - name: "write (save) the file, but don't exit" key: ":w" - name: "write (save) and quit" key: ":wq" - name: "write (save) and quit" key: ":x" - name: "quit (fails if there are unsaved changes)" key: ":q" - name: "quit and throw away unsaved changes" key: ":q!" - name: "quit all buffers and windows" key: ":qa" - name: "write (save) current file, if modified, and quit" key: "ZZ" - name: "quit without checking for changes" key: "ZQ" - name: vim (insert/appending text) keybinds: - name: "insert before the cursor" key: "i" - name: "insert at the beginning of the line" key: "I" - name: "insert (append) after the cursor" key: "a" - name: "insert (append) at the end of the line" key: "A" - name: "append (open) a new line below the current line" key: "o" - name: "append (open) a new line above the current line" key: "O" - name: "insert (append) at the end of the word" key: "ea" - name: "exit insert mode" key: "Esc" - name: vim (cut and paste) keybinds: - name: "yank (copy) a line" key: "yy" - name: "yank (copy) 2 lines" key: "2yy" - name: "yank lines n through N" key: ":n,Ny" - name: "yank (copy) word" key: "yw" - name: "yank (copy) to end of line" key: "y$" - name: "put (paste) the clipboard after cursor" key: "p" - name: "put (paste) before cursor" key: "P" - name: "delete (cut) a line" key: "dd" - name: "delete (cut) 2 lines" key: "2dd" - name: "delete lines n through N" key: ":n,Nd" - name: "delete (cut) word" key: "dw" - name: "delete (cut) to the end of the line" key: "D" - name: "delete (cut) to the end of the line" key: "d$" - name: "delete (cut) character" key: "x" - name: vim (visual mode) keybinds: - name: "start visual mode" key: "v" - name: "start linewise visual mode" key: "V" - name: "move to other end of marked area" key: "vo" - name: "start visual block mode" key: "[Ctrl] [v]" - name: "move to other corner of block" key: "vO" - name: "mark a word" key: "vaw" - name: "mark a block with ()" key: "vab" - name: "mark a block with {}" key: "vaB" - name: "mark inner block with ()" key: "vib" - name: "mark inner block with {}" key: "viB" - name: "exit visual mode" key: "Esc" - name: vim (multiple files) keybinds: - name: "edit a file in a new buffer" key: ":e filename" - name: "open a blank file for editing" key: ":ene" - name: "go to the next buffer" key: "[:bnext] / [:bn]" - name: "go to the previous buffer" key: "[:bprev] / [:bp]" - name: "delete a buffer (close a file)" key: ":bd" - name: "new buffer in split window" key: ":sp filename" - name: "new buffer in vertically split window" key: ":vs filename" - name: "split window" key: "[Ctrl] [w], [s]" - name: "switch windows" key: "[Ctrl] [w], [w]" - name: "quit a window" key: "[Ctrl] [w], [q]" - name: "split window vertically" key: "[Ctrl] [w], [v]" - name: "move cursor to window left" key: "[Ctrl] [w], [h]" - name: "move cursor to window right" key: "[Ctrl] [w], [l]" - name: "move cursor to window above" key: "[Ctrl] [w], [k]" - name: "move cursor to window below" key: "[Ctrl] [w], [j]" - name: "rotate windows clockwise" key: "[Ctrl] [w], [r]" - name: "move current window to a new tab" key: "[Ctrl] [w], [T]" - name: "close all windows except current window" key: ":on" - name: "maximize width of active window" key: "[Ctrl] [w], [|]" - name: "minimize width of active window" key: "[Ctrl] [w], [1], [|]" - name: "maximize height of active window" key: "[Ctrl] [w], [_]" - name: "minimize height of active window" key: "[Ctrl] [w], [1], [_]" - name: "equalize size of windows" key: "[Ctrl] [w], [=]" - name: vim (cursor movement) keybinds: - name: "move cursor left" key: "h" - name: "move cursor down" key: "j" - name: "move cursor up" key: "k" - name: "move cursor right" key: "l" - name: "jump forwards to the start of a word" key: "w" - name: "jump forwards to the start of a word" key: "W" - name: "jump forwards to the end of a word" key: "e" - name: "jump forwards to the end of a word" key: "E" - name: "jump backwards to the start of a word" key: "b" - name: "jump backwards to the start of a word" key: "B" - name: "jump to the start of the line" key: "0" - name: "jump to the first non-blank character of the line" key: "^" - name: "jump to the end of the line" key: "$" - name: "go to the last line of the document" key: "G" - name: "go to line number n" key: "nG" - name: "go to line number n" key: ":n" - name: "To the position before the latest jump" key: "''" - name: "jump to next occurrence of character x" key: "fx" - name: "jump to one character before the character x" key: "tx" - name: "jump to next paragraph" key: "}" - name: "jump to previous paragraph" key: "{" - name: "jump to home (top) of screen" key: "H" - name: "jump to last line of screen" key: "L" - name: "jump to middle of screen" key: "M" - name: "jump to 3rd instance of character x forward from cursor on current line." key: "3, f, x" - name: "jump to 3rd instance of character x back from cursor on current line." key: "3, F, x" - name: vim (screen movement) keybinds: - name: "move screen up by half page" key: "[Ctrl] [u]" - name: "move screen up by one page" key: "[Ctrl] [b]" - name: "move screen down by half page" key: "[Ctrl] [d]" - name: "move screen down by one page" key: "[Ctrl] [f]" - name: "center screen on cursor" key: "zz" - name: "align top of screen with cursor" key: "zt" - name: "align bottom of screen with cursor" key: "zb" - name: vim (visual commands) keybinds: - name: "shift text right" key: ">" - name: "shift text left" key: "<" - name: "auto-indent current line" key: "==" - name: "shift current line left by shiftwidth" key: "<<" - name: "shift current line right by shiftwidth" key: ">>" - name: "yank (copy) marked text" key: "y" - name: "delete marked text" key: "d" - name: "switch case" key: "~" - name: vim (search and replace) keybinds: - name: "search for word under cursor" key: "*" - name: "search for pattern" key: "/pattern" - name: "search backward for pattern" key: "?pattern" - name: "repeat search in same direction" key: "n" - name: "repeat search in opposite direction" key: "N" - name: "replace all" key: ":%s/old/new/g" - name: "replace all with confirmations" key: ":%s/old/new/gc" - name: vim (extra commands) keybinds: - name: "run a compiler and enter quickfix mode" key: "[:mak] / [:make]" - name: "execute external shell command" key: ":!" - name: "read external program output into current file" key: "[:r] / [:read]" - name: "move lines x through y to z (delete + put)" key: ":x,ymz" - name: "yank lines x through y and put to z (yank + put)" key: ":x,ytz" - name: "current line (cursor location)" key: ":." - name: "last line (bottom of file)" key: ":$" - name: "first line (top of file)" key: ":0" - name: "list all open files" key: ":ls" - name: "create html representation of current working buffer" key: ":%TOhtml" - name: vim (basic configuration) keybinds: - name: "show line numbers" key: ":set nu" - name: "lets you switch buffers without saving" key: ":set hid" - name: "highlight search matches" key: ":set hls" - name: "show commands as you type them" key: ":set sc" - name: "show line and column number of the cursor" key: ":set ru" - name: vim (folding) keybinds: - name: "creates a fold from the cursor down # lines" key: "zf#j" - name: "string creates a fold from the cursor to string" key: "zf/" - name: "moves the cursor to the next fold" key: "zj" - name: "moves the cursor to the previous fold" key: "zk" - name: "toggle a fold under cursor" key: "za" - name: "opens a fold at the cursor" key: "zo" - name: "opens all folds at the cursor" key: "zO" - name: "closes a fold under cursor" key: "zc" - name: "increases the foldlevel by one" key: "zm" - name: "closes all open folds" key: "zM" - name: "decreases the foldlevel by one" key: "zr" - name: "open all folds" key: "zR" - name: "deletes the fold at the cursor" key: "zd" - name: "deletes all folds" key: "zE" - name: "move to start of open fold" key: "[z" - name: "move to end of open fold" key: "]z" ================================================ FILE: examples/vscode.yml ================================================ # Visual Studio Code # source: https://code.visualstudio.com/docs/customization/keybinds - name: vscode (basic editing) keybinds: - name: Cut line (empty selection) key: ctrl + x - name: "Copy line (empty selection)" key: "[Ctrl] [C]" - name: "Delete Line" key: "[Ctrl] [Shift] [K]" - name: "Insert Line Below" key: "[Ctrl] [Enter]" - name: "Insert Line Above" key: "[Ctrl] [Shift] [Enter]" - name: "Move Line Down" key: "[Alt] [Down]" - name: "Move Line Up" key: "[Alt] [Up]" - name: "Copy Line Down" key: "[Shift] [Alt] [Down]" - name: "Copy Line Up" key: "[Shift] [Alt] [Up]" - name: "Add Selection To Next Find Match" key: "[Ctrl] [D]" - name: "Move Last Selection To Next Find Match" key: "[Ctrl] [K], [Ctrl] [D]" - name: "Undo last cursor operation" key: "[Ctrl] [U]" - name: "Select all occurrences of current selection" key: "[Ctrl] [Shift] [L]" - name: "Select all occurrences of current word" key: "[Ctrl] [F2]" - name: "Select current line" key: "[Ctrl] [I]" - name: "Insert Cursor Below" key: "[Ctrl] [Alt] [Down]" - name: "Insert Cursor Above" key: "[Ctrl] [Alt] [Up]" - name: "Jump to matching bracket" key: "[Ctrl] [Shift] [\\]" - name: "Indent Line" key: "[Ctrl] {]}" - name: "Outdent Line" key: "[Ctrl] {[}" - name: "Go to Beginning of Line" key: "Home" - name: "Go to End of Line" key: "End" - name: "Go to End of File" key: "[Ctrl] [End]" - name: "Go to Beginning of File" key: "[Ctrl] [Home]" - name: "Scroll Line Down" key: "[Ctrl] [Down]" - name: "Scroll Line Up" key: "[Ctrl] [Up]" - name: "Scroll Page Down" key: "[Ctrl] [PageDown]" - name: "Scroll Page Up" key: "[Ctrl] [PageUp]" - name: "Fold (collapse) region" key: "[Ctrl] [Shift] {[}" - name: "Unfold (uncollapse) region" key: "[Ctrl] [Shift] {]}" - name: "Fold (collapse) all regions" key: "[Ctrl] [Shift] [Alt] {[}" - name: "Unfold (uncollapse) all regions" key: "[Ctrl] [Shift] [Alt] {]}" - name: "Add Line Comment" key: "[Ctrl] [K], [Ctrl] [C]" - name: "Remove Line Comment" key: "[Ctrl] [K], [Ctrl] [U]" - name: "Toggle Line Comment" key: "[Ctrl] [/]" - name: "Toggle Block Comment" key: "[Shift] [Alt] [A]" - name: "Find" key: "[Ctrl] [F]" - name: "Replace" key: "[Ctrl] [H]" - name: "Find Next" key: "F3" - name: "Find Previous" key: "[Shift] [F3]" - name: "Toggle Find Case Sensitive" key: "[Alt] [C]" - name: "Toggle Find Regex" key: "[Alt] [R]" - name: "Toggle Find Whole Word" key: "[Alt] [W]" - name: "Toggle Use of Tab Key for Setting Focus" key: "[Ctrl] [M]" - name: vscode (rich language editing) keybinds: - name: "Trigger Suggest" key: "[Ctrl] [Space]" - name: "Trigger Parameter Hints" key: "[Ctrl] [Shift] [Space]" - name: "Format Code" key: "[Shift] [Alt] [F]" - name: "Go to Definition" key: "F12" - name: "Peek Definition" key: "[Alt] [F12]" - name: "Open Definition to the Side" key: "[Ctrl] [K], [F12]" - name: "Quick Fix" key: "[Ctrl] [.]" - name: "Show References" key: "[Shift] [F12]" - name: "Rename Symbol" key: "F2" - name: "Replace with Next Value" key: "[Ctrl] [Shift] [.]" - name: "Replace with Previous Value" key: "[Ctrl] [Shift] [,]" - name: "Expand AST Select" key: "[Shift] [Alt] [Right]" - name: "Shrink AST Select" key: "[Shift] [Alt] [Left]" - name: "Trim Trailing Whitespace" key: "[Ctrl] [Shift] [X]" - name: "Change Language Mode" key: "[Ctrl] [K], [M]" - name: vscode (navigation) keybinds: - name: "Show All Symbols" key: "[Ctrl] [T]" - name: "Go to Line..." key: "[Ctrl] [G]" - name: "Go to File..., Quick Open" key: "[Ctrl] [P]" - name: "Go to Symbol..." key: "[Ctrl] [Shift] [O]" - name: "Show Errors and Warnings" key: "[Ctrl] [Shift] [M]" - name: "Go to Next Error or Warning" key: "F8" - name: "Go to Previous Error or Warning" key: "[Shift] [F8]" - name: "Show All Commands" key: "F1" - name: "Navigate History" key: "[Ctrl] [Tab]" - name: "Go Back" key: "[Alt] [Left]" - name: "Go Forward" key: "[Alt] [Right]" - name: vscode (editor/window management) keybinds: - name: "New Window" key: "[Ctrl] [Shift] [N]" - name: "Close Window" key: "[Ctrl] [Shift] [W]" - name: "Close Editor" key: "[Ctrl] [F4]" - name: "Close Folder" key: "[Ctrl] [K], [F]" - name: "Cycle Between Opened Editors" key: "[Ctrl] [`]" - name: "Split Editor" key: "[Ctrl] [\\]" - name: "Focus into Left Hand Editor" key: "[Ctrl] [1]" - name: "Focus into Side Editor" key: "[Ctrl] [2]" - name: "Focus into Right Hand Editor" key: "[Ctrl] [3]" - name: "Focus into Next Editor on the Left" key: "[Ctrl] [Alt] [Left]" - name: "Focus into Next Editor on the Right" key: "[Ctrl] [Alt] [Right]" - name: "Move Active Editor Left" key: "[Ctrl] [K], [Left]" - name: "Move Active Editor Right" key: "[Ctrl] [K], [Right]" - name: vscode (file management) keybinds: - name: "New File" key: "[Ctrl] [N]" - name: "Open File..." key: "[Ctrl] [O]" - name: "Save" key: "[Ctrl] [S]" - name: "Save As..." key: "[Ctrl] [Shift] [S]" - name: "Close File" key: "[Ctrl] [K], [W]" - name: "Close All Files" key: "[Ctrl] [K], [Ctrl] [W]" - name: "Close Other Files" key: "[Ctrl] [K], [Ctrl] [Shift] [W]" - name: "Add to Working Files" key: "[Ctrl] [K], [Enter]" - name: "Open Next Working File" key: "[Ctrl] [K], [Down]" - name: "Open Previous Working File" key: "[Ctrl] [K], [Up]" - name: "Copy Path of Active File" key: "[Ctrl] [K], [P]" - name: "Reveal Active File in Windows" key: "[Ctrl] [K], [R]" - name: "Show Opened File in New Window" key: "[Ctrl] [K], [O]" - name: vscode (display) keybinds: - name: "Toggle Full Screen" key: "F11" - name: "Zoom in" key: "[Ctrl] [=]" - name: "Zoom out" key: "[Ctrl] [-]" - name: "Toggle Sidebar Visibility" key: "[Ctrl] [B]" - name: "Show Debug" key: "[Ctrl] [Shift] [D]" - name: "Show Explorer" key: "[Ctrl] [Shift] [E]" - name: "Show Git" key: "[Ctrl] [Shift] [G]" - name: "Show Search" key: "[Ctrl] [Shift] [F]" - name: "Toggle Search Details" key: "[Ctrl] [Shift] [J]" - name: "Open New Command Prompt" key: "[Ctrl] [Shift] [C]" - name: "Show Output" key: "[Ctrl] [Shift] [U]" - name: "Toggle Markdown Preview" key: "[Ctrl] [Shift] [V]" - name: "Open Preview to the Side" key: "[Ctrl] [K], [V]" - name: vscode (debug) keybinds: - name: "Toggle Breakpoint" key: "F9" - name: "Continue" key: "F5" - name: "Pause" key: "F5" - name: "Step Into" key: "F11" - name: "Step Out" key: "[Shift] [F11]" - name: "Step Over" key: "F10" - name: "Stop" key: "[Shift] [F5]" - name: "Show Hover" key: "[Ctrl] [K], [Ctrl] [I]" - name: vscode (tasks) keybinds: - name: "Run Build Task" key: "[Ctrl] [Shift] [B]" - name: "Run Test Task" key: "[Ctrl] [Shift] [T]" ================================================ FILE: examples/weechat.yml ================================================ # https://github.com/cheat/cheatsheets - name: weechat keybinds: - name: "To set an unread marker on all windows:" key: "Ctrl-s Ctrl-u" - name: "To switch to the left buffer:" key: "Ctrl-p, Alt-left" - name: "To switch to the right buffer:" key: "Ctrl-n, Alt-right" - name: "To switch to the next buffer with activity:" key: "Alt-a" - name: "To switch buffers:" key: "Alt-0...9" - name: "To scroll buffer title:" key: "F9/F10" - name: "To scroll nick list:" key: "F11/F12" ================================================ FILE: examples/zip.yml ================================================ # https://github.com/cheat/cheatsheets - name: zip keybinds: - name: "To create zip file" key: "zip archive.zip file1 directory/" - name: "To create zip file from directory" key: "zip -r archive.zip directory/" - name: "To create zip file with password" key: "zip -P password archive.zip file1" - name: "To join a split zip file (.zip, .z01, .z02, etc)" key: "zip -FF splitZipfile.zip --out joined.zip" - name: "To list, test and extract zip archives, see unzip" key: "cheat unzip" ================================================ FILE: examples/zsh.yml ================================================ # ZSH # https://github.com/cheat/cheatsheets - name: zsh (basic) keybinds: - name: "A plain old glob" key: "print -l *.txt" - name: "Show text files that end in a number from 1 to 10" key: "print -l **/*<1-10>.txt" - name: "Show text files that start with the letter a" key: "print -l **/[a]*.txt" - name: "Show text files that start with either ab or bc" key: "print -l **/(ab|bc)*.txt" - name: "Show text files that don't start with a lower or uppercase c" key: "print -l **/[^cC]*.txt" - name: "Show only directories" key: "print -l **/*(/)" - name: "Show only regular files" key: "print -l **/*(.)" - name: "Show empty files" key: "print -l **/*(L0)" - name: "Show files greater than 3 KB" key: "print -l **/*(Lk+3)" - name: "Show files modified in the last hour" key: "print -l **/*(mh-1)" - name: "Sort files from most to least recently modified and show the last 3" key: "print -l **/*(om[1,3])" - name: "show files, smaller than 2MB, modified in last hour, only first 3 files" key: "print -l **/*(.Lm-2mh-1om[1,3])" - name: "Show every directory that contain directory `.git`" key: "print -l **/*(e:'[[ -d $REPLY/.git ]]':)" - name: "Return the file name (t stands for tail)" key: "print -l *.txt(:t)" - name: "Return the file name without the extension (r stands for remove_extension)" key: "print -l *.txt(:t:r)" - name: "Return the extension" key: "print -l *.txt(:e)" - name: "Return the parent folder of the file (h stands for head)" key: "print -l *.txt(:h)" - name: "Return the parent folder of the parent" key: "print -l *.txt(:h:h)" - name: "Return the parent folder of the first file" key: "print -l *.txt([1]:h)" - name: "Split the file name at each underscore" key: "echo ${(s._.)file}" - name: "Join expansion flag, opposite of the split flag." key: "array=(a b c d)" - name: "Short if" key: "if [[ ... ]] command" - name: "Short for" key: "for i in word ... ; command" - name: "Short while" key: "while [[ ... ]] { command ... }" - name: "Short until" key: "until [[ ... ]] { command ... }" - name: "Use output of command, when using pipe is not possible Similar to <( ), but creates temporary file (instead of FD or FIFO), when program needs to seek in output." key: "=( command )" - name: zsh (Parameter expansion) keybinds: - name: "store a glob in a variable" key: "files=(*.txt)" - name: "store a glob in a variable" key: "print -l $files" - name: "this is the syntax we saw before" key: "print -l $files(:h)" - name: "don't mix the two, or you'll get an error" key: "print -l ${files(:h)}" - name: "the :u modifier makes the text uppercase" key: "print -l ${files:u}" - name: zsh (:s modifier) keybinds: - name: "path/aaaBCd" key: "echo ${variable:s/bc/BC/}" - name: "path/aaaBCd" key: "echo ${variable:s_bc_BC_}" - name: "path.aaabcd (escaping the slash /)" key: "echo ${variable:s///./}" - name: "path.aaabcd (slightly more readable)" key: "echo ${variable:s_/_._}" - name: "path/aaabcd (only first A is replaced)" key: "echo ${variable:s/a/A/}" - name: "path/AAAbcd (all A is replaced)" key: "echo ${variable:gs/a/A/}" ================================================ FILE: go.mod ================================================ module github.com/kencx/keyb go 1.26.1 require ( github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/juju/ansiterm v1.0.0 github.com/muesli/reflow v0.3.0 github.com/sahilm/fuzzy v0.1.1 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= ================================================ FILE: main.go ================================================ package main import ( "flag" "fmt" "log" "os" tea "github.com/charmbracelet/bubbletea" "github.com/kencx/keyb/config" "github.com/kencx/keyb/output" "github.com/kencx/keyb/ui" ) const ( help = `usage: keyb [options] Options: -p, --print Print to stdout -e, --export Export to file [yaml, json] -k, --key Key bindings at custom path -c, --config Config file at custom path -v, --version Version info -h, --help Show help Commands: a, add Add keybind to keyb file ` addHelp = `usage: keyb [-k file] add [app; name; key] Options: -k, --key Key bindings file at custom path -b, --binding Key binding -p, --prefix Ignore prefix ` ) var version string func main() { log.SetPrefix("keyb: ") log.SetFlags(log.Lshortfile) var ( stdout bool exportFile string keybFile string configFile string addBind string addPrefix bool ) shortVersion := flag.Bool("v", false, "version information") longVersion := flag.Bool("version", false, "version information") flag.BoolVar(&stdout, "p", false, "print to stdout") flag.BoolVar(&stdout, "print", false, "print to stdout") flag.StringVar(&exportFile, "e", "", "export to file") flag.StringVar(&exportFile, "export", "", "export to file") flag.StringVar(&keybFile, "k", "", "keybindings file") flag.StringVar(&keybFile, "key", "", "keybindings file") flag.StringVar(&configFile, "c", "", "config file") flag.StringVar(&configFile, "config", "", "config file") addCmd := flag.NewFlagSet("add", flag.ExitOnError) addCmd.StringVar(&addBind, "b", "", "keybind") addCmd.StringVar(&addBind, "binding", "", "keybind") addCmd.BoolVar(&addPrefix, "p", false, "prefix") addCmd.BoolVar(&addPrefix, "prefix", false, "prefix") flag.Usage = func() { os.Stdout.Write([]byte(help)) } flag.Parse() if *shortVersion || *longVersion { fmt.Println(version) os.Exit(0) } keys, cfg, err := config.Parse(configFile, keybFile) if err != nil { log.Fatal(err) } args := flag.Args() if len(args) > 1 { switch args[0] { case "add", "a": addCmd.Usage = func() { os.Stdout.Write([]byte(addHelp)) } addCmd.Parse(args[1:]) var addFile string if keybFile != "" { // use flag -k path addFile = keybFile } else { // use default path in config addFile = cfg.KeybPath } if err := config.AddEntry(addFile, addBind, addPrefix); err != nil { log.Fatal(err) } fmt.Printf("%s added to %s", addBind, addFile) os.Exit(0) default: fmt.Print(help) os.Exit(1) } } m := ui.NewModel(keys, cfg) if stdout { if err := output.ToStdout(m); err != nil { log.Fatal(err) } os.Exit(0) } if exportFile != "" { if err := output.ToFile(m, exportFile); err != nil { log.Fatal(err) } os.Exit(0) } if err := start(m); err != nil { log.Fatal(err) } } func start(m *ui.Model) error { p := tea.NewProgram(m, tea.WithMouseCellMotion(), tea.WithAltScreen()) if _, err := p.Run(); err != nil { return fmt.Errorf("failed to start: %w", err) } return nil } ================================================ FILE: output/output.go ================================================ package output import ( "encoding/json" "fmt" "os" "path/filepath" "github.com/kencx/keyb/ui" "gopkg.in/yaml.v2" ) func ToFile(m *ui.Model, path string) error { var ( output []byte err error ) path = os.ExpandEnv(path) ext := filepath.Ext(path) switch ext { case ".json": output, err = json.Marshal(m.Apps) if err != nil { return fmt.Errorf("failed to marshal to json: %w", err) } case ".yml", ".yaml": output, err = yaml.Marshal(m.Apps) if err != nil { return fmt.Errorf("failed to marshal to yaml: %w", err) } default: output = []byte(m.List.UnstyledString()) } if err := os.WriteFile(path, output, 0664); err != nil { return fmt.Errorf("failed to write to file: %w", err) } return nil } func ToStdout(m *ui.Model) error { output := m.List.UnstyledString() _, err := os.Stdout.Write([]byte(output)) if err != nil { return fmt.Errorf("failed to write to stdout: %w", err) } return nil } ================================================ FILE: output/output_test.go ================================================ package output import ( "io" "os" "path/filepath" "reflect" "testing" "github.com/kencx/keyb/config" "github.com/kencx/keyb/ui" "github.com/kencx/keyb/ui/list" "github.com/kencx/keyb/ui/table" ) var ( testTable = table.New([]*table.Row{table.NewHeading("foo"), {Text: "bar"}}) testConfig = &config.Config{} testApps = &config.Apps{ &config.App{ Name: "foo", Prefix: "bar", Keybinds: []config.KeyBind{ { Name: "key foo", Key: "key bar", }, }, }, } m = &ui.Model{List: list.New(testTable, testConfig), Apps: testApps} ) func TestToJson(t *testing.T) { tempDir := t.TempDir() path := filepath.Join(tempDir, "test.json") err := ToFile(m, path) if err != nil { t.Fatal(err) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } want := []byte(`[{"prefix":"bar","name":"foo","keybinds":[{"name":"key foo","key":"key bar"}]}]`) if !reflect.DeepEqual(got, want) { t.Fatalf("got %s, want %s", got, want) } } func TestToStdout(t *testing.T) { rescueStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err := ToStdout(m) if err != nil { t.Fatal(err) } w.Close() got, _ := io.ReadAll(r) os.Stdout = rescueStdout want := "foo \nbar " if string(got) != want { t.Errorf("got %q, want %q", got, want) } } ================================================ FILE: testdata/testConfig.json ================================================ { "settings": { "keyb_path": "./custom.yml", "debug": true, "reverse": true, "mouse": false, "search_mode": false, "sort_keys": true, "title": "", "prompt": "keys > ", "prompt_location": "bottom", "placeholder": "...", "prefix_sep": ";", "sep_width": 4, "margin": 1, "padding": 1, "border": "normal" }, "color": { "prompt": "", "cursor_fg": "", "cursor_bg": "", "filter_fg": "#FFA066", "filter_bg": "", "border_color": "" }, "keys": { "quit": "q, ctrl+c", "up": "k, up", "down": "j, down", "up_focus": "alt+k", "down_focus": "alt+j", "half_up": "ctrl+u", "half_down": "ctrl+d", "full_up": "ctrl+b", "full_bottom": "ctrl+f", "first_line": "g", "last_line": "G", "top": "H", "middle": "M", "bottom": "L", "search": "/", "clear_search": "alt+d", "normal": "esc", "cursor_word_forward": "alt+right, alt+f", "cursor_word_backward": "alt+left, alt+b", "cursor_delete_word_backward": "alt+backspace", "cursor_delete_word_forward": "alt+delete", "cursor_delete_after_cursor": "alt+k", "cursor_delete_before_cursor": "alt+u", "cursor_line_start": "home, ctrl+a", "cursor_line_end": "end, ctrl+e", "cursor_paste": "ctrl+v" } } ================================================ FILE: testdata/testConfig.yml ================================================ settings: keyb_path: "./custom.yml" debug: true reverse: true mouse: false search_mode: false sort_keys: true title: "" prompt: 'keys > ' prompt_location: "bottom" placeholder: '...' prefix_sep: ; sep_width: 4 margin: 1 padding: 1 border: normal color: prompt: "" cursor_fg: "" cursor_bg: "" filter_fg: "#FFA066" filter_bg: "" border_color: "" keys: quit: q, ctrl+c up: k, up down: j, down up_focus: alt+k down_focus: alt+j half_up: ctrl+u half_down: ctrl+d full_up: ctrl+b full_bottom: ctrl+f first_line: g last_line: G top: H middle: M bottom: L search: / clear_search: alt+d normal: esc cursor_word_forward: "alt+right, alt+f" cursor_word_backward: "alt+left, alt+b" cursor_delete_word_backward: "alt+backspace" cursor_delete_word_forward: "alt+delete" cursor_delete_after_cursor: "alt+k" cursor_delete_before_cursor: "alt+u" cursor_line_start: "home, ctrl+a" cursor_line_end: "end, ctrl+e" cursor_paste: "ctrl+v" ================================================ FILE: testdata/testConfigMinimal.json ================================================ { "settings": {} } ================================================ FILE: testdata/testConfigMinimal.yml ================================================ --- settings: ================================================ FILE: testdata/testkeyb.json ================================================ [ { "name": "test", "keybinds": [ { "name": "foo", "key": "bar" } ] } ] ================================================ FILE: testdata/testkeyb.yml ================================================ - name: test keybinds: - name: foo key: bar ================================================ FILE: ui/list/keymap.go ================================================ package list import ( "strings" "github.com/charmbracelet/bubbles/key" "github.com/kencx/keyb/config" ) type KeyMap struct { Quit key.Binding Up key.Binding Down key.Binding HalfUp key.Binding HalfDown key.Binding FullUp key.Binding FullDown key.Binding UpFocus key.Binding DownFocus key.Binding GoToFirstLine key.Binding GoToLastLine key.Binding GoToTop key.Binding GoToMiddle key.Binding GoToBottom key.Binding CenterCursor key.Binding Search key.Binding ClearSearch key.Binding Normal key.Binding TextInputKeyMap } type TextInputKeyMap struct { CharacterForward key.Binding CharacterBackward key.Binding WordForward key.Binding WordBackward key.Binding DeleteWordBackward key.Binding DeleteWordForward key.Binding DeleteAfterCursor key.Binding DeleteBeforeCursor key.Binding DeleteCharacterBackward key.Binding DeleteCharacterForward key.Binding LineStart key.Binding LineEnd key.Binding Paste key.Binding AcceptSuggestion key.Binding NextSuggestion key.Binding PrevSuggestion key.Binding } func CreateKeyMap(keys config.Keys) KeyMap { return KeyMap{ Quit: SetKey(keys.Quit), Up: SetKey(keys.Up), Down: SetKey(keys.Down), HalfUp: SetKey(keys.HalfUp), HalfDown: SetKey(keys.HalfDown), FullUp: SetKey(keys.FullUp), FullDown: SetKey(keys.FullDown), UpFocus: SetKey(keys.UpFocus), DownFocus: SetKey(keys.DownFocus), GoToFirstLine: SetKey(keys.GoToFirstLine), GoToLastLine: SetKey(keys.GoToLastLine), GoToTop: SetKey(keys.GoToTop), GoToMiddle: SetKey(keys.GoToMiddle), GoToBottom: SetKey(keys.GoToBottom), Search: SetKey(keys.Search), ClearSearch: SetKey(keys.ClearSearch), Normal: SetKey(keys.Normal), TextInputKeyMap: TextInputKeyMap{ CharacterForward: SetKey("right"), CharacterBackward: SetKey("left"), WordForward: SetKey(keys.CursorWordForward), WordBackward: SetKey(keys.CursorWordBackward), DeleteWordBackward: SetKey(keys.CursorDeleteWordBackward), DeleteWordForward: SetKey(keys.CursorDeleteWordForward), DeleteAfterCursor: SetKey(keys.CursorDeleteAfterCursor), DeleteBeforeCursor: SetKey(keys.CursorDeleteBeforeCursor), DeleteCharacterBackward: SetKey("backspace"), DeleteCharacterForward: SetKey("delete"), LineStart: SetKey(keys.CursorLineStart), LineEnd: SetKey(keys.CursorLineEnd), Paste: SetKey(keys.CursorPaste), AcceptSuggestion: SetKey("tab"), NextSuggestion: SetKey("ctrl+n"), PrevSuggestion: SetKey("ctrl+p"), }, } } func SetKey(s string) key.Binding { return key.NewBinding( key.WithKeys(splitAndTrim(s, ",")...), ) } func splitAndTrim(s, sep string) []string { sl := strings.Split(s, sep) for i := range sl { sl[i] = strings.TrimSpace(sl[i]) } return sl } ================================================ FILE: ui/list/list.go ================================================ package list import ( "github.com/kencx/keyb/config" "github.com/kencx/keyb/ui/table" "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type filterState int const ( unfiltered filterState = iota filtering ) type Model struct { keys KeyMap viewport viewport.Model table *table.Model searchBar textinput.Model search bool startInSearchMode bool filterState filterState filteredTable *table.Model currentHeading string title string debug bool cursor int maxRows int // max number of rows regardless of filterState margin int padding int scrollOffset int border lipgloss.Style counterStyle lipgloss.Style promptLocation string } func New(t *table.Model, c *config.Config) Model { keyMap := CreateKeyMap(c.Keys) m := Model{ keys: keyMap, viewport: viewport.Model{ YOffset: 0, MouseWheelDelta: 3, MouseWheelEnabled: c.Mouse, }, table: t, searchBar: textinput.Model{ Prompt: c.Prompt, PromptStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(c.PromptColor)), Placeholder: c.Placeholder, PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), EchoCharacter: '*', CharLimit: 0, Cursor: cursor.New(), KeyMap: textinput.KeyMap(keyMap.TextInputKeyMap), ShowSuggestions: false, }, startInSearchMode: c.SearchMode, filteredTable: table.NewEmpty(t.LineCount), title: c.Title, debug: c.Debug, cursor: 0, maxRows: t.LineCount, margin: c.Margin, padding: c.Padding, scrollOffset: 5, counterStyle: lipgloss.NewStyle().Faint(true).Margin(0, 1), promptLocation: c.PromptLocation, } m.table.SepWidth = c.SepWidth m.filteredTable.SepWidth = c.SepWidth m.scrollOffset += (m.margin * 2) + (m.padding * 2) m.style(c) if m.startInSearchMode { m.startSearch() } return m } func (m *Model) style(c *config.Config) { if c.PlaceholderFg != "" || c.PlaceholderBg != "" { m.searchBar.PlaceholderStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(c.PlaceholderFg)). Background(lipgloss.Color(c.PlaceholderBg)) } if c.CounterFg != "" || c.CounterBg != "" { m.counterStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(c.CounterFg)).Background(lipgloss.Color(c.CounterBg)).Margin(0, 1) } var b lipgloss.Border switch c.BorderStyle { case "normal": b = lipgloss.NormalBorder() case "rounded": b = lipgloss.RoundedBorder() case "double": b = lipgloss.DoubleBorder() case "thick": b = lipgloss.ThickBorder() default: b = lipgloss.HiddenBorder() } m.border = lipgloss.NewStyle().BorderStyle(b).BorderForeground(lipgloss.Color(c.BorderColor)) // row specific config if !m.table.Empty() { cursor := lipgloss.NewStyle().Bold(true). Foreground(lipgloss.Color(c.CursorFg)). Background(lipgloss.Color(c.CursorBg)) s := table.RowStyles{ Normal: lipgloss.NewStyle().Margin(0, 2).TabWidth(lipgloss.NoTabConversion), Heading: lipgloss.NewStyle().Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion), Selected: cursor.Margin(0, 2).TabWidth(lipgloss.NoTabConversion), SelectedHeading: cursor.Margin(0, 1).Bold(true).TabWidth(lipgloss.NoTabConversion), Filtered: lipgloss.NewStyle(). Foreground(lipgloss.Color(c.FilterFg)). Background(lipgloss.Color(c.FilterBg)). TabWidth(lipgloss.NoTabConversion), } for _, row := range m.table.Rows { row.PrefixSep = c.PrefixSep row.Reversed = c.Reverse row.Styles = s } } } func (m *Model) Init() tea.Cmd { return nil } func (m *Model) Resize(width, height int) { m.viewport.Width = width m.viewport.Height = height - m.scrollOffset } // Resets list to unfiltered state func (m *Model) Reset() { m.filteredTable.Reset() m.filterState = unfiltered m.currentHeading = "" m.cursorToBeginning() m.visibleRows() } // Sets items to be shown. All items are shown unless filtered func (m *Model) visibleRows() { if !m.filteredTable.Empty() { m.SyncContent(m.filteredTable) } else { m.SyncContent(m.table) } } // Sync content by updating cursor and visible rows func (m *Model) SyncContent(table *table.Model) { if table.Empty() { m.viewport.SetContent("") return } if m.cursor > table.LineCount { m.cursor = table.LineCount - 1 } for i, row := range table.Rows { if i == m.cursor { row.IsSelected = true m.currentHeading = row.Heading } else { row.IsSelected = false } } m.viewport.SetContent(table.Render()) m.maxRows = table.LineCount } func (m *Model) UnstyledString() string { return m.table.GetAlignedRows() } func (m *Model) searchMode() bool { return m.search && m.searchBar.Focused() } func (m *Model) startSearch() tea.Cmd { m.search = true m.filterState = filtering m.searchBar.Focus() return textinput.Blink } func (m *Model) cursorToBeginning() { m.cursor = 0 } func (m *Model) cursorToEnd() { m.cursor = m.maxRows - 1 } func (m *Model) cursorToViewTop() { m.cursor = m.viewport.YOffset + 3 } func (m *Model) cursorToViewMiddle() { m.cursor = (m.viewport.YOffset + m.viewport.Height) / 2 } func (m *Model) cursorToViewBottom() { m.cursor = m.viewport.YOffset + m.viewport.Height - 3 } func (m *Model) cursorPastViewTop() bool { return m.cursor < m.viewport.YOffset } func (m *Model) cursorPastViewBottom() bool { return m.cursor > m.viewport.YOffset+m.viewport.Height-1 } func (m *Model) cursorPastBeginning() bool { return m.cursor < 0 } func (m *Model) cursorPastEnd() bool { return m.cursor > m.maxRows-1 } ================================================ FILE: ui/list/list_test.go ================================================ package list import ( "testing" "github.com/kencx/keyb/config" "github.com/kencx/keyb/ui/table" ) var ( testRows = []*table.Row{ table.NewHeading("fooTable"), {Text: "foo"}, {Text: "bar"}, {Text: "baz"}, } testTable = table.New(testRows) testConfig = &config.Config{ Settings: config.Settings{ Title: "foo", Debug: true, Reverse: true, Mouse: true, Prompt: "prompt", Placeholder: "placeholder", PrefixSep: "$", SepWidth: 4, }} ) func TestNew(t *testing.T) { t.Run("populated", func(t *testing.T) { tm := New(testTable, testConfig) assertEqual(t, tm.table.LineCount, 4) assertEqual(t, tm.filterState, unfiltered) assertEqual(t, tm.filteredTable.LineCount, 0) assertEqual(t, tm.title, "foo") assertEqual(t, tm.debug, true) assertEqual(t, tm.table.Rows[0].Reversed, true) assertEqual(t, tm.viewport.MouseWheelEnabled, true) assertEqual(t, tm.searchBar.Prompt, "prompt") assertEqual(t, tm.searchBar.Placeholder, "placeholder") assertEqual(t, tm.table.Rows[0].PrefixSep, "$") assertEqual(t, tm.table.SepWidth, 4) }) t.Run("empty", func(t *testing.T) { tm := New(table.New([]*table.Row{table.EmptyRow(), table.EmptyRow()}), testConfig) assertEqual(t, tm.title, "foo") assertEqual(t, tm.table.LineCount, 0) assertEqual(t, tm.filterState, unfiltered) assertEqual(t, tm.filteredTable.LineCount, 0) }) } func TestReset(t *testing.T) { tm := New(testTable, testConfig) tm.filterState = filtering tm.searchBar.SetValue("searching...") tm.cursor = 10 tm.Reset() assertEqual(t, tm.filteredTable.Render(), "") assertEqual(t, tm.searchBar.Value(), "searching...") assertEqual(t, tm.filterState, unfiltered) assertEqual(t, tm.cursor, 0) assertEqual(t, tm.maxRows, tm.table.LineCount) } func assertEqual[T comparable](t *testing.T, got, want T) { if got != want { t.Errorf("got %#v, want %#v", got, want) } } ================================================ FILE: ui/list/update.go ================================================ package list import ( "sort" "strings" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/kencx/keyb/ui/table" "github.com/sahilm/fuzzy" ) func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: // to play nice with borders and truncation, // <2 results in border exceeding max width m.viewport.Width = msg.Width - max(2, (m.padding*2+m.margin*2)) m.viewport.Height = msg.Height - m.scrollOffset m.table.MaxWidth = m.viewport.Width - m.padding*2 m.filteredTable.MaxWidth = m.viewport.Width - m.padding*2 if m.cursorPastViewBottom() { m.cursor = m.viewport.YOffset + m.viewport.Height - 1 } case tea.MouseMsg: if !m.viewport.MouseWheelEnabled { break } switch msg.Button { case tea.MouseButtonWheelUp: m.cursor -= m.viewport.MouseWheelDelta if m.cursorPastViewTop() { m.viewport.ScrollUp(m.viewport.MouseWheelDelta) } case tea.MouseButtonWheelDown: m.cursor += m.viewport.MouseWheelDelta if m.cursorPastViewBottom() { m.viewport.ScrollDown(m.viewport.MouseWheelDelta) } } } switch { case m.searchMode(): cmds = append(cmds, m.handleSearch(msg)) default: cmds = append(cmds, m.handleNormal(msg)) } // cursor loop around if m.cursorPastBeginning() { m.cursorToEnd() m.viewport.GotoBottom() } else if m.cursorPastEnd() { m.cursorToBeginning() m.viewport.GotoTop() } m.visibleRows() return m, tea.Batch(cmds...) } func (m *Model) handleNormal(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, m.keys.Quit): return tea.Quit case key.Matches(msg, m.keys.Search): return m.startSearch() case key.Matches(msg, m.keys.ClearSearch): m.searchBar.Reset() return m.startSearch() case key.Matches(msg, m.keys.Up): m.cursor-- if m.cursorPastViewTop() { m.viewport.ScrollUp(1) } case key.Matches(msg, m.keys.Down): m.cursor++ if m.cursorPastViewBottom() { m.viewport.ScrollDown(1) } case key.Matches(msg, m.keys.UpFocus): m.cursor-- if m.cursorPastViewTop() { m.viewport.ScrollUp(1) } case key.Matches(msg, m.keys.DownFocus): m.cursor++ if m.cursorPastViewBottom() { m.viewport.ScrollDown(1) } case key.Matches(msg, m.keys.HalfUp): m.cursor -= m.viewport.Height / 2 if m.cursorPastViewTop() { m.viewport.HalfPageUp() } // don't loop around if m.cursorPastBeginning() { m.cursorToBeginning() m.viewport.GotoTop() } case key.Matches(msg, m.keys.HalfDown): m.cursor += m.viewport.Height / 2 if m.cursorPastViewBottom() { m.viewport.HalfPageDown() } // don't loop around if m.cursorPastEnd() { m.cursorToEnd() m.viewport.GotoBottom() } case key.Matches(msg, m.keys.FullUp): m.cursor -= m.viewport.Height if m.cursorPastViewTop() { m.viewport.PageUp() } // don't loop around if m.cursorPastBeginning() { m.cursorToBeginning() m.viewport.GotoTop() } case key.Matches(msg, m.keys.FullDown): m.cursor += m.viewport.Height if m.cursorPastViewBottom() { m.viewport.PageDown() } // don't loop around if m.cursorPastEnd() { m.cursorToEnd() m.viewport.GotoBottom() } case key.Matches(msg, m.keys.GoToFirstLine): m.cursorToBeginning() m.viewport.GotoTop() case key.Matches(msg, m.keys.GoToLastLine): m.cursorToEnd() m.viewport.GotoBottom() case key.Matches(msg, m.keys.GoToTop): m.cursorToViewTop() case key.Matches(msg, m.keys.GoToMiddle): m.cursorToViewMiddle() case key.Matches(msg, m.keys.GoToBottom): m.cursorToViewBottom() // case key.Matches(msg, m.keys.CenterCursor): // middle := m.viewport.Height / 2 // diff := m.cursor - middle // if diff > 0 { // m.viewport.LineDown(diff) // } else { // m.viewport.LineUp(diff) // } } } return nil } func (m *Model) handleSearch(msg tea.Msg) tea.Cmd { var ( cmds []tea.Cmd cmd tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: switch { case msg.String() == "ctrl+c": return tea.Quit case key.Matches(msg, m.keys.ClearSearch): m.searchBar.Reset() return m.startSearch() // scrolling in search mode case key.Matches(msg, m.keys.UpFocus): m.cursor-- if m.cursorPastViewTop() { m.viewport.ScrollUp(1) } return nil case key.Matches(msg, m.keys.DownFocus): m.cursor++ if m.cursorPastViewBottom() { m.viewport.ScrollDown(1) } return nil case key.Matches(msg, m.keys.HalfUp): m.cursor -= m.viewport.Height / 2 if m.cursorPastViewTop() { m.viewport.HalfPageUp() } // don't loop around if m.cursorPastBeginning() { m.cursorToBeginning() m.viewport.GotoTop() } case key.Matches(msg, m.keys.HalfDown): m.cursor += m.viewport.Height / 2 if m.cursorPastViewBottom() { m.viewport.HalfPageDown() } // don't loop around if m.cursorPastEnd() { m.cursorToEnd() m.viewport.GotoBottom() } case key.Matches(msg, m.keys.FullUp): m.cursor -= m.viewport.Height if m.cursorPastViewTop() { m.viewport.PageUp() } // don't loop around if m.cursorPastBeginning() { m.cursorToBeginning() m.viewport.GotoTop() } case key.Matches(msg, m.keys.FullDown): m.cursor += m.viewport.Height if m.cursorPastViewBottom() { m.viewport.PageDown() } // don't loop around if m.cursorPastEnd() { m.cursorToEnd() m.viewport.GotoBottom() } case key.Matches(msg, m.keys.Normal): m.search = false m.searchBar.Blur() if m.filteredTable.Empty() { m.filterState = unfiltered } return nil } } // filter with search input m.searchBar, cmd = m.searchBar.Update(msg) cmds = append(cmds, cmd) prefix := "h:" if strings.HasPrefix(m.searchBar.Value(), prefix) { matchHeadings(m, prefix) } else { matchRows(m) } // reset if search input is empty regardless of filterState if m.searchBar.Value() == "" { m.Reset() // remain in filtering state // until user explicitly returns to Normal mode m.filterState = filtering } return tea.Batch(cmds...) } func filter(term string, target []string) fuzzy.Matches { matches := fuzzy.Find(term, target) sort.Stable(matches) return matches } func matchHeadings(m *Model, prefix string) { var matches fuzzy.Matches value := strings.TrimSpace(strings.TrimPrefix(m.searchBar.Value(), prefix)) if !m.table.Empty() { matches = filter(value, m.table.GetPlainHeadings()) } // present new filtered rows m.filteredTable.Reset() if len(matches) == 0 { m.filteredTable.AppendRow(table.EmptyRow()) } else { var hlMatches []*table.Row // get non-pointers as filtering is ephemeral headings := m.table.GetCopyOfHeadings() for _, match := range matches { heading := headings[match.Index] heading.IsFiltered = true heading.MatchedIndex = match.MatchedIndexes hlMatches = append(hlMatches, &heading) hlMatches = append(hlMatches, m.table.GetAllRowsofHeading(heading.Text)...) } m.filteredTable.AppendRows(hlMatches...) } } func matchRows(m *Model) { var matches fuzzy.Matches if !m.table.Empty() { matches = filter(m.searchBar.Value(), m.table.GetPlainRowsWithoutHeadings()) } // present new filtered rows m.filteredTable.Reset() if len(matches) == 0 { m.filteredTable.AppendRow(table.EmptyRow()) } else { var hlMatches []*table.Row // get non-pointers as filtering is ephemeral rows := m.table.GetCopyOfRowsWithoutHeadings() for _, match := range matches { row := rows[match.Index] row.IsFiltered = true row.MatchedIndex = match.MatchedIndexes hlMatches = append(hlMatches, &row) } m.filteredTable.AppendRows(hlMatches...) } } ================================================ FILE: ui/list/view.go ================================================ package list import ( "fmt" "github.com/charmbracelet/lipgloss" ) func (m *Model) View() string { if m.table.Empty() { m.viewport.SetContent("\nNo key bindings found") } counter := formCounter(m) var view string if m.promptLocation == "bottom" { view = lipgloss.JoinVertical( lipgloss.Left, m.viewport.View(), counter, m.searchBar.View(), ) } else { view = lipgloss.JoinVertical( lipgloss.Left, m.searchBar.View(), counter, m.viewport.View(), ) } style := m.border. Margin(m.margin). Padding(m.padding). Width(m.viewport.Width) return style.Render(view) } func formCounter(m *Model) string { var counter string if m.filterState == filtering && m.searchBar.Value() != "" { counter = fmt.Sprintf("%d/%d %s", m.filteredTable.LineCount, m.table.LineCount, m.currentHeading) } else { counter = fmt.Sprintf("%d/%d %s", m.table.LineCount, m.table.LineCount, m.currentHeading) } if m.debug { counter = fmt.Sprintf("%s\tLine: %d YOffset: %d Height: %d", counter, m.cursor, m.viewport.YOffset, m.viewport.Height) } return m.counterStyle.Render(counter) } ================================================ FILE: ui/table/row.go ================================================ package table import ( "fmt" "github.com/charmbracelet/lipgloss" ) type Row struct { Text string Key string Prefix string PrefixSep string // default false unless prefix defined ShowPrefix bool // only used to show row's corresponding heading during filtering Heading string MatchedIndex []int Styles RowStyles IsHeading bool IsSelected bool IsFiltered bool Reversed bool } type RowStyles struct { Normal lipgloss.Style Heading lipgloss.Style Selected lipgloss.Style SelectedHeading lipgloss.Style Filtered lipgloss.Style } func NewHeading(text string) *Row { return &Row{ Text: text, IsHeading: true, } } func NewRow(text, key, prefix, heading string) *Row { r := &Row{ Text: text, Key: key, Prefix: prefix, Heading: heading, } r.ShowPrefix = r.Prefix != "" return r } func EmptyRow() *Row { return &Row{} } func (r *Row) String() string { if r.Text == "" && r.Key == "" { return "" } if r.IsHeading { return fmt.Sprintf("%s\t ", r.Text) } if r.Reversed { return r.ReverseString() } if !r.ShowPrefix { return fmt.Sprintf("%s\t%s", r.Text, r.Key) } key := fmt.Sprintf("%s %s %s", r.Prefix, r.PrefixSep, r.Key) return fmt.Sprintf("%s\t%s", r.Text, key) } func (r *Row) ReverseString() string { if !r.ShowPrefix { return fmt.Sprintf("%s\t%s", r.Key, r.Text) } key := fmt.Sprintf("%s %s %s", r.Prefix, r.PrefixSep, r.Key) return fmt.Sprintf("%s\t%s", key, r.Text) } func (r *Row) Render() string { s := r.Styles if r.IsSelected { if r.IsFiltered { // Inline to remove margins, paddings and borders from styledrunes unmatched := s.Selected.Inline(true) matched := s.Filtered.Inherit(unmatched) str := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched) if r.IsHeading { return s.SelectedHeading.Render(str) } return s.Selected.Render(str) } if r.IsHeading { return s.SelectedHeading.Render(r.String()) } return s.Selected.Render(r.String()) } if r.IsFiltered { unmatched := s.Normal.Inline(true) matched := s.Filtered.Inherit(unmatched) str := lipgloss.StyleRunes(r.String(), r.MatchedIndex, matched, unmatched) if r.IsHeading { return s.Heading.Render(str) } return s.Normal.Render(str) } if r.IsHeading { return s.Heading.Render(r.String()) } return s.Normal.Render(r.String()) } ================================================ FILE: ui/table/table.go ================================================ package table import ( "fmt" "strings" "github.com/juju/ansiterm/tabwriter" "github.com/muesli/reflow/truncate" ) type Model struct { Rows []*Row LineCount int SepWidth int MaxWidth int // prevents line wrapping } func New(rows []*Row) *Model { t := &Model{ Rows: rows, } for _, row := range rows { if row.String() != "" { t.LineCount += 1 } } return t } func NewEmpty(n int) *Model { return &Model{ Rows: make([]*Row, 1, max(2, n)), LineCount: 0, } } func (t *Model) Empty() bool { return t.LineCount <= 0 } func (t *Model) AppendRow(row *Row) { t.Rows = append(t.Rows, row) t.LineCount += 1 } func (t *Model) AppendRows(rows ...*Row) { t.Rows = append(t.Rows, rows...) t.LineCount += len(rows) } func (t *Model) Join(table *Model) { t.Rows = append(t.Rows, table.Rows...) t.LineCount += table.LineCount } func (t *Model) Reset() { t.Rows = nil t.LineCount = 0 } // Align and style rows func (t *Model) Render() string { var sb strings.Builder tw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0) for _, row := range t.Rows { if row != nil && row.String() != "" { fmt.Fprintln(tw, row.Render()) } } tw.Flush() if sb.String() != "" { sl := strings.Split(strings.TrimSuffix(sb.String(), "\n"), "\n") sb.Reset() // Unable to truncate while aligning due to nature of tabwriter for _, row := range sl { if t.MaxWidth > 0 { fmt.Fprintln(&sb, truncate.StringWithTail(row, uint(t.MaxWidth), "...")) } else { fmt.Fprintln(&sb, row) } } } return sb.String() } // Helper functions for retrieving specific rows in a table // Aligned but unstyled rows func (t *Model) GetAlignedRows() string { var sb strings.Builder tw := tabwriter.NewWriter(&sb, 8, 4, t.SepWidth, ' ', 0) for _, row := range t.Rows { if row != nil && row.String() != "" { fmt.Fprintln(tw, row.String()) } } tw.Flush() return strings.TrimSuffix(sb.String(), "\n") } func (t *Model) GetPlainHeadings() []string { var res []string for _, r := range t.Rows { if r.IsHeading { res = append(res, r.String()) } } return res } func (t *Model) GetPlainRowsWithoutHeadings() []string { var res []string for _, r := range t.Rows { if !r.IsHeading { res = append(res, r.String()) } } return res } func (t *Model) GetCopyOfHeadings() []Row { var res []Row for _, r := range t.Rows { if r.IsHeading { res = append(res, *r) } } return res } func (t *Model) GetCopyOfRowsWithoutHeadings() []Row { var res []Row for _, r := range t.Rows { if !r.IsHeading { res = append(res, *r) } } return res } func (t *Model) GetAllRowsofHeading(heading string) []*Row { var res []*Row for _, r := range t.Rows { if !r.IsHeading && r.Heading == heading { res = append(res, r) } } return res } ================================================ FILE: ui/table/table_test.go ================================================ package table import ( "testing" ) var ( testRows = []*Row{ NewHeading("heading"), {Text: "foo", Key: "bar"}, {Text: "baz", Key: "foo"}, } ) func TestNew(t *testing.T) { t.Run("populated", func(t *testing.T) { tt := New(testRows) assertEqual(t, tt.LineCount, 3) }) t.Run("empty", func(t *testing.T) { tt := NewEmpty(5) assertEqual(t, len(tt.Rows), 1) assertEqual(t, cap(tt.Rows), 5) assertEqual(t, tt.LineCount, 0) assertEqual(t, tt.Render(), "") }) } func TestAppend(t *testing.T) { row1 := NewRow("baz", "", "", "") row2 := NewRow("foobar", "", "", "") t.Run("appendRow", func(t *testing.T) { tt := New(testRows) originalCount := tt.LineCount tt.AppendRow(row1) assertEqual(t, tt.LineCount, originalCount+1) }) t.Run("appendRows", func(t *testing.T) { tt := New(testRows) originalCount := tt.LineCount tt.AppendRows(row1, row2) assertEqual(t, tt.LineCount, originalCount+2) }) } func TestJoin(t *testing.T) { t1 := New(testRows) t1Count := t1.LineCount t2 := New([]*Row{{Text: "baz"}}) t2Count := t2.LineCount t1.Join(t2) assertEqual(t, t1.LineCount, t1Count+t2Count) } func TestReset(t *testing.T) { tt := New(testRows) tt.Reset() assertEqual(t, tt.LineCount, 0) assertEqual(t, tt.Render(), "") } func assertEqual[T comparable](t *testing.T, got, want T) { if got != want { t.Errorf("got %#v, want %#v", got, want) } } ================================================ FILE: ui/ui.go ================================================ package ui import ( "sort" "strings" "github.com/kencx/keyb/config" "github.com/kencx/keyb/ui/list" "github.com/kencx/keyb/ui/table" tea "github.com/charmbracelet/bubbletea" ) type Model struct { List list.Model Apps *config.Apps } func NewModel(a config.Apps, config *config.Config) *Model { table := createParentTable(a, config.SortKeys) return &Model{ List: list.New(table, config), Apps: &a, } } func createParentTable(a config.Apps, sortKeys bool) *table.Model { if len(a) <= 0 { t := table.NewEmpty(1) return t } sort.Slice(a, func(i, j int) bool { return a[i].Name < a[j].Name }) parent := appToTable(a[0].Name, *a[0], sortKeys) if len(a) > 1 { for _, k := range a[1:] { child := appToTable(k.Name, *k, sortKeys) parent.Join(child) } } return parent } func appToTable(heading string, app config.App, sortKeys bool) *table.Model { var rows []*table.Row h := table.NewHeading(heading) rows = append(rows, h) if sortKeys { sort.Slice(app.Keybinds, func(i, j int) bool { return strings.ToLower(app.Keybinds[i].Name) < strings.ToLower(app.Keybinds[j].Name) }) } // convert Keybind to Row for _, kb := range app.Keybinds { row := table.NewRow(kb.Name, kb.Key, app.Prefix, heading) // KeyBind's ignore prefix defaults to false // so user can choose to ignore prefix for a specific kb if kb.IgnorePrefix { // Row's show prefix defaults to false // so prefix is not shown unless defined row.ShowPrefix = false } rows = append(rows, row) } return table.New(rows) } func (m *Model) Init() tea.Cmd { return nil } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case tea.WindowSizeMsg: m.List.Resize(msg.Width, msg.Height) } m.List, cmd = m.List.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m *Model) View() string { return m.List.View() }