agentleFS
kaynetikkaynetik/skills/lua-projects/SKILL.md

lua-projects

Skill1 starsChanged 8 months agoMITView on GitHub

Guides idiomatic Lua 5.4 programming, module design, and project maintenance for Neovim plugin/config ecosystems (LazyVim, lazy.nvim) and macOS bar tools (SketchyBar/SbarLua). Covers style, tooling (StyLua, Selene, LuaLS), module patterns, and testing. Use when writing Lua code, configuring Neovim plugins, working with SketchyBar Lua configs, debugging Lua require/module errors, or when the user mentions Lua, .lua files, LazyVim, lazy.nvim, SbarLua, SketchyBar, luarocks, LuaLS, StyLua, or Selene.

  • Installs packages

The file

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 1 is distinctive, and 10 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use 2 spaces (common in Neovim/LazyVim ecosystem) or tabs (SketchyBar default) -- be consistent within a project

    line 29
  2. Very specific:

    No spaces inside {}, (), or [].

    line 32
  3. Very specific:

    Run: stylua . or integrate as a pre-commit hook / editor format-on-save.

    line 52
  4. Very specific:

    Every module file should return a table (or a single value).

    line 127
  5. Very specific:

    Use opts table merging over config functions when possible

    line 198
  6. Very specific:

    Always call sbar.event_loop() at the end of init.lua

    line 242
  7. Very specific:

    Use sbar.exec() instead of os.execute() to avoid blocking the event handler

    line 243
  8. Very specific:

    Keep color/icon/settings as pure data modules that return a table

    line 246
  9. Very specific:

    Run: busted or busted spec/

    line 314
  10. Very specific:

    Use plenary.nvim's test harness for Neovim-specific tests:

    line 318
  11. Very specific:

    Prefer ipairs over pairs when iterating sequential arrays (faster and order-guaranteed)

    line 329

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Lua Projects

Scope: Lua 5.4 (with LuaJIT notes where relevant). Targets config-driven projects like Neovim distributions, SketchyBar setups, and general Lua modules.

Style and Formatting

Naming

  • snake_case for variables, functions, and file names
  • PascalCase only for class-like constructor tables (rare)
  • UPPER_SNAKE for true constants
  • Prefix unused variables with _ (e.g., for _, v in ipairs(t))

Indentation and Layout

  • Use 2 spaces (common in Neovim/LazyVim ecosystem) or tabs (SketchyBar default) -- be consistent within a project
  • Wrap lines at 100 characters; hard limit 120
  • Trailing commas in multi-line tables are encouraged
  • No spaces inside {}, (), or []; spaces after commas and around operators

Formatter: StyLua

StyLua (v2.4+) is the standard Lua formatter. Configure via stylua.toml at the project root:

column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"

For SketchyBar configs using tabs:

indent_type = "Tabs"

Run: stylua . or integrate as a pre-commit hook / editor format-on-save.

Linting

Selene (recommended)

Modern Lua linter written in Rust. Actively maintained (v0.30+). Provides rich diagnostics with named lint rules.

Config file selene.toml:

std = "lua54"

For Neovim configs, use the vim standard library definition so globals like vim are recognized:

std = "lua54+vim"

Run: selene .

Luacheck (legacy)

Still detects some things Selene does not (uninitialized vars, unreachable code), but has been unmaintained since 2018. Use as a secondary pass if needed.

Config file .luacheckrc:

std = "lua54"
globals = { "vim" }

Language Server: LuaLS

lua-language-server (LuaLS) provides diagnostics, completion, hover, and type checking.

Settings file .luarc.json at project root:

{
  "runtime": { "version": "Lua 5.4" },
  "diagnostics": { "globals": ["vim"] },
  "workspace": {
    "library": [],
    "checkThirdParty": false
  }
}

For SketchyBar projects, add the SbarLua install path to workspace.library so LuaLS resolves the sketchybar module:

"workspace": {
  "library": ["~/.local/share/sketchybar_lua"]
}

Type Annotations

LuaLS supports EmmyLua-style annotations. Use them for public API surfaces:

---@param name string
---@param opts? { padding?: number, icon?: string }
---@return table item
local function add_item(name, opts)
  -- ...
end

Module Patterns

The Return-Table Pattern

Every module file should return a table (or a single value). Avoid polluting globals.

local M = {}

function M.greet(name)
  return "hello " .. name
end

return M

Init Modules

A directory with an init.lua is requireable by its directory name. Use init.lua to re-export or orchestrate sub-modules:

items/
  init.lua      -- require("items") loads this
  apple.lua
  calendar.lua
-- items/init.lua
require("items.apple")
require("items.calendar")

Avoid Globals

-- BAD: implicit global
sbar = require("sketchybar")

-- GOOD: local binding (use upvalues or pass explicitly)
local sbar = require("sketchybar")

Exception: SketchyBar's example config sets sbar as a global in init.lua because sub-modules reference it without an explicit require path. If following that convention, document it clearly and configure your linter to allow it:

# selene.toml
[lints]
global_usage = "allow"

Project Structure Patterns

Neovim Config (LazyVim / lazy.nvim)

~/.config/nvim/
  init.lua                -- minimal bootstrap: vim.loader.enable(), require("config.lazy")
  lua/
    config/
      lazy.lua            -- lazy.nvim bootstrap and setup
      options.lua
      keymaps.lua
      autocmds.lua
    plugins/
      lsp.lua
      treesitter.lua
      ui.lua
      editor.lua

