Skip to content

Commit

Permalink
Merge pull request #45 from Merrit/dev
Browse files Browse the repository at this point in the history
Major refactor for DDD, split toggle active to separate package, bug fixes and performance improvements...
  • Loading branch information
Merrit authored Aug 14, 2021
2 parents 1f5ee3a + cf01766 commit 38c94b8
Show file tree
Hide file tree
Showing 199 changed files with 11,002 additions and 2,116 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/build-linux-appimage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ jobs:
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libblkid-dev zsync
sudo apt install -y python3 jq
pip install yq
- name: Set environment variables
run: |
VER=$(cat VERSION)
VER=$(yq -r .version < pubspec.yaml)
echo "VERSION=$VER" >> $GITHUB_ENV
- name: Flutter setup
run: |
Expand All @@ -39,7 +41,7 @@ jobs:
args: --recipe packaging/linux/appimage/AppImageBuilder.yml --skip-test
- uses: actions/upload-artifact@v2
with:
name: Nyrna-AppImage
name: Nyrna-linux-AppImage
path: |
*.AppImage
*.zsync
13 changes: 8 additions & 5 deletions .github/workflows/build-linux-portable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: sudo apt-get update -y
- run: sudo apt-get install -y ninja-build libgtk-3-dev libblkid-dev
- run: sudo apt-get install -y ninja-build libgtk-3-dev libblkid-dev tree
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1.4.0
with:
channel: beta
- run: |
VER=$(cat VERSION)
echo "VERSION=$VER" >> $GITHUB_ENV
flutter config --enable-linux-desktop
flutter pub get
flutter build linux
mv build/linux/x64/release/bundle nyrna
touch nyrna/PORTABLE
cp VERSION nyrna/VERSION
- name: Build toggle_active_window
run: |
cd packages/active_window
dart pub get
dart compile exe bin/toggle_active_window.dart -o bin/toggle_active_window
cp bin/toggle_active_window ../../nyrna
cd ../..
- uses: actions/upload-artifact@v2
with:
name: nyrna-linux-portable
Expand Down
23 changes: 19 additions & 4 deletions .github/workflows/build-win32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: beta
- name: Read VERSION file
id: getversion
run: echo "::set-output name=version::$(cat VERSION)"
- name: Set up Visual Studio shell
uses: egor-tensin/vs-shell@v2
with:
arch: x64
- name: Install AutoHotkey
run: choco install autohotkey
- name: Add AutoHotkey to path
run: echo "C:\Program Files\AutoHotkey\Compiler" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build Nyrna portable
run: |
flutter config --enable-windows-desktop
Expand All @@ -24,13 +29,23 @@ jobs:
move build\windows\runner\Release build\Nyrna
copy VERSION build\Nyrna
xcopy C:\Windows\System32\msvcp140.dll build\Nyrna
xcopy C:\Windows\System32\vcruntime140.dll build\Nyrna
xcopy C:\Windows\System32\vcruntime140_1.dll build\Nyrna
cd .\packages\active_window
dart pub get
dart compile exe .\bin\toggle_active_window.dart
editbin.exe /subsystem:windows .\bin\toggle_active_window.exe
xcopy .\bin\toggle_active_window.exe ..\..\build\Nyrna
Ahk2Exe.exe /in .\hotkey\toggle_active_hotkey.ahk /out ..\..\build\Nyrna\toggle_active_hotkey.exe
cd ..\..
- name: Package with Inno Setup
run: |
iscc packaging\win32\inno_setup_script.iss
- name: Add PORTABLE file
run: New-Item -Path .\build\Nyrna\ -Name "PORTABLE" -ItemType "file"
- uses: actions/upload-artifact@v2
with:
name: nyrna-windows-portable
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Packaging output
*.AppImage
*.exe
*.snap
*.zsync
appimage-builder-cache
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

**Suspend games and applications.**

![Nyrna interface](assets/../docs/assets/images/nyrna-window.png)

Similar to the incredibly useful sleep/suspend function found in consoles like the Nintendo Switch and Sony PlayStation; suspend your game (and its resource usage) at any time, and resume whenever you wish - at the push of a button.

Nyrna can be used to suspend normal, non-game applications as well. For example:
Expand Down
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

40 changes: 16 additions & 24 deletions docs/_pages/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,25 @@ permalink: /usage

## Toggle active window

