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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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>[''];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)'}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>{};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user