A from-scratch, keyboard-first text editor written in Rust, built on
wgpu and winit.
Fenix is modal (Vim-grammar editing) with a SPC-leader mnemonic layer
(Doom Emacs/Spacemacs-style) on top, a real file explorer, project-aware
fuzzy pickers, tree-sitter syntax highlighting, and a startup dashboard.
This is an early, personal project — expect rough edges. It's shared as-is for anyone curious to poke around or build on it.
-
Modal editing: Normal/Insert/Visual (char/line/block)/Replace/Command modes, the standard motion set (
h j k l w b e 0 ^ $ gg G f F t T ; , % { }), operators (d c ycomposing with motions and text objects, plus the doubleddd/cc/yyforms),iw/awtext objects, numeric counts (3dw,2dd, ...), yank/paste mirrored onto the OS clipboard (the unnamed register only --y/d/cpush to it,p/Ppull from it first, so copying in Fenix and pasting elsewhere -- or vice versa -- just works), named registers ("a-"z/"A-"Zto select one for the nexty/d/c/x/s/p/P, uppercase appends), undo/redo, search (/,?,n,N,*,#) with a live incsearch preview and persistent match highlighting while it's active,:ssubstitute with backreferences, indentation (>>/<<, auto-indent,:set shiftwidth=N). -
Macros (
q{a-zA-Z}to record, a second bareqto stop,@ {register}to replay,@@to repeat whichever was last played,3@a-style count prefix): real Vim's own model, not a separate storage -- a macro is just a named register's text (Vim's own keycode notation,<Esc>,<C-r>, ...), so"appastes a recorded macro's literal keystrokes as text and yanking text into a register makes it@-executable. Recording captures every keystroke, includingSPC-leader sequences and prompts, not just what reaches Vim's own motion/operator dispatch. A self-referential macro is bailed out by a depth/key-count guard rather than actually hanging, unlike real Vim. -
Jumplist (
Ctrl-O/Ctrl-I): back/forward through recent cursor positions, recorded ongg/G/%/a confirmed search/*/#and on jumping to a symbol definition, a grep match, a quickfix entry, or a mark -- so jumping to a definition (SPC c s), a search hit (SPC p s), or a mark (`a) and hittingCtrl-Otakes you right back, even across files. A single global back/forward pair of stacks (like a browser's history), not real Vim's per-window circular list -- a disclosed simplification. -
Marks (
m{a-zA-Z}to set,`{mark}/'{mark}to jump -- exact position vs. the mark's line's first non-blank, real Vim's own split): named position bookmarks that work across files, unlike real Vim's lowercase-is-buffer-local/uppercase-is-global distinction, which Fenix doesn't replicate -- every mark can jump to wherever it was set, any file. Not composable with operators (nod`a/d'a) -- jump-only. -
SPC-leader menu with a live which-key popup showing available continuations as you type a sequence -- reachable from Visual mode as well as Normal, so e.g.SPC c f(indent the selection) can act on an active selection without leaving it first. -
Syntax highlighting via tree-sitter for Rust, TOML, Markdown, JSON, YAML, Python, JavaScript/TypeScript/TSX, C, C++, Bash, Tcl, Dockerfile/ Containerfile, and Batch (
.bat/.cmd). Docker Compose files already get full highlighting for free via the existing YAML support -- no separate grammar needed.Dockerfile/Containerfileare detected by filename (they conventionally have no extension), including per-stage names likeDockerfile.prod. In Tcl, a bare word in command position is only colored as a command if it's actually known -- a built-in, a ctags-scanned project definition, or a symbols-file entry (the same three sources autocompletion draws from), matched against its fully-qualified path with an optional leading::-- not just any word that happens to be first on a line, and including the procs defined in the file you're looking at (which is the whole of what's known for a lone script with no project forctagsto scan). Markdown gets a real second pass beyond its own block structure (headings, lists, code fences): tree-sitter-md ships two grammars, and the block one only ever marks a span of prose with a bare(inline)node rather than parsing what's actually inside it -- bold, italic, inline code spans, and links are that second, inline grammar's own job, run as a genuine (if lightweight) language injection over each such span. Each reads as a distinctly colored token -- via color alone, not also a different font weight: making**bold**render in an actual bold typeface would mean threading a style flag through every function between a syntax capture and the glyph it becomes, several of them shared by every other language's own highlighting, for a purely cosmetic gain on one language -- more risk than the result was worth. -
Markdown list continuation: pressing Enter or
oon a-/*/+bullet or a1./1)ordered item continues it onto the next line at the same indent -- an ordered marker increments by one (not renumbered through the rest of the list; CommonMark renderers only look at a list's first number anyway, so a "wrong" one past that point is cosmetic in the source and invisible once rendered), and a- [ ]/- [x]task item continues unchecked regardless of whether the one you just finished was checked. Enter on an empty item (nothing typed after its marker yet) leaves the list instead of repeating the marker forever. No per-file-type gate -- a list shaped like a list continues wherever it appears, the same "the text shape alone is the signal" posture bracket-depth reindenting (SPC c f/SPC c F) already has.O(open line above) doesn't get this -- it would need renumbering every ordered item from there down to stay correct, real complexity for a much rarer motion than Enter/o.SPC c xtoggles a GFM task checkbox (- [ ]/- [x]/- [X]) on the current line.SPC c ois a fuzzy picker over every heading in a Markdown buffer, indented by nesting depth -- confirming jumps straight to it, the same shapeSPC s s(fuzzy-find any line) already has, just filtered to headings. Gated to a detected Markdown buffer specifically, unlike list continuation/checkbox toggling:#means "comment," not "heading," in half the languages this editor highlights. -
File explorer (dired-style):
SPC f jopens a real, Vim-navigable buffer (splittable, closable withSPC b k, listed inSPC b b) with the whole feature set -- marks, batch create/rename/copy/move/delete, git-status badges, subtree expansion, sorting by name/size/date/type; ordinary motions (j k gg G /) work for free since it's real text, and the cursor is the selection, so operations always act on the row you are looking at. A persistent sidebar (SPC e t) shows the same listing in a strip, reading the same key table, so a binding can never mean two different things in the two forms.SPC e darranges the classic two-listings-side-by-side layout out of the window system rather than as a panel of its own, so both halves are ordinary buffers that split, close and switch like any other -- andC/Mseed their destination with the directory the other half is showing, which is the whole reason to arrange two listings.Reading a directory happens off the main thread, for all three forms of the listing -- the pane, the sidebar and the directory picker. A slow path -- a network share, a disk waking up -- leaves the editor completely usable, and the pane keeps showing where you are until the new listing arrives; after a moment the header names what it is waiting on and
Escstops waiting and leaves you where you were. (Nothing tries to cancel the read itself: a blockedread_diron a share that has gone away sits in the kernel until SMB gives up, minutes later, and no amount of asking shortens that. Its result simply arrives stale and is dropped.) Git badges follow as a separate pass, so they never hold up the listing, and are skipped for\\server\sharepaths where runninggit statusacross the wire would cost more than it is worth.Getting somewhere does not mean walking there.
SPC e pis a path bar: type it, withTabcompleting a directory at a time,~and%APPDATA%expanded, quotes stripped off anything pasted out of Explorer, and forward slashes accepted. Typing a file opens it rather than refusing. Typing\serverwith no share asks the server what it has and offers the answer as a list -- off the main thread, because a host that is not there takes tens of seconds to say so.SPC e bis everywhere else worth going in one list: bookmarks first, then the machine's drives with free space (a mapped drive shows the share it really points at, sinceZ:on its own tells you nothing), then the directories you have actually been to, then registered projects.SPC e mbookmarks where you are, named after the folder, with no prompt -- a bookmark you can add without stopping to think is one you will actually add. Bookmarks live inconfig.ini's[explorer]section and can be hand-edited between sessions.Renaming in bulk is the thing an editor can do that a file manager cannot.
SPC e wre-renders the listing as one bare name per line and makes it editable, so:%s/, visual block, macros and counts become bulk-rename tools nobody had to build;SPC e Wworks out what the edit asks for and shows a real example before doing any of it. A name may contain/, so it reorganises as well as renames. Line position is identity -- line N is entry N -- which is why adding or removing a line is refused rather than guessed at: a deleted line is not a deleted file. Two names given the same value, or a name landing on an untouched file, are refused up front, so a rejected edit changes nothing and stays on screen to fix. Swapping two names works, because when a rename set has a cycle in it everything is moved aside first and then into place -- and if any part of that fails, all of it is put back, since a bulk rename is one edit and half of one is a directory nobody asked for.Copying and moving run in the background, with a live count of files and bytes in the modeline and
SPC e kto stop. Cancelling stops between files -- a file already being written has to finish, since there is no way to abandon a copy part way through without leaving a truncated one behind -- and what has already been copied stays, because that is what actually happened.zpacks the marked set into a.zip(or.tar.gz, by extension) andxunpacks the archive at point into a folder of its own; both go through thebsdtarWindows ships, named by absolute path, because thetaronPATHis often GNU tar and GNU tar answers a request for a.zipwith an uncompressed tar under that name.ishows what a column cannot hold -- every timestamp, the attributes, where a link points -- and on a folder counts what is inside it, which is the one number a listing cannot show without walking the whole tree.wflips read-only, the attribute that stops an ordinary save.The last mile is the set of things that make trusting all of this easy.
SPC e oopens the entry with whatever the system associates with it, because the answer to a.xlsxis Excel and not a hex dump.SPC e Oshows it in Explorer, selected -- being unable to leave is not the same as not needing to.SPC e ycopies the full path (the path, not the name: a path is what you paste into a terminal or another program's open dialog).SPC e Topens a shell here, which is what the pane-terminal work made possible -- without a working directory the first thing anybody types is acd.SPC e gsearches this directory once, leaving the next unqualified search meaning the project again, andSPC e Gmakes this the project and opens the Git panel on it, soSPC p fandSPC s pfollow rather than one panel pointing somewhere the rest of the editor is not.Links and junctions are shown as links (
name/@) and coloured differently from real directories, because following one into a tree you did not expect to be in is exactly the surprise worth preventing;isays where one points.Deleting means the Recycle Bin, so a mistake is recoverable through Windows' own restore. Copying or moving onto something that already exists asks first -- overwrite, skip, or keep both -- rather than silently destroying it, and skipping a move leaves the source where it was.
SPC f estarts that same fuller explorer at your home directory instead of the current file's -- for a file that isn't in any project and isn't worth typing an absolute path for (something in~/Downloads, say): navigate down with the usualj k l h/Enterand open it directly, or pressSonce you're close to fuzzy-search every file under wherever you've navigated to, recursively (the same candidate listSPC p fbuilds, just rooted there instead of the project). -
Files menu (
SPC f ...):SPC f fopens a file by typing its path (~expands to home, a relative path resolves against the project root) -- unlike every fuzzy finder here, it doesn't enumerate anything, so a.gitignore'd file (.env, say) opens exactly like any other, and a path that doesn't exist yet opens an empty buffer to save later.SPC f aisSPC p f's fuzzy-find sibling but including gitignored files, for when you want to search by name rather than type the exact path.SPC f rfuzzy-finds a recently-opened file.SPC f R/SPC f D/SPC f yrename/delete (with confirmation)/copy-the-path of the file currently open. -
Project tooling: fuzzy find-file (
SPC p f), project-wide search via ripgrep (SPC p s) with the result list kept around afterward as a quickfix list --SPC p n/SPC p Nstep to the next/previous match directly in the editor without reopening the picker or re-running the search, clamping (not wrapping) at either end -- switch between known projects (SPC p p). -
Files changing on disk: every open file is checked against what's actually on disk a couple of times a second, and whenever the window regains focus. Both are needed -- focus catches editing in another application, and the timer catches Fenix's own terminal panel, which never takes focus away from the window at all. A clean buffer is re-read in place, keeping every pane on the line it was on. A buffer with unsaved edits is never touched: it's flagged instead, shown as
[disk]in the modeline, and:won it refuses rather than overwriting --:w!keeps yours,:e!takes theirs. That refusal is the point of the whole feature. A plain write is a whole-file overwrite, so without it, runninggit checkoutor a formatter in the terminal two splits away and then pressing:wsilently discards it, with nothing to recover from.A deleted file never blanks its buffer -- a "safe write" in another editor removes and recreates the file, and an editor that empties your buffer for that moment is worse than one that says nothing. A file rewritten with identical content (a build, a formatter that changed nothing) says nothing either. The periodic check is a
statper open file, comparing modification time and length; the save guard reads the file outright, because that check can miss a same-length edit made inside the filesystem's timestamp granularity and a write is the one moment where being approximately right costs somebody their work.[editor] watch_files = falseturns the whole thing off, for a working copy on a network share. -
Safe saves: documents, settings, and recovery snapshots are staged beside their destination, explicitly flushed and synced, then replaced. Windows replacement preserves destination ACLs and refuses sharing violations; failed saves leave buffers dirty.
:w <path>names and saves an unnamed buffer without overwriting an existing file (spaces in the path are preserved). -
Crash recovery: every editable buffer with unsaved changes, including unnamed scratch work, is written to a snapshot a couple of times a second, so a crash, a power cut or a killed process costs at most a moment's typing instead of everything since the last
:w. The snapshot is deleted the instant the buffer is saved or deliberately closed, so a normal session leaves nothing behind and only an abnormal exit leaves anything to find. On the next start, if anything survived, the modeline says which files and thatSPC f vrecovers them; recovering loads the text back as an unsaved edit, so:waccepts it and:e!throws it away -- the same pair of answers as any other two versions that disagree. Recovery continues when file watching is disabled. A failed snapshot displays[recovery failed]until recovery succeeds or the affected work is saved or discarded. Unnamed snapshots recover into unnamed, unsaved buffers. Snapshots older than two weeks are cleaned up on startup.Deliberately not Vim's swap files. Those live next to the file being edited, which means
.gitignoreentries and build tools tripping over them, and they carry a locking protocol for "another instance has this file open" that Fenix has no use for -- their famous failure mode, a stale.swpprompting about a file nobody is editing, is worse than the loss they prevent. Snapshots here live in one directory under Fenix's own config location, keyed by a hash of the file's path, and never touch the working tree. Nor is there a modal "recover?" prompt on launch: it says what's there and gets out of the way. -
Search and replace (
SPC s ...):SPC s sfuzzy-finds any line in the current buffer (live-filtered as you type).SPC s rprompts for a pattern then a replacement and shows the match count before applying -- a real UI over the same regex engine:salready uses, scoped to the current Visual selection's lines if invoked from Visual mode (mirroring real Vim's own:'<,'>s), the whole buffer otherwise.SPC s pdoes the same across the whole project: searches with ripgrep (respecting.gitignoreby default, so build/generated files never show up), groups matches by file, and opens a real, navigable review buffer -- toggle files out withSpace, apply witha/Enterbehind a y/n confirmation. An already-open file is edited in memory (left dirty, for you to review/save); anything else is edited on disk directly. One fresh regex pass per included file, not a snapshot from search time -- a file that changed since the search is safely skipped rather than misapplied. -
Windows, buffers, workspaces: splits (
SPC w v/SPC w s) with each pane keeping its own independent cursor and scroll position, directional navigation, a buffer switcher (SPC b b), and Doom-Emacs-style workspaces (SPC TAB) -- name one (SPC TAB r), jump straight to it by name instead of only cycling (SPC TAB TAB), or define a shelf of them inconfig.iniand open-or-switch-to one on demand (SPC TAB f, see[workspaces]below). Every pane shows a small title bar naming its buffer (the filename, or a placeholder like*dashboard*/*docker*/ a dired buffer's own directory for one with no path) -- with a split open, two different files are labeled at a glance, not just whichever one happens to be focused (the modeline only ever names that one). One editor can also drive several OS windows at once (SPC w n) -- one per monitor, say. They are one process sharing one buffer list, one undo history and one PDF worker, so the same file can be open in both and edits show up in each; only the split layout is per window. Inside theSPC wgroup, lowercase acts on a split and uppercase on the whole OS window.fenix --new-window <file>adds one from a shortcut or the shell instead of handing the file to the window already open. Directional pane navigation ignores the boundary between them: walking off the left edge of the window on one monitor continues into the window on the monitor beside it, landing on whichever of its panes is actually adjacent (its rightmost, coming from that side -- not its first). It is one rule over one list of rectangles in desktop coordinates, so panes in the current window win simply by being nearer, and an open sidebar is reached before the next monitor for the same reason. The windows share one font database and one glyph atlas, so opening a second one doesn't re-scan your system fonts, and each scales text by its own monitor's DPI factor --font_sizeis a logical size, so the same setting looks the same on a 100% screen and a 150% one. The focused pane's title is colored with an accent so it's obvious at a glance which one has focus, whether you're editing, or inside the Docker or Git panel. On the Docker and Git panels specifically, every title is also prefixed with a number (1. Containers,2. Images, ...) -- pressing that digit jumps focus straight to the matching pane. -
Modeline: mode badge, filename, and cursor position on the left; a live local date/time clock flush against the right edge, ticking in place as you work (omitted rather than overlapping anything if the window's too narrow to fit it).
-
Startup dashboard: a real, Vim-navigable buffer listing known projects and recent files, shown when Fenix is launched with no file argument (
SPC o dto reopen it later). -
Docker panel (Lazydocker-style):
SPC d dopens a real, six-pane workspace -- Containers/Images/Volumes/Networks on the left (each its own real, Vim-navigable buffer with a title bar), Status and Logs stacked on the right. Status live-updates to whatever's under the cursor in the focused left pane, including a selected container's CPU/MEM (which also ticks on its own every ~2s without a keypress); Logs is a dedicated pane for streamed log output. Each Containers row is just[X] name, prefixed with a one-letter, color-coded status badge (Rrunning,Ppaused,Xexited, etc.) instead of inline text that used to clip at small font sizes.s/S/Rstart/stop/ restart the container under the cursor,rruns a new container from the image under the cursor,dremoves the container/image/network under the cursor (with ay/nconfirmation),urefreshes.lswitches Logs into a live tail of that container's logs (docker logs -f), streaming new lines in and auto-scrolling to the bottom while you're already there -- scroll up to read earlier output and it leaves you alone until you navigate back to the end. Per-pane keybinding hints no longer clip inline either -- pressxon a Containers/Images/ Volumes/Networks pane for a Lazydocker-style contextual popup listing that pane's available keys; it's purely informational and dismisses on the very next keypress, which still does whatever it would normally do. A long Status value (a long bind-mount path, say) word-wraps onto indented continuation lines rather than running off the pane's right edge; the Containers/Images/Volumes/Networks list rows themselves never wrap, so they stay aligned.SPC d bbuilds an image from the current project root'sDockerfile;SPC d qcloses the whole session. -
Git panel (Lazygit-style):
SPC g gopens a real, seven-pane workspace -- Status/Staged/Unstaged/Branches/Commits/Stash stacked on the left (each its own real, Vim-navigable buffer with a title bar), Main on the right showing a diff of whatever's under the cursor in Staged/Unstaged/Commits/Stash. Main is a real diff viewer, not colored text: every row carries the file, hunk and old/new line numbers it came from, shown in two dim line-number columns beside the verbatim patch line. That's what makes hunk-level work possible --s/Sstage/unstage just the hunk under the cursor (a patch built from that one hunk and piped throughgit apply, so the rest of the file stays exactly as it was),ddiscards it (with confirmation),]/[jump between hunks,Tabfolds a file down to its header, andEnteropens the real file at the line under the cursor. Staged and Unstaged are two independent views of the same file list (a file that's both staged and further modified appears in both, since git tracks the two halves separately) --s/Sstage/unstage the file under the cursor from either pane,a/Astage/unstage everything,ccommits (opening a compose buffer for the message -- a subject, a blank line and a body, which is the convention and which a one-line prompt cannot express),don Unstaged discards the file under the cursor (y/nconfirm, handling untracked files correctly viagit cleanrather thangit checkout),zstashes every change,P/ppush/pull. Status is a fixed repo-overview summary (branch, upstream, ahead/behind, staged/ unstaged/untracked counts) that live-updates on its own every ~2s via a background poller, independent of cursor movement -- the inverse of the Docker panel's Status/Logs split, same pattern, roles swapped to match what's actually true of each domain. A long Status value word- wraps onto indented continuation lines, same as the Docker panel's own Status pane. On Branches:cchecks out the branch under the cursor,ncreates a new one (prompts for a name),ddeletes it (confirm). On Stash:aapplies the entry under the cursor,gpops it,ddrops it (confirm).urefreshes the whole session from any pane;xon Staged/Unstaged/Branches/Commits/ Stash shows a contextual popup of that pane's keys, same dismiss-on- next-keypress convention as Docker's. Real lazygit's own<space>stage-toggle isn't used here --SPCis already Fenix's global leader-key trigger -- so Staged/Unstaged use separates/Skeys instead, matching the Docker panel's owns/S/Rprecedent. Main's diff fetch runs off the input thread (a background thread posts the result back when it lands, discarding a slow one a faster later selection already superseded) so scrolling through many files never blocks the UI waiting on agitsubprocess.SPC g qcloses the whole session. -
History / commit graph (
SPC g l): a real commit DAG across every branch (git log --all), drawn the waygit log --graphdraws it -- two columns per lane, with a connector row below a merge (|\) and above the commit that branches converge on (|/), so the shape of the history is legible instead of crammed onto one line per commit. Each row carries its short hash (padded to a common width, as git abbreviates hashes to differing lengths), the refs pointing at it ((HEAD -> develop),(origin/main), tags), and its subject; moving down the graph shows that commit's author, date, full message and diff in the shared diff viewer -- with its files folded when it touched more than one, so a wide-ranging commit reads as a scannable list of what it touched andTabopens whichever file you want to look at.1/2/3jump straight to the Graph, Refs and Commit panes, andxlists the keys the focused pane understands. Rails are ASCII by default because the box-drawing alternatives are missing from many monospace fonts, and the fallback font's different character width knocks every row out of alignment --[git] graph_style = unicodeopts in if yours has them. Beside it, a refs tree of Local / Remotes / Tags, where every local branch is badged with how it stands against its upstream --[=]in sync,[^2]ahead,[v3]behind,[^2 v3]diverged,[gone]when the remote branch was deleted,[--]when there's no upstream at all -- led by how long ago you last fetched, since every one of those badges is only as current as that.SPC g ffetches (--all --prune, so deleted remote branches actually disappear and[gone]becomes true);urefreshes;SPC g Lcloses. -
Compare refs (
SPC g c): pick any two refs -- branches, remote branches or tags -- and see what one adds over the other: the commits between them beside the full diff, hunk-navigable like every other diff here. Defaults to three-dot (base...head, measured from the merge base: "what does this branch actually do", the same thing a merge request shows), withttoggling two-dot (base..head, every difference between the two trees, including what the base gained meanwhile).rre-targets without closing,urefreshes,SPC g Ccloses. The base picker leads with[git] base_branchfromconfig.ini, falling back to whichever ofmain/masterthe repo actually has, so "how does this differ from the mainline" is two keys and an Enter; each step says which side it's asking for, and the second echoes the base you already chose (master...?). -
Rebase, merge and conflicts:
SPC g rrebases the current branch onto a ref you pick,SPC g mmerges one in,SPC g ppulls with--rebase, andSPC g Fforce-pushes -- always--force-with-lease, never a bare--force, so a rebase-and-push can't silently discard what someone else pushed in between. When one of those stops, the Status pane leads with a banner naming what's suspended and how far it got (REBASING 3/7 -- SPC g R continue, SPC g A abort), and lists the conflicted files as their own section;SPC g RandSPC g Afinish or undo whichever operation it is -- rebase, merge, cherry-pick or revert -- so there's one pair of keys to remember rather than one per operation. In a conflicted file,SPC g jandSPC g kwalk the markers andSPC g o/SPC g t/SPC g bkeep ours, theirs, or both for the conflict under the cursor, markers removed. Anything that rewrites the working tree also re-reads the files you already have open, so a buffer never keeps showing what a file said before the rebase -- saving that stale text would have written it straight back over git's conflict markers. A file with unsaved edits is left alone and counted in a message instead: your own work always outranks the refresh. -
Resolving conflicts (
SPC g x): the conflicted files on the left, the selected one shown as two aligned columns on the right -- your side and the incoming side on the same row, with shared text spanning the full width so it still reads as one file.okeeps the left,tthe right,bboth,n/pwalk the conflicts,sstages the file once nothing is left (and refuses while markers remain, so they can't be committed), anduputs the conflict back if a choice went the wrong way. On the file list,o/ttake a whole file from one side and stage it in one step.The reason this is a view and not just the raw markers: both columns are labelled with the branch they actually came from. During a rebase git replays your commits onto the target, so at every step
HEAD-- what git calls "ours" -- is the branch you're rebasing onto, and "theirs" is your own work. Reading<<<<<<< HEADas "my version" and keeping it deletes the commit you were rebasing. So nothing here says "ours" or "theirs": the columns, the keys, the status banner and the message after each choice all name the branch (kept develop,o = keep myfeature) and spell out its role. Opening a conflicted file directly still works, and its markers are colored with the same two colors the columns use. -
GitLab merge requests (
SPC g M): the project's open merge requests on the left, the selected one in full on the right -- author, source -> target, state, pipeline result, approvals, comment count, description, and the list of changed files.fcycles the filter (all open / mine / assigned to me),Entershows one,urefreshes, andcchecks one out locally: fetched from GitLab's own publishedrefs/merge-requests/N/headon the project's remote, so a request opened from a fork needs no extra remote, and landing onmr-42rather than the source branch's name, which may not exist locally or may mean something else entirely. Each row's badge is colored by what would stop it merging -- failing CI or conflicts read as bad without reading a word of the titles. The two panes split the window evenly, and both wrap to their own real width -- long titles, paths, URLs and descriptions fold rather than running off the edge, with every line of a wrapped row still answering the action keys. Diffs and the merge view's two columns are deliberately never wrapped: a wrapped diff line no longer lines up with its neighbours, which is the whole point of showing it. -
Reviewing a merge request, in the third pane of the same view. The whole diff is reassembled from GitLab's per-file hunks and rendered by the same diff viewer everything else here goes through, so folding (
Tab), hunk navigation (]/[) and "open the real file at this line" (Enter) work without the review knowing anything about them. Review threads are drawn inline, under the line they hang on -- reading a comment about a line anywhere else means holding the line in your head while you look elsewhere for what was said about it. Every row a thread produces answers the same keys, sor(reply) andR(resolve / reopen) work with the cursor anywhere in it.Cstarts a new thread on the diff line under the cursor, anchored to the new side for an added or context line and the old side for a removed one, and quoting the merge request's own base/head/start SHAs so it can't land on a line of a version you weren't looking at -- if those SHAs are missing, commenting is refused rather than posted and lost.Aapproves or withdraws an approval, andmmerges, twice: it's the one action here that can't be taken back, so it arms first and any other key backs out. Comments on the request as a whole (which hang on no line) appear in the detail pane instead, and the forge's own narration ("changed the description") is kept off the diff entirely.Comments are written in a compose buffer: a real scratch buffer in a strip under what you're reading, with ordinary Vim editing and undo, sent with
Enterfrom Normal mode and abandoned withq-- the keys are in the pane title, since a buffer you type prose into has to say how to send it without pressing anything first. A send that fails leaves the draft where it is, which is the difference between retrying and retyping.Verified against a real GitLab, not just a stub:
dev/gitlabbrings up a containerized instance and seeds it with a project, two merge requests and a review thread, andcargo test -p fenix-gitlab --test live -- --ignoredpluscargo test -p fenix-gui live_ -- --ignoredrun the whole integration against it. That is what caught the one bug a stub structurally cannot: a comment on an unchanged line needs bothold_lineandnew_line, and GitLab rejects a position carrying only one of them.The only configuration is
[gitlab] base_urlandtokeninconfig.ini(the instance root, not/api/v4; a personal access token withapiscope). Which project a repo belongs to is read from its ownoriginremote -- SSH,ssh://, or HTTPS -- so one pair of values covers every repo on the instance, and nothing is configured per checkout. Each of the three ways that can fail says which one it was. The client is written against aForgetrait rather than GitLab's JSON, so a second forge would be a second client, not a second panel. -
JIRA dashboard (
SPC j ...): track projects and users by hand (SPC j p a/SPC j u ato add,SPC j p d/SPC j u dto remove), thenSPC j jopens a four-pane workspace -- Projects | Users | Issues | Detail, every pane read-only (like the Docker/Git panels -- it's a generated listing, not something you edit;xon any pane shows its own key list, the same which-key-style popup Docker/Git already have). Moving the cursor onto a tracked user runs a JQL search for every issue assigned to them, scoped to every currently- tracked project, and lists the results in Issues; moving onto an issue shows its full detail (description, status, assignee, reporter, dates, comments) in Detail -- the description and every comment's body word-wrap onto as many lines as they need (paragraph breaks survive wrapping intact), so a long real-world description reads as actual prose instead of one line running off the pane.SPC j rrefreshes,SPC j qcloses the session,SPC j gjumps straight to any issue by key (even one not present in the current query). Talks to a self-hosted Jira Server/ Data Center instance's REST API via a personal access token (see Configuration).SPC j i acreates a new issue in a tracked project (pick the project, type an issue type and a summary). On Issues or Detail:ttransitions the issue's status (fetches the real available transitions for its current workflow state and offers a picker -- never a fixed list),Tedits the title,Areassigns to one of your tracked users,Pchanges priority (fetched live from the instance's real configured scheme, not a guessed list),llogs time (Jira's own duration syntax, e.g.2h 30m),ycopies the issue's browse URL to the clipboard, andc/eopen a real, Vim- navigable scratch buffer -- empty for a new comment, pre-filled with the current text for a description edit -- so you get full editing power for anything longer than one line;SPC j ssubmits it,SPC j xdiscards it. On Issues,fopens a multi-select picker over every status seen so far this session (Tabtoggles a status on/off without closing the picker,Enterapplies -- an empty selection means "show everything") to hide statuses you don't want cluttering the list (e.g. Done/Closed) -- folded into the JQL query itself, so it stays applied across refreshes; resets when you close and reopen the panel. -
VNC console panes (
SPC v ...): configure VM hosts by hand under[vnc], thenSPC v vfuzzy-picks one by name to open (or switch back to) a live VNC connection as an ordinary, splittable pane. Each session connects once and stays live in the background indefinitely (instant switching thereafter), auto-reconnects with backoff if it drops, and throttles its poll rate while unfocused. Mouse and keyboard forward straight to the VM while the pane is focused (Ctrl-\to release, same convention as the terminal panel); clipboard is mirrored both ways.SPC v ssaves the current frame as a PNG. Remote resizing: resizing the pane asks the VM to match its own resolution, falling back to client-side scaling when the server doesn't support that or declines a particular size. No encryption or authentication at all -- trusted-network hosts only. -
PDF viewer (
SPC r ...): open a.pdffile the same way you'd open any other file (typed path, the explorer, a CLI argument) and it renders as a scaled-to-fit page in an ordinary, splittable pane instead of loading as text. The mouse wheel,j/kand the arrow keys scroll the document continuously -- straight through page boundaries, so a scroll never dead-ends at the bottom of a page -- andPageDown/PageUp,n/p,Home/Endturn/jump pages outright, all as bare single keystrokes while a PDF pane is focused.SPC r gjumps straight to a typed page number;+/-/0/w(orSPC r =/SPC r -/SPC r 0/SPC r w) zoom in/out and fit the page/width, withh/lpanning sideways across whatever doesn't fit in the pane at the current zoom. The status line showsPage N/Mand the current zoom in place of the line/column an ordinary buffer shows. The render re-fits automatically on window resize (except at a fixed percentage zoom, which stays put across a resize on purpose).SPC r otoggles a split-pane outline/bookmarks panel -- a real, Vim-navigable listing whereEnteron an entry jumps the PDF straight to its page.SPC r /searches the whole document for a word or phrase and lists every match (page number plus surrounding context) in its own split pane,Enterjumping straight to that match's page the same way the outline does. Requirespdfium.dll(see Optional external tools) -- without it, opening a PDF shows an error instead of a blank pane. -
Autocompletion: a popup that's always available, sourced from whatever's already been typed in the current buffer (
<C-n>/<C-p>- style buffer-word completion, any language) -- layered, for Tcl specifically, with a built-in keyword list, Universal Ctags-scanned project definitions, and an optional external symbols file (see Configuration). Namespaced procs show their fully- qualified path (myns::subns::proc, no leading::), not just the bare proc name. -
Language servers (LSP): a real
lsp-types/JSON-RPC client, spawned per-language on demand for whichever buffer you open, with a built-in default command for Python (pyright'spyright-langserver), Rust (rust-analyzer), C/C++ (clangd), Bash (bash-language-server), and JavaScript/TypeScript/TSX (typescript-language-server) -- anything else (or an override for one of these) via a[lsp]command you configure -- see Configuration. Live diagnostics (inline severity-colored markup, modeline error/warning counts),gdgo-to-definition,grfind-references (populates the quickfix list --SPC p n/SPC p Nsteps through it the same way a project grep does),Khover,SPC c rrename,SPC c acode actions, andSPC c f/SPC c Freach for the server's own formatter before falling back to the structural reindenter below. Completion candidates from the server merge straight into the same popup autocompletion already opens (CompletionKind::Lsp), rather than being a separate mechanism. Every one of these degrades gracefully to "not available" (not a hard error) when no server is attached, or the attached one doesn't advertise that capability. On Windows, a server installed as an npm-style.cmdshim (typescript-language-server,bash-language-server, and most other JS-ecosystem tooling) launches correctly despitePATHresolution quirks Rust's own process-spawning otherwise has no answer for. -
Build/task runner:
SPC p tfuzzy-picks a task discovered from the focused buffer's project root -- built-in defaults for whichever ecosystem markers are present (cargo build/test/clippyfor aCargo.toml,pytest/ruff checkfor apyproject.toml, CMake configure/build/ctestfor aCMakeLists.txt,npm run build/testfor apackage.json-- every matching ecosystem contributes its own set, not just the first one found), plus any project-local overrides from.fenix/project.ini's[tasks]section (taskN = NAME|COMMAND, same numbered-key conventionconfig.ini's own lists already use). Runs in a live-streamed single-pane Task Output panel (SPC p Treruns the last task,SPC p kends it early);cargo build/test/clippyspecifically run with--message-format=jsonso each diagnostic's recovered file/line/column feeds the same quickfix list a project grep or LSP references already populate (SPC p n/SPC p Nsteps through them), while the panel itself still shows cargo's own human-readable output (itsrenderedfield), not raw JSON -- every other tool's plainfile:line:col: messageconvention (gcc/clang/pytest/ctest) is recovered the same way without needing--message-format=jsonat all. Task cancellation terminates the process tree on Windows, including children holding output pipes. Closing or rerunning a task never waits for reader threads. Windows also cleans up tasks when the editor exits abruptly. Output from both streams is drained before the result appears; stale events from a previous run are ignored. Cleanup has a two-second deadline and reports errors rather than leaving the interface waiting indefinitely. A task's descendants are terminated when its main process exits, so use a separate terminal for background services intended to outlive a build. -
Debugger (DAP):
SPC u ustarts a real Debug Adapter Protocol session for the focused buffer (Python viadebugpy'spython -m debugpy.adapter--pip install debugpyis the only setup needed; other languages have no built-in adapter yet, same "detect + guide, no wrong guesses" posturelsp::default_server_commandalready has), or continues one that's stopped at a breakpoint.SPC u btoggles a breakpoint on the current line (persists across buffers/sessions, resent live if a session is already running);SPC u n/SPC u i/SPC u ostep over/into/out;SPC u wwatches the identifier before the cursor;SPC u qends the session. A four-pane Call Stack/Variables/Watches/Breakpoints panel (mirroring the Docker panel's own multi-pane shape) updates on every stop, and stopping moves the cursor straight to the current line -- opening the file in a fresh split if it wasn't already showing somewhere, without ever displacing whatever the debug panel's own panes are showing. A project's own launch target -- required for anything that isn't "the script I have open" -- comes from.fenix/project.ini's[launch]section (program/args, the same per-project config file[tasks]above already introduced). -
Tool status:
SPC l mopens a single-pane listing of every language with a built-in LSP server or DAP adapter -- the exact command that would be launched (a[lsp]override if configured, else the built-in default), whether it's found onPATH, whether a session for it is running right now, and, for anything missing, the one-line command that installs it (rustup component add rust-analyzer,uv tool install pyright,npm install -g typescript-language-server typescript, ...). Detect and guide only -- Fenix never downloads or manages a tool binary itself, the same posture the debugger bullet above already takes for adapters it doesn't have. -
Symbol picker:
SPC c sopens a fuzzy-find popup listing every known Tcl definition (proc/namespace) by its fully-qualified name, sourced from the same Universal Ctags scan autocompletion draws on -- confirming a selection opens the file it's defined in (if not already open) and jumps straight to that line. -
Indent region:
SPC c freindents the active Visual selection,SPC c Fthe whole buffer -- a language server's own formatter first, if one's attached and advertises formatting support (see the LSP bullet above), falling back to a structural reindent from{/(/[nesting depth (Emacs' ownindent-region, real Vim's=operator) otherwise, not by shelling out to a per-language external tool. Works on any buffer regardless of detected language; when one is detected, a fresh syntax parse excludes every string/comment span from the bracket scan so a stray brace inside a string or comment can't throw off the result.SPCreaches the leader menu from Visual mode as well as Normal for this reason, soSPC c fcan act on a selection without leaving it first. -
SCOS-2000 MIB (
SPC m ...): fuzzy-find and inspect telecommands (SPC m t), TM packets (SPC m k), TM parameters (SPC m p), and calibration definitions (SPC m c, numeric curves/status enumerations/range checks) from one or more configured MIB directories (see Configuration) -- each opens a real, Vim- navigable buffer with the definition's summary, related rows (a telecommand's parameters with their calibration references, a TM packet's parameters, a TM parameter's packet occurrences), and raw fields.SPC m ibuilds and inserts a telecommand: pick one, build or skip its variable arguments (an argument with known engineering aliases offers a picker of them; one with a known numeric range warns, without blocking, if the typed value falls outside it), review the rendered command, confirm to insert at wherever the wizard started.SPC m rreparses the configured MIB directories from disk.SPC m aregisters a new MIB directory without leaving the editor: browse to it in the file explorer,Sto select it, then type a label -- persisted toconfig.iniimmediately, same as everything else here.SPC m dfuzzy-finds a configured directory to remove the same way. Ported from an ICD 7.2 SCOS-2000 MIB workflow in the author's previous (Emacs) config -- see that config's own MIB module for the original. -
Themes:
Orbit Dark,TempleOS,Gruvbox Dark,Nord,Dracula,Solarized Dark, andOne Dark, jumped to directly by name with a fuzzy picker (SPC t p), persisted. Two rules hold across all of them, and are pinned by tests: comments sit closer to the background than body text does, so they read as quieter rather than as more code; and operators and punctuation share an accent that is neither the body-text color nor the comment color, so the structure of a line is visible. Both used to be violated -- punctuation was body-colored (invisible as a distinct thing) or comment-colored (actively de-emphasized) depending on the theme, which is most of why a brace-and-bracket language like Tcl looked flat. -
Terminals, in two shapes, because they answer two different questions.
SPC o tis the popup panel: one shell for the whole application, a full-width strip along the bottom of the window under every existing split, that drops down over whatever you were looking at and follows you from workspace to workspace -- for the build you start, glance at, and dismiss.SPC o Tputs a shell in the focused pane instead: it lives in that pane, in that workspace, beside the code it goes with, splits and resizes like any other pane, and is still there when you switch away and back. Open as many as you like (*terminal 1*,*terminal 2*, ... inSPC b b); numbers are never reused, since a recycled one would point at a different shell than the one you remembered.Both run
powershell.exeon Windows,$SHELL(falling back to/bin/sh) elsewhere, with full ANSI color support (16-color, 256-color, and RGB foreground/background). Neither is killed by looking away: hiding the panel, or pointing a pane at another buffer, leaves the shell and anything running in it going, and coming back shows it caught up to wherever it got to. Closing a terminal buffer (:q,SPC b k) is the one gesture that ends its shell -- that is the point at which nothing could show it again.Ctrl-\hands the keyboard back to the editor without closing or hiding anything (Neovim's own:terminalconvention), soSPC w ...can move focus elsewhere while the shell stays on screen;SPC o T(or a click into the pane) takes it back, and runningexithands it back on its own. A pane terminal only receives what you type while its own pane is focused, so a keystroke meant for the file next door never lands in a shell.A terminal buffer holds no text of its own -- what you see is the shell's screen grid, rendered directly -- so there is nothing there for
:wto write to a file and nothing Vim's editing commands can corrupt.v1 limitations: no mouse reporting, no bracketed paste, no F-keys, no application-cursor-mode variants -- covers ordinary shell/REPL/pager use, not a full terminfo-correct implementation. Terminal queries are answered, though (cursor position, device attributes), which is not optional on Windows: ConPTY opens every session by asking where the cursor is and produces no further output at all until it is told -- an emulator that only listens sits looking at a live shell and an empty screen forever.
-
Table/spreadsheet view:
SPC f ttoggles the focused buffer between plain text and an elastic-column table view of its own, genuinely tab-separated content -- real elastic tabstops, not a padding trick: the renderer expands each real\tto the visual column its column needs (computed from the widest value currently in it, re-measured after every edit), so the file on disk stays exactly what you see, always genuinely tab-separated, and ordinary Vim editing (i,cw, ...) between two tabs just works.]/[jump to the next/previous column,cfuzzy-finds one by name, andj/kare reinterpreted to move a row while staying in the same visual column -- plain char-based motion doesn't track "same column" once rows have different raw lengths up to it. Built for browsing MIB.datfiles and any other TSV data, but general-purpose.
Requires a recent stable Rust toolchain (edition 2021).
cargo build --releaseThe binary is target/release/fenix. To run without building a
release binary first:
cargo run -p fenix-gui # opens the startup dashboard
cargo run -p fenix-gui -- path/to/fileSome features shell out to standard tools if they're present on PATH,
and degrade gracefully (never a hard error) if they're not:
ripgrep(rg) — project-wide search (SPC p s).git— git-status badges in the file explorer.Universal Ctags(ctags) — project-definition completion for Tcl (SPC c s,SPC c T). If it's missing, exits non-zero, or produces output this parser doesn't recognize, the reason is logged to stderr rather than just silently yielding no definitions — check the terminal Fenix was launched from.dockerorpodman— the Docker panel (SPC d d). Fenix probesdockerfirst and falls back topodmanifdockerisn't runnable (auto-detected once per run) — so a plain Podman install works with no configuration, and apodman-dockercompatibility shim (wheredockeritself resolves to Podman) works too, indistinguishably. With neither onPATH(or an unreachable daemon) the panel just shows an empty listing instead of failing.
The PDF viewer (SPC r ...) needs a native library rather than a
PATH executable, so it's set up once by hand rather than
auto-detected:
pdfium— download the prebuilt release for your platform (pdfium-win-x64.tgzon Windows) and placepdfium.dll(orlibpdfium.so/libpdfium.dylibelsewhere) next tofenix.exe, i.e. in whichevertarget/debug/ortarget/release/directory you actually run the built binary from.FENIX_PDFIUM_PATHcan point at a different directory instead (handy for switching betweendebug/releasebuilds without copying it twice), and a system-wide install is tried as a last resort. Without it, opening a.pdfshows a status-line error naming where it looked rather than a blank pane or a crash.
cargo test --workspaceFenix follows real Vim for editing and a Doom-Emacs-style SPC leader
for everything else. SPC starts a leader sequence from Normal mode; a
popup shows what keys continue it.
| Keys | Action |
|---|---|
SPC SPC |
Find file in project (same as SPC p f) |
SPC f s |
Save |
:w! / :e! |
Save over a file that changed on disk / re-read it, discarding your edits |
SPC f v |
Recover unsaved work a previous session left behind |
SPC f j |
Open the file explorer at the current file's directory |
SPC f e |
Open the file explorer at your home directory; open a file directly, or S fuzzy-searches recursively from wherever you navigate to |
SPC f t |
Toggle the focused buffer between plain text and table view |
SPC f f |
Open a file by typing its path (bypasses .gitignore) |
SPC f a |
Fuzzy-find a file in the project, including gitignored ones |
SPC f r |
Fuzzy-find a recently-opened file |
SPC f R |
Rename the current file on disk |
SPC f D |
Delete the current file (with confirmation) |
SPC f y |
Copy the current file's path to the clipboard |
SPC q q |
Quit |
SPC t n |
Cycle line numbers (off / absolute / relative) |
SPC t p |
Pick a theme by name (fuzzy picker) |
SPC t = / SPC t - / SPC t 0 |
Font size: increase / decrease / reset |
SPC t f |
Toggle fullscreen |
SPC t a |
Toggle caret-fade/scroll-ease/yank-pulse animations on/off |
SPC e e |
Open the file explorer here |
SPC e d |
Two listings side by side (copy/move default to the other one) |
SPC e o / SPC e O |
Open with the system's default app / show it in Explorer |
SPC e y |
Copy the full path |
SPC e T |
Open a shell in this directory |
SPC e g |
Search this directory |
SPC e G |
Make this the project and open the Git panel |
SPC e k |
Stop the running file operation |
SPC e w |
Edit the listing's names as text |
SPC e W |
Apply the edited names |
SPC e p |
Go to a path you type (Tab completes; ~, %VAR% and \server\share all work) |
SPC e b |
Places: bookmarks, drives, recent directories, project roots |
SPC e r |
Recent directories |
SPC e m |
Bookmark the directory you are in |
SPC e t |
Toggle the file explorer sidebar |
SPC p f |
Find file in project |
SPC p s |
Search project (ripgrep) |
SPC p n / SPC p N |
Next / previous match in the last project search (quickfix) |
SPC p p |
Switch project |
SPC p a / SPC p d |
Add / remove a project from the known-projects list |
SPC p t |
Fuzzy-pick and run a discovered project task in the Task Output panel |
SPC p T |
Rerun the most recently run task |
SPC p k |
End the currently running task |
SPC u u |
Start a debug session, or continue one that's stopped |
SPC u b |
Toggle a breakpoint on the focused buffer's current line |
SPC u n / SPC u i / SPC u o |
Step over / into / out |
SPC u w |
Watch the identifier before the cursor |
SPC u q |
End the running debug session |
SPC l m |
Show LSP/DAP tool status (found on PATH, running, install hints) |
SPC s s |
Fuzzy-find a line in the current buffer |
SPC s r |
Search and replace in the current buffer (Visual-scoped if invoked from Visual mode) |
SPC s p |
Search and replace across the project |
SPC o d |
Open the startup dashboard |
SPC o t |
Toggle the terminal panel |
SPC o T |
Open a shell in the focused pane |
SPC d d |
Open (or refocus/refresh) the Docker panel |
SPC d b |
Build an image from the current project's Dockerfile |
SPC d q |
Close the Docker panel session |
SPC g g |
Open (or refocus/refresh) the Git panel |
SPC g q |
Close the Git panel session |
SPC g l |
Open the History view (commit graph, refs, commit diff) |
SPC g L |
Close the History view |
SPC g f |
Fetch all remotes and prune deleted branches |
SPC g c |
Compare two refs (pick base, then head) |
SPC g C |
Close the Compare view |
SPC g r / SPC g m |
Rebase onto / merge in a ref you pick |
SPC g p / SPC g F |
Pull with --rebase / push --force-with-lease |
SPC g R / SPC g A |
Continue / abort the suspended operation |
SPC g j / SPC g k |
Next / previous conflict in the focused file |
SPC g o / SPC g t / SPC g b |
Keep ours / theirs / both for the conflict under the cursor |
SPC g x / SPC g X |
Open / close the Merge view (conflicts side by side) |
SPC g M / SPC g Q |
Open / close the GitLab Merge Requests view |
1 / 2 (Merge Requests) |
Jump to the list / detail pane |
Enter / f / c / u (Merge Requests) |
Show this one / cycle filter / check it out locally / refresh |
1 / 2 / 3 (Merge Requests) |
Jump to the list / detail / review pane |
r / R / C (Review pane) |
Reply to this thread / resolve or reopen it / comment on this line |
A / m (Merge Requests) |
Approve or withdraw / merge (press twice) |
Enter / q (Compose) |
Send what's written / discard it (also used for commit messages) |
SPC g s |
Stage the selected conflicted file as resolved |
1 / 2 (Merge) |
Jump to the Conflicts / Merge pane |
Enter / o / t (Merge files) |
Resolve line by line / take the whole file from the left / from the right |
n / p / u (Merge) |
Next / previous conflict / put the conflict back |
1 / 2 / 3 (History) |
Jump to the Graph / Refs / Commit pane |
u / f (History) |
Refresh / fetch |
1 / 2 (Compare) |
Jump to the Commits / Changes pane |
t / r / u (Compare) |
Toggle three-dot vs two-dot / re-target refs / refresh |
x (any Git view) |
Show the keys the focused pane understands |
s / S (working-tree diff) |
Stage / unstage the hunk under the cursor |
d (working-tree diff) |
Discard the hunk under the cursor (confirms first) |
] / [ (any diff) |
Next / previous hunk |
Tab (any diff) |
Fold the file under the cursor down to its header |
Enter (any diff) |
Open the real file at the line under the cursor |
SPC j j |
Open (or refocus) the JIRA dashboard |
SPC j p a / SPC j p d |
Add / remove a tracked JIRA project |
SPC j u a / SPC j u d |
Add / remove a tracked JIRA user |
SPC j i a |
Create a new issue in a tracked project |
SPC j g |
Jump straight to any issue by key |
SPC j r |
Refresh the JIRA dashboard's current issues/detail |
SPC j q |
Close the JIRA dashboard session |
SPC j s / SPC j x |
Submit / cancel a pending comment or description edit |
SPC v v |
Open (or switch to) a configured VNC session by name |
SPC v q |
Close the focused VNC session |
SPC v s |
Save the focused VNC session's current frame as a PNG |
SPC r n |
Turn the focused PDF session to the next page |
SPC r p |
Turn the focused PDF session to the previous page |
SPC r g |
Prompt for a page number and jump to it |
SPC r [ / SPC r ] |
Jump to the first / last page |
SPC r = / SPC r - |
Zoom the focused PDF session in / out |
SPC r f |
Open a document from the config.ini [documents] index |
SPC r 0 |
Fit the page to the pane |
SPC r w |
Fit the page's width to the pane |
SPC r o |
Toggle the focused PDF session's outline/bookmarks panel |
SPC r / |
Search the focused PDF session's text for a word or phrase |
wheel, j / k, Down / Up |
Scroll the document, continuing onto the next/previous page at an edge (PDF panes only) |
PageDown / PageUp, n / p |
Next / previous page (PDF panes only) |
Home / End, g / G |
First / last page (PDF panes only) |
h / l, Left / Right |
Pan sideways while the page is wider than the pane (PDF panes only) |
+ / - / 0 / w / / |
Zoom in / out, fit page, fit width, search (PDF panes only) |
gd |
Go to definition (LSP) |
gr |
Find references (LSP) -- populates the quickfix list, SPC p n / SPC p N to step through |
K |
Show hover information for the symbol under the cursor (LSP) |
SPC c r |
Rename the symbol under the cursor across the project (LSP) |
SPC c a |
Choose a code action and preview its edits (LSP) |
SPC c T |
Refresh completion tags (re-scans with ctags, re-reads the symbols file) |
SPC c f |
Indent region -- reindent the active Visual selection structurally, or (with an attached language server) reformat the whole document (LSP) |
SPC c F |
Indent region -- reindent the whole focused buffer structurally, or (with an attached language server) reformat it (LSP) |
SPC c s |
Fuzzy-find a Tcl symbol by its fully-qualified name and jump to its definition |
SPC c x |
Toggle the GFM task checkbox (- [ ]/- [x]) on the current line |
SPC c o |
Fuzzy-find a Markdown heading and jump to it |
SPC m i |
Build and insert a telecommand from the MIB |
SPC m t |
Fuzzy-find a MIB telecommand and view its details |
SPC m k |
Fuzzy-find a MIB TM packet and view its details |
SPC m p |
Fuzzy-find a MIB TM parameter and view its details |
SPC m c |
Fuzzy-find a MIB calibration definition and view its details |
SPC m r |
Reparse the configured MIB directories from disk |
SPC m a |
Browse to and register a new MIB root directory |
SPC m d |
Fuzzy-find and remove a configured MIB root |
SPC w v / SPC w s |
Split window vertically / horizontally |
SPC w h/j/k/l |
Move focus between windows -- and across OS windows, by where they sit on the desktop |
SPC w w |
Cycle to the next window |
SPC w q / SPC w o / SPC w = |
Close window / close all others / balance splits |
SPC w n |
Open another OS window, on the next monitor with no Fenix window on it |
SPC w W |
Cycle to the next OS window |
SPC w Q / SPC w O |
Close this OS window / close all the others |
SPC b b |
Switch buffer |
SPC b n / SPC b p |
Next / previous buffer |
SPC b k |
Kill (close) the focused buffer |
SPC b X |
New scratch buffer |
SPC TAB n |
New workspace |
SPC TAB ] / SPC TAB [ |
Next / previous workspace |
SPC TAB d |
Remove the active workspace |
SPC TAB TAB |
Switch to an open workspace by name |
SPC TAB f |
Open a workspace from config.ini's [workspaces] shelf -- switches to it if already open, otherwise creates it |
SPC TAB r |
Rename the active workspace |
| Keys | Action |
|---|---|
j / k |
Move down / up |
l / Enter |
Open |
h / - |
Go to parent directory |
Tab |
Expand / collapse a directory |
m / u / U / t |
Mark / unmark / unmark all / toggle all marks |
D |
Delete (marked, or entry under cursor) |
R |
Rename |
c / + |
Create file / directory |
C / M |
Copy / move to... |
. |
Toggle hidden files |
o / O |
Cycle sort key (name/size/date/type) / reverse it |
f / F |
Filter the listing / find by name under here |
r / g r |
Refresh |
S |
Select this directory (when picking a project root) |
q / Esc |
Quit |
A real buffer, so every ordinary Vim motion works (j k gg G / n N ...)
and the cursor is the selection. The keys below are the same table the
sidebar reads -- only j/k differ, because here they are the cursor.
| Keys | Action |
|---|---|
Enter / l |
Open the file, or navigate into the directory, at point |
- |
Go to the parent directory |
Tab |
Expand / collapse a directory in place |
m / u / U / t |
Mark / unmark / unmark all / toggle all marks |
D |
Delete to the Recycle Bin (marked, or entry under cursor) |
R |
Rename |
c / + |
Create file / directory (either may include /, and missing parents are created) |
C / M |
Copy / move to... |
. |
Toggle hidden files |
o / O |
Cycle sort key (name/size/date/type) / reverse it |
z / x |
Pack the marked set into an archive / unpack the one at point |
i |
Properties (and, on a folder, count what is inside) |
w |
Toggle read-only |
f |
Filter the listing as you type (Esc widens it back out) |
F |
Find by name through everything under here |
r |
Refresh |
Esc |
Stop waiting for a directory that isn't answering |
Operations act on the marked set if there is one, and on the row under the cursor otherwise -- dired's own convention.
Toggles the focused buffer in place -- same file, same undo history,
just rendered with elastic-column alignment instead of plain text.
Every ordinary Vim motion and edit works (j/k and c are
reinterpreted, everything else is unchanged):
| Keys | Action |
|---|---|
] / [ |
Jump to the start of the next / previous column |
j / k |
Move a row, staying in the same visual column |
c |
Fuzzy-find a column by name and jump to it |
A real, Vim-navigable buffer listing every file a pending project-wide
replace would touch, one row each (j k gg G / n N ... all work):
| Keys | Action |
|---|---|
Space / t |
Toggle the file under the cursor in/out of the replace |
a / Enter |
Arm the apply confirmation (y/n), or apply if already armed |
q / Esc |
Cancel -- closes the buffer, writes nothing |
Opens its own workspace with six real, titled panes -- Containers,
Images, Volumes, and Networks stacked on the left, Status and Logs
stacked on the right. Each is an ordinary Vim-navigable buffer (j k gg G / n N ... all work); moving the cursor in a left pane live-updates
Status with that row's info. A Containers row is just a color-coded
status badge plus the container's name ([R] green for running, [P]
yellow for paused, [X] red for exited, etc.) -- press x on a pane for
a which-key-style popup of its available keys instead. Each title bar is
numbered (1. Containers, 2. Images, ...) and the focused one is
shown in an accent color -- pressing that digit jumps straight to it.
Only these are special, and only on the pane named:
| Keys | Pane | Action |
|---|---|---|
1-6 |
any | Jump to the pane numbered that in its title bar |
s |
Containers | Start the container under the cursor |
S |
Containers | Stop the container under the cursor |
R |
Containers | Restart the container under the cursor |
l |
Containers | Stream that container's logs live into the Logs pane (docker logs -f) |
r |
Images | Run a new detached container from the image under the cursor |
d |
Containers, Images, Networks | Remove the entry under the cursor (y/n to confirm) |
u |
any | Refresh the whole session |
x |
Containers, Images, Volumes, Networks | Show this pane's available keys |
Opens its own workspace with six real, titled panes -- Status, Files,
Branches, Commits, and Stash stacked on the left, Main on the right.
Each is an ordinary Vim-navigable buffer (j k gg G / n N ... all
work). Moving the cursor in Files, Commits, or Stash re-syncs Main to
that row's diff; Status doesn't follow the cursor -- it's a fixed
repo-overview summary that live-updates on its own every ~2s
regardless of where the cursor is. Each title bar is numbered (1. Status, 2. Files, ...) and the focused one is shown in an accent
color -- pressing that digit jumps straight to it. Only these are
special, and only on the pane named:
Files is a collapsible directory tree, not a flat list -- changed
paths are grouped by directory (> src/ collapsed, v src/ expanded),
so Tab on a directory reveals or hides its files, and every action
key (s/S/d) works on a directory the same way it works on a
single file: stage, unstage, or discard everything underneath it in
one keypress. Discarding a directory runs both git checkout -- (for
tracked changes) and git clean -fd -- (for untracked files) under it,
since a real directory routinely holds a mix of both at once. Every
directory starts collapsed; expansion state persists across u
refreshes within the session.
| Keys | Pane | Action |
|---|---|---|
1-6 |
any | Jump to the pane numbered that in its title bar |
Tab |
Files | Expand/collapse the directory under the cursor |
s |
Files | Stage the file (or every file under the directory) under the cursor |
S |
Files | Unstage the file (or directory) under the cursor |
a |
Files | Stage every changed file |
A |
Files | Unstage every staged file |
c |
Files | Commit (prompts for a message) |
d |
Files | Discard the file (or directory) under the cursor (y/n to confirm) |
z |
Files | Stash every change |
P / p |
Files | Push / pull |
c |
Branches | Checkout the branch under the cursor |
n |
Branches | New branch (prompts for a name) |
d |
Branches | Delete the branch under the cursor (y/n to confirm) |
a |
Stash | Apply the entry under the cursor |
g |
Stash | Pop the entry under the cursor |
d |
Stash | Drop the entry under the cursor (y/n to confirm) |
u |
any | Refresh the whole session |
x |
Files, Branches, Commits, Stash | Show this pane's available keys |
Real lazygit's own <space> stage-toggle isn't used here, since SPC
is already Fenix's global leader-key trigger -- Files uses separate
s/S keys instead, the same distinct-keys-per-action convention the
Docker panel's own s/S/R already established.
Opens its own workspace with four real, titled panes -- Projects and
Users stacked on the left, Issues (the main pane) and Detail on the
right. Each is an ordinary Vim-navigable buffer (j k gg G / n N ...
all work) but genuinely read-only, like the Docker/Git panels -- it's
a generated listing, not something you edit; any edit that slips
through is silently reverted. Moving the cursor onto a tracked user
re-runs the query behind Issues; moving onto an issue re-fetches
Detail. Only these are special, and only on the pane named:
| Keys | Pane | Action |
|---|---|---|
1-4 |
any | Jump to the pane numbered that in its title bar |
t |
Issues, Detail | Transition the issue's status (fetches the real available transitions, offers a picker) |
T |
Issues, Detail | Edit the title |
A |
Issues, Detail | Reassign to one of your tracked users (picker) |
P |
Issues, Detail | Change priority (picker, fetched live from the instance's real configured scheme) |
c |
Issues, Detail | Add a comment (opens a real scratch buffer) |
e |
Issues, Detail | Edit the description (opens a real scratch buffer, pre-filled) |
l |
Issues, Detail | Log time (Jira's own duration syntax, e.g. 2h 30m) |
y |
Issues, Detail | Copy the issue's browse URL to the clipboard |
f |
Issues | Open the multi-select status filter |
x |
Issues, Detail | Show this pane's available keys |
c/e hand you a genuine, full-featured buffer -- write as much as
you want, over as many lines as you want, with every ordinary Vim
motion and edit available. SPC j s submits it (posts the comment, or
saves the new description) and restores Detail to its normal view;
SPC j x discards it the same way, without submitting anything. These
are leader bindings rather than pane-scoped bare keys on purpose: real
prose routinely contains the letters c/e/t/T/l, and a bare-key
trigger would hijack ordinary typing the moment it did.
f opens a multi-select picker over every status Issues has shown at
least once this session -- Tab toggles the entry under the cursor
on/off without closing the picker (any already-excluded statuses show
up pre-checked), Enter applies whatever's checked (nothing checked
means "show everything," the normal way to clear the filter), Esc
cancels without changing anything. The filter is folded directly into
the JQL query (AND status NOT IN (...)), so it stays applied across
SPC j r refreshes; it resets when the panel is closed and reopened,
and isn't saved to config.ini.
Embeds a live VNC (RFB) connection to a VM as an ordinary, splittable
pane -- configure hosts once under [vnc] (see Configuration below),
then SPC v v fuzzy-picks one by name to open or switch straight to
it. Each session connects the first time you pick it and then stays
live in the background indefinitely, so switching back later is
instant, not a fresh handshake; an unfocused/hidden session polls at a
much slower rate to stay cheap while you're not looking at it, and a
dropped connection retries automatically with backoff before giving up
and leaving the pane on its last frame.
| Keys | Action |
|---|---|
SPC v v |
Open or switch to a configured VNC session (picker by name) |
SPC v q |
Close the focused VNC session |
SPC v s |
Save the focused session's current frame as a timestamped PNG |
Ctrl-\ |
Release keyboard capture back to the editor (same chord as the terminal panel) |
Clicking into a VNC pane both focuses it and starts sending your mouse there; every other key while it's focused is forwarded to the VM instead of Vim, exactly like the terminal panel. Clipboard content is mirrored in both directions: the VM's clipboard always flows to yours as it changes, and yours flows to the VM whenever you focus a session.
Resizing a VNC pane asks the VM to match its own resolution to the pane's, the same "remote resizing" real VNC clients offer, so the picture renders at native size instead of a stretched/letterboxed scale. Whether that request actually lands depends on the server: it has to advertise support for it in the first place (confirmed automatically per-connection, nothing to configure), and even then can still decline a specific size (administrative policy, an unsupported resolution, ...). Either way, the video keeps scaling to fit the pane as a fallback -- a pane and a VM sitting at different resolutions is never broken, just not pixel-native.
The connection is always made in the clear -- there's no encryption or authentication support at all, matching the assumption that every configured host is on a trusted local network. Don't point this at anything reachable over an untrusted network without your own tunnel (SSH port-forwarding, a VPN) in front of it.
Opening a .pdf -- by typed path (SPC f f), the explorer, a recent
file, or a CLI argument -- renders it as a scaled-to-fit page in an
ordinary, splittable pane instead of loading its raw bytes as text.
Rendering happens on one shared background worker (every open PDF
shares it), so opening a document never blocks the editor and several
can be open at once.
Reading is done with bare single keystrokes while the PDF pane is
focused -- a three-key leader chord per page is not a page-turn gesture
anyone would use to read a 50-page document. The SPC r ... bindings all
still work (and are what the which-key menu discovers); they're the same
commands, just reachable in one keystroke here.
| Keys | Action |
|---|---|
mouse wheel, j / k, Down / Up |
Scroll the page; at the bottom/top edge, continue onto the next/previous page |
PageDown / PageUp, n / p, SPC r n / SPC r p |
Next / previous page |
Home / End, g / G, SPC r [ / SPC r ] |
First / last page |
SPC r g |
Prompt for a page number and jump to it |
+ / -, SPC r = / SPC r - |
Zoom in / out, in coarse 10% steps |
0, SPC r 0 |
Fit the whole page to the pane (the default) |
w, SPC r w |
Fit the page's width to the pane -- a tall page then scrolls vertically instead of shrinking further |
h / l, Left / Right |
Pan sideways once the page is wider than the pane |
SPC r o |
Toggle the outline/bookmarks panel |
/, SPC r / |
Search the document's text |
SPC r f opens a fuzzy picker over a document index you define by
hand in config.ini:
[documents]
doc1 = Space Packet Protocol|C:\refs\133x0b2e2.pdf
doc2 = Time Code Formats|C:\refs\301x0b4.pdfEach entry is a display name and a path. The picker lists and
fuzzy-matches the names, so a reference you open constantly is two
keystrokes and a few characters away rather than a path to go hunting
for. Confirming opens that document in the focused pane, replacing
whatever it was showing -- unlike every other way of opening a PDF
(SPC f f, the explorer, a CLI argument), which gives the document its
own workspace. Picking a reference off a shelf means "show it to me
here", and if the pane already held a different PDF, that one (and its
outline/search companion panes) is retired first. An entry can point at
any file Fenix opens, not just a PDF -- a Markdown or plain-text
reference opens as ordinary editable text. A path that has since moved
is reported by name instead of opening an empty buffer, and an empty or
missing [documents] section says so rather than opening a picker over
nothing.
Scrolling is continuous across page boundaries in both directions: scrolling past the bottom of a page turns to the next one at its top, and scrolling back up past the top turns to the previous one at its bottom, so scrolling back retraces exactly what scrolling forward covered. Under the default fit-page zoom there is never anything to scroll within a page, so every scroll gesture simply turns the page.
The status line shows Page N/M and the current zoom (Fit page,
Fit width, or a percentage) where an ordinary buffer shows Ln/Col
-- a PDF pane has no text and no cursor, so a line/column there would be
meaningless.
The outline panel (SPC r o) opens as a split next to the PDF pane,
listing the document's bookmark tree flattened into indented lines (a
nested bookmark just gets deeper indentation -- there's no tree widget,
so this is the whole tree in one flat, ordinary buffer). It's real,
Vim-navigable text: move around it with j/k/gg/G// like
anything else, and press Enter on an entry to jump the PDF straight to
its page. SPC r o again -- from either the outline pane or the PDF
pane -- closes it. The outline is fetched once per document (a PDF's
bookmarks can't change while it's open) and cached, so reopening it is
instant after the first time; a PDF with no bookmarks at all shows a
single explanatory line instead of an empty pane.
SPC r / prompts for a search query and, once it comes back, opens (or
reuses, if one's already showing for this document) a results pane
listing every match in page order as p.NNN <context> -- one line per
occurrence, anywhere in the document, not just the current page. It's
the same kind of real, Vim-navigable buffer the outline panel is;
Enter on a result jumps the PDF pane straight to that match's page. A
query with no hits shows an explanatory placeholder line rather than an
empty pane, same as the outline's no-bookmarks case. Search runs fresh
against the document each time rather than keeping the whole document's
text extracted in memory between searches -- there's no results cache to
go stale, just a brief "searching..." status message while it works.
The page re-renders to fit whenever its pane is resized, except at a
fixed zoom percentage (SPC r =/SPC r -), which stays exactly where
you left it across a resize instead of silently re-fitting -- panning
with hjkl then just shows a different part of the same render, no
fresh page turn needed. Fit-page/fit-width do still re-render on
resize, since what "fits" depends on the pane's own size by definition.
Pages are rasterized straight to BGRA and uploaded to a GPU texture. The crop that's uploaded is only recomputed when the visible window actually changes (a page turn, a resize, a zoom, a pan), the texture behind it is only recreated when that crop's size changes, and a render that already fits the pane exactly -- the fit-page default -- is uploaded without being copied through a crop buffer at all.
A PDF pane's buffer is always empty and pathless -- the rendered page
lives in a GPU texture, not the buffer's own text -- so :w/SPC f s
on one is a no-op, same as every other generated panel in Fenix;
there's no risk of a stray save overwriting the real PDF file on disk.
Needs pdfium.dll (see Optional external tools)
-- without it, opening a PDF reports the error in the status line
rather than rendering.
| Keys | Action |
|---|---|
Ctrl-Space |
Force-open the popup (even with no prefix typed) |
Up/Down or Ctrl-P/Ctrl-N |
Move selection |
Tab / Enter / Ctrl-Y |
Accept the selected candidate (stays in Insert mode) |
Ctrl-E |
Dismiss the popup, keep typing |
Esc |
Leave Insert mode, as it always does -- the popup closes with it |
Esc is deliberately not spent on the popup. It used to be, which meant
returning to Normal mode took two presses, and which one you needed
depended on whether a popup you may not have been looking at happened to
be open. That is the modeless-editor reflex: where Esc has nothing to
do but dismiss things, spending it on a popup costs nothing. Here it
costs the one key the whole grammar rests on. Vim itself agrees -- :h popupmenu-keys lists every key with a special meaning while the menu is
up, and Esc is not among them; Ctrl-E is the dismiss key and
Ctrl-Y the accept key.
Fenix reads a single INI-format settings file:
- Windows:
%AppData%\fenix\config.ini - Linux/macOS:
~/.config/fenix/config.ini(or wherever$XDG_CONFIG_HOME/the platform's config directory points)
It's created automatically the first time you change a setting at
runtime (picking a theme, font size, :set shiftwidth=N); you can also
hand-edit it directly. Every key is optional — a missing or unparsable
value just falls back to the built-in default instead of failing to
load. A value's surrounding whitespace is always trimmed; wrap it in
double quotes (key = " ") to keep whitespace that actually matters.
[editor]
theme = TempleOS
font_size = 16
font_family = Fira Code
indent_width = 4
tab_width = 8
animations = true
[completion]
symbols_file = /home/you/tcl-symbols.txt
[lsp]
server1 = python|C:\Users\you\.local\bin\pyright-langserver.exe --stdio
server2 = rust|rust-analyzer
[mib]
root1 = MIB-A|C:\data\mib-a
root2 = MIB-B|C:\data\mib-b
telecommand_template = telecommand_send PUS_T={type} PUS_ST={stype} APID={apid} MNEMO={mnemo} ARGUMENTS=[{arguments}]
telecommand_argument_template = {name}={value}
telecommand_argument_separator = ", "
[jira]
base_url = https://jira.example.com
token = your-personal-access-token
project1 = PROJ|My Project
user1 = jo1111111|John Doe
[git]
graph_limit = 200
base_branch = develop
graph_style = ascii
[vnc]
host1 = build-vm|10.0.0.5|5900
host2 = test-vm|10.0.0.6|5900
[documents]
doc1 = Space Packet Protocol|C:\refs\133x0b2e2.pdf
doc2 = Time Code Formats|C:\refs\301x0b4.pdf
doc3 = Team Onboarding Notes|C:\refs\onboarding.md
[workspaces]
ws1 = Editor|
ws2 = Git|git
ws3 = Podman|docker
ws4 = Jira|jira
ws5 = VNC Build|vnc:build-vm
ws6 = VNC Test|vnc:test-vm
ws7 = fenix|project:C:\src\fenix
[windows]
restore_windows = true
window1 = 1920,0,2560,1400|true
window2 = 4480,0,1920,1040|true| Section | Key | Meaning |
|---|---|---|
editor |
theme |
Orbit Dark, TempleOS, Gruvbox Dark, Nord, Dracula, Solarized Dark, or One Dark (case-insensitive) |
editor |
font_size |
Body text size in points |
editor |
font_family |
Body text font family, by name, as installed on your system. Overrides whatever the active theme names; unset falls back to the theme's own choice (and from there to your system's default monospace font) |
editor |
indent_width |
Spaces per indent level (>>/<<, Tab, auto-indent) |
editor |
tab_width |
Visual columns a literal tab character expands to when rendered (real Vim's own :set tabstop) -- distinct from indent_width, which governs what Tab/>>/<< actually insert (always spaces) |
editor |
animations |
true/false -- whether caret-fade, scroll-ease, and yank/paste-pulse animations play at all; unset defaults to true. SPC t a toggles and persists this live |
completion |
symbols_file |
Path to a plain-text symbols list, one identifier per line (blank lines and #-comments ignored), merged into the Tcl completion popup |
lsp |
server1, server2, ... |
A language server to launch, as LANGUAGE|COMMAND (numbered, same reason as mib's roots) -- LANGUAGE is one of python, rust, c, cpp, bash, javascript, typescript, tsx, ...; COMMAND is the program plus arguments, split on whitespace (no shell-quoting support). Overrides the built-in default for that language if one exists (python → pyright-langserver --stdio, rust → rust-analyzer, c/cpp → clangd, bash → bash-language-server start, javascript/typescript/tsx → typescript-language-server --stdio); required for every other language |
mib |
root1, root2, ... |
A configured SCOS-2000 MIB directory, as LABEL|PATH (numbered since a plain INI key can't repeat) — see the SCOS-2000 MIB feature above |
mib |
telecommand_template |
Template used when SPC m i inserts a telecommand -- {type}, {stype}, {apid}, {mnemo}, {description}, {mib}, {arguments} |
mib |
telecommand_argument_template |
Template for one variable telecommand argument within {arguments} -- {name}, {value} |
mib |
telecommand_argument_separator |
Separator joining rendered arguments together. Every INI value here has its surrounding whitespace stripped, so a separator that depends on it (a trailing space, or one that's pure whitespace) needs to be wrapped in double quotes -- ", " or " " -- to survive; an unquoted , works exactly as before |
documents |
doc1, doc2, ... |
One entry in the SPC r f document index, as NAME|PATH (numbered, same reason as mib's roots). NAME is what the picker lists and fuzzy-matches; PATH can be any file Fenix opens, PDF or not |
workspaces |
ws1, ws2, ... |
One entry in the SPC TAB f workspace shelf, as NAME|ACTION (numbered, same convention as documents). ACTION is git/jira/docker (opens that built-in panel -- docker covers Podman too, since the panel autodetects the engine), vnc:HOST (a name from [vnc]), project:PATH (switches to that project root and opens a find-file picker in it, same as SPC p p; adds it to the known-projects list if it isn't there yet), or anything else (including empty, as for a plain "Editor" entry) for a workspace with no live session behind it. Picking an already-open entry switches to it instead of creating a duplicate; picking a fresh one creates it and renames it to match, so it shows up correctly next time |
jira |
base_url |
The self-hosted Jira Server/Data Center instance's REST API root (e.g. https://jira.example.com) — see the JIRA dashboard feature above |
jira |
token |
A personal access token for base_url, sent as a Bearer token — plaintext, same as every other setting in this file |
jira |
project1, project2, ... |
A tracked project, as KEY|Display Name (numbered, same convention as mib's root1/root2) — added/removed via SPC j p a/SPC j p d rather than hand-edited, though either works |
jira |
user1, user2, ... |
A tracked user, as id|Display Name — added/removed via SPC j u a/SPC j u d |
git |
graph_limit |
How many commits the History view's graph loads (SPC g l); unset means 200 |
editor |
watch_files |
false stops Fenix noticing files that change on disk while they're open; unset means on |
gitlab |
base_url |
The GitLab instance's own root, e.g. https://gitlab.mycompany.com -- not /api/v4, which Fenix appends itself |
gitlab |
token |
A GitLab personal access token with api scope. There is deliberately no project setting: it's read from each repo's origin remote |
git |
base_branch |
The ref SPC g c's base picker leads with, e.g. develop; unset falls back to whichever of main/master exists |
git |
graph_style |
ascii (default) or unicode -- which characters the commit graph's rails are drawn with. Unicode only lines up if your font actually has the box-drawing glyphs |
vnc |
host1, host2, ... |
A configured VNC target, as NAME|HOST|PORT (numbered, same convention as mib's root1/root2) — see the VNC console panes feature above. No authentication support — every host is assumed to be unauthenticated and reachable only over a trusted network |
windows |
restore_windows |
true/false -- whether to reopen last session's OS windows on their monitors at startup; unset defaults to true |
windows |
window1, window2, ... |
One remembered OS window, as X,Y,WIDTH,HEIGHT|MAXIMIZED. Written by Fenix on exit, not hand-authored -- X,Y is the outer frame's desktop position and WIDTH,HEIGHT the client area, which is the pair a window can actually be restored from. A window whose saved rectangle no longer lands on a connected monitor is placed by the window manager instead of opening off-screen |
Known projects (SPC p a/SPC p d) and recently-opened files (used by
the dashboard) are stored separately as plain newline-separated path
lists in the same directory (projects.txt, recent_files.txt) — they're
data, not settings, so they don't live in config.ini.
[vnc]/[documents]/[workspaces]/[lsp] are shelves, not an
auto-start list. Adding hosts to [vnc] makes them selectable from
SPC v v's picker; adding entries to [workspaces] makes them
selectable from SPC TAB f. Neither opens or connects to anything by
itself at launch -- restore_windows/[windows] is the only thing
Fenix does automatically on startup, and it only reopens each OS
window's position and size, not what was open inside it (every
restored window starts on a fresh scratch buffer). If you want a
particular set of VNC connections or projects up the moment Fenix
starts, run them from SPC TAB f once you're in -- there's no
"autostart on launch" key yet.
config.ini can be saved as UTF-8 with or without a byte-order mark --
both parse correctly (Notepad's "UTF-8" option and PowerShell's
Out-File/Set-Content both write one by default; a BOM on a file's
very first line used to make that whole first section silently vanish,
fixed since).
Fenix is a Cargo workspace split into small, mostly host-agnostic
crates, each independently unit-tested (cargo test --workspace):
| Crate | Role |
|---|---|
fenix-core |
The rope-backed Buffer/Cursor, undo/redo |
fenix-keymap |
Generic key-sequence trie (KeyPress, KeyTrie, Matcher) — shared by Vim's normal/visual keymaps and the leader menu |
fenix-vim |
Modal editing: motions, operators, text objects, search/substitute, indentation |
fenix-syntax |
tree-sitter-backed incremental parsing and highlight-span extraction |
fenix-buffers |
The open-buffer registry (BufferId → buffer/cursor/syntax state) |
fenix-window |
A generic split-window tree (layout, navigation, resize) — no knowledge of buffers |
fenix-explorer |
Directory listing, marking, file operations, git-status — no GPU/rendering |
fenix-picker |
Generic fuzzy matching + live-filtered candidate list, used by every fuzzy-finder |
fenix-project |
Project-root detection, ripgrep/fd shelling, known-projects/recent-files persistence |
fenix-completion |
Completion sources: Tcl keywords, ctags-scanned definitions, external symbols file |
fenix-format |
Structural, language-independent reindentation (bracket-nesting depth) — SPC c f/SPC c F |
fenix-mib |
SCOS-2000 MIB parsing (ICD 7.2) and telecommand/TM-packet/TM-parameter/calibration queries — SPC m ... |
fenix-table |
Pure layout math for a delimited table (row parsing, per-column widths, tab-stop positions) — feeds fenix-gui's elastic-column table view, SPC f t |
fenix-docker |
Docker/Podman CLI shelling (auto-detected): container/image listing, start/stop/restart/remove/run/build |
fenix-diff |
Unified-diff parsing (files/hunks/lines, both sides' line numbers) and single-hunk patch synthesis — pure, no I/O; what hunk staging and diff rendering are both built on |
fenix-git |
Shells out to git: status/files/branches/remotes/tags, commit graph topology and lane assignment, diffs (working tree, commit, ref-to-ref), fetch, and applying a patch to stage/unstage/discard one hunk |
fenix-jira |
A Jira Server/Data Center REST API client (ureq, PAT auth) — issue search and single-issue fetch, no thread/event-loop knowledge of its own |
fenix-config |
The unified config.ini reader/writer |
fenix-terminal |
PTY spawn/read/write/resize (portable-pty) plus ANSI screen-grid state (vt100) and terminal-query replies for both terminal surfaces — no thread/event-loop knowledge of its own |
fenix-gui |
Everything GPU/window-facing: wgpu rendering, winit input, and App, which wires all of the above together |
No license has been chosen yet — treat this as source-available for reference until one is added.
LSP rename and edit-based code actions open a read-only multi-file preview.
For a list of code actions, move to an action and press Enter to preview it.
Press a or Enter to apply every proposed text edit in memory, or q / Escape
to cancel. Files remain unsaved; use the usual save commands after review.
:undo-refactor reverses the latest refactor across all affected buffers, provided
none has since been edited, renamed, or closed. Normal u undoes only one file.
Edits validate document versions, exact UTF-16 ranges, overlapping ranges, and buffer/disk changes before applying. Unopened targets are loaded only on apply. File create/rename/delete operations, annotated edits, command-based actions, and lazy action resolution are not supported; unsupported edits are rejected whole.
Run pwsh -NoProfile -File ./scripts/ci.ps1 -Suite Workspace for the Windows
workspace test, scoped lint, and editor build gates. -Suite Reliability runs
the smaller filesystem/process/protocol suite used by the Linux CI job.
Both use the lockfile and save diagnostic logs under target/ci/.
See CI documentation for setup, coverage, and required-check names.
Language servers are scoped by language and project root. Configure literal
executables, argument arrays, working directories, and child environment values
in .fenix/tools.json for LSP, DAP, and tasks. :lsp-restart reloads language
services for the current project. Task rerun history is per project; debugger
and task controls guard against operating on another project's active process.
See project tool settings for examples and precedence.
Fenix restores documents, unsaved buffers, workspaces, splits, focus, cursors, and
scroll positions at startup. Use :session-save to checkpoint immediately or
:session-quit to exit and resume unsaved work next time. Force quit still discards
unsaved work. Missing files and disk conflicts are reported without overwriting
files. See session restoration for configuration,
recovery behavior, and current limits.