Skip to content

Commit

Permalink
Added debounce
Browse files Browse the repository at this point in the history
  • Loading branch information
VinceBT committed Apr 22, 2022
1 parent 09fa142 commit 683a32e
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 85 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ All these settings can be changed inside `settings.json`, they will be applied i

- `daemon`: Where the LittleBigMouse_Daemon.exe file is located on your system
- `interval`: The interval in milliseconds between each time the process will check the name of your current focused window, default is 1000ms
- `debounce`: The debounced time before turning on/off LBM when Alt+Tab-ing from a blacklisted application, default is 10000ms
- `startup`: If this program should launch at Windows startup, default is true
- `blacklist`: The list of programs that should turn OFF LBM, feel free to add your own

Expand Down
26 changes: 15 additions & 11 deletions client/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Container } from "typedi";

import { ActiveWindow } from "../../../../server/src/types/Common";
import { SocketHandler } from "../../managers/SocketHandler";
import { bottomScroller } from "../../utils/HTMLUtils";
import { DebuggerRef } from "../Debugger/constants";
import { Debugger } from "../Debugger/Debugger";

Expand All @@ -24,7 +23,6 @@ const App = (props: AppProps) => {
const [logs, setLogs] = useState<string[]>([]);
const [activeWindow, setActiveWindow] = useState<ActiveWindow>();

const $console = useRef<HTMLPreElement>(null);
const $debugger = useRef<DebuggerRef>(null);

const socketHandler = Container.get(SocketHandler);
Expand All @@ -34,9 +32,22 @@ const App = (props: AppProps) => {
if (string[string.length - 1] !== "\n") {
string += "\n";
}
setLogs((prevState) => prevState.concat(string));
setLogs((prevState) => [string].concat(prevState));
}, []);

/*
useEffect(() => {
let count = 0;
const handler = setInterval(() => {
setLogs((prevState) => [count % 2 === 0 ? "Tic" : "Tac"].concat(prevState));
count++;
}, 1000);
return () => {
clearInterval(handler);
};
}, []);
*/

const handleActiveWindowChange = useCallback((data) => {
setActiveWindow(data);
}, []);
Expand Down Expand Up @@ -68,17 +79,10 @@ const App = (props: AppProps) => {
};
}, [handleNewMessages, handleActiveWindowChange, socketHandler]);

useEffect(() => {
const unsubscribe = bottomScroller($console.current);
return () => {
unsubscribe();
};
}, []);

return (
<div className={classes.container}>
<div className={classes.consoleContainer}>
<pre ref={$console} className={classes.console}>
<pre className={classes.console}>
{logs.map((log, i) => (
<span key={log + i} className={classes.item}>
{log}
Expand Down
12 changes: 5 additions & 7 deletions client/src/components/App/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,21 @@ export const styles = {
consoleContainer: {
display: "flex",
flex: 1,
overflow: "hidden",
flexFlow: "column",
background: "black",
overflowY: "auto",
flexFlow: "column-reverse",
},
console: {
display: "flex",
flex: 1,
flexFlow: "column",
overflow: "auto",
background: "black",
flexFlow: "column-reverse",
color: "white",
fontFamily: "monospace",
marginBottom: "auto",
},
item: {
//
},
activeWindow: {
display: "none",
flexFlow: "column",
background: "red",
color: "white",
Expand Down
72 changes: 50 additions & 22 deletions client/src/utils/HTMLUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
function getScrollBottom(target: HTMLElement) {
const { height: rectHeight } = target.getBoundingClientRect();
const scrollHeight = target.scrollHeight + (Math.floor(rectHeight) - rectHeight);
return Math.max(0, scrollHeight - (rectHeight + target.scrollTop));
const { height: containerHeight } = target.getBoundingClientRect();
const scrollHeight = target.scrollHeight + (Math.floor(containerHeight) - containerHeight);
return Math.max(0, scrollHeight - (containerHeight + target.scrollTop));
}

export function bottomScroller(target: HTMLElement | null) {
if (!target) return () => undefined;

export function bottomScroller(target: HTMLElement) {
let preventScrollHandling = false;
let scrollBottomSaved = getScrollBottom(target);
let preventScrollComputation = false;
let timeoutHandler: NodeJS.Timer;

function handleWheel() {
scrollBottomSaved = getScrollBottom(target);
}

function handleScroll() {
if (!target) return;
if (!preventScrollComputation) scrollBottomSaved = getScrollBottom(target);
if (target.scrollTop <= 1) target.scrollTop = 1;
if (preventScrollHandling) {
preventScrollHandling = false;
} else {
scrollBottomSaved = getScrollBottom(target);
}
}

function handleResize() {
if (!target) return;
preventScrollComputation = true;
const { height: rectHeight } = target.getBoundingClientRect();
const scrollHeight = target.scrollHeight;
target.scrollTop = scrollHeight - (rectHeight + scrollBottomSaved) + (scrollBottomSaved <= 1 ? 1 : 0);
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(() => {
preventScrollComputation = false;
}, 100);
preventScrollHandling = true;
const { height: containerHeight } = target.getBoundingClientRect();
const scrollHeight = target.scrollHeight + (Math.floor(containerHeight) - containerHeight);
target.scrollTop = scrollHeight - (containerHeight + scrollBottomSaved) + (scrollBottomSaved <= 1 ? 1 : 0); // This will trigger a scroll event
}

target.addEventListener("scroll", handleScroll);

const ro = new ResizeObserver((entries) => entries.forEach(handleResize));
ro.observe(target);

target.addEventListener("scroll", handleScroll);
target.addEventListener("wheel", handleWheel);

return () => {
target.removeEventListener("scroll", handleScroll);
ro.unobserve(target);

target.removeEventListener("scroll", handleScroll);
target.removeEventListener("wheel", handleWheel);
};
}

export function autoScrollWhenScrollable(target: HTMLElement) {
const targetChild = target.children[0] as HTMLElement;

let parentHeight = target.offsetHeight;
let childHeight = targetChild.offsetHeight;
let scrollable = childHeight > parentHeight;

function handleResize() {
parentHeight = target.offsetHeight;
childHeight = targetChild.offsetHeight;
const nextScrollable = childHeight > parentHeight;
if (nextScrollable && !scrollable) {
target.scrollTop = 999999;
}
scrollable = nextScrollable;
}

const ro = new ResizeObserver((entries) => entries.forEach(handleResize));
ro.observe(targetChild);

return () => {
ro.unobserve(targetChild);
};
}
3 changes: 1 addition & 2 deletions client/src/utils/ReactUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useCallback, useState } from "react";

export function useForceUpdate(): () => void {
// eslint-disable-next-line @typescript-eslint/ban-types
const [, dispatch] = useState<{}>(Object.create(null));
const [, dispatch] = useState<unknown>(Object.create(null));

// Turn dispatch(required_parameter) into dispatch().
const memoizedDispatch = useCallback((): void => {
Expand Down
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"postportable": "copyfiles *.vbs settings.json locales/* assets/* public/**/* node_modules/**/*.node node_modules/**/*.exe portable && npm run pack",
"dev": "concurrently \"npm run client-dev\" \"npm run server-dev\"",
"build": "npm run executable -- -o LBMM.exe",
"build-all": "concurrently \"npm run build\" \"npm run portable\"",
"clean": "rimraf LBMM.exe portable dist LBMM.zip logs.txt",
"lint": "eslint --ext=.jsx,.js,.tsx,.ts server,client --quiet --fix",
"postinstall": "patch-package",
Expand Down
2 changes: 1 addition & 1 deletion server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "reflect-metadata";
import { Container } from "typedi";

import { LittleBigMouseManager } from "./managers/LittleBigMouseManager";
import { instancesRunning } from "./utils/processes";
import { instancesRunning } from "./utils/ProcessesUtils";

if (instancesRunning("LBMM.exe") > 1) {
dialog.show({
Expand Down
74 changes: 38 additions & 36 deletions server/src/managers/LittleBigMouseManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Service } from "typedi";
import { Configuration } from "../settings/Configuration";
import { Logger } from "../settings/Logger";
import { Settings, SettingsChangeCallback } from "../settings/Settings";
import { isRunning, runCommandSync } from "../utils/processes";
import { sleep } from "../utils/JavascriptUtils";
import { isRunning, runCommandSync } from "../utils/ProcessesUtils";

import { ActiveWindowListener, ActiveWindowListenerCallback } from "./ActiveWindowListener";
import { MainDisplayManager } from "./MainDisplayManager";
Expand All @@ -19,6 +20,7 @@ export class LittleBigMouseManager {
private isLBMActive = true;
private shouldIgnoreNextCalls = false;
private callQueue: PQueue;
private canceller?: () => boolean;

constructor(
private logger: Logger,
Expand Down Expand Up @@ -65,51 +67,51 @@ export class LittleBigMouseManager {
`Focused window (blacklisted=${activeWindow.isBlacklisted ? "true" : "false"}): ${activeWindowName}`,
);
}
this.callQueue.clear();
if (activeWindow && !activeWindow.isBlacklisted) {
this.callQueue.add(async () => await this.turnOnLBM());
} else {
this.callQueue.add(async () => await this.turnOffLBM());
}
};

private async changeLBMStatus(active: boolean) {
if (this.isLBMRunning()) {
runCommandSync(this.settings.daemon, [active ? "--start" : "--stop"]);
if (active) {
this.logger.log("LBM switched on");
const hasCancelled = this.canceller?.(); // Cancel any pending promises
this.callQueue.clear(); // Reduce queue to 0
if (!hasCancelled) {
if (activeWindow && !activeWindow.isBlacklisted) {
this.callQueue.add(async () => await this.updateLBMStatus(true));
} else {
this.logger.log("LBM switched off");
this.callQueue.add(async () => await this.updateLBMStatus(false));
}
} else {
this.callQueue.pause();
this.checkRunning();
}
}
};

private async turnOnLBM() {
if (this.isLBMActive) {
private async updateLBMStatus(nextActive: boolean) {
if (this.isLBMActive === nextActive) {
if (!this.shouldIgnoreNextCalls) {
this.logger.log("LBM is already on, skipping and next logs will be muted...");
this.logger.log(`LBM is already ${nextActive ? "on" : "off"}, skipping and next logs will be muted...`);
this.shouldIgnoreNextCalls = true;
}
} else {
this.shouldIgnoreNextCalls = false;
this.isLBMActive = true;
await this.changeLBMStatus(true);
}
}

private async turnOffLBM() {
if (!this.isLBMActive) {
if (!this.shouldIgnoreNextCalls) {
this.logger.log("LBM is already off, skipping and next logs will be muted...");
this.shouldIgnoreNextCalls = true;
if (this.isLBMRunning()) {
if (nextActive) {
try {
this.logger.log(`Waiting ${this.settings.debounce} to turn on`);
await sleep(this.settings.debounce, (cancel) => (this.canceller = cancel));
runCommandSync(this.settings.daemon, ["--start"]);
this.isLBMActive = true;
this.logger.log(`LBM switched on`);
} catch (e) {
this.logger.log(`Cancelling switch-on due to debounce`);
}
} else {
try {
this.logger.log(`Waiting ${this.settings.debounce} to turn off`);
await sleep(this.settings.debounce, (cancel) => (this.canceller = cancel));
runCommandSync(this.settings.daemon, ["--stop"]);
this.isLBMActive = false;
this.logger.log(`LBM switched off`);
} catch (e) {
this.logger.log(`Cancelling switch-off due to debounce`);
}
}
} else {
this.callQueue.pause();
this.checkRunning();
}
} else {
this.shouldIgnoreNextCalls = false;
this.isLBMActive = false;
await this.changeLBMStatus(false);
}
}
}
2 changes: 1 addition & 1 deletion server/src/managers/TrayManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Service } from "typedi";
import { I18n } from "../settings/I18n";
import { Logger, pathToLogs } from "../settings/Logger";
import { pathToSettingsJson, Settings } from "../settings/Settings";
import { runCommandSync } from "../utils/processes";
import { runCommandSync } from "../utils/ProcessesUtils";

import { MainDisplayManager } from "./MainDisplayManager";

Expand Down
5 changes: 5 additions & 0 deletions server/src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const pathToSettingsJson = path.resolve(process.cwd(), "settings.json");
interface SettingsMap {
daemon: string;
interval: number;
debounce: number;
startup: boolean;
blacklist: string[];
}
Expand All @@ -40,6 +41,10 @@ export class Settings {
return this.json.interval;
}

get debounce() {
return this.json.debounce;
}

get startup() {
return this.json.startup;
}
Expand Down
16 changes: 16 additions & 0 deletions server/src/utils/JavascriptUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const sleep = (ms: number, callback?: (cancelCallback: () => boolean) => void) =>
new Promise<void>((resolve, reject) => {
let timeout: NodeJS.Timeout | undefined = setTimeout(() => {
timeout = undefined;
resolve();
}, ms);
callback?.(() => {
const timeoutSaved = timeout;
timeout = undefined;
reject();
if (timeoutSaved) {
clearTimeout(timeoutSaved);
}
return !!timeoutSaved;
});
});
File renamed without changes.
Loading

0 comments on commit 683a32e

Please sign in to comment.