| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- import 'dart:convert';
- import 'dart:io';
- import 'dart:math';
- import 'package:example/src/platform_menu.dart';
- import 'package:example/src/suggestion.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_pty/flutter_pty.dart';
- import 'package:xterm/xterm.dart';
- final engine = SuggestionEngine();
- Future<Map<String, dynamic>> loadSuggestion() async {
- final data = await rootBundle.load('assets/specs_v1.json.gz');
- return await Stream.value(data.buffer.asUint8List())
- .cast<List<int>>()
- .transform(gzip.decoder)
- .transform(utf8.decoder)
- .transform(json.decoder)
- .first as Map<String, dynamic>;
- }
- void main() async {
- WidgetsFlutterBinding.ensureInitialized();
- engine.load(await loadSuggestion());
- runApp(MyApp());
- }
- class MyApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'xterm.dart demo',
- debugShowCheckedModeBanner: false,
- home: AppPlatformMenu(child: Home()),
- );
- }
- }
- class Home extends StatefulWidget {
- Home({Key? key}) : super(key: key);
- @override
- // ignore: library_private_types_in_public_api
- _HomeState createState() => _HomeState();
- }
- class _HomeState extends State<Home> {
- late final terminal = Terminal(
- maxLines: 10000,
- onPrivateOSC: _handlePrivateOSC,
- );
- final terminalController = TerminalController();
- final terminalKey = GlobalKey<TerminalViewState>();
- final suggestionOverlay = OverlayPortalController();
- late final Pty pty;
- @override
- void initState() {
- super.initState();
- terminal.addListener(_handleTerminalChanged);
- WidgetsBinding.instance.endOfFrame.then(
- (_) {
- if (mounted) _startPty();
- },
- );
- }
- @override
- void dispose() {
- super.dispose();
- terminal.removeListener(_handleTerminalChanged);
- }
- void _startPty() {
- pty = Pty.start(
- shell,
- columns: terminal.viewWidth,
- rows: terminal.viewHeight,
- );
- pty.output
- .cast<List<int>>()
- .transform(Utf8Decoder())
- .listen(terminal.write);
- pty.exitCode.then((code) {
- terminal.write('the process exited with exit code $code');
- });
- terminal.onOutput = (data) {
- pty.write(const Utf8Encoder().convert(data));
- };
- terminal.onResize = (w, h, pw, ph) {
- pty.resize(h, w);
- };
- }
- CellAnchor? _promptStart;
- CellAnchor? _commandStart;
- CellAnchor? _commandEnd;
- CellAnchor? _commandFinished;
- void _handlePrivateOSC(String code, List<String> args) {
- switch (code) {
- case '133':
- _handleFinalTermOSC(args);
- }
- }
- void _handleFinalTermOSC(List<String> args) {
- switch (args) {
- case ['A']:
- _promptStart?.dispose();
- _promptStart = terminal.buffer.createAnchorFromCursor();
- _commandStart?.dispose();
- _commandStart = null;
- _commandEnd?.dispose();
- _commandEnd = null;
- _commandFinished?.dispose();
- _commandFinished = null;
- case ['B']:
- _commandStart?.dispose();
- _commandStart = terminal.buffer.createAnchorFromCursor();
- break;
- case ['C', ..._]:
- _commandEnd?.dispose();
- _commandEnd = terminal.buffer.createAnchorFromCursor();
- // _handleCommandEnd();
- break;
- case ['D', String exitCode]:
- _commandFinished?.dispose();
- _commandFinished = terminal.buffer.createAnchorFromCursor();
- // _handleCommandFinished(int.tryParse(exitCode));
- break;
- }
- }
- // void _handleCommandEnd() {
- // if (_commandStart == null || _commandEnd == null) return;
- // final command = terminal.buffer
- // .getText(BufferRangeLine(_commandStart!.offset, _commandEnd!.offset))
- // .trim();
- // print('command: $command');
- // }
- // void _handleCommandFinished(int? exitCode) {
- // if (_commandEnd == null || _commandFinished == null) return;
- // final result = terminal.buffer
- // .getText(BufferRangeLine(_commandEnd!.offset, _commandFinished!.offset))
- // .trim();
- // print('result: $result');
- // print('exit code $exitCode');
- // }
- final suggestions = ValueNotifier<List<FigSuggestion>>([]);
- void _handleTerminalChanged() {
- final commandStart = _commandStart;
- if (commandStart == null || _commandEnd != null) {
- suggestionOverlay.hide();
- return;
- }
- var commandRange = BufferRangeLine(
- commandStart.offset,
- CellOffset(
- terminal.buffer.cursorX,
- terminal.buffer.absoluteCursorY,
- ),
- );
- final command = terminal.buffer.getText(commandRange).trim();
- if (command.isEmpty) {
- suggestionOverlay.hide();
- return;
- }
- print('command: $command');
- suggestions.value = engine.getSuggestions(command).toList();
- print(suggestions.value);
- if (suggestions.value.isNotEmpty) {
- suggestionOverlay.show();
- } else {
- suggestionOverlay.hide();
- }
- }
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: Colors.transparent,
- body: OverlayPortal(
- controller: suggestionOverlay,
- overlayChildBuilder: (context) {
- return ValueListenableBuilder<List<FigSuggestion>>(
- valueListenable: suggestions,
- builder: (context, suggestions, _) {
- return SuggestionOverlay(
- suggestions,
- cursorRect: terminalKey.currentState!.cursorRect,
- );
- },
- );
- },
- child: TerminalView(
- terminal,
- key: terminalKey,
- controller: terminalController,
- autofocus: true,
- backgroundOpacity: 0.7,
- ),
- ),
- );
- }
- }
- class SuggestionOverlay extends StatelessWidget {
- const SuggestionOverlay(
- this.suggestions, {
- super.key,
- required this.cursorRect,
- });
- final Rect cursorRect;
- final List<FigSuggestion> suggestions;
- @override
- Widget build(BuildContext context) {
- print('build suggestions');
- const kScreenPadding = 8.0;
- const kPanelContentDistance = 8.0;
- const kPanelWidth = 300.0;
- const kPanelHeight = 300.0;
- final paddingAbove = MediaQuery.paddingOf(context).top + kScreenPadding;
- final availableHeight =
- cursorRect.top - kPanelContentDistance - paddingAbove;
- final fitsAbove = kPanelHeight <= availableHeight;
- return CustomSingleChildLayout(
- delegate: _SuggestionOverlayDelegate(cursorRect, fitsAbove),
- child: ConstrainedBox(
- constraints: BoxConstraints(
- maxWidth: kPanelWidth,
- maxHeight: kPanelHeight,
- ),
- child: _buildSuggestions(context),
- ),
- );
- }
- Widget _buildSuggestions(BuildContext context) {
- final list = ListView.builder(
- itemCount: suggestions.length,
- itemBuilder: (context, index) {
- final suggestion = suggestions[index];
- final (icon, color, content) = _suggestionContent(suggestion);
- return SuggestionTile(icon: icon, color: color, content: content ?? '');
- },
- );
- return Container(
- decoration: BoxDecoration(
- color: Colors.grey[800],
- borderRadius: BorderRadius.circular(4),
- // border: Border.all(color: Colors.grey[900]!),
- ),
- child: DefaultTextStyle(
- style: TerminalStyle().toTextStyle().copyWith(height: 1.5),
- child: Column(
- children: [
- Expanded(
- child: list,
- ),
- ],
- ),
- ),
- );
- }
- static (IconData, Color, String?) _suggestionContent(
- FigSuggestion suggestion) {
- return switch (suggestion) {
- FigSubCommand(:final names) => (
- Icons.subdirectory_arrow_right,
- Colors.blue,
- names.join(', '),
- ),
- FigOption(:final name) => (
- Icons.settings,
- Colors.green,
- name.join(', '),
- ),
- FigArgument(:final name) => (
- Icons.text_fields,
- Colors.yellow,
- name,
- ),
- };
- }
- }
- class _SuggestionOverlayDelegate extends SingleChildLayoutDelegate {
- _SuggestionOverlayDelegate(this.cursorRect, this.fitsAbove);
- final Rect cursorRect;
- final bool fitsAbove;
- @override
- BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
- return constraints.loosen();
- }
- @override
- Offset getPositionForChild(Size size, Size childSize) {
- TextSelectionToolbarLayoutDelegate;
- const kPanelContentDistance = 8.0;
- final dx = min(cursorRect.left, size.width - childSize.width);
- final dy = fitsAbove
- ? cursorRect.top - childSize.height
- : cursorRect.bottom + kPanelContentDistance;
- return Offset(dx, dy);
- }
- @override
- bool shouldRelayout(_SuggestionOverlayDelegate oldDelegate) {
- return cursorRect != oldDelegate.cursorRect;
- }
- }
- class SuggestionTile extends StatelessWidget {
- const SuggestionTile({
- super.key,
- required this.icon,
- required this.content,
- required this.color,
- });
- final IconData icon;
- final Color color;
- final String content;
- @override
- Widget build(BuildContext context) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- SizedBox(width: 4),
- Icon(
- icon,
- size: 14,
- color: color,
- ),
- SizedBox(width: 4),
- Text(content),
- ],
- );
- }
- }
- String get shell {
- if (Platform.isMacOS || Platform.isLinux) {
- return Platform.environment['SHELL'] ?? 'bash';
- }
- if (Platform.isWindows) {
- return 'cmd.exe';
- }
- return 'sh';
- }
|