Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

focus.nvim

Keep the eye where the cursor is. Lines near the cursor stay legible; everything else fades out or dissolves into noise, so the section you are editing is the only thing that reads as code.

focus.nvim in action

Terminals cannot blur text, so the unfocus effect is built from the two things a terminal can do: recolour a line, and paint different glyphs over it.

Modes

Mode Effect
dim Foreground ramps toward the background with distance. No virtual text at all, so it is the cheapest mode.
scramble Glyphs become noise. Layout is not preserved.
shape Indentation, punctuation and word lengths survive; letters become noise. Still reads as code from the corner of the eye.
blocks Glyphs become block characters (░▒▓█) that solidify with distance, so text dissolves into bars as it recedes.
dimshape Default. Dim while close to the cursor, scramble once far from it.

Scramble modes paint overlay virtual text. The buffer is never modified, so undo history, LSP positions and git status are all untouched. Lines inside the focus region are always left exactly as written, whatever the mode.

Regions

Region Behaviour
fixed Everything within radius is lit; everything beyond sits at floor.
core Default. core lines fully lit, then a falloff out to radius.
block The enclosing syntax block via treesitter, with falloff beyond it. Small helpers get a tight spotlight; long functions stay fully lit. Degrades to core when no parser is available.

Install

lazy.nvim

{
  "RedkillTech/focus.nvim",
  event = "VeryLazy",
  opts = {
    mode = "dimshape",
    region = "core",
    radius = 20,
    core = 6,
  },
  keys = {
    { "<leader>zf", "<cmd>FocusToggle<cr>", desc = "Focus: toggle" },
    { "<leader>zm", "<cmd>FocusMode<cr>", desc = "Focus: cycle mode" },
  },
}

To run it from a local checkout instead, swap the repo string for dir:

{
  dir = vim.fn.expand "~/path/to/focus.nvim",
  name = "focus.nvim",
  event = "VeryLazy",
  opts = { mode = "dimshape" },
}

opts is passed straight to require("focus").setup(). A ready-made spec lives in lazy.lua.

Configuration

Defaults in full:

require("focus").setup({
  mode = "dimshape",        -- "dim" | "scramble" | "shape" | "blocks" | "dimshape"
  region = "core",          -- "fixed" | "core" | "block"

  radius = 20,              -- lines from the cursor to the outer edge
  core = 6,                 -- lines that stay fully lit

  curve = "ease",           -- "linear" | "ease" | "steps"
  floor = 0.18,             -- dimmest level, 0.0 .. 1.0
  bands = 20,               -- precomputed highlight groups

  scramble_threshold = 0.45, -- dimshape: falloff fraction before scrambling
  keep_structure = true,     -- keep indent and brackets lit while scrambling

  noise_chars = "ABC...",           -- character pool for scramble modes
  block_chars = { "", "", "", "" }, -- glyphs for blocks, lightest first
  base_hl = "Normal",               -- highlight group the ramp is derived from

  debounce_ms = 0,           -- coalesce cursor bursts; 0 = same-tick defer

  disable_in_insert = true,
  disable_in_visual = false,
  max_lines = 20000,         -- skip buffers larger than this

  filetype_denylist = { "help", "qf", "terminal", ... },
  buftype_denylist = { "terminal", "quickfix", "prompt", "nofile", "help" },

  auto_enable = true,        -- start on load
})

The knobs worth touching

  • floor — how dark unfocused text gets. Raise it if the edges are too aggressive.
  • scramble_threshold — for dimshape, how far out scrambling begins.
  • curvesteps quantizes to four bands and is the cheapest ramp; ease holds bright near the cursor and drops late.
  • keep_structure — turn off for heavier obfuscation, at the cost of the code no longer reading as code peripherally.

Commands

Command Action
:FocusToggle Toggle focus mode
:FocusEnable / :FocusDisable Explicit on/off
:FocusMode [mode] Set the mode, or cycle with no argument
:FocusRadius {n} Set the radius

:FocusMode completes the mode names.

API

local focus = require("focus")

focus.enable()
focus.disable()
focus.toggle()
focus.set_mode("blocks")
focus.cycle_mode()
focus.set({ radius = 12, curve = "steps" })  -- any config key, applied live

focus.set() takes effect immediately, so it is convenient for tuning:

:lua require("focus").set({ floor = 0.3, scramble_threshold = 0.6 })

Performance

Measured on a 10,000-line buffer:

Path Cost
dim / scramble / shape ~0.15 ms per render
dimshape ~0.45 ms per render
region = "block" (treesitter) ~0.62 ms per render
Unchanged cursor (cache hit) ~0.003 ms

At 60fps the frame budget is 16.7 ms, so a render costs a few percent of it.

How it stays cheap:

  • Only the visible viewport is drawn, so cost scales with window height rather than file size.
  • Highlight groups for the dim ramp are built once at setup and rebuilt only on ColorScheme; rendering allocates no highlights.
  • A per-window render signature skips the entire pass when nothing observable changed, so holding j does not repaint identical frames.
  • Scramble noise is hashed from (line, column) rather than sampled randomly. Random noise would resample on every redraw and make unfocused text shimmer.
  • Each window has its own namespace, so one buffer split twice focuses each window's own cursor independently.

Notes

  • Distances are measured in screen lines when wrap or folds are active, so a long wrapped line does not consume the entire radius.
  • Focus is suppressed in insert mode by default, and always in operator-pending and command-line modes.
  • Scramble output preserves display width exactly, including tabs and multibyte characters, so the overlay lines up with the real text.
  • The noise is stable rather than random. That makes it obfuscation for peripheral vision, not concealment — a fixed pattern is learnable if you stare at it.

Design mockup

The effects were prototyped in mockup/index.html, a standalone page that fakes a terminal and lets every option be toggled live. It also contains two effects that were not shipped: full-noise scrambling without shape preservation, and a collapse mode that hides distant lines outright.

Requirements

  • Neovim 0.10+ (uses vim.uv, nvim_set_hl, extmark overlays)
  • treesitter parser for the buffer's language, only for region = "block"

About

Focus mode for neovim

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages