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:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-05T06:49:53Z",
|
||||
"exported_at": "2026-05-05T06:50:38Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
|
||||
|
||||
abstract class TerminalSnapshot {
|
||||
void trimScrollback();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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,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,
|
||||
),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user