diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5625597e..2aa8bed3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get -y install tree # Install Flutter dependencies RUN apt-get -y install curl file git unzip xz-utils zip clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev # Install app-specific dependencies -RUN apt-get -y install keybinder-3.0 appindicator3-0.1 libappindicator3-1 libappindicator3-dev +RUN apt-get -y install keybinder-3.0 libayatana-appindicator3-dev # Install Flutter RUN git clone https://github.com/flutter/flutter.git -b stable /home/vscode/flutter diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 670349dc..8db7cb38 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] + + env: + # Needed so we don't get errors in CI + XDG_SESSION_TYPE: "x11" + XDG_CURRENT_DESKTOP: "KDE" + steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 @@ -38,7 +44,7 @@ jobs: run: flutter gen-l10n - name: Run lint - run: flutter analyze + run: flutter analyze --no-fatal-infos - name: Run tests run: flutter test diff --git a/assets/lib/linux/active_window_kde.js b/assets/lib/linux/active_window_kde.js new file mode 100644 index 00000000..ca1b594a --- /dev/null +++ b/assets/lib/linux/active_window_kde.js @@ -0,0 +1,41 @@ +function print(str) { + console.info('Nyrna KDE Wayland: ' + str); +} + +print('Updating active window on DBus'); + +function windowToJson(window) { + return JSON.stringify({ + caption: window.caption, + pid: window.pid, + internalId: window.internalId, + }); +} + +function updateActiveWindowOnDBus() { + let activeWindow = workspace.activeWindow(); + + if (!activeWindow) { + print('No active window found'); + return; + } + + let windowJson = windowToJson(activeWindow); + + callDBus( + 'codes.merritt.Nyrna', + '/', + 'codes.merritt.Nyrna', + 'updateActiveWindow', + windowJson, + (result) => { + if (result) { + print('Successfully updated active window on DBus'); + } else { + print('Failed to update active window on DBus'); + } + } + ); +} + +updateActiveWindowOnDBus(); diff --git a/assets/lib/linux/list_windows_kde.js b/assets/lib/linux/list_windows_kde.js new file mode 100644 index 00000000..7726679f --- /dev/null +++ b/assets/lib/linux/list_windows_kde.js @@ -0,0 +1,94 @@ +// https://unix.stackexchange.com/a/706478/379240 + +function print(str) { + console.info('Nyrna: ' + str); +} + +let windows = workspace.windowList(); +print('Found ' + windows.length + ' windows'); + +function updateWindowsOnDBus(windows) { + let windowsList = []; + + for (let window of windows) { + windowsList.push({ + caption: window.caption, + pid: window.pid, + internalId: window.internalId, + onCurrentDesktop: isWindowOnCurrentDesktop(window), + }); + } + + callDBus( + 'codes.merritt.Nyrna', + '/', + 'codes.merritt.Nyrna', + 'updateWindows', + JSON.stringify(windowsList), + (result) => { + if (result) { + print('Successfully updated windows on DBus'); + } else { + print('Failed to update windows on DBus'); + } + } + ); +} + +function isWindowOnCurrentDesktop(window) { + let windowDesktops = Object.values(window.desktops); + let windowIsOnCurrentDesktop = window.onAllDesktops; + + if (!windowIsOnCurrentDesktop) { + for (let windowDesktop of windowDesktops) { + if (windowDesktop.id === workspace.currentDesktop.id) { + windowIsOnCurrentDesktop = true; + break; + } else { + windowIsOnCurrentDesktop = false; + } + } + } + + return windowIsOnCurrentDesktop; +} + +function updateCurrentDesktopOnDBus() { + print('Current desktop id: ' + workspace.currentDesktop.id); + + callDBus( + 'codes.merritt.Nyrna', + '/', + 'codes.merritt.Nyrna', + 'updateCurrentDesktop', + workspace.currentDesktop, + (result) => { + if (result) { + print('Successfully updated current desktop on DBus'); + } else { + print('Failed to update current desktop on DBus'); + } + } + ); +} + +updateCurrentDesktopOnDBus(); +updateWindowsOnDBus(windows); + +workspace.currentDesktopChanged.connect(() => { + print('Current desktop changed'); + updateCurrentDesktopOnDBus(); + updateWindowsOnDBus(windows); +}); + +workspace.windowAdded.connect(window => { + print('Window added: ' + window.caption); + windows.push(window); + updateWindowsOnDBus(windows); +}); + +workspace.windowRemoved.connect(window => { + print('Window removed: ' + window.caption); + windows = windows.filter(w => w.internalId !== window.internalId); + updateWindowsOnDBus(windows); +}); diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/active_window/src/active_window.dart b/lib/active_window/src/active_window.dart index 22abdf3c..d5ad3cd8 100644 --- a/lib/active_window/src/active_window.dart +++ b/lib/active_window/src/active_window.dart @@ -86,13 +86,31 @@ class ActiveWindow { log.i('Suspending'); for (int attempt = 0; attempt < _maxRetries; attempt++) { - final window = await _nativePlatform.activeWindow(); + final window = _nativePlatform.activeWindow; + if (window == null) { + log.w('No active window found, retrying.'); + await _nativePlatform.checkActiveWindow(); + await Future.delayed(const Duration(milliseconds: 500)); + } + + if (window == null && attempt < _maxRetries - 1) { + continue; + } else if (window == null && attempt == _maxRetries - 1) { + log.e('Failed to find active window after $_maxRetries attempts.'); + return false; + } else if (window == null) { + log.e('Failed to find active window.'); + return false; + } + final String executable = window.process.executable; if (executable == 'nyrna' || executable == 'nyrna.exe') { log.w('Active window is Nyrna, hiding and retrying.'); await _appWindow.hide(); await Future.delayed(const Duration(milliseconds: 500)); + await _nativePlatform.checkActiveWindow(); + await Future.delayed(const Duration(milliseconds: 500)); continue; } @@ -141,7 +159,7 @@ class ActiveWindow { return false; } - Future _minimize(int windowId) async { + Future _minimize(String windowId) async { final shouldMinimize = await _getShouldMinimize(); if (!shouldMinimize) return; @@ -150,7 +168,7 @@ class ActiveWindow { if (!minimized) log.e('Failed to minimize window.'); } - Future _restore(int windowId) async { + Future _restore(String windowId) async { final shouldRestore = await _getShouldMinimize(); if (!shouldRestore) return; diff --git a/lib/app/cubit/app_cubit.dart b/lib/app/cubit/app_cubit.dart index 360d8029..32ce84bd 100644 --- a/lib/app/cubit/app_cubit.dart +++ b/lib/app/cubit/app_cubit.dart @@ -58,7 +58,7 @@ class AppCubit extends Cubit { /// blocking the UI, since none of the data fetched here is critical. Future _init() async { await _checkForFirstRun(); - await _checkLinuxSessionType(); + _checkLinuxSessionType(); await _fetchVersionData(); await _fetchReleaseNotes(); _listenToSystemTrayEvents(); @@ -74,19 +74,19 @@ class AppCubit extends Cubit { } /// For Linux, checks if the session type is Wayland. - Future _checkLinuxSessionType() async { + void _checkLinuxSessionType() { if (defaultTargetPlatform != TargetPlatform.linux) return; - final sessionType = await (_nativePlatform as Linux).sessionType(); + final sessionType = (_nativePlatform as Linux).sessionType; final unknownSessionMsg = ''' Unable to determine session type. The XDG_SESSION_TYPE environment variable is set to "$sessionType". Please note that Wayland is not currently supported.'''; const waylandNotSupportedMsg = ''' -Wayland is not currently supported. +Wayland is currently supported only on KDE Plasma. -Only xwayland apps will be detected. +For other desktop environments, only xwayland apps will be detected. If Wayland support is important to you, consider voting on the issue: @@ -106,12 +106,19 @@ env QT_QPA_PLATFORM=xcb Otherwise, [consider signing in using X11 instead](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/).'''; - switch (sessionType) { - case 'wayland': - log.w(waylandNotSupportedMsg); - emit(state.copyWith(linuxSessionMessage: waylandNotSupportedMsg)); - return; - case 'x11': + emit(state.copyWith(sessionType: sessionType)); + + log.i('Session type: $sessionType'); + + switch (sessionType.displayProtocol) { + case DisplayProtocol.wayland: + if (sessionType.environment == DesktopEnvironment.kde) { + log.i('KDE Wayland session detected and is supported, proceeding.'); + } else { + log.w(waylandNotSupportedMsg); + emit(state.copyWith(linuxSessionMessage: waylandNotSupportedMsg)); + } + case DisplayProtocol.x11: break; default: log.w(unknownSessionMsg); @@ -202,4 +209,10 @@ Otherwise, [consider signing in using X11 instead](https://docs.fedoraproject.or return false; } } + + @override + Future close() async { + await _nativePlatform.dispose(); + await super.close(); + } } diff --git a/lib/app/cubit/app_state.dart b/lib/app/cubit/app_state.dart index 792298aa..eda3d41a 100644 --- a/lib/app/cubit/app_state.dart +++ b/lib/app/cubit/app_state.dart @@ -7,6 +7,11 @@ class AppState with _$AppState { /// session type is unknown. String? linuxSessionMessage, + /// The type of desktop session the user is running. + /// + /// Currently only used on Linux. + SessionType? sessionType, + /// True if this is the first run of the app. required bool firstRun, required String runningVersion, diff --git a/lib/apps_list/cubit/apps_list_cubit.dart b/lib/apps_list/cubit/apps_list_cubit.dart index 04a7aaf3..a7fb02bf 100644 --- a/lib/apps_list/cubit/apps_list_cubit.dart +++ b/lib/apps_list/cubit/apps_list_cubit.dart @@ -226,6 +226,7 @@ class AppsListCubit extends Cubit { _storage, ); + await _nativePlatform.checkActiveWindow(); return await activeWindow.toggle(); } diff --git a/lib/apps_list/models/interaction_error.dart b/lib/apps_list/models/interaction_error.dart index 0a73d6fe..926ead76 100644 --- a/lib/apps_list/models/interaction_error.dart +++ b/lib/apps_list/models/interaction_error.dart @@ -5,7 +5,7 @@ import '../enums.dart'; class InteractionError { final InteractionType interactionType; final ProcessStatus statusAfterInteraction; - final int windowId; + final String windowId; const InteractionError({ required this.interactionType, diff --git a/lib/loading/cubit/loading_cubit.dart b/lib/loading/cubit/loading_cubit.dart index ee65da9c..f01b166a 100644 --- a/lib/loading/cubit/loading_cubit.dart +++ b/lib/loading/cubit/loading_cubit.dart @@ -10,9 +10,7 @@ part 'loading_cubit.freezed.dart'; class LoadingCubit extends Cubit { final NativePlatform nativePlatform; - LoadingCubit() - : nativePlatform = NativePlatform(), - super(const LoadingInitial()) { + LoadingCubit(this.nativePlatform) : super(const LoadingInitial()) { checkDependencies(); } diff --git a/lib/loading/loading_page.dart b/lib/loading/loading_page.dart index df546fe5..06c9cfe8 100644 --- a/lib/loading/loading_page.dart +++ b/lib/loading/loading_page.dart @@ -16,44 +16,40 @@ class LoadingPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LoadingCubit(), - lazy: false, - child: Scaffold( - body: Center( - child: BlocConsumer( - listener: (context, state) { - if (state is LoadingSuccess) { - Navigator.pushReplacementNamed(context, AppsListPage.id); - } - }, - builder: (context, state) { - switch (state) { - case LoadingError(): - return Padding( - padding: const EdgeInsets.all(16.0), - child: Card( - child: Container( - padding: const EdgeInsets.all(20.0), - child: MarkdownBody( - data: state.errorMsg, - onTapLink: (text, href, title) { - if (href == null) { - log.e('Broken link: $href'); - return; - } + return Scaffold( + body: Center( + child: BlocConsumer( + listener: (context, state) { + if (state is LoadingSuccess) { + Navigator.pushReplacementNamed(context, AppsListPage.id); + } + }, + builder: (context, state) { + switch (state) { + case LoadingError(): + return Padding( + padding: const EdgeInsets.all(16.0), + child: Card( + child: Container( + padding: const EdgeInsets.all(20.0), + child: MarkdownBody( + data: state.errorMsg, + onTapLink: (text, href, title) { + if (href == null) { + log.e('Broken link: $href'); + return; + } - AppCubit.instance.launchURL(href); - }, - ), + AppCubit.instance.launchURL(href); + }, ), ), - ); - default: - return const CircularProgressIndicator(); - } - }, - ), + ), + ); + default: + return const CircularProgressIndicator(); + } + }, ), ), ); diff --git a/lib/localization/app_localizations.dart b/lib/localization/app_localizations.dart new file mode 100644 index 00000000..720ebd58 --- /dev/null +++ b/lib/localization/app_localizations.dart @@ -0,0 +1,377 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_de.dart'; +import 'app_localizations_en.dart'; +import 'app_localizations_it.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'localization/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('de'), + Locale('it') + ]; + + /// Label for a cancel button + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Label for a confirm button + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// Hint text for searchbox that allows the user to filter windows + /// + /// In en, this message translates to: + /// **'Filter windows'** + String get filterWindows; + + /// Tooltip for the add to favorites button + /// + /// In en, this message translates to: + /// **'Add to favorites'** + String get favoriteButtonTooltipAdd; + + /// Tooltip for the remove from favorites button + /// + /// In en, this message translates to: + /// **'Remove from favorites'** + String get favoriteButtonTooltipRemove; + + /// The title of the details dialog + /// + /// In en, this message translates to: + /// **'Details'** + String get detailsDialogTitle; + + /// Label for the window title field in the details dialog + /// + /// In en, this message translates to: + /// **'Window Title'** + String get detailsDialogWindowTitle; + + /// Label for the executable name field in the details dialog + /// + /// In en, this message translates to: + /// **'Executable Name'** + String get detailsDialogExecutableName; + + /// Label for the PID field in the details dialog + /// + /// In en, this message translates to: + /// **'PID'** + String get detailsDialogPID; + + /// Label for the current status field in the details dialog + /// + /// In en, this message translates to: + /// **'Current Status'** + String get detailsDialogCurrentStatus; + + /// Label for the copy logs button + /// + /// In en, this message translates to: + /// **'Copy logs'** + String get copyLogs; + + /// Notification displayed when logs are copied to clipboard + /// + /// In en, this message translates to: + /// **'Logs copied to clipboard'** + String get logsCopiedNotification; + + /// Label for the donate button + /// + /// In en, this message translates to: + /// **'Donate'** + String get donate; + + /// Message displayed on the donate page + /// + /// In en, this message translates to: + /// **'If you like this application, please consider donating to support its development.'** + String get donateMessage; + + /// Introduction to application author + /// + /// In en, this message translates to: + /// **'Made with 💙 by '** + String get madeBy; + + /// The title of the settings page + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// The title of the behaviour section of the settings page. + /// + /// In en, this message translates to: + /// **'Behaviour'** + String get behaviourTitle; + + /// Label for the auto refresh setting + /// + /// In en, this message translates to: + /// **'Auto Refresh'** + String get autoRefresh; + + /// Description for the auto refresh setting + /// + /// In en, this message translates to: + /// **'Update window & process info automatically'** + String get autoRefreshDescription; + + /// Label for the auto refresh interval setting + /// + /// In en, this message translates to: + /// **'Auto Refresh Interval'** + String get autoRefreshInterval; + + /// The amount of time between auto refreshes + /// + /// In en, this message translates to: + /// **'{interval} seconds'** + String autoRefreshIntervalAmount(int interval); + + /// Label for the close to tray setting + /// + /// In en, this message translates to: + /// **'Close to tray'** + String get closeToTray; + + /// Label for the minimize / restore windows setting + /// + /// In en, this message translates to: + /// **'Minimize / restore windows'** + String get minimizeAndRestoreWindows; + + /// Whether to pin suspended windows to the top of the window list + /// + /// In en, this message translates to: + /// **'Pin suspended windows'** + String get pinSuspendedWindows; + + /// Tooltip for the pin suspended windows setting + /// + /// In en, this message translates to: + /// **'If enabled, suspended windows will always be shown at the top of the window list.'** + String get pinSuspendedWindowsTooltip; + + /// Label for the show hidden windows setting + /// + /// In en, this message translates to: + /// **'Show hidden windows'** + String get showHiddenWindows; + + /// Tooltip for the show hidden windows setting + /// + /// In en, this message translates to: + /// **'Includes windows from other virtual desktops and special windows that are not normally detected.'** + String get showHiddenWindowsTooltip; + + /// No description provided for @themeTitle. + /// + /// In en, this message translates to: + /// **'Theme'** + String get themeTitle; + + /// Label for the dark theme setting + /// + /// In en, this message translates to: + /// **'Dark'** + String get dark; + + /// Label for the light theme setting + /// + /// In en, this message translates to: + /// **'Light'** + String get light; + + /// Label for the pitch black theme setting + /// + /// In en, this message translates to: + /// **'Pitch Black'** + String get pitchBlack; + + /// The title of the system integration section of the settings page. + /// + /// In en, this message translates to: + /// **'System Integration'** + String get systemIntegrationTitle; + + /// Label for the start automatically at system boot setting + /// + /// In en, this message translates to: + /// **'Start automatically at system boot'** + String get startAutomatically; + + /// Label for the start hidden in system tray setting + /// + /// In en, this message translates to: + /// **'Start hidden in system tray'** + String get startInTray; + + /// The title of the troubleshooting section of the settings page. + /// + /// In en, this message translates to: + /// **'Troubleshooting'** + String get troubleshootingTitle; + + /// Label for the logs button + /// + /// In en, this message translates to: + /// **'Logs'** + String get logs; + + /// The title of the about section of the settings page. + /// + /// In en, this message translates to: + /// **'About'** + String get aboutTitle; + + /// Label for the version number + /// + /// In en, this message translates to: + /// **'Nyrna version'** + String get version; + + /// Label for the homepage link + /// + /// In en, this message translates to: + /// **'Nyrna homepage'** + String get homepage; + + /// Label for the repository link + /// + /// In en, this message translates to: + /// **'GitHub repository'** + String get repository; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['de', 'en', 'it'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); + case 'it': + return AppLocalizationsIt(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/lib/localization/app_localizations_de.dart b/lib/localization/app_localizations_de.dart new file mode 100644 index 00000000..6965ec2a --- /dev/null +++ b/lib/localization/app_localizations_de.dart @@ -0,0 +1,138 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get cancel => 'Abbrechen'; + + @override + String get confirm => 'Bestätigen'; + + @override + String get filterWindows => 'Filter windows'; + + @override + String get favoriteButtonTooltipAdd => 'Add to favorites'; + + @override + String get favoriteButtonTooltipRemove => 'Remove from favorites'; + + @override + String get detailsDialogTitle => 'Details'; + + @override + String get detailsDialogWindowTitle => 'Fenstertitel'; + + @override + String get detailsDialogExecutableName => 'Ausführbarer Name'; + + @override + String get detailsDialogPID => 'PID'; + + @override + String get detailsDialogCurrentStatus => 'Aktueller Status'; + + @override + String get copyLogs => 'Protokolle kopieren'; + + @override + String get logsCopiedNotification => 'Protokolle kopiert!'; + + @override + String get donate => 'Spenden'; + + @override + String get donateMessage => + 'Wenn Sie diese App nützlich finden, können Sie uns mit einer Spende unterstützen.'; + + @override + String get madeBy => 'Mit 💙 gemacht von '; + + @override + String get settingsTitle => 'Einstellungen'; + + @override + String get behaviourTitle => 'Verhalten'; + + @override + String get autoRefresh => 'Automatische Aktualisierung'; + + @override + String get autoRefreshDescription => 'Automatische Aktualisierung der Daten'; + + @override + String get autoRefreshInterval => + 'Intervall der automatischen Aktualisierung'; + + @override + String autoRefreshIntervalAmount(int interval) { + return '$interval Sekunden'; + } + + @override + String get closeToTray => 'Schließen Sie das Fenster in den Systembereich'; + + @override + String get minimizeAndRestoreWindows => + 'Minimieren und Wiederherstellen von Fenstern'; + + @override + String get pinSuspendedWindows => 'Pin suspended windows'; + + @override + String get pinSuspendedWindowsTooltip => + 'If enabled, suspended windows will always be shown at the top of the window list.'; + + @override + String get showHiddenWindows => 'Versteckte Fenster anzeigen'; + + @override + String get showHiddenWindowsTooltip => + 'Schließen Sie Fenster ein, die normalerweise ausgeblendet sind.'; + + @override + String get themeTitle => 'Thema'; + + @override + String get dark => 'Dunkel'; + + @override + String get light => 'Hell'; + + @override + String get pitchBlack => 'Pechschwarz'; + + @override + String get systemIntegrationTitle => 'Systemintegration'; + + @override + String get startAutomatically => + 'Starten Sie die App automatisch mit dem System'; + + @override + String get startInTray => 'Starten Sie die App im Systembereich'; + + @override + String get troubleshootingTitle => 'Fehlerbehebung'; + + @override + String get logs => 'Protokolle'; + + @override + String get aboutTitle => 'Über'; + + @override + String get version => 'Nyrna Version'; + + @override + String get homepage => 'Nyrna Homepage'; + + @override + String get repository => 'GitHub Repository'; +} diff --git a/lib/localization/app_localizations_en.dart b/lib/localization/app_localizations_en.dart new file mode 100644 index 00000000..395802ed --- /dev/null +++ b/lib/localization/app_localizations_en.dart @@ -0,0 +1,136 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get cancel => 'Cancel'; + + @override + String get confirm => 'Confirm'; + + @override + String get filterWindows => 'Filter windows'; + + @override + String get favoriteButtonTooltipAdd => 'Add to favorites'; + + @override + String get favoriteButtonTooltipRemove => 'Remove from favorites'; + + @override + String get detailsDialogTitle => 'Details'; + + @override + String get detailsDialogWindowTitle => 'Window Title'; + + @override + String get detailsDialogExecutableName => 'Executable Name'; + + @override + String get detailsDialogPID => 'PID'; + + @override + String get detailsDialogCurrentStatus => 'Current Status'; + + @override + String get copyLogs => 'Copy logs'; + + @override + String get logsCopiedNotification => 'Logs copied to clipboard'; + + @override + String get donate => 'Donate'; + + @override + String get donateMessage => + 'If you like this application, please consider donating to support its development.'; + + @override + String get madeBy => 'Made with 💙 by '; + + @override + String get settingsTitle => 'Settings'; + + @override + String get behaviourTitle => 'Behaviour'; + + @override + String get autoRefresh => 'Auto Refresh'; + + @override + String get autoRefreshDescription => + 'Update window & process info automatically'; + + @override + String get autoRefreshInterval => 'Auto Refresh Interval'; + + @override + String autoRefreshIntervalAmount(int interval) { + return '$interval seconds'; + } + + @override + String get closeToTray => 'Close to tray'; + + @override + String get minimizeAndRestoreWindows => 'Minimize / restore windows'; + + @override + String get pinSuspendedWindows => 'Pin suspended windows'; + + @override + String get pinSuspendedWindowsTooltip => + 'If enabled, suspended windows will always be shown at the top of the window list.'; + + @override + String get showHiddenWindows => 'Show hidden windows'; + + @override + String get showHiddenWindowsTooltip => + 'Includes windows from other virtual desktops and special windows that are not normally detected.'; + + @override + String get themeTitle => 'Theme'; + + @override + String get dark => 'Dark'; + + @override + String get light => 'Light'; + + @override + String get pitchBlack => 'Pitch Black'; + + @override + String get systemIntegrationTitle => 'System Integration'; + + @override + String get startAutomatically => 'Start automatically at system boot'; + + @override + String get startInTray => 'Start hidden in system tray'; + + @override + String get troubleshootingTitle => 'Troubleshooting'; + + @override + String get logs => 'Logs'; + + @override + String get aboutTitle => 'About'; + + @override + String get version => 'Nyrna version'; + + @override + String get homepage => 'Nyrna homepage'; + + @override + String get repository => 'GitHub repository'; +} diff --git a/lib/localization/app_localizations_it.dart b/lib/localization/app_localizations_it.dart new file mode 100644 index 00000000..3bd0ea6d --- /dev/null +++ b/lib/localization/app_localizations_it.dart @@ -0,0 +1,137 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Italian (`it`). +class AppLocalizationsIt extends AppLocalizations { + AppLocalizationsIt([String locale = 'it']) : super(locale); + + @override + String get cancel => 'Cancella'; + + @override + String get confirm => 'Conferma'; + + @override + String get filterWindows => 'Filter windows'; + + @override + String get favoriteButtonTooltipAdd => 'Add to favorites'; + + @override + String get favoriteButtonTooltipRemove => 'Remove from favorites'; + + @override + String get detailsDialogTitle => 'Dettagli'; + + @override + String get detailsDialogWindowTitle => 'Titolo finestra'; + + @override + String get detailsDialogExecutableName => 'Nome eseguibile'; + + @override + String get detailsDialogPID => 'PID'; + + @override + String get detailsDialogCurrentStatus => 'Stato attuale'; + + @override + String get copyLogs => 'Copia log'; + + @override + String get logsCopiedNotification => 'Log copiati negli appunti'; + + @override + String get donate => 'Donazione'; + + @override + String get donateMessage => + 'Se ti piace questa applicazione, considera la possibilità di fare una donazione per sostenerne lo sviluppo.'; + + @override + String get madeBy => 'Fatto con il 💙 da '; + + @override + String get settingsTitle => 'Impostazioni'; + + @override + String get behaviourTitle => 'Comportamento'; + + @override + String get autoRefresh => 'Aggiornamento automatico'; + + @override + String get autoRefreshDescription => + 'Aggiorna automaticamente le informazioni sulla finestra e sul processo'; + + @override + String get autoRefreshInterval => 'Intervallo di aggiornamento automatico'; + + @override + String autoRefreshIntervalAmount(int interval) { + return '$interval secondi'; + } + + @override + String get closeToTray => 'Vicino alla barra delle applicazioni'; + + @override + String get minimizeAndRestoreWindows => 'Minimizza / ripristina finestre'; + + @override + String get pinSuspendedWindows => 'Pin suspended windows'; + + @override + String get pinSuspendedWindowsTooltip => + 'If enabled, suspended windows will always be shown at the top of the window list.'; + + @override + String get showHiddenWindows => 'Mostra finestre nascoste'; + + @override + String get showHiddenWindowsTooltip => + 'Include finestre di altri desktop virtuali e finestre speciali che normalmente non vengono rilevate.'; + + @override + String get themeTitle => 'Tema'; + + @override + String get dark => 'Scuro'; + + @override + String get light => 'Chiaro'; + + @override + String get pitchBlack => 'Nero pece'; + + @override + String get systemIntegrationTitle => 'Integrazione del sistema'; + + @override + String get startAutomatically => + 'Avvia automaticamente all\'avvio del sistema'; + + @override + String get startInTray => 'Avvia nascosto nella barra delle applicazioni'; + + @override + String get troubleshootingTitle => 'Risoluzione dei problemi'; + + @override + String get logs => 'Log'; + + @override + String get aboutTitle => 'Informazioni'; + + @override + String get version => 'Versione di Nyrna'; + + @override + String get homepage => 'Homepage di Nyrna'; + + @override + String get repository => 'Repository GitHub'; +} diff --git a/lib/main.dart b/lib/main.dart index 3988a2d4..5a53c819 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'apps_list/apps_list.dart'; import 'argument_parser/argument_parser.dart'; import 'autostart/autostart_service.dart'; import 'hotkey/global/hotkey_service.dart'; +import 'loading/loading.dart'; import 'logs/logs.dart'; import 'native_platform/native_platform.dart'; import 'settings/cubit/settings_cubit.dart'; @@ -33,7 +34,6 @@ Future main(List args) async { ..parseArgs(args); final storage = await StorageRepository.initialize(Hive); - final nativePlatform = NativePlatform(); bool verbose = argParser.verbose; if (!verbose) { @@ -52,10 +52,14 @@ Future main(List args) async { final packageInfo = await PackageInfo.fromPlatform(); log.i('Starting Nyrna v${packageInfo.version}'); + final nativePlatform = await NativePlatform.initialize(); final processRepository = ProcessRepository.init(); final appWindow = AppWindow(storage); - appWindow.initialize(); + // appWindow.initialize(); + if (!argParser.toggleActiveWindow) { + await appWindow.initialize(); + } final activeWindow = ActiveWindow( appWindow, @@ -67,6 +71,7 @@ Future main(List args) async { // If we receive the toggle argument, suspend or resume the active // window and then exit without showing the GUI. if (argParser.toggleActiveWindow) { + await nativePlatform.checkActiveWindow(); await activeWindow.toggle(); // On Windows the program stays running in the background, so we don't want @@ -119,6 +124,8 @@ Future main(List args) async { appVersion: AppVersion(packageInfo), ); + final loadingCubit = LoadingCubit(nativePlatform); + runApp( MultiRepositoryProvider( providers: [ @@ -128,6 +135,7 @@ Future main(List args) async { providers: [ BlocProvider.value(value: appCubit), BlocProvider.value(value: appsListCubit), + BlocProvider.value(value: loadingCubit), BlocProvider.value(value: settingsCubit), BlocProvider.value(value: themeCubit), ], diff --git a/lib/native_platform/src/linux/active_window/active_window_service.dart b/lib/native_platform/src/linux/active_window/active_window_service.dart new file mode 100644 index 00000000..88b64953 --- /dev/null +++ b/lib/native_platform/src/linux/active_window/active_window_service.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import '../../typedefs.dart'; +import '../../window.dart'; +import '../linux.dart'; +import 'active_window_wayland.dart'; +import 'active_window_x11.dart'; + +/// Desktop and window manager agnostic interface to get the active window. +class ActiveWindowService { + final Linux _linux; + final RunFunction _run; + + ActiveWindowService(this._linux, this._run); + + /// Fetches the active window, which will be emitted by the [Linux] object. + Future fetch() async { + switch (_linux.sessionType.displayProtocol) { + case DisplayProtocol.wayland: + await ActiveWindowWayland.fetch(_linux); + case DisplayProtocol.x11: + await ActiveWindowX11.fetch(_linux, _run); + case DisplayProtocol.unknown: + throw UnimplementedError('Unknown display protocol'); + } + } + + /// Stream of the currently active window. + final _activeWindowController = StreamController.broadcast(); + + Stream get activeWindow => _activeWindowController.stream; +} diff --git a/lib/native_platform/src/linux/active_window/active_window_wayland.dart b/lib/native_platform/src/linux/active_window/active_window_wayland.dart new file mode 100644 index 00000000..49fcc13d --- /dev/null +++ b/lib/native_platform/src/linux/active_window/active_window_wayland.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:helpers/helpers.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../../logs/logging_manager.dart'; +import '../../../native_platform.dart'; +import '../linux.dart'; + +class ActiveWindowWayland { + static const String _kdeWaylandScriptName = 'nyrna_get_active_window'; + + final String _kdeWaylandScriptPath; + final Linux _linux; + + ActiveWindowWayland._(this._kdeWaylandScriptPath, this._linux); + + static Future fetch(Linux linux) async { + final kdeWaylandScriptPath = await _getKdeWaylandScriptPath(); + final service = ActiveWindowWayland._(kdeWaylandScriptPath, linux); + + switch (linux.sessionType.environment) { + case DesktopEnvironment.kde: + await service._fetchActiveWindowKde(); + default: + throw UnimplementedError(); + } + } + + Future _fetchActiveWindowKde() async { + log.i('Loading KWin script for active window on KDE Wayland..'); + await _linux.kwin.loadScript(_kdeWaylandScriptPath, _kdeWaylandScriptName); + + _linux.kwin.scriptOutput + .where((event) => event.contains('Nyrna KDE Wayland:')) + .listen((event) { + log.t('KWin script output: $event'); + }); + + // Listen for the update from nyrnadbus' activeWindowUpdates + _linux.nyrnaDbus.activeWindowUpdates.listen((windowString) async { + log.t('Active window update: $windowString'); + final windowJson = jsonDecode(windowString); + final pid = int.tryParse(windowJson['pid']); + if (pid == null) return; + final executable = await _linux.getExecutableName(pid); + + final process = Process( + pid: pid, + executable: executable, + status: ProcessStatus.unknown, + ); + + final window = Window( + id: windowJson['internalId'], + process: process, + title: windowJson['caption'], + ); + + _linux.activeWindow = window; + // _linux.updateActiveWindow(window); + }); + + // Short wait to give the script time to run + await Future.delayed(const Duration(milliseconds: 500)); + } + + static Future _getKdeWaylandScriptPath() async { + if (io.Platform.environment['FLUTTER_TEST'] == 'true') return ''; + + final dataDir = await getApplicationSupportDirectory(); + final tempFile = await assetToTempDir('assets/lib/linux/active_window_kde.js'); + final file = + io.File('${dataDir.path}${io.Platform.pathSeparator}active_window_kde.js'); + await tempFile.copy(file.path); + return file.path; + } +} diff --git a/lib/native_platform/src/linux/active_window/active_window_x11.dart b/lib/native_platform/src/linux/active_window/active_window_x11.dart new file mode 100644 index 00000000..f263fef1 --- /dev/null +++ b/lib/native_platform/src/linux/active_window/active_window_x11.dart @@ -0,0 +1,75 @@ +import '../../../native_platform.dart'; +import '../../typedefs.dart'; +import '../linux.dart'; + +/// Information on the active window in X11. +/// +/// Fetches the information when created. +class ActiveWindowX11 { + final Linux _linux; + final RunFunction _run; + + ActiveWindowX11._(this._linux, this._run); + + /// Returns a [Window] object representing the active window. + static Future fetch(Linux linux, RunFunction run) async { + final activeWindow = ActiveWindowX11._(linux, run); + await activeWindow._fetchActiveWindowInfo(); + } + + Future _fetchActiveWindowInfo() async { + final windowId = await _activeWindowId(); + if (windowId == '0') throw (Exception('No window id')); + + final pid = await _activeWindowPid(windowId); + if (pid == 0) throw (Exception('No pid')); + + final executable = await _linux.getExecutableName(pid); + final title = await _activeWindowTitle(); + + final process = Process( + pid: pid, + executable: executable, + status: ProcessStatus.unknown, + ); + + // return Window( + // id: windowId, + // process: process, + // title: title, + // ); + + final window = Window( + id: windowId, + process: process, + title: title, + ); + + _linux.activeWindow = window; + // _linux.updateActiveWindow(window); + } + + // Returns the unique hex ID of the active window as reported by xdotool. + Future _activeWindowId() async { + final result = await _run('xdotool', ['getactivewindow']); + final windowId = result.stdout.toString().trim(); + return windowId; + } + + Future _activeWindowPid(String windowId) async { + final result = await _run( + 'xdotool', + ['getwindowpid', windowId], + ); + final pid = int.tryParse(result.stdout.toString().trim()); + return pid ?? 0; + } + + Future _activeWindowTitle() async { + final result = await _run( + 'xdotool', + ['getactivewindow getwindowname'], + ); + return result.stdout.toString().trim(); + } +} diff --git a/lib/native_platform/src/linux/dbus/codes.merritt.Nyrna.xml b/lib/native_platform/src/linux/dbus/codes.merritt.Nyrna.xml new file mode 100644 index 00000000..9cd513ba --- /dev/null +++ b/lib/native_platform/src/linux/dbus/codes.merritt.Nyrna.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/native_platform/src/linux/dbus/nyrna_dbus.dart b/lib/native_platform/src/linux/dbus/nyrna_dbus.dart new file mode 100644 index 00000000..c356101a --- /dev/null +++ b/lib/native_platform/src/linux/dbus/nyrna_dbus.dart @@ -0,0 +1,139 @@ +// This file was generated using the following command and may be overwritten. +// dart-dbus generate-object lib/native_platform/src/linux/codes.merritt.Nyrna.xml + +import 'dart:async'; + +import 'package:dbus/dbus.dart'; + +import '../../../../logs/logs.dart'; + +class NyrnaDbus extends DBusObject { + /// Creates a new object to expose on [path]. + NyrnaDbus._({DBusObjectPath path = const DBusObjectPath.unchecked('/')}) : super(path); + + static Future initialize() async { + final nyrnaDbus = NyrnaDbus._(); + await nyrnaDbus._registerNyrnaDbusObject(); + return nyrnaDbus; + } + + /// Register Nyrna's service on DBus. + Future _registerNyrnaDbusObject() async { + final client = DBusClient.session(); + + DBusRequestNameReply result; + + try { + result = await client.requestName( + 'codes.merritt.Nyrna', + flags: { + DBusRequestNameFlag.allowReplacement, + DBusRequestNameFlag.doNotQueue, + DBusRequestNameFlag.replaceExisting, + }, + ); + } catch (e) { + log.e('Failed to request name: $e'); + rethrow; + } + + if (result != DBusRequestNameReply.primaryOwner) { + log.e('Failed to request name: $result'); + throw Exception('Failed to request name: $result'); + } + + await client.registerObject(this); + } + + /// The JSON of windows found by the companion KDE KWin script. + /// + /// This will be updated by the `updateWindows` method, and read externally by the + /// `Linux` class. + String windowsJson = ''; + + /// Called by the companion KDE KWin script to update the JSON of windows. + Future updateWindows(String windows) async { + windowsJson = windows; + + return DBusMethodSuccessResponse([ + const DBusBoolean(true), + ]); + } + + final _activeWindowController = StreamController.broadcast(); + Stream get activeWindowUpdates => _activeWindowController.stream; + + /// Called by the companion KDE KWin script to update the active window. + Future updateActiveWindow(String windowId) async { + log.t('Received active window update on DBus: $windowId'); + _activeWindowController.add(windowId); + return DBusMethodSuccessResponse([const DBusBoolean(true)]); + } + + @override + List introspect() { + return [ + DBusIntrospectInterface( + 'codes.merritt.Nyrna', + methods: [ + DBusIntrospectMethod('updateCurrentDesktop'), + DBusIntrospectMethod('updateWindows'), + DBusIntrospectMethod('updateActiveWindow'), + ], + ) + ]; + } + + @override + Future handleMethodCall(DBusMethodCall methodCall) async { + log.t( + 'Received method call: ${methodCall.interface}.${methodCall.name}, signature: ${methodCall.signature}, values: ${methodCall.values}'); + + if (methodCall.interface != 'codes.merritt.Nyrna') { + return DBusMethodErrorResponse.unknownInterface(); + } + + switch (methodCall.name) { + case 'updateCurrentDesktop': + return DBusMethodErrorResponse.unknownMethod(); + case 'updateWindows': + if (methodCall.signature != DBusSignature('s')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return await updateWindows(methodCall.values[0].asString()); + case 'updateActiveWindow': + if (methodCall.signature != DBusSignature('s')) { + return DBusMethodErrorResponse.invalidArgs(); + } + return await updateActiveWindow(methodCall.values[0].asString()); + default: + return DBusMethodErrorResponse.unknownMethod(); + } + } + + @override + Future getProperty(String interface, String name) async { + if (interface == 'codes.merritt.Nyrna') { + return DBusMethodErrorResponse.unknownProperty(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + @override + Future setProperty( + String interface, String name, DBusValue value) async { + if (interface == 'codes.merritt.Nyrna') { + return DBusMethodErrorResponse.unknownProperty(); + } else { + return DBusMethodErrorResponse.unknownProperty(); + } + } + + Future dispose() async { + await _activeWindowController.close(); + final client = DBusClient.session(); + await client.releaseName('codes.merritt.Nyrna'); + await client.close(); + } +} diff --git a/lib/native_platform/src/linux/linux.dart b/lib/native_platform/src/linux/linux.dart index 8424872a..cba7a398 100644 --- a/lib/native_platform/src/linux/linux.dart +++ b/lib/native_platform/src/linux/linux.dart @@ -1,21 +1,94 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; +import 'package:kwin/kwin.dart'; + import '../../../logs/logs.dart'; import '../native_platform.dart'; import '../process/models/process.dart'; import '../typedefs.dart'; import '../window.dart'; +import 'active_window/active_window_service.dart'; +import 'dbus/nyrna_dbus.dart'; +import 'session_type.dart'; + +export 'flatpak.dart'; +export 'session_type.dart'; /// System-level or non-app executables. Nyrna shouldn't show these. const List _filteredWindows = [ 'nyrna', + // Remove any instances of plasmashell, which is the KDE desktop. + // It appears to show up for each monitor and virtual desktop. + 'plasmashell', + 'xwaylandvideobridge', ]; /// Interact with the native Linux operating system. class Linux implements NativePlatform { + /// Name of the KWin script that fetches info when running KDE Wayland. + static const String _kdeWaylandScriptName = 'nyrna_wayland'; + + /// Path to the KWin script that fetches info when running KDE Wayland. + /// + /// The path is an argument to allow for dependency injection in tests. + final String _kdeWaylandScriptPath; + + final KWin kwin; + final NyrnaDbus nyrnaDbus; final RunFunction _run; - Linux(this._run); + final SessionType sessionType; + + Linux._( + this._kdeWaylandScriptPath, + this.kwin, + this.nyrnaDbus, + this._run, + this.sessionType, + ); + + @override + Window? activeWindow; + + static Future initialize( + RunFunction run, [ + String kdeWaylandScriptPath = '', + KWin? kwin, // Allows overriding for testing. + NyrnaDbus? nyrnaDbus, // Allows overriding for testing. + ]) async { + final dbusService = nyrnaDbus ?? await NyrnaDbus.initialize(); + final kwinService = kwin ?? KWin(); + final session = await SessionType.fromEnvironment(); + final linux = Linux._(kdeWaylandScriptPath, kwinService, dbusService, run, session); + + if (session.displayProtocol == DisplayProtocol.wayland && + session.environment == DesktopEnvironment.kde) { + await linux._loadKdeWaylandScript(); + } + + return linux; + } + + Future _loadKdeWaylandScript() async { + log.i('Loading KWin script for KDE Wayland. Path: $_kdeWaylandScriptPath'); + + await kwin.loadScript(_kdeWaylandScriptPath, _kdeWaylandScriptName); + + // _kwin.scriptOutput.listen((event) { + // log.t('KWin script output: $event'); + // }); + + // print script output, but filter for online lines containing 'Nyrna:' + kwin.scriptOutput.where((event) => event.contains('Nyrna:')).listen((event) { + log.t('KWin script output: $event'); + }); + + // Wait for the script to be loaded. Otherwise when the window loads it looks briefly + // as though no windows were found. + await Future.delayed(const Duration(seconds: 1)); + } int? _desktop; @@ -34,6 +107,70 @@ class Linux implements NativePlatform { // Gets all open windows as reported by wmctrl. @override Future> windows({bool showHidden = false}) async { + switch (sessionType.displayProtocol) { + case DisplayProtocol.wayland: + return await _getWindowsWayland(showHidden); + case DisplayProtocol.x11: + return await _getWindowsX11(showHidden); + default: + return Future.error('Unknown session type: $sessionType'); + } + } + + Future> _getWindowsWayland(bool showHidden) async { + if (sessionType.displayProtocol != DisplayProtocol.wayland) { + throw Future.error( + 'Expected Wayland session but got ${sessionType.displayProtocol}'); + } + + switch (sessionType.environment) { + case DesktopEnvironment.kde: + return await _getWindowsKdeWayland(showHidden); + case DesktopEnvironment.gnome: + throw UnimplementedError(); + default: + throw Future.error('Unknown desktop environment: ${sessionType.environment}'); + } + } + + Future> _getWindowsKdeWayland(bool showHidden) async { + if (nyrnaDbus.windowsJson.isEmpty) { + log.w('No windows found from KDE Wayland'); + return []; + } + + final windowsJson = jsonDecode(nyrnaDbus.windowsJson); + final windows = []; + + for (var window in windowsJson) { + final onCurrentDesktop = window['onCurrentDesktop'] == true; + if (!onCurrentDesktop && !showHidden) continue; + + final windowId = window['internalId']; + final windowTitle = window['caption']; + final pid = window['pid']; + final executable = await getExecutableName(pid); + if (_filteredWindows.contains(executable)) continue; + + final process = Process( + pid: pid, + executable: executable, + status: ProcessStatus.unknown, + ); + + windows.add(Window( + id: windowId, + process: process, + title: windowTitle, + )); + } + + log.i('Windows from KDE Wayland: found ${windows.length} windows'); + + return windows; + } + + Future> _getWindowsX11(bool showHidden) async { await currentDesktop(); final wmctrlOutput = await _run('bash', ['-c', 'wmctrl -lp']); @@ -50,10 +187,6 @@ class Linux implements NativePlatform { if (window != null) windows.add(window); } - // Remove any instances of plasmashell, which is the KDE desktop. - // It appears to show up on X11 sessions, for each monitor and virtual desktop. - windows.removeWhere((window) => window.process.executable == 'plasmashell'); - return windows; } @@ -79,7 +212,7 @@ class Linux implements NativePlatform { final id = int.tryParse(parts[0]); if ((pid == null) || (id == null)) return null; - final executable = await _getExecutableName(pid); + final executable = await getExecutableName(pid); if (_filteredWindows.contains(executable)) return null; final process = Process( @@ -89,65 +222,35 @@ class Linux implements NativePlatform { ); final title = parts.sublist(4).join(' '); - return Window(id: id, process: process, title: title); + return Window(id: '$id', process: process, title: title); } - Future _getExecutableName(int pid) async { + Future getExecutableName(int pid) async { final result = await _run('readlink', ['/proc/$pid/exe']); final executable = result.stdout.toString().split('/').last.trim(); return executable; } @override - Future activeWindow() async { - final windowId = await _activeWindowId(); - if (windowId == 0) throw (Exception('No window id')); - - final pid = await _activeWindowPid(windowId); - if (pid == 0) throw (Exception('No pid')); - - final executable = await _getExecutableName(pid); - final process = Process( - pid: pid, - executable: executable, - status: ProcessStatus.unknown, - ); - final windowTitle = await _activeWindowTitle(); - - return Window( - id: windowId, - process: process, - title: windowTitle, - ); + Future checkActiveWindow() async { + final activeWindowService = ActiveWindowService(this, _run); + await activeWindowService.fetch(); } - // Returns the unique hex ID of the active window as reported by xdotool. - Future _activeWindowId() async { - final result = await _run('xdotool', ['getactivewindow']); - final windowId = int.tryParse(result.stdout.toString().trim()); - return windowId ?? 0; - } + // final _activeWindowController = StreamController.broadcast(); + final _activeWindowController = StreamController(); - Future _activeWindowPid(int windowId) async { - final result = await _run( - 'xdotool', - ['getwindowpid', '$windowId'], - ); - final pid = int.tryParse(result.stdout.toString().trim()); - return pid ?? 0; - } + @override + Stream get activeWindowStream => _activeWindowController.stream; - Future _activeWindowTitle() async { - final result = await _run( - 'xdotool', - ['getactivewindow getwindowname'], - ); - return result.stdout.toString().trim(); - } + // Future updateActiveWindow(Window window) async { + // _activeWindowController.add(window); + // } // Verify wmctrl and xdotool are present on the system. @override Future checkDependencies() async { + // TODO: Update for Wayland final xdotoolResult = await _run('bash', [ '-c', 'command -v xdotool >/dev/null 2>&1 || { echo >&2 "xdotool is required but it\'s not installed."; exit 1; }' @@ -175,30 +278,123 @@ Make sure these are installed on your host system.''', return dependenciesAvailable; } - /// Return is `x11` or `wayland` depending on which session type is running. - Future sessionType() async { - final sessionType = io.Platform.environment['XDG_SESSION_TYPE']; - log.i('Current session type: $sessionType'); - return sessionType!; - } - @override - Future minimizeWindow(int windowId) async { + Future minimizeWindow(String windowId) async { log.i('Minimizing window with id $windowId'); + + switch (sessionType.displayProtocol) { + case DisplayProtocol.wayland: + switch (sessionType.environment) { + case DesktopEnvironment.kde: + return await _minimizeWindowWaylandKDE(windowId); + case DesktopEnvironment.gnome: + return Future.error('Minimize not implemented for GNOME Wayland'); + default: + return Future.error( + 'Unknown desktop environment: ${sessionType.environment}'); + } + case DisplayProtocol.x11: + return await _minimizeWindowX11(windowId); + default: + return Future.error('Unknown session type: $sessionType.displayProtocol'); + } + } + + Future _minimizeWindowX11(String windowId) async { final result = await _run( - 'xdotool', - ['windowminimize', '$windowId'], + 'wmctrl', + ['-i', '-r', windowId, '-b', 'add,hidden'], ); return (result.stderr == '') ? true : false; } + static const String kwinMinimizeRestoreScript = ''' +function print(str) { + console.info('Nyrna: ' + str); +} + +let windows = workspace.windowList(); +let targetWindowId = "%windowId%"; +let targetWindow = windows.find(w => w.internalId.toString() === targetWindowId); + +if (!targetWindow) { + print('Window with id ' + targetWindowId + ' not found'); +} + +let shouldMinimize = %minimize%; +targetWindow.minimized = shouldMinimize; + +print('Window with id ' + targetWindowId + ' ' + (shouldMinimize ? 'minimized' : 'restored')); + +if (!shouldMinimize) { + workspace.activeWindow = targetWindow; +} +'''; + + Future _minimizeWindowWaylandKDE(String windowId) async { + // Create a javascript file in the tmp directory, which will be populated with the + // script to minimize the window. + final scriptFile = io.File('${io.Directory.systemTemp.path}/nyrna_minimize.js'); + await scriptFile.writeAsString( + kwinMinimizeRestoreScript + .replaceAll('%windowId%', windowId) + .replaceAll('%minimize%', 'true'), + ); + + // Run the script with kwin. + await kwin.loadScript(scriptFile.path, 'nyrna_minimize'); + return true; + } + @override - Future restoreWindow(int windowId) async { + Future restoreWindow(String windowId) async { log.i('Restoring window with id $windowId'); + + switch (sessionType.displayProtocol) { + case DisplayProtocol.wayland: + switch (sessionType.environment) { + case DesktopEnvironment.kde: + return await _restoreWindowWaylandKDE(windowId); + case DesktopEnvironment.gnome: + return Future.error('Restore not implemented for GNOME Wayland'); + default: + return Future.error( + 'Unknown desktop environment: ${sessionType.environment}'); + } + case DisplayProtocol.x11: + return await _restoreWindowX11(windowId); + default: + return Future.error('Unknown session type: $sessionType.displayProtocol'); + } + } + + Future _restoreWindowX11(String windowId) async { final result = await _run( - 'xdotool', - ['windowactivate', '$windowId'], + 'wmctrl', + ['-i', '-r', windowId, '-b', 'remove,hidden'], ); return (result.stderr == '') ? true : false; } + + Future _restoreWindowWaylandKDE(String windowId) async { + // Create a javascript file in the tmp directory, which will be populated with the + // script to restore the window. + final scriptFile = io.File('${io.Directory.systemTemp.path}/nyrna_restore.js'); + await scriptFile.writeAsString( + kwinMinimizeRestoreScript + .replaceAll('%windowId%', windowId) + .replaceAll('%minimize%', 'false'), + ); + + // Run the script with kwin. + await kwin.loadScript(scriptFile.path, 'nyrna_restore'); + return true; + } + + @override + Future dispose() async { + await kwin.unloadScript(_kdeWaylandScriptName); + await kwin.dispose(); + await nyrnaDbus.dispose(); + } } diff --git a/lib/native_platform/src/linux/session_type.dart b/lib/native_platform/src/linux/session_type.dart new file mode 100644 index 00000000..511020cb --- /dev/null +++ b/lib/native_platform/src/linux/session_type.dart @@ -0,0 +1,80 @@ +import 'dart:io' as io; + +import '../../../logs/logging_manager.dart'; + +/// Enum representing the display protocol used by the Linux session. +enum DisplayProtocol { + wayland, + x11, + unknown; + + /// Create DisplayProtocol from string. + static DisplayProtocol fromString(String protocol) { + return DisplayProtocol.values.firstWhere( + (e) => e.name.toLowerCase() == protocol.toLowerCase(), + orElse: () => throw ArgumentError('Invalid display protocol: $protocol'), + ); + } +} + +/// Enum representing the Desktop Environment. +enum DesktopEnvironment { + /// KDE Plasma desktop environment. + kde, + + /// GNOME desktop environment. + gnome, + + /// XFCE desktop environment. + xfce, + + /// Unknown desktop environment. + unknown; + + /// Create DesktopEnvironment from string. + static DesktopEnvironment fromString(String environment) { + return DesktopEnvironment.values.firstWhere( + (e) => e.name.toLowerCase() == environment.toLowerCase(), + orElse: () => DesktopEnvironment.unknown, + ); + } +} + +/// Information about the current Linux desktop session. +class SessionType { + const SessionType({ + required this.displayProtocol, + required this.environment, + }); + + final DisplayProtocol displayProtocol; + final DesktopEnvironment environment; + + /// Create a SessionType from environment variables. + static Future fromEnvironment() async { + final displayProtocolEnv = io.Platform.environment['XDG_SESSION_TYPE']; + if (displayProtocolEnv == null || displayProtocolEnv.isEmpty) { + log.e('XDG_SESSION_TYPE is not set'); + return Future.error('XDG_SESSION_TYPE is not set'); + } + + final displayProtocol = DisplayProtocol.fromString(displayProtocolEnv); + + final environmentEnv = io.Platform.environment['XDG_CURRENT_DESKTOP']; + if (environmentEnv == null || environmentEnv.isEmpty) { + log.e('XDG_CURRENT_DESKTOP is not set'); + return Future.error('XDG_CURRENT_DESKTOP is not set'); + } + + final environment = DesktopEnvironment.fromString(environmentEnv); + return SessionType( + displayProtocol: displayProtocol, + environment: environment, + ); + } + + @override + String toString() { + return 'SessionType(displayProtocol: $displayProtocol, environment: $environment)'; + } +} diff --git a/lib/native_platform/src/native_platform.dart b/lib/native_platform/src/native_platform.dart index d29ee694..8a942bc4 100644 --- a/lib/native_platform/src/native_platform.dart +++ b/lib/native_platform/src/native_platform.dart @@ -1,8 +1,8 @@ import 'dart:io' as io; import 'package:helpers/helpers.dart'; +import 'package:path_provider/path_provider.dart'; -import 'linux/flatpak.dart'; import 'linux/linux.dart'; import 'win32/win32.dart'; import 'window.dart'; @@ -13,10 +13,11 @@ import 'window.dart'; /// Used by [Linux] and [Win32]. abstract class NativePlatform { // Return correct subtype depending on the current operating system. - factory NativePlatform() { + static Future initialize() async { if (io.Platform.isLinux) { final runFunction = (runningInFlatpak()) ? flatpakRun : io.Process.run; - return Linux(runFunction); + final kdeWaylandScriptPath = await _getKdeWaylandScriptPath(); + return await Linux.initialize(runFunction, kdeWaylandScriptPath); } else { return Win32(); } @@ -32,14 +33,41 @@ abstract class NativePlatform { Future> windows({bool showHidden}); /// The active, foreground window. - Future activeWindow(); + Window? activeWindow; + + /// Update our knowledge of the active window. + /// + /// Active window will be emitted on the [activeWindowStream]. + Future checkActiveWindow(); + + /// Stream of the active window. + Stream get activeWindowStream; /// Verify dependencies are present on the system. Future checkDependencies(); /// Minimize the window with the given [windowId]. - Future minimizeWindow(int windowId); + Future minimizeWindow(String windowId); /// Restore / unminimize the window with the given [windowId]. - Future restoreWindow(int windowId); + Future restoreWindow(String windowId); + + /// Safely dispose of resources when done. + Future dispose(); +} + +/// Get the path to the KDE Wayland script. +/// +/// The script will be kept in the application support directory so we can provide the +/// path to it over D-Bus, which can't be done whhen the script is in the app bundle. +/// +/// The script will be copied over on every launch to ensure it's up-to-date. +Future _getKdeWaylandScriptPath() async { + if (io.Platform.environment['FLUTTER_TEST'] == 'true') return ''; + + final dataDir = await getApplicationSupportDirectory(); + final tempFile = await assetToTempDir('assets/lib/linux/list_windows_kde.js'); + final file = io.File('${dataDir.path}${io.Platform.pathSeparator}list_windows_kde.js'); + await tempFile.copy(file.path); + return file.path; } diff --git a/lib/native_platform/src/process/repository/process_repository.dart b/lib/native_platform/src/process/repository/process_repository.dart index 5b7ea574..e386869c 100644 --- a/lib/native_platform/src/process/repository/process_repository.dart +++ b/lib/native_platform/src/process/repository/process_repository.dart @@ -2,7 +2,7 @@ import 'dart:io' as io; import 'package:helpers/helpers.dart'; -import '../../linux/flatpak.dart'; +import '../../linux/linux.dart'; import '../models/process.dart'; import 'src/linux_process_repository.dart'; import 'src/win32_process_repository.dart'; diff --git a/lib/native_platform/src/win32/win32.dart b/lib/native_platform/src/win32/win32.dart index 0f2f51e8..0eb1f99b 100644 --- a/lib/native_platform/src/win32/win32.dart +++ b/lib/native_platform/src/win32/win32.dart @@ -1,5 +1,6 @@ // ignore_for_file: constant_identifier_names +import 'dart:async'; import 'dart:ffi'; import 'package:ffi/ffi.dart'; @@ -43,18 +44,40 @@ class Win32 implements NativePlatform { } @override - Future activeWindow() async { + Window? activeWindow; + + /// Win32 uses this signal and slot system rather than a function that returns directly + /// because it inherits from the abstract class [NativePlatform], and the Linux class + /// needs to use a signal and slot system. + final _activeWindowController = StreamController.broadcast(); + + @override + Stream get activeWindowStream => _activeWindowController.stream; + + @override + Future checkActiveWindow() async { final windowId = await _activeWindowId(); final pid = await _pidFromWindowId(windowId); final executable = await getExecutableName(pid); + final process = Process( pid: pid, executable: executable, status: ProcessStatus.unknown, ); + final title = getWindowTitle(windowId); - return Window(id: windowId, process: process, title: title); + // return Window(id: windowId.toString(), process: process, title: title); + + final window = Window( + id: windowId.toString(), + process: process, + title: title, + ); + + activeWindow = window; + // _activeWindowController.add(window); } Future _activeWindowId() async => GetForegroundWindow(); @@ -100,16 +123,16 @@ class Win32 implements NativePlatform { } @override - Future minimizeWindow(int windowId) async { + Future minimizeWindow(String windowId) async { log.i('Minimizing window with id $windowId'); - ShowWindow(windowId, SHOW_WINDOW_CMD.SW_FORCEMINIMIZE); + ShowWindow(int.parse(windowId), SHOW_WINDOW_CMD.SW_FORCEMINIMIZE); return true; // [ShowWindow] return value doesn't confirm success. } @override - Future restoreWindow(int windowId) async { + Future restoreWindow(String windowId) async { log.i('Restoring window with id $windowId'); - ShowWindow(windowId, SHOW_WINDOW_CMD.SW_RESTORE); + ShowWindow(int.parse(windowId), SHOW_WINDOW_CMD.SW_RESTORE); return true; // [ShowWindow] return value doesn't confirm success. } @@ -147,6 +170,13 @@ class Win32 implements NativePlatform { return executable; } + + @override + Future dispose() async { + // No cleanup needed. + } + + } // Static methods required because the win32 callback is required to be static. @@ -171,7 +201,7 @@ class WindowBuilder { final correctedWindows = []; for (var window in _windows) { - final process = await Win32().processFromWindowId(window.id); + final process = await Win32().processFromWindowId(int.parse(window.id)); if (_filteredWindows.contains(process.executable)) continue; @@ -212,7 +242,7 @@ class WindowBuilder { _windows.add( Window( - id: hWnd, + id: hWnd.toString(), process: const Process( executable: '', pid: 0, diff --git a/lib/native_platform/src/window.dart b/lib/native_platform/src/window.dart index 98a86a3e..63bdd53c 100644 --- a/lib/native_platform/src/window.dart +++ b/lib/native_platform/src/window.dart @@ -9,7 +9,9 @@ part 'window.freezed.dart'; class Window with _$Window { const factory Window({ /// The unique window ID number associated with this window. - required int id, + /// + /// Can be either a number or a UUID (e.g. on KDE Wayland). + required String id, /// The process associated with this window. required Process process, diff --git a/lib/settings/widgets/integration_section.dart b/lib/settings/widgets/integration_section.dart index 0b6a7ecc..27b2ba35 100644 --- a/lib/settings/widgets/integration_section.dart +++ b/lib/settings/widgets/integration_section.dart @@ -3,8 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; +import '../../app/app.dart'; import '../../apps_list/apps_list.dart'; import '../../hotkey/global/hotkey_service.dart'; +import '../../native_platform/src/linux/linux.dart'; import '../../theme/styles.dart'; import '../settings.dart'; @@ -119,22 +121,31 @@ class _HotkeyConfigWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( - title: const Text('Hotkey'), - leading: const Icon(Icons.keyboard), - trailing: ElevatedButton( - onPressed: () => showDialog( - context: context, - builder: (context) => _RecordHotKeyDialog( - initialHotkey: settingsCubit.state.hotKey, + return BlocBuilder( + builder: (context, state) { + if (state.sessionType?.displayProtocol == DisplayProtocol.wayland) { + // Wayland does not support global hotkeys. + return const SizedBox(); + } + + return ListTile( + title: const Text('Hotkey'), + leading: const Icon(Icons.keyboard), + trailing: ElevatedButton( + onPressed: () => showDialog( + context: context, + builder: (context) => _RecordHotKeyDialog( + initialHotkey: settingsCubit.state.hotKey, + ), + ), + child: BlocBuilder( + builder: (context, state) { + return Text(state.hotKey.toStringHelper()); + }, + ), ), - ), - child: BlocBuilder( - builder: (context, state) { - return Text(state.hotKey.toStringHelper()); - }, - ), - ), + ); + }, ); } } @@ -232,49 +243,58 @@ class _AppSpecificHotkeys extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - return Card( - child: Column( - children: [ - const ListTile( - title: Text('App specific hotkeys'), - leading: Icon(Icons.keyboard), - trailing: Tooltip( - message: - 'Hotkeys to directly toggle suspend/resume for specific apps, even when they are not focused.', - child: Icon(Icons.help_outline), - ), - ), - for (var hotkey in state.appSpecificHotKeys) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Card( - elevation: 2, - child: ListTile( - leading: Text(hotkey.hotkey.toStringHelper()), - title: Text(hotkey.executable), - trailing: ElevatedButton( - onPressed: () => settingsCubit.removeAppSpecificHotkey( - hotkey.executable, + if (state.sessionType?.displayProtocol == DisplayProtocol.wayland) { + // Wayland does not support global hotkeys. + return const SizedBox(); + } + + return BlocBuilder( + builder: (context, state) { + return Card( + child: Column( + children: [ + const ListTile( + title: Text('App specific hotkeys'), + leading: Icon(Icons.keyboard), + trailing: Tooltip( + message: + 'Hotkeys to directly toggle suspend/resume for specific apps, even when they are not focused.', + child: Icon(Icons.help_outline), + ), + ), + for (var hotkey in state.appSpecificHotKeys) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Card( + elevation: 2, + child: ListTile( + leading: Text(hotkey.hotkey.toStringHelper()), + title: Text(hotkey.executable), + trailing: ElevatedButton( + onPressed: () => settingsCubit.removeAppSpecificHotkey( + hotkey.executable, + ), + child: const Icon(Icons.delete), + ), ), - child: const Icon(Icons.delete), ), ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const _AddAppSpecificHotkeyDialog(), + ); + }, + child: const Icon(Icons.add), ), - ), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => const _AddAppSpecificHotkeyDialog(), - ); - }, - child: const Icon(Icons.add), + const SizedBox(height: 10), + ], ), - const SizedBox(height: 10), - ], - ), + ); + }, ); }, ); diff --git a/pubspec.lock b/pubspec.lock index a7c7d367..4ffc83f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,47 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "7.4.5" archive: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: "direct main" description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -58,58 +53,58 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -122,18 +117,18 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.10.1" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -142,38 +137,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" console: dependency: transitive description: @@ -186,74 +189,74 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + sha256: "4b8701e48a58f7712492c9b1f7ba0bb9d525644dd66d023b62e1fc8cdb560c8a" url: "https://pub.dev" source: hosted - version: "1.9.2" + version: "1.14.0" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" dart_style: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.1.0" dbus: - dependency: transitive + dependency: "direct main" description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -372,10 +375,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -409,10 +412,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -467,26 +470,26 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.5.4" integration_test: dependency: "direct dev" description: flutter @@ -496,26 +499,26 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -528,10 +531,19 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.5" + kwin: + dependency: "direct main" + description: + path: "." + ref: a52b403ef2b8e087add9670ad55ebb6e81dfa12b + resolved-ref: a52b403ef2b8e087add9670ad55ebb6e81dfa12b + url: "https://github.com/Merrit/kwin-dart.git" + source: git + version: "0.1.0" launch_at_startup: dependency: "direct main" description: @@ -544,18 +556,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -576,42 +588,34 @@ packages: dependency: "direct main" description: name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" + version: "1.3.0" markdown: dependency: transitive description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -632,10 +636,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -648,18 +652,18 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a" url: "https://pub.dev" source: hosted - version: "5.4.4" + version: "5.4.6" msix: dependency: "direct dev" description: name: msix - sha256: c50d6bd1aafe0d071a3c1e5a5ccb056404502935cb0a549e3178c4aae16caf33 + sha256: edde648a8133bf301883c869d19d127049683037c65ff64173ba526ac7a8af2f url: "https://pub.dev" source: hosted - version: "3.16.8" + version: "3.16.9" nested: dependency: transitive description: @@ -680,10 +684,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -704,34 +708,34 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -760,18 +764,18 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -820,38 +824,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" process: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" provider: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: "direct main" description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" screen_retriever: dependency: transitive description: @@ -864,10 +876,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -888,10 +900,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" shortid: dependency: transitive description: @@ -904,23 +916,23 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -933,50 +945,50 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -989,98 +1001,98 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.7" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.8" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tray_manager: dependency: "direct main" description: name: tray_manager - sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7 + sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.3.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1101,10 +1113,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: @@ -1125,18 +1137,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -1157,10 +1169,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.1.0" webkit_inspection_protocol: dependency: transitive description: @@ -1173,10 +1185,10 @@ packages: dependency: "direct main" description: name: win32 - sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.5.5" + version: "5.13.0" win32_registry: dependency: transitive description: @@ -1214,18 +1226,18 @@ packages: dependency: "direct main" description: name: xdg_desktop_portal - sha256: "8a630ea1ebb7d1a9733d0cf6d159839f427d10322776de0c93e7951f62975c8d" + sha256: "10d56d2212bfa33a676d209b312a82fe5862fed5a91d2cfcb39143da75d084c1" url: "https://pub.dev" source: hosted - version: "0.1.12" + version: "0.1.13" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1238,10 +1250,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index c35e6bff..7d615fda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: args: ^2.2.0 collection: ^1.16.0 + dbus: ^0.7.10 ffi: ^2.0.1 flutter: sdk: flutter @@ -36,12 +37,16 @@ dependencies: http: ^1.1.0 intl: any json_annotation: ^4.8.1 + kwin: + git: + url: https://github.com/Merrit/kwin-dart.git + ref: a52b403ef2b8e087add9670ad55ebb6e81dfa12b launch_at_startup: ^0.2.2 logger: ^2.0.1 package_info_plus: ^5.0.1 path_provider: ^2.0.1 pub_semver: ^2.0.0 - tray_manager: ^0.2.2 + tray_manager: ^0.3.2 url_launcher: ^6.0.4 win32: ^5.0.5 win32_suspend_process: ^1.1.0 @@ -75,6 +80,7 @@ flutter: generate: true assets: - assets/icons/ + - assets/lib/linux/ - assets/lib/windows/ - packaging/linux/codes.merritt.Nyrna.desktop diff --git a/test/active_window/src/active_window_test.dart b/test/active_window/src/active_window_test.dart index 4047ef4b..ca824023 100644 --- a/test/active_window/src/active_window_test.dart +++ b/test/active_window/src/active_window_test.dart @@ -28,7 +28,7 @@ const testProcess = Process( ); const testWindow = Window( - id: 130023427, + id: '130023427', process: testProcess, title: 'Untitled-2 - Visual Studio Code - Insiders', ); @@ -59,7 +59,7 @@ void main() { when(appWindow.hide()).thenAnswer((_) async => true); // NativePlatform - when(nativePlatform.activeWindow()).thenAnswer((_) async => testWindow); + when(nativePlatform.activeWindow).thenReturn(testWindow); when(nativePlatform.minimizeWindow(any)).thenAnswer((_) async => true); when(nativePlatform.restoreWindow(any)).thenAnswer((_) async => true); @@ -109,7 +109,7 @@ void main() { test('explorer.exe executable aborts on Win32', () async { debugDefaultTargetPlatformOverride = TargetPlatform.windows; - when(nativePlatform.activeWindow()).thenAnswer((_) async => testWindow.copyWith( + when(nativePlatform.activeWindow).thenReturn(testWindow.copyWith( process: testProcess.copyWith(executable: 'explorer.exe'), )); final successful = await activeWindow.toggle(); @@ -130,13 +130,14 @@ void main() { final nyrnaWindow = testWindow.copyWith( process: testProcess.copyWith(executable: 'nyrna'), ); - when(nativePlatform.activeWindow()).thenAnswerInOrder([ - Future.value(nyrnaWindow), - Future.value(testWindow), + when(nativePlatform.activeWindow).thenReturnInOrder([ + nyrnaWindow, + testWindow, ]); + await nativePlatform.checkActiveWindow(); final successful = await activeWindow.toggle(); expect(successful, true); - verify(nativePlatform.activeWindow()).called(2); + verify(nativePlatform.checkActiveWindow()).called(2); verify(appWindow.hide()).called(1); }); @@ -145,13 +146,14 @@ void main() { final nyrnaWindow = testWindow.copyWith( process: testProcess.copyWith(executable: 'nyrna.exe'), ); - when(nativePlatform.activeWindow()).thenAnswerInOrder([ - Future.value(nyrnaWindow), - Future.value(testWindow), + when(nativePlatform.activeWindow).thenReturnInOrder([ + nyrnaWindow, + testWindow, ]); + await nativePlatform.checkActiveWindow(); final successful = await activeWindow.toggle(); expect(successful, true); - verify(nativePlatform.activeWindow()).called(2); + verify(nativePlatform.checkActiveWindow()).called(2); verify(appWindow.hide()).called(1); }); diff --git a/test/apps_list/cubit/apps_list_cubit_test.dart b/test/apps_list/cubit/apps_list_cubit_test.dart index d31dd90d..d9c137ab 100644 --- a/test/apps_list/cubit/apps_list_cubit_test.dart +++ b/test/apps_list/cubit/apps_list_cubit_test.dart @@ -35,7 +35,7 @@ const msPaintProcess = Process( ); const msPaintWindow = Window( - id: 132334, + id: '132334', process: msPaintProcess, title: 'Untitled - Paint', ); @@ -45,7 +45,7 @@ Window get msPaintWindowState => state // .singleWhere((element) => element.id == msPaintWindow.id); const mpvWindow1 = Window( - id: 180355074, + id: '180355074', process: Process( executable: 'mpv', pid: 1355281, @@ -59,7 +59,7 @@ Window get mpvWindow1State => state // .singleWhere((element) => element.id == mpvWindow1.id); const mpvWindow2 = Window( - id: 197132290, + id: '197132290', process: Process( executable: 'mpv', pid: 1355477, @@ -206,7 +206,7 @@ void main() { when(nativePlatform.windows(showHidden: anyNamed('showHidden'))) .thenAnswer((_) async => [ const Window( - id: 7363, + id: '7363', process: Process( executable: 'kate', pid: 836482, @@ -215,7 +215,7 @@ void main() { title: 'Kate', ), const Window( - id: 29347, + id: '29347', process: Process( executable: 'evince', pid: 94847, @@ -224,7 +224,7 @@ void main() { title: 'Evince', ), const Window( - id: 89374, + id: '89374', process: Process( executable: 'ark', pid: 9374623, diff --git a/test/apps_list/widgets/window_tile_test.dart b/test/apps_list/widgets/window_tile_test.dart index cf1a193a..8fe8e863 100644 --- a/test/apps_list/widgets/window_tile_test.dart +++ b/test/apps_list/widgets/window_tile_test.dart @@ -35,7 +35,7 @@ final mockStorageRepository = MockStorageRepository(); final mockSystemTrayManager = MockSystemTrayManager(); const defaultTestWindow = Window( - id: 548331, + id: '548331', process: Process( executable: 'firefox-bin', pid: 8749655, diff --git a/test/native_platform/src/linux/linux_test.dart b/test/native_platform/src/linux/linux_test.dart index 32ec3701..f67f0d32 100644 --- a/test/native_platform/src/linux/linux_test.dart +++ b/test/native_platform/src/linux/linux_test.dart @@ -1,17 +1,34 @@ import 'dart:io'; +import 'package:kwin/kwin.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:nyrna/logs/logs.dart'; import 'package:nyrna/native_platform/native_platform.dart'; +import 'package:nyrna/native_platform/src/linux/dbus/nyrna_dbus.dart'; import 'package:nyrna/native_platform/src/linux/linux.dart'; import 'package:nyrna/native_platform/src/typedefs.dart'; import 'package:test/test.dart'; +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +import 'linux_test.mocks.dart'; + +var mockKWin = MockKWin(); +var mockNyrnaDbus = MockNyrnaDbus(); + late RunFunction mockRun; final stubSuccessfulProcessResult = ProcessResult(0, 0, '', ''); final stubFailureProcessResult = ProcessResult(0, 1, '', 'error'); void main() { + if (Platform.operatingSystem != 'linux') { + return; + } + setUpAll(() async { await LoggingManager.initialize(verbose: false); }); @@ -20,6 +37,9 @@ void main() { mockRun = (String executable, List args) async { return ProcessResult(1, 1, '', ''); }; + + reset(mockKWin); + reset(mockNyrnaDbus); }); group('Linux:', () { @@ -31,7 +51,7 @@ void main() { 1 * DG: 8948x2873 VP: 0,0 WA: 0,0 8948x2420 Workspace 2'''; return ProcessResult(982333, 0, wmctrlReturnValue, ''); }); - Linux linux = Linux(mockRun); + Linux linux = await Linux.initialize(mockRun); int desktop = await linux.currentDesktop(); expect(desktop, 1); @@ -41,13 +61,18 @@ void main() { 1 - DG: 8948x2873 VP: 0,0 WA: 0,0 8948x2420 Workspace 2'''; return ProcessResult(982333, 0, wmctrlReturnValue, ''); }); - linux = Linux(mockRun); + linux = await Linux.initialize(mockRun); desktop = await linux.currentDesktop(); expect(desktop, 0); }); }); - test('windows() returns appropriate list of Window objects', () async { + test('windows() returns appropriate list of Window objects on x11', () async { + if (Platform.environment['XDG_SESSION_TYPE'] != 'x11') { + return; + } + + // Mock the run function to return the expected wmctrl output for x11. mockRun = ((executable, args) async { const wmctrlReturnValue = ''' 0x0640003e 0 8062 shodan Muesli - Wikipedia — Mozilla Firefox @@ -77,11 +102,13 @@ void main() { return ProcessResult(982333, 0, wmctrlReturnValue, ''); }); - final linux = Linux(mockRun); + + final linux = await Linux.initialize(mockRun); final windows = await linux.windows(); + final expected = [ const Window( - id: 104857662, + id: '104857662', process: Process( executable: 'firefox-bin', pid: 8062, @@ -90,7 +117,7 @@ void main() { title: 'Muesli - Wikipedia — Mozilla Firefox', ), const Window( - id: 41943046, + id: '41943046', process: Process( executable: 'Telegram', pid: 140564, @@ -99,7 +126,7 @@ void main() { title: 'Telegram (4)', ), const Window( - id: 48234538, + id: '48234538', process: Process( executable: 'nautilus', pid: 157040, @@ -111,6 +138,75 @@ void main() { expect(windows, expected); }); + test('windows() returns appropriate list of Window objects on wayland', () async { + if (Platform.environment['XDG_SESSION_TYPE'] != 'wayland') { + return; + } + + const mockWindowJsonFromKdeWaylandScript = + '[{"caption":"Wayland to X Recording bridge — Xwayland Video Bridge","pid":4962,"internalId":"{008b967c-58df-4128-ab4b-008acec9c4c8}","onCurrentDesktop":true},{"caption":"Muesli - Wikipedia — Mozilla Firefox","pid":7284,"internalId":"{b0c70d8b-07ae-4ee6-9d49-78a930adef3e}","onCurrentDesktop":true},{"caption":"Home — Dolphin","pid":627076,"internalId":"{d36459ad-bb52-44da-906e-990a393a9c8b}","onCurrentDesktop":true},{"caption":"doctor.cpp - libkscreen - Visual Studio Code","pid":945351,"internalId":"{efe8603d-5049-4451-8957-ed2e8eec5e04}","onCurrentDesktop":true},{"caption":"","pid":4419,"internalId":"{c892b0e3-ff47-4f77-b83c-5eef80104601}","onCurrentDesktop":true},{"caption":"QDBusViewer","pid":417686,"internalId":"{962a58dc-ee64-4223-b450-c2ba7861fc89}","onCurrentDesktop":false}]'; + + when(mockNyrnaDbus.windowsJson).thenReturn(mockWindowJsonFromKdeWaylandScript); + + mockRun = ((executable, args) async { + String returnValue = ''; + if (executable == 'readlink') { + switch (args.first) { + case '/proc/4962/exe': + returnValue = '/usr/bin/xwaylandvideobridge'; + case '/proc/7284/exe': + returnValue = '/usr/lib64/firefox/firefox'; + case '/proc/627076/exe': + returnValue = '/usr/bin/dolphin'; + case '/proc/945351/exe': + returnValue = '/usr/share/code/code'; + case '/proc/4419/exe': + returnValue = '/usr/bin/plasmashell'; + case '/proc/417686/exe': + returnValue = '/usr/bin/qdbusviewer'; + default: + returnValue = ''; + } + } + return ProcessResult(0, 0, returnValue, ''); + }); + + final linux = await Linux.initialize(mockRun, '', mockKWin, mockNyrnaDbus); + final windows = await linux.windows(); + + final expected = [ + const Window( + id: '{b0c70d8b-07ae-4ee6-9d49-78a930adef3e}', + process: Process( + executable: 'firefox', + pid: 7284, + status: ProcessStatus.unknown, + ), + title: 'Muesli - Wikipedia — Mozilla Firefox', + ), + const Window( + id: '{d36459ad-bb52-44da-906e-990a393a9c8b}', + process: Process( + executable: 'dolphin', + pid: 627076, + status: ProcessStatus.unknown, + ), + title: 'Home — Dolphin', + ), + const Window( + id: '{efe8603d-5049-4451-8957-ed2e8eec5e04}', + process: Process( + executable: 'code', + pid: 945351, + status: ProcessStatus.unknown, + ), + title: 'doctor.cpp - libkscreen - Visual Studio Code', + ), + ]; + + expect(windows, expected); + }); + group('checkDependencies:', () { test('finds dependencies when present', () async { mockRun = ((executable, args) async { @@ -119,7 +215,7 @@ void main() { : stubFailureProcessResult; }); - final linux = Linux(mockRun); + final linux = await Linux.initialize(mockRun); final haveDependencies = await linux.checkDependencies(); expect(haveDependencies, true); }); @@ -131,7 +227,7 @@ void main() { : stubFailureProcessResult; }); - final linux = Linux(mockRun); + final linux = await Linux.initialize(mockRun); final haveDependencies = await linux.checkDependencies(); expect(haveDependencies, false); }); @@ -143,7 +239,7 @@ void main() { : stubFailureProcessResult; }); - final linux = Linux(mockRun); + final linux = await Linux.initialize(mockRun); final haveDependencies = await linux.checkDependencies(); expect(haveDependencies, false); }); diff --git a/test/native_platform/src/native_platform_test.dart b/test/native_platform/src/native_platform_test.dart index f8f6fe80..f1b38341 100644 --- a/test/native_platform/src/native_platform_test.dart +++ b/test/native_platform/src/native_platform_test.dart @@ -1,12 +1,14 @@ import 'dart:io' as io; +import 'package:nyrna/logs/logging_manager.dart'; import 'package:nyrna/native_platform/native_platform.dart'; import 'package:test/test.dart'; import 'skip_github.dart'; -void main() { - final platform = NativePlatform(); +Future main() async { + await LoggingManager.initialize(verbose: false); + final platform = await NativePlatform.initialize(); group('NativePlatform:', () { if (runningInCI) return; @@ -24,8 +26,10 @@ void main() { return; } - final activeWindow = await platform.activeWindow(); - expect(activeWindow.id, isPositive); + final activeWindow = platform.activeWindow; + await platform.checkActiveWindow(); + expect(activeWindow, isA()); + expect(activeWindow!.id, isPositive); expect(activeWindow.process.pid, isPositive); }); }); diff --git a/test/native_platform/src/window_test.dart b/test/native_platform/src/window_test.dart index c02dd685..c189f6e6 100644 --- a/test/native_platform/src/window_test.dart +++ b/test/native_platform/src/window_test.dart @@ -2,7 +2,7 @@ import 'package:nyrna/native_platform/native_platform.dart'; import 'package:test/test.dart'; void main() { - const fakeId = 8172363; + const fakeId = '8172363'; const fakeExecutable = 'firefox'; const fakePid = 1723128; const fakeProcess = Process(