refactor(jiggler): simplify JigglerSetting and integrate with settings page

This commit is contained in:
Adam Shiervani 2025-08-12 14:31:37 +02:00
parent 77ffdb4362
commit 0feb5a9c77
2 changed files with 228 additions and 239 deletions

View File

@ -1,13 +1,9 @@
import { useCallback, useEffect, useState } from "react";
import { useState } from "react";
import { Button } from "@components/Button";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { InputFieldWithLabel } from "./InputField";
import ExtLink from "./ExtLink";
export interface JigglerConfig {
inactivity_limit_seconds: number;
@ -15,205 +11,84 @@ export interface JigglerConfig {
schedule_cron_tab: string;
}
const jigglerCrontabConfigs = [
{
label: "Every 20 seconds",
value: "*/20 * * * * *",
},
{
label: "Every 40 seconds",
value: "*/40 * * * * *",
},
{
label: "Every 1 minute",
value: "0 * * * * *",
},
{
label: "Every 3 minutes",
value: "0 */3 * * * *",
},
];
const jigglerJitterConfigs = [
{
label: "No Jitter",
value: "0",
},
{
label: "10%",
value: "20",
},
{
label: "25%",
value: "25",
},
{
label: "50%",
value: "50",
},
];
const jigglerInactivityConfigs = [
{
label: "20 Seconds",
value: "20",
},
{
label: "40 Seconds",
value: "40",
},
{
label: "1 Minute",
value: "60",
},
{
label: "3 Minutes",
value: "180",
},
];
export function JigglerSetting() {
const [send] = useJsonRpc();
const [loading, setLoading] = useState(false);
const [jitterPercentage, setJitterPercentage] = useState("");
const [scheduleCronTab, setScheduleCronTab] = useState("");
export function JigglerSetting({
onSave,
}: {
onSave: (jigglerConfig: JigglerConfig) => void;
}) {
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({
inactivity_limit_seconds: 20,
jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *"
jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *",
});
const syncJigglerConfig = useCallback(() => {
send("getJigglerConfig", {}, resp => {
if ("error" in resp) return;
const result = resp.result as JigglerConfig;
setJigglerConfigState(result);
const jitterPercentage = jigglerJitterConfigs.map(u => u.value).includes(result.jitter_percentage.toString())
? result.jitter_percentage.toString()
: "custom";
setJitterPercentage(jitterPercentage)
const scheduleCronTab = jigglerCrontabConfigs.map(u => u.value).includes(result.schedule_cron_tab)
? result.schedule_cron_tab
: "custom";
setScheduleCronTab(scheduleCronTab)
});
}, [send]);
useEffect(() => {
syncJigglerConfig()
}, [send, syncJigglerConfig]);
const handleJigglerInactivityLimitSecondsChange = (value: string) => {
setJigglerConfigState({ ...jigglerConfigState, inactivity_limit_seconds: Number(value) });
};
const handleJigglerJitterPercentageChange = (value: string) => {
setJigglerConfigState({ ...jigglerConfigState, jitter_percentage: Number(value) });
};
const handleJigglerScheduleCronTabChange = (value: string) => {
setJigglerConfigState({ ...jigglerConfigState, schedule_cron_tab: value });
};
const handleJigglerConfigSave = useCallback(
(jigglerConfig: JigglerConfig) => {
setLoading(true);
send("setJigglerConfig", { jigglerConfig }, async resp => {
if ("error" in resp) {
notifications.error(
`Failed to set jiggler config: ${resp.error.data || "Unknown error"}`,
);
setLoading(false);
return;
}
setLoading(false);
notifications.success(
`Jiggler Config successfully updated`,
);
syncJigglerConfig();
});
},
[send, syncJigglerConfig],
);
return (
<div className="">
<div className="grid grid-cols-2 gap-4">
<SelectMenuBasic
<div className="space-y-2">
<div className="grid max-w-sm grid-cols-1 items-end gap-y-2">
<InputFieldWithLabel
required
size="SM"
label="Schedule"
className="max-w-[192px]"
value={scheduleCronTab}
fullWidth
onChange={e => {
setScheduleCronTab(e.target.value);
if (e.target.value != "custom") {
handleJigglerScheduleCronTabChange(e.target.value);
}
}}
options={[...jigglerCrontabConfigs, {value: "custom", label: "Custom"}]}
label="Cron Schedule"
description={
<span>
Generate with{" "}
<ExtLink className="text-blue-700 underline" href="https://crontab.guru/">
crontab.guru
</ExtLink>
</span>
}
placeholder="*/20 * * * * *"
defaultValue={jigglerConfigState.schedule_cron_tab}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
schedule_cron_tab: e.target.value,
})
}
/>
{scheduleCronTab === "custom" && (
<InputFieldWithLabel
required
label="Jiggler Crontab"
placeholder="*/20 * * * * *"
defaultValue={jigglerConfigState.schedule_cron_tab}
onChange={e => handleJigglerScheduleCronTabChange(e.target.value)}
/>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<SelectMenuBasic
size="SM"
label="Jitter Percentage"
className="max-w-[192px]"
value={jitterPercentage}
fullWidth
onChange={e => {
setJitterPercentage(e.target.value);
if (e.target.value != "custom") {
handleJigglerJitterPercentageChange(e.target.value)
}
}}
options={[...jigglerJitterConfigs, {value: "custom", label: "Custom"}]}
/>
{jitterPercentage === "custom" && (
<InputFieldWithLabel
required
label="Jitter Percentage"
placeholder="25"
defaultValue={jigglerConfigState.jitter_percentage}
type="number"
min="1"
max="100"
onChange={e => handleJigglerJitterPercentageChange(e.target.value)}
/>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<SelectMenuBasic
<InputFieldWithLabel
size="SM"
label="Inactivity Limit Seconds"
className="max-w-[192px]"
description="Seconds of inactivity before triggering a jiggle again"
value={jigglerConfigState.inactivity_limit_seconds}
fullWidth
onChange={e => {
handleJigglerInactivityLimitSecondsChange(e.target.value);
}}
options={[...jigglerInactivityConfigs]}
type="number"
min="1"
max="100"
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
inactivity_limit_seconds: Number(e.target.value),
})
}
/>
<InputFieldWithLabel
required
size="SM"
label="Random delay"
description="To avoid recognizable patterns"
placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
defaultValue={jigglerConfigState.jitter_percentage}
type="number"
min="0"
max="100"
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
jitter_percentage: Number(e.target.value),
})
}
/>
</div>
<div className="mt-6 flex gap-x-2">
<Button
loading={loading}
size="SM"
theme="primary"
text="Update Jiggler Config"
onClick={() => handleJigglerConfigSave(jigglerConfigState)}
text="Save Jiggler Config"
onClick={() => onSave(jigglerConfigState)}
/>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { CheckCircleIcon } from "@heroicons/react/16/solid";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg";
@ -7,15 +7,63 @@ import { GridCard } from "@/components/Card";
import { Checkbox } from "@/components/Checkbox";
import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JigglerSetting } from "@components/JigglerSetting";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { cx } from "../cva.config";
import { SettingsItem } from "./devices.$id.settings";
import notifications from "../notifications";
import SettingsNestedSection from "../components/SettingsNestedSection";
import { JigglerSetting } from "@components/JigglerSetting";
export interface JigglerConfig {
inactivity_limit_seconds: number;
jitter_percentage: number;
schedule_cron_tab: string;
}
const jigglerOptions = [
{ value: "disabled", label: "Disabled", config: null },
{
value: "frequent",
label: "Frequent - 30s",
config: {
inactivity_limit_seconds: 30,
jitter_percentage: 25,
schedule_cron_tab: "*/30 * * * * *",
},
},
{
value: "standard",
label: "Standard - 1m",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * * * * *",
},
},
{
value: "light",
label: "Light - 5m",
config: {
inactivity_limit_seconds: 300,
jitter_percentage: 25,
schedule_cron_tab: "0 */5 * * * *",
},
},
{
value: "business_hours",
label: "Business Hours - 1m - (Mon-Fri 9-17)",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
},
},
] as const;
type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom";
export default function SettingsMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden);
@ -24,12 +72,11 @@ export default function SettingsMouseRoute() {
const mouseMode = useSettingsStore(state => state.mouseMode);
const setMouseMode = useSettingsStore(state => state.setMouseMode);
const [jiggler, setJiggler] = useState(false);
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
const setScrollThrottling = useSettingsStore(
state => state.setScrollThrottling,
);
const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling);
const [selectedJigglerOption, setSelectedJigglerOption] =
useState<JigglerValues | null>(null);
const scrollThrottlingOptions = [
{ value: "0", label: "Off" },
@ -41,28 +88,85 @@ export default function SettingsMouseRoute() {
const [send] = useJsonRpc();
useEffect(() => {
const syncJigglerSettings = useCallback(() => {
send("getJigglerState", {}, resp => {
if ("error" in resp) return;
setJiggler(resp.result as boolean);
});
const isEnabled = resp.result as boolean;
send("getJigglerConfig", {}, resp => {
if ("error" in resp) return;
setJiggler(resp.result as boolean);
// If the jiggler is disabled, set the selected option to "disabled" and nothing else
if (!isEnabled) return setSelectedJigglerOption("disabled");
send("getJigglerConfig", {}, resp => {
if ("error" in resp) return;
const result = resp.result as JigglerConfig;
const value = jigglerOptions.find(
o =>
o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds &&
o?.config?.jitter_percentage === result.jitter_percentage &&
o?.config?.schedule_cron_tab === result.schedule_cron_tab,
)?.value;
setSelectedJigglerOption(value || "custom");
});
});
}, [send]);
const handleJigglerChange = (enabled: boolean) => {
send("setJigglerState", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
return;
}
setJiggler(enabled);
});
useEffect(() => {
syncJigglerSettings();
}, [syncJigglerSettings]);
const saveJigglerConfig = useCallback(
(jigglerConfig: JigglerConfig) => {
// We assume the jiggler should be set to enabled if the config is being updated
send("setJigglerState", { enabled: true }, async resp => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
}
});
send("setJigglerConfig", { jigglerConfig }, async resp => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler config: ${resp.error.data || "Unknown error"}`,
);
}
notifications.success(`Jiggler Config successfully updated`);
syncJigglerSettings();
});
},
[send, syncJigglerSettings],
);
const handleJigglerChange = (option: JigglerValues) => {
if (option === "custom") {
setSelectedJigglerOption("custom");
// We don't need to sync the jiggler settings when the option is "custom". The user will press "Save" to save the custom settings.
return;
}
// We don't need to update the device jiggler state when the option is "disabled"
if (option === "disabled") {
send("setJigglerState", { enabled: false }, async resp => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
}
});
notifications.success(`Jiggler Config successfully updated`);
return setSelectedJigglerOption("disabled");
}
const jigglerConfig = jigglerOptions.find(o => o.value === option)?.config;
if (!jigglerConfig) {
return notifications.error("There was an error setting the jiggler config");
}
saveJigglerConfig(jigglerConfig);
};
return (
@ -83,39 +187,49 @@ export default function SettingsMouseRoute() {
/>
</SettingsItem>
<SettingsItem
title="Scroll Throttling"
description="Reduce the frequency of scroll events"
>
<SelectMenuBasic
size="SM"
label=""
className="max-w-[292px]"
value={scrollThrottling}
fullWidth
onChange={e => setScrollThrottling(parseInt(e.target.value))}
options={scrollThrottlingOptions}
/>
</SettingsItem>
<SettingsItem
title="Scroll Throttling"
description="Reduce the frequency of scroll events"
>
<SelectMenuBasic
size="SM"
label=""
className="max-w-[292px]"
value={scrollThrottling}
fullWidth
onChange={e => setScrollThrottling(parseInt(e.target.value))}
options={scrollThrottlingOptions}
/>
</SettingsItem>
<SettingsItem
title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
>
<Checkbox
checked={jiggler}
onChange={e => handleJigglerChange(e.target.checked)}
<SelectMenuBasic
size="SM"
label=""
value={selectedJigglerOption || "disabled"}
options={[
...jigglerOptions.map(option => ({
value: option.value,
label: option.label,
})),
{ value: "custom", label: "Custom" },
]}
onChange={e => {
handleJigglerChange(
e.target.value as (typeof jigglerOptions)[number]["value"],
);
}}
fullWidth
/>
</SettingsItem>
{jiggler && (
<>
<SettingsSectionHeader
title="Jiggler Config"
description="Control the jiggler schedule"
/>
<JigglerSetting />
</>
{selectedJigglerOption === "custom" && (
<SettingsNestedSection>
<JigglerSetting onSave={saveJigglerConfig} />
</SettingsNestedSection>
)}
<div className="space-y-4">
<SettingsItem title="Modes" description="Choose the mouse input mode" />