Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
matrix:
# os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
"editor.rulers": [
90
]
}
},
"cmake.ignoreCMakeListsMissing": true
}
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "packaging/**" # flatpak-builder dir will kill the analyzer

formatter:
trailing_commas: preserve
Expand Down
43 changes: 43 additions & 0 deletions assets/lib/linux/active_window_kde.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
function print(str) {
console.info('Nyrna KDE Wayland: ' + str);
}

print('Setting up persistent active window listener');

function windowToJson(window) {
return JSON.stringify({
caption: window.caption,
pid: window.pid,
internalId: window.internalId,
});
}

function sendActiveWindowUpdate(window) {
if (!window) {
print('No active window');
return;
}

let windowJson = windowToJson(window);

callDBus(
'codes.merritt.Nyrna',
'/',
'codes.merritt.Nyrna',
'updateActiveWindow',
windowJson,
(result) => {
if (result) {
print('Updated active window on DBus');
} else {
print('Failed to update active window on DBus');
}
}
);
}

// Send the current active window immediately on load.
sendActiveWindowUpdate(workspace.activeWindow);

// Listen for every subsequent focus change.
workspace.windowActivated.connect(sendActiveWindowUpdate);
73 changes: 73 additions & 0 deletions assets/lib/linux/list_windows_kde.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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;
}

updateWindowsOnDBus(windows);

workspace.currentDesktopChanged.connect(() => {
print('Current desktop changed');
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);
});
3 changes: 3 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -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:
21 changes: 18 additions & 3 deletions lib/active_window/src/active_window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,28 @@ class ActiveWindow {
log.i('Suspending');

for (int attempt = 0; attempt < _maxRetries; attempt++) {
final window = await _nativePlatform.activeWindow();
await _nativePlatform.checkActiveWindow();
final window = _nativePlatform.activeWindow;

if (window == null) {
log.w('No active window found, retrying.');
if (attempt < _maxRetries - 1) {
// checkActiveWindow() already waits internally (KDE Wayland uses a
// polling loop with a 2-second timeout; X11 is synchronous), so no
// additional delay is needed here.
continue;
}
log.e('Failed to find active window after $_maxRetries attempts.');
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));
// checkActiveWindow() will be called at the top of the next iteration.
continue;
}

Expand Down Expand Up @@ -141,7 +156,7 @@ class ActiveWindow {
return false;
}

Future<void> _minimize(int windowId) async {
Future<void> _minimize(String windowId) async {
final shouldMinimize = await _getShouldMinimize();
if (!shouldMinimize) return;

Expand All @@ -150,7 +165,7 @@ class ActiveWindow {
if (!minimized) log.e('Failed to minimize window.');
}

Future<void> _restore(int windowId) async {
Future<void> _restore(String windowId) async {
final shouldRestore = await _getShouldMinimize();
if (!shouldRestore) return;

Expand Down
37 changes: 25 additions & 12 deletions lib/app/cubit/app_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:url_launcher/url_launcher.dart' as url_launcher;

import '../../logs/logs.dart';
import '../../native_platform/native_platform.dart';
import '../../native_platform/src/linux/linux.dart';
import '../../storage/storage_repository.dart';
import '../../system_tray/system_tray.dart';
import '../../updates/updates.dart';
Expand Down Expand Up @@ -58,7 +57,7 @@ class AppCubit extends Cubit<AppState> {
/// blocking the UI, since none of the data fetched here is critical.
Future<void> _init() async {
await _checkForFirstRun();
await _checkLinuxSessionType();
_checkLinuxSessionType();
await _fetchVersionData();
await _fetchReleaseNotes();
_listenToSystemTrayEvents();
Expand All @@ -74,20 +73,21 @@ class AppCubit extends Cubit<AppState> {
}

/// For Linux, checks if the session type is Wayland.
Future<void> _checkLinuxSessionType() async {
void _checkLinuxSessionType() {
if (defaultTargetPlatform != TargetPlatform.linux) return;

final sessionType = await (_nativePlatform as Linux).sessionType();
final sessionType = _nativePlatform.sessionType;
if (sessionType == null) return;

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:

Expand All @@ -107,12 +107,19 @@ env QT_QPA_PLATFORM=xcb <app>

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);
Expand Down Expand Up @@ -211,4 +218,10 @@ Otherwise, [consider signing in using X11 instead](https://docs.fedoraproject.or
return false;
}
}

@override
Future<void> close() async {
await _nativePlatform.dispose();
await super.close();
}
}
5 changes: 5 additions & 0 deletions lib/app/cubit/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ abstract 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,
Expand Down
1 change: 1 addition & 0 deletions lib/apps_list/cubit/apps_list_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class AppsListCubit extends Cubit<AppsListState> {
_storage,
);

await _nativePlatform.checkActiveWindow();
return await activeWindow.toggle();
}

Expand Down
2 changes: 1 addition & 1 deletion lib/apps_list/models/interaction_error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/loading/cubit/loading_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ part 'loading_cubit.freezed.dart';
class LoadingCubit extends Cubit<LoadingState> {
final NativePlatform nativePlatform;

LoadingCubit() : nativePlatform = NativePlatform(), super(const LoadingInitial()) {
LoadingCubit(this.nativePlatform) : super(const LoadingInitial()) {
checkDependencies();
}

Expand Down
Loading
Loading