mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
215 lines
5.6 KiB
Dart
215 lines
5.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
import 'package:xterm/core.dart';
|
|
|
|
part 'snippet.g.dart';
|
|
part 'snippet.freezed.dart';
|
|
|
|
@freezed
|
|
abstract class Snippet with _$Snippet {
|
|
const factory Snippet({
|
|
required String name,
|
|
required String script,
|
|
List<String>? tags,
|
|
String? note,
|
|
|
|
/// List of server id that this snippet should be auto run on
|
|
List<String>? autoRunOn,
|
|
}) = _Snippet;
|
|
|
|
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
|
|
|
|
static const example = Snippet(
|
|
name: 'example',
|
|
script: 'echo hello',
|
|
tags: ['tag'],
|
|
note: 'note',
|
|
autoRunOn: ['server_id'],
|
|
);
|
|
}
|
|
|
|
extension SnippetX on Snippet {
|
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
|
|
|
String fmtWithSpi(Spi spi) {
|
|
return script.replaceAllMapped(
|
|
fmtFinder,
|
|
(match) {
|
|
final key = match.group(0);
|
|
final func = fmtArgs[key];
|
|
if (func != null) return func(spi);
|
|
// If not found, return the original content for further processing
|
|
return key ?? '';
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> runInTerm(
|
|
Terminal terminal,
|
|
Spi spi, {
|
|
bool autoEnter = false,
|
|
}) async {
|
|
final argsFmted = fmtWithSpi(spi);
|
|
final matches = fmtFinder.allMatches(argsFmted);
|
|
|
|
/// There is no [TerminalKey] in the script
|
|
if (matches.isEmpty) {
|
|
terminal.textInput(argsFmted);
|
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
|
return;
|
|
}
|
|
|
|
// Records all start and end indexes of the matches
|
|
final (starts, ends) = matches.fold((<int>[], <int>[]), (pre, e) {
|
|
pre.$1.add(e.start);
|
|
pre.$2.add(e.end);
|
|
return pre;
|
|
});
|
|
|
|
// Check all indexes, `(idx + 1).start` must >= `idx.end`
|
|
for (var i = 0; i < starts.length - 1; i++) {
|
|
final lastEnd = ends[i];
|
|
final nextStart = starts[i + 1];
|
|
if (nextStart < lastEnd) {
|
|
throw 'Invalid format: $nextStart < $lastEnd';
|
|
}
|
|
}
|
|
|
|
// Start term input
|
|
if (starts.first > 0) {
|
|
terminal.textInput(argsFmted.substring(0, starts.first));
|
|
}
|
|
|
|
// Process matched
|
|
for (var idx = 0; idx < starts.length; idx++) {
|
|
final start = starts[idx];
|
|
final end = ends[idx];
|
|
final key = argsFmted.substring(start, end).toLowerCase();
|
|
|
|
// Special funcs
|
|
final special = _find(SnippetFuncs.specialCtrl, key);
|
|
if (special != null) {
|
|
final raw = key.substring(special.key.length + 1, key.length - 1);
|
|
await special.value((term: terminal, raw: raw));
|
|
} else {
|
|
// Term keys
|
|
final termKey = _find(fmtTermKeys, key);
|
|
if (termKey != null) {
|
|
await _doTermKeys(terminal, termKey, key);
|
|
} else {
|
|
// Normal input
|
|
terminal.textInput(key);
|
|
}
|
|
}
|
|
|
|
// Text between this and next match
|
|
if (idx < starts.length - 1) {
|
|
terminal.textInput(argsFmted.substring(end, starts[idx + 1]));
|
|
}
|
|
}
|
|
|
|
// End term input
|
|
if (ends.last < argsFmted.length) {
|
|
terminal.textInput(argsFmted.substring(ends.last));
|
|
}
|
|
|
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
|
}
|
|
|
|
Future<void> _doTermKeys(
|
|
Terminal terminal,
|
|
MapEntry<String, TerminalKey> termKey,
|
|
String key,
|
|
) async {
|
|
// if (termKey.value == TerminalKey.enter) {
|
|
// terminal.keyInput(TerminalKey.enter);
|
|
// return;
|
|
// }
|
|
|
|
final ctrlAlt = switch (termKey.value) {
|
|
TerminalKey.control => (ctrl: true, alt: false),
|
|
TerminalKey.alt => (ctrl: false, alt: true),
|
|
_ => (ctrl: false, alt: false),
|
|
};
|
|
|
|
if (!key.contains('+')) return;
|
|
|
|
// `${ctrl+ad}` -> `ctrla + d`
|
|
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
|
if (chars.isEmpty) return;
|
|
final ok = terminal.charInput(
|
|
chars.codeUnitAt(0),
|
|
ctrl: ctrlAlt.ctrl,
|
|
alt: ctrlAlt.alt,
|
|
);
|
|
if (!ok) {
|
|
Loggers.app.warning('Failed to input: $key');
|
|
}
|
|
|
|
terminal.textInput(chars.substring(1));
|
|
}
|
|
|
|
MapEntry<String, T>? _find<T>(Map<String, T> map, String key) {
|
|
return map.entries.firstWhereOrNull((e) => key.startsWith(e.key));
|
|
}
|
|
|
|
static final fmtArgs = {
|
|
r'${host}': (Spi spi) => spi.ip,
|
|
r'${port}': (Spi spi) => spi.port.toString(),
|
|
r'${user}': (Spi spi) => spi.user,
|
|
r'${pwd}': (Spi spi) => spi.pwd ?? '',
|
|
r'${id}': (Spi spi) => spi.id,
|
|
r'${name}': (Spi spi) => spi.name,
|
|
};
|
|
|
|
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
|
static final fmtTermKeys = {
|
|
r'${ctrl': TerminalKey.control,
|
|
r'${alt': TerminalKey.alt,
|
|
};
|
|
}
|
|
|
|
class SnippetResult {
|
|
final String? dest;
|
|
final String result;
|
|
final Duration time;
|
|
|
|
SnippetResult({
|
|
required this.dest,
|
|
required this.result,
|
|
required this.time,
|
|
});
|
|
}
|
|
|
|
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
|
|
|
abstract final class SnippetFuncs {
|
|
static final specialCtrl = {
|
|
// `${sleep 3}` -> sleep 3 seconds
|
|
r'${sleep': SnippetFuncs.sleep,
|
|
r'${enter': SnippetFuncs.enter,
|
|
};
|
|
|
|
static const help = {
|
|
'sleep': 'Sleep for a few seconds',
|
|
'enter': 'Enter a few times',
|
|
};
|
|
|
|
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
|
final seconds = int.tryParse(ctx.raw);
|
|
if (seconds == null) return;
|
|
final duration = Duration(seconds: seconds);
|
|
await Future.delayed(duration);
|
|
}
|
|
|
|
static FutureOr<void> enter(SnippetFuncCtx ctx) async {
|
|
final times = int.tryParse(ctx.raw) ?? 1;
|
|
for (var i = 0; i < times; i++) {
|
|
ctx.term.keyInput(TerminalKey.enter);
|
|
}
|
|
}
|
|
}
|