view method

  1. @override
Object view()
override

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);
}