update method

  1. @override
(TextInputModel, Cmd?) update(
  1. Msg msg
)
override

Updates the component state in response to a message.

Returns the updated component (often this) and an optional command.

Implementation

@override
(TextInputModel, Cmd?) update(Msg msg) {
  final cmds = <Cmd>[];

  if (msg is MouseMsg) {
    if (multiline) {
      return _handleMultilineMouse(msg);
    }
    if (msg.action == MouseAction.wheel) {
      switch (msg.button) {
        case MouseButton.wheelUp:
        case MouseButton.wheelLeft:
          _scrollSingleLineBy(-1);
          break;
        case MouseButton.wheelDown:
        case MouseButton.wheelRight:
          _scrollSingleLineBy(1);
          break;
        default:
          break;
      }
      return (this, null);
    }
    if (msg.y != 0) {
      if (msg.action == MouseAction.press && msg.button == MouseButton.left) {
        selectionStart = null;
        selectionEnd = null;
        _mouseSelecting = false;
        _focused = false;
      }
      return (this, null);
    }
    final promptWidth = stringWidth(prompt);
    final localX = msg.x - promptWidth;
    final visibleValue = _value.sublist(_offset, _offsetRight);
    final visibleText = visibleValue.join();
    final idxInVisible = layout.localCellXToGraphemeIndex(
      visibleText,
      localX,
    );
    final x = _offset + idxInVisible;

    if (msg.action == MouseAction.press && msg.button == MouseButton.left) {
      _focused = true;
      _mouseSelecting = true;
      final now = DateTime.now();
      if (_lastClickTime != null &&
          now.difference(_lastClickTime!) <
              const Duration(milliseconds: 500) &&
          _lastClickPos == x) {
        // Double click: select word
        final (start, end) = _findWordAt(x);
        selectionStart = start;
        selectionEnd = end;
        _pos = end;
        _lastClickTime = now;
        _lastClickPos = x;
      } else {
        position = x;
        selectionStart = _pos;
        selectionEnd = _pos;
        _lastClickTime = now;
        _lastClickPos = x;
      }
    } else if (msg.action == MouseAction.motion && _mouseSelecting) {
      position = x;
      selectionEnd = _pos;
    } else if (msg.action == MouseAction.release && _mouseSelecting) {
      _mouseSelecting = false;
      if (selectionStart == selectionEnd) {
        selectionStart = null;
        selectionEnd = null;
        return (this, null);
      }
      final cmd = _copySelectionCmdIfAny();
      selectionStart = null;
      selectionEnd = null;
      return (this, cmd);
    }
    return (this, null);
  }

  if (!_focused) {
    return (this, null);
  }

  // Check for suggestion acceptance first
  if (msg is KeyMsg && keyMatches(msg.key, [keyMap.acceptSuggestion])) {
    if (_canAcceptSuggestion()) {
      final suggestion = _matchedSuggestions[_currentSuggestionIndex];
      _value = [..._value, ...suggestion.sublist(_value.length)];
      _invalidateWrappedLines();
      cursorEnd();
    }
  }

  if (msg is KeyMsg) {
    if (keyMatches(msg.key, [keyMap.copy])) {
      final selected = getSelectedText();
      if (selected.isNotEmpty) {
        return (this, Cmd.setClipboardBestEffort(selected));
      }
    }

    if (keyMatches(msg.key, [keyMap.cut])) {
      final selected = getSelectedText();
      if (selected.isNotEmpty) {
        _deleteSelection();
        return (this, Cmd.setClipboardBestEffort(selected));
      }
    }

    if (keyMatches(msg.key, [keyMap.selectAll])) {
      selectionStart = 0;
      selectionEnd = _value.length;
      position = _value.length;
      return (this, null);
    }

    // Multi-line: newline insertion (Enter / Shift+Enter)
    if (multiline && keyMatches(msg.key, [keyMap.newline])) {
      _deleteSelection();
      _resetDesiredCol();
      _insertNewline();
      _updateSuggestions();
      _handleOverflow();
      return (this, null);
    }

    if (msg.key.type == KeyType.space) {
      _resetDesiredCol();
      _insertRunes([0x20]);
      return (this, null);
    }

    if (keyMatches(msg.key, [keyMap.deleteWordBackward])) {
      _resetDesiredCol();
      if (!_deleteSelection()) {
        _deleteWordBackward();
      }
    } else if (keyMatches(msg.key, [keyMap.deleteCharacterBackward])) {
      _resetDesiredCol();
      if (!_deleteSelection()) {
        error = null;
        if (_value.isNotEmpty && _pos > 0) {
          _value.removeAt(_pos - 1);
          _invalidateWrappedLines();
          error = _validate(_value);
          if (_pos > 0) position = _pos - 1;
        }
      }
    } else if (keyMatches(msg.key, [keyMap.wordBackward])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      _wordBackward();
    } else if (keyMatches(msg.key, [keyMap.selectWordBackward])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      _wordBackward();
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.characterBackward])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      if (_pos > 0) position = _pos - 1;
    } else if (keyMatches(msg.key, [keyMap.selectCharacterBackward])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      if (_pos > 0) position = _pos - 1;
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.wordForward])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      _wordForward();
    } else if (keyMatches(msg.key, [keyMap.selectWordForward])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      _wordForward();
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.characterForward])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      if (_pos < _value.length) position = _pos + 1;
    } else if (keyMatches(msg.key, [keyMap.selectCharacterForward])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      if (_pos < _value.length) position = _pos + 1;
      selectionEnd = _pos;
    } else if (multiline && keyMatches(msg.key, [keyMap.documentStart])) {
      // Multi-line: Ctrl+Home — go to document start
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      cursorStart();
    } else if (multiline && keyMatches(msg.key, [keyMap.documentEnd])) {
      // Multi-line: Ctrl+End — go to document end
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      cursorEnd();
    } else if (keyMatches(msg.key, [keyMap.lineStart])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      if (multiline) {
        _cursorLineStart();
      } else {
        cursorStart();
      }
    } else if (keyMatches(msg.key, [keyMap.selectLineStart])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      if (multiline) {
        _cursorLineStart();
      } else {
        cursorStart();
      }
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.deleteCharacterForward])) {
      _resetDesiredCol();
      if (!_deleteSelection()) {
        if (_value.isNotEmpty && _pos < _value.length) {
          _value.removeAt(_pos);
          _invalidateWrappedLines();
          error = _validate(_value);
        }
      }
    } else if (keyMatches(msg.key, [keyMap.lineEnd])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      if (multiline) {
        _cursorLineEnd();
      } else {
        cursorEnd();
      }
    } else if (keyMatches(msg.key, [keyMap.selectLineEnd])) {
      _resetDesiredCol();
      selectionStart ??= _pos;
      if (multiline) {
        _cursorLineEnd();
      } else {
        cursorEnd();
      }
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.deleteAfterCursor])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      _deleteAfterCursor();
    } else if (keyMatches(msg.key, [keyMap.deleteBeforeCursor])) {
      _resetDesiredCol();
      selectionStart = null;
      selectionEnd = null;
      _deleteBeforeCursor();
    } else if (keyMatches(msg.key, [keyMap.paste])) {
      _resetDesiredCol();
      _deleteSelection();
      // Return paste command - caller handles clipboard
      return (this, _pasteCmd());
    } else if (keyMatches(msg.key, [keyMap.deleteWordForward])) {
      _resetDesiredCol();
      if (!_deleteSelection()) {
        _deleteWordForward();
      }
    } else if (multiline && keyMatches(msg.key, [keyMap.lineUp])) {
      // Multi-line: Up arrow — move cursor up one line
      selectionStart = null;
      selectionEnd = null;
      _lineUp();
    } else if (multiline && keyMatches(msg.key, [keyMap.selectLineUp])) {
      selectionStart ??= _pos;
      _lineUp();
      selectionEnd = _pos;
    } else if (multiline && keyMatches(msg.key, [keyMap.lineDown])) {
      // Multi-line: Down arrow — move cursor down one line
      selectionStart = null;
      selectionEnd = null;
      _lineDown();
    } else if (multiline && keyMatches(msg.key, [keyMap.selectLineDown])) {
      selectionStart ??= _pos;
      _lineDown();
      selectionEnd = _pos;
    } else if (keyMatches(msg.key, [keyMap.nextSuggestion])) {
      _nextSuggestion();
    } else if (keyMatches(msg.key, [keyMap.prevSuggestion])) {
      _previousSuggestion();
    } else if (!msg.key.alt &&
        msg.key.type == KeyType.runes &&
        msg.key.runes.length == 1 &&
        msg.key.runes.first == 0x03) {
      final selected = getSelectedText();
      if (selected.isNotEmpty) {
        return (this, Cmd.setClipboardBestEffort(selected));
      }
    } else if (msg.key.runes.isNotEmpty && !msg.key.ctrl && !msg.key.alt) {
      // Regular character input
      final insertable = <int>[];
      for (final r in msg.key.runes) {
        if (r >= 0x20 && r != 0x7F) {
          insertable.add(r);
        }
      }
      if (insertable.isEmpty) {
        _updateSuggestions();
        return (this, null);
      }
      _resetDesiredCol();
      _deleteSelection();
      _insertRunes(insertable);
    }

    _updateSuggestions();
  } else if (msg is _PasteChunkMsg) {
    _applyNextPasteChunk();
    if (_pendingPasteRunes != null) {
      cmds.add(_schedulePasteChunk());
    }
  } else if (msg is PasteMsg || msg is PasteTextMsg) {
    final content = msg is PasteMsg
        ? msg.content
        : (msg as PasteTextMsg).content;
    if (TuiTrace.enabled) {
      final kind = msg is PasteMsg ? 'PasteMsg' : 'PasteTextMsg';
      TuiTrace.log(
        'paste.msg kind=$kind chars=${content.length} focused=$_focused',
        tag: TraceTag.input,
      );
    }
    if (_shouldCollapsePaste(content)) {
      _insertCollapsedPasteReference(content);
    } else {
      final cmd = _startChunkedPaste(content);
      if (cmd != null) {
        cmds.add(cmd);
      }
    }
  } else if (msg is PasteErrorMsg) {
    error = msg.error.toString();
  }

  // Update cursor
  final (newCursor, cursorCmd) = cursor.update(msg);
  cursor = newCursor;
  if (cursorCmd != null) cmds.add(cursorCmd);

  // Avoid scheduling a blink command on every keypress. Cursor blinking is
  // already driven by its own timer loop while focused.

  _handleOverflow();
  return (this, cmds.isNotEmpty ? Cmd.batch(cmds) : null);
}