Skip to content

v0.20.12

Latest

Choose a tag to compare

@github-actions github-actions released this 06 Sep 11:57
· 20 commits to main since this release
a7e6d18

See CHANGELOG.md for full history.

  • Fixed — a repeat command could stop with "a Things update has redesigned it again" when nothing of the sort had happened. Setting up a repeating to-do picks a frequency and then reads the dialog to see which controls that frequency produced. The pick was reported as done the moment it was clicked, without checking that the dialog had taken it, and the read that followed did not wait for Things to rebuild that part of the dialog — so on some Macs the command read the dialog as it was a moment earlier, found the controls it expected missing, and blamed the app. It now confirms the frequency actually changed before going on, waits for the rebuilt controls, and if it genuinely cannot recognize the dialog it says what it found there instead of what it assumes happened. Refs #695.

  • Fixed — making a to-do repeat on two weekdays (or on a day of the month that was not its own) refused, and created nothing. things todo make-repeating <to-do> --frequency weekly --weekdays monday,thursday stopped with "the Repeat dialog does not hold what this drive entered — 1 control(s) differ: Next (first occurrence)" and exited 3, on a request Things is perfectly happy to accept. The command had read the dialog's first-occurrence field before setting the weekdays, found it already showing the date it wanted, and decided it had nothing to do there — and then setting the weekdays moved that date to the earliest matching day, so the check it runs before committing found the wrong one and stopped. Nothing was ever created wrongly; the request was simply refused. The same thing happened for --on-day, --on-weekday/--on-ordinal and --yearly-month whenever the day asked for was not the to-do's own. The first occurrence is now set whenever anything else in the rule can move it. Refs #695.

  • Changed — the repeat-rule dialog is now driven through the Accessibility API directly, which makes every repeat command substantially faster. Setting up a repeating to-do means reading and setting a dozen controls in Things' Repeat dialog. Each of those used to be a separate round trip through System Events, and the round trips — not the work — were most of what the command spent its time on. The same reads and clicks are now made directly, in far fewer processes, and the parts of the drive that must stay as they were (typing into the number fields, selecting a row) are unchanged. The command asks the app for exactly what it asked before, checks everything it checked before, and refuses in the same words; THINGS_API_TRACE=1 now reports each individual step with its own duration. If anything about it misbehaves, THINGS_API_REPEAT_RAWAX=0 restores the previous behavior exactly. Refs #695, #687.

  • Fixed — turning a to-do into a repeating one with the Things window closed told you to click a Dock icon; now it opens the window and gets on with it. Reordering an area already did this. The repeat verbs did not: they saw no window, said "Things is running but has no open window", and stopped — on a Mac that was unlocked, in front of someone who had simply closed the window. They now do what the sidebar move does — ask whether the screen is locked, and if it is not, ask Things to reopen its window, check that one actually appeared, and carry on. The window is left open afterwards, and the result says so: "Things had no open window, so one was reopened to run this — it was left open". If the screen IS locked, or the reopen produces no window, the refusal is unchanged and nothing has been created. Refs #732.

  • Improved — on a Mac with the helpers installed, the repeat-rule dialog stops asking Things questions it has already been told the answer to. Two of the waits in that dialog belong between one step and the next: after picking a frequency, Things rebuilds the row of cadence controls, and after any pop-up selection it takes a moment to put the menu away. The next step used to discover both by asking the app the same question over and over — and on a Mac where the helpers carry the automation, that was the only way it could, because the app's own announcements were only reachable from inside a script and scripts routed through the helpers are not allowed to open a socket.

    They are reachable from outside one. The helper has been listening on behalf of the command since before the step ran, so the command now waits for Things to say the row was rebuilt, and for it to say the menu closed, and only then sends the next step — which is generated without the loop that would have asked. It is the same script that a Mac with the developer tools has been running all along, so nothing new is being tried; what is new is that a routed Mac can now get to it. The wait is armed only where Things is certain to announce something, and if the announcement does not arrive the command falls back to asking, exactly as before.

    A Mac without the helpers is unaffected — byte for byte, the same scripts as before. THINGS_API_TRACE=1 records each wait, whether it was satisfied, and which step dropped its loop. Refs #695, #676.

  • Fixed — making a to-do that already had a deadline repeat reported a failure, for doing exactly what Things does. Turn a deadlined to-do into a repeating one and Things' own Repeat dialog opens with "Add deadlines" already ticked and the gap between the start and the due date already filled in — so the series it creates is deadlined, and its first occurrence starts where the to-do started and is due the same number of days later. That is the app's default and it is what you would get by hand. This command committed exactly that and then compared it against a rule that had never mentioned a deadline, decided the first occurrence had moved, and exited 3 on a series that was correct.

    The deadline the item already carries is now the series' deadline: the first occurrence lands on the to-do's own scheduled date, every occurrence is due the same number of days later, and the result says so — "the to-do's own deadline came with it: every occurrence is due 3 days after its start — pass --deadline (or --start-days-earlier) to set a different one". Passing --deadline (or --start-days-earlier) overrides it, as before. A deadline that falls BEFORE the start is not inherited, because the dialog discards it.

  • Changed — a command that makes several changes to do one thing now holds the write lock for the whole thing, so two commands can no longer interleave halfway through each other. Moving a set of to-dos, reordering a list, editing one checklist item, clearing a reminder, archiving a heading with its children, dragging an area in the sidebar: each of those is several changes in a row, and each one used to take and release the lock per change. Another things command running at the same time could land in any of the gaps — between reading a checklist and writing the edited version back (its edit disappears), between working out where an area will land in the sidebar and dragging it there (it lands somewhere else), between parking a to-do and putting it back (it comes back to the wrong place).

    A second command now WAITS for the first to finish, for up to thirty seconds, and if it is still waiting after that it says what it is waiting for: "another operation holds the mutation lock: area.reorder (pid 4321), since 2026-09-05T10:02:11Z, held for 12s — pid 4321 is still running". If the holder is gone, it says the lock is stale and that running the command again takes it. A --dry-run never waits for anything: it changes nothing, so it has nothing to serialize against.

  • Fixed — a command killed mid-write no longer leaves its lock behind for the next one to wait out. A command interrupted by a timeout or a Ctrl-C prints its "outcome uncertain" line and exits, and that exit path never released the lock — so the next write spent its full wait discovering the holder was dead. It now releases on the way out. A hard kill (kill -9, a power cut) still cannot run any code, and that case is unchanged in behavior but sturdier underneath: a lock is recognized as abandoned by the holder's process identity rather than by its process number alone, so a recycled number can no longer make a dead command's lock look alive forever. things rescue status now also names the operation that holds the lock, not just its process number.

  • Changed — reordering sidebar areas stays off by default, and the reason it gives is now the real one. The refusal used to say the command was off "until it completes inside five seconds on real hardware", which framed it as a countdown to either promotion or deletion. The ruling is different: a command that drives the window is fragile in the presence of a person using the app, and belongs on a Mac nobody is working in — so it stays available behind things config set experimental-area-reorder true rather than being removed, and the refusal now says that, along with the fact that other changes wait while it runs. Refs #676.

  • Fixed — a GUI command run while the Mac was locked said Things had no open window and told you to click its Dock icon. It could not know that. A locked screen hides every window from every application, so "no window" is what the command sees whether the window is closed, the screen is locked, or the window is on another desktop — and it picked the first of the three and said it as a fact, behind a lock screen, after five and a half seconds of looking. It also reported the result as a failed change, though it had not touched anything.

    Every command that has to read or click the Things window now asks the Mac whether the screen is locked before it looks at anything else. Locked, it stops immediately — in about a fifth of a second, with nothing sent and nothing changed — and says so: "Refused to drive the Things window: the screen is locked, so no window can be read or clicked. Nothing was changed. Unlock the Mac and re-run." A running screen saver gets the same treatment. Commands that only press a menu item are unaffected: those work with the screen locked, and always have.

    If the Mac will not say whether it is locked, nothing is refused — but the older message stops claiming to know, and reads "no Things window could be read — the window may be closed, or the Mac's screen may be locked, or the window may be on another desktop" instead. With the screen confirmed unlocked, the original wording stands, because then it is true.

    things doctor --ui-state gains a session: row — locked, screen saver, unlocked, or unknown — printed above everything else it reports about the screen. Refs #732.

  • Improved — that check no longer costs anything. Asking the Mac whether its screen is locked took a fifth of a second, every time, before every GUI-driven command — almost all of it the cost of starting a helper process rather than of the question, which takes microseconds. The question now travels with the first thing the command was already going to do (bringing Things to the front), so a command on an unlocked Mac pays nothing for it at all. todo make-repeating was asking twice, once before it copies anything and once during the drive; both are gone. Measured in a clean VM against the previous build: area reorder 7 round trips to 6, make-repeating 16 to 14. Refs #732.

  • New — a screen saver is now woken up instead of refused, when the Mac is not asking for a password. A running screen saver counts as a locked screen as far as the window server is concerned, so a GUI command met one with "unlock the Mac" — on a Mac that would have let anyone in with a keypress. The command now presses a single Shift key, waits for the screen to report itself awake, and carries on if it does. If the Mac is asking for a password nothing happens, and the command says so and stops: "Refused to drive the Things window: the screen saver is up and did not clear when the Mac was nudged, so the Mac is asking for a password. Nothing was changed." Nothing here can get past a password — there is no way to tell the two situations apart until the key has been tried, which is why it is tried and then re-checked rather than assumed either way.

    A woken screen stays awake, so a command that had to do this says so: "the screen saver was up and was dismissed to run this — the Mac did not ask for a password, and the screen is awake now." Refs #732.

  • New — reordering an area with no Things window open now opens one, instead of telling you to click the Dock icon. With the screen confirmed unlocked, a missing window is a thing the command can fix for itself: it asks Things to reopen its window — the app's own command, which puts the window back on the desktop you are looking at — reads the sidebar again, and does the move. If reopening does not produce a window, the old refusal stands.

    The window is left open. It is where the move happened, it may be what you are looking at by the time the command finishes, and closing it again would be a second surprise on top of the first — so the result tells you instead: "Things had no open window, so one was reopened to run this — it was left open." Refs #732.