Documentation
¶
Overview ¶
Package ui draws nem's frame onto a tcell screen.
The package is split along one rule, and it is the rule that keeps the Lip Gloss bridge out of the hot path:
- The text area is written with scr.SetContent, one cell at a time. It is redrawn on every keystroke and needs exact control of every cell, and it is where syntax highlighting will later hang per-cell styles. Serialising buffer text into ANSI only to parse it back out would be waste.
- The chrome - modelines, dividers, the echo area - is built as Lip Gloss strings and blitted in. Lip Gloss does real layout work there (padding, alignment, truncation by display width), and it is drawn at most once per frame per window.
So if the blitter were ever to prove fragile, only the chrome is exposed.
Index ¶
Constants ¶
const BranchMark = '⎇'
BranchMark precedes the branch name so it cannot be mistaken for part of the file name.
U+2387 is a standard codepoint rather than a Private Use Area glyph, so it needs no patched font - but coverage in terminal fonts is thinner than for the box-drawing runes used elsewhere, and a font without it shows a replacement box. It is a constant so that swapping it costs one line.
const DefaultScrollMargin = 2
DefaultScrollMargin is how many rows of context the render path keeps above and below point where the buffer allows.
const DividerRune = '│'
DividerRune is drawn down the column between side-by-side windows.
const ModelineRule = '─'
ModelineRule fills a modeline between the buffer name and the position readout. The rule is what separates stacked panes, since a horizontal split draws no divider row of its own - the modeline is the boundary.
const ModifiedMark = '▍'
ModifiedMark sits at the head of a modeline when the buffer has unsaved changes, in place of emacs's ** flag. One mark carries the whole state: a clean buffer shows a blank cell, so the buffer name never shifts position when you start typing.
const ParenScanLimit = 20000
ParenScanLimit caps how many runes a bracket search examines in each direction.
The search runs on every frame in which point sits next to a bracket, so it cannot be unbounded: a large file with one stray opening brace would otherwise scan to the end of the buffer on every keystroke. Twenty thousand runes is far more than any real nesting needs and is a fraction of a millisecond, so the cap only ever bites on genuinely unbalanced text - which is exactly the case that should give up and report a mismatch.
const ScratchName = "*scratch*"
ScratchName is the modeline name for a buffer with no file.
const TruncMarker = '$'
TruncMarker is shown in the last column of a line continuing past the right edge of its window. nem truncates long lines rather than wrapping them, so this is the only hint that there is more text out of view.
Variables ¶
This section is empty.
Functions ¶
func Render ¶
Render draws f onto scr using th. It does not call Show; the caller decides when to flush, so a frame and a cursor move are one update rather than two.
Rendering is a pure function of the frame, the theme and the screen: every piece of per-window state it needs - which line is at the top, which column is at the left - lives on the view.Window it is drawing. That is why this is a function and not a method on a renderer object.
func TerminalIsLight ¶
func TerminalIsLight() bool
TerminalIsLight guesses whether the terminal has a light background.
COLORFGBG is set by several terminals as "fg;bg" (sometimes "fg;default;bg") with the colour numbers of each. Background 7 or 15 is white or bright white; everything else is treated as dark. It is a heuristic - many terminals do not set it at all - so the answer is only a default, overridable with the theme setting. Guessing dark is the safer miss: most terminals are dark, and the dark palette on a light ground is faint rather than invisible.
Types ¶
type BranchFunc ¶
BranchFunc reports the git branch a buffer's file sits on, or empty when it is not in a repository.
Reading .git is I/O, and this is consulted for every window on every frame, so the editor caches the answer. See editor/vcs.go.
type Frame ¶
type Frame struct {
// Tree arranges the windows. Required.
Tree *view.Tree
// Active is the window holding the cursor, unless the minibuffer does.
Active *view.Window
// Echo is the bottom row: an echo-area message, or the minibuffer's prompt
// and contents when MiniOn.
Echo string
// MiniPt is the cursor's display column within the echo row, counted from
// the left edge of the screen, so the caller decides how the prompt and the
// text it has typed combine.
MiniPt text.ColIdx
// MiniOn reports whether a minibuffer prompt is active, which is what moves
// the cursor to the echo row.
MiniOn bool
// Panels are drawn over the tiled frame, in slice order, so a later panel
// overlaps an earlier one. They are not part of the split tree and so cannot
// disturb its layout; view.PlacePanel decides where each one sits.
Panels []Panel
// CursorSet moves the cursor to CursorX, CursorY in absolute screen
// coordinates, overriding both the active window and the echo row.
//
// It exists because a prompt can render inside a panel, and neither of the
// other two can express that: MiniPt addresses a column of the echo row, and
// the window path derives its position from point in a buffer. The caller
// that drew the prompt into a panel already knows the exact cell, so it says
// so rather than the renderer inferring it.
CursorX, CursorY int
CursorSet bool
// NameOf reports what each buffer is called, for the modeline. Optional: a
// nil NameOf falls back to naming a buffer after its file, which is all the
// renderer can work out on its own. See NameFunc.
NameOf NameFunc
// SpansOf reports how each line is classified, for syntax colour. Optional:
// a nil SpansOf draws the text uncoloured. See SpansFunc.
SpansOf SpansFunc
// TypeOf reports each buffer's file type, and BranchOf the git branch its
// file sits on, for the modeline's segments. Both optional: a nil source
// simply contributes no segment. See TypeFunc and BranchFunc.
TypeOf TypeFunc
BranchOf BranchFunc
// ListingOf reports which buffers are listings rather than text. Optional:
// a nil ListingOf treats every buffer as text. See ListingFunc.
ListingOf ListingFunc
// MiniRows lays a prompt's candidates out the emacs way, as Vertico does:
// the prompt on its row and the candidates on the rows below it, down to
// the bottom of the screen, full width. The windows give up the rows this
// takes rather than being drawn over, so every window and its modeline stay
// whole above the minibuffer. Empty means the minibuffer is the single echo
// row. See also MiniNote.
MiniRows []PanelLine
// MiniNote is drawn quietly at the right-hand end of the prompt row, when
// it fits beside what is typed: the candidate count.
MiniNote string
}
Frame is everything the renderer needs to draw one frame. It is a snapshot owned by the caller: Render reads it and does not retain it.
type ListingFunc ¶ added in v0.1.3
ListingFunc reports whether a buffer is a listing - a directory, say - whose lines are items rather than text.
A listing is drawn differently in two ways. It has no line numbers, which would count nothing a reader cares about. And the row point is on is drawn as a bar across the window, as the selected row of a completion panel is, because in a list the row is what point selects; a lone cursor cell in a column of names is easy to lose.
type NameFunc ¶
NameFunc reports what a buffer is called.
Buffer names live in the editor, which owns the name map; text.Buffer carries only a path, by design. Without this the renderer can only guess a name from the path, which labels every path-less buffer *scratch* - so a listing buffer renders its contents correctly while the modeline insists you are still in *scratch*. Each half looks right alone, which is why no unit test caught it.
type Panel ¶
type Panel struct {
Rect view.Rect
// Title is set into the top border when non-empty. It is styled as border
// rather than as content, because it labels the panel; anything the user is
// meant to read or edit belongs in a Line.
Title string
Lines []PanelLine
}
Panel is a box to draw over the frame.
Rect includes the border, matching view.PanelReq, so the interior is the rect inset by one cell on every side. A rect too small to have an interior draws its border alone - which is what a panel at view.MinPanelWidth by MinPanelHeight amounts to.
func StartupPanel ¶
StartupPanel builds the welcome panel for frame, reporting false when there is no room for it.
A frame too small returns false rather than a squeezed box: the buffer underneath is perfectly usable, and half a panel over it is worse than none.
type PanelLine ¶
type PanelLine struct {
Text string
// Match holds rune indices within Text to emphasise, ascending, as
// fuzzy.Match.Indices reports them. They are rune indices and not columns:
// after a wide glyph the two diverge, and emphasising by column would light
// the wrong cell. Indices outside Text are ignored rather than fatal, since
// they arrive from another package.
Match []int
// Selected marks the row the user is currently on, which is filled across
// the whole interior so it reads as a bar rather than stopping at the end of
// its text.
Selected bool
// Icon, when not zero, is drawn before Text with a space after it, in the
// colour IconClass has in the syntax palette. It is not part of Text, so
// Match indices and the candidate a prompt returns are unaffected.
Icon rune
IconClass syntax.Class
// Spans colour parts of Text by syntax class, for a row that is more than
// one thing - a key and the command it runs. Rune indices, as Match.
Spans []syntax.Span
}
PanelLine is one row of a panel's content.
type Screen ¶
Screen owns the terminal. It is a thin wrapper over tcell.Screen whose only real job is making sure Lip Gloss is pointed at the right place.
func Wrap ¶
Wrap adopts an already-initialised screen. Tests pass a simulation screen here; NewScreen passes a real one.
It syncs the Lip Gloss colour profile, which must happen after Init and is not optional. Lip Gloss decides how much colour to emit by probing os.Stdout, and that probe is meaningless once tcell owns the terminal: it can silently degrade to rendering every style with no colour at all. tcell has already done real terminfo negotiation, so its answer is the authoritative one. Skipping this produces unstyled chrome that looks exactly like a bug in the blitter and is not.
func (*Screen) Resync ¶
func (s *Screen) Resync()
Resync redraws the terminal from scratch and re-syncs the colour profile.
Call it after anything that may have reinitialised the screen or left another process's output on it: a suspend/resume cycle, or a shell command run in the foreground. A plain resize does not need the profile sync, but doing it here too costs nothing and means there is one method to reach for rather than two that differ subtly.
type SpansFunc ¶
SpansFunc reports how one line is classified, for colouring.
It is how the renderer asks about something it cannot work out on its own: lexing needs a language, a cache and the state of every line above, all of which live in the editor. This follows NameFunc - ui stays a pure function of the frame, and the caller that owns the state answers questions about it.
A nil SpansFunc means no highlighting, so ui does not require the field.
type Theme ¶
type Theme struct {
// Text area, applied via tcell directly.
Text tcell.Style
Trunc tcell.Style
// ParenMatch styles both halves of a matched bracket pair, ParenMismatch a
// bracket whose partner is missing or of the wrong kind. Match is marked by
// weight and an underline rather than by a hue, so it needs no colour and
// cannot clash with a terminal palette; a mismatch borrows the accent,
// since an unclosed bracket is the other thing worth interrupting you for.
ParenMatch tcell.Style
ParenMismatch tcell.Style
// LineNumbers turns the gutter on. It is on by default: a line number is
// the one piece of chrome you want in view constantly rather than on
// request, and every editor people arrive from shows them.
//
// The numbers live only here, in the renderer. They are never in the buffer,
// so a region cannot reach them and C-w cannot copy them - the gutter is
// drawn outside the span handed to drawLine, which is what makes that
// structural rather than a rule someone has to remember.
//
// LineNumber is quiet for the same reason inactive buffer names are: it is
// secondary information you read only when you are looking for it.
// LineNumberCurrent marks the line point is on, using the terminal's own
// foreground plus weight rather than a colour, so it reads correctly on a
// light or a dark background without nem knowing which it is on.
LineNumbers bool
LineNumber tcell.Style
LineNumberCurrent tcell.Style
// Region styles the cells between point and the mark.
//
// A selection is the one thing in the editor that legitimately needs a
// background: a span cannot be indicated with a foreground alone. That sits
// against the palette's rule that nothing paints a background, which exists
// so nem works on a light or a dark terminal without knowing which it is on.
//
// Reverse resolves it. Swapping foreground and background guarantees correct
// contrast on any palette because it adapts to the palette rather than
// guessing at it, and an inverted span is the universal terminal convention
// for a selection.
//
// This is deliberately not a contradiction of dropping reverse video from
// bracket matching. Reverse as decoration - marking something that is merely
// interesting - is the dated tell. Reverse as selection is what it means
// everywhere, and is the only palette-independent way to mark a span.
Region tcell.Style
// Panels - the completion list and prefix-key discovery - are drawn cell by
// cell rather than through Lip Gloss, because they need to emphasise
// individual runes inside a row and to clip a wide glyph at the border.
//
// PanelBorder is the same hairline as the modeline rule and the pane divider,
// so all of nem's chrome reads as one system rather than three.
//
// PanelSelected marks the row the user is on, and is reverse for exactly the
// reason Region is: it is a selection, and swapping foreground and background
// adapts to the terminal's palette instead of guessing at it.
//
// PanelMatch emphasises the runes a fuzzy query matched. Weight alone,
// deliberately: it composes with PanelSelected's inversion instead of
// fighting it, it needs no colour so it cannot clash with a palette, and it
// leaves the single accent spent where it belongs - on unsaved changes.
PanelBorder tcell.Style
PanelSelected tcell.Style
PanelMatch tcell.Style
// ListCursor is the bar across the row point is on in a listing buffer. It
// is the panel's selected row in another place - the same act of picking
// one item from a list - so it is reverse for the same reason.
ListCursor tcell.Style
// MiniNote styles the candidate count beside a prompt at the bottom of the
// screen: quiet, since it is secondary to what is being typed.
MiniNote tcell.Style
// Chrome, rendered through Lip Gloss and blitted in. The modeline is built
// from four separately styled segments rather than one flat bar, which is
// what lets focus read as weight instead of as a block of colour.
//
// ModelineName carries no colour on purpose: the focused buffer name uses
// the terminal's own foreground, so it is the most legible thing on screen
// whatever palette you run.
ModelineName lipgloss.Style
ModelineNameOff lipgloss.Style
ModelineRule lipgloss.Style
ModelinePos lipgloss.Style
ModelineMark lipgloss.Style
Divider lipgloss.Style
// Echo styles a transient message, which is quiet to read as ephemeral.
Echo lipgloss.Style
// Mini styles an active minibuffer prompt. It is deliberately not quieted:
// text being typed is ordinary content, not a passing notice.
Mini lipgloss.Style
// Syntax enables colouring the text area from a SpansFunc. Off restores
// exactly the uncoloured rendering.
Syntax bool
// SyntaxStyle maps a syntax.Class to its style. Foregrounds only: see
// defaultSyntaxStyles and the note above about never painting a background.
SyntaxStyle [numSyntaxClasses]tcell.Style
// ScrollMargin is the rows of context kept around point.
ScrollMargin int
}
Theme collects every colour and style in one place.
It deliberately holds two kinds of style. The text area is drawn through tcell and so needs tcell.Style; the chrome is built with Lip Gloss and so needs lipgloss.Style. Mixing them in one struct is not an oversight - it is the package's central rule made visible.
func (*Theme) UseSyntaxPalette ¶
UseSyntaxPalette switches the syntax colours between the two palettes.
type TypeFunc ¶
TypeFunc reports a buffer's file type for the modeline: "go", "lua", "json", "md", or empty for text nem has no grammar for.
It comes from the editor rather than being derived here so that it cannot disagree with the colouring: both answer from the lexer the editor already chose. A modeline claiming "go" over uncoloured text would be worse than no segment at all, because it would look like the highlighter had failed.