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