Skip to content

Commit

Permalink
fix: hotkey getting Nyrna for active window
Browse files Browse the repository at this point in the history
  • Loading branch information
Merrit committed Sep 25, 2024
1 parent 3221e1e commit e4cd7fa
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 42 deletions.
89 changes: 51 additions & 38 deletions lib/active_window/src/active_window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ import '../../argument_parser/argument_parser.dart';
import '../../logs/logs.dart';
import '../../native_platform/native_platform.dart';
import '../../storage/storage_repository.dart';
import '../../window/app_window.dart';

/// Manage the active window.
///
/// We use extra logging here in order to debug issues since this has
/// no user interface.
class ActiveWindow {
final AppWindow _appWindow;
final NativePlatform _nativePlatform;
final ProcessRepository _processRepository;
final StorageRepository _storageRepository;

const ActiveWindow(
this._appWindow,
this._nativePlatform,
this._processRepository,
this._storageRepository,
);

/// Maximum number of retries to suspend the active window.
static const int _maxRetries = 3;

/// Toggle suspend / resume for the active, foreground window.
Future<bool> toggle() async {
log.i('Toggling active window.');
Expand Down Expand Up @@ -79,53 +85,60 @@ class ActiveWindow {
Future<bool> _suspend() async {
log.i('Suspending');

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

final String executable = window.process.executable;
if (executable == 'nyrna' || executable == 'nyrna.exe') {
log.w('Active window is Nyrna, not suspending.');
return false;
}
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));
continue;
}

log.i('Active window: $window');
log.i('Active window: $window');

if (defaultTargetPlatform == TargetPlatform.windows) {
// Once in a blue moon on Windows we get "explorer.exe" as the active
// window, even when no file explorer windows are open / the desktop
// is not the active element, etc. So we filter it just in case.
if (window.process.executable == 'explorer.exe') {
log.e('Only got explorer as active window!');
return false;
if (defaultTargetPlatform == TargetPlatform.windows) {
// Once in a blue moon on Windows we get "explorer.exe" as the active
// window, even when no file explorer windows are open / the desktop
// is not the active element, etc. So we filter it just in case.
if (window.process.executable == 'explorer.exe') {
log.e('Only got explorer as active window!');
return false;
}
}
}

await _minimize(window.id);
await _minimize(window.id);

// Small delay on Windows to ensure the window actually minimizes.
// Doesn't seem to be necessary on Linux.
if (defaultTargetPlatform == TargetPlatform.windows) {
await Future.delayed(const Duration(milliseconds: 500));
}
// Small delay on Windows to ensure the window actually minimizes.
// Doesn't seem to be necessary on Linux.
if (defaultTargetPlatform == TargetPlatform.windows) {
await Future.delayed(const Duration(milliseconds: 500));
}

final suspended = await _processRepository.suspend(window.process.pid);
if (!suspended) {
log.e('Failed to suspend active window.');
return false;
}
final suspended = await _processRepository.suspend(window.process.pid);
if (!suspended) {
log.e('Failed to suspend active window.');
return false;
}

await _storageRepository.saveValue(
key: 'pid',
value: window.process.pid,
storageArea: 'activeWindow',
);
await _storageRepository.saveValue(
key: 'windowId',
value: window.id,
storageArea: 'activeWindow',
);
log.i('Suspended ${window.process.pid} successfully');
await _storageRepository.saveValue(
key: 'pid',
value: window.process.pid,
storageArea: 'activeWindow',
);
await _storageRepository.saveValue(
key: 'windowId',
value: window.id,
storageArea: 'activeWindow',
);
log.i('Suspended ${window.process.pid} successfully');

return true;
}

return true;
log.e('Failed to suspend after $_maxRetries attempts.');
return false;
}

