dlants / doc-views
Install for your project team
Run this command in your project directory to install the skill for your entire team:
mkdir -p .claude/skills/doc-views && curl -L -o skill.zip "https://fastmcp.me/Skills/Download/3169" && unzip -o skill.zip -d .claude/skills/doc-views && rm skill.zip
Project Skills
This skill will be saved in .claude/skills/doc-views/ and checked into git. All team members will have access to it automatically.
Important: Please verify the skill by reviewing its instructions before using it.
Comprehensive guide for the view system in magenta.nvim, including template literal syntax, component composition, interactive bindings, and TUI-specific rendering patterns
0 views
0 installs
Skill Content
---
name: doc-views
description: Comprehensive guide for the view system in magenta.nvim, including template literal syntax, component composition, interactive bindings, and TUI-specific rendering patterns
---
# View System in magenta.nvim
**THIS IS NOT REACT**. **DO NOT USE REACT VIEWS OR DOM**. This uses a templating library for a TUI running inside a neovim buffer.
Views in magenta.nvim are built using a declarative templating approach with the `d` template literal tag and view functions that render controller state to neovim buffers.
## Core Concepts
The view system is based on several key principles:
1. **Declarative rendering**: Views describe what should be displayed, not how to update the buffer
2. **Template composition**: Small view functions combine to build complex UIs
3. **Interactive bindings**: Attach keybindings to specific regions of rendered text
4. **Automatic updates**: Views re-render on state changes triggered by dispatched messages
## Template Literal Syntax (`d`)
The `d` tag function is the foundation of the view system:
```typescript
// Basic text rendering
d`This is some text`;
// Dynamic content interpolation
d`User: ${username}`;
// Conditional rendering
d`${isLoading ? d`Loading...` : d`Content loaded!`}`;
// Rendering lists
d`${items.map((item) => d`- ${item.name}\n`)}`;
// Component composition
d`Header: ${headerView({ title })}\nBody: ${bodyView({ content })}`;
```
### Interpolation Rules
- String values are inserted as-is
- Other `d` templates can be nested
- Arrays of `d` templates are joined together
- Undefined/null values render as empty strings
- Numbers are converted to strings
## Component Composition
Break down complex views into smaller, reusable functions:
```typescript
function headerView(title: string) {
return d`
===================
${title}
===================
`;
}
function itemView(item: Item) {
return d`
- ${item.name}: ${item.description}
Status: ${item.status}
`;
}
function listView(items: Item[]) {
return d`
${headerView("My Items")}
${items.map((item) => itemView(item))}
`;
}
```
## Adding Interactivity with `withBindings`
Attach keybindings to sections of text using `withBindings`:
```typescript
withBindings(d`Press Enter to continue`, {
"<CR>": () => dispatch({ type: "continue" }),
q: () => dispatch({ type: "quit" }),
});
```
### Multiple Bindings
You can attach multiple keybindings to the same text region:
```typescript
withBindings(d`[Submit]`, {
"<CR>": () => dispatch({ type: "submit" }),
"<Space>": () => dispatch({ type: "submit" }),
s: () => dispatch({ type: "submit" }),
});
```
### Bindings on Lists
Each item in a list can have its own bindings:
```typescript
d`
${items.map((item, index) =>
withBindings(d`[${index + 1}] ${item.name}\n`, {
"<CR>": () => dispatch({ type: "select-item", index }),
d: () => dispatch({ type: "delete-item", index }),
}),
)}
`;
```
## Controller View Methods
Controllers implement a `view()` method that returns their rendered state:
```typescript
class MyController {
state: {
count: number;
items: string[];
};
view() {
return d`
Counter: ${this.state.count}
${withBindings(d`[Increment]`, {
"<CR>": () => this.myDispatch({ type: "increment" }),
})}
Items:
${this.state.items.map((item) => d`- ${item}\n`)}
`;
}
}
```
## Rendering Cycle
1. User action triggers a keybinding or command
2. Binding dispatches a message
3. Message flows through the dispatch system to the appropriate controller
4. Controller updates its state
5. View is re-rendered based on new state
6. Buffer content is updated with new view
## TUI-Specific Considerations
### Text Alignment and Spacing
Unlike web UIs, TUI rendering requires careful attention to spacing:
```typescript
// Good: Explicit newlines for vertical spacing
d`
Line 1
Line 2
Line 4 (with gap above)
`;
// Bad: Implicit spacing assumptions
d`Line 1${"\n"}Line 2`; // Hard to read
```
### Buffer Width
Be aware that text may wrap based on terminal/buffer width:
```typescript
// Consider line length when formatting
d`
This is a very long line that might wrap in narrow terminals
Consider breaking long text into multiple lines
`;
```
### Visual Separators
Use ASCII art for visual structure:
```typescript
d`
===================
Section Header
===================
Content goes here
-------------------
Footer
`;
```
## Common Patterns
### Loading States
```typescript
view() {
if (this.state.loading) {
return d`Loading...`;
}
return d`
${this.state.data}
${withBindings(d`[Refresh]`, {
"<CR>": () => this.myDispatch({ type: "refresh" }),
})}
`;
}
```
### Error Display
```typescript
view() {
if (this.state.error) {
return d`
ERROR: ${this.state.error.message}
${withBindings(d`[Retry]`, {
"<CR>": () => this.myDispatch({ type: "retry" }),
})}
`;
}
// Normal view...
}
```
### Conditional Sections
```typescript
view() {
return d`
Main content
${this.state.showDetails ? d`
Details:
${this.state.details}
` : d``}
${withBindings(d`[${this.state.showDetails ? "Hide" : "Show"} Details]`, {
"<CR>": () => this.myDispatch({ type: "toggle-details" }),
})}
`;
}
```
### Lists with Actions
```typescript
view() {
return d`
Tasks:
${this.state.tasks.map((task, idx) => d`
${withBindings(d`[${task.done ? "✓" : " "}] ${task.name}`, {
"<CR>": () => this.myDispatch({ type: "toggle-task", index: idx }),
d: () => this.myDispatch({ type: "delete-task", index: idx }),
})}
`)}
${withBindings(d`[Add Task]`, {
"<CR>": () => this.myDispatch({ type: "add-task" }),
})}
`;
}
```
### Multi-Column Layouts
Use spacing to create columns:
```typescript
function formatRow(name: string, value: string) {
const nameWidth = 20;
const paddedName = name.padEnd(nameWidth);
return d`${paddedName} ${value}`;
}
view() {
return d`
${formatRow("Name:", this.state.name)}
${formatRow("Status:", this.state.status)}
${formatRow("Created:", this.state.created)}
`;
}
```
## Best Practices
### Keep Views Pure
Views should be pure functions of state - no side effects:
```typescript
// Good: Pure view function
view() {
return d`Count: ${this.state.count}`;
}
// Bad: Side effects in view
view() {
this.logCount(); // Don't do this!
return d`Count: ${this.state.count}`;
}
```
### Extract Complex Logic
Don't put complex logic in templates:
```typescript
// Good: Extract to helper method
private formatItem(item: Item): string {
return `${item.name} (${item.status})`;
}
view() {
return d`${this.state.items.map((item) => d`${this.formatItem(item)}\n`)}`;
}
// Bad: Complex logic in template
view() {
return d`${this.state.items.map((item) => d`${item.name} (${item.done ? "✓" : item.pending ? "..." : "✗"})\n`)}`;
}
```
### Use Descriptive Binding Labels
Make interactive elements obvious:
```typescript
// Good: Clear interactive elements
withBindings(d`[ Submit ]`, { "<CR>": handler });
withBindings(d`Press Enter to continue`, { "<CR>": handler });
// Bad: Unclear what's interactive
withBindings(d`Submit`, { "<CR>": handler });
withBindings(d`>`, { "<CR>": handler });
```
### Handle Empty States
Always consider what happens with empty data:
```typescript
view() {
if (this.state.items.length === 0) {
return d`
No items found.
${withBindings(d`[Add Item]`, {
"<CR>": () => this.myDispatch({ type: "add" }),
})}
`;
}
return d`${this.state.items.map(/* ... */)}`;
}
```
### Avoid Deep Nesting
Keep template nesting shallow for readability:
```typescript
// Good: Flat structure with helper functions
view() {
return d`
${this.renderHeader()}
${this.renderContent()}
${this.renderFooter()}
`;
}
// Bad: Deep nesting
view() {
return d`
${this.state.show ? d`
${this.state.loading ? d`
Loading...
` : d`
${this.state.items.map((item) => d`
${item.visible ? d`${item.name}` : d``}
`)}
`}
` : d``}
`;
}
```
## Debugging Views
### Check Rendered Output
The view is rendered to a neovim buffer - you can inspect the actual buffer content to debug rendering issues:
```typescript
// In tests
const bufferContent = await driver.getDisplayBuffer();
console.log(bufferContent);
```
### Validate Bindings
Ensure bindings are attached to the correct regions:
```typescript
// In tests
const pos = await driver.assertDisplayBufferContains("[Submit]");
await driver.triggerDisplayBufferKey(pos, "<CR>");
```
### Log State
Add temporary logging to see state during rendering:
```typescript
view() {
this.context.nvim.logger.debug("Rendering with state:", this.state);
return d`...`;
}
```
## Performance Considerations
### Minimize Re-renders
Only dispatch messages when state actually changes:
```typescript
// Good: Check before updating
myUpdate(msg: Msg) {
if (msg.type === "set-filter") {
if (this.state.filter !== msg.filter) {
this.state.filter = msg.filter;
// View will re-render
}
}
}
// Bad: Always update
myUpdate(msg: Msg) {
if (msg.type === "set-filter") {
this.state.filter = msg.filter; // Re-renders even if same value
}
}
```
### Avoid Expensive Computations in Views
Compute derived data in update methods, not in views:
```typescript
// Good: Pre-compute in update
myUpdate(msg: Msg) {
this.state.items = msg.items;
this.state.filteredItems = this.state.items.filter(/* ... */);
}
view() {
return d`${this.state.filteredItems.map(/* ... */)}`;
}
// Bad: Compute in view
view() {
const filtered = this.state.items.filter(/* expensive filter */);
return d`${filtered.map(/* ... */)}`;
}
```
## Common Pitfalls
### Don't Mix String Concatenation with `d`
```typescript
// Bad: Mixing strings
d`Hello ` + userName; // Wrong!
// Good: Use interpolation
d`Hello ${userName}`;
```
### Don't Forget Newlines in Lists
```typescript
// Bad: Items will run together
d`${items.map((item) => d`${item.name}`)}`;
// Good: Explicit newlines
d`${items.map((item) => d`${item.name}\n`)}`;
```
### Don't Return Plain Strings
```typescript
// Bad: Plain string
view() {
return "Hello"; // Won't work!
}
// Good: Use d template
view() {
return d`Hello`;
}
```
### Don't Mutate State in View
```typescript
// Bad: Side effects
view() {
this.state.viewCount++; // Never do this!
return d`Viewed ${this.state.viewCount} times`;
}
// Good: Pure view
view() {
return d`Viewed ${this.state.viewCount} times`;
}
```