chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)

Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+5 -7
View File
@@ -29,14 +29,12 @@ const String argvSentinelCmd = '_argv';
ArgvParseResult unwrapArgvRequest(IpcRequest outer) {
final raw = outer.args['argv'];
if (raw is! List) {
return ArgvError(IpcResponse.err(
id: outer.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '_argv requires args.argv to be a JSON array',
return ArgvError(
IpcResponse.err(
id: outer.id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: '_argv requires args.argv to be a JSON array'),
),
));
);
}
return parseArgv(raw.cast<String>(), requestId: outer.id);
}
+18 -24
View File
@@ -73,11 +73,7 @@ ArgvParseResult parseArgv(List<String> argv, {required String requestId}) {
if (parsed is _TailError) {
return ArgvError(_err(requestId, parsed.message));
}
return ArgvParsed(IpcRequest(
id: requestId,
cmd: first,
args: (parsed as _TailOk).toArgs(),
));
return ArgvParsed(IpcRequest(id: requestId, cmd: first, args: (parsed as _TailOk).toArgs()));
}
// Subsystem.verb form: need at least two tokens.
@@ -97,11 +93,7 @@ ArgvParseResult parseArgv(List<String> argv, {required String requestId}) {
if (parsed is _TailError) {
return ArgvError(_err(requestId, parsed.message));
}
return ArgvParsed(IpcRequest(
id: requestId,
cmd: '$subsystem.$verb',
args: (parsed as _TailOk).toArgs(),
));
return ArgvParsed(IpcRequest(id: requestId, cmd: '$subsystem.$verb', args: (parsed as _TailOk).toArgs()));
}
/// Route a `clide://` deep link to the `deeplink.invoke` command (T-56). A
@@ -109,9 +101,15 @@ ArgvParseResult parseArgv(List<String> argv, {required String requestId}) {
/// it is NOT translated into a command here. The raw URL is handed to the
/// deeplink handler, which validates it against a paranoid (default-deny)
/// allowlist and prompts the user before doing anything (D-90).
ArgvParseResult _deepLinkToRequest(String url, String requestId) => ArgvParsed(IpcRequest(id: requestId, cmd: 'deeplink.invoke', args: {
'positional': [url]
}));
ArgvParseResult _deepLinkToRequest(String url, String requestId) => ArgvParsed(
IpcRequest(
id: requestId,
cmd: 'deeplink.invoke',
args: {
'positional': [url],
},
),
);
// -- internals --------------------------------------------------------------
@@ -126,10 +124,10 @@ class _TailOk extends _TailParseResult {
final List<String> passthrough;
Map<String, Object?> toArgs() => {
if (positional.isNotEmpty) 'positional': positional,
if (flags.isNotEmpty) 'flags': flags,
if (passthrough.isNotEmpty) 'passthrough': passthrough,
};
if (positional.isNotEmpty) 'positional': positional,
if (flags.isNotEmpty) 'flags': flags,
if (passthrough.isNotEmpty) 'passthrough': passthrough,
};
}
class _TailError extends _TailParseResult {
@@ -221,10 +219,6 @@ bool _isValidIdentifier(String s) {
bool _isValidFlagName(String s) => _isValidIdentifier(s);
IpcResponse _err(String id, String message) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message),
);
+15 -36
View File
@@ -77,19 +77,10 @@ class DaemonDispatcher {
return h(IpcRequest(id: req.id, cmd: req.cmd, args: result.values!));
}
Future<IpcResponse> _ping(IpcRequest req) async => IpcResponse.ok(
id: req.id,
data: {
'pong': true,
'ts': DateTime.now().toUtc().toIso8601String(),
'version': clideVersion,
},
);
Future<IpcResponse> _ping(IpcRequest req) async =>
IpcResponse.ok(id: req.id, data: {'pong': true, 'ts': DateTime.now().toUtc().toIso8601String(), 'version': clideVersion});
Future<IpcResponse> _version(IpcRequest req) async => IpcResponse.ok(
id: req.id,
data: {'version': clideVersion},
);
Future<IpcResponse> _version(IpcRequest req) async => IpcResponse.ok(id: req.id, data: {'version': clideVersion});
/// Reflects the live command registry so the surface is discoverable, not
/// just present (T-248). Every registered verb is listed — split into
@@ -139,11 +130,7 @@ class DaemonDispatcher {
tools.add({
'name': '$prefix$cmd',
'description': subsystem.isEmpty ? verb : '$subsystem: $verb',
'inputSchema': {
'type': 'object',
'properties': props,
if (required.isNotEmpty) 'required': required,
},
'inputSchema': {'type': 'object', 'properties': props, if (required.isNotEmpty) 'required': required},
});
}
return tools;
@@ -153,17 +140,9 @@ class DaemonDispatcher {
static Map<String, Object?> _argJsonSchema(ArgSpec s) {
switch (s.type) {
case ArgType.string:
return {
'type': 'string',
if (s.allowed != null) 'enum': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
};
return {'type': 'string', if (s.allowed != null) 'enum': (s.allowed!.toList()..sort()), if (s.pattern != null) 'pattern': s.pattern!.pattern};
case ArgType.number:
return {
'type': 'number',
if (s.min != null) 'minimum': s.min,
if (s.max != null) 'maximum': s.max,
};
return {'type': 'number', if (s.min != null) 'minimum': s.min, if (s.max != null) 'maximum': s.max};
case ArgType.boolean:
return {'type': 'boolean'};
case ArgType.stringList:
@@ -176,13 +155,13 @@ class DaemonDispatcher {
}
static Map<String, Object?> _argSpecJson(ArgSpec s) => {
'type': s.type.name,
if (s.required) 'required': true,
if (s.allowed != null) 'allowed': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
if (s.min != null) 'min': s.min,
if (s.max != null) 'max': s.max,
if (s.maxItems != null) 'maxItems': s.maxItems,
if (s.rejectLeadingDash) 'rejectLeadingDash': true,
};
'type': s.type.name,
if (s.required) 'required': true,
if (s.allowed != null) 'allowed': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
if (s.min != null) 'min': s.min,
if (s.max != null) 'max': s.max,
if (s.maxItems != null) 'maxItems': s.maxItems,
if (s.rejectLeadingDash) 'rejectLeadingDash': true,
};
}
+20 -28
View File
@@ -28,8 +28,17 @@ export '../editor/buffer.dart' show Selection;
const _idArg = CommandSchema(positional: ['id'], args: {'id': ArgSpec()});
void registerEditorCommands(DaemonDispatcher d, EditorRegistry registry) {
d.register('editor.open', (req) => _open(req, registry),
schema: const CommandSchema(positional: ['path', 'line'], args: {'path': ArgSpec(), 'line': ArgSpec(type: ArgType.number)}));
d.register(
'editor.open',
(req) => _open(req, registry),
schema: const CommandSchema(
positional: ['path', 'line'],
args: {
'path': ArgSpec(),
'line': ArgSpec(type: ArgType.number),
},
),
);
d.register('editor.active', (req) => _active(req, registry));
d.register('editor.activate', (req) => _activate(req, registry), schema: _idArg);
d.register('editor.list', (req) => _list(req, registry));
@@ -43,23 +52,14 @@ void registerEditorCommands(DaemonDispatcher d, EditorRegistry registry) {
}
IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: msg,
hint: hint,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: msg, hint: hint),
);
IpcResponse _notFound(String id, String msg) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: msg,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: msg),
);
String? _resolveId(IpcRequest req, EditorRegistry r) {
final id = req.args['id'] as String?;
@@ -94,20 +94,12 @@ Future<IpcResponse> _open(IpcRequest req, EditorRegistry r) async {
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'editor.open failed: ${e.message}',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'editor.open failed: ${e.message}'),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'editor.open failed: $e',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'editor.open failed: $e'),
);
}
}
@@ -145,7 +137,7 @@ Future<IpcResponse> _list(IpcRequest req, EditorRegistry r) async {
return IpcResponse.ok(
id: req.id,
data: {
'buffers': [for (final b in r.buffers) b.toJson()]
'buffers': [for (final b in r.buffers) b.toJson()],
},
);
}
+23 -43
View File
@@ -28,12 +28,7 @@ const int _filesReadMaxBytes = 10 * 1024 * 1024;
/// Daemon-side state for the `files` subsystem. Holds one
/// [FileWatcher] rooted at the workspace and a resolved [IgnoreSet].
class FilesService {
FilesService({
required this.root,
required this.events,
IgnoreSet? ignore,
this.extraReadRoots = const [],
}) : ignore = ignore ?? _defaultIgnore(root);
FilesService({required this.root, required this.events, IgnoreSet? ignore, this.extraReadRoots = const []}) : ignore = ignore ?? _defaultIgnore(root);
/// Build from the current working directory, walking up to the git
/// root if present. Falls back to CWD otherwise.
@@ -58,12 +53,7 @@ class FilesService {
_watcher = w;
await w.start();
w.stream.listen((change) {
events.emit(IpcEvent(
subsystem: 'files',
kind: 'files.changed',
timestamp: DateTime.now().toUtc(),
data: change.toJson(),
));
events.emit(IpcEvent(subsystem: 'files', kind: 'files.changed', timestamp: DateTime.now().toUtc(), data: change.toJson()));
});
}
@@ -74,20 +64,15 @@ class FilesService {
}
void registerFilesCommands(DaemonDispatcher d, FilesService files) {
d.register(
'files.root',
(req) async => IpcResponse.ok(
id: req.id,
data: {
'path': files.root.absolute.path,
'ignorePatterns': files.ignore.length,
},
));
d.register('files.root', (req) async => IpcResponse.ok(id: req.id, data: {'path': files.root.absolute.path, 'ignorePatterns': files.ignore.length}));
d.register('files.read', (req) async {
final path = req.args['path'] as String?;
if (path == null || path.isEmpty) {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'files.read requires a path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'files.read requires a path'),
);
}
final String absPath;
try {
@@ -96,11 +81,17 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
// the trusted extra read roots (Claude config dirs, D-80).
absPath = resolveUnderRootsFollowingSymlinks(files.root, files.extraReadRoots, path);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $path'),
);
}
final file = File(absPath);
if (!file.existsSync()) {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file not found: $path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file not found: $path'),
);
}
// Cap response size so a single IPC call can't OOM the UI on a
// multi-gigabyte log file. Caller can paginate / stream via a
@@ -109,11 +100,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
if (length > _filesReadMaxBytes) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'file too large: $path ($length bytes; cap $_filesReadMaxBytes)',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file too large: $path ($length bytes; cap $_filesReadMaxBytes)'),
);
}
final content = file.readAsStringSync();
@@ -126,14 +113,13 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
try {
resolveUnderRootFollowingSymlinks(files.root, dir);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $dir'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $dir'),
);
}
}
final entries = await listDir(
root: files.root,
dir: dir,
ignore: files.ignore,
);
final entries = await listDir(root: files.root, dir: dir, ignore: files.ignore);
return IpcResponse.ok(
id: req.id,
data: {
@@ -156,10 +142,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
d.register('files.watch', (req) async {
await files.startWatching();
return IpcResponse.ok(
id: req.id,
data: const {'subscribed': true},
);
return IpcResponse.ok(id: req.id, data: const {'subscribed': true});
});
}
@@ -192,8 +175,5 @@ IgnoreSet _defaultIgnore(Directory root) {
// Merge: built-in patterns first, user patterns last. "Last match
// wins" semantics give the user the ability to un-ignore via `!`
// in a future extension of the matcher.
return IgnoreSet([
...IgnoreSet.builtin().patterns,
...user.patterns,
]);
return IgnoreSet([...IgnoreSet.builtin().patterns, ...user.patterns]);
}
+33 -76
View File
@@ -28,12 +28,7 @@ const int _gitPathsMaxCount = 256;
/// handler; the leading-dash guard stops argv injection at the
/// dispatcher (mirrors [validateGitRef], kept below as
/// defense-in-depth since the git client is also UI-reachable).
const CommandSchema _checkoutSchema = CommandSchema(
positional: ['branch'],
args: {
'branch': ArgSpec(required: true, rejectLeadingDash: true),
},
);
const CommandSchema _checkoutSchema = CommandSchema(positional: ['branch'], args: {'branch': ArgSpec(required: true, rejectLeadingDash: true)});
/// Schema for `git.push` (D-74). `clide git push <remote> <branch>`.
/// Both refs optional (bare `git.push` is valid); leading-dash
@@ -47,11 +42,7 @@ const CommandSchema _pushSchema = CommandSchema(
},
);
void registerGitCommands(
DaemonDispatcher d,
GitClient git,
DaemonEventSink events,
) {
void registerGitCommands(DaemonDispatcher d, GitClient git, DaemonEventSink events) {
d.register('git.status', (req) async {
try {
final status = await git.status();
@@ -68,10 +59,13 @@ void registerGitCommands(
final tooMany = _tooManyPaths(req.id, paths);
if (tooMany != null) return tooMany;
final diffs = await git.diff(staged: staged, paths: paths);
return IpcResponse.ok(id: req.id, data: {
'staged': staged,
'diffs': [for (final d in diffs) d.toJson()],
});
return IpcResponse.ok(
id: req.id,
data: {
'staged': staged,
'diffs': [for (final d in diffs) d.toJson()],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -82,12 +76,7 @@ void registerGitCommands(
if (paths.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.stage requires paths',
hint: 'pass {paths: ["file.txt"]}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.stage requires paths', hint: 'pass {paths: ["file.txt"]}'),
);
}
final tooMany = _tooManyPaths(req.id, paths);
@@ -127,11 +116,7 @@ void registerGitCommands(
if (patch == null || patch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.stage-hunk requires a patch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.stage-hunk requires a patch'),
);
}
try {
@@ -148,11 +133,7 @@ void registerGitCommands(
if (patch == null || patch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.unstage-hunk requires a patch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.unstage-hunk requires a patch'),
);
}
try {
@@ -169,11 +150,7 @@ void registerGitCommands(
if (paths.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.discard requires paths',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.discard requires paths'),
);
}
try {
@@ -190,11 +167,7 @@ void registerGitCommands(
if (message == null || message.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.commit requires a message',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.commit requires a message'),
);
}
try {
@@ -233,18 +206,17 @@ void registerGitCommands(
if (count > _gitLogMaxCount) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.log count $count exceeds cap $_gitLogMaxCount',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.log count $count exceeds cap $_gitLogMaxCount'),
);
}
try {
final entries = await git.log(count: count);
return IpcResponse.ok(id: req.id, data: {
'entries': [for (final e in entries) e.toJson()],
});
return IpcResponse.ok(
id: req.id,
data: {
'entries': [for (final e in entries) e.toJson()],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -275,11 +247,14 @@ void registerGitCommands(
d.register('git.branches', (req) async {
try {
final b = await git.branches();
return IpcResponse.ok(id: req.id, data: {
'branches': [
for (final e in b) {'name': e.name, 'current': e.current}
],
});
return IpcResponse.ok(
id: req.id,
data: {
'branches': [
for (final e in b) {'name': e.name, 'current': e.current},
],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -290,11 +265,7 @@ void registerGitCommands(
if (branch == null || branch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.checkout requires a branch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.checkout requires a branch'),
);
}
try {
@@ -317,31 +288,17 @@ IpcResponse? _tooManyPaths(String id, List<String> paths) {
if (paths.length <= _gitPathsMaxCount) return null;
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'paths length ${paths.length} exceeds cap $_gitPathsMaxCount',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'paths length ${paths.length} exceeds cap $_gitPathsMaxCount'),
);
}
void _emitChanged(DaemonEventSink events) {
events.emit(IpcEvent(
subsystem: 'git',
kind: 'git.changed',
timestamp: DateTime.now().toUtc(),
data: const {},
));
events.emit(IpcEvent(subsystem: 'git', kind: 'git.changed', timestamp: DateTime.now().toUtc(), data: const {}));
}
IpcResponse _gitError(String id, GitException e) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: e.message,
hint: e.stderr.isNotEmpty ? e.stderr : null,
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: e.message, hint: e.stderr.isNotEmpty ? e.stderr : null),
);
}
+7 -19
View File
@@ -35,11 +35,7 @@ typedef ImagePathResolver = String? Function(String path);
/// publisher so both ends point at one name.
const imageShowChannel = 'image';
void registerImageCommands(
DaemonDispatcher d,
MessagePublisher? Function() publisher, {
ImagePathResolver? resolve,
}) {
void registerImageCommands(DaemonDispatcher d, MessagePublisher? Function() publisher, {ImagePathResolver? resolve}) {
d.register(
'image.show',
(req) async => _show(req, publisher, resolve),
@@ -55,15 +51,11 @@ void registerImageCommands(
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
Future<IpcResponse> _show(
IpcRequest req,
MessagePublisher? Function() publisherSource,
ImagePathResolver? resolve,
) async {
Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisherSource, ImagePathResolver? resolve) async {
final path = req.args['path'] as String?;
if (path == null || path.trim().isEmpty) {
return _userErr(req.id, 'an image path is required (e.g. `image show docs/diagram.png`)');
@@ -71,11 +63,7 @@ Future<IpcResponse> _show(
final ext = _extensionOf(path);
if (!imageShowExtensions.contains(ext)) {
return _userErr(
req.id,
'unsupported image format${ext.isEmpty ? '' : ' ".$ext"'}',
hint: 'one of: ${(imageShowExtensions.toList()..sort()).join(', ')}',
);
return _userErr(req.id, 'unsupported image format${ext.isEmpty ? '' : ' ".$ext"'}', hint: 'one of: ${(imageShowExtensions.toList()..sort()).join(', ')}');
}
// Resolve to a concrete file before publishing, so the CLI fails honestly on
@@ -114,7 +102,7 @@ Future<IpcResponse> _show(
if (caption != null && caption.trim().isNotEmpty) 'caption': caption.trim(),
if (fullscreen) 'fullscreen': true,
});
return IpcResponse.ok(id: req.id, data: {'path': resolved, if (caption != null) 'caption': caption, 'fullscreen': fullscreen, 'shown': true});
return IpcResponse.ok(id: req.id, data: {'path': resolved, 'caption': ?caption, 'fullscreen': fullscreen, 'shown': true});
}
/// Lower-cased extension (without the dot) of [path], or '' if none.
+29 -44
View File
@@ -36,10 +36,23 @@ void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSo
d.register('pane.spawn', (req) => _spawn(req, registry));
d.register('pane.list', (req) => _list(req, registry, viewPanes));
d.register('pane.close', (req) => _close(req, registry), schema: idArg);
d.register('pane.write', (req) => _write(req, registry), schema: const CommandSchema(positional: ['id', 'text'], args: {'id': ArgSpec(), 'text': ArgSpec()}));
d.register('pane.resize', (req) => _resize(req, registry),
schema: const CommandSchema(
positional: ['id', 'cols', 'rows'], args: {'id': ArgSpec(), 'cols': ArgSpec(type: ArgType.number), 'rows': ArgSpec(type: ArgType.number)}));
d.register(
'pane.write',
(req) => _write(req, registry),
schema: const CommandSchema(positional: ['id', 'text'], args: {'id': ArgSpec(), 'text': ArgSpec()}),
);
d.register(
'pane.resize',
(req) => _resize(req, registry),
schema: const CommandSchema(
positional: ['id', 'cols', 'rows'],
args: {
'id': ArgSpec(),
'cols': ArgSpec(type: ArgType.number),
'rows': ArgSpec(type: ArgType.number),
},
),
);
d.register('pane.focus', (req) => _focus(req, registry), schema: idArg);
// pane.tail is a streaming/no-op verb (events arrive via the tail stream),
// a poor request/response MCP tool — keep it off the MCP surface (D-86).
@@ -47,23 +60,14 @@ void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSo
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
hint: hint,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
IpcResponse _notFound(String id, String message) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: message,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: message),
);
Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
final args = req.args;
@@ -87,9 +91,7 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
final envArg = args['env'];
Map<String, String>? env;
if (envArg is Map) {
env = {
for (final e in envArg.entries) '${e.key}': '${e.value}',
};
env = {for (final e in envArg.entries) '${e.key}': '${e.value}'};
}
try {
@@ -108,29 +110,17 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
if (errno != null) {
return IpcResponse.err(
id: req.id,
error: errnoToIpcError(
errno: errno,
op: 'pane.spawn',
target: argv.isNotEmpty ? argv.first : null,
),
error: errnoToIpcError(errno: errno, op: 'pane.spawn', target: argv.isNotEmpty ? argv.first : null),
);
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'pane.spawn failed: ${e.message}',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'pane.spawn failed: ${e.message}'),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'pane.spawn failed: $e',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'pane.spawn failed: $e'),
);
}
}
@@ -143,7 +133,7 @@ Future<IpcResponse> _list(IpcRequest req, PaneRegistry registry, ViewPaneSource?
for (final p in registry.panes) p.toJson(),
if (viewPanes != null)
for (final v in viewPanes()) v.toJson(),
]
],
},
);
}
@@ -198,12 +188,7 @@ Future<IpcResponse> _focus(IpcRequest req, PaneRegistry registry) async {
if (registry.get(id) == null) return _notFound(req.id, 'no such pane: $id');
// Focus is advisory on the daemon side — UIs track their own focus
// state. We just emit the event so subscribers know what changed.
registry.events.emit(IpcEvent(
subsystem: 'pane',
kind: 'pane.focused',
timestamp: DateTime.now().toUtc(),
data: {'id': id},
));
registry.events.emit(IpcEvent(subsystem: 'pane', kind: 'pane.focused', timestamp: DateTime.now().toUtc(), data: {'id': id}));
return IpcResponse.ok(id: req.id, data: {'id': id});
}
+8 -23
View File
@@ -86,37 +86,22 @@ Future<IpcResponse> _resize(IpcRequest req, PanelResizer r) async {
} else {
r.bumpEditorRatio(value);
}
return IpcResponse.ok(id: req.id, data: {
'slot': slot,
'ratio': r.currentEditorRatio,
});
return IpcResponse.ok(id: req.id, data: {'slot': slot, 'ratio': r.currentEditorRatio});
}
final ok = hasTo ? r.setSlotSize(slot, value) : r.bumpSlotSize(slot, value);
if (!ok) {
return _notFound(req.id, 'no such slot: $slot');
}
return IpcResponse.ok(id: req.id, data: {
'slot': slot,
'size': r.currentSlotSize(slot),
});
return IpcResponse.ok(id: req.id, data: {'slot': slot, 'size': r.currentSlotSize(slot)});
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
hint: hint,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
IpcResponse _notFound(String id, String message) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: message,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: message),
);
+8 -31
View File
@@ -128,11 +128,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
d.register('pql.decisions.list', (req) async {
try {
final results = await pql.decisionList(
type: req.args['type'] as String?,
domain: req.args['domain'] as String?,
status: req.args['status'] as String?,
);
final results = await pql.decisionList(type: req.args['type'] as String?, domain: req.args['domain'] as String?, status: req.args['status'] as String?);
return IpcResponse.ok(id: req.id, data: {'decisions': results});
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -158,11 +154,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
return _userError(req.id, 'pql.decisions.show requires an id');
}
try {
final result = await pql.decisionShow(
id,
withRefs: req.args['withRefs'] as bool? ?? false,
withTickets: req.args['withTickets'] as bool? ?? false,
);
final result = await pql.decisionShow(id, withRefs: req.args['withRefs'] as bool? ?? false, withTickets: req.args['withTickets'] as bool? ?? false);
return IpcResponse.ok(id: req.id, data: result);
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -189,11 +181,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
return _userError(req.id, 'pql.tickets.show requires an id');
}
try {
final result = await pql.ticketShow(
id,
withContext: req.args['withContext'] as bool? ?? false,
withBlockers: req.args['withBlockers'] as bool? ?? false,
);
final result = await pql.ticketShow(id, withContext: req.args['withContext'] as bool? ?? false, withBlockers: req.args['withBlockers'] as bool? ?? false);
return IpcResponse.ok(id: req.id, data: result);
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -205,8 +193,8 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
final ids = rawIds is List
? rawIds.cast<String>()
: rawIds is String
? [rawIds]
: <String>[];
? [rawIds]
: <String>[];
final status = req.args['status'] as String?;
if (ids.isEmpty || status == null || status.isEmpty) {
return _userError(req.id, 'pql.tickets.status requires ids and status');
@@ -221,9 +209,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
d.register('pql.tickets.board', (req) async {
try {
final board = await pql.ticketBoard(
team: req.args['team'] as String?,
);
final board = await pql.ticketBoard(team: req.args['team'] as String?);
return IpcResponse.ok(id: req.id, data: {'columns': board});
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -243,22 +229,13 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
IpcResponse _userError(String id, String message) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message),
);
}
IpcResponse _pqlError(String id, PqlException e) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: e.message,
hint: e.stderr.isNotEmpty ? e.stderr : null,
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: e.message, hint: e.stderr.isNotEmpty ? e.stderr : null),
);
}
+8 -45
View File
@@ -25,12 +25,7 @@ import 'dispatcher.dart';
/// sink. One instance per workspace, constructed alongside the other
/// daemon services.
class SearchService {
SearchService({
required this.root,
required this.ignore,
required this.events,
this.useIsolates = true,
});
SearchService({required this.root, required this.ignore, required this.events, this.useIsolates = true});
final Directory root;
final IgnoreSet ignore;
@@ -64,12 +59,7 @@ class SearchService {
/// path-safety guard. The clean-git-tree safety gate is enforced by
/// the caller (the UI checks `git.status` before requesting apply).
Future<Map<String, Object?>> replace(SearchQuery query, String replacement, {required bool apply}) async {
final files = await computeReplacements(
root: root,
ignore: ignore,
query: query,
replacement: replacement,
);
final files = await computeReplacements(root: root, ignore: ignore, query: query, replacement: replacement);
if (!apply) {
return {
'apply': false,
@@ -103,13 +93,7 @@ class SearchService {
Future<void> _run(String id, SearchQuery query, CancelToken cancel) async {
try {
await for (final batch in grepWorkspace(
root: root,
ignore: ignore,
query: query,
cancel: cancel,
useIsolates: useIsolates,
)) {
await for (final batch in grepWorkspace(root: root, ignore: ignore, query: query, cancel: cancel, useIsolates: useIsolates)) {
if (cancel.isCancelled) break;
_emit('search.match', {
'searchId': id,
@@ -127,12 +111,7 @@ class SearchService {
}
void _emit(String kind, Map<String, Object?> data) {
events.emit(IpcEvent(
subsystem: 'search',
kind: kind,
timestamp: DateTime.now().toUtc(),
data: data,
));
events.emit(IpcEvent(subsystem: 'search', kind: kind, timestamp: DateTime.now().toUtc(), data: data));
}
}
@@ -166,11 +145,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (query.pattern.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.grep requires a non-empty pattern',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.grep requires a non-empty pattern'),
);
}
final id = search.start(query);
@@ -184,11 +159,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (query.pattern.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.replace requires a non-empty pattern',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.replace requires a non-empty pattern'),
);
}
try {
@@ -197,11 +168,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
} on FormatException catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'invalid regex: ${e.message}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'invalid regex: ${e.message}'),
);
}
}, schema: _replaceSchema);
@@ -211,11 +178,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (id == null || id.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.cancel requires a searchId',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.cancel requires a searchId'),
);
}
search.cancel(id);
+7 -22
View File
@@ -47,27 +47,20 @@ const Set<String> _toastSeverities = {'success', 'warning', 'error', 'info'};
/// nothing has reported a value for that address (or there is no live UI).
typedef FilterValueSource = String? Function(String address);
void registerUiCommands(
DaemonDispatcher d,
MessagePublisher? Function() publisher, {
FilterValueSource? filterValue,
}) {
void registerUiCommands(DaemonDispatcher d, MessagePublisher? Function() publisher, {FilterValueSource? filterValue}) {
d.register('ui.open', (req) async => _open(req, publisher));
d.register('ui.toast', (req) async => _toast(req, publisher));
d.register(
'ui.filter',
(req) async => _filter(req, publisher, filterValue),
schema: const CommandSchema(
positional: ['address', 'query'],
args: {'address': ArgSpec(required: true), 'query': ArgSpec()},
),
schema: const CommandSchema(positional: ['address', 'query'], args: {'address': ArgSpec(required: true), 'query': ArgSpec()}),
);
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
Future<IpcResponse> _open(IpcRequest req, MessagePublisher? Function() publisherSource) async {
// Accept CLI positionals (`ui open <reader> <ref>`) or named args.
@@ -134,11 +127,7 @@ Future<IpcResponse> _toast(IpcRequest req, MessagePublisher? Function() publishe
);
}
// Channel literal must match ToastService's `toastChannel`.
publish('cli', 'toast', {
'message': message,
'severity': severity,
if (durationMs != null) 'durationMs': durationMs,
});
publish('cli', 'toast', {'message': message, 'severity': severity, 'durationMs': ?durationMs});
return IpcResponse.ok(id: req.id, data: {'message': message, 'severity': severity, 'shown': true});
}
@@ -154,11 +143,7 @@ Future<IpcResponse> _toast(IpcRequest req, MessagePublisher? Function() publishe
/// `address` is a pane/box id from `clide pane list`. With a `query` arg the
/// verb *drives* — publishes a `filter.set` the box consumes. Without one it
/// *observes* — reads the box's last reported value from the FilterStateCache.
Future<IpcResponse> _filter(
IpcRequest req,
MessagePublisher? Function() publisherSource,
FilterValueSource? filterValue,
) async {
Future<IpcResponse> _filter(IpcRequest req, MessagePublisher? Function() publisherSource, FilterValueSource? filterValue) async {
final address = req.args['address'] as String?;
if (address == null || address.isEmpty) {
return _userErr(req.id, 'an address is required', hint: 'a pane/box id from `clide pane list` (e.g. decisions.panel)');
+12 -26
View File
@@ -11,9 +11,7 @@ import 'editor_settings.dart';
class Selection {
const Selection({required this.start, required this.end});
const Selection.collapsed(int offset)
: start = offset,
end = offset;
const Selection.collapsed(int offset) : start = offset, end = offset;
final int start;
final int end;
@@ -23,10 +21,7 @@ class Selection {
Map<String, Object?> toJson() => {'start': start, 'end': end};
factory Selection.fromJson(Map<String, Object?> j) => Selection(
start: (j['start'] as num).toInt(),
end: (j['end'] as num).toInt(),
);
factory Selection.fromJson(Map<String, Object?> j) => Selection(start: (j['start'] as num).toInt(), end: (j['end'] as num).toInt());
@override
bool operator ==(Object other) => other is Selection && other.start == start && other.end == end;
@@ -39,14 +34,8 @@ class Selection {
}
class EditorBuffer {
EditorBuffer({
required this.id,
required this.path,
required this.content,
Selection? selection,
this.dirty = false,
this.settings = EditorSettings.empty,
}) : selection = selection ?? const Selection.collapsed(0);
EditorBuffer({required this.id, required this.path, required this.content, Selection? selection, this.dirty = false, this.settings = EditorSettings.empty})
: selection = selection ?? const Selection.collapsed(0);
/// Stable daemon-local id (`b_1`, `b_2`, …).
final String id;
@@ -77,18 +66,15 @@ class EditorBuffer {
EditorSettings settings;
Map<String, Object?> toJson() => {
'id': id,
'path': path,
'length': content.length,
'selection': selection.toJson(),
'dirty': dirty,
'editorSettings': settings.toJson(),
};
'id': id,
'path': path,
'length': content.length,
'selection': selection.toJson(),
'dirty': dirty,
'editorSettings': settings.toJson(),
};
/// Full snapshot including [content] — for `editor.read` / tests /
/// anything that needs the text explicitly.
Map<String, Object?> toFullJson() => {
...toJson(),
'content': content,
};
Map<String, Object?> toFullJson() => {...toJson(), 'content': content};
}
+21 -21
View File
@@ -54,11 +54,11 @@ class EditorSettings {
/// The line terminator [endOfLine] names, or null when unset.
String? get eolString => switch (endOfLine) {
'lf' => '\n',
'crlf' => '\r\n',
'cr' => '\r',
_ => null,
};
'lf' => '\n',
'crlf' => '\r\n',
'cr' => '\r',
_ => null,
};
/// The text one Tab press inserts, or null to keep the editor's default
/// (Flutter's focus traversal) — the editor only takes over Tab when a source
@@ -75,25 +75,25 @@ class EditorSettings {
/// first) lives in the resolver — a higher-precedence source (settings panel,
/// clide settings file) merges over a lower one (.editorconfig).
EditorSettings merge(EditorSettings other) => EditorSettings(
indentStyle: other.indentStyle ?? indentStyle,
indentSize: other.indentSize ?? indentSize,
tabWidth: other.tabWidth ?? tabWidth,
endOfLine: other.endOfLine ?? endOfLine,
maxLineLength: other.maxLineLength ?? maxLineLength,
trimTrailingWhitespace: other.trimTrailingWhitespace ?? trimTrailingWhitespace,
insertFinalNewline: other.insertFinalNewline ?? insertFinalNewline,
);
indentStyle: other.indentStyle ?? indentStyle,
indentSize: other.indentSize ?? indentSize,
tabWidth: other.tabWidth ?? tabWidth,
endOfLine: other.endOfLine ?? endOfLine,
maxLineLength: other.maxLineLength ?? maxLineLength,
trimTrailingWhitespace: other.trimTrailingWhitespace ?? trimTrailingWhitespace,
insertFinalNewline: other.insertFinalNewline ?? insertFinalNewline,
);
/// Only the set keys, for the IPC payload. Empty map when [isEmpty].
Map<String, Object?> toJson() => {
if (indentStyle != null) 'indent_style': indentStyle,
if (indentSize != null) 'indent_size': indentSize,
if (tabWidth != null) 'tab_width': tabWidth,
if (endOfLine != null) 'end_of_line': endOfLine,
if (maxLineLength != null) 'max_line_length': maxLineLength,
if (trimTrailingWhitespace != null) 'trim_trailing_whitespace': trimTrailingWhitespace,
if (insertFinalNewline != null) 'insert_final_newline': insertFinalNewline,
};
if (indentStyle != null) 'indent_style': indentStyle,
if (indentSize != null) 'indent_size': indentSize,
if (tabWidth != null) 'tab_width': tabWidth,
if (endOfLine != null) 'end_of_line': endOfLine,
if (maxLineLength != null) 'max_line_length': maxLineLength,
if (trimTrailingWhitespace != null) 'trim_trailing_whitespace': trimTrailingWhitespace,
if (insertFinalNewline != null) 'insert_final_newline': insertFinalNewline,
};
factory EditorSettings.fromJson(Object? raw) {
if (raw is! Map) return empty;
+4 -4
View File
@@ -113,10 +113,10 @@ int? _posInt(String? v) {
}
bool? _bool(String? v) => switch (v) {
'true' => true,
'false' => false,
_ => null,
};
'true' => true,
'false' => false,
_ => null,
};
// ---------------------------------------------------------------------------
// INI parsing
+12 -57
View File
@@ -15,10 +15,7 @@ import 'buffer.dart';
import 'editor_settings_resolver.dart';
class EditorRegistry {
EditorRegistry({
required this.events,
required this.workspaceRoot,
});
EditorRegistry({required this.events, required this.workspaceRoot});
final DaemonEventSink events;
@@ -53,12 +50,7 @@ class EditorRegistry {
}
final id = 'b_${_nextId++}';
final buf = EditorBuffer(
id: id,
path: path,
content: content,
settings: resolveEditorSettings(workspaceRoot, path),
);
final buf = EditorBuffer(id: id, path: path, content: content, settings: resolveEditorSettings(workspaceRoot, path));
_buffers[id] = buf;
_pathToId[path] = id;
@@ -111,10 +103,7 @@ class EditorRegistry {
void setSelection(String id, Selection sel) {
final buf = _buffers[id];
if (buf == null) return;
final clamped = Selection(
start: sel.start.clamp(0, buf.content.length),
end: sel.end.clamp(0, buf.content.length),
);
final clamped = Selection(start: sel.start.clamp(0, buf.content.length), end: sel.end.clamp(0, buf.content.length));
if (clamped.start == buf.selection.start && clamped.end == buf.selection.end) {
return;
}
@@ -131,23 +120,12 @@ class EditorRegistry {
if (buf == null) return;
buf.content = content;
if (selection != null) {
buf.selection = Selection(
start: selection.start.clamp(0, content.length),
end: selection.end.clamp(0, content.length),
);
buf.selection = Selection(start: selection.start.clamp(0, content.length), end: selection.end.clamp(0, content.length));
} else {
buf.selection = Selection(
start: buf.selection.start.clamp(0, content.length),
end: buf.selection.end.clamp(0, content.length),
);
buf.selection = Selection(start: buf.selection.start.clamp(0, content.length), end: buf.selection.end.clamp(0, content.length));
}
buf.dirty = true;
_emit('editor.edited', {
'id': id,
'kind': 'replace',
'length': content.length,
'selection': buf.selection.toJson(),
});
_emit('editor.edited', {'id': id, 'kind': 'replace', 'length': content.length, 'selection': buf.selection.toJson()});
}
/// Persist [id] to disk. Applies the buffer's on-save settings (EOL,
@@ -167,19 +145,11 @@ class EditorRegistry {
if (changed) {
buf.content = normalized;
buf.selection = Selection(
start: buf.selection.start.clamp(0, normalized.length),
end: buf.selection.end.clamp(0, normalized.length),
);
buf.selection = Selection(start: buf.selection.start.clamp(0, normalized.length), end: buf.selection.end.clamp(0, normalized.length));
// Re-broadcast so the UI reloads the normalized text (the editor.edited
// handler re-reads the buffer); emitted before editor.saved clears dirty.
buf.dirty = false;
_emit('editor.edited', {
'id': id,
'kind': 'replace',
'length': normalized.length,
'selection': buf.selection.toJson(),
});
_emit('editor.edited', {'id': id, 'kind': 'replace', 'length': normalized.length, 'selection': buf.selection.toJson()});
}
buf.dirty = false;
@@ -197,11 +167,7 @@ class EditorRegistry {
final next = resolveEditorSettings(workspaceRoot, buf.path);
if (next.toJson().toString() == buf.settings.toJson().toString()) continue;
buf.settings = next;
_emit('editor.settings-changed', {
'id': buf.id,
'path': buf.path,
'editorSettings': next.toJson(),
});
_emit('editor.settings-changed', {'id': buf.id, 'path': buf.path, 'editorSettings': next.toJson()});
}
}
@@ -235,26 +201,15 @@ class EditorRegistry {
void _emitActive() {
final buf = active;
_emit('editor.active-changed', {
'id': buf?.id,
'path': buf?.path,
});
_emit('editor.active-changed', {'id': buf?.id, 'path': buf?.path});
}
void _emitSelection(EditorBuffer buf) {
_emit('editor.selection-changed', {
'id': buf.id,
'selection': buf.selection.toJson(),
});
_emit('editor.selection-changed', {'id': buf.id, 'selection': buf.selection.toJson()});
}
void _emit(String kind, Map<String, Object?> data) {
events.emit(IpcEvent(
subsystem: 'editor',
kind: kind,
timestamp: DateTime.now().toUtc(),
data: data,
));
events.emit(IpcEvent(subsystem: 'editor', kind: kind, timestamp: DateTime.now().toUtc(), data: data));
}
String _absolutePathOf(String repoRelative) {
+2 -10
View File
@@ -18,13 +18,7 @@ library;
/// A single parsed gitignore pattern.
class IgnorePattern {
IgnorePattern._({
required this.source,
required this.negated,
required this.directoryOnly,
required this.anchored,
required this.regex,
});
IgnorePattern._({required this.source, required this.negated, required this.directoryOnly, required this.anchored, required this.regex});
/// The raw line from the file (for diagnostics).
final String source;
@@ -168,7 +162,5 @@ class IgnoreSet {
/// ignore files. Matches D-004's "walker magic: none except
/// `.git/`" — but the tree-view UI benefits from hiding `.pql/` and
/// `.dart_tool/` too since users never edit those by hand.
static IgnoreSet builtin() => IgnoreSet.parse(const [
'.git/\n.pql/\n.clide/\n.dart_tool/\nbuild/\nnode_modules/\n',
]);
static IgnoreSet builtin() => IgnoreSet.parse(const ['.git/\n.pql/\n.clide/\n.dart_tool/\nbuild/\nnode_modules/\n']);
}
+20 -33
View File
@@ -10,14 +10,7 @@ import 'dart:io';
import 'ignore.dart';
class FileEntry {
const FileEntry({
required this.name,
required this.path,
required this.isDirectory,
required this.isSymlink,
this.sizeBytes,
this.modifiedMs,
});
const FileEntry({required this.name, required this.path, required this.isDirectory, required this.isSymlink, this.sizeBytes, this.modifiedMs});
/// Display name (basename).
final String name;
@@ -30,23 +23,19 @@ class FileEntry {
final int? modifiedMs;
Map<String, Object?> toJson() => {
'name': name,
'path': path,
'isDirectory': isDirectory,
'isSymlink': isSymlink,
if (sizeBytes != null) 'sizeBytes': sizeBytes,
if (modifiedMs != null) 'modifiedMs': modifiedMs,
};
'name': name,
'path': path,
'isDirectory': isDirectory,
'isSymlink': isSymlink,
if (sizeBytes != null) 'sizeBytes': sizeBytes,
if (modifiedMs != null) 'modifiedMs': modifiedMs,
};
}
/// List the immediate children of [dir] (repo-relative path) under
/// [root]. Filters against [ignore]. Returns entries sorted
/// directory-first, then by name.
Future<List<FileEntry>> listDir({
required Directory root,
required String dir,
required IgnoreSet ignore,
}) async {
Future<List<FileEntry>> listDir({required Directory root, required String dir, required IgnoreSet ignore}) async {
final resolved = dir.isEmpty ? root : Directory('${root.absolute.path}${Platform.pathSeparator}${dir.replaceAll('/', Platform.pathSeparator)}');
if (!await resolved.exists()) return const [];
@@ -57,14 +46,16 @@ Future<List<FileEntry>> listDir({
final stat = await e.stat();
final isDir = stat.type == FileSystemEntityType.directory;
if (ignore.isIgnored(rel, isDirectory: isDir)) continue;
entries.add(FileEntry(
name: name,
path: rel,
isDirectory: isDir,
isSymlink: stat.type == FileSystemEntityType.link,
sizeBytes: isDir ? null : stat.size,
modifiedMs: stat.modified.millisecondsSinceEpoch,
));
entries.add(
FileEntry(
name: name,
path: rel,
isDirectory: isDir,
isSymlink: stat.type == FileSystemEntityType.link,
sizeBytes: isDir ? null : stat.size,
modifiedMs: stat.modified.millisecondsSinceEpoch,
),
);
}
entries.sort((a, b) {
@@ -96,11 +87,7 @@ class WalkResult {
/// cap is hit the walk stops early and [WalkResult.truncated] is set so
/// callers can surface "results truncated". The returned list is sorted
/// by repo-relative path for a deterministic contract.
Future<WalkResult> walkFiles({
required Directory root,
required IgnoreSet ignore,
int maxFiles = 50000,
}) async {
Future<WalkResult> walkFiles({required Directory root, required IgnoreSet ignore, int maxFiles = 50000}) async {
final out = <FileEntry>[];
// DFS over repo-relative directory paths; '' is the root itself.
final stack = <String>[''];
+4 -19
View File
@@ -38,11 +38,7 @@ enum FileChangeKind {
}
class FileChange {
const FileChange({
required this.kind,
required this.path,
required this.isDirectory,
});
const FileChange({required this.kind, required this.path, required this.isDirectory});
final FileChangeKind kind;
@@ -50,11 +46,7 @@ class FileChange {
final String path;
final bool isDirectory;
Map<String, Object?> toJson() => {
'kind': kind.wire,
'path': path,
'isDirectory': isDirectory,
};
Map<String, Object?> toJson() => {'kind': kind.wire, 'path': path, 'isDirectory': isDirectory};
}
class FileWatcher {
@@ -70,10 +62,7 @@ class FileWatcher {
Future<void> start() async {
if (_sub != null) return;
_sub = root.watch(recursive: true).listen(
_onEvent,
onError: (Object e, StackTrace _) => _controller.addError(e),
);
_sub = root.watch(recursive: true).listen(_onEvent, onError: (Object e, StackTrace _) => _controller.addError(e));
}
Future<void> stop() async {
@@ -87,11 +76,7 @@ class FileWatcher {
if (rel == null) return;
final isDir = e.isDirectory;
if (_ignored(rel, isDir)) return;
_controller.add(FileChange(
kind: FileChangeKind.fromEvent(e),
path: rel,
isDirectory: isDir,
));
_controller.add(FileChange(kind: FileChangeKind.fromEvent(e), path: rel, isDirectory: isDir));
}
/// True if `rel` is ignored, or sits inside an ignored directory.
+13 -26
View File
@@ -61,13 +61,7 @@ class GitClient {
return GitStatus(branch: branch, upstream: upstream, ahead: ahead, behind: behind, entries: const []);
}
return GitStatus(
branch: branch,
upstream: upstream,
ahead: ahead,
behind: behind,
entries: parsePorcelainV1(result.stdout as String),
);
return GitStatus(branch: branch, upstream: upstream, ahead: ahead, behind: behind, entries: parsePorcelainV1(result.stdout as String));
}
Future<List<GitDiff>> diff({bool staged = false, List<String> paths = const []}) async {
@@ -83,12 +77,7 @@ class GitClient {
}
Future<List<GitLogEntry>> log({int count = 20}) async {
final r = await _run([
'log',
'--format=%H%x00%h%x00%s%x00%an%x00%aI%x00%b%x01',
'-n',
'$count',
]);
final r = await _run(['log', '--format=%H%x00%h%x00%s%x00%an%x00%aI%x00%b%x01', '-n', '$count']);
if (r.exitCode != 0) return const [];
return parseLog(r.stdout as String);
}
@@ -115,11 +104,7 @@ class GitClient {
/// Resolve a path to its git repo root. Returns null if not a git repo.
Future<String?> repoRoot(String path) async {
try {
final r = await Process.run(
toolchain.git,
['rev-parse', '--show-toplevel'],
workingDirectory: path,
);
final r = await Process.run(toolchain.git, ['rev-parse', '--show-toplevel'], workingDirectory: path);
if (r.exitCode != 0) return null;
final out = (r.stdout as String).trim();
return out.isEmpty ? null : out;
@@ -252,14 +237,16 @@ List<GitLogEntry> parseLog(String output) {
if (trimmed.isEmpty) continue;
final fields = trimmed.split('\x00');
if (fields.length < 5) continue;
entries.add(GitLogEntry(
hash: fields[0],
shortHash: fields[1],
subject: fields[2],
author: fields[3],
date: fields[4],
body: fields.length > 5 ? fields[5].trim() : '',
));
entries.add(
GitLogEntry(
hash: fields[0],
shortHash: fields[1],
subject: fields[2],
author: fields[3],
date: fields[4],
body: fields.length > 5 ? fields[5].trim() : '',
),
);
}
return entries;
}
+36 -88
View File
@@ -12,12 +12,7 @@ import 'operations.dart' show gitBin;
enum DiffLineKind { context, addition, removal, header }
class DiffLine {
const DiffLine({
required this.kind,
required this.text,
this.oldLineNo,
this.newLineNo,
});
const DiffLine({required this.kind, required this.text, this.oldLineNo, this.newLineNo});
final DiffLineKind kind;
final String text;
@@ -25,22 +20,15 @@ class DiffLine {
final int? newLineNo;
Map<String, Object?> toJson() => {
'kind': kind.name,
'text': text,
if (oldLineNo != null) 'oldLineNo': oldLineNo,
if (newLineNo != null) 'newLineNo': newLineNo,
};
'kind': kind.name,
'text': text,
if (oldLineNo != null) 'oldLineNo': oldLineNo,
if (newLineNo != null) 'newLineNo': newLineNo,
};
}
class GitHunk {
const GitHunk({
required this.header,
required this.oldStart,
required this.oldCount,
required this.newStart,
required this.newCount,
required this.lines,
});
const GitHunk({required this.header, required this.oldStart, required this.oldCount, required this.newStart, required this.newCount, required this.lines});
final String header;
final int oldStart;
@@ -70,15 +58,15 @@ class GitHunk {
}
Map<String, Object?> toJson() => {
'header': header,
'oldStart': oldStart,
'oldCount': oldCount,
'newStart': newStart,
'newCount': newCount,
'additions': additions,
'removals': removals,
'lines': [for (final l in lines) l.toJson()],
};
'header': header,
'oldStart': oldStart,
'oldCount': oldCount,
'newStart': newStart,
'newCount': newCount,
'additions': additions,
'removals': removals,
'lines': [for (final l in lines) l.toJson()],
};
}
class GitDiff {
@@ -104,37 +92,29 @@ class GitDiff {
int get removals => hunks.fold(0, (s, h) => s + h.removals);
Map<String, Object?> toJson() => {
'path': path,
if (oldPath != null) 'oldPath': oldPath,
'binary': isBinary,
'new': isNew,
'deleted': isDeleted,
'renamed': isRenamed,
'additions': additions,
'removals': removals,
'hunks': [for (final h in hunks) h.toJson()],
};
'path': path,
if (oldPath != null) 'oldPath': oldPath,
'binary': isBinary,
'new': isNew,
'deleted': isDeleted,
'renamed': isRenamed,
'additions': additions,
'removals': removals,
'hunks': [for (final h in hunks) h.toJson()],
};
}
/// Run `git diff` and parse the result.
///
/// [staged] controls `--cached`. [paths] narrows to specific files.
Future<List<GitDiff>> gitDiff(
Directory workDir, {
bool staged = false,
List<String> paths = const [],
}) async {
Future<List<GitDiff>> gitDiff(Directory workDir, {bool staged = false, List<String> paths = const []}) async {
final args = ['diff', '--unified=3'];
if (staged) args.add('--cached');
if (paths.isNotEmpty) {
args.add('--');
args.addAll(paths);
}
final result = await Process.run(
gitBin,
args,
workingDirectory: workDir.path,
);
final result = await Process.run(gitBin, args, workingDirectory: workDir.path);
if (result.exitCode != 0) return const [];
return parseDiffOutput(result.stdout as String);
}
@@ -210,15 +190,9 @@ List<GitDiff> parseDiffOutput(String output) {
}
}
diffs.add(GitDiff(
path: path,
oldPath: isRenamed ? oldPath : null,
hunks: hunks,
isBinary: isBinary,
isNew: isNew,
isDeleted: isDeleted,
isRenamed: isRenamed,
));
diffs.add(
GitDiff(path: path, oldPath: isRenamed ? oldPath : null, hunks: hunks, isBinary: isBinary, isNew: isNew, isDeleted: isDeleted, isRenamed: isRenamed),
);
}
return diffs;
}
@@ -251,48 +225,22 @@ _HunkParseResult? _parseHunk(List<String> lines, int start) {
if (line.startsWith('diff --git ') || line.startsWith('@@')) break;
if (line.startsWith('+')) {
hunkLines.add(DiffLine(
kind: DiffLineKind.addition,
text: line.substring(1),
newLineNo: newLine,
));
hunkLines.add(DiffLine(kind: DiffLineKind.addition, text: line.substring(1), newLineNo: newLine));
newLine++;
} else if (line.startsWith('-')) {
hunkLines.add(DiffLine(
kind: DiffLineKind.removal,
text: line.substring(1),
oldLineNo: oldLine,
));
hunkLines.add(DiffLine(kind: DiffLineKind.removal, text: line.substring(1), oldLineNo: oldLine));
oldLine++;
} else if (line.startsWith(' ')) {
hunkLines.add(DiffLine(
kind: DiffLineKind.context,
text: line.substring(1),
oldLineNo: oldLine,
newLineNo: newLine,
));
hunkLines.add(DiffLine(kind: DiffLineKind.context, text: line.substring(1), oldLineNo: oldLine, newLineNo: newLine));
oldLine++;
newLine++;
} else if (line == r'\ No newline at end of file') {
hunkLines.add(DiffLine(
kind: DiffLineKind.header,
text: line,
));
hunkLines.add(DiffLine(kind: DiffLineKind.header, text: line));
} else {
break;
}
i++;
}
return _HunkParseResult(
GitHunk(
header: header,
oldStart: oldStart,
oldCount: oldCount,
newStart: newStart,
newCount: newCount,
lines: hunkLines,
),
i,
);
return _HunkParseResult(GitHunk(header: header, oldStart: oldStart, oldCount: oldCount, newStart: newStart, newCount: newCount, lines: hunkLines), i);
}
+33 -103
View File
@@ -53,14 +53,7 @@ void validateGitRef(String? value, {required String kind}) {
}
class GitLogEntry {
const GitLogEntry({
required this.hash,
required this.shortHash,
required this.subject,
required this.author,
required this.date,
this.body = '',
});
const GitLogEntry({required this.hash, required this.shortHash, required this.subject, required this.author, required this.date, this.body = ''});
final String hash;
final String shortHash;
@@ -70,13 +63,13 @@ class GitLogEntry {
final String body;
Map<String, Object?> toJson() => {
'hash': hash,
'shortHash': shortHash,
'subject': subject,
'author': author,
'date': date,
if (body.isNotEmpty) 'body': body,
};
'hash': hash,
'shortHash': shortHash,
'subject': subject,
'author': author,
'date': date,
if (body.isNotEmpty) 'body': body,
};
}
/// Stage files. Empty [paths] means stage all (`git add -A`).
@@ -120,22 +113,14 @@ Future<void> gitUnstageHunk(Directory workDir, String patch) async {
/// Discard unstaged changes for [paths]. Uses `git checkout -- <paths>`.
Future<void> gitDiscard(Directory workDir, List<String> paths) async {
if (paths.isEmpty) return;
final r = await Process.run(
gitBin,
['checkout', '--', ...paths],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['checkout', '--', ...paths], workingDirectory: workDir.path);
if (r.exitCode != 0) {
throw GitException('git checkout failed', stderr: r.stderr as String);
}
}
/// Commit staged changes.
Future<String> gitCommit(
Directory workDir,
String message, {
bool amend = false,
}) async {
Future<String> gitCommit(Directory workDir, String message, {bool amend = false}) async {
final args = ['commit', '-m', message];
if (amend) args.add('--amend');
final r = await Process.run(gitBin, args, workingDirectory: workDir.path);
@@ -143,20 +128,12 @@ Future<String> gitCommit(
throw GitException('git commit failed', stderr: r.stderr as String);
}
// Return the new commit hash.
final hashResult = await Process.run(
gitBin,
['rev-parse', 'HEAD'],
workingDirectory: workDir.path,
);
final hashResult = await Process.run(gitBin, ['rev-parse', 'HEAD'], workingDirectory: workDir.path);
return (hashResult.stdout as String).trim();
}
/// Stash working changes.
Future<void> gitStash(
Directory workDir, {
String? message,
bool includeUntracked = false,
}) async {
Future<void> gitStash(Directory workDir, {String? message, bool includeUntracked = false}) async {
final args = ['stash', 'push'];
if (message != null) {
args.addAll(['-m', message]);
@@ -170,42 +147,22 @@ Future<void> gitStash(
/// Pop the top stash entry.
Future<void> gitStashPop(Directory workDir) async {
final r = await Process.run(
gitBin,
['stash', 'pop'],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['stash', 'pop'], workingDirectory: workDir.path);
if (r.exitCode != 0) {
throw GitException('git stash pop failed', stderr: r.stderr as String);
}
}
/// Git log. Returns the most recent [count] entries.
Future<List<GitLogEntry>> gitLog(
Directory workDir, {
int count = 20,
}) async {
final r = await Process.run(
gitBin,
[
'log',
'--format=%H%x00%h%x00%s%x00%an%x00%aI%x00%b%x01',
'-n',
'$count',
],
workingDirectory: workDir.path,
);
Future<List<GitLogEntry>> gitLog(Directory workDir, {int count = 20}) async {
final r = await Process.run(gitBin, ['log', '--format=%H%x00%h%x00%s%x00%an%x00%aI%x00%b%x01', '-n', '$count'], workingDirectory: workDir.path);
if (r.exitCode != 0) return const [];
return _parseLog(r.stdout as String);
}
/// Pull from remote.
Future<String> gitPull(Directory workDir) async {
final r = await Process.run(
gitBin,
['pull'],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['pull'], workingDirectory: workDir.path);
if (r.exitCode != 0) {
throw GitException('git pull failed', stderr: r.stderr as String);
}
@@ -213,12 +170,7 @@ Future<String> gitPull(Directory workDir) async {
}
/// Push to remote.
Future<String> gitPush(
Directory workDir, {
String? remote,
String? branch,
bool setUpstream = false,
}) async {
Future<String> gitPush(Directory workDir, {String? remote, String? branch, bool setUpstream = false}) async {
if (remote != null) validateGitRef(remote, kind: 'remote');
if (branch != null) validateGitRef(branch, kind: 'branch');
final args = ['push'];
@@ -238,11 +190,7 @@ Future<String> gitPush(
/// List local branches. Returns (name, isCurrent) pairs.
Future<List<({String name, bool current})>> gitBranches(Directory workDir) async {
final r = await Process.run(
gitBin,
['branch', '--format=%(refname:short)|%(HEAD)'],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['branch', '--format=%(refname:short)|%(HEAD)'], workingDirectory: workDir.path);
if (r.exitCode != 0) return const [];
final out = <({String name, bool current})>[];
for (final line in (r.stdout as String).split('\n')) {
@@ -265,11 +213,7 @@ Future<List<({String name, bool current})>> gitBranches(Directory workDir) async
/// argv-injection defence here. Use `gitSwitch` if/when we adopt it.
Future<void> gitCheckout(Directory workDir, String branch) async {
validateGitRef(branch, kind: 'branch');
final r = await Process.run(
gitBin,
['checkout', branch],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['checkout', branch], workingDirectory: workDir.path);
if (r.exitCode != 0) {
throw GitException('git checkout failed', stderr: r.stderr as String);
}
@@ -277,11 +221,7 @@ Future<void> gitCheckout(Directory workDir, String branch) async {
/// Get the current branch name.
Future<String?> gitCurrentBranch(Directory workDir) async {
final r = await Process.run(
gitBin,
['symbolic-ref', '--short', 'HEAD'],
workingDirectory: workDir.path,
);
final r = await Process.run(gitBin, ['symbolic-ref', '--short', 'HEAD'], workingDirectory: workDir.path);
if (r.exitCode != 0) return null;
return (r.stdout as String).trim();
}
@@ -297,43 +237,33 @@ List<GitLogEntry> _parseLog(String output) {
if (trimmed.isEmpty) continue;
final fields = trimmed.split('\x00');
if (fields.length < 5) continue;
entries.add(GitLogEntry(
hash: fields[0],
shortHash: fields[1],
subject: fields[2],
author: fields[3],
date: fields[4],
body: fields.length > 5 ? fields[5].trim() : '',
));
entries.add(
GitLogEntry(
hash: fields[0],
shortHash: fields[1],
subject: fields[2],
author: fields[3],
date: fields[4],
body: fields.length > 5 ? fields[5].trim() : '',
),
);
}
return entries;
}
Future<void> _applyPatch(
Directory workDir,
String patch, {
bool cached = false,
bool reverse = false,
}) async {
Future<void> _applyPatch(Directory workDir, String patch, {bool cached = false, bool reverse = false}) async {
final args = ['apply'];
if (cached) args.add('--cached');
if (reverse) args.add('--reverse');
args.add('--unidiff-zero');
args.add('-');
final proc = await Process.start(
'git',
args,
workingDirectory: workDir.path,
);
final proc = await Process.start('git', args, workingDirectory: workDir.path);
proc.stdin.write(patch);
await proc.stdin.close();
final exitCode = await proc.exitCode;
if (exitCode != 0) {
final stderr = await proc.stderr.transform(const SystemEncoding().decoder).join();
throw GitException(
'git apply failed',
stderr: stderr,
);
throw GitException('git apply failed', stderr: stderr);
}
}
+38 -84
View File
@@ -9,34 +9,12 @@ import 'dart:io';
import 'operations.dart' show gitBin;
enum GitFileState {
added,
modified,
deleted,
renamed,
copied,
untracked,
ignored,
}
enum GitFileState { added, modified, deleted, renamed, copied, untracked, ignored }
enum GitConflictType {
bothModified,
bothAdded,
addedByUs,
addedByThem,
deletedByUs,
deletedByThem,
bothDeleted,
}
enum GitConflictType { bothModified, bothAdded, addedByUs, addedByThem, deletedByUs, deletedByThem, bothDeleted }
class GitFileStatus {
const GitFileStatus({
required this.path,
required this.indexState,
required this.workTreeState,
this.origPath,
this.conflictType,
});
const GitFileStatus({required this.path, required this.indexState, required this.workTreeState, this.origPath, this.conflictType});
final String path;
final GitFileState? indexState;
@@ -50,26 +28,20 @@ class GitFileStatus {
bool get isConflicted => conflictType != null;
Map<String, Object?> toJson() => {
'path': path,
if (indexState != null) 'indexState': indexState!.name,
if (workTreeState != null) 'workTreeState': workTreeState!.name,
if (origPath != null) 'origPath': origPath,
if (conflictType != null) 'conflictType': conflictType!.name,
'staged': isStaged,
'unstaged': isUnstaged,
'untracked': isUntracked,
'conflicted': isConflicted,
};
'path': path,
if (indexState != null) 'indexState': indexState!.name,
if (workTreeState != null) 'workTreeState': workTreeState!.name,
if (origPath != null) 'origPath': origPath,
if (conflictType != null) 'conflictType': conflictType!.name,
'staged': isStaged,
'unstaged': isUnstaged,
'untracked': isUntracked,
'conflicted': isConflicted,
};
}
class GitStatus {
const GitStatus({
required this.branch,
required this.entries,
this.upstream,
this.ahead = 0,
this.behind = 0,
});
const GitStatus({required this.branch, required this.entries, this.upstream, this.ahead = 0, this.behind = 0});
final String? branch;
final String? upstream;
@@ -86,28 +58,24 @@ class GitStatus {
bool get hasConflicts => entries.any((e) => e.isConflicted);
Map<String, Object?> toJson() => {
'branch': branch,
if (upstream != null) 'upstream': upstream,
'ahead': ahead,
'behind': behind,
'clean': isClean,
'hasConflicts': hasConflicts,
'staged': [for (final e in staged) e.toJson()],
'unstaged': [for (final e in unstaged) e.toJson()],
'untracked': [for (final e in untracked) e.toJson()],
'conflicted': [for (final e in conflicted) e.toJson()],
};
'branch': branch,
if (upstream != null) 'upstream': upstream,
'ahead': ahead,
'behind': behind,
'clean': isClean,
'hasConflicts': hasConflicts,
'staged': [for (final e in staged) e.toJson()],
'unstaged': [for (final e in unstaged) e.toJson()],
'untracked': [for (final e in untracked) e.toJson()],
'conflicted': [for (final e in conflicted) e.toJson()],
};
}
/// Run `git status` and parse the result.
Future<GitStatus> gitStatus(Directory workDir) async {
final ProcessResult branchResult;
try {
branchResult = await Process.run(
gitBin,
['status', '--porcelain=v2', '--branch', '-z'],
workingDirectory: workDir.path,
);
branchResult = await Process.run(gitBin, ['status', '--porcelain=v2', '--branch', '-z'], workingDirectory: workDir.path);
} on ProcessException {
return const GitStatus(branch: null, entries: []);
}
@@ -136,33 +104,17 @@ Future<GitStatus> gitStatus(Directory workDir) async {
final ProcessResult result;
try {
result = await Process.run(
gitBin,
['status', '--porcelain=v1', '-z'],
workingDirectory: workDir.path,
);
result = await Process.run(gitBin, ['status', '--porcelain=v1', '-z'], workingDirectory: workDir.path);
} on ProcessException {
return GitStatus(branch: branch, entries: const [], upstream: upstream, ahead: ahead, behind: behind);
}
if (result.exitCode != 0) {
return GitStatus(
branch: branch,
upstream: upstream,
ahead: ahead,
behind: behind,
entries: const [],
);
return GitStatus(branch: branch, upstream: upstream, ahead: ahead, behind: behind, entries: const []);
}
final entries = parsePorcelainV1(result.stdout as String);
return GitStatus(
branch: branch,
upstream: upstream,
ahead: ahead,
behind: behind,
entries: entries,
);
return GitStatus(branch: branch, upstream: upstream, ahead: ahead, behind: behind, entries: entries);
}
List<GitFileStatus> parsePorcelainV1(String output) {
@@ -193,13 +145,15 @@ List<GitFileStatus> parsePorcelainV1(String output) {
}
final conflict = _conflictType(x, y);
entries.add(GitFileStatus(
path: path,
indexState: conflict != null ? null : _parseState(x),
workTreeState: conflict != null ? null : _parseState(y),
origPath: origPath,
conflictType: conflict,
));
entries.add(
GitFileStatus(
path: path,
indexState: conflict != null ? null : _parseState(x),
workTreeState: conflict != null ? null : _parseState(y),
origPath: origPath,
conflictType: conflict,
),
);
i++;
}
return entries;
+3 -7
View File
@@ -211,10 +211,6 @@ class SchemaResult {
/// Build the standard `userError` response for a schema violation.
IpcResponse schemaError(String id, String message) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message),
);
+31 -72
View File
@@ -27,47 +27,25 @@ sealed class IpcMessage {
}
class IpcRequest extends IpcMessage {
IpcRequest({
required this.id,
required this.cmd,
this.args = const {},
});
IpcRequest({required this.id, required this.cmd, this.args = const {}});
final String id;
final String cmd;
final Map<String, Object?> args;
@override
Map<String, Object?> toJson() => {
'type': 'request',
'v': ipcSchemaVersion,
'id': id,
'cmd': cmd,
'args': args,
};
Map<String, Object?> toJson() => {'type': 'request', 'v': ipcSchemaVersion, 'id': id, 'cmd': cmd, 'args': args};
factory IpcRequest.fromJson(Map<String, Object?> j) => IpcRequest(
id: j['id']! as String,
cmd: j['cmd']! as String,
args: (j['args'] as Map?)?.cast<String, Object?>() ?? const {},
);
factory IpcRequest.fromJson(Map<String, Object?> j) =>
IpcRequest(id: j['id']! as String, cmd: j['cmd']! as String, args: (j['args'] as Map?)?.cast<String, Object?>() ?? const {});
}
class IpcResponse extends IpcMessage {
IpcResponse.ok({required this.id, this.data = const {}})
: ok = true,
error = null;
IpcResponse.ok({required this.id, this.data = const {}}) : ok = true, error = null;
IpcResponse.err({required this.id, required IpcError this.error})
: ok = false,
data = const {};
IpcResponse.err({required this.id, required IpcError this.error}) : ok = false, data = const {};
IpcResponse._({
required this.id,
required this.ok,
required this.data,
required this.error,
});
IpcResponse._({required this.id, required this.ok, required this.data, required this.error});
final String id;
final bool ok;
@@ -76,13 +54,13 @@ class IpcResponse extends IpcMessage {
@override
Map<String, Object?> toJson() => {
'type': 'response',
'v': ipcSchemaVersion,
'id': id,
'ok': ok,
if (ok) 'data': data,
if (!ok && error != null) 'error': error!.toJson(),
};
'type': 'response',
'v': ipcSchemaVersion,
'id': id,
'ok': ok,
if (ok) 'data': data,
if (!ok && error != null) 'error': error!.toJson(),
};
factory IpcResponse.fromJson(Map<String, Object?> j) {
final ok = j['ok'] as bool? ?? false;
@@ -96,40 +74,21 @@ class IpcResponse extends IpcMessage {
}
class IpcError {
IpcError({
required this.code,
required this.kind,
required this.message,
this.hint,
});
IpcError({required this.code, required this.kind, required this.message, this.hint});
final int code;
final String kind;
final String message;
final String? hint;
Map<String, Object?> toJson() => {
'code': code,
'kind': kind,
'message': message,
if (hint != null) 'hint': hint,
};
Map<String, Object?> toJson() => {'code': code, 'kind': kind, 'message': message, if (hint != null) 'hint': hint};
factory IpcError.fromJson(Map<String, Object?> j) => IpcError(
code: (j['code'] as num).toInt(),
kind: j['kind']! as String,
message: j['message']! as String,
hint: j['hint'] as String?,
);
factory IpcError.fromJson(Map<String, Object?> j) =>
IpcError(code: (j['code'] as num).toInt(), kind: j['kind']! as String, message: j['message']! as String, hint: j['hint'] as String?);
}
class IpcEvent extends IpcMessage {
IpcEvent({
required this.subsystem,
required this.kind,
required this.timestamp,
this.data = const {},
});
IpcEvent({required this.subsystem, required this.kind, required this.timestamp, this.data = const {}});
final String subsystem;
final String kind;
@@ -138,18 +97,18 @@ class IpcEvent extends IpcMessage {
@override
Map<String, Object?> toJson() => {
'type': 'event',
'v': ipcSchemaVersion,
'subsystem': subsystem,
'kind': kind,
'ts': timestamp.toIso8601String(),
'data': data,
};
'type': 'event',
'v': ipcSchemaVersion,
'subsystem': subsystem,
'kind': kind,
'ts': timestamp.toIso8601String(),
'data': data,
};
factory IpcEvent.fromJson(Map<String, Object?> j) => IpcEvent(
subsystem: j['subsystem']! as String,
kind: j['kind']! as String,
timestamp: DateTime.parse(j['ts']! as String),
data: (j['data'] as Map?)?.cast<String, Object?>() ?? const {},
);
subsystem: j['subsystem']! as String,
kind: j['kind']! as String,
timestamp: DateTime.parse(j['ts']! as String),
data: (j['data'] as Map?)?.cast<String, Object?>() ?? const {},
);
}
+8 -42
View File
@@ -33,20 +33,11 @@ abstract class PosixErrno {
/// (e.g. `pane.spawn`, `editor.open`) on optional [target] (a path,
/// command name, etc.). The returned error uses `notFound`,
/// `userError`, or `toolError` based on what's actionable.
IpcError errnoToIpcError({
required int errno,
required String op,
String? target,
String? raw,
}) {
IpcError errnoToIpcError({required int errno, required String op, String? target, String? raw}) {
final what = target != null ? ' ($target)' : '';
switch (errno) {
case PosixErrno.enoent:
return IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: '$op: not found$what',
);
return IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: '$op: not found$what');
case PosixErrno.eacces:
case PosixErrno.eperm:
return IpcError(
@@ -56,23 +47,11 @@ IpcError errnoToIpcError({
hint: 'check file permissions or run with appropriate access',
);
case PosixErrno.eisdir:
return IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '$op: is a directory$what',
);
return IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: '$op: is a directory$what');
case PosixErrno.enotdir:
return IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '$op: not a directory$what',
);
return IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: '$op: not a directory$what');
case PosixErrno.eexist:
return IpcError(
code: IpcExitCode.conflict,
kind: IpcErrorKind.conflict,
message: '$op: already exists$what',
);
return IpcError(code: IpcExitCode.conflict, kind: IpcErrorKind.conflict, message: '$op: already exists$what');
case PosixErrno.emfile:
case PosixErrno.enfile:
return IpcError(
@@ -82,23 +61,10 @@ IpcError errnoToIpcError({
hint: 'system or per-process file descriptor limit reached',
);
case PosixErrno.enomem:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op: out of memory',
);
return IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: '$op: out of memory');
case PosixErrno.eagain:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op: resource temporarily unavailable',
hint: 'retry may succeed',
);
return IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: '$op: resource temporarily unavailable', hint: 'retry may succeed');
default:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op failed${raw != null ? ': $raw' : ' (errno=$errno)'}',
);
return IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: '$op failed${raw != null ? ': $raw' : ' (errno=$errno)'}');
}
}
+8 -17
View File
@@ -63,14 +63,7 @@ class _McpSession {
/// HTTP + SSE MCP server. Lifecycle mirrors [IpcServer]: `start()`
/// binds + writes the discovery file; `stop()` unbinds + removes it.
class McpServer {
McpServer({
required this.workspaceRoot,
required this.log,
this.dispatcher,
this.discoveryDirOverride,
this.bindHost = '127.0.0.1',
this.bindPort = 0,
});
McpServer({required this.workspaceRoot, required this.log, this.dispatcher, this.discoveryDirOverride, this.bindHost = '127.0.0.1', this.bindPort = 0});
/// Workspace root reported in the discovery file. Helps Claude
/// Code show "which clide is this" when multiple are running.
@@ -109,9 +102,12 @@ class McpServer {
_http = server;
_port = server.port;
_lockFile = await _writeDiscoveryFile();
server.listen(_route, onError: (Object e, StackTrace st) {
log.warn('mcp', 'http error: $e');
});
server.listen(
_route,
onError: (Object e, StackTrace st) {
log.warn('mcp', 'http error: $e');
},
);
log.info('mcp', 'MCP/SSE listening at http://$bindHost:${server.port} (workspace: $workspaceRoot)');
}
@@ -331,12 +327,7 @@ class McpServer {
dirHandle.createSync(recursive: true);
}
final path = '$dir/$pid.lock';
final body = jsonEncode({
'pid': pid,
'workspace': workspaceRoot,
'transport': 'sse',
'url': 'http://$bindHost:$_port/sse',
});
final body = jsonEncode({'pid': pid, 'workspace': workspaceRoot, 'transport': 'sse', 'url': 'http://$bindHost:$_port/sse'});
File(path).writeAsStringSync(body);
return path;
}
+18 -51
View File
@@ -23,14 +23,7 @@ import 'package:clide/src/ipc/schema_v1.dart';
/// D-72. Per-handler isolate offload is the dispatcher / handler's
/// concern, not this layer's.
class IpcServer {
IpcServer({
required this.dispatcher,
required this.workspaceRoot,
required this.log,
this.events,
this.replayDepth = 16,
this.eventLogDepth = 1024,
});
IpcServer({required this.dispatcher, required this.workspaceRoot, required this.log, this.events, this.replayDepth = 16, this.eventLogDepth = 1024});
final DaemonDispatcher dispatcher;
final String workspaceRoot;
@@ -94,10 +87,7 @@ class IpcServer {
final path = workspaceSocketPath(workspaceRoot);
await _prepareParentDir(path);
await _unlinkStale(path);
final socket = await ServerSocket.bind(
InternetAddress(path, type: InternetAddressType.unix),
0,
);
final socket = await ServerSocket.bind(InternetAddress(path, type: InternetAddressType.unix), 0);
try {
await _chmod(path, 0x180); // 0o600
} catch (e, st) {
@@ -108,9 +98,12 @@ class IpcServer {
}
_socket = socket;
_socketPath = path;
_accepts = socket.listen(_onClient, onError: (Object e, StackTrace st) {
log.error('ipc', 'accept loop error', error: e, stackTrace: st);
});
_accepts = socket.listen(
_onClient,
onError: (Object e, StackTrace st) {
log.error('ipc', 'accept loop error', error: e, stackTrace: st);
},
);
// Subscribe to the bus so we can populate the replay ring AND
// fan out to live `tail --events` subscribers. Idempotent —
// we only attach when a bus is supplied.
@@ -198,11 +191,7 @@ class IpcServer {
if (msg is! IpcRequest) {
response = IpcResponse.err(
id: '',
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'expected request, got ${msg.runtimeType}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'expected request, got ${msg.runtimeType}'),
);
} else {
// Peel off the `_argv` envelope at the server layer so the
@@ -238,21 +227,13 @@ class IpcServer {
} on FormatException catch (e) {
response = IpcResponse.err(
id: '',
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'malformed request: ${e.message}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'malformed request: ${e.message}'),
);
} catch (e, st) {
log.error('ipc', 'dispatch threw for "$reqCmd"', error: e, stackTrace: st);
response = IpcResponse.err(
id: '',
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'internal error: $e',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'internal error: $e'),
);
}
try {
@@ -280,10 +261,7 @@ class IpcServer {
if (!f.existsSync()) return;
// Probe: try connecting. If something answers, refuse to bind.
try {
final test = await Socket.connect(
InternetAddress(path, type: InternetAddressType.unix),
0,
).timeout(const Duration(milliseconds: 200));
final test = await Socket.connect(InternetAddress(path, type: InternetAddressType.unix), 0).timeout(const Duration(milliseconds: 200));
await test.close();
throw StateError('another clide IPC server is already listening on $path');
} on SocketException {
@@ -355,11 +333,7 @@ class IpcServer {
if (since == null) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '--since must be a non-negative integer cursor',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: '--since must be a non-negative integer cursor'),
);
}
final out = <Map<String, Object?>>[];
@@ -371,12 +345,10 @@ class IpcServer {
// A gap only means something when the caller had a prior position
// (since > 0); a first read (since 0) just gets whatever's retained.
final gap = since > 0 && since < _droppedThrough;
return IpcResponse.ok(id: req.id, data: {
'events': out,
'cursor': _lastCursor,
'gap': gap,
if (gap) 'oldestCursor': _eventLog.isEmpty ? _lastCursor : _eventLog.first.cursor,
});
return IpcResponse.ok(
id: req.id,
data: {'events': out, 'cursor': _lastCursor, 'gap': gap, if (gap) 'oldestCursor': _eventLog.isEmpty ? _lastCursor : _eventLog.first.cursor},
);
}
/// Parse the `--since` flag (a string from argv or an int from a typed
@@ -390,12 +362,7 @@ class IpcServer {
}
void _onBusEvent(DaemonEvent e) {
final ev = IpcEvent(
subsystem: e.subsystem,
kind: e.kind,
data: e.data,
timestamp: e.ts,
);
final ev = IpcEvent(subsystem: e.subsystem, kind: e.kind, data: e.data, timestamp: e.ts);
// Push to replay ring.
final ring = _replay.putIfAbsent(e.subsystem, () => Queue<IpcEvent>());
ring.addLast(ev);
+10 -21
View File
@@ -16,23 +16,12 @@ enum PaneKind {
String get wire => name;
static PaneKind parse(String s) {
return PaneKind.values.firstWhere(
(v) => v.wire == s,
orElse: () => throw ArgumentError.value(s, 'kind', 'unknown pane kind'),
);
return PaneKind.values.firstWhere((v) => v.wire == s, orElse: () => throw ArgumentError.value(s, 'kind', 'unknown pane kind'));
}
}
class Pane {
Pane({
required this.id,
required this.kind,
required this.pid,
required this.argv,
this.cwd,
this.title,
this.isClosed = false,
});
Pane({required this.id, required this.kind, required this.pid, required this.argv, this.cwd, this.title, this.isClosed = false});
final String id;
final PaneKind kind;
@@ -47,12 +36,12 @@ class Pane {
bool isClosed;
Map<String, Object?> toJson() => {
'id': id,
'kind': kind.wire,
'pid': pid,
'argv': argv,
if (cwd != null) 'cwd': cwd,
if (title != null) 'title': title,
'closed': isClosed,
};
'id': id,
'kind': kind.wire,
'pid': pid,
'argv': argv,
if (cwd != null) 'cwd': cwd,
if (title != null) 'title': title,
'closed': isClosed,
};
}
+5 -29
View File
@@ -52,36 +52,17 @@ class PaneRegistry {
'COLORTERM': 'truecolor',
'LANG': 'en_US.UTF-8',
'LC_ALL': 'en_US.UTF-8',
if (env != null) ...env,
...?env,
};
final session = NativePty.start(
executable: executable,
arguments: arguments,
columns: cols,
rows: rows,
workingDirectory: cwd,
environment: fullEnv,
);
final pane = Pane(
id: id,
kind: kind,
pid: session.pid,
argv: argv,
cwd: cwd,
title: title,
);
final session = NativePty.start(executable: executable, arguments: arguments, columns: cols, rows: rows, workingDirectory: cwd, environment: fullEnv);
final pane = Pane(id: id, kind: kind, pid: session.pid, argv: argv, cwd: cwd, title: title);
_panes[id] = pane;
_sessions[id] = session;
_emit('pane.spawned', id, pane.toJson());
_subs[id] = session.output.listen(
(bytes) => _emit('pane.output', id, {
'bytes_b64': base64Encode(bytes),
}),
onDone: () => _onExit(pane),
);
_subs[id] = session.output.listen((bytes) => _emit('pane.output', id, {'bytes_b64': base64Encode(bytes)}), onDone: () => _onExit(pane));
return pane;
}
@@ -136,11 +117,6 @@ class PaneRegistry {
}
void _emit(String kind, String id, Map<String, Object?> data) {
events.emit(IpcEvent(
subsystem: 'pane',
kind: kind,
timestamp: DateTime.now().toUtc(),
data: {'id': id, ...data},
));
events.emit(IpcEvent(subsystem: 'pane', kind: kind, timestamp: DateTime.now().toUtc(), data: {'id': id, ...data}));
}
}
+2 -16
View File
@@ -11,13 +11,7 @@
library;
class ViewPane {
const ViewPane({
required this.id,
required this.slot,
required this.title,
required this.active,
required this.visible,
});
const ViewPane({required this.id, required this.slot, required this.title, required this.active, required this.visible});
/// Stable contribution id (e.g. `claude`, `files`, `editor`) — the same id
/// `pane.focus` would target.
@@ -36,13 +30,5 @@ class ViewPane {
/// Whether the tab's slot is currently visible (not collapsed/hidden).
final bool visible;
Map<String, Object?> toJson() => {
'id': id,
'kind': 'view',
'slot': slot,
'title': title,
'active': active,
'visible': visible,
'source': 'ui',
};
Map<String, Object?> toJson() => {'id': id, 'kind': 'view', 'slot': slot, 'title': title, 'active': active, 'visible': visible, 'source': 'ui'};
}
+7 -36
View File
@@ -79,11 +79,7 @@ class PqlClient {
return _runObject(['decisions', 'validate']);
}
Future<List<Map<String, Object?>>> decisionList({
String? type,
String? domain,
String? status,
}) async {
Future<List<Map<String, Object?>>> decisionList({String? type, String? domain, String? status}) async {
final args = ['decisions', 'list'];
if (type != null) args.addAll(['--type', type]);
if (domain != null) args.addAll(['--domain', domain]);
@@ -91,11 +87,7 @@ class PqlClient {
return _runList(args);
}
Future<Map<String, Object?>> decisionShow(
String id, {
bool withRefs = false,
bool withTickets = false,
}) async {
Future<Map<String, Object?>> decisionShow(String id, {bool withRefs = false, bool withTickets = false}) async {
final args = ['decisions', 'show', id];
if (withRefs) args.add('--with-refs');
if (withTickets) args.add('--with-tickets');
@@ -106,12 +98,7 @@ class PqlClient {
return _runObject(['decisions', 'read', id]);
}
Future<List<Map<String, Object?>>> ticketList({
String? status,
String? team,
String? assigned,
String? decision,
}) async {
Future<List<Map<String, Object?>>> ticketList({String? status, String? team, String? assigned, String? decision}) async {
final args = ['ticket', 'list'];
if (status != null) args.addAll(['--status', status]);
if (team != null) args.addAll(['--team', team]);
@@ -120,11 +107,7 @@ class PqlClient {
return _runList(args);
}
Future<Map<String, Object?>> ticketShow(
String id, {
bool withContext = false,
bool withBlockers = false,
}) async {
Future<Map<String, Object?>> ticketShow(String id, {bool withContext = false, bool withBlockers = false}) async {
final args = ['ticket', 'show', id];
if (withContext) args.add('--with-context');
if (withBlockers) args.add('--with-blockers');
@@ -171,17 +154,9 @@ class PqlClient {
for (var attempt = 1; attempt <= _kMaxAttempts; attempt++) {
final ProcessResult r;
try {
r = await Process.run(
toolchain.pql,
args,
workingDirectory: workDir.path,
);
r = await Process.run(toolchain.pql, args, workingDirectory: workDir.path);
} on ProcessException catch (e) {
throw PqlException(
'pql ${args.first}: ${e.message}',
exitCode: e.errorCode,
stderr: e.toString(),
);
throw PqlException('pql ${args.first}: ${e.message}', exitCode: e.errorCode, stderr: e.toString());
}
final stderr = (r.stderr as String).trim();
// pql 1.5+ returns exit 0 with an empty `[]` for zero matches, so any
@@ -196,11 +171,7 @@ class PqlClient {
await Future<void>.delayed(Duration(milliseconds: 100 * attempt));
continue;
}
throw PqlException(
'pql ${args.first} failed',
exitCode: r.exitCode,
stderr: stderr,
);
throw PqlException('pql ${args.first} failed', exitCode: r.exitCode, stderr: stderr);
}
final stdout = (r.stdout as String).trim();
if (stdout.isEmpty) return null;
+3 -15
View File
@@ -25,12 +25,7 @@ String _buildExpandedPath() {
final base = Platform.environment['PATH'] ?? '';
if (!Platform.isMacOS) return base;
final home = Platform.environment['HOME'] ?? '';
final extras = <String>[
if (home.isNotEmpty) '$home/.local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
];
final extras = <String>[if (home.isNotEmpty) '$home/.local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin'];
final existing = base.split(':').toSet();
final missing = extras.where((p) => !existing.contains(p));
if (missing.isEmpty) return base;
@@ -53,13 +48,6 @@ const Map<String, String> clidePtyEnvDefaults = {
/// Merge [base] onto the process environment; clide defaults override
/// user env where they overlap. Explicit [overrides] win over both.
Map<String, String> mergePtyEnv({
required Map<String, String> processEnv,
Map<String, String>? overrides,
}) {
return {
...processEnv,
...clidePtyEnvDefaults,
if (overrides != null) ...overrides,
};
Map<String, String> mergePtyEnv({required Map<String, String> processEnv, Map<String, String>? overrides}) {
return {...processEnv, ...clidePtyEnvDefaults, ...?overrides};
}
+15 -73
View File
@@ -64,82 +64,28 @@ const int sigwinch = 28;
// Typedefs
// ---------------------------------------------------------------------------
typedef _SocketpairC = ffi.Int32 Function(
ffi.Int32 domain,
ffi.Int32 type,
ffi.Int32 protocol,
ffi.Pointer<ffi.Int32> sv,
);
typedef _SocketpairD = int Function(
int domain,
int type,
int protocol,
ffi.Pointer<ffi.Int32> sv,
);
typedef _SocketpairC = ffi.Int32 Function(ffi.Int32 domain, ffi.Int32 type, ffi.Int32 protocol, ffi.Pointer<ffi.Int32> sv);
typedef _SocketpairD = int Function(int domain, int type, int protocol, ffi.Pointer<ffi.Int32> sv);
typedef _RecvmsgC = ffi.IntPtr Function(
ffi.Int32 sockfd,
ffi.Pointer<Msghdr> msg,
ffi.Int32 flags,
);
typedef _RecvmsgD = int Function(
int sockfd,
ffi.Pointer<Msghdr> msg,
int flags,
);
typedef _RecvmsgC = ffi.IntPtr Function(ffi.Int32 sockfd, ffi.Pointer<Msghdr> msg, ffi.Int32 flags);
typedef _RecvmsgD = int Function(int sockfd, ffi.Pointer<Msghdr> msg, int flags);
typedef _RecvmsgDarwinC = ffi.IntPtr Function(
ffi.Int32 sockfd,
ffi.Pointer<MsghdrDarwin> msg,
ffi.Int32 flags,
);
typedef _RecvmsgDarwinD = int Function(
int sockfd,
ffi.Pointer<MsghdrDarwin> msg,
int flags,
);
typedef _RecvmsgDarwinC = ffi.IntPtr Function(ffi.Int32 sockfd, ffi.Pointer<MsghdrDarwin> msg, ffi.Int32 flags);
typedef _RecvmsgDarwinD = int Function(int sockfd, ffi.Pointer<MsghdrDarwin> msg, int flags);
typedef _ReadC = ffi.IntPtr Function(
ffi.Int32 fd,
ffi.Pointer<ffi.Uint8> buf,
ffi.IntPtr count,
);
typedef _ReadD = int Function(
int fd,
ffi.Pointer<ffi.Uint8> buf,
int count,
);
typedef _ReadC = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Uint8> buf, ffi.IntPtr count);
typedef _ReadD = int Function(int fd, ffi.Pointer<ffi.Uint8> buf, int count);
typedef _WriteC = ffi.IntPtr Function(
ffi.Int32 fd,
ffi.Pointer<ffi.Uint8> buf,
ffi.IntPtr count,
);
typedef _WriteD = int Function(
int fd,
ffi.Pointer<ffi.Uint8> buf,
int count,
);
typedef _WriteC = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Uint8> buf, ffi.IntPtr count);
typedef _WriteD = int Function(int fd, ffi.Pointer<ffi.Uint8> buf, int count);
typedef _CloseC = ffi.Int32 Function(ffi.Int32 fd);
typedef _CloseD = int Function(int fd);
typedef _IoctlPtrC = ffi.Int32 Function(
ffi.Int32 fd,
ffi.UnsignedLong request,
ffi.Pointer<Winsize> argp,
);
typedef _IoctlPtrD = int Function(
int fd,
int request,
ffi.Pointer<Winsize> argp,
);
typedef _IoctlPtrC = ffi.Int32 Function(ffi.Int32 fd, ffi.UnsignedLong request, ffi.Pointer<Winsize> argp);
typedef _IoctlPtrD = int Function(int fd, int request, ffi.Pointer<Winsize> argp);
typedef _FcntlIntC = ffi.Int32 Function(
ffi.Int32 fd,
ffi.Int32 cmd,
ffi.Int32 arg,
);
typedef _FcntlIntC = ffi.Int32 Function(ffi.Int32 fd, ffi.Int32 cmd, ffi.Int32 arg);
typedef _FcntlIntD = int Function(int fd, int cmd, int arg);
typedef _ErrnoLocationC = ffi.Pointer<ffi.Int32> Function();
@@ -257,16 +203,12 @@ final _FcntlIntD fcntlInt = _libc.lookupFunction<_FcntlIntC, _FcntlIntD>('fcntl'
/// uses `__error`.
int get errno {
try {
final fn = _libc.lookupFunction<_ErrnoLocationC, _ErrnoLocationD>(
'__errno_location',
);
final fn = _libc.lookupFunction<_ErrnoLocationC, _ErrnoLocationD>('__errno_location');
return fn().value;
} on ArgumentError {
// Fall through to macOS-style.
}
final fn = _libc.lookupFunction<_ErrnoLocationC, _ErrnoLocationD>(
'__error',
);
final fn = _libc.lookupFunction<_ErrnoLocationC, _ErrnoLocationD>('__error');
return fn().value;
}
+45 -28
View File
@@ -65,38 +65,62 @@ final _ptsname = _dl.lookupFunction<ffi.Pointer<Utf8> Function(ffi.Int32), ffi.P
// larger than any documented platform layout) and pass it as Pointer<Void>.
// init() writes the real layout into our memory; destroy() releases any
// internal nested allocations.
final _posixSpawn = _dl.lookupFunction<
ffi.Int32 Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<Utf8>, ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Pointer<Utf8>>,
ffi.Pointer<ffi.Pointer<Utf8>>),
int Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<Utf8>, ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Pointer<Utf8>>,
ffi.Pointer<ffi.Pointer<Utf8>>)>('posix_spawn');
final _posixSpawn = _dl
.lookupFunction<
ffi.Int32 Function(
ffi.Pointer<ffi.Int32>,
ffi.Pointer<Utf8>,
ffi.Pointer<ffi.Void>,
ffi.Pointer<ffi.Void>,
ffi.Pointer<ffi.Pointer<Utf8>>,
ffi.Pointer<ffi.Pointer<Utf8>>,
),
int Function(
ffi.Pointer<ffi.Int32>,
ffi.Pointer<Utf8>,
ffi.Pointer<ffi.Void>,
ffi.Pointer<ffi.Void>,
ffi.Pointer<ffi.Pointer<Utf8>>,
ffi.Pointer<ffi.Pointer<Utf8>>,
)
>('posix_spawn');
final _spawnattrInit = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>), int Function(ffi.Pointer<ffi.Void>)>('posix_spawnattr_init');
final _spawnattrDestroy = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>), int Function(ffi.Pointer<ffi.Void>)>('posix_spawnattr_destroy');
final _spawnattrSetflags =
_dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int16), int Function(ffi.Pointer<ffi.Void>, int)>('posix_spawnattr_setflags');
final _spawnattrSetflags = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int16), int Function(ffi.Pointer<ffi.Void>, int)>(
'posix_spawnattr_setflags',
);
final _faInit = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>), int Function(ffi.Pointer<ffi.Void>)>('posix_spawn_file_actions_init');
final _faDestroy = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>), int Function(ffi.Pointer<ffi.Void>)>('posix_spawn_file_actions_destroy');
final _faAddopen = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int32, ffi.Pointer<Utf8>, ffi.Int32, ffi.Uint32),
int Function(ffi.Pointer<ffi.Void>, int, ffi.Pointer<Utf8>, int, int)>('posix_spawn_file_actions_addopen');
final _faAddopen = _dl
.lookupFunction<
ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int32, ffi.Pointer<Utf8>, ffi.Int32, ffi.Uint32),
int Function(ffi.Pointer<ffi.Void>, int, ffi.Pointer<Utf8>, int, int)
>('posix_spawn_file_actions_addopen');
final _faAdddup2 = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int32, ffi.Int32), int Function(ffi.Pointer<ffi.Void>, int, int)>(
'posix_spawn_file_actions_adddup2');
final _faAddclose =
_dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int32), int Function(ffi.Pointer<ffi.Void>, int)>('posix_spawn_file_actions_addclose');
'posix_spawn_file_actions_adddup2',
);
final _faAddclose = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Int32), int Function(ffi.Pointer<ffi.Void>, int)>(
'posix_spawn_file_actions_addclose',
);
// glibc 2.29+ / macOS 10.15+. Both ship the `_np` suffix.
final _faAddchdir = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.Pointer<Utf8>), int Function(ffi.Pointer<ffi.Void>, ffi.Pointer<Utf8>)>(
'posix_spawn_file_actions_addchdir_np');
'posix_spawn_file_actions_addchdir_np',
);
// libc primitives shared with the reader isolate / lifecycle.
final _nativeWrite =
_dl.lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), int Function(int, ffi.Pointer<ffi.Void>, int)>('write');
final _nativeWrite = _dl.lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), int Function(int, ffi.Pointer<ffi.Void>, int)>(
'write',
);
final _nativeClose = _dl.lookupFunction<ffi.Int32 Function(ffi.Int32), int Function(int)>('close');
final _ioctl =
_dl.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl');
final _ioctl = _dl.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), int Function(int, int, ffi.Pointer<_Winsize>)>(
'ioctl',
);
final _nativeKill = _dl.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill');
final _waitpid =
_dl.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32), int Function(int, ffi.Pointer<ffi.Int32>, int)>('waitpid');
final _waitpid = _dl.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32), int Function(int, ffi.Pointer<ffi.Int32>, int)>(
'waitpid',
);
// Constants — all duplicated from <fcntl.h>, <sys/ioctl.h>, <spawn.h>.
final int _kTiocsWinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
@@ -378,11 +402,7 @@ class NativePty {
}
var written = 0;
while (written < bytes.length) {
final n = _nativeWrite(
_fd,
(buf + written).cast(),
bytes.length - written,
);
final n = _nativeWrite(_fd, (buf + written).cast(), bytes.length - written);
if (n < 0) {
final err = libc.errno;
if (err == PosixErrno.eintr) continue;
@@ -451,10 +471,7 @@ class NativePty {
// Wait for the isolate to send `null` (EOF) — confirms it has
// exited its poll loop and won't touch the fd again.
if (_readerExited != null) {
await _readerExited!.future.timeout(
const Duration(milliseconds: 500),
onTimeout: () {},
);
await _readerExited!.future.timeout(const Duration(milliseconds: 500), onTimeout: () {});
}
_nativeClose(_fd);
+8 -33
View File
@@ -69,9 +69,7 @@ Stream<List<SearchMatch>> grepWorkspace({
final rootPath = root.absolute.path;
// Launch every chunk concurrently; await in chunk order to stream.
final futures = <Future<List<SearchMatch>>>[
for (final c in chunks) _runChunk(rootPath, c, query, maxPerFile, useIsolates),
];
final futures = <Future<List<SearchMatch>>>[for (final c in chunks) _runChunk(rootPath, c, query, maxPerFile, useIsolates)];
var emitted = 0;
for (final f in futures) {
@@ -104,13 +102,7 @@ void _drain(List<Future<List<SearchMatch>>> futures) {
}
}
Future<List<SearchMatch>> _runChunk(
String rootPath,
List<String> paths,
SearchQuery query,
int maxPerFile,
bool useIsolates,
) {
Future<List<SearchMatch>> _runChunk(String rootPath, List<String> paths, SearchQuery query, int maxPerFile, bool useIsolates) {
if (useIsolates) {
return Isolate.run(() => grepChunk(rootPath, paths, query, maxPerFile));
}
@@ -133,12 +125,7 @@ List<List<String>> _chunk(List<String> items, int buckets) {
/// Grep a chunk of files. Runs in a worker isolate (or in-process for
/// tests). Reads each file, skips binaries, and collects up to
/// [maxPerFile] matches per file.
List<SearchMatch> grepChunk(
String rootPath,
List<String> relPaths,
SearchQuery query,
int maxPerFile,
) {
List<SearchMatch> grepChunk(String rootPath, List<String> relPaths, SearchQuery query, int maxPerFile) {
final compiled = CompiledQuery(query);
final out = <SearchMatch>[];
for (final rel in relPaths) {
@@ -160,25 +147,13 @@ List<SearchMatch> grepChunk(
/// Match [content]'s lines, appending up to [maxPerFile] hits to [out].
/// Exposed (with a pre-built [compiled]) for unit testing without I/O.
void grepContent(
String relPath,
String content,
CompiledQuery compiled,
int maxPerFile,
List<SearchMatch> out,
) {
void grepContent(String relPath, String content, CompiledQuery compiled, int maxPerFile, List<SearchMatch> out) {
var lineNo = 0;
final added0 = out.length;
for (final line in const LineSplitter().convert(content)) {
lineNo++;
for (final span in compiled.matches(line)) {
out.add(SearchMatch(
path: relPath,
line: lineNo,
matchStart: span.$1,
matchEnd: span.$2,
preview: line.length > 500 ? line.substring(0, 500) : line,
));
out.add(SearchMatch(path: relPath, line: lineNo, matchStart: span.$1, matchEnd: span.$2, preview: line.length > 500 ? line.substring(0, 500) : line));
if (out.length - added0 >= maxPerFile) return;
}
}
@@ -188,9 +163,9 @@ void grepContent(
/// matcher with an optional case-insensitive fast-path.
class CompiledQuery {
CompiledQuery(SearchQuery q)
: _regex = q.regex ? RegExp(q.pattern, caseSensitive: !q.ignoreCase) : null,
_needle = q.regex ? '' : (q.ignoreCase ? q.pattern.toLowerCase() : q.pattern),
_ignoreCase = q.ignoreCase;
: _regex = q.regex ? RegExp(q.pattern, caseSensitive: !q.ignoreCase) : null,
_needle = q.regex ? '' : (q.ignoreCase ? q.pattern.toLowerCase() : q.pattern),
_ignoreCase = q.ignoreCase;
final RegExp? _regex;
final String _needle;
+16 -40
View File
@@ -9,13 +9,7 @@ library;
/// result, navigate to the line, and (for replace, T-53) address the
/// matched span within the line.
class SearchMatch {
const SearchMatch({
required this.path,
required this.line,
required this.matchStart,
required this.matchEnd,
required this.preview,
});
const SearchMatch({required this.path, required this.line, required this.matchStart, required this.matchEnd, required this.preview});
/// Repo-relative, forward-slashed path of the file.
final String path;
@@ -32,32 +26,20 @@ class SearchMatch {
/// The matched line's text (capped for transport/render).
final String preview;
Map<String, Object?> toJson() => {
'path': path,
'line': line,
'matchStart': matchStart,
'matchEnd': matchEnd,
'preview': preview,
};
Map<String, Object?> toJson() => {'path': path, 'line': line, 'matchStart': matchStart, 'matchEnd': matchEnd, 'preview': preview};
factory SearchMatch.fromJson(Map<String, Object?> j) => SearchMatch(
path: j['path'] as String,
line: (j['line'] as num).toInt(),
matchStart: (j['matchStart'] as num).toInt(),
matchEnd: (j['matchEnd'] as num).toInt(),
preview: j['preview'] as String,
);
path: j['path'] as String,
line: (j['line'] as num).toInt(),
matchStart: (j['matchStart'] as num).toInt(),
matchEnd: (j['matchEnd'] as num).toInt(),
preview: j['preview'] as String,
);
}
/// Parameters for a workspace search.
class SearchQuery {
const SearchQuery({
required this.pattern,
this.regex = false,
this.ignoreCase = false,
this.include = const [],
this.exclude = const [],
});
const SearchQuery({required this.pattern, this.regex = false, this.ignoreCase = false, this.include = const [], this.exclude = const []});
/// The literal text (when [regex] is false) or regular expression
/// source (when true) to search for.
@@ -71,21 +53,15 @@ class SearchQuery {
/// Glob patterns; matching paths are skipped (applied after [include]).
final List<String> exclude;
Map<String, Object?> toJson() => {
'pattern': pattern,
'regex': regex,
'ignoreCase': ignoreCase,
'include': include,
'exclude': exclude,
};
Map<String, Object?> toJson() => {'pattern': pattern, 'regex': regex, 'ignoreCase': ignoreCase, 'include': include, 'exclude': exclude};
factory SearchQuery.fromJson(Map<String, Object?> j) => SearchQuery(
pattern: (j['pattern'] as String?) ?? '',
regex: j['regex'] == true,
ignoreCase: j['ignoreCase'] == true,
include: _strings(j['include']),
exclude: _strings(j['exclude']),
);
pattern: (j['pattern'] as String?) ?? '',
regex: j['regex'] == true,
ignoreCase: j['ignoreCase'] == true,
include: _strings(j['include']),
exclude: _strings(j['exclude']),
);
static List<String> _strings(Object? v) => v is List ? [for (final e in v) '$e'] : const [];
}
+10 -15
View File
@@ -28,11 +28,8 @@ class ReplacementEdit {
Map<String, Object?> toJson() => {'line': line, 'before': before, 'after': after};
factory ReplacementEdit.fromJson(Map<String, Object?> j) => ReplacementEdit(
line: (j['line'] as num).toInt(),
before: j['before'] as String,
after: j['after'] as String,
);
factory ReplacementEdit.fromJson(Map<String, Object?> j) =>
ReplacementEdit(line: (j['line'] as num).toInt(), before: j['before'] as String, after: j['after'] as String);
}
/// The set of edits a replacement would make to one file.
@@ -46,18 +43,16 @@ class FileReplacement {
final List<ReplacementEdit> edits;
Map<String, Object?> toJson() => {
'path': path,
'count': count,
'edits': [for (final e in edits) e.toJson()],
};
'path': path,
'count': count,
'edits': [for (final e in edits) e.toJson()],
};
factory FileReplacement.fromJson(Map<String, Object?> j) => FileReplacement(
path: j['path'] as String,
count: (j['count'] as num).toInt(),
edits: [
for (final e in (j['edits'] as List? ?? const []).whereType<Map>()) ReplacementEdit.fromJson(e.cast<String, Object?>()),
],
);
path: j['path'] as String,
count: (j['count'] as num).toInt(),
edits: [for (final e in (j['edits'] as List? ?? const []).whereType<Map>()) ReplacementEdit.fromJson(e.cast<String, Object?>())],
);
}
/// Apply [query]'s pattern to [text], substituting [replacement]. Returns
+3 -14
View File
@@ -24,12 +24,7 @@ class Buffer {
/// defaults to [defaultWordSeparators].
final Set<int>? wordSeparators;
Buffer(
this.terminal, {
required this.maxLines,
required this.isAltBuffer,
this.wordSeparators,
}) {
Buffer(this.terminal, {required this.maxLines, required this.isAltBuffer, this.wordSeparators}) {
for (int i = 0; i < terminal.viewHeight; i++) {
lines.push(_newEmptyLine());
}
@@ -549,19 +544,13 @@ class Buffer {
return null;
}
return BufferRangeLine(
CellOffset(start, position.y),
CellOffset(end, position.y),
);
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 ??= BufferRangeLine(CellOffset(0, 0), CellOffset(viewWidth - 1, height - 1));
range = range.normalized;
+2 -7
View File
@@ -20,10 +20,7 @@ const _cellAttributes = 2;
const _cellContent = 3;
class BufferLine with IndexedItem {
BufferLine(
this._length, {
this.isWrapped = false,
}) : _data = Uint32List(_calcCapacity(_length) * _cellSize);
BufferLine(this._length, {this.isWrapped = false}) : _data = Uint32List(_calcCapacity(_length) * _cellSize);
int _length;
@@ -373,9 +370,7 @@ class BufferLine with IndexedItem {
/// 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;
CellAnchor(int offset, {BufferLine? owner}) : _offset = offset, _owner = owner;
int _offset;
@@ -81,14 +81,8 @@ class BufferRangeBlock extends BufferRange {
// 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),
);
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);
}
+2 -12
View File
@@ -3,20 +3,10 @@
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,
});
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,
);
return CellData(foreground: 0, background: 0, flags: 0, content: 0);
}
int foreground;
+1 -4
View File
@@ -2,10 +2,7 @@
typedef CharsetTranslator = int Function(int);
final _charsets = <int, CharsetTranslator>{
'0'.codeUnitAt(0): decSpecGraphicsTranslator,
'B'.codeUnitAt(0): asciiTranslator,
};
final _charsets = <int, CharsetTranslator>{'0'.codeUnitAt(0): decSpecGraphicsTranslator, 'B'.codeUnitAt(0): asciiTranslator};
class Charset {
var _charsetMap = <int, CharsetTranslator>{};
+1 -5
View File
@@ -95,11 +95,7 @@ class CascadeInputHandler implements TerminalInputHandler {
///
/// See also:
/// * [CascadeInputHandler]
const defaultInputHandler = CascadeInputHandler([
KeytabInputHandler(),
CtrlInputHandler(),
AltInputHandler(),
]);
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.
@@ -831,7 +831,6 @@ enum TerminalKey {
control,
// Missing flutter keys.
backtab,
returnKey,
}
@@ -7,10 +7,7 @@ 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,
});
Keytab({required this.name, required this.records});
factory Keytab.parse(String source) {
final tokens = tokenize(source).toList();
@@ -3,10 +3,7 @@
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,
}
enum KeytabActionType { input, shortcut }
class KeytabAction {
KeytabAction(this.type, this.value);
@@ -2,16 +2,7 @@
import 'dart:math' show min;
enum KeytabTokenType {
keyDefine,
keyboard,
keyName,
mode,
modeStatus,
colon,
input,
shortcut,
}
enum KeytabTokenType { keyDefine, keyboard, keyName, mode, modeStatus, colon, input, shortcut }
class KeytabToken {
KeytabToken(this.type, this.value);
@@ -15,7 +15,7 @@ const qtKeynameMap = <String, TerminalKey>{
'Delete': TerminalKey.delete,
'Pause': TerminalKey.pause,
'Print': TerminalKey.print,
// 'SysReq': TerminalKey.sysReq,
// 'SysReq': TerminalKey.sysReq,
'Clear': TerminalKey.numpadClear,
'Home': TerminalKey.home,
'End': TerminalKey.end,
@@ -31,7 +31,7 @@ const qtKeynameMap = <String, TerminalKey>{
'Control': TerminalKey.control,
'Meta': TerminalKey.meta,
'Alt': TerminalKey.alt,
// 'AltGr': TerminalKey.altGr,
// 'AltGr': TerminalKey.altGr,
'CapsLock': TerminalKey.capsLock,
'NumLock': TerminalKey.numLock,
'ScrollLock': TerminalKey.scrollLock,
@@ -59,25 +59,25 @@ const qtKeynameMap = <String, TerminalKey>{
'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,
// '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,
// 'Direction_L': TerminalKey.direction_L,
// 'Direction_R': TerminalKey.direction_R,
'Space': TerminalKey.space,
// 'Any': TerminalKey.any,
// 'Exclam': TerminalKey.exclam,
@@ -105,13 +105,13 @@ const qtKeynameMap = <String, TerminalKey>{
'7': TerminalKey.digit7,
'8': TerminalKey.digit8,
'9': TerminalKey.digit9,
// 'Colon': TerminalKey.colon,
// 'Colon': TerminalKey.colon,
'Semicolon': TerminalKey.semicolon,
// 'Less': TerminalKey.less,
// 'Equal': TerminalKey.equal,
// 'Greater': TerminalKey.greater,
// 'Question': TerminalKey.question,
// 'At': TerminalKey.at,
// 'Less': TerminalKey.less,
// 'Equal': TerminalKey.equal,
// 'Greater': TerminalKey.greater,
// 'Question': TerminalKey.question,
// 'At': TerminalKey.at,
'A': TerminalKey.keyA,
'B': TerminalKey.keyB,
'C': TerminalKey.keyC,
@@ -141,339 +141,339 @@ const qtKeynameMap = <String, TerminalKey>{
'BracketLeft': TerminalKey.bracketLeft,
'Backslash': TerminalKey.backslash,
'BracketRight': TerminalKey.bracketRight,
// 'AsciiCircum': TerminalKey.asciiCircum,
// 'AsciiCircum': TerminalKey.asciiCircum,
// 'Underscore': TerminalKey.underscore,
// 'QuoteLeft': TerminalKey.quoteLeft,
// 'BraceLeft': TerminalKey.braceLeft,
// 'BraceLeft': TerminalKey.braceLeft,
// 'Bar': TerminalKey.bar,
// 'BraceRight': TerminalKey.braceRight,
// '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,
// '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,
// 'BassUp': TerminalKey.bassUp,
// 'BassDown': TerminalKey.bassDown,
// 'TrebleUp': TerminalKey.trebleUp,
// 'TrebleDown': TerminalKey.trebleDown,
'MediaPlay': TerminalKey.mediaPlay,
'MediaStop': TerminalKey.mediaStop,
// 'MediaPrevious': TerminalKey.mediaPrevious,
// 'MediaNext': TerminalKey.mediaNext,
// '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,
// '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,
// '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,
// '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,
// '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,
// '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,
// '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,
// 'Guide': TerminalKey.guide,
'Info': TerminalKey.info,
// 'Settings': TerminalKey.settings,
// 'MicVolumeUp': TerminalKey.micVolumeUp,
// 'MicVolumeDown': TerminalKey.micVolumeDown,
// 'New': TerminalKey.new,
// '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,
// '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,
// '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,
// 'Zoom': TerminalKey.zoom,
'Exit': TerminalKey.exit,
// 'Cancel': TerminalKey.cancel,
// 'Cancel': TerminalKey.cancel,
};
+1 -2
View File
@@ -13,8 +13,7 @@ enum TerminalMouseButton {
wheelLeft(id: 64 + 6, isWheel: true),
wheelRight(id: 64 + 7, isWheel: true),
;
wheelRight(id: 64 + 7, isWheel: true);
/// The id that is used to report a button press or release to the terminal.
///
@@ -1,7 +1,3 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalMouseButtonState {
up,
down,
}
enum TerminalMouseButtonState { up, down }
+4 -23
View File
@@ -24,19 +24,10 @@ class TerminalMouseEvent {
/// The platform of the terminal.
final TerminalTargetPlatform platform;
TerminalMouseEvent({
required this.button,
required this.buttonState,
required this.position,
required this.state,
required this.platform,
});
TerminalMouseEvent({required this.button, required this.buttonState, required this.position, required this.state, required this.platform});
}
const defaultMouseHandler = CascadeMouseHandler([
ClickMouseHandler(),
UpDownMouseHandler(),
]);
const defaultMouseHandler = CascadeMouseHandler([ClickMouseHandler(), UpDownMouseHandler()]);
abstract class TerminalMouseHandler {
const TerminalMouseHandler();
@@ -70,12 +61,7 @@ class ClickMouseHandler implements TerminalMouseHandler {
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 MouseReporter.report(event.button, event.buttonState, event.position, event.state.mouseReportMode);
}
return null;
case MouseMode.none:
@@ -103,12 +89,7 @@ class UpDownMouseHandler implements TerminalMouseHandler {
if (event.button.isWheel && event.buttonState == TerminalMouseButtonState.up) {
return null;
}
return MouseReporter.report(
event.button,
event.buttonState,
event.position,
event.state.mouseReportMode,
);
return MouseReporter.report(event.button, event.buttonState, event.position, event.state.mouseReportMode);
}
}
}
+1 -2
View File
@@ -10,8 +10,7 @@ enum MouseMode {
upDownScrollDrag(reportScroll: true),
upDownScrollMove(reportScroll: true),
;
upDownScrollMove(reportScroll: true);
const MouseMode({this.reportScroll = false});
@@ -6,12 +6,7 @@ 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,
) {
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;
@@ -28,8 +23,9 @@ abstract class MouseReporter {
// 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);
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;
+1 -17
View File
@@ -1,19 +1,3 @@
// 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,
}
enum TerminalTargetPlatform { unknown, android, ios, fuchsia, linux, macos, windows, web }
+1 -5
View File
@@ -173,11 +173,7 @@ class _LineReflow {
}
}
List<BufferLine> reflow(
IndexAwareCircularBuffer<BufferLine> lines,
int oldWidth,
int newWidth,
) {
List<BufferLine> reflow(IndexAwareCircularBuffer<BufferLine> lines, int oldWidth, int newWidth) {
final result = <BufferLine>[];
for (var i = 0; i < lines.length; i++) {
+8 -50
View File
@@ -92,19 +92,9 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
late var _buffer = _mainBuffer;
late final _mainBuffer = Buffer(
this,
maxLines: maxLines,
isAltBuffer: false,
wordSeparators: wordSeparators,
);
late final _mainBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: false, wordSeparators: wordSeparators);
late final _altBuffer = Buffer(
this,
maxLines: maxLines,
isAltBuffer: true,
wordSeparators: wordSeparators,
);
late final _altBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: true, wordSeparators: wordSeparators);
final _tabStops = TabStops();
@@ -236,22 +226,9 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
/// - [charInput]
/// - [textInput]
/// - [paste]
bool keyInput(
TerminalKey key, {
bool shift = false,
bool alt = false,
bool ctrl = false,
}) {
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,
),
TerminalKeyboardEvent(key: key, shift: shift, alt: alt, ctrl: ctrl, state: this, altBuffer: isUsingAltBuffer, platform: platform),
);
if (output != null) {
@@ -269,11 +246,7 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
/// - [keyInput]
/// - [textInput]
/// - [paste]
bool charInput(
int charCode, {
bool alt = false,
bool ctrl = false,
}) {
bool charInput(int charCode, {bool alt = false, bool ctrl = false}) {
if (ctrl) {
// a(97) ~ z(122)
if (charCode >= Ascii.a && charCode <= Ascii.z) {
@@ -328,18 +301,8 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
}
// 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,
));
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;
@@ -351,12 +314,7 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
/// 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,
]) {
void resize(int newWidth, int newHeight, [int? pixelWidth, int? pixelHeight]) {
newWidth = max(newWidth, 1);
newHeight = max(newHeight, 1);
+5 -20
View File
@@ -181,9 +181,7 @@ class TerminalViewState extends State<TerminalView> {
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_controller = widget.controller ?? TerminalController();
_shortcutManager = ShortcutManager(
shortcuts: widget.shortcuts ?? defaultTerminalShortcuts,
);
_shortcutManager = ShortcutManager(shortcuts: widget.shortcuts ?? defaultTerminalShortcuts);
super.initState();
}
@@ -268,11 +266,7 @@ class TerminalViewState extends State<TerminalView> {
);
}
child = TerminalActions(
terminal: widget.terminal,
controller: _controller,
child: child,
);
child = TerminalActions(terminal: widget.terminal, controller: _controller, child: child);
child = TerminalGestureHandler(
terminalView: this,
@@ -285,10 +279,7 @@ class TerminalViewState extends State<TerminalView> {
child: child,
);
child = MouseRegion(
cursor: widget.mouseCursor,
child: child,
);
child = MouseRegion(cursor: widget.mouseCursor, child: child);
child = Container(
color: widget.theme.background.withValues(alpha: widget.backgroundOpacity),
@@ -296,10 +287,7 @@ class TerminalViewState extends State<TerminalView> {
child: child,
);
return Listener(
onPointerSignal: _onPointerSignal,
child: child,
);
return Listener(onPointerSignal: _onPointerSignal, child: child);
}
void requestKeyboard() {
@@ -380,10 +368,7 @@ class TerminalViewState extends State<TerminalView> {
// ancestor. Wrapping in a Shortcuts widget would invert that.
// T-107 approved leaving this suppression with an inline reason.
// ignore: invalid_use_of_protected_member
final shortcutResult = _shortcutManager.handleKeypress(
focusNode.context!,
event,
);
final shortcutResult = _shortcutManager.handleKeypress(focusNode.context!, event);
if (shortcutResult != KeyEventResult.ignored) {
return shortcutResult;
+1 -4
View File
@@ -16,8 +16,5 @@ Size calcCharSize(TerminalStyle style, TextScaler textScaler) {
final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: double.infinity));
return Size(
paragraph.maxIntrinsicWidth / test.length,
paragraph.height,
);
return Size(paragraph.maxIntrinsicWidth / test.length, paragraph.height);
}
+6 -20
View File
@@ -16,9 +16,9 @@ class TerminalController with ChangeNotifier {
SelectionMode selectionMode = SelectionMode.line,
PointerInputs pointerInputs = const PointerInputs({PointerInput.tap}),
bool suspendPointerInput = false,
}) : _selectionMode = selectionMode,
_pointerInputs = pointerInputs,
_suspendPointerInputs = suspendPointerInput;
}) : _selectionMode = selectionMode,
_pointerInputs = pointerInputs,
_suspendPointerInputs = suspendPointerInput;
CellAnchor? _selectionBase;
CellAnchor? _selectionExtent;
@@ -123,17 +123,8 @@ class TerminalController with ChangeNotifier {
/// 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,
);
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();
@@ -156,12 +147,7 @@ class TerminalHighlight with Disposable {
final Color color;
TerminalHighlight(
this.owner, {
required this.p1,
required this.p2,
required this.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.
+1 -7
View File
@@ -1,9 +1,3 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum TerminalCursorType {
block,
underline,
verticalBar,
}
enum TerminalCursorType { block, underline, verticalBar }
+5 -21
View File
@@ -89,12 +89,7 @@ class CustomTextEditState extends State<CustomTextEdit> with TextInputClient {
@override
Widget build(BuildContext context) {
return Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onKeyEvent: _onKeyEvent,
child: widget.child,
);
return Focus(focusNode: widget.focusNode, autofocus: widget.autofocus, onKeyEvent: _onKeyEvent, child: widget.child);
}
bool get hasInputConnection => _connection != null && _connection!.attached;
@@ -123,10 +118,7 @@ class CustomTextEditState extends State<CustomTextEdit> with TextInputClient {
return;
}
_connection?.setEditableSizeAndTransform(
rect.size,
Matrix4.translationValues(0, 0, 0),
);
_connection?.setEditableSizeAndTransform(rect.size, Matrix4.translationValues(0, 0, 0));
_connection?.setCaretRect(caretRect);
}
@@ -188,14 +180,8 @@ class CustomTextEditState extends State<CustomTextEdit> with TextInputClient {
}
TextEditingValue get _initEditingState => widget.deleteDetection
? const TextEditingValue(
text: ' ',
selection: TextSelection.collapsed(offset: 2),
)
: const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
);
? const TextEditingValue(text: ' ', selection: TextSelection.collapsed(offset: 2))
: const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0));
late var _currentEditingState = _initEditingState.copyWith();
@@ -226,9 +212,7 @@ class CustomTextEditState extends State<CustomTextEdit> with TextInputClient {
if (_currentEditingState.text.length < _initEditingState.text.length) {
widget.onDelete();
} else {
final textDelta = _currentEditingState.text.substring(
_initEditingState.text.length,
);
final textDelta = _currentEditingState.text.substring(_initEditingState.text.length);
widget.onInsert(textDelta);
}
@@ -99,16 +99,15 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
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;
},
);
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(() => TapGestureRecognizer(debugOwner: this), (
TapGestureRecognizer instance,
) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
..onSecondaryTapDown = widget.onSecondaryTapDown
..onSecondaryTapUp = widget.onSecondaryTapUp;
});
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
@@ -127,10 +126,7 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
);
gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(
debugOwner: this,
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse},
),
() => PanGestureRecognizer(debugOwner: this, supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse}),
(PanGestureRecognizer instance) {
instance
..dragStartBehavior = DragStartBehavior.down
@@ -139,10 +135,6 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
},
);
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true,
child: widget.child,
);
return RawGestureDetector(gestures: gestures, excludeFromSemantics: true, child: widget.child);
}
}
@@ -70,20 +70,11 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
bool get _shouldSendTapEvent => !widget.readOnly && widget.terminalController.shouldSendPointerInput(PointerInput.tap);
void _tapDown(
GestureTapDownCallback? callback,
TapDownDetails details,
TerminalMouseButton button, {
bool forceCallback = false,
}) {
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,
);
handled = renderTerminal.mouseEvent(button, TerminalMouseButtonState.down, details.localPosition);
}
// If the event was not handled by the terminal, use the supplied callback.
if (!handled || forceCallback) {
@@ -91,20 +82,11 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
}
}
void _tapUp(
GestureTapUpCallback? callback,
TapUpDetails details,
TerminalMouseButton button, {
bool forceCallback = false,
}) {
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,
);
handled = renderTerminal.mouseEvent(button, TerminalMouseButtonState.up, details.localPosition);
}
// If the event was not handled by the terminal, use the supplied callback.
if (!handled || forceCallback) {
@@ -115,12 +97,7 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
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,
);
_tapDown(widget.onTapDown, details, TerminalMouseButton.left, forceCallback: true);
}
void onSingleTapUp(TapUpDetails details) {
@@ -145,10 +122,7 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
}
void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
renderTerminal.selectWord(
_lastLongPressStartDetails!.localPosition,
details.localPosition,
);
renderTerminal.selectWord(_lastLongPressStartDetails!.localPosition, details.localPosition);
}
// void onLongPressUp() {}
@@ -162,9 +136,6 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
}
void onDragUpdate(DragUpdateDetails details) {
renderTerminal.selectCharacters(
_lastDragStartDetails!.localPosition,
details.localPosition,
);
renderTerminal.selectCharacters(_lastDragStartDetails!.localPosition, details.localPosition);
}
}
@@ -41,11 +41,6 @@ class CustomKeyboardListener extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focusNode,
autofocus: autofocus,
onKeyEvent: _onKeyEvent,
child: child,
);
return Focus(focusNode: focusNode, autofocus: autofocus, onKeyEvent: _onKeyEvent, child: child);
}
}
+12 -45
View File
@@ -9,13 +9,10 @@ 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;
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();
@@ -60,18 +57,13 @@ class TerminalPainter {
final textStyle = _textStyle.toTextStyle();
final builder = ParagraphBuilder(textStyle.getParagraphStyle());
builder.pushStyle(
textStyle.getTextStyle(textScaler: _textScaler),
);
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,
);
final result = Size(paragraph.maxIntrinsicWidth / test.length, paragraph.height);
paragraph.dispose();
return result;
@@ -88,12 +80,7 @@ class TerminalPainter {
}
/// Paints the cursor based on the current cursor type.
void paintCursor(
Canvas canvas,
Offset offset, {
required TerminalCursorType cursorType,
bool hasFocus = true,
}) {
void paintCursor(Canvas canvas, Offset offset, {required TerminalCursorType cursorType, bool hasFocus = true}) {
final paint = Paint()
..color = _theme.cursor
..strokeWidth = 1;
@@ -110,17 +97,9 @@ class TerminalPainter {
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,
);
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,
);
return canvas.drawLine(Offset(offset.dx, 0), Offset(offset.dx, _cellSize.height), paint);
}
}
@@ -132,19 +111,12 @@ class TerminalPainter {
..color = color
..strokeWidth = 1;
canvas.drawRect(
Rect.fromPoints(offset, endOffset),
paint,
);
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,
) {
void paintLine(Canvas canvas, Offset offset, BufferLine line) {
final cellData = CellData.empty();
final cellWidth = _cellSize.width;
@@ -205,12 +177,7 @@ class TerminalPainter {
char = String.fromCharCode(0xA0);
}
paragraph = _paragraphCache.performAndCacheLayout(
char,
style,
_textScaler,
cacheKey,
);
paragraph = _paragraphCache.performAndCacheLayout(char, style, _textScaler, cacheKey);
}
canvas.drawParagraph(paragraph, offset);
+1 -5
View File
@@ -10,11 +10,7 @@ class PaletteBuilder {
PaletteBuilder(this.theme);
List<Color> build() {
return List<Color>.generate(
256,
paletteColor,
growable: false,
);
return List<Color>.generate(256, paletteColor, growable: false);
}
/// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+1 -6
View File
@@ -34,12 +34,7 @@ class ParagraphCache {
Paragraph? getLayoutFromCache(int key) => _cache[key];
Paragraph performAndCacheLayout(
String text,
TextStyle style,
TextScaler textScaler,
int 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);
+1 -7
View File
@@ -21,11 +21,5 @@ class PointerInputs {
const PointerInputs.none() : inputs = const <PointerInput>{};
const PointerInputs.all()
: inputs = const <PointerInput>{
PointerInput.tap,
PointerInput.scroll,
PointerInput.drag,
PointerInput.move,
};
const PointerInputs.all() : inputs = const <PointerInput>{PointerInput.tap, PointerInput.scroll, PointerInput.drag, PointerInput.move};
}
+28 -99
View File
@@ -36,21 +36,17 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
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,
_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) {
@@ -248,10 +244,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
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),
);
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].
@@ -283,28 +276,18 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
void selectCharacters(Offset from, [Offset? to]) {
final fromPosition = getCellOffset(from);
if (to == null) {
_controller.setSelection(
_terminal.buffer.createAnchorFromOffset(fromPosition),
_terminal.buffer.createAnchorFromOffset(fromPosition),
);
_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),
);
_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,
) {
bool mouseEvent(TerminalMouseButton button, TerminalMouseButtonState buttonState, Offset offset) {
final position = getCellOffset(offset);
return _terminal.mouseInput(button, buttonState, position);
}
@@ -312,12 +295,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
void _notifyEditableRect() {
final cursor = localToGlobal(cursorOffset);
final rect = Rect.fromLTRB(
cursor.dx,
cursor.dy,
size.width,
cursor.dy + _painter.cellSize.height,
);
final rect = Rect.fromLTRB(cursor.dx, cursor.dy, size.width, cursor.dy + _painter.cellSize.height);
final caretRect = cursor & _painter.cellSize;
@@ -331,10 +309,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return;
}
final viewportSize = TerminalSize(
size.width ~/ _painter.cellSize.width,
_viewportHeight ~/ _painter.cellSize.height,
);
final viewportSize = TerminalSize(size.width ~/ _painter.cellSize.width, _viewportHeight ~/ _painter.cellSize.height);
if (_viewportSize != viewportSize) {
_viewportSize = viewportSize;
@@ -345,12 +320,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// 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(),
);
_terminal.resize(_viewportSize!.width, _viewportSize!.height, _painter.cellSize.width.round(), _painter.cellSize.height.round());
}
}
@@ -383,10 +353,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// 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,
);
return Offset(_terminal.buffer.cursorX * _painter.cellSize.width, _terminal.buffer.absoluteCursorY * _painter.cellSize.height + _lineOffset);
}
Size get cellSize {
@@ -415,11 +382,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
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],
);
_painter.paintLine(canvas, offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()), lines[i]);
}
if (_terminal.buffer.absoluteCursorY >= effectFirstLine && _terminal.buffer.absoluteCursorY <= effectLastLine) {
@@ -428,29 +391,14 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
if (_shouldShowCursor) {
_painter.paintCursor(
canvas,
offset + cursorOffset,
cursorType: _cursorType,
hasFocus: _focusNode.hasFocus,
);
_painter.paintCursor(canvas, offset + cursorOffset, cursorType: _cursorType, hasFocus: _focusNode.hasFocus);
}
}
_paintHighlights(
canvas,
_controller.highlights,
effectFirstLine,
effectLastLine,
);
_paintHighlights(canvas, _controller.highlights, effectFirstLine, effectLastLine);
if (_controller.selection != null) {
_paintSelection(
canvas,
_controller.selection!,
effectFirstLine,
effectLastLine,
);
_paintSelection(canvas, _controller.selection!, effectFirstLine, effectLastLine);
}
}
@@ -469,14 +417,8 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
);
final builder = ParagraphBuilder(style.getParagraphStyle());
builder.addPlaceholder(
offset.dx,
_painter.cellSize.height,
PlaceholderAlignment.middle,
);
builder.pushStyle(
style.getTextStyle(textScaler: _painter.textScaler),
);
builder.addPlaceholder(offset.dx, _painter.cellSize.height, PlaceholderAlignment.middle);
builder.pushStyle(style.getTextStyle(textScaler: _painter.textScaler));
builder.addText(composingText);
final paragraph = builder.build();
@@ -485,12 +427,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
canvas.drawParagraph(paragraph, Offset(0, offset.dy));
}
void _paintSelection(
Canvas canvas,
BufferRange selection,
int firstLine,
int lastLine,
) {
void _paintSelection(Canvas canvas, BufferRange selection, int firstLine, int lastLine) {
for (final segment in selection.toSegments()) {
if (segment.line >= _terminal.buffer.lines.length) {
break;
@@ -508,12 +445,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
}
void _paintHighlights(
Canvas canvas,
List<TerminalHighlight> highlights,
int firstLine,
int lastLine,
) {
void _paintHighlights(Canvas canvas, List<TerminalHighlight> highlights, int firstLine, int lastLine) {
for (var highlight in _controller.highlights) {
final range = highlight.range?.normalized;
@@ -540,10 +472,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final start = segment.start ?? 0;
final end = segment.end ?? _terminal.viewWidth;
final startOffset = Offset(
start * _painter.cellSize.width,
segment.line * _painter.cellSize.height + _lineOffset,
);
final startOffset = Offset(start * _painter.cellSize.width, segment.line * _painter.cellSize.height + _lineOffset);
_painter.paintHighlight(canvas, startOffset, end - start, color);
}
+1 -5
View File
@@ -1,7 +1,3 @@
// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory.
enum SelectionMode {
line,
block,
}
enum SelectionMode { line, block }
+3 -14
View File
@@ -7,12 +7,7 @@ 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,
});
const TerminalActions({super.key, required this.terminal, required this.controller, required this.child});
final Terminal terminal;
@@ -53,14 +48,8 @@ class TerminalActions extends StatelessWidget {
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,
),
terminal.buffer.createAnchor(0, terminal.buffer.height - terminal.viewHeight),
terminal.buffer.createAnchor(terminal.viewWidth, terminal.buffer.height - 1),
mode: SelectionMode.line,
);
return null;
@@ -50,13 +50,7 @@ class TerminalStyle {
final List<String> fontFamilyFallback;
TextStyle toTextStyle({
Color? color,
Color? backgroundColor,
bool bold = false,
bool italic = false,
bool underline = false,
}) {
TextStyle toTextStyle({Color? color, Color? backgroundColor, bool bold = false, bool italic = false, bool underline = false}) {
return TextStyle(
fontSize: fontSize,
height: height,
@@ -70,12 +64,7 @@ class TerminalStyle {
);
}
TerminalStyle copyWith({
double? fontSize,
double? height,
String? fontFamily,
List<String>? fontFamilyFallback,
}) {
TerminalStyle copyWith({double? fontSize, double? height, String? fontFamily, List<String>? fontFamilyFallback}) {
return TerminalStyle(
fontSize: fontSize ?? this.fontSize,
height: height ?? this.height,
@@ -77,10 +77,7 @@ class IndexAwareCircularBuffer<T extends IndexedItem> {
// 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,
);
final newArray = List<T?>.generate(value, (index) => index < _length ? _getChild(index) : null);
_startIndex = 0;
_array = newArray;