Future<void> _minimize(int windowId) async {
Expand Down
7 changes: 6 additions & 1 deletion lib/apps_list/cubit/apps_list_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import '../../logs/logs.dart';
import '../../native_platform/native_platform.dart';
import '../../storage/storage_repository.dart';
import '../../system_tray/system_tray.dart';
import '../../window/app_window.dart';
import '../apps_list.dart';

part 'apps_list_state.dart';
part 'apps_list_cubit.freezed.dart';

class AppsListCubit extends Cubit<AppsListState> {
final AppWindow _appWindow;
final HotkeyService _hotkeyService;
final NativePlatform _nativePlatform;
final ProcessRepository _processRepository;
Expand All @@ -28,6 +30,7 @@ class AppsListCubit extends Cubit<AppsListState> {
final AppVersion _appVersion;

AppsListCubit({
required AppWindow appWindow,
required HotkeyService hotkeyService,
required NativePlatform nativePlatform,
required ProcessRepository processRepository,
Expand All @@ -36,7 +39,8 @@ class AppsListCubit extends Cubit<AppsListState> {
required SystemTrayManager systemTrayManager,
required AppVersion appVersion,
bool testing = false,
}) : _hotkeyService = hotkeyService,
}) : _appWindow = appWindow,
_hotkeyService = hotkeyService,
_nativePlatform = nativePlatform,
_settingsCubit = settingsCubit,
_processRepository = processRepository,
Expand Down Expand Up @@ -216,6 +220,7 @@ class AppsListCubit extends Cubit<AppsListState> {

Future<bool> toggleActiveWindow() async {
final activeWindow = ActiveWindow(
_appWindow,
_nativePlatform,
_processRepository,
_storage,
Expand Down
8 changes: 5 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ Future<void> main(List<String> args) async {

final processRepository = ProcessRepository.init();

final appWindow = AppWindow(storage);
appWindow.initialize();

final activeWindow = ActiveWindow(
appWindow,
nativePlatform,
processRepository,
storage,
Expand All @@ -81,9 +85,6 @@ Future<void> main(List<String> args) async {

final hotkeyService = HotkeyService();

final appWindow = AppWindow(storage);
appWindow.initialize();

final settingsCubit = await SettingsCubit.init(
autostartService: AutostartService(),
hotkeyService: hotkeyService,
Expand All @@ -108,6 +109,7 @@ Future<void> main(List<String> args) async {
);

final appsListCubit = AppsListCubit(
appWindow: appWindow,
hotkeyService: hotkeyService,
nativePlatform: nativePlatform,
settingsCubit: settingsCubit,
Expand Down
38 changes: 38 additions & 0 deletions test/active_window/src/active_window_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import 'package:nyrna/argument_parser/argument_parser.dart';
import 'package:nyrna/logs/logs.dart';
import 'package:nyrna/native_platform/native_platform.dart';
import 'package:nyrna/storage/storage_repository.dart';
import 'package:nyrna/window/app_window.dart';
import 'package:test/test.dart';

import '../../helpers.dart';
@GenerateNiceMocks(<MockSpec>[
MockSpec<AppWindow>(),
MockSpec<ArgumentParser>(),
MockSpec<NativePlatform>(),
MockSpec<ProcessRepository>(),
Expand All @@ -30,6 +33,7 @@ const testWindow = Window(
title: 'Untitled-2 - Visual Studio Code - Insiders',
);

MockAppWindow appWindow = MockAppWindow();
MockArgumentParser argParser = MockArgumentParser();
MockNativePlatform nativePlatform = MockNativePlatform();
MockProcessRepository processRepository = MockProcessRepository();
Expand All @@ -51,6 +55,9 @@ void main() {

// Setup initial dummy responses for mocks.

// AppWindow
when(appWindow.hide()).thenAnswer((_) async => true);

// NativePlatform
when(nativePlatform.activeWindow()).thenAnswer((_) async => testWindow);
when(nativePlatform.minimizeWindow(any)).thenAnswer((_) async => true);
Expand All @@ -76,6 +83,7 @@ void main() {
when(storageRepository.close()).thenAnswer((_) async {});

activeWindow = ActiveWindow(
appWindow,
nativePlatform,
processRepository,
storageRepository,
Expand Down Expand Up @@ -117,6 +125,36 @@ void main() {
expect(successful, false);
});

test('active window being Nyrna calls hide on window and tries again (Linux)',
() async {
final nyrnaWindow = testWindow.copyWith(
process: testProcess.copyWith(executable: 'nyrna'),
);
when(nativePlatform.activeWindow()).thenAnswerInOrder([
Future.value(nyrnaWindow),
Future.value(testWindow),
]);
final successful = await activeWindow.toggle();
expect(successful, true);
verify(nativePlatform.activeWindow()).called(2);
verify(appWindow.hide()).called(1);
});

test('active window being Nyrna calls hide on window and tries again (Windows)',
() async {
final nyrnaWindow = testWindow.copyWith(
process: testProcess.copyWith(executable: 'nyrna.exe'),
);
when(nativePlatform.activeWindow()).thenAnswerInOrder([
Future.value(nyrnaWindow),
Future.value(testWindow),
]);
final successful = await activeWindow.toggle();
expect(successful, true);
verify(nativePlatform.activeWindow()).called(2);
verify(appWindow.hide()).called(1);
});

group('minimizing & restoring:', () {
test('no flag or preference defaults to minimizing', () async {
expect(argParser.minimize, null);
Expand Down
4 changes: 4 additions & 0 deletions test/apps_list/cubit/apps_list_cubit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import 'package:nyrna/native_platform/native_platform.dart';
import 'package:nyrna/settings/settings.dart';
import 'package:nyrna/storage/storage_repository.dart';
import 'package:nyrna/system_tray/system_tray_manager.dart';
import 'package:nyrna/window/app_window.dart';
import 'package:test/test.dart';

@GenerateNiceMocks(<MockSpec>[
MockSpec<AppWindow>(),
MockSpec<HotkeyService>(),
MockSpec<NativePlatform>(),
MockSpec<SettingsCubit>(),
Expand Down Expand Up @@ -70,6 +72,7 @@ Window get mpvWindow2State => state //
.windows
.singleWhere((element) => element.id == mpvWindow2.id);

final appWindow = MockAppWindow();
final hotkeyService = MockHotkeyService();
final nativePlatform = MockNativePlatform();
final settingsCubit = MockSettingsCubit();
Expand Down Expand Up @@ -128,6 +131,7 @@ void main() {
when(storage.getValue('minimizeWindows')).thenAnswer((_) async => true);

cubit = AppsListCubit(
appWindow: appWindow,
hotkeyService: hotkeyService,
nativePlatform: nativePlatform,
settingsCubit: settingsCubit,
Expand Down
4 changes: 4 additions & 0 deletions test/apps_list/widgets/window_tile_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import 'package:nyrna/native_platform/native_platform.dart';
import 'package:nyrna/settings/settings.dart';
import 'package:nyrna/storage/storage_repository.dart';
import 'package:nyrna/system_tray/system_tray_manager.dart';
import 'package:nyrna/window/app_window.dart';

@GenerateNiceMocks(<MockSpec>[
MockSpec<AppVersion>(),
MockSpec<AppWindow>(),
MockSpec<HotkeyService>(),
MockSpec<NativePlatform>(),
MockSpec<ProcessRepository>(),
Expand All @@ -24,6 +26,7 @@ import 'package:nyrna/system_tray/system_tray_manager.dart';
import 'window_tile_test.mocks.dart';

final mockAppVersion = MockAppVersion();
final mockAppWindow = MockAppWindow();
final mockHotkeyService = MockHotkeyService();
final mockNativePlatform = MockNativePlatform();
final mockProcessRepository = MockProcessRepository();
Expand Down Expand Up @@ -57,6 +60,7 @@ void main() {
testWidgets('Clicking more actions button shows context menu', (tester) async {
final appsListCubit = AppsListCubit(
appVersion: mockAppVersion,
appWindow: mockAppWindow,
hotkeyService: mockHotkeyService,
nativePlatform: mockNativePlatform,
processRepository: mockProcessRepository,
Expand Down
12 changes: 12 additions & 0 deletions test/helpers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:mockito/mockito.dart';

/// Extension to allow `thenAnswerInOrder` (different values for multiple async calls).
///
/// From: https://github.com/dart-lang/mockito/issues/221#issuecomment-2034267995
/// Open issue: https://github.com/dart-lang/mockito/issues/704
extension When<T> on PostExpectation<T> {
void thenAnswerInOrder(List<T> values) {
int callCount = 0;
thenAnswer((_) => values[callCount++]);
}
}

0 comments on commit e4cd7fa

Please sign in to comment.