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 lineNumberDigits = showLineNumbers ? '${_lines.length}'.length : 0;
  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 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)} '
            : ' ' * (lineNumberDigits + 1);
        lnNumber = style.computedLineNumber.render(lnText);
      }
      final selectionStyle = Style()
          .background(const AnsiColor(7))
          .foreground(const AnsiColor(0));

      // 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 = _lines[rowIdx].length;
          } else if (rowIdx == endY) {
            rowStart = 0;
            rowEnd = y1 < y2 ? x2 : x1;
          } else {
            rowStart = 0;
            rowEnd = _lines[rowIdx].length;
          }

          rowStart = rowStart.clamp(0, _lines[rowIdx].length);
          rowEnd = rowEnd.clamp(0, _lines[rowIdx].length);

          // 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 cursorCol = displayLine.hasCursor
          ? (_col - displayLine.charOffset)
          : -1;

      var renderedBody = '';
      for (var j = 0; j < gs.length; j++) {
        String part;
        if (displayLine.hasCursor && useVirtualCursor && j == cursorCol) {
          cursor = cursor.setChar(gs[j]);
          part = cursor.view();
        } else {
          part = style.computedText.render(gs[j]);
        }

        final isSelected =
            selStart != null && selEnd != null && j >= selStart && j < selEnd;
        if (isSelected) {
          part = selectionStyle.render(part);
        }
        renderedBody += part;
      }

      if (displayLine.hasCursor &&
          useVirtualCursor &&
          cursorCol >= gs.length) {
        cursor = cursor.setChar(' ');
        var part = cursor.view();
        // If the selection is anchored past EOL (rare), don't attempt to style it.
        renderedBody += part;
      }

      final renderedLine = displayLine.hasCursor && !useVirtualCursor
          ? style.computedCursorLine.render(renderedBody)
          : renderedBody;

      buffer.writeln(
        '${style.computedPrompt.render(p)}$lnNumber$renderedLine',
      );
    }

    // 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 = buffer.toString().trimRight();
  if (useVirtualCursor || !_focused) {
    return content;
  }

  return View(content: content, cursor: terminalCursor);
}