Key conventions:

  • init.lua stays minimal -- call vim.loader.enable() then require config
  • One plugin spec per file (or group related specs) in lua/plugins/
  • lazy.nvim auto-loads everything in lua/plugins/
  • Use opts table merging over config functions when possible

Plugin spec pattern:

return {
  "author/plugin.nvim",
  event = "VeryLazy",
  dependencies = { "nvim-lua/plenary.nvim" },
  opts = {
    setting = true,
  },
  keys = {
    { "<leader>x", "<cmd>PluginAction<cr>", desc = "Do thing" },
  },
}

SketchyBar Config (SbarLua)

~/.config/sketchybar/
  sketchybarrc              -- shell entry: sketchybar --config init.lua (or similar)
  init.lua                  -- requires sbar, wraps in begin_config/end_config, runs event_loop
  bar.lua                   -- bar-level properties
  default.lua               -- default item properties
  colors.lua                -- color palette table
  icons.lua                 -- icon constants (SF Symbols / Nerd Font)
  settings.lua              -- shared settings (paddings, fonts)
  items/
    init.lua                -- requires each item module
    spaces.lua
    front_app.lua
    media.lua
    ...
  helpers/
    init.lua
    app_icons.lua
    default_font.lua
    event_providers/        -- native helpers (C compiled)

Key conventions:

  • Wrap all setup between sbar.begin_config() / sbar.end_config() for batching
  • Always call sbar.event_loop() at the end of init.lua
  • Use sbar.exec() instead of os.execute() to avoid blocking the event handler
  • Properties use sub-tables instead of dot notation: icon = { y_offset = 10 } not icon.y_offset
  • Color values are 0xAARRGGBB hex integers
  • Keep color/icon/settings as pure data modules that return a table

Common Patterns

Safe Require

local ok, mod = pcall(require, "optional_module")
if not ok then
  return
end

Metatables for OOP-ish Tables

local Item = {}
Item.__index = Item

function Item.new(name)
  return setmetatable({ name = name }, Item)
end

function Item:display()
  return self.name
end

Config Merging

local defaults = { padding = 4, color = 0xffffffff }

local function apply(user_opts)
  local cfg = {}
  for k, v in pairs(defaults) do cfg[k] = v end
  for k, v in pairs(user_opts or {}) do cfg[k] = v end
  return cfg
end

For deep merging in Neovim: vim.tbl_deep_extend("force", defaults, user_opts).

Testing

Busted

The standard Lua test framework. Install via luarocks:

luarocks install busted
-- spec/greet_spec.lua
describe("greet", function()
  local mod = require("mymod")

  it("returns greeting", function()
    assert.are.equal("hello world", mod.greet("world"))
  end)

  it("handles nil", function()
    assert.has_error(function() mod.greet(nil) end)
  end)
end)

Run: busted or busted spec/

Neovim Plugin Testing

Use plenary.nvim's test harness for Neovim-specific tests:

nvim --headless -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.lua'}"

Performance Notes

  • local lookups are register-based; global lookups go through _ENV hash. Always localize hot-path references.
  • Pre-size tables with known lengths: local t = table.create and table.create(n) or {}
  • String concatenation in loops: accumulate in a table and table.concat() at the end
  • Prefer ipairs over pairs when iterating sequential arrays (faster and order-guaranteed)
  • In LuaJIT (Neovim): avoid NYI (Not Yet Implemented) operations in tight loops -- check https://wiki.luajit.org/NYI

Lua 5.4 Specifics

Features available in 5.4 that older references may not cover:

  • Integer subtype: integers and floats are distinct; type(1) is "number" but math.type(1) is "integer"
  • Bitwise operators: &, |, ~ (xor), ~ (unary not), <<, >> -- no need for bit32 or bit libraries
  • Integer for-loop: for i = 1, n uses native integers
  • Generational GC: collectgarbage("generational") for lower-latency collection
  • <const> and <close>: local attributes for immutability and deterministic cleanup
local path <const> = "/tmp/data"
local f <close> = io.open(path, "r")

Note: Neovim uses LuaJIT (Lua 5.1 compatible), so 5.4 features are not available in Neovim configs. Use 5.4 features only in standalone Lua or SketchyBar (if built against Lua 5.4).

Debugging

  • print(vim.inspect(t)) in Neovim for table inspection
  • print(require("inspect")(t)) in standalone Lua (install via luarocks)
  • SketchyBar logs to ~/.local/share/sketchybar/ -- check there for Lua errors
  • LuaLS diagnostics surface most type/require errors before runtime

Tooling Summary

ToolPurposeConfig FileInstall
StyLuaFormatterstylua.tomlcargo install stylua or npm
SeleneLinterselene.tomlcargo install selene
LuaLSLanguage server.luarc.jsonvia Mason or package manager
BustedTest framework.bustedluarocks install busted
LuarocksPackage manager*.rockspecsystem package manager

Reference Projects

ProjectURLFocus
LazyVimhttps://github.com/LazyVim/LazyVimNeovim distribution, plugin orchestration
lazy.nvimhttps://github.com/folke/lazy.nvimPlugin manager, spec system
SbarLuahttps://github.com/FelixKratz/SbarLuaSketchyBar Lua bindings
SketchyBarhttps://github.com/FelixKratz/SketchyBarmacOS bar, config examples
nvim-lspconfighttps://github.com/neovim/nvim-lspconfigLSP client configs
plenary.nvimhttps://github.com/nvim-lua/plenary.nvimNeovim Lua utilities and test harness
telescope.nvimhttps://github.com/nvim-telescope/telescope.nvimFuzzy finder, well-structured plugin

Additional Resources

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.