diff --git a/assets/images/console.svg b/assets/images/console.svg
new file mode 100644
index 0000000..8c555d8
--- /dev/null
+++ b/assets/images/console.svg
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/lib/src/pages/manager.dart b/lib/src/pages/manager.dart
index 9e95aeb..734142d 100644
--- a/lib/src/pages/manager.dart
+++ b/lib/src/pages/manager.dart
@@ -1,8 +1,10 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import 'package:file_picker/file_picker.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'dart:io';
import '../globals.dart';
@@ -23,12 +25,16 @@ class Manager extends StatefulWidget {
class _ManagerState extends State with PreferencesMixin {
List _currentVms = [];
Map _activeVms = {};
- final List _spicyVms = [];
+ bool _spicy = false;
+ final List _sshVms = [];
+ String? _terminalEmulator;
Timer? refreshTimer;
@override
void initState() {
super.initState();
+ _getTerminalEmulator();
+ _detectSpice();
getPreference(prefWorkingDirectory).then((pref) {
setState(() {
if (pref == null) {
@@ -38,7 +44,6 @@ class _ManagerState extends State with PreferencesMixin {
});
Future.delayed(Duration.zero, () => _getVms(context)); // Reload VM list when we enter the page.
});
-
refreshTimer = Timer.periodic(const Duration(seconds: 5), (Timer t) {
_getVms(context);
}); // Reload VM list every 5 seconds.
@@ -50,6 +55,24 @@ class _ManagerState extends State with PreferencesMixin {
super.dispose();
}
+ void _getTerminalEmulator() async {
+ ProcessResult result = Process.runSync('x-terminal-emulator', ['-h']);
+ RegExp pattern = RegExp(r"usage:\s+([^\s]+)", multiLine: true, caseSensitive: false);
+ RegExpMatch? match = pattern.firstMatch(result.stdout);
+ if (match != null) {
+ setState(() {
+ _terminalEmulator = match.group(1);
+ });
+ }
+ }
+
+ void _detectSpice() async {
+ ProcessResult result = await Process.run('which', ['spicy']);
+ setState(() {
+ _spicy = result.exitCode == 0;
+ });
+ }
+
VmInfo _parseVmInfo(name) {
VmInfo info = VmInfo();
List lines = File(name + '/' + name + '.ports').readAsLinesSync();
@@ -107,8 +130,21 @@ class _ManagerState extends State with PreferencesMixin {
});
}
+ Future _detectSsh(int port) async {
+ bool isSSH = false;
+ try {
+ Socket socket = await Socket.connect('localhost', port);
+ isSSH = await socket.any((event) => utf8.decode(event).contains('SSH'));
+ socket.close();
+ return isSSH;
+ } catch (exception) {
+ return false;
+ }
+ }
+
Widget _buildVmList() {
List _widgetList = [];
+ final Color buttonColor = Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Theme.of(context).colorScheme.primary;
_widgetList.add(
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -122,7 +158,7 @@ class _ManagerState extends State with PreferencesMixin {
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).canvasColor,
- onPrimary: Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Theme.of(context).colorScheme.primary,
+ onPrimary: buttonColor
),
onPressed: () async {
String? result = await FilePicker.platform.getDirectoryPath();
@@ -141,7 +177,7 @@ class _ManagerState extends State with PreferencesMixin {
),
);
List> rows = _currentVms.map((vm) {
- return _buildRow(vm);
+ return _buildRow(vm, buttonColor);
}).toList();
for (var row in rows) {
_widgetList.addAll(row);
@@ -153,18 +189,30 @@ class _ManagerState extends State with PreferencesMixin {
);
}
- List _buildRow(String currentVm) {
+ List _buildRow(String currentVm, Color buttonColor) {
final bool active = _activeVms.containsKey(currentVm);
- final bool spicy = _spicyVms.contains(currentVm);
+ final bool sshy = _sshVms.contains(currentVm);
+ VmInfo vmInfo = VmInfo();
String connectInfo = '';
if (active) {
- VmInfo vmInfo = _activeVms[currentVm]!;
- if (vmInfo.sshPort != null) {
- connectInfo += context.t('SSH port') + ': ' + vmInfo.sshPort! + ' ';
- }
+ vmInfo = _activeVms[currentVm]!;
if (vmInfo.spicePort != null) {
connectInfo += context.t('SPICE port') + ': ' + vmInfo.spicePort! + ' ';
}
+ if (vmInfo.sshPort != null && _terminalEmulator != null) {
+ connectInfo += context.t('SSH port') + ': ' + vmInfo.sshPort! + ' ';
+ _detectSsh(int.parse(vmInfo.sshPort!)).then((sshRunning) {
+ if (sshRunning && !sshy) {
+ setState(() {
+ _sshVms.add(currentVm);
+ });
+ } else if (!sshRunning && sshy) {
+ setState(() {
+ _sshVms.remove(currentVm);
+ });
+ }
+ });
+ }
}
return [
ListTile(
@@ -172,41 +220,24 @@ class _ManagerState extends State with PreferencesMixin {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
- IconButton(
- icon: Icon(Icons.monitor,
- color: spicy ? Colors.red : null, semanticLabel: spicy ? context.t('Using SPICE display') : context.t('Click to use SPICE display')),
- tooltip: spicy ? context.t('Using SPICE display') : context.t('Use SPICE display'),
- onPressed: () {
- if (spicy) {
- setState(() {
- _spicyVms.remove(currentVm);
- });
- } else {
- setState(() {
- _spicyVms.add(currentVm);
- });
- }
- }),
IconButton(
icon: Icon(
active ? Icons.play_arrow : Icons.play_arrow_outlined,
- color: active ? Colors.green : null,
+ color: active ? Colors.green : buttonColor,
semanticLabel: active ? 'Running' : 'Run',
),
- onPressed: () async {
- if (!active) {
- Map activeVms = _activeVms;
- List args = ['--vm', currentVm + '.conf'];
- if (spicy) {
- args.addAll(['--display', 'spice']);
- }
- await Process.start('quickemu', args);
- VmInfo info = _parseVmInfo(currentVm);
- activeVms[currentVm] = info;
- setState(() {
- _activeVms = activeVms;
- });
+ onPressed: active ? null : () async {
+ Map activeVms = _activeVms;
+ List args = ['--vm', currentVm + '.conf'];
+ if (_spicy) {
+ args.addAll(['--display', 'spice']);
}
+ await Process.start('quickemu', args);
+ VmInfo info = _parseVmInfo(currentVm);
+ activeVms[currentVm] = info;
+ setState(() {
+ _activeVms = activeVms;
+ });
}),
IconButton(
icon: Icon(
@@ -214,41 +245,113 @@ class _ManagerState extends State with PreferencesMixin {
color: active ? Colors.red : null,
semanticLabel: active ? 'Stop' : 'Not running',
),
- onPressed: () {
- if (active) {
- showDialog(
- context: context,
- builder: (BuildContext context) => AlertDialog(
- title: Text(context.t('Stop The Virtual Machine?')),
- content: Text('${context.t('You are about to terminate the virtual machine')} $currentVm'),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(context, false),
- child: Text(context.t('Cancel')),
- ),
- TextButton(
- onPressed: () => Navigator.pop(context, true),
- child: Text(context.t('OK')),
- ),
- ],
- ),
- ).then((result) {
- result = result ?? false;
- if (result) {
- Process.run('killall', [currentVm]);
- setState(() {
- _activeVms.remove(currentVm);
- });
- }
- });
- }
+ onPressed: !active ? null : () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) => AlertDialog(
+ title: Text(context.t('Stop The Virtual Machine?')),
+ content: Text('${context.t('You are about to terminate the virtual machine')} $currentVm'),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: Text(context.t('Cancel')),
+ ),
+ TextButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: Text(context.t('OK')),
+ ),
+ ],
+ ),
+ ).then((result) {
+ result = result ?? false;
+ if (result) {
+ Process.run('killall', [currentVm]);
+ setState(() {
+ _activeVms.remove(currentVm);
+ });
+ }
+ });
},
),
],
)),
if (connectInfo.isNotEmpty)
ListTile(
- title: Text(connectInfo),
+ title: Text(
+ connectInfo,
+ style: TextStyle(fontSize: 12)
+ ),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ icon: Icon(
+ Icons.monitor,
+ color: _spicy ? buttonColor : null,
+ semanticLabel: 'Connect display with SPICE',
+ ),
+ tooltip: _spicy? 'Connect display with SPICE' : 'SPICE client not found',
+ onPressed: !_spicy? null : () {
+ Process.start('spicy', ['-p', vmInfo.spicePort!]);
+ },
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/images/console.svg',
+ semanticsLabel: 'Connect with SSH',
+ color: sshy ? buttonColor : Colors.grey
+ ),
+ tooltip: sshy ? 'Connect with SSH' : 'SSH server not detected on guest',
+ onPressed: !sshy ? null : () {
+ TextEditingController _usernameController = TextEditingController();
+ showDialog(
+ context: context,
+ builder: (BuildContext context) => AlertDialog(
+ title: Text('Launch SSH connection to $currentVm'),
+ content: TextField(
+ controller: _usernameController,
+ decoration: const InputDecoration(hintText: "SSH username"),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: const Text('Cancel'),
+ ),
+ TextButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: const Text('Connect'),
+ ),
+ ],
+ ),
+ ).then((result) {
+ result = result ?? false;
+ if (result) {
+ List sshArgs = ['ssh', '-p', vmInfo.sshPort!, _usernameController.text + '@localhost'];
+ switch(_terminalEmulator) {
+ case 'gnome-terminal':
+ case 'mate-terminal':
+ sshArgs.insert(0, '--');
+ break;
+ case 'xterm':
+ case 'konsole':
+ sshArgs.insert(0, '-e');
+ break;
+ case 'terminator':
+ case 'xfce4-terminal':
+ sshArgs.insert(0, '-x');
+ break;
+ case 'guake':
+ String command = sshArgs.join(' ');
+ sshArgs = ['-e', command];
+ break;
+ }
+ Process.start(_terminalEmulator!, sshArgs);
+ }
+ });
+ },
+ ),
+ ]
+ )
),
const Divider()
];
diff --git a/pubspec.yaml b/pubspec.yaml
index 9a0b676..f0e5aa2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -51,6 +51,7 @@ dependencies:
flutter_localizations:
sdk: flutter
desktop_notifications: ^0.6.1
+ flutter_svg: ^0.23.0
dev_dependencies:
flutter_test: