diff --git a/lib/models/distribution.dart b/lib/models/distribution.dart index 4659e83..62145c1 100644 --- a/lib/models/distribution.dart +++ b/lib/models/distribution.dart @@ -13,6 +13,12 @@ class LinuxDistribution { late final String? amd64PackageUrl; late final String? arm64PackageUrl; late final String packageFamilyName; + + static FutureLazyDynamicCan> distributions = + FutureLazyDynamicCan( + builder: fetch, + ); + static Future> fetch() async { var url = ArcheBus() .of() diff --git a/lib/views/pages/install.dart b/lib/views/pages/install.dart index 48ce985..6e60971 100644 --- a/lib/views/pages/install.dart +++ b/lib/views/pages/install.dart @@ -9,7 +9,9 @@ import 'package:wslconfigurer/models/distribution.dart'; import 'package:wslconfigurer/views/widgets/basic.dart'; import 'package:wslconfigurer/views/widgets/divider.dart'; import 'package:wslconfigurer/views/widgets/optfeat.dart'; +import 'package:wslconfigurer/views/widgets/process.dart'; import 'package:wslconfigurer/windows/ms_open.dart'; +import 'package:wslconfigurer/windows/utf16.dart'; class InstallPage extends StatefulWidget { const InstallPage({super.key}); @@ -28,13 +30,18 @@ class _InstallPageState extends State { ListTile( leading: const Icon(FontAwesomeIcons.section), title: context.i18nText("install.install_linux_distro"), + trailing: IconButton( + onPressed: () => LinuxDistribution.distributions + .reload() + .then((_) => setState(() {})), + icon: const Icon(Icons.refresh)), ), Padding( padding: const EdgeInsets.all(8), child: Column( children: [ FutureBuilder( - future: LinuxDistribution.fetch(), + future: LinuxDistribution.distributions.getValue(), builder: (context, snapshot) { var data = snapshot.data; if (data == null) { @@ -55,42 +62,55 @@ class _InstallPageState extends State { children: data .map( (distro) => ListTile( - title: Text(distro.friendlyName), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - var messager = - ScaffoldMessenger.of(context); - messager.clearSnackBars(); - messager.showSnackBar( - const SnackBar( - content: Text("Copyied!"), - ), - ); + title: Text(distro.friendlyName), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () { + var messager = + ScaffoldMessenger.of(context); + messager.clearSnackBars(); + messager.showSnackBar( + const SnackBar( + content: Text("Copyied!"), + ), + ); - Clipboard.setData(ClipboardData( - text: - "wsl.exe --install -d ${distro.name}")); - }, - icon: const Icon(Icons.copy), - ), - IconButton( - onPressed: () { - ComplexDialog.instance.text( + Clipboard.setData(ClipboardData( + text: + "wsl.exe --install -d ${distro.name} --no-launch")); + }, + icon: const Icon(Icons.copy), + ), + IconButton( + onPressed: () => ComplexDialog + .instance + .copy(barrierDismissible: false) + .text( context: context, - ); - }, - icon: const Icon(Icons.terminal), - ), - IconButton( - onPressed: () => openMSStoreProduct( - distro.storeAppId), - icon: const Icon(Icons.store), - ), - ], - )), + content: + ProcessCommandRunWidget( + executable: "wsl.exe", + arguments: [ + "--install", + "-d", + distro.name, + "--no-launch" + ], + codec: utf16, + ), + ), + icon: const Icon(Icons.terminal), + ), + IconButton( + onPressed: () => openMSStoreProduct( + distro.storeAppId), + icon: const Icon(Icons.store), + ), + ], + ), + ), ) .toList(), ), diff --git a/lib/views/widgets/optfeat.dart b/lib/views/widgets/optfeat.dart index b711ccd..7ac6982 100644 --- a/lib/views/widgets/optfeat.dart +++ b/lib/views/widgets/optfeat.dart @@ -96,6 +96,7 @@ class _CheckOptionalFeatureWidgetState content: SingleChildScrollView( child: ProcessText( process: process, + latest: true, ), ), ) diff --git a/lib/views/widgets/process.dart b/lib/views/widgets/process.dart index a77b738..ab0f6dd 100644 --- a/lib/views/widgets/process.dart +++ b/lib/views/widgets/process.dart @@ -1,24 +1,162 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:wslconfigurer/windows/sh.dart'; -class ProcessText extends StatelessWidget { +class ProcessText extends StatefulWidget { final Process process; + final bool latest; + final Encoding codec; + const ProcessText({ + super.key, + required this.process, + this.latest = false, + this.codec = systemEncoding, + }); - const ProcessText({super.key, required this.process}); + @override + State createState() => _ProcessTextState(); +} + +class _ProcessTextState extends State { + List<(bool, String)> span = []; + + @override + void initState() { + super.initState(); + + var process = widget.process; + + process.stderr.listen( + (data) => setState(() { + span.add((true, widget.codec.decode(data).trim())); + }), + ); + process.stdout.listen( + (data) => setState(() { + span.add((false, widget.codec.decode(data).trim())); + }), + ); + } + + @override + Widget build(BuildContext context) { + if (span.isEmpty) { + return const SizedBox.shrink(); + } + + if (widget.latest) { + return span.lastOrNull == null + ? const Text("") + : SelectableText( + span.last.$2, + style: span.last.$1 ? const TextStyle(color: Colors.red) : null, + ); + } + + return Column( + children: span + .map((data) => SelectableText(data.$2, + style: data.$1 ? const TextStyle(color: Colors.red) : null)) + .toList(), + ); + } +} + +class ProcessCommandRunWidget extends StatefulWidget { + final String executable; + final Iterable arguments; + final bool su; + final bool runInShell; + final Encoding codec; + + const ProcessCommandRunWidget({ + super.key, + required this.executable, + this.arguments = const [], + this.su = false, + this.runInShell = false, + this.codec = systemEncoding, + }); + + @override + State createState() => _ProcessCommandRunWidgetState(); +} + +class _ProcessCommandRunWidgetState extends State { + bool confirm = false; + Process? process; + + @override + void dispose() { + super.dispose(); + process?.kill(); + } @override Widget build(BuildContext context) { - return StreamBuilder( - stream: process.stdout, - builder: (context, snapshot) { - var data = snapshot.data; - if (data == null) { - return const Text(""); - } - - return SelectableText(const SystemEncoding().decode(data)); - }, + if (confirm) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + [widget.executable, ...widget.arguments].join(" "), + ), + const IconButton( + onPressed: null, + icon: Icon(Icons.keyboard_arrow_right_rounded), + ), + ], + ), + FutureBuilder( + future: Process.start( + widget.executable, + widget.arguments.toList(), + runInShell: widget.runInShell, + ), + builder: (context, snapshot) { + var proc = snapshot.data; + if (proc == null) { + return const CircularProgressIndicator(); + } + process = proc; + + return ProcessText( + process: proc, + codec: widget.codec, + ); + }, + ) + ], + ); + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + [widget.executable, ...widget.arguments].join(" "), + ), + IconButton( + onPressed: () { + if (widget.su) { + su( + context, + () => setState(() { + confirm = true; + })); + } else { + setState(() { + confirm = true; + }); + } + }, + icon: const Icon(Icons.keyboard_arrow_right_rounded), + ), + ], ); } } diff --git a/lib/windows/sh.dart b/lib/windows/sh.dart index 35fed5a..4c9fbef 100644 --- a/lib/windows/sh.dart +++ b/lib/windows/sh.dart @@ -30,4 +30,3 @@ Future enableFeature(String featurename) async { "/norestart" ]); } - diff --git a/lib/windows/utf16.dart b/lib/windows/utf16.dart new file mode 100644 index 0000000..4ce71fa --- /dev/null +++ b/lib/windows/utf16.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +const utf16 = Utf16Codec(); + +final class Utf16Codec extends Encoding { + const Utf16Codec(); + @override + Converter, String> get decoder => Utf16Decoder(); + + @override + Converter> get encoder => Utf16Encoder(); + + @override + String get name => "utf-16"; +} + +class Utf16Decoder extends Converter, String> { + @override + String convert(List input) { + return String.fromCharCodes( + Uint16List.sublistView(Uint8List.fromList(input))); + } +} + +class Utf16Encoder extends Converter> { + @override + List convert(String input) { + return input.codeUnits; + } +} diff --git a/lib/windows/wsl.dart b/lib/windows/wsl.dart index ebd9f3e..fe156e7 100644 --- a/lib/windows/wsl.dart +++ b/lib/windows/wsl.dart @@ -2,7 +2,7 @@ import 'dart:io'; class WindowsSubSystemLinux { static void shutdown(String target, {String? distro}) async { - await Process.run("wsl", [ + await Process.run("wsl.exe", [ ...distro != null ? ["-d", distro] : [], "--shutdown" ]); @@ -40,7 +40,7 @@ class WindowsSubSystemLinux { } static Future> getAvailableDistro() async { - return (await Process.run("wsl", ["-l", "-q"])) + return (await Process.run("wsl.exe", ["-l", "-q"])) .stdout .toString() .split("\n") @@ -50,7 +50,7 @@ class WindowsSubSystemLinux { } static List getAvailableDistroSync() { - return Process.runSync("wsl", ["-l", "-q"]) + return Process.runSync("wsl.exe", ["-l", "-q"]) .stdout .toString() .split("\n") diff --git a/pubspec.yaml b/pubspec.yaml index a9d6219..5808b20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,7 @@ flutter: - assets/i18n/ - assets/i18n/en_US/ - assets/i18n/zh_CN/ - + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware