decouple the key listener from the main window

This commit is contained in:
bea
2026-10-01 23:35:19 +02:00
parent 2a4795773a
commit 43a3251bac
3 changed files with 118 additions and 12 deletions
@@ -16,6 +16,7 @@ public class Main {
private static final Logger LOGGER = LogManager.getLogger(Main.class);
private static MainWindow mainWindow;
private static AutoSquealConfig config;
/** Whether the hook and its listener are registered, so retries don't add duplicates. */
private static boolean hookRegistered = false;
@@ -23,7 +24,7 @@ public class Main {
public static void main(String[] args) {
LOGGER.info("Hello world!");
AutoSquealConfig config = new ConfigStore().load();
config = new ConfigStore().load();
mainWindow = new MainWindow(config);
// ⌘Q on macOS, the IDE stop button and signals all bypass
@@ -58,7 +59,8 @@ public class Main {
LOGGER.info("Registering jnativehook library...");
try {
GlobalScreen.registerNativeHook();
GlobalScreen.addNativeKeyListener(new KeyPressListener());
GlobalScreen.addNativeKeyListener(
new KeyPressListener(mainWindow::toggleRunning, mainWindow::notifyKeyboardActivity, config));
hookRegistered = true;
LOGGER.info("Successfully registered jnativehook library!");
return true;
@@ -89,8 +91,4 @@ public class Main {
}
}
public static MainWindow getMainWindow() {
return mainWindow;
}
}
@@ -4,16 +4,34 @@ import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import wtf.beatrice.autosqueal.Main;
import wtf.beatrice.autosqueal.config.AutoSquealConfig;
import java.util.ArrayList;
import java.util.List;
public class KeyPressListener implements NativeKeyListener
{
/**
* Listens for the global toggle hotkey, and reports every key press to the
* away detection: a user who is typing is present, hotkey or not.
*
* The listener is fully decoupled: it receives the actions to run instead
* of reaching into the main window, so it can be wired — and tested — on
* its own.
*/
public class KeyPressListener implements NativeKeyListener {
private static final Logger LOGGER = LogManager.getLogger(KeyPressListener.class);
private final Runnable toggleAction;
private final Runnable keyboardActivityReporter;
private final AutoSquealConfig config;
private final List<Integer> pressedKeys = new ArrayList<>();
public KeyPressListener(Runnable toggleAction, Runnable keyboardActivityReporter, AutoSquealConfig config) {
this.toggleAction = toggleAction;
this.keyboardActivityReporter = keyboardActivityReporter;
this.config = config;
}
@Override
public void nativeKeyPressed(NativeKeyEvent e) {
// the OS fires repeated "press" events while a key is being held down; ignore them,
@@ -27,18 +45,22 @@ public class KeyPressListener implements NativeKeyListener
// any key the user presses means they are present: let the away
// detection know, so the app doesn't grab the mouse while they type
Main.getMainWindow().notifyKeyboardActivity();
keyboardActivityReporter.run();
if (!config.isHotkeyEnabled()) {
return;
}
// toggle only when the second key of the combo is pressed down: not only does this
// fire exactly once per combo, but it also avoids toggling when any other key is
// pressed or released while ctrl+alt happen to be held down
if ((e.getKeyCode() == NativeKeyEvent.VC_CONTROL && pressedKeys.contains(NativeKeyEvent.VC_ALT))
|| (e.getKeyCode() == NativeKeyEvent.VC_ALT && pressedKeys.contains(NativeKeyEvent.VC_CONTROL))) {
LOGGER.warn("Received shutdown keystroke: [{}][{}]",
LOGGER.info("Received toggle hotkey: [{}][{}]",
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_CONTROL),
NativeKeyEvent.getKeyText(NativeKeyEvent.VC_ALT));
Main.getMainWindow().toggleRunning();
toggleAction.run();
}
}
@@ -0,0 +1,86 @@
package wtf.beatrice.autosqueal.listener;
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
import org.junit.jupiter.api.Test;
import wtf.beatrice.autosqueal.config.AutoSquealConfig;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class KeyPressListenerTest
{
private final List<String> events = new ArrayList<>();
private final AutoSquealConfig config = new AutoSquealConfig();
private final KeyPressListener listener = new KeyPressListener(
() -> events.add("toggle"), () -> events.add("activity"), config);
private static NativeKeyEvent press(int keyCode) {
return new NativeKeyEvent(NativeKeyEvent.NATIVE_KEY_PRESSED, 0, 0, keyCode, ' ');
}
private static NativeKeyEvent release(int keyCode) {
return new NativeKeyEvent(NativeKeyEvent.NATIVE_KEY_RELEASED, 0, 0, keyCode, ' ');
}
@Test
void theComboTogglesOnceOnItsSecondKey() {
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
// ctrl alone is not the combo: it only reports presence
assertEquals(List.of("activity"), events);
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
// the second key completes the combo: presence first, then the toggle
assertEquals(List.of("activity", "activity", "toggle"), events);
}
@Test
void theComboWorksInTheOtherOrderToo() {
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
assertEquals(List.of("activity", "activity", "toggle"), events);
}
@Test
void repeatedPressesAreIgnored() {
// the OS repeats press events while a key is held down: they must
// not toggle repeatedly
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
events.clear();
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
assertEquals(List.of(), events);
}
@Test
void theComboCanFireAgainAfterBothKeysAreReleased() {
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
listener.nativeKeyReleased(release(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyReleased(release(NativeKeyEvent.VC_ALT));
events.clear();
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
assertEquals(List.of("activity", "activity", "toggle"), events);
}
@Test
void theHotkeyCanBeDisabledWithoutLosingPresenceReporting() {
config.setHotkeyEnabled(false);
listener.nativeKeyPressed(press(NativeKeyEvent.VC_CONTROL));
listener.nativeKeyPressed(press(NativeKeyEvent.VC_ALT));
// typing still counts as presence, but the combo does nothing
assertEquals(List.of("activity", "activity"), events);
}
}