import 'dart:math' show max; import 'package:xterm/core/input/handler.dart'; import 'package:xterm/core/input/keys.dart'; import 'package:xterm/core/buffer/buffer.dart'; import 'package:xterm/core/cursor.dart'; import 'package:xterm/core/escape/emitter.dart'; import 'package:xterm/core/escape/handler.dart'; import 'package:xterm/core/escape/parser.dart'; import 'package:xterm/core/buffer/line.dart'; import 'package:xterm/core/mouse.dart'; import 'package:xterm/utils/platform.dart'; import 'package:xterm/core/state.dart'; import 'package:xterm/core/tabs.dart'; import 'package:xterm/utils/ascii.dart'; import 'package:xterm/utils/circular_list.dart'; import 'package:xterm/utils/observable.dart'; class Terminal with Observable implements TerminalState, EscapeHandler { final int maxLines; void Function()? onBell; void Function(String)? onTitleChange; void Function(String)? onIconChange; void Function(String)? onOutput; void Function(int width, int height, int pixelWidth, int pixelHeight)? onResize; /// Flag to toggle os specific behaviors. final TerminalTargetPlatform platform; final TerminalInputHandler inputHandler; Terminal({ this.maxLines = 1000, this.onBell, this.onTitleChange, this.onIconChange, this.onOutput, this.onResize, this.platform = TerminalTargetPlatform.unknown, this.inputHandler = defaultInputHandler, }); late final _parser = EscapeParser(this); final _emitter = const EscapeEmitter(); late var _buffer = _mainBuffer; late final _mainBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: false); late final _altBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: true); final tabStops = TabStops(); 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 */ @override int get viewWidth => _viewWidth; @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; 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; Buffer get buffer => _buffer; Buffer get mainBuffer => _mainBuffer; Buffer get altBuffer => _altBuffer; bool get isUsingAltBuffer => _buffer == _altBuffer; CircularList get lines => _buffer.lines; void write(String data) { _parser.write(data); notifyListeners(); } bool keyInput( TerminalKey key, { bool shift = false, bool alt = false, bool ctrl = false, }) { final output = inputHandler( TerminalInputEvent( key: key, shift: shift, alt: alt, ctrl: ctrl, state: this, altBuffer: isUsingAltBuffer, platform: platform, ), ); if (output != null) { onOutput?.call(output); return true; } return false; } 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; } void textInput(String text) { onOutput?.call(text); } void paste(String text) { if (_bracketedPasteMode) { onOutput?.call(_emitter.bracketedPaste(text)); } else { textInput(text); } } /// 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. 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(); } /* 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, _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) { _buffer.charset.use(charset); } @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 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) { // no-op } }