Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Greed

Greed, a General Runtime for Editing, Extending and Delegating, is an editor you use in a terminal or a window, on your machine or on the one where the work runs, with a small Rust core and everything else in Luau plugins you can read, change or replace while it runs. You edit in the Vim, Helix or VS Code style, modal or without modes, and switch with :style. Agents use the editor through MCP, the shell and Claude Code’s /ide, or run inside it, and propose edits you review in the buffer.

New to Greed? Start it, answer its few questions, and run :tutor: it teaches your style’s keys with lines to practise on, then shows you around.

  • Using Greed: editing, searching, files, sessions, configuration.
  • Several clients: terminals and windows on one session.
  • The shell: commands as blocks you keep, search and run again.
  • Editing styles: the Vim, Helix and VS Code styles, their keys, and making your own.
  • Keys: finding out what a key does, binding your own, panes and tabs.
  • Highlighting: how code is highlighted and themed.
  • Writing plugins: commands, keys, decorations, panels, tests and types.
  • Agents: greed ctl, MCP, proposals, /ide, agents inside Greed and plugins written by agents.
  • How Greed is built: what lives in Rust, what lives in Luau, and why.

These pages are plain Markdown, readable in a terminal or in Greed itself, and they make the documentation at greed.rs.

Using Greed

Starting and sessions

greed FILE opens a file. The editor runs as a session in the background and your terminal attaches to it, so closing the terminal (or typing :detach) leaves everything as it was: open files, undo history, language servers.

Each project gets its own session, named after its folder. Running greed anywhere in a project attaches to that project’s session if it’s running (opening the file you named), or starts it. Projects in separate sessions don’t affect each other: a slow plugin or a crash in one leaves the others alone.

CommandWhat it does
greed [FILE or FOLDER]Attach to the project’s session, or start it
greed --session NAME [PATH]Attach to or start a session with this name, e.g. a second one for a project
:detachLeave the session running
greed attach [NAME]Attach to a session; the only one if there’s one
greed lsList running sessions and their projects
greed serve [PATH]Run a project’s session with no terminal
:qClose the pane, as Vim closes a window, when the tab has others; the tab, when it’s the last pane and there are other tabs. Otherwise close the shown project and leave; on the last one, end the session unless something is running in it (see below). In a panel or a float (an agent’s transcript, help) it closes just that
:quit-session, :qaEnd the session, whatever runs in it. With unsaved files it lists them and stops; :wa saves them, :wqa saves them and ends it, :qa! ends it anyway
greed edit [--wait] FILE...Open files in the running editor; with --wait, return once each is closed with :q or :wq (:cq fails it), as $EDITOR for git commit. Greed’s own shells set this up already
greed ctl quit [--force] [NAME]End a session from a shell, unless it has unsaved changes (--force ends it anyway)

Several terminals can show one session at once: run greed or greed attach in another terminal, or greed ssh from another machine, and both stay attached. They share the buffers and their undo history; each has its own cursors, focus and mode, and :detach lets only that one go. A terminal that comes back from the same machine gets its cursors, focus and mode back; a new one starts where the one used last is. Copies go to the clipboard of the terminal that copied, and desktop notifications to the one used last.

Attached terminals follow each other: they show the same project, tab and panes, each with its own focus and pane zoom. :client-independent lays one out on its own, and :client-follow has it follow again. Pickers, prompts and the command line show only on the terminal that opened them, and a program in a terminal pane is sized for the client that last typed into it. Several clients has the details.

greed commands lists every command line command, including the ones plugins add (greed ls, greed new and greed bring are plugins too).

Switching to another session (space P, :session) moves your terminal there and leaves the session you were in running. Over greed ssh it reconnects through a new ssh connection, which is close to instant with ssh connection sharing (ControlMaster).

In one of Greed’s own terminals, greed FILE opens the file in that session and returns, rather than starting an editor inside the editor, and greed ctl talks to that session. Programs there can tell from $GREED_TERMINAL, which names the session.

If a session can’t start in the background, GREED_NO_SESSION=1 greed FILE runs the editor directly in the terminal.

A session keeps running the greed it started with. After an upgrade, a session from before may not understand the new client, and greed says so with both versions. greed ctl quit NAME ends that session (it works across versions), and the next greed starts a new one.

A session that quits remembers its projects, their open files and where the cursor was, and opens them again the next time it starts. It writes this down when it quits and whenever you save a file, in the cache folder (~/.cache/greed/sessions). Starting with a file keeps that file shown. Turn it off with greed.option.set("restore-session", false).

Quitting or keeping a session

:q and :wq in a pane with others beside it close just that pane, as they close a window in Vim, and in a tab’s last pane they close the tab while there are others. The file stays open, so this never asks about unsaved changes. ctrl-q in the vscode style asks whether to save unsaved files, quit without saving or cancel, as VS Code does.

:q on the last project ends the session, unless ending it would stop something: a command running in one of its terminals, a program like a task, or an agent at work. Then it detaches instead and the session keeps going, so greed comes back to it. A shell waiting at its prompt doesn’t count. Over greed ssh, :q keeps the session by default, since coming back to a running one is quicker than starting again over the network. :quit-session (or :qa) ends it whatever runs in it. Files with unsaved changes stop it: it lists them, :wa saves them and :qa! ends the session anyway. Terminals, shells, agents’ panes and other scratch buffers never count as unsaved.

To decide for yourself, set what :q does, here and over greed ssh:

local greed = require("@greed")

-- "auto": end it unless something runs; "end": always end it;
-- "keep": always leave it running
greed.option.set("on-quit", "auto")
greed.option.set("on-quit-remote", "keep")

A session with nothing attached, nothing running and nothing unsaved ends by itself after a day, so sessions don’t pile up on machines you visit; session-idle-hours changes how long, and 0 keeps them for good. Plugins can say what of theirs is running with require("@core").file.running.

The first time

Started without a config (~/.config/greed/init.luau), Greed shows a page with a few questions: how you like to edit, which theme, whether your font has icons (in a terminal, which needs a Nerd Font for them; the window has them built in and doesn’t ask), whether to add Greed’s tools to Claude Code, and which language model to use (Anthropic, OpenAI, Berget, Ollama on this machine, or none for now). A style or theme you pick switches at once, so you can try it on the page.

The page is a questionnaire in Markdown, and works the same in every editing style: tab and shift-tab go from choice to choice, enter or space picks one or presses a button, and a click does too. Under the models, a button sets the API key (it goes in the system keyring) or signs you in; for Ollama the page says whether it answers, and if not, to run ollama serve and ollama pull llama3.2.

Save writes an init.luau from your answers and opens it, and has the fast and reasoning roles use the provider you picked (:models changes them later). Under your answers it lists settings worth trying, commented out: remove the -- in front of one and save, and it applies at once. It also says how to find the rest: space h o describes an option, and :set NAME VALUE tries one for the session. Not now, esc, q or closing the page any other way closes it without writing anything. Either way it doesn’t show again by itself, and :setup brings it back, or goes to it if it’s open. Save never replaces a config that’s there; it shows what to add instead, and esc closes that.

:tutor teaches the keys of the style you picked, with a few lines to practise on in each lesson, which say done once you’ve got them right, and then shows you around Greed: the leader, finding files, panes, the shell and sessions. Picking another style on the page switches to it, and the lessons change with it. Nothing on the page is saved; :tutor gives you a fresh one.

In a window

greed --gui [FILE] shows the session in a window instead of the terminal, drawn on the GPU. It’s the same session either way: attach from a terminal later and you see the same files, and greed ssh HOST --gui shows a session on another machine in a window here. Closing the window lets the session go, as :detach does.

The window draws with your system’s monospaced font, ligatures included when the font has them (Fira Code, JetBrains Mono, Cascadia Code), and draws box lines itself so borders join cleanly. Images show as they do in a terminal with kitty graphics, PDFs included. Dead keys and input methods type as they do elsewhere, and the window opens at the size it last had. On Linux it needs Wayland or X11 and a Vulkan or GL driver.

On macOS the menu bar holds Greed’s menus (File, Edit, View, Pane, Help), each item a command. Elsewhere there’s no menu bar, and the command palette (space ?, or ctrl-P in the vscode style) lists every command. Plugins and your config add items: require("@core").menus.add("Tools", "Run tests", "test-run").

What you copy in Greed goes on the system clipboard, and what you copied in another program becomes Greed’s clipboard when the window gets focus, so p (or ctrl-v in the vscode style) pastes it. ctrl-shift-v and shift-insert (cmd-v on macOS) paste the system clipboard at the cursors in any mode, as a terminal’s paste does.

Whole lines stay whole lines from one editing style to another: lines copied with y y in the Vim style, x y in Helix’s or ctrl-c with nothing selected in the vscode style paste above or below the line in all three. Text copied in another program pastes where the cursor is.

ctrl-= and ctrl-- make the text bigger and smaller, and ctrl-0 puts it back to the size it had before; with several windows on one session, each zooms on its own. Pick another font and size in your init.luau; the windows follow as soon as they change:

local greed = require("@greed")
greed.option.set("gui-font", "JetBrains Mono")
greed.option.set("gui-font-size", 14) -- pixels, before the screen's scale
greed.option.set("gui-font-prose", "Inter") -- prose, like Markdown's text

Prose (Markdown’s text, and any style with font = "prose") is set in a proportional font, the system’s sans-serif unless gui-font-prose names one, at its own width; code stays in the grid’s font. The cursor, selections and clicks follow the text where it’s drawn.

Floats (pickers, prompts, hover info) show as boxes with rounded corners and a soft shadow, as the theme says for the window; terminals draw box lines. The window draws the round ends of the status line’s pills itself, so they need no Powerline font. For square floats without shadows:

greed.theme.set({ ["ui.float"] = { gui = { rounded = false, shadow = false } } })

Projects

A project is a folder you work in: the nearest folder above a file that holds .jj or .git, or the folder itself if none does. A session can also hold several projects, for ones you work on together: each has its own files, main view and panels (the file tree, search results), and the file picker, project search and tree work within the shown one.

Keys
:project PATHOpen the project PATH is in, or switch to it
space PGo to a project: one open here, another running session, a recent one, or one your sources find; type a folder path (~/src/) to list the folders in it, tab to go into one
alt-enter in space POpen the project in a new session, even if one is running for it
space C or :connect HOSTShow a session on another machine here, as greed ssh does: pick from the hosts you connected to lately and those in ~/.ssh/config, or type one
:session NAME or :session PATHGo to a running session, or to the session for a project (starting it)
:project-closeClose the shown project
:qIn the last pane of the last tab, close the shown project and leave; on the last project, end the session unless something runs in it
:qaEnd the session and every project in it

A recent project or a folder typed into space P opens in its own session, starting it if it isn’t running, like greed FOLDER from a terminal. :project PATH opens a project in this session instead. Like every space key here, it’s the leader’s: in the vscode style, where space types, it’s ctrl-space P.

To find projects you haven’t opened yet, give space P sources in your init.luau: folders to look in, and commands that print one path per line.

require("@projects").sources({
	-- every folder one level under these
	{ dirs = { "~/Development", "~/Work" } },
	-- deeper, but only folders that look like projects
	{ dirs = { "~/src" }, depth = 3, markers = { ".jj", ".git" } },
	-- any command that prints paths
	{ command = { "zoxide", "query", "-l" } },
	{ name = "ghq", command = { "ghq", "list", "-p" } },
})

The picker shows what they found the last time it looked, so it opens at once, and looks again in the background each time it opens and when the editor starts. Each entry says where it came from.

Editing on another machine

greed ssh build-box src/main.rs

greed ssh HOST [--session NAME] [FILE] shows a session running on another machine, starting one there if none is running. Files, language servers, search and agents all run on that machine, and only keys and screen updates cross the network. If the connection drops or you :detach, the session keeps running, and greed ssh HOST comes back to it.

From inside Greed, space C (:connect, or Connect to Host… in the window’s File menu) does the same without a terminal: it lists the hosts you connected to lately and those in ~/.ssh/config, and the window or terminal you’re in moves to that machine’s session. That way Greed.app on a Mac, or a window on a Linux laptop, can be the screen for a workstation where the work runs.

greed ssh HOST --terminal shows the session and opens a terminal on that machine in a new tab, for when a shell is what you’re after. It runs in the session, so it’s still there after you disconnect. greed --terminal does the same here.

Your config comes along: greed ssh copies ~/.config/greed (your init.luau and plugins) to the other machine first, and a session it starts there runs that copy, so the editor works the way you set it up. The other machine’s own ~/.config/greed runs after it and wins, and greed ssh never touches it. Changes you make there, or an agent makes, or plugins an agent installs, stay on that machine across connections. A session that was already running keeps the config it started with. GREED_SSH_CONFIG=0 leaves your config at home.

To keep a plugin you made over there, bring it home. greed bring HOST lists the plugins in the other machine’s own config and data folders, and greed bring HOST PLUGIN copies one into your config’s plugins folder. A running Greed loads it at once, asking you first to approve what it uses.

$ greed bring build-box
build-box has these; greed bring build-box PLUGIN copies one here:
  /home/me/.config/greed/plugins/todo-sync
$ greed bring build-box todo-sync

Your login shell on the other machine can be anything (bash, fish, nushell): greed ssh only asks it to start sh, and needs a POSIX sh and tar there. Where commands over ssh come without XDG_RUNTIME_DIR, a session it starts uses /run/user/<uid> like a login would, and so do the programs in its terminals.

Greed doesn’t have to be installed on the other machine. The first time you connect with a version, greed ssh puts the same version there, in ~/.local/share/greed/server, and uses it from then on. It copies the greed you run when it runs there; otherwise, as when the other machine is NixOS or an arm machine, it fetches that version’s static build from the releases and checks it against the release’s checksums. A greed of the same version already on the other machine’s PATH is used as it is.

GREED_REMOTE sets the path of greed on the other machine, and nothing is copied (the two ends check they speak the same protocol, and say so if they don’t); GREED_REMOTE_BINARY names a greed built for it to copy (like nix build .#static’s, between releases). GREED_SSH sets the ssh program to use. It’s run as one program, so for options like a port use ~/.ssh/config or a small wrapper script.

Editing

Greed has three editing styles, and you pick the one you like: Helix’s (select, then act), Vim’s (act, then where) or modeless editing as in VS Code. Greed asks the first time it starts, :style vim switches at any time, and Editing styles has each one’s keys and how to make one your own.

Keys you type while a key’s command is still working (say, on a slow program) wait for it and then run in order, so typing ahead does what typing slowly would. They wait half a second at most; esc and ctrl-c reach a working command at once, unless keys typed before them are still waiting.

Messages show on the status line, one line each; a longer one, like an error with its stack trace, ends in …. :messages shows the last ones in full.

This page shows the Helix style, Greed’s default. In the Helix style you select text, then say what to do with it. There is always a selection; a cursor is a selection one character wide.

Moving and selecting (normal mode)

Keys
h j k l, arrowsMove
w b eSelect to the next word, previous word, word end
W B EThe same for WORDs, which include punctuation (a.b)
f t + a characterSelect to the next one, or up to it; F T go back
xSelect the line; again, the next one too
g g g eStart of the file, start of its last line
G, 5 GThe last line, or line 5
g h g l g sStart and end of the line, first character that isn’t a space
m mThe matching bracket
g wLabel every word on screen; type a label to jump there
; alt-;Collapse the selection to the cursor, swap its ends
ctrl-h ctrl-kBack to the selection before the last command, and forward again
%Select the whole file
ctrl-f ctrl-bA screen down or up (also pagedown pageup)
ctrl-d ctrl-uHalf a screen down or up
z z z c, z t, z bPut the cursor’s line in the middle, at the top or at the bottom of the screen
z j z kScroll a line down or up; Z keeps the view keys going until esc
g t g c g bThe top, middle or bottom line on the screen
g n g p, g a, g mThe next or previous open file, the one you were in before, the one you changed last
g . g |, g fWhere the file last changed, column N (8 g |), the file named under the cursor
alt-.Do the last f or t again
ctrl-o ctrl-iBack to where the last jump (a search, g g, g d, a file you opened) started, and forward again (ctrl-i needs a terminal that tells it from tab, as most do now)

:set line-numbers relative numbers the lines by how far they are from the cursor’s line, so the line marked 5 below is 5 j away. hybrid does the same with the cursor’s line showing its own number, and absolute goes back.

Changing

Keys
i aInsert before or after the selection
o OOpen a line below or above
I AInsert at the start or end of the line
d cDelete, or delete and insert, copying what was deleted
alt-d alt-cThe same without copying
r + a characterReplace every selected character with it
RReplace the selections with what was copied
~ ` alt-`Switch case, lowercase, uppercase
> <Indent or dedent the lines by one level of the language’s indent
JJoin lines with one space
:reflow [WIDTH]Rewrap the selected lines to the text-width option (80), or WIDTH
|Pipe each selection through a shell command, a Luau filter or a model (below)
space c, ctrl-cComment the lines, or uncomment them
=Format the file
] space [ spaceAdd an empty line below or above, staying put
y p PCopy, paste after, paste before; copied lines paste above or below the line
" + a letterUse that register for the next copy, paste, delete or macro, instead of the clipboard
Q qStart or stop recording a macro, play it (3 q three times)
:macro-save NAME [REGISTER]Keep the last macro as a command, in a macros plugin in your config that opens to edit
u UUndo, redo
alt-u alt-UBack or on through every state the text has been in, in the order it was made, including what an undo followed by an edit left on another branch
space u tThe undo tree: every state and branch; moving takes the text there, enter keeps it, esc goes back
.Repeat the last change at the selections, with what it typed (3 . with a new count)
escBack to normal mode

In insert mode, tab types a level of indent, ctrl-w (or alt-backspace) and alt-d delete a word back or forward, ctrl-u and ctrl-k delete to the start or end of the line, ctrl-x asks for completions, and ctrl-r + a register inserts what it holds.

A number before a motion repeats it: 3 j, 2 w. It also counts for x (lines), p and P, u and U, > and < (levels), C and alt-C, and picks brackets further out with m i ( and the like.

Some registers aren’t storage: "_ throws a copy away, "+ and "* are the clipboard, "% is the file’s path, ". what each selection holds, "# each selection’s number and "/ the last search.

| asks what to pipe the selections through, and puts back what comes out, each selection its own:

TypeWhat it does
sort -uRuns a shell command, with the selection as its input
luau: snake_caseRuns a Luau filter: upper, lower, trim, snake_case, kebab_case, camel_case, pascal_case, sort, reverse, or one you add with require("@pipe").define
model: add doc commentsAsks a language model (the fast role, or :set pipe-model-role reasoning)

Helix’s other shell keys work too: ! puts what a command prints before each selection and alt-! after it, alt-| gives each selection to a command without changing it, and $ keeps the selections a command succeeds on ($ grep -q TODO).

All the selections change in one step, so u takes them back. A model’s changes are also an operation you can review with space u o, and if you edit while the model works, the answers still land where the selections were. See Models to set one up.

New lines from enter, o and O start at the indent the code calls for: a level in inside a block, bracket or let, and lined up with the block’s first line before its } or end. Between a pair of brackets, enter puts the closing one on a line of its own. Languages without a grammar keep the line’s indent, one level more after an opening bracket (or a : in Python and YAML), and so does code that doesn’t parse yet, like a Ruby def before its end, going by the language’s keywords (indent_after and outdent_before in its entry). A line holding just a closer like end, else or } lines up with its block when you press enter on it, and a closing bracket does as you type it at a line’s start. Each language has its own indent, like two spaces in Nix or a tab in Go, set with require("@languages").set("nix", { indent = 4 }); the indent option is for files in no language. A file gets its language from its name; :language NAME gives a buffer another one (:setf too, as in Vim), and :language alone says which it has.

Many selections

Keys
sSelect every regex match inside the selections
SSplit the selections at a regex
alt-sSplit the selections into lines
K alt-KKeep or drop the selections that match a regex
C alt-CAdd a selection on the line below or above
, alt-,Keep only the primary selection, or drop it
( )Make the previous or next selection primary
X alt-xExtend each selection to whole lines, or shrink it to the whole lines inside
alt-_Merge selections that touch (:merge-selections merges them all)
_ &Trim spaces off the selections, line them up at one column
alt-( alt-)Rotate what the selections hold, back or forward
ctrl-a ctrl-xAdd to or subtract from the number at each cursor (5 ctrl-a)
vSelect mode: moves extend the selection, n N add the next match as another selection; it lasts until you change or copy the text

With several selections, everything you type or delete happens at each one. %, then s foo, then c bar replaces every foo in the file. s with nothing typed uses the last search, so * on a word, then % s enter, selects every place it appears.

Text objects and surround

Keys
m i w m a wThe word, or the word and the space after it (W for WORDs)
m i ( m a (Inside or around the brackets; also [ { <
m i " m a "Inside or around the quotes; also ' and `
m i m m a mInside or around the nearest brackets or quotes, whichever they are
m i x m a xInside or around the XML or HTML element
m i n ( m i l (Inside the next or last pair after or before the cursor (m a for around); any bracket or quote
m i i m a iThe lines indented as far as this one, or with the line above too
m i d m a dThe number at or after the cursor, or with its sign
m i g m a gThe change since the last commit under the cursor, or its whole lines
m s (Wrap each selection in brackets, quotes, or any character
m d (Take away the brackets or quotes around each selection
m r ( [Swap them for others

By syntax, in languages Greed has a grammar for (:health lists them):

Keys
m i f m a fInside or around the function
m i t m a tInside or around the type: a struct, enum, impl or table
m i a m a aAn argument or parameter, around with its comma
m a cThe comment
m i T m a TInside or around the test (Rust, Python, Go, JavaScript and TypeScript)
m a eAn entry in a table, map or struct
] f [ fThe next or previous function; also t, a, c, T and e
alt-o alt-iGrow the selection to the syntax node around it, or shrink it back
] n [ nThe next or previous node beside it (Helix has these on alt-n alt-p, which are pane keys here)
alt-a alt-IEvery node beside it, or every node inside it
alt-shift-right alt-shift-leftMove the selected node past the next or previous one: swap arguments, list items, statements
alt-b alt-eThe start or end of the node around it
space sJump to a function or type in the file

In Vim’s style the same objects are if af, ia aa and ic ac, as in daf or cia, and the next/last, indent and number objects are in( il(, ii ai and id ad, as in cin( or dii.

Folding

Keys
tab or z aFold the block the cursor is in under its first line, or unfold it
shift-tabUnfold everything when anything is folded, or else fold every outermost block
z C z oFold it, or unfold the folds the cursor is in (z c in the Vim style)
z RUnfold everything

A block is the smallest function, body, table or similar around the cursor’s line, from the language’s grammar. In a language without one, it’s the lines indented under the nearest line indented less, and in Markdown it’s a fenced code block, a list item with lines under it (a todo and its subtasks) or the section under a heading, whichever is innermost. Vim’s style folds with tab, z a, z o, z R and z c (it has no z C or shift-tab), and also z M to fold every outermost block, z f

  • a motion to fold those lines, z j z k to go to the next or previous fold, and z d z E to unfold.

If you’d rather edit like Vim, or without modes, :style vim and :style vscode switch to those; Editing styles explains each.

Prose: m i p, m i s and m i h select a paragraph, sentence or Markdown section (m a ... includes the space around it), and ] p [ p, ] s [ s move between them.

Searching

Keys
/ ?Search forward or back as you type
n NNext and previous match
*Search for the selected text, exactly as written; a whole word only matches whole words
alt-*The same, also inside longer words
space /Search the whole project as you type

Patterns are regexes. How searches you type mind case is one option, search-case: "smart" (the default) ignores case unless what you type has a capital letter, and "ignore" and "match" always do one thing. It covers searching a file, the project search, :g and the pickers’ fuzzy matching. A few kinds of search have their own option, with the default that suits them, which "default" hands back to search-case:

OptionDefault
notes-search-case"ignore"Searching notes (space n /)
substitute-search-case"match":s, as in Vim; its i and I flags still say otherwise for one run
shell-filter-search-case"ignore"/ on a shell block’s output
local option = require("@greed").option
option.set("search-case", "ignore") -- every search ignores case...
option.set("substitute-search-case", "default") -- ...:s too

In the project search, enter opens a match and ctrl-e puts what was found in a panel, a third of the screen wide: a multibuffer, with each matching line under its file’s name and beside its line number. Edit those lines with any of the editing keys above (s to select the matches, then c to change them all) and the files change as you type; :w saves them, enter goes to the line and q closes the panel. A file that changed on disk since the search is left alone and reported. :grep PATTERN fills the panel directly.

Files and buffers

Keys
space fFind a file in the project
space FFind a file under the folder Greed was started in
space 'Open the last picker again
space .What you can do with what’s under the cursor (a file, link, symbol, problem, change or the selection), to pick one; alt-. in the vscode style
space bSwitch to an open file
space jPick a place from the jumplist
space y space p space RCopy, paste and replace, as in Helix (copies always go to the clipboard here)
space eFile tree in a side panel
:w :w PATHSave, or save as; an untitled buffer asks where to save it (ctrl-s too, in the vscode style)
:wa (:write-all)Save every file with unsaved changes
:e PATHOpen a file
ctrl-sSave
:b NAMESwitch to the open file whose path is NAME or has it in it; :b# (or ctrl-^ in normal mode) goes back to the one before
:ls (:buffers)Pick an open file, like space b
:bn :bpNext and previous open file
:bc (:bd)Close the file; the one you were in before takes its place. :bc! closes it with unsaved changes
:bcoClose every other file
:closeClose the pane (the file stays open), like alt-p x; :only closes the others
:sp :vs :new :vnew :tabnewSplit the pane, or open a new pane or tab, as in Vim
:reload (:rl), :rlaLoad the file again from disk, or every open file
:nohTake away search highlights
:set NAME VALUESet an option. Yes-or-no ones also take Vim’s :set NAME, :set noNAME and :set NAME!, :set NAME? shows one, and :set nu, nonu, rnu and nornu show and hide line numbers. :toggle NAME flips one, as in Helix. A name that isn’t an option gets the names like it
:stallsThe times plugin code held the editor up, with the plugin and what it was doing
:errorsErrors from plugin code that nothing caught, newest first: the plugin, what it was doing, the message and where in its code it failed
:profileStart counting where the editor’s time goes; again, stop and show it by plugin and what it did
:secret-set NAMEKeep a secret, like an API key, in the system keyring (or Greed’s own secrets file where there’s none); what you type shows as dots
:login PROVIDER, :logout PROVIDERSign in to a model provider whose plan comes with an account, like Berget Code, instead of using a key (see Models)

Closing a terminal’s buffer ends its program.

Saving writes a new copy of the file beside it and moves it into place, so a crash or a full disk never leaves a file half written. The file keeps its permissions, and saving through a symlink writes the file it points to. A file that isn’t UTF-8 opens read-only, with what can’t be read shown as �, and :w refuses to write over it, since that would replace those bytes; :w PATH saves what you see to another file.

greed bench [FILE] measures how quickly keys show: it starts a session with your plugins and config on the file (a large generated Rust file without one), types and moves around as you would, and prints how long each kind of key took from leaving the client to the frame showing it arriving, with the slowest named, and where the time went per key (as :profile shows it). It types into a terminal too, timing each character until its echo shows. greed bench --draw also draws each frame in the terminal you run it in, the way greed does, and says how long that took, how many bytes went out, and how long your terminal took to take them in.

In the file tree, enter or l opens a file or folds a folder, h folds the folder you’re in, r reads the disk again and q closes it. a makes a file in the folder under the cursor (end the name with / for a folder), R renames or moves what’s under the cursor, taking files you have open along, and d deletes it once you press y. A click does what enter does, and the row under the mouse lights up.

Without the tree, :file-rename NAME renames the file you’re in (a path moves it, from the project’s root), and :file-delete deletes it once you press y. :set tree-icons nerd shows an icon by each name (see Icons).

When a file changes on disk, from another program or an agent, its buffer loads the change as an edit you can undo with u. A buffer with unsaved changes is left alone and Greed tells you; :reload loads the file into it anyway. :set auto-reload false stops loading changes by itself.

:set auto-save true saves files on their own a second after you stop editing them, so a burst of typing is one save; :set auto-save-delay 3000 waits three seconds instead. Put it in your init.luau to keep it: require("@greed").option.set("auto-save", true).

Icons

The file tree’s icons and the status line’s rounded ends come from Nerd Fonts. The window (greed --gui) has them built in, so they show whatever font you draw in, even when another installed font claims the same characters. A terminal shows them only if its own font has them: install a Nerd Font, or one of its icon-only “Symbols” fonts, and set your terminal to use it. Without one you see boxes where icons should be.

In the window, emoji come from your system’s emoji font; one it can’t draw in color, like the newest Noto Color Emoji, is passed over for another, or the plain Noto Emoji.

Two options use them. Out of the box you see plain text that works anywhere: tree-icons is "none", and the rounded ends show only once statusline-pills is on (see The status line).

local option = require("@greed").option
option.set("tree-icons", "nerd") -- "none" for no icons
option.set("statusline-caps", "round") -- "none" for square ends

Setup turns icons on when you run it in the window, or when you say a terminal shows them. The same config works for the window and terminals, so if you set Greed up in the window and later use a terminal without a Nerd Font, set both options back to "none" in your init.luau.

Language servers

Greed knows the usual languages and their servers and starts a server when a file needs one and it’s installed. :health shows each language’s server, whether it was found, and the servers running. Without a server nothing complains until you ask for something only a server can answer, like g d; then it says which program to install.

Each project gets a server of its own, so files from two projects (or two jj workspaces of one) are checked separately. A server runs in the topmost folder of the file’s project that has one of the language’s root files (Cargo.toml for Rust, package.json for TypeScript), so one server covers a whole Cargo or npm workspace; without one, it runs in the project’s root. A server that crashes starts again, up to three times in a row, and :lsp-restart starts this file’s server again when it’s stuck (:lsp-restart all every one). Servers that support it hear only what changed with each edit, so big files stay quick.

Keys
space kWhat’s under the cursor, with any problems there
g dGo to the definition
g D g y g iGo to the declaration, the type’s definition, an implementation
g rPick a place it’s used, with a preview
space HSelect every place in this file it’s used (Helix’s space h, which is help here)
space SPick a symbol anywhere in the project
:renameRename it everywhere it’s used (space r; also g r n in the Vim style, f2 in the vscode style)
space aPick a fix or refactor for the cursor or selection
space dPick a problem in this file
space DPick a problem in any file the server checked, open or not
] d [ dNext and previous problem; ] D [ D the last and first

Problems are underlined, and their lines get a sign and a colored line number. They move with your edits until the server checks the file again.

In the g r and space D lists, ctrl-e puts the places shown in a panel like the project search’s: each reference with a line around it, or each problem’s line with its message beside the file name (two on one line get a line each, and one too long for the panel ends in …). Edit them there the same way.

A rename or code action that changes other files opens them without showing them and leaves them unsaved, so you can look the change over (space b) and undo it before saving.

Completions show as you type, in a menu at the cursor, once typing pauses after a letter or a .. The menu narrows as you keep typing, with what starts with what you typed first, then what holds it, then looser matches.

Keys
tab ctrl-n downNext completion
shift-tab ctrl-p upPrevious completion
enterPut the current one in
escClose the menu (and leave insert mode)
ctrl-spaceAsk for completions now (in the vscode style it opens the leader instead)

These apply to every completion menu, the shell’s included:

  • completion-delay (150) is how many milliseconds typing pauses before a menu opens by itself; :set completion-delay 300 waits longer, and 0 opens it only when you ask, with ctrl-space (or tab in the shell). Asking always shows it at once.
  • completion-enter says what enter does in a menu: first puts in the first item, selected only an item you moved to with tab or the arrows, and otherwise enter does its usual job, like starting a new line or running a shell’s line. auto, the default, lets each menu say: a language server’s takes the first, the shell’s only one you chose.

In Vim’s style, ctrl-n and ctrl-p in insert mode complete from the words in open files.

Typing ( or , in a call shows what the function takes, above the cursor, with the parameter you’re on picked out. It follows along as you type and closes when the call does or you leave insert mode. To ask for it with a key, bind signature-help in insert mode, e.g. greed.keymap.bind({ role = "typing" }, { ["ctrl-k"] = "signature-help" }).

Version control

In a jj or git repository, lines added (+), changed (~) and removed (-) get a sign beside them. With jj that’s against the working copy’s parent, so the signs show what your current change does. With git it’s against what’s staged, so a change loses its sign once you stage it.

Keys
] g [ gNext and previous change; ] G [ G the last and first
space g rPut the change under the cursor back the way it was
space g aStage the change under the cursor (git)
space g bWho last changed this line, and when
space g wWhy the selected lines are as they are: each change that made them, with its description and diff
space g gPick a file changed since the last commit
space g s (:changes)Every change in the project, in one buffer
space g S (:staged)What’s staged, to unstage hunks and commit (git)
space g l (:change-stack)The change stack: describe, split, squash, move and abandon changes

:set vcs-signs false hides the signs.

space g s shows each change in every file, with a couple of lines around it: added lines in green, and removed ones in red where they were, under each file’s name. With git it shows what’s not staged yet. It’s a multibuffer like the project search’s results, so you can edit the changes there and :w saves the files; space g r on a change puts it back, space g a stages just that change, and enter goes to it in its file. New files show whole, and deleted ones are left out.

space g S shows what’s staged as a diff: u on a hunk unstages it, c commits with a message you type, r reads it again and q closes it. jj has no staging area; split a change in the change stack instead.

space g l lists your changes with jj, newest first: from the working copy (@) down to the last immutable change (◆), with their bookmarks and descriptions. On the change under the cursor:

Keys
enterShow its diff; q goes back
dDescribe it
nStart a new change on top of it
eMake it the working copy, to edit it
sSquash it into its parent, keeping the parent’s description
SSplit it: pick the hunks for a new change before it
aAbandon it, after asking
alt-k alt-jMove it up or down the stack
uUndo the last jj operation
rRead the stack again
qClose it
?A menu of these keys

In a git repository it lists the last 30 commits, and enter shows one.

Operations

An operation is a piece of work across files taken as one thing: a rename an agent did in 14 files, a plugin’s refactor, the changes you accepted from a proposal. Its edits in every file undo together, and you can review it in one buffer.

KeysWhat it does
space u o (:operations)Pick a recent operation to review
space u u (:operation-undo)Undo the latest operation, in every file at once
space u c (:operation-commit)Save the latest operation’s files and commit just them, named after it

The review is a multibuffer of the operation’s changes, with the programs it ran (a test run, a formatter) and how they exited at the top. Edit it like any multibuffer, space g r puts a change back, and space u u in it undoes that operation.

If you’ve edited its files since, undo keeps your edits: it puts back each of the operation’s changes you haven’t touched, leaves the ones you have, and says how many it left. Operations are kept across restarts (in the cache folder), so you can still review and undo yesterday’s agent work the same way.

space u c turns an operation into a commit of its own (a jj change, or a git commit): it saves the operation’s files and commits only them, with the operation’s name as the message, so other changes you have going stay where they were. In a review it commits the operation shown.

Every edit also records who made it (you, a plugin, an agent, or a change on disk) and the command it came from. Agents can ask with the history_recent tool, and plugins with buffer:history().

Models

Features that use a language model ask for a role, and you choose which model plays it. Out of the box:

RoleModel
fastClaude Haiku 4.5, at Anthropic
reasoningClaude Opus 5.5, at Anthropic
localLlama 3.2, on Ollama on this machine

Greed’s own agent asks for agent, which uses reasoning until you set it; reviewing code an agent wants to run asks for review, also reasoning until set; and compacting a long conversation of Greed’s agent asks for compact, which uses agent until set.

:models shows every role, its model and whether its provider’s key is found, and lets you change them:

Key
enterPick the provider and model for the role under the cursor (for Ollama, from the models it has pulled); asks for the key if it needs one
kSet the API key of the role’s provider: it goes in the system keyring, or Greed’s secrets file where there’s none
lSign in to the role’s provider, for those with an account instead of a key, like Berget
qClose

What you pick there is kept in Greed’s data folder and comes back next time. Your init.luau is read after it, so a role set there wins, and :models says “(set in your config)” beside it. Instead of the key, you can set the variable :models names, like ANTHROPIC_API_KEY, or run :secret-set anthropic. Anything that needs a model and has no key says the same: which variable sets it, and that :models adds one.

:usage shows how many tokens the models were sent and wrote: today, by model, and each day of the last week, as the providers reported them for Greed’s agent and the features with tools. Tokens read from the provider’s cache are shown apart, as “(52.1k cached)”: they cost a fraction of the rest, so a low share means caching isn’t working. Under each model, how many of Greed’s agent’s edits worked, by the way it made them. It’s kept a file a day in Greed’s data folder (usage/2026-10-09.json), by hour and model.

Ollama needs no key, but it has to run: ollama serve starts it, and ollama pull llama3.2 fetches a model. Greed says so when it can’t reach it or the model isn’t there.

You can also set any of it in your init.luau, with OpenAI, Ollama or any server that speaks OpenAI’s chat completions API:

local models = require("@models")
models.roles.fast = { provider = "ollama", model = "qwen2.5-coder" }
models.roles.reasoning = { provider = "openai", model = "gpt-5" }
models.providers.work = { kind = "openai", url = "https://llm.example.com/v1", secret = "work" }
models.roles.review = { provider = "work", model = "reviewer-1" }

A role or provider can also say how many tokens its model has room for, as context = 128000. Greed knows that for well-known models by name and assumes 32k for others; Greed’s own agent uses it to compact long conversations (see Agents).

A provider with a secret reads its key with :secret-set NAME, or from GREED_SECRET_NAME. The built-in providers send their keys only to their own servers. The first time any other key would go to an address, Greed asks (y yes, n no) and keeps your answer, so a plugin can’t add a provider that sends your key somewhere you didn’t choose.

Some plans come with an account instead of a key, like Berget Code’s fixed monthly price. :login berget signs you in: it shows a link and a code, you approve it in a browser on any device, and the berget provider uses that sign-in from then on, renewing it as needed. :models says “signed in”; :logout berget forgets it, and the provider goes back to its key.

models.roles.agent = { provider = "berget", model = "the model you want" }
Keys
space i eExplain the selection (or the line) in a panel, as the answer arrives
space i aAsk something about the selection
space i fFix the problem the language server reports on this line, as proposed changes to review
space i cDraft a description of the changes in progress; a uses it (jj describe, or a git commit of what’s staged)

Fixes come like an agent’s proposals: ] o goes to each change, space o a or space o r accepts or rejects it, and what you accept undoes as one operation. These use the fast role; :set assist-role reasoning asks another.

The context

Pin what a model or an agent should see, and it goes with every question:

Keys
space i pPin the selection, or the whole file when nothing is selected
space i iShow what’s pinned, one item a line with its size in tokens; delete a line (x d) to drop it
:context-add-problemsPin this file’s problems from the language server
:context-add-diffPin the changes in progress
:context-clearDrop everything

Items are read fresh each time they’re sent: a pinned selection follows the text as you edit, and the diff is the diff as it is now. The assist commands send the context with each question, and agents read it with the context_get tool. Each project keeps its own, across restarts.

Formatting

Saving a file formats it, and :format does it without saving. Greed uses the language’s formatter when it’s installed (gofmt, ruff, stylua, prettier, shfmt, …) and otherwise asks the language server, which is how Rust gets rustfmt through rust-analyzer. Only what the formatter changed is replaced, so your cursor stays put.

-- A different formatter for a language
require("@languages").set("python", { formatter = { "black", "-q", "-" } })

-- No formatting on save
require("@greed").option.set("format-on-save", false)

A formatter gets the text on its input and prints it formatted; {path} in its command stands for the file’s path.

Markdown

Markdown files show as documents: headings without their hashes (a top-level heading twice the size), **bold**, *italic*, ~~struck~~ and `code` without their markers, links as their underlined text, fenced code on a background of its own, quotes with a bar, --- as a line, list bullets (-, * or +) as dots and tables in aligned columns with borders. Code blocks keep their markup: a # comment there is code, not a heading. The file stays plain Markdown: while you type on a line (insert mode) it shows as written, and in the vscode style markup shows once the cursor is inside it. The window draws big headings big and sets the text in a proportional font (code and tables stay monospaced); terminals other than kitty show headings at the normal size.

:set markdown-look false turns the look off everywhere. To see one file as written, or one file as a document while the option is off, toggle it for that file with space m l (alt-V in the vscode style, or :markdown-look-toggle). The status line shows markdown · plain while a file shows as written.

Reading view

space m v (alt-v in the vscode style, or :markdown-read) shows the file for reading: read-only, without line numbers, and with no markup showing anywhere, even on the cursor’s line. Images on a line of their own show in place (in the window, and in terminals that show images; elsewhere you see their description), and links show as their text. esc, q or the same key again goes back to editing, at the same place.

Keys
j k h l, arrowsMove
pagedown pageup, ctrl-f ctrl-b, ctrl-d ctrl-uPage and half page
/, n, NSearch
enter or a click on a linkFollow it
esc, qBack to editing

A link to #a-heading goes to that heading, a link to another Markdown file opens it in the reading view too (at its heading, with other.md#a-heading), a web or mail link opens in your browser or mail program (the link-opener option says which), and a file:// link opens the file in Greed. Links of other kinds, which any file you read could hold, don’t open.

Choices

A list whose items start with ( ), one of them (x), is a set of choices. They show as ◯ and ◉ (round buttons in the window), and pressing one (a click, or enter on it) picks it and unpicks the others. The answer is the text itself, so it reads the same anywhere:

How do you like to edit?

- (x) Helix: select first, then act
- ( ) Vim: say what, then where

Picking only changes the text. Plugins read the answers and act on them (see Writing plugins), and so can code blocks in the file (below).

Questionnaires

Choices and runnable blocks together make a page of questions that does things, which still reads as plain Markdown anywhere. Comments, which GitHub and the like don’t show either, hold what makes it work:

Which theme? <!-- id=theme -->

```luau output=choices
return require("@theme").list()
```

```luau when=theme
require("@theme").use(answer)
```

Will you use agents? <!-- id=agents -->

- (x) Yes
- ( ) No

<!-- if agents=yes -->
Which language model? <!-- id=provider -->

- ( ) Anthropic
- ( ) Ollama: on this machine <!-- value=ollama -->
<!-- end -->

```luau button="Save"
require("@greed").echo(`theme {answers.theme}, model {answers.provider}`)
```
  • <!-- id=NAME --> on a question names it. Its answer is the picked option’s value: its text up to the first : (“Ollama”), or a <!-- value=... --> on the option.
  • A block with output=choices writes the choices it returns (a list, or one per line; (x) in front picks one) under itself, and running it again keeps your pick. In a file you trust, these run as it opens.
  • A block with when=NAME runs each time that question’s answer changes, with answer set to it.
  • A block with button="Label" shows as a button, and runs when pressed.
  • Every block gets answers, each named question’s answer (programs get them as JSON in ANSWERS, and ANSWER).
  • <!-- if NAME=VALUE --> to <!-- end --> is a part that shows only for that answer (!= for any other), or for any of several, as in <!-- if style=vim,helix -->. Its questions don’t count otherwise.

Blocks that make choices or hear picks hide behind the page, and buttons show as buttons: put the cursor there to see their code. They run as any block does: in a file you haven’t trusted, the first asks whether to. A Luau block can’t wait, so one that asks for something, like a prompt or a secret, does it in greed.spawn. Greed’s own setup page is a questionnaire like this: setup.md in the setup plugin.

Runnable code blocks

A fenced block in a language Greed can run runs when you ask, and its output is saved in a result block right under it. That way the file shows the output anywhere, from GitHub to your phone, and a diff shows what changed. Running a block again replaces its result.

```sh id=files
ls *.md
```

```result
README.md
using.md
```
Keys
space m r, or enter on the fence once it shows ▶ runRun the block under the cursor
space m RRun every block in the file, in order
space m cClear the block’s result (:blocks-results-clear clears them all)
space m sStop the block running under the cursor

Blocks run as sh, bash, zsh, fish, python, node, ruby, luau and http:

  • A program gets the code to run, and prints the result. When it fails, the result block says how it exited (result exit=1).
  • A luau block runs in the editor, with the whole API: its result is what it prints and what it returns. A one-line expression like 6 * 7 is its own result.
  • An http block is a request: the method and URL on its first line (or just a URL, for GET), then header lines, then after a blank line the body. The result is the status and the body; headers=true adds the response’s headers.

Options go after the language:

  • id=NAME names a block, and input=NAME gives another block that block’s result: on stdin for a program, as input in Luau. The named block runs first if it has no result yet, so a page of blocks works like a small pipeline. With rerun=true on the block taking the input, its input runs again every time, so it never reads a stale result.
  • dir=PATH runs a program in another folder than the file’s.
  • timeout=SECONDS stops it after that long.

While a block runs, its fence shows ■ running; click that or press space m s to stop it, along with anything it started, like a server. A result keeps at most blocks-max-lines lines of output (1000).

Nothing runs when a file opens. The first time you run a block in a file, Greed asks whether to: y for now, a to always trust that file (:blocks-untrust takes it back). Trusted files, and files whose front matter says greed: run, show a ▶ run button after each block’s fence. Front matter only shows the buttons; running still asks. The status line shows markdown · blocks when they’re on. Agents can list and run blocks too (blocks_list, blocks_run), but only in files you trust, and only blocks as they were when you last ran them yourself or trusted the file (a, or :blocks-trust). A block an agent changed, in the buffer or on disk, doesn’t run for an agent until you’ve run it, and neither does a block taking its input from one.

Add a language from Luau:

local blocks = require("@blocks")
blocks.runner("lua", blocks.program({ "lua", "-e" }))

Notes

Notes are Markdown files in a folder (~/notes unless you set notes-folder), so git or Syncthing can carry them between machines and to your phone. Link one to another with [[name]], or [[name|other words]] to show other words.

Keys
space n dToday’s note, in daily/
space n cAdd a todo to the inbox, from wherever you are: the project’s when it keeps notes, otherwise yours (:notes-capture-mine always yours)
space n aThe agenda: open todos from every note, soonest due first
space n fFind a note; a name that isn’t one yet makes it
space n /Search every note as you type
space n xTick a todo or untick it; a plain line becomes one
space n tJot a thought or an idea down in the inbox, with today’s date: for things that aren’t todos
space n rDismiss the reminders notice (below)
space kOn a due date or a reminder, say when it is in words (“tomorrow, 18:00, in 25 hours”)
space n lHave Greed’s agent connect the inbox (or the note you’re in) to your other notes (below)
g dFollow the link under the cursor, making the note if it’s new
z aFold the section under the cursor, or unfold it (any Markdown)

A todo is a list item with a box, - [ ] call the bank, and due:2026-10-03 anywhere in it gives it a date for the agenda. In a note, links show without their brackets (except while typing on their line) and the notes linking to it are listed at its end. Other Markdown files, like a project’s README, look as they always do; :set notes-everywhere true treats every Markdown file as a note, and :set notes-conceal false shows the brackets and the markup below as written.

require("@greed").option.set("notes-folder", "~/Documents/notes")

Reminders

Put remind:2026-10-06 18:00 on a todo, or on any line of a note, and Greed reminds you then. A date alone, remind:2026-10-06, goes off at 09:00 (:set notes-remind-time 08:30 to change it). Times are your computer’s local time. Ticked todos don’t remind you.

When a reminder comes due:

  • A notice opens at the top right and stays until you dismiss it with space n r. Click a reminder in it to open its note at that line.
  • Your desktop shows a notification, with notify-send on Linux or osascript on macOS, when they’re installed. :set notes-remind-desktop false turns this off.
  • In a terminal that isn’t focused, Greed also sends the terminal’s notification escape and a bell, so the terminal can tell you even over greed ssh. Terminals that understand it (kitty, WezTerm, Ghostty, iTerm2, foot and others) show a notification of their own. Inside tmux only the bell gets through, and only with tmux’s focus-events on. When the terminal and notify-send both show one, :set notes-remind-desktop false leaves only the terminal’s.

Reminders that came due while Greed wasn’t running show the next time it starts. Greed remembers which ones went off in its cache folder, so each goes off once.

The status line says how many todos are due today, like 2 due today, in a warning color when some are overdue (1 overdue, 2 due today). Click it to open the agenda.

Agents can set reminders too: ask Greed’s agent to “remind me this evening to call the bank” (see Agents).

Connecting your notes

Jot thoughts and todos down as they come (space n t, space n c), then let Greed’s agent make sense of them: space n l has it go through your inbox, or the note you’re in, find the notes each item relates to, link them with [[name]] and add a line of context. It changes notes that are there only as proposed edits you accept or reject one by one, makes new notes for items that belong together, and ends with a summary. It uses the model playing the agent role.

Any agent can work with your notes the same way through the notes tool group (see Agents); :set agent-tool-groups "plan notes" gives it to every agent from the start. Agents reach only the notes in your note folders, by name: ../x and other names leading out of them are refused. Rewriting a note that’s there comes to you as a proposal, adding a line or a todo goes straight in, and decisions can only be proposed with decision_propose.

Notes in a project, and other places

A project can keep notes of its own in .greed/notes/ (or wherever notes-project-folder says, inside the project). Make the folder and its notes count: they’re found, searched and linked with yours, their todos are in the agenda, and capture puts todos in the project’s inbox. Their names start with project:, like project:plan; a link in a project note goes to a note beside it first, so [[design]] there means project:design when there is one. The plan and decisions you share with agents (see Agents) are notes there too. Commit the folder to share it, or leave it out of the repository to keep it yours.

More folders of your own, each with a name for its notes:

require("@notes").add_place("work", "~/work/notes") -- notes named work:NAME

How notes look

A note’s markup shows as symbols: todo boxes as ☐ and ☑ (real checkboxes in the window; done ones dimmed and struck through), headings as a symbol for their level, list bullets as dots, due:2026-10-03 as 📅 3 Oct (red once it’s past, yellow on the day and in the three days before), and remind:2026-10-06 18:00 as ⏰ 6 Oct 18:00. Code blocks keep their markup, and the agenda leaves out todos in them. The file stays plain Markdown: while you type on a line (insert mode), it shows as written; in the vscode style, markup shows once the cursor is inside it. Click a box to tick it, or press enter on it, and a heading’s symbol folds its section.

The symbols, rules of your own and how they all look are set from your config:

local notes = require("@notes")

-- other symbols, e.g. from a Nerd Font
notes.symbols({ todo = "", done = "", bullet = "-" })

-- tags in their own colour, and !! shown as a flame
notes.rule({ pattern = "#[%w_-]+", style = "notes.tag" })
notes.rule({ pattern = "!!", show = "🔥", style = "notes.urgent" })

-- how a style looks is the theme's, like everything else
require("@greed").theme.set({ ["notes.tag"] = { fg = "magenta" } })

A rule’s pattern is a Luau pattern; with show the match is replaced by that text, and with action (a command name) clicking it or pressing enter on it runs the command. The styles notes use are notes.todo, notes.done, notes.done.box, notes.heading, notes.bullet, notes.due, notes.due.soon, notes.due.today, notes.overdue and notes.remind.

Agents can read and write your notes and todos too; see Agents.

Terminals

:terminal opens your shell in the project’s folder, and :terminal COMMAND runs a program instead, e.g. :terminal cargo test. Every key goes to the program, including ones the editor would otherwise take, like ctrl-q.

Keys
space tSwitch to a terminal, or open a new one
space T (in a file)Send the selection, or the cursor’s line, to the terminal you used last and run it: for a REPL or a shell beside your code
ctrl-\, esc escStop typing into the program, to move around its output; one esc still goes to the program
ctrl-spaceThe same, then open the leader: ctrl-space f finds a file from inside a terminal
alt-p and the other alt pane keysWork as in any pane: alt-p x closes the terminal’s pane
f1The keys that work while typing into it
i, a, enterType into it again
pPaste the latest copy into the program
g x, ctrl-clickOpen the link under the cursor (or the mouse), or the file a path there names, at its line
[ p ] pThe command before or after the cursor, where your shell marks them (below)
g oSelect the output of the command at the cursor, to copy or search it
] e [ eThe next or previous file and line the output names, like a compiler’s error at src/main.rs:12:5; g x opens it
space dPick from every file and line the output names, with what it says there, to open one
:terminal-closeEnd the program and close the terminal

Out of the terminal, its output is text like any other: select it, search it, copy it. Lines that scroll off the top stay above the screen, up to terminal-scrollback lines (10000). When the program ends well its terminal closes; when it fails, the output stays with how it ended, so you can read what went wrong. A terminal keeps running while you look at other files, and while you’re detached. :set terminal-shell "fish -l" changes what new terminals run. A terminal is called by what it runs and the folder its shell is in, like nu ~/src or cargo test ~/src: on a floating terminal’s border, in the dock, the tab line and the status line.

Programs that ask for the kitty keyboard protocol (Helix, Neovim, fish, recent shells) get it, so they can tell tab from ctrl-i or enter from shift-enter. For those keys to reach Greed in the first place your own terminal needs the protocol too (kitty, WezTerm, foot, Ghostty, Alacritty and iTerm2 have it); Greed asks for it when it starts. GREED_KEYS=legacy turns that off.

Links programs print (as ls --hyperlink and gcc do) are underlined, and URLs written in the output open too. They open with xdg-open (open on macOS), or whatever :set link-opener firefox names.

Tasks

space o t picks a task to run: a command you run again and again, like building or testing. It runs in a terminal below the file, which stays open when it’s done so you can read it, and running it again replaces the last run. ] e and space d in its output go to the files and lines it names, like a compiler’s errors. The last task you ran comes first, so space o t enter runs it again; :task test runs one by name.

Greed finds tasks in the project: Cargo’s build, test, run and clippy, a package.json’s scripts, and a Makefile’s targets. Add your own in init.luau; they win over found ones with the same name:

local terminal = require("@terminal")
terminal.tasks.lint = { command = "ruff check ." }
terminal.tasks.serve = { command = { "python", "-m", "http.server" }, cwd = "~/site" }

Commands your shell marks

A shell can mark where each prompt, command and its output start, and how the command ended. Greed then knows the commands in a terminal: [ p and ] p go between them, g o selects one’s output, and a command that failed shows its exit code at the end of its line.

fish 4 marks them on its own. For zsh, add this to ~/.zshrc:

_greed_precmd() { local code=$?; print -n "\e]133;D;$code\a\e]133;A\a" }
_greed_preexec() { print -n "\e]133;C\a" }
precmd_functions+=(_greed_precmd)
preexec_functions+=(_greed_preexec)
PS1="$PS1%{\e]133;B\a%}"

and for bash, to ~/.bashrc:

PROMPT_COMMAND='printf "\e]133;D;%s\a\e]133;A\a" "$?"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
PS1="$PS1\[\e]133;B\a\]"
PS0='\e]133;C\a'

These are the same marks kitty, WezTerm, Ghostty and iTerm2 read, so they work there too.

The folder your shell is in

A shell can also say which folder it’s in. Then a terminal you open from a terminal starts in the same folder, and g x (or ctrl-click) on a path in the output, like src/main.rs:12:5 from a compiler, opens that file at that line, finding it from that folder. Without it, paths are found from the project’s folder.

For fish, add this to ~/.config/fish/config.fish:

function __greed_folder --on-variable PWD
    printf '\e]7;file://%s%s\a' (hostname) (string escape --style=url $PWD)
end
__greed_folder

For zsh:

_greed_folder() { print -n "\e]7;file://$HOST$PWD\a" }
chpwd_functions+=(_greed_folder)
_greed_folder

and for bash, in PROMPT_COMMAND:

PROMPT_COMMAND='printf "\e]7;file://%s%s\a" "$HOSTNAME" "$PWD"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"

Shells

:shell opens a shell in a buffer, and space o s switches to one: type a command at the prompt at the bottom and enter runs it. Each command becomes a block above the prompt, its command, what it printed and how it ended, which stays to search, fold, copy, run again and send to the next command or to a buffer ($3 | sort, cargo test > buf:tests). Agents run their commands there too. space o w lists what’s running in every shell: enter opens one in a float, ctrl-o goes to it and ctrl-x stops it. Programs a shell runs that want an editor, like git commit, open the file in Greed ($EDITOR is greed edit --wait there; :set shell-editor false turns that off). The shell has the rest: keys, builtins, which shell runs a line, terminals, and limits.

Panes and tabs

Split the screen between files and terminals, the way a terminal multiplexer does, and keep tabs of layouts. Everything stays running in the session, so after :detach and greed attach (or over greed ssh) the panes, tabs and terminals are where you left them.

Keys
alt-h alt-j alt-k alt-lMove to the pane or panel (the file tree, an agent) on that side; from a pane at the left or right edge, on to the tab before or after, stopping at the first and last. In a zoomed tab it shows every pane again
alt-nA terminal in a new pane, or a floating one while floating views are shown
alt-fHide the tab’s floating views or show them again; with none, open a floating terminal
alt-FThis file in a new floating view
alt-wLay the tab’s floats out with the next float layout
alt-{ alt-}Bring the floating view before or after to the front
alt-WPick a floating view from the dock
alt-= alt--More room for the pane (or floating view), or less
alt-< alt->, alt-1…The tab before or after (round the ends), or tab N. alt-[ and alt-] do the same in terminals with the kitty keyboard protocol; in others they start an escape code, so they can’t be keys
alt-p, alt-tThe pane and tab modes, for everything else
alt-rThe resize mode: h j k l make the pane, panel or floating view bigger toward that side, H J K L smaller from it (at the screen’s edge, the other edge moves), the arrows move a floating view
ctrl-w v, ctrl-w s, ctrl-w qSplit and close, as in Vim and Helix; ctrl-w H J K L swap with the pane on that side, ctrl-w f the file under the cursor in a split, ctrl-w n s an empty one

The alt keys work everywhere, including inside terminals. Moving to a terminal pane types into it; moving to a file goes back to normal mode. Entering the pane or tab mode shows its most used keys at the bottom, as many as fit; f1 there lists them all. Keys in these modes that open something (a terminal, a new tab, a prompt to name the tab, a picker of layouts) leave the mode, so what they open goes back to where you were. All the keys, and how to change them, are in Keys.

To act on a pane without moving to it first, alt-p g puts a number over each pane; type one to go there, and you’re still in the pane mode, so alt-p g 3 f zooms the third pane and alt-p g 3 x closes it; esc leaves the pane mode. ctrl-w g and space w g label them too. While a pane is zoomed the status line says “zoomed”.

Floating views belong to the tab they opened in: switching tabs hides them and closing the tab closes them. As in zellij, alt-f hides all of the tab’s floats and shows them again where they were (terminals keep running), and opens a floating terminal when there are none. While floats are shown, alt-n opens another floating terminal rather than a pane. alt-F opens the file you’re in as a floating view (:float-file PATH for another), and alt-p e takes the pane or panel you’re in out to float over the tab, or puts a floating view back in as a pane. Floating the tab’s last pane leaves that pane showing another open file. Drag a float by its top border to move it or by an edge to resize it, or use the resize mode (alt-r). How the floats are laid out is up to the tab’s float layout.

To move what a pane shows somewhere else, alt-p b takes it to a new tab, alt-p t to a tab you pick (or a new one), and alt-p P into a panel on the right; its cursor goes with it and a terminal keeps running. They work on a floating view too, and t and b on a panel. :pane-to-new-tab, :pane-to-tab and :pane-to-panel do the same.

To put a new pane somewhere particular, alt-p p shows a frame where it would go, on the right of the pane you’re in. h j k l move the frame to either side of any pane, or along a whole edge of the tab; enter opens a pane there showing this file, t a terminal, and esc leaves things as they were.

Layouts

Each tab has a layout that arranges its panes as they open and close:

  • manual: panes go where you split them. The default.
  • main-stack: one main pane, the rest stacked beside it. A new pane takes the main place and the old main joins the stack.
  • columns: all side by side.
  • spiral: each pane takes half of what’s left, winding inward.

alt-space goes to the next layout and alt-p y picks one; either says which at the bottom, and the status line shows it while it isn’t manual. A tab’s panes keep an order, and that’s what you change: alt-m swaps the pane into main, alt-p o moves them all one place along, and alt-p [ and ] give main less room or more. Resizing a pane by hand switches the tab to manual, keeping the shape it had.

:layout-save NAME saves the tab as it is: its panes, their sizes, and what each shows, a file (relative to the project) or a terminal and what it runs. It goes in the project, in .greed/layouts/, so it can be shared with the rest of the repository; :layout-save-mine NAME keeps it in your config, for any project. :layout-open picks one and opens it as a new tab. A project can open one by itself from its .greed/init.luau:

require("@panes").saved.open("dev", { here = true })

:set pane-layout main-stack sets the layout new tabs start with. A layout is a Luau function from the panes, in order, to how they’re arranged, so you can write your own:

-- every pane in a row of its own, one above another
require("@panes").layouts.define("rows", function(panes)
	return if #panes == 1 then panes[1] else { split = "column", children = panes }
end)

Float layouts

With several floats open (agent sessions, terminals), the tab’s float layout says where they go and which of them show. alt-w goes to the next one:

  • free: they stay where you put them. The default. Coming back to free from another layout puts each float back where it was.
  • one: one at a time, like tabs. Every float takes the same place at the same size and only the one in front shows; alt-} and alt-{ bring the next one or the one before to the front, round the ends, without anything moving. The others keep running out of sight. Moving or resizing the one in front moves them all.
  • tiled: side by side in a grid over the panes, laid out again as floats open and close and as the window changes size.
  • cascade: overlapping, each a step down and right of the one before, so every title shows.

In one the float in front says where it is on its border: ‹ 2/5 › at the right of the top, with the names of the floats either side when it’s wide (‹ nu ~/src · 2/5 · claude ›; of two, the other on its side: ‹ 1/2 · claude ›), and a dot for each float along the bottom, the one in front filled. Click ‹ or a name before it for the float before, › or a name after it for the next, or a dot for that float (:float-go N does the same). A dock along the bottom of the tab lists them all by number and name; click a name to bring that float to the front, or press alt-W and then its number (or move with the arrows and press enter). :float-dock shows or hides the dock in any layout.

A float that needs you gets a colored dot, in its place along the border and in the dock: an agent session waiting for your answer, or a terminal whose command finished while it was hidden (until you look at it).

alt-p O, T and C pick one, tiled and cascade directly, alt-p Y picks from a list (:float-layout-pick), and :set float-layout one sets the layout new tabs start with.

A layout of your own is a function from the tab’s floats to where each one goes, its border included, and whether it shows:

require("@panes").floats.define("left-half", function(a)
	local places = {}
	for i in a.floats do
		local box = { x = a.room.x, y = a.room.y, width = a.room.width // 2, height = a.room.height }
		places[i] = { box = box, hidden = i ~= a.current }
	end
	return places
end)

a.floats are the tab’s floats in the order they opened, a.current the one in front, a.room the room the panes take and a.boxes where each float is now. Plugins that show things in floats use require("@panes").floats.show(buffer), which opens the buffer in a float of the tab (or finds the one it has) and brings it to the front, floats.focus(view) for a float they have, and core.floats.attention(buffer, true) to give it the dot.

Clipboard

What you copy goes on your system clipboard too, through your terminal (OSC 52), so it works over greed ssh and in tmux with set-clipboard on. Most terminals allow it; some ask first or need it turned on (iTerm2: “Applications in terminal may access clipboard”). GREED_CLIPBOARD=off keeps copies inside Greed. A program in a Greed terminal that copies the same way puts the text on Greed’s clipboard, so p pastes it.

Mouse

A click puts the cursor where you click and focuses that pane, dragging selects, and the wheel scrolls whatever’s under it. A double click selects the word, a triple click the line, shift-click extends the selection to where you click and alt-click adds a cursor there. Clicking a tab in the tab line shows it. Drag a floating view by its top border (where the title is) to move it, or by another edge or a corner to resize it. Drag the line between two panes, or beside a panel, to give one more room.

Clicking a button (a proposal’s accept and reject, a todo’s box) presses it. So does enter in normal mode with the cursor on it; where several buttons share a spot, enter lists them to pick from.

In a terminal, a program that asks for the mouse (vim, htop, less with --mouse) gets it. Otherwise the wheel scrolls back through the output, which stops typing into the program, and scrolling back down to the bottom types into it again; in a full screen program the wheel sends arrow keys. A click types into the program, and a drag selects output to copy with y.

Most terminals still select text their own way while you hold shift. GREED_MOUSE=off leaves the mouse to your terminal.

Images

Greed shows images in terminals that speak the kitty graphics protocol: kitty, Ghostty, WezTerm and Konsole. It finds out from the variables those terminals set; GREED_IMAGES=kitty turns images on in another that speaks the protocol, and GREED_IMAGES=none turns them off. Elsewhere, the text under an image shows instead.

Programs in Greed’s terminals can show images too, with the same kitty protocol or iTerm2’s: kitten icat picture.png, imgcat, chafa’s and yazi’s previews. They scroll with the output, and text written over them takes their place, as in any terminal.

Bigger text, like a heading a plugin scales up, is drawn big in the window and in kitty, which can size text. Other terminals show it at the normal size on the rows it takes. GREED_TEXT_SIZING=on or off decides it for another terminal that can size text, or turns it off in kitty.

Image files

Opening a PNG, JPEG or GIF shows the image, as wide as the pane at most, under a line with its name, its size in pixels and the zoom. + and - draw it bigger and smaller, = fits it to the pane again and 0 draws it at its own size. It shows in the window and in terminals that show images.

PDFs

Opening a PDF shows its first page, as wide as the pane. ] and [ go to the next and previous page (with a count, that many), + and - draw it bigger and smaller, and = as wide as the pane again. t switches to the text of every page, which reads in any terminal and searches with /; t again shows the page the cursor is on. Other files that aren’t text open read-only, so saving can’t change them.

The command line

: (or alt-x in the vscode style) runs any command by name, with completion. space ? lists every command, space h c describes one and space h k tells you what a key does. space h h describes anything the editor is made of (commands, options with their values, events and who listens to them, the plugin API, plugins, modes and editing styles), and enter in the help window goes to where it’s defined. space h i inspects what’s under the cursor (a buffer, a shell block, a terminal, a file), or with nothing there the editor itself: its state, the objects it relates to (enter goes to one, backspace comes back) and what it can do (enter runs that). After a key like space, a hint at the bottom right shows what can follow, with groups of keys named (w panes…), and f1 shows the keys of the mode you’re in, in every mode. A message on the status line goes away with the next key.

The short names Vim and Helix users type work too: :w :wa :q :qa :wq :wqa :x :e :b :bn :bp :bc :ls :sp :vs :only :tabnew :noh :rl :rla :set :toggle, :colo (:colorscheme) for :theme, :setf for :language, Neovim’s :checkhealth for :health, and Helix’s long ones like :write-all and :buffer-close. Arguments go after a space (:w notes.md); only Vim’s line commands and :b take one right after the name (:m0, :t$, :b#). A command that refuses, like :q with unsaved changes, says why in plain words; an error from a bug in a plugin also names the plugin, and :errors has the details.

Vim’s line commands work in every style. A range in front picks the lines a command works on; without one, it’s the lines the selection touches (:g and :v take the whole file).

Command
:42, :$Go to line 42, or the last line
:s/regex/text/flagsReplace on the lines: g every match on a line, i ignore case, I match case, n only count; \1 and & put back what matched. Undo puts the cursor back on the first line changed
:g/regex/command, :v/regex/commandRun a command on each line that matches, or doesn’t; with no command, select those lines
:d :yDelete or copy the lines
:m 0 :t $Move or copy the lines below a line (0 is the top)
:norm keysType the keys in normal mode on each line
:j :> :<Join, indent or dedent the lines
:sortSort the lines (the whole file without a range): :sort! backwards, and u one of each, i ignoring case, n by the first number
:%!sortPut the lines through a program, as the pipe key does with a selection
:!cargo testWithout a range, run it in this project’s shell, where its output stays
:e!Load the file again from disk (an undo takes it back)

Ranges are line numbers, . (the cursor’s line), $ (the last), /regex/ (the next line matching), '< and '> (the selection’s first and last line) and % (every line), with +n or -n after any of them: :%s/old/new/g, :.,+3d, :/^fn/,$norm A;. Patterns are regexes, as in search. In Vim’s style, : in visual mode starts with '<,'> and 3 : with .,.+2.

While you type :s, the lines on screen show what it will do: the pattern’s matches, then, once you start the replacement, each match struck through with its replacement next to it. Nothing changes until you press enter, and esc leaves the file as it was. :set substitute-preview false turns it off.

Configuration

Your config is ~/.config/greed/init.luau, plain Luau with the full plugin API. :config-open (or :config) opens it, making the folder if it isn’t there yet. Your own plugins go in ~/.config/greed/plugins/NAME/. A plugin that appears there, or whose files change, loads by itself, asking you first if it wants something you haven’t approved; :plugin-reload NAME loads one again by hand. Writing plugins has more.

Where Greed keeps things, and profiles

Greed keeps four folders: your config (~/.config/greed), data like saved agent sessions (~/.local/share/greed), a cache (~/.cache/greed) and state like the plugins you approved and kept secrets (~/.local/state/greed). The XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME and XDG_STATE_HOME variables move them as usual.

A profile keeps everything in one folder of your choosing, with sessions of its own that never meet your usual ones:

greed --profile ~/greed-work            # a setup of its own
greed --profile "$(mktemp -d)"           # Greed as on its first run

The profile holds config, data, cache, state and run (its sessions’ sockets). Each folder can also be set on its own with --config-dir, --data-dir, --cache-dir and --state-dir, for scripts that move only one. Each flag has a variable too (GREED_PROFILE, GREED_CONFIG_DIR, GREED_DATA_DIR, GREED_CACHE_DIR, GREED_STATE_DIR): a folder’s own setting wins over the profile, which wins over the XDG variables. Sessions’ sockets go in GREED_RUN_DIR when it’s set, which helps when a profile’s path is too long for a socket (they’re limited to about a hundred characters). Sessions, terminals, shells and agents Greed starts get the same folders, so greed ctl inside one of its terminals finds its session. HOME stays yours, so git, kubectl and your keys work as usual.

Saving your config, in Greed or anywhere else, runs it again within a second: what it registered (commands, keys, theme entries, event handlers) is taken away and set up anew, and the status line says “config reloaded” or what went wrong. Options it set keep their values until it sets them again. :config-reload does the same by hand.

For luau-lsp to check your config and complete it, it has to know what require("@greed"), require("@theme") and the other plugins are. Greed writes a .luaurc that tells it, beside the config folder (~/.config/.luaurc, since luau-lsp looks for an init.luau’s settings in the folder above), and keeps it pointing at the Greed you run. It only writes over a .luaurc it wrote itself: if you had one there or change it, it’s yours, and :config-types says so.

Config runs in layers, each after the one before, so later ones win:

  1. The default plugins.
  2. The config greed ssh brought from your machine, in a session on another one.
  3. This machine’s own config, ~/.config/greed.
  4. The project’s own config, .greed/init.luau in the project, once you trust it.

A project’s config is code from whoever wrote the repository, so it doesn’t run until you say so: opening a project that has one tells you, and :trust runs it, now and whenever the project opens again. Trusted projects are listed in ~/.cache/greed/trusted-projects; take a line out to stop trusting one.

local greed = require("@greed")

-- Keys
greed.keymap.bind({ layer = "leader" }, { W = "save-all" })

-- Options (`:set` changes them while running)
greed.option.set("flash-duration", 150)

-- The theme: change a few names, or all of them
greed.theme.set({
	["ui.selection"] = { bg = "#3b3f51" },
	["keyword"] = { fg = "magenta", bold = true },
})

-- A language
require("@languages").set("python", { servers = { { "pylsp" } } })

-- Turn off a default plugin
greed.plugin.unload("hints")

Keys covers key names, modes and taking keys away.

The status line

The status line shows the mode, the tab, the tab’s layout when it isn’t manual and zoomed while one pane fills it, the file and whether a macro is recording on the left. The file is the one you’re in: a focused panel or floating view (a floating terminal) counts, a picker or the command line doesn’t. Buffers without a file show by name, like terminal: nu ~/src, or untitled for a new file. While you type a command after : the mode says COMMAND, and in a prompt like search’s PROMPT. The file gets [+] while it has unsaved changes; undoing back to what was saved takes it away. On the right it shows:

  • the branch and the lines added, changed and removed in the file, like main +3 ~1 -2 (with jj the nearest bookmark and the change you’re on, like main · nwpr)
  • todos due today, like 2 due today (see Reminders)
  • problems
  • the language and the features on for the file, like markdown · notes
  • the language server: its name, what it’s working on (rust-analyzer: indexing 40%), or that it’s starting, stopped or not installed
  • the editing style, dim, like helix; click it to pick another
  • anything unusual about the file, and only then: CRLF line breaks, tabs (or spaces) where its language indents the other way, or read-only
  • how many selections there are, once there’s more than one
  • agents at work or waiting for you, like ◆ 1 working · 1 waiting for you
  • the cursor’s line and column (the character the cursor is drawn on, at the end of a selection too), the machine and the session

Two options make it livelier, both off unless you turn them on:

local option = require("@greed").option
-- The mode, tab, machine and session as pills in their theme colors, with
-- rounded ends (they need a Nerd Font; statusline-caps "none" makes them square)
option.set("statusline-pills", true)
-- A wave of light through the letters of the tab or the file when you go
-- to another, a glow from the middle of the mode when it changes, and in
-- an agent's pane, spinning tools and a status that moves while it works
option.set("effects", true)

The wave’s letters glow towards the theme’s ui.glow color: brighter text in a dark theme, the accent in a light one. Effects need a theme with #rrggbb colors, so the default 16-color theme has none.

Add pieces of your own, or take some away:

local statusline = require("@statusline")
statusline.add("right", "clock", function()
	return os.date("%H:%M")
end)
-- A pill in a theme style
statusline.add("left", "env", function()
	return { text = "staging", style = "ui.statusline.tab" }
end)
statusline.remove("host")

A piece gets the main view, its buffer, the mode and the message, and returns text, a pill ({ text, style }), a list of pills shown joined, or nil to show nothing. The pills’ colors are the theme’s ui.statusline.mode, .tab, .tab.number, .host and .session; .dim is the editing style’s.

A piece is drawn with every frame, so it has to be quick and can’t run programs. For anything that takes time, like a repository’s state in a big monorepo, add an async piece: its refresh runs in the background and can run programs, and the status line shows what it last found, never waiting for it.

statusline.add_async("right", "change", {
	refresh = function()
		local done = require("@greed").process.run({
			"jj", "log", "-r", "@", "--no-graph", "-T", "change_id.short()",
		})
		return done and done.code == 0 and done.stdout or nil
	end,
	every = 5000, -- milliseconds
	on = { "buffer.saved" }, -- and whenever these happen
})

Themes

:theme lists the themes and tries each one as you move through the list. enter keeps it, esc goes back to the one you had, and :theme nord switches straight away. Tab through the matches after :theme tries each one the same way. The theme you pick is remembered for next time, unless your init.luau sets one with theme.use, which runs later and wins.

Greed comes with default (your terminal’s 16 colors, so it follows your terminal’s palette), nord, nord-light, dracula, catppuccin-mocha, catppuccin-macchiato, catppuccin-frappe, catppuccin-latte, gruvbox, gruvbox-light, onedark, tokyonight, tokyonight-storm, solarized-dark, solarized-light, rose-pine, rose-pine-dawn, kanagawa, everforest and monokai.

Themes are Luau. Set one, or make your own, in init.luau:

local theme = require("@theme")
theme.use("nord")

-- A built-in theme with changes
theme.define("my-nord", {
	inherits = "nord",
	styles = { comment = { fg = "#7b88a1", italic = false } },
})

-- A theme from a palette: backgrounds, text and hues, and optionally
-- fg_alt, accent, and colors for syntax: keyword, func, string, type,
-- constant and operator
theme.define("mine", {
	palette = {
		bg = "#101418", bg_alt = "#181e24", bg_high = "#232b33", selection = "#2c3640",
		fg = "#d0d6dc", comment = "#5c6773", gutter = "#3a444e",
		red = "#e06c75", orange = "#d19a66", yellow = "#e5c07b", green = "#98c379",
		cyan = "#56b6c2", blue = "#61afef", purple = "#c678dd",
		keyword = "#c678dd",
	},
})

greed.theme.set changes single names on top of whichever theme is in use, and those changes stay when you switch themes. Each changes only the fields it gives: greed.theme.set({ comment = { italic = false } }) keeps the theme’s comment color. A theme’s own styles work the same way on what it inherits. Names fall back to their parent, so keyword.control uses keyword unless it has its own entry; Highlighting lists them.

With several terminals attached to a session, each shows the others’ cursors and selections in their own color: client color N (1 to 6) uses ui.cursor.peer.N and ui.selection.peer.N, which fall back to ui.cursor and ui.selection. The built-in themes give each a color of their palette, with selections underlined.

One theme serves the terminal and the window. Where they should differ, an entry’s tui and gui parts change it for one of them, like ["ui.float"] = { bg = "black", gui = { bg = "#25282c", rounded = true } }. rounded, shadow, badge, checkbox and font are for the window only: the first two make floats boxes with rounded corners and a soft shadow, badge draws a style’s background as a small box with rounded corners, like a key cap (motion hints and buttons use it), checkbox = "empty" or "checked" draws a checkbox over the text (todo boxes use it), "choice" or "chosen" a round button (Markdown choices use it), and font = "prose" sets text in the proportional font.

#rrggbb colors go out as they are when the terminal says it shows 24-bit color (COLORTERM=truecolor, as most do), and as the nearest of 256 colors when it doesn’t, like tmux without its RGB feature. GREED_COLORS=truecolor or GREED_COLORS=256 decides for a terminal that says the wrong thing.

Highlighting and syntax queries

Greed has grammars built in for Rust, Nix, Bash, Python, Go, JavaScript, TypeScript, C, Luau, JSON, TOML, YAML and Markdown. For C++, C#, Java, Ruby, Scala, Haskell, OCaml, Julia, HTML and CSS it downloads the grammar the first time you open a file in that language, checks it against a checksum Greed knows, and keeps it in ~/.cache/greed/grammars. :health shows which languages are built in, which download, and which have no grammar. :grammar-fetch ruby downloads one ahead of time, say before going offline, and greed.option.set("grammar-download", false) turns downloading off.

What gets highlighted, and what counts as a function for m i f, comes from tree-sitter queries: files named queries/LANGUAGE/KIND.scm, where KIND is highlights, injections (languages inside others, like the code blocks in Markdown, which are highlighted in their own language), textobjects, folds (the blocks z a folds, captured as @fold) or indents (nodes whose lines go a level in, @indent, and closing tokens that line up with their block’s first line, @outdent). Greed reads them from every plugin and from ~/.config/greed/queries, so a file there replaces Greed’s own. To add to it instead, start the file with ; extends:

; extends
; ~/.config/greed/queries/rust/highlights.scm: TODO in comments stands out
((line_comment) @comment.todo (#match? @comment.todo "TODO"))

Highlighting lists the names highlights queries use and the theme styles.

A query with a mistake in it says so when Greed starts, and the one before it stays in use. require("@languages").load_queries() reads the files again after you change them.

Several clients on one session

Any number of terminals and windows can show one session at once: run greed or greed attach in a second terminal, greed --gui for a window, or greed ssh HOST from another machine. Each is a client of the session. They edit the same buffers, with one undo history per buffer, and each has its own cursors.

greed notes.md            # in one terminal: starts the session
greed attach              # in another: shows the same session
greed ssh desk notes.md   # from a laptop: the session on desk

What each client has

Each client keeps these to itself:

  • its cursors and selections, and where each view is scrolled;
  • the view it has focused, and its mode and half-typed keys;
  • pane zoom (:pane-zoom), even while it follows the others;
  • pickers, prompts, the command line, completion and hover: they show only on the client that opened them, and only its keys reach them;
  • ., which repeats that client’s own last change, and macro recording;
  • the font size of a window: ctrl-= and ctrl-- zoom the window you type them in, and setting gui-font-size sets it for every window again;
  • copies, which go to the system clipboard of the client that copied.

Buffers, their undo history, the clipboard history, projects and options are shared. Desktop notifications go to the client you used last.

Following, or laid out on its own

A client that attaches follows the others: it shows the same project, the same tab and the same panes, with its own focus and zoom. When one opens a tab, switches project or splits a pane, the others show that too.

Another client never moves you while what you’re in still shows: you can go on typing in a file or a terminal while someone splits a pane or opens a panel. When what you had focused goes out of sight (another tab or project is shown, or the pane closes), you move to the shown tab’s active pane, what you had typed halfway is dropped, and you’re back in normal mode there, so keys meant for a terminal never land in a file.

:client-independent gives the client you type it in a layout of its own. It starts from a copy of what it showed (the same tabs, panes, panels and the tab’s floating terminals, as new views of the same buffers, scrolled and selected the same) and from then on its tabs, panes and project change apart from the others’. :client-follow has it follow again: the panes it had on its own close, while their buffers stay open, and you go on in the file you were in, in the pane the others show it in. Both say what they did at the bottom.

Floats keep their place and size in proportion on screens of different sizes: a float in the middle of a big window is in the middle of a small terminal too. A tab’s floats, like floating terminals, are shown to every client showing that tab.

Seeing each other

Each client’s cursors and selections show for the others in a color of its own, one of six: client color N uses ui.cursor.peer.N and ui.selection.peer.N, which fall back to ui.cursor and ui.selection. The built-in themes give each a color of their palette. A client laid out on its own shows its cursors where it works, in a view of the same buffer.

Terminals

A program in a terminal pane has one screen size. It’s the size of the client that last typed into the terminal, pasted into it or focused it, and a terminal window resizing changes it only for that client, so two clients never fight over it. The others see the program’s screen as it is: a terminal crops it to the rows around the program’s cursor (or its bottom rows), and where it’s smaller than the pane, shows a note like 80×24 for tui@laptop in the room left over; a window scales it to fit. Typing into it from another client sizes it for that one.

Image files and PDFs fit the width of the client that opened them or last focused them, in the same way.

Leaving and coming back

:detach (or closing the terminal) lets one client go and leaves the others attached. A client is known by its kind and machine, like tui@laptop or gui@desk, with a number when there are two of a kind on one machine (tui@laptop#2). One coming back by the same name gets its cursors, focus, mode and layout back: it takes the first name no attached client has, so the second terminal on a laptop to come back gets what tui@laptop#2 had. A new one follows the others and starts where the last-used client is.

:quit-session ends the session for every client, whatever runs in it. From a shell, greed ctl quit NAME ends it unless files have unsaved changes (--force ends it anyway); it works on a session of another Greed version too, as after an upgrade.

For plugins

Code always acts for one client: the one whose key, click or menu ran a command, or the one an event is about. ctx.client and greed.client.id() say which, greed.client.list() lists them, and greed.client.set_layout("independent" | "follow", id?) is what the two commands use. State a plugin keeps for what a client has open, like a picker, belongs in a table keyed by greed.client.id(). A float is shown only to the client it opened for unless its spec says shared = true; sizes and places like "37.5%" keep it in proportion on every client. greed.option.set(name, value, { client = true }) gives the acting client a value of its own, and buffer:set_screen_size lays a buffer out at one size in every view, as terminals do. Writing plugins has more.

A client that attaches over the session socket can ask to be laid out on its own with "layout": "independent" in its attach request.

The shell

:shell opens a shell in a buffer. You type a command at the prompt at the bottom, enter runs it, and it becomes a block above the prompt: the command, what it printed, and how it ended. Blocks stay, so you can search them, fold them, copy from them, run them again and send their output to the next command. It’s meant for the commands you run while editing, and for output you want to keep working with; a terminal pane is still the place for a long interactive session.

❯ 1 cargo test -q                         ✗ exit 101 · 4.1s
error[E0308]: mismatched types
 --> src/main.rs:12:5
❯ 2 git status --short                    ✓ 0.02s
 M src/main.rs
⎇ main ~/s/app ❯ |

The prompt shows the shell’s folder, every folder but the last cut to its first letter, and before it what the prompt knows, like the git branch. space o s switches to the shell of this project, or opens one; with several, or from the only one, it lists them with a new one to open. :shell NAME opens another. Each shell has its own folder and variables, and a project can have several. Shells and their blocks last until you close them or the session ends, and what you type in one is nothing to save: :q doesn’t ask about it. A new shell says to type help, which lists what the shell runs itself, what a line can hold, which shell runs the rest, and the keys in the style you use; help TOPIC shows one part.

The prompt

Before the folder, the prompt shows the branch of the repository the shell is in (with jj, the nearest bookmark and the change), and the kubernetes context and namespace kubectl would use. The context comes from kubectl’s config file, the ones KUBECONFIG names or ~/.kube/config, read without running kubectl, so it’s there at once and costs nothing. They’re worked out again after every command, so a cd, a checkout or kubectl config use-context shows on the next prompt.

⎇ main ⎈ prod-eu:payments ~/s/infra ❯ kubectl get pods

In a project with a devenv environment, ❄ devenv shows too (see Project environments).

A context matching a pattern in shell-production-contexts (prod by default; several go with spaces between them, as Luau patterns, like :set shell-production-contexts "prod ^live%-") shows in red, and so does the ❯, so a command about to run against production looks like it. Plugins add pieces of their own, like a cloud profile; help prompt lists them.

Each piece has a color of its own from the theme (shell.segment.git, shell.segment.kubernetes, shell.segment.environment). With a Nerd Font, :set shell-icons nerd uses its icons in place of ⎇ ⎈ ❄.

If you use starship, :set shell-prompt starship shows your starship prompt instead, as your other shells do, with its colors: Greed runs starship prompt after each command with the shell’s folder, variables, the last exit code and how long it took. The ❯ stays Greed’s own, red in production, so starship’s prompt character is left out. Put it in your config to keep it:

greed.option.set("shell-prompt", "starship")

Blocks

A block’s command is the line it starts on, with ❯ before it (◆ for one an agent ran) and its number, the 3 of $3. After the command comes how it went: ◐ while it runs with how long so far, then ✓, ✗ exit 1, ■ INT when it was stopped, ↗ when it was handed to a terminal, or ? for a question to a model that brought no command back, how long it took, where cd went and which variables it set. A block that went quiet while it could be reading a line says it may be waiting for input.

Output that’s JSON or a table is kept as a value on the block too, and the status says so (JSON · 3 items, table · 12 rows); see Filtering and tables. Places in files the output names, like src/main.rs:12:5, are links: ] e and [ e go to them, and enter on one opens the file there, taking relative paths from the folder the block ran in.

A block shows at most shell-max-lines (5000) lines of its output, the first half and the latest half, saying how many it leaves out between them; space v opens all of it in a buffer of its own. Past shell-max-record (8 MB) the whole output goes to a file in the cache folder, which space v opens.

A prompt of more than shell-confirm-lines (5) lines, as pasting a transcript or a file can leave there, asks y/n before it runs as one command.

Undo at the prompt takes back what you typed since the last command ran, while output keeps streaming in above it; it never reaches back into a command that ran.

Keys

In Helix and Vim you type at the prompt in insert mode. enter on a block, the keys for moving between blocks and files, g o and the space keys work in normal mode; up, down, alt-enter and tab work while typing.

Keys
enterAt the prompt, run what’s there. On a running block’s line above the prompt, open the block in a float. On a block, put its command at the prompt to change; on a file and line it names, open it there; on a row that’s a thing (a file, a pod), do its usual action
alt-enterAnother line at the prompt
up downAt the prompt: the commands typed before that start with what’s typed
ctrl-rPick a command typed before, in any shell
right end ctrl-eAt the end of the prompt, take the rest of the command it suggests
tabComplete a program, its arguments and flags, a file or folder, a variable, $3, buf: or @sel: the only match or what they all start with at once, else a menu, which tab again goes through
ctrl-cInterrupt the running block
ctrl-tType into the running block’s program, in a terminal in the shell’s place
[ p ] pThe block before or after the cursor
g oSelect the block’s output
] e [ eThe next or previous file and line the output names
space r space RRun the block again as it ran, or in the shell’s folder now
space y space YCopy the block’s output, or its command
space xStop the running block for good
space vOpen the block’s whole output in a buffer
space cTake the finished blocks away
/On a block’s output, keep the lines (or a table’s rows) with what you type; elsewhere, search
space s space SSort a table by the column under the cursor; show a block as it came
tab shift-tabOn a block, fold it under its command or unfold it; fold or unfold every block (see Folding blocks)

In the vscode style you’re always typing, so the keys that need normal mode have others: ctrl-c copies when something is selected and interrupts otherwise, ctrl-up and ctrl-down go between blocks, f8 and shift-f8 between the files and lines the output names, ctrl-f filters a block, ctrl-k ctrl-l folds or unfolds a block (ctrl-k ctrl-0 folds them all, ctrl-k ctrl-j unfolds them all), and the space keys above are ctrl-space ones. space . (alt-. in the vscode style) on a block, or on a row that’s a thing (see Rows you can act on), lists what you can do with it.

Folding blocks

A folded block shows only its command line, with how it ended and how many lines it hides (⋯ 240 lines), so a long session stays easy to look through. tab on a block folds or unfolds it, and shift-tab folds or unfolds them all. A block that’s still printing stays folded as its output grows.

Resting the mouse on a folded block shows it in a float without moving the cursor: scroll it there, or click into it to filter it with / and act on its rows. Moving the mouse away closes it.

Blocks can fold by themselves. Blocks that failed never do, since those are the ones to read again:

local greed = require("@greed")
-- Fold a block that printed more than 40 lines as it ends.
greed.option.set("shell-fold-lines", 40)
-- Running a command folds the finished blocks above it.
greed.option.set("shell-fold-older", true)

What runs your commands

The shell runs a few commands itself:

Command
cd [DIR]Change the shell’s folder: a path, - for the one before, nothing for home
export NAME=VALUE ...Set variables for the commands after
unset NAME ...Leave variables out of the commands after
env [reload]List the variables this shell sets or leaves out, and the environment its folder brings (devenv, direnv); reload loads that again
historyThe commands typed before
clearTake the finished blocks away
open PATH ...Open files, or buf:NAME buffers, in the editor
help [TOPIC]What works here, and the keys in the style you use; a topic for one part
which NAME ...What a word at the start of a line runs: a builtin, a table stage, one of your aliases or a program
stop [N ... | all]Stop running blocks by their number, or all of them; without one, list them
watch [-n SECONDS] COMMANDRun a command again and again, every two seconds, its block showing what it printed last with the changes marked

What open opens comes up in your style’s own mode, to read and move around in, not typing as at the prompt. Plugins add commands of their own (Writing plugins), and help builtins lists them with these.

Every other line goes to your shell, so ls and ls | head behave the same: your $SHELL when it’s a POSIX shell (sh, bash, zsh, dash, ksh and the like), fish or nu, and sh otherwise; :set shell-fallback bash picks one. cd and export there carry over to the commands after, as they would in a terminal. If shell-fallback names a shell of another kind, the line still runs in it, and its block says cd and export in it don’t carry over. Your shell’s startup files aren’t read for each line, so its functions aren’t there unless you :set shell-fallback-rc true.

Your aliases are. The shell asks your shell for them once, with its startup files, and a line starting with one runs what it stands for, as in a terminal; the block shows what you typed. The shell’s own commands win over an alias of the same name, ^NAME runs the program NAME whatever else has that name (^env, or $3 | ^sort for the sort program rather than the table stage), and which NAME says what runs. :set shell-aliases false leaves aliases alone.

As in bash and zsh, !! in a line is the command before, !$ its last word, and a line ^old^new runs the command before with old changed to new. The block shows the command as it ran.

Programs get PAGER=cat and GIT_PAGER=cat, so their output comes back as text, GREED_BIN, the greed program running the editor, GREED_SESSION and GREED_BLOCK, the block they run in.

Editing files from a command

A program that opens an editor, like git commit, jj describe, kubectl edit or crontab -e, opens the file in Greed, in the pane the command ran from. :wq (or :q once it’s saved) hands it back and the shell shows there again; :cq stops the program with an error, so git commit gives up. EDITOR, VISUAL, GIT_EDITOR, JJ_EDITOR and KUBE_EDITOR are greed edit --wait, so this wins over an editor set in git’s or jj’s config. An agent’s commands keep your own editor, and :set shell-editor false turns it off; export EDITOR=vim in a shell picks another for that shell.

greed edit FILE... works from any terminal too: it opens the files in the running editor, and with --wait returns once each is done with.

Project environments

In a folder with a devenv.nix, the shell loads devenv’s environment, as devenv shell would, and its commands run with it: the project’s tools on the PATH, its variables, and what enterShell sets up. No direnv is needed, and devenv’s own rule applies: a project loads once you’ve run devenv allow there. The prompt says it’s there, ❄ devenv, with … while it loads (a command typed meanwhile waits for it, and ctrl-c runs it without), or that it failed, which env says more about.

Changing devenv.nix, devenv.yaml, devenv.lock or anything else devenv read loads it again; env reload does too. Leaving the project leaves its environment behind, and what you export wins over it. Every shell in a project shares one load, and an agent’s commands get it too.

A folder with an .envrc and no devenv.nix loads it through direnv, when that’s installed and you’ve run direnv allow there, the prompt saying ❄ direnv. Changing the .envrc loads it again; for other files it reads, env reload does. A project with both, like an .envrc holding use devenv, gets devenv’s straight away.

shell-environments (devenv direnv) says which kinds load and which is asked first; :set shell-environments "" turns them off. Plugins add other kinds.

Terminals

Commands you type run in a pseudo terminal of their own, so they see a terminal: they print their colors and progress bars, and their output and errors come in order. A line longer than the screen stays one line in the buffer, so each client shows it wrapped at its own width.

A program that wants the whole terminal (an editor, a pager, htop, ssh, sudo asking for a password) takes over the shell’s pane as a terminal, and the shell comes back when it’s done with the screen or exits, its block marked ↗. ctrl-t does the same for any running block, to answer a question it’s waiting on. Start a line with ! to run it in a terminal from the start.

Data from the editor

A line can start with something from the editor, piped into the command:

$3 | sortWhat block 3 printed
buf:NAME | wc -lThe text of a buffer: one > buf: made, an open file at that path, or an open file of that name
@sel | jq .The selection in the file you’re in, each range on a line of its own
@file | headThe whole file you’re in, unsaved changes included

And end with where its output goes:

... > buf:NAMEInto a buffer, made if there’s none, emptied first
... >> buf:NAMEAdded to the end of a buffer

Errors still show in the block. One of these alone passes it on: $3 shows block 3’s output again, $3 > buf:keep keeps it in a buffer, and open buf:keep shows that buffer. Such lines run through a pipe rather than a terminal, so a > buf: gets only what the program printed, and can’t be combined with !.

After a command’s first word, $3 is a file holding what block 3 printed, for programs that read files rather than their input:

❯ diff $3 $5
❯ kubectl apply -f $7
❯ open $3

The file ends .json when the block printed JSON. Single quotes keep $3 as it is (awk '{print $3}'), and so does \$3.

Filtering and tables

Most output is text, and stays text. On top of it the shell keeps what it can read out of it, and every layer below works on what’s under it.

Filtering a block

/ on a block’s output keeps the lines with what you type in them, as you type it; several words keep the lines with all of them, in any case (shell-filter-search-case changes that), and a filter that starts with / is a regex (/^error|warn). enter keeps the filter; esc, while typing it or after in normal mode on the block, shows every line again, and space S also undoes a sort. The block shows the lines kept and the status says so (2 of 340 lines · / error), while its record keeps them all, so $3, space y and space v give the whole output. On a block’s command or at the prompt, / searches as usual.

Tables

Output in columns under a header, as kubectl get, docker ps, ps aux and df print it, is also kept as a table on the block, and the status says so (table · 12 rows). So is ls -l’s output. The shell looks for a header in capitals with the columns lined up under it in every row, and when it isn’t sure, the output is text: prose is never taken for a table.

In a table, / keeps rows: a word like STATUS=Running or NAME~api looks in one column, RESTARTS>0 and AGE<1h compare numbers, and other words look in every cell. space s sorts the rows by the column under the cursor, again from the greatest, and a third time as they came.

❯ 3 kubectl get pods                   ✓ 0.41s · 2 of 5 rows · / STATUS=Running api
NAME                  READY   STATUS    RESTARTS        AGE
api-7d9f8b6c5-2xkqz   1/1     Running   0               3d16h
api-7d9f8b6c5-9wlmn   1/1     Running   3 (3d16h ago)   5d2h

A table also goes to the next command through $, where the shell itself runs these on it, each making a new table, as in $3 | where STATUS = Running:

where COL OP VALUEThe rows passing a test: = != ~ !~ < > <= >=
select COL,...Only those columns, in that order
sort COL [-r]Sorted by a column, from the greatest with -r

Column names don’t mind case, and container_id names CONTAINER ID. Numbers are compared as numbers, with sizes (512Mi, 1.5G, 250m of a CPU), percentages and ages (45s, 12m, 3d16h) read as what they mean, so $3 | where AGE < 1h and $3 | sort RESTARTS do what they say; kubectl’s 3 (3d16h ago) restarts count as 3. They chain, and a program after them reads the table as text:

❯ $1 | where STATUS != Running | select NAME,STATUS,AGE
❯ $1 | sort RESTARTS -r | where RESTARTS > 0 | wc -l
❯ $1 | where IMAGE ~ postgres > buf:databases

sort without a column is the sort program, as always.

They work after a program too, in one line: what it prints is read as a table, as a block’s output is, and the stages run on that.

❯ kubectl get pods | where STATUS != Running | select NAME,AGE
❯ docker ps --format json | where Image ~ postgres
❯ $1 | grep api | where RESTARTS > 0

where, select, get, columns, from, lines, filter, map, count, group, first, to and each start the stages after a program, since no program goes by those names; sort, split, uniq, sum and last are programs too, so they’re stages only after one of those, and only when they name a column (a number for last). What the program prints before the stages isn’t shown, only what comes out at the end, and its errors. A stage on output that isn’t a table says so, and what makes one.

More stages

filter EXPRThe rows a Luau expression holds for, with the row’s cells as r and its numbers as n
map COL = EXPRA column made (or changed) by a Luau expression on r and n
countHow many rows there are
first [N]The first N rows, one without N
last NThe last N rows
uniq COLThe first row for each value in a column
sum COLThe numbers in a column added up
group COLEach value in a column and how many rows have it
to json|csv|tsvThe table as JSON, CSV or tab separated values
each COMMANDRun a command for every row, {COL} in it the row’s cell, quoted

filter and map take Luau. In it, r is the row, its cells as text (r.STATUS, r["CONTAINER ID"]), and n the cells that read as numbers, sizes and ages included (n.RESTARTS, n.AGE in seconds). It runs in a sandbox that can read the editor and nothing more, so a line an agent wrote can’t do anything else either.

❯ kubectl get pods | filter n.RESTARTS > 0 and r.STATUS ~= "Running"
❯ kubectl get pods | map hours = n.AGE / 3600 | sort hours -r | first 5
❯ kubectl get pods | group STATUS
❯ kubectl get pods | where STATUS = Evicted | each kubectl delete pod {NAME}
❯ docker ps | to csv > buf:containers.csv

each runs its command once for every row, with {COL} the row’s cell, quoted for your shell; the commands run one after another in your shell and print into the block. What comes after each is all the command, so each echo {NAME} | wc -c counts each name.

Plugins add stages of their own (Writing plugins), and help tables lists them all.

JSON

Output that’s JSON is kept as a value on the block (JSON · 3 items), and an array of objects is a table too, with a column for each key, so / and space s show it lined up in columns, and $3 | where works on it. Many tools print JSON when asked, which is the surest way to a table: kubectl get pods -o json gives every field, where its columns give a few. The shell never runs your command again with other flags; it reads what it printed.

The stages work on an object too: its fields are the columns of one row, so $3 | select name gives its name, and when one of its fields holds an array of objects, like kubectl’s items, the columns it doesn’t have are that array’s, so $3 | where phase = Failed looks through the items. get picks a field:

get PATHA field of JSON, like items or items.0.name (arrays count from 0)

$3 | get items is the items’ table, $3 | get items.0.name the first one’s name, $3 | get metadata.labels an object, printed as JSON, and $3 | get items.name each item’s name.

Saying what it is

When the shell finds no table, say how to read the output, as in $3 | from csv:

columns [-H]Text lined up in columns, under a header or with -H none
from csv|tsv|jsonCSV, tab separated values, or JSON or JSON lines
split SEPEach line cut at SEP into columns 1, 2, …
linesA row for each line, in a column line

columns -H names the columns 1, 2, …, as split does, and from json reads JSON lines too, an object on each line, as many logs are.

They make tables like any other, for where, select and sort: $3 | split : | where 7 ~ zsh | select 1. A buffer or the selection works too: buf:data.csv | from csv | where total > 100. A program in a terminal prints tabs as spaces, so tab separated output reads best from a file, with buf:.

Parsers from plugins

A plugin can read its commands’ output as tables, ahead of the shell’s own look (Writing plugins has how). And when jc is installed, output the shell found no table in, from a command jc knows (ifconfig, dig, mount, uptime and many more), gets jc’s reading of it.

Watching a command

watch COMMAND runs a command again and again, every two seconds (-n 5 for every five), and its block shows what it printed last in place of what it printed before, with the lines that changed since the time before marked. It’s the shell’s own, so it stays a block among the others rather than taking the screen; ^watch runs the watch program in a terminal.

❯ 7 watch kubectl get pods       ◐ 1m12s · every 2s · 1 changed · 14:05:02
NAME                     READY   STATUS             RESTARTS   AGE
api-7d9f8b6c5-2xkqz      1/1     Running            0          3d16h
worker-5c6b7d8f9-abcde   0/1     CrashLoopBackOff   13         2h

The table and its rows follow along: a / filter or a space s sort stays on what each run shows, space . on a pod acts on it as it is now, and $7 | where STATUS != Running reads what it shows. The whole line is watched, pipes and table stages included, so watch kubectl get pods | where RESTARTS > 0 shows only the pods that restarted, as they are now. ctrl-c or space x stops it, keeping what it showed last; it also stops by itself after shell-watch-minutes (60), and space r starts it again. Agents can’t watch, as a watch runs until someone stops it.

kubectl get pods -w needs no watch: kubectl prints a pod again each time it changes, and the block shows each pod once, in its place, lined up under the header, the ones that changed since they first showed marked, rather than a longer and longer list. Its rows are pods to act on, as any kubectl get block’s are. It ends when kubectl does, or with ctrl-c.

Running blocks above the prompt

A watch, or anything else still running, scrolls up out of sight as you run more commands. So every running block but the last gets a line just above the prompt, saying what it shows now (how many rows, for a table) and what changed in it last, or how long it has run:

❯ 9 git status --short                    ✓ 0.02s
 M src/main.rs

◐ 7 watch kubectl get pods  · 3 pods · api-7d9: STATUS Running → Error · 14:05
◐ 8 cargo build --release   · 1m 12s
~/s/infra ❯

What changed names the row: new worker-abc, worker-abc gone, or the first column that changed, with how many more rows did. Columns that change by themselves, like AGE or the (5m ago) of RESTARTS, don’t count, so a quiet watch stays quiet. To hear about changes while you’re in another window, space . on the block (or its line) and “notify on changes” (shell-notify): each change then sends a desktop notification saying the same, while Greed isn’t focused. Again to stop.

Such a line stands for its block, a line or two up from the prompt: ctrl-c or space x there stops that block rather than the latest, space . lists what you can do with it, and space r runs it again. enter (or a click on its number) opens the block in a float, and / opens it there to filter. In the float it’s the block itself: / filters it, enter or space . on a row acts on the pod or file it is, and ctrl-c stops it. A watch’s float follows it as it refreshes, and grows and shrinks with it. esc closes it, keeping the filter, which a watch keeps on each run. space . on any block offers “open in a float” too. A line goes when its block ends.

At the prompt, ctrl-c stops the latest running block and says which others still run. stop lists them by number, stop 7 stops block 7, and stop all stops them all.

From anywhere, even a file you’re editing, space o w lists what runs in every shell, this project’s first, with each one’s output beside it: enter opens it in a float, ctrl-o goes to it in its shell, and ctrl-x stops it.

Rows you can act on

Some tables’ rows are things: an ls -l row is a file or a folder, a ps row a process, a kubectl get row a pod, deployment or other kubernetes object, a docker ps row a container. The status says so (table · 5 pods). On such a row, space . lists what you can do with it, the same actions that kind of thing has anywhere in Greed, and enter does the usual one: a file opens, a folder becomes the shell’s folder, a pod or another kubernetes object is described, a container shows its logs.

❯ 4 kubectl get pods                      ✓ 0.38s · table · 3 pods
NAME                     READY   STATUS             RESTARTS   AGE
api-7d9f8b6c5-2xkqz      1/1     Running            0          3d16h
worker-5c6b7d8f9-abcde   0/1     CrashLoopBackOff   12         2h      ← space .
                                   describe · logs · follow logs · shell
                                   yaml · json · events · delete

What an action does is a command, run as a block of its own, so you see it, can run it again and change it:

❯ 5 kubectl logs worker-5c6b7d8f9-abcde --tail 200 --context prod-eu -n payments

A row remembers where it came from. The kubernetes context and namespace are the ones the command ran against, from its flags or else kubectl’s config as it was when the block finished, so acting on a row of an old block after kubectl config use-context still goes to that row’s cluster, and the command says which. With -A, each row’s namespace is its own. docker --context is kept the same way.

Actions that delete or stop something (delete, restart, killing a process, stopping a container) ask first, and say so when the row is in a context matching shell-production-contexts:

delete worker-5c6b7d8f9-abcde - in PRODUCTION (prod-eu)? y/n

shell on a pod or a container opens a shell in it in a terminal.

What the blocks above showed completes the next command. After kubectl get pods, kubectl logs <tab> offers those pods first, each saying which block it’s from (pod · $4), then whatever kubectl itself offers; so do exec, describe pod, delete deploy and rollout restart deployment for theirs. Only rows from where the command goes are offered: the context and namespace its flags name, or else kubectl’s config now. docker logs <tab> offers the containers of a docker ps above, and kill <tab> the processes of a ps.

Plugins add kinds of rows and actions of their own (Writing plugins).

Asking for a command

A line that starts with ? asks a model for a command instead of running anything: ? the ten biggest files under here. So does a line ending in ? that reads as a question (three words or more, so ls a? stays a glob), and one with a ? between what you’ve typed so far and the question, ls | where ? only files over 1 MB. The model gets the shell’s folder, which shell runs your lines, and your last few commands with how they ended and the end of their output, marked as text someone else wrote. Its command goes to the prompt for you to read, change and run.

It asks the model playing shell-ask-role (fast); :models shows and sets them up. Without one, or without its key, a block marked ? (nothing ran, so it has no exit code) says why, naming the variable that sets the key, like ANTHROPIC_API_KEY.

History and completion

The commands you type at every shell are kept in your data folder (~/.local/share/greed/shell/history.jsonl), the last shell-history-size (5000) of them, so up and ctrl-r find them in every session. An agent’s commands aren’t kept.

As you type, the prompt suggests the rest of the newest command that starts with what’s there, dimmed after the cursor, as fish does: right, end or ctrl-e at the end takes it, and typing on goes past it. :set shell-suggestions false turns it off.

~/s/app ❯ cargo t|est -q --workspace

tab completes what fits where the cursor is, as a shell does: the only match goes in at once, and with several, what they all start with. When that’s all there is, tab shows the menu, and tab again puts each item in place in turn (shift-tab goes back); enter or typing on keeps it. Command names that start with what you typed come first, then those holding it, then looser matches, so hist offers history first.

A menu also opens by itself once typing pauses (completion-delay, 150 ms). It’s a hint until you move into it with tab: enter runs the line as typed, and up and down go through the history. So typing ls -la, pausing and pressing enter runs ls -la. :set completion-enter first makes enter take the menu’s first item instead.

What the shell itself knows is marked greed in the menu, with what it does beside it:

  • A command’s first word: the builtins, your aliases and the programs on your PATH; after $3 |, the stages too.
  • A builtin’s arguments: folders after cd, files and buffers after open, variables after export and unset, topics after help.
  • A stage’s: the columns of the table it reads ($3 | where ), then the comparisons, then the values in that column; from’s formats.
  • $ for variables and earlier blocks, buf: for buffers, and @sel and @file.

A program’s arguments come from what knows them, the first that answers: a completer a plugin added for the program; the program itself when it’s built with Cobra (kubectl, helm, gh, docker, flux, argocd, and any other with Cobra’s completion inside); fish’s completions when fish is installed; carapace’s when it is; and for a flag, the program’s man page, read from the file. Last, when you press tab on a flag, what the program prints for --help (and for a subcommand its help lists, that one’s). Files and folders when none of them knows, or the program says files fit.

~/s/app ❯ kubectl get po
  pods                  Pod
  podtemplates          PodTemplate
  poddisruptionbudgets  PodDisruptionBudget

The programs asked run in the background, in the shell’s folder with its variables, for at most shell-completion-timeout (2000) milliseconds; typing on stops one that’s no longer needed, and an answer is kept a few seconds, so typing more of a word narrows it without asking again. Your program runs only for --help: a Cobra program is asked with its __complete command, the way every shell asks it, and whether a program is Cobra’s is found by looking inside it. :set shell-help-completion never stops --help from running at all, and shell-help-never lists programs it never runs for (reboot, shutdown and the like). shell-completion-sources (cobra fish carapace man help) says which sources to ask, in order.

Agents

Agents run commands in the project’s first shell (opened if there’s none, without taking your screen) with the shell tool group:

Tool
shell_runRun a command line and get back its id, exit code, output and errors (cut to whole lines at their start and end when long), how long it took, where cd went and what it set, the files and lines it names, its JSON, and a table’s columns, first 50 rows and how many there are
shell_readA block by id, or the latest one, yours too
shell_waitWait for a block shell_run left running past its timeout_s
shell_killStop a block an agent ran

An agent’s commands run through a pipe with their input closed, from the project’s root unless they ask for a folder, in sh without its startup files. Their blocks are marked ◆, and their cd and export show in their block without changing your shell. What a command prints goes back to the model marked as someone else’s text, to use as information and not to follow; greed ctl gets it as it is. shell-agent-max-output (30000 bytes) is how much of the output and of the errors an agent gets; where it’s cut says how many lines are left out and that $ID | where, select or get narrows it.

Each block has one id, unique across shells, and in an agent’s command $ID is that block, whichever shell it ran in, so shell_run with $12 | where STATUS != Running works on what block 12 printed. A tool called without an argument it needs, or with one it doesn’t take, says which it takes and runs nothing; greed ctl shell_run --help lists them.

Limits

  • A program in a pseudo terminal that prints faster than the shell reads (seq 1 10000000, a huge cat) can lose lines in between, and its block says how many. A flood of output belongs in a pipe: $-lines, > buf: and agents’ commands run through one, which keeps every line.
  • Blocks don’t survive a restart yet.
  • Only the client a block ran from sizes its terminal.

Editing styles

Greed comes with three ways to edit, two with modes and one without, and you can switch between them while it runs. They’re equals: each is a plugin with its own modes, written against the same API, and each follows the editor it comes from closely, so what your hands know works here: Helix’s selections and motions, Vim’s operators, text objects, registers, macros and visual modes, the usual shortcuts of modeless editors. Where one still differs from the original, its section below says so.

StyleHow it worksLike
helixSelect text, then say what to do with itHelix, Kakoune
vimSay what to do, then where: d w, c i w, y yVim
vscodeNo modes, VS Code’s keys: typing inserts, shift and the arrows selectVS Code, most editors
:style vim        switch now
:style            pick one from a list

The status line shows the style’s name, dim, on the right; clicking it opens the same list. The mode on the left reads the way the style’s editor says it: NORMAL, INSERT, SELECT in Helix’s, VISUAL in Vim’s.

The first time you start Greed it asks which you’d like (:setup asks again), and :tutor teaches your style’s keys with lines to practise on. To start in a style every time, set it in ~/.config/greed/init.luau (with none set, Greed starts in Helix’s):

require("@greed").option.set("editing-style", "vim")

The keys that aren’t about editing live in the leader, which every style opens its own way: space in Helix and Vim, ctrl-space in the vscode style, where typing a letter inserts it. So space f finds files and space / searches the project, and in the vscode style ctrl-space f and ctrl-space / do the same; after the leader a hint lists what can follow. Helix and Vim also share : for commands, g d to go to a definition and ] d to the next problem. Prompts and pickers work the same in all three.

Keys in every style

Bind keys for the leader or for a role, and they work in whichever style you use, styles added later included:

local greed = require("@greed")

greed.keymap.bind({ layer = "leader" }, { W = "save-all" })      -- space W, or ctrl-space W
greed.keymap.bind({ role = "typing" }, { ["ctrl-s"] = "save" })   -- insert mode, or vscode's mode
greed.keymap.bind({ style = "vim", role = "command" }, { ["g h"] = "line-start" })

The roles are command (Helix’s and Vim’s normal mode), typing (insert mode, vscode’s mode) and selecting (Helix’s select mode, Vim’s visual mode). A binding for one style’s role wins over the style’s own keys, and those win over a binding for every style’s role. Keys has the whole order.

Helix

Normal mode is for moving and selecting, and every movement selects: w selects to the next word, x the line, % the whole file. Then a command acts on what’s selected: d deletes it, c changes it, y copies it.

x d        select the line, delete it
x x d      select two lines, delete them
% s foo    select every "foo" in the file; typing now edits them all

The cursor sits on a character, as in Helix, and is part of what you select: v l l selects three characters, ; leaves the cursor on the character it was on, and d on a bare cursor deletes that character. w, b and e move as Helix’s do.

With :set motion-hints true (off by default), a word motion (w b e and their WORD forms) numbers the next nine places doing it again would take you, and typing a number goes there in one key: w 3 instead of w w w w. In select mode it extends the selection. Right after such a motion a digit is then a number to jump to rather than the start of a count; any other key drops the numbers. Vim’s style has the same option for w b e W B E g e g E, in normal and visual mode. The nearest three numbers use the theme’s ui.motion.hint style and the rest the quieter ui.motion.hint.far; in the window both are drawn as badges, like key caps. A number sits on the space beside its place when the place is a letter, so it hides none of the text.

Why it’s here: you see what a command will act on before you run it, and many selections are part of the model rather than a separate feature. s, S, C and alt-s make selections, and every edit then happens at each one, which covers most of what macros and search-and-replace are used for. Using Greed lists the keys. The style is the helix plugin, and its modes are helix, helix-insert and helix-select.

Vim

Vim’s normal mode works the other way round: an operator first, then a motion saying how far. d w deletes to the next word, c i w changes the word under the cursor, 3 j moves three lines down. The cursor sits on a character, and v starts a visual mode for selecting by hand.

Keys
h j k l w b e W B E g e 0 ^ $ g _ g g G %Move, with counts: 3 w; % to the matching bracket (of the next one on the line, off a bracket), 50 % halfway down
{ } ( ) H M L - + _ |Paragraphs, sentences, the top, middle or bottom of the screen, lines, a column
f t F T + a character, ; ,To the next or previous character on the line; again, or back
d c y + a motion or text objectDelete, change or copy; doubled (d d) for whole lines
i w a w i W i ( a " i s i p i t …Text objects: words, brackets (b B too), quotes, sentences, paragraphs, tags, and the rest of the objects Helix’s m i takes (i f a function, i d a number, i n ( the next parens…)
> < g c + a motionIndent, dedent, comment; > > < < g c c for the line
g ~ g u g U g ? + a motionSwitch case, lowercase, uppercase, rot13 (~ u U in visual mode)
g q g w = ! + a motionRewrap to the text-width option (g w keeps the cursor), reindent, pipe through a command
x X D C s SThe usual shortcuts for deleting and changing
r ~ J g JReplace a character, switch case, join lines (g J without adding or removing spaces)
ctrl-a ctrl-xAdd to or subtract from the number at or after the cursor, with counts
p PPaste after or before
i a I A o O, g iInsert; with a count (3 i) what you type goes in that many times; g i where you last stopped typing
u ctrl-r UUndo, redo, put back the last changed line
g - g +Back or on through every state of the text, across undo branches (space u t shows them all)
v V ctrl-vVisual mode, by character, line or block; i/a there select a text object
o r J p in visual modeGo to the other end, replace every character, join the lines, paste over the selection
g v g nSelect the last visual selection again, or the next search match (c g n then . changes match after match)
y s + a motion or object + a character, d s c s, S in visual modeSurround, as vim-surround does: ysiw) wraps the word in brackets (( with spaces inside), ds" takes the quotes away, cs"' changes them, yss wraps the line
alt-o alt-iSelect the syntax node around the cursor, growing it in visual mode, or shrink it back
alt-shift-right alt-shift-leftMove the selected syntax node past the next or previous one
K, g r n g r a g r r g r i g r t, g OAs in Neovim: hover, rename, code actions, references, implementations, type definition, symbols in the file
g sLabel the words on screen and jump to the one you type
ctrl-nSelect the word under the cursor, then add its next match each time, as vim-visual-multi does; c d y i a I A act on every selection, ctrl-x skips a match, ctrl-p drops the last; esc after typing keeps a cursor in each place, and esc then goes back to one
Z Z Z QSave and close, close without saving; in a split, like :wq and :q, they close just that window
ctrl-^The file you were in before, like :b#
I A in visual block modeType before or after the block on every line ($ takes it to each line’s end)
* #Search for the word under the cursor, forward or back
ctrl-f ctrl-b ctrl-d ctrl-u ctrl-e ctrl-yPage, half page and a line down and up
z z z t z b, z a z c z o z R z M z f z j z k z d z ECenter the view, cursor line to the top or bottom; folds
RType over the text; backspace puts back what it replaced
g ; g ,Back and forward through the places you changed
ctrl-o ctrl-t ctrl-d ctrl-n ctrl-p in insert modeOne normal mode command, indent, dedent, complete from words in open files
ctrl-o ctrl-iBack and forward through where you jumped from
m + a letter, ' ` + a letterSet a mark (a–z in the file, A–Z across files), go to its line or to it
.Repeat the last change, with what it typed
q + a letter, qRecord a macro into that register, stop
@ + a letter, @ @Play a macro, play the last one again
" + a letterUse that register for the next yank, delete or paste; an uppercase letter appends; "0 holds the last yank, "1–"9 the last line deletes, "- a small one

Why it’s here: Vim’s keys are in a lot of people’s hands, and switching editors shouldn’t mean relearning them. It also shows how far a plugin can go: the Vim style is plain Luau against the same API any plugin uses, with its own modes (vim, vim-insert, vim-visual, vim-visual-line, vim-visual-block, vim-replace, vim-multi) beside Helix’s.

The unnamed register is the system clipboard, so p pastes what you copied in another program too; "+ is the same and "_ keeps nothing. Text from another program pastes as it is, even when it ends in a line break. Panels like the file tree and help keep their own keys.

Line commands with ranges (:%s/a/b/g, :g/regex/d, :'<,'>norm A;) are on the command line; Using Greed lists them. Some of Vim is still missing: a copied block pastes as plain text (one piece per line), and Vim’s own regex syntax (\(, \<) since patterns are the same regexes search uses.

VS Code

No modes, VS Code’s keys. Typing inserts text, replacing a selection if there is one, and the keys are the ones VS Code and most editors use.

Keys
arrows, home end, ctrl-left ctrl-rightMove; home goes to the first non-blank, then the line’s start
pageup pagedown, ctrl-home ctrl-endA screen up or down, the start or end of the file
shift + arrows, home end, ctrl-left ctrl-rightSelect
ctrl-a, ctrl-lSelect all, select the line (again for the next one)
ctrl-c ctrl-x ctrl-vCopy, cut, paste; with nothing selected, copy and cut take the line
ctrl-z ctrl-yUndo, redo (ctrl-shift-z redoes too, where the terminal can tell it from ctrl-z)
tab shift-tab, ctrl-]Indent or dedent the selected lines; tab alone types a level of indent
ctrl-backspace ctrl-deleteDelete a word back or forward
alt-up alt-down, alt-shift-up alt-shift-downMove the line up or down, copy it up or down
ctrl-shift-k, ctrl-enter ctrl-shift-enterDelete the line, start a new line below or above
ctrl-d, ctrl-shift-lSelect the word, then add its next match as another cursor; select every match
ctrl-alt-up ctrl-alt-down, alt-clickAdd a cursor above or below, or where you click
ctrl-/Comment or uncomment the lines
ctrl-fFind as you type, in any case, with the matches counted (2 of 5); enter and shift-enter go to the next and previous match with the find bar open, esc closes it and leaves the match selected
f3 shift-f3The next or previous match
ctrl-hReplace in the file: type what to find (its matches light up and are counted), enter, then what to put instead; enter replaces them all as one change
ctrl-gGo to a line: type its number
f12 shift-f12 f2 ctrl-.Go to the definition, find references, rename, code actions
f8 shift-f8, ctrl-shift-mThe next or previous problem, all problems
ctrl-shift-o ctrl-tGo to a symbol in the file, in the project
alt-left alt-rightBack and forward through where you jumped from
ctrl-u ctrl-shift-uBack to the cursors before the last command, and forward again
ctrl-p ctrl-b, ctrl-wFind a file, switch to an open file, close the file (the one you were in before takes its place)
ctrl-\Split the pane to the right
ctrl-k then a keyVS Code’s chords: ctrl-k ctrl-0 and ctrl-k ctrl-j fold and unfold everything, ctrl-k ctrl-l folds or unfolds the block, ctrl-k ctrl-i shows what’s under the cursor, ctrl-k ctrl-q goes to the last change, ctrl-k ctrl-t picks a theme, ctrl-k s saves every file
ctrl-P, alt-xPick a command to run from a list; open the command line (: in the other styles)
ctrl-spaceThe leader: ctrl-space f finds a file, ctrl-space / searches the project, the rest as space in Helix and Vim
ctrl-s, ctrl-qSave; quit, asking first whether to save unsaved files

Keys with ctrl-shift need a terminal that reports them, such as one with the kitty keyboard protocol (kitty, WezTerm, foot, Ghostty).

Why it’s here: for people who don’t want modes, or don’t want them yet, and for the odd edit where modes get in the way. Everything else in Greed still works, through the leader, ctrl-P and the command line (alt-x). Completion comes up as you type; ctrl-space is the leader here. The style is the vscode plugin: VS Code’s keys over core’s editing without modes, which another modeless keymap can use the same way (see below).

Making your own

A style is a few modes with keys bound in them, registered with the core plugin. It says which of its modes plays each role and which keys open the leader, and everything bound for those comes with it, with no plugin having to know the style exists:

local greed = require("@greed")

greed.keymap.bind("mine", { j = "move-down", k = "move-up", i = "insert-mode" })
greed.keymap.fallback("mine-insert", "insert-char")

require("@core").style.define({
	name = "mine",
	doc = "My own keys",
	mode = "mine",
	-- Which mode plays each role; insert-mode goes to the typing one.
	roles = { command = "mine", typing = "mine-insert" },
	-- What the status line shows for each mode.
	labels = { mine = "normal", ["mine-insert"] = "insert" },
	-- The keys that open the leader, where `space f` and the rest live.
	leader = "space",
})

That’s enough for :style mine to switch to it and for it to show in the :style list and the setup page. The leader opens with space, with the same hint after it; keys plugins bind for the command role (:, g d, ] d) work in mine, and the typing keys every style shares (enter, backspace, the arrows, esc back to mine) in mine-insert. The file tree, pickers, help and other panels keep their keys in it. Completion, signature help and Markdown shown as written while typing follow the typing mode; a style without modes makes its own mode the typing one (roles = { typing = "mine" }). . works in any style: it types the keys of the last change again; require("@core").commands.tag("no-repeat", { "my-undo" }) leaves out commands that shouldn’t count as one. The vim plugin, which is the Vim style, is a full example to read.

A style without modes, like a JetBrains keymap, is shorter still: bind its keys in its one mode, and core’s editing without modes (the vscode style’s) does the typing, replacing the selection, and the rest:

local greed = require("@greed")

greed.keymap.bind("jetbrains", {
	["ctrl-c"] = "copy-or-line",
	["ctrl-v"] = "paste-over",
	["ctrl-d"] = "copy-lines-down",
	["ctrl-y"] = "delete-lines-at-cursor",
	["ctrl-shift-up"] = "move-lines-up",
})

require("@core").modeless.define({
	name = "jetbrains",
	doc = "No modes, JetBrains' keys",
	leader = "ctrl-space",
})

Roles decide where keys live, not what commands mean, so two styles still differ in what d does. And Vim’s operators read the motion after them straight from the keyboard, so leader and role keys don’t apply between d and its motion.

Keys

Every key runs a command, and every binding can be changed from your config. This page covers finding out what keys do, binding your own, and how keymaps are organised.

Finding out what a key does

Keys
space h kPress a key and see the command it runs, its other keys and where it’s defined
space h cPick a command and see what it does and which keys run it
space h hDescribe anything: commands, options, events, functions of the plugin API, plugins, modes, editing styles and the objects of the running editor, from one picker
space h iInspect what’s under the cursor, or with nothing there the editor itself: an object’s state, the objects it relates to (enter goes to one, backspace comes back) and its actions (enter runs one)
space h o space h e space h a space h p space h m space h sDescribe an option (its value, default and who set it), an event (what handlers get, who listens), an API function, a plugin, a mode and its keys, or an editing style: its modes, the role each plays and its leader
space ?Every command, with its keys; enter runs one
space . (alt-. in the vscode style)What you can do with what’s under the cursor: a file path, a link, a symbol, a problem, a change, a button, the selection; alt-. in a picker does the same for the current item
space, g, ], …After the first key of a sequence, a hint lists what can follow, naming the groups of keys that lead further (require("@hints").leader_groups names your own)

Commands have names like goto-end or select-line, and : runs any of them by name. Keys starting with space are the leader’s: in the vscode style they start with ctrl-space instead.

Binding keys

Bindings go in ~/.config/greed/init.luau. Most commands you reach for by name, like finding a file or searching the project, live in the leader: a layer every editing style opens its own way, space in Helix and Vim, ctrl-space in the vscode style. Bind there and the key works in whichever style you use:

local greed = require("@greed")

-- space q in Helix and Vim, ctrl-space q in the vscode style
greed.keymap.bind({ layer = "leader" }, {
	q = "quit",
	W = "save-all",
	["g p"] = "pick-changed-file",
})

After the leader’s keys, a hint lists what can follow, the same in every style. Check a key is free first (space h k, or the hint): a binding replaces every longer sequence that starts with the same keys, so binding the leader’s w alone would take away space w v and the other window keys.

Other keys belong to a role. Each style says which of its modes plays each one: command is where you move around and run commands (Helix’s and Vim’s normal mode; the vscode style has none), typing is where what you type goes in (insert mode, or the vscode style’s one mode), and selecting is where moving selects (Helix’s select mode, Vim’s visual mode). Bind for a role and the key works in every style’s mode for it, including styles a plugin adds later; add style for one style only:

-- In every style's typing mode: insert mode in Helix and Vim, vscode's mode
greed.keymap.bind({ role = "typing" }, { ["ctrl-s"] = "save" })

-- In normal mode in every style that has one
greed.keymap.bind({ role = "command" }, { ["g P"] = "pick-changed-file" })

-- Only in Vim's normal mode
greed.keymap.bind({ style = "vim", role = "command" }, { ["g h"] = "line-start" })

For exact control, bind in a mode by its name:

greed.keymap.bind("helix", { ["g q"] = "quit-all" })

The mode names depend on your editing style: helix, helix-insert and helix-select are the Helix style’s, the Vim style’s are vim, vim-insert and vim-visual, and the vscode style has one, vscode. The status line shows them as you’d expect (NORMAL, INSERT, VISUAL); all of them are listed under Modes and layers.

A binding maps a key, or a sequence of keys separated by spaces, to a command name. When the same keys are bound in several places, the more specific wins:

  1. keys for the kind of buffer you’re in, like the file tree’s
  2. keys for one style’s role ({ style = "vim", role = "command" })
  3. the leader, for the keys that open it
  4. the mode’s own keys, like those its style binds
  5. keys for a role in every style ({ role = "command" })
  6. keys for every mode ({})

Within one place, a newer binding replaces an older one for the same keys, so your config, loaded last, wins over the default plugins. To change a key a style binds itself, bind it for that style’s role or its mode; a binding for every style’s role stays behind it. Helix’s g p, for one, still goes to the previous buffer whatever you bind for { role = "command" }.

Roles decide where keys live, not what commands mean: Helix still selects and then acts, and Vim’s d w reads its motion straight from the keyboard, so role and leader keys don’t apply after an operator.

Key names:

  • Letters, digits and symbols as themselves: x, X, 5, %, /. Capitals are shifted letters.
  • Named keys: space, enter, esc, tab, backspace, delete, insert, up, down, left, right, home, end, pageup, pagedown.
  • Modifiers in front, joined with -: ctrl-s, alt-x, ctrl-shift-left, ctrl-- for ctrl and minus.

Taking keys away

-- Hide some keys; whatever they did before comes back if the plugin that
-- unbound them is removed
greed.keymap.unbind("helix", { "U", "g w" })

-- Start a mode over: everything bound in it so far is hidden, and bindings
-- made afterwards apply as usual
greed.keymap.clear("helix")

Panes, tabs and terminals

These follow zellij: alt keys that work everywhere, in every editing style and while typing into a terminal, and two modes where single keys act until esc.

Keys
alt-h alt-j alt-k alt-l (or alt and an arrow)Move to the pane or panel (the file tree, an agent) on that side; from a pane at the left or right edge, on to the tab before or after, stopping at the first and last tab. A panel at the edge stays. In a zoomed tab, every pane shows again
alt-nA terminal in a new pane, on the pane’s longer side; while floating views are shown, a floating terminal
alt-fHide the tab’s floating views or show them again, where they were (terminals keep running); with none, open a floating terminal
alt-FThis file in a new floating view (:float-file PATH for another)
alt-wLay the tab’s floating views out with the next float layout: free, one, tiled, cascade
alt-{ alt-}Bring the floating view before or after to the front (in one, the one that shows)
alt-WPick a floating view from the dock: its number, or the arrows and enter
alt-= alt--Give the pane more room, or less
alt-< alt->, alt-1 to alt-9The tab before or after (round the ends), or tab N. alt-[ and alt-] too, in terminals with the kitty keyboard protocol: in others they start an escape code
alt-spaceArrange the tab with the next layout
alt-mSwap the pane with the main one
alt-pPane mode: h j k l move, n / r / d a terminal (anywhere, right, below), v / s this file again (right, below), x close, f zoom in on the pane, g label the panes and go to the one you type (esc leaves the mode), w (or z) hide or show the floats, W another floating terminal, E this file floating, e (or F) float the pane or panel (the tab’s last pane shows another buffer instead) or put a float back, b move the pane to a new tab, t to a tab you pick, P into a panel, T tile the floats, C cascade them, O one at a time, Y pick a float layout, { } the float before or after, H J K L make the pane bigger toward that side, = - resize, R the resize mode, p place a new pane (a frame shows where; h j k l move it, enter opens it, t a terminal, esc cancels), space next layout, y pick a layout, m swap with main, o rotate, [ ] main smaller or bigger, f1 every key, esc done. Keys that open or move something (n, x, b, t, P, y, Y, p) leave the mode
alt-tTab mode: h l switch, n new, t new with a terminal, x close, r rename (these four leave the mode), f1 every key, esc done
alt-rResize mode: h j k l make the pane, panel or floating view bigger toward that side, H J K L smaller from that side (at the screen’s edge the other edge moves), arrows move a floating view, = - every way, esc done

The window keys from Vim and Helix follow ctrl-w in normal mode, and the leader’s w in every style (space w, or ctrl-space w): v and s split, h j k l move, H J K L swap the pane with its neighbour, w the next pane, q close, o close the others, t a terminal, z zoom, g label the panes, f and F open the file named under the cursor below or to the right, n s and n v a new empty buffer below or to the right. In the Vim style, g t and g T change tabs.

All of these are ordinary bindings to commands (pane-left, pane-terminal, tab-next, …), so they change like any other:

local greed = require("@greed")

-- alt-n is something else in my shell
greed.keymap.unbind({}, { "alt-n" })
greed.keymap.unbind("terminal", { "alt-n" })

-- Split with space | and space -
greed.keymap.bind({ layer = "leader" }, {
	["|"] = "pane-split-right",
	["-"] = "pane-split-down",
})

-- The pane mode on ctrl-p, in terminals too, as in zellij
greed.keymap.bind({}, { ["ctrl-p"] = "pane-mode" })
greed.keymap.bind("terminal", { ["ctrl-p"] = "pane-mode" })

The terminal’s mode takes every key, so the alt keys are bound in the terminal layer too; unbind a key there as well to let it through to the program. require("@panes").keys holds the defaults, by layer.

In a terminal, keys go to the program except those bound in the terminal layer: ctrl-\ to stop typing into it, ctrl-space to stop and open the leader (so ctrl-space f finds a file), and the alt keys above. Bind more there the same way:

-- ctrl-o finds a file without leaving the terminal first
greed.keymap.bind("terminal", { ["ctrl-o"] = "pick-file" })

Modes and layers

Bindings live in layers. Most belong to a mode:

ModeUsed by
helix, helix-insert, helix-selectThe Helix style
vscodeThe vscode style
vim, vim-insert, vim-visual (and vim-visual-line, vim-visual-block, vim-replace, vim-multi)The Vim style
terminalTyping into a terminal (takes every key)
pane, tab, resizeThe pane, tab and resize modes
viewThe sticky view mode (Z in the Helix style)

The leader layer isn’t a mode: each style’s mode opens it with its own keys (space, or ctrl-space in the vscode style), and what’s bound in it works after them. A style can open any layer this way with greed.keymap.enter(mode, keys, layer). Role layers aren’t modes either: each style’s mode for a role uses { role = ... } after its own keys and { style = ..., role = ... } ahead of them.

A layer can also be limited to one kind of buffer, so a key does something different in the file tree or a picker than in a file. Bound for the kind alone, a panel’s keys work in every style and every mode, ahead of the mode’s own:

-- o opens the file under the cursor, but only in the file tree
greed.keymap.bind({ kind = "tree" }, { o = "tree-open" })

-- Only in Helix's mode, in the file tree
greed.keymap.bind({ mode = "helix", kind = "tree" }, { O = "tree-open" })

-- An empty layer applies in every mode
greed.keymap.bind({}, { ["ctrl-q"] = "quit" })

When a key is pressed, Greed looks first in the layers for the buffer’s kind: the style’s role’s, the mode’s, the leader’s, those of the layers and modes it inherits, then the kind’s for every mode. Then come the style’s role layer, the leader if the keys open it, the mode’s own layer, the layers and modes it inherits (the role’s for every style), and the layer for every mode. The first layer where the keys run a command, or start a longer sequence, decides.

greed.keymap.inherit lets a mode use another mode’s keys, all of them or only those starting with some prefixes, and what that mode inherits in turn. Helix’s select mode uses it to keep normal mode’s keys; see Editing styles.

Binding your own commands

Any Luau function can become a command, and then a key:

local greed = require("@greed")

greed.command({
	name = "upcase-line",
	doc = "Upper-case the line the cursor is on",
	run = function(ctx)
		local buffer = ctx.buffer
		local line = buffer:line_of(ctx.view:cursor())
		local from, to = buffer:line_start(line), buffer:line_end(line)
		buffer:edit(function(e)
			e:replace(from, to, string.upper(buffer:slice(from, to)))
		end)
	end,
})

-- alt-u while typing, in every style
greed.keymap.bind({ role = "typing" }, { ["alt-u"] = "upcase-line" })

Writing plugins covers commands, and the rest of the API, in more detail.

Highlighting

Greed highlights code with tree-sitter. A language’s highlights query gives pieces of the text names like keyword.control.return or markup.heading.1, and the theme gives each name a look. The names are the same in every language, so one theme entry styles keywords everywhere. Files are parsed again in the background after each edit; until that’s done, the highlights from before move along with the text you typed.

A name falls back to its parent when the theme has no entry for it: keyword.control.return uses keyword.control, then keyword. A theme can be as short as the top-level names, or style any of the finer ones.

require("@greed").theme.set({
	keyword = { fg = "magenta" },
	["keyword.control.return"] = { fg = "red", bold = true },
	["markup.heading.1"] = { fg = "blue", bold = true, underline = true },
})

These are the names Greed’s queries and themes use, and the ones to use in queries you write. They’re the names Helix uses, so its queries work as they are. A theme entry can also be set for one kind of client only, with tui and gui parts:

require("@greed").theme.set({
	comment = { fg = "bright-black", gui = { italic = true } },
})

Code

NameFor
attribute#[derive(Debug)], decorators
commentComments
comment.line// ...
comment.line.documentation/// ...
comment.block/* ... */
comment.block.documentation/** ... */
constantConstants
constant.builtinnil, None, self as a value
constant.builtin.booleantrue, false
constant.character'a'
constant.character.escape\n in a string
constant.numericNumbers
constant.numeric.integer42
constant.numeric.float4.2
constructorSome(...), new Foo
functionFunctions
function.builtinprint, len
function.methodx.len()
function.macroprintln!
function.specialPreprocessor functions and the like
keywordKeywords
keyword.controlKeywords that change the flow
keyword.control.conditionalif, else, match
keyword.control.repeatfor, while, loop
keyword.control.importuse, import, require
keyword.control.returnreturn, break, continue
keyword.control.exceptiontry, catch, throw
keyword.directive#include, #!
keyword.functionfn, function, def
keyword.operatorand, or, in
keyword.storagelet, const, static
keyword.storage.typestruct, enum, class
keyword.storage.modifierpub, mut, async
label'outer:, a code block’s language
namespaceModules and packages
operator+, =, ->
punctuationPunctuation
punctuation.bracket( ) [ ] { }
punctuation.delimiter, ; .
punctuation.special${ in strings, Markdown’s >
specialAnything else a query wants to stand out
stringStrings
string.regexpRegular expressions
string.specialDates, symbols and the like
string.special.pathPaths
string.special.urlURLs
string.special.symbol:symbol, atoms
tagHTML and XML tags
tag.builtinHTML’s own tags
typeTypes
type.builtinu8, string, int
type.parameterT in Vec<T>
type.enum.variantEnum variants
variableVariables
variable.builtinself, this
variable.parameterFunction parameters
variable.other.memberFields, properties, keys

Prose

NameFor
markup.headingHeadings
markup.heading.markerThe # of a heading
markup.heading.1A level 1 heading, and so on to markup.heading.6
markup.heading.2
markup.heading.3
markup.heading.4
markup.heading.5
markup.heading.6
markup.listList markers
markup.list.unnumbered-, *
markup.list.numbered1.
markup.list.checked[x]
markup.list.unchecked[ ]
markup.bold**bold**
markup.italic*italic*
markup.strikethrough~~gone~~
markup.linkLinks
markup.link.urlA link’s address
markup.link.label[label] in a reference link
markup.link.textA link’s text
markup.quoteBlock quotes
markup.rawCode
markup.raw.inline`code`
markup.raw.blockCode blocks

Writing queries

Queries are files named queries/LANGUAGE/highlights.scm, in a plugin or in ~/.config/greed/queries; see Using Greed. In greed repl, tostring(require("@greed").view():buffer():tree()) shows the current buffer’s syntax tree, which is what a query matches against. A capture whose name starts with _ (like @_name) is only there for a predicate and isn’t drawn.

Writing plugins

Everything you use in Greed is a plugin written against the API described here, so the default plugins in runtime/plugins are the best examples: core holds the editing commands, helix gives them Helix’s keys, grep is the project search, agent the MCP tools.

A plugin

~/.config/greed/plugins/greet/
  plugin.toml        name = "greet", what it may use
  init.luau          runs when the plugin loads
  test/greet.luau    tests, run with `greed test greet`

Run in ~/.config/greed/plugins, greed new greet creates this, with a working test and type checking set up. plugin.toml can list plugins this one uses, like requires = ["core", "picker"]; they load first and are available as require("@core") and require("@picker"). A plugin’s init.luau requires its other files with require("@self/name"), and those files require each other with require("./name"). A plugin can’t require files outside its folder.

Capabilities

A plugin can edit buffers, add commands, keys, pickers and the rest of the editor freely. Anything that reaches outside the editor, it declares in plugin.toml:

name = "greet"
capabilities = ["proc", "fs.read"]
CapabilityWhat it opens
procRunning programs: greed.process.run and spawn, greed.terminal.spawn, greed.lsp.server, typing into a terminal, and a language server’s workspace/executeCommand. A program can do anything you can, so this is full access
fs.readFiles by path: greed.fs reads, greed.fs.copy, greed.hash.sha256_file, greed.files, greed.load, greed.open, greed.image.load, greed.pdf.open, greed.syntax.tags, greed.syntax.load_grammar
fs.writegreed.fs.write, mkdir, remove, rename and copy, greed.http.request with save, and buffer:save(path) to a path other than the buffer’s own file
clipboardReading what you copied (greed.clipboard.get, history, and image for the image on the system clipboard of the client you’re at); setting it needs nothing
netThe network: greed.http to any host, and the Claude Code connection
net:HOSTgreed.http to one host, e.g. net:api.anthropic.com
secrets:NAMEgreed.secrets.get("NAME"), e.g. secrets:anthropic for an API key
secretsEvery secret, and keeping new ones
evalgreed.eval, which runs Luau with every capability
pluginsLoading, approving, unloading and checking other plugins. A plugin it loads can have any capability, so this is full access. Approving still needs you, or a plugin with plugins, to have started it
commandsRunning any command or keys for you with what the command’s own plugin may do, as the command line and the command picker do (see below)
controlgreed.control: answering agents and editors connected to the session, as the agent plugin does

Using something the plugin didn’t declare raises an error that says what to add, e.g. greet can't run programs: add "proc" to capabilities in its plugin.toml. Your init.luau and a trusted project’s .greed folder are config, not plugins, and can use everything.

Capabilities follow the code that uses them, and don’t pass from one plugin to another:

  • A command runs with its own plugin’s capabilities when you start it, with keys, the command line, a menu or your config. When plugin code starts it, with greed.run or by feeding keys with greed.keys.feed, it can do only what that plugin may too, so a plugin without proc can’t run programs by running another plugin’s command. This carries on through commands those commands run, what they start with greed.spawn, and after they wait. A plugin with commands passes on whatever it was started with, since it runs what you typed or picked.
  • Keys a plugin feeds never answer a command waiting for you to press a key, like a “y” to confirm.
  • Event handlers run with their own plugin’s capabilities, whatever caused the event: they’re that plugin reacting to what happened. Plugins can’t send events of their own.
  • Calling what another plugin offers (require("@core").project.open(dir)) works with what both plugins may: if open writes a file, the caller needs fs.write too. The same goes for a function one plugin hands another, like a picker callback or a hook, and for coroutines a plugin makes: every plugin whose code is running counts. So the plugins Greed ships that run other plugins’ hooks, like core and picker, declare what those hooks need. Code a plugin loads with loadstring counts as that plugin’s, and plugins don’t get getfenv or setfenv.
  • A plugin that offers a service can vouch for one of its own operations with greed.vouch(f, ...): then only its own capabilities count for what f does, not those of the plugin that asked. The models plugin vouches for sending a request with your key, so any plugin can ask a model without being able to read the key, and the key goes only where you let it. Code handed to a vouched function still counts as its own plugin’s. Anything you vouch for, any plugin can make you do, so vouch only for narrow operations you check yourself.
  • What a plugin registers and another runs later, like a formatter, a block runner, a project source or a shell alias, runs within the limit of the plugin that registered it, so it can do no more than that plugin may. Your own registries can do the same: note greed.plugin.caller() when something is registered, and run it with greed.plugin.within(by, f, ...). Hand out copies of what you keep, so callers change it only through your functions.
  • Luau run with greed.eval has every capability, as :lua needs, even though it runs inside the command line’s code. Calling greed.eval takes eval from every plugin whose code is running and every one that started it, so a plugin without eval can’t get it by calling or starting one that has it. greed.eval(code, { sandbox = true }) runs the code in a fresh environment that can read the editor and edit buffers and nothing else: running programs or commands, changing keys or reaching files fails with an error starting “needs approval:”.

And they limit plugins only: an agent that runs commands in a shell of its own isn’t affected.

A plugin that isn’t one of Greed’s own and declares capabilities loads once you’ve approved them. Until then Greed says it waits, and :plugins-approve shows each waiting plugin with what it asks for; choose one to approve and load it. Approvals are kept per plugin folder in ~/.local/state/greed/approved.json, and a plugin that later asks for more waits again. Plugins that use nothing beyond the editor load at once, and installing a plugin an agent wrote approves it, since its review shows the same list. Plugins can’t write the approvals file through Greed.

The plugins folders are watched while Greed runs. A plugin folder that appears loads, and a plugin reloads when its plugin.toml or code changes, with no restart. One asking for something you haven’t approved waits and :plugins-approve opens; until you approve, the version that was running keeps going as it was. :plugin-reload NAME loads any plugin again from its folder by hand, Greed’s own included, as after changing its code.

Checking a plugin that isn’t approved yet (greed.plugin.check, or an agent’s plugin_try) runs its tests with only what you approved for it, which before you install it is nothing. A test that needs more fails with “needs approval”, whether the plugin’s own code asks or a command or plugin it uses does. greed test from your shell runs a plugin’s tests with everything it declares.

Plugins are Luau, a typed dialect of Lua. If you know Lua, the differences you’ll meet are types (local n: number = 1), if a then b else c as an expression, backtick strings with {interpolation}, +=, and for k, v in t do without pairs.

Commands and keys

local greed = require("@greed")

greed.command({
	name = "greet",
	doc = "Say hello",
	args = { { name = "who", doc = "Who to greet" } },
	run = function(ctx)
		greed.echo(`hello {ctx.args[1] or "there"}`)
	end,
})

greed.keymap.bind({ layer = "leader" }, { g = "greet" })

A command name can’t have spaces, so it can be typed after :. Defining a command that another plugin already has replaces it, and Greed says so in a message; your config can replace any command without one. When the editor starts, it names keys bound to commands that don’t exist, which usually means a typo. greed.plugin.check reports both as warnings.

Commands get a context: ctx.buffer and ctx.view where they run, ctx.args from the command line (:greet world), ctx.count when a count was typed, ctx.keys that ran it, ctx.force for ! (as in :q!), ctx.char for keys that type a character, and ctx.client, the id of the client that ran it (see Clients). Keys for commands people reach for by name go in the leader, { layer = "leader" }, which every editing style opens its own way (space g in Helix and Vim, ctrl-space g in the vscode style). Keys for normal mode or for typing go to a role, { role = "command" } or { role = "typing" }, so they work in every style’s mode for it, styles added later included; { style = "helix", role = "command" } is for one style’s. Other keymaps are bound per mode, or per kind of buffer with { kind = "tree" }. Bindings made later win, and greed.keymap.unbind and greed.keymap.clear take keys away, so a plugin can rebuild a mode from scratch. A mode is a name, and greed.mode.set("visual") makes one. greed.keymap.inherit(mode, parent, prefixes) lets a mode use another’s keys (some of them, with prefixes), and greed.keymap.enter(mode, keys, layer) opens a layer with keys, the way each style opens the leader; see Editing styles. greed.keys.record(name) and greed.keys.stop(name) keep what’s typed, for macros and the like; several recordings can run at once. greed.keymap.fallback(mode, command) runs a command for any single key nothing else takes (a mode inheriting all of another’s keys uses that one’s), and greed.keymap.isolate(mode) keeps keys bound for every mode out of one, the way a terminal takes every key.

Keys bound for a kind of buffer alone work in every editing style and every mode, ahead of the mode’s own, so a panel binds them once, with { kind = "tree" }. A buffer kind you type into, like a prompt, says so with core.style.typing_kind("my-prompt"): focusing one goes to the style’s typing mode, and core.floats.typing opens a float in it. core.style.label_kind("my-prompt", "rename") has the status line call the mode RENAME while one has focus, rather than the typing mode’s name. Rather than naming other styles’ modes, ask core.style: is_typing() says whether the current mode types (insert, Vim’s insert, the vscode style), role() which role the current mode plays, mode_for(role) the style in use’s mode for a role, and each_typing(function(mode) ... end) calls back with every style’s typing modes, including styles added later, for modes of your own made from them, like the completion menu’s. Commands can carry tags others check: core.commands.tag("typing", { "my-type-char" }) keeps them out of the selection history and in one undo step in the vscode style, and "no-repeat" leaves them out of what . repeats.

For keys that should mean something in one buffer only for a while, buffer:set_keys({ y = "my-answer-yes", esc = "my-hide" }) binds single keys for that buffer, in every mode and ahead of every other binding, while it has focus; buffer:set_keys(nil) drops them. The agent pane uses it to answer a question with enter or a letter whatever mode you’re in.

Actions on things

space . lists what you can do with what’s under the cursor, and alt-. in a picker with the current item. A plugin adds both halves: a finder that says what’s at the cursor, and actions for a kind of thing. Picker items get actions by giving a kind, like { label = path, value = path, kind = "file" }.

local actions = require("@core").actions

actions.finder({
	name = "ticket",
	find = function(view, range)
		local buffer = view:buffer()
		local text = buffer:line_text(buffer:line_of(range.head))
		local id = string.match(text, "%u+%-%d+")
		return if id then { { kind = "ticket", label = id, value = id } } else nil
	end,
})

actions.action({
	kind = "ticket",
	name = "open",
	doc = "Open it in the browser",
	run = function(target)
		require("@core").file.open_link(`https://example.atlassian.net/browse/{target.value}`)
	end,
})

An action that is an existing command has a short form: actions.command("text", "search", "search-selection", "Search for it") (kind, name, command, doc).

The kinds Greed itself uses are file, folder, link, symbol, problem, change, button, text (the selection) and command, and the shell’s rows add process, container, pod, deployment and shell-block, so a plugin can add actions to those too. A plugin’s finders and actions go when it’s unloaded.

Objects

The things in a running Greed are objects, each with a kind and an id (buffer:12, tab:3, shell-block:7.2). An object has its state, the objects it relates to and its kind’s actions. space h i inspects one, and agents read them through the describe tool. Greed’s own kinds are editor (where the inspector starts), buffer, view, tab, plugin, client, file, folder, terminal, shell-block, language-server and proposal.

A plugin adds a kind for its own things. Everything but the name is optional: list when there’s a way to list them all, get to find one by the key in its id, inspect for its state as a table, and relations for the objects it points at, by name.

local objects = require("@core").objects

local function ticket(key: string): core.Object
	return { kind = "ticket", id = `ticket:{key}`, label = key, value = key }
end

objects.kind({
	name = "ticket",
	doc = "A ticket in the tracker",
	get = function(key)
		return ticket(key)
	end,
	inspect = function(key)
		local found = tracker.lookup(key)
		return { status = found.status, owner = found.owner }
	end,
	relations = function(key)
		local blocks = {}
		for _, other in tracker.lookup(key).blocks do
			table.insert(blocks, ticket(other))
		end
		return { blocks = blocks }
	end,
})

What a finder finds is an object once it has an id: the kind’s key(value) gives one, or the value itself when it’s a string or a number, as a file’s path is. An object that is somewhere in a buffer says where (buffer, from, to); objects.act(object, name) then puts the cursor on it before running the action, so actions that work on what’s under the cursor work on it from anywhere, the inspector included.

objects.list(kind), objects.get(id), objects.inspect(object), objects.related(object) and objects.at(view) read them. A plugin’s kinds go when it’s unloaded.

Going to definitions and hover

g d (going to where something is defined) and space k (what it is) ask each plugin that answers for them in turn, so one key works for a note’s [[link]] and a language server’s symbol alike, in every style. An answer returns whether it handled what’s under the cursor; a fallback, like the language server’s, is asked only when nothing else answered.

local core = require("@core")

core.lookup.add("definition", function(ctx)
	local line = ctx.buffer:line_text(ctx.buffer:line_of(ctx.view:cursor()))
	local id = string.match(line, "%u+%-%d+")
	if id then
		core.file.open_link(`https://example.atlassian.net/browse/{id}`)
	end
	return id ~= nil
end)

Use "hover" for space k. What a plugin adds goes when it’s unloaded.

core.transient.show puts a menu at the bottom of the screen listing what each key does, in groups, like Emacs’s Transient. Switches flip and stay while you pick; an action runs with the switches that are on and closes the menu, and any other key closes it. It waits for keys, so call it from a command. The change stack’s ? is one.

local transient = require("@core").transient

greed.command({
	name = "push-menu",
	run = function()
		transient.show({
			title = "Push",
			groups = {
				{ title = "Switches", items = { { key = "f", doc = "force", switch = "force" } } },
				{
					title = "Push",
					items = {
						{
							key = "p",
							doc = "to origin",
							run = function(on)
								greed.process.run(if on.force then { "git", "push", "-f" } else { "git", "push" })
							end,
						},
					},
				},
			},
		})
	end,
})

Switch keys show as -f and can be typed as f or - f.

Buffers, views and edits

Positions are byte offsets, lines are counted from 0, and a selection is a list of ranges with an anchor and a head.

local buffer, view = ctx.buffer, ctx.view
local line = buffer:line_of(view:cursor())
buffer:edit(function(e)
	e:insert(buffer:line_start(line), "-- ")
end)

Edits made in one edit call are one undo step and are applied together, so their positions all refer to the text before the edit. Selections move with the text.

To change the text of every selection and say where each selection goes afterwards, core.motions.replace_each does the bookkeeping. Return the new text, which the selection then goes around, or nil to leave that selection alone. A table says more: place is where the selection goes (“around”, “keep” for around it the way it pointed, “start”, “end”, or a number of bytes in), and from and to replace other text than the selection.

local core = require("@core")
-- Wrap each selection in stars.
core.motions.replace_each(ctx.view, function(i, range)
	return "*" .. ctx.buffer:slice(core.text.span(range)) .. "*"
end)
-- Put "TODO " before each selection, leaving a cursor after it.
core.motions.replace_each(ctx.view, function(i, range)
	local at = core.text.span(range)
	return { text = "TODO ", place = "end", from = at, to = at }
end)

core.textobjects.find(buffer, pos, key, around) finds the text objects every editing style uses, by Helix’s keys: "w" a word, "p" a paragraph, "(" the parentheses around pos, "f" a function from the syntax tree, and so on. It gives from, to, linewise, or nil. core.textobjects.pair(char) is what surround wraps text in for a key.

view:cursor() is the head of the primary selection and view:set_cursor(pos) makes the selection one cursor there. require("@core").text.span(range) gives a range’s start and end, whichever way it points. buffer:line_text(line) is a line without its line break. buffer:display_column(pos) is the screen column pos is drawn at, counting wide characters as two columns and tabs up to the next tab stop, and buffer:at_display_column(line, col) goes back from a column to a position, which is how up and down keep their column.

Copying and pasting goes through require("@core").registers, so text keeps whether it was whole lines in every editing style. A register holds { pieces, linewise }, one piece per selection: registers.copy(pieces, linewise) puts them in the register picked with " or on the clipboard, registers.paste(ctx) gives back what to paste, and registers.join(r) makes one text of it. Text on the clipboard that Greed didn’t put there comes back with linewise false.

greed.clipboard.image() waits for the image on the system clipboard of the client you’re at, as PNG bytes, read on that client’s own machine (so with greed ssh it’s the laptop’s), or nil. To take pasted images in a kind of buffer, register with core.images.taker(kind, function(png, ctx) ... end). The paste-image command then hands the image to it: the window’s paste keys run it when the clipboard holds an image and no text, and in a terminal, whose paste carries only text, bind a key to it (the agent box uses alt-v). greed.base64 encodes the bytes for JSON.

greed.open(path) shows a file as text (core.file.open, below, also knows images and PDFs), greed.load(path) opens one without showing it, greed.create_buffer(kind, text) makes a scratch buffer, and buffer:find, buffer:find_all and greed.fs.grep search with regexes. A scratch buffer goes once nothing shows it, unless buffer:set_kept(true) keeps it, and buffer:set_line_numbers(false) hides its line numbers.

Places that move with edits

To find a place again after the text around it changed, say where a command should put its result once a slow program finishes, track it:

local here = buffer:track(pos)                  -- a position
local line = buffer:track_range(from, to)       -- a range
-- ... edits happen, here or elsewhere ...
here:pos()                                      -- where it is now
local from, to = line:range()                   -- nil, nil once its text was deleted
line:deleted()
here:forget()

Gravity says what happens to text inserted exactly at a tracked place. A position with { gravity = "right" } (the default) ends up after the new text, and with "left" it stays before it. A range takes from and to gravities, one per end. By default text typed at either end stays outside, and { from = "left", to = "right" } takes it in. Deleting text around a position moves it to where the deletion was. A range is deleted once all of its text is, and stays deleted even if undo brings the text back; one that started empty never is.

Nothing draws tracked places, and each has its own handle, so two commands tracking the same buffer never get in each other’s way. They go when you call forget, when the buffer closes, or when your plugin is unloaded. After that pos and range give nil.

Buffers, views and terminals are handles: two handles to the same one are equal with ==. A handle you keep can outlive what it refers to. handle:valid() says whether it’s still there, and handle:id() keeps working after it’s gone, while its other methods fail with an error.

-- The panel this plugin shows, reused while it's open.
local panel: greed.View? = nil

local function show(buffer: greed.Buffer)
	if panel and panel:valid() then
		panel:show(buffer)
	else
		panel = greed.panel(buffer, { side = "right" })
	end
end

Paths and the files behind them

Buffers know their files by full path, with . and .. worked out, so greed.buffer_for(path) finds a file’s open buffer however the path is written (or gives nil, without opening it). Paths an agent or a user gives a plugin are relative to the project, and core.project turns them into full paths and back:

local core = require("@core")
core.project.resolve("src/main.rs")    -- the shown project's root/src/main.rs; ~ is home
core.project.resolve("a.rs", "/tmp/x") -- from another folder
core.project.relative(path)            -- "src/main.rs", or the full path outside the project
core.project.short(path)               -- like relative, but "~/..." under your home folder
core.project.tilde(path)               -- "~/src/greed" for a path in your home folder
core.project.basename(path)            -- "main.rs" for "src/main.rs"; nil for "/"
core.project.identity(root)            -- the repository's store, the same in all its workspaces

core.file reads and writes files the way you’d want an agent to: core.file.text(path) is the text as you have it, unsaved changes included, and core.file.write(path, text) goes through the open buffer, so undo takes it back and the language server sees it, then saves; a closed file is written to disk. { from, to } replaces just that part, and { load = true } opens the file first. Both take paths as core.project.resolve does, and core.file.buffer(path) is the open buffer for one.

A buffer of your own with no file, like a page of results or a command’s output, can be scratch: core.file.scratch(buffer) keeps its changes from counting as unsaved, so :q doesn’t refuse over them and the status line shows no [+], until it’s saved as a file. core.file.unsaved(buffer) says whether a buffer has changes to save. A buffer someone types into in turns, like a prompt, can let go of its undo history with buffer:clear_history(), so undo never reaches past the last turn. To tell someone how to set a secret, greed.secrets.variable("anthropic") is the variable that does (ANTHROPIC_API_KEY).

To show a file to the user, core.file.open(path) opens it in the main view, as the picture for an image and the pages for a PDF, and core.file.open_at(path, line, col) puts the cursor there too (0-based line, byte column). core.file.open_link(uri) opens a web address with the link-opener option or the system’s opener. A plugin that shows some kind of file its own way registers an opener; it goes when the plugin does:

core.file.opener(function(path)
	return string.match(path, "%.csv$") ~= nil
end, function(path, views)
	local table_buffer = greed.create_buffer("csv", render(path), { readonly = true })
	for _, view in views do
		view:show(table_buffer)
	end
	return nil -- or why it can't, and the file opens as text
end)

Files opened another way, like greed FILE from a shell or greed.open, open as text first and the opener then takes over the views showing them.

For files on disk with no buffer, greed.fs has the usual operations, so a plugin needs no rm or mv. Each returns true, or nil and why:

greed.fs.write(path, text)                        -- creates its folders too
greed.fs.write(path, text, { private = true })    -- readable only by you (0600)
greed.fs.mkdir(dir)                               -- and the folders above it
greed.fs.copy(from, to)
greed.fs.rename(from, to)
greed.fs.remove(path)                             -- a file, link or empty folder
greed.fs.remove(dir, { recursive = true })        -- a folder and all in it
greed.hash.sha256(text)                           -- hex, to tell if text changed
greed.hash.sha256_file(path)                      -- to check a download

Something already gone counts as removed. remove and rename raise an error rather than touch the root folder, your home folder, a path with .. in it or a folder holding Greed’s own approvals or secrets, so a path built wrong can’t take more than it should. greed.fs.stat(path) says what’s there: its kind, size, mode and when it was modified (seconds since 1970). greed.hostname() is the machine’s name.

History

Every edit is recorded with who made it and why. buffer:history() lists the transactions, newest first, each with its author ("user", "plugin", "agent" or "external"), cause (the command it was made for), time, group and changes. Filter by any of those, or with since and limit:

-- What did the user just change?
for _, tx in buffer:history({ author = "user", limit = 5 }) do
	print(tx.cause, tx.changes[1].text)
	local before = buffer:text_before(tx.id) -- the text before it
end

A command’s edits carry its name as their cause; greed.history.set_cause names them otherwise. To make edits in several buffers undo as one step, put them in a group:

local group = greed.history.new_group()
greed.history.set_group(group)
-- ... edit any buffers ...
greed.history.set_group(nil)
greed.history.undo_group(group) -- every buffer back, or nil and why not

Undoing a group refuses, and changes nothing, when something was done on top of it in one of the buffers since.

Undo keeps every branch. buffer:undo_tree() gives every state the text has been in, { nodes = { { id, parent, children, time } }, current }, where node 0 is the text as loaded and undo goes to a node’s parent; and view:undo_to(id) takes the text to any of them, on any branch, which is what alt-u and the undo-tree plugin’s viewer are built on.

A buffer whose edits nobody undoes, like a terminal’s or a log a plugin keeps adding to, can stop keeping them with buffer:forget_history(), so it doesn’t grow without end.

When a plugin writes into a buffer someone also types in, like output streaming in above a prompt, buffer:edit(f, { undo = false }) keeps its edit out of undo: undo skips it and still takes back only what was typed, moved to wherever the plugin’s text put it. Redo can’t go down other branches after such an edit, and typing the plugin’s edit replaced or deleted is no longer there to undo.

buffer:edit(function(e)
	e:insert(output_end, line .. "\n")
end, { undo = false })

The operations plugin builds on groups: it names the work, remembers the files it changed and the programs it ran, and gives it a review and an undo key.

local operations = require("@operations")
operations.run("rename parse to parse_line", function(op)
	-- edit any buffers, then
	operations.exec(op, { "cargo", "test" })
end)

Syntax trees

Buffers in a language with a tree-sitter grammar have a syntax tree. buffer:node_at(pos) is the smallest named node there, and nodes know their kind, from, to and field, and can go to their parent(), children(), child("body"), next() and prev(). buffer:query(source) runs a tree-sitter query and returns each match as a table from capture name to node:

-- Select the name of every function in the file
local ranges = {}
for _, m in buffer:query("(function_item name: (identifier) @name)") do
	table.insert(ranges, { anchor = m.name.from, head = m.name.to })
end
view:set_selection({ ranges = ranges, primary = 1 })

tostring(node) shows the node’s subtree, which helps when writing a query. A node describes the text as it was when you got it; after an edit, find it again. Right after an edit that would take a while to parse (a big file, or code tree-sitter has to recover from), the tree is the one before with the edit applied, its nodes moved along with the text, until the new one comes a moment later.

Each language’s queries are in greed.syntax: greed.syntax.query("rust", "textobjects") is the one in use, and greed.syntax.set_query sets one. A plugin can also ship them as files in its own queries/LANGUAGE/ folder (see Configuration), and the plugin.loaded event says when a plugin with some arrives.

Folding uses a language’s folds query, or indentation without one. To fold a language another way, give core a provider that returns the first and last line of the block around a line: require("@core").folds.provider("org", function(buffer, line) ... end). Markdown folds sections this way. New lines’ indent works the same way, with the indents query and require("@core").indent.provider, whose functions get the buffer and the place a line break goes in and return the new line’s indent.

The editor can have several projects open. greed.project.current() is the one shown, with its root; greed.project.open, switch and close manage them, and buffer:project() says which one a buffer belongs to. Panels belong to the project they were opened in, so a plugin that keeps one (like a results panel) should keep one per project.

Each project has tabs (greed.tab.list, new, switch, close, rename), and each tab has panes laid out side by side and one above another. view:split("right", buffer) opens a pane, view:neighbor("left") finds the one beside it, view:resize("down", 2) moves its edge, and view:close() closes it. greed.view() is the focused view, and the shown tab’s active pane is where greed.open opens files. Tab ids work whichever project the tab is in: greed.tab.switch and view:focus() on a tab or view of another project show that project.

greed.tab.layout() gives a tab’s arrangement as a tree, and greed.tab.set_layout(tree) arranges its panes another way:

local a, b, c = table.unpack(greed.tab.current().panes)
-- a across the top, a quarter high; b and c side by side under it
greed.tab.set_layout({
	split = "column",
	sizes = { 1, 3 },
	children = { a, { split = "row", children = { b, c } } },
})

Showing things

  • Decorations style text without changing it, and move with edits: buffer:decorate("my-plugin", { { from = 0, to = 5, style = "diagnostic.error" } }). A mark can style whole lines, add virtual text inside or after a line, put a sign in the gutter, or draw text over the buffer’s (like jump labels). It can also hide text (conceal, like a link’s brackets), shown again as reveal says: "line" (the default) when a cursor is on its line, "cursor" when one is inside it, "never", or "auto", which follows the mode: on the cursor’s line while typing, inside it in edit mode, never otherwise. Markdown and notes use "auto", so their marks stay as they are when the mode changes (greed.reveal(how) is what core calls as it does). buffer:decorate(ns, marks, { from = a, to = b }) replaces only the marks starting from a up to b, for plugins that draw again just the lines an edit changed. plain = true draws a mark’s text without syntax highlighting, as for a tool’s output in a Markdown buffer. A mark can fold lines away under the first one (fold), or show lines that aren’t in the buffer under its own (lines), whole or in pieces with styles of their own. A mark’s text with an action (a command name) is a button: clicking it, or enter on it in normal mode, puts the cursor at the mark and runs the command. scale = 2 (up to 4) makes its text bigger, like a heading: each character takes twice the cells across and its row two rows down. The window draws it big, and so does kitty; other terminals show it at the normal size. gutter styles the line numbers of the lines a mark touches, and data keeps anything you like with a mark (plain tables, strings, numbers), given back by buffer:decorations(ns). buffer:marks_at(pos) gives the marks over a place in every namespace. Language servers’ diagnostics are marks too, in the "diagnostics" namespace, which is how they follow edits.
  • Multibuffers show excerpts of many files in one buffer, edited in place: require("@multibuffer").open({ { path = path, from = 9, to = 12 } }, { summary = "3 places" }) gives a buffer to show in a panel or float, or in the shared results panel with .show(buffer, "Title"). An excerpt can carry marks to style ranges in it and a note shown after its file’s name. Edits in it change the files as you type, and changes to the files show in it. Search results, references and the problems list use one.
  • Images: greed.image.load(path) reads a PNG, JPEG or GIF and gives an id, and a mark with image = { id = id, cols = 20, rows = 8 } draws it over that many cells from its start. greed.image.cells(id) says how many cells it covers at its own size, and greed.image.cell_size() how many pixels a cell is.
  • PDFs: local doc = greed.pdf.open(path) reads one, doc.pages counts its pages, doc:render(page, scale) draws one as an image id at scale pixels per point, and doc:text(page) gives its text, line by line. All three wait, like greed.sleep.
  • Floats show a buffer over the main view, and panels beside it: greed.float(buffer, { title = "Info", width = "60%" }), greed.panel(buffer, { side = "right", width = 40 }). For text to read, core.floats.show_text(title, text, { kind = "my-info" }) opens a read-only float that esc or q closes, replacing the one before of that kind; anchor = "cursor" with a width and height opens it small by the cursor, like a hover. For a float to type in, like your own prompt, core.floats.typing(buffer, spec, { on_change = ..., on_close = ... }) switches to the editing style’s typing mode (insert, in Helix’s and Vim’s) and puts the mode and focus back however the float closes, whether by its close() or by closing its view; it’s what the prompt, pickers, the command line and the completion menu use. A float’s spec can also put text on its border (label at the right of the top, footer in the middle of the bottom, plain or in styled pieces), leave the border off (border = false, for a one-line strip), or hide it (hidden = true: its view and buffer stay, and focusing it shows it again; greed.floats({ hidden = true }) lists hidden ones too). Sizes and places (x, y) are cells, or shares of the screen like "37.5%", which keep a float in proportion on every attached client’s screen; core.floats.in_shares(box) turns a box in cells into those. anchor puts it in a corner or at an edge ("top-left", "right", …) when x or y is left out. With several clients attached, a float is shown only to the client it opened for, like a picker or a prompt, unless it’s shared = true (meant for everyone, like a review) or belongs to a tab (tab = true). Keep what a float you open belongs to apart for each client too, by greed.client.id(). To show something in a float of the tab, laid out by its float layout, use require("@panes").floats.show(buffer), and say a buffer needs the user with core.floats.attention(buffer, true).
  • Pickers (require("@picker").pick(title, items, choose, options)) filter a list as you type, with an optional preview, or ask a function for items as the query changes. on_move hears each item as it becomes the current one and on_cancel hears esc, for trying things out live. With paths = { dirs = true, choose = function(path) ... end }, typing a path lists the folder’s contents, and tab completes one into the query.
  • The completion menu asks sources before the language server, in any buffer, a file or not: require("@lsp").completion.source(function(view, ask) ... end) returns items like { label = "src/", text = "src/", from = 12 } (from is where the text it replaces starts; the word before the cursor by default), or nil to leave it to the next source. detail shows dimmed after the label and badge before it, a word saying whose it is (the shell’s own things say greed). ask.explicit is true when you asked with tab or ctrl-space rather than by a pause in typing. A source can wait, for a program say; its answer is dropped if more was typed meanwhile. A second argument says how its menu behaves: { enter = "selected" } has enter take only an item moved to while completion-enter is auto ("first", the default, takes the first); tab = "insert" makes tab complete in place as a shell does (the only match or what all matches start with at once, then each item in turn); and arrows = "after-tab" leaves up and down to the buffer while a menu that opened by itself hasn’t been moved into. The shell completes its prompt this way, with all three. A plugin that binds tab, enter or the arrows for typing in its own kind of buffer calls completion.keys_over(kind) so the menu keeps those keys while it’s open.
  • The theme maps style names to looks, as in greed.theme.set({ ["my-plugin.match"] = { fg = "yellow" } }). Use your own names so users can restyle them. Themes themselves go under those entries: greed.theme.base(entries) replaces the whole base, which is how the theme plugin switches themes.
  • The status line is pieces you can add to or take away from with require("@statusline").add and .remove, or replace with greed.statusline. A piece returning { text = "3 due", action = "notes-agenda" } runs the command when clicked, and style = "ui.statusline.warning" draws it as a warning.
  • greed.client.notify(title, body) sends a notification to your desktop through the client you used last: the terminal shows it (OSC 9, or OSC 777 in foot and urxvt) with a bell while it isn’t focused.
  • Features are what a plugin turns on for some buffers beyond their language, like notes for Markdown in the notes folder. Name yours with require("@core").features.add("todos", function(buffer) return ... end), and the status line shows it after the language (markdown · todos). core.features.of(buffer) lists the ones on.
  • Choices in Markdown (- ( ), - (x)) are read with require("@markdown").choices.list(buffer): each group with its question (the line above it) and its options, picked or not. choices.on_pick(fn) hears each pick, after the text has changed. A pick only edits text; what it does is up to the plugin listening.
  • require("@markdown").structure(buffer) reads a Markdown buffer once per edit and gives its headings (level, text, lines), code blocks (info string, fences’ lines, the code’s bytes), list items (bullet, todo box, lines), tables and links, with heading_at, code_at, item_at and table_at by line, and the lines’ text. It uses the syntax tree, so a # comment in a code block isn’t a heading. Give it a file’s text instead of a buffer to read that. markdown.conceal({ ns, wants, draw }) draws marks over the Markdown buffers wants says, calling draw(buffer, structure, first, last), which returns the marks, for the lines an edit changed (and any code block or table they touch) rather than the whole file. It returns a function that draws a buffer, or all of them, again in full.

Clients

Several terminals or windows can show one session at once, like your desktop and a laptop over greed ssh. Each is a client, with its own cursors, focus, mode, half-typed keys and macro recording; buffers and their undo history are shared. Code always acts for one client: a command for the client whose key, click or menu ran it, an event handler for the client the event is about, and what either starts (greed.spawn, a timer) for the same client. greed.view(), greed.mode.get(), greed.next_key() and selections all belong to that client.

greed.command({
	name = "who",
	run = function(ctx)
		local me = greed.client.current()
		greed.echo(`{me.name} ({me.kind}), one of {#greed.client.list()}`)
	end,
})

greed.client.current() and greed.client.list() describe clients: id, name (like tui@laptop), kind, remote, size, layout, color (the color its cursors show in for the others) and person. ctx.client is the id of the client a command runs for, and events about a client carry it as event.client.

Waiting

Commands and event handlers can wait without freezing the editor, and waiting always looks the same: the call returns once the answer is there.

greed.command({
	name = "rename-file",
	run = function(ctx)
		local name = core.prompt.ask("New name", ctx.buffer:path())
		if name == nil then
			return -- cancelled
		end
		local moved = greed.process.run({ "git", "mv", ctx.buffer:path(), name })
		if moved == nil or moved.code ~= 0 then
			greed.echo(moved and moved.stderr or "git isn't installed")
			return
		end
		local choice = picker.choose("Open it?", { { label = "yes" }, { label = "no" } })
		if choice and choice.label == "yes" then
			greed.open(name)
		end
	end,
})

greed.sleep(ms), greed.next_key(), greed.process.run(...), greed.http.request(...), greed.lsp.request(...), greed.fs.grep(...) and greed.plugin.check(...) wait like this, and so do picker.choose(title, items) and core.prompt.ask(title), which give nil when you close them. (picker.pick and core.prompt.open take callbacks instead, for pickers that stay open while you work. core.prompt.open returns the prompt’s view, to put a label on its border, and its kind option gives the prompt keys of its own, the way the vscode style’s find bar binds enter to the next match.) greed.signal() lets one piece of code wait for a value another sends. greed.animate(ms, step) calls step(t) once a frame, which is all the flash on copy is. greed.fs.watch(path) makes a “file.changed” event come whenever that file changes, from anywhere; it’s how your config reloads.

A search the user types should mind case as they set it: core.search.pattern(regex) gives the regex to search with, as search-case says, and greed.fuzzy(query, items, { case = core.search.case() }) the same for fuzzy matching. A kind of search with a better default of its own gets an option for it, KIND-search-case, from core.search.kind(name, default, what), as notes do with core.search.kind("notes", "ignore", "searching notes"); then pass the kind, core.search.pattern(query, "notes").

greed.run(name) starts a command and returns as soon as that command waits for something, so the code after it doesn’t see what the command did after a key or a reply. greed.run(name, { wait = true }) waits until the command has finished, the same way as the calls above.

Waiting only works where code runs as a coroutine: commands, event handlers, and greed.spawn(f, ...), which runs f that way and returns at once, for work in the background. Called anywhere else, like a status line piece or a plugin’s top level, the calls that wait raise an error instead of running on the editor’s thread, so no plugin can freeze the editor on a slow program or server.

Heavy work doesn’t freeze it either. A command or handler that works for more than 15 ms without waiting is paused, the editor handles keys and draws, and it carries on where it was, so a slow plugin is only slow itself. On Linux only time spent working counts toward the 15 ms, so a busy machine doesn’t change where code is paused. coroutine.yield() with nothing pauses the same way on purpose. Your own coroutines, like a generator made with coroutine.wrap, are never paused this way. One that works for a whole minute without finishing is stopped and Greed says so. Code that can’t be paused, like a status line piece or a plugin’s top level, is timed instead: anything that holds the editor up for 50 ms or more is kept with its plugin and what it was doing, :stalls lists them, and over 250 ms Greed tells you right away. To see where time goes when nothing stalls, :profile (or greed.profile.start() and stop()) counts every run of plugin code and the editor’s own work by plugin and what it did, the most time first.

Keys typed while a key’s command is still at work wait their turn, so typing ahead gives the same result as typing slowly. At work means paused as above, or waiting on a timer, a program, a request or a command that is. A command waiting for you (greed.next_key(), a prompt, a picker, any greed.signal()) isn’t at work, and gets the next keys as they come. Keys wait half a second at most, then run anyway. esc and ctrl-c get through to a command at work at once, so they can cancel it, but behind keys already waiting they keep their place. Pastes and clicks wait in line with the keys.

An error your command, handler or task doesn’t catch shows on the status line with your plugin and what it was running (“myplugin (on buffer.opened): …”). The last 100 are kept with where in your code they happened: :errors lists them, greed.errors() returns them, and a session also writes them to its log. A message of several lines, from greed.echo or an error, shows its first line on the status line; :messages shows recent ones whole, and greed.messages() returns them.

Whatever you pass the API, it answers with a value or an error: a position past the end, a closed buffer or a size no screen has is an error you can pcall, or is kept in range where the function says so. Should your code run into a bug in Greed itself, that becomes an error too, starting “internal error, a bug in Greed”, kept like the others while the editor carries on. Please report those.

Language servers ask things too: greed.lsp.on_request(method, handler) answers a server’s request, e.g. workspace/applyEdit, with what the handler returns, and the handler can wait, e.g. on a picker, before it answers. greed.lsp.on_notification(method, handler) hears what servers tell the editor without asking, like $/progress; the handler gets the parameters, the server’s language and its root. The status line’s language server piece is built on it.

A server runs per language and project root. greed.lsp.root(buffer) is the folder the buffer’s server runs in, and greed.lsp.running() lists the servers running, each with its language, root, command, whether it’s ready, and the last lines it wrote to stderr. A request whose server stops before answering returns nil and an error saying so.

Events

A plugin reacts to what happens in the editor with greed.on(event, handler). The handler gets one table describing what happened. Format on save, the status line, reloading files changed on disk, auto-save and following an agent around are all built this way.

local greed = require("@greed")

-- Trim spaces at the ends of lines whenever a file is saved.
greed.on("buffer.saved", function(event: greed.BufferChanged)
	local buffer = event.buffer
	local trimmed = string.gsub(buffer:text(), "[ \t]+\n", "\n")
	if trimmed ~= buffer:text() then
		buffer:edit(function(e)
			e:replace(0, buffer:len(), trimmed)
		end)
		buffer:save()
	end
end)
EventWhenWhat the handler gets
buffer.changedAfter every edit to a buffer: yours, a plugin’s, an agent’s, or the file changing on diskbuffer, and from_line and to_line: the lines that changed since the last one, as they are now. buffer:version() grows with each edit, for caches
buffer.openedA buffer was openedbuffer
buffer.savedA buffer was written to its filebuffer
buffer.closedA buffer was closedid, the closed buffer’s
file.changedA file watched with greed.fs.watch(path) was written, created or removed, by anythingpath
mode.changedA client’s mode changedfrom, to
command.runA command is about to runname, the keys that ran it, count
selection.changedA view’s selection changed for a client, or it shows another buffer, by anything: keys, the mouse, undo, a plugin or an agent. Once a frame per view, before it’s drawn, however many changes there wereview
view.focusedAnother view has a client’s focus: a pane, a float or a panel. Once a frame, before it’s drawnview, from, the view that had it (nil if it was closed), and moved, true when it moved under the client: another client showed another tab or closed what it had focused, or it followed again
keys.pendingA key sequence is under way, e.g. after space; "" once it’s donekeys
option.changedAn option was setname, value
lsp.diagnosticsA language server reported problems for a bufferbuffer; read them with greed.lsp.diagnostics(buffer)
view.resizedA view has room, for a client, for a different number of rows or columnsview, rows, cols
view.scrolledA view starts, for a client, at a different line; what the handler changes shows in the same frame, so one view can scroll another alongview, line
view.closedA view was closed, e.g. a plugin’s panelview, the closed view’s id
pane.openedA split opened, from whichever plugin made itview, and from, the pane beside it
mouseThe mouse pressed, released, dragged, moved or scrolledkind, button or dir, the screen col and row, the keys held, and what’s under it: view, pos in the buffer, a floating view’s border, or a tab
terminal.outputA terminal’s program printed somethingterminal; read it with terminal:screen()
terminal.exitedA terminal’s program endedterminal, code
client.attachedA client attached to the session, e.g. greed FILEpath it asked to open, whether it wants a terminal, whether it came over greed ssh (remote)
client.detachedA terminal or window went; the session goes on without itclient
client.pasteText was pasted from the system clipboardtext
control.requestA request came on the session socket (greed ctl, agents)id, method, params, client, connection; answer with greed.control.reply
control.closedA session socket connection closed, after its last request came; requests from it still waiting have nobody to answerconnection
plugin.loadedA plugin was loaded or reloadedname, dir
plugin.unloadedA plugin was unloadedname
editor.startedOnce, when the editor is up with every plugin and your config loadednothing
editor.quittingJust before the editor quitsnothing

space h e (:describe-event) shows any event’s fields, and the types that come with require("@greed") have them all. Give the handler’s parameter its type, as above (greed.BufferChanged), so the type checker catches a misspelled field. A name that isn’t on this list, like "buffer.chnaged", is an error, from the type checker and when the code runs.

A handler stays until its plugin unloads or greed.off(event, handler) removes it, given the same function. Removing one that’s gone does nothing, and a plugin can only remove its own. That lets something listen only while it’s open:

local function show_notes(buffer: greed.Buffer)
	local view = greed.float(buffer, { title = "Notes" })
	local function saved(event: greed.BufferChanged)
		if not view:valid() then
			greed.off("buffer.saved", saved)
		elseif event.buffer == buffer then
			greed.echo("notes saved")
		end
	end
	greed.on("buffer.saved", saved)
end

Who made an edit is in the buffer’s history: buffer:history({ limit = 1 })[1].author is "user", "plugin", "agent" or "external" (the disk), so a handler can skip the edits it doesn’t care about, or its own.

A plugin with buffers of its own kind can take their mouse events first with require("@core").mouse.claim(kind, handler).

Writing handlers that stay fast

Each handler runs in a coroutine of its own as soon as the event happens, so it can wait like a command (see Waiting) and a slow one is paused after 15 ms instead of holding up the editor. Still, some events come often: buffer.changed on every key you type in insert mode, mouse on every move, view.scrolled on every line scrolled. Handlers for those should do little each time, and leave heavy work until things are quiet.

The usual way is to count, wait, and only act if nothing came since. This is how auto-save works:

local edits = {}

greed.on("buffer.changed", function(event)
	local id = event.buffer:id()
	local this = (edits[id] or 0) + 1
	edits[id] = this
	greed.sleep(1000)
	if edits[id] == this then
		-- A second without edits: do the heavy work now.
	end
end)

A handler that edits the buffer that changed makes another buffer.changed, so check whether there’s anything to do first, as the trimming example does, or it goes round forever.

Options

greed.option.define({ name, doc, default }) adds an option users set with :set name value or greed.option.set(name, value) in their config. The default fixes its type, a boolean, a number or a string, and setting it to anything else is refused, as is a name no plugin defined (the error names options with names like it). greed.option.get(name) reads it, and option.changed says when it changes. When a plugin is reloaded, an option it defines again with the same type keeps the value it was set to. greed.option.set(name, value, { client = true }) sets a value for the client you’re at alone, like a window’s font size; get gives that client its own value when it has one.

greed.option.define({
	name = "trim-on-save",
	default = true,
	doc = "Trim spaces at the ends of lines when saving",
})

greed.on("buffer.saved", function(event: greed.BufferChanged)
	if greed.option.get("trim-on-save") then
		-- ...
	end
end)

An option that takes one of a few values lists them as choices. Setting it to anything else fails with an error naming them, :set offers them as you type, and :describe-option shows them:

greed.option.define({
	name = "trim-style",
	default = "trailing",
	choices = { "trailing", "all", "off" },
	doc = "What trim-on-save takes away",
})

An option whose value gets run, like a command line, says which capability setting it takes with needs. Then only code with that capability can set it, besides you (:set, your config), so a plugin without proc can’t make yours run a program of its choosing. Greed’s own options like link-opener and terminal-shell need proc, and agent-eval needs eval. An option belongs to the plugin that defined it: another plugin can’t define it again.

greed.option.define({
	name = "deploy-command",
	default = "make deploy",
	needs = "proc",
	doc = "What :deploy runs",
})

Programs and terminals

greed.process.run(command, opts) runs a program to the end and returns what it printed. greed.terminal.spawn({ command, rows, cols }) starts one in a terminal: terminal.output events say there’s something new to read with terminal:screen() and terminal:scrollback(from), and terminal:key("ctrl-c"), terminal:write(text) and terminal:resize(rows, cols) talk back. Styles in a screen are inline names like =fg:red bold, which you can use in your own marks too. A row says wrapped when its line carries on on the next row, so long lines can be joined again. terminal:modes() tells what kind of program runs: alternate (a full screen program), canonical and echo (both off in raw mode, only echo off while a password is read), mouse, app_cursor and bracketed_paste. In env, a variable set to false is left out of what the program inherits. A terminal ends with the plugin that started it.

greed.process.spawn(command, { cwd, env }) starts a program to talk to over its input and output, like an agent or a server speaking JSON-RPC. child:write(text) sends it input, child:read() waits for the next piece it prints (nil once it exited), child:close() closes its input, and child:wait() waits for its exit code, or nil and the signal that ended it (nil, "INT"). It ends with the plugin that started it, too.

greed.spawn(function()
	local child = assert(greed.process.spawn({ "sh", "-c", "read x; echo got $x" }))
	child:write("hello\n")
	greed.echo(child:read()) -- got hello
end)

What it prints as errors is kept for child:stderr(). With stderr = "stream" it arrives from child:read() as well, in the order it comes, and read says where each piece came from. With group = true the program starts a process group of its own, so child:signal("INT") and child:kill() reach whatever it started too, as ctrl-c does in a shell. As for terminals, false in env leaves a variable out.

greed.spawn(function()
	local child = assert(greed.process.spawn({ "make" }, { stderr = "stream", group = true }))
	while true do
		local piece, from = child:read()
		if piece == nil then
			break
		end
		greed.echo(`{from}: {piece}`)
	end
	print(child:wait()) -- 0, or nil and "INT" after child:signal("INT")
end)

To show a terminal you started yourself the way :terminal shows one, hand it to the terminal plugin: require("@terminal").adopt(terminal, { return_on_primary = true }) shows it in the focused view, sized for your screen and typing into it, and with return_on_primary gives the view back once a full screen program is done or the program exits. The terminal stays yours to read and close. references(buffer, { from, to, cwd }) finds the files and lines some output names, and row_marks turns a terminal’s rows into marks for a buffer of your own.

The shell

require("@shell") is the shell: shell.run(sh, line) runs a line in a shell as a new block, shell.of(buffer) is the shell a buffer shows, and shell.wait(block) waits for a block to finish. A block’s record has what ran and how it ended, its output, and block.value when the output was JSON.

block.table is the output read as a table, when it was one: columns (the header’s names, in order) and rows, each a map from a column’s name to its cell’s text. shell.tables reads and works on them: tables.detect(text, command), tables.from_value(json), tables.where(tbl, "AGE < 1h"), tables.select(tbl, "NAME,AGE"), tables.sort(tbl, "AGE -r") (each a new table), tables.number("512Mi"), and tables.draw(tbl) lines it up as text; tables.columns(text, true), tables.csv(text), tables.from_json(text), tables.split(text, ":") and tables.lines(text) read text that’s known to be a table.

shell.parser(match, parse) reads the output of the commands match names as tables, ahead of the shell’s own look for one: a program’s name, or a pattern for the command line. parse(text, block) returns a table, a list of objects as JSON has them, or nil to leave it to the shell.

local shell = require("@shell")

-- `terraform state list` prints one address a line.
shell.parser("^terraform state list", function(text)
	local rows = {}
	for address in string.gmatch(text, "[^\n]+") do
		table.insert(rows, { address = address, type = string.match(address, "^[%w_]+") or "" })
	end
	return rows
end)

shell.rows.row_at(sh, block, pos) is the row under a position in the buffer, filtered and sorted as the block shows it, for actions on a row:

local shell = require("@shell")

greed.command({
	name = "pod-logs",
	run = function(ctx)
		local sh = shell.of(ctx.buffer)
		local pos = ctx.view:cursor()
		local block = sh and shell.log.block_at(sh, pos)
		local row = block and block.table and shell.rows.row_at(sh, block, pos)
		if sh and block and block.table and row then
			shell.run(sh, `kubectl logs {block.table.rows[row].NAME}`)
		end
	end,
})

shell.completer(name, complete) completes program name’s arguments at a shell’s prompt, ahead of Cobra, fish, carapace and the program’s help. complete(request) gets the command’s words (the program first, the word being completed last), its line so far, the cwd and env it would run with, and explicit (you pressed tab), and returns candidates as text or { text, detail }, or nil to leave it to the other sources. It can run programs; shell.sources.run(name, argv, cwd, env) runs one the way the shell’s sources do, stopped by a newer run of the same name and by shell-completion-timeout.

local shell = require("@shell")

-- `deploy ENV`: the environments are the folders under deploy/.
shell.completer("deploy", function(request)
	if #request.words ~= 2 then
		return nil
	end
	local found = {}
	local entries: { greed.DirEntry } = greed.fs.list(request.cwd .. "/deploy") or {}
	for _, entry in entries do
		if entry.dir then
			table.insert(found, { text = entry.name, detail = "environment" })
		end
	end
	return found
end)

shell.segment(name, provider) puts a piece in every shell’s prompt before its folder, after the shell’s own (the git branch, the kubernetes context and the project environment). provider(ctx) gets the shell, its cwd and the env its programs get, and returns { text, style, warning } or nil for nothing; warning = true turns the prompt’s ❯ red too. It runs in the background when a shell opens and after each command, so it can run a program; the prompt shows the new pieces once every segment is done. With shell-prompt set to starship, starship draws the prompt instead, and segments only add their warnings.

local shell = require("@shell")

-- The AWS profile in use, from the shell's variables.
shell.segment("aws", function(ctx)
	local profile = ctx.env.AWS_PROFILE
	if profile == nil then
		return nil
	end
	local production = string.find(profile, "prod") ~= nil
	return {
		text = `aws:{profile}`,
		style = if production then "shell.segment.warning" else "shell.segment",
		warning = production,
	}
end)

require("@vcs").branch(dir) is the branch of the repository a folder is in, as the status line shows it.

shell.environment(name, provider) adds a kind of project environment, like devenv’s, for shell-environments to name. provider.root(dir, env) says which folder’s environment applies in dir, or nil, with a reason when there’s one it won’t load (“not allowed”); it runs after every command, so keep it quick. provider.load(root, env) loads it and returns { vars, watch, output }: the variables to set (a string) or leave out (false), the files whose change loads it again, and what loading printed. It can run programs and wait; an error says why it failed.

local greed = require("@greed")
local shell = require("@shell")

-- A folder with a .env file brings its NAME=VALUE lines.
shell.environment("dotenv", {
	root = function(dir)
		return if greed.fs.stat(`{dir}/.env`) then dir else nil
	end,
	load = function(root)
		local vars = {}
		for _, line in string.split(greed.fs.read(`{root}/.env`) or "", "\n") do
			local name, value = string.match(line, "^([%w_]+)=(.*)$")
			if name and value then
				vars[name] = value
			end
		end
		return { vars = vars, watch = { `{root}/.env` }, output = "" }
	end,
})

:set shell-environments "devenv dotenv" then asks devenv first.

shell.stage(spec, run) adds a table stage like where. spec has its name, usage and doc (for help and completion), args (“column”, “columns”, “condition” or “format”, for completion), and starts = true when it may follow a program too, as in kubectl get pods | NAME; give that only to names no program has. run(tbl, rest) gets the table and the rest of the stage’s words, and returns a new table ({ columns, rows }, each row a map of column to text), text (which ends the stages, as to csv does), or nil and why it can’t.

-- `top COL N`: the N rows with the most in a column.
shell.stage({ name = "top", usage = "top COL N", doc = "The rows with the most in a column", args = "column", starts = true }, function(tbl, rest)
	local name, count = string.match(rest, "^(%S+)%s*(%d*)$")
	local column = name and shell.tables.column(tbl, name)
	if column == nil then
		return nil, "top COL N, like top RESTARTS 5"
	end
	local rows = shell.tables.sorted(tbl, shell.tables.all(tbl), column, true)
	local kept = {}
	for i = 1, math.min(tonumber(count) or 10, #rows) do
		kept[i] = tbl.rows[rows[i]]
	end
	return { columns = tbl.columns, rows = kept }
end)

shell.builtin(spec, run) adds a command the shell runs itself, ahead of programs and aliases of that name (^NAME still runs the program). spec has name, usage, doc and args as builtins have them. run(args, ctx) gets the words after the name, and ctx with the shell, the block, its cwd, and write(text) and error(text) to print in the block; it returns the exit code, 0 when it returns nothing, and an error fails the block with its message. It can wait, as commands can. Neither can replace the shell’s own, and both go when the plugin does.

shell.kind(spec) says what the rows of some commands’ tables are, so they’re things to act on like any other (core.actions). spec.match names the commands as shell.parser does. spec.capture(block, shell) runs once as the block finishes and keeps what the rows can’t say themselves, like the context a command ran against; returning nil says the block isn’t this kind’s after all. spec.row(row, captured, block) gives { kind, label, value } for a row (its cells by column), and spec.called(captured, block) what one row and several are called in the block’s status (“pod”, “pods”). Rows then take the actions of their kind, whoever added them; ls -l rows are file things, so every file action works on them.

spec.complete makes the rows complete later commands. It maps a program’s name to a function given the command’s words (the word being completed last) and the shell; it returns nil when no thing of this kind fits there, or a function giving the word to put in for a thing’s value, nil for one that doesn’t fit (from another context, say). The rows of the newest blocks come first in the menu, before what the program offers itself.

spec.streams(words) says whether a command line of the kind keeps printing rows again as they change, as kubectl get pods -w does. The shell then reads it through a pipe and shows each row once, in its place, known by its first cell (its first two under a NAMESPACE column).

shell.action_command(kind, name, command, opts) adds an action that runs a command as a block in the shell, so what it does is there to read and run again. command is a template whose {field}s are the thing’s value’s fields, quoted, or a function from the value to the line. opts.doc says what it does, opts.danger makes it ask first (saying when the value’s context is a production one), and opts.default makes it what enter on such a row does.

-- `systemctl list-units`'s rows as services.
shell.kind({
	name = "systemd",
	match = "^systemctl list%-units",
	row = function(row)
		local unit = row.UNIT
		if unit == nil or not string.match(unit, "%.service$") then
			return nil
		end
		return { kind = "service", label = unit, value = { unit = unit } }
	end,
	called = function()
		return "unit", "units"
	end,
})
shell.action_command("service", "status", "systemctl status {unit}", { doc = "How it's doing", default = true })
shell.action_command("service", "restart", "sudo systemctl restart {unit}", { doc = "Restart it", danger = true })

The command line

greed.cli({ name, doc, usage, run }) adds a command to greed itself. greed NAME ARGS starts an editor with no screen, with your plugins and config, and calls run with the arguments. It can wait like any command (on greed.process.run, greed.http.request, greed.sleep); what it echoes is printed as it goes, what it returns is printed at the end, and an error makes greed exit with a failure.

greed.cli({
	name = "hello",
	doc = "Say hello",
	usage = "[NAME]",
	run = function(args)
		return `hello {args[1] or "there"}`
	end,
})

greed hello world prints hello world, and greed commands lists it. Names are lowercase words with dashes; a file of that name in the current folder wins, and greed NAME with no such command opens NAME as a file.

The network

greed.http.request({ url, method, headers, body, timeout }) returns the whole response (status, headers with lowercase names, body), or nil and why. greed.http.stream(...) returns once the status and headers are in, and stream:read() gives the body piece by piece as it arrives, which is how model APIs stream their answers:

local stream, err = greed.http.stream({
	url = "https://api.example.com/v1/generate",
	headers = { ["content-type"] = "application/json" },
	body = greed.json.encode({ prompt = "Hello", stream = true }),
})
if stream == nil then
	error(err, 0)
end
while true do
	local piece = stream:read()
	if piece == nil then
		break
	end
	greed.echo(piece)
end

Each request runs on a thread of its own and both wait like greed.sleep, so they work in commands and event handlers and never hold up the editor. A plugin needs net, or net:HOST for each host it talks to.

To download a file, give request a save path: a successful (2xx) response’s body is written there as it arrives, with no size limit, and the file appears only once it’s whole. Any other response comes back with its body as usual and nothing is written. Saving needs fs.write too.

local got, err = greed.http.request({ url = url, save = cache .. "/grammar.wasm" })
if got == nil or got.status >= 300 then
	error(err or `status {got.status}`, 0)
end

Keys for those servers come from greed.secrets.get("anthropic"), never from config or plugin source. It looks in the environment (GREED_SECRET_ANTHROPIC, or the usual ANTHROPIC_API_KEY), then the system keyring, then Greed’s own secrets file, and waits like the rest. A plugin declares each secret it reads as secrets:NAME, and Greed’s file APIs refuse to read the secrets file itself.

Models

The models plugin talks to language models for you. Ask for a role, not a provider, and the user’s config decides which model plays it:

local models = require("@models")
local stream, err = models.stream("fast", {
	system = "Answer in one sentence.",
	messages = { { role = "user", content = "What is a rope data structure?" } },
})
if stream == nil then
	error(err, 0)
end
while true do
	local piece = stream:next()
	if piece == nil then
		break
	end
	greed.echo(stream.text)
end

models.ask(role, request) waits for the whole answer instead. Both wait like the rest of the API. The roles that come set up are fast, reasoning and local; a plugin can ask for any role, and the user maps it in their config or in :models.

A role of your own can use another role’s model until the user sets one, and :models lists it with the model it uses then:

models.fallback("summary", "fast", "summaries of long notes")
models.playing("summary") -- "fast" until models.roles.summary is set

Before asking, models.check(role) says what’s missing without sending anything: no model plays the role, its provider isn’t set up, or it has no key. models.no_key(provider) is the message every feature shows for a missing key, naming the variable that sets it and :models.

For a model that calls tools, models.turn(role, request, on_text) runs one turn: request has the conversation as said (each { role = "user", text = ... }, { role = "assistant", text = ..., calls = ... } or { role = "tool", call = ID, text = ... }), tools (each a name, a description and a JSON schema as input) and an optional system. on_text gets the answer’s text as it arrives, and the turn comes back as its text, the calls it asks for (id, name, input) and stop: "done", or "tools" when it wants them run and their results sent in the next turn. It works the same with Anthropic and with servers that speak OpenAI’s chat completions. A user’s turn can carry images, a list of PNG file paths read when the request goes out; models.sees_images(role) says whether the role’s model can see them, and they’re left out for one that can’t.

Providers you sign in to

Some plans come with an account instead of an API key, like Berget Code. A provider like that names a login, and :login NAME signs you in with OAuth’s device flow: Greed shows a link and a code, you approve it in a browser on any device, and the tokens are kept with your other secrets and renewed before they run out. Once you’re signed in, the provider sends the token instead of its key.

Add one in your init.luau or a plugin, from what the provider’s sign-in server says about itself (its OpenID configuration usually lists the two endpoints):

local models = require("@models")

models.login.logins.acme = {
	title = "Acme AI",
	device_url = "https://auth.acme.example/oauth/device/code",
	token_url = "https://auth.acme.example/oauth/token",
	client_id = "acme-cli",
	scope = "openid offline_access",
}
models.providers.acme = {
	kind = "openai",
	url = "https://api.acme.example/v1",
	login = "acme",
	-- Optional: a key to use when you aren't signed in.
	secret = "acme",
}
models.roles.agent = { provider = "acme", model = "acme-large" }

That is how your config does it. A plugin gets other plugins’ modules read-only, so it adds the provider and the role with models.provider("acme", { ... }) and models.role("agent", { ... }); both go when the plugin does. Adding a login works the same from both.

Then :login acme, and :models says “signed in”. :logout acme forgets the tokens, and the provider goes back to its key. The name is used for the secret the tokens are kept in, so it takes lowercase letters, digits and dashes. Once a login is added it can’t be replaced or changed, and its tokens stay inside the models plugin: no plugin can read them or send them elsewhere.

Any plugin can add a provider, so a key or a sign-in goes only where the user let it: the built-in providers’ keys to their own servers, and any other the first time it would go somewhere new, after Greed asks (“Send your acme key to https://api.acme.example from now on? Type yes to allow it”; anything but yes says no). The answer is kept, so each key and address asks once.

scope needs whatever the server wants for a refresh token, often offline_access; without one, you’d sign in again whenever the token runs out. Tokens are renewed with OAuth’s own refresh at token_url. A provider that renews another way gives a renew function, which gets the tokens (access, refresh, expires in seconds like os.time()) and returns new ones, or nil and why. Berget’s posts the refresh token to its own API, like this:

renew = function(tokens)
	local status, answer = models.login.post(
		"https://api.acme.example/v1/auth/refresh",
		{ refresh_token = tokens.refresh },
		"json"
	)
	if status ~= 200 or type(answer.token) ~= "string" then
		return nil, `Acme said {status}`
	end
	return {
		access = answer.token,
		refresh = answer.refresh_token or tokens.refresh,
		expires = os.time() + answer.expires_in,
	}
end,

models.login.post(url, body, "form" | "json") sends a POST and returns the status and the answer decoded from JSON. Only the device flow is supported for signing in; a provider that only signs in through the browser on your own machine can’t be added this way yet.

Agents

Plugins can offer tools to agents. A tool shows up in Claude Code (through greed mcp) and as greed ctl NAME in the shell:

require("@agent").tool({
	name = "word_count",
	description = "How many words the current file has",
	run = function()
		local _, words = string.gsub(require("@greed").view():buffer():text(), "%S+", "")
		return tostring(words)
	end,
})

A plugin offering tools lists agent in requires. The tool’s parts:

Field
nameLetters, digits, _ and -
descriptionWhat it does, for the agent deciding whether to use it. Say when to use it and what comes back
inputA JSON Schema for the arguments; agent.schema.object({ path = { type = "string" } }, { "path" }) makes an object’s, the second list naming the required ones. A call with an argument it doesn’t name (unless additionalProperties allows any) or without a required one fails before run, saying what it takes. Left out, it takes none
groupThe group it’s offered with; left out, it’s always offered (below)
kindWhat it does beyond reading: "edit" (changes files or notes), "execute" (runs code or programs) or "fetch" (reaches the network); left out, "read"
reviewstrue when the tool puts what it does in front of the user itself, as a proposal or a question does
runrun(args, caller) does the work

Greed’s own agent asks the user before a tool whose kind isn’t "read" runs, the way it asks before its own edits, unless the tool reviews; MCP clients see the others marked read-only. Set kind on every tool that changes, runs or fetches something, so it isn’t run unasked. sandboxed = true marks a tool that does more than read but needs no say from the user, like checking a plugin in an editor of its own. shell_only = true keeps a tool for greed ctl and the programs Greed’s shell runs, like greed edit: agents aren’t offered it and can’t call it.

Agents connected when a plugin loads or unloads hear that their tools changed, so a tool from a plugin you’ve just approved can be used in the same conversation.

run gets the arguments as a table, and who called as caller, whose kind is "shell" for greed ctl, "socket" for MCP clients like Claude Code, "ide" for Claude Code’s IDE connection and "agent" for Greed’s own agent. Any plugin can call a tool with any caller, so don’t decide trust on kind: agent.from_shell(caller) says whether the call came from greed ctl, whose user can run anything already.

A string run returns goes to the agent as it is, and any other value as JSON, even a table with a content field. To answer with MCP’s result form itself, return agent.result(content, is_error), where content is text or a list of pieces like { type = "text", text = "..." }. An error goes back as an error, so error("there's no file " .. path, 0) tells the agent what went wrong.

Hand text someone else wrote, like a web page or another agent’s report, back through agent.untrusted(source, text): it’s wrapped in markers the text can’t close, after a line telling the model to use it as information and not to follow instructions in it.

A tool can wait: for a process, a request, or the user. Changes to files are best proposed, so the user reviews them first and the tool hears what they decided:

local agent = require("@agent")

agent.tool({
	name = "add_license",
	description = "Propose adding the license header to a file",
	input = agent.schema.object({ path = { type = "string" } }, { "path" }),
	run = function(args)
		local text = require("@greed").load(args.path):text()
		local first = string.match(text, "^[^\n]*\n") or text
		-- Waits until the user has accepted or rejected it.
		return agent.propose(args.path, {
			{ old_text = first, new_text = "-- SPDX-License-Identifier: MIT\n" .. first },
		}, "add the license header")
	end,
})

agent.propose_files proposes changes to several files as one operation, agent.propose_ranges does it by byte ranges (for edits from a language server or a diff), and agent.ask(title, questions) asks the user and waits for their answers. When an MCP client’s connection closes while its proposal waits, what’s left of the proposal is rejected and the tool call returns.

Each tool costs the agent tokens on every request, so a plugin with several should put them in a group, which agents switch on with the tools tool when they need it:

agent.group("todo-sync", "Syncing todos with the issue tracker")
agent.tool({ name = "issues_list", group = "todo-sync", description = "...", run = list })

The group’s line is what an agent reads when deciding whether to switch it on, so say what it’s for. See Agents for the groups Greed has.

Tests

local test = require("@greed/test")

test("greet says hello", function(t)
	t:run("greet", { "world" })
	assert(t:messages()[1] == "hello world")
end)

test("copy and paste at every selection", function(t)
	-- | marks each selection's head, ^ its anchor
	t:buffer("^ab| ^cd|")
	t:keys("y p")
	t:expect("ab^ab| cd^cd|")
end)

greed test DIR runs a plugin’s tests, each in a fresh editor with the default plugins loaded. Give it several folders (greed test runtime/plugins/*/) to test several plugins at once. Tests run side by side, as many at a time as the machine has cores, and the results are printed plugin by plugin.

t:keys("i") presses keys and t:type("hello world") types text a character at a time, newlines as enter. t:draw(cols, rows) and t:screen(cols, rows) give what a client would show, as lines of text, t:status() the status line, t:floats() the floats’ text and t:notices() the desktop notifications sent; t:clipboard_image(png) sets what greed.clipboard.image() gives. t:wait(ms) moves the clock forward for code that sleeps, t:wait_for(check) waits for real until check() is true (for a program’s output), and t:tempdir(files) makes a folder of files to work on; t:open(path) opens one of them as greed.open does, without your plugin needing fs.read for it. local dir, git = t:repo("git", files) makes a repository (jj or git) with the files committed and returns a function that runs git there and fails the test if it fails; t:vcs("jj", dir) gives that function for a folder you set up yourself.

Tests don’t show the message about code that held the editor up, so a busy machine doesn’t change what t:messages() returns. Each key a test presses and each command it runs gets the time limit a key press gets in the editor, and time spent waiting for programs and background work doesn’t count toward it.

t acts as the session’s first client. t:client(name) adds another, as if a second terminal attached, seeing what the first one does; it has keys, run, mouse, draw, screen, status, marked, expect, expect_mode and id, all as that client:

test("each client has its own mode", function(t)
	t:buffer("|hello")
	local laptop = t:client("laptop")
	laptop:keys("i")
	laptop:expect_mode("helix-insert")
	t:expect_mode("helix")
end)

Types

The API is fully typed in runtime/types/greed. greed new sets up a .luaurc so luau-lsp in your editor and luau-analyze --mode=strict check your plugin against it.

greed reference DIR writes the reference pages this site has, as Markdown: every loaded plugin with its commands, keys and options, every option, and the whole API. With your config and plugins loaded, they describe your own setup.

Loading and reloading

Greed loads the default plugins, then yours, then init.luau. Each plugin runs in its own environment and owns what it registers: reloading a plugin replaces its commands, keys, options, handlers and theme entries, and greed.plugin.unload(name) removes them. Both also stop what the plugin was running (its tasks from greed.spawn, and commands and handlers waiting on a timer or a reply), clear the marks it set (in a namespace two plugins write to, the marks belong to whichever set them last), close its floats and panels, forget its images, stop its greed.fs.watch watches, and put back the file types and language servers it replaced. A plugin that runs too long is stopped, so a mistake in a loop can’t hang the editor.

What a plugin adds through another plugin goes with it too: its agent tools, status line pieces, actions, features, styles, command line aliases and the like. There’s no need to pass your plugin’s name or to clean up on plugin.unloaded.

Keeping a registry of your own

A plugin that lets others add to it, like a list of handlers, does the same with greed.plugin.tie(remove): called in the function others call, it runs remove when the plugin that called it is unloaded or reloaded, and returns that plugin’s name. Calls from your own plugin aren’t tied, since what they added goes with your plugin anyway. greed.plugin.caller() just says who called. require("@core").registry makes the remove: set puts a value in a table under a key, put adds to a list, and each returns a function that takes the entry out unless it was replaced since. For an entry you put in a list yourself, remover(list, entry) makes that function.

local core = require("@core")

local M = {}
local greeters: { [string]: () -> string } = {}

function M.greeter(name: string, greet: () -> string)
	greed.plugin.tie(core.registry.set(greeters, name, greet))
end

Other plugins’ modules are read-only

require("@name") gives a plugin a read-only view of another plugin’s module: reading works as usual, nested tables too, and setting anything raises an error, so no plugin can replace another’s functions or settings. Offer functions for what others may change, as core.style.typing_kind or models.role do. Your config, a trusted project’s .greed folder, the REPL and tests get the modules themselves, so the settings the docs show you assigning, like models.roles.fast = ..., work there. Go through a view with for k, v in t do and #t; pairs, ipairs and the table functions don’t see through it.

Trying code

:lua CODE runs Luau in a scratch environment whose globals last, and := EXPR shows what an expression is. In any buffer, space l e runs the selection, or the line the cursor is on, the same way, and shows what it returned at the end of its last line (or the error); the next edit takes it away. Handy while writing a plugin or your init.luau:

#greed.buffers()  -- space l e here shows how many are open, like  ⇒ 3

Agents

Agents use Greed the way you do, as clients of the running editor. They can read buffers with your unsaved changes, see what you have selected and what the language servers report, open files for you, and propose edits that you review in the buffer. Every tool is also a shell command, for scripts and agents that prefer a CLI.

From the shell: greed ctl

greed ctl tools                       # what the editor offers
greed ctl read --help                 # what a tool does and the arguments it takes
greed ctl focus                       # the current file, selections, visible lines
greed ctl read path=src/main.rs from_line=10 to_line=20
greed ctl diagnostics
greed ctl eval code='return require("@greed").mode.get()'

Arguments are key=value pairs (values that parse as JSON are JSON) or one JSON object; an argument a tool doesn’t take, or a missing one it needs, is an error naming the ones it takes. A relative path is from the root of the project Greed shows, wherever Greed or the agent was started, and ~ is your home folder. greed ctl talks to the running session: the one named by $GREED_SESSION, or the only one running. Each session’s socket is readable only by you and needs a token from a file only you can read.

Claude Code and other MCP clients

claude mcp add greed -- greed mcp

greed mcp connects an MCP client to the running editor. As it connects, the client is told where your config is and how to add to Greed with a plugin, so an agent asked to change a setting or add a command knows where to go. The tools:

ToolWhat it does
focusThe file you are in (the last one, while an agent’s pane has focus), your selections with their text, the lines on screen, the mode (normal, insert, …) and the editing style
buffersOpen files and which have unsaved changes
readA file as the editor has it, unsaved changes included
diagnosticsErrors and warnings from language servers
history_recentWhat changed lately in open files, who changed it (you, a plugin, an agent, the disk) and a diff
context_getWhat you pinned for agents: selections, files, problems, the changes in progress
working_setThe files you’ve worked in lately, most active first, with how much you and agents edited each and the lines you were on
repo_mapA map of the project: the functions and types that matter most, with their files and lines, ranked toward what you’re working on (below)
symbolsFunctions and types anywhere in the project whose names match, with their files and lines
grepThe lines matching a regex in the project, with open files searched as you have them, unsaved changes included; also in your config and its plugins, and Greed’s own plugins and types
filesThe project’s files, or those whose paths match a fuzzy query; or a folder’s, in the project, your config or Greed’s own plugins
outlineA file’s functions and types with their lines, from its syntax tree
syntax_queryRun a tree-sitter query over a file and get what it matched
openOpen a file for you, at a line
proposePropose edits to one or more files for you to review
renameRename a name everywhere it’s used, as the language server works it out, proposed for you to review
code_actions, code_actionThe language server’s quick fixes and refactors for some lines, and proposing one; actions that would run a command in the server are refused, since they can’t be shown first
askAsk you questions, with choices or a typed answer, on a page you fill in and send; the answers come back to the agent
projectThe project’s root folder
evalRun Luau inside the editor: sandboxed first, then a reviewer or you for anything more (below); what it edits is one operation you can review and undo
describeWhat a command, option, event, API function, plugin or mode is: its doc, keys, value, who defined it and where
whyHow some lines came to be: each change that made them, newest first, with its description and diff
plugin_guide, plugin_try, plugin_installWrite a plugin (below)
notes_list, notes_search, notes_read, notes_writeYour notes: find, read, write and add to them; rewriting a note that’s there is proposed for you to review
notes_todos, notes_add_todo, notes_add_thoughtThe todos in your notes, soonest due first, and adding a todo or a thought
notes_update_todoChange one todo: its text, due date or reminder, or tick it
notes_remindSet a reminder on a line of a note, or add a todo with one (“remind me this evening”)
plan_read, plan_writeThe plan you share (below): its checklist as you have it, and replacing it, which gives back the version to write with next
decisions, decision_read, decision_proposeWhat was decided for the project, and proposing a decision for you to accept
memory_list, memory_proposeYour standing preferences (below), and proposing one for you to accept
subagentHand a task to a helper agent with an empty context, in a workspace of its own by default, and get its report (below)
blocks_list, blocks_runThe runnable code blocks in a Markdown file, and running one, in files you trust and as you last ran it; the result is saved under the block
web_search, web_fetchSearch the web with the service you set up, and read a page as text (below)
shell_runRun a command in the project’s shell, where you see it marked ◆, and get back its id, exit code, output, errors, how long it took, the files and lines it names, its JSON and a table’s first rows; one that runs past timeout_s comes back as running
shell_read, shell_wait, shell_killA shell block by id, or the latest one (yours too), waiting for one still running, and stopping one an agent ran
toolsThe groups of tools below, and switching one on

Tool groups

Every tool an agent is offered costs tokens on every request, so most of the tools above come in groups that an agent switches on with tools when it needs them. These are always offered: focus, buffers, read, diagnostics, propose, open, project, files, grep, symbols, outline, ask, eval, context_get, memory_list, memory_propose and tools.

GroupTools
planplan_read, plan_write, decisions, decision_read, decision_propose
maprepo_map, working_set
refactorrename, code_actions, code_action
historyhistory_recent, why
notesthe notes_ tools
syntaxsyntax_query
blocksblocks_list, blocks_run
pluginsdescribe, plugin_guide, plugin_try, plugin_install
subagentssubagent
webweb_search, web_fetch
shellshell_run, shell_read, shell_wait, shell_kill

:set agent-tool-groups "plan notes" gives agents those groups from the start (the default is plan), and "all" gives every tool. Claude Code and other MCP clients hear that their list changed when a group is switched on, and each connection has its own. greed ctl always has every tool.

The web

With the web group on, an agent can search the web and read pages, for documentation and facts the project doesn’t hold. web_fetch works without setup. Searching needs a service:

local greed = require("@greed")
greed.option.set("web-search", "brave") -- or "exa", "tavily", "searxng"
-- then :secret-set brave (exa, tavily); SearXNG needs no key, only where it is:
greed.option.set("web-search-url", "https://searx.example.org")

Pages and results are written by strangers and can say anything, including “ignore your instructions and…”. They reach the agent marked as someone else’s text to use as information, and everything an agent does with them still goes through you: edits come as proposals or ask first, commands ask, and eval runs in its sandbox. The group is off until an agent switches it on, unless agent-tool-groups names it.

The repo map

repo_map gives an agent a few thousand tokens’ worth of the project’s layout: the first line of each function and type that matters most, grouped by file. Greed reads every source file’s syntax tree, links each file to the files defining the names it uses, and ranks them toward your working set, so the map changes with what you’re working on. :repo-map shows what an agent would get now.

The working set is the files you’ve worked in lately. Your edits count most, an agent’s edits less, and moving around in a file a little; what you did 15 minutes ago counts half as much as what you do now. :working-set shows it.

What eval may do

greed ctl eval from a shell runs as you: whoever runs it can already run anything. Greed knows a call came from greed ctl by its connection, so a plugin calling the tool can’t claim to be one. For agents, through greed mcp, Claude Code’s /ide or Greed’s own agents, eval runs as the agent-eval option says:

Value
"auto" (the default)Sandboxed first: the code can read the editor and how it’s set up (options, keys, plugins), open files in the project and edit buffers, and nothing else. When it needs more (running programs or commands, other files, keys, options, plugins), a reviewer decides: the model playing the review role, or reasoning without one (see :models). A small fast model is too easy to talk round, so it never reviews. If the reviewer isn’t sure, or there’s none, you’re asked.
"ask"You see the code and press Run every time
"allow"It runs with every capability, as a plugin would
"off"There’s no eval

The sandbox is enforced by the editor, so what it keeps out doesn’t depend on anyone reading the code right. Its edits are taken back before the code runs again with more, so nothing happens twice. The reviewer gets the code wrapped as someone else’s text, is told that comments or strings in it may try to talk it into allowing, and answers in a fixed form. A model can still be fooled, so set agent-eval to "ask" if you’d rather see every run past the sandbox yourself. When you’re asked, the page shows the code, what it would do and the reviewer’s reason, with Run, Allow for this session and Deny (or r, s and d); closing it, or leaving it five minutes, denies. :eval-log lists every run past the sandbox and who let it.

Reviewing proposed edits

propose takes a file and a list of edits, or several files each with its own, each edit replacing some exact text with new text. The old text is struck through in the buffer and the new text shows after it; when whole lines change, as in Claude Code’s diffs (with or without the last line’s newline), the new lines show under the old ones, with the words that changed picked out on both. A message says how many changes came in, in which file, and the keys to review them: in a file on screen, the keys that go to each change and decide; otherwise the key that lists them all. Nothing changes until you decide, with the keys or by clicking accept, reject or note after each change:

Keys
] o [ oNext and previous proposed change
space o a space o rAccept or reject the changes your selections touch
space o nReject them and tell the agent what to do instead: what you type goes back to it with the change, so it can propose again
space o A space o RAccept or reject every change in the file
space o oPick from every pending change
space o dShow the file with every change in, beside it; again to hide

Changes follow the edits you make while they wait. The agent waits too, and then hears which changes went in, with the errors and warnings the language server reports in those files once it has seen them, so it can fix what it broke without running the compiler. An agent that goes away while its proposal waits, like a Claude Code session you quit, takes the proposal with it: what’s left of it is rejected. What you accept from one proposal is an operation (see Using Greed): space u u undoes it in every file at once, and space u o reviews it. A file the undo takes back to what’s saved no longer shows as changed.

Side by side, the two panes scroll together, matching lines across the changes, and the right one keeps up as you edit, accept and reject.

Questions from agents

An agent that needs to know something can ask. Greed shows its questions as a page in a float: each with choices to pick one of (enter or a click), if it has any, and a line to type your own answer after Answer:, alone or to go with a choice. Send gives your answers back to the agent, and Cancel, or closing the page, tells it you didn’t answer. enter picks the choice under the cursor and goes on to the next question, starts typing on an Answer: line, and from there goes on to the next, and on the last line sends; ctrl-s sends from anywhere on the page.

The agent sends questions as plain data, and Greed writes the page itself. Its words go on lines of their own, with anything that could start Markdown’s syntax escaped, so a question can’t add a button, a code block or a choice of its own, and only you can send the page.

A plan and decisions you share

An agent can keep its plan in the project’s notes (.greed/notes/plan.md, the note project:plan; see Using Greed), a Markdown checklist you see and change while it works: tick steps (space n x), reorder them, add or delete them. :plan opens it, and its open steps are in your agenda with your other todos. The agent reads the plan as you have it, and when it writes a new one it gives the version it read; if you changed the plan since, its write is refused and it gets yours to work from, so it can’t write over your changes.

Agents like Claude Code and opencode keep todo lists of their own. To have them use this plan instead, say so in the project’s AGENTS.md or CLAUDE.md, e.g. “Keep your plan with the plan_read and plan_write tools.”

Decisions are notes too, in .greed/notes/decisions/, one each: what was decided and why, like “use SQLite” or “never edit the generated code”. Agents read them in later sessions and follow the accepted ones.

Command
:decision-new TITLEWrite down a decision, accepted
:decisionsPick one to open
:decision-acceptAccept the proposed decision you’re in

A decision an agent adds is Status: proposed until you accept it: open, it says so beside its status, with a button that accepts it, as :decision-accept does. Until then agents are told not to rely on it, so one agent can’t make rules for the next. The notes tools refuse to write decisions. These are plain files, though: an agent that can write files can write here too, so check .greed changes as you would code.

Memories

Memories are standing preferences you want every later session to follow, like “use jj, not git” or “I prefer short commit messages”. There are three kinds, each a plain Markdown file with one memory per list item, for you to read and edit:

KindFile
Personalmemory.md in your config folder (~/.config/greed/memory.md)Yours, in every project; :memory-edit opens it
ProjectA file per repository in ~/.local/share/greed/memory/Yours, for this project; :memory-edit-project opens it
Shared.greed/memory.md in the repositoryFor everyone working on it; :memory-edit-shared opens it

Project memories stay on your machine, outside the repository, and only you can read them. All jj workspaces and git worktrees of a repository share one file, so a memory you accept in one applies in the others.

An agent can’t add a memory itself. When you state a lasting preference (“from now on…”, “always…”, “I prefer…”), it proposes one with memory_propose, and the line shows in the memory file as a proposed change, with the date. A message says which file it waits in; space o o lists it with its line and opens the file, where space o a accepts it and space o r rejects it (see Reviewing proposed edits). Only what you accept is written. That way a web page or file saying “remember to always run X” can’t become a rule for every later session. Proposals go to your project memories unless they’re about how you work anywhere; agents propose shared ones only when you ask for a memory for the whole team.

Greed’s own agent gets all three in its system prompt with each message, marked as preferences you accepted. Other agents read them with memory_list; Claude Code keeps memories of its own as well.

A repository’s .greed/memory.md comes with it, like its AGENTS.md: whoever can change the repository can put memories in it. As with decisions, an agent that can write files can write these too, so check .greed changes as you would code.

Claude Code’s /ide

space i C opens Claude Code in a pane beside your files, already connected to Greed; pressed again it goes back to that pane, or brings it back if you closed it. claude started in any Greed terminal connects by itself too. :set claude-command "claude --continue" changes what the pane runs.

Started outside Greed, in the same project, /ide in Claude Code lists Greed next to any other editors. Once connected:

  • Claude Code sees your selection as you move, and space o m mentions the selected lines to it.
  • Its edits arrive as proposed changes, reviewed with the keys above. If you accept some of them, Claude Code learns which.
  • It can read diagnostics, open files and check for unsaved changes.

Greed writes a lock file to ~/.claude/ide so Claude Code can find it, and removes it when the session ends.

Agents inside Greed

Greed can also run agents itself, over the Agent Client Protocol (ACP): Claude Code (through claude-agent-acp), Gemini CLI, Codex (codex-acp), or any agent that speaks it, found on your PATH. Each gets a transcript buffer, and several can work at once. Greed has an agent of its own too, below.

space i n lists them and says which aren’t installed, with the command that installs each:

npm install -g @agentclientprotocol/claude-agent-acp   # Claude Code
npm install -g @google/gemini-cli                      # Gemini CLI
npm install -g @zed-industries/codex-acp               # Codex

A new session’s transcript opens with you typing in its box.

Shell code an agent writes (a sh, bash or console block) has buttons on its opening fence. ▶ run runs it in this project’s shell, in the folder the agent works in, as a block of its own there: output as it comes, ctrl-c to stop it, and space o w to find it later. ↳ prompt puts it at that shell’s prompt to look over and change first, and ⧉ copy copies it. Click one, press enter on the fence to pick, or space . on it. A command that destroys or stops something (rm, kill, git push --force, kubectl delete and the like) asks y/n before it runs. A console block’s commands are its lines after a $ prompt.

To show the agent a screenshot, copy it and press alt-v in the transcript; in the window, the paste keys do it too when the clipboard holds an image. The image goes with your next message, shown as a chip in the box (🖼 image 1 · 1280×720), and alt-x drops it. It’s read from the clipboard of the machine you’re at, so with greed ssh it’s your laptop’s, and kept in Greed’s cache folder where the agent runs. An agent that can see images gets the image itself: Claude Code, and Greed’s own agent when its model can (Claude, GPT-4o and later, Gemini, Kimi K2.5 and later, Qwen-VL, Llama 4 and the like, known by name; images = true on a role says so for a model Greed doesn’t know). One that can’t is told where the file is and that it can’t see it, so it can still work with the file. In a terminal, Greed reads the clipboard with wl-paste (Wayland), xclip (X11) or osascript (macOS).

Greed’s own agent

Pick “Greed” in space i n for an agent that runs in the editor itself, with any model the models setup knows: Anthropic’s, or any server with OpenAI’s API, like berget or a local Ollama. It uses the model playing the agent role (or reasoning without one); alt-M in its transcript picks another role, and :models sets which model plays it. When that model’s provider has no key, the bottom of the transcript says so in place of “ready”, and a message you send stays in the box until there’s one.

local models = require("@models")
models.roles.agent = { provider = "berget", model = "the model you want" }
-- then :login berget for a Berget Code plan, or :secret-set berget for an API key

It has the tools the list above gives agents (the repo map, working set, grep, symbols, diagnostics, the plan, …) but propose, plus edit_file, write_file, move_file, delete_file and run for shell commands: its edits show the change and ask first, so a proposal left for review isn’t needed. Reading never asks; editing and running ask the way other agents’ questions do (y once, a for the rest of the session, n no), and your policies answer them too. So do the tools above that write, run or fetch something, like notes_write, blocks_run and web_fetch, by the kind of thing they do; tools that show you their work themselves, like propose, ask and eval, don’t ask first. Its edits go through the file’s buffer, so u takes them back, and it hears what the language server reports about the file right after. Several changes to a file in one edit_file are one change to undo. It won’t write a whole file over one it hasn’t read, or over your changes made since it read it, and it can’t write a line like // ... existing code ... in place of code it left out. When it edits your config, the config runs again right away, and the agent hears whether it ran or what error it hit, so a setting that doesn’t exist gets fixed in the same turn.

By default the model edits by giving the text to replace. Some models copy text badly and do better naming lines instead: with edits = "hashline" on the role (or the provider), read gives each line a name of its number and a short hash of its text (12#a3f|local x = 1), and the model edits lines by those names. A line that moved since it was read is found by its hash; one that changed is refused, so the model reads it again.

models.roles.agent = { provider = "berget", model = "moonshotai/Kimi-K3", edits = "hashline" }

:usage shows how the edits went, by model and way of editing, to compare the two, beside the tokens each model was sent and wrote today and over the last week, and how many came from the provider’s cache (Models has more). Its system prompt includes the project’s AGENTS.md or CLAUDE.md. For now it works in the project itself, without a workspace of its own.

Each session is written down after every turn, so the session list (space i l) shows it with the other agents’ sessions and takes it up where it left off: the transcript comes back and the conversation goes on with everything said so far, the same model and the tool groups it had on. Your “allow always” answers aren’t kept, so a session taken up asks again. The files are in Greed’s data folder (~/.local/share/greed/agent-sessions), one folder per repository, which all its jj workspaces and git worktrees share, readable only by you, since they hold what you typed and what tools returned. Long tool output is cut there. The newest 50 sessions of each repository are kept and older ones deleted (:set agent-sessions-kept 100 keeps more); closing one with x in the session list deletes it. Agents don’t read other sessions’ conversations; only you see them in the list.

A tool result longer than 2,000 lines or 30,000 characters reaches Greed’s agent as its start and where the whole of it is: a file in Greed’s cache folder (agent-output, only yours to read, cleared after a week) that it reads on in or searches with grep, so nothing is lost.

Every request sends the whole conversation, so a long file read early on costs again on every turn after. Providers cache the start of a conversation that’s the same as last time and charge much less for it, so Greed’s agent keeps that start steady: the system prompt doesn’t change from turn to turn (the time comes in each of your messages instead), a tool group switched on adds its tools after the ones there already, and requests to Anthropic mark the tools, the system prompt and the newest message for caching. Old messages are never changed afterwards, except when the conversation is compacted.

When a conversation nears the model’s limit, it’s compacted before the next request: the model summarises everything but its end, and the summary takes its place. The summary has fixed headings (objective, open requests, decisions, work state, next step, the files that matter and exact details still needed), folds in the summary before it when there is one, and is followed by a pointer to the plan, which is kept apart from the conversation. The end kept as it was is as many whole turns as fit in about 15k tokens (a fifth of the window, for a small one), the last turn at least, so a tool call and its result always stay together. Near the limit means the conversation, as the provider last counted it plus what came since, leaves less than room for the longest answer and a reserve (10% of the context window or 16k tokens, whichever is more, but at most a quarter of it). A request the provider still turns down as too long is compacted and tried again, keeping half as much of the end each time, up to three times. The transcript shows a folded conversation compacted (N messages → summary) row; tab on it shows the summary. :agent-compact does it by hand. The summary goes to the model as its own earlier words rather than as instructions, and the system prompt (your project’s instructions, where it works) is built fresh as always. The model playing the compact role summarises, if you set one, and otherwise the session’s own model:

models.roles.compact = { provider = "anthropic", model = "claude-haiku-4-5-20251001" }

Greed knows the context window of Claude, GPT and common open models (Llama 3, Mistral Small, Devstral, Qwen3 Coder, GLM, DeepSeek, Gemma 3, Kimi, gpt-oss) by their names, and assumes 32k tokens for any other. Set context on the role or provider when it’s wrong, as for an Ollama model whose context you’ve raised:

models.roles.agent = { provider = "ollama", model = "qwen2.5-coder", context = 128000 }

One answer from Greed’s agent may be up to 32k tokens long, or a quarter of the context window when that’s smaller; set output on the role or provider to change it. An answer that runs out of room anyway, such as one writing a long file, runs none of its tool calls. The agent is told and goes on in smaller pieces, and stops after three such answers in a row.

Keys
space i n (:agent-new)Start an agent, Greed’s own or one over ACP: pick which, and where it works; one that isn’t installed says the command that installs it
:agentShow the latest session, or start one as space i n does when none runs
space i l (:agent-list)The session list: every session of the repository, running and kept
space i m (:agent-move)Move the transcript you’re in to a pane, a tab, a float or a panel
:agent-forkFork the agent of the transcript you’re in
:agent-compactCompact the conversation with Greed’s own agent now
▍ You
▍ Fix the off-by-one in the pager

▍ Claude
▍ Found it in pager.rs.
▍ ✓ Edit pager.rs · +1 -1
▍ ✗ Run cargo test · 12 lines ▸

╭─ Claude · Plan · Sonnet ────── alt-enter sends · ctrl-c stops ─╮
│ now add a test                                                 │
╰─ ● thinking · 12s ─────────────────────── 14.2k / 200k tokens ─╯

The transcript shows what you and the agent said, each with a coloured bar beside it (yours on a shaded background), and its plan. Each tool call is one row: ○ waiting, ◐ running, ✓ done or ✗ failed, with what it was about and what came back in a few words, like read src/main.rs · 120 lines. A ▸ means there’s more: tab on the row shows all of its output, and tab again folds it. When Greed’s own agent asks before editing a file, like your config, the edit’s row already holds the change (-1 +1 · tab shows the change): tab shows the lines going and the ones coming, in the theme’s diff colours, before you answer.

While the box is on screen, the transcript scrolls down as the agent adds to it, taking your cursor back to the box (unless you’re selecting text). Scroll up away from the box and it stays where you are.

Write in the box at the bottom; it grows as you write, and its top shows the agent, its mode and model. alt-enter sends and ctrl-c stops the agent’s turn. The box’s bottom line says what the agent is doing (the tool it runs, or thinking) and for how long this turn has taken, waiting for you when it asks something, and how many tokens of the model’s context the conversation takes when the agent says (out of how many it has room for, when it says that too; Greed’s own agent always does). With the effects option on (:set effects true), running tools spin, the status shimmers while the agent thinks and the waiting chip pulses; nothing moves while the agent is idle. Themes style it with agent.you (your messages’ background), agent.you.bar, agent.bar, agent.box, agent.box.title, agent.hint, agent.working, agent.waiting, agent.done and agent.failed.

When the agent asks before doing something, the question shows as a row of choices at the top of the box, like Allow Edit a.txt? [y] Allow once [a] Allow always [n] Reject, and you answer it from there in any mode and editing style:

Keys
enterThe highlighted choice (the first, until you move)
tab, shift-tab, left, rightHighlight the next or previous choice
A choice’s letterThat choice, unless you’re typing with something in the box already
escHide the row, to read the question in the transcript; enter outside typing (or alt-q) shows it again

Clicking a choice picks it too. What you’ve typed in the box stays as it was. Letters type as usual once the box has something in it, so a message you’re writing doesn’t answer by accident; enter and tab still work then. The box’s bottom line says which keys answer at the moment. The agent reads open files as you have them, unsaved changes included, and its edits to an open file are made in the buffer and saved, so undo takes them back.

Each message you send takes along what you have on screen: the file you were last in (the one beside the agent’s pane while you type there), the cursor’s line and column, the selected text (cut after 4000 characters) and the other files showing. It’s marked as context from the editor, so the agent treats the file text in it as data. When nothing changed since your last message it isn’t sent again. :set agent-editor-context false turns it off.

Agents also get Greed’s own tools, the ones greed mcp offers above (diagnostics, outlines, syntax_query, propose, …), connected to this session. :set agent-greed-tools false leaves them out.

While an agent works you see where: the line it’s on in an open file gets a ◆ beside it and the agent’s name and what it’s doing at the end, in a colour of its own (an agent in a workspace shows on your copy of the file). The status line says how many agents are working and how many wait for you.

alt-m picks the mode the agent works in, when it has modes (Claude Code has Manual, Accept edits and Plan), and alt-M the model, when it offers a choice; the top of the box you write in shows both. The transcript is a buffer in a panel: move and copy in it as in any other, and :q or ctrl-w q closes the panel while the agent goes on (space i l brings it back).

The session list

space i l lists the sessions of the repository you’re in: those running and those kept to take up again, from every jj workspace or git worktree of it, with this one’s first and then the newest. Each line says which agent, what you asked it first, what it’s doing (working, spinning with effects on, waiting for you, idle, failed with why its last turn failed, exited or saved), when it last did something, the tokens it takes when the agent says, and where it works: in other-workspace for one that ran in another workspace of the repository, workspace NAME for one in a workspace of its own.

  Claude    Fix the off-by-one in the pager  ⠋ working  just now   14.2k tokens
  Greed     Add a test for the pager         idle       4 min ago  3.1k tokens
  Claude 2  Look at the parser               saved      2 h ago                 in greed-b
Keys
enterShow the session, taking it up first if it’s kept or exited; with several marked, show them all as panes
rTake up the session (or the marked ones) without showing it
hTake up Greed’s own session here, in the workspace you’re in
mMark the session, or unmark it, to act on several
p, t, w, sShow it in a pane, a tab of its own, a floating view or a panel on the right
/Narrow the list to sessions with what you type in them; esc puts back the filter there was
nStart another, asking which agent as space i n does
fFork it: a new session that starts with everything said so far (not Greed’s own sessions yet)
xClose it: stop it, forget it and remove its workspace, asking first when it’s running or has a workspace
d, b, RWhat it changed in its workspace, bring that in, or review it (below)
ctrl-cStop its turn
q, escClose the list

A session taken up from another workspace works in the folder it ran in. To carry on with one of Greed’s own sessions where you are now, take it up with h; from then on it’s kept as this workspace’s. Other agents, like Claude Code, keep their sessions by folder, so theirs stay where they ran, as do sessions in a workspace of their own.

A transcript shows in one place at a time, and moving it doesn’t restart the agent: space i m and then p, t, w or s moves the one you’re in to a pane, a tab, a float or a side panel (:agent-show-pane, :agent-show-tab, :agent-show-float, :agent-show-panel). The box you write in, its keys and the status line under it work the same in each. Several marked sessions shown with p split the biggest pane each time, with t they share one new tab, and with w each gets a float of its own. Closing a transcript’s view, its pane or its tab leaves the session running; only x in the list, or quitting Greed, ends it.

Some questions can be answered for you. An agent in a workspace of its own edits, moves and deletes files there without asking, since nothing reaches your files until you bring it in (:set agent-workspace-edits false to be asked anyway). That holds only for edits that name their files, all inside the workspace; anything else asks, and Greed won’t write a file outside the workspace for it at all. An agent working in the project asks again before Greed writes a file outside the project for it. A question’s locations lists the files it touches, for rules of your own. Those go in init.luau; the first that returns an option’s id answers, and nil leaves it to the next, and in the end to you:

require("@acp").policy(function(session, question)
	-- Reading files never needs asking.
	if question.kind == "read" then
		for _, option in question.options do
			if option.kind == "allow_once" then
				return option.id
			end
		end
	end
	return nil
end)

One kind of question is always yours. A write to a plugin.toml that adds capabilities, letting a plugin do more than it could, asks every time and says what it adds, like `Write plugin.toml, which widens what acp may do:

  • plugins`. No policy answers it, “allow always” doesn’t cover it, and it has no “allow always” of its own. This matters most for Greed’s own plugins, which load without asking you to approve their capabilities. It covers writes that go through Greed. An agent with a shell of its own can still change files there.

An agent works either in the project with you, or in a workspace of its own: a jj workspace, or a git worktree on a branch greed/NAME, in Greed’s cache folder. There it can change what it likes without touching your files. In the session list:

Keys
dWhat it changed in its workspace, as a diff
RReview that on your files, change by change, as proposed edits (see Reviewing proposed edits)
bBring that into your working copy: with jj its change is squashed into yours, with git its diff is applied

R takes the agent’s edits and moves them past whatever you’ve changed since it started, so you review only what it did. An edit to lines you changed too is left out, and the message says how many files had one; d shows them. What you accept lands in your buffers, unsaved, and undoes in one step. The agent’s workspace keeps its change, so after a review close it with x rather than bringing it in with b.

A jj workspace starts from your working copy’s parent, so it doesn’t see changes you haven’t committed. A fork of an agent in a jj workspace gets a workspace of its own with the edits made so far; in a git worktree, it starts from the branch’s last commit.

Sessions are kept in the cache folder until you close them with x, so after a restart space i l lists them, with what you asked first, and takes one up: the agent tells the conversation again into a new transcript, and its workspace is still there. Agents that can’t take up a session (or fork one) say so. Greed’s own agent keeps its sessions itself, as described above.

An agent can start another with the subagent tool: for a search across the project, a side task or a second opinion, without filling its own context. The helper is an agent from the list here (the first installed, or the one asked for), in a workspace of its own unless the caller says otherwise. Its transcript shows in the session list and it asks you before doing things like any other; the caller gets its last answer as a report. At most three work at once, so helpers starting helpers can’t run away. Greed’s own agent can’t have a workspace yet, so as a helper it works in the project, and the result says so. A helper whose turn fails, like one whose model has no key, comes back as an error saying why.

Add an agent that isn’t in the list in your init.luau; the first of its commands that’s installed is used, and model is the model it starts with, when it offers models, and install the command space i n shows when it isn’t installed:

require("@acp").register({
	name = "opencode",
	title = "opencode",
	commands = { { "opencode", "acp" } },
	model = "berget/llama-3.3-70b",
	install = "npm install -g opencode-ai",
})

Which models an agent offers is up to the agent: opencode, for example, offers the ones its own config sets up.

Plugins written by agents

An agent can extend Greed while you work:

  1. plugin_guide gives it the plugin conventions and the full typed API.
  2. plugin_try writes a plugin to a staging folder (~/.cache/greed/staging) and checks it: strict types against the API, then the plugin’s own tests in a headless editor, with none of its capabilities until you approve it. It runs without asking you, since nothing it does reaches your editor or your files. The agent gets back what failed and tries again, sending only the files it changed: the rest come from its last try. Type errors count only in the plugin’s own files, and tests that fail only because a capability isn’t approved yet don’t stop it going to you: once you install it, they run again with what you approved, and the agent hears how they did.
  3. plugin_install asks you. A float shows what the plugin may do beyond the editor (its capabilities, like running programs or reading files), the code and the check results; a installs it into ~/.config/greed/plugins (or ~/.local/share/greed/plugins when your config can’t be written, as when Nix manages it), approves what it asks for and loads it right away, r rejects it.

Pages like this one, and the one asking before an agent’s code runs, show one at a time: when an agent asks for two at once, the second shows once you’ve answered the first, so neither waits hidden behind the other.

Ask for something like “a command that jumps from a use line to the crate’s entry in Cargo.toml” and the agent goes through these steps. You only see it once its checks pass. Greed’s own agent is told to build new things for Greed this way rather than in the project you’re in, and the tools say so to other agents; it changes the project only when you ask it to change the project.

Writing your own tools

Any plugin can offer tools; see Writing plugins.

How Greed is built

Greed has a small core written in Rust and a large layer written in Luau. Knowing where the line is tells you what you can change from a plugin (almost everything) and where to look when something is slow.

The rule

If it can be written in Luau, it is. The core holds what needs speed, threads or the operating system, and offers it to Luau as small, general functions. Everything you see and use is built from those functions, the same way any plugin would build it.

In Luau:

  • The editing model: modes, motions, selections, operators, the keymaps. The core plugin holds the editing commands; Helix, Vim and vscode are plugins that give them keys, each with modes of its own, and :style switches between them.
  • Commands, the command line, pickers, the file tree, search and replace, the status line, key hints, help.
  • The theme and every animation.
  • Language settings, which language server to start and how.
  • The agent integration: the MCP tools, the review of proposed edits, Claude Code’s /ide protocol, plugins written by agents, and Greed’s own agent with its chat.
  • The shell: its prompt, blocks, tables, completion and watches.

In Rust:

  • Text: the rope, selections, changes, undo history.
  • Layout and rendering: turning buffers, decorations and the theme into frames a client draws.
  • Parsing (tree-sitter), regex search, project-wide grep and diffs.
  • Programs and terminals: running processes, the terminal emulator, and reading images and PDFs.
  • Talking to the outside: language servers, the session socket, Claude Code’s WebSocket, files.
  • Running things off the editor thread, and waking the Luau code waiting for them.
  • Loading, sandboxing and checking plugins.

When a feature needs something Luau can’t do, the core grows by the smallest primitive that unblocks it, and the feature itself stays in Luau. Project search is an example: the core searches files on a background thread, while the picker, the waiting for a pause in typing, the results panel and writing edits back are Luau.

Sessions and clients

The editor runs as a session: one process that owns the buffers, the Luau plugins and the language servers. Clients connect to it. The terminal UI is one client: it sends keys and draws the frames it gets back. The window (greed --gui) is another that draws the same frames, and greed ctl, greed mcp and Claude Code connect too.

Because the session owns the state, a terminal can detach and reattach without losing anything, and greed ssh attaches a terminal on one machine to a session on another. Any number of clients can be attached at once, each with its own cursors, focus, mode and screen size; the server draws a frame for each. A client says which protocol it speaks when it attaches, and the session refuses clients it can’t talk to instead of misreading them.

The editor acts for one client at a time, and every command, key and event handler knows which. Clients that follow share one layout of projects, tabs and panes, each with its own focus and zoom; an independent client has a layout of its own, a copy of the shared one with views of its own. Floats are placed in shares of the screen, so they keep their proportions on screens of other sizes, and the ones a client opens, like a picker, are drawn for it alone. A view can be laid out at one size for every client, as a terminal is for the client typing into it; the others crop or scale it. Several clients describes this from the user’s side.

Frames describe what to show by meaning, like “selection”, “keyword” or “diagnostic.error”, and carry the theme that maps those names to colors. A terminal and a GUI can draw the same frame, and theme entries can style each one differently. A frame is a stack of surfaces (panes, panels, floats, the tab and status lines), each with its own rows and a role that says how the client draws around it. An attached client gets a whole frame once, and after that only what changed, surface by surface: typing a character sends a row or two of one pane, focusing another pane sends two roles, moving a float sends its new place, and the theme goes along only when it changes.

Plugins are contained

Each plugin runs in its own environment with a read-only standard library. A call that runs too long is stopped, so a buggy loop can’t freeze the editor. Everything a plugin registers or starts (commands, keys, options, event handlers, theme entries, tools, tasks, marks, floats and panels) belongs to it, so reloading or unloading a plugin cleanly replaces or removes all of it.

Plugins that wait (for a timer, a key, a language server, a search) are paused and resumed by the editor. The code reads top to bottom with no callbacks, and the editor keeps responding meanwhile.

Agents are clients too

Agents see the same editor state you do, and their edits still go through you: they arrive as proposals in the buffer that you accept or reject, or, for Greed’s own agent, ask first with the change shown as a diff, unless you allowed that kind of change for the session. When they extend Greed, their plugins go through the same type checking and tests as any other, and you review them before they load.

Performance budgets

How fast Greed is gets checked like everything else: cargo test runs a set of budgets, each a measurement with a limit it must stay under.

  • Starting with every default plugin, to the first frame, on an empty file and on a 10,000 line Rust file.
  • Opening a file of a million lines, and one of 100 MB, then moving around and typing in them: the time per key, and the memory the file adds.
  • 500 files open: opening them, moving, the buffer picker.
  • A long session of opening, editing, undoing, splitting and closing, for the memory each round leaves behind, so a leak fails a test.
  • Three clients attached, one typing beside a split, a terminal and a float: the time per key and the bytes each client gets for it, and what the session uses while nothing happens.
  • Searching 2,000 files, and highlighting a 20,000 line file again after an edit.

Times are CPU time, which a busy machine changes far less than the clock, and each measurement runs in a process of its own. The limits are a few times what was measured, so a loaded machine or a debug build doesn’t fail them while something becoming several times slower does.

To see each measurement next to its limit:

GREED_BUDGETS=report cargo test --release -p greed-core --test budgets -- --nocapture

Adding --include-ignored also runs a session of several minutes. For how keys feel in your own setup, with your plugins and config, greed bench times each key from the terminal client’s side.

Plugins

Everything you see in Greed comes from these plugins, each a folder of Luau you can read, change or replace. Unload one with greed.plugin.unload(name) in your config.

acp

Agents in Greed over the Agent Client Protocol: Claude Code, Gemini, Codex or any agent that speaks ACP, each in a transcript buffer you write to and read from, working in the project or in a workspace of its own.

  space i n   start an agent: pick which, and where it works
  space i l   the sessions, running and kept: show, take up, review or bring them in
  space i m   move this session's transcript to a pane, tab, float or panel

In a transcript, alt-enter sends what’s in the box at the bottom and ctrl-c stops the agent’s turn. A question the agent asks shows as a row of choices in the box, answered with enter or a choice’s letter in any mode (question.luau).

Agents are registered by name with the commands that run them; the first one installed is used. Add your own in init.luau:

    require("@acp").register({ name = "mine", commands = { { "my-agent", "--acp" } } })

Uses: core, picker, statusline, vcs, agent, models, panes, markdown.

CommandKeysWhat it does
agentShow the latest agent session, or start one when none runs
agent-answeralt-q (in agent-transcript buffers), enter (every style’s command mode; in agent-transcript buffers)Show the question the agent waits on in the box, to answer it there; on a button, like a code block’s run, press it
agent-choice-clickAnswer the agent’s question with the choice under the cursor
agent-choice-hideHide the agent’s question from the box; it still waits in the transcript
agent-choice-keyAnswer the agent’s question with the choice of the key pressed
agent-choice-nextHighlight the next choice of the agent’s question
agent-choice-pickAnswer the agent’s question with the highlighted choice
agent-choice-prevHighlight the previous choice of the agent’s question
agent-code-copyCopy the shell code block at the cursor
agent-code-promptPut the shell code block at the cursor at this project’s shell’s prompt, to look over before running it
agent-code-runRun the shell code block at the cursor in this project’s shell, as a block there; one that destroys something asks first
agent-compactCompact the conversation with Greed’s own agent: the model summarises all but the last few turns
agent-drop-imagesalt-x (in agent-transcript buffers)Drop the images pasted for your next message to the agent
agent-foldtab (every style’s command mode; in agent-transcript buffers)Show all of the output of the tool call under the cursor, or fold it again
agent-forkf (in agents buffers)Fork the agent: a new session that starts with everything said so far
agent-listesc, q (in agents buffers), i l (after the leader)List the agent sessions, running and kept, to show, take up, move, review or close them
agent-modealt-m (in agent-transcript buffers)Pick the mode the agent works in, like plan or accept edits
agent-modelalt-M (in agent-transcript buffers)Pick the model the agent uses, from those it offers
agent-movei m (after the leader)Move the session’s transcript to a pane, a tab, a float or a panel: the next key says which
agent-newn (in agents buffers), i n (after the leader)Start an agent: Greed’s own, or Claude Code, Gemini or Codex over ACP; pick which, and where it works
agent-resumeThe same as agent-list
agent-sendalt-enter (in agent-transcript buffers)Send what’s in the transcript’s box to its agent
agent-show-floatw (in agents buffers)Show the session (the transcript’s, or those chosen in the list) in a floating view
agent-show-panep (in agents buffers)Show the session (the transcript’s, or those chosen in the list) in a pane of the tab
agent-show-panels (in agents buffers)Show the session (the transcript’s, or those chosen in the list) in a panel on the right
agent-show-tabt (in agents buffers)Show the session (the transcript’s, or those chosen in the list) in a tab of its own
agent-stopctrl-c (every style’s command mode; in agents, agent-transcript buffers)Stop the agent’s turn
agentsThe same as agent-list
agents-bringb (in agents buffers)Bring what the agent under the cursor changed into your working copy
agents-closex (in agents buffers)Close the sessions marked in the list, or the one under the cursor: stop and forget them and remove their workspaces, asking first for those running or with a workspace
agents-diffd (in agents buffers)Show what the agent under the cursor changed in its workspace
agents-filter/ (in agents buffers)Narrow the list to the sessions with the text typed
agents-markm (in agents buffers)Mark the session under the cursor in the list, or unmark it, to act on several
agents-resumer (in agents buffers)Take up the sessions marked in the list, or the one under the cursor, without showing them
agents-resume-hereh (in agents buffers)Take up Greed’s own sessions marked in the list, or the one under the cursor, in this workspace
agents-reviewR (in agents buffers)Review what the agent under the cursor changed as proposals on your files
agents-showenter (in agents buffers)Show the sessions marked in the list, or the one under the cursor, taking them up if they’re kept
OptionDefaultWhat it does
agent-editor-contexttrueSend agents the file you’re in, the cursor, the selection and the other files on screen with each message
agent-greed-toolstrueGive agents Greed’s own tools (buffers, diagnostics, syntax queries, proposals) through greed mcp
agent-sessions-kept50How many sessions with Greed’s own agent are kept per repository to take up again; older ones are deleted
agent-workspace-editstrueLet agents in a workspace of their own edit, move and delete files there without asking

agent

Lets agents and scripts use the editor: greed ctl from a shell, and MCP clients like Claude Code through greed mcp.

Requests come in on the session socket. registry.luau answers MCP’s methods (initialize, tools/list, tools/call), so the tools are the same for an agent and for greed ctl TOOL. Add your own with require("@agent").tool { ... }; tools.luau has the built-in ones, and proposals.luau the review of edits agents propose.

Uses: core, picker, terminal, operations, approvals, describe, markdown, models.

CommandKeysWhat it does
ask-cancelAnswer none of these questions, telling the agent you didn’t
ask-enterOn a page of questions: pick the choice under the cursor and go on to the next question, start typing an answer, or go on; on the last line, send
ask-sendSend your answers to the agent that asked these questions
claudei C (after the leader)Go to the Claude Code pane, or open one
eval-allow-sessionLet the agent’s code run, and every run until the editor quits
eval-denyDon’t let the agent’s code run
eval-logShow each run of an agent’s code past the sandbox, with who let it
eval-runLet the agent’s code run, this once
ide-mentiono m (after the leader)Mention the selected lines to Claude Code, like @file#L1-5
memory-editOpen your personal memories: standing preferences agents follow
memory-edit-projectOpen your private memories of this project, the same in all its workspaces
memory-edit-sharedOpen this project’s shared memories, .greed/memory.md in the repository
plugin-review-accepta (in plugin-review buffers)Install, approve and load the plugin under review
plugin-review-rejectesc, q, r (in plugin-review buffers)Reject the plugin under review
proposal-accepto a (after the leader)Accept the proposed changes your selections touch
proposal-accept-allo A (after the leader)Accept every proposed change in this file
proposal-compareo d (after the leader)Show the file with its proposed changes in, side by side
proposal-next] o (every style’s command mode)Select the next proposed change
proposal-noteo n (after the leader)Reject the proposed changes your selections touch, telling the agent what to do instead
proposal-prev[ o (every style’s command mode)Select the previous proposed change
proposal-rejecto r (after the leader)Reject the proposed changes your selections touch
proposal-reject-allo R (after the leader)Reject every proposed change in this file
proposalso o (after the leader)Pick a proposed change to review
repo-mapShow the repo map agents get: the functions and types that matter most, ranked toward the open files
working-setShow the files you’ve been working in lately, the way agents see them
OptionDefaultWhat it does
agent-eval"auto"How Luau from agents runs: “auto” (sandboxed, a reviewer or you for more), “ask”, “allow” or “off”
agent-tool-groups"plan"The tool groups agents get from the start, besides those they switch on: names with spaces between, or “all”
claude-command"claude"How the Claude pane starts Claude Code, e.g. “claude –continue”
web-search""The search service agents search the web with: “brave”, “exa”, “tavily” or “searxng”; empty for none
web-search-url""Where your SearXNG is, for web-search searxng

approvals

Approving plugins. A plugin that isn’t one of Greed’s own and declares capabilities (running programs, files by path, the network, …) waits until you approve them, and again if it later asks for more.

  :plugins-approve   review the waiting plugins and approve one
  :plugins           every plugin, where it comes from and what it may use
  :plugin-reload NAME load a plugin again from its folder

Uses: core, picker.

CommandKeysWhat it does
plugin-reloadLoad a plugin again from its folder, as after changing its code
pluginsList the plugins, where each comes from and what it may use; enter opens one
plugins-approveReview the plugins waiting for approval of what they use, and approve one

assist

Commands that ask a language model, in the roles :models shows, with what you pinned to the context (space i p, see the context plugin):

  space i e   explain the selection, in a panel as the answer arrives
  space i a   ask something about the selection
  space i f   fix the problem under the cursor, as changes to review
  space i c   draft a description of the changes in progress

Fixes come as proposed changes (] o, space o a, space o r), which undo as one operation. In the description buffer, a applies it (jj describe, or a git commit of what’s staged).

Uses: core, models, agent, vcs, context.

CommandKeysWhat it does
ask-abouti a (after the leader)Ask a language model something about the selection (or the line)
assist-closeq (in answer, commit-message buffers)Close the answer panel
commit-messagei c (after the leader)Draft a description of the changes in progress, from their diff
commit-message-applya (in commit-message buffers)Use the drafted description: jj describe, or a git commit of what’s staged
explaini e (after the leader)Explain the selection (or the line), in a panel as the answer arrives
fixi f (after the leader)Ask a language model to fix the problem under the cursor, as changes to review
OptionDefaultWhat it does
assist-role"fast"The model role assist’s commands ask; see :models

blocks

Runnable blocks in Markdown: a fenced block whose language has a runner (```sh, ```python, ```luau, ```http) runs when you ask, and its output is saved in a result block right under it, so the file shows it anywhere and a diff shows what changed. Running it again replaces the result.

  space m r   run the block under the cursor (enter on its fence too)
  space m R   run every block in the file, in order
  space m c   clear the block's result; :blocks-results-clear for all
  space m s   stop the block running under the cursor

Nothing runs when a file opens. The first time you run a block in a file Greed asks whether to: y for now, a to always trust that file. Files you trust, and those whose front matter says greed: run, show a run button on each block. Agents run blocks only in files you trust, and only as they were when you last ran them or trusted the file, so a block an agent rewrote waits for you to run it first.

Options go in the info string: id=NAME names a block’s result, and input=NAME gives another block that result (on stdin, or as input in Luau), running the named block first if it has no result yet, or every time with rerun=true. dir=PATH runs a program elsewhere than the file’s folder, and timeout=SECONDS stops it after that long. Add a language from Luau:

    require("@blocks").runner("lua", require("@blocks").program({ "lua", "-e" }))

Uses: core, agent, markdown.

CommandKeysWhat it does
block-result-clearm c (after the leader)Remove the result under the code block at the cursor
block-runm r (after the leader)Run the code block under the cursor and save its output in a result block under it
block-stopm s (after the leader)Stop the code block running under the cursor
blocks-results-clearRemove every code block’s result in the file
blocks-run-allm R (after the leader)Run every code block in the file, in order, saving each one’s result
blocks-trustAlways let this file’s code blocks run, without asking
blocks-untrustAsk again before this file’s code blocks run
OptionDefaultWhat it does
blocks-max-lines1000The most lines of output a block’s result keeps; the rest is cut

cli

Greed’s command line commands that are only Luau: greed ls, greed new, greed bring and greed reference. Plugins add their own the same way:

    greed.cli({
        name = "hello",
        doc = "Say hello",
        usage = "[NAME]",
        run = function(args)
            return `hello {args[1] or "there"}`
        end,
    })

greed hello world then prints “hello world”.

cmdline

The command line: press “:” (alt-x in the vscode style) and type a command, like in Vim or Helix.

Every command can be run by name (:save, :pick-file), and the short names Vim and Helix users know are aliases (:w, :q, :wq, :e). Words after the command are its arguments (:open src/main.rs), and a trailing “!” forces it (:q! quits with unsaved changes). :lua runs Luau and := shows the value of an expression. A line range in front (:%s/a/b/g, :5,10d) selects those lines first; ex.luau has Vim’s line commands, and :g/regex/command is here.

Matches show above the prompt as you type. Tab and shift-tab go through them, up and down go through earlier command lines starting with what’s typed, enter runs and esc closes.

Other plugins add aliases and argument completion through require("@cmdline"):

    local cmdline = require("@cmdline")
    cmdline.alias("gs", "git-status")
    cmdline.completer("branch", function(word)
        return { { text = "main" }, { text = "dev" } }
    end)

A command then uses args = { { name = "branch", complete = "branch" } }.

Uses: core.

CommandKeysWhat it does
command-line: (read, every style’s command mode), ctrl-g (vscode), alt-x (vscode’s typing mode)Type a command to run, with completion, starting with the text given
command-line-closectrl-c, esc (in cmdline buffers)Close the command line without running anything
command-line-newerctrl-n, down (in cmdline buffers)Show the next command line that starts with what’s typed
command-line-nexttab (in cmdline buffers)Fill in the next match on the command line
command-line-olderctrl-p, up (in cmdline buffers)Show the previous command line that starts with what’s typed
command-line-prevshift-tab (in cmdline buffers)Fill in the previous match on the command line
command-line-runenter (in cmdline buffers)Run what’s typed on the command line
copy-linesCopy the selected lines below a line, 0 for the top (:t)
delete-linesDelete the selected lines, copying them (:d)
eval-selectionl e (after the leader)Run the selected Luau, or the line, and show what it returns at the end of it
get-optionShow an option’s value
move-linesMove the selected lines below a line, 0 for the top (:m)
normalType keys in normal mode on each selected line (:norm)
setSet an option: NAME VALUE; for a yes-or-no one, NAME, noNAME or NAME! as in Vim; NAME? shows it
sort-linesSort the selected lines (:sort): ! reverses, u keeps one of each, i ignores case, n sorts by the first number
substituteReplace a regex with text on the selected lines (:s/regex/text/flags)
toggle-optionTurn a yes-or-no option on or off
yank-linesCopy the selected lines (:y)
OptionDefaultWhat it does
substitute-previewtrueShow what :s will change as you type it, before it runs

context

The context: what you’ve pinned for models and agents to see, as live references read fresh each time they’re sent.

  space i p   pin the selection, or the file when nothing is selected
  space i i   show the pinned items, one a line with its size; delete a
              line (x d) to drop the item

:context-add-problems pins this file’s problems, :context-add-diff the changes in progress, :context-clear drops everything. Assist commands send the context with each question, and agents read it with the context_get tool. It’s kept per project.

A plugin adds items that are worked out when sent:

    require("@context").define("todos", function()
        return "the TODO lines of the project ..."
    end)
    require("@context").add({ kind = "computed", name = "todos" })

Uses: core, agent, vcs.

CommandKeysWhat it does
contexti i (after the leader)Show what’s pinned for models and agents, with sizes; delete a line to drop it
context-addi p (after the leader)Pin the selection to the context, or the file when nothing is selected
context-add-diffPin the changes in progress to the context
context-add-problemsPin this file’s problems to the context
context-clearDrop everything pinned to the context
context-closeesc, q (in context buffers)Close the context list

core

Greed’s editing engine: the commands, motions, text objects, registers, floats, prompts and the editing styles registry, written against the same API any plugin uses. The editing styles themselves, like Helix and Vim, are plugins that give these commands keys.

CommandKeysWhat it does
add-newline-above[ space (helix, vim)Add an empty line above each selection, staying put
add-newline-below] space (helix, vim)Add an empty line below each selection, staying put
align-selections& (helix)Line the selections up at one column, with spaces before them
align-view-bottomz b (helix, vim), b (view)Scroll so the cursor’s line is at the bottom of the screen
align-view-topz t (helix, vim), t (view)Scroll so the cursor’s line is at the top of the screen
append-line-endA (helix)Insert at the end of each line
append-modea (helix)Insert after each selection
big-word-endE (helix)Select to the end of the WORD
bufferShow an open file by its name or part of it, as Vim’s :b does; # is the one you were in before
buffer-closectrl-w (vscode)Close this buffer, showing another file in its place; refuses with unsaved changes (force closes anyway)
buffer-close-othersClose every file but this one; refuses if one has unsaved changes (force closes anyway)
buffer-nextg n (helix)Show the next open file
buffer-prevg p (helix)Show the previous open file
center-viewz c (helix), z z (helix, vim), c, z (view)Scroll so the cursor’s line is in the middle of the screen
changec (helix)Copy each selection, delete it and switch to insert mode
change-without-copyalt-c (helix)Delete each selection and switch to insert mode, leaving the clipboard alone
client-followHave this client show the project, tabs and panes the other clients follow, closing the panes it had on its own (their buffers stay open)
client-independentLay this client out on its own, starting from what it shows now: its tabs, panes and project no longer follow the other clients
collapse; (helix), esc (vscode)Collapse each selection to its cursor
config-openOpen your config, init.luau, to change it; it runs again when saved
config-reloadRun your config again, as saving it does
copyspace Y, space y, y (helix)Copy each selection to the clipboard
copy-lines-downalt-shift-down (vscode)Copy the cursors’ lines below them
copy-lines-upalt-shift-up (vscode)Copy the cursors’ lines above them
copy-or-linectrl-c (vscode)Copy the selection, or the line when nothing is selected
copy-selection-downC (helix), ctrl-alt-down (vscode)Add a selection on the next line, at the same columns
copy-selection-upalt-C (helix), ctrl-alt-up (vscode)Add a selection on the line above, at the same columns
cutCopy each selection to the clipboard and delete it
cut-or-linectrl-x (vscode)Cut the selection, or the line when nothing is selected
decrementctrl-x (helix)Subtract the count (or 1) from the number at each cursor
dedent< (helix), ctrl-d (vim-insert), shift-tab (vscode)Take one level of indent off the lines of each selection
deleted (helix)Copy each selection and delete it
delete-backbackspace (every style’s typing mode)Delete the character before each cursor
delete-forwarddelete (every style’s typing mode)Delete the character after each cursor
delete-lines-at-cursorctrl-K (vscode)Delete the lines the cursors are on
delete-to-line-endctrl-k (every style’s typing mode)Delete to the end of the line from each cursor
delete-to-line-startctrl-u (every style’s typing mode)Delete back to the start of the line from each cursor
delete-without-copyalt-d (helix)Delete each selection, leaving the clipboard alone
delete-word-backalt-backspace, ctrl-w (every style’s typing mode), ctrl-backspace (vscode)Delete back to the start of the word before each cursor
delete-word-forwardalt-d (every style’s typing mode), ctrl-delete (vscode)Delete to the start of the next word after each cursor
detachLeave the editor running in the background; greed attach comes back
earlieralt-u (helix), g - (vim)Go back to the state of the text made before this one, on any undo branch
erase-backbackspace (vscode)Delete the selection, or the character before the cursor
erase-forwarddelete (vscode)Delete the selection, or the character after the cursor
errorsList errors from plugin code that nothing caught, with the plugin, what it ran and where it failed
expand-selectionalt-o (helix)Grow each selection to the syntax node around it
extend-big-word-endE (helix-select)Extend the selection to the end of the WORD
extend-big-word-leftB (helix-select)Extend the selection to the previous WORD
extend-big-word-rightW (helix-select)Extend the selection to the next WORD
extend-downdown, j (helix-select), shift-down (vscode)Extend the selection one line down
extend-endg e (helix-select), ctrl-shift-end (vscode)Extend the selection to the end of the buffer
extend-find-next-charf (helix-select)Extend to and including the next character typed
extend-find-prev-charF (helix-select)Extend back to the previous character typed
extend-first-non-blankg s (helix-select)Extend the selection to the first character that isn’t a space
extend-goto-lineG (helix-select)Extend the selection to the line given as a count, or the last line
extend-lefth, left (helix-select), shift-left (vscode)Extend the selection one character left
extend-line-endend, g l (helix-select), shift-end (vscode)Extend the selection to the end of the line
extend-line-startg h, home (helix-select)Extend the selection to the start of the line
extend-rightl, right (helix-select), shift-right (vscode)Extend the selection one character right
extend-smart-homeshift-home (vscode)Select to the first non-blank of the line, or to its start
extend-startg g (helix-select), ctrl-shift-home (vscode)Extend the selection to the start of the buffer
extend-till-next-chart (helix-select)Extend up to the next character typed
extend-till-prev-charT (helix-select)Extend back to just after the previous character typed
extend-to-line-boundsX (helix)Extend each selection to whole lines
extend-upk, up (helix-select), shift-up (vscode)Extend the selection one line up
extend-word-ende (helix-select)Extend the selection to the end of the word
extend-word-leftb (helix-select), ctrl-shift-left (vscode)Extend the selection to the previous word
extend-word-rightw (helix-select), ctrl-shift-right (vscode)Extend the selection to the next word
file-deleteDelete this file from the disk, after you press y, and close it
file-renameRename or move this file: a name alone keeps it in its folder, a path is from the project
findctrl-f (vscode)Find text in the file as you type, as VS Code’s ctrl-f does: enter and shift-enter go to the next and previous match, esc closes
find-nextenter, f3 (in find buffers)Go to the next match with the find bar open
find-next-charf (helix)Select up to and including the next character typed
find-prevshift-enter, shift-f3 (in find buffers)Go to the previous match with the find bar open
find-prev-charF (helix)Select back to the previous character typed
first-non-blankg s (helix)Move to the first character on the line that isn’t a space
flipalt-; (helix)Swap the anchor and head of each selection
fold-closez C (helix), z c (vim)Fold the block the cursor is in under its first line
fold-close-allz M (vim), ctrl-k ctrl-0 (vscode)Fold every outermost block in the buffer
fold-nextz j (vim)Go to the start of the block below the cursor
fold-openz o (helix, vim), z d (vim)Unfold the folds the cursor is in
fold-open-allz R (helix, vim), z E (vim), ctrl-k ctrl-j (vscode)Unfold everything in the buffer
fold-prevz k (vim)Go to the end of the block above the cursor
fold-selectionFold the selected lines under the first
fold-toggletab, z a (helix, vim), ctrl-k ctrl-l (vscode)Fold the block the cursor is in, or unfold it
fold-toggle-allshift-tab (helix)Unfold everything when anything is folded, or else fold every outermost block
font-biggerctrl-+, ctrl-= (global)Make the window’s text bigger
font-resetctrl-0 (global)Put the window’s text back to its size before zooming
font-smallerctrl-- (global)Make the window’s text smaller
goto-columng | (helix)Move to the column given as a count, on each cursor’s line
goto-definitiong d (every style’s command mode), f12 (vscode)Go to where the thing under the cursor is defined
goto-endctrl-end (vscode)Move to the end of the buffer
goto-fileg f (helix)Open the file named under the cursor or in the selection
goto-last-accessed-fileg a (helix), ctrl-6, ctrl-^ (every style’s command mode)Show the file you were in before this one
goto-last-lineg e (helix)Move to the start of the last line
goto-last-modificationg . (helix), ctrl-k ctrl-q (vscode)Move to where this file was last changed
goto-last-modified-fileg m (helix)Show the file you changed last, other than this one
goto-lineG (helix)Move to the line given as a count, or the last line
goto-parent-endalt-e (helix)Move to the end of the syntax node around each selection’s
goto-parent-startalt-b (helix)Move to the start of the syntax node around each selection’s
goto-startg g (helix), ctrl-home (vscode)Move to the start of the buffer, or with a count, to that line
goto-window-bottomg b (helix)Move to the bottom line on the screen
goto-window-centerg c (helix)Move to the middle line on the screen
goto-window-topg t (helix)Move to the top line on the screen
half-page-downctrl-d (helix, read, view, vim), z ctrl-d (helix)Move half a screen down
half-page-upctrl-u (helix, read, view, vim), z ctrl-u (helix)Move half a screen up
hoverk (after the leader), K (vim), ctrl-k ctrl-i (vscode)Show what’s under the cursor, and any problems there
incrementctrl-a (helix)Add the count (or 1) to the number at each cursor
indent> (helix), ctrl-t (vim-insert), ctrl-] (vscode)Indent the lines of each selection
insert-charInsert the typed character at each cursor
insert-line-abovectrl-shift-enter (vscode)Start a new line above this one
insert-line-belowctrl-enter (vscode)Start a new line below this one
insert-line-startI (helix)Insert at the start of each line, after its indent
insert-modei (helix)Insert before each selection
insert-newlineenter (every style’s typing mode), alt-enter (every style’s typing mode; in shell buffers)Insert a line break at each cursor, starting the new line at its indent
insert-registerctrl-r (every style’s typing mode)Insert what the register typed next holds at each cursor
insert-tabtab (every style’s typing mode)Insert a level of indent at each cursor
join-linesJ (helix)Join the next line onto each cursor’s, or the lines of each selection, with one space
jump-backctrl-o (helix, vim), alt-left (vscode)Go back to where the last jump started
jump-forwardctrl-i (helix, vim), alt-right (vscode)Go forward again after jumping back
jump-to-wordg w (helix)Label the words on screen and select the one whose label is typed
keep-matchingK (helix)Keep the selections that match a regex
keep-primary, (helix)Keep only the primary selection
lateralt-U (helix), g + (vim)Go on to the state of the text made after this one, on any undo branch
line-endend (helix, every style’s typing mode, vscode), g l (helix)Move to the end of the line
line-startg h (helix), home (helix, every style’s typing mode)Move to the start of the line
lowercase``` (helix)Make the selections lowercase
macro-saveSave the last macro (or the one in a register) as a command you can run and edit
match-bracketm m (helix)Move to the matching bracket, or the closing one of the pair around the cursor
merge-consecutive-selectionsalt-_ (helix)Merge selections that touch or overlap
merge-selectionsMerge all selections into one, from the first to the last
motion-hint-1Do the last motion 1 more times
motion-hint-2Do the last motion 2 more times
motion-hint-3Do the last motion 3 more times
motion-hint-4Do the last motion 4 more times
motion-hint-5Do the last motion 5 more times
motion-hint-6Do the last motion 6 more times
motion-hint-7Do the last motion 7 more times
motion-hint-8Do the last motion 8 more times
motion-hint-9Do the last motion 9 more times
move-downdown (helix, read, every style’s typing mode, vscode), j (helix, read)Move one line down
move-lefth (helix, read), left (helix, read, every style’s typing mode, vscode)Move one character left
move-lines-downalt-down (vscode)Move the cursors’ lines down one
move-lines-upalt-up (vscode)Move the cursors’ lines up one
move-rightl (helix, read), right (helix, read, every style’s typing mode, vscode)Move one character right
move-upk (helix, read), up (helix, read, every style’s typing mode, vscode)Move one line up
move-word-leftctrl-left (vscode)Move to the start of the previous word
move-word-rightctrl-right (vscode)Move to the start of the next word
next-big-wordW (helix)Select to the start of the next WORD (punctuation included)
next-comment] c (helix, vim)Select the next comment
next-entry] e (helix, vim)Select the next entry
next-function] f (helix, vim)Select the next function
next-parameter] a (helix, vim)Select the next parameter
next-test] T (helix, vim)Select the next test
next-type] t (helix, vim)Select the next type
next-wordw (helix)Select to the start of the next word
normal-modeesc (every style’s typing mode)Back to the editing style’s own mode (normal, in Helix’s)
openOpen a file in the main view
open-aboveO (helix)Open a new line above each cursor and insert there
open-belowo (helix)Open a new line below each cursor and insert there
page-downctrl-f (helix, read, view, vim), pagedown (helix, read, every style’s typing mode, vim, vscode), z ctrl-f (helix)Move a screen down
page-upctrl-b (helix, read, view, vim), pageup (helix, read, every style’s typing mode, vim, vscode), z ctrl-b (helix)Move a screen up
paste-afterp, space p (helix)Paste after each selection
paste-beforeP (helix)Paste before each selection
paste-imagealt-v (in agent-transcript buffers)Paste the image on your clipboard into what you’re in, like an agent’s box
paste-overctrl-v (vscode)Paste, replacing each selection; whole lines go above the cursor’s line
play-macroq (helix)Play the macro in @, or the register picked
press-buttonenter (helix, read, vim)Run the button under the cursor, as clicking it does
prev-big-wordB (helix)Select to the start of the previous WORD
prev-comment[ c (helix, vim)Select the previous comment
prev-entry[ e (helix, vim)Select the previous entry
prev-function[ f (helix, vim)Select the previous function
prev-parameter[ a (helix, vim)Select the previous parameter
prev-test[ T (helix, vim)Select the previous test
prev-type[ t (helix, vim)Select the previous type
prev-wordb (helix)Select to the start of the previous word
profileStart counting where the editor’s time goes; again, stop and show it, the most time first
projectOpen a project, or switch to it if it’s open
project-closeClose the shown project, unless it has unsaved changes (force closes anyway)
project-configRun the shown project’s own config in .greed/, if you trusted it
prompt-cancelctrl-c, esc (in find, prompt buffers)Close the prompt without accepting
prompt-submitenter (in prompt buffers)Accept what’s typed in the prompt
quitctrl-q (global)Close this pane when there are others; otherwise close the project and leave, or exit the editor if it’s the last one, refusing with unsaved changes (force goes anyway)
quit-abortIn a file a program waits on, like git commit’s message, stop the program with an error, as Vim’s :cq does; whatever was written is left as it is
quit-allEnd the session and every project in it, whatever runs, unless files have unsaved changes (force ends it anyway)
quit-askctrl-q (vscode)Leave the project, or exit the editor, from any pane, as VS Code’s ctrl-q does; asks whether to save unsaved files first rather than refusing
quit-sessionEnd the session, whatever runs in it, unless files have unsaved changes (force ends it anyway); terminals, shells and agents never count as unsaved
record-macroQ (helix)Start recording keys into a macro (@, or the register picked), or stop
redoU (helix), ctrl-Z, ctrl-y (vscode)Redo the last undone change
reflowRewrap the lines of each selection to the text width, or the width given
reindent-linesIndent each selected line as its language and the lines around it say
reloadLoad the file again from disk, as an edit undo can take back
reload-allLoad every open file again from disk, as edits undo can take back
remove-matchingalt-K (helix)Remove the selections that match a regex
remove-primaryalt-, (helix)Remove the primary selection
repeat-change. (helix, vim)Repeat the last change, at the cursors; a count replaces the one it had
repeat-findalt-. (helix)Do the last f, t, F or T again; in select mode, extending
replace-charr (helix)Replace every selected character with the next one typed
replace-in-filectrl-h (vscode)Replace text everywhere in the file, as VS Code’s ctrl-h does: what to find, with its matches lit and counted, then what to put instead
replace-with-copyR, space R (helix)Replace each selection with what was copied
rotate-contentsalt-) (helix)Move each selection’s text to the selection after it, the last’s to the first
rotate-contents-backalt-( (helix)Move each selection’s text to the selection before it, the first’s to the last
rotate-primary) (helix)Make the next selection the primary one
rotate-primary-back( (helix)Make the previous selection the primary one
savectrl-s (global)Write the buffer to its file, or to a new one
save-allctrl-k s (vscode)Write every file with unsaved changes
save-quitZ Z (vim)Write the buffer to its file if it changed, then quit
save-quit-allWrite every file with unsaved changes, then end the session
scroll-downz down, z j (helix), down, j (view), ctrl-e (vim)Scroll the view a line down
scroll-upz k, z up (helix), k, up (view), ctrl-y (vim)Scroll the view a line up
search/ (helix, read, vim)Search forward for a regex as you type
search-back? (helix, vim)Search backward for a regex as you type
search-clearTake away the search’s match highlights, as Vim’s :noh does
search-nextn (helix, read, vim), f3 (vscode)Select the next match of the last search
search-next-addn (helix-select)Add the next match of the last search as another selection
search-prevN (helix, read, vim), shift-f3 (vscode)Select the previous match of the last search
search-prev-addN (helix-select)Add the previous match of the last search as another selection
search-selection* (helix)Search for the selected text, as whole words where it starts or ends one
search-selection-anywherealt-* (helix)Search for the selected text, also inside longer words
secret-setKeep a secret, like an API key, in the system keyring (or Greed’s secrets file)
select-all% (helix), ctrl-a (vscode)Select the whole buffer
select-all-childrenalt-I (helix)Select every syntax node inside each selection’s
select-all-matchesctrl-L (vscode)Select every match of the selected text (or the word at the cursor)
select-all-siblingsalt-a (helix)Select every syntax node beside each selection’s, its own included
select-around-anglesm a <, m a > (helix)Select around the angles
select-around-backticks`m a `` (helix)Select around the backticks
select-around-big-wordm a W (helix)Select the WORD and the space after it
select-around-bracesm a {, m a } (helix)Select around the braces
select-around-bracketsm a [, m a ] (helix)Select around the brackets
select-around-commentm a c (helix)Select around the comment at each selection
select-around-double-quotesm a " (helix)Select around the double-quotes
select-around-entrym a e (helix)Select around the entry at each selection
select-around-functionm a f (helix)Select around the function at each selection
select-around-indentm a i (helix)Select the lines indented as far as this one, and the one above
select-around-lastm a l (helix)Select around the last pair of the bracket or quote typed next
select-around-nextm a n (helix)Select around the next pair of the bracket or quote typed next
select-around-numberm a d (helix)Select the number at or after the cursor, with its sign
select-around-pairm a m (helix)Select around the nearest brackets or quotes
select-around-parameterm a a (helix)Select around the parameter at each selection
select-around-parensm a (, m a ) (helix)Select around the parens
select-around-quotesm a ' (helix)Select around the quotes
select-around-tagm a x (helix)Select around the XML or HTML element
select-around-testm a T (helix)Select around the test at each selection
select-around-typem a t (helix)Select around the type at each selection
select-around-wordm a w (helix)Select the word and the space after it
select-inside-anglesm i <, m i > (helix)Select inside the angles
select-inside-backticks`m i `` (helix)Select inside the backticks
select-inside-big-wordm i W (helix)Select the WORD
select-inside-bracesm i {, m i } (helix)Select inside the braces
select-inside-bracketsm i [, m i ] (helix)Select inside the brackets
select-inside-commentm i c (helix)Select inside the comment at each selection
select-inside-double-quotesm i " (helix)Select inside the double-quotes
select-inside-entrym i e (helix)Select inside the entry at each selection
select-inside-functionm i f (helix)Select inside the function at each selection
select-inside-indentm i i (helix)Select the lines indented as far as this one
select-inside-lastm i l (helix)Select inside the last pair of the bracket or quote typed next
select-inside-nextm i n (helix)Select inside the next pair of the bracket or quote typed next
select-inside-numberm i d (helix)Select the number at or after the cursor
select-inside-pairm i m (helix)Select inside the nearest brackets or quotes
select-inside-parameterm i a (helix)Select inside the parameter at each selection
select-inside-parensm i (, m i ) (helix)Select inside the parens
select-inside-quotesm i ' (helix)Select inside the quotes
select-inside-tagm i x (helix)Select inside the XML or HTML element
select-inside-testm i T (helix)Select inside the test at each selection
select-inside-typem i t (helix)Select inside the type at each selection
select-inside-wordm i w (helix)Select the word
select-linex (helix)Select the whole line, including its line break; again, the next line too
select-linesctrl-l (vscode)Select the cursor’s line, or the next one too when whole lines are selected
select-next-matchctrl-d (vscode)Select the word at the cursor, then add the next match of it as another selection
select-next-sibling] n (helix, vim)Select the next syntax node beside each selection
select-prev-sibling[ n (helix, vim)Select the previous syntax node beside each selection
select-regexs (helix)Select every match of a regex inside the selections
select-register" (helix, vim)Pick a register, by the next key typed, for the next copy, paste or macro
selection-redoctrl-k (helix), ctrl-U (vscode)Go forward to the selection selection-undo went back from
selection-undoctrl-h (helix), ctrl-u (vscode)Go back to the selection before the last command
sessionGo to another session: a running one by name, or a project’s (started if needed)
shrink-selectionalt-i (helix)Shrink each selection back, or to the first syntax node inside it
shrink-to-line-boundsalt-x (helix)Shrink each selection to the whole lines inside it
smart-homehome (vscode)Go to the first non-blank of the line, or to its start
split-linesalt-s (helix)Split the selections into lines
split-regexS (helix)Split the selections at every match of a regex
stallsList the times plugin code held the editor up, with the plugin and what it ran
styleSwitch editing style, or pick one
surround-addm s (helix)Wrap each selection in the pair of the next character typed
surround-deletem d (helix)Take away the pair of the next character typed from around each selection
surround-replacem r (helix)Swap the pair of the next character typed around each selection for another’s
swap-node-nextalt-shift-right (helix, vim, vim-visual)Move the syntax node selected past the one after it
swap-node-prevalt-shift-left (helix, vim, vim-visual)Move the syntax node selected past the one before it
switch-case~ (helix)Switch the case of each selected letter
text-float-closeClose the float showing text
till-next-chart (helix)Select up to the next character typed
till-prev-charT (helix)Select back to just after the previous character typed
toggle-commentctrl-c, space c (helix), ctrl-/, ctrl-_ (vscode)Comment the lines of each selection, or uncomment them if they all are
trim-selections_ (helix)Trim the spaces and line breaks off both ends of each selection
trustTrust the shown project’s own config in .greed/, and run it now and whenever it opens
type-charType the pressed character, replacing any selection
type-newlineenter (vscode)Type a line break, replacing any selection, and start the new line at its indent
type-tabtab (vscode)Indent the selected lines when a selection spans lines, otherwise type a level of indent
undou (helix), ctrl-z (vscode)Undo the last change
uppercase`alt-`` (helix)Make the selections uppercase
view-modeZ (helix)Keys for the view until esc: center, top, bottom, scroll
view-mode-leaveesc (view)Back from the view mode
word-ende (helix)Select to the end of the word
OptionDefaultWhat it does
auto-reloadtrueLoad files changed on disk into their buffers, unless they have unsaved changes
auto-savefalseSave files on their own a moment after their edits stop
auto-save-delay1000How long after the last edit auto-save saves, in milliseconds
editing-style"helix"How editing works: helix, vim, vscode or another style a plugin adds (see :style)
effectsfalseSmall animations, like a wave of light through the status line when you switch files
gui-font""The font the window draws text in, by family name; empty for the system’s monospaced font
gui-font-prose""The proportional font the window draws prose in (styles with font = “prose”, like Markdown’s text), by family name; empty for the system’s sans-serif
gui-font-size15The size of the window’s text in pixels, before the screen’s scale; zooming gives a window a size of its own
indent" "One level of indent, spaces or a tab, in languages that don’t set their own
jump-label-alphabet"abcdefghijklmnopqrstuvwxyz"The letters jump labels are made of, most comfortable first
line-numbers"absolute"How line numbers count: “absolute”, “relative” to the cursor’s line, or “hybrid” (relative, with the cursor’s line its own number)
link-opener""What opens a link, e.g. “firefox”; empty for xdg-open, or open on macOS
motion-hintsfalseNumber the next places a word motion would go, and go there on a digit
notes-search-case"ignore"How searching notes minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
on-quit"auto"What :q on the last project does to the session: “auto” ends it unless something is running (a command in a terminal, an agent at work) and keeps it then, “end” always ends it, “keep” always leaves it running to attach to again
on-quit-remote"keep"Like on-quit, when you’re attached over greed ssh
search-case"smart"How searches you type mind case, unless their kind says otherwise: “smart” (only when what you type has a capital letter), “ignore” or “match”
session-idle-hours24A session nothing is attached to, with nothing running and nothing unsaved, ends after this many hours; 0 keeps it
shell-filter-search-case"ignore"How filtering a shell block with / minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
substitute-search-case"match"How :s minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
text-width80How wide :reflow (and Vim’s gq) makes lines

describe

Help: describe anything the editor is made of, to see what it does, who defined it and where. In the help window, enter jumps to the definition and esc (or q) closes it.

  space h h   describe anything: commands, options, events, the API,
              plugins and modes, in one picker
  space h c   a command          space h o   an option
  space h e   an event           space h a   a function of the API
  space h p   a plugin           space h m   a mode and its keys
  space h k   press a key to see what it runs
  space h i   inspect what's under the cursor, or the editor: an
              object's state, what it relates to and what it can do

Agents get the same through the describe tool, from text.

Uses: core, picker.

CommandKeysWhat it does
describeh h (after the leader)Describe anything: a command, option, event, API function, plugin or mode
describe-apih a (after the leader)Describe a function or type of the plugin API
describe-commandh c (after the leader)Describe a command
describe-eventh e (after the leader)Describe an event
describe-keyh k (after the leader)Press keys to see what command they run
describe-modeh m (after the leader)Describe a mode
describe-objectDescribe an object
describe-optionh o (after the leader)Describe an option
describe-pluginh p (after the leader)Describe a plugin
describe-styleh s (after the leader)Describe a style
help-againr (in help buffers)In the inspector, look at the object again, as it is now
help-backbackspace (in help buffers)In the inspector, go back to the object inspected before
help-closeesc, q (in help buffers)Close the help window
help-gotoenter (in help buffers)Open the file what’s described is defined in, at its definition; in the inspector, inspect the object on this line or run its action
inspecth i (after the leader)Inspect what’s under the cursor: its state, what it relates to, what it can do; with nothing there, start at the editor

flash

Flashes text as it’s copied, so you can see what went to the clipboard.

With a “#rrggbb” background for “ui.flash” in the theme it fades out; with the 16 terminal colors it shows briefly and goes. Either way it’s a decoration and a theme entry changing over time, which is all an animation is.

To change how long it lasts, set “flash-duration” from your config:

    require("@greed").option.set("flash-duration", 150)

Uses: core.

OptionDefaultWhat it does
flash-duration300How long copied text flashes, in milliseconds

format

Formatting: :format, and on every save unless format-on-save is off. A language’s formatter program (see the languages plugin) is used when it’s installed; otherwise the language server is asked. Only what changed is replaced, so the cursor and selections stay where they were.

To stop formatting on save:

    require("@greed").option.set("format-on-save", false)

Uses: core, languages.

CommandKeysWhat it does
format= (helix’s command mode)Format the buffer with its language’s formatter or language server
OptionDefaultWhat it does
format-on-savetrueFormat files when they’re saved

grep

Search the whole project, and replace by editing what was found.

  space /             search the project for a regex as you type
  ctrl-e              in that picker: put the matches in a results panel
  :grep PATTERN DIR?  put the matches in a results panel straight away

The results panel is a multibuffer: each matching line under its file’s name. Edit the lines with anything (s to select the matches and c to change them all works well) and the files change as you type; :w saves them, enter opens the match under the cursor, and q closes the panel.

Patterns are regexes; ones in lowercase ignore case. The project is the folder holding .jj or .git (see core’s project.luau).

Uses: picker, multibuffer.

CommandKeysWhat it does
grepPut the lines matching a regex in a panel, to go to them or edit them
grep-closeClose the search results
search-project/ (after the leader)Search the project for a regex as you type, and go to a match

helix

Helix’s editing style, the default: select text, then say what to do with it. Its modes are helix (normal), helix-insert and helix-select; the status line shows them as NORMAL, INSERT and SELECT. The commands are core’s; this plugin gives them Helix’s keys. Copy any part of it into your own config to change it.

Uses: core.

CommandKeysWhat it does
select-modev (helix, helix-select), esc (helix-select)Start select mode, where moves extend the selection, or leave it

hints

Key hints: after a key that starts longer sequences, like “space”, show what can come next and what each key does. f1 shows the keys of the mode you’re in, and the first time you’re in a mode listed in intro (like typing into a terminal), a line in the status line says how to get out.

Uses: core.

CommandKeysWhat it does
keys-helpf1 (global, pane, resize, tab, terminal)Show the keys of the mode you’re in and what each does

hosts

Connecting to another machine from inside Greed: space C (:connect) lists the hosts you connected to lately and those in ~/.ssh/config, and going to one shows its session here, as greed ssh HOST would, starting one there if none is running. Typing a host that isn’t listed connects to it too.

Uses: core, picker.

CommandKeysWhat it does
connectC (after the leader)Show a session on another machine here, as greed ssh does: pick a host, or type one

image

Image files open as the image: PNG, JPEG and GIF, drawn as wide as the view at most (in the window, and in terminals that show images), under a line with its name, size and zoom.

  +  -  bigger, smaller
  =     as wide as the view, or its own size when it's smaller
  0     its own size

Uses: core.

CommandKeysWhat it does
image-zoom-actual0 (in image buffers)Draw the image at its own size
image-zoom-fit= (in image buffers)Draw the image as wide as the view, or at its own size when it’s smaller
image-zoom-in+ (in image buffers)Draw the image bigger
image-zoom-out- (in image buffers)Draw the image smaller

languages

Languages, batteries included: which files are in which language, their language servers and how they’re commented and indented. The table is in languages.luau. A server only starts if it’s installed, so nothing needs setting up; :health shows what’s found and what’s missing.

Change an entry from your config. Fields you give replace those fields:

    local languages = require("@languages")
    languages.set("python", { servers = { { "pylsp" } } })
    languages.set("odin", { extensions = { "odin" }, servers = { { "ols" } } })

Uses: core, cmdline.

CommandKeysWhat it does
grammar-fetchDownload a grammar Greed knows where to get, ahead of opening a file in it
healthShow each language’s highlighting and servers, and which are installed
languageSet this buffer’s language, for its highlighting, indent and language server; with none, say which it has
OptionDefaultWhat it does
grammar-downloadtrueDownload the grammar for a language Greed has none for when a file in it opens

lsp

Language servers: hover, definitions, references, rename, code actions and diagnostics. Lines with problems get a sign, and the worst problem’s message shows after the line (turn that off with :set diagnostic-messages false).

  space k   show what's under the cursor, with any problems there
  g d       go to the definition
  g r       pick a place it's used
  space r   rename it everywhere
  space a   pick a fix or refactor
  space d   pick a problem in this file
  space D   pick a problem in any file
  ] d / [ d next / previous problem

Servers start the first time a file of their language is open, if one is installed; the languages plugin says which (see :health).

Uses: core, picker, multibuffer, statusline.

CommandKeysWhat it does
code-actiona (after the leader), g r a (vim), ctrl-. (vscode)Pick a fix or refactor for the cursor or the selection
completion-acceptenter (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Put the current completion in place of the word being typed; with none chosen, close the menu and do what enter does
completion-askctrl-x (helix’s typing mode), ctrl-space (every style’s typing mode), tab (every style’s typing mode; in shell buffers)Ask what could go at the cursor: a source like the shell’s, or the language server
completion-closeesc (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Close the completion menu and do what esc does where you were typing
completion-downdown (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Move to the next completion, or in a shell’s menu that opened by itself, the command after
completion-nextctrl-n (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Move to the next completion
completion-prevctrl-p, shift-tab (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Move to the previous completion
completion-tabtab (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Move to the next completion; where tab completes in place, as in a shell, first put in what every match starts with
completion-upup (completion-helix-insert, completion-vim-insert, completion-vscode; in shell buffers)Move to the previous completion, or in a shell’s menu that opened by itself, the command before
completion-wordsctrl-n (vim-insert)Complete the word being typed from words in open files
completion-words-backctrl-p (vim-insert)Complete the word being typed from words in open files, starting with the last
first-diagnostic[ D (every style’s command mode)Go to the first problem in this file
goto-declarationg D (every style’s command mode)Go to where the thing under the cursor is declared
goto-implementationg i (every style’s command mode), g r i (vim)Go to where the thing under the cursor is implemented
goto-referencesg r (every style’s command mode), g r r (vim), shift-f12 (vscode)Pick a place where the thing under the cursor is used, and go there
goto-type-definitiong y (every style’s command mode), g r t (vim)Go to where the type of the thing under the cursor is defined
last-diagnostic] D (every style’s command mode)Go to the last problem in this file
lsp-restartStart this file’s language server again, as when it’s stuck; with all, every server
next-diagnostic] d (every style’s command mode), f8 (vscode)Go to the next problem in this file
pick-diagnosticd (after the leader)Pick a problem in this file and go to it
pick-problemD (after the leader), ctrl-M (vscode)Pick a problem in any file, open or not, and go to it
pick-workspace-symbolS (after the leader), ctrl-t (vscode)Pick a function, type or other symbol anywhere in the project, as the language server knows them
prev-diagnostic[ d (every style’s command mode), shift-f8 (vscode)Go to the previous problem in this file
renamer (after the leader), g r n (vim), f2 (vscode)Rename the thing under the cursor everywhere it’s used
select-referencesH (after the leader)Select every place in this file where the thing under the cursor is used
signature-helpWhile typing, show what the function being called takes
OptionDefaultWhat it does
completion-delay150Milliseconds of no typing before a completion menu opens by itself; 0 opens it only when asked
completion-enter"auto"What enter does in a completion menu: “first” puts in the first item, “selected” only one moved to with tab or the arrows (otherwise enter does what it does without a menu, like running a shell’s line), “auto” lets each source say: the shell “selected”, language servers “first”
diagnostic-messagestrueShow each line’s worst problem after the end of the line

markdown

Markdown as a document: headings without their hashes (the top level twice the size), bold, italic, struck and code without their markers, links as their text, fenced code on a background of its own, quotes with a bar, --- as a line, list dashes as bullets and tables in columns with borders (tables.luau). The file stays plain Markdown: typing on a line shows it as written (see core.reveal). Turn it off with :set markdown-look false, or for one file with markdown-look-toggle. tab folds the section, list item or code block around the cursor (folds.luau).

The reading view (markdown-read) shows the file read-only with no markup at all, images in place and links to follow (links.luau), in the “read” mode; esc goes back to editing at the same place.

Other plugins can draw some parts their own way: notes draws headings and bullets as symbols, and claims them with require("@markdown").claim.

Uses: core.

CommandKeysWhat it does
choice-pickPick the choice under the cursor, unpicking the others in its group
markdown-followFollow the Markdown link under the cursor: to a heading, another file or a web page
markdown-look-togglem l (after the leader), alt-V (vscode’s typing mode)Show this Markdown file as a document or as written, whatever markdown-look says
markdown-readm v (after the leader), alt-v (read, vscode’s typing mode), esc, q, space m v (read)Read this Markdown file as a document, with images and links to follow; again or esc to edit
OptionDefaultWhat it does
markdown-looktrueShow Markdown with its markup hidden and its headings, code and quotes drawn as such

models

Language models by role. Plugins ask for a role, like “fast” or “reasoning”, and your config says which provider and model plays it, so no plugin hard-codes a provider and switching to a local model is one line.

  :models   each role's provider and model and whether its key is found;
            enter changes a role, k sets a key, l signs in

What you pick in :models (or the setup page) is kept in Greed’s data folder and read before your config, which has the last word.

In your config:

    local models = require("@models")
    models.roles.fast = { provider = "ollama", model = "llama3.2" }
    models.providers.work = { kind = "openai", url = "https://llm.example.com/v1", secret = "work" }

Keys come from greed.secrets (:models, :secret-set anthropic, or ANTHROPIC_API_KEY).

Uses: core, picker.

CommandKeysWhat it does
loginSign in to a provider whose plan comes with an account, like Berget Code, instead of using an API key
logoutForget the sign-in kept for a provider
modelsShow and change which model plays each role, set API keys and sign in
models-changeOn the :models page, pick the provider and model that play the role under the cursor
models-closeClose the :models page
models-keyOn the :models page, set the API key of the provider of the role under the cursor
models-loginOn the :models page, sign in to the provider of the role under the cursor
usageHow many tokens the models were sent and wrote today and over the last week

multibuffer

Multibuffers: one buffer made of excerpts from many files, edited in place. Typing in an excerpt edits its file as you go (the file opens in Greed the first time one of its excerpts changes), and a change made to the file anywhere else shows in the excerpt. :w saves every file changed this way, enter goes to the line under the cursor and q closes the view.

Search results open in one. A plugin makes its own with

    require("@multibuffer").open({ { path = "/src/main.rs", from = 9, to = 12 } },
        { summary = "1 place" })

Each file’s name shows above its excerpts and its line numbers beside them; neither is text you can edit. Excerpts that overlap or touch are joined.

Uses: core.

CommandKeysWhat it does
multibuffer-closeq (every style’s command mode; in staged-diff, why, multibuffer buffers)Close the view showing the multibuffer
multibuffer-openenter (every style’s command mode; in multibuffer buffers)Go to the line under the cursor in its file
multibuffer-revertg r (after the leader; in multibuffer buffers)Put the change under the cursor back the way it was, in a view of changes

notes

Notes: a folder of plain Markdown files, linked with [[note]], with todos and an agenda. It’s only files, so git or Syncthing can carry them to other machines and a phone.

  space n d   today's note            space n f   find a note
  space n c   capture a todo          space n /   search the notes
  space n a   the agenda: open todos, soonest due first
  space n x   tick a todo, or untick it
  space n t   jot a thought down in the inbox
  space n r   dismiss the reminders notice
  space n l   have Greed's agent link the inbox to your other notes
  space k     on a due date or a reminder, say when it is in words
  g d         follow the [[link]] under the cursor (making the note)
  tab, z a    fold the section, list item or code block under the cursor,
              or unfold it (Markdown's folds)

In notes (Markdown files in the notes folder, or every Markdown file with notes-everywhere), links show without their brackets, markup shows as symbols (look.luau), and each note lists the notes linking to it at its end. Todos and lines can carry reminders (reminders.luau).

    local greed = require("@greed")
    greed.option.set("notes-folder", "~/Documents/notes")

Uses: core, picker, agent, markdown, statusline, acp.

CommandKeysWhat it does
notes-agendan a (after the leader)Pick from the open todos in every note, soonest due first
notes-capturen c (after the leader)Add a todo to the inbox note, from wherever you are: the project’s when it keeps notes
notes-capture-mineAdd a todo to your own inbox note, even in a project that keeps notes
notes-connectn l (after the leader)Have Greed’s agent go through the inbox (or the note you’re in), link it to your other notes and add context, as changes you review
notes-dailyn d (after the leader)Open today’s note, making it if there isn’t one
notes-findn f (after the leader)Open a note by name
notes-reminder-openOpen the first reminder in the notice, and close the notice
notes-reminders-dismissn r (after the leader)Close the reminders notice
notes-searchn / (after the leader)Search every note as you type
notes-thoughtn t (after the leader)Jot a thought or an idea down in the inbox note, with today’s date, from wherever you are
todo-togglen x (after the leader)Tick the todo on each selected line, or untick it; a plain line becomes a todo
OptionDefaultWhat it does
notes-concealtrueShow links in notes without their brackets, and markup as symbols
notes-everywherefalseTreat every Markdown file as a note: concealed links and backlinks
notes-folder"~/notes"The folder notes live in; ~ is your home folder
notes-project-folder".greed/notes"Where a project keeps its notes, in the project: they’re named project:NAME, and the plan and decisions you share with agents are among them
notes-remind-desktoptrueShow reminders as desktop notifications with notify-send or osascript, where installed
notes-remind-time"09:00"When a reminder with a date and no time goes off, as HH:MM

operations

Operations: work across files taken as one thing. An operation groups the edits it makes in every buffer and the programs it runs, so you can review it in one view and undo it in one step.

  space u o   recent operations, to review one
  space u u   undo the latest operation (in a review, that one)

In a review, space g r puts back the change under the cursor.

A plugin or agent makes one with

    local operations = require("@operations")
    operations.run("rename parse to parse_line", function(op)
        -- edit buffers, then
        operations.exec(op, { "cargo", "test" })
    end)

Uses: core, picker, multibuffer.

CommandKeysWhat it does
operation-undou u (after the leader)Undo the latest operation, or the one being reviewed, in every file at once
operationsu o (after the leader)Pick a recent operation to review: its changes across files and the programs it ran

panes

Panes and tabs, the way a terminal multiplexer has them: split the screen between files and terminals, move between them, give a pane more room, keep tabs of layouts (arranged by the layout managers in layouts.luau). The keys follow zellij’s where they can:

  • alt keys work everywhere, in every editing style and in terminals:
  alt-h/j/k/l move between panes (and to the next tab at the edge),
  alt-n opens a terminal pane (a floating one while floats are shown),
  alt-f shows or hides the tab's floating views (opening a floating
  terminal when there are none), alt-F floats this file, alt-= and
  alt-- resize, alt-< and alt-> (alt-[ and alt-] too) or alt-1..9
  change tabs. alt-w changes how the tab lays out its floats (see
  floats.luau), alt-{ and alt-} cycle through them and alt-W picks one
  from the dock.
  • alt-p and alt-t open the pane and tab modes, where single keys act until
  esc.
  • In normal mode, ctrl-w (and the leader’s w) is the window prefix Vim and Helix
  users know, and in Vim, g t and g T change tabs.

Every key is in M.keys; rebind or unbind them from your config like any other (see docs/keys.md).

Uses: core, picker, terminal, statusline.

CommandKeysWhat it does
float-dockShow the dock of this tab’s floating views, or hide it
float-dock-pickalt-W (global, terminal)Pick a floating view from the dock: 1 to 9, or the arrows and enter
float-filealt-F (global, terminal), E (pane)Open a file (this one if none is given) in a new floating view
float-flipF, e (pane)Take this pane out to float over the tab, or put a floating view back in as a pane
float-goBring this tab’s floating view number N to the front, as the dots on its border count
float-layout-nextalt-w (global, terminal)Lay this tab’s floating views out with the next float layout
float-layout-pickY (pane)Pick how this tab lays out its floating views
float-move-downdown (resize)Move the floating view down
float-move-leftleft (resize)Move the floating view left
float-move-rightright (resize)Move the floating view right
float-move-upup (resize)Move the floating view up
float-nextalt-} (global, terminal), } (pane)Bring the next floating view in this tab to the front
float-previousalt-{ (global, terminal), { (pane)Bring the floating view before in this tab to the front
float-terminal-newW (pane)Open another floating terminal in this tab
floats-cascadeC (pane)Stack this tab’s floating views in a cascade, from now on
floats-oneO (pane)Show this tab’s floating views one at a time, like tabs, all in the same place, from now on
floats-tileT (pane)Lay this tab’s floating views out side by side, from now on
floats-togglealt-f (global, terminal), w, z (pane)Hide this tab’s floating views or show them again; with none, open a floating terminal
layout-main-grow] (pane)Give the main pane more room
layout-main-shrink[ (pane)Give the main pane less room
layout-nextalt-space (global, terminal), space (pane)Arrange this tab with the next layout
layout-openOpen a saved layout as a new tab
layout-picky (pane)Pick how this tab arranges its panes
layout-rotateo (pane)Move every pane one place along
layout-saveSave this tab’s panes, and what they show, as a layout of the project
layout-save-mineSave this tab’s panes, and what they show, as a layout for any project
layout-swap-mainalt-m (global, terminal), m (pane)Swap this pane with the main one
pane-closew c, w q (after the leader), ctrl-w c, ctrl-w q (every style’s command mode)Close this pane
pane-close-donex (pane)pane-close, then back from the mode
pane-downalt-down, alt-j (global, terminal), w j (after the leader), down, j (pane), ctrl-w j (every style’s command mode)Move to the pane below
pane-file-downw f (after the leader), ctrl-w f (every style’s command mode)Open the file named under the cursor in a pane below
pane-file-rightw F (after the leader), ctrl-w F (every style’s command mode)Open the file named under the cursor in a pane on the right
pane-growalt-+, alt-= (global, terminal), +, = (pane, resize)Give this pane more room
pane-leftalt-h, alt-left (global, terminal), w h (after the leader), h, left (pane), ctrl-w h (every style’s command mode)Move to the pane on the left, or the tab before
pane-modealt-p (global, terminal)Keys for panes until esc: move, split, resize, close
pane-mode-leavealt-p (pane), enter, esc (pane, resize, tab), alt-r (resize), alt-t (tab)Back from the pane or tab mode
pane-new-downw n s (after the leader), ctrl-w n s (every style’s command mode)Open a pane below with a new, empty buffer
pane-new-rightw n v (after the leader), ctrl-w n v (every style’s command mode)Open a pane on the right with a new, empty buffer
pane-nextw w (after the leader), tab (pane), ctrl-w w (every style’s command mode)Move to the next pane
pane-onlyw o (after the leader), ctrl-w o (every style’s command mode)Close every other pane in the tab
pane-pickw g (after the leader), g (pane), ctrl-w g (every style’s command mode)Label the panes and go to the one whose label you type next; esc leaves the pane mode
pane-placep (pane)Show where a new pane would go, to move it and open it there
pane-place-cancelesc (pane-place)Put things back without a new pane
pane-place-downdown, j (pane-place)Move the new pane’s frame down
pane-place-hereenter (pane-place)Open a pane with this file where the frame is
pane-place-lefth, left (pane-place)Move the new pane’s frame left
pane-place-rightl, right (pane-place)Move the new pane’s frame right
pane-place-terminalt (pane-place)Open a terminal where the frame is
pane-place-upk, up (pane-place)Move the new pane’s frame up
pane-resize-downJ (pane), j (resize)Make this pane bigger by moving its bottom edge down, or the other edge at the screen’s edge
pane-resize-leftH (pane), h (resize)Make this pane bigger by moving its left edge out, or the other edge at the screen’s edge
pane-resize-rightL (pane), l (resize)Make this pane bigger by moving its right edge out, or the other edge at the screen’s edge
pane-resize-upK (pane), k (resize)Make this pane bigger by moving its top edge up, or the other edge at the screen’s edge
pane-rightalt-l, alt-right (global, terminal), w l (after the leader), l, right (pane), ctrl-w l (every style’s command mode)Move to the pane on the right, or the tab after
pane-shrinkalt-- (global, terminal), - (pane, resize)Give this pane less room
pane-shrink-downJ (resize)Make this pane smaller by moving its bottom edge up, or the other edge at the screen’s edge
pane-shrink-leftH (resize)Make this pane smaller by moving its left edge in, or the other edge at the screen’s edge
pane-shrink-rightL (resize)Make this pane smaller by moving its right edge in, or the other edge at the screen’s edge
pane-shrink-upK (resize)Make this pane smaller by moving its top edge down, or the other edge at the screen’s edge
pane-split-downw s (after the leader), s (pane), ctrl-w s (every style’s command mode)Show this pane’s file in a new pane below
pane-split-rightw v (after the leader), v (pane), ctrl-w v (every style’s command mode), ctrl-\ (vscode)Show this pane’s file in a new pane on the right
pane-swap-downw J (after the leader), ctrl-w J (every style’s command mode)Swap this pane with the one below
pane-swap-leftw H (after the leader), ctrl-w H (every style’s command mode)Swap this pane with the one on its left
pane-swap-rightw L (after the leader), ctrl-w L (every style’s command mode)Swap this pane with the one on its right
pane-swap-upw K (after the leader), ctrl-w K (every style’s command mode)Swap this pane with the one above
pane-terminalalt-n (global, terminal), w t (after the leader), ctrl-w t (every style’s command mode)Open a terminal in a new pane, or a floating one while floating views are shown
pane-terminal-donen (pane)pane-terminal, then back from the mode
pane-terminal-downd (pane)Open a terminal in a new pane below
pane-terminal-rightr (pane)Open a terminal in a new pane on the right
pane-to-new-tabMove this pane (or panel, or floating view) to a new tab
pane-to-new-tab-doneb (pane)pane-to-new-tab, then back from the mode
pane-to-panelMove this pane (or floating view) into a panel on the right
pane-to-panel-doneP (pane)pane-to-panel, then back from the mode
pane-to-tabMove this pane (or panel, or floating view) to a tab you pick, or a new one
pane-to-tab-donet (pane)pane-to-tab, then back from the mode
pane-upalt-k, alt-up (global, terminal), w k (after the leader), k, up (pane), ctrl-w k (every style’s command mode)Move to the pane above
pane-zoomw z (after the leader), f (pane), ctrl-w z (every style’s command mode)Show only this pane, or all of them again
resize-modealt-r (global, terminal), R (pane)Keys for resizing the pane or floating view until esc, and moving a float
tab-closeClose this tab and its panes
tab-close-donex (tab)tab-close, then back from the mode
tab-goalt-1, alt-2, alt-3, alt-4, alt-5, alt-6, alt-7, alt-8, alt-9 (global, terminal)Show tab N
tab-modealt-t (global, terminal)Keys for tabs until esc: new, close, switch, rename
tab-newOpen a new tab
tab-new-donen (tab)tab-new, then back from the mode
tab-nextalt->, alt-] (global, terminal), ], l, right (tab), g t (vim’s command mode)Show the next tab; with a count, tab N, as Vim’s 3gt
tab-previousalt-<, alt-[ (global, terminal), [, h, left (tab), g T (vim’s command mode)Show the tab before; with a count, that many tabs back
tab-renameName this tab
tab-rename-doner (tab)tab-rename, then back from the mode
tab-terminalOpen a new tab with a terminal
tab-terminal-donet (tab)tab-terminal, then back from the mode
terminal-floatShow the floating terminal, or hide it
OptionDefaultWhat it does
float-layout"free"How new tabs lay out their floating views: “free”, “one”, “tiled”, “cascade” or your own
pane-layout"manual"How new tabs arrange their panes: “manual”, “main-stack”, “columns”, “spiral” or your own

pdf

PDF files open as their pages: one page at a time, drawn as wide as the view (in terminals that show images, and in the window), or as the text on every page, which reads anywhere and searches with /.

  ]  [     next and previous page
  +  -  =  bigger, smaller, as wide as the view
  t        pages or text

Uses: core.

CommandKeysWhat it does
pdf-next-page] (in pdf buffers)Show the next page of the PDF
pdf-previous-page[ (in pdf buffers)Show the previous page of the PDF
pdf-textt (in pdf buffers)Switch between the PDF’s pages and the text on them
pdf-zoom-fit= (in pdf buffers)Draw the PDF’s page as wide as the view
pdf-zoom-in+ (in pdf buffers)Draw the PDF’s page bigger
pdf-zoom-out- (in pdf buffers)Draw the PDF’s page smaller

picker

Pickers: type to narrow a list down, then choose an item.

A picker is one floating buffer. Its first line is what you type; the lines below are the items that match, best first, with the current one highlighted. Move with up/down (or ctrl-n/ctrl-p), choose with enter, close with esc.

Other plugins open one with require("@self/picker").pick(...); see pickers.luau for examples.

Uses: core.

CommandKeysWhat it does
act. (after the leader), alt-. (vscode’s typing mode)List what you can do with what’s under the cursor (a file, link, symbol, problem, change or the selection), and do it
command-palette? (after the leader), ctrl-P (vscode)Run any command by name
pick-bufferb (after the leader), ctrl-b (vscode)Switch to an open file
pick-filef (after the leader), ctrl-p (vscode)Open a file in the project, skipping ignored files
pick-file-hereF (after the leader)Open a file under the folder Greed was started in, skipping ignored files
pick-jumpj (after the leader)Pick a place from the jumplist and go there
pick-outlines (after the leader), g O (vim), ctrl-O (vscode)Jump to a function or type in the file, from its syntax tree
pick-stylePick an editing style
picker-actalt-. (in picker buffers)List what you can do with the current item, and do it
picker-chooseenter (in picker buffers)Choose the current item and close the picker
picker-closeesc (in picker buffers)Close the picker without choosing
picker-completetab (in picker buffers)Put the current path in the picker’s query, to go on from there
picker-keyRun what the open picker does for the key pressed
picker-nextctrl-n, down (in picker buffers)Move to the next item in the picker
picker-prevctrl-p, up (in picker buffers)Move to the previous item in the picker
picker-resume' (after the leader)Open the last picker again

pipe

Pipe each selection through something and put back what comes out:

  |               ask what to pipe through
  :pipe TARGET    the same, from the command line

The target says what it is:

  sort -u                        a shell command, given the selection as input
  luau: snake_case               a Luau function (see pipe.define)
  model: use the new client API  a language model, in the pipe-model role

Every selection gets its own result, and they all go in as one change, so one undo takes them back. A model’s changes are an operation too, to review with space u o; each result lands in its place even if you edit while the model works.

Uses: core, models, operations.

CommandKeysWhat it does
append-outputalt-! (helix’s command mode)Insert what a shell command prints after each selection
insert-output! (helix’s command mode)Insert what a shell command prints before each selection
keep-pipe$ (helix’s command mode)Keep the selections a shell command succeeds on, given each as input
pipe| (helix’s command mode)Pipe each selection through a shell command, a Luau filter (luau: name) or a model (model: what to do)
pipe-toalt-| (helix’s command mode)Give each selection to a shell command as input, leaving the text as it is
OptionDefaultWhat it does
pipe-model-role"fast"The model role | model: ... asks; see :models

plan

A plan and decisions you share with agents, as notes in the project’s notes folder (.greed/notes, see the notes plugin), so they’re searched, linked and in the agenda like any note.

The plan (project:plan) is an agent’s checklist where you can see and change it: tick, reorder, add or delete items, and the agent reads what you did. An agent replaces the whole plan when it writes, giving the version it read, so it can’t write over changes you made since.

Decisions (project:decisions/*) are what you settled, kept across sessions: “use sqlite”, “never touch the generated code”. Ones an agent adds stay proposed until you accept them, and agents are told so, so an agent can’t make itself rules the next one follows.

Uses: core, agent, picker, notes.

CommandKeysWhat it does
decision-acceptAccept the proposed decision you’re in, so agents follow it
decision-newWrite down a decision for this project, kept for you and agents
decisionsPick a decision kept for this project to open it
planOpen the plan you share with agents, in the project’s notes

projects

Projects: space P lists, in one picker, the projects open here, the sessions running on this machine, recent projects, and the projects your sources find. enter goes to one: it switches to it if it’s open or has a session, and opens it in a session of its own if not. alt-enter opens it in a new session even when one is running for it. A typed folder opens too.

Sources are folders to look in and commands that print paths:

    require("@projects").sources({
        -- every folder one level under these
        { dirs = { "~/Development", "~/Work" } },
        -- deeper, but only folders that look like projects
        { dirs = { "~/src" }, depth = 3, markers = { ".jj", ".git" } },
        -- any command that prints one path per line
        { command = { "zoxide", "query", "-l" } },
    })

Sources are looked at in the background, when the editor starts and each time the picker opens, and what they found last is kept, so the picker opens at once.

Uses: core, picker.

CommandKeysWhat it does
pick-projectP (after the leader)Go to a project: one open here, a running session, a recent one or one your sources find; or type a folder to open

prose

Text objects for prose: select or move by sentence, paragraph and Markdown section.

  m i p / m a p   select the paragraph (a: with the blank lines after it)
  m i s / m a s   select the sentence (a: with the spaces after it)
  m i h / m a h   select the section under a heading (a: with the heading)
  ] p / [ p       next / previous paragraph
  ] s / [ s       next / previous sentence

Uses: core, markdown.

CommandKeysWhat it does
next-paragraph] p (every style’s command mode)Move to the start of the next paragraph
next-sentence] s (every style’s command mode)Move to the start of the next sentence
prev-paragraph[ p (every style’s command mode)Move to the start of the paragraph, or the one before
prev-sentence[ s (every style’s command mode)Move to the start of the sentence, or the one before
select-paragraphm i p (helix’s command mode)Select the paragraph
select-paragraph-aroundm a p (helix’s command mode)Select the paragraph and the blank lines after it
select-sectionm i h (helix’s command mode)Select the text under the Markdown heading
select-section-aroundm a h (helix’s command mode)Select the Markdown section, heading included
select-sentencem i s (helix’s command mode)Select the sentence
select-sentence-aroundm a s (helix’s command mode)Select the sentence and the spaces after it

restore

Session restore: a session remembers its projects, the files open in each and where the cursor was, and opens them again when it starts. Written down when it quits and when a file is saved, per session name in the cache, so it only works for editors running as a session (which greed is, unless GREED_NO_SESSION is set).

Uses: core.

OptionDefaultWhat it does
restore-sessiontrueOpen the files a session had open when it starts again

setup

Setting Greed up: the first time you start it without a config, a page asks how you like to edit, which theme, whether your font has icons, whether you use Claude Code and which language model to use. The page is setup.md, a questionnaire in Markdown (see the markdown and blocks plugins): a style or theme picked switches at once, and Save writes a config from the answers and opens it, and has the model’s roles use the provider picked. :setup brings the page back any time.

tab and shift-tab go from choice to choice and button to button, enter (or space) or a click picks one or presses it, and esc or q closes it as “Not now”.

Uses: core, theme, models, markdown, blocks.

CommandKeysWhat it does
config-typesSet up type checking for your config: a .luaurc beside it that knows @greed and the plugins
setupAnswer a few questions to set Greed up: editing style, theme, icons, Claude Code, a model
setup-closeClose the setup page without saving; :setup brings it back
setup-nextGo to the next choice or button on the setup page
setup-prevGo to the previous choice or button on the setup page
setup-saveSave the setup page’s answers as your config, and open it
OptionDefaultWhat it does
setup-first-starttrueShow the setup page the first time a terminal or window attaches without a config

shell

A shell in a buffer: type a command at the prompt at the bottom, enter runs it, and it becomes a block above: the command, what it printed, and how it ended. Blocks stay to read, search, fold, copy and run again.

  space o s   switch to a shell, or open one (:shell opens a new one)
  enter       run what's at the prompt; on a block, edit its command
  [ p  ] p    the block before or after the cursor
  g o         select the block's output
  ] e  [ e    the next or previous file:line the output names; enter opens it
  ctrl-c      interrupt the running block (vscode style: copy a selection)
  ctrl-t      type into the running block's program, in a terminal
  space r     run the block again, as it ran; space R here and now
  space y/Y   copy its output / its command
  space x     stop it; space v opens its whole output; space c clears

The shell runs cd, export, unset, env, history, clear, open and help itself; every other line goes to a real shell, yours (sh for an agent), whose cd and export carry over (fallback.luau). It runs in a pseudo terminal (pty.luau), handed to a terminal when a program wants one; an agent’s run through a pipe. Each shell has its own folder and variables, and a project can have several.

$3 | sort, buf:NAME | ..., @sel | ... and @file | ... send block 3’s output, a buffer, the selection or the file you’re in to a command, and ... > buf:NAME (>> adds) sends its output to a buffer (data.luau). Output that’s JSON is kept as a value on the block, and a table (kubectl get, docker ps, ps) as a table, which $3 | where ..., select and sort work on (tables.luau). / filters what a block shows, and space s sorts a table (rows.luau).

Agents run commands with shell_run and read blocks with shell_read (tools.luau).

Uses: core, picker, terminal.

CommandKeysWhat it does
pick-runningo w (after the leader)Pick among what runs in every shell, its output beside: enter opens it in a float, ctrl-o goes to it, ctrl-x stops it
pick-shello s (after the leader)Switch to a shell of this project, or open a new one
shellOpen a new shell: a buffer of commands and what they printed
shell-accept-or-endctrl-e, end (every style’s typing mode; in shell buffers)At the end of the prompt, take the rest of the command it suggests; elsewhere go to the end of the line
shell-accept-or-rightright (every style’s typing mode; in shell buffers)At the end of the prompt, take the rest of the command it suggests; elsewhere move right
shell-clearc (after the leader; in shell buffers)Take the finished blocks out of the shell
shell-copy-commandY (after the leader; in shell buffers)Copy the block’s command
shell-copy-or-interruptctrl-c (vscode’s typing mode; in shell buffers)Copy the selection, or with nothing selected interrupt the running block
shell-copy-outputy (after the leader; in shell buffers)Copy what the block printed
shell-editPut the block’s command at the prompt, to change and run again
shell-enterenter (every style’s command mode, every style’s typing mode; in shell buffers)Run what’s at the prompt; on a block, put its command at the prompt to edit, or on a row that’s a file or a pod, open or describe it
shell-filter/ (every style’s command mode; in shell buffers), ctrl-f (vscode’s typing mode; in shell buffers)Keep the lines of the block at the cursor with what’s typed in them (/ for a regex), or a table’s rows (COL=VALUE, COL~TEXT, COL>N); elsewhere, search
shell-handoffctrl-t (every style’s command mode, every style’s typing mode; in shell buffers)Show the running block’s program in a terminal, to type into it
shell-historyctrl-r (every style’s command mode, every style’s typing mode; in shell buffers)Pick a command typed before, in any shell, to put at the prompt
shell-history-backup (every style’s typing mode; in shell buffers)Put the command typed before at the prompt, one starting with what’s typed there
shell-history-forwarddown (every style’s typing mode; in shell buffers)Put the command typed after at the prompt, or back to what was typed
shell-interruptctrl-c (every style’s command mode, every style’s typing mode; in shell buffers)Interrupt the running block at the cursor, or the latest one, as ctrl-c does
shell-killx (after the leader; in shell buffers)Stop the running block at the cursor, or the latest one, for good
shell-lineRun a command line in this project’s shell, opening one if there’s none, as :!cmd does
shell-next-block] p (every style’s command mode; in shell buffers), ctrl-down (vscode’s typing mode; in shell buffers)Go to the block after the cursor, or the prompt
shell-next-reference] e (every style’s command mode; in shell buffers), f8 (vscode’s typing mode; in shell buffers)Go to the next file and line a block’s output names, like an error’s place; enter opens it
shell-notifySend a notification each time a watch’s rows change, saying how, while Greed isn’t focused; again to stop
shell-output-openv (after the leader; in shell buffers)Open the block’s whole output in a buffer of its own
shell-peekOpen the block in a float, to filter it, act on its rows or stop it, and close it again with esc
shell-prev-block[ p (every style’s command mode; in shell buffers), ctrl-up (vscode’s typing mode; in shell buffers)Go to the block before the cursor
shell-prev-reference[ e (every style’s command mode; in shell buffers), shift-f8 (vscode’s typing mode; in shell buffers)Go to the file and line a block’s output names before the cursor
shell-rerunr (after the leader; in shell buffers)Run the block’s command again, in the folder and with the variables it had
shell-rerun-hereR (after the leader; in shell buffers)Run the block’s command again in the shell’s folder now
shell-runRun what’s at the prompt
shell-select-outputg o (every style’s command mode; in shell buffers)Select the block’s output, to copy or search
shell-show-allS (after the leader; in shell buffers)Show the block at the cursor’s output as it came, without a filter or a sort
shell-sorts (after the leader; in shell buffers)Sort the table at the cursor by the column under it; again to reverse, a third time as it came
shell-unfilteresc (every style’s command mode; in shell buffers)Show every line, or row, of the filtered block at the cursor again, keeping its sort; in a block’s float, close it
OptionDefaultWhat it does
shell-agent-max-output30000How many bytes of a command’s output and of its errors the shell tools give an agent: the start and the end, with what’s between left out
shell-aliasestrueWhether a line can start with one of your shell’s aliases, read once from its startup files
shell-ask-role"fast"The model role a line ending or starting with ? asks for a command; see :models
shell-completion-sources"cobra fish carapace man help"Where a shell completes a program’s arguments from, in order: cobra, fish, carapace, man, help
shell-completion-timeout2000Milliseconds a program asked for completions (kubectl, fish, carapace, –help) gets to answer
shell-confirm-lines5Running a prompt of more lines than this asks first, as after pasting a transcript
shell-editortrueWhether programs you run in a shell, like git commit, open files to edit in Greed: $EDITOR, $VISUAL, $GIT_EDITOR, $JJ_EDITOR and $KUBE_EDITOR are greed edit –wait
shell-environments"devenv direnv"Which environments a shell loads in folders that bring one, in order, the first that applies winning: devenv, direnv, and those plugins add; empty for none
shell-fallback""The shell that runs lines with pipes, globs and the like, e.g. “bash”; empty for your $SHELL (sh unless it’s sh, bash, zsh, dash, ksh, fish or nu)
shell-fallback-rcfalseWhether the shell running those lines reads its startup files (-i), for your aliases and functions
shell-fold-lines0A block that printed more lines than this folds as it ends, unless it failed; 0 never
shell-fold-olderfalseRunning a command folds the finished blocks above it, except those that failed
shell-help-completion"on-flag"When a shell runs a program with –help to complete its flags: on-flag (when you press tab on one) or never
shell-help-never"reboot shutdown halt poweroff init telinit kill killall pkill"Programs a shell never runs with –help to complete their flags
shell-history-size5000How many commands typed at shells are kept
shell-icons"plain"The icons of a shell’s own segments: “plain” ones any font has, or “nerd” ones a Nerd Font has
shell-max-lines5000How many lines of a block’s output a shell shows: the first half and the latest half
shell-max-record8388608How many bytes of a block’s output its record keeps; the rest goes to a file in the cache folder
shell-production-contexts"prod"Patterns for kubernetes contexts a shell’s prompt warns about, like prod; space between them
shell-prompt"greed"What draws a shell’s prompt before its ❯: “greed” (its own segments and folder) or “starship” (your starship prompt, when starship is installed)
shell-suggestionstrueWhether a shell’s prompt suggests the rest of a command typed before, after the cursor
shell-watch-minutes60How long a shell’s watch runs before it stops by itself, in minutes; 0 for until it’s stopped

statusline

The status line: on the left the mode, the tab, the file (or the latest message) and whether keys are being recorded; on the right the branch, problems, the language and its server, the editing style (dim), anything unusual about the file, how many selections there are, where the cursor is, the machine and the session. With the statusline-pills option some are pills, with rounded ends drawn in the theme’s colors, and with the effects option light goes through the tab or the file when you go to another, and the mode when it changes.

Add to it from your config, or take a piece away:

    local statusline = require("@statusline")
    statusline.add("right", "clock", function() return os.date("%H:%M") end)
    statusline.remove("host")

greed.statusline with a function of your own replaces it all; the last one set wins.

Uses: core.

CommandKeysWhat it does
messagesShow the last messages in full, newest last
OptionDefaultWhat it does
statusline-caps"round"The ends of status line pills: “round” (needs a Nerd Font) or “none”
statusline-pillsfalseShow the mode, tab, machine and session in the status line as pills in their colors

terminal

Terminals: a shell, or any program, in a buffer. :terminal [command] opens one in the main view and sends it every key; ctrl-\ goes back to the editing style’s mode, where the output is text to select, search and copy, and i or enter types into the program again.

The buffer holds the lines that scrolled off the top, then the screen. Output replaces the screen part and adds what scrolled off above it.

Uses: core, picker.

CommandKeysWhat it does
open-linkg x (in terminal buffers)Open the link under the cursor, or the file it names, at the line it says
pick-terminalt (after the leader)Switch to a terminal, or open a new one
tasko t (after the leader)Run a task, like the project’s build or tests, in a terminal that stays open; without a name, pick one
terminalOpen a terminal running a command, or your shell
terminal-closeEnd the terminal’s program and close it
terminal-escesc (terminal)Send esc to the terminal’s program; a second one quickly after stops typing into it
terminal-inputa, enter, i (in terminal buffers)Type into the terminal’s program
terminal-keySend the key to the terminal’s program
terminal-leaderctrl-space (terminal)Stop typing into the terminal and open the leader, for the keys after it
terminal-leavectrl-\ (terminal)Stop typing into the terminal, to move around its output
terminal-next-command] p (in terminal buffers)Go to the command after the cursor in a terminal, where its shell marks commands
terminal-next-reference] e (in terminal buffers)Go to the next file and line the output names, like an error’s place; g x opens it
terminal-pastep (in terminal buffers)Paste the latest copy into the terminal’s program
terminal-prev-command[ p (in terminal buffers)Go to the command before the cursor in a terminal, where its shell marks commands
terminal-prev-reference[ e (in terminal buffers)Go to the file and line the output names before the cursor
terminal-referencesd (after the leader; in terminal buffers)Pick from the files and lines the output names, like a compiler’s errors, to open one
terminal-select-outputg o (in terminal buffers)Select the output of the command at the cursor in a terminal, to copy or search
terminal-sendT (after the leader)Send the selection, or the cursor’s line, to the terminal you used last, and run it
OptionDefaultWhat it does
terminal-scrollback10000How many lines that scrolled off a terminal’s screen are kept
terminal-shell""What a new terminal runs, e.g. “fish -l”; empty for your $SHELL

theme

Themes: what every style name looks like. Greed comes with the default (the terminal’s 16 colors) and the usual ones from other editors: Nord, Dracula, Catppuccin, Gruvbox, Tokyo Night, One Dark and more.

  :theme NAME   switch to a theme
  :theme        pick one, trying each as you move; esc goes back

The theme picked is remembered (in the cache folder) until you pick another. To set it from your config, or define your own:

    local theme = require("@theme")
    theme.use("nord")
    theme.define("mine", {
        palette = { bg = "#101418", ... },  -- see palette.luau
        styles = { comment = { fg = "#5c6773" } },
    })
    theme.define("nord-bold", { inherits = "nord", styles = { keyword = { bold = true } } })

A theme is the base under greed.theme.set: entries set that way, from your config or a plugin, stay on top whichever theme is in use.

Uses: picker, cmdline.

CommandKeysWhat it does
themectrl-k ctrl-t (vscode)Switch to a theme, or pick one trying each as you move

tree

A file tree in a side panel. space e opens it (or moves to it, or closes it if you’re in it). In the tree: enter or l opens a file or folds a folder, h folds the folder you’re in, r reads the disk again, q closes; a makes a file (or a folder, ending in /), R renames, d deletes. Hidden and ignored files are left out, like in the file picker.

It’s only a plugin: to turn it off, greed.plugin.unload("tree") from your config.

Uses: core.

CommandKeysWhat it does
treee (after the leader)Open the file tree, move to it, or close it if you’re in it
tree-adda (in tree buffers)Make a file in the folder on the cursor, or a folder with a / at the end
tree-closeq (in tree buffers)Close the file tree
tree-deleted (in tree buffers)Delete the file or folder on the cursor, after you press y
tree-foldh (in tree buffers)Fold the folder on the cursor, or the one it’s in
tree-openenter, l (in tree buffers)Open the file on the cursor, or fold or unfold the folder
tree-refreshr (in tree buffers)Read the tree from the disk again
tree-renameR (in tree buffers)Rename or move the file or folder on the cursor; open files go along
OptionDefaultWhat it does
tree-icons"none"Icons by the names in the file tree: “none”, or “nerd” for a Nerd Font’s

tutor

The tutor: :tutor opens tutor.md, a page to read and edit with the keys of your editing style. It asks which style first, switches to the one picked, and shows each lesson’s keys for it, with <!-- if style=... --> parts (see the markdown plugin’s questions).

A lesson’s exercise is a practice block followed by a want block, the text it should become. The tutor hides the want block and marks the practice block done once its text matches. The want block’s options give each style’s keys for it (vim="w d w"); the tests press them, so the keys the page teaches are known to work.

Uses: core, markdown, blocks.

CommandKeysWhat it does
tutorLearn to edit with your editing style’s keys, and get around Greed

undo-tree

The undo tree, like Emacs’s vundo: every state the text has been in, the branches an undo followed by an edit left behind included. space u t shows it; moving up and down takes the text to that state as you go, enter keeps it, and esc goes back to where you were.

  ○ 0  loaded
  ○ 1  3m
  │ ○ 2  2m        a branch left behind
  ● 3  now         where the text is

The newest branch of each state carries on below it; older ones are indented under it.

Uses: core.

CommandKeysWhat it does
undo-treeu t (after the leader)Show every state the file has been in, branches included, and go to one
undo-tree-cancelesc (in undo-tree buffers)Close the undo tree and put the text back as it was
undo-tree-downdown, j (in undo-tree buffers)Go to the next state down the undo tree
undo-tree-keepenter, q (in undo-tree buffers)Close the undo tree, keeping the text as it is
undo-tree-upk, up (in undo-tree buffers)Go to the next state up the undo tree

vcs

Version control: signs beside lines added (+), changed (~) and removed (-) since the last commit, with jj, or git where there’s no .jj.

  ] g / [ g   next / previous change; ] G / [ G the last and first
  m i g       the change under the cursor; m a g its lines
  space g r   put the change under the cursor back the way it was
  space g b   who last changed this line, and when
  space g s   every change since the last commit, in one multibuffer
  space u c   commit just the latest operation's files, named after it
  space g a   stage the change under the cursor (git)
  space g S   what's staged (git), to unstage hunks and commit

With jj the comparison is with the working copy’s parent (@-), so the signs show what the change you’re working on does to the file. With git it’s with what’s staged, so staged changes lose their signs.

Uses: core, agent, multibuffer, operations, picker, statusline.

CommandKeysWhat it does
blame-lineg b (after the leader)Show who last changed the line under the cursor, and when
change-diff-closeq (in change-diff buffers)Go back to the change stack
change-stackg l (after the leader)Show the changes from the last pushed one to the working copy, to describe, squash, move or abandon
change-stack-abandona (in change-stack buffers)Abandon it, after asking
change-stack-closeq (in change-stack buffers)Close the stack and go back to what was shown before
change-stack-described (in change-stack buffers)Describe the change
change-stack-downalt-j (in change-stack buffers)Move it below the change below it
change-stack-edite (in change-stack buffers)Make it the working copy, to edit it
change-stack-menu? (in change-stack buffers)List the change stack’s keys and pick one
change-stack-newn (in change-stack buffers)Start a new change on top of it
change-stack-refreshr (in change-stack buffers)Read the stack again
change-stack-showenter (in change-stack buffers)Show the change’s diff
change-stack-splitS (in change-stack buffers)Split it: pick the hunks that go in a change of their own before it
change-stack-squashs (in change-stack buffers)Squash it into its parent, keeping the parent’s description
change-stack-undou (in change-stack buffers)Undo the last jj operation
change-stack-upalt-k (in change-stack buffers)Move it above the change above it
changesg s (after the leader)Show every change since the last commit in one buffer, to read, edit or put back
first-change[ G (every style’s command mode)Go to the first change since the last commit
hunk-picker-alla (in hunk-picker buffers)Pick every hunk, or none if all are picked
hunk-picker-cancelesc, q (in hunk-picker buffers)Put back what was shown, picking nothing
hunk-picker-doneenter (in hunk-picker buffers)Go on with the hunks picked
hunk-picker-togglespace (in hunk-picker buffers)Pick the hunk under the cursor, or put it back, and go to the next
last-change] G (every style’s command mode)Go to the last change since the last commit
next-change] g (every style’s command mode)Go to the next change since the last commit
operation-commitu c (after the leader)Save the files of the latest operation (or the one being reviewed) and commit just them, named after it
pick-changed-fileg g (after the leader)Open a file changed since the last commit
prev-change[ g (every style’s command mode)Go to the previous change since the last commit
revert-changeg r (after the leader)Put the change under the cursor back the way it was at the last commit
select-around-changem a g (helix’s command mode)Select the lines of the change since the last commit under the cursor
select-inside-changem i g (helix’s command mode)Select the text of the change since the last commit under the cursor
stage-changeg a (after the leader)Stage the change under the cursor for the next git commit, leaving the rest
stagedg S (after the leader)Show what’s staged for the next git commit, to unstage hunks or commit
staged-commitc (in staged-diff buffers)Commit what’s staged
staged-refreshr (in staged-diff buffers)Read what’s staged again
staged-unstageu (in staged-diff buffers)Unstage the hunk under the cursor
why-linesg w (after the leader)Show how the selected lines came to be: each change that made them, with its description and diff
OptionDefaultWhat it does
vcs-signstrueShow signs beside lines changed since the last commit

vim

Vim-style editing, written only against the plugin API. It has modes of its own (vim, vim-insert, vim-replace and the three visual modes), beside Helix’s, and says which plays each role: keys other plugins bind for the command role (the language server’s, the next change) and the leader (space) work here as in every style.

Switch to it with :style vim, or put this in ~/.config/greed/init.luau:

    require("@greed").option.set("editing-style", "vim")

It reads in this order: common.luau, text helpers; motions.luau, the motions (one table, each saying how an operator takes its range) and marks; objects.luau, ranges and text objects; operators.luau, operators applied through apply and the line and case tables; visual.luau, the visual modes (block mode keeps a selection per line), with the commands for motions and operators; then this file, with insert, replace, the change list and U; keys.luau last. Ex commands (:s, :g, ranges) are the command line plugin’s, and . is core’s.

Uses: core.

CommandKeysWhat it does
vim-appenda (vim)Insert after the cursor
vim-append-line-endA (vim)Insert at the end of the line
vim-block-appendA (vim-visual-block)Type after the block on each of its lines
vim-block-insertI (vim-visual-block)Type before the block on each of its lines
vim-change-lineS (vim)Change the whole line
vim-change-newerg , (vim)Go to a newer place in the change list
vim-change-olderg ; (vim)Go to an older place in the change list
vim-change-to-endC (vim)Change to the end of the line
vim-command-line: (vim, vim-visual)Open the command line, with the selected lines (or the count’s) as its range
vim-decrementctrl-x (vim)Subtract the count (or 1) from the number at or after the cursor
vim-delete-charx (vim)Delete the character under the cursor
vim-delete-char-beforeX (vim)Delete the character before the cursor
vim-delete-to-endD (vim)Delete to the end of the line
vim-expand-selectionalt-o (vim, vim-visual)Select the syntax node around the cursor or selection, in visual mode
vim-incrementctrl-a (vim)Add the count (or 1) to the number at or after the cursor
vim-inserti (vim)Insert before the cursor
vim-insert-lastg i (vim)Insert where insert mode was last left
vim-insert-line-startI (vim)Insert at the first non-blank of the line
vim-insert-onectrl-o (vim-insert)Run one normal mode command, then go on typing
vim-joinJ (vim)Join the next line onto this one, with one space
vim-join-rawg J (vim)Join the next line onto this one as it is
vim-jump-to-wordg s (vim)Label every word on screen and jump to the one typed
vim-macroq (vim)Record a macro into the register typed next, or stop recording
vim-markm (vim)Set the mark typed next (a to z here, A to Z across files) at the cursor
vim-motion-$$ (vim, vim-visual)Vim motion $
vim-motion-%% (vim, vim-visual)Vim motion %
vim-motion-'' (vim, vim-visual)Vim motion ’
vim-motion-(( (vim, vim-visual)Vim motion (
vim-motion-)) (vim, vim-visual)Vim motion )
vim-motion-++ (vim, vim-visual)Vim motion +
vim-motion-,, (vim, vim-visual)Vim motion ,
vim-motion--- (vim, vim-visual)Vim motion -
vim-motion-00 (vim, vim-visual)Vim motion 0
vim-motion-;; (vim, vim-visual)Vim motion ;
vim-motion-BB (vim, vim-visual)Vim motion B
vim-motion-EE (vim, vim-visual)Vim motion E
vim-motion-FF (vim, vim-visual)Vim motion F
vim-motion-GG (vim, vim-visual)Vim motion G
vim-motion-HH (vim, vim-visual)Vim motion H
vim-motion-LL (vim, vim-visual)Vim motion L
vim-motion-MM (vim, vim-visual)Vim motion M
vim-motion-TT (vim, vim-visual)Vim motion T
vim-motion-WW (vim, vim-visual)Vim motion W
vim-motion-^^ (vim, vim-visual)Vim motion ^
vim-motion-__ (vim, vim-visual)Vim motion _
`vim-motion-````` (vim, vim-visual)Vim motion `
vim-motion-bb (vim, vim-visual)Vim motion b
vim-motion-ee (vim, vim-visual)Vim motion e
vim-motion-ff (vim, vim-visual)Vim motion f
vim-motion-gEg E (vim, vim-visual)Vim motion g E
vim-motion-g_g _ (vim, vim-visual)Vim motion g _
vim-motion-geg e (vim, vim-visual)Vim motion g e
vim-motion-ggg g (vim, vim-visual)Vim motion g g
vim-motion-hh (vim, vim-visual)Vim motion h
vim-motion-jj (vim, vim-visual)Vim motion j
vim-motion-kk (vim, vim-visual)Vim motion k
vim-motion-ll (vim, vim-visual)Vim motion l
vim-motion-tt (vim, vim-visual)Vim motion t
vim-motion-ww (vim, vim-visual)Vim motion w
vim-motion-{{ (vim, vim-visual)Vim motion {
`vim-motion-`| (vim, vim-visual)
vim-motion-}} (vim, vim-visual)Vim motion }
vim-multi-appendA, a (vim-multi)Type after every selection
vim-multi-backctrl-p (vim-multi)Drop the match added last
vim-multi-changec, s (vim-multi)Delete every selection and type in each place
vim-multi-copyy (vim-multi)Copy every selection
vim-multi-deleted, x (vim-multi)Delete every selection
vim-multi-insertI, i (vim-multi)Type before every selection
vim-multi-nextctrl-n (vim, vim-multi)Select the word under the cursor, or add its next match
vim-multi-skipctrl-x (vim-multi)Drop this match and add the next one instead
vim-normalesc (vim-insert, vim-multi, vim-visual), V, v (vim-visual), ctrl-v (vim-visual-block)Back to normal mode
vim-open-aboveO (vim)Open a line above and insert
vim-open-belowo (vim)Open a line below and insert
vim-operator-!! (vim, vim-visual)Vim operator !, followed by a motion
vim-operator-<< (vim, vim-visual)Vim operator <, followed by a motion
vim-operator-== (vim, vim-visual)Vim operator =, followed by a motion
vim-operator->> (vim, vim-visual)Vim operator >, followed by a motion
vim-operator-cc (vim, vim-visual)Vim operator c, followed by a motion
vim-operator-dd (vim, vim-visual), x (vim-visual)Vim operator d, followed by a motion
vim-operator-g?g ? (vim, vim-visual)Vim operator g?, followed by a motion
vim-operator-gUg U (vim), U (vim-visual)Vim operator gU, followed by a motion
vim-operator-gcg c (vim, vim-visual)Vim operator gc, followed by a motion
vim-operator-gqg q (vim, vim-visual)Vim operator gq, followed by a motion
vim-operator-gug u (vim), u (vim-visual)Vim operator gu, followed by a motion
vim-operator-gwg w (vim)Vim operator gw, followed by a motion
vim-operator-g~g ~ (vim, vim-visual), ~ (vim-visual)Vim operator g~, followed by a motion
vim-operator-yy (vim, vim-visual)Vim operator y, followed by a motion
vim-operator-zfz f (vim, vim-visual)Vim operator zf, followed by a motion
vim-paste-afterp (vim)Paste after the cursor, or below the line
vim-paste-beforeP (vim)Paste before the cursor, or above the line
vim-play-macro@ (vim)Play the macro in the register typed next; @@ plays the last one again
vim-quit-unsavedZ Q (vim)Close without saving (ZQ)
vim-redoctrl-r (vim)Redo
vim-replace-backbackspace (vim-replace)Put back the character typed over last, or move left
vim-replace-charr (vim)Replace the character under the cursor with the next one typed
vim-replace-modeR (vim)Type over the text
vim-replace-typedType over the character under the cursor
vim-reselectg v (vim)Select the last visual selection again
vim-search-word* (vim)Search for the word under the cursor
vim-search-word-back# (vim)Search for the word under the cursor
vim-select-matchg n (vim)Select the search match under the cursor, or the next one
vim-shrink-selectionalt-i (vim, vim-visual)Shrink the selection back, or to the first syntax node inside it
vim-substitutes (vim)Change the character under the cursor
vim-switch-case~ (vim)Switch the case of the character under the cursor and move on
vim-undou (vim)Undo
vim-undo-lineU (vim)Put back the line last changed as it was before (U)
vim-visualv (vim)Select characters, starting here
vim-visual-arounda (vim-visual)Select around the text object typed next
vim-visual-blockctrl-v (vim, vim-visual)Select a block of columns across lines
vim-visual-insidei (vim-visual)Select inside the text object typed next
vim-visual-joinJ (vim-visual)Join the selected lines, with one space
vim-visual-lineV (vim)Select whole lines, starting with this one
vim-visual-pasteP, p (vim-visual)Replace the selection with what’s in the register
vim-visual-replacer (vim-visual)Replace every selected character with the next one typed
vim-visual-surroundS (vim-visual)Wrap the selection in the pair typed next
vim-visual-swapo (vim-visual)Go to the other end of the selection

vscode

The vscode style: no modes, VS Code’s keys. Typing inserts text (replacing any selection), arrows move, shift and the arrows select, and the usual ctrl shortcuts do the usual things. The editing itself is core’s (core/modeless.luau), shared with any other style without modes; this plugin gives it VS Code’s keys.

Switch to it with :style vscode, or to use it all the time, put this in ~/.config/greed/init.luau:

    require("@greed").option.set("editing-style", "vscode")

Uses: core, picker.

Options

Set any of these in your init.luau, e.g. require("@greed").option.set("motion-hints", true), or while the editor runs with :set motion-hints true.

OptionDefaultPluginWhat it does
agent-editor-contexttrueacpSend agents the file you’re in, the cursor, the selection and the other files on screen with each message
agent-eval"auto"agentHow Luau from agents runs: “auto” (sandboxed, a reviewer or you for more), “ask”, “allow” or “off”
agent-greed-toolstrueacpGive agents Greed’s own tools (buffers, diagnostics, syntax queries, proposals) through greed mcp
agent-sessions-kept50acpHow many sessions with Greed’s own agent are kept per repository to take up again; older ones are deleted
agent-tool-groups"plan"agentThe tool groups agents get from the start, besides those they switch on: names with spaces between, or “all”
agent-workspace-editstrueacpLet agents in a workspace of their own edit, move and delete files there without asking
assist-role"fast"assistThe model role assist’s commands ask; see :models
auto-reloadtruecoreLoad files changed on disk into their buffers, unless they have unsaved changes
auto-savefalsecoreSave files on their own a moment after their edits stop
auto-save-delay1000coreHow long after the last edit auto-save saves, in milliseconds
blocks-max-lines1000blocksThe most lines of output a block’s result keeps; the rest is cut
claude-command"claude"agentHow the Claude pane starts Claude Code, e.g. “claude –continue”
completion-delay150lspMilliseconds of no typing before a completion menu opens by itself; 0 opens it only when asked
completion-enter"auto"lspWhat enter does in a completion menu: “first” puts in the first item, “selected” only one moved to with tab or the arrows (otherwise enter does what it does without a menu, like running a shell’s line), “auto” lets each source say: the shell “selected”, language servers “first”
diagnostic-messagestruelspShow each line’s worst problem after the end of the line
editing-style"helix"coreHow editing works: helix, vim, vscode or another style a plugin adds (see :style)
effectsfalsecoreSmall animations, like a wave of light through the status line when you switch files
flash-duration300flashHow long copied text flashes, in milliseconds
float-layout"free"panesHow new tabs lay out their floating views: “free”, “one”, “tiled”, “cascade” or your own
format-on-savetrueformatFormat files when they’re saved
grammar-downloadtruelanguagesDownload the grammar for a language Greed has none for when a file in it opens
gui-font""coreThe font the window draws text in, by family name; empty for the system’s monospaced font
gui-font-prose""coreThe proportional font the window draws prose in (styles with font = “prose”, like Markdown’s text), by family name; empty for the system’s sans-serif
gui-font-size15coreThe size of the window’s text in pixels, before the screen’s scale; zooming gives a window a size of its own
indent" "coreOne level of indent, spaces or a tab, in languages that don’t set their own
jump-label-alphabet"abcdefghijklmnopqrstuvwxyz"coreThe letters jump labels are made of, most comfortable first
line-numbers"absolute"coreHow line numbers count: “absolute”, “relative” to the cursor’s line, or “hybrid” (relative, with the cursor’s line its own number)
link-opener""coreWhat opens a link, e.g. “firefox”; empty for xdg-open, or open on macOS
markdown-looktruemarkdownShow Markdown with its markup hidden and its headings, code and quotes drawn as such
motion-hintsfalsecoreNumber the next places a word motion would go, and go there on a digit
notes-concealtruenotesShow links in notes without their brackets, and markup as symbols
notes-everywherefalsenotesTreat every Markdown file as a note: concealed links and backlinks
notes-folder"~/notes"notesThe folder notes live in; ~ is your home folder
notes-project-folder".greed/notes"notesWhere a project keeps its notes, in the project: they’re named project:NAME, and the plan and decisions you share with agents are among them
notes-remind-desktoptruenotesShow reminders as desktop notifications with notify-send or osascript, where installed
notes-remind-time"09:00"notesWhen a reminder with a date and no time goes off, as HH:MM
notes-search-case"ignore"coreHow searching notes minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
on-quit"auto"coreWhat :q on the last project does to the session: “auto” ends it unless something is running (a command in a terminal, an agent at work) and keeps it then, “end” always ends it, “keep” always leaves it running to attach to again
on-quit-remote"keep"coreLike on-quit, when you’re attached over greed ssh
pane-layout"manual"panesHow new tabs arrange their panes: “manual”, “main-stack”, “columns”, “spiral” or your own
pipe-model-role"fast"pipeThe model role | model: ... asks; see :models
restore-sessiontruerestoreOpen the files a session had open when it starts again
search-case"smart"coreHow searches you type mind case, unless their kind says otherwise: “smart” (only when what you type has a capital letter), “ignore” or “match”
session-idle-hours24coreA session nothing is attached to, with nothing running and nothing unsaved, ends after this many hours; 0 keeps it
setup-first-starttruesetupShow the setup page the first time a terminal or window attaches without a config
shell-agent-max-output30000shellHow many bytes of a command’s output and of its errors the shell tools give an agent: the start and the end, with what’s between left out
shell-aliasestrueshellWhether a line can start with one of your shell’s aliases, read once from its startup files
shell-ask-role"fast"shellThe model role a line ending or starting with ? asks for a command; see :models
shell-completion-sources"cobra fish carapace man help"shellWhere a shell completes a program’s arguments from, in order: cobra, fish, carapace, man, help
shell-completion-timeout2000shellMilliseconds a program asked for completions (kubectl, fish, carapace, –help) gets to answer
shell-confirm-lines5shellRunning a prompt of more lines than this asks first, as after pasting a transcript
shell-editortrueshellWhether programs you run in a shell, like git commit, open files to edit in Greed: $EDITOR, $VISUAL, $GIT_EDITOR, $JJ_EDITOR and $KUBE_EDITOR are greed edit –wait
shell-environments"devenv direnv"shellWhich environments a shell loads in folders that bring one, in order, the first that applies winning: devenv, direnv, and those plugins add; empty for none
shell-fallback""shellThe shell that runs lines with pipes, globs and the like, e.g. “bash”; empty for your $SHELL (sh unless it’s sh, bash, zsh, dash, ksh, fish or nu)
shell-fallback-rcfalseshellWhether the shell running those lines reads its startup files (-i), for your aliases and functions
shell-filter-search-case"ignore"coreHow filtering a shell block with / minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
shell-fold-lines0shellA block that printed more lines than this folds as it ends, unless it failed; 0 never
shell-fold-olderfalseshellRunning a command folds the finished blocks above it, except those that failed
shell-help-completion"on-flag"shellWhen a shell runs a program with –help to complete its flags: on-flag (when you press tab on one) or never
shell-help-never"reboot shutdown halt poweroff init telinit kill killall pkill"shellPrograms a shell never runs with –help to complete their flags
shell-history-size5000shellHow many commands typed at shells are kept
shell-icons"plain"shellThe icons of a shell’s own segments: “plain” ones any font has, or “nerd” ones a Nerd Font has
shell-max-lines5000shellHow many lines of a block’s output a shell shows: the first half and the latest half
shell-max-record8388608shellHow many bytes of a block’s output its record keeps; the rest goes to a file in the cache folder
shell-production-contexts"prod"shellPatterns for kubernetes contexts a shell’s prompt warns about, like prod; space between them
shell-prompt"greed"shellWhat draws a shell’s prompt before its ❯: “greed” (its own segments and folder) or “starship” (your starship prompt, when starship is installed)
shell-suggestionstrueshellWhether a shell’s prompt suggests the rest of a command typed before, after the cursor
shell-watch-minutes60shellHow long a shell’s watch runs before it stops by itself, in minutes; 0 for until it’s stopped
statusline-caps"round"statuslineThe ends of status line pills: “round” (needs a Nerd Font) or “none”
statusline-pillsfalsestatuslineShow the mode, tab, machine and session in the status line as pills in their colors
substitute-previewtruecmdlineShow what :s will change as you type it, before it runs
substitute-search-case"match"coreHow :s minds case: “smart” (only when what you type has a capital letter), “ignore”, “match”, or “default” for as search-case says
terminal-scrollback10000terminalHow many lines that scrolled off a terminal’s screen are kept
terminal-shell""terminalWhat a new terminal runs, e.g. “fish -l”; empty for your $SHELL
text-width80coreHow wide :reflow (and Vim’s gq) makes lines
tree-icons"none"treeIcons by the names in the file tree: “none”, or “nerd” for a Nerd Font’s
vcs-signstruevcsShow signs beside lines changed since the last commit
web-search""agentThe search service agents search the web with: “brave”, “exa”, “tavily” or “searxng”; empty for none
web-search-url""agentWhere your SearXNG is, for web-search searxng

Luau API

What plugins and your config get from require("@greed"): the module first, then the types it hands out. Positions are 0-based byte offsets and lines are 0-based, as language servers count them.

Greed

command

command: (spec: CommandSpec) -> ()

cli

cli: (spec: CliSpec) -> ()

Adds a command line command, greed NAME ARGS; see CliSpec.

run

run: (name: string, opts: { args: { string }?, force: boolean?, count: number?, wait: boolean? }?) -> ()

Runs a command by name, optionally with arguments, force and a count. It returns once the command waits for something (a key, a prompt, a reply); with wait, a command or handler instead waits until it has finished, without blocking the editor. The command can do only what this plugin may too, unless it has “commands”.

eval

eval: (source: string, opts: { sandbox: boolean? }?) -> (boolean, string)

Runs Luau code in a scratch environment whose globals persist, like a REPL. Returns true and the values as text (tab-separated), or false and the error. The code has every capability; calling it needs eval from every plugin involved. With sandbox, it runs in a fresh environment that can read the editor and edit buffers and nothing else: what it would do beyond that (run programs or commands, change keys, reach files) fails with an error starting “needs approval:”.

on

on: ((event: "mode.changed", handler: (event: ModeChanged) -> ()) -> ())
	& ((event: "buffer.changed", handler: (event: BufferChanged) -> ()) -> ())
	& ((event: "keys.pending", handler: (event: KeysPending) -> ()) -> ())
	& ((event: "command.run", handler: (event: CommandRun) -> ()) -> ())
	& ((event: "lsp.diagnostics", handler: (event: DiagnosticsChanged) -> ()) -> ())
	& ((event: "control.request", handler: (event: ControlRequest) -> ()) -> ())
	& ((event: "control.closed", handler: (event: ControlClosed) -> ()) -> ())
	& ((event: "client.attached", handler: (event: ClientAttached) -> ()) -> ())
	& ((event: "client.detached", handler: (event: ClientDetached) -> ()) -> ())
	& ((event: "client.paste", handler: (event: ClientPaste) -> ()) -> ())
	& ((event: "buffer.opened", handler: (event: BufferChanged) -> ()) -> ())
	& ((event: "buffer.saved", handler: (event: BufferChanged) -> ()) -> ())
	& ((event: "buffer.closed", handler: (event: BufferClosed) -> ()) -> ())
	& ((event: "view.closed", handler: (event: ViewClosed) -> ()) -> ())
	& ((event: "view.resized", handler: (event: ViewResized) -> ()) -> ())
	& ((event: "pane.opened", handler: (event: PaneOpened) -> ()) -> ())
	& ((event: "view.scrolled", handler: (event: ViewScrolled) -> ()) -> ())
	& ((event: "selection.changed", handler: (event: SelectionChanged) -> ()) -> ())
	& ((event: "view.focused", handler: (event: ViewFocused) -> ()) -> ())
	& ((event: "mouse", handler: (event: MouseEvent) -> ()) -> ())
	& ((event: "option.changed", handler: (event: OptionChanged) -> ()) -> ())
	& ((event: "plugin.loaded", handler: (event: PluginLoaded) -> ()) -> ())
	& ((event: "file.changed", handler: (event: FileChanged) -> ()) -> ())
	& ((event: "plugin.unloaded", handler: (event: PluginUnloaded) -> ()) -> ())
	& ((event: "editor.started", handler: (event: {}) -> ()) -> ())
	& ((event: "editor.quitting", handler: (event: {}) -> ()) -> ())
	& ((event: "terminal.output", handler: (event: TerminalOutput) -> ()) -> ())
	& ((event: "terminal.exited", handler: (event: TerminalExited) -> ()) -> ())

Calls handler every time event happens, until greed.off removes it or the plugin unloads.

off

off: (event: EventName, handler: (event: any) -> ()) -> ()

Removes handler, added by this plugin with greed.on(event, handler). Does nothing if it’s not there.

echo

echo: (...any) -> ()

Shows a message to the user.

messages

messages: () -> { string }

The last messages shown, oldest first and whole (the status line shows a message’s first line).

view

view: () -> View

The focused view.

quit

quit: () -> ()

Asks the editor to exit.

line_numbers

line_numbers: (style: "absolute" | "relative" | "hybrid") -> ()

How line numbers count, where buffers show them: each line’s own number, “relative” to the cursor’s line (0 on it), or “hybrid”, relative with the cursor’s line showing its own.

reveal

reveal: (how: "line" | "cursor" | "never") -> ()

When text hidden by marks whose reveal is “auto” shows: “line” (on a cursor’s line, the default), “cursor” or “never”, as for a mark’s own reveal. Core sets it as the mode changes (see core.reveal).

detach

detach: () -> ()

Lets the attached terminal go and keeps the editor running in the background, for greed attach to come back to.

sleep

sleep: (ms: number) -> ()

Waits ms milliseconds without blocking the editor. Works in commands and event handlers.

spawn

spawn: (f: (...any) -> ...any, ...any) -> ()

Runs f on its own, the way a command runs, and returns at once: it can wait (greed.sleep, greed.process.run, …) while the editor and the code that started it go on. For work in the background, like a status line piece refreshing.

stalls

stalls: () -> { { ms: number, plugin: string?, what: string, time: number } }

Runs of plugin code that held the editor up (50 ms or more), newest first, for tracing a freeze to its plugin and what it was doing: a command’s name, “the status line”, “loading”. ms is how long it took; time is when, in seconds since 1970 (as os.time() counts, with a fraction).

errors

errors: () -> { { plugin: string?, what: string, message: string, traceback: string, time: number } }

Errors from plugin code that nothing caught (a command, an event handler or a task failing), newest first, up to 100: the plugin, what it ran (“on buffer.opened”, a command’s name), the message, where in its Luau it failed, and when, in seconds since 1970.

profile

profile: { start: () -> (), stop: () -> { ProfileRow }, running: () -> boolean }

Counting where the editor’s time goes: every run of plugin code (each command, event handler, status line) and the editor’s own work (handling a key, parsing, drawing), by plugin and what it did. start begins, stop ends and returns the rows, the most time first; running says whether one is being taken.

now

now: () -> number

Milliseconds on the editor’s clock, for timing animations. While a timer wakes code, it reads the time the timer was due.

hostname

hostname: () -> string?

This machine’s name, or nil if the system doesn’t say.

animate

animate: (ms: number, step: (t: number) -> ()) -> ()

Calls step(t) about once a frame for ms milliseconds, with t going from 0 to 1, e.g. to fade a theme color or move a decoration. Works in commands and event handlers.

next_key

next_key: () -> string

Waits for the next key press and returns its name, e.g. “g” or “ctrl-s”. The key doesn’t run anything. Works in commands and event handlers.

keys

keys: Keys

See Keys.

process

process: Process

See Process.

http

http: Http

See Http.

secrets

secrets: Secrets

See Secrets.

terminal

terminal: Terminals

See Terminals.

image

image: Images

See Images.

pdf

pdf: Pdfs

See Pdfs.

signal

signal: () -> Signal

A one-shot value that one piece of code waits for and another sends, e.g. a tool waiting for the user’s review. wait works in commands and event handlers; a value sent before anyone waits is kept for them.

vouch

vouch: <A..., R...>(f: (A...) -> R..., A...) -> R...

Runs f with ... as your plugin’s own action: only your capabilities count for what it does, not those of the plugins whose code called you or started what runs. For services that check what they do themselves, like the models plugin sending a key only to its provider. Anything you vouch for, any plugin can make you do: vouch only for narrow, checked operations. It can wait.

statusline

statusline: (build: (ctx: StatusContext) -> StatusParts) -> ()

Sets the function that builds the status line. It runs for every frame.

set_menus

set_menus: (menus: { { title: string, items: { { label: string, command: string } } } }) -> ()

Sets the menus a window shows as its menu bar (on macOS, the system’s), replacing those before; each item runs its command. The core plugin keeps them, so add to require("@core").menus rather than calling this.

create_buffer

create_buffer: (kind: string, text: string?, opts: { readonly: boolean?, language: string? }?) -> Buffer

A new buffer that isn’t shown anywhere yet. It goes away when the last view showing it closes.

language_of

language_of: (path: string) -> string?

The language of the file at path, by its name or extension.

filetype

filetype: (language: string, spec: { extensions: { string }?, names: { string }? }) -> ()

Makes files with these extensions (without the dot) or exact names, like “Makefile”, be in language, replacing whatever it had before.

grammars

grammars: () -> { string }

The languages Greed has a grammar for, built in or loaded, sorted.

which

which: (program: string) -> string?

Where program is on PATH, or nil if it isn’t installed.

float

float: (buffer: Buffer, spec: FloatSpec?) -> View

Shows buffer in a view floating over the main one, and focuses it.

floats

floats: (opts: { hidden: boolean? }?) -> { View }

The floating views shown, the one in front last; with hidden = true, the hidden ones too.

panel

panel: (buffer: Buffer, spec: { side: ("left" | "right")?, width: (number | string)?, title: string?, focus: boolean? }?) -> View

Shows buffer in a panel beside the main view, which gives up the room. Defaults: the left side, 30 cells wide, focused. Close it with view:close().

buffers

buffers: () -> { Buffer }

Every open buffer.

open

open: (path: string) -> ()

Opens a file in the main view.

load

load: (path: string) -> Buffer

The buffer for the file at path, opening it without showing it.

buffer_for

buffer_for: (path: string) -> Buffer?

The open buffer for the file at path, or nil when it isn’t open; it doesn’t open it. ~, . and .. are worked out and a relative path is from the folder Greed started in, so every spelling of a file finds its buffer.

diff

diff: (old: string, new: string) -> { { from: number, to: number, text: string } }

The changes that turn old into new, whole lines at a time: each replaces from..to (byte offsets into old) with text. Sorted.

commands

commands: () -> { CommandInfo }

Every command, sorted by name.

handlers

handlers: () -> { HandlerInfo }

Every event handler, by event and then the order they were added.

fuzzy

fuzzy: (query: string, items: { string }, opts: { case: ("smart" | "ignore" | "match")? }?) -> { number }

The 1-based indices of items that match query, best first. case says how it minds case: “smart” (the default: only when query has a capital letter), “ignore” or “match”; core.search.case() gives what the user chose.

fuzzy_positions

fuzzy_positions: (query: string, text: string, opts: { case: ("smart" | "ignore" | "match")? }?) -> { number }

The byte offsets of the characters in text that query matches, for highlighting them; empty if it doesn’t match. case as for fuzzy.

files

files: (dir: string?) -> { string }

Files under dir (default: the current directory), skipping hidden and ignored files.

keymap

keymap: Keymap

See Keymap.

mode

mode: Mode

See Mode.

option

option: Options

See Options.

history

history: History

See History.

clipboard

clipboard: Clipboard

See Clipboard.

base64

base64: { encode: (bytes: string) -> string, decode: (text: string) -> string? }

Base64, as data goes in JSON, like an image for a model: encode takes bytes, decode gives them back, or nil for text that isn’t base64.

client

client: Client

See Client.

lsp

lsp: Lsp

See Lsp.

fs

fs: Fs

See Fs.

hash

hash: Hash

See Hash.

control

control: Control

See Control.

json

json: Json

See Json.

project

project: Projects

See Projects.

tab

tab: Tabs

See Tabs.

session

session: Sessions

See Sessions.

plugin

plugin: Plugins

See Plugins.

syntax

syntax: Syntax

See Syntax.

theme

theme: Theme

See Theme.

Range

A range of text. anchor is where it started and head is where the cursor is; a cursor is a range where the two are equal.

anchor

anchor: number
head: number

Selection

One or more ranges, sorted and never overlapping. primary is the 1-based index of the main range.

ranges

ranges: { Range }

primary

primary: number

Edits

Collects the edits made inside Buffer:edit. They are applied together, as one undoable change, when the callback returns.

insert

insert: (self: Edits, pos: number, text: string) -> ()

delete

delete: (self: Edits, from: number, to: number) -> ()

replace

replace: (self: Edits, from: number, to: number, text: string) -> ()

select

select: (self: Edits, selection: Selection) -> ()

Sets the selection after the edit (of every view showing the buffer, when the buffer didn’t come from a view). Without this, the current selection is moved along with the text: a cursor ends up after text inserted where it is, and after the new text of a replace or delete that covers it; a range doesn’t grow to take in text inserted at its edges.

Buffer

Open text, backed by a file or not. Two handles to the same buffer are equal (==).

id

id: (self: Buffer) -> number

Its id, even after it was closed.

valid

valid: (self: Buffer) -> boolean

Whether it is still open. Other methods fail once it was closed.

len

len: (self: Buffer) -> number

Length in bytes.

text

text: (self: Buffer) -> string

slice

slice: (self: Buffer, from: number, to: number) -> string

line_count

line_count: (self: Buffer) -> number

line_of

line_of: (self: Buffer, pos: number) -> number

The line pos is on.

line_start

line_start: (self: Buffer, line: number) -> number

line_end

line_end: (self: Buffer, line: number) -> number

The end of line, before its line break.

line_text

line_text: (self: Buffer, line: number) -> string

The text of line, without its line break.

display_column

display_column: (self: Buffer, pos: number, tab_width: number?) -> number

The screen column pos is drawn at in its line: wide characters take two columns and tabs reach the next tab stop, every tab_width columns (4 if not given).

at_display_column

at_display_column: (self: Buffer, line: number, col: number, tab_width: number?) -> number

The position in line drawn at screen column col: the start of the character covering it, or the line’s end if the line is shorter.

char_at

char_at: (self: Buffer, pos: number) -> string?

The character at pos, or nil at the end of the buffer.

next_char

next_char: (self: Buffer, pos: number) -> number

prev_char

prev_char: (self: Buffer, pos: number) -> number

sentence

sentence: (self: Buffer, pos: number) -> (number, number)

The start and end of the sentence around pos, without the spaces after it. Sentences end at blank lines.

kind

kind: (self: Buffer) -> string

What the buffer is: “file”, “terminal”, …

path

path: (self: Buffer) -> string?

project

project: (self: Buffer) -> number?

The id of the project it was opened in.

readonly

readonly: (self: Buffer) -> boolean

True if only plugins can change the text: edits from keys and undo are refused.

set_readonly

set_readonly: (self: Buffer, readonly: boolean) -> ()

name

name: (self: Buffer) -> string?

The name it was given, for a buffer without a file, like a terminal’s “nu ~/src”.

set_name

set_name: (self: Buffer, name: string?) -> ()

Names it where it shows (the tab line, float titles, the status line); nil or “” leaves it unnamed.

title

title: (self: Buffer) -> string

What it’s called where it shows: its file’s name, the name it was given, “untitled” for a file not saved yet, or its kind.

line_numbers

line_numbers: (self: Buffer) -> boolean

Whether the main view shows line numbers beside it (the default).

set_line_numbers

set_line_numbers: (self: Buffer, on: boolean) -> ()

views

views: (self: Buffer) -> { View }

The views showing it.

kept

kept: (self: Buffer) -> boolean

Whether it stays open while nothing shows it, though it has no file.

set_kept

set_kept: (self: Buffer, kept: boolean) -> ()

Keeps it open while nothing shows it, like a terminal to come back to; with false it goes once nothing shows it (right away if nothing does now).

set_screen_size

set_screen_size: (self: Buffer, size: { rows: number, cols: number, note: string?, cursor_row: number? }?) -> ()

Lays the text out at rows by cols in every view, whatever room a view has, like a terminal whose program has one screen size for one client: the screen is the buffer’s last rows lines. A client whose view is another size crops it (a terminal), keeping row cursor_row (0-based, where the program’s cursor is) in sight, or scales it (a window), and shows note (default “COLSxROWS”) in the room left over. nil lays it out at each view’s own size again.

screen_size

screen_size: (self: Buffer) -> { rows: number, cols: number, note: string?, cursor_row: number? }?

The size set_screen_size set, if any.

set_keys

set_keys: (self: Buffer, keys: { [string]: string }?) -> ()

Binds single keys for this buffer alone, e.g. { y = "answer-yes" }, in every mode and ahead of every other binding, while it has focus. Replaces what was bound for it before; nil drops them.

forget_history

forget_history: (self: Buffer) -> ()

Stops keeping undo history, for buffers whose edits nobody undoes, like a terminal’s: what’s kept goes, and later edits aren’t kept.

clear_history

clear_history: (self: Buffer) -> ()

Lets go of the undo history so far and keeps later edits: undo can’t go back past now, as at a shell’s prompt after a command runs.

language

language: (self: Buffer) -> string?

The language the text is highlighted as, e.g. “rust”.

set_language

set_language: (self: Buffer, language: string?) -> ()

Sets the language, or with nil goes back to the one the file’s extension implies.

tree

tree: (self: Buffer) -> Node?

The root of the syntax tree, or nil if the language has no grammar or the buffer is too big to parse. Just after an edit, when parsing again would hold the editor up, it’s the tree before with the edit applied (nodes moved with the text) until the new one is in.

node_at

node_at: (self: Buffer, from: number, to: number?) -> Node?

The smallest named node covering from..to (default: just from).

query

query: (self: Buffer, source: string, range: { from: number?, to: number? }?) -> { { [string]: Node } }

Runs a tree-sitter query over the buffer, or only for matches overlapping from..to, e.g. "(function_item name: (identifier) @name) @fn". One table per match, from capture name to node. Errors on a query the language’s grammar can’t compile; a buffer without a grammar has no matches, so any query gives an empty list there.

edit

edit: (self: Buffer, f: (e: Edits) -> (), opts: { undo: boolean? }?) -> ()

Edits the buffer: f records edits on e, and they are applied together, as one undoable change, when it returns. With undo = false the change isn’t kept to undo, like output streaming in above where someone types: undo skips it and still takes back only what the steps before it did (redo branches are dropped).

save

save: (self: Buffer, path: string?) -> ()

Writes the buffer to its file, or to path, which becomes its file.

modified

modified: (self: Buffer) -> boolean

True if the text changed since it was opened or last saved.

version

version: (self: Buffer) -> number

A number that grows with every edit, for caches that follow the text.

reload

reload: (self: Buffer) -> boolean

Reads its file again, as an edit by “external” that undo can take back, and counts it as saved. Returns whether the text changed.

close

close: (self: Buffer) -> ()

Closes the buffer, unsaved changes and all. Errors while a view shows it: show something else there first. Plugins hear “buffer.closed”.

decorate

decorate: (self: Buffer, ns: string, marks: { Mark }, within: { from: number, to: number? }?) -> ()

Replaces the marks in namespace ns (pick one per plugin feature, e.g. “search”). Marks move with edits; {} clears them. With within, only those starting from from up to (not at) to (default: the end) are replaced, e.g. on the lines an edit changed.

decorations

decorations: (self: Buffer, ns: string) -> { Mark }

The marks in namespace ns, where they are now.

marks_at

marks_at: (self: Buffer, pos: number) -> { Mark & { ns: string } }

The marks over pos in every namespace, each with its ns: those whose range holds it, and empty ones that start there.

track

track: (self: Buffer, pos: number, opts: { gravity: ("left" | "right")? }?) -> Anchor

Follows pos through edits. Text inserted exactly there goes before it with gravity = "right" (the default) and after it with “left”. Deleting text around it moves it to where the deletion was.

track_range

track_range: (self: Buffer, from: number, to: number, opts: { from: ("left" | "right")?, to: ("left" | "right")? }?) -> Tracked

Follows from..to through edits. from and to in opts are each end’s gravity, as for track; by default text inserted at either end stays outside. It’s deleted once all of its text is.

find

find: (self: Buffer, pattern: string, from: number?, opts: { backward: boolean?, wrap: boolean? }?) -> (number?, number?, string?)

The first match of the regex pattern starting at or after from (default 0), or with backward the last one starting before it; wrap goes round the other end. Returns its start and end, nil when nothing matches, or nil, nil and the error for a bad pattern.

find_all

find_all: (self: Buffer, pattern: string, from: number?, to: number?) -> ({ { from: number, to: number } }?, string?)

Every match of the regex pattern between from and to (default: the whole buffer), or nil and the error for a bad pattern.

find_groups

find_groups: (self: Buffer, pattern: string, from: number?, to: number?) -> ({ { { from: number, to: number } | false } }?, string?)

Like find_all, but each match is the whole match’s range followed by each group’s, false for a group that took no part.

history

history: (self: Buffer, filter: HistoryFilter?) -> { HistoryEntry }

The transactions applied to it, newest first: by author (“user”, “plugin”, “agent” or “external”), plugin, cause (the command that made them, “undo”, …), group, made at or after since (seconds, like time), at most limit.

text_before

text_before: (self: Buffer, id: number) -> string?

The text as it was before transaction id, or nil if the buffer has none by that id.

undo_tree

undo_tree: (self: Buffer) -> { nodes: { UndoNode }, current: number }

The undo tree: every state the text has been in, from node 0 (as it was loaded) on, each with the step before it as parent; undo goes to the parent and redo to the last child. current is the node the text is at.

Node

A node in a buffer’s syntax tree, from buffer:tree(), buffer:node_at() or buffer:query(). It describes the buffer as it was when found; after an edit, find it again. tostring(node) shows its subtree, handy for writing queries.

kind

kind: string

The grammar’s name for it, e.g. “function_item” or “(”.

from

from: number

to

to: number

named

named: boolean

False for punctuation and keywords the grammar doesn’t name.

error

error: boolean

True for text the grammar couldn’t parse, or a missing piece it assumed.

field

field: string?

Its field name in its parent, e.g. “name” or “body”, if it has one.

parent

parent: (self: Node) -> Node?

children

children: (self: Node) -> { Node }

named_children

named_children: (self: Node) -> { Node }

Its children without the unnamed ones (punctuation, keywords).

child

child: (self: Node, field: string) -> Node?

The child in field field, e.g. fn:child("body").

next

next: (self: Node) -> Node?

The next named sibling.

prev

prev: (self: Node) -> Node?

The previous named sibling.

text

text: (self: Node) -> string

Its text. Errors if the buffer changed since the node was found.

Anchor

A position that moves with edits, from buffer:track. It goes when forgotten, when its buffer closes, or when the plugin that made it is unloaded.

pos

pos: (self: Anchor) -> number?

Where it is now, or nil once it’s gone.

forget

forget: (self: Anchor) -> ()

Stops following it.

Tracked

A range that moves with edits, from buffer:track_range. It goes as an Anchor does.

range

range: (self: Tracked) -> (number?, number?)

Where it is now, or nil once its text was deleted or it’s gone.

deleted

deleted: (self: Tracked) -> boolean

Whether all of its text was deleted, or it’s gone. Undo bringing the text back doesn’t change that.

forget

forget: (self: Tracked) -> ()

Stops following it.

MarkLine

A line a mark shows under its range: text, or pieces with styles of their own. On a float’s border (label, footer), a piece’s action is a command that a click on it runs, with any arguments after its name (“float-go 3”).

MarkLine = string | { { text: string, style: string?, action: string? } }

Mark

Text drawn with a style name, e.g. { from = 6, to = 11, style = "match" }. Text typed at either edge stays outside it.

from

from: number

to

to: number

style

style: string

line

line: boolean?

Style every whole line the mark touches, not just its text.

text

text: string?

Text to show that isn’t in the buffer, like an inlay hint, before the character at from. The cursor skips over it.

eol

eol: boolean?

Show text after the end of the line instead, cut off at the edge.

sign

sign: string?

A symbol for the sign column next to the line numbers, e.g. “E”.

gutter

gutter: string?

A style for the line number of each line the mark touches, e.g. “ui.gutter.diagnostic.error”.

above

above: boolean?

Draw over selections and cursors rather than under them.

overlay

overlay: boolean?

Draw text over the characters from from instead of inserting it, like a jump label. The text doesn’t move.

conceal

conceal: boolean?

Hide the text, showing text (if any) in its place, e.g. the brackets of a [[link]], except where reveal says.

reveal

reveal: ("line" | "cursor" | "never" | "auto")?

When concealed text shows: “line” (the default) when a cursor is on its line, “cursor” when a cursor is inside it past its start (a click leaves it hidden), “never”, or “auto” as greed.reveal last said, so the marks needn’t be drawn again when that changes.

fold

fold: boolean?

Hide the lines after the first one, up to the last, unless a cursor is in them, like a section folded under its heading.

plain

plain: boolean?

Draw the text without syntax highlighting, like a tool’s output in a Markdown buffer.

lines

lines: { MarkLine }?

Lines shown under the last line that aren’t in the buffer, like backlinks. The cursor never goes on them. Each is a string, or pieces with styles of their own on top of the mark’s, e.g. { { text = "x = " }, { text = "2", style = "diff.plus.change" } }.

image

image: { id: number, cols: number, rows: number, col: number?, row: number? }?

An image (from greed.image) drawn over cols by rows cells right of and below from, whatever text is there. col and row say which of its cells is at from, for an image cut off at the top or left. Shown in terminals that can show images.

action

action: string?

A command that clicking text runs, with the cursor put at from first, so the text works as a button, e.g. “proposal-accept”.

scale

scale: number?

How many times bigger the range’s text (and text) is, 1 to 4, e.g. 2 for a heading: each character takes that many cells across, and its row that many rows down. The window draws it big, and so does kitty (its text sizing); other terminals show it at the normal size.

data

data: any?

Anything to keep with the mark, given back where it is read: plain data (tables, strings, numbers, booleans), never drawn.

View

A buffer shown on screen, with its own selection. Two handles to the same view are equal (==).

id

id: (self: View) -> number

Its id, even after it was closed.

valid

valid: (self: View) -> boolean

Whether it is still open. Other methods fail once it was closed.

selection

selection: (self: View) -> Selection

set_selection

set_selection: (self: View, selection: Selection) -> ()

cursor

cursor: (self: View) -> number

The head of the primary selection.

set_cursor

set_cursor: (self: View, pos: number) -> ()

Makes the selection a single cursor at pos.

visible

visible: (self: View) -> (number, number)

The start and end of the text on screen when the view was last drawn, or of the whole buffer before that.

size

size: (self: View) -> (number?, number?)

The rows and columns of text it had room for when last drawn; nil before it’s drawn. “view.resized” says when they change.

area

area: (self: View) -> { x: number, y: number, width: number, height: number }?

Where it was last drawn on the screen, in cells, its gutter included; nil before it’s drawn.

hidden_lines

hidden_lines: (self: View) -> { { from: number, to: number } }

The lines folds hide, 0-based and inclusive.

scroll

scroll: (self: View) -> number

The 0-based line at the top of the view.

set_scroll

set_scroll: (self: View, line: number) -> ()

Scrolls so line is at the top. Drawing scrolls again if the primary cursor would be off screen, so move it first.

buffer

buffer: (self: View) -> Buffer

show

show: (self: View, buffer: Buffer) -> ()

Shows another buffer in this view.

focus

focus: (self: View) -> ()

Focuses this view, so keys go to it, showing its tab and project if it’s a pane or panel.

close

close: (self: View) -> boolean

Closes the view. Closing a tab’s last pane closes the tab; the project’s last pane can’t be closed, and returns false.

configure

configure: (self: View, spec: FloatSpec) -> boolean

Changes how a floating view is shown; fields left out stay as they are. False, changing nothing, if it isn’t floating; fails if it was closed.

raise

raise: (self: View) -> boolean

Brings a floating view in front of the other floats; false if it isn’t floating.

float_spec

float_spec: (self: View) -> FloatSpec?

How a floating view is shown, as greed.float takes it; nil if it isn’t floating.

is_pane

is_pane: (self: View) -> boolean

Whether it’s a pane of a tab, rather than a panel or a floating view.

split

split: (self: View, dir: "left" | "right" | "up" | "down", buffer: Buffer?) -> View

Opens a pane beside this one on its dir side, showing buffer, or this view’s buffer as it shows it, and focuses it. Fails if this view isn’t a pane.

neighbor

neighbor: (self: View, dir: "left" | "right" | "up" | "down") -> View?

The pane next to this one on its dir side, as last drawn: of several, the one active most recently.

resize

resize: (self: View, dir: "left" | "right" | "up" | "down", cells: number) -> boolean

Moves this pane’s edge on its dir side by cells (negative to shrink). False when no pane is on that side. A panel moves the edge by the main view, and only that.

undo

undo: (self: View) -> boolean

Undoes the last change. Returns false if there was nothing to undo.

undo_to

undo_to: (self: View, id: number) -> boolean

Takes the buffer to node id of its undo tree, undoing and redoing on the way, as one move; redo follows that branch from then on. False if there’s no such node or the text is there already.

redo

redo: (self: View) -> boolean

Context

What a command gets when it runs.

view

view: View

See View.

buffer

buffer: Buffer

See Buffer.

keys

keys: string?

The keys that ran the command, e.g. “g g”.

char

char: string?

The character typed, when a single printable key ran the command.

args

args: { string }

Arguments, e.g. from the command line; empty when a key ran it.

force

force: boolean

Go ahead even if the command would normally refuse, like :q! quitting with unsaved changes.

count

count: number?

The number typed before the keys, in modes with counts on.

client

client: number

The id of the client it runs for, whose view and selections these are.

ArgSpec

An argument a command takes, for completion and help.

name

name: string

doc

doc: string?

complete

complete: string?

What completes it, e.g. “file”, “command”, “option” or “buffer”.

CommandSpec

name

name: string

Plain words with dashes, e.g. “select-line”.

doc

doc: string?

args

args: { ArgSpec }?

run

run: (ctx: Context) -> ()

CliSpec

A command for the command line, run as greed NAME ARGS in an editor with no screen. run can wait like a command; what it echoes and returns is printed, and an error makes greed fail.

name

name: string

Plain words with dashes, e.g. “bring”.

doc

doc: string?

usage

usage: string?

What follows the name, e.g. “HOST [PLUGIN]”.

run

run: (args: { string }) -> string?

Layer

A keymap layer. Leave mode or kind out to match any. In place of a mode: layer names a layer that isn’t a mode, like the “leader”, which every editing style enters its own way (space, or ctrl-space without modes); role is an editing role (“command”, “typing”, “selecting”) in every style, and with style in that style only.

mode

mode: string?

layer

layer: string?

role

role: string?

style

style: string?

kind

kind: string?

Keymap

bind

bind: (layer: string | Layer, bindings: { [string]: string }) -> ()

Binds key sequences to command names, e.g. { ["g g"] = "goto-start" }. layer is a mode name or a Layer.

unbind

unbind: (layer: string | Layer, keys: { string }) -> ()

Hides what each key sequence runs in layer, e.g. { "w", "g g" }. Removing (or reloading) the plugin that unbound them brings them back.

clear

clear: (layer: string | Layer) -> ()

Hides everything bound in layer so far, to start it over. Bindings made afterwards, by any plugin, apply as usual.

counts

counts: (mode: string) -> ()

Makes digits typed before a key sequence in mode a count, which the command gets as ctx.count. A leading 0 stays a key.

isolate

isolate: (mode: string) -> ()

Makes mode use only keys bound in it (and its fallback), skipping inherited ones and those bound for every mode or for a buffer kind, e.g. for a terminal that takes every key.

inherit

inherit: (mode: string, parent: string | Layer, prefixes: { string }?) -> ()

Makes mode fall back to parent’s keys for sequences starting with one of prefixes (e.g. { "space", ":" }), or for every key without them. Keys bound in mode itself come first; what parent inherits applies in turn. parent can be a layer like { role = "typing" }.

inherit_first

inherit_first: (mode: string, parent: string | Layer) -> ()

Makes mode use parent’s keys ahead of its own, e.g. { style = "vim", role = "command" } for the Vim style’s command mode. core.style.define does this for each style’s roles.

enter

enter: (mode: string, keys: string, layer: string) -> ()

Makes keys typed in mode lead into layer, so what’s bound there works after them, e.g. enter("helix", "space", "leader"). It comes ahead of the mode’s own keys. core.style.define does this for each style’s leader.

fallback

fallback: (mode: string, command: string) -> ()

The command that handles a single unbound key in mode. A mode without one uses that of a mode it inherits every key from.

next

next: (keys: string?) -> { { key: string, command: string? } }

The keys that can follow keys (e.g. “space”) in the current mode, and the command each runs. Keys without a command start longer sequences.

find

find: (command: string?) -> { { layer: string, keys: string, command: string } }

Every key sequence that runs command (every binding, without one), the layer it’s bound in (a mode name, a named layer like “leader”, “role:typing” or “vim:command”, or “global”) and its command.

active

active: () -> { { keys: string, command: string } }

Every key sequence that runs a command in the current mode and buffer, through the layers it enters and inherits (the leader’s as “space f”), less those a more specific layer hides.

overlay

overlay: (bindings: { [string]: string }?) -> ()

Binds single keys for the next key press only, over every layer, e.g. { ["1"] = "hint-1" }; any other key drops them and does what it always does. Nil drops them now.

resolve

resolve: (keys: string) -> (string?, boolean)

What keys runs in the current mode: the command, or nil and whether more keys could still finish a sequence.

Mode

get

get: () -> string

set

set: (name: string) -> ()

Switches mode, emitting “mode.changed” if it changed.

set_cursor

set_cursor: (mode: string, shape: "block" | "bar" | "underline") -> ()

Sets how the cursor looks in mode; modes without a shape get a block.

cursor_of

cursor_of: (mode: string?) -> "block" | "bar" | "underline"

The cursor shape of mode, or of the current mode. A block or underline sits on a character, the last of a selection going forward; a bar sits between characters.

cursor

cursor: (mode: string, shape: "block" | "bar" | "underline") -> ()

The older name of set_cursor.

shape

shape: (mode: string?) -> "block" | "bar" | "underline"

The older name of cursor_of.

OptionValue

OptionValue = boolean | number | string

Options

define

define: (spec: { name: string, doc: string?, default: OptionValue, choices: { OptionValue }?, needs: string? }) -> ()

Defines an option. With choices, it takes only those values (the default among them), and :set completes them. With needs, a capability like “proc”, only code that has it, or you yourself (:set, your config), may set it: for an option whose value is run, like a command line. A plugin can’t define an option another plugin defined.

get

get: (name: string) -> OptionValue?

The option’s value for the acting client: its own, if it has one, else everyone’s.

set

set: (name: string, value: OptionValue?, opts: { client: boolean? }?) -> ()

Sets an option for every client; fails on an unknown name, the wrong type or a value that isn’t one of its choices. With client, sets a value of the acting client’s own instead, like a window’s font size, which nil takes away again; setting it for every client takes away each client’s own.

list

list: () -> { OptionInfo }

Every option, sorted by name.

OptionInfo

name

name: string

doc

doc: string

value

value: OptionValue

See OptionValue.

default

default: OptionValue

See OptionValue.

choices

choices: { OptionValue }

The values it can take; empty when any value of its type will do.

owner

owner: string

The plugin that defined it.

set_by

set_by: string?

The plugin that last set it (your config is a plugin too), if anything has.

source

source: string?

Where it was defined, e.g. “core/edit.luau:12”, and the file and line when that file is on disk.

path

path: string?

line

line: number?

HandlerInfo

An event handler, as greed.handlers() lists it.

event

event: string

owner

owner: string

The plugin that added it.

source

source: string?

path

path: string?

line

line: number?

Keys

Recording what’s typed and feeding keys back, for macros and repeating changes. Recordings have names (macros use the default, “”), so several can run at once.

record

record: (name: string?, opts: { fed: boolean? }?) -> ()

Starts keeping every key typed in the recording name, until stop; with fed, keys feed presses too.

stop

stop: (name: string?) -> { string }?

Stops the recording name and returns the keys typed, e.g. { "i", "x", "esc" }, or nil if it wasn’t recording.

recording

recording: (name: string?) -> boolean

feed

feed: (keys: string) -> ()

Presses the keys of a space-separated sequence as if they were typed. Only recordings started with fed keep them. What they run can do only what this plugin may (unless it has “commands”), and they don’t answer a command waiting for you to press a key. A command that works long enough to give the editor a turn finishes before the next key; one waiting on a timer or a job doesn’t hold them up.

HttpRequest

A request for greed.http. method defaults to POST with a body and GET without; timeout is in milliseconds, for the whole request. With save, request writes a successful (2xx) response’s body to that file as it arrives, creating its folders, and returns an empty body; the file appears only once it’s whole. Other responses come back as usual and nothing is written. Saving needs “fs.write”.

url

url: string

method

method: string?

headers

headers: { [string]: string }?

body

body: string?

timeout

timeout: number?

save

save: string?

HttpResponse

A whole response. Header names are lowercase.

status

status: number

headers

headers: { [string]: string }

body

body: string

HttpStream

A response whose body arrives in pieces. read waits for the next piece and returns it, nil at the end, or nil and why it failed; close stops reading.

status

status: number

headers

headers: { [string]: string }

read

read: (self: HttpStream) -> (string?, string?)

close

close: (self: HttpStream) -> ()

Http

Requests over the network, each on a thread of its own so the editor never waits. Both wait like greed.sleep, so they work in commands and event handlers. A plugin needs the “net” capability, or “net:HOST” for one host.

request

request: (request: HttpRequest) -> (HttpResponse?, string?)

Sends a request and returns the whole response, or nil and why.

stream

stream: (request: HttpRequest) -> (HttpStream?, string?)

Sends a request and returns once the status and headers are in; read the body as it arrives, e.g. tokens from a model.

Secrets

Secrets like API keys, kept out of config and plugin source: from the environment (GREED_SECRET_NAME, or the usual variable, e.g. ANTHROPIC_API_KEY), the system keyring, or Greed’s own secrets file. Both wait like greed.sleep. A plugin needs “secrets:NAME” for each secret it reads, or “secrets” for all.

get

get: (name: string) -> (string?, string?)

The secret name, or nil if it isn’t set anywhere.

set

set: (name: string, value: string) -> (string?, string?)

Keeps value as the secret name: in the keyring if there is one, otherwise in Greed’s secrets file. Returns where it went, or nil and why not.

variable

variable: (name: string) -> string

The environment variable that sets the secret name, to tell someone: the usual one, like ANTHROPIC_API_KEY, or GREED_SECRET_NAME. Reads nothing, so it needs no capability.

Process

Running other programs.

run

run: (
	command: { string },
	opts: { stdin: string?, cwd: string? }?
) -> ({ code: number?, stdout: string, stderr: string }?, string?)

Runs command (the program, then its arguments), with stdin as its input and cwd as its folder, and returns how it exited and what it printed; nil and the error if it couldn’t start. It runs in the background and waits like greed.sleep, so only in commands, event handlers and greed.spawn.

spawn

spawn: (command: { string }, opts: { cwd: string?, env: { [string]: string | false }?, stderr: ("keep" | "stream")?, group: boolean? }?) -> (Child?, string?)

Starts command to talk to over its input and output, like an agent speaking JSON-RPC, and returns at once; nil and the error if it couldn’t start. It runs until it exits, is killed, or the plugin that started it unloads. In env, false leaves a variable out of what it inherits. With stderr = "stream", what it prints as errors arrives from read too, in the order it comes; by default it’s only kept for child:stderr(). With group, it starts a process group of its own, which kill and signal reach, so the programs it starts end with it.

Child

A program started with greed.process.spawn.

pid

pid: number

write

write: (self: Child, text: string) -> boolean

Sends text to its input; false once that’s closed.

read

read: (self: Child) -> (string?, ("stdout" | "stderr")?)

Waits for the next piece of what it prints, and returns it with the output it came from, “stdout” or “stderr” (only with stderr = "stream"); nil once it exited and everything was read. Only in commands, event handlers and greed.spawn.

close

close: (self: Child) -> ()

Closes its input, which tells most programs to finish.

kill

kill: (self: Child) -> ()

Ends it (and with group, its process group) with KILL.

signal

signal: (self: Child, name: string) -> boolean

Sends it a signal by name, like “INT”, “TERM”, “HUP” or “QUIT”; with group, to its process group, even after it exited, as a shell signals a job. Returns whether it went.

wait

wait: (self: Child) -> (number?, string?)

Waits for it to exit and returns its exit code, or nil and the name of the signal that ended it, like “INT”.

stderr

stderr: (self: Child) -> string

The end of what it printed as errors so far.

TerminalRow

A line of a terminal: its text, and the parts with a style of their own (byte offsets, 0-based, to exclusive), styled with inline names like “=fg:red bold”.

text

text: string

spans

spans: { { from: number, to: number, style: string } }

images

images: { { from: number, id: number, col: number, row: number, cols: number, rows: number } }

Images on the row, as runs of cells from byte from: from the image’s cell (col, row) on, of an image cols by rows cells. id is a greed.image id, for a mark’s image.

links: { TerminalLink }

Links the program printed (OSC 8), as byte ranges and where they go.

wrapped

wrapped: boolean

Whether the line carries on on the next row: the program printed past the last column and the terminal wrapped it.

TerminalModes

The modes a terminal’s program set, from terminal:modes().

alternate

alternate: boolean

A full screen program, like an editor or a pager, is showing.

canonical

canonical: boolean

Input goes to the program a line at a time; off in raw mode, which editors and prompts set.

echo

echo: boolean

Typed text is echoed; off while a program reads a password.

mouse

mouse: boolean

The program asked for mouse events.

app_cursor

app_cursor: boolean

Arrow keys are sent in application mode.

bracketed_paste

bracketed_paste: boolean

Pastes are marked as pastes.

TerminalLink = { from: number, to: number, uri: string }

TerminalScreen

What a terminal shows.

rows

rows: { TerminalRow }

cursor

cursor: { row: number, col: number }?

The cursor’s row (0-based) and byte offset in it, unless the program hid it.

alternate

alternate: boolean

Whether a full screen program, like an editor or a pager, is showing.

Terminal

A program running in a terminal. Two handles to the same terminal are equal (==).

id

id: (self: Terminal) -> number

Its id, even after it was closed.

valid

valid: (self: Terminal) -> boolean

Whether it is still open. Other methods fail once it was closed.

screen

screen: (self: Terminal) -> TerminalScreen

scrollback

scrollback: (self: Terminal, from: number?) -> { first: number, rows: { TerminalRow } }

The lines that scrolled off the top, from line from (counting from the terminal’s first line) on, or from the oldest one kept. first is the number of the first line returned.

marks

marks: (self: Terminal, from: number?) -> { TerminalMark }

The places the shell marked (OSC 133) on line from (counting as scrollback does) and after, oldest first: where a prompt starts, where the typed command starts, where its output starts, and where it was done, with its exit code if the shell said. col is a byte in the line’s text.

marks_made

marks_made: (self: Terminal) -> number

How many marks the shell has made so far, to tell when there are new ones without asking for them all.

cwd

cwd: (self: Terminal) -> string?

The folder the shell last said it’s in (OSC 7), or the one it started in.

write

write: (self: Terminal, text: string) -> ()

Sends text to the program as if typed. This and the other methods that send input need “proc”: the plugin that started the terminal has it through the handle it got; others with one from an event need their own.

key

key: (self: Terminal, name: string) -> ()

Sends a key, e.g. “ctrl-c” or “up”, as a terminal encodes it.

paste

paste: (self: Terminal, text: string) -> ()

Sends pasted text, marked as a paste if the program asked for that.

resize

resize: (self: Terminal, rows: number, cols: number) -> ()

Changes its size, kept to between 1 by 1 and 1024 rows by 2048 columns.

wants_mouse

wants_mouse: (self: Terminal) -> boolean

Whether the program asked for mouse events, as editors and htop do.

modes

modes: (self: Terminal) -> TerminalModes

The modes the program set: the alternate screen, line input and echo (from the terminal’s settings), mouse, application cursor keys and bracketed paste.

mouse

mouse: (self: Terminal, kind: string, col: number, row: number, which: string?, mods: { ctrl: boolean?, alt: boolean?, shift: boolean? }?) -> boolean

Sends a mouse event at the program’s screen cell (col, row), 0-based, if it asked for that kind: kind as in a MouseEvent, with a button or a scroll direction. Returns whether it was sent.

title

title: (self: Terminal) -> string?

What the program called its window, if it did.

program

program: (self: Terminal) -> string

The name of the program it started, like “nu” or “cargo”: its file name, or the shell’s for a terminal started without a command.

exit_code

exit_code: (self: Terminal) -> number?

The program’s exit code, once it has ended.

busy

busy: (self: Terminal) -> boolean

Whether a program other than the one it started is in the foreground, like a command its shell runs. False once it has ended.

kill

kill: (self: Terminal) -> ()

Asks the program to end.

close

close: (self: Terminal) -> ()

Ends the program and lets the terminal go; its handle stops working.

Terminals

Programs in terminals. A terminal belongs to the plugin that started it.

spawn

spawn: (spec: { command: { string }?, cwd: string?, env: { [string]: string | false }?, rows: number, cols: number, scrollback: number? }) -> Terminal

Starts command (the program, then its arguments; the user’s shell without) in a terminal of rows by cols. Its output arrives as “terminal.output” events and its end as “terminal.exited”. scrollback lines are kept above the screen (default 10000). The size is kept to between 1 by 1 and 1024 rows by 2048 columns. In env, false leaves a variable out of what the program inherits.

TerminalMark

A place a shell marked in a terminal, from terminal:marks().

line

line: number

col

col: number

kind

kind: "prompt" | "command" | "output" | "done"

code

code: number?

The exit code, for “done”, if the shell said.

TerminalOutput

A terminal printed something; read it with terminal:screen().

terminal

terminal: Terminal

See Terminal.

TerminalExited

The program in a terminal ended.

terminal

terminal: Terminal

See Terminal.

code

code: number

UndoNode

A state of a buffer’s text in its undo tree, from buffer:undo_tree().

id

id: number

0 is the text as loaded; later states have higher ids.

parent

parent: number?

children

children: { number }

Oldest first; redo follows the last.

time

time: number?

When the step was last added to, in seconds like os.time(); nil for node 0.

HistoryEntry

A transaction applied to a buffer, from buffer:history().

id

id: number

author

author: "user" | "plugin" | "agent" | "external"

plugin

plugin: string?

The plugin, for edits a plugin made.

cause

cause: string?

Why: the command it was made for, “undo”, “redo”, …

group

group: number?

The undo group it was made in.

time

time: number

When, in seconds since 1970.

changes

changes: { { from: number, to: number, text: string } }

What it replaced, in the text before it, in order.

HistoryFilter

author

author: string?

plugin

plugin: string?

cause

cause: string?

group

group: number?

since

since: number?

limit

limit: number?

History

start_group

start_group: () -> ()

Edits until end_group become one undo step.

end_group

end_group: () -> ()

new_group

new_group: () -> number

A group id nothing has used yet, for set_group.

group

group: () -> number?

The group edits go into now, if any.

set_group

set_group: (group: number?) -> ()

Puts the edits that follow, in every buffer, into group (one used before carries on), or with nil into none. Edits in a group undo as one step.

cause

cause: () -> string?

Why edits are being made: by default the command running.

set_cause

set_cause: (cause: string?) -> ()

Sets why the edits that follow are made, for the rest of this command.

undo_group

undo_group: (group: number) -> (number?, string?)

Undoes group in every buffer it changed, as one step. Returns how many buffers changed (0 if it was undone already), or nil and why not, when something was done on top of it since.

FloatSpec

How a floating view looks. Sizes are cells, or a share of the screen as a string like “60%” or “37.5%”, which keeps the float in proportion on clients with screens of other sizes; both default to “60%”.

title

title: string?

width

width: (number | string)?

height

height: (number | string)?

anchor

anchor: ("center" | "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "cursor" | "above-cursor")?

Where it goes when x or y is left out: in the middle, at an edge or in a corner. “cursor” opens it just below the cursor (or above, near the bottom), like a completion menu; “above-cursor” prefers above, like signature help.

focus

focus: boolean?

Whether the view takes focus. Defaults to true.

x

x: (number | string)?

Where the outside of the border goes, from the screen’s left and top edges, in cells or like “37.5%”. Left out, the float goes where anchor says on that axis. It’s always kept on screen.

y

y: (number | string)?

tab

tab: boolean?

Whether it belongs to the tab it opens in: shown only with that tab, and closed with it, like a floating terminal. Otherwise it floats over every tab, like a picker. Defaults to false.

shared

shared: boolean?

Whether every client is shown it, like a review meant for everyone. Otherwise only the client it opened for is (the one acting then), as with a picker or a prompt, unless it belongs to a tab, which every client showing the tab sees. Defaults to false.

label

label: MarkLine?

Text at the right end of the top border, e.g. “‹ 2/5 ›”, or pieces with styles of their own. “” takes it off.

See MarkLine.

footer: MarkLine?

Text in the middle of the bottom border, as label takes it.

See MarkLine.

border

border: boolean?

Whether it has a border; defaults to true. Without one its text fills its whole box, like a one-line strip.

hidden

hidden: boolean?

Hides it: its view and buffer stay (a terminal keeps running), but it isn’t drawn, greed.floats() leaves it out and focus moves on. Focusing it shows it again.

CommandInfo

name

name: string

doc

doc: string

args

args: { ArgSpec }

owner

owner: string

The plugin that defined it.

source

source: string?

Where it was defined, e.g. “core/motions.luau:120”.

path

path: string?

The file and line it was defined on, when that file is on disk.

line

line: number?

BufferClosed

A buffer was closed. Only its id is left.

id

id: number

BufferChanged

buffer

buffer: Buffer

See Buffer.

from_line

from_line: number?

On “buffer.changed”: the first and last line (0-based, as the text is now) of what changed since the last “buffer.changed” for the buffer.

to_line

to_line: number?

ControlRequest

A request from the session socket, e.g. from greed ctl or an agent. Answer it with greed.control.reply.

id

id: number

client

client: string

“socket” for the session socket (greed ctl, greed mcp), “ide” for Claude Code’s IDE connection.

connection

connection: number?

The session socket connection it came on, for greed.control.send; nil for Claude Code’s IDE connection.

method

method: string

params

params: any

ControlClosed

A session socket connection closed, after its last request came.

connection

connection: number

The connection, as ControlRequest.connection gave it.

Signal

wait

wait: (self: Signal) -> any

Waits until a value is sent, and returns it.

send

send: (self: Signal, value: any) -> ()

Sends value to whoever waits. Only the first send counts.

ProjectInfo

A project: a folder with its own main view and panels.

id

id: number

root

root: string

current

current: boolean

Whether it’s the one shown.

Projects

list

list: () -> { ProjectInfo }

Every open project, in the order they were opened.

current

current: () -> ProjectInfo

open

open: (root: string) -> number

Opens a project rooted at root, with an empty main view, and shows it. Returns its id.

switch

switch: (id: number) -> boolean

Shows project id. False if there is none.

close

close: (id: number) -> boolean

Closes project id with its views and the buffers opened in it. The last project can’t be closed; false then.

Images

Images for marks to show. Each is kept until forgotten.

load

load: (path: string) -> (number?, string?)

Reads a PNG, JPEG or GIF file. Returns the image’s id, or nil and why not.

decode

decode: (bytes: string) -> (number?, string?)

The same, from the file’s bytes.

size

size: (id: number) -> (number?, number?)

Its width and height in pixels.

cells

cells: (id: number) -> (number?, number?)

How many cells it covers at its own size on the attached screen.

cell_size

cell_size: () -> (number, number)

A cell’s width and height in pixels on the attached screen.

forget

forget: (id: number) -> ()

Pdfs

PDF documents.

open

open: (path: string) -> (PdfDocument?, string?)

Reads the PDF at path. Returns the document, or nil and why not. It waits like greed.sleep, so only in commands, event handlers and greed.spawn.

PdfDocument

A PDF read with greed.pdf.open. Pages count from 1.

pages

pages: number

size

size: (self: PdfDocument, page: number) -> (number?, number?)

A page’s width and height in points (1/72 inch).

render

render: (self: PdfDocument, page: number, scale: number?) -> (number?, string?)

Draws a page at scale pixels per point (1 if not given) and returns it as a greed.image id, or nil and why not. Forget the image when done with it. Waits like greed.sleep.

text

text: (self: PdfDocument, page: number) -> (string?, string?)

The text on a page, line by line from the top, or nil and why not. Waits like greed.sleep.

TabInfo

A tab of the shown project: panes laid out side by side and one above another.

id

id: number

name

name: string?

The name it was given, if any.

current

current: boolean

Whether it’s the one shown.

zoomed

zoomed: boolean

Whether only its active pane is shown.

panes

panes: { View }

Its panes, left to right and top to bottom.

active

active: View

The pane keys go to while it’s shown.

See View.

Tabs

list

list: () -> { TabInfo }

The shown project’s tabs, in order.

current

current: () -> TabInfo

new

new: (buffer: Buffer?) -> number

Opens a tab after the shown one, with a pane showing buffer (or an empty buffer), and shows it. Returns its id.

switch

switch: (id: number) -> boolean

Shows tab id, of whichever project, and its project. False if there’s no such tab.

close

close: (id: number) -> boolean

Closes tab id and its panes. The last tab can’t be closed; false then.

rename

rename: (id: number, name: string?) -> boolean

Names tab id; nil or “” names it after what it shows again.

set_zoomed

set_zoomed: (zoomed: boolean) -> ()

Shows only the active pane of the shown tab, or all of them again.

layout

layout: (id: number?) -> any

How tab id (the shown one by default) lays out its panes, as a PaneLayout, with sizes adding up to 1 in each split.

set_layout

set_layout: (layout: any, id: number?) -> ()

Lays tab id’s panes out as layout (a PaneLayout) instead. It arranges the panes the tab has, so it has to hold each of them once; opening and closing panes is view:split and view:close. A split of one pane is that pane; splits nest at most 64 deep.

PaneLayout

A tab’s panes: a pane is its view, a split lays out its children side by side (“row”) or one above another (“column”), sharing the room by sizes, which are relative to each other (equal when left out).

PaneLayout = View | { split: "row" | "column", children: { PaneLayout }, sizes: { number }? }

PaneOpened

A pane was opened, beside from.

view

view: View

See View.

from

from: View

See View.

SessionInfo

A session running on this machine: an editor process of its own.

name

name: string

root

root: string?

The project it was started for.

current

current: boolean

Whether it’s this session.

Sessions

list

list: () -> { SessionInfo }

The sessions running on this machine, by name.

current

current: () -> string?

This session’s name, when the editor runs as one.

remote

remote: () -> boolean

Whether the attached client (or the last one) came over greed ssh; before any attached, whether greed ssh started the session.

attached

attached: () -> boolean

Whether a terminal or window is attached to the session.

program

program: () -> string?

Where the greed program running this editor is, to start greed mcp or greed ctl for this session.

switch

switch: (to: { name: string?, path: string?, new: boolean?, host: string? }) -> ()

Sends the attached client to the session name, or to the session for the project path is in (opening it there), starting it if it isn’t running. This session goes on without the client. With new, a new session for path’s project, even when one is running for it. With host, the session is on that machine, reached as greed ssh does.

Json

JSON. Tables with keys 1..n become arrays, other tables objects; an empty table is an object unless it came from array.

encode

encode: (value: any) -> string

value as JSON. Text that isn’t UTF-8, like a program’s binary output, has its broken bytes written as U+FFFD.

decode

decode: (text: string) -> (any, string?)

The value, or nil and why the text isn’t JSON. null becomes nil.

array

array: <T>(items: { T }?) -> { T }

Marks items (or a new table) to encode as an array even when empty.

Control

Needs the “control” capability (and serve_ide “net” too).

reply

reply: (id: number, result: any, message: string?) -> ()

Answers request id with result, or with an error message. Every request needs exactly one answer.

send

send: (connection: number?, method: string, params: any) -> boolean

Sends a notification of method with params to the session socket connection a request came on (its connection), e.g. MCP’s “notifications/tools/list_changed”. False if that connection has closed.

serve_ide

serve_ide: (folders: { string }, lock_dir: string?) -> number

Lets Claude Code find this editor with /ide: listens on a localhost WebSocket and writes a lock file naming folders as open, in lock_dir (default ~/.claude/ide). Its requests come with client “ide”. Returns the port; calling it again replaces the connection.

notify

notify: (method: string, params: any) -> ()

Sends a notification, e.g. “selection_changed”, to Claude Code.

ProfileRow

One row of greed.profile.stop(): what a plugin (nil for the editor itself) spent doing one thing, in all and at most once, in milliseconds.

ProfileRow = { plugin: string?, what: string, ms: number, longest: number, runs: number }

ClientAttached

A client attached, e.g. greed FILE in a session that was running. Handlers run as that client.

client

client: number

The client’s id.

path

path: string?

The file or folder it asked to open, if any.

terminal

terminal: boolean

Whether it asked for a terminal, e.g. greed ssh HOST --terminal.

remote

remote: boolean

Whether it came over greed ssh.

ClientDetached

A client went; the session goes on. Handlers run as that client, just before it’s gone.

client

client: number

ClientPaste

Text pasted into the client from the system clipboard: a terminal’s paste, or a window’s paste keys (ctrl-shift-v, shift-insert, cmd-v). The core plugin puts it at the cursors.

client

client: number

The client it was pasted into, which handlers run as.

text

text: string

ViewClosed

A view was closed. Its handle no longer works, but its id can be compared with view:id().

view

view: number

MouseEvent

What the mouse did, and what was under it.

client

client: number

The client whose mouse it was, which handlers run as.

kind

kind: "press" | "release" | "drag" | "move" | "scroll"

button

button: ("left" | "middle" | "right")?

For a press, release or drag.

dir

dir: ("up" | "down" | "left" | "right")?

For a scroll.

col

col: number

The screen cell, from the top left.

row

row: number

ctrl

ctrl: boolean

alt

alt: boolean

shift

shift: boolean

view

view: View?

The view under it, if any.

See View.

pos

pos: number?

The place in the view’s buffer under it: the character there, or the end of the line past it. Over a mark’s text, where the mark starts.

action

action: string?

The action of the mark whose text it’s over, or of the status line segment (with no view), if that has one.

x

x: number?

The cell in the view’s text, from its top left, right of the gutter.

y

y: number?

tab

tab: number?

The tab whose name it’s over, in the tab line.

border

border: ("top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right")?

On a floating view’s border (with view set), which part: “top” (the title’s), “bottom”, “left”, “right”, or a corner like “bottom-right”.

edge

edge: ("left" | "right" | "down")?

On the line between a pane or panel (view) and its neighbor, the side of view it’s on: “right” or “down” for panes, the side by the main view for panels. Dragging it resizes view with resize.

ViewResized

A view was drawn for a client with room for a different number of rows or columns of text than before. Handlers run as that client.

client

client: number

view

view: View

See View.

rows

rows: number

cols

cols: number

ViewScrolled

A view was drawn for a client starting at a different line than before. What a handler changes shows in the same frame, so a view can scroll another along with it. Handlers run as that client.

client

client: number

view

view: View

See View.

line

line: number

The 0-based line now at the top.

SelectionChanged

A view’s selection changed for a client, or it shows another buffer, since the last frame: told once a frame however many changes there were, whatever made them (keys, the mouse, undo, a plugin or an agent). Handlers run as that client.

client

client: number

view

view: View

See View.

ViewFocused

Another view has a client’s focus. from had it before; nil when it was closed. Handlers run as that client.

client

client: number

view

view: View

See View.

from

from: View?

See View.

moved

moved: boolean

The focus was moved under the client rather than by it: what it had focused went out of sight by another client’s doing (another tab or project shown, a pane closed), or it followed the others again. Its half-typed keys were dropped.

OptionChanged

An option was set, with greed.option.set or :set.

name

name: string

value

value: OptionValue

Its value now, as the client it was set for has it.

See OptionValue.

client

client: number?

The client it was set for, when one got a value of its own.

FileChanged

A file watched with greed.fs.watch was written, created or removed.

path

path: string

PluginLoaded

A plugin was loaded (or reloaded) and its entry file ran.

name

name: string

dir

dir: string?

Its folder, for plugins loaded from one.

PluginUnloaded

A plugin was unloaded, and everything it registered removed.

name

name: string

CommandRun

client

client: number

The client it runs for, which handlers run as.

name

name: string

The command about to run.

keys

keys: string?

The keys that ran it, when keys did, e.g. “g g”.

count

count: number?

The number typed before the keys.

KeysPending

client

client: number

The client typing them, which handlers run as.

keys

keys: string

The unfinished key sequence, e.g. “space”; “” once it’s finished.

StatusSegment

A piece of the status line: plain text, or text with a style.

text

text: string

style

style: string?

A style name, e.g. “ui.statusline.mode”, drawn over “ui.statusline”.

action

action: string?

A command a click on it runs.

StatusContext

What a status line function gets, for the main view.

view

view: View

See View.

buffer

buffer: Buffer

See Buffer.

mode

mode: string

message

message: string?

The latest message or error, if there is one to show.

width

width: number

The width of the screen in cells.

StatusParts

Segments for each side. The space between is padded; if the line is too long, the last segment on the left keeps only its end.

left

left: { StatusSegment }?
right: { StatusSegment }?

Clipboard

Copied text. Each copy holds one piece per selection it came from, so pasting into the same number of selections puts each piece back.

set

set: (pieces: string | { string }) -> ()

get

get: () -> { string }?

The latest copy.

history

history: () -> { { string } }

Recent copies, newest first.

image

image: () -> string?

The image on the system clipboard of the client acting now, as PNG bytes, read on that client’s own machine (so with greed ssh, your laptop’s); nil when there’s none or the client can’t read it. Waits like greed.sleep.

ClientInfo

A client attached to the editor: a terminal or window, on this machine or over greed ssh.

id

id: number

name

name: string

Its kind and machine, like “tui@laptop”; a client coming back with the same name gets its cursors, focus and mode back.

kind

kind: string

“tui”, “gui”, “headless” (the editor’s own, with nothing attached) or “test”.

remote

remote: boolean

Whether it came over greed ssh.

size

size: { width: number, height: number }?

Its screen in cells, once it has said.

layout

layout: "follow" | "independent"

“follow”: it shows the project, tab and panes the other followers do, with its own focus and zoom. “independent”: a layout of its own.

color

color: number

Which of the peer colors (1 to 6) its cursors show in for the other clients, as ui.cursor.peer.N.

person

person: string

Who it belongs to: “owner”, the session’s owner, for now.

Client

The clients you see the editor through. Commands, keys and event handlers act for one client at a time: the one that typed, clicked or ran them, or the one an event is about.

current

current: () -> ClientInfo

The client the running code acts for.

id

id: () -> number

The id of the client the running code acts for: current().id, without building the rest. For keeping what each client has open apart, like a picker of its own.

list

list: () -> { ClientInfo }

Every client, in the order they attached.

notify

notify: (title: string, body: string?) -> ()

Sends a notification to your desktop through the acting client: a terminal shows it with its notification escape (OSC 9, or OSC 777 in foot and urxvt) and a bell, while it isn’t focused. Without a client attached it goes nowhere.

set_layout

set_layout: (layout: "follow" | "independent", client: number?) -> boolean

Lays a client (the acting one by default) out on its own, “independent”, starting from a copy of the tabs, panes and panels it had, with views of its own; or has it “follow” the others again, closing those views (their buffers stay). Returns whether it changed.

Diagnostic

A problem a language server found.

from

from: number

to

to: number

severity

severity: number

1 error, 2 warning, 3 information, 4 hint.

message

message: string

source

source: string?

LspError

The error a language server answered a request with.

code

code: number

message

message: string

data

data: any?

LspPosition

A position as language servers count it.

line

line: number

character

character: number

Lsp

Language servers. A server runs per language and project root, started the first time a file that needs it is open, and started again if it stops.

server

server: (spec: { language: string, command: { string }?, commands: { { string } }?, root_markers: { string }? }) -> ()

Sets the server for a language, e.g. { language = "rust", command = { "rust-analyzer" }, root_markers = { "Cargo.toml" } }. With commands, the first one whose program is installed runs, when a file of the language first opens; with none installed, nothing does. A server runs in the topmost folder with one of root_markers inside the file’s project, or the project’s root without one.

remove

remove: (language: string) -> ()

Stops starting a server for a language; one running keeps running.

restart

restart: (language: string?) -> number

Stops the servers running for language (every language without one) and starts them again, as after they got stuck. Returns how many were running. Needs “proc”.

servers

servers: () -> { [string]: { { string } } }

The commands set for each language’s server, by language.

running

running: () -> { LspServer }

The servers running now, by language and then root.

root

root: (buffer: Buffer) -> string?

The folder the buffer’s server runs in, or would: nil without a file or a server set for its language.

request

request: (buffer: Buffer, method: string, params: any) -> (any, LspError?)

Sends a request to the buffer’s server and waits for the answer. Returns the result, or nil and the error; a server that stops before answering gives the error “the language server stopped”. Works in commands and event handlers. “workspace/executeCommand” needs “proc”, since a server’s commands can do anything.

uri

uri: (buffer: Buffer) -> string

The buffer’s file as a URI, for request parameters.

path

path: (uri: string) -> string?

The file path in a file:// URI.

position

position: (buffer: Buffer, pos: number) -> LspPosition

Byte offset pos as the buffer’s server counts positions.

offset

offset: (buffer: Buffer, position: LspPosition) -> number

The byte offset of a server position.

diagnostics

diagnostics: (buffer: Buffer) -> { Diagnostic }

The buffer’s latest diagnostics, where they are now: they move with edits. They are marks in the buffer’s “diagnostics” namespace, with { severity, message, source } as their data.

problems

problems: () -> { LspProblem }

Every file’s latest problems, including files that aren’t open, with ranges as the server counts positions. Those in open files are where they are now.

capabilities

capabilities: (buffer: Buffer) -> any

What the buffer’s server said it can do (its ServerCapabilities), or nil without a running server.

on_request

on_request: (method: string, handler: (params: any) -> any) -> ()

Answers requests of method from servers, e.g. “workspace/applyEdit”, with what handler returns. It can wait, e.g. to ask you; the server gets the answer when it returns.

on_notification

on_notification: (method: string, handler: (params: any, language: string, root: string) -> ()) -> ()

Hears notifications of method from servers that the editor doesn’t deal with itself, e.g. “$/progress”, with the language and root of the server that sent it.

LspServer

A language server that’s running.

language

language: string

root

root: string

The folder it runs in.

command

command: { string }

ready

ready: boolean

Whether it has answered initialize.

stderr

stderr: { string }

The last lines it wrote to stderr.

LspProblem

uri

uri: string

range

range: { start: LspPosition, ["end"]: LspPosition }

severity

severity: number

message

message: string

source

source: string?

DiagnosticsChanged

buffer

buffer: Buffer

See Buffer.

ModeChanged

A client’s mode changed. Handlers run as that client.

client

client: number

from

from: string

to

to: string

DirEntry

name

name: string

dir

dir: boolean

FileInfo

kind

kind: "file" | "dir" | "other"

size

size: number

Size in bytes; 0 for a directory.

mode

mode: number

Its permission bits, like 420 (644 in octal, as string.format("%o", mode) shows).

modified

modified: number?

When it last changed, in seconds since 1970, where the system says.

Hash

SHA-256 digests as lowercase hex, for checking a download or telling whether some text changed.

sha256

sha256: (data: string) -> string

The digest of data.

sha256_file

sha256_file: (path: string) -> (string?, string?)

The digest of the file at path, read in pieces, or nil and why. Needs “fs.read”.

Fs

Reading and writing the file system. Failures return nil and a message.

list

list: (dir: string, opts: { hidden: boolean?, ignored: boolean? }?) -> ({ DirEntry }?, string?)

The entries directly in dir, folders first, then by name. Hidden entries and ones .gitignore excludes are left out unless asked for.

read

read: (path: string, limit: number?) -> (string?, string?)

The file’s text, or its first limit bytes.

read_bytes

read_bytes: (path: string) -> (string?, string?)

The file’s bytes as they are, for a file that isn’t text, like an image.

stat

stat: (path: string) -> (FileInfo?, string?)

What’s at path; nil if nothing is, and nil and why when it can’t tell, as when a folder above it can’t be read.

write

write: (path: string, text: string, opts: { private: boolean? }?) -> (boolean?, string?)

Replaces the file’s contents with text, creating it and its folders if needed. It’s written whole or not at all. With private, only you can read it (mode 0600), and folders made for it are only yours.

mkdir

mkdir: (path: string) -> (boolean?, string?)

Makes the folder at path and any missing folders above it. Needs “fs.write”.

remove

remove: (path: string, opts: { recursive: boolean? }?) -> (boolean?, string?)

Removes the file, link or empty folder at path; with recursive, a folder and everything in it. Nothing being there counts as removed. It refuses (an error) the root, your home folder, a path with .. and a folder holding Greed’s approvals or secrets. Needs “fs.write”.

rename

rename: (from: string, to: string) -> (boolean?, string?)

Moves or renames from to to, replacing a file there. It refuses what remove does. Needs “fs.write”.

copy

copy: (from: string, to: string) -> (boolean?, string?)

Copies the file from to to, creating its folders. Needs “fs.read” and “fs.write”.

watch

watch: (path: string) -> ()

Watches the file at path: whenever it changes, from inside the editor or out, plugins hear “file.changed”. It needn’t exist yet.

cwd

cwd: () -> string

The directory Greed was started in.

dirs

dirs: () -> { home: string?, config: string?, cache: string?, data: string?, brought: string? }

Your home folder, Greed’s config folder (where init.luau and plugins/ were read from), its cache folder, its data folder (where plugins made in the editor go when the config can’t be written) and, on the far end of greed ssh, the copy of your config it brought.

grep

grep: (
	pattern: string,
	dir: string?,
	opts: { hidden: boolean?, ignored: boolean?, limit: number? }?
) -> ({ GrepLine }?, string?)

The lines matching the regex pattern in the text files under dir (default: the current directory), sorted by path and line. It searches in the background and waits like greed.sleep (commands, handlers, greed.spawn). Hidden and ignored files are skipped unless asked for; limit (default 10000) caps the lines returned. A bad pattern gives nil and why.

GrepLine

A line greed.fs.grep found.

path

path: string

Relative to the directory searched.

line

line: number

0-based.

text

text: string

The line, without its line break.

matches

matches: { { from: number, to: number } }

Byte offsets of the matches in text.

PluginCheck

What greed.plugin.check found. types.skipped says why types weren’t checked; tests.error why the tests couldn’t run.

types

types: { ok: boolean?, output: string?, skipped: string? }

tests

tests: { passed: number?, failed: { { file: string, name: string, message: string } }?, error: string? }

warnings

warnings: { string }

What looks wrong once it’s loaded with the others: a command it replaces from another plugin, keys bound to commands that don’t exist.

Plugins

list

list: () -> { string }

The names of the loaded plugins, sorted.

unload

unload: (name: string) -> ()

Removes a plugin and everything it registered: commands, keys, options and event handlers. Fails while another loaded plugin requires it. To turn off a default plugin, call this from your config.

load

load: (dir: string, name: string?) -> string

Loads (or reloads) the plugin in dir and returns its name. With name, a folder without a plugin.toml loads too, with init.luau as its entry.

dirs

dirs: () -> { [string]: string }

The folder of each plugin loaded from one, by name.

check

check: (dir: string) -> PluginCheck

Checks the plugin in dir without loading it: strict types against Greed’s API (if luau-analyze is installed), then its tests with the loaded plugins. Waits, like greed.sleep, while it runs. A plugin that needs approving is tested with only what you approved for it, so what needs more fails with “needs approval”, through the plugins it calls and the commands it runs too.

describe

describe: (dir: string) -> PluginInfo

What the plugin in dir is and asks for, from its plugin.toml.

pending

pending: () -> { PluginInfo }

The plugins waiting until what they use is approved.

approve

approve: (dir: string) -> string

Approves what the plugin in dir declares, loads it, and loads the plugins that waited for it. Returns its name. Fails when a plugin without “plugins” started what runs: approving is yours to do.

caller

caller: () -> string?

The plugin whose code called yours: the innermost other plugin on the stack, or the one whose command, handler or task is running; nil if none but yours.

started_within

started_within: (capability: string) -> boolean

Whether everything that started the code running now may use capability: you (keys, the command line, your config), or plugins that have it. A vouch doesn’t hide what started it. For actions only you should take, like accepting a review.

within

within: <A..., R...>(name: string?, f: (A...) -> R..., A...) -> R...

Runs f with ... within the limit of the plugin name too, as if it had started it: for running what a plugin registered (a formatter, a runner, a source) later, from your plugin, so it can do no more than its plugin may. Config and unknown names add nothing. It can wait.

tie

tie: (remove: () -> ()) -> string?

For registries: runs remove when the plugin whose code called yours (see caller) is unloaded or reloaded, so what it added goes with it. Returns that plugin’s name, or nil (and does nothing) if none but yours called.

PluginInfo

A plugin, as greed.plugin.describe tells it, for a review.

name

name: string

dir

dir: string

requires

requires: { string }

capabilities

capabilities: { { name: string, what: string } }

What it declares, each with what it lets the plugin do in words.

missing

missing: { string }

The capabilities not approved yet; empty when it may load.

Style

How text looks. Colors are “red”, “bright-blue” and the other 16 terminal colors, “default”, or “#rrggbb”. Fields left out leave whatever is underneath, so styles layer.

fg

fg: string?

bg

bg: string?

underline

underline: (boolean | string)?

true, or “line”, “curl”, “dotted”, “dashed” or “double”.

underline_color

underline_color: string?

bold

bold: boolean?

italic

italic: boolean?

dim

dim: boolean?

reverse

reverse: boolean?

strikethrough

strikethrough: boolean?

rounded

rounded: boolean?

For floats in the window (ui.float, in its gui part): rounded corners and an outline instead of box lines, and a soft shadow.

shadow

shadow: boolean?

badge

badge: boolean?

For text in the window: its background as a badge, a box with rounded corners a little inside its cells, as motion hints have.

checkbox

checkbox: ("empty" | "checked" | "choice" | "chosen")?

In the window, drawn as a control over the cells of its text: a checkbox (“empty” or “checked”, for todos) or a choice (“choice” or “chosen”). Terminals show the text.

font

font: ("mono" | "prose")?

The window’s font for the text: “mono”, the grid’s (gui-font), or “prose”, a proportional one (gui-font-prose) fitted into the same cells. Terminals have only the one.

ThemeEntry

A theme entry: a style for every client, with changes for the terminal (tui) or a graphical client (gui).

tui

tui: Style?

See Style.

gui

gui: Style?

See Style.

Syntax

The tree-sitter queries each language uses: “highlights” for highlighting, “injections” for languages inside others (code blocks in Markdown), and others (“textobjects”, …) that plugins read. The languages plugin sets them from the queries/<language>/<kind>.scm files in plugins and your config.

set_query

set_query: (language: string, kind: string, source: string?) -> ()

Sets the kind query for language, or with nil takes this plugin’s away. Errors if the language’s grammar can’t compile it. The newest one set is used, and unloading a plugin takes its queries away.

query

query: (language: string, kind: string) -> string?

The kind query in use for language: the one set, or for “highlights” and “injections” the one that comes with the grammar.

load_grammar

load_grammar: (language: string, path: string, symbol: string?) -> ()

Loads the grammar for language from a WebAssembly file, like one built with tree-sitter build --wasm, so buffers in it get a syntax tree. symbol is the grammar’s own name when it differs, e.g. “c_sharp” for “c-sharp”; by default the language with “-” as “_”. Queries come separately. Needs “fs.read”.

tags

tags: (dir: string?, opts: { limit: number? }?) -> { FileTags }

What the source files under dir (default: the current directory) define and use, from their syntax trees: the functions and types their “textobjects” query finds and how often each name appears, sorted by path. Hidden and ignored files are skipped; limit (default 10000) caps the files. It parses in the background and waits like greed.sleep. Needs “fs.read”.

FileTags

What one file defines and uses, from greed.syntax.tags.

path

path: string

Relative to the directory searched.

language

language: string

definitions

definitions: { { name: string, kind: "function" | "type", line: number, signature: string } }

Each with its 0-based line and its first line as signature.

references

references: { [string]: number }

How often each name appears in the file.

Theme

What style names look like. Everything on screen has one: “ui.text”, “ui.selection”, “ui.statusline.mode”, “ui.float.border”, “keyword”, “diagnostic.error”, or a name a plugin made up. Names fall back to their parent, so “markup.heading.1” uses “markup.heading” if it has no entry.

set

set: (entries: { [string]: ThemeEntry }) -> ()

Sets entries by name. Each changes only the fields it gives of the theme’s entry underneath, and replaces what was set for that name before.

base

base: (entries: { [string]: ThemeEntry }) -> ()

Replaces the base theme, the entries under those set with set: a theme plugin calls it to switch themes without touching what your config or other plugins set.

get

get: (name: string) -> ThemeEntry?

The entry for exactly name, if there is one.

EventName

The events greed.on and greed.off take; any other name is an error.

EventName =