import 'dart:math' show max; import 'package:xterm/src/base/observable.dart'; import 'package:xterm/src/core/buffer/buffer.dart'; import 'package:xterm/src/core/buffer/cell_offset.dart'; import 'package:xterm/src/core/buffer/line.dart'; import 'package:xterm/src/core/cursor.dart'; import 'package:xterm/src/core/escape/emitter.dart'; import 'package:xterm/src/core/escape/handler.dart'; import 'package:xterm/src/core/escape/parser.dart'; import 'package:xterm/src/core/input/handler.dart'; import 'package:xterm/src/core/input/keys.dart'; import 'package:xterm/src/core/mouse/button.dart'; import 'package:xterm/src/core/mouse/button_state.dart'; import 'package:xterm/src/core/mouse/handler.dart'; import 'package:xterm/src/core/mouse/mode.dart'; import 'package:xterm/src/core/platform.dart'; import 'package:xterm/src/core/state.dart'; import 'package:xterm/src/core/tabs.dart'; import 'package:xterm/src/utils/ascii.dart'; import 'package:xterm/src/utils/circular_buffer.dart'; /// [Terminal] is an interface to interact with command line applications. It /// translates escape sequences from the application into updates to the /// [buffer] and events such as [onTitleChange] or [onBell], as well as /// translating user input into escape sequences that the application can /// understand. class Terminal with Observable implements TerminalState, EscapeHandler { /// The number of lines that the scrollback buffer can hold. If the buffer /// exceeds this size, the lines at the top of the buffer will be removed. final int maxLines; /// Function that is called when the program requests the terminal to ring /// the bell. If not set, the terminal will do nothing. void Function()? onBell; /// Function that is called when the program requests the terminal to change /// the title of the window to [title]. void Function(String title)? onTitleChange; /// Function that is called when the program requests the terminal to change /// the icon of the window. [icon] is the name of the icon. void Function(String icon)? onIconChange; /// Function that is called when the terminal emits data to the underlying /// program. This is typically caused by user inputs from [textInput], /// [keyInput], [mouseInput], or [paste]. void Function(String data)? onOutput; /// Function that is called when the dimensions of the terminal change. void Function(int width, int height, int pixelWidth, int pixelHeight)? onResize; /// The [TerminalInputHandler] used by this terminal. [defaultInputHandler] is /// used when not specified. User of this class can provide their own /// implementation of [TerminalInputHandler] or extend [defaultInputHandler] /// with [CascadeInputHandler]. TerminalInputHandler? inputHandler; TerminalMouseHandler? mouseHandler; /// The callback that is called when the terminal receives a unrecognized /// escape sequence. void Function(String code, List args)? onPrivateOSC; /// Flag to toggle os specific behaviors. final TerminalTargetPlatform platform; /// Characters that break selection when double clicking. If not set, the /// [Buffer.defaultWordSeparators] will be used. final Set? wordSeparators; Terminal({ this.maxLines = 1000, this.onBell, this.onTitleChange, this.onIconChange, this.onOutput, this.onResize, this.platform = TerminalTargetPlatform.unknown, this.inputHandler = defaultInputHandler, this.mouseHandler = defaultMouseHandler, this.onPrivateOSC, this.reflowEnabled = true, this.wordSeparators, }); late final _parser = EscapeParser(this); final _emitter = const EscapeEmitter(); late var _buffer = _mainBuffer; late final _mainBuffer = Buffer( this, maxLines: maxLines, isAltBuffer: false, wordSeparators: wordSeparators, ); late final _altBuffer = Buffer( this, maxLines: maxLines, isAltBuffer: true, wordSeparators: wordSeparators, ); final _tabStops = TabStops(); /// The last character written to the buffer. Used to implement some escape /// sequences that repeat the last character. var _precedingCodepoint = 0; /* TerminalState */ int _viewWidth = 80; int _viewHeight = 24; final _cursorStyle = CursorStyle(); bool _insertMode = false; bool _lineFeedMode = false; bool _cursorKeysMode = false; bool _reverseDisplayMode = false; bool _originMode = false; bool _autoWrapMode = true; MouseMode _mouseMode = MouseMode.none; MouseReportMode _mouseReportMode = MouseReportMode.normal; bool _cursorBlinkMode = false; bool _cursorVisibleMode = true; bool _appKeypadMode = false; bool _reportFocusMode = false; bool _altBufferMouseScrollMode = false; bool _bracketedPasteMode = false; /* State getters */ /// Number of cells in a terminal row. @override int get viewWidth => _viewWidth; /// Number of rows in this terminal. @override int get viewHeight => _viewHeight; @override CursorStyle get cursor => _cursorStyle; @override bool get insertMode => _insertMode; @override bool get lineFeedMode => _lineFeedMode; @override bool get cursorKeysMode => _cursorKeysMode; @override bool get reverseDisplayMode => _reverseDisplayMode; @override bool get originMode => _originMode; @override bool get autoWrapMode => _autoWrapMode; @override MouseMode get mouseMode => _mouseMode; @override MouseReportMode get mouseReportMode => _mouseReportMode; @override bool get cursorBlinkMode => _cursorBlinkMode; @override bool get cursorVisibleMode => _cursorVisibleMode; @override bool get appKeypadMode => _appKeypadMode; @override bool get reportFocusMode => _reportFocusMode; @override bool get altBufferMouseScrollMode => _altBufferMouseScrollMode; @override bool get bracketedPasteMode => _bracketedPasteMode; /// Current active buffer of the terminal. This is initially [mainBuffer] and /// can be switched back and forth from [altBuffer] to [mainBuffer] when /// the underlying program requests it. Buffer get buffer => _buffer; Buffer get mainBuffer => _mainBuffer; Buffer get altBuffer => _altBuffer; bool get isUsingAltBuffer => _buffer == _altBuffer; /// Lines of the active buffer. IndexAwareCircularBuffer get lines => _buffer.lines; /// Whether the terminal performs reflow when the viewport size changes or /// simply truncates lines. true by default. @override bool reflowEnabled; /// Writes the data from the underlying program to the terminal. Calling this /// updates the states of the terminal and emits events such as [onBell] or /// [onTitleChange] when the escape sequences in [data] request it. void write(String data) { _parser.write(data); notifyListeners(); } /// Sends a key event to the underlying program. /// /// See also: /// - [charInput] /// - [textInput] /// - [paste] bool keyInput( TerminalKey key, { bool shift = false, bool alt = false, bool ctrl = false, }) { final output = inputHandler?.call( TerminalKeyboardEvent( key: key, shift: shift, alt: alt, ctrl: ctrl, state: this, altBuffer: isUsingAltBuffer, platform: platform, ), ); if (output != null) { onOutput?.call(output); return true; } return false; } /// Similary to [keyInput], but takes a character as input instead of a /// [TerminalKey]. /// /// See also: /// - [keyInput] /// - [textInput] /// - [paste] bool charInput( int charCode, { bool alt = false, bool ctrl = false, }) { if (ctrl) { // a(97) ~ z(122) if (charCode >= Ascii.a && charCode <= Ascii.z) { final output = charCode - Ascii.a + 1; onOutput?.call(String.fromCharCode(output)); return true; } // [(91) ~ _(95) if (charCode >= Ascii.openBracket && charCode <= Ascii.underscore) { final output = charCode - Ascii.openBracket + 27; onOutput?.call(String.fromCharCode(output)); return true; } } if (alt && platform != TerminalTargetPlatform.macos) { if (charCode >= Ascii.a && charCode <= Ascii.z) { final code = charCode - Ascii.a + 65; final input = [0x1b, code]; onOutput?.call(String.fromCharCodes(input)); return true; } } return false; } /// Sends regular text input to the underlying program. /// /// See also: /// - [keyInput] /// - [charInput] /// - [paste] void textInput(String text) { onOutput?.call(text); } /// Similar to [textInput], except that when the program tells the terminal /// that it supports [bracketedPasteMode], the text is wrapped in escape /// sequences to indicate that it is a paste operation. Prefer this method /// over [textInput] when pasting text. /// /// See also: /// - [textInput] void paste(String text) { if (_bracketedPasteMode) { onOutput?.call(_emitter.bracketedPaste(text)); } else { textInput(text); } } // Handle a mouse event and return true if it was handled. bool mouseInput( TerminalMouseButton button, TerminalMouseButtonState buttonState, CellOffset position, ) { final output = mouseHandler?.call(TerminalMouseEvent( button: button, buttonState: buttonState, position: position, state: this, platform: platform, )); if (output != null) { onOutput?.call(output); return true; } return false; } /// Resize the terminal screen. [newWidth] and [newHeight] should be greater /// than 0. Text reflow is currently not implemented and will be avaliable in /// the future. @override void resize( int newWidth, int newHeight, [ int? pixelWidth, int? pixelHeight, ]) { newWidth = max(newWidth, 1); newHeight = max(newHeight, 1); onResize?.call(newWidth, newHeight, pixelWidth ?? 0, pixelHeight ?? 0); //we need to resize both buffers so that they are ready when we switch between them _altBuffer.resize(_viewWidth, _viewHeight, newWidth, newHeight); _mainBuffer.resize(_viewWidth, _viewHeight, newWidth, newHeight); _viewWidth = newWidth; _viewHeight = newHeight; if (buffer == _altBuffer) { buffer.clearScrollback(); } _altBuffer.resetVerticalMargins(); _mainBuffer.resetVerticalMargins(); } @override String toString() { return 'Terminal(#$hashCode, $_viewWidth x $_viewHeight, ${_buffer.height} lines)'; } /* Handlers */ @override void writeChar(int char) { _precedingCodepoint = char; _buffer.writeChar(char); } /* SBC */ @override void bell() { onBell?.call(); } @override void backspaceReturn() { _buffer.moveCursorX(-1); } @override void tab() { final nextStop = _tabStops.find(_buffer.cursorX + 1, _viewWidth); if (nextStop != null) { _buffer.setCursorX(nextStop); } else { _buffer.setCursorX(_viewWidth); _buffer.cursorGoForward(); // Enter pending-wrap state } } @override void lineFeed() { _buffer.lineFeed(); } @override void carriageReturn() { _buffer.setCursorX(0); } @override void shiftOut() { _buffer.charset.use(1); } @override void shiftIn() { _buffer.charset.use(0); } @override void unknownSBC(int char) { // no-op } /* ANSI sequence */ @override void saveCursor() { _buffer.saveCursor(); } @override void restoreCursor() { _buffer.restoreCursor(); } @override void index() { _buffer.index(); } @override void nextLine() { _buffer.index(); _buffer.setCursorX(0); } @override void setTapStop() { _tabStops.isSetAt(_buffer.cursorX); } @override void reverseIndex() { _buffer.reverseIndex(); } @override void designateCharset(int charset, int name) { _buffer.charset.designate(charset, name); } @override void unkownEscape(int char) { // no-op } /* CSI */ @override void repeatPreviousCharacter(int count) { if (_precedingCodepoint == 0) { return; } for (var i = 0; i < count; i++) { _buffer.writeChar(_precedingCodepoint); } } @override void setCursor(int x, int y) { _buffer.setCursor(x, y); } @override void setCursorX(int x) { _buffer.setCursorX(x); } @override void setCursorY(int y) { _buffer.setCursorY(y); } @override void moveCursorX(int offset) { _buffer.moveCursorX(offset); } @override void moveCursorY(int n) { _buffer.moveCursorY(n); } @override void clearTabStopUnderCursor() { _tabStops.clearAt(_buffer.cursorX); } @override void clearAllTabStops() { _tabStops.clearAll(); } @override void sendPrimaryDeviceAttributes() { onOutput?.call(_emitter.primaryDeviceAttributes()); } @override void sendSecondaryDeviceAttributes() { onOutput?.call(_emitter.secondaryDeviceAttributes()); } @override void sendTertiaryDeviceAttributes() { onOutput?.call(_emitter.tertiaryDeviceAttributes()); } @override void sendOperatingStatus() { onOutput?.call(_emitter.operatingStatus()); } @override void sendCursorPosition() { onOutput?.call(_emitter.cursorPosition(_buffer.cursorX, _buffer.cursorY)); } @override void setMargins(int top, [int? bottom]) { _buffer.setVerticalMargins(top, bottom ?? viewHeight - 1); } @override void cursorNextLine(int amount) { _buffer.moveCursorY(amount); _buffer.setCursorX(0); } @override void cursorPrecedingLine(int amount) { _buffer.moveCursorY(-amount); _buffer.setCursorX(0); } @override void eraseDisplayBelow() { _buffer.eraseDisplayFromCursor(); } @override void eraseDisplayAbove() { _buffer.eraseDisplayToCursor(); } @override void eraseDisplay() { _buffer.eraseDisplay(); } @override void eraseScrollbackOnly() { _buffer.clearScrollback(); } @override void eraseLineRight() { _buffer.eraseLineFromCursor(); } @override void eraseLineLeft() { _buffer.eraseLineToCursor(); } @override void eraseLine() { _buffer.eraseLine(); } @override void insertLines(int amount) { _buffer.insertLines(amount); } @override void deleteLines(int amount) { _buffer.deleteLines(amount); } @override void deleteChars(int amount) { _buffer.deleteChars(amount); } @override void scrollUp(int amount) { _buffer.scrollUp(amount); } @override void scrollDown(int amount) { _buffer.scrollDown(amount); } @override void eraseChars(int amount) { _buffer.eraseChars(amount); } @override void insertBlankChars(int amount) { _buffer.insertBlankChars(amount); } @override void sendSize() { onOutput?.call(_emitter.size(viewHeight, viewWidth)); } @override void unknownCSI(int finalByte) { // no-op } /* Modes */ @override void setInsertMode(bool enabled) { _insertMode = enabled; } @override void setLineFeedMode(bool enabled) { _lineFeedMode = enabled; } @override void setUnknownMode(int mode, bool enabled) { // no-op } /* DEC Private modes */ @override void setCursorKeysMode(bool enabled) { _cursorKeysMode = enabled; } @override void setReverseDisplayMode(bool enabled) { _reverseDisplayMode = enabled; } @override void setOriginMode(bool enabled) { _originMode = enabled; } @override void setColumnMode(bool enabled) { // no-op } @override void setAutoWrapMode(bool enabled) { _autoWrapMode = enabled; } @override void setMouseMode(MouseMode mode) { _mouseMode = mode; } @override void setCursorBlinkMode(bool enabled) { _cursorBlinkMode = enabled; } @override void setCursorVisibleMode(bool enabled) { _cursorVisibleMode = enabled; } @override void useAltBuffer() { _buffer = _altBuffer; } @override void useMainBuffer() { _buffer = _mainBuffer; } @override void clearAltBuffer() { _altBuffer.clear(); } @override void setAppKeypadMode(bool enabled) { _appKeypadMode = enabled; } @override void setReportFocusMode(bool enabled) { _reportFocusMode = enabled; } @override void setMouseReportMode(MouseReportMode mode) { _mouseReportMode = mode; } @override void setAltBufferMouseScrollMode(bool enabled) { _altBufferMouseScrollMode = enabled; } @override void setBracketedPasteMode(bool enabled) { _bracketedPasteMode = enabled; } @override void setUnknownDecMode(int mode, bool enabled) { // no-op } /* Select Graphic Rendition (SGR) */ @override void resetCursorStyle() { _cursorStyle.reset(); } @override void setCursorBold() { _cursorStyle.setBold(); } @override void setCursorFaint() { _cursorStyle.setFaint(); } @override void setCursorItalic() { _cursorStyle.setItalic(); } @override void setCursorUnderline() { _cursorStyle.setUnderline(); } @override void setCursorBlink() { _cursorStyle.setBlink(); } @override void setCursorInverse() { _cursorStyle.setInverse(); } @override void setCursorInvisible() { _cursorStyle.setInvisible(); } @override void setCursorStrikethrough() { _cursorStyle.setStrikethrough(); } @override void unsetCursorBold() { _cursorStyle.unsetBold(); } @override void unsetCursorFaint() { _cursorStyle.unsetFaint(); } @override void unsetCursorItalic() { _cursorStyle.unsetItalic(); } @override void unsetCursorUnderline() { _cursorStyle.unsetUnderline(); } @override void unsetCursorBlink() { _cursorStyle.unsetBlink(); } @override void unsetCursorInverse() { _cursorStyle.unsetInverse(); } @override void unsetCursorInvisible() { _cursorStyle.unsetInvisible(); } @override void unsetCursorStrikethrough() { _cursorStyle.unsetStrikethrough(); } @override void setForegroundColor16(int color) { _cursorStyle.setForegroundColor16(color); } @override void setForegroundColor256(int index) { _cursorStyle.setForegroundColor256(index); } @override void setForegroundColorRgb(int r, int g, int b) { _cursorStyle.setForegroundColorRgb(r, g, b); } @override void resetForeground() { _cursorStyle.resetForegroundColor(); } @override void setBackgroundColor16(int color) { _cursorStyle.setBackgroundColor16(color); } @override void setBackgroundColor256(int index) { _cursorStyle.setBackgroundColor256(index); } @override void setBackgroundColorRgb(int r, int g, int b) { _cursorStyle.setBackgroundColorRgb(r, g, b); } @override void resetBackground() { _cursorStyle.resetBackgroundColor(); } @override void unsupportedStyle(int param) { // no-op } /* OSC */ @override void setTitle(String name) { onTitleChange?.call(name); } @override void setIconName(String name) { onIconChange?.call(name); } @override void unknownOSC(String ps, List pt) { onPrivateOSC?.call(ps, pt); } }