inline terminal emulator, drop xterm.dart dependency

Replace the xterm pub.dev package with owned code under
lib/src/terminal/. Based on xterm.dart v4.0.0 by xuty (MIT).
Quiver LRU replaced with hand-rolled LinkedHashMap cache.
Scrollable removed from TerminalView — scroll events are forwarded
via Listener.onPointerSignal instead. zmodem, debugger, and
suggestion modules stripped as unused.

Also: bundle clide.tmux.conf (no status bar, 50k scrollback,
mouse on, zero escape delay, isolated -L clide socket), bump PTY
read buffer to 64KB, add 2px terminal padding, drop bold
JetBrains Mono registration.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-05 08:50:38 +02:00
co-authored by Claude
parent d4369aabc3
commit 9c1597a450
79 changed files with 12122 additions and 36 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"exported_at": "2026-05-05T06:49:53Z",
"exported_at": "2026-05-05T06:50:38Z",
"decisions": [
{
"id": "D-1",
+12
View File
@@ -16,6 +16,18 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
## [Unreleased]
### Changed
- Inline terminal emulator based on xterm.dart v4.0.0 — replaces the
pub.dev dependency with owned code under `lib/src/terminal/`. Drops
three transitive dependencies (xterm, quiver, zmodem).
- Bundle clide-specific tmux.conf for Claude pane sessions: no status
bar, 50k scrollback, mouse on, zero escape delay, isolated socket.
- PTY read buffer increased from 4KB to 64KB.
- Terminal view 2px padding on all sides.
- Remove bold JetBrains Mono font registration to prevent glyph width
mismatch in terminal rendering.
## [2.0.0] — 2026-05-03
### Fixed
+35
View File
@@ -0,0 +1,35 @@
# clide tmux.conf — loaded via tmux -f for every Claude pane session.
# Tuned for embedding inside xterm.dart; no status bar, large
# scrollback, mouse-scroll passthrough, zero escape delay.
# No status bar — clide renders its own pane chrome.
set -g status off
# 50k lines of scrollback (tmux default is 2000).
set -g history-limit 50000
# Zero escape delay — xterm.dart delivers escape sequences
# atomically, so the 500ms default just adds latency.
set -sg escape-time 0
# Mouse on — scroll wheel events reach tmux's copy-mode so the
# user can scroll back through Claude output.
set -g mouse on
# 256color + true-color passthrough.
set -g default-terminal "xterm-256color"
set -ga terminal-overrides ",xterm-256color:Tc"
# Don't ring the bell visually or audibly — clide owns notifications.
set -g visual-bell off
set -g bell-action none
# Keep the session alive when the shell exits — clide manages
# lifecycle via pane.close, not tmux session destruction.
set -g remain-on-exit off
# Allow alt-screen passthrough for full-screen programs.
set -g alternate-screen on
# Focus events let the terminal's focus tracking work through tmux.
set -g focus-events on
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:clide/clide.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:xterm/xterm.dart';
import 'package:clide/src/terminal/terminal.dart';
/// General-purpose terminal pane. Spawns the user's `$SHELL` under the
/// daemon's PTY (via `pane.spawn`), feeds the `pane.output` event
+1 -1
View File
@@ -370,7 +370,7 @@ class _ReaderArgs {
/// each chunk back to the main isolate as a `Uint8List`. Exits on
/// EOF, close, or error.
void _readerEntrypoint(_ReaderArgs args) {
const chunk = 4096;
const chunk = 65536;
final buf = pkg_ffi.calloc<ffi.Uint8>(chunk);
try {
while (true) {
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 xuty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/base/event.dart';
mixin Disposable {
final _disposables = <Disposable>[];
bool get disposed => _disposed;
bool _disposed = false;
Event get onDisposed => _onDisposed.event;
final _onDisposed = EventEmitter();
void register(Disposable disposable) {
assert(!_disposed);
_disposables.add(disposable);
}
void registerCallback(void Function() callback) {
assert(!_disposed);
_disposables.add(_DisposeCallback(callback));
}
void dispose() {
_disposed = true;
for (final disposable in _disposables) {
disposable.dispose();
}
_onDisposed.emit(null);
}
}
class _DisposeCallback with Disposable {
final void Function() callback;
_DisposeCallback(this.callback);
@override
void dispose() {
super.dispose();
callback();
}
}
+44
View File
@@ -0,0 +1,44 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/base/disposable.dart';
typedef EventListener<T> = void Function(T event);
class Event<T> {
final EventEmitter<T> emitter;
Event(this.emitter);
void call(EventListener<T> listener) {
emitter(listener);
}
}
class EventEmitter<T> {
final _listeners = <EventListener<T>>[];
EventSubscription<T> call(EventListener<T> listener) {
_listeners.add(listener);
return EventSubscription(this, listener);
}
void emit(T event) {
for (final listener in _listeners) {
listener(event);
}
}
Event<T> get event => Event(this);
}
class EventSubscription<T> with Disposable {
final EventEmitter<T> emitter;
final EventListener<T> listener;
EventSubscription(this.emitter, this.listener);
@override
void dispose() {
emitter._listeners.remove(listener);
}
}
+19
View File
@@ -0,0 +1,19 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
mixin Observable {
final listeners = <void Function()>{};
void addListener(void Function() listener) {
listeners.add(listener);
}
void removeListener(void Function() listener) {
listeners.remove(listener);
}
void notifyListeners() {
for (var listener in listeners) {
listener();
}
}
}
@@ -0,0 +1,606 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show max, min;
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/line.dart';
import 'package:clide/src/terminal/src/core/buffer/range_line.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
import 'package:clide/src/terminal/src/core/charset.dart';
import 'package:clide/src/terminal/src/core/cursor.dart';
import 'package:clide/src/terminal/src/core/reflow.dart';
import 'package:clide/src/terminal/src/core/state.dart';
import 'package:clide/src/terminal/src/utils/circular_buffer.dart';
import 'package:clide/src/terminal/src/utils/unicode_v11.dart';
class Buffer {
final TerminalState terminal;
final int maxLines;
final bool isAltBuffer;
/// Characters that break selection when calling [getWordBoundary]. If null,
/// defaults to [defaultWordSeparators].
final Set<int>? wordSeparators;
Buffer(
this.terminal, {
required this.maxLines,
required this.isAltBuffer,
this.wordSeparators,
}) {
for (int i = 0; i < terminal.viewHeight; i++) {
lines.push(_newEmptyLine());
}
resetVerticalMargins();
}
int _cursorX = 0;
int _cursorY = 0;
late int _marginTop;
late int _marginBottom;
var _savedCursorX = 0;
var _savedCursorY = 0;
final _savedCursorStyle = CursorStyle();
final charset = Charset();
/// Width of the viewport in columns. Also the index of the last column.
int get viewWidth => terminal.viewWidth;
/// Height of the viewport in rows. Also the index of the last line.
int get viewHeight => terminal.viewHeight;
/// lines of the buffer. the length of [lines] should always be equal or
/// greater than [viewHeight].
late final lines = IndexAwareCircularBuffer<BufferLine>(maxLines);
/// Total number of lines in the buffer. Always equal or greater than
/// [viewHeight].
int get height => lines.length;
/// Horizontal position of the cursor relative to the top-left cornor of the
/// screen, starting from 0.
int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
/// Vertical position of the cursor relative to the top-left cornor of the
/// screen, starting from 0.
int get cursorY => _cursorY;
/// Index of the first line in the scroll region.
int get marginTop => _marginTop;
/// Index of the last line in the scroll region.
int get marginBottom => _marginBottom;
/// The number of lines above the viewport.
int get scrollBack => height - viewHeight;
/// Vertical position of the cursor relative to the top of the buffer,
/// starting from 0.
int get absoluteCursorY => _cursorY + scrollBack;
/// Absolute index of the first line in the scroll region.
int get absoluteMarginTop => _marginTop + scrollBack;
/// Absolute index of the last line in the scroll region.
int get absoluteMarginBottom => _marginBottom + scrollBack;
/// Writes data to the _terminal. Terminal sequences or special characters are
/// not interpreted and directly added to the buffer.
///
/// See also: [Terminal.write]
void write(String text) {
for (var char in text.runes) {
writeChar(char);
}
}
/// Writes a single character to the _terminal. Escape sequences or special
/// characters are not interpreted and directly added to the buffer.
///
/// See also: [Terminal.writeChar]
void writeChar(int codePoint) {
codePoint = charset.translate(codePoint);
final cellWidth = unicodeV11.wcwidth(codePoint);
if (_cursorX >= terminal.viewWidth) {
index();
setCursorX(0);
if (terminal.autoWrapMode) {
currentLine.isWrapped = true;
}
}
final line = currentLine;
line.setCell(_cursorX, codePoint, cellWidth, terminal.cursor);
if (_cursorX < viewWidth) {
_cursorX++;
}
if (cellWidth == 2) {
writeChar(0);
}
}
/// The line at the current cursor position.
BufferLine get currentLine {
return lines[absoluteCursorY];
}
void backspace() {
if (_cursorX == 0 && currentLine.isWrapped) {
currentLine.isWrapped = false;
moveCursor(viewWidth - 1, -1);
} else if (_cursorX == viewWidth) {
moveCursor(-2, 0);
} else {
moveCursor(-1, 0);
}
}
/// Erases the viewport from the cursor position to the end of the buffer,
/// including the cursor position.
void eraseDisplayFromCursor() {
eraseLineFromCursor();
for (var i = absoluteCursorY + 1; i < height; i++) {
final line = lines[i];
line.isWrapped = false;
line.eraseRange(0, viewWidth, terminal.cursor);
}
}
/// Erases the viewport from the top-left corner to the cursor, including the
/// cursor.
void eraseDisplayToCursor() {
eraseLineToCursor();
for (var i = 0; i < _cursorY; i++) {
final line = lines[i + scrollBack];
line.isWrapped = false;
line.eraseRange(0, viewWidth, terminal.cursor);
}
}
/// Erases the whole viewport.
void eraseDisplay() {
for (var i = 0; i < viewHeight; i++) {
final line = lines[i + scrollBack];
line.isWrapped = false;
line.eraseRange(0, viewWidth, terminal.cursor);
}
}
/// Erases the line from the cursor to the end of the line, including the
/// cursor position.
void eraseLineFromCursor() {
currentLine.isWrapped = false;
currentLine.eraseRange(_cursorX, viewWidth, terminal.cursor);
}
/// Erases the line from the start of the line to the cursor, including the
/// cursor.
void eraseLineToCursor() {
currentLine.isWrapped = false;
currentLine.eraseRange(0, _cursorX, terminal.cursor);
}
/// Erases the line at the current cursor position.
void eraseLine() {
currentLine.isWrapped = false;
currentLine.eraseRange(0, viewWidth, terminal.cursor);
}
/// Erases [count] cells starting at the cursor position.
void eraseChars(int count) {
final start = _cursorX;
currentLine.eraseRange(start, start + count, terminal.cursor);
}
void scrollDown(int lines) {
for (var i = absoluteMarginBottom; i >= absoluteMarginTop; i--) {
if (i >= absoluteMarginTop + lines) {
this.lines[i] = this.lines[i - lines];
} else {
this.lines[i] = _newEmptyLine();
}
}
}
void scrollUp(int lines) {
for (var i = absoluteMarginTop; i <= absoluteMarginBottom; i++) {
if (i <= absoluteMarginBottom - lines) {
this.lines[i] = this.lines[i + lines];
} else {
this.lines[i] = _newEmptyLine();
}
}
}
/// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND Index
///
/// ESC D
///
/// [index] causes the active position to move downward one line without
/// changing the column position. If the active position is at the bottom
/// margin, a scroll up is performed.
void index() {
if (isInVerticalMargin) {
if (_cursorY == _marginBottom) {
if (marginTop == 0 && !isAltBuffer) {
lines.insert(absoluteMarginBottom + 1, _newEmptyLine());
} else {
scrollUp(1);
}
} else {
moveCursorY(1);
}
return;
}
// the cursor is not in the scrollable region
if (_cursorY >= viewHeight - 1) {
// we are at the bottom
if (isAltBuffer) {
scrollUp(1);
} else {
lines.push(_newEmptyLine());
}
} else {
// there're still lines so we simply move cursor down.
moveCursorY(1);
}
}
void lineFeed() {
index();
if (terminal.lineFeedMode) {
setCursorX(0);
}
}
/// https://terminalguide.namepad.de/seq/a_esc_cm/
void reverseIndex() {
if (isInVerticalMargin) {
if (_cursorY == _marginTop) {
scrollDown(1);
} else {
moveCursorY(-1);
}
} else {
moveCursorY(-1);
}
}
void cursorGoForward() {
_cursorX = min(_cursorX + 1, viewWidth);
}
void setCursorX(int cursorX) {
_cursorX = cursorX.clamp(0, viewWidth - 1);
}
void setCursorY(int cursorY) {
_cursorY = cursorY.clamp(0, viewHeight - 1);
}
void moveCursorX(int offset) {
setCursorX(_cursorX + offset);
}
void moveCursorY(int offset) {
setCursorY(_cursorY + offset);
}
void setCursor(int cursorX, int cursorY) {
var maxCursorY = viewHeight - 1;
if (terminal.originMode) {
cursorY += _marginTop;
maxCursorY = _marginBottom;
}
_cursorX = cursorX.clamp(0, viewWidth - 1);
_cursorY = cursorY.clamp(0, maxCursorY);
}
void moveCursor(int offsetX, int offsetY) {
final cursorX = _cursorX + offsetX;
final cursorY = _cursorY + offsetY;
setCursor(cursorX, cursorY);
}
/// Save cursor position, charmap and text attributes.
void saveCursor() {
_savedCursorX = _cursorX;
_savedCursorY = _cursorY;
_savedCursorStyle.foreground = terminal.cursor.foreground;
_savedCursorStyle.background = terminal.cursor.background;
_savedCursorStyle.attrs = terminal.cursor.attrs;
charset.save();
}
/// Restore cursor position, charmap and text attributes.
void restoreCursor() {
_cursorX = _savedCursorX;
_cursorY = _savedCursorY;
terminal.cursor.foreground = _savedCursorStyle.foreground;
terminal.cursor.background = _savedCursorStyle.background;
terminal.cursor.attrs = _savedCursorStyle.attrs;
charset.restore();
}
/// Sets the vertical scrolling margin to [top] and [bottom].
/// Both values must be between 0 and [viewHeight] - 1.
void setVerticalMargins(int top, int bottom) {
_marginTop = top.clamp(0, viewHeight - 1);
_marginBottom = bottom.clamp(0, viewHeight - 1);
_marginTop = min(_marginTop, _marginBottom);
_marginBottom = max(_marginTop, _marginBottom);
}
bool get isInVerticalMargin {
return _cursorY >= _marginTop && _cursorY <= _marginBottom;
}
void resetVerticalMargins() {
setVerticalMargins(0, viewHeight - 1);
}
void deleteChars(int count) {
final start = _cursorX.clamp(0, viewWidth);
count = min(count, viewWidth - start);
currentLine.removeCells(start, count, terminal.cursor);
}
/// Remove all lines above the top of the viewport.
void clearScrollback() {
if (height <= viewHeight) {
return;
}
lines.trimStart(scrollBack);
}
/// Clears the viewport and scrollback buffer. Then fill with empty lines.
void clear() {
lines.clear();
for (int i = 0; i < viewHeight; i++) {
lines.push(_newEmptyLine());
}
}
void insertBlankChars(int count) {
currentLine.insertCells(_cursorX, count, terminal.cursor);
}
void insertLines(int count) {
if (!isInVerticalMargin) {
return;
}
setCursorX(0);
// Number of lines from the cursor to the bottom of the scrollable region
// including the cursor itself.
final linesBelow = absoluteMarginBottom - absoluteCursorY + 1;
// Number of empty lines to insert.
final linesToInsert = min(count, linesBelow);
// Number of lines to move up.
final linesToMove = linesBelow - linesToInsert;
for (var i = 0; i < linesToMove; i++) {
final index = absoluteMarginBottom - i;
lines[index] = lines.swap(index - linesToInsert, _newEmptyLine());
}
for (var i = linesToMove; i < linesToInsert; i++) {
lines[absoluteCursorY + i] = _newEmptyLine();
}
}
/// Remove [count] lines starting at the current cursor position. Lines below
/// the removed lines are shifted up. This only affects the scrollable region.
/// Lines outside the scrollable region are not affected.
void deleteLines(int count) {
if (!isInVerticalMargin) {
return;
}
setCursorX(0);
count = min(count, absoluteMarginBottom - absoluteCursorY + 1);
final linesToMove = absoluteMarginBottom - absoluteCursorY + 1 - count;
for (var i = 0; i < linesToMove; i++) {
final index = absoluteCursorY + i;
lines[index] = lines[index + count];
}
for (var i = 0; i < count; i++) {
lines[absoluteMarginBottom - i] = _newEmptyLine();
}
}
void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
// 1. Adjust the height.
if (newHeight > oldHeight) {
// Grow larger
for (var i = 0; i < newHeight - oldHeight; i++) {
if (newHeight > lines.length) {
lines.push(_newEmptyLine(newWidth));
} else {
_cursorY++;
}
}
} else {
// Shrink smaller
for (var i = 0; i < oldHeight - newHeight; i++) {
if (_cursorY > newHeight - 1) {
_cursorY--;
} else {
lines.pop();
}
}
}
// Ensure cursor is within the screen.
_cursorX = _cursorX.clamp(0, newWidth - 1);
_cursorY = _cursorY.clamp(0, newHeight - 1);
// 2. Adjust the width.
if (newWidth != oldWidth) {
if (terminal.reflowEnabled && !isAltBuffer) {
final reflowResult = reflow(lines, oldWidth, newWidth);
while (reflowResult.length < newHeight) {
reflowResult.add(_newEmptyLine(newWidth));
}
lines.replaceWith(reflowResult);
} else {
lines.forEach((item) => item.resize(newWidth));
}
}
}
/// Create a new [CellAnchor] at the specified [x] and [y] coordinates.
CellAnchor createAnchor(int x, int y) {
return lines[y].createAnchor(x);
}
/// Create a new [CellAnchor] at the specified [x] and [y] coordinates.
CellAnchor createAnchorFromOffset(CellOffset offset) {
return lines[offset.y].createAnchor(offset.x);
}
CellAnchor createAnchorFromCursor() {
return createAnchor(cursorX, absoluteCursorY);
}
/// Create a new empty [BufferLine] with the current [viewWidth] if [width]
/// is not specified.
BufferLine _newEmptyLine([int? width]) {
final line = BufferLine(width ?? viewWidth);
return line;
}
static final defaultWordSeparators = <int>{
0,
r' '.codeUnitAt(0),
r'.'.codeUnitAt(0),
r':'.codeUnitAt(0),
r'-'.codeUnitAt(0),
r'\'.codeUnitAt(0),
r'"'.codeUnitAt(0),
r'*'.codeUnitAt(0),
r'+'.codeUnitAt(0),
r'/'.codeUnitAt(0),
r'\'.codeUnitAt(0),
};
BufferRangeLine? getWordBoundary(CellOffset position) {
var separators = wordSeparators ?? defaultWordSeparators;
if (position.y >= lines.length) {
return null;
}
var line = lines[position.y];
var start = position.x;
var end = position.x;
do {
if (start == 0) {
break;
}
final char = line.getCodePoint(start - 1);
if (separators.contains(char)) {
break;
}
start--;
} while (true);
do {
if (end >= viewWidth) {
break;
}
final char = line.getCodePoint(end);
if (separators.contains(char)) {
break;
}
end++;
} while (true);
if (start == end) {
return null;
}
return BufferRangeLine(
CellOffset(start, position.y),
CellOffset(end, position.y),
);
}
/// Get the plain text content of the buffer including the scrollback.
/// Accepts an optional [range] to get a specific part of the buffer.
String getText([BufferRange? range]) {
range ??= BufferRangeLine(
CellOffset(0, 0),
CellOffset(viewWidth - 1, height - 1),
);
range = range.normalized;
final builder = StringBuffer();
for (var segment in range.toSegments()) {
if (segment.line < 0 || segment.line >= height) {
continue;
}
final line = lines[segment.line];
if (!(segment.line == range.begin.y ||
segment.line == 0 ||
line.isWrapped)) {
builder.write("\n");
}
builder.write(line.getText(segment.start, segment.end));
}
return builder.toString();
}
/// Returns a debug representation of the buffer.
@override
String toString() {
final builder = StringBuffer();
final lineNumberLength = lines.length.toString().length;
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
builder.write('${i.toString().padLeft(lineNumberLength)}: |${lines[i]}|');
if (line.isWrapped) {
builder.write(' (⏎)');
}
builder.write('\n');
}
return builder.toString();
}
}
@@ -0,0 +1,11 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
abstract class CellFlags {
static const bold = 1 << 0;
static const faint = 1 << 1;
static const italic = 1 << 2;
static const underline = 1 << 3;
static const blink = 1 << 4;
static const inverse = 1 << 5;
static const invisible = 1 << 6;
}
@@ -0,0 +1,57 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/range.dart';
class CellOffset {
final int x;
final int y;
const CellOffset(this.x, this.y);
bool isEqual(CellOffset other) {
return other.x == x && other.y == y;
}
bool isBefore(CellOffset other) {
return y < other.y || (y == other.y && x < other.x);
}
bool isAfter(CellOffset other) {
return y > other.y || (y == other.y && x > other.x);
}
bool isBeforeOrSame(CellOffset other) {
return y < other.y || (y == other.y && x <= other.x);
}
bool isAfterOrSame(CellOffset other) {
return y > other.y || (y == other.y && x >= other.x);
}
bool isAtSameRow(CellOffset other) {
return y == other.y;
}
bool isAtSameColumn(CellOffset other) {
return x == other.x;
}
bool isWithin(BufferRange range) {
return range.contains(this);
}
@override
String toString() => 'CellOffset($x, $y)';
@override
int get hashCode => x.hashCode ^ y.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CellOffset &&
runtimeType == other.runtimeType &&
x == other.x &&
y == other.y;
}
+420
View File
@@ -0,0 +1,420 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show min;
import 'dart:typed_data';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/cell.dart';
import 'package:clide/src/terminal/src/core/cursor.dart';
import 'package:clide/src/terminal/src/utils/circular_buffer.dart';
import 'package:clide/src/terminal/src/utils/unicode_v11.dart';
const _cellSize = 4;
const _cellForeground = 0;
const _cellBackground = 1;
const _cellAttributes = 2;
const _cellContent = 3;
class BufferLine with IndexedItem {
BufferLine(
this._length, {
this.isWrapped = false,
}) : _data = Uint32List(_calcCapacity(_length) * _cellSize);
int _length;
Uint32List _data;
Uint32List get data => _data;
var isWrapped = false;
int get length => _length;
final _anchors = <CellAnchor>[];
List<CellAnchor> get anchors => _anchors;
int getForeground(int index) {
return _data[index * _cellSize + _cellForeground];
}
int getBackground(int index) {
return _data[index * _cellSize + _cellBackground];
}
int getAttributes(int index) {
return _data[index * _cellSize + _cellAttributes];
}
int getContent(int index) {
return _data[index * _cellSize + _cellContent];
}
int getCodePoint(int index) {
return _data[index * _cellSize + _cellContent] & CellContent.codepointMask;
}
int getWidth(int index) {
return _data[index * _cellSize + _cellContent] >> CellContent.widthShift;
}
void getCellData(int index, CellData cellData) {
final offset = index * _cellSize;
cellData.foreground = _data[offset + _cellForeground];
cellData.background = _data[offset + _cellBackground];
cellData.flags = _data[offset + _cellAttributes];
cellData.content = _data[offset + _cellContent];
}
CellData createCellData(int index) {
final cellData = CellData.empty();
final offset = index * _cellSize;
_data[offset + _cellForeground] = cellData.foreground;
_data[offset + _cellBackground] = cellData.background;
_data[offset + _cellAttributes] = cellData.flags;
_data[offset + _cellContent] = cellData.content;
return cellData;
}
void setForeground(int index, int value) {
_data[index * _cellSize + _cellForeground] = value;
}
void setBackground(int index, int value) {
_data[index * _cellSize + _cellBackground] = value;
}
void setAttributes(int index, int value) {
_data[index * _cellSize + _cellAttributes] = value;
}
void setContent(int index, int value) {
_data[index * _cellSize + _cellContent] = value;
}
void setCodePoint(int index, int char) {
final width = unicodeV11.wcwidth(char);
setContent(index, char | (width << CellContent.widthShift));
}
void setCell(int index, int char, int witdh, CursorStyle style) {
final offset = index * _cellSize;
_data[offset + _cellForeground] = style.foreground;
_data[offset + _cellBackground] = style.background;
_data[offset + _cellAttributes] = style.attrs;
_data[offset + _cellContent] = char | (witdh << CellContent.widthShift);
}
void setCellData(int index, CellData cellData) {
final offset = index * _cellSize;
_data[offset + _cellForeground] = cellData.foreground;
_data[offset + _cellBackground] = cellData.background;
_data[offset + _cellAttributes] = cellData.flags;
_data[offset + _cellContent] = cellData.content;
}
void eraseCell(int index, CursorStyle style) {
final offset = index * _cellSize;
_data[offset + _cellForeground] = style.foreground;
_data[offset + _cellBackground] = style.background;
_data[offset + _cellAttributes] = style.attrs;
_data[offset + _cellContent] = 0;
}
void resetCell(int index) {
final offset = index * _cellSize;
_data[offset + _cellForeground] = 0;
_data[offset + _cellBackground] = 0;
_data[offset + _cellAttributes] = 0;
_data[offset + _cellContent] = 0;
}
/// Erase cells whose index satisfies [start] <= index < [end]. Erased cells
/// are filled with [style].
void eraseRange(int start, int end, CursorStyle style) {
// reset cell one to the left if start is second cell of a wide char
if (start > 0 && getWidth(start - 1) == 2) {
eraseCell(start - 1, style);
}
// reset cell one to the right if end is second cell of a wide char
if (end < _length && getWidth(end - 1) == 2) {
eraseCell(end - 1, style);
}
end = min(end, _length);
for (var i = start; i < end; i++) {
eraseCell(i, style);
}
}
/// Remove [count] cells starting at [start]. Cells that are empty after the
/// removal are filled with [style].
void removeCells(int start, int count, [CursorStyle? style]) {
assert(start >= 0 && start < _length);
assert(count >= 0 && start + count <= _length);
style ??= CursorStyle.empty;
if (start + count < _length) {
final moveStart = start * _cellSize;
final moveEnd = (_length - count) * _cellSize;
final moveOffset = count * _cellSize;
for (var i = moveStart; i < moveEnd; i++) {
_data[i] = _data[i + moveOffset];
}
}
for (var i = _length - count; i < _length; i++) {
eraseCell(i, style);
}
if (start > 0 && getWidth(start - 1) == 2) {
eraseCell(start - 1, style);
}
// Update anchors, remove anchors that are inside the removed range.
for (var i = 0; i < _anchors.length; i++) {
final anchor = _anchors[i];
if (anchor.x >= start) {
if (anchor.x < start + count) {
anchor.dispose();
} else {
anchor.reposition(anchor.x - count);
}
}
}
}
/// Inserts [count] cells at [start]. New cells are initialized with [style].
void insertCells(int start, int count, [CursorStyle? style]) {
style ??= CursorStyle.empty;
if (start > 0 && getWidth(start - 1) == 2) {
eraseCell(start - 1, style);
}
if (start + count < _length) {
final moveStart = start * _cellSize;
final moveEnd = (_length - count) * _cellSize;
final moveOffset = count * _cellSize;
for (var i = moveEnd - 1; i >= moveStart; i--) {
_data[i + moveOffset] = _data[i];
}
}
final end = min(start + count, _length);
for (var i = start; i < end; i++) {
eraseCell(i, style);
}
if (getWidth(_length - 1) == 2) {
eraseCell(_length - 1, style);
}
// Update anchors, move anchors that are after the inserted range.
for (var i = 0; i < _anchors.length; i++) {
final anchor = _anchors[i];
if (anchor.x >= start + count) {
anchor.reposition(anchor.x + count);
// Remove anchors that are now outside the buffer.
if (anchor.x >= _length) {
anchor.dispose();
}
}
}
}
void resize(int length) {
assert(length >= 0);
if (length == _length) {
return;
}
if (length > _length) {
final newBufferSize = _calcCapacity(length) * _cellSize;
if (newBufferSize > _data.length) {
final newBuffer = Uint32List(newBufferSize);
newBuffer.setRange(0, _data.length, _data);
_data = newBuffer;
}
}
_length = length;
for (var i = 0; i < _anchors.length; i++) {
final anchor = _anchors[i];
if (anchor.x > _length) {
anchor.reposition(_length);
}
}
}
/// Returns the offset of the last cell that has content from the start of
/// the line.
int getTrimmedLength([int? cols]) {
final maxCols = _data.length ~/ _cellSize;
if (cols == null || cols > maxCols) {
cols = maxCols;
}
if (cols <= 0) {
return 0;
}
for (var i = cols - 1; i >= 0; i--) {
var codePoint = getCodePoint(i);
if (codePoint != 0) {
// we are at the last cell in this line that has content.
// the length of this line is the index of this cell + 1
// the only exception is that if that last cell is wider
// than 1 then we have to add the diff
final lastCellWidth = getWidth(i);
return i + lastCellWidth;
}
}
return 0;
}
/// Copies [len] cells from [src] starting at [srcCol] to [dstCol] at this
/// line.
void copyFrom(BufferLine src, int srcCol, int dstCol, int len) {
resize(dstCol + len);
// data.setRange(
// dstCol * _cellSize,
// (dstCol + len) * _cellSize,
// Uint32List.sublistView(src.data, srcCol * _cellSize, len * _cellSize),
// );
var srcOffset = srcCol * _cellSize;
var dstOffset = dstCol * _cellSize;
for (var i = 0; i < len * _cellSize; i++) {
_data[dstOffset++] = src._data[srcOffset++];
}
}
static int _calcCapacity(int length) {
assert(length >= 0);
var capacity = 64;
if (length < 256) {
while (capacity < length) {
capacity *= 2;
}
} else {
capacity = 256;
while (capacity < length) {
capacity += 32;
}
}
return capacity;
}
String getText([int? from, int? to]) {
if (from == null || from < 0) {
from = 0;
}
if (to == null || to > _length) {
to = _length;
}
final builder = StringBuffer();
for (var i = from; i < to; i++) {
final codePoint = getCodePoint(i);
final width = getWidth(i);
if (codePoint != 0 && i + width <= to) {
builder.writeCharCode(codePoint);
}
}
return builder.toString();
}
CellAnchor createAnchor(int offset) {
final anchor = CellAnchor(offset, owner: this);
_anchors.add(anchor);
return anchor;
}
void dispose() {
for (final anchor in _anchors) {
anchor.dispose();
}
}
@override
String toString() {
return getText();
}
}
/// A handle to a cell in a [BufferLine] that can be used to track the location
/// of the cell. Anchors are guaranteed to be stable, retaining their relative
/// position to each other after mutations to the buffer.
class CellAnchor {
CellAnchor(int offset, {BufferLine? owner})
: _offset = offset,
_owner = owner;
int _offset;
int get x {
return _offset;
}
int get y {
assert(attached);
return _owner!.index;
}
CellOffset get offset {
assert(attached);
return CellOffset(_offset, _owner!.index);
}
BufferLine? _owner;
BufferLine? get line => _owner;
bool get attached => _owner?.attached ?? false;
void reparent(BufferLine owner, int offset) {
_owner?._anchors.remove(this);
_owner = owner;
_owner?._anchors.add(this);
_offset = offset;
}
void reposition(int offset) {
_offset = offset;
}
void dispose() {
_owner?._anchors.remove(this);
_owner = null;
}
@override
String toString() {
if (attached) {
return 'CellAnchor($x, $y)';
} else {
return 'CellAnchor($x, detached)';
}
}
}
@@ -0,0 +1,57 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/segment.dart';
abstract class BufferRange {
final CellOffset begin;
final CellOffset end;
const BufferRange(this.begin, this.end);
BufferRange.collapsed(this.begin) : end = begin;
bool get isNormalized {
return begin.isBefore(end) || begin.isEqual(end);
}
bool get isCollapsed {
return begin.isEqual(end);
}
BufferRange get normalized;
/// Convert this range to segments of single lines.
Iterable<BufferSegment> toSegments();
/// Returns true if the given[position] is within this range.
bool contains(CellOffset position);
/// Returns the smallest range that contains both this range and the given
/// [range].
BufferRange merge(BufferRange range);
/// Returns the smallest range that contains both this range and the given
/// [position].
BufferRange extend(CellOffset position);
@override
operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! BufferRange) {
return false;
}
return begin == other.begin && end == other.end;
}
@override
int get hashCode => begin.hashCode ^ end.hashCode;
@override
String toString() => 'Range($begin, $end)';
}
@@ -0,0 +1,113 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
import 'package:clide/src/terminal/src/core/buffer/segment.dart';
class BufferRangeBlock extends BufferRange {
BufferRangeBlock(super.begin, super.end);
BufferRangeBlock.collapsed(super.begin) : super.collapsed();
@override
bool get isNormalized {
// A block range is normalized if begin is the top left corner of the range
// and end the bottom right corner.
return (begin.isBefore(end) && begin.x <= end.x) || begin.isEqual(end);
}
@override
BufferRangeBlock get normalized {
if (isNormalized) {
return this;
}
// Determine new normalized begin and end offset, such that begin is the
// top left corner and end is the bottom right corner of the block.
final normalBegin = CellOffset(min(begin.x, end.x), min(begin.y, end.y));
final normalEnd = CellOffset(max(begin.x, end.x), max(begin.y, end.y));
return BufferRangeBlock(normalBegin, normalEnd);
}
@override
Iterable<BufferSegment> toSegments() sync* {
var begin = this.begin;
var end = this.end;
if (!isNormalized) {
end = this.begin;
begin = this.end;
}
final startX = min(begin.x, end.x);
final endX = max(begin.x, end.x);
for (var i = begin.y; i <= end.y; i++) {
yield BufferSegment(this, i, startX, endX);
}
}
@override
bool contains(CellOffset position) {
var begin = this.begin;
var end = this.end;
if (!isNormalized) {
end = this.begin;
begin = this.end;
}
if (!(begin.y <= position.y && position.y <= end.y)) {
return false;
}
final startX = min(begin.x, end.x);
final endX = max(begin.x, end.x);
return startX <= position.x && position.x <= endX;
}
@override
BufferRangeBlock merge(BufferRange range) {
// Enlarge the block such that both borders of the range
// are within the selected block.
return extend(range.begin).extend(range.end);
}
@override
BufferRangeBlock extend(CellOffset position) {
// If the position is within the block, there is nothing to do.
if (contains(position)) {
return this;
}
// Otherwise normalize the block and push the borders outside up to
// the position to which the block has to extended.
final normal = normalized;
final extendBegin = CellOffset(
min(normal.begin.x, position.x),
min(normal.begin.y, position.y),
);
final extendEnd = CellOffset(
max(normal.end.x, position.x),
max(normal.end.y, position.y),
);
return BufferRangeBlock(extendBegin, extendEnd);
}
@override
operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! BufferRangeBlock) {
return false;
}
return begin == other.begin && end == other.end;
}
@override
int get hashCode => begin.hashCode ^ end.hashCode;
@override
String toString() => 'Block Range($begin, $end)';
}
@@ -0,0 +1,68 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
import 'package:clide/src/terminal/src/core/buffer/segment.dart';
class BufferRangeLine extends BufferRange {
BufferRangeLine(super.begin, super.end);
BufferRangeLine.collapsed(super.begin) : super.collapsed();
@override
BufferRangeLine get normalized {
return isNormalized ? this : BufferRangeLine(end, begin);
}
@override
Iterable<BufferSegment> toSegments() sync* {
final self = normalized;
for (var i = self.begin.y; i <= self.end.y; i++) {
var startX = i == self.begin.y ? self.begin.x : null;
var endX = i == self.end.y ? self.end.x : null;
yield BufferSegment(this, i, startX, endX);
}
}
@override
bool contains(CellOffset position) {
final self = normalized;
return self.begin.isBeforeOrSame(position) &&
self.end.isAfterOrSame(position);
}
@override
BufferRangeLine merge(BufferRange range) {
final self = normalized;
final begin = self.begin.isBefore(range.begin) ? self.begin : range.begin;
final end = self.end.isAfter(range.end) ? self.end : range.end;
return BufferRangeLine(begin, end);
}
@override
BufferRangeLine extend(CellOffset position) {
final self = normalized;
final begin = self.begin.isAfter(position) ? position : self.begin;
final end = self.end.isBefore(position) ? position : self.end;
return BufferRangeLine(begin, end);
}
@override
operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! BufferRangeLine) {
return false;
}
return begin == other.begin && end == other.end;
}
@override
int get hashCode => begin.hashCode ^ end.hashCode;
@override
String toString() => 'Line Range($begin, $end)';
}
@@ -0,0 +1,60 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
/// A BufferSegment represents a range within a line.
class BufferSegment {
/// The range that this segment belongs to.
final BufferRange range;
/// The line that this segment resides on.
final int line;
/// The start position of this segment. [null] means the start of the line.
final int? start;
/// The end position of this segment. [null] means the end of the line.
/// Should be greater than or equal to [start].
final int? end;
const BufferSegment(this.range, this.line, this.start, this.end)
: assert((start != null && end != null) ? start <= end : true);
bool isWithin(CellOffset position) {
if (position.y != line) {
return false;
}
if (start != null && position.x < start!) {
return false;
}
if (end != null && position.x > end!) {
return false;
}
return true;
}
@override
String toString() {
final start = this.start != null ? this.start.toString() : 'start';
final end = this.end != null ? this.end.toString() : 'end';
return 'Segment($line, $start -> $end)';
}
@override
int get hashCode =>
range.hashCode ^ line.hashCode ^ start.hashCode ^ end.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BufferSegment &&
runtimeType == other.runtimeType &&
range == other.range &&
line == other.line &&
start == other.start &&
end == other.end;
}
+68
View File
@@ -0,0 +1,68 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/utils/hash_values.dart';
class CellData {
CellData({
required this.foreground,
required this.background,
required this.flags,
required this.content,
});
factory CellData.empty() {
return CellData(
foreground: 0,
background: 0,
flags: 0,
content: 0,
);
}
int foreground;
int background;
int flags;
int content;
int getHash() {
return hashValues(foreground, background, flags, content);
}
@override
String toString() {
return 'CellData{foreground: $foreground, background: $background, flags: $flags, content: $content}';
}
}
abstract class CellAttr {
static const bold = 1 << 0;
static const faint = 1 << 1;
static const italic = 1 << 2;
static const underline = 1 << 3;
static const blink = 1 << 4;
static const inverse = 1 << 5;
static const invisible = 1 << 6;
static const strikethrough = 1 << 7;
}
abstract class CellColor {
static const valueMask = 0xFFFFFF;
static const typeShift = 25;
static const typeMask = 3 << typeShift;
static const normal = 0 << typeShift;
static const named = 1 << typeShift;
static const palette = 2 << typeShift;
static const rgb = 3 << typeShift;
}
abstract class CellContent {
static const codepointMask = 0x1fffff;
static const widthShift = 22;
// static const widthMask = 3 << widthShift;
}
+97
View File
@@ -0,0 +1,97 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
typedef CharsetTranslator = int Function(int);
final _charsets = <int, CharsetTranslator>{
'0'.codeUnitAt(0): decSpecGraphicsTranslator,
'B'.codeUnitAt(0): asciiTranslator,
};
class Charset {
var _charsetMap = <int, CharsetTranslator>{};
var _currentIndex = 0;
var _savedCharsetMap = <int, CharsetTranslator>{};
var _savedIndex = 0;
var _cached = asciiTranslator;
void _updateCache() {
_cached = _charsetMap[_currentIndex] ?? asciiTranslator;
}
int translate(int codePoint) {
return _cached(codePoint);
}
void designate(int index, int name) {
final charset = _charsets[name];
if (charset != null) {
_charsetMap[index] = charset;
_updateCache();
}
}
void use(int index) {
_currentIndex = index;
_updateCache();
}
void save() {
_savedCharsetMap = Map.from(_charsetMap);
_savedIndex = _currentIndex;
}
void restore() {
_charsetMap = _savedCharsetMap;
_currentIndex = _savedIndex;
_updateCache();
}
}
const decSpecGraphics = <int, int>{
0x5f: 0x00A0, // NO-BREAK SPACE
0x60: 0x25C6, // BLACK DIAMOND
0x61: 0x2592, // MEDIUM SHADE
0x62: 0x2409, // SYMBOL FOR HORIZONTAL TABULATION
0x63: 0x240C, // SYMBOL FOR FORM FEED
0x64: 0x240D, // SYMBOL FOR CARRIAGE RETURN
0x65: 0x240A, // SYMBOL FOR LINE FEED
0x66: 0x00B0, // DEGREE SIGN
0x67: 0x00B1, // PLUS-MINUS SIGN
0x68: 0x2424, // SYMBOL FOR NEWLINE
0x69: 0x240B, // SYMBOL FOR VERTICAL TABULATION
0x6a: 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT
0x6b: 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT
0x6c: 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT
0x6d: 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT
0x6e: 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
0x6f: 0x23BA, // HORIZONTAL SCAN LINE-1
0x70: 0x23BB, // HORIZONTAL SCAN LINE-3
0x71: 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL
0x72: 0x23BC, // HORIZONTAL SCAN LINE-7
0x73: 0x23BD, // HORIZONTAL SCAN LINE-9
0x74: 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT
0x75: 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT
0x76: 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL
0x77: 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
0x78: 0x2502, // BOX DRAWINGS LIGHT VERTICAL
0x79: 0x2264, // LESS-THAN OR EQUAL TO
0x7a: 0x2265, // GREATER-THAN OR EQUAL TO
0x7b: 0x03C0, // GREEK SMALL LETTER PI
0x7c: 0x2260, // NOT EQUAL TO
0x7d: 0x00A3, // POUND SIGN
0x7e: 0x00B7, // MIDDLE DOT
};
int asciiTranslator(int codePoint) {
return codePoint;
}
int decSpecGraphicsTranslator(int codePoint) {
if (codePoint >= 127) {
return codePoint;
}
return decSpecGraphics[codePoint] ?? codePoint;
}
+21
View File
@@ -0,0 +1,21 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
abstract class NamedColor {
static const black = 0;
static const red = 1;
static const green = 2;
static const yellow = 3;
static const blue = 4;
static const magenta = 5;
static const cyan = 6;
static const white = 7;
static const brightBlack = 8;
static const brightRed = 9;
static const brightGreen = 10;
static const brightYellow = 11;
static const brightBlue = 12;
static const brightMagenta = 13;
static const brightCyan = 14;
static const brightWhite = 15;
}
+139
View File
@@ -0,0 +1,139 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/cell.dart';
class CursorStyle {
int foreground;
int background;
int attrs;
CursorStyle({this.foreground = 0, this.background = 0, this.attrs = 0});
static final empty = CursorStyle();
void setBold() {
attrs |= CellAttr.bold;
}
void setFaint() {
attrs |= CellAttr.faint;
}
void setItalic() {
attrs |= CellAttr.italic;
}
void setUnderline() {
attrs |= CellAttr.underline;
}
void setBlink() {
attrs |= CellAttr.blink;
}
void setInverse() {
attrs |= CellAttr.inverse;
}
void setInvisible() {
attrs |= CellAttr.invisible;
}
void setStrikethrough() {
attrs |= CellAttr.strikethrough;
}
void unsetBold() {
attrs &= ~CellAttr.bold;
}
void unsetFaint() {
attrs &= ~CellAttr.faint;
}
void unsetItalic() {
attrs &= ~CellAttr.italic;
}
void unsetUnderline() {
attrs &= ~CellAttr.underline;
}
void unsetBlink() {
attrs &= ~CellAttr.blink;
}
void unsetInverse() {
attrs &= ~CellAttr.inverse;
}
void unsetInvisible() {
attrs &= ~CellAttr.invisible;
}
void unsetStrikethrough() {
attrs &= ~CellAttr.strikethrough;
}
bool get isBold => (attrs & CellAttr.bold) != 0;
bool get isFaint => (attrs & CellAttr.faint) != 0;
bool get isItalis => (attrs & CellAttr.italic) != 0;
bool get isUnderline => (attrs & CellAttr.underline) != 0;
bool get isBlink => (attrs & CellAttr.blink) != 0;
bool get isInverse => (attrs & CellAttr.inverse) != 0;
bool get isInvisible => (attrs & CellAttr.invisible) != 0;
void setForegroundColor16(int color) {
foreground = color | CellColor.named;
}
void setForegroundColor256(int color) {
foreground = color | CellColor.palette;
}
void setForegroundColorRgb(int r, int g, int b) {
foreground = (r << 16) | (g << 8) | b | CellColor.rgb;
}
void resetForegroundColor() {
foreground = 0; // | CellColor.normal;
}
void setBackgroundColor16(int color) {
background = color | CellColor.named;
}
void setBackgroundColor256(int color) {
background = color | CellColor.palette;
}
void setBackgroundColorRgb(int r, int g, int b) {
background = (r << 16) | (g << 8) | b | CellColor.rgb;
}
void resetBackgroundColor() {
background = 0; // | CellColor.normal;
}
void reset() {
foreground = 0;
background = 0;
attrs = 0;
}
}
class CursorPosition {
int x;
int y;
CursorPosition(this.x, this.y);
}
@@ -0,0 +1,35 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
class EscapeEmitter {
const EscapeEmitter();
String primaryDeviceAttributes() {
return '\x1b[?1;2c';
}
String secondaryDeviceAttributes() {
const model = 0;
const version = 0;
return '\x1b[>$model;$version;0c';
}
String tertiaryDeviceAttributes() {
return '\x1bP!|00000000\x1b\\';
}
String operatingStatus() {
return '\x1b[0n';
}
String cursorPosition(int x, int y) {
return '\x1b[$y;${x}R';
}
String bracketedPaste(String text) {
return '\x1b[200~$text\x1b[201~';
}
String size(int rows, int cols) {
return '\x1b[8;$rows;${cols}t';
}
}
@@ -0,0 +1,217 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
abstract class EscapeHandler {
void writeChar(int char);
/* SBC */
void bell();
void backspaceReturn();
void tab();
void lineFeed();
void carriageReturn();
void shiftOut();
void shiftIn();
void unknownSBC(int char);
/* ANSI sequence */
void saveCursor();
void restoreCursor();
void index();
void nextLine();
void setTapStop();
void reverseIndex();
void designateCharset(int charset, int name);
void unkownEscape(int char);
/* CSI */
void repeatPreviousCharacter(int n);
void setCursor(int x, int y);
void setCursorX(int x);
void setCursorY(int y);
void sendPrimaryDeviceAttributes();
void clearTabStopUnderCursor();
void clearAllTabStops();
void moveCursorX(int offset);
void moveCursorY(int n);
void sendSecondaryDeviceAttributes();
void sendTertiaryDeviceAttributes();
void sendOperatingStatus();
void sendCursorPosition();
void setMargins(int i, [int? bottom]);
void cursorNextLine(int amount);
void cursorPrecedingLine(int amount);
void eraseDisplayBelow();
void eraseDisplayAbove();
void eraseDisplay();
void eraseScrollbackOnly();
void eraseLineRight();
void eraseLineLeft();
void eraseLine();
void insertLines(int amount);
void deleteLines(int amount);
void deleteChars(int amount);
void scrollUp(int amount);
void scrollDown(int amount);
void eraseChars(int amount);
void insertBlankChars(int amount);
void unknownCSI(int finalByte);
/* Modes */
void setInsertMode(bool enabled);
void setLineFeedMode(bool enabled);
void setUnknownMode(int mode, bool enabled);
/* DEC Private modes */
void setCursorKeysMode(bool enabled);
void setReverseDisplayMode(bool enabled);
void setOriginMode(bool enabled);
void setColumnMode(bool enabled);
void setAutoWrapMode(bool enabled);
void setMouseMode(MouseMode mode);
void setCursorBlinkMode(bool enabled);
void setCursorVisibleMode(bool enabled);
void useAltBuffer();
void useMainBuffer();
void clearAltBuffer();
void setAppKeypadMode(bool enabled);
void setReportFocusMode(bool enabled);
void setMouseReportMode(MouseReportMode mode);
void setAltBufferMouseScrollMode(bool enabled);
void setBracketedPasteMode(bool enabled);
void setUnknownDecMode(int mode, bool enabled);
void resize(int cols, int rows);
void sendSize();
/* Select Graphic Rendition (SGR) */
void resetCursorStyle();
void setCursorBold();
void setCursorFaint();
void setCursorItalic();
void setCursorUnderline();
void setCursorBlink();
void setCursorInverse();
void setCursorInvisible();
void setCursorStrikethrough();
void unsetCursorBold();
void unsetCursorFaint();
void unsetCursorItalic();
void unsetCursorUnderline();
void unsetCursorBlink();
void unsetCursorInverse();
void unsetCursorInvisible();
void unsetCursorStrikethrough();
void setForegroundColor16(int color);
void setForegroundColor256(int index);
void setForegroundColorRgb(int r, int g, int b);
void resetForeground();
void setBackgroundColor16(int color);
void setBackgroundColor256(int index);
void setBackgroundColorRgb(int r, int g, int b);
void resetBackground();
void unsupportedStyle(int param);
/* OSC */
void setTitle(String name);
void setIconName(String name);
void unknownOSC(String code, List<String> args);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,212 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keys.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab.dart';
import 'package:clide/src/terminal/src/core/state.dart';
import 'package:clide/src/terminal/src/core/platform.dart';
/// The key event received from the keyboard, along with the state of the
/// modifier keys and state of the terminal. Typically consumed by the
/// [TerminalInputHandler] to produce a escape sequence that can be recognized
/// by the terminal.
///
/// See also:
/// - [TerminalInputHandler]
class TerminalKeyboardEvent {
final TerminalKey key;
final bool shift;
final bool ctrl;
final bool alt;
final TerminalState state;
final bool altBuffer;
final TerminalTargetPlatform platform;
TerminalKeyboardEvent({
required this.key,
required this.shift,
required this.ctrl,
required this.alt,
required this.state,
required this.altBuffer,
required this.platform,
});
TerminalKeyboardEvent copyWith({
TerminalKey? key,
bool? shift,
bool? ctrl,
bool? alt,
TerminalState? state,
bool? altBuffer,
TerminalTargetPlatform? platform,
}) {
return TerminalKeyboardEvent(
key: key ?? this.key,
shift: shift ?? this.shift,
ctrl: ctrl ?? this.ctrl,
alt: alt ?? this.alt,
state: state ?? this.state,
altBuffer: altBuffer ?? this.altBuffer,
platform: platform ?? this.platform,
);
}
}
/// TerminalInputHandler contains the logic for translating a [TerminalKeyboardEvent]
/// into escape sequences that can be recognized by the terminal.
abstract class TerminalInputHandler {
/// Translates a [TerminalKeyboardEvent] into an escape sequence. If the event
/// cannot be translated, null is returned.
String? call(TerminalKeyboardEvent event);
}
/// A [TerminalInputHandler] that chains multiple handlers together. If any
/// handler returns a non-null value, it is returned. Otherwise, null is
/// returned.
class CascadeInputHandler implements TerminalInputHandler {
final List<TerminalInputHandler> _handlers;
const CascadeInputHandler(this._handlers);
@override
String? call(TerminalKeyboardEvent event) {
for (var handler in _handlers) {
final result = handler(event);
if (result != null) {
return result;
}
}
return null;
}
}
/// The default input handler for the terminal. That is composed of a
/// [KeytabInputHandler], a [CtrlInputHandler], and a [AltInputHandler].
///
/// It's possible to override the default input handler behavior by chaining
/// another input handler before or after the default input handler using
/// [CascadeInputHandler].
///
/// See also:
/// * [CascadeInputHandler]
const defaultInputHandler = CascadeInputHandler([
KeytabInputHandler(),
CtrlInputHandler(),
AltInputHandler(),
]);
/// A [TerminalInputHandler] that translates key events according to a keytab
/// file. If no keytab is provided, [Keytab.defaultKeytab] is used.
class KeytabInputHandler implements TerminalInputHandler {
const KeytabInputHandler([this.keytab]);
final Keytab? keytab;
@override
String? call(TerminalKeyboardEvent event) {
final keytab = this.keytab ?? Keytab.defaultKeytab;
final record = keytab.find(
event.key,
ctrl: event.ctrl,
alt: event.alt,
shift: event.shift,
newLineMode: event.state.lineFeedMode,
appCursorKeys: event.state.appKeypadMode,
appKeyPad: event.state.appKeypadMode,
appScreen: event.altBuffer,
macos: event.platform == TerminalTargetPlatform.macos,
);
if (record == null) {
return null;
}
var result = record.action.unescapedValue();
result = insertModifiers(event, result);
return result;
}
String insertModifiers(TerminalKeyboardEvent event, String action) {
String? code;
if (event.shift && event.alt && event.ctrl) {
code = '8';
} else if (event.ctrl && event.alt) {
code = '7';
} else if (event.shift && event.ctrl) {
code = '6';
} else if (event.ctrl) {
code = '5';
} else if (event.shift && event.alt) {
code = '4';
} else if (event.alt) {
code = '3';
} else if (event.shift) {
code = '2';
}
if (code != null) {
return action.replaceAll('*', code);
}
return action;
}
}
/// A [TerminalInputHandler] that translates ctrl + key events into escape
/// sequences. For example, ctrl + a becomes ^A.
class CtrlInputHandler implements TerminalInputHandler {
const CtrlInputHandler();
@override
String? call(TerminalKeyboardEvent event) {
if (!event.ctrl || event.shift || event.alt) {
return null;
}
final key = event.key;
if (key.index >= TerminalKey.keyA.index &&
key.index <= TerminalKey.keyZ.index) {
final input = key.index - TerminalKey.keyA.index + 1;
return String.fromCharCode(input);
}
return null;
}
}
/// A [TerminalInputHandler] that translates alt + key events into escape
/// sequences. For example, alt + a becomes ^[a.
class AltInputHandler implements TerminalInputHandler {
const AltInputHandler();
@override
String? call(TerminalKeyboardEvent event) {
if (!event.alt || event.ctrl || event.shift) {
return null;
}
if (event.platform == TerminalTargetPlatform.macos) {
return null;
}
final key = event.key;
if (key.index >= TerminalKey.keyA.index &&
key.index <= TerminalKey.keyZ.index) {
final charCode = key.index - TerminalKey.keyA.index + 65;
final input = [0x1b, charCode];
return String.fromCharCodes(input);
}
return null;
}
}
+837
View File
@@ -0,0 +1,837 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalKey {
/// Represents the logical "None" key on the keyboard.
none,
/// Represents the logical "Hyper" key on the keyboard.
hyper,
/// Represents the logical "Super Key" key on the keyboard.
superKey,
/// Represents the logical "Fn Lock" key on the keyboard.
fnLock,
/// Represents the logical "Suspend" key on the keyboard.
suspend,
/// Represents the logical "Resume" key on the keyboard.
resume,
/// Represents the logical "Turbo" key on the keyboard.
turbo,
/// Represents the logical "Privacy Screen Toggle" key on the keyboard.
privacyScreenToggle,
/// Represents the logical "Sleep" key on the keyboard.
sleep,
/// Represents the logical "Wake Up" key on the keyboard.
wakeUp,
/// Represents the logical "Display Toggle Int Ext" key on the keyboard.
displayToggleIntExt,
/// Represents the logical "Usb Reserved" key on the keyboard.
usbReserved,
/// Represents the logical "Usb Error Roll Over" key on the keyboard.
usbErrorRollOver,
/// Represents the logical "Usb Post Fail" key on the keyboard.
usbPostFail,
/// Represents the logical "Usb Error Undefined" key on the keyboard.
usbErrorUndefined,
/// Represents the logical "Key A" key on the keyboard.
keyA,
/// Represents the logical "Key B" key on the keyboard.
keyB,
/// Represents the logical "Key C" key on the keyboard.
keyC,
/// Represents the logical "Key D" key on the keyboard.
keyD,
/// Represents the logical "Key E" key on the keyboard.
keyE,
/// Represents the logical "Key F" key on the keyboard.
keyF,
/// Represents the logical "Key G" key on the keyboard.
keyG,
/// Represents the logical "Key H" key on the keyboard.
keyH,
/// Represents the logical "Key I" key on the keyboard.
keyI,
/// Represents the logical "Key J" key on the keyboard.
keyJ,
/// Represents the logical "Key K" key on the keyboard.
keyK,
/// Represents the logical "Key L" key on the keyboard.
keyL,
/// Represents the logical "Key M" key on the keyboard.
keyM,
/// Represents the logical "Key N" key on the keyboard.
keyN,
/// Represents the logical "Key O" key on the keyboard.
keyO,
/// Represents the logical "Key P" key on the keyboard.
keyP,
/// Represents the logical "Key Q" key on the keyboard.
keyQ,
/// Represents the logical "Key R" key on the keyboard.
keyR,
/// Represents the logical "Key S" key on the keyboard.
keyS,
/// Represents the logical "Key T" key on the keyboard.
keyT,
/// Represents the logical "Key U" key on the keyboard.
keyU,
/// Represents the logical "Key V" key on the keyboard.
keyV,
/// Represents the logical "Key W" key on the keyboard.
keyW,
/// Represents the logical "Key X" key on the keyboard.
keyX,
/// Represents the logical "Key Y" key on the keyboard.
keyY,
/// Represents the logical "Key Z" key on the keyboard.
keyZ,
/// Represents the logical "Digit 1" key on the keyboard.
digit1,
/// Represents the logical "Digit 2" key on the keyboard.
digit2,
/// Represents the logical "Digit 3" key on the keyboard.
digit3,
/// Represents the logical "Digit 4" key on the keyboard.
digit4,
/// Represents the logical "Digit 5" key on the keyboard.
digit5,
/// Represents the logical "Digit 6" key on the keyboard.
digit6,
/// Represents the logical "Digit 7" key on the keyboard.
digit7,
/// Represents the logical "Digit 8" key on the keyboard.
digit8,
/// Represents the logical "Digit 9" key on the keyboard.
digit9,
/// Represents the logical "Digit 0" key on the keyboard.
digit0,
/// Represents the logical "Enter" key on the keyboard.
enter,
/// Represents the logical "Escape" key on the keyboard.
escape,
/// Represents the logical "Backspace" key on the keyboard.
backspace,
/// Represents the logical "Tab" key on the keyboard.
tab,
/// Represents the logical "Space" key on the keyboard.
space,
/// Represents the logical "Minus" key on the keyboard.
minus,
/// Represents the logical "Equal" key on the keyboard.
equal,
/// Represents the logical "Bracket Left" key on the keyboard.
bracketLeft,
/// Represents the logical "Bracket Right" key on the keyboard.
bracketRight,
/// Represents the logical "Backslash" key on the keyboard.
backslash,
/// Represents the logical "Semicolon" key on the keyboard.
semicolon,
/// Represents the logical "Quote" key on the keyboard.
quote,
/// Represents the logical "Backquote" key on the keyboard.
backquote,
/// Represents the logical "Comma" key on the keyboard.
comma,
/// Represents the logical "Period" key on the keyboard.
period,
/// Represents the logical "Slash" key on the keyboard.
slash,
/// Represents the logical "Caps Lock" key on the keyboard.
capsLock,
/// Represents the logical "F1" key on the keyboard.
f1,
/// Represents the logical "F2" key on the keyboard.
f2,
/// Represents the logical "F3" key on the keyboard.
f3,
/// Represents the logical "F4" key on the keyboard.
f4,
/// Represents the logical "F5" key on the keyboard.
f5,
/// Represents the logical "F6" key on the keyboard.
f6,
/// Represents the logical "F7" key on the keyboard.
f7,
/// Represents the logical "F8" key on the keyboard.
f8,
/// Represents the logical "F9" key on the keyboard.
f9,
/// Represents the logical "F10" key on the keyboard.
f10,
/// Represents the logical "F11" key on the keyboard.
f11,
/// Represents the logical "F12" key on the keyboard.
f12,
/// Represents the logical "Print Screen" key on the keyboard.
printScreen,
/// Represents the logical "Scroll Lock" key on the keyboard.
scrollLock,
/// Represents the logical "Pause" key on the keyboard.
pause,
/// Represents the logical "Insert" key on the keyboard.
insert,
/// Represents the logical "Home" key on the keyboard.
home,
/// Represents the logical "Page Up" key on the keyboard.
pageUp,
/// Represents the logical "Delete" key on the keyboard.
delete,
/// Represents the logical "End" key on the keyboard.
end,
/// Represents the logical "Page Down" key on the keyboard.
pageDown,
/// Represents the logical "Arrow Right" key on the keyboard.
arrowRight,
/// Represents the logical "Arrow Left" key on the keyboard.
arrowLeft,
/// Represents the logical "Arrow Down" key on the keyboard.
arrowDown,
/// Represents the logical "Arrow Up" key on the keyboard.
arrowUp,
/// Represents the logical "Num Lock" key on the keyboard.
numLock,
/// Represents the logical "Numpad Divide" key on the keyboard.
numpadDivide,
/// Represents the logical "Numpad Multiply" key on the keyboard.
numpadMultiply,
/// Represents the logical "Numpad Subtract" key on the keyboard.
numpadSubtract,
/// Represents the logical "Numpad Add" key on the keyboard.
numpadAdd,
/// Represents the logical "Numpad Enter" key on the keyboard.
numpadEnter,
/// Represents the logical "Numpad 1" key on the keyboard.
numpad1,
/// Represents the logical "Numpad 2" key on the keyboard.
numpad2,
/// Represents the logical "Numpad 3" key on the keyboard.
numpad3,
/// Represents the logical "Numpad 4" key on the keyboard.
numpad4,
/// Represents the logical "Numpad 5" key on the keyboard.
numpad5,
/// Represents the logical "Numpad 6" key on the keyboard.
numpad6,
/// Represents the logical "Numpad 7" key on the keyboard.
numpad7,
/// Represents the logical "Numpad 8" key on the keyboard.
numpad8,
/// Represents the logical "Numpad 9" key on the keyboard.
numpad9,
/// Represents the logical "Numpad 0" key on the keyboard.
numpad0,
/// Represents the logical "Numpad Decimal" key on the keyboard.
numpadDecimal,
/// Represents the logical "Intl Backslash" key on the keyboard.
intlBackslash,
/// Represents the logical "Context Menu" key on the keyboard.
contextMenu,
/// Represents the logical "Power" key on the keyboard.
power,
/// Represents the logical "Numpad Equal" key on the keyboard.
numpadEqual,
/// Represents the logical "F13" key on the keyboard.
f13,
/// Represents the logical "F14" key on the keyboard.
f14,
/// Represents the logical "F15" key on the keyboard.
f15,
/// Represents the logical "F16" key on the keyboard.
f16,
/// Represents the logical "F17" key on the keyboard.
f17,
/// Represents the logical "F18" key on the keyboard.
f18,
/// Represents the logical "F19" key on the keyboard.
f19,
/// Represents the logical "F20" key on the keyboard.
f20,
/// Represents the logical "F21" key on the keyboard.
f21,
/// Represents the logical "F22" key on the keyboard.
f22,
/// Represents the logical "F23" key on the keyboard.
f23,
/// Represents the logical "F24" key on the keyboard.
f24,
/// Represents the logical "Open" key on the keyboard.
open,
/// Represents the logical "Help" key on the keyboard.
help,
/// Represents the logical "Select" key on the keyboard.
select,
/// Represents the logical "Again" key on the keyboard.
again,
/// Represents the logical "Undo" key on the keyboard.
undo,
/// Represents the logical "Cut" key on the keyboard.
cut,
/// Represents the logical "Copy" key on the keyboard.
copy,
/// Represents the logical "Paste" key on the keyboard.
paste,
/// Represents the logical "Find" key on the keyboard.
find,
/// Represents the logical "Audio Volume Mute" key on the keyboard.
audioVolumeMute,
/// Represents the logical "Audio Volume Up" key on the keyboard.
audioVolumeUp,
/// Represents the logical "Audio Volume Down" key on the keyboard.
audioVolumeDown,
/// Represents the logical "Numpad Comma" key on the keyboard.
numpadComma,
/// Represents the logical "Intl Ro" key on the keyboard.
intlRo,
/// Represents the logical "Kana Mode" key on the keyboard.
kanaMode,
/// Represents the logical "Intl Yen" key on the keyboard.
intlYen,
/// Represents the logical "Convert" key on the keyboard.
convert,
/// Represents the logical "Non Convert" key on the keyboard.
nonConvert,
/// Represents the logical "Lang 1" key on the keyboard.
lang1,
/// Represents the logical "Lang 2" key on the keyboard.
lang2,
/// Represents the logical "Lang 3" key on the keyboard.
lang3,
/// Represents the logical "Lang 4" key on the keyboard.
lang4,
/// Represents the logical "Lang 5" key on the keyboard.
lang5,
/// Represents the logical "Abort" key on the keyboard.
abort,
/// Represents the logical "Props" key on the keyboard.
props,
/// Represents the logical "Numpad Paren Left" key on the keyboard.
numpadParenLeft,
/// Represents the logical "Numpad Paren Right" key on the keyboard.
numpadParenRight,
/// Represents the logical "Numpad Backspace" key on the keyboard.
numpadBackspace,
/// Represents the logical "Numpad Memory Store" key on the keyboard.
numpadMemoryStore,
/// Represents the logical "Numpad Memory Recall" key on the keyboard.
numpadMemoryRecall,
/// Represents the logical "Numpad Memory Clear" key on the keyboard.
numpadMemoryClear,
/// Represents the logical "Numpad Memory Add" key on the keyboard.
numpadMemoryAdd,
/// Represents the logical "Numpad Memory Subtract" key on the keyboard.
numpadMemorySubtract,
/// Represents the logical "Numpad Sign Change" key on the keyboard.
numpadSignChange,
/// Represents the logical "Numpad Clear" key on the keyboard.
numpadClear,
/// Represents the logical "Numpad Clear Entry" key on the keyboard.
numpadClearEntry,
/// Represents the logical "Control Left" key on the keyboard.
controlLeft,
/// Represents the logical "Shift Left" key on the keyboard.
shiftLeft,
/// Represents the logical "Alt Left" key on the keyboard.
altLeft,
/// Represents the logical "Meta Left" key on the keyboard.
metaLeft,
/// Represents the logical "Control Right" key on the keyboard.
controlRight,
/// Represents the logical "Shift Right" key on the keyboard.
shiftRight,
/// Represents the logical "Alt Right" key on the keyboard.
altRight,
/// Represents the logical "Meta Right" key on the keyboard.
metaRight,
/// Represents the logical "Info" key on the keyboard.
info,
/// Represents the logical "Closed Caption Toggle" key on the keyboard.
closedCaptionToggle,
/// Represents the logical "Brightness Up" key on the keyboard.
brightnessUp,
/// Represents the logical "Brightness Down" key on the keyboard.
brightnessDown,
/// Represents the logical "Brightness Toggle" key on the keyboard.
brightnessToggle,
/// Represents the logical "Brightness Minimum" key on the keyboard.
brightnessMinimum,
/// Represents the logical "Brightness Maximum" key on the keyboard.
brightnessMaximum,
/// Represents the logical "Brightness Auto" key on the keyboard.
brightnessAuto,
/// Represents the logical "Media Last" key on the keyboard.
mediaLast,
/// Represents the logical "Launch Phone" key on the keyboard.
launchPhone,
/// Represents the logical "Program Guide" key on the keyboard.
programGuide,
/// Represents the logical "Exit" key on the keyboard.
exit,
/// Represents the logical "Channel Up" key on the keyboard.
channelUp,
/// Represents the logical "Channel Down" key on the keyboard.
channelDown,
/// Represents the logical "Media Play" key on the keyboard.
mediaPlay,
/// Represents the logical "Media Pause" key on the keyboard.
mediaPause,
/// Represents the logical "Media Record" key on the keyboard.
mediaRecord,
/// Represents the logical "Media Fast Forward" key on the keyboard.
mediaFastForward,
/// Represents the logical "Media Rewind" key on the keyboard.
mediaRewind,
/// Represents the logical "Media Track Next" key on the keyboard.
mediaTrackNext,
/// Represents the logical "Media Track Previous" key on the keyboard.
mediaTrackPrevious,
/// Represents the logical "Media Stop" key on the keyboard.
mediaStop,
/// Represents the logical "Eject" key on the keyboard.
eject,
/// Represents the logical "Media Play Pause" key on the keyboard.
mediaPlayPause,
/// Represents the logical "Speech Input Toggle" key on the keyboard.
speechInputToggle,
/// Represents the logical "Bass Boost" key on the keyboard.
bassBoost,
/// Represents the logical "Media Select" key on the keyboard.
mediaSelect,
/// Represents the logical "Launch Word Processor" key on the keyboard.
launchWordProcessor,
/// Represents the logical "Launch Spreadsheet" key on the keyboard.
launchSpreadsheet,
/// Represents the logical "Launch Mail" key on the keyboard.
launchMail,
/// Represents the logical "Launch Contacts" key on the keyboard.
launchContacts,
/// Represents the logical "Launch Calendar" key on the keyboard.
launchCalendar,
/// Represents the logical "Launch App2" key on the keyboard.
launchApp2,
/// Represents the logical "Launch App1" key on the keyboard.
launchApp1,
/// Represents the logical "Launch Internet Browser" key on the keyboard.
launchInternetBrowser,
/// Represents the logical "Log Off" key on the keyboard.
logOff,
/// Represents the logical "Lock Screen" key on the keyboard.
lockScreen,
/// Represents the logical "Launch Control Panel" key on the keyboard.
launchControlPanel,
/// Represents the logical "Select Task" key on the keyboard.
selectTask,
/// Represents the logical "Launch Documents" key on the keyboard.
launchDocuments,
/// Represents the logical "Spell Check" key on the keyboard.
spellCheck,
/// Represents the logical "Launch Keyboard Layout" key on the keyboard.
launchKeyboardLayout,
/// Represents the logical "Launch Screen Saver" key on the keyboard.
launchScreenSaver,
/// Represents the logical "Launch Assistant" key on the keyboard.
launchAssistant,
/// Represents the logical "Launch Audio Browser" key on the keyboard.
launchAudioBrowser,
/// Represents the logical "New Key" key on the keyboard.
newKey,
/// Represents the logical "Close" key on the keyboard.
close,
/// Represents the logical "Save" key on the keyboard.
save,
/// Represents the logical "Print" key on the keyboard.
print,
/// Represents the logical "Browser Search" key on the keyboard.
browserSearch,
/// Represents the logical "Browser Home" key on the keyboard.
browserHome,
/// Represents the logical "Browser Back" key on the keyboard.
browserBack,
/// Represents the logical "Browser Forward" key on the keyboard.
browserForward,
/// Represents the logical "Browser Stop" key on the keyboard.
browserStop,
/// Represents the logical "Browser Refresh" key on the keyboard.
browserRefresh,
/// Represents the logical "Browser Favorites" key on the keyboard.
browserFavorites,
/// Represents the logical "Zoom In" key on the keyboard.
zoomIn,
/// Represents the logical "Zoom Out" key on the keyboard.
zoomOut,
/// Represents the logical "Zoom Toggle" key on the keyboard.
zoomToggle,
/// Represents the logical "Redo" key on the keyboard.
redo,
/// Represents the logical "Mail Reply" key on the keyboard.
mailReply,
/// Represents the logical "Mail Forward" key on the keyboard.
mailForward,
/// Represents the logical "Mail Send" key on the keyboard.
mailSend,
/// Represents the logical "Keyboard Layout Select" key on the keyboard.
keyboardLayoutSelect,
/// Represents the logical "Show All Windows" key on the keyboard.
showAllWindows,
/// Represents the logical "Game Button 1" key on the keyboard.
gameButton1,
/// Represents the logical "Game Button 2" key on the keyboard.
gameButton2,
/// Represents the logical "Game Button 3" key on the keyboard.
gameButton3,
/// Represents the logical "Game Button 4" key on the keyboard.
gameButton4,
/// Represents the logical "Game Button 5" key on the keyboard.
gameButton5,
/// Represents the logical "Game Button 6" key on the keyboard.
gameButton6,
/// Represents the logical "Game Button 7" key on the keyboard.
gameButton7,
/// Represents the logical "Game Button 8" key on the keyboard.
gameButton8,
/// Represents the logical "Game Button 9" key on the keyboard.
gameButton9,
/// Represents the logical "Game Button 10" key on the keyboard.
gameButton10,
/// Represents the logical "Game Button 11" key on the keyboard.
gameButton11,
/// Represents the logical "Game Button 12" key on the keyboard.
gameButton12,
/// Represents the logical "Game Button 13" key on the keyboard.
gameButton13,
/// Represents the logical "Game Button 14" key on the keyboard.
gameButton14,
/// Represents the logical "Game Button 15" key on the keyboard.
gameButton15,
/// Represents the logical "Game Button 16" key on the keyboard.
gameButton16,
/// Represents the logical "Game Button A" key on the keyboard.
gameButtonA,
/// Represents the logical "Game Button B" key on the keyboard.
gameButtonB,
/// Represents the logical "Game Button C" key on the keyboard.
gameButtonC,
/// Represents the logical "Game Button Left 1" key on the keyboard.
gameButtonLeft1,
/// Represents the logical "Game Button Left 2" key on the keyboard.
gameButtonLeft2,
/// Represents the logical "Game Button Mode" key on the keyboard.
gameButtonMode,
/// Represents the logical "Game Button Right 1" key on the keyboard.
gameButtonRight1,
/// Represents the logical "Game Button Right 2" key on the keyboard.
gameButtonRight2,
/// Represents the logical "Game Button Select" key on the keyboard.
gameButtonSelect,
/// Represents the logical "Game Button Start" key on the keyboard.
gameButtonStart,
/// Represents the logical "Game Button Thumb Left" key on the keyboard.
gameButtonThumbLeft,
/// Represents the logical "Game Button Thumb Right" key on the keyboard.
gameButtonThumbRight,
/// Represents the logical "Game Button X" key on the keyboard.
gameButtonX,
/// Represents the logical "Game Button Y" key on the keyboard.
gameButtonY,
/// Represents the logical "Game Button Z" key on the keyboard.
gameButtonZ,
/// Represents the logical "Fn" key on the keyboard.
fn,
/// Represents the logical "Shift" key on the keyboard.
///
/// This key represents the union of the keys {shiftLeft, shiftRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
shift,
/// Represents the logical "Meta" key on the keyboard.
///
/// This key represents the union of the keys {metaLeft, metaRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
meta,
/// Represents the logical "Alt" key on the keyboard.
///
/// This key represents the union of the keys {altLeft, altRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
alt,
/// Represents the logical "Control" key on the keyboard.
///
/// This key represents the union of the keys {controlLeft, controlRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
control,
// Missing flutter keys.
backtab,
returnKey,
}
@@ -0,0 +1,115 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keys.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_default.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_parse.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_record.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_token.dart';
class Keytab {
Keytab({
required this.name,
required this.records,
});
factory Keytab.parse(String source) {
final tokens = tokenize(source).toList();
final parser = KeytabParser()..addTokens(tokens);
return parser.result;
}
static final defaultKeytab = Keytab.parse(kDefaultKeytab);
final String? name;
final List<KeytabRecord> records;
KeytabRecord? find(
TerminalKey key, {
bool ctrl = false,
bool alt = false,
bool shift = false,
bool newLineMode = false,
bool appCursorKeys = false,
bool appKeyPad = false,
bool keyPad = false,
bool appScreen = false,
bool macos = false,
// bool meta,
}) {
for (var record in records) {
if (record.key != key) {
continue;
}
if (record.anyModifier == true) {
if (ctrl == false && alt == false && shift == false) {
continue;
}
} else if (record.anyModifier == false) {
if (ctrl != false || alt != false || shift != false) {
continue;
}
} else {
if (record.ctrl != null && record.ctrl != ctrl) {
continue;
}
if (record.shift != null && record.shift != shift) {
continue;
}
if (record.alt != null && record.alt != alt) {
continue;
}
}
if (record.newLine != null && record.newLine != newLineMode) {
continue;
}
if (record.appCursorKeys != null &&
record.appCursorKeys != appCursorKeys) {
continue;
}
if (record.appKeyPad != null && record.appKeyPad != appKeyPad) {
continue;
}
if (record.keyPad != null && record.keyPad != keyPad) {
continue;
}
if (record.appScreen != null && record.appScreen != appScreen) {
continue;
}
if (record.macos != null && record.macos != macos) {
continue;
}
// TODO: support VT52
if (record.ansi == false) {
continue;
}
return record;
}
return null;
}
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('keyboard "$name"');
for (var record in records) {
buffer.writeln(record);
}
return buffer.toString();
}
}
@@ -0,0 +1,215 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keytab/keytab_parse.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_token.dart';
const kDefaultKeytab = r'''
# [README.default.Keytab] Default Keyboard Table
#
# To customize your keyboard, copy this file to something ending with
# .keytab and change it to meet you needs.
#
# Please read the README-KeyTab and the doc/user/README.keyboard files
# in this case.
#
# --------------------------------------------------------------
keyboard "Default (XFree 4)"
# --------------------------------------------------------------
#
# Note that this particular table is a "risc" version made to
# ease customization without bothering with obsolete details.
# See VT100.keytab for the more hairy stuff.
#
# --------------------------------------------------------------
# common keys
key Escape : "\E"
key Tab -Shift : "\t"
key Tab +Shift+Ansi : "\E[Z"
key Tab +Shift-Ansi : "\t"
key Backtab +Ansi : "\E[Z"
key Backtab -Ansi : "\t"
key Return-Shift-NewLine : "\r"
key Return-Shift+NewLine : "\r\n"
key Return+Shift : "\EOM"
key Backspace +Alt : "\x17"
# Backspace and Delete codes are preserving CTRL-H.
#
# Backspace without CTRL sends '^?'; this matches XTerm behaviour, so that
# pressing Alt+Backspace will send \E + Del, which is the expected behaviour
# in some apps (e.g. emacs), and it was the behaviour before the commit
# that add the Backspace +Control rule
key Backspace -Control : "\x7f"
# Match xterm behaviour: Backspace sends '^H' when Control is pressed
# BS, hex \x08, \b
key Backspace +Control : "\b"
# Arrow keys in VT52 mode
# shift up/down are reserved for scrolling.
# shift left/right are reserved for switching between tabs (this is hardcoded).
key Up -Shift-Ansi : "\EA"
key Down -Shift-Ansi : "\EB"
key Right-Shift-Ansi : "\EC"
key Left -Shift-Ansi : "\ED"
# Arrow keys in ANSI mode with Application - and Normal Cursor Mode)
key Up -Shift-AnyMod+Ansi+AppCuKeys : "\EOA"
key Down -Shift-AnyMod+Ansi+AppCuKeys : "\EOB"
key Right -Shift-AnyMod+Ansi+AppCuKeys : "\EOC"
key Left -Shift-AnyMod+Ansi+AppCuKeys : "\EOD"
key Up -Shift-AnyMod+Ansi-AppCuKeys : "\E[A"
key Down -Shift-AnyMod+Ansi-AppCuKeys : "\E[B"
key Right -Shift-AnyMod+Ansi-AppCuKeys : "\E[C"
key Left -Shift-AnyMod+Ansi-AppCuKeys : "\E[D"
key Up -Shift+AnyMod+Ansi : "\E[1;5A"
key Down -Shift+AnyMod+Ansi : "\E[1;5B"
# Right / Left with Control
key Right -Shift-Alt+Control+Ansi : "\E[1;5C"
key Left -Shift-Alt+Control+Ansi : "\E[1;5D"
# Right / Left with Alt not on a Mac
key Right -Shift+Alt-Control+Ansi-Mac : "\E[1;5C"
key Left -Shift+Alt-Control+Ansi-Mac : "\E[1;5D"
# Right / Left with Alt on a Mac
key Right -Shift+Alt-Control+Ansi+Mac : "\Ef"
key Left -Shift+Alt-Control+Ansi+Mac : "\Eb"
key Up +Shift+AppScreen : "\E[1;*A"
key Down +Shift+AppScreen : "\E[1;*B"
key Left +Shift+AppScreen : "\E[1;*D"
key Right +Shift+AppScreen : "\E[1;*C"
# Keypad keys with NumLock ON
# (see https://web.archive.org/web/20070807181942/http://www.nw.com/nw/WWW/products/wizcon/vt100.html
# https://vt100.net/docs/vt100-ug/chapter3.html)
#
# Not enabled for now because it breaks the keypad in Vim.
#
#key 0 +KeyPad+AppKeyPad : "\EOp"
#key 1 +KeyPad+AppKeyPad : "\EOq"
#key 2 +KeyPad+AppKeyPad : "\EOr"
#key 3 +KeyPad+AppKeyPad : "\EOs"
#key 4 +KeyPad+AppKeyPad : "\EOt"
#key 5 +KeyPad+AppKeyPad : "\EOu"
#key 6 +KeyPad+AppKeyPad : "\EOv"
#key 7 +KeyPad+AppKeyPad : "\EOw"
#key 8 +KeyPad+AppKeyPad : "\EOx"
#key 9 +KeyPad+AppKeyPad : "\EOy"
#key + +KeyPad+AppKeyPad : "\EOl"
#key - +KeyPad+AppKeyPad : "\EOm"
#key . +KeyPad+AppKeyPad : "\EOn"
#key * +KeyPad+AppKeyPad : "\EOM"
#key Enter +KeyPad+AppKeyPad : "\r"
# Keypad keys with NumLock Off
key Up -Shift+Ansi+AppCuKeys+KeyPad : "\EOA"
key Down -Shift+Ansi+AppCuKeys+KeyPad : "\EOB"
key Right -Shift+Ansi+AppCuKeys+KeyPad : "\EOC"
key Left -Shift+Ansi+AppCuKeys+KeyPad : "\EOD"
key Up -Shift+Ansi-AppCuKeys+KeyPad : "\E[A"
key Down -Shift+Ansi-AppCuKeys+KeyPad : "\E[B"
key Right -Shift+Ansi-AppCuKeys+KeyPad : "\E[C"
key Left -Shift+Ansi-AppCuKeys+KeyPad : "\E[D"
key Home +AppCuKeys+KeyPad : "\EOH"
key End +AppCuKeys+KeyPad : "\EOF"
key Home -AppCuKeys+KeyPad : "\E[H"
key End -AppCuKeys+KeyPad : "\E[F"
key Insert +KeyPad : "\E[2~"
key Delete +KeyPad : "\E[3~"
key PgUp -Shift+KeyPad : "\E[5~"
key PgDown -Shift+KeyPad : "\E[6~"
# the key labelled 5 on the Keypad, is Qt::Key_Clear (a very intuitive
# and discoverable name...)
key Clear +KeyPad : "\E[E"
# other grey PC keys
key Enter+NewLine : "\r\n"
key Enter-NewLine : "\r"
key NumEnter+NewLine : "\r\n"
key NumEnter-NewLine : "\r"
key Home -AnyMod-AppCuKeys : "\E[H"
key End -AnyMod-AppCuKeys : "\E[F"
key Home -AnyMod+AppCuKeys : "\EOH"
key End -AnyMod+AppCuKeys : "\EOF"
key Home +AnyMod : "\E[1;*H"
key End +AnyMod : "\E[1;*F"
key Insert -AnyMod : "\E[2~"
key Delete -AnyMod : "\E[3~"
key Insert +AnyMod : "\E[2;*~"
key Delete +AnyMod : "\E[3;*~"
key PgUp -Shift-AnyMod : "\E[5~"
key PgDown -Shift-AnyMod : "\E[6~"
key PgUp -Shift+AnyMod : "\E[5;*~"
key PgDown -Shift+AnyMod : "\E[6;*~"
# Function keys
key F1 -AnyMod : "\EOP"
key F2 -AnyMod : "\EOQ"
key F3 -AnyMod : "\EOR"
key F4 -AnyMod : "\EOS"
key F5 -AnyMod : "\E[15~"
key F6 -AnyMod : "\E[17~"
key F7 -AnyMod : "\E[18~"
key F8 -AnyMod : "\E[19~"
key F9 -AnyMod : "\E[20~"
key F10 -AnyMod : "\E[21~"
key F11 -AnyMod : "\E[23~"
key F12 -AnyMod : "\E[24~"
key F1 +AnyMod : "\EO*P"
key F2 +AnyMod : "\EO*Q"
key F3 +AnyMod : "\EO*R"
key F4 +AnyMod : "\EO*S"
key F5 +AnyMod : "\E[15;*~"
key F6 +AnyMod : "\E[17;*~"
key F7 +AnyMod : "\E[18;*~"
key F8 +AnyMod : "\E[19;*~"
key F9 +AnyMod : "\E[20;*~"
key F10 +AnyMod : "\E[21;*~"
key F11 +AnyMod : "\E[23;*~"
key F12 +AnyMod : "\E[24;*~"
# Work around dead keys
key Space +Control : "\x00"
# Some keys are used by konsole to cause operations.
# The scroll* operations refer to the history buffer.
key Up +Shift-AppScreen : scrollLineUp
key PgUp +Shift-AppScreen : scrollPageUp
key Home +Shift-AppScreen : scrollUpToTop
key Down +Shift-AppScreen : scrollLineDown
key PgDown +Shift-AppScreen : scrollPageDown
key End +Shift-AppScreen : scrollDownToBottom
''';
void main() {
final tokens = tokenize(kDefaultKeytab).toList();
final parser = KeytabParser()..addTokens(tokens);
print(parser.result);
}
@@ -0,0 +1,23 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
final _esc = String.fromCharCode(0x1b);
String keytabUnescape(String str) {
str = str
.replaceAll(r'\E', _esc)
.replaceAll(r'\\', '\\')
.replaceAll(r'\"', '"')
.replaceAll(r'\t', '\t')
.replaceAll(r'\r', '\r')
.replaceAll(r'\n', '\n')
.replaceAll(r'\b', '\b');
final hexPattern = RegExp(r'\\x([0-9a-fA-F][0-9a-fA-F])');
str = str.replaceAllMapped(hexPattern, (match) {
final hexString = match.group(1)!;
final hexValue = int.parse(hexString, radix: 16);
return String.fromCharCode(hexValue);
});
return str;
}
@@ -0,0 +1,188 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keytab/keytab.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_record.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_token.dart';
import 'package:clide/src/terminal/src/core/input/keytab/qt_keyname.dart';
class ParseError {}
class TokensReader {
TokensReader(this.tokens);
final List<KeytabToken> tokens;
var _pos = 0;
bool get done => _pos > tokens.length - 1;
KeytabToken? take() {
final result = peek();
_pos += 1;
return result;
}
KeytabToken? peek() {
if (done) return null;
return tokens[_pos];
}
}
class KeytabParser {
String? _name;
final _records = <KeytabRecord>[];
void addTokens(List<KeytabToken> tokens) {
final reader = TokensReader(tokens);
while (!reader.done) {
if (reader.peek()!.type == KeytabTokenType.keyboard) {
_parseName(reader);
continue;
}
if (reader.peek()!.type == KeytabTokenType.keyDefine) {
_parseKeyDefine(reader);
continue;
}
throw ParseError();
}
}
Keytab get result {
return Keytab(name: _name, records: _records);
}
void _parseName(TokensReader reader) {
if (reader.take()!.type != KeytabTokenType.keyboard) {
throw ParseError();
}
final name = reader.take()!;
if (name.type != KeytabTokenType.input) {
throw ParseError();
}
_name = name.value;
}
void _parseKeyDefine(TokensReader reader) {
if (reader.take()!.type != KeytabTokenType.keyDefine) {
throw ParseError();
}
final keyName = reader.take()!;
if (keyName.type != KeytabTokenType.keyName) {
throw ParseError();
}
final key = qtKeynameMap[keyName.value];
if (key == null) {
throw ParseError();
}
bool? alt;
bool? ctrl;
bool? shift;
bool? anyModifier;
bool? ansi;
bool? appScreen;
bool? keyPad;
bool? appCursorKeys;
bool? appKeyPad;
bool? newLine;
bool? mac;
while (reader.peek()!.type == KeytabTokenType.modeStatus) {
bool modeStatus;
switch (reader.take()!.value) {
case '+':
modeStatus = true;
break;
case '-':
modeStatus = false;
break;
default:
throw ParseError();
}
final mode = reader.take();
if (mode!.type != KeytabTokenType.mode) {
throw ParseError();
}
switch (mode.value) {
case 'Alt':
alt = modeStatus;
break;
case 'Control':
ctrl = modeStatus;
break;
case 'Shift':
shift = modeStatus;
break;
case 'AnyMod':
anyModifier = modeStatus;
break;
case 'Ansi':
ansi = modeStatus;
break;
case 'AppScreen':
appScreen = modeStatus;
break;
case 'KeyPad':
keyPad = modeStatus;
break;
case 'AppCuKeys':
appCursorKeys = modeStatus;
break;
case 'AppKeyPad':
appKeyPad = modeStatus;
break;
case 'NewLine':
newLine = modeStatus;
break;
case 'Mac':
mac = modeStatus;
break;
default:
throw ParseError();
}
}
if (reader.take()!.type != KeytabTokenType.colon) {
throw ParseError();
}
final actionToken = reader.take()!;
KeytabAction action;
if (actionToken.type == KeytabTokenType.input) {
action = KeytabAction(KeytabActionType.input, actionToken.value);
} else if (actionToken.type == KeytabTokenType.shortcut) {
action = KeytabAction(KeytabActionType.shortcut, actionToken.value);
} else {
throw ParseError();
}
final record = KeytabRecord(
qtKeyName: keyName.value,
key: key,
action: action,
alt: alt,
ctrl: ctrl,
shift: shift,
anyModifier: anyModifier,
ansi: ansi,
appScreen: appScreen,
keyPad: keyPad,
appCursorKeys: appCursorKeys,
appKeyPad: appKeyPad,
newLine: newLine,
macos: mac,
);
_records.add(record);
}
}
@@ -0,0 +1,136 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keys.dart';
import 'package:clide/src/terminal/src/core/input/keytab/keytab_escape.dart';
enum KeytabActionType {
input,
shortcut,
}
class KeytabAction {
KeytabAction(this.type, this.value);
final KeytabActionType type;
final String value;
String unescapedValue() {
if (type == KeytabActionType.input) {
return keytabUnescape(value);
} else {
return value;
}
}
@override
String toString() {
switch (type) {
case KeytabActionType.input:
return '"$value"';
case KeytabActionType.shortcut:
return value;
}
}
}
class KeytabRecord {
KeytabRecord({
required this.qtKeyName,
required this.key,
required this.action,
required this.alt,
required this.ctrl,
required this.shift,
required this.anyModifier,
required this.ansi,
required this.appScreen,
required this.keyPad,
required this.appCursorKeys,
required this.appKeyPad,
required this.newLine,
required this.macos,
});
String qtKeyName;
TerminalKey key;
KeytabAction action;
bool? alt;
bool? ctrl;
bool? shift;
bool? anyModifier;
bool? ansi;
bool? appScreen;
bool? keyPad;
bool? appCursorKeys;
bool? appKeyPad;
bool? newLine;
bool? macos;
@override
String toString() {
final buffer = StringBuffer();
buffer.write('$qtKeyName ');
if (alt != null) {
buffer.write(_toMode(alt!, 'Alt'));
}
if (ctrl != null) {
buffer.write(_toMode(ctrl!, 'Control'));
}
if (shift != null) {
buffer.write(_toMode(shift!, 'Shift'));
}
if (anyModifier != null) {
buffer.write(_toMode(anyModifier!, 'AnyMod'));
}
if (ansi != null) {
buffer.write(_toMode(ansi!, 'Ansi'));
}
if (appScreen != null) {
buffer.write(_toMode(appScreen!, 'AppScreen'));
}
if (keyPad != null) {
buffer.write(_toMode(keyPad!, 'KeyPad'));
}
if (appCursorKeys != null) {
buffer.write(_toMode(appCursorKeys!, 'AppCuKeys'));
}
if (appKeyPad != null) {
buffer.write(_toMode(appKeyPad!, 'AppKeyPad'));
}
if (newLine != null) {
buffer.write(_toMode(newLine!, 'NewLine'));
}
if (macos != null) {
buffer.write(_toMode(macos!, 'Mac'));
}
buffer.write(' : $action');
return buffer.toString();
}
static String _toMode(bool status, String mode) {
if (status == true) {
return '+$mode';
}
if (status == false) {
return '-$mode';
}
return '';
}
}
@@ -0,0 +1,181 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show min;
enum KeytabTokenType {
keyDefine,
keyboard,
keyName,
mode,
modeStatus,
colon,
input,
shortcut,
}
class KeytabToken {
KeytabToken(this.type, this.value);
final KeytabTokenType type;
final String value;
@override
String toString() {
return '$type<$value>';
}
}
class LineReader {
LineReader(this.line);
final String line;
var _pos = 0;
bool get done => _pos > line.length - 1;
String? take([int count = 1]) {
final result = peek(count);
_pos += count;
return result;
}
String? peek([int count = 1]) {
if (done) return null;
final end = min(_pos + count, line.length);
final result = line.substring(_pos, end);
return result;
}
void skipWhitespace() {
while (peek() == ' ' || peek() == '\t') {
_pos += 1;
}
}
String readString() {
final buffer = StringBuffer();
final pattern = RegExp(r'\w|_');
while (!done && line[_pos].contains(pattern)) {
buffer.write(line[_pos]);
_pos++;
}
return buffer.toString();
}
String readUntil(Pattern pattern, {bool inclusive = false}) {
final buffer = StringBuffer();
while (!done && !line[_pos].contains(pattern)) {
buffer.write(line[_pos]);
_pos++;
}
if (!done && inclusive) {
buffer.write(line[_pos]);
_pos++;
}
return buffer.toString();
}
}
class TokenizeError {}
Iterable<KeytabToken> tokenize(String source) sync* {
final lines = source.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
line = line.replaceFirst(RegExp('#.*'), '');
if (line == '') {
continue;
}
if (_isKeyboardNameDefine(line)) {
yield* _parseKeyboardNameDefine(line);
}
if (_isKeyDefine(line)) {
yield* _parseKeyDefine(line);
}
}
}
bool _isKeyboardNameDefine(String line) {
return line.startsWith('keyboard ');
}
bool _isKeyDefine(String line) {
return line.startsWith('key ');
}
Iterable<KeytabToken> _parseKeyboardNameDefine(String line) sync* {
final reader = LineReader(line.trim());
if (reader.readString() == 'keyboard') {
yield KeytabToken(KeytabTokenType.keyboard, 'keyboard');
} else {
throw TokenizeError();
}
reader.skipWhitespace();
yield _readInput(reader);
}
Iterable<KeytabToken> _parseKeyDefine(String line) sync* {
final reader = LineReader(line.trim());
if (reader.readString() == 'key') {
yield KeytabToken(KeytabTokenType.keyDefine, 'key');
} else {
throw TokenizeError();
}
reader.skipWhitespace();
final keyName = reader.readString();
yield KeytabToken(KeytabTokenType.keyName, keyName);
reader.skipWhitespace();
while (reader.peek() == '+' || reader.peek() == '-') {
final modeStatus = reader.take()!;
yield KeytabToken(KeytabTokenType.modeStatus, modeStatus);
final mode = reader.readString();
yield KeytabToken(KeytabTokenType.mode, mode);
reader.skipWhitespace();
}
if (reader.take() == ':') {
yield KeytabToken(KeytabTokenType.colon, ':');
} else {
throw TokenizeError();
}
reader.skipWhitespace();
if (reader.peek() == '"') {
yield _readInput(reader);
} else {
final action = reader.readString();
yield KeytabToken(KeytabTokenType.shortcut, action);
}
}
KeytabToken _readInput(LineReader reader) {
reader.skipWhitespace();
if (reader.take() != '"') {
throw TokenizeError();
}
final value = reader.readUntil('"');
reader.take();
return KeytabToken(KeytabTokenType.input, value);
}
@@ -0,0 +1,479 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/input/keys.dart';
/// See: https://doc.qt.io/qt-5/qt.html#Key-enum
const qtKeynameMap = <String, TerminalKey>{
'Escape': TerminalKey.escape,
'Tab': TerminalKey.tab,
'Backtab': TerminalKey.backtab,
'Backspace': TerminalKey.backspace,
'Return': TerminalKey.returnKey,
'Enter': TerminalKey.enter,
'NumEnter': TerminalKey.numpadEnter,
'Insert': TerminalKey.insert,
'Delete': TerminalKey.delete,
'Pause': TerminalKey.pause,
'Print': TerminalKey.print,
// 'SysReq': TerminalKey.sysReq,
'Clear': TerminalKey.numpadClear,
'Home': TerminalKey.home,
'End': TerminalKey.end,
'Left': TerminalKey.arrowLeft,
'Up': TerminalKey.arrowUp,
'Right': TerminalKey.arrowRight,
'Down': TerminalKey.arrowDown,
'PageUp': TerminalKey.pageUp,
'PageDown': TerminalKey.pageDown,
'PgUp': TerminalKey.pageUp,
'PgDown': TerminalKey.pageDown,
'Shift': TerminalKey.shift,
'Control': TerminalKey.control,
'Meta': TerminalKey.meta,
'Alt': TerminalKey.alt,
// 'AltGr': TerminalKey.altGr,
'CapsLock': TerminalKey.capsLock,
'NumLock': TerminalKey.numLock,
'ScrollLock': TerminalKey.scrollLock,
'F1': TerminalKey.f1,
'F2': TerminalKey.f2,
'F3': TerminalKey.f3,
'F4': TerminalKey.f4,
'F5': TerminalKey.f5,
'F6': TerminalKey.f6,
'F7': TerminalKey.f7,
'F8': TerminalKey.f8,
'F9': TerminalKey.f9,
'F10': TerminalKey.f10,
'F11': TerminalKey.f11,
'F12': TerminalKey.f12,
'F13': TerminalKey.f13,
'F14': TerminalKey.f14,
'F15': TerminalKey.f15,
'F16': TerminalKey.f16,
'F17': TerminalKey.f17,
'F18': TerminalKey.f18,
'F19': TerminalKey.f19,
'F20': TerminalKey.f20,
'F21': TerminalKey.f21,
'F22': TerminalKey.f22,
'F23': TerminalKey.f23,
'F24': TerminalKey.f24,
// 'F25': TerminalKey.f25,
// 'F26': TerminalKey.f26,
// 'F27': TerminalKey.f27,
// 'F28': TerminalKey.f28,
// 'F29': TerminalKey.f29,
// 'F30': TerminalKey.f30,
// 'F31': TerminalKey.f31,
// 'F32': TerminalKey.f32,
// 'F33': TerminalKey.f33,
// 'F34': TerminalKey.f34,
// 'F35': TerminalKey.f35,
// 'Super_L': TerminalKey.super_L,
// 'Super_R': TerminalKey.super_R,
// 'Menu': TerminalKey.menu,
// 'Hyper_L': TerminalKey.hyper_L,
// 'Hyper_R': TerminalKey.hyper_R,
'Help': TerminalKey.help,
// 'Direction_L': TerminalKey.direction_L,
// 'Direction_R': TerminalKey.direction_R,
'Space': TerminalKey.space,
// 'Any': TerminalKey.any,
// 'Exclam': TerminalKey.exclam,
// 'QuoteDbl': TerminalKey.quoteDbl,
// 'NumberSign': TerminalKey.numberSign,
// 'Dollar': TerminalKey.dollar,
// 'Percent': TerminalKey.percent,
// 'Ampersand': TerminalKey.ampersand,
// 'Apostrophe': TerminalKey.apostrophe,
'ParenLeft': TerminalKey.numpadParenLeft,
'ParenRight': TerminalKey.numpadParenRight,
// 'Asterisk': TerminalKey.asterisk,
// 'Plus': TerminalKey.plus,
'Comma': TerminalKey.comma,
'Minus': TerminalKey.minus,
'Period': TerminalKey.period,
'Slash': TerminalKey.slash,
'0': TerminalKey.digit0,
'1': TerminalKey.digit1,
'2': TerminalKey.digit2,
'3': TerminalKey.digit3,
'4': TerminalKey.digit4,
'5': TerminalKey.digit5,
'6': TerminalKey.digit6,
'7': TerminalKey.digit7,
'8': TerminalKey.digit8,
'9': TerminalKey.digit9,
// 'Colon': TerminalKey.colon,
'Semicolon': TerminalKey.semicolon,
// 'Less': TerminalKey.less,
// 'Equal': TerminalKey.equal,
// 'Greater': TerminalKey.greater,
// 'Question': TerminalKey.question,
// 'At': TerminalKey.at,
'A': TerminalKey.keyA,
'B': TerminalKey.keyB,
'C': TerminalKey.keyC,
'D': TerminalKey.keyD,
'E': TerminalKey.keyE,
'F': TerminalKey.keyF,
'G': TerminalKey.keyG,
'H': TerminalKey.keyH,
'I': TerminalKey.keyI,
'J': TerminalKey.keyJ,
'K': TerminalKey.keyK,
'L': TerminalKey.keyL,
'M': TerminalKey.keyM,
'N': TerminalKey.keyN,
'O': TerminalKey.keyO,
'P': TerminalKey.keyP,
'Q': TerminalKey.keyQ,
'R': TerminalKey.keyR,
'S': TerminalKey.keyS,
'T': TerminalKey.keyT,
'U': TerminalKey.keyU,
'V': TerminalKey.keyV,
'W': TerminalKey.keyW,
'X': TerminalKey.keyX,
'Y': TerminalKey.keyY,
'Z': TerminalKey.keyZ,
'BracketLeft': TerminalKey.bracketLeft,
'Backslash': TerminalKey.backslash,
'BracketRight': TerminalKey.bracketRight,
// 'AsciiCircum': TerminalKey.asciiCircum,
// 'Underscore': TerminalKey.underscore,
// 'QuoteLeft': TerminalKey.quoteLeft,
// 'BraceLeft': TerminalKey.braceLeft,
// 'Bar': TerminalKey.bar,
// 'BraceRight': TerminalKey.braceRight,
// 'AsciiTilde': TerminalKey.asciiTilde,
// 'nobreakspace': TerminalKey.nobreakspace,
// 'exclamdown': TerminalKey.exclamdown,
// 'cent': TerminalKey.cent,
// 'sterling': TerminalKey.sterling,
// 'currency': TerminalKey.currency,
// 'yen': TerminalKey.yen,
// 'brokenbar': TerminalKey.brokenbar,
// 'section': TerminalKey.section,
// 'diaeresis': TerminalKey.diaeresis,
// 'copyright': TerminalKey.copyright,
// 'ordfeminine': TerminalKey.ordfeminine,
// 'guillemotleft': TerminalKey.guillemotleft,
// 'notsign': TerminalKey.notsign,
// 'hyphen': TerminalKey.hyphen,
// 'registered': TerminalKey.registered,
// 'macron': TerminalKey.macron,
// 'degree': TerminalKey.degree,
// 'plusminus': TerminalKey.plusminus,
// 'twosuperior': TerminalKey.twosuperior,
// 'threesuperior': TerminalKey.threesuperior,
// 'acute': TerminalKey.acute,
// // 'mu': TerminalKey.mu,
// 'paragraph': TerminalKey.paragraph,
// 'periodcentered': TerminalKey.periodcentered,
// 'cedilla': TerminalKey.cedilla,
// 'onesuperior': TerminalKey.onesuperior,
// 'masculine': TerminalKey.masculine,
// 'guillemotright': TerminalKey.guillemotright,
// 'onequarter': TerminalKey.onequarter,
// 'onehalf': TerminalKey.onehalf,
// 'threequarters': TerminalKey.threequarters,
// 'questiondown': TerminalKey.questiondown,
// 'Agrave': TerminalKey.agrave,
// 'Aacute': TerminalKey.aacute,
// 'Acircumflex': TerminalKey.acircumflex,
// 'Atilde': TerminalKey.atilde,
// 'Adiaeresis': TerminalKey.adiaeresis,
// 'Aring': TerminalKey.aring,
// 'AE': TerminalKey.aE,
// 'Ccedilla': TerminalKey.ccedilla,
// 'Egrave': TerminalKey.egrave,
// 'Eacute': TerminalKey.eacute,
// 'Ecircumflex': TerminalKey.ecircumflex,
// 'Ediaeresis': TerminalKey.ediaeresis,
// 'Igrave': TerminalKey.igrave,
// 'Iacute': TerminalKey.iacute,
// 'Icircumflex': TerminalKey.icircumflex,
// 'Idiaeresis': TerminalKey.idiaeresis,
// 'ETH': TerminalKey.eTH,
// 'Ntilde': TerminalKey.ntilde,
// 'Ograve': TerminalKey.ograve,
// 'Oacute': TerminalKey.oacute,
// 'Ocircumflex': TerminalKey.ocircumflex,
// 'Otilde': TerminalKey.otilde,
// 'Odiaeresis': TerminalKey.odiaeresis,
// 'multiply': TerminalKey.multiply,
// 'Ooblique': TerminalKey.ooblique,
// 'Ugrave': TerminalKey.ugrave,
// 'Uacute': TerminalKey.uacute,
// 'Ucircumflex': TerminalKey.ucircumflex,
// 'Udiaeresis': TerminalKey.udiaeresis,
// 'Yacute': TerminalKey.yacute,
// 'THORN': TerminalKey.tHORN,
// 'ssharp': TerminalKey.ssharp,
// 'division': TerminalKey.division,
// 'ydiaeresis': TerminalKey.ydiaeresis,
// 'Multi_key': TerminalKey.multi_key,
// 'Codeinput': TerminalKey.codeinput,
// 'SingleCandidate': TerminalKey.singleCandidate,
// 'MultipleCandidate': TerminalKey.multipleCandidate,
// 'PreviousCandidate': TerminalKey.previousCandidate,
// 'Mode_switch': TerminalKey.mode_switch,
// 'Kanji': TerminalKey.kanji,
// 'Muhenkan': TerminalKey.muhenkan,
// 'Henkan': TerminalKey.henkan,
// 'Romaji': TerminalKey.romaji,
// 'Hiragana': TerminalKey.hiragana,
// 'Katakana': TerminalKey.katakana,
// 'Hiragana_Katakana': TerminalKey.hiragana_Katakana,
// 'Zenkaku': TerminalKey.zenkaku,
// 'Hankaku': TerminalKey.hankaku,
// 'Zenkaku_Hankaku': TerminalKey.zenkaku_Hankaku,
// 'Touroku': TerminalKey.touroku,
// 'Massyo': TerminalKey.massyo,
// 'Kana_Lock': TerminalKey.kana_Lock,
// 'Kana_Shift': TerminalKey.kana_Shift,
// 'Eisu_Shift': TerminalKey.eisu_Shift,
// 'Eisu_toggle': TerminalKey.eisu_toggle,
// 'Hangul': TerminalKey.hangul,
// 'Hangul_Start': TerminalKey.hangul_Start,
// 'Hangul_End': TerminalKey.hangul_End,
// 'Hangul_Hanja': TerminalKey.hangul_Hanja,
// 'Hangul_Jamo': TerminalKey.hangul_Jamo,
// 'Hangul_Romaja': TerminalKey.hangul_Romaja,
// 'Hangul_Jeonja': TerminalKey.hangul_Jeonja,
// 'Hangul_Banja': TerminalKey.hangul_Banja,
// 'Hangul_PreHanja': TerminalKey.hangul_PreHanja,
// 'Hangul_PostHanja': TerminalKey.hangul_PostHanja,
// 'Hangul_Special': TerminalKey.hangul_Special,
// 'Dead_Grave': TerminalKey.dead_Grave,
// 'Dead_Acute': TerminalKey.dead_Acute,
// 'Dead_Circumflex': TerminalKey.dead_Circumflex,
// 'Dead_Tilde': TerminalKey.dead_Tilde,
// 'Dead_Macron': TerminalKey.dead_Macron,
// 'Dead_Breve': TerminalKey.dead_Breve,
// 'Dead_Abovedot': TerminalKey.dead_Abovedot,
// 'Dead_Diaeresis': TerminalKey.dead_Diaeresis,
// 'Dead_Abovering': TerminalKey.dead_Abovering,
// 'Dead_Doubleacute': TerminalKey.dead_Doubleacute,
// 'Dead_Caron': TerminalKey.dead_Caron,
// 'Dead_Cedilla': TerminalKey.dead_Cedilla,
// 'Dead_Ogonek': TerminalKey.dead_Ogonek,
// 'Dead_Iota': TerminalKey.dead_Iota,
// 'Dead_Voiced_Sound': TerminalKey.dead_Voiced_Sound,
// 'Dead_Semivoiced_Sound': TerminalKey.dead_Semivoiced_Sound,
// 'Dead_Belowdot': TerminalKey.dead_Belowdot,
// 'Dead_Hook': TerminalKey.dead_Hook,
// 'Dead_Horn': TerminalKey.dead_Horn,
// 'Dead_Stroke': TerminalKey.dead_Stroke,
// 'Dead_Abovecomma': TerminalKey.dead_Abovecomma,
// 'Dead_Abovereversedcomma': TerminalKey.dead_Abovereversedcomma,
// 'Dead_Doublegrave': TerminalKey.dead_Doublegrave,
// 'Dead_Belowring': TerminalKey.dead_Belowring,
// 'Dead_Belowmacron': TerminalKey.dead_Belowmacron,
// 'Dead_Belowcircumflex': TerminalKey.dead_Belowcircumflex,
// 'Dead_Belowtilde': TerminalKey.dead_Belowtilde,
// 'Dead_Belowbreve': TerminalKey.dead_Belowbreve,
// 'Dead_Belowdiaeresis': TerminalKey.dead_Belowdiaeresis,
// 'Dead_Invertedbreve': TerminalKey.dead_Invertedbreve,
// 'Dead_Belowcomma': TerminalKey.dead_Belowcomma,
// 'Dead_Currency': TerminalKey.dead_Currency,
// 'Dead_a': TerminalKey.dead_a,
// 'Dead_A': TerminalKey.dead_A,
// 'Dead_e': TerminalKey.dead_e,
// 'Dead_E': TerminalKey.dead_E,
// 'Dead_i': TerminalKey.dead_i,
// 'Dead_I': TerminalKey.dead_I,
// 'Dead_o': TerminalKey.dead_o,
// 'Dead_O': TerminalKey.dead_O,
// 'Dead_u': TerminalKey.dead_u,
// 'Dead_U': TerminalKey.dead_U,
// 'Dead_Small_Schwa': TerminalKey.dead_Small_Schwa,
// 'Dead_Capital_Schwa': TerminalKey.dead_Capital_Schwa,
// 'Dead_Greek': TerminalKey.dead_Greek,
// 'Dead_Lowline': TerminalKey.dead_Lowline,
// 'Dead_Aboveverticalline': TerminalKey.dead_Aboveverticalline,
// 'Dead_Belowverticalline': TerminalKey.dead_Belowverticalline,
// 'Dead_Longsolidusoverlay': TerminalKey.dead_Longsolidusoverlay,
// 'Back': TerminalKey.back,
// 'Forward': TerminalKey.forward,
// 'Stop': TerminalKey.stop,
// 'Refresh': TerminalKey.refresh,
'VolumeDown': TerminalKey.audioVolumeDown,
'VolumeMute': TerminalKey.audioVolumeMute,
'VolumeUp': TerminalKey.audioVolumeUp,
'BassBoost': TerminalKey.bassBoost,
// 'BassUp': TerminalKey.bassUp,
// 'BassDown': TerminalKey.bassDown,
// 'TrebleUp': TerminalKey.trebleUp,
// 'TrebleDown': TerminalKey.trebleDown,
'MediaPlay': TerminalKey.mediaPlay,
'MediaStop': TerminalKey.mediaStop,
// 'MediaPrevious': TerminalKey.mediaPrevious,
// 'MediaNext': TerminalKey.mediaNext,
'MediaRecord': TerminalKey.mediaRecord,
'MediaPause': TerminalKey.mediaPause,
'MediaTogglePlayPause': TerminalKey.mediaPlayPause,
'HomePage': TerminalKey.browserHome,
// 'Favorites': TerminalKey.favorites,
// 'Search': TerminalKey.search,
// 'Standby': TerminalKey.standby,
// 'OpenUrl': TerminalKey.openUrl,
// 'LaunchMail': TerminalKey.launchMail,
// 'LaunchMedia': TerminalKey.launchMedia,
// 'Launch0': TerminalKey.launch0,
// 'Launch1': TerminalKey.launch1,
// 'Launch2': TerminalKey.launch2,
// 'Launch3': TerminalKey.launch3,
// 'Launch4': TerminalKey.launch4,
// 'Launch5': TerminalKey.launch5,
// 'Launch6': TerminalKey.launch6,
// 'Launch7': TerminalKey.launch7,
// 'Launch8': TerminalKey.launch8,
// 'Launch9': TerminalKey.launch9,
// 'LaunchA': TerminalKey.launchA,
// 'LaunchB': TerminalKey.launchB,
// 'LaunchC': TerminalKey.launchC,
// 'LaunchD': TerminalKey.launchD,
// 'LaunchE': TerminalKey.launchE,
// 'LaunchF': TerminalKey.launchF,
// 'LaunchG': TerminalKey.launchG,
// 'LaunchH': TerminalKey.launchH,
'MonBrightnessUp': TerminalKey.brightnessUp,
'MonBrightnessDown': TerminalKey.brightnessDown,
// 'KeyboardLightOnOff': TerminalKey.keyboardLightOnOff,
// 'KeyboardBrightnessUp': TerminalKey.keyboardBrightnessUp,
// 'KeyboardBrightnessDown': TerminalKey.keyboardBrightnessDown,
'PowerOff': TerminalKey.power,
'WakeUp': TerminalKey.wakeUp,
'Eject': TerminalKey.eject,
// 'ScreenSaver': TerminalKey.screenSaver,
// 'WWW': TerminalKey.wWW,
// 'Memo': TerminalKey.memo,
// 'LightBulb': TerminalKey.lightBulb,
// 'Shop': TerminalKey.shop,
// 'History': TerminalKey.history,
// 'AddFavorite': TerminalKey.addFavorite,
// 'HotLinks': TerminalKey.hotLinks,
// 'BrightnessAdjust': TerminalKey.brightnessAdjust,
// 'Finance': TerminalKey.finance,
// 'Community': TerminalKey.community,
// 'AudioRewind': TerminalKey.audioRewind,
// 'BackForward': TerminalKey.backForward,
// 'ApplicationLeft': TerminalKey.applicationLeft,
// 'ApplicationRight': TerminalKey.applicationRight,
// 'Book': TerminalKey.book,
// 'CD': TerminalKey.cD,
// 'Calculator': TerminalKey.calculator,
// 'ToDoList': TerminalKey.toDoList,
// 'ClearGrab': TerminalKey.clearGrab,
'Close': TerminalKey.close,
'Copy': TerminalKey.copy,
'Cut': TerminalKey.cut,
// 'Display': TerminalKey.display,
// 'DOS': TerminalKey.dOS,
// 'Documents': TerminalKey.documents,
// 'Excel': TerminalKey.excel,
// 'Explorer': TerminalKey.explorer,
// 'Game': TerminalKey.game,
// 'Go': TerminalKey.go,
// 'iTouch': TerminalKey.iTouch,
// 'LogOff': TerminalKey.logOff,
// 'Market': TerminalKey.market,
// 'Meeting': TerminalKey.meeting,
// 'MenuKB': TerminalKey.menuKB,
// 'MenuPB': TerminalKey.menuPB,
// 'MySites': TerminalKey.mySites,
// 'News': TerminalKey.news,
// 'OfficeHome': TerminalKey.officeHome,
// 'Option': TerminalKey.option,
// 'Paste': TerminalKey.paste,
// 'Phone': TerminalKey.phone,
// 'Calendar': TerminalKey.calendar,
// 'Reply': TerminalKey.reply,
// 'Reload': TerminalKey.reload,
// 'RotateWindows': TerminalKey.rotateWindows,
// 'RotationPB': TerminalKey.rotationPB,
// 'RotationKB': TerminalKey.rotationKB,
'Save': TerminalKey.save,
// 'Send': TerminalKey.send,
// 'Spell': TerminalKey.spell,
// 'SplitScreen': TerminalKey.splitScreen,
// 'Support': TerminalKey.support,
// 'TaskPane': TerminalKey.taskPane,
// 'Terminal': TerminalKey.terminal,
// 'Tools': TerminalKey.tools,
// 'Travel': TerminalKey.travel,
// 'Video': TerminalKey.video,
// 'Word': TerminalKey.word,
// 'Xfer': TerminalKey.xfer,
'ZoomIn': TerminalKey.zoomIn,
'ZoomOut': TerminalKey.zoomOut,
// 'Away': TerminalKey.away,
// 'Messenger': TerminalKey.messenger,
// 'WebCam': TerminalKey.webCam,
// 'MailForward': TerminalKey.mailForward,
// 'Pictures': TerminalKey.pictures,
// 'Music': TerminalKey.music,
// 'Battery': TerminalKey.battery,
// 'Bluetooth': TerminalKey.bluetooth,
// 'WLAN': TerminalKey.wLAN,
// 'UWB': TerminalKey.uWB,
// 'AudioForward': TerminalKey.audioForward,
// 'AudioRepeat': TerminalKey.audioRepeat,
// 'AudioRandomPlay': TerminalKey.audioRandomPlay,
// 'Subtitle': TerminalKey.subtitle,
// 'AudioCycleTrack': TerminalKey.audioCycleTrack,
// 'Time': TerminalKey.time,
// 'Hibernate': TerminalKey.hibernate,
// 'View': TerminalKey.view,
// 'TopMenu': TerminalKey.topMenu,
// 'PowerDown': TerminalKey.powerDown,
// 'Suspend': TerminalKey.suspend,
// 'ContrastAdjust': TerminalKey.contrastAdjust,
// 'TouchpadToggle': TerminalKey.touchpadToggle,
// 'TouchpadOn': TerminalKey.touchpadOn,
// 'TouchpadOff': TerminalKey.touchpadOff,
// 'MicMute': TerminalKey.micMute,
// 'Red': TerminalKey.red,
// 'Green': TerminalKey.green,
// 'Yellow': TerminalKey.yellow,
// 'Blue': TerminalKey.blue,
'ChannelUp': TerminalKey.channelUp,
'ChannelDown': TerminalKey.channelDown,
// 'Guide': TerminalKey.guide,
'Info': TerminalKey.info,
// 'Settings': TerminalKey.settings,
// 'MicVolumeUp': TerminalKey.micVolumeUp,
// 'MicVolumeDown': TerminalKey.micVolumeDown,
// 'New': TerminalKey.new,
'Open': TerminalKey.open,
'Find': TerminalKey.find,
'Undo': TerminalKey.undo,
'Redo': TerminalKey.redo,
'MediaLast': TerminalKey.mediaLast,
// 'unknown': TerminalKey.unknown,
// 'Call': TerminalKey.call,
// 'Camera': TerminalKey.camera,
// 'CameraFocus': TerminalKey.cameraFocus,
// 'Context1': TerminalKey.context1,
// 'Context2': TerminalKey.context2,
// 'Context3': TerminalKey.context3,
// 'Context4': TerminalKey.context4,
// 'Flip': TerminalKey.flip,
// 'Hangup': TerminalKey.hangup,
// 'No': TerminalKey.no,
'Select': TerminalKey.select,
// 'Yes': TerminalKey.yes,
// 'ToggleCallHangup': TerminalKey.toggleCallHangup,
// 'VoiceDial': TerminalKey.voiceDial,
// 'LastNumberRedial': TerminalKey.lastNumberRedial,
// 'Execute': TerminalKey.execute,
// 'Printer': TerminalKey.printer,
// 'Play': TerminalKey.play,
'Sleep': TerminalKey.sleep,
// 'Zoom': TerminalKey.zoom,
'Exit': TerminalKey.exit,
// 'Cancel': TerminalKey.cancel,
};
@@ -0,0 +1,31 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalMouseButton {
left(id: 0),
middle(id: 1),
right(id: 2),
wheelUp(id: 64 + 4, isWheel: true),
wheelDown(id: 64 + 5, isWheel: true),
wheelLeft(id: 64 + 6, isWheel: true),
wheelRight(id: 64 + 7, isWheel: true),
;
/// The id that is used to report a button press or release to the terminal.
///
/// Mouse wheel up / down use button IDs 4 = 0100 (binary) and 5 = 0101 (binary).
/// The bits three and four of the button are transposed by 64 and 128
/// respectively, when reporting the id of the button and have have to be
/// adjusted correspondingly.
final int id;
/// Whether this button is a mouse wheel button.
final bool isWheel;
const TerminalMouseButton({required this.id, this.isWheel = false});
}
@@ -0,0 +1,7 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalMouseButtonState {
up,
down,
}
@@ -0,0 +1,116 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/reporter.dart';
import 'package:clide/src/terminal/src/core/platform.dart';
import 'package:clide/src/terminal/src/core/state.dart';
class TerminalMouseEvent {
/// The button that is pressed or released.
final TerminalMouseButton button;
/// The current state of the button.
final TerminalMouseButtonState buttonState;
/// The position of button state change.
final CellOffset position;
/// The state of the terminal.
final TerminalState state;
/// The platform of the terminal.
final TerminalTargetPlatform platform;
TerminalMouseEvent({
required this.button,
required this.buttonState,
required this.position,
required this.state,
required this.platform,
});
}
const defaultMouseHandler = CascadeMouseHandler([
ClickMouseHandler(),
UpDownMouseHandler(),
]);
abstract class TerminalMouseHandler {
const TerminalMouseHandler();
String? call(TerminalMouseEvent event);
}
class CascadeMouseHandler implements TerminalMouseHandler {
final List<TerminalMouseHandler> _handlers;
const CascadeMouseHandler(this._handlers);
@override
String? call(TerminalMouseEvent event) {
for (var handler in _handlers) {
final result = handler(event);
if (result != null) {
return result;
}
}
return null;
}
}
class ClickMouseHandler implements TerminalMouseHandler {
const ClickMouseHandler();
@override
String? call(TerminalMouseEvent event) {
switch (event.state.mouseMode) {
case MouseMode.clickOnly:
// Only clicks and only the first 3 buttons are reported.
if (event.buttonState == TerminalMouseButtonState.down &&
(event.button.id < 3)) {
return MouseReporter.report(
event.button,
event.buttonState,
event.position,
event.state.mouseReportMode,
);
}
return null;
case MouseMode.none:
case MouseMode.upDownScroll:
case MouseMode.upDownScrollDrag:
case MouseMode.upDownScrollMove:
return null;
}
}
}
class UpDownMouseHandler implements TerminalMouseHandler {
const UpDownMouseHandler();
@override
String? call(TerminalMouseEvent event) {
switch (event.state.mouseMode) {
case MouseMode.none:
case MouseMode.clickOnly:
return null;
case MouseMode.upDownScroll:
case MouseMode.upDownScrollDrag:
case MouseMode.upDownScrollMove:
// Up events are never reported for mouse wheel buttons.
if (event.button.isWheel &&
event.buttonState == TerminalMouseButtonState.up) {
return null;
}
return MouseReporter.report(
event.button,
event.buttonState,
event.position,
event.state.mouseReportMode,
);
}
}
}
+38
View File
@@ -0,0 +1,38 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
/// https://terminalguide.namepad.de/mouse/
enum MouseMode {
none,
clickOnly,
upDownScroll(reportScroll: true),
upDownScrollDrag(reportScroll: true),
upDownScrollMove(reportScroll: true),
;
const MouseMode({this.reportScroll = false});
final bool reportScroll;
}
/// https://terminalguide.namepad.de/mouse/
enum MouseReportMode {
/// The default mouse reporting mode where digits are encoded as bytes with
/// `32 + code`. This mode has a range from 1 to 223.
normal,
/// When code < 96 this is the same as [normal], otherwise the `code + 32` is
/// encoded as 2 bytes in UTF-8. This mode has a range from 1 to 2015.
utf,
/// In this mode the code are encoded as 10-based numbers. Tha range is
/// unlimited.
sgr,
/// Similar to [sgr], the difference is that the button id is encoded as
/// `32 + code`.
urxvt,
}
@@ -0,0 +1,50 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
abstract class MouseReporter {
static String report(
TerminalMouseButton button,
TerminalMouseButtonState state,
CellOffset position,
MouseReportMode reportMode,
) {
// x and y offsets have to be incremented by 1 as the offset if 0-based,
// The position has to be reported using 1-based coordinates.
final x = position.x + 1;
final y = position.y + 1;
switch (reportMode) {
case MouseReportMode.normal:
case MouseReportMode.utf:
// Button ID 3 is used to signal a button release.
final buttonID = state == TerminalMouseButtonState.up ? 3 : button.id;
// The button ID is reported as shifted by 32 to produce a printable
// character.
final btn = String.fromCharCode(32 + buttonID);
// Normal mode only supports a maximum position of 223, while utf
// supports positions up to 2015. Both modes send a null byte if the
// position exceeds that limit.
final col = (reportMode == MouseReportMode.normal && x > 223) ||
(reportMode == MouseReportMode.utf && x > 2015)
? '\x00'
: String.fromCharCode(32 + x);
final row = (reportMode == MouseReportMode.normal && y > 223) ||
(reportMode == MouseReportMode.utf && y > 2015)
? '\x00'
: String.fromCharCode(32 + y + 1);
return "\x1b[M$btn$col$row";
case MouseReportMode.sgr:
final buttonID = button.id;
final upDown = state == TerminalMouseButtonState.down ? 'M' : 'm';
return "\x1b[<$buttonID;$x;$y$upDown";
case MouseReportMode.urxvt:
// The button ID uses the same id as to report it as in normal mode.
final buttonID =
32 + (state == TerminalMouseButtonState.up ? 3 : button.id);
return "\x1b[$buttonID;$x;${y}M";
}
}
}
+19
View File
@@ -0,0 +1,19 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalTargetPlatform {
unknown,
android,
ios,
fuchsia,
linux,
macos,
windows,
web,
}
+199
View File
@@ -0,0 +1,199 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/buffer/line.dart';
import 'package:clide/src/terminal/src/utils/circular_buffer.dart';
class _LineBuilder {
_LineBuilder([this._capacity = 80]) {
_result = BufferLine(_capacity);
}
final int _capacity;
late BufferLine _result;
int _length = 0;
int get length => _length;
bool get isEmpty => _length == 0;
bool get isNotEmpty => _length != 0;
/// Adds a range of cells from [src] to the builder. Anchors within the range
/// will be reparented to the new line returned by [take].
void add(BufferLine src, int start, int length) {
_result.copyFrom(src, start, _length, length);
_length += length;
}
/// Reuses the given [line] as the initial buffer for this builder.
void setBuffer(BufferLine line, int length) {
_result = line;
_length = length;
}
void addAnchor(CellAnchor anchor, int offset) {
anchor.reparent(_result, _length + offset);
}
BufferLine take({required bool wrapped}) {
final result = _result;
result.isWrapped = wrapped;
// result.resize(_length);
_result = BufferLine(_capacity);
_length = 0;
return result;
}
}
/// Holds a the state of reflow operation of a single logical line.
class _LineReflow {
final int oldWidth;
final int newWidth;
_LineReflow(this.oldWidth, this.newWidth);
final _lines = <BufferLine>[];
late final _builder = _LineBuilder(newWidth);
/// Adds a line to the reflow operation. This method will try to reuse the
/// given line if possible.
void add(BufferLine line) {
final trimmedLength = line.getTrimmedLength(oldWidth);
// A fast path for empty lines
if (trimmedLength == 0) {
_lines.add(line);
return;
}
// We already have some content in the buffer, so we copy the content into
// the builder instead of reusing the line.
if (_lines.isNotEmpty || _builder.isNotEmpty) {
_addPart(line, from: 0, to: trimmedLength);
return;
}
if (newWidth >= oldWidth) {
// Reuse the line to avoid copying the content and object allocation.
_builder.setBuffer(line, trimmedLength);
} else {
_lines.add(line);
if (trimmedLength > newWidth) {
if (line.getWidth(newWidth - 1) == 2) {
_addPart(line, from: newWidth - 1, to: trimmedLength);
} else {
_addPart(line, from: newWidth, to: trimmedLength);
}
}
}
line.resize(newWidth);
if (line.getWidth(newWidth - 1) == 2) {
line.resetCell(newWidth - 1);
}
}
/// Adds part of [line] from [from] to [to] to the reflow operation.
/// Anchors within the range will be removed from [line] and reparented to
/// the new line(s) returned by [finish].
void _addPart(BufferLine line, {required int from, required int to}) {
var cellsLeft = to - from;
while (cellsLeft > 0) {
final bufferRemainingCells = newWidth - _builder.length;
// How many cells we should copy in this iteration.
var cellsToCopy = cellsLeft;
// Whether the buffer is filled up in this iteration.
var lineFilled = false;
if (cellsToCopy >= bufferRemainingCells) {
cellsToCopy = bufferRemainingCells;
lineFilled = true;
}
// Leave the last cell to the next iteration if it's a wide char.
if (lineFilled && line.getWidth(from + cellsToCopy - 1) == 2) {
cellsToCopy--;
}
for (var anchor in line.anchors.toList()) {
if (anchor.x >= from && anchor.x <= from + cellsToCopy) {
_builder.addAnchor(anchor, anchor.x - from);
}
}
_builder.add(line, from, cellsToCopy);
from += cellsToCopy;
cellsLeft -= cellsToCopy;
// Create a new line if the buffer is filled up.
if (lineFilled) {
_lines.add(_builder.take(wrapped: _lines.isNotEmpty));
}
}
if (line.anchors.isNotEmpty) {
for (var anchor in line.anchors.toList()) {
if (anchor.x >= to) {
_builder.addAnchor(anchor, anchor.x - to);
}
}
}
}
/// Finalizes the reflow operation and returns the result.
List<BufferLine> finish() {
if (_builder.isNotEmpty) {
_lines.add(_builder.take(wrapped: _lines.isNotEmpty));
}
return _lines;
}
}
List<BufferLine> reflow(
IndexAwareCircularBuffer<BufferLine> lines,
int oldWidth,
int newWidth,
) {
final result = <BufferLine>[];
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
final reflow = _LineReflow(oldWidth, newWidth);
reflow.add(line);
for (var offset = i + 1; offset < lines.length; offset++) {
final nextLine = lines[offset];
if (!nextLine.isWrapped) {
break;
}
i++;
reflow.add(nextLine);
}
result.addAll(reflow.finish());
}
for (var line in result) {
line.resize(newWidth);
}
return result;
}
+5
View File
@@ -0,0 +1,5 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
abstract class TerminalSnapshot {
void trimScrollback();
}
+46
View File
@@ -0,0 +1,46 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:clide/src/terminal/src/core/cursor.dart';
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
abstract class TerminalState {
int get viewWidth;
int get viewHeight;
CursorStyle get cursor;
bool get reflowEnabled;
/* Modes */
bool get insertMode;
bool get lineFeedMode;
/* DEC Private modes */
bool get cursorKeysMode;
bool get reverseDisplayMode;
bool get originMode;
bool get autoWrapMode;
MouseMode get mouseMode;
MouseReportMode get mouseReportMode;
bool get cursorBlinkMode;
bool get cursorVisibleMode;
bool get appKeypadMode;
bool get reportFocusMode;
bool get altBufferMouseScrollMode;
bool get bracketedPasteMode;
}
+70
View File
@@ -0,0 +1,70 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show min;
const _kMaxColumns = 1024;
/// Manages the tab stop state for a terminal.
class TabStops {
final _stops = List<bool>.filled(_kMaxColumns, false);
TabStops() {
_initialize();
}
/// Initializes the tab stops to the default 8 column intervals.
void _initialize() {
const interval = 8;
for (var i = 0; i < _kMaxColumns; i += interval) {
_stops[i] = true;
}
}
/// Finds the next tab stop index, which satisfies [start] <= index < [end].
int? find(int start, int end) {
if (start >= end) {
return null;
}
end = min(end, _stops.length);
for (var i = start; i < end; i++) {
if (_stops[i]) {
return i;
}
}
return null;
}
/// Sets the tab stop at [index]. If there is already a tab stop at [index],
/// this method does nothing.
///
/// See also:
/// * [clearAt] which does the opposite.
void setAt(int index) {
assert(index >= 0 && index < _kMaxColumns);
_stops[index] = true;
}
/// Clears the tab stop at [index]. If there is no tab stop at [index], this
/// method does nothing.
void clearAt(int index) {
assert(index >= 0 && index < _kMaxColumns);
_stops[index] = false;
}
/// Clears all tab stops without resetting them to the default 8 column
/// intervals.
void clearAll() {
_stops.fillRange(0, _kMaxColumns, false);
}
/// Returns true if there is a tab stop at [index].
bool isSetAt(int index) {
return _stops[index];
}
/// Resets the tab stops to the default 8 column intervals.
void reset() {
clearAll();
_initialize();
}
}
+908
View File
@@ -0,0 +1,908 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show max;
import 'package:clide/src/terminal/src/base/observable.dart';
import 'package:clide/src/terminal/src/core/buffer/buffer.dart';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/line.dart';
import 'package:clide/src/terminal/src/core/cursor.dart';
import 'package:clide/src/terminal/src/core/escape/emitter.dart';
import 'package:clide/src/terminal/src/core/escape/handler.dart';
import 'package:clide/src/terminal/src/core/escape/parser.dart';
import 'package:clide/src/terminal/src/core/input/handler.dart';
import 'package:clide/src/terminal/src/core/input/keys.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
import 'package:clide/src/terminal/src/core/mouse/handler.dart';
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
import 'package:clide/src/terminal/src/core/platform.dart';
import 'package:clide/src/terminal/src/core/state.dart';
import 'package:clide/src/terminal/src/core/tabs.dart';
import 'package:clide/src/terminal/src/utils/ascii.dart';
import 'package:clide/src/terminal/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<String> 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<int>? 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<BufferLine> 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<String> pt) {
onPrivateOSC?.call(ps, pt);
}
}
+545
View File
@@ -0,0 +1,545 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/input/keys.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
import 'package:clide/src/terminal/src/terminal.dart';
import 'package:clide/src/terminal/src/ui/controller.dart';
import 'package:clide/src/terminal/src/ui/cursor_type.dart';
import 'package:clide/src/terminal/src/ui/custom_text_edit.dart';
import 'package:clide/src/terminal/src/ui/gesture/gesture_handler.dart';
import 'package:clide/src/terminal/src/ui/input_map.dart';
import 'package:clide/src/terminal/src/ui/keyboard_listener.dart';
import 'package:clide/src/terminal/src/ui/keyboard_visibility.dart';
import 'package:clide/src/terminal/src/ui/render.dart';
import 'package:clide/src/terminal/src/ui/shortcut/actions.dart';
import 'package:clide/src/terminal/src/ui/shortcut/shortcuts.dart';
import 'package:clide/src/terminal/src/ui/terminal_text_style.dart';
import 'package:clide/src/terminal/src/ui/terminal_theme.dart';
import 'package:clide/src/terminal/src/ui/themes.dart';
class TerminalView extends StatefulWidget {
const TerminalView(
this.terminal, {
super.key,
this.controller,
this.theme = TerminalThemes.defaultTheme,
this.textStyle = const TerminalStyle(),
this.textScaler,
this.padding,
this.scrollController,
this.autoResize = true,
this.backgroundOpacity = 1,
this.focusNode,
this.autofocus = false,
this.onTapUp,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.mouseCursor = SystemMouseCursors.text,
this.keyboardType = TextInputType.emailAddress,
this.keyboardAppearance = Brightness.dark,
this.cursorType = TerminalCursorType.block,
this.alwaysShowCursor = false,
this.deleteDetection = false,
this.shortcuts,
this.onKeyEvent,
this.readOnly = false,
this.hardwareKeyboardOnly = false,
this.simulateScroll = true,
});
/// The underlying terminal that this widget renders.
final Terminal terminal;
final TerminalController? controller;
/// The theme to use for this terminal.
final TerminalTheme theme;
/// The style to use for painting characters.
final TerminalStyle textStyle;
final TextScaler? textScaler;
/// Padding around the inner [Scrollable] widget.
final EdgeInsets? padding;
/// Scroll controller for the inner [Scrollable] widget.
final ScrollController? scrollController;
/// Should this widget automatically notify the underlying terminal when its
/// size changes. [true] by default.
final bool autoResize;
/// Opacity of the terminal background. Set to 0 to make the terminal
/// background transparent.
final double backgroundOpacity;
/// An optional focus node to use as the focus node for this widget.
final FocusNode? focusNode;
/// True if this widget will be selected as the initial focus when no other
/// node in its scope is currently focused.
final bool autofocus;
/// Callback for when the user taps on the terminal.
final void Function(TapUpDetails, CellOffset)? onTapUp;
/// Function called when the user taps on the terminal with a secondary
/// button.
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
/// Function called when the user stops holding down a secondary button.
final void Function(TapUpDetails, CellOffset)? onSecondaryTapUp;
/// The mouse cursor for mouse pointers that are hovering over the terminal.
/// [SystemMouseCursors.text] by default.
final MouseCursor mouseCursor;
/// The type of information for which to optimize the text input control.
/// [TextInputType.emailAddress] by default.
final TextInputType keyboardType;
/// The appearance of the keyboard. [Brightness.dark] by default.
///
/// This setting is only honored on iOS devices.
final Brightness keyboardAppearance;
/// The type of cursor to use. [TerminalCursorType.block] by default.
final TerminalCursorType cursorType;
/// Whether to always show the cursor. This is useful for debugging.
/// [false] by default.
final bool alwaysShowCursor;
/// Workaround to detect delete key for platforms and IMEs that does not
/// emit hardware delete event. Prefered on mobile platforms. [false] by
/// default.
final bool deleteDetection;
/// Shortcuts for this terminal. This has higher priority than input handler
/// of the terminal If not provided, [defaultTerminalShortcuts] will be used.
final Map<ShortcutActivator, Intent>? shortcuts;
/// Keyboard event handler of the terminal. This has higher priority than
/// [shortcuts] and input handler of the terminal.
final FocusOnKeyEventCallback? onKeyEvent;
/// True if no input should send to the terminal.
final bool readOnly;
/// True if only hardware keyboard events should be used as input. This will
/// also prevent any on-screen keyboard to be shown.
final bool hardwareKeyboardOnly;
/// If true, when the terminal is in alternate buffer (for example running
/// vim, man, etc), if the application does not declare that it can handle
/// scrolling, the terminal will simulate scrolling by sending up/down arrow
/// keys to the application. This is standard behavior for most terminal
/// emulators. True by default.
final bool simulateScroll;
@override
State<TerminalView> createState() => TerminalViewState();
}
class TerminalViewState extends State<TerminalView> {
late FocusNode _focusNode;
late final ShortcutManager _shortcutManager;
final _customTextEditKey = GlobalKey<CustomTextEditState>();
final _scrollableKey = GlobalKey<ScrollableState>();
final _viewportKey = GlobalKey();
String? _composingText;
late TerminalController _controller;
late ScrollController _scrollController;
RenderTerminal get renderTerminal =>
_viewportKey.currentContext!.findRenderObject() as RenderTerminal;
void _onPointerSignal(PointerSignalEvent event) {
if (event is! PointerScrollEvent) return;
final lh = renderTerminal.lineHeight;
if (lh <= 0) return;
final position = renderTerminal.getCellOffset(event.localPosition);
final lines = (event.scrollDelta.dy / lh).round().clamp(-5, 5);
for (var i = 0; i < lines.abs(); i++) {
final up = lines < 0;
final handled = widget.terminal.mouseInput(
up ? TerminalMouseButton.wheelUp : TerminalMouseButton.wheelDown,
TerminalMouseButtonState.down,
position,
);
if (!handled && widget.simulateScroll) {
widget.terminal.keyInput(up ? TerminalKey.arrowUp : TerminalKey.arrowDown);
}
}
}
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_controller = widget.controller ?? TerminalController();
_scrollController = widget.scrollController ?? ScrollController();
_shortcutManager = ShortcutManager(
shortcuts: widget.shortcuts ?? defaultTerminalShortcuts,
);
super.initState();
}
@override
void didUpdateWidget(TerminalView oldWidget) {
if (oldWidget.focusNode != widget.focusNode) {
if (oldWidget.focusNode == null) {
_focusNode.dispose();
}
_focusNode = widget.focusNode ?? FocusNode();
}
if (oldWidget.controller != widget.controller) {
if (oldWidget.controller == null) {
_controller.dispose();
}
_controller = widget.controller ?? TerminalController();
}
if (oldWidget.scrollController != widget.scrollController) {
if (oldWidget.scrollController == null) {
_scrollController.dispose();
}
_scrollController = widget.scrollController ?? ScrollController();
}
_shortcutManager.shortcuts = widget.shortcuts ?? defaultTerminalShortcuts;
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
}
if (widget.controller == null) {
_controller.dispose();
}
if (widget.scrollController == null) {
_scrollController.dispose();
}
_shortcutManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget child = _TerminalView(
key: _viewportKey,
terminal: widget.terminal,
controller: _controller,
offset: ViewportOffset.zero(),
padding: MediaQuery.of(context).padding,
autoResize: widget.autoResize,
textStyle: widget.textStyle,
textScaler: widget.textScaler ?? MediaQuery.textScalerOf(context),
theme: widget.theme,
focusNode: _focusNode,
cursorType: widget.cursorType,
alwaysShowCursor: widget.alwaysShowCursor,
onEditableRect: _onEditableRect,
composingText: _composingText,
);
if (!widget.hardwareKeyboardOnly) {
child = CustomTextEdit(
key: _customTextEditKey,
focusNode: _focusNode,
autofocus: widget.autofocus,
inputType: widget.keyboardType,
keyboardAppearance: widget.keyboardAppearance,
deleteDetection: widget.deleteDetection,
onInsert: _onInsert,
onDelete: () {
_scrollToBottom();
widget.terminal.keyInput(TerminalKey.backspace);
},
onComposing: _onComposing,
onAction: (action) {
_scrollToBottom();
if (action == TextInputAction.done) {
widget.terminal.keyInput(TerminalKey.enter);
}
},
onKeyEvent: _handleKeyEvent,
readOnly: widget.readOnly,
child: child,
);
} else if (!widget.readOnly) {
// Only listen for key input from a hardware keyboard.
child = CustomKeyboardListener(
child: child,
focusNode: _focusNode,
autofocus: widget.autofocus,
onInsert: _onInsert,
onComposing: _onComposing,
onKeyEvent: _handleKeyEvent,
);
}
child = TerminalActions(
terminal: widget.terminal,
controller: _controller,
child: child,
);
child = KeyboardVisibilty(
onKeyboardShow: _onKeyboardShow,
child: child,
);
child = TerminalGestureHandler(
terminalView: this,
terminalController: _controller,
onTapUp: _onTapUp,
onTapDown: _onTapDown,
onSecondaryTapDown:
widget.onSecondaryTapDown != null ? _onSecondaryTapDown : null,
onSecondaryTapUp:
widget.onSecondaryTapUp != null ? _onSecondaryTapUp : null,
readOnly: widget.readOnly,
child: child,
);
child = MouseRegion(
cursor: widget.mouseCursor,
child: child,
);
child = Container(
color: widget.theme.background.withOpacity(widget.backgroundOpacity),
padding: widget.padding,
child: child,
);
return Listener(
onPointerSignal: _onPointerSignal,
child: child,
);
}
void requestKeyboard() {
_customTextEditKey.currentState?.requestKeyboard();
}
void closeKeyboard() {
_customTextEditKey.currentState?.closeKeyboard();
}
Rect get cursorRect {
return renderTerminal.cursorOffset & renderTerminal.cellSize;
}
Rect get globalCursorRect {
return renderTerminal.localToGlobal(renderTerminal.cursorOffset) &
renderTerminal.cellSize;
}
void _onTapUp(TapUpDetails details) {
final offset = renderTerminal.getCellOffset(details.localPosition);
widget.onTapUp?.call(details, offset);
}
void _onTapDown(_) {
if (_controller.selection != null) {
_controller.clearSelection();
} else {
if (!widget.hardwareKeyboardOnly) {
_customTextEditKey.currentState?.requestKeyboard();
} else {
_focusNode.requestFocus();
}
}
}
void _onSecondaryTapDown(TapDownDetails details) {
final offset = renderTerminal.getCellOffset(details.localPosition);
widget.onSecondaryTapDown?.call(details, offset);
}
void _onSecondaryTapUp(TapUpDetails details) {
final offset = renderTerminal.getCellOffset(details.localPosition);
widget.onSecondaryTapUp?.call(details, offset);
}
bool get hasInputConnection {
return _customTextEditKey.currentState?.hasInputConnection == true;
}
void _onInsert(String text) {
final key = charToTerminalKey(text.trim());
// On mobile platforms there is no guarantee that virtual keyboard will
// generate hardware key events. So we need first try to send the key
// as a hardware key event. If it fails, then we send it as a text input.
final consumed = key == null ? false : widget.terminal.keyInput(key);
if (!consumed) {
widget.terminal.textInput(text);
}
_scrollToBottom();
}
void _onComposing(String? text) {
setState(() => _composingText = text);
}
KeyEventResult _handleKeyEvent(FocusNode focusNode, KeyEvent event) {
final resultOverride = widget.onKeyEvent?.call(focusNode, event);
if (resultOverride != null && resultOverride != KeyEventResult.ignored) {
return resultOverride;
}
// ignore: invalid_use_of_protected_member
final shortcutResult = _shortcutManager.handleKeypress(
focusNode.context!,
event,
);
if (shortcutResult != KeyEventResult.ignored) {
return shortcutResult;
}
if (event is KeyUpEvent) {
return KeyEventResult.ignored;
}
final key = keyToTerminalKey(event.logicalKey);
if (key == null) {
return KeyEventResult.ignored;
}
final handled = widget.terminal.keyInput(
key,
ctrl: HardwareKeyboard.instance.isControlPressed,
alt: HardwareKeyboard.instance.isAltPressed,
shift: HardwareKeyboard.instance.isShiftPressed,
);
if (handled) {
_scrollToBottom();
}
return handled ? KeyEventResult.handled : KeyEventResult.ignored;
}
void _onKeyboardShow() {
if (_focusNode.hasFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
}
void _onEditableRect(Rect rect, Rect caretRect) {
_customTextEditKey.currentState?.setEditableRect(rect, caretRect);
}
void _scrollToBottom() {
final position = _scrollableKey.currentState?.position;
if (position != null) {
position.jumpTo(position.maxScrollExtent);
}
}
}
class _TerminalView extends LeafRenderObjectWidget {
const _TerminalView({
super.key,
required this.terminal,
required this.controller,
required this.offset,
required this.padding,
required this.autoResize,
required this.textStyle,
required this.textScaler,
required this.theme,
required this.focusNode,
required this.cursorType,
required this.alwaysShowCursor,
this.onEditableRect,
this.composingText,
});
final Terminal terminal;
final TerminalController controller;
final ViewportOffset offset;
final EdgeInsets padding;
final bool autoResize;
final TerminalStyle textStyle;
final TextScaler textScaler;
final TerminalTheme theme;
final FocusNode focusNode;
final TerminalCursorType cursorType;
final bool alwaysShowCursor;
final EditableRectCallback? onEditableRect;
final String? composingText;
@override
RenderTerminal createRenderObject(BuildContext context) {
return RenderTerminal(
terminal: terminal,
controller: controller,
offset: offset,
padding: padding,
autoResize: autoResize,
textStyle: textStyle,
textScaler: textScaler,
theme: theme,
focusNode: focusNode,
cursorType: cursorType,
alwaysShowCursor: alwaysShowCursor,
onEditableRect: onEditableRect,
composingText: composingText,
);
}
@override
void updateRenderObject(BuildContext context, RenderTerminal renderObject) {
renderObject
..terminal = terminal
..controller = controller
..offset = offset
..padding = padding
..autoResize = autoResize
..textStyle = textStyle
..textScaler = textScaler
..theme = theme
..focusNode = focusNode
..cursorType = cursorType
..alwaysShowCursor = alwaysShowCursor
..onEditableRect = onEditableRect
..composingText = composingText;
}
}
+23
View File
@@ -0,0 +1,23 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:clide/src/terminal/src/ui/terminal_text_style.dart';
Size calcCharSize(TerminalStyle style, TextScaler textScaler) {
const test = 'mmmmmmmmmm';
final textStyle = style.toTextStyle();
final builder = ParagraphBuilder(textStyle.getParagraphStyle());
builder.pushStyle(textStyle.getTextStyle(textScaler: textScaler));
builder.addText(test);
final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: double.infinity));
return Size(
paragraph.maxIntrinsicWidth / test.length,
paragraph.height,
);
}
+176
View File
@@ -0,0 +1,176 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:clide/src/terminal/src/base/disposable.dart';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/line.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
import 'package:clide/src/terminal/src/core/buffer/range_block.dart';
import 'package:clide/src/terminal/src/core/buffer/range_line.dart';
import 'package:clide/src/terminal/src/ui/pointer_input.dart';
import 'package:clide/src/terminal/src/ui/selection_mode.dart';
class TerminalController with ChangeNotifier {
TerminalController({
SelectionMode selectionMode = SelectionMode.line,
PointerInputs pointerInputs = const PointerInputs({PointerInput.tap}),
bool suspendPointerInput = false,
}) : _selectionMode = selectionMode,
_pointerInputs = pointerInputs,
_suspendPointerInputs = suspendPointerInput;
CellAnchor? _selectionBase;
CellAnchor? _selectionExtent;
SelectionMode get selectionMode => _selectionMode;
SelectionMode _selectionMode;
/// The set of pointer events which will be used as mouse input for the terminal.
PointerInputs get pointerInput => _pointerInputs;
PointerInputs _pointerInputs;
/// True if sending pointer events to the terminal is suspended.
bool get suspendedPointerInputs => _suspendPointerInputs;
bool _suspendPointerInputs;
List<TerminalHighlight> get highlights => _highlights;
final _highlights = <TerminalHighlight>[];
BufferRange? get selection {
final base = _selectionBase;
final extent = _selectionExtent;
if (base == null || extent == null) {
return null;
}
if (!base.attached || !extent.attached) {
return null;
}
return _createRange(base.offset, extent.offset);
}
/// Set selection on the terminal from [base] to [extent]. This method takes
/// the ownership of [base] and [extent] and will dispose them when the
/// selection is cleared or changed.
void setSelection(CellAnchor base, CellAnchor extent, {SelectionMode? mode}) {
_selectionBase?.dispose();
_selectionBase = base;
_selectionExtent?.dispose();
_selectionExtent = extent;
if (mode != null) {
_selectionMode = mode;
}
notifyListeners();
}
BufferRange _createRange(CellOffset begin, CellOffset end) {
switch (selectionMode) {
case SelectionMode.line:
return BufferRangeLine(begin, end);
case SelectionMode.block:
return BufferRangeBlock(begin, end);
}
}
/// Controls how the terminal behaves when the user selects a range of text.
/// The default is [SelectionMode.line]. Setting this to [SelectionMode.block]
/// enables block selection mode.
void setSelectionMode(SelectionMode newSelectionMode) {
// If the new mode is the same as the old mode,
// nothing has to be changed.
if (_selectionMode == newSelectionMode) {
return;
}
// Set the new mode.
_selectionMode = newSelectionMode;
notifyListeners();
}
/// Clears the current selection.
void clearSelection() {
_selectionBase?.dispose();
_selectionBase = null;
_selectionExtent?.dispose();
_selectionExtent = null;
notifyListeners();
}
// Select which type of pointer events are send to the terminal.
void setPointerInputs(PointerInputs pointerInput) {
_pointerInputs = pointerInput;
notifyListeners();
}
// Toggle sending pointer events to the terminal.
void setSuspendPointerInput(bool suspend) {
_suspendPointerInputs = suspend;
notifyListeners();
}
// Returns true if this type of PointerInput should be send to the Terminal.
@internal
bool shouldSendPointerInput(PointerInput pointerInput) {
// Always return false if pointer input is suspended.
return _suspendPointerInputs
? false
: _pointerInputs.inputs.contains(pointerInput);
}
/// Creates a new highlight on the terminal from [p1] to [p2] with the given
/// [color]. The highlight will be removed when the returned object is
/// disposed.
TerminalHighlight highlight({
required CellAnchor p1,
required CellAnchor p2,
required Color color,
}) {
final highlight = TerminalHighlight(
this,
p1: p1,
p2: p2,
color: color,
);
_highlights.add(highlight);
notifyListeners();
highlight.registerCallback(() {
_highlights.remove(highlight);
notifyListeners();
});
return highlight;
}
}
class TerminalHighlight with Disposable {
final TerminalController owner;
final CellAnchor p1;
final CellAnchor p2;
final Color color;
TerminalHighlight(
this.owner, {
required this.p1,
required this.p2,
required this.color,
});
/// Returns the range of the highlight. May be null if the anchors that
/// define the highlight are not attached to the terminal.
BufferRange? get range {
if (!p1.attached || !p2.attached) {
return null;
}
return BufferRangeLine(p1.offset, p2.offset);
}
}
+9
View File
@@ -0,0 +1,9 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalCursorType {
block,
underline,
verticalBar,
}
@@ -0,0 +1,283 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextEdit extends StatefulWidget {
CustomTextEdit({
super.key,
required this.child,
required this.onInsert,
required this.onDelete,
required this.onComposing,
required this.onAction,
required this.onKeyEvent,
required this.focusNode,
this.autofocus = false,
this.readOnly = false,
// this.initEditingState = TextEditingValue.empty,
this.inputType = TextInputType.text,
this.inputAction = TextInputAction.newline,
this.keyboardAppearance = Brightness.light,
this.deleteDetection = false,
});
final Widget child;
final void Function(String) onInsert;
final void Function() onDelete;
final void Function(String?) onComposing;
final void Function(TextInputAction) onAction;
final KeyEventResult Function(FocusNode, KeyEvent) onKeyEvent;
final FocusNode focusNode;
final bool autofocus;
final bool readOnly;
final TextInputType inputType;
final TextInputAction inputAction;
final Brightness keyboardAppearance;
final bool deleteDetection;
@override
CustomTextEditState createState() => CustomTextEditState();
}
class CustomTextEditState extends State<CustomTextEdit> with TextInputClient {
TextInputConnection? _connection;
@override
void initState() {
widget.focusNode.addListener(_onFocusChange);
super.initState();
}
@override
void didUpdateWidget(CustomTextEdit oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_onFocusChange);
widget.focusNode.addListener(_onFocusChange);
}
if (!_shouldCreateInputConnection) {
_closeInputConnectionIfNeeded();
} else {
if (oldWidget.readOnly && widget.focusNode.hasFocus) {
_openInputConnection();
}
}
}
@override
void dispose() {
widget.focusNode.removeListener(_onFocusChange);
_closeInputConnectionIfNeeded();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onKeyEvent: _onKeyEvent,
child: widget.child,
);
}
bool get hasInputConnection => _connection != null && _connection!.attached;
void requestKeyboard() {
if (widget.focusNode.hasFocus) {
_openInputConnection();
} else {
widget.focusNode.requestFocus();
}
}
void closeKeyboard() {
if (hasInputConnection) {
_connection?.close();
}
}
void setEditingState(TextEditingValue value) {
_currentEditingState = value;
_connection?.setEditingState(value);
}
void setEditableRect(Rect rect, Rect caretRect) {
if (!hasInputConnection) {
return;
}
_connection?.setEditableSizeAndTransform(
rect.size,
Matrix4.translationValues(0, 0, 0),
);
_connection?.setCaretRect(caretRect);
}
void _onFocusChange() {
_openOrCloseInputConnectionIfNeeded();
}
KeyEventResult _onKeyEvent(FocusNode focusNode, KeyEvent event) {
if (_currentEditingState.composing.isCollapsed) {
return widget.onKeyEvent(focusNode, event);
}
return KeyEventResult.skipRemainingHandlers;
}
void _openOrCloseInputConnectionIfNeeded() {
if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) {
_openInputConnection();
} else if (!widget.focusNode.hasFocus) {
_closeInputConnectionIfNeeded();
}
}
bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
void _openInputConnection() {
if (!_shouldCreateInputConnection) {
return;
}
if (hasInputConnection) {
_connection!.show();
} else {
final config = TextInputConfiguration(
inputType: widget.inputType,
inputAction: widget.inputAction,
keyboardAppearance: widget.keyboardAppearance,
autocorrect: false,
enableSuggestions: false,
enableIMEPersonalizedLearning: false,
);
_connection = TextInput.attach(this, config);
_connection!.show();
// setEditableRect(Rect.zero, Rect.zero);
_connection!.setEditingState(_initEditingState);
}
}
void _closeInputConnectionIfNeeded() {
if (_connection != null && _connection!.attached) {
_connection!.close();
_connection = null;
}
}
TextEditingValue get _initEditingState => widget.deleteDetection
? const TextEditingValue(
text: ' ',
selection: TextSelection.collapsed(offset: 2),
)
: const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
);
late var _currentEditingState = _initEditingState.copyWith();
@override
TextEditingValue? get currentTextEditingValue {
return _currentEditingState;
}
@override
AutofillScope? get currentAutofillScope {
return null;
}
@override
void updateEditingValue(TextEditingValue value) {
_currentEditingState = value;
// Get input after composing is done
if (!_currentEditingState.composing.isCollapsed) {
final text = _currentEditingState.text;
final composingText = _currentEditingState.composing.textInside(text);
widget.onComposing(composingText);
return;
}
widget.onComposing(null);
if (_currentEditingState.text.length < _initEditingState.text.length) {
widget.onDelete();
} else {
final textDelta = _currentEditingState.text.substring(
_initEditingState.text.length,
);
widget.onInsert(textDelta);
}
// Reset editing state if composing is done
if (_currentEditingState.composing.isCollapsed &&
_currentEditingState.text != _initEditingState.text) {
_connection!.setEditingState(_initEditingState);
}
}
@override
void performAction(TextInputAction action) {
// print('performAction $action');
widget.onAction(action);
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
// print('updateFloatingCursor $point');
}
@override
void showAutocorrectionPromptRect(int start, int end) {
// print('showAutocorrectionPromptRect');
}
@override
void connectionClosed() {
// print('connectionClosed');
}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {
// print('performPrivateCommand $action');
}
@override
void insertTextPlaceholder(Size size) {
// print('insertTextPlaceholder');
}
@override
void removeTextPlaceholder() {
// print('removeTextPlaceholder');
}
@override
void showToolbar() {
// print('showToolbar');
}
}
@@ -0,0 +1,164 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
class TerminalGestureDetector extends StatefulWidget {
const TerminalGestureDetector({
super.key,
this.child,
this.onSingleTapUp,
this.onTapUp,
this.onTapDown,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.onTertiaryTapDown,
this.onTertiaryTapUp,
this.onLongPressStart,
this.onLongPressMoveUpdate,
this.onLongPressUp,
this.onDragStart,
this.onDragUpdate,
this.onDoubleTapDown,
});
final Widget? child;
final GestureTapUpCallback? onTapUp;
final GestureTapUpCallback? onSingleTapUp;
final GestureTapDownCallback? onTapDown;
final GestureTapDownCallback? onSecondaryTapDown;
final GestureTapUpCallback? onSecondaryTapUp;
final GestureTapDownCallback? onDoubleTapDown;
final GestureTapDownCallback? onTertiaryTapDown;
final GestureTapUpCallback? onTertiaryTapUp;
final GestureLongPressStartCallback? onLongPressStart;
final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
final GestureLongPressUpCallback? onLongPressUp;
final GestureDragStartCallback? onDragStart;
final GestureDragUpdateCallback? onDragUpdate;
@override
State<TerminalGestureDetector> createState() =>
_TerminalGestureDetectorState();
}
class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
Timer? _doubleTapTimer;
Offset? _lastTapOffset;
// True if a second tap down of a double tap is detected. Used to discard
// subsequent tap up / tap hold of the same tap.
bool _isDoubleTap = false;
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void _handleTapDown(TapDownDetails details) {
widget.onTapDown?.call(details);
if (_doubleTapTimer != null &&
_isWithinDoubleTapTolerance(details.globalPosition)) {
// If there was already a previous tap, the second down hold/tap is a
// double tap down.
widget.onDoubleTapDown?.call(details);
_doubleTapTimer!.cancel();
_doubleTapTimeout();
_isDoubleTap = true;
}
}
void _handleTapUp(TapUpDetails details) {
if (!_isDoubleTap) {
widget.onSingleTapUp?.call(details);
_lastTapOffset = details.globalPosition;
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
}
_isDoubleTap = false;
}
void _doubleTapTimeout() {
_doubleTapTimer = null;
_lastTapOffset = null;
}
bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
if (_lastTapOffset == null) {
return false;
}
final Offset difference = secondTapOffset - _lastTapOffset!;
return difference.distance <= kDoubleTapSlop;
}
@override
Widget build(BuildContext context) {
final gestures = <Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
..onSecondaryTapDown = widget.onSecondaryTapDown
..onSecondaryTapUp = widget.onSecondaryTapUp
..onTertiaryTapDown = widget.onTertiaryTapDown
..onTertiaryTapUp = widget.onTertiaryTapUp;
},
);
gestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
debugOwner: this,
supportedDevices: {
PointerDeviceKind.touch,
// PointerDeviceKind.mouse, // for debugging purposes only
},
),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = widget.onLongPressStart
..onLongPressMoveUpdate = widget.onLongPressMoveUpdate
..onLongPressUp = widget.onLongPressUp;
},
);
gestures[PanGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(
debugOwner: this,
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse},
),
(PanGestureRecognizer instance) {
instance
..dragStartBehavior = DragStartBehavior.down
..onStart = widget.onDragStart
..onUpdate = widget.onDragUpdate;
},
);
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true,
child: widget.child,
);
}
}
@@ -0,0 +1,193 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
import 'package:clide/src/terminal/src/terminal_view.dart';
import 'package:clide/src/terminal/src/ui/controller.dart';
import 'package:clide/src/terminal/src/ui/gesture/gesture_detector.dart';
import 'package:clide/src/terminal/src/ui/pointer_input.dart';
import 'package:clide/src/terminal/src/ui/render.dart';
class TerminalGestureHandler extends StatefulWidget {
const TerminalGestureHandler({
super.key,
required this.terminalView,
required this.terminalController,
this.child,
this.onTapUp,
this.onSingleTapUp,
this.onTapDown,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.onTertiaryTapDown,
this.onTertiaryTapUp,
this.readOnly = false,
});
final TerminalViewState terminalView;
final TerminalController terminalController;
final Widget? child;
final GestureTapUpCallback? onTapUp;
final GestureTapUpCallback? onSingleTapUp;
final GestureTapDownCallback? onTapDown;
final GestureTapDownCallback? onSecondaryTapDown;
final GestureTapUpCallback? onSecondaryTapUp;
final GestureTapDownCallback? onTertiaryTapDown;
final GestureTapUpCallback? onTertiaryTapUp;
final bool readOnly;
@override
State<TerminalGestureHandler> createState() => _TerminalGestureHandlerState();
}
class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
TerminalViewState get terminalView => widget.terminalView;
RenderTerminal get renderTerminal => terminalView.renderTerminal;
DragStartDetails? _lastDragStartDetails;
LongPressStartDetails? _lastLongPressStartDetails;
@override
Widget build(BuildContext context) {
return TerminalGestureDetector(
child: widget.child,
onTapUp: widget.onTapUp,
onSingleTapUp: onSingleTapUp,
onTapDown: onTapDown,
onSecondaryTapDown: onSecondaryTapDown,
onSecondaryTapUp: onSecondaryTapUp,
onTertiaryTapDown: onSecondaryTapDown,
onTertiaryTapUp: onSecondaryTapUp,
onLongPressStart: onLongPressStart,
onLongPressMoveUpdate: onLongPressMoveUpdate,
// onLongPressUp: onLongPressUp,
onDragStart: onDragStart,
onDragUpdate: onDragUpdate,
onDoubleTapDown: onDoubleTapDown,
);
}
bool get _shouldSendTapEvent =>
!widget.readOnly &&
widget.terminalController.shouldSendPointerInput(PointerInput.tap);
void _tapDown(
GestureTapDownCallback? callback,
TapDownDetails details,
TerminalMouseButton button, {
bool forceCallback = false,
}) {
// Check if the terminal should and can handle the tap down event.
var handled = false;
if (_shouldSendTapEvent) {
handled = renderTerminal.mouseEvent(
button,
TerminalMouseButtonState.down,
details.localPosition,
);
}
// If the event was not handled by the terminal, use the supplied callback.
if (!handled || forceCallback) {
callback?.call(details);
}
}
void _tapUp(
GestureTapUpCallback? callback,
TapUpDetails details,
TerminalMouseButton button, {
bool forceCallback = false,
}) {
// Check if the terminal should and can handle the tap up event.
var handled = false;
if (_shouldSendTapEvent) {
handled = renderTerminal.mouseEvent(
button,
TerminalMouseButtonState.up,
details.localPosition,
);
}
// If the event was not handled by the terminal, use the supplied callback.
if (!handled || forceCallback) {
callback?.call(details);
}
}
void onTapDown(TapDownDetails details) {
// onTapDown is special, as it will always call the supplied callback.
// The TerminalView depends on it to bring the terminal into focus.
_tapDown(
widget.onTapDown,
details,
TerminalMouseButton.left,
forceCallback: true,
);
}
void onSingleTapUp(TapUpDetails details) {
_tapUp(widget.onSingleTapUp, details, TerminalMouseButton.left);
}
void onSecondaryTapDown(TapDownDetails details) {
_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right);
}
void onSecondaryTapUp(TapUpDetails details) {
_tapUp(widget.onSecondaryTapUp, details, TerminalMouseButton.right);
}
void onTertiaryTapDown(TapDownDetails details) {
_tapDown(widget.onTertiaryTapDown, details, TerminalMouseButton.middle);
}
void onTertiaryTapUp(TapUpDetails details) {
_tapUp(widget.onTertiaryTapUp, details, TerminalMouseButton.right);
}
void onDoubleTapDown(TapDownDetails details) {
renderTerminal.selectWord(details.localPosition);
}
void onLongPressStart(LongPressStartDetails details) {
_lastLongPressStartDetails = details;
renderTerminal.selectWord(details.localPosition);
}
void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
renderTerminal.selectWord(
_lastLongPressStartDetails!.localPosition,
details.localPosition,
);
}
// void onLongPressUp() {}
void onDragStart(DragStartDetails details) {
_lastDragStartDetails = details;
details.kind == PointerDeviceKind.mouse
? renderTerminal.selectCharacters(details.localPosition)
: renderTerminal.selectWord(details.localPosition);
}
void onDragUpdate(DragUpdateDetails details) {
renderTerminal.selectCharacters(
_lastDragStartDetails!.localPosition,
details.localPosition,
);
}
}
@@ -0,0 +1,117 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// The function called when the user scrolls the [InfiniteScrollView]. [offset]
/// is the current offset of the scroll view, ranging from [double.negativeInfinity]
/// to [double.infinity].
typedef ScrollCallback = void Function(double offset);
/// A [Scrollable] that can be scrolled infinitely in both directions. When
/// scroll happens, the [onScroll] callback is called with the new offset.
class InfiniteScrollView extends StatelessWidget {
const InfiniteScrollView({
super.key,
required this.onScroll,
required this.child,
});
final ScrollCallback onScroll;
final Widget child;
@override
Widget build(BuildContext context) {
return Scrollable(
viewportBuilder: (context, position) {
return _InfiniteScrollView(
position: position,
onScroll: onScroll,
child: child,
);
},
);
}
}
class _InfiniteScrollView extends SingleChildRenderObjectWidget {
const _InfiniteScrollView({
// super.key,
super.child,
required this.position,
required this.onScroll,
});
final ViewportOffset position;
final ScrollCallback onScroll;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderInfiniteScrollView(
position: position,
onScroll: onScroll,
);
}
@override
void updateRenderObject(
BuildContext context,
_RenderInfiniteScrollView renderObject,
) {
renderObject
..position = position
..onScroll = onScroll;
}
}
class _RenderInfiniteScrollView extends RenderShiftedBox {
_RenderInfiniteScrollView({
RenderBox? child,
required ViewportOffset position,
required ScrollCallback onScroll,
}) : _position = position,
_scrollCallback = onScroll,
super(child);
ViewportOffset _position;
set position(ViewportOffset value) {
if (_position == value) return;
if (attached) _position.removeListener(markNeedsLayout);
_position = value;
if (attached) _position.addListener(markNeedsLayout);
markNeedsLayout();
}
ScrollCallback _scrollCallback;
set onScroll(ScrollCallback value) {
if (_scrollCallback == value) return;
_scrollCallback = value;
markNeedsLayout();
}
void _onScroll() {
_scrollCallback(_position.pixels);
}
@override
void attach(covariant PipelineOwner owner) {
super.attach(owner);
_position.addListener(_onScroll);
}
@override
void detach() {
super.detach();
_position.removeListener(_onScroll);
}
@override
void performLayout() {
child?.layout(constraints, parentUsesSize: true);
size = child?.size ?? Size.zero;
_position.applyViewportDimension(size.height);
_position.applyContentDimensions(double.negativeInfinity, double.infinity);
}
}
+290
View File
@@ -0,0 +1,290 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/services.dart';
import 'package:clide/src/terminal/src/core/input/keys.dart';
final _keyToTerminalKey = {
LogicalKeyboardKey.hyper: TerminalKey.hyper,
LogicalKeyboardKey.superKey: TerminalKey.superKey,
LogicalKeyboardKey.fnLock: TerminalKey.fnLock,
LogicalKeyboardKey.suspend: TerminalKey.suspend,
LogicalKeyboardKey.resume: TerminalKey.resume,
LogicalKeyboardKey.sleep: TerminalKey.sleep,
LogicalKeyboardKey.wakeUp: TerminalKey.wakeUp,
LogicalKeyboardKey.keyA: TerminalKey.keyA,
LogicalKeyboardKey.keyB: TerminalKey.keyB,
LogicalKeyboardKey.keyC: TerminalKey.keyC,
LogicalKeyboardKey.keyD: TerminalKey.keyD,
LogicalKeyboardKey.keyE: TerminalKey.keyE,
LogicalKeyboardKey.keyF: TerminalKey.keyF,
LogicalKeyboardKey.keyG: TerminalKey.keyG,
LogicalKeyboardKey.keyH: TerminalKey.keyH,
LogicalKeyboardKey.keyI: TerminalKey.keyI,
LogicalKeyboardKey.keyJ: TerminalKey.keyJ,
LogicalKeyboardKey.keyK: TerminalKey.keyK,
LogicalKeyboardKey.keyL: TerminalKey.keyL,
LogicalKeyboardKey.keyM: TerminalKey.keyM,
LogicalKeyboardKey.keyN: TerminalKey.keyN,
LogicalKeyboardKey.keyO: TerminalKey.keyO,
LogicalKeyboardKey.keyP: TerminalKey.keyP,
LogicalKeyboardKey.keyQ: TerminalKey.keyQ,
LogicalKeyboardKey.keyR: TerminalKey.keyR,
LogicalKeyboardKey.keyS: TerminalKey.keyS,
LogicalKeyboardKey.keyT: TerminalKey.keyT,
LogicalKeyboardKey.keyU: TerminalKey.keyU,
LogicalKeyboardKey.keyV: TerminalKey.keyV,
LogicalKeyboardKey.keyW: TerminalKey.keyW,
LogicalKeyboardKey.keyX: TerminalKey.keyX,
LogicalKeyboardKey.keyY: TerminalKey.keyY,
LogicalKeyboardKey.keyZ: TerminalKey.keyZ,
LogicalKeyboardKey.digit1: TerminalKey.digit1,
LogicalKeyboardKey.digit2: TerminalKey.digit2,
LogicalKeyboardKey.digit3: TerminalKey.digit3,
LogicalKeyboardKey.digit4: TerminalKey.digit4,
LogicalKeyboardKey.digit5: TerminalKey.digit5,
LogicalKeyboardKey.digit6: TerminalKey.digit6,
LogicalKeyboardKey.digit7: TerminalKey.digit7,
LogicalKeyboardKey.digit8: TerminalKey.digit8,
LogicalKeyboardKey.digit9: TerminalKey.digit9,
LogicalKeyboardKey.digit0: TerminalKey.digit0,
LogicalKeyboardKey.enter: TerminalKey.enter,
LogicalKeyboardKey.escape: TerminalKey.escape,
LogicalKeyboardKey.backspace: TerminalKey.backspace,
LogicalKeyboardKey.tab: TerminalKey.tab,
LogicalKeyboardKey.space: TerminalKey.space,
LogicalKeyboardKey.minus: TerminalKey.minus,
LogicalKeyboardKey.equal: TerminalKey.equal,
LogicalKeyboardKey.bracketLeft: TerminalKey.bracketLeft,
LogicalKeyboardKey.bracketRight: TerminalKey.bracketRight,
LogicalKeyboardKey.backslash: TerminalKey.backslash,
LogicalKeyboardKey.semicolon: TerminalKey.semicolon,
LogicalKeyboardKey.quote: TerminalKey.quote,
LogicalKeyboardKey.backquote: TerminalKey.backquote,
LogicalKeyboardKey.comma: TerminalKey.comma,
LogicalKeyboardKey.period: TerminalKey.period,
LogicalKeyboardKey.slash: TerminalKey.slash,
LogicalKeyboardKey.capsLock: TerminalKey.capsLock,
LogicalKeyboardKey.f1: TerminalKey.f1,
LogicalKeyboardKey.f2: TerminalKey.f2,
LogicalKeyboardKey.f3: TerminalKey.f3,
LogicalKeyboardKey.f4: TerminalKey.f4,
LogicalKeyboardKey.f5: TerminalKey.f5,
LogicalKeyboardKey.f6: TerminalKey.f6,
LogicalKeyboardKey.f7: TerminalKey.f7,
LogicalKeyboardKey.f8: TerminalKey.f8,
LogicalKeyboardKey.f9: TerminalKey.f9,
LogicalKeyboardKey.f10: TerminalKey.f10,
LogicalKeyboardKey.f11: TerminalKey.f11,
LogicalKeyboardKey.f12: TerminalKey.f12,
LogicalKeyboardKey.printScreen: TerminalKey.printScreen,
LogicalKeyboardKey.scrollLock: TerminalKey.scrollLock,
LogicalKeyboardKey.pause: TerminalKey.pause,
LogicalKeyboardKey.insert: TerminalKey.insert,
LogicalKeyboardKey.home: TerminalKey.home,
LogicalKeyboardKey.pageUp: TerminalKey.pageUp,
LogicalKeyboardKey.delete: TerminalKey.delete,
LogicalKeyboardKey.end: TerminalKey.end,
LogicalKeyboardKey.pageDown: TerminalKey.pageDown,
LogicalKeyboardKey.arrowRight: TerminalKey.arrowRight,
LogicalKeyboardKey.arrowLeft: TerminalKey.arrowLeft,
LogicalKeyboardKey.arrowDown: TerminalKey.arrowDown,
LogicalKeyboardKey.arrowUp: TerminalKey.arrowUp,
LogicalKeyboardKey.numLock: TerminalKey.numLock,
LogicalKeyboardKey.numpadDivide: TerminalKey.numpadDivide,
LogicalKeyboardKey.numpadMultiply: TerminalKey.numpadMultiply,
LogicalKeyboardKey.numpadSubtract: TerminalKey.numpadSubtract,
LogicalKeyboardKey.numpadAdd: TerminalKey.numpadAdd,
LogicalKeyboardKey.numpadEnter: TerminalKey.numpadEnter,
LogicalKeyboardKey.numpad1: TerminalKey.numpad1,
LogicalKeyboardKey.numpad2: TerminalKey.numpad2,
LogicalKeyboardKey.numpad3: TerminalKey.numpad3,
LogicalKeyboardKey.numpad4: TerminalKey.numpad4,
LogicalKeyboardKey.numpad5: TerminalKey.numpad5,
LogicalKeyboardKey.numpad6: TerminalKey.numpad6,
LogicalKeyboardKey.numpad7: TerminalKey.numpad7,
LogicalKeyboardKey.numpad8: TerminalKey.numpad8,
LogicalKeyboardKey.numpad9: TerminalKey.numpad9,
LogicalKeyboardKey.numpad0: TerminalKey.numpad0,
LogicalKeyboardKey.numpadDecimal: TerminalKey.numpadDecimal,
LogicalKeyboardKey.intlBackslash: TerminalKey.intlBackslash,
LogicalKeyboardKey.contextMenu: TerminalKey.contextMenu,
LogicalKeyboardKey.power: TerminalKey.power,
LogicalKeyboardKey.numpadEqual: TerminalKey.numpadEqual,
LogicalKeyboardKey.f13: TerminalKey.f13,
LogicalKeyboardKey.f14: TerminalKey.f14,
LogicalKeyboardKey.f15: TerminalKey.f15,
LogicalKeyboardKey.f16: TerminalKey.f16,
LogicalKeyboardKey.f17: TerminalKey.f17,
LogicalKeyboardKey.f18: TerminalKey.f18,
LogicalKeyboardKey.f19: TerminalKey.f19,
LogicalKeyboardKey.f20: TerminalKey.f20,
LogicalKeyboardKey.f21: TerminalKey.f21,
LogicalKeyboardKey.f22: TerminalKey.f22,
LogicalKeyboardKey.f23: TerminalKey.f23,
LogicalKeyboardKey.f24: TerminalKey.f24,
LogicalKeyboardKey.open: TerminalKey.open,
LogicalKeyboardKey.help: TerminalKey.help,
LogicalKeyboardKey.select: TerminalKey.select,
LogicalKeyboardKey.again: TerminalKey.again,
LogicalKeyboardKey.undo: TerminalKey.undo,
LogicalKeyboardKey.cut: TerminalKey.cut,
LogicalKeyboardKey.copy: TerminalKey.copy,
LogicalKeyboardKey.paste: TerminalKey.paste,
LogicalKeyboardKey.find: TerminalKey.find,
LogicalKeyboardKey.audioVolumeMute: TerminalKey.audioVolumeMute,
LogicalKeyboardKey.audioVolumeUp: TerminalKey.audioVolumeUp,
LogicalKeyboardKey.audioVolumeDown: TerminalKey.audioVolumeDown,
LogicalKeyboardKey.numpadComma: TerminalKey.numpadComma,
LogicalKeyboardKey.intlRo: TerminalKey.intlRo,
LogicalKeyboardKey.kanaMode: TerminalKey.kanaMode,
LogicalKeyboardKey.intlYen: TerminalKey.intlYen,
LogicalKeyboardKey.convert: TerminalKey.convert,
LogicalKeyboardKey.nonConvert: TerminalKey.nonConvert,
LogicalKeyboardKey.lang1: TerminalKey.lang1,
LogicalKeyboardKey.lang2: TerminalKey.lang2,
LogicalKeyboardKey.lang3: TerminalKey.lang3,
LogicalKeyboardKey.lang4: TerminalKey.lang4,
LogicalKeyboardKey.lang5: TerminalKey.lang5,
LogicalKeyboardKey.abort: TerminalKey.abort,
LogicalKeyboardKey.props: TerminalKey.props,
LogicalKeyboardKey.numpadParenLeft: TerminalKey.numpadParenLeft,
LogicalKeyboardKey.numpadParenRight: TerminalKey.numpadParenRight,
LogicalKeyboardKey.controlLeft: TerminalKey.controlLeft,
LogicalKeyboardKey.shiftLeft: TerminalKey.shiftLeft,
LogicalKeyboardKey.altLeft: TerminalKey.altLeft,
LogicalKeyboardKey.metaLeft: TerminalKey.metaLeft,
LogicalKeyboardKey.controlRight: TerminalKey.controlRight,
LogicalKeyboardKey.shiftRight: TerminalKey.shiftRight,
LogicalKeyboardKey.altRight: TerminalKey.altRight,
LogicalKeyboardKey.metaRight: TerminalKey.metaRight,
LogicalKeyboardKey.info: TerminalKey.info,
LogicalKeyboardKey.closedCaptionToggle: TerminalKey.closedCaptionToggle,
LogicalKeyboardKey.brightnessUp: TerminalKey.brightnessUp,
LogicalKeyboardKey.brightnessDown: TerminalKey.brightnessDown,
LogicalKeyboardKey.mediaLast: TerminalKey.mediaLast,
LogicalKeyboardKey.launchPhone: TerminalKey.launchPhone,
LogicalKeyboardKey.exit: TerminalKey.exit,
LogicalKeyboardKey.channelUp: TerminalKey.channelUp,
LogicalKeyboardKey.channelDown: TerminalKey.channelDown,
LogicalKeyboardKey.mediaPlay: TerminalKey.mediaPlay,
LogicalKeyboardKey.mediaPause: TerminalKey.mediaPause,
LogicalKeyboardKey.mediaRecord: TerminalKey.mediaRecord,
LogicalKeyboardKey.mediaFastForward: TerminalKey.mediaFastForward,
LogicalKeyboardKey.mediaRewind: TerminalKey.mediaRewind,
LogicalKeyboardKey.mediaTrackNext: TerminalKey.mediaTrackNext,
LogicalKeyboardKey.mediaTrackPrevious: TerminalKey.mediaTrackPrevious,
LogicalKeyboardKey.mediaStop: TerminalKey.mediaStop,
LogicalKeyboardKey.eject: TerminalKey.eject,
LogicalKeyboardKey.mediaPlayPause: TerminalKey.mediaPlayPause,
LogicalKeyboardKey.speechInputToggle: TerminalKey.speechInputToggle,
LogicalKeyboardKey.launchWordProcessor: TerminalKey.launchWordProcessor,
LogicalKeyboardKey.launchSpreadsheet: TerminalKey.launchSpreadsheet,
LogicalKeyboardKey.launchMail: TerminalKey.launchMail,
LogicalKeyboardKey.launchContacts: TerminalKey.launchContacts,
LogicalKeyboardKey.launchCalendar: TerminalKey.launchCalendar,
LogicalKeyboardKey.logOff: TerminalKey.logOff,
LogicalKeyboardKey.launchControlPanel: TerminalKey.launchControlPanel,
LogicalKeyboardKey.spellCheck: TerminalKey.spellCheck,
LogicalKeyboardKey.launchScreenSaver: TerminalKey.launchScreenSaver,
LogicalKeyboardKey.launchAssistant: TerminalKey.launchAssistant,
LogicalKeyboardKey.newKey: TerminalKey.newKey,
LogicalKeyboardKey.close: TerminalKey.close,
LogicalKeyboardKey.save: TerminalKey.save,
LogicalKeyboardKey.print: TerminalKey.print,
LogicalKeyboardKey.browserSearch: TerminalKey.browserSearch,
LogicalKeyboardKey.browserHome: TerminalKey.browserHome,
LogicalKeyboardKey.browserBack: TerminalKey.browserBack,
LogicalKeyboardKey.browserForward: TerminalKey.browserForward,
LogicalKeyboardKey.browserStop: TerminalKey.browserStop,
LogicalKeyboardKey.browserRefresh: TerminalKey.browserRefresh,
LogicalKeyboardKey.browserFavorites: TerminalKey.browserFavorites,
LogicalKeyboardKey.zoomIn: TerminalKey.zoomIn,
LogicalKeyboardKey.zoomOut: TerminalKey.zoomOut,
LogicalKeyboardKey.zoomToggle: TerminalKey.zoomToggle,
LogicalKeyboardKey.redo: TerminalKey.redo,
LogicalKeyboardKey.mailReply: TerminalKey.mailReply,
LogicalKeyboardKey.mailForward: TerminalKey.mailForward,
LogicalKeyboardKey.mailSend: TerminalKey.mailSend,
LogicalKeyboardKey.gameButton1: TerminalKey.gameButton1,
LogicalKeyboardKey.gameButton2: TerminalKey.gameButton2,
LogicalKeyboardKey.gameButton3: TerminalKey.gameButton3,
LogicalKeyboardKey.gameButton4: TerminalKey.gameButton4,
LogicalKeyboardKey.gameButton5: TerminalKey.gameButton5,
LogicalKeyboardKey.gameButton6: TerminalKey.gameButton6,
LogicalKeyboardKey.gameButton7: TerminalKey.gameButton7,
LogicalKeyboardKey.gameButton8: TerminalKey.gameButton8,
LogicalKeyboardKey.gameButton9: TerminalKey.gameButton9,
LogicalKeyboardKey.gameButton10: TerminalKey.gameButton10,
LogicalKeyboardKey.gameButton11: TerminalKey.gameButton11,
LogicalKeyboardKey.gameButton12: TerminalKey.gameButton12,
LogicalKeyboardKey.gameButton13: TerminalKey.gameButton13,
LogicalKeyboardKey.gameButton14: TerminalKey.gameButton14,
LogicalKeyboardKey.gameButton15: TerminalKey.gameButton15,
LogicalKeyboardKey.gameButton16: TerminalKey.gameButton16,
LogicalKeyboardKey.gameButtonA: TerminalKey.gameButtonA,
LogicalKeyboardKey.gameButtonB: TerminalKey.gameButtonB,
LogicalKeyboardKey.gameButtonC: TerminalKey.gameButtonC,
LogicalKeyboardKey.gameButtonLeft1: TerminalKey.gameButtonLeft1,
LogicalKeyboardKey.gameButtonLeft2: TerminalKey.gameButtonLeft2,
LogicalKeyboardKey.gameButtonMode: TerminalKey.gameButtonMode,
LogicalKeyboardKey.gameButtonRight1: TerminalKey.gameButtonRight1,
LogicalKeyboardKey.gameButtonRight2: TerminalKey.gameButtonRight2,
LogicalKeyboardKey.gameButtonSelect: TerminalKey.gameButtonSelect,
LogicalKeyboardKey.gameButtonStart: TerminalKey.gameButtonStart,
LogicalKeyboardKey.gameButtonThumbLeft: TerminalKey.gameButtonThumbLeft,
LogicalKeyboardKey.gameButtonThumbRight: TerminalKey.gameButtonThumbRight,
LogicalKeyboardKey.gameButtonX: TerminalKey.gameButtonX,
LogicalKeyboardKey.gameButtonY: TerminalKey.gameButtonY,
LogicalKeyboardKey.gameButtonZ: TerminalKey.gameButtonZ,
LogicalKeyboardKey.fn: TerminalKey.fn,
LogicalKeyboardKey.shift: TerminalKey.shift,
LogicalKeyboardKey.meta: TerminalKey.meta,
LogicalKeyboardKey.alt: TerminalKey.alt,
LogicalKeyboardKey.control: TerminalKey.control,
};
final _keyById = () {
final map = <int, TerminalKey>{};
for (final entry in _keyToTerminalKey.entries) {
map[entry.key.keyId] = entry.value;
}
return map;
}();
final _keyByChar = () {
final map = <String, TerminalKey>{};
for (final entry in _keyToTerminalKey.entries) {
final label = entry.key.keyLabel;
if (label.isEmpty || label.length > 1) {
continue;
}
map[label] = entry.value;
map[label.toUpperCase()] = entry.value;
map[label.toLowerCase()] = entry.value;
}
return map;
}();
/// Converts a [LogicalKeyboardKey] to a [TerminalKey]. Returns `null` if the
/// key does not have a corresponding [TerminalKey].
///
/// For example, `LogicalKeyboardKey.keyA` will be converted to
/// `TerminalKey.keyA`.
TerminalKey? keyToTerminalKey(LogicalKeyboardKey key) {
return _keyById[key.keyId];
}
/// Converts a character to a [TerminalKey]. Returns `null` if the character
/// does not have a corresponding [TerminalKey].
///
/// For example, `charToTerminalKey('a')` or `charToTerminalKey('A')` will both
/// return [TerminalKey.a].
TerminalKey? charToTerminalKey(String char) {
if (char.length != 1) {
return null;
}
return _keyByChar[char];
}
@@ -0,0 +1,51 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
class CustomKeyboardListener extends StatelessWidget {
final Widget child;
final FocusNode focusNode;
final bool autofocus;
final void Function(String) onInsert;
final void Function(String?) onComposing;
final KeyEventResult Function(FocusNode, KeyEvent) onKeyEvent;
const CustomKeyboardListener({
super.key,
required this.child,
required this.focusNode,
this.autofocus = false,
required this.onInsert,
required this.onComposing,
required this.onKeyEvent,
});
KeyEventResult _onKeyEvent(FocusNode focusNode, KeyEvent keyEvent) {
// First try to handle the key event directly.
final handled = onKeyEvent(focusNode, keyEvent);
if (handled == KeyEventResult.ignored) {
// If it was not handled, but the key corresponds to a character,
// insert the character.
if (keyEvent.character != null && keyEvent.character != "") {
onInsert(keyEvent.character!);
return KeyEventResult.handled;
}
}
return handled;
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focusNode,
autofocus: autofocus,
onKeyEvent: _onKeyEvent,
child: child,
);
}
}
@@ -0,0 +1,60 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
class KeyboardVisibilty extends StatefulWidget {
const KeyboardVisibilty({
super.key,
required this.child,
this.onKeyboardShow,
this.onKeyboardHide,
});
final Widget child;
final VoidCallback? onKeyboardShow;
final VoidCallback? onKeyboardHide;
@override
KeyboardVisibiltyState createState() => KeyboardVisibiltyState();
}
class KeyboardVisibiltyState extends State<KeyboardVisibilty>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
final bottomInset = View.of(context).viewInsets.bottom;
if (bottomInset != _lastBottomInset) {
if (bottomInset > 0) {
widget.onKeyboardShow?.call();
} else {
widget.onKeyboardHide?.call();
}
}
_lastBottomInset = bottomInset;
super.didChangeMetrics();
}
var _lastBottomInset = 0.0;
@override
Widget build(BuildContext context) {
return widget.child;
}
}
+281
View File
@@ -0,0 +1,281 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:clide/src/terminal/src/ui/palette_builder.dart';
import 'package:clide/src/terminal/src/ui/paragraph_cache.dart';
import 'package:clide/src/terminal/terminal.dart';
/// Encapsulates the logic for painting various terminal elements.
class TerminalPainter {
TerminalPainter({
required TerminalTheme theme,
required TerminalStyle textStyle,
required TextScaler textScaler,
}) : _textStyle = textStyle,
_theme = theme,
_textScaler = textScaler;
/// A lookup table from terminal colors to Flutter colors.
late var _colorPalette = PaletteBuilder(_theme).build();
/// Size of each character in the terminal.
late var _cellSize = _measureCharSize();
/// The cached for cells in the terminal. Should be cleared when the same
/// cell no longer produces the same visual output. For example, when
/// [_textStyle] is changed, or when the system font changes.
final _paragraphCache = ParagraphCache(10240);
TerminalStyle get textStyle => _textStyle;
TerminalStyle _textStyle;
set textStyle(TerminalStyle value) {
if (value == _textStyle) return;
_textStyle = value;
_cellSize = _measureCharSize();
_paragraphCache.clear();
}
TextScaler get textScaler => _textScaler;
TextScaler _textScaler = TextScaler.linear(1.0);
set textScaler(TextScaler value) {
if (value == _textScaler) return;
_textScaler = value;
_cellSize = _measureCharSize();
_paragraphCache.clear();
}
TerminalTheme get theme => _theme;
TerminalTheme _theme;
set theme(TerminalTheme value) {
if (value == _theme) return;
_theme = value;
_colorPalette = PaletteBuilder(value).build();
_paragraphCache.clear();
}
Size _measureCharSize() {
const test = 'mmmmmmmmmm';
final textStyle = _textStyle.toTextStyle();
final builder = ParagraphBuilder(textStyle.getParagraphStyle());
builder.pushStyle(
textStyle.getTextStyle(textScaler: _textScaler),
);
builder.addText(test);
final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: double.infinity));
final result = Size(
paragraph.maxIntrinsicWidth / test.length,
paragraph.height,
);
paragraph.dispose();
return result;
}
/// The size of each character in the terminal.
Size get cellSize => _cellSize;
/// When the set of font available to the system changes, call this method to
/// clear cached state related to font rendering.
void clearFontCache() {
_cellSize = _measureCharSize();
_paragraphCache.clear();
}
/// Paints the cursor based on the current cursor type.
void paintCursor(
Canvas canvas,
Offset offset, {
required TerminalCursorType cursorType,
bool hasFocus = true,
}) {
final paint = Paint()
..color = _theme.cursor
..strokeWidth = 1;
if (!hasFocus) {
paint.style = PaintingStyle.stroke;
canvas.drawRect(offset & _cellSize, paint);
return;
}
switch (cursorType) {
case TerminalCursorType.block:
paint.style = PaintingStyle.fill;
canvas.drawRect(offset & _cellSize, paint);
return;
case TerminalCursorType.underline:
return canvas.drawLine(
Offset(offset.dx, _cellSize.height - 1),
Offset(offset.dx + _cellSize.width, _cellSize.height - 1),
paint,
);
case TerminalCursorType.verticalBar:
return canvas.drawLine(
Offset(offset.dx, 0),
Offset(offset.dx, _cellSize.height),
paint,
);
}
}
@pragma('vm:prefer-inline')
void paintHighlight(Canvas canvas, Offset offset, int length, Color color) {
final endOffset =
offset.translate(length * _cellSize.width, _cellSize.height);
final paint = Paint()
..color = color
..strokeWidth = 1;
canvas.drawRect(
Rect.fromPoints(offset, endOffset),
paint,
);
}
/// Paints [line] to [canvas] at [offset]. The x offset of [offset] is usually
/// 0, and the y offset is the top of the line.
void paintLine(
Canvas canvas,
Offset offset,
BufferLine line,
) {
final cellData = CellData.empty();
final cellWidth = _cellSize.width;
for (var i = 0; i < line.length; i++) {
line.getCellData(i, cellData);
final charWidth = cellData.content >> CellContent.widthShift;
final cellOffset = offset.translate(i * cellWidth, 0);
paintCell(canvas, cellOffset, cellData);
if (charWidth == 2) {
i++;
}
}
}
@pragma('vm:prefer-inline')
void paintCell(Canvas canvas, Offset offset, CellData cellData) {
paintCellBackground(canvas, offset, cellData);
paintCellForeground(canvas, offset, cellData);
}
/// Paints the character in the cell represented by [cellData] to [canvas] at
/// [offset].
@pragma('vm:prefer-inline')
void paintCellForeground(Canvas canvas, Offset offset, CellData cellData) {
final charCode = cellData.content & CellContent.codepointMask;
if (charCode == 0) return;
final cacheKey = cellData.getHash() ^ _textScaler.hashCode;
var paragraph = _paragraphCache.getLayoutFromCache(cacheKey);
if (paragraph == null) {
final cellFlags = cellData.flags;
var color = cellFlags & CellFlags.inverse == 0
? resolveForegroundColor(cellData.foreground)
: resolveBackgroundColor(cellData.background);
if (cellData.flags & CellFlags.faint != 0) {
color = color.withOpacity(0.5);
}
final style = _textStyle.toTextStyle(
color: color,
bold: cellFlags & CellFlags.bold != 0,
italic: cellFlags & CellFlags.italic != 0,
underline: cellFlags & CellFlags.underline != 0,
);
// Flutter does not draw an underline below a space which is not between
// other regular characters. As only single characters are drawn, this
// will never produce an underline below a space in the terminal. As a
// workaround the regular space CodePoint 0x20 is replaced with
// the CodePoint 0xA0. This is a non breaking space and a underline can be
// drawn below it.
var char = String.fromCharCode(charCode);
if (cellFlags & CellFlags.underline != 0 && charCode == 0x20) {
char = String.fromCharCode(0xA0);
}
paragraph = _paragraphCache.performAndCacheLayout(
char,
style,
_textScaler,
cacheKey,
);
}
canvas.drawParagraph(paragraph, offset);
}
/// Paints the background of a cell represented by [cellData] to [canvas] at
/// [offset].
@pragma('vm:prefer-inline')
void paintCellBackground(Canvas canvas, Offset offset, CellData cellData) {
late Color color;
final colorType = cellData.background & CellColor.typeMask;
if (cellData.flags & CellFlags.inverse != 0) {
color = resolveForegroundColor(cellData.foreground);
} else if (colorType == CellColor.normal) {
return;
} else {
color = resolveBackgroundColor(cellData.background);
}
final paint = Paint()..color = color;
final doubleWidth = cellData.content >> CellContent.widthShift == 2;
final widthScale = doubleWidth ? 2 : 1;
final size = Size(_cellSize.width * widthScale + 1, _cellSize.height);
canvas.drawRect(offset & size, paint);
}
/// Get the effective foreground color for a cell from information encoded in
/// [cellColor].
@pragma('vm:prefer-inline')
Color resolveForegroundColor(int cellColor) {
final colorType = cellColor & CellColor.typeMask;
final colorValue = cellColor & CellColor.valueMask;
switch (colorType) {
case CellColor.normal:
return _theme.foreground;
case CellColor.named:
case CellColor.palette:
return _colorPalette[colorValue];
case CellColor.rgb:
default:
return Color(colorValue | 0xFF000000);
}
}
/// Get the effective background color for a cell from information encoded in
/// [cellColor].
@pragma('vm:prefer-inline')
Color resolveBackgroundColor(int cellColor) {
final colorType = cellColor & CellColor.typeMask;
final colorValue = cellColor & CellColor.valueMask;
switch (colorType) {
case CellColor.normal:
return _theme.background;
case CellColor.named:
case CellColor.palette:
return _colorPalette[colorValue];
case CellColor.rgb:
default:
return Color(colorValue | 0xFF000000);
}
}
}
@@ -0,0 +1,120 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/src/ui/terminal_theme.dart';
import 'package:clide/src/terminal/src/utils/lookup_table.dart';
class PaletteBuilder {
final TerminalTheme theme;
PaletteBuilder(this.theme);
List<Color> build() {
return List<Color>.generate(
256,
paletteColor,
growable: false,
);
}
/// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
Color paletteColor(int colNum) {
switch (colNum) {
case 0:
return theme.black;
case 1:
return theme.red;
case 2:
return theme.green;
case 3:
return theme.yellow;
case 4:
return theme.blue;
case 5:
return theme.magenta;
case 6:
return theme.cyan;
case 7:
return theme.white;
case 8:
return theme.brightBlack;
case 9:
return theme.brightRed;
case 10:
return theme.brightGreen;
case 11:
return theme.brightYellow;
case 12:
return theme.brightBlue;
case 13:
return theme.brightMagenta;
case 14:
return theme.brightCyan;
case 15:
return theme.white;
}
if (colNum < 232) {
var r = 0;
var g = 0;
var b = 0;
final index = colNum - 16;
for (var i = 0; i < index; i++) {
if (b == 0) {
b = 95;
} else if (b < 255) {
b += 40;
} else {
b = 0;
if (g == 0) {
g = 95;
} else if (g < 255) {
g += 40;
} else {
g = 0;
if (r == 0) {
r = 95;
} else if (r < 255) {
r += 40;
} else {
break;
}
}
}
}
return Color.fromARGB(0xFF, r, g, b);
}
return Color(_grayscaleColors[colNum.clamp(232, 255)]!);
}
}
final _grayscaleColors = FastLookupTable({
232: 0xff080808,
233: 0xff121212,
234: 0xff1c1c1c,
235: 0xff262626,
236: 0xff303030,
237: 0xff3a3a3a,
238: 0xff444444,
239: 0xff4e4e4e,
240: 0xff585858,
241: 0xff626262,
242: 0xff6c6c6c,
243: 0xff767676,
244: 0xff808080,
245: 0xff8a8a8a,
246: 0xff949494,
247: 0xff9e9e9e,
248: 0xffa8a8a8,
249: 0xffb2b2b2,
250: 0xffbcbcbc,
251: 0xffc6c6c6,
252: 0xffd0d0d0,
253: 0xffdadada,
254: 0xffe4e4e4,
255: 0xffeeeeee,
});
@@ -0,0 +1,58 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:collection';
import 'dart:ui';
import 'package:flutter/widgets.dart';
class _LruCache<K, V> {
_LruCache(this._maxSize);
final int _maxSize;
final _map = LinkedHashMap<K, V>();
V? operator [](K key) {
final value = _map.remove(key);
if (value != null) _map[key] = value;
return value;
}
void operator []=(K key, V value) {
_map.remove(key);
_map[key] = value;
while (_map.length > _maxSize) {
_map.remove(_map.keys.first);
}
}
void clear() => _map.clear();
int get length => _map.length;
}
class ParagraphCache {
ParagraphCache(int maximumSize) : _cache = _LruCache<int, Paragraph>(maximumSize);
final _LruCache<int, Paragraph> _cache;
Paragraph? getLayoutFromCache(int key) => _cache[key];
Paragraph performAndCacheLayout(
String text,
TextStyle style,
TextScaler textScaler,
int key,
) {
final builder = ParagraphBuilder(style.getParagraphStyle());
builder.pushStyle(style.getTextStyle(textScaler: textScaler));
builder.addText(text);
final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: double.infinity));
_cache[key] = paragraph;
return paragraph;
}
void clear() => _cache.clear();
int get length => _cache.length;
}
@@ -0,0 +1,31 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum PointerInput {
/// Taps / buttons presses & releases.
tap,
/// Scroll / mouse wheels events.
scroll,
/// Drag events, a pointer is in a down state and dragged across the terminal.
drag,
/// Move events, a pointer is in an up state and moved across the terminal.
move,
}
class PointerInputs {
final Set<PointerInput> inputs;
const PointerInputs(this.inputs);
const PointerInputs.none() : inputs = const <PointerInput>{};
const PointerInputs.all()
: inputs = const <PointerInput>{
PointerInput.tap,
PointerInput.scroll,
PointerInput.drag,
PointerInput.move,
};
}
+554
View File
@@ -0,0 +1,554 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:math' show max;
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/core/buffer/range.dart';
import 'package:clide/src/terminal/src/core/buffer/segment.dart';
import 'package:clide/src/terminal/src/core/mouse/button.dart';
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
import 'package:clide/src/terminal/src/terminal.dart';
import 'package:clide/src/terminal/src/ui/controller.dart';
import 'package:clide/src/terminal/src/ui/cursor_type.dart';
import 'package:clide/src/terminal/src/ui/painter.dart';
import 'package:clide/src/terminal/src/ui/selection_mode.dart';
import 'package:clide/src/terminal/src/ui/terminal_size.dart';
import 'package:clide/src/terminal/src/ui/terminal_text_style.dart';
import 'package:clide/src/terminal/src/ui/terminal_theme.dart';
typedef EditableRectCallback = void Function(Rect rect, Rect caretRect);
class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
RenderTerminal({
required Terminal terminal,
required TerminalController controller,
required ViewportOffset offset,
required EdgeInsets padding,
required bool autoResize,
required TerminalStyle textStyle,
required TextScaler textScaler,
required TerminalTheme theme,
required FocusNode focusNode,
required TerminalCursorType cursorType,
required bool alwaysShowCursor,
EditableRectCallback? onEditableRect,
String? composingText,
}) : _terminal = terminal,
_controller = controller,
_offset = offset,
_padding = padding,
_autoResize = autoResize,
_focusNode = focusNode,
_cursorType = cursorType,
_alwaysShowCursor = alwaysShowCursor,
_onEditableRect = onEditableRect,
_composingText = composingText,
_painter = TerminalPainter(
theme: theme,
textStyle: textStyle,
textScaler: textScaler,
);
Terminal _terminal;
set terminal(Terminal terminal) {
if (_terminal == terminal) return;
if (attached) _terminal.removeListener(_onTerminalChange);
_terminal = terminal;
if (attached) _terminal.addListener(_onTerminalChange);
_resizeTerminalIfNeeded();
markNeedsLayout();
}
TerminalController _controller;
set controller(TerminalController controller) {
if (_controller == controller) return;
if (attached) _controller.removeListener(_onControllerUpdate);
_controller = controller;
if (attached) _controller.addListener(_onControllerUpdate);
markNeedsLayout();
}
ViewportOffset _offset;
set offset(ViewportOffset value) {
if (value == _offset) return;
if (attached) _offset.removeListener(_onScroll);
_offset = value;
if (attached) _offset.addListener(_onScroll);
markNeedsLayout();
}
EdgeInsets _padding;
set padding(EdgeInsets value) {
if (value == _padding) return;
_padding = value;
markNeedsLayout();
}
bool _autoResize;
set autoResize(bool value) {
if (value == _autoResize) return;
_autoResize = value;
markNeedsLayout();
}
set textStyle(TerminalStyle value) {
if (value == _painter.textStyle) return;
_painter.textStyle = value;
markNeedsLayout();
}
set textScaler(TextScaler value) {
if (value == _painter.textScaler) return;
_painter.textScaler = value;
markNeedsLayout();
}
set theme(TerminalTheme value) {
if (value == _painter.theme) return;
_painter.theme = value;
markNeedsPaint();
}
FocusNode _focusNode;
set focusNode(FocusNode value) {
if (value == _focusNode) return;
if (attached) _focusNode.removeListener(_onFocusChange);
_focusNode = value;
if (attached) _focusNode.addListener(_onFocusChange);
markNeedsPaint();
}
TerminalCursorType _cursorType;
set cursorType(TerminalCursorType value) {
if (value == _cursorType) return;
_cursorType = value;
markNeedsPaint();
}
bool _alwaysShowCursor;
set alwaysShowCursor(bool value) {
if (value == _alwaysShowCursor) return;
_alwaysShowCursor = value;
markNeedsPaint();
}
EditableRectCallback? _onEditableRect;
set onEditableRect(EditableRectCallback? value) {
if (value == _onEditableRect) return;
_onEditableRect = value;
markNeedsLayout();
}
String? _composingText;
set composingText(String? value) {
if (value == _composingText) return;
_composingText = value;
markNeedsPaint();
}
TerminalSize? _viewportSize;
final TerminalPainter _painter;
var _stickToBottom = true;
void _onScroll() {
_stickToBottom = _scrollOffset >= _maxScrollExtent;
markNeedsLayout();
_notifyEditableRect();
}
void _onFocusChange() {
markNeedsPaint();
}
void _onTerminalChange() {
markNeedsLayout();
_notifyEditableRect();
}
void _onControllerUpdate() {
markNeedsLayout();
}
@override
final isRepaintBoundary = true;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(_onScroll);
_terminal.addListener(_onTerminalChange);
_controller.addListener(_onControllerUpdate);
_focusNode.addListener(_onFocusChange);
}
@override
void detach() {
super.detach();
_offset.removeListener(_onScroll);
_terminal.removeListener(_onTerminalChange);
_controller.removeListener(_onControllerUpdate);
_focusNode.removeListener(_onFocusChange);
}
@override
bool hitTestSelf(Offset position) {
return true;
}
@override
void systemFontsDidChange() {
_painter.clearFontCache();
super.systemFontsDidChange();
}
@override
void performLayout() {
size = constraints.biggest;
_updateViewportSize();
_updateScrollOffset();
if (_stickToBottom) {
_offset.correctBy(_maxScrollExtent - _scrollOffset);
}
}
/// Total height of the terminal in pixels. Includes scrollback buffer.
double get _terminalHeight =>
_terminal.buffer.lines.length * _painter.cellSize.height;
/// The distance from the top of the terminal to the top of the viewport.
// double get _scrollOffset => _offset.pixels;
double get _scrollOffset {
// return _offset.pixels ~/ _painter.cellSize.height * _painter.cellSize.height;
return _offset.pixels;
}
/// The height of a terminal line in pixels. This includes the line spacing.
/// Height of the entire terminal is expected to be a multiple of this value.
double get lineHeight => _painter.cellSize.height;
/// Get the top-left corner of the cell at [cellOffset] in pixels.
Offset getOffset(CellOffset cellOffset) {
final row = cellOffset.y;
final col = cellOffset.x;
final x = col * _painter.cellSize.width;
final y = row * _painter.cellSize.height;
return Offset(x + _padding.left, y + _padding.top - _scrollOffset);
}
/// Get the [CellOffset] of the cell that [offset] is in.
CellOffset getCellOffset(Offset offset) {
final x = offset.dx - _padding.left;
final y = offset.dy - _padding.top + _scrollOffset;
final row = y ~/ _painter.cellSize.height;
final col = x ~/ _painter.cellSize.width;
return CellOffset(
col.clamp(0, _terminal.viewWidth - 1),
row.clamp(0, _terminal.buffer.lines.length - 1),
);
}
/// Selects entire words in the terminal that contains [from] and [to].
void selectWord(Offset from, [Offset? to]) {
final fromOffset = getCellOffset(from);
final fromBoundary = _terminal.buffer.getWordBoundary(fromOffset);
if (fromBoundary == null) return;
if (to == null) {
_controller.setSelection(
_terminal.buffer.createAnchorFromOffset(fromBoundary.begin),
_terminal.buffer.createAnchorFromOffset(fromBoundary.end),
mode: SelectionMode.line,
);
} else {
final toOffset = getCellOffset(to);
final toBoundary = _terminal.buffer.getWordBoundary(toOffset);
if (toBoundary == null) return;
final range = fromBoundary.merge(toBoundary);
_controller.setSelection(
_terminal.buffer.createAnchorFromOffset(range.begin),
_terminal.buffer.createAnchorFromOffset(range.end),
mode: SelectionMode.line,
);
}
}
/// Selects characters in the terminal that starts from [from] to [to]. At
/// least one cell is selected even if [from] and [to] are same.
void selectCharacters(Offset from, [Offset? to]) {
final fromPosition = getCellOffset(from);
if (to == null) {
_controller.setSelection(
_terminal.buffer.createAnchorFromOffset(fromPosition),
_terminal.buffer.createAnchorFromOffset(fromPosition),
);
} else {
var toPosition = getCellOffset(to);
if (toPosition.x >= fromPosition.x) {
toPosition = CellOffset(toPosition.x + 1, toPosition.y);
}
_controller.setSelection(
_terminal.buffer.createAnchorFromOffset(fromPosition),
_terminal.buffer.createAnchorFromOffset(toPosition),
);
}
}
/// Send a mouse event at [offset] with [button] being currently in [buttonState].
bool mouseEvent(
TerminalMouseButton button,
TerminalMouseButtonState buttonState,
Offset offset,
) {
final position = getCellOffset(offset);
return _terminal.mouseInput(button, buttonState, position);
}
void _notifyEditableRect() {
final cursor = localToGlobal(cursorOffset);
final rect = Rect.fromLTRB(
cursor.dx,
cursor.dy,
size.width,
cursor.dy + _painter.cellSize.height,
);
final caretRect = cursor & _painter.cellSize;
_onEditableRect?.call(rect, caretRect);
}
/// Update the viewport size in cells based on the current widget size in
/// pixels.
void _updateViewportSize() {
if (size <= _painter.cellSize) {
return;
}
final viewportSize = TerminalSize(
size.width ~/ _painter.cellSize.width,
_viewportHeight ~/ _painter.cellSize.height,
);
if (_viewportSize != viewportSize) {
_viewportSize = viewportSize;
_resizeTerminalIfNeeded();
}
}
/// Notify the underlying terminal that the viewport size has changed.
void _resizeTerminalIfNeeded() {
if (_autoResize && _viewportSize != null) {
_terminal.resize(
_viewportSize!.width,
_viewportSize!.height,
_painter.cellSize.width.round(),
_painter.cellSize.height.round(),
);
}
}
/// Update the scroll offset based on the current terminal state. This should
/// be called in [performLayout] after the viewport size has been updated.
void _updateScrollOffset() {
_offset.applyViewportDimension(_viewportHeight);
_offset.applyContentDimensions(0, _maxScrollExtent);
}
bool get _isComposingText {
return _composingText != null && _composingText!.isNotEmpty;
}
bool get _shouldShowCursor {
return _terminal.cursorVisibleMode || _alwaysShowCursor || _isComposingText;
}
double get _viewportHeight {
return size.height - _padding.vertical;
}
double get _maxScrollExtent {
return max(_terminalHeight - _viewportHeight, 0.0);
}
double get _lineOffset {
return -_scrollOffset + _padding.top;
}
/// The offset of the cursor from the top left corner of this render object.
Offset get cursorOffset {
return Offset(
_terminal.buffer.cursorX * _painter.cellSize.width,
_terminal.buffer.absoluteCursorY * _painter.cellSize.height + _lineOffset,
);
}
Size get cellSize {
return _painter.cellSize;
}
@override
void paint(PaintingContext context, Offset offset) {
_paint(context, offset);
context.setWillChangeHint();
}
void _paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
final lines = _terminal.buffer.lines;
final charHeight = _painter.cellSize.height;
final firstLineOffset = _scrollOffset - _padding.top;
final lastLineOffset = _scrollOffset + size.height + _padding.bottom;
final firstLine = firstLineOffset ~/ charHeight;
final lastLine = lastLineOffset ~/ charHeight;
final effectFirstLine = firstLine.clamp(0, lines.length - 1);
final effectLastLine = lastLine.clamp(0, lines.length - 1);
for (var i = effectFirstLine; i <= effectLastLine; i++) {
_painter.paintLine(
canvas,
offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()),
lines[i],
);
}
if (_terminal.buffer.absoluteCursorY >= effectFirstLine &&
_terminal.buffer.absoluteCursorY <= effectLastLine) {
if (_isComposingText) {
_paintComposingText(canvas, offset + cursorOffset);
}
if (_shouldShowCursor) {
_painter.paintCursor(
canvas,
offset + cursorOffset,
cursorType: _cursorType,
hasFocus: _focusNode.hasFocus,
);
}
}
_paintHighlights(
canvas,
_controller.highlights,
effectFirstLine,
effectLastLine,
);
if (_controller.selection != null) {
_paintSelection(
canvas,
_controller.selection!,
effectFirstLine,
effectLastLine,
);
}
}
/// Paints the text that is currently being composed in IME to [canvas] at
/// [offset]. [offset] is usually the cursor position.
void _paintComposingText(Canvas canvas, Offset offset) {
final composingText = _composingText;
if (composingText == null) {
return;
}
final style = _painter.textStyle.toTextStyle(
color: _painter.resolveForegroundColor(_terminal.cursor.foreground),
backgroundColor: _painter.theme.background,
underline: true,
);
final builder = ParagraphBuilder(style.getParagraphStyle());
builder.addPlaceholder(
offset.dx,
_painter.cellSize.height,
PlaceholderAlignment.middle,
);
builder.pushStyle(
style.getTextStyle(textScaler: _painter.textScaler),
);
builder.addText(composingText);
final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: size.width));
canvas.drawParagraph(paragraph, Offset(0, offset.dy));
}
void _paintSelection(
Canvas canvas,
BufferRange selection,
int firstLine,
int lastLine,
) {
for (final segment in selection.toSegments()) {
if (segment.line >= _terminal.buffer.lines.length) {
break;
}
if (segment.line < firstLine) {
continue;
}
if (segment.line > lastLine) {
break;
}
_paintSegment(canvas, segment, _painter.theme.selection);
}
}
void _paintHighlights(
Canvas canvas,
List<TerminalHighlight> highlights,
int firstLine,
int lastLine,
) {
for (var highlight in _controller.highlights) {
final range = highlight.range?.normalized;
if (range == null ||
range.begin.y > lastLine ||
range.end.y < firstLine) {
continue;
}
for (var segment in range.toSegments()) {
if (segment.line < firstLine) {
continue;
}
if (segment.line > lastLine) {
break;
}
_paintSegment(canvas, segment, highlight.color);
}
}
}
@pragma('vm:prefer-inline')
void _paintSegment(Canvas canvas, BufferSegment segment, Color color) {
final start = segment.start ?? 0;
final end = segment.end ?? _terminal.viewWidth;
final startOffset = Offset(
start * _painter.cellSize.width,
segment.line * _painter.cellSize.height + _lineOffset,
);
_painter.paintHighlight(canvas, startOffset, end - start, color);
}
}
+102
View File
@@ -0,0 +1,102 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/terminal.dart';
class TerminalScrollGestureHandler extends StatefulWidget {
const TerminalScrollGestureHandler({
super.key,
required this.terminal,
required this.getCellOffset,
required this.getLineHeight,
this.simulateScroll = true,
required this.child,
});
final Terminal terminal;
final CellOffset Function(Offset) getCellOffset;
final double Function() getLineHeight;
final bool simulateScroll;
final Widget child;
@override
State<TerminalScrollGestureHandler> createState() =>
_TerminalScrollGestureHandlerState();
}
class _TerminalScrollGestureHandlerState
extends State<TerminalScrollGestureHandler> {
var isAltBuffer = false;
var _lastPointerPosition = Offset.zero;
@override
void initState() {
widget.terminal.addListener(_onTerminalUpdated);
isAltBuffer = widget.terminal.isUsingAltBuffer;
super.initState();
}
@override
void dispose() {
widget.terminal.removeListener(_onTerminalUpdated);
super.dispose();
}
@override
void didUpdateWidget(covariant TerminalScrollGestureHandler oldWidget) {
if (oldWidget.terminal != widget.terminal) {
oldWidget.terminal.removeListener(_onTerminalUpdated);
widget.terminal.addListener(_onTerminalUpdated);
isAltBuffer = widget.terminal.isUsingAltBuffer;
}
super.didUpdateWidget(oldWidget);
}
void _onTerminalUpdated() {
if (isAltBuffer != widget.terminal.isUsingAltBuffer) {
isAltBuffer = widget.terminal.isUsingAltBuffer;
setState(() {});
}
}
void _sendScrollEvent(bool up) {
final position = widget.getCellOffset(_lastPointerPosition);
final handled = widget.terminal.mouseInput(
up ? TerminalMouseButton.wheelUp : TerminalMouseButton.wheelDown,
TerminalMouseButtonState.down,
position,
);
if (!handled && widget.simulateScroll) {
widget.terminal.keyInput(
up ? TerminalKey.arrowUp : TerminalKey.arrowDown,
);
}
}
void _onPointerSignal(PointerSignalEvent event) {
if (event is! PointerScrollEvent) return;
_lastPointerPosition = event.position;
final lineHeight = widget.getLineHeight();
if (lineHeight <= 0) return;
final lines = (event.scrollDelta.dy / lineHeight).round().clamp(-5, 5);
for (var i = 0; i < lines.abs(); i++) {
_sendScrollEvent(lines < 0);
}
}
@override
Widget build(BuildContext context) {
if (!isAltBuffer) return widget.child;
// Intercept scroll at the pointer level so the inner Scrollable
// (normal buffer) never sees the event in alt-buffer mode.
return Listener(
onPointerSignal: _onPointerSignal,
onPointerDown: (event) => _lastPointerPosition = event.position,
child: widget.child,
);
}
}
@@ -0,0 +1,7 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum SelectionMode {
line,
block,
}
@@ -0,0 +1,73 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/src/terminal.dart';
import 'package:clide/src/terminal/src/ui/controller.dart';
import 'package:clide/src/terminal/src/ui/selection_mode.dart';
class TerminalActions extends StatelessWidget {
const TerminalActions({
super.key,
required this.terminal,
required this.controller,
required this.child,
});
final Terminal terminal;
final TerminalController controller;
final Widget child;
@override
Widget build(BuildContext context) {
return Actions(
actions: {
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (intent) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
if (text != null) {
terminal.paste(text);
controller.clearSelection();
}
return null;
},
),
CopySelectionTextIntent: CallbackAction<CopySelectionTextIntent>(
onInvoke: (intent) async {
final selection = controller.selection;
if (selection == null) {
return;
}
final text = terminal.buffer.getText(selection);
await Clipboard.setData(ClipboardData(text: text));
return null;
},
),
SelectAllTextIntent: CallbackAction<SelectAllTextIntent>(
onInvoke: (intent) {
controller.setSelection(
terminal.buffer.createAnchor(
0,
terminal.buffer.height - terminal.viewHeight,
),
terminal.buffer.createAnchor(
terminal.viewWidth,
terminal.buffer.height - 1,
),
mode: SelectionMode.line,
);
return null;
},
),
},
child: child,
);
}
}
@@ -0,0 +1,36 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
Map<ShortcutActivator, Intent> get defaultTerminalShortcuts {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _defaultShortcuts;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _defaultAppleShortcuts;
}
}
final _defaultShortcuts = {
SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true):
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.keyV, control: true):
const PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.keyA, control: true):
const SelectAllTextIntent(SelectionChangedCause.keyboard),
};
final _defaultAppleShortcuts = {
SingleActivator(LogicalKeyboardKey.keyC, meta: true):
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.keyV, meta: true):
const PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.keyA, meta: true):
const SelectAllTextIntent(SelectionChangedCause.keyboard),
};
@@ -0,0 +1,26 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
class TerminalSize {
final int width;
final int height;
const TerminalSize(this.width, this.height);
@override
String toString() => 'TerminalSize($width, $height)';
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! TerminalSize) {
return false;
}
return other.width == width && other.height == height;
}
@override
int get hashCode => width.hashCode ^ height.hashCode;
}
@@ -0,0 +1,89 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
const _kDefaultFontSize = 13.0;
const _kDefaultHeight = 1.2;
const _kDefaultFontFamily = 'monospace';
const _kDefaultFontFamilyFallback = [
'Menlo',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'Noto Sans Mono CJK SC',
'Noto Sans Mono CJK TC',
'Noto Sans Mono CJK KR',
'Noto Sans Mono CJK JP',
'Noto Sans Mono CJK HK',
'Noto Color Emoji',
'Noto Sans Symbols',
'monospace',
'sans-serif',
];
class TerminalStyle {
const TerminalStyle({
this.fontSize = _kDefaultFontSize,
this.height = _kDefaultHeight,
this.fontFamily = _kDefaultFontFamily,
this.fontFamilyFallback = _kDefaultFontFamilyFallback,
});
factory TerminalStyle.fromTextStyle(TextStyle textStyle) {
return TerminalStyle(
fontSize: textStyle.fontSize ?? _kDefaultFontSize,
height: textStyle.height ?? _kDefaultHeight,
fontFamily: textStyle.fontFamily ??
textStyle.fontFamilyFallback?.first ??
_kDefaultFontFamily,
fontFamilyFallback:
textStyle.fontFamilyFallback ?? _kDefaultFontFamilyFallback,
);
}
final double fontSize;
final double height;
final String fontFamily;
final List<String> fontFamilyFallback;
TextStyle toTextStyle({
Color? color,
Color? backgroundColor,
bool bold = false,
bool italic = false,
bool underline = false,
}) {
return TextStyle(
fontSize: fontSize,
height: height,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
color: color,
backgroundColor: backgroundColor,
fontWeight: bold ? FontWeight.bold : FontWeight.normal,
fontStyle: italic ? FontStyle.italic : FontStyle.normal,
decoration: underline ? TextDecoration.underline : TextDecoration.none,
);
}
TerminalStyle copyWith({
double? fontSize,
double? height,
String? fontFamily,
List<String>? fontFamilyFallback,
}) {
return TerminalStyle(
fontSize: fontSize ?? this.fontSize,
height: height ?? this.height,
fontFamily: fontFamily ?? this.fontFamily,
fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback,
);
}
}
@@ -0,0 +1,59 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
class TerminalTheme {
const TerminalTheme({
required this.cursor,
required this.selection,
required this.foreground,
required this.background,
required this.black,
required this.white,
required this.red,
required this.green,
required this.yellow,
required this.blue,
required this.magenta,
required this.cyan,
required this.brightBlack,
required this.brightRed,
required this.brightGreen,
required this.brightYellow,
required this.brightBlue,
required this.brightMagenta,
required this.brightCyan,
required this.brightWhite,
required this.searchHitBackground,
required this.searchHitBackgroundCurrent,
required this.searchHitForeground,
});
final Color cursor;
final Color selection;
final Color foreground;
final Color background;
final Color black;
final Color red;
final Color green;
final Color yellow;
final Color blue;
final Color magenta;
final Color cyan;
final Color white;
final Color brightBlack;
final Color brightRed;
final Color brightGreen;
final Color brightYellow;
final Color brightBlue;
final Color brightMagenta;
final Color brightCyan;
final Color brightWhite;
final Color searchHitBackground;
final Color searchHitBackgroundCurrent;
final Color searchHitForeground;
}
+58
View File
@@ -0,0 +1,58 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'package:flutter/widgets.dart';
import 'package:clide/src/terminal/src/ui/terminal_theme.dart';
class TerminalThemes {
static const defaultTheme = TerminalTheme(
cursor: Color(0XAAAEAFAD),
selection: Color(0XAAAEAFAD),
foreground: Color(0XFFCCCCCC),
background: Color(0XFF1E1E1E),
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
static const whiteOnBlack = TerminalTheme(
cursor: Color(0XFFAEAFAD),
selection: Color(0XFFAEAFAD),
foreground: Color(0XFFFFFFFF),
background: Color(0XFF000000),
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
}
+405
View File
@@ -0,0 +1,405 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
// ignore_for_file: constant_identifier_names
abstract class Ascii {
/*
* Helper functions
*/
static bool isNonPrintable(int c) {
return c < 32 || c == 127;
}
/*
* Non-printable ASCII characters
*/
/// Null character
static const NULL = 00;
/// Start of Header
static const SOH = 01;
/// Start of Text
static const STX = 02;
/// End of Text, hearts card suit
static const ETX = 03;
/// End of Transmission, diamonds card suit
static const EOT = 04;
/// Enquiry, clubs card suit
static const ENQ = 05;
/// Acknowledgement, spade card suit
static const ACK = 06;
/// Bell
static const BEL = 07;
/// Backspace
static const BS = 08;
/// Horizontal Tab
static const HT = 09;
/// Line feed
static const LF = 10;
/// Vertical Tab, male symbol, symbol for Mars
static const VT = 11;
/// Form feed, female symbol, symbol for Venus
static const FF = 12;
/// Carriage return
static const CR = 13;
/// Shift Out
static const SO = 14;
/// Shift In
static const SI = 15;
/// Data link escape
static const DLE = 16;
/// Device control 1
static const DC1 = 17;
/// Device control 2
static const DC2 = 18;
/// Device control 3
static const DC3 = 19;
/// Device control 4
static const DC4 = 20;
/// NAK Negative-acknowledge
static const NAK = 21;
/// Synchronous idle
static const SYN = 22;
/// End of trans. block
static const ETB = 23;
/// Cancel
static const CAN = 24;
/// End of medium
static const EM = 25;
/// Substitute
static const SUB = 26;
/// Escape
static const ESC = 27;
/// File separator
static const FS = 28;
/// Group separator
static const GS = 29;
/// Record separator
static const RS = 30;
/// Unit separator
static const US = 31;
/// Delete
static const DEL = 127;
/*
* Printable ASCII characters
*/
/// Space " "
static const space = 32;
/// Exclamation mark "!"
static const exclamationMark = 33;
/// Double quotes '"'
static const doubleQuotes = 34;
/// Number sign '#'
static const numberSign = 35;
/// Dollar sign '$'
static const dollarSign = 36;
/// Percent sign '%'
static const percentSign = 37;
/// Ampersand '&'
static const ampersand = 38;
/// Single quote "'"
static const singleQuote = 39;
/// round brackets or parentheses, opening round bracket '('
static const openParentheses = 40;
/// parentheses or round brackets, closing parentheses ')'
static const closeParentheses = 41;
/// Asterisk '*'
static const asterisk = 42;
/// Plus sign '+'
static const plus = 43;
/// Comma ","
static const comma = 44;
/// Hyphen , minus sign '-'
static const minus = 45;
/// Dot, full stop '.'
static const dot = 46;
/// Slash , forward slash , fraction bar , division slash '/'
static const slash = 47;
/// number zero
static const num0 = 48;
/// number one
static const num1 = 49;
/// number two
static const num2 = 50;
/// number three
static const num3 = 51;
/// number four
static const num4 = 52;
/// number five
static const num5 = 53;
/// number six
static const num6 = 54;
/// number seven
static const num7 = 55;
/// number eight
static const num8 = 56;
/// number nine
static const num9 = 57;
/// Colon ':'
static const colon = 58;
/// Semicolon ';'
static const semicolon = 59;
/// Less-than sign '<'
static const lessThan = 60;
/// Equals sign '='
static const equal = 61;
/// Greater-than sign ; Inequality sign '>'
static const greaterThan = 62;
/// Question mark '?'
static const questionMark = 63;
/// At sign '@'
static const atSign = 64;
/// Capital letter A
static const A = 65;
/// Capital letter B
static const B = 66;
/// Capital letter C
static const C = 67;
/// Capital letter D
static const D = 68;
/// Capital letter E
static const E = 69;
/// Capital letter F
static const F = 70;
/// Capital letter G
static const G = 71;
/// Capital letter H
static const H = 72;
/// Capital letter I
static const I = 73;
/// Capital letter J
static const J = 74;
/// Capital letter K
static const K = 75;
/// Capital letter L
static const L = 76;
/// Capital letter M
static const M = 77;
/// Capital letter N
static const N = 78;
/// Capital letter O
static const O = 79;
/// Capital letter P
static const P = 80;
/// Capital letter Q
static const Q = 81;
/// Capital letter R
static const R = 82;
/// Capital letter S
static const S = 83;
/// Capital letter T
static const T = 84;
/// Capital letter U
static const U = 85;
/// Capital letter V
static const V = 86;
/// Capital letter W
static const W = 87;
/// Capital letter X
static const X = 88;
/// Capital letter Y
static const Y = 89;
/// Capital letter Z
static const Z = 90;
/// square brackets or box brackets, opening bracket '['
static const openBracket = 91;
/// Backslash , reverse slash '\\'
static const backslash = 92;
/// box brackets or square brackets, closing bracket ']'
static const closeBracket = 93;
/// Circumflex accent or Caret '^'
static const caret = 94;
/// underscore , understrike , underbar or low line '_'
static const underscore = 95;
/// Grave accent '`'
static const graveAccent = 96;
/// Lowercase letter a , minuscule a
static const a = 97;
/// Lowercase letter b , minuscule b
static const b = 98;
/// Lowercase letter c , minuscule c
static const c = 99;
/// Lowercase letter d , minuscule d
static const d = 100;
/// Lowercase letter e , minuscule e
static const e = 101;
/// Lowercase letter f , minuscule f
static const f = 102;
/// Lowercase letter g , minuscule g
static const g = 103;
/// Lowercase letter h , minuscule h
static const h = 104;
/// Lowercase letter i , minuscule i
static const i = 105;
/// Lowercase letter j , minuscule j
static const j = 106;
/// Lowercase letter k , minuscule k
static const k = 107;
/// Lowercase letter l , minuscule l
static const l = 108;
/// Lowercase letter m , minuscule m
static const m = 109;
/// Lowercase letter n , minuscule n
static const n = 110;
/// Lowercase letter o , minuscule o
static const o = 111;
/// Lowercase letter p , minuscule p
static const p = 112;
/// Lowercase letter q , minuscule q
static const q = 113;
/// Lowercase letter r , minuscule r
static const r = 114;
/// Lowercase letter s , minuscule s
static const s = 115;
/// Lowercase letter t , minuscule t
static const t = 116;
/// Lowercase letter u , minuscule u
static const u = 117;
/// Lowercase letter v , minuscule v
static const v = 118;
/// Lowercase letter w , minuscule w
static const w = 119;
/// Lowercase letter x , minuscule x
static const x = 120;
/// Lowercase letter y , minuscule y
static const y = 121;
/// Lowercase letter z , minuscule z
static const z = 122;
/// braces or curly brackets, opening braces '{'
static const openBrace = 123;
/// vertical-bar, vbar, vertical line or vertical slash '|'
static const verticalBar = 124;
/// curly brackets or braces, closing curly brackets '}'
static const closeBrace = 125;
/// Tilde ; swung dash '~'
static const tilde = 126;
}
@@ -0,0 +1,109 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
import 'dart:collection';
class ByteConsumer {
final _queue = ListQueue<List<int>>();
final _consumed = ListQueue<List<int>>();
var _currentOffset = 0;
var _length = 0;
var _totalConsumed = 0;
void add(String data) {
if (data.isEmpty) return;
final runes = data.runes.toList(growable: false);
_queue.addLast(runes);
_length += runes.length;
}
int peek() {
final data = _queue.first;
if (_currentOffset < data.length) {
return data[_currentOffset];
} else {
final result = consume();
rollback();
return result;
}
}
int consume() {
final data = _queue.first;
if (_currentOffset >= data.length) {
_consumed.add(_queue.removeFirst());
_currentOffset -= data.length;
return consume();
}
_length--;
_totalConsumed++;
return data[_currentOffset++];
}
/// Rolls back the last [n] call.
void rollback([int n = 1]) {
_currentOffset -= n;
_totalConsumed -= n;
_length += n;
while (_currentOffset < 0) {
final rollback = _consumed.removeLast();
_queue.addFirst(rollback);
_currentOffset += rollback.length;
}
}
/// Rolls back to the state when this consumer had [length] bytes.
void rollbackTo(int length) {
rollback(length - _length);
}
int get length => _length;
int get totalConsumed => _totalConsumed;
bool get isEmpty => _length == 0;
bool get isNotEmpty => _length != 0;
/// Unreferences data blocks that have been consumed. After calling this
/// method, the consumer will not be able to roll back to consumed blocks.
void unrefConsumedBlocks() {
_consumed.clear();
}
/// Resets the consumer to its initial state.
void reset() {
_queue.clear();
_consumed.clear();
_currentOffset = 0;
_totalConsumed = 0;
_length = 0;
}
}
// void main() {
// final consumer = ByteConsumer();
// consumer.add(Uint8List.fromList([1, 2, 3]));
// consumer.add(Uint8List.fromList([4, 5, 6]));
// while (consumer.isNotEmpty) {
// print(consumer.consume());
// }
// consumer.rollback(5);
// while (consumer.isNotEmpty) {
// print(consumer.consume());
// }
// consumer.rollbackTo(3);
// while (consumer.isNotEmpty) {
// print(consumer.consume());
// }
// }
@@ -0,0 +1,7 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
extension StringCharCode on String {
int get charCode {
return codeUnitAt(0);
}
}
@@ -0,0 +1,317 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
/// A circular buffer in which elements know their index in the buffer.
class IndexAwareCircularBuffer<T extends IndexedItem> {
/// Creates a new circular list with the specified [maxLength].
IndexAwareCircularBuffer(int maxLength)
: _array = List<T?>.filled(maxLength, null);
/// The backing array for this list. Length is always equal to [maxLength].
late List<T?> _array;
/// The number of elements in the list. This is always less than or equal to
/// [maxLength].
var _length = 0;
/// The index of the first element in [_array].
var _startIndex = 0;
/// The start index of this list, including items that has been dropped in
/// overflow
var _absoluteStartIndex = 0;
/// Gets the cyclic index for the specified regular index. The cyclic index
/// can then be used on the backing array to get the element associated with
/// the regular index.
@pragma('vm:prefer-inline')
int _getCyclicIndex(int index) {
return (_startIndex + index) % _array.length;
}
/// Removes the element at [index] from the list.
@pragma('vm:prefer-inline')
void _dropChild(int index) {
final cyclicIndex = _getCyclicIndex(index);
_array[cyclicIndex]?._detach();
_array[cyclicIndex] = null;
}
/// Adds the specified [child] to the list at the specified [index].
@pragma('vm:prefer-inline')
void _adoptChild(int index, T child) {
final cyclicIndex = _getCyclicIndex(index);
_array[cyclicIndex]?._detach();
_array[cyclicIndex] = child.._attach(this, index);
}
/// Moves the element at [fromIndex] to [toIndex]. Both indexes should be
/// less than [maxLength].
@pragma('vm:prefer-inline')
void _moveChild(int fromIndex, int toIndex) {
final fromCyclicIndex = _getCyclicIndex(fromIndex);
final toCyclicIndex = _getCyclicIndex(toIndex);
_array[toCyclicIndex]?._detach();
_array[toCyclicIndex] = _array[fromCyclicIndex]?.._move(toIndex);
_array[fromCyclicIndex] = null;
}
/// Gets the element at the specified [index] in the list.
@pragma('vm:prefer-inline')
T? _getChild(int index) {
return _array[_getCyclicIndex(index)];
}
/// The number of elements that can be stored in the list.
int get maxLength {
return _array.length;
}
/// Sets the number of elements that can be stored in the list. This operation
/// is relatively expensive, as it requires the backing array to be
/// reallocated.
set maxLength(int value) {
if (value <= 0) {
throw ArgumentError.value(value, 'value', "maxLength can't be negative!");
}
if (value == _array.length) return;
// Reconstruct array, starting at index 0. Only transfer values from the
// indexes 0 to length.
final newArray = List<T?>.generate(
value,
(index) => index < _length ? _getChild(index) : null,
);
_startIndex = 0;
_array = newArray;
}
/// Number of elements in the list.
int get length {
return _length;
}
/// Iterates over the list and calls [callback] for each element.
void forEach(void Function(T item) callback) {
final length = _length;
for (int i = 0; i < length; i++) {
callback(_getChild(i)!);
}
}
/// Gets the element at the specified [index] in the list. Throws if the
/// index is out of bounds.
T operator [](int index) {
RangeError.checkValueInInterval(index, 0, length - 1, 'index');
return _getChild(index)!;
}
/// Sets the element at the specified [index] in the list. Throws if the
/// index is out of bounds.
operator []=(int index, T value) {
RangeError.checkValueInInterval(index, 0, length - 1, 'index');
_adoptChild(index, value);
}
/// Removes all elements from the list.
void clear() {
for (var i = 0; i < _length; i++) {
_dropChild(i);
}
_startIndex = 0;
_length = 0;
}
/// Adds all elements in [items] to the list.
void pushAll(Iterable<T> items) {
for (var element in items) {
push(element);
}
}
/// Adds [value] to the end of the list. May cause the first element to be
/// trimmed if the list is full.
void push(T value) {
_adoptChild(_length, value);
if (_length == _array.length) {
// When the list is full, we trim the first element
_startIndex++;
_absoluteStartIndex++;
if (_startIndex == _array.length) {
_startIndex = 0;
}
} else {
// When the list is not full, we just increase the length
_length++;
}
}
/// Removes and returns the last value on the list, throws if the list is
/// empty.
T pop() {
assert(_length > 0, 'Cannot pop from an empty list');
final result = _getChild(_length - 1);
_dropChild(_length - 1);
_length--;
return result!;
}
/// Deletes [count] elements starting at [index], shifting all elements after
/// [index] to the left.
void remove(int index, [int count = 1]) {
if (count > 0) {
if (index + count >= _length) {
count = _length - index;
}
for (var i = index; i < _length - count; i++) {
_moveChild(i + count, i);
}
for (var i = _length - count; i < _length; i++) {
_dropChild(i);
}
_length -= count;
}
}
/// Inserts [item] at [index], shifting all elements after [index] to the
/// right. May cause the first element to be trimmed if the list is full.
void insert(int index, T item) {
RangeError.checkValueInInterval(index, 0, _length, 'index');
if (index == _length) {
return push(item);
}
if (index == 0 && _length >= _array.length) {
// when something is inserted at index 0 and the list is full then
// the new value immediately gets removed => nothing changes
return;
}
for (var i = _length - 1; i >= index; i--) {
_moveChild(i, i + 1);
}
_adoptChild(index, item);
if (_length >= _array.length) {
_startIndex += 1;
_absoluteStartIndex += 1;
} else {
_length++;
}
}
/// Inserts [items] at [index] in order.
void insertAll(int index, List<T> items) {
for (var i = items.length - 1; i >= 0; i--) {
insert(index, items[i]);
// when the list is full then we have to move the index down
// as newly inserted values remove values with a lower index
if (_length >= _array.length) {
index--;
if (index < 0) {
return;
}
}
}
}
/// Removes [count] elements starting at [index], shifting all elements after
/// [index] to the left.
///
/// This method is cheap since it does not actually modify the list, but
/// instead just adjusts the start index and length.
void trimStart(int count) {
if (count > _length) count = _length;
_startIndex += count;
_startIndex %= _array.length;
_length -= count;
}
/// Replaces all elements in the list with [replacement].
void replaceWith(List<T> replacement) {
for (var i = 0; i < _length; i++) {
_dropChild(i);
}
var copyStart = 0;
if (replacement.length > maxLength) {
copyStart = replacement.length - maxLength;
}
for (var i = 0; i < copyStart; i++) {
_dropChild(i);
}
final copyLength = replacement.length - copyStart;
for (var i = 0; i < copyLength; i++) {
_adoptChild(i, replacement[copyStart + i]);
}
_startIndex = 0;
_length = copyLength;
}
/// Replaces the element at [index] with [value] and returns the replaced
/// item.
T swap(int index, T value) {
final result = _getChild(index);
_adoptChild(index, value);
return result!;
}
/// Whether adding another element would cause the first element to be
/// trimmed.
bool get isFull => length == maxLength;
/// Returns a list containing all elements in the list.
List<T> toList() {
return List<T>.generate(length, (index) => this[index]);
}
String debugDump() {
final buffer = StringBuffer();
buffer.writeln('CircularList:');
for (var i = 0; i < _length; i++) {
final child = _getChild(i);
buffer.writeln(' $i: $child');
}
return buffer.toString();
}
}
mixin IndexedItem {
IndexAwareCircularBuffer? _owner;
int? _absoluteIndex;
/// The index of this item in the buffer. Must only be accessed when
/// [attached] is true.
int get index => _absoluteIndex! - _owner!._absoluteStartIndex;
/// Whether this item is currently stored in a buffer.
bool get attached => _owner != null;
/// Sets the owner and index of this item. This is called by the buffer when
/// the item is adopted.
void _attach(IndexAwareCircularBuffer owner, int index) {
_owner = owner;
_absoluteIndex = owner._absoluteStartIndex + index;
}
/// Marks this item as detached from a buffer. This is called after the item
/// has been removed from the buffer.
void _detach() {
_owner = null;
_absoluteIndex = null;
}
/// Moves this item to [newIndex] in the buffer.
void _move(int newIndex) {
assert(attached);
_absoluteIndex = _owner!._absoluteStartIndex + newIndex;
}
}
+113
View File
@@ -0,0 +1,113 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
class _HashEnd {
const _HashEnd();
}
const _HashEnd _hashEnd = _HashEnd();
class _Jenkins {
static int combine(int hash, Object? o) {
assert(o is! Iterable);
hash = 0x1fffffff & (hash + o.hashCode);
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
int hashValues(
Object? arg01,
Object? arg02, [
Object? arg03 = _hashEnd,
Object? arg04 = _hashEnd,
Object? arg05 = _hashEnd,
Object? arg06 = _hashEnd,
Object? arg07 = _hashEnd,
Object? arg08 = _hashEnd,
Object? arg09 = _hashEnd,
Object? arg10 = _hashEnd,
Object? arg11 = _hashEnd,
Object? arg12 = _hashEnd,
Object? arg13 = _hashEnd,
Object? arg14 = _hashEnd,
Object? arg15 = _hashEnd,
Object? arg16 = _hashEnd,
Object? arg17 = _hashEnd,
Object? arg18 = _hashEnd,
Object? arg19 = _hashEnd,
Object? arg20 = _hashEnd,
]) {
int result = 0;
result = _Jenkins.combine(result, arg01);
result = _Jenkins.combine(result, arg02);
if (!identical(arg03, _hashEnd)) {
result = _Jenkins.combine(result, arg03);
if (!identical(arg04, _hashEnd)) {
result = _Jenkins.combine(result, arg04);
if (!identical(arg05, _hashEnd)) {
result = _Jenkins.combine(result, arg05);
if (!identical(arg06, _hashEnd)) {
result = _Jenkins.combine(result, arg06);
if (!identical(arg07, _hashEnd)) {
result = _Jenkins.combine(result, arg07);
if (!identical(arg08, _hashEnd)) {
result = _Jenkins.combine(result, arg08);
if (!identical(arg09, _hashEnd)) {
result = _Jenkins.combine(result, arg09);
if (!identical(arg10, _hashEnd)) {
result = _Jenkins.combine(result, arg10);
if (!identical(arg11, _hashEnd)) {
result = _Jenkins.combine(result, arg11);
if (!identical(arg12, _hashEnd)) {
result = _Jenkins.combine(result, arg12);
if (!identical(arg13, _hashEnd)) {
result = _Jenkins.combine(result, arg13);
if (!identical(arg14, _hashEnd)) {
result = _Jenkins.combine(result, arg14);
if (!identical(arg15, _hashEnd)) {
result = _Jenkins.combine(result, arg15);
if (!identical(arg16, _hashEnd)) {
result = _Jenkins.combine(result, arg16);
if (!identical(arg17, _hashEnd)) {
result = _Jenkins.combine(result, arg17);
if (!identical(arg18, _hashEnd)) {
result = _Jenkins.combine(result, arg18);
if (!identical(arg19, _hashEnd)) {
result = _Jenkins.combine(result, arg19);
if (!identical(arg20, _hashEnd)) {
result = _Jenkins.combine(result, arg20);
// I can see my house from here!
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
return _Jenkins.finish(result);
}
int hashList(Iterable<Object> arguments) {
int result = 0;
for (Object argument in arguments) {
result = _Jenkins.combine(result, argument);
}
return _Jenkins.finish(result);
}
@@ -0,0 +1,36 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
/// Fixed-size list based lookup table, optimized for small positive integer
/// keys.
class FastLookupTable<T> {
FastLookupTable(Map<int, T> data) {
var maxIndex = data.keys.first;
for (var key in data.keys) {
if (key > maxIndex) {
maxIndex = key;
}
}
_maxIndex = maxIndex;
_table = List<T?>.filled(maxIndex + 1, null);
for (var entry in data.entries) {
_table[entry.key] = entry.value;
}
}
late final List<T?> _table;
late final int _maxIndex;
T? operator [](int index) {
if (index > _maxIndex) {
return null;
}
return _table[index];
}
int get maxIndex => _maxIndex;
}
+514
View File
@@ -0,0 +1,514 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
// ignore_for_file: constant_identifier_names
import 'dart:typed_data';
const BMP_COMBINING = [
[0x0300, 0x036F],
[0x0483, 0x0489],
[0x0591, 0x05BD],
[0x05BF, 0x05BF],
[0x05C1, 0x05C2],
[0x05C4, 0x05C5],
[0x05C7, 0x05C7],
[0x0600, 0x0605],
[0x0610, 0x061A],
[0x061C, 0x061C],
[0x064B, 0x065F],
[0x0670, 0x0670],
[0x06D6, 0x06DD],
[0x06DF, 0x06E4],
[0x06E7, 0x06E8],
[0x06EA, 0x06ED],
[0x070F, 0x070F],
[0x0711, 0x0711],
[0x0730, 0x074A],
[0x07A6, 0x07B0],
[0x07EB, 0x07F3],
[0x07FD, 0x07FD],
[0x0816, 0x0819],
[0x081B, 0x0823],
[0x0825, 0x0827],
[0x0829, 0x082D],
[0x0859, 0x085B],
[0x08D3, 0x0902],
[0x093A, 0x093A],
[0x093C, 0x093C],
[0x0941, 0x0948],
[0x094D, 0x094D],
[0x0951, 0x0957],
[0x0962, 0x0963],
[0x0981, 0x0981],
[0x09BC, 0x09BC],
[0x09C1, 0x09C4],
[0x09CD, 0x09CD],
[0x09E2, 0x09E3],
[0x09FE, 0x09FE],
[0x0A01, 0x0A02],
[0x0A3C, 0x0A3C],
[0x0A41, 0x0A42],
[0x0A47, 0x0A48],
[0x0A4B, 0x0A4D],
[0x0A51, 0x0A51],
[0x0A70, 0x0A71],
[0x0A75, 0x0A75],
[0x0A81, 0x0A82],
[0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5],
[0x0AC7, 0x0AC8],
[0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3],
[0x0AFA, 0x0AFF],
[0x0B01, 0x0B01],
[0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F],
[0x0B41, 0x0B44],
[0x0B4D, 0x0B4D],
[0x0B56, 0x0B56],
[0x0B62, 0x0B63],
[0x0B82, 0x0B82],
[0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD],
[0x0C00, 0x0C00],
[0x0C04, 0x0C04],
[0x0C3E, 0x0C40],
[0x0C46, 0x0C48],
[0x0C4A, 0x0C4D],
[0x0C55, 0x0C56],
[0x0C62, 0x0C63],
[0x0C81, 0x0C81],
[0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF],
[0x0CC6, 0x0CC6],
[0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3],
[0x0D00, 0x0D01],
[0x0D3B, 0x0D3C],
[0x0D41, 0x0D44],
[0x0D4D, 0x0D4D],
[0x0D62, 0x0D63],
[0x0DCA, 0x0DCA],
[0x0DD2, 0x0DD4],
[0x0DD6, 0x0DD6],
[0x0E31, 0x0E31],
[0x0E34, 0x0E3A],
[0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1],
[0x0EB4, 0x0EBC],
[0x0EC8, 0x0ECD],
[0x0F18, 0x0F19],
[0x0F35, 0x0F35],
[0x0F37, 0x0F37],
[0x0F39, 0x0F39],
[0x0F71, 0x0F7E],
[0x0F80, 0x0F84],
[0x0F86, 0x0F87],
[0x0F8D, 0x0F97],
[0x0F99, 0x0FBC],
[0x0FC6, 0x0FC6],
[0x102D, 0x1030],
[0x1032, 0x1037],
[0x1039, 0x103A],
[0x103D, 0x103E],
[0x1058, 0x1059],
[0x105E, 0x1060],
[0x1071, 0x1074],
[0x1082, 0x1082],
[0x1085, 0x1086],
[0x108D, 0x108D],
[0x109D, 0x109D],
[0x1160, 0x11FF],
[0x135D, 0x135F],
[0x1712, 0x1714],
[0x1732, 0x1734],
[0x1752, 0x1753],
[0x1772, 0x1773],
[0x17B4, 0x17B5],
[0x17B7, 0x17BD],
[0x17C6, 0x17C6],
[0x17C9, 0x17D3],
[0x17DD, 0x17DD],
[0x180B, 0x180E],
[0x1885, 0x1886],
[0x18A9, 0x18A9],
[0x1920, 0x1922],
[0x1927, 0x1928],
[0x1932, 0x1932],
[0x1939, 0x193B],
[0x1A17, 0x1A18],
[0x1A1B, 0x1A1B],
[0x1A56, 0x1A56],
[0x1A58, 0x1A5E],
[0x1A60, 0x1A60],
[0x1A62, 0x1A62],
[0x1A65, 0x1A6C],
[0x1A73, 0x1A7C],
[0x1A7F, 0x1A7F],
[0x1AB0, 0x1ABE],
[0x1B00, 0x1B03],
[0x1B34, 0x1B34],
[0x1B36, 0x1B3A],
[0x1B3C, 0x1B3C],
[0x1B42, 0x1B42],
[0x1B6B, 0x1B73],
[0x1B80, 0x1B81],
[0x1BA2, 0x1BA5],
[0x1BA8, 0x1BA9],
[0x1BAB, 0x1BAD],
[0x1BE6, 0x1BE6],
[0x1BE8, 0x1BE9],
[0x1BED, 0x1BED],
[0x1BEF, 0x1BF1],
[0x1C2C, 0x1C33],
[0x1C36, 0x1C37],
[0x1CD0, 0x1CD2],
[0x1CD4, 0x1CE0],
[0x1CE2, 0x1CE8],
[0x1CED, 0x1CED],
[0x1CF4, 0x1CF4],
[0x1CF8, 0x1CF9],
[0x1DC0, 0x1DF9],
[0x1DFB, 0x1DFF],
[0x200B, 0x200F],
[0x202A, 0x202E],
[0x2060, 0x2064],
[0x2066, 0x206F],
[0x20D0, 0x20F0],
[0x2CEF, 0x2CF1],
[0x2D7F, 0x2D7F],
[0x2DE0, 0x2DFF],
[0x302A, 0x302D],
[0x3099, 0x309A],
[0xA66F, 0xA672],
[0xA674, 0xA67D],
[0xA69E, 0xA69F],
[0xA6F0, 0xA6F1],
[0xA802, 0xA802],
[0xA806, 0xA806],
[0xA80B, 0xA80B],
[0xA825, 0xA826],
[0xA8C4, 0xA8C5],
[0xA8E0, 0xA8F1],
[0xA8FF, 0xA8FF],
[0xA926, 0xA92D],
[0xA947, 0xA951],
[0xA980, 0xA982],
[0xA9B3, 0xA9B3],
[0xA9B6, 0xA9B9],
[0xA9BC, 0xA9BD],
[0xA9E5, 0xA9E5],
[0xAA29, 0xAA2E],
[0xAA31, 0xAA32],
[0xAA35, 0xAA36],
[0xAA43, 0xAA43],
[0xAA4C, 0xAA4C],
[0xAA7C, 0xAA7C],
[0xAAB0, 0xAAB0],
[0xAAB2, 0xAAB4],
[0xAAB7, 0xAAB8],
[0xAABE, 0xAABF],
[0xAAC1, 0xAAC1],
[0xAAEC, 0xAAED],
[0xAAF6, 0xAAF6],
[0xABE5, 0xABE5],
[0xABE8, 0xABE8],
[0xABED, 0xABED],
[0xFB1E, 0xFB1E],
[0xFE00, 0xFE0F],
[0xFE20, 0xFE2F],
[0xFEFF, 0xFEFF],
[0xFFF9, 0xFFFB],
];
const HIGH_COMBINING = [
[0x101FD, 0x101FD],
[0x102E0, 0x102E0],
[0x10376, 0x1037A],
[0x10A01, 0x10A03],
[0x10A05, 0x10A06],
[0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A],
[0x10A3F, 0x10A3F],
[0x10AE5, 0x10AE6],
[0x10D24, 0x10D27],
[0x10F46, 0x10F50],
[0x11001, 0x11001],
[0x11038, 0x11046],
[0x1107F, 0x11081],
[0x110B3, 0x110B6],
[0x110B9, 0x110BA],
[0x110BD, 0x110BD],
[0x110CD, 0x110CD],
[0x11100, 0x11102],
[0x11127, 0x1112B],
[0x1112D, 0x11134],
[0x11173, 0x11173],
[0x11180, 0x11181],
[0x111B6, 0x111BE],
[0x111C9, 0x111CC],
[0x1122F, 0x11231],
[0x11234, 0x11234],
[0x11236, 0x11237],
[0x1123E, 0x1123E],
[0x112DF, 0x112DF],
[0x112E3, 0x112EA],
[0x11300, 0x11301],
[0x1133B, 0x1133C],
[0x11340, 0x11340],
[0x11366, 0x1136C],
[0x11370, 0x11374],
[0x11438, 0x1143F],
[0x11442, 0x11444],
[0x11446, 0x11446],
[0x1145E, 0x1145E],
[0x114B3, 0x114B8],
[0x114BA, 0x114BA],
[0x114BF, 0x114C0],
[0x114C2, 0x114C3],
[0x115B2, 0x115B5],
[0x115BC, 0x115BD],
[0x115BF, 0x115C0],
[0x115DC, 0x115DD],
[0x11633, 0x1163A],
[0x1163D, 0x1163D],
[0x1163F, 0x11640],
[0x116AB, 0x116AB],
[0x116AD, 0x116AD],
[0x116B0, 0x116B5],
[0x116B7, 0x116B7],
[0x1171D, 0x1171F],
[0x11722, 0x11725],
[0x11727, 0x1172B],
[0x1182F, 0x11837],
[0x11839, 0x1183A],
[0x119D4, 0x119D7],
[0x119DA, 0x119DB],
[0x119E0, 0x119E0],
[0x11A01, 0x11A0A],
[0x11A33, 0x11A38],
[0x11A3B, 0x11A3E],
[0x11A47, 0x11A47],
[0x11A51, 0x11A56],
[0x11A59, 0x11A5B],
[0x11A8A, 0x11A96],
[0x11A98, 0x11A99],
[0x11C30, 0x11C36],
[0x11C38, 0x11C3D],
[0x11C3F, 0x11C3F],
[0x11C92, 0x11CA7],
[0x11CAA, 0x11CB0],
[0x11CB2, 0x11CB3],
[0x11CB5, 0x11CB6],
[0x11D31, 0x11D36],
[0x11D3A, 0x11D3A],
[0x11D3C, 0x11D3D],
[0x11D3F, 0x11D45],
[0x11D47, 0x11D47],
[0x11D90, 0x11D91],
[0x11D95, 0x11D95],
[0x11D97, 0x11D97],
[0x11EF3, 0x11EF4],
[0x13430, 0x13438],
[0x16AF0, 0x16AF4],
[0x16B30, 0x16B36],
[0x16F4F, 0x16F4F],
[0x16F8F, 0x16F92],
[0x1BC9D, 0x1BC9E],
[0x1BCA0, 0x1BCA3],
[0x1D167, 0x1D169],
[0x1D173, 0x1D182],
[0x1D185, 0x1D18B],
[0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244],
[0x1DA00, 0x1DA36],
[0x1DA3B, 0x1DA6C],
[0x1DA75, 0x1DA75],
[0x1DA84, 0x1DA84],
[0x1DA9B, 0x1DA9F],
[0x1DAA1, 0x1DAAF],
[0x1E000, 0x1E006],
[0x1E008, 0x1E018],
[0x1E01B, 0x1E021],
[0x1E023, 0x1E024],
[0x1E026, 0x1E02A],
[0x1E130, 0x1E136],
[0x1E2EC, 0x1E2EF],
[0x1E8D0, 0x1E8D6],
[0x1E944, 0x1E94A],
[0xE0001, 0xE0001],
[0xE0020, 0xE007F],
[0xE0100, 0xE01EF],
];
const BMP_WIDE = [
[0x1100, 0x115F],
[0x231A, 0x231B],
[0x2329, 0x232A],
[0x23E9, 0x23EC],
[0x23F0, 0x23F0],
[0x23F3, 0x23F3],
[0x25FD, 0x25FE],
[0x2614, 0x2615],
[0x2648, 0x2653],
[0x267F, 0x267F],
[0x2693, 0x2693],
[0x26A1, 0x26A1],
[0x26AA, 0x26AB],
[0x26BD, 0x26BE],
[0x26C4, 0x26C5],
[0x26CE, 0x26CE],
[0x26D4, 0x26D4],
[0x26EA, 0x26EA],
[0x26F2, 0x26F3],
[0x26F5, 0x26F5],
[0x26FA, 0x26FA],
[0x26FD, 0x26FD],
[0x2705, 0x2705],
[0x270A, 0x270B],
[0x2728, 0x2728],
[0x274C, 0x274C],
[0x274E, 0x274E],
[0x2753, 0x2755],
[0x2757, 0x2757],
[0x2795, 0x2797],
[0x27B0, 0x27B0],
[0x27BF, 0x27BF],
[0x2B1B, 0x2B1C],
[0x2B50, 0x2B50],
[0x2B55, 0x2B55],
[0x2E80, 0x2E99],
[0x2E9B, 0x2EF3],
[0x2F00, 0x2FD5],
[0x2FF0, 0x2FFB],
[0x3000, 0x3029],
[0x302E, 0x303E],
[0x3041, 0x3096],
[0x309B, 0x30FF],
[0x3105, 0x312F],
[0x3131, 0x318E],
[0x3190, 0x31BA],
[0x31C0, 0x31E3],
[0x31F0, 0x321E],
[0x3220, 0x3247],
[0x3250, 0x4DBF],
[0x4E00, 0xA48C],
[0xA490, 0xA4C6],
[0xA960, 0xA97C],
[0xAC00, 0xD7A3],
[0xF900, 0xFAFF],
[0xFE10, 0xFE19],
[0xFE30, 0xFE52],
[0xFE54, 0xFE66],
[0xFE68, 0xFE6B],
[0xFF01, 0xFF60],
[0xFFE0, 0xFFE6],
];
const HIGH_WIDE = [
[0x16FE0, 0x16FE3],
[0x17000, 0x187F7],
[0x18800, 0x18AF2],
[0x1B000, 0x1B11E],
[0x1B150, 0x1B152],
[0x1B164, 0x1B167],
[0x1B170, 0x1B2FB],
[0x1F004, 0x1F004],
[0x1F0CF, 0x1F0CF],
[0x1F18E, 0x1F18E],
[0x1F191, 0x1F19A],
[0x1F200, 0x1F202],
[0x1F210, 0x1F23B],
[0x1F240, 0x1F248],
[0x1F250, 0x1F251],
[0x1F260, 0x1F265],
[0x1F300, 0x1F320],
[0x1F32D, 0x1F335],
[0x1F337, 0x1F37C],
[0x1F37E, 0x1F393],
[0x1F3A0, 0x1F3CA],
[0x1F3CF, 0x1F3D3],
[0x1F3E0, 0x1F3F0],
[0x1F3F4, 0x1F3F4],
[0x1F3F8, 0x1F43E],
[0x1F440, 0x1F440],
[0x1F442, 0x1F4FC],
[0x1F4FF, 0x1F53D],
[0x1F54B, 0x1F54E],
[0x1F550, 0x1F567],
[0x1F57A, 0x1F57A],
[0x1F595, 0x1F596],
[0x1F5A4, 0x1F5A4],
[0x1F5FB, 0x1F64F],
[0x1F680, 0x1F6C5],
[0x1F6CC, 0x1F6CC],
[0x1F6D0, 0x1F6D2],
[0x1F6D5, 0x1F6D5],
[0x1F6EB, 0x1F6EC],
[0x1F6F4, 0x1F6FA],
[0x1F7E0, 0x1F7EB],
[0x1F90D, 0x1F971],
[0x1F973, 0x1F976],
[0x1F97A, 0x1F9A2],
[0x1F9A5, 0x1F9AA],
[0x1F9AE, 0x1F9CA],
[0x1F9CD, 0x1F9FF],
[0x1FA70, 0x1FA73],
[0x1FA78, 0x1FA7A],
[0x1FA80, 0x1FA82],
[0x1FA90, 0x1FA95],
[0x20000, 0x2FFFD],
[0x30000, 0x3FFFD],
];
final table = buildTable();
Uint8List buildTable() {
final table = Uint8List(65536);
table.fillRange(0, table.length, 1);
table[0] = 0;
table.fillRange(1, 32, 0);
table.fillRange(0x7f, 0xa0, 0);
for (var r = 0; r < BMP_COMBINING.length; ++r) {
table.fillRange(BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1, 0);
}
for (var r = 0; r < BMP_WIDE.length; ++r) {
table.fillRange(BMP_WIDE[r][0], BMP_WIDE[r][1] + 1, 2);
}
return table;
}
bool bisearch(int ucs, List<List<int>> data) {
var min = 0;
var max = data.length - 1;
int mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
class UnicodeV11 {
final version = '11';
int wcwidth(int codePoint) {
if (codePoint < 32) return 0;
if (codePoint < 127) return 1;
if (codePoint < 65536) return table[codePoint];
if (bisearch(codePoint, HIGH_COMBINING)) return 0;
if (bisearch(codePoint, HIGH_WIDE)) return 2;
return 1;
}
}
final unicodeV11 = UnicodeV11();
+36
View File
@@ -0,0 +1,36 @@
// clide terminal — based on xterm.dart v4.0.0 by xuty (MIT).
// See LICENSE in this directory for the original copyright notice.
library;
export 'src/core/buffer/buffer.dart';
export 'src/core/buffer/cell_flags.dart';
export 'src/core/buffer/cell_offset.dart';
export 'src/core/buffer/line.dart';
export 'src/core/buffer/range.dart';
export 'src/core/buffer/range_block.dart';
export 'src/core/buffer/range_line.dart';
export 'src/core/buffer/segment.dart';
export 'src/core/cell.dart';
export 'src/core/color.dart';
export 'src/core/cursor.dart';
export 'src/core/escape/handler.dart';
export 'src/core/escape/parser.dart';
export 'src/core/input/handler.dart';
export 'src/core/input/keys.dart';
export 'src/core/mouse/button.dart';
export 'src/core/mouse/button_state.dart';
export 'src/core/mouse/handler.dart';
export 'src/core/mouse/mode.dart';
export 'src/core/platform.dart';
export 'src/core/state.dart';
export 'src/terminal.dart';
export 'src/terminal_view.dart';
export 'src/ui/controller.dart';
export 'src/ui/cursor_type.dart';
export 'src/ui/keyboard_visibility.dart';
export 'src/ui/pointer_input.dart';
export 'src/ui/selection_mode.dart';
export 'src/ui/shortcut/shortcuts.dart';
export 'src/ui/terminal_text_style.dart';
export 'src/ui/terminal_theme.dart';
export 'src/ui/themes.dart';
+2 -2
View File
@@ -2,7 +2,7 @@ import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:clide/widgets/src/typography.dart';
import 'package:flutter/widgets.dart';
import 'package:xterm/xterm.dart';
import 'package:clide/src/terminal/terminal.dart';
/// Theme-aware terminal view. Wraps xterm.dart's [TerminalView] with
/// clide token bindings, JetBrainsMono as the face, and a Semantics
@@ -54,7 +54,7 @@ class ClidePtyView extends StatelessWidget {
fontFamily: clideMonoFamily,
fontFamilyFallback: clideMonoFamilyFallback,
),
padding: EdgeInsets.zero,
padding: const EdgeInsets.all(2),
backgroundOpacity: 1,
cursorType: TerminalCursorType.block,
),
-24
View File
@@ -410,14 +410,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
quiver:
dependency: transitive
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.2"
shelf:
dependency: transitive
description:
@@ -623,14 +615,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.1"
xterm:
dependency: "direct main"
description:
name: xterm
sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
yaml:
dependency: "direct main"
description:
@@ -639,14 +623,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.3"
zmodem:
dependency: transitive
description:
name: zmodem
sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf"
url: "https://pub.dev"
source: hosted
version: "0.0.6"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.32.0"
+1 -7
View File
@@ -37,8 +37,6 @@ dependencies:
yaml: 3.1.3
xterm: 4.0.0
ffi: 2.1.3
jovial_svg: 1.1.26
@@ -74,6 +72,7 @@ flutter:
- assets/logo/logo-192.png
- assets/logo/appicon.svg
- assets/logo/appicon-256.png
- assets/clide.tmux.conf
- assets/grammars/
- assets/queries/
@@ -96,8 +95,3 @@ flutter:
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Italic.ttf
style: italic
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf
weight: 700
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-BoldItalic.ttf
weight: 700
style: italic