view method
Renders the current model state for display.
This method is called after every update to refresh the screen. It should return either a String or a View object.
Guidelines
- Keep view functions pure - no side effects
- View should only depend on model state
- Use string interpolation or StringBuffer for complex views
- Consider terminal width/height for responsive layouts
Example
@override
String view() {
final buffer = StringBuffer();
// Header
buffer.writeln('╔════════════════════════════╗');
buffer.writeln('║ My Application ║');
buffer.writeln('╚════════════════════════════╝');
buffer.writeln();
// Content
if (loading) {
buffer.writeln('Loading...');
} else {
for (final item in items) {
final prefix = item == selectedItem ? '▸ ' : ' ';
buffer.writeln('$prefix$item');
}
}
buffer.writeln();
// Footer
buffer.writeln('↑/↓: Navigate Enter: Select q: Quit');
return buffer.toString();
}
Implementation
@override
Object view() {
final style = activeStyle();
final activeSelections = _selections;
final primarySelection = activeSelections?.primary;
final secondaryCursorOffsets = <int>{
if (activeSelections != null)
for (final range in activeSelections.ranges)
if (range.isCollapsed && range != primarySelection) range.endOffset,
};
final additionalSelectedRanges = activeSelections == null
? const <TextSelectionRange>[]
: activeSelections.ranges
.where((range) => !range.isCollapsed && range != primarySelection)
.toList(growable: false);
final lineNumberDigits = _lineNumberDigits;
final displayLines = _softWrappedLines(lineNumberDigits);
final buffer = StringBuffer();
if (value.isEmpty && placeholder.isNotEmpty) {
final p =
promptFunc?.call((
lineIndex: 0,
isFocused: _focused,
row: _row,
col: _col,
)) ??
prompt;
final ph = style.computedPlaceholder.render(placeholder);
buffer.write('${style.computedPrompt.render(p)}$ph');
} else {
for (var i = 0; i < displayLines.length; i++) {
final displayLine = displayLines[i];
final lineDecorations = _lineDecorationsForRow(displayLine.rowIndex);
final lineDecorationStyle = _lineDecorationStyleForDecorations(
style,
lineDecorations,
);
final lineNumberDecorationStyle =
_lineNumberDecorationStyleForDecorations(style, lineDecorations);
final lineNumberMarker = _normalizedLineNumberMarker(
displayLine.charOffset == 0
? _lineNumberMarkerForDecorations(lineDecorations)
: null,
);
final p =
promptFunc?.call((
lineIndex: i,
isFocused: _focused,
row: displayLine.rowIndex,
col: _col,
)) ??
prompt;
String lnNumber = '';
if (showLineNumbers) {
final lnText = displayLine.charOffset == 0
? '${(displayLine.rowIndex + 1).toString().padLeft(lineNumberDigits)}$lineNumberMarker'
: ' ' * (lineNumberDigits + 1);
lnNumber = (lineNumberDecorationStyle ?? style.computedLineNumber)
.render(lnText);
}
// Compute selection overlap for this visual segment.
int? selStart;
int? selEnd;
if (_selectionStart != null && _selectionEnd != null) {
final (x1, y1) = _selectionStart!;
final (x2, y2) = _selectionEnd!;
final startY = math.min(y1, y2);
final endY = math.max(y1, y2);
final rowIdx = displayLine.rowIndex;
if (rowIdx >= startY && rowIdx <= endY) {
// Selection range in the original (unwrapped) row coordinates.
int rowStart;
int rowEnd;
if (startY == endY) {
rowStart = math.min(x1, x2);
rowEnd = math.max(x1, x2);
} else if (rowIdx == startY) {
rowStart = y1 < y2 ? x1 : x2;
rowEnd = _document.lineLength(rowIdx);
} else if (rowIdx == endY) {
rowStart = 0;
rowEnd = y1 < y2 ? x2 : x1;
} else {
rowStart = 0;
rowEnd = _document.lineLength(rowIdx);
}
rowStart = rowStart.clamp(0, _document.lineLength(rowIdx));
rowEnd = rowEnd.clamp(0, _document.lineLength(rowIdx));
// Map to this segment via charOffset.
final segStart = displayLine.charOffset;
final segLen = uni.graphemes(displayLine.text).length;
final segEnd = segStart + segLen;
final overlapStart = math.max(rowStart, segStart);
final overlapEnd = math.min(rowEnd, segEnd);
if (overlapStart < overlapEnd) {
selStart = overlapStart - segStart;
selEnd = overlapEnd - segStart;
}
}
}
final gs = uni.graphemes(displayLine.text).toList(growable: false);
final segmentStart =
_document.lineStartOffset(displayLine.rowIndex) +
displayLine.charOffset;
final decorationRanges = _segmentDecorationRanges(
segmentStart,
segmentStart + gs.length,
);
final cursorCol = displayLine.hasCursor
? (_col - displayLine.charOffset)
: -1;
final renderedBody = StringBuffer();
final run = StringBuffer();
String? runDecorationStyleKey;
var runSelected = false;
var runUsesCursorStyle = false;
void flushRun() {
if (run.isEmpty) return;
final decorationStyle = runDecorationStyleKey == null
? null
: style.computedDecorationStyle(runDecorationStyleKey);
final partStyle = _textCellStyle(
style,
lineDecorationStyle: lineDecorationStyle,
decorationStyle: decorationStyle,
isSelected: runSelected,
useCursorStyle: runUsesCursorStyle,
);
renderedBody.write(partStyle.render(run.toString()));
run.clear();
}
for (var j = 0; j < gs.length; j++) {
final documentOffset = segmentStart + j;
final isSelected =
(selStart != null &&
selEnd != null &&
j >= selStart &&
j < selEnd) ||
_selectionRangesContainOffset(
additionalSelectedRanges,
documentOffset,
);
final isCursor =
(displayLine.hasCursor && j == cursorCol) ||
secondaryCursorOffsets.contains(documentOffset);
final decorationStyleKey = _decorationStyleKeyForColumn(
decorationRanges,
j,
);
final usesCursorStyle = useVirtualCursor && isCursor;
final grapheme = gs[j];
final mustRenderAlone = grapheme == '<' || grapheme == '\t';
if (run.isNotEmpty &&
(runDecorationStyleKey != decorationStyleKey ||
runSelected != isSelected ||
runUsesCursorStyle != usesCursorStyle ||
mustRenderAlone)) {
flushRun();
}
runDecorationStyleKey = decorationStyleKey;
runSelected = isSelected;
runUsesCursorStyle = usesCursorStyle;
run.write(grapheme);
if (mustRenderAlone) flushRun();
}
flushRun();
final hasCursorAtEnd =
(displayLine.hasCursor && cursorCol >= gs.length) ||
secondaryCursorOffsets.contains(segmentStart + gs.length);
if (useVirtualCursor && hasCursorAtEnd) {
final partStyle = _textCellStyle(
style,
lineDecorationStyle: lineDecorationStyle,
isSelected: false,
useCursorStyle: true,
);
renderedBody.write(partStyle.render(' '));
}
buffer.writeln(
'${style.computedPrompt.render(p)}$lnNumber${renderedBody.toString()}',
);
}
// end of buffer indicator
final remaining = (_height - displayLines.length);
if (remaining > 0) {
final eob = style.computedEndOfBuffer.render('~');
for (var i = 0; i < remaining; i++) {
buffer.writeln(eob);
}
}
}
final content = _renderCompletionPopup(buffer.toString().trimRight());
if (useVirtualCursor || !_focused) {
return content;
}
return View(content: content, cursor: terminalCursor);
}