Nyrna can be used to toggle the suspend / resume state of one active window by
setting a hotkey to launch Nyrna with the `-t` or `--toggle` flag.
Nyrna can be used to toggle the suspend / resume state of one active window:

Once the hotkey is set, simply press that key to have the window of your
- Linux
- Set a hotkey in system settings to run the `toggle_active_window` executable
from the Nyrna directory

- Windows
- Run the `toggle_active_window.exe` file from the Nyrna directory to add an
icon to the system tray that will listen for the `Pause` key to be pressed.
- Optionally, from Nyrna's settings you can set this hotkey monitor to run
automatically at boot.

Now simply press the hotkey on your keyboard to have the window of your
currently active application suspended. A subsequent press will resume that
application.

*Note: Not reliable if Nyrna is already running.*
**Tip:** You can use something like
[AntiMicro](https://github.com/AntiMicro/antimicro) to trigger this hotkey with
your gamepad, allowing you to suspend/resume your game with just your controller.


**Linux Example**
Expand All @@ -50,7 +61,7 @@ application.
- Edit ->
- New ->
- Global Shortcut
- Action: `/path/to/nyrna -t`
- Action: `/path/to/nyrna/toggle_active_window`

![KDE custom shortcut](assets/images/custom-shortcut-linux-kde.png)
{% endcapture %}
Expand All @@ -59,22 +70,3 @@ application.
<h4 class="no_toc">KDE System Settings:</h4>
{{ linux-example-text | markdownify }}
</div>


**Windows Example**

{% capture windows-example-text %}
- Right click shortcut -> Properties
- Shortcut tab
- Target -> Edit ending to include `-t` or `--toggle`, like:
`C:\Nyrna\nyrna.exe -t`
- Shortcut key -> Choose key to trigger the toggle function
- Apply

![Windows custom shortcut](assets/images/custom-shortcut-windows.png)
{% endcapture %}

<div class="notice--info">
<h4 class="no_toc">Create or edit Nyrna shortcut:</h4>
{{ windows-example-text | markdownify }}
</div>
1 change: 1 addition & 0 deletions lib/application/app/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'cubit/app_cubit.dart';
216 changes: 216 additions & 0 deletions lib/application/app/cubit/app_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import 'dart:async';
import 'dart:io' as io;

import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:native_platform/native_platform.dart';
import 'package:nyrna/application/preferences/cubit/preferences_cubit.dart';
import 'package:nyrna/infrastructure/preferences/preferences.dart';
import 'package:nyrna/infrastructure/versions/versions.dart';
import 'package:url_launcher/url_launcher.dart';

part 'app_state.dart';

/// Convenience access to the main app cubit.
late AppCubit appCubit;

class AppCubit extends Cubit<AppState> {
final NativePlatform _nativePlatform;
final Preferences _prefs;
final PreferencesCubit _prefsCubit;
final Versions _versionRepo;

AppCubit({
required NativePlatform nativePlatform,
required Preferences prefs,
required PreferencesCubit prefsCubit,
required Versions versionRepository,
}) : _nativePlatform = nativePlatform,
_prefs = prefs,
_prefsCubit = prefsCubit,
_versionRepo = versionRepository,
super(AppState.initial()) {
appCubit = this;
_initialize();
}

Future<void> _initialize() async {
await fetchData();
emit(state.copyWith(loading: false));
await _checkIsPortable();
setAutoRefresh(
autoRefresh: _prefsCubit.state.autoRefresh,
refreshInterval: _prefsCubit.state.refreshInterval,
);
await fetchVersionData();
}

Future<void> _checkIsPortable() async {
final file = io.File('PORTABLE');
final isPortable = await file.exists();
emit(state.copyWith(isPortable: isPortable));
}

Timer? _timer;

/// The timer which auto-refreshes the list of open windows.
void setAutoRefresh({
required bool autoRefresh,
required int refreshInterval,
}) {
if (_timer != null) _timer?.cancel();
if (autoRefresh) {
_timer = Timer.periodic(
Duration(seconds: refreshInterval),
(timer) => fetchData(),
);
}
}

Future<void> fetchData() async {
await _fetchDesktop();
await _fetchWindows();
}

Future<void> _fetchDesktop() async {
final currentDesktop = await _nativePlatform.currentDesktop;
emit(state.copyWith(currentDesktop: currentDesktop));
}

List<Window> _sortWindows(List<Window> windows) {
return windows.sortedBy(
(window) => window.process.executable.toLowerCase(),
);
}

/// Populate the list of visible windows.
Future<void> _fetchWindows() async {
var windows = await _nativePlatform.windows();
windows.removeWhere(
(window) => _filteredWindows.contains(window.process.executable),
);
windows = await _checkWindowStatuses(windows);
final sortedWindows = _sortWindows(windows);
emit(state.copyWith(windows: sortedWindows));
}

Future<ProcessStatus> _getProcessStatus(int pid) async {
final process = NativeProcess(pid);
final status = await process.status;
return status;
}

Future<List<Window>> _checkWindowStatuses(List<Window> windows) async {
final processedWindows = <Window>[];
for (var window in windows) {
final existingWindow = state.windows.singleWhereOrNull(
(stateWindow) => stateWindow.id == window.id,
);
if (existingWindow != null) {
processedWindows.add(
window.copyWith(process: existingWindow.process),
);
} else {
final pid = window.process.pid;
final status = await _getProcessStatus(pid);
processedWindows.add(
window.copyWith(
process: window.process.copyWith(status: status),
),
);
}
}
return processedWindows;
}

Future<void> manualRefresh() async {
emit(state.copyWith(loading: true));
await fetchData();
emit(state.copyWith(loading: false));
}

@visibleForTesting
Future<void> fetchVersionData() async {
final runningVersion = await _versionRepo.runningVersion();
final latestVersion = await _versionRepo.latestVersion();
final ignoredUpdate = _prefs.getString('ignoredUpdate');
final updateHasBeenIgnored = (latestVersion == ignoredUpdate);
final updateAvailable =
(updateHasBeenIgnored) ? false : await _versionRepo.updateAvailable();
emit(state.copyWith(
runningVersion: runningVersion,
updateVersion: latestVersion,
updateAvailable: updateAvailable,
));
}

/// Toggle suspend / resume for the process associated with the given window.
Future<bool> toggle(Window window) async {
final pid = window.process.pid;
ProcessStatus status = await _getProcessStatus(pid);
bool success;
if (status == ProcessStatus.suspended) {
success = await _resume(window);
status = await _getProcessStatus(pid);
if (status != ProcessStatus.normal) success = false;
} else {
success = await _suspend(window);
status = await _getProcessStatus(pid);
if (status != ProcessStatus.suspended) success = false;
}
final updatedWindow = window.copyWith(
process: window.process.copyWith(status: status),
);
final windows = List<Window>.from(state.windows);
windows.removeWhere((e) => e.id == window.id);
windows.add(updatedWindow);
final sortedWindows = _sortWindows(windows);
emit(state.copyWith(windows: sortedWindows));
return success;
}

Future<bool> _resume(Window window) async {
final nativeProcess = NativeProcess(window.process.pid);
final success = await nativeProcess.resume();
// Restore the window _after_ resuming or it might not restore.
await _nativePlatform.restoreWindow(window.id);
return (success) ? true : false;
}

Future<bool> _suspend(Window window) async {
// Minimize the window before suspending or it might not minimize.
await _nativePlatform.minimizeWindow(window.id);
// Small delay on Win32 to ensure the window actually minimizes.
// Doesn't seem to be necessary on Linux.
if (io.Platform.isWindows) {
await Future.delayed(Duration(milliseconds: 500));
}
final nativeProcess = NativeProcess(window.process.pid);
final success = await nativeProcess.suspend();
return (success) ? true : false;
}

Future<void> launchURL(String url) async {
await canLaunch(url)
? await launch(url)
: throw 'Could not launch url: $url';
}
}

/// System-level or non-app executables. Nyrna shouldn't show these.
List<String> _filteredWindows = [
'nyrna.exe',
'ApplicationFrameHost.exe', // Manages UWP (Universal Windows Platform) apps
'explorer.exe', // Windows File Explorer
'googledrivesync.exe',
'LogiOverlay.exe', // Logitech Options
'PenTablet.exe', // XP-PEN driver
'perfmon.exe', // Resource Monitor
'Rainmeter.exe',
'SystemSettings.exe', // Windows system settings
'Taskmgr.exe', // Windows Task Manager
'TextInputHost.exe', // Microsoft Text Input Application
'WinStore.App.exe', // Windows Store
];
Loading

0 comments on commit 38c94b8

Please sign in to comment.