diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..926ccaa --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +doc/tags diff --git a/README.md b/README.md new file mode 100644 index 0000000..19194b9 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +
+# DelayTrain +_Train yourself to stop repeating keys... gently_ + +[How does it work?](#how-does-it-work) • [Installation](#installation) +• [Configuration](#configuration) • [Commands](#commands) +• [Contributing](#contributing) + +TODO: quick demo gif ~/Videos/delaytrain.gif +
+ +## "Stop using arrow keys!" "Stop using hjkl!" + +If you're familiar with the (neo)vim community you've probably heard this a +million times. The whole point of vim is to seamlessly navigate where you +need to go without constantly repeating keypresses. Why press `j` ten times +when you can press `10j`, or `}`, or search. + +But if you've been using vim for a while this may be a harder habit to break. +You try to stop relying on this type of navigation but you only catch +yourself after the fact. You might chastize yourself and move on but the +habit stays there. + +Most recommendations involve disabling these keys altogether, but this only +increases frustration. Sometimes your next position is directly above and +the quickest option is to press `k`. This might help in the long run but +it's incredibly annoying and hard to stick with. + +That's where DelayTrain comes in. + +DelayTrain will still let you use these keybindings, but it only punishes +you when you _keep_ hitting them. If you need to navigate directly below, +you can still do that. But if you need to navigate 5 lines below using +repeated keypresses, DelayTrain will gently remind you that there might +be a better way by stopping the keypress from working for a certain +amount of time. + +And DelayTrain doesn't just work for hjkl. Mappings are included to +prevent repeated arrow key presses and you can configure delaytrain to +prevent anything else like `w` or `b`. + +## How does it work? + +DelayTrain takes two configurable values, `delay_ms` and `grace_period`. + +When you first hit a configured keypress (like `j`), the `delay_ms` timer +starts. You are given a `grace_period` of repeated keypresses within the +`delay_ms` timer before the key stops working. Once the `delay_ms` timer +ends, everything is reset. + +### Examples + +We'll use a few tables to show how this works. Assume the follwing: + +* `delay_ms = 1000` +* `grace_period = 2` + +By default the `grace_period` is 1, but setting it to 2 allows you to +press the key twice before it stops working. + +| Keypress | Time | Grace Period | Does it work? | +| ---------- | ----------- | ------------ | ------------- | +| `j` | 0ms | 1 | Yes | +| `j` | 200ms | 2 | Yes | +| `j` | 500ms | 3 | No | +| `j` | 1000ms | 1 | Yes | + +Each keypress starts a dedicated `delay_ms` timer and has a dedicated +`grace_period`. So if you're trying to navigate down and to the left, +this still works. + +| Keypress | Time | Grace Period | Does it work? | +| ---------- | ----------- | ------------ | ------------- | +| `j' | 0ms | 1 | Yes | +| `j' | 200ms | 2 | Yes | +| `h' | 500ms | 1 | Yes | +| `j' | 700ms | 3 | No | +| `h' | 1000ms | 2 | Yes | +| `j' | 1200ms | 1 | Yes | +| `h' | 1400ms | 3 | No | +| `h' | 1500ms | 1 | Yes | + + +## Installation + +Install with [vim-plug](https://github.com/junegunn/vim-plug): + +```vim +Plug '[TODO GITHUB NAME]/delaytrain.nvim' +``` + +or with [packer](https://github.com/wbthomason/packer.nvim): + +```lua + -- Delay repeat execution of certain keys + use '[TODO GITHUB NAME]/delaytrain.nvim' +``` + +For the default setup (see defaults below), you can simply place the following +into your `init.lua`: + +```lua +require('delaytrain').setup() +``` + +## Configuration + +You can configure all DelayTrain settings through the `setup()` function. The +default DelayTrain mappings are included below: + +```lua + require('delaytrain').setup { + delay_ms = 1000, -- How long repeated usage of a key should be prevented + grace_period = 1, -- How many repeated keypresses are allowed + keys = { -- Which keys (in which modes) should be delayed + ['nv'] = {'h', 'j', 'k', 'l'}, + ['nvi'] = {'', '', '', ''}, + }, + } +``` + +### Mappings + +The keys option allows you to delay different keypresses in different modes. +This takes the following KV pair: + +```lua + ['list_of_applicable_modes'] = {'keys', 'you', 'want', 'delayed'}, +``` + +Modes can be added based on their short-names (ex: normal is 'n', insert is +'i') and multiple modes can be added to a single keymap. + +This option ties into a call to `vim.keymap.set()`, so mode short-names and +key names should match what is possible in that function. + +### Options + +Global options can be modified to change delay/grace period settings on the +fly: + +* `g:delaytrain_delay_ms` +* `g:delaytrain_grace_period` + +## Commands + +The following commands allow you to turn DelayTrain on and off without +calling `setup()` again: + +* `:DelayTrainEnable` +* `:DelayTrainDisable` +* `:DelayTrainToggle` + +By default, DelayTrain is turned on when the `setup()` function is called. + +## Contributing + +This has been tested on my personal and work machines using nvim-nightly and +[Neovide](https://github.com/neovide/neovide). This is a REALLY SMALL plugin +so while there shouldn't be a lot of issues it's entirely possible I missed +something. I'm also brand new to plugin development so if you notice anything +off please feel free to open up an issue or send me a PR! diff --git a/doc/delaytrain.txt b/doc/delaytrain.txt new file mode 100644 index 0000000..6b19edd --- /dev/null +++ b/doc/delaytrain.txt @@ -0,0 +1,118 @@ +*delaytrain.txt* DelayTrain +*delaytrain.nvim* + +Author: James Ford +Version: 1.0.0 +Homepage: <> + +============================================================================= +INTRODUCTION *delaytrain* + +When learning how to use Vim keybindings, sometimes it's helpful to avoid +repetitive keypresses. For example, repetitive use of hjkl or arrow keys can +often be replaced with a numeric prefix, or an even better key to navigate +exactly where you want to go. + +To help train your muscle memory, most people suggest un-mapping these keys +to avoid using them completely. But there are certain cases where hjkl may +be necessary and another key combination would be overkill. + +DelayTrain helps train this muscle memory by preventing a keypress from +being used after a certain amount of repeated usage inside a timeframe, +rather than disabling the key completely. This way you can still go about +your work with a gentle reminder that there might be a better way to do +what you want. + +============================================================================== +USAGE *delaytrain-usage* + +Most people will be training with hjkl and arrow keys, so for a default setup +you can simply run: +> + require('delaytrain').setup() +< + +============================================================================== +CONFIGURATION *delaytrain-configuration* + +For more granular configuration and additional keymaps, you can pass the +following configuration (default settings included): +> + require('delaytrain').setup { + delay_ms = 1000, -- How long repeated usage of a key should be prevented + grace_period = 1, -- How many repeated keypresses are allowed + keys = { -- Which keys (in which modes) should be delayed + ['nv'] = {'h', 'j', 'k', 'l'}, + ['nvi'] = {'', '', '', ''}, + }, + } +< +Keep in mind that the `delay_ms` timer starts on the FIRST keypress and not the +final `grace_period` keypress. For example (with default settings) if you hit j, +the timer starts immediately. If you hit j again after 500ms, the key will not +work. If you wait ANOTHER 500ms and hit j again, it will work. Likewise, if +you hit j, wait 1000ms, and hit j again, both keypresses will work. + +With an increased `grace_period`, you can hit a key `grace_period` amount of times +inside `delay_ms` before it stops working. + +`grace_period` and `delay_ms` only affect the current key being pressed. With +the default settings, if you hit j and then hit k after 500ms, both keypresses +will work. If you wait another 200ms and hit j again, the keypress will not +work. + +------------------------------------------------------------------------------ +MAPPINGS *delaytrain-mappings* + +The keys option allows you to delay different keypresses in different modes. +This takes the following KV pair: +> + ['list_of_applicable_modes'] = {'keys', 'you', 'want', 'delayed'}, +< +Modes can be added based on their short-names (ex: normal is 'n', insert is +'i') and multiple modes can be added to a single keymap. + +This option ties into a call to |vim.keymap.set()|, so mode short-names and +key names should match what is possible in that function. + + +------------------------------------------------------------------------------ +OPTIONS *delaytrain-options* + +Global options can be modified to change delay/grace period settings on the +fly. + +------------------------------------------------------------------------------ +g:delaytrain_delay_ms~ *g:delaytrain_delay_ms* + +How long repeated usage of a key should be prevented. Keep in mind that the +timer starts on the FIRST keypress and delays repeated usage of a key based on +`grace_period` in this timeframe. + + Type: |Number| + Default value: `1000` + +------------------------------------------------------------------------------ +g:delaytrain_grace_period~ *g:delaytrain_grace_period* + +How many repeated keypresses are allowed inside the `delay_ms` timeframe. By +default this is 1, so you can press a key 1 time inside `delay_ms` before it's +disabled. Setting this value to 0 will completely disable the key. + + Type: |Number| + Default value: `1` + +============================================================================== +COMMANDS *delaytrain-commands* + +:DelayTrainEnable *:DelayTrainEnable* + Enable DelayTrain by setting defined keymaps + +:DelayTrainDisable *:DelayTrainDisable* + Disable DelayTrain by deleting defined keymaps + +:DelayTrainToggle *:DelayTrainToggle* + Toggle DelayTrain by setting/deleting keymaps + +------------------------------------------------------------------------------ +vim:tw=78:ts=8:ft=help:norl: diff --git a/lua/delaytrain/init.lua b/lua/delaytrain/init.lua new file mode 100644 index 0000000..7dfe316 --- /dev/null +++ b/lua/delaytrain/init.lua @@ -0,0 +1,96 @@ +local M = {} + +vim.g.delaytrain_delay_ms = 1000 +vim.g.delaytrain_grace_period = 1 + +-- Map of keys to their individual current grace period +-- This keeps track of how many times a key has been pressed +local current_grace_period_intervals = {} + +local keymaps = { + ['nv'] = {'h', 'j', 'k', 'l'}, + ['nvi'] = {'', '', '', ''}, +} + +local is_enabled = false + +function M.try_delay_keypress(key) + current_interval = current_grace_period_intervals[key] + + -- Start a timer on the first keypress to reset the interval + if current_interval == 0 then + vim.loop.new_timer():start(vim.g.delaytrain_delay_ms, 0, function() + current_grace_period_intervals[key] = 0 + end) + end + + -- Pass the key through only if we haven't reached the grace period + if current_interval < vim.g.delaytrain_grace_period then + current_grace_period_intervals[key] = current_interval + 1 + + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes(key, true, false, true), + 'n', + false + ) + end +end + +function M.setup(opts) + if opts then + if opts.delay_ms then + vim.g.delaytrain_delay_ms = opts.delay_ms + end + + if opts.grace_period then + vim.g.delaytrain_grace_period = opts.grace_period + end + + if opts.keys then + keymaps = opts.keys + end + end + + M.enable() +end + +function M.enable() + is_enabled = true + + for modes, keys in pairs(keymaps) do + mode_array = {} + for mode in modes:gmatch"." do + table.insert(mode_array, mode) + end + for _, key in ipairs(keys) do + -- Set the current grace period for the given key + current_grace_period_intervals[key] = 0 + + vim.keymap.set(mode_array, key, function() M.try_delay_keypress(key) end, {expr = true}) + end + end +end + +function M.disable() + is_enabled = false + + for modes, keys in pairs(keymaps) do + mode_array = {} + for mode in modes:gmatch"." do + table.insert(mode_array, mode) + end + for _, key in ipairs(keys) do + vim.keymap.del(mode_array, key) + end + end +end + +function M.toggle() + if is_enabled then + M.disable() + else + M.enable() + end +end + +return M diff --git a/plugin/delaytrain.lua b/plugin/delaytrain.lua new file mode 100644 index 0000000..51b72d4 --- /dev/null +++ b/plugin/delaytrain.lua @@ -0,0 +1,11 @@ +vim.api.nvim_create_user_command("DelayTrainEnable", function() + require("delaytrain").enable() +end, { desc = "Enable DelayTrain by setting defined keymaps" }) + +vim.api.nvim_create_user_command("DelayTrainDisable", function() + require("delaytrain").disable() +end, { desc = "Disable DelayTrain by deleting defined keymaps" }) + +vim.api.nvim_create_user_command("DelayTrainToggle", function() + require("delaytrain").toggle() +end, { desc = "Toggle DelayTrain by setting/deleting keymaps" })