fix fold wakeup bug

This commit is contained in:
pj committed 2026-09-03 16:21:30 +05:30
1 parent 9e7d3414d9
commit c35202674d
5 files changed
+66 -26

No files matched your search

+3 -1
View File
@@ -6,7 +6,9 @@
machine expects to find applications: `/Applications` on macOS, or the package manager on Linux. machine expects to find applications: `/Applications` on macOS, or the package manager on Linux.
It is the same command whether or not the app is already installed, so it doubles as the update. It is the same command whether or not the app is already installed, so it doubles as the update.
On macOS it asks a running copy to quit first, because replacing a bundle under a live process On macOS it asks a running copy to quit first, because replacing a bundle under a live process
leaves it half old and half new. `just uninstall` reverses it and leaves the data directory alone. leaves it half old and half new, and starts the new one once it is in place, so the copy on screen
is never older than the copy installed. `just uninstall` reverses it and leaves the data directory
alone.
The local build skips the dmg and builds only the `.app`, since nothing about copying a bundle The local build skips the dmg and builds only the `.app`, since nothing about copying a bundle
into place needs a disk image and building one is the slowest part of a mac bundle. That makes a into place needs a disk image and building one is the slowest part of a mac bundle. That makes a
+2 -1
View File
@@ -37,7 +37,7 @@ build:
*) echo "just: no local build for $(uname -s); macOS and Linux are the desktop targets." >&2; exit 1 ;; *) echo "just: no local build for $(uname -s); macOS and Linux are the desktop targets." >&2; exit 1 ;;
esac esac
# Build and install, replacing whatever version is already installed. # Build, install over whatever version is already installed, and start the new one.
install: build install: build
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
@@ -75,6 +75,7 @@ _install-macos:
cp -R "$src" "$dest" cp -R "$src" "$dest"
version=$(defaults read "$dest/Contents/Info.plist" CFBundleShortVersionString 2> /dev/null || echo "?") version=$(defaults read "$dest/Contents/Info.plist" CFBundleShortVersionString 2> /dev/null || echo "?")
echo "Installed $version to $dest" echo "Installed $version to $dest"
open "$dest"
_install-linux: _install-linux:
#!/usr/bin/env bash #!/usr/bin/env bash
+1 -1
View File
@@ -2276,7 +2276,7 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "margin-calendar" name = "margin-calendar"
version = "0.0.4" version = "0.0.5"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2", "block2",
+14 -23
View File
@@ -1,15 +1,15 @@
// The wall clock, at the two granularities anything on the grid cares about: the minute the now // The wall clock, at the two granularities anything on the grid cares about: the minute the now
// line moves on, and the hour the fit holds open around it. // line moves on, and the hour the fit holds open around it.
// //
// Both schedule off the clock rather than off an interval, so a tab that was asleep and a machine // Both are read off the clock once a minute rather than counted down to. A timer's deadline is
// that woke up catch up on the next tick instead of drifting further out every hour. A timer is // measured in time the machine spent awake, so one set for the top of the hour before the lid
// still a timer, though: one set before the lid closed can fire long after its wall-clock // closed is still waiting out the rest of that hour when it opens, however late it is by then.
// deadline, and a hidden window's timers run late or not at all. So the window coming back, as a // Reading once a minute puts a minute on how far behind either can be; the window coming back, as
// focus or a visibility change, is read as a tick of its own. // a focus or a visibility change, is read as a tick of its own so it is usually less.
// //
// They are separate hooks because they are separate rerenders. The minute belongs to the one // They are separate hooks because they are separate rerenders. The minute belongs to the one
// component that draws the line; putting the fit on that tick would solve the whole axis sixty // component that draws the line. The hour is read as often but only changes once an hour, and a
// times an hour to answer a question that changes once. // state set to the value it already holds rerenders nothing.
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -22,20 +22,12 @@ function hourStart(ms: number): number {
return d.getTime(); return d.getTime();
} }
/** The next one. `setMinutes(60)` rolls the date over for us, daylight saving and all. */ function useTick<T>(read: (now: number) => T): T {
function nextHourStart(ms: number): number {
const d = new Date(ms);
d.setMinutes(60, 0, 0);
return d.getTime();
}
function useTick<T>(read: (now: number) => T, next: (now: number) => number): T {
const [value, setValue] = useState(() => read(Date.now())); const [value, setValue] = useState(() => read(Date.now()));
useEffect(() => { useEffect(() => {
let timer = 0; let timer = 0;
const schedule = () => { const schedule = () => {
const now = Date.now(); timer = window.setTimeout(tick, MINUTE - (Date.now() % MINUTE));
timer = window.setTimeout(tick, Math.max(0, next(now) - now));
}; };
const tick = () => { const tick = () => {
window.clearTimeout(timer); window.clearTimeout(timer);
@@ -53,25 +45,24 @@ function useTick<T>(read: (now: number) => T, next: (now: number) => number): T
document.removeEventListener("visibilitychange", woke); document.removeEventListener("visibilitychange", woke);
window.removeEventListener("focus", woke); window.removeEventListener("focus", woke);
}; };
// Both callbacks are module-level functions in every caller, so there is nothing to rebind. // `read` is a module-level function in every caller, so there is nothing to rebind.
}, [read, next]); }, [read]);
return value; return value;
} }
const now = (ms: number) => ms; const now = (ms: number) => ms;
const nextMinute = (ms: number) => ms + MINUTE - (ms % MINUTE);
/** Now, refreshed on every wall-clock minute. */ /** Now, refreshed on every wall-clock minute. */
export function useMinuteTick(): number { export function useMinuteTick(): number {
return useTick(now, nextMinute); return useTick(now);
} }
/** /**
* The top of the hour it is now, refreshed when the clock turns over into the next one. * The top of the hour it is now, read every minute and changed once an hour.
* *
* Returning the hour rather than the instant is what makes this cheap to depend on: the value is * Returning the hour rather than the instant is what makes this cheap to depend on: the value is
* identical for a whole hour, so a memo keyed on it recomputes once, when the answer changed. * identical for a whole hour, so a memo keyed on it recomputes once, when the answer changed.
*/ */
export function useHourStart(): number { export function useHourStart(): number {
return useTick(hourStart, nextHourStart); return useTick(hourStart);
} }
+46
View File
@@ -280,6 +280,52 @@ test.describe("the hour it is now", () => {
expect((await axis(page)).map((entry) => entry.text)).not.toContain("11pm"); expect((await axis(page)).map((entry) => entry.text)).not.toContain("11pm");
await expect(page.locator(".grid-now")).toHaveCount(0); await expect(page.locator(".grid-now")).toHaveCount(0);
}); });
// The two below are about an app left open, so the clock is a running one rather than a pinned
// instant: `install` lets it be moved, and the strip that reads "8pm to 10pm" only exists once
// the hour it holds open has moved from nine to ten.
test("moves on with the clock while the app stays open", async ({ page }) => {
await page.clock.install({ time: clockAt(21, 58) });
await openApp(page);
await page.clock.pauseAt(clockAt(21, 59));
const before = (await axis(page)).map((entry) => entry.text);
expect(before).toContain("9pm");
expect(before).not.toContain("10pm");
await page.clock.runFor(2 * 60_000);
await expect(page.locator(".grid-strip", { hasText: "8pm to 10pm" })).toHaveCount(1);
const after = (await axis(page)).map((entry) => entry.text);
expect(after).toContain("10pm");
expect(after).not.toContain("9pm");
await expect(page.locator(".grid-now")).toHaveCount(1);
});
test("catches up when the clock jumped, as it does after the lid was closed", async ({ page }) => {
await page.clock.install({ time: clockAt(21, 30) });
await openApp(page);
await page.clock.pauseAt(clockAt(21, 31));
expect((await axis(page)).map((entry) => entry.text)).toContain("9pm");
// Asleep, the wall clock moves on and the timers do not: one set for the top of the hour is
// still waiting for the rest of its half hour when the machine wakes at five past ten. What
// the app gets is one minute of being awake.
await page.clock.setSystemTime(clockAt(22, 5));
await page.clock.runFor(60_000);
await expect(page.locator(".grid-strip", { hasText: "8pm to 10pm" })).toHaveCount(1);
const hours = (await axis(page)).map((entry) => entry.text);
expect(hours).toContain("10pm");
expect(hours).not.toContain("9pm");
await expect(page.locator(".grid-now")).toHaveCount(1);
const line = await box(page.locator(".grid-now"));
const row = (await gridFit(page)).rowHeight;
const ten = (await axis(page)).find((entry) => entry.text === "10pm");
const canvas = await box(page.locator(".grid-canvas"));
expect(line.top - canvas.top).toBeGreaterThan(ten!.y);
expect(line.top - canvas.top).toBeLessThan(ten!.y + row);
});
}); });
test.describe("the all-day band", () => { test.describe("the all-day band", () => {