TickTick for Omarchy
What's due and what you haven't done yet, in the Omarchy bar. Tasks, habits and a focus timer in one popup — add, edit and complete without leaving the desktop.

The bar shows a count ( 3 2♦ — three tasks due, two habits open) and
turns urgent when something is late. Left click opens the panel.
Features
- Tasks due today, tomorrow, or the next seven days, overdue ones first
- Edit a task in place —
efills the add field with the task's own line - Completing a task or checking a habit in is a click on its circle — the row itself is not a hit target, so a stray click costs nothing
- Habits with today's state and a running streak
- Quantified habits (8 cups of water) advance one step per click
- Quick-add field — type a title, hit enter, it lands due today
- Undo window — a completion or check-in is held for a few seconds before it is sent, so a misclick costs nothing
- Focus timer — a pomodoro that uses your TickTick durations, counts down in the bar, and uploads each finished block to your focus statistics
- Fully keyboard driven, with an in-panel shortcut list
- Tag colours come straight from TickTick; due state follows your Omarchy theme
- Everything is theme aware and follows the bar's vertical/horizontal layout
Requirements
| Dependency | Required | Why |
|---|---|---|
| Omarchy 4 (Quattro) with Quickshell | yes | the shell that hosts the plugin |
python3 |
yes | the CLI; standard library only, no pip packages |
curl |
no | not used — the CLI speaks HTTP through urllib |
| A TickTick account | yes | free accounts work; habits need TickTick's own habit feature |
No external Python packages, no build step, and nothing is compiled.
Install
omarchy plugin add https://github.com/SotoAugusto/omarchy-ticktick.git --enable
Then click the widget in the bar. It shows a plug icon until it is connected, and clicking it opens a setup card with three steps and a paste field:
- Open
ticktick.comand sign in F12→ Application → Cookies →https://ticktick.com- Copy the value of the cookie named
t, paste it, press Connect
That is the whole setup. The field is masked, the token is handed to the CLI
through a file in a 0700 directory rather than on a command line, and the
file is deleted as soon as it is read. Nothing reads your browser.
Why a cookie and not a password: see About the API. Short version — TickTick's risk control rejects scripted password logins, so the browser session is the reliable path.
If you would rather stay in a terminal:
~/.config/omarchy/plugins/io.github.sotoaugusto.ticktick/bin/omarchy-ticktick login --token -
--token - prompts with the input hidden, so the credential stays out of
your shell history.
When the token eventually expires, the panel returns to the same card and says so. Reconnecting is the same paste.
Removal
omarchy plugin remove io.github.sotoaugusto.ticktick
That disables the widget, drops its entry from ~/.config/omarchy/shell.json,
and deletes the plugin directory. Two things live outside it and are left
behind on purpose, because they are your data and your credential:
omarchy-ticktick logout # forget the token + keyring entry
rm -rf ~/.local/state/omarchy/ticktick # remove the cache and session file
Run logout before removing the plugin if you want the keyring entry
cleared too — the CLI is what knows how to clear it, and removal deletes the
CLI. Nothing in TickTick itself is touched: no tasks, habits, or focus
sessions are deleted by uninstalling.
About the API
TickTick's documented Open API (v1, OAuth) has no habits endpoint at all. It covers projects and tasks and nothing else. This plugin therefore speaks the same private v2 API the TickTick web app uses, authenticated with a session token.
That is a deliberate trade and you should know what you are taking on:
- It is undocumented. TickTick can change or break it without notice.
- Scripted password login is unreliable and risky.
/api/v2/user/signonanswers a correct password withusername_password_not_matchwhen its risk control does not recognise the client, and repeated attempts get the account flagged. Use the browser token. - The edge rejects requests that imitate the web app too closely. Sending
Origin/Referer, or the fullx-deviceobject from TickTick's own bundle, returnsaccess_forbidden; a minimalx-deviceis what works. - No password is written to disk. Only the session token is, at
~/.local/state/omarchy/ticktick/session.json, mode 0600. /api/v2/user/signin— the path several published wrappers still use — is a dead 404. The live one is/user/signon.
Sessions expire eventually. When one does the panel returns to its setup card and asks for a fresh token. There is no automatic renewal: it would need a stored password, and TickTick refuses scripted password logins anyway, so the machinery would sit there unable to do the one thing it exists for.
Using TickTick's Chinese service instead? Set TICKTICK_DOMAIN=dida365.com.
How it fits together
The shell never talks to TickTick. bin/omarchy-ticktick owns the session
and every request, and writes a cache to
~/.local/state/omarchy/ticktick/data.json. Panel.qml watches that file
and shells back out for writes. So a long-lived credential stays out of the
shell process, and every mutation is one command you can run yourself.
bin/omarchy-ticktick session, API calls, the JSON cache
Model.js task filtering, due labels, habit streaks (node-testable)
Service.qml everything that must exist once, not once per screen
Panel.qml the popup — a view of the service
BarWidget.qml the bar slot
Multiple monitors
A bar surface is created per monitor, so the widget and its panel exist once per screen. Anything stateful left in the panel is therefore duplicated, and that is not merely wasteful: two focus clocks each upload the block they finish, inflating the very statistics the timer exists to keep honest.
Service.qml is a service-kind plugin, which the shell mounts exactly once
and hands to views through shell.serviceFor(id) — the same arrangement the
first-party media plugin uses. It owns the cache, the sync timer, the write
queue, the undo window, and the focus clock. The panels render it and keep
only what is genuinely per-screen, such as which date range that screen is
showing.
Panel routing is the bar's job, not the plugin's. A widget lives once per
monitor, but an IPC target resolves to a single handler, so a keybind used to
open the panel on whichever instance registered first. The bar already
answers this for shell.summon by asking Hyprland which output is focused,
so the plugin's own IPC calls borrow that resolution rather than acting
locally.
Two things stay defensive even so, because separate processes are involved:
delivery of the outbox runs under an exclusive lock, and timer-driven syncs
pass --max-age so a sync another process just finished is not repeated.
CLI
omarchy-ticktick login --token - # paste the browser's `t` cookie
omarchy-ticktick login [--email ADDR] # password fallback
omarchy-ticktick sync [--scope tasks|habits|pomo|full]
omarchy-ticktick add "Pay rent" --due today [--priority 0|1|3|5] [--tags work,ops]
omarchy-ticktick update <taskId> [--title T] [--due D] [--priority P] [--tags a,b]
omarchy-ticktick complete <taskId>
omarchy-ticktick reopen <taskId>
omarchy-ticktick delete <taskId>
omarchy-ticktick checkin "Read" --toggle # by name or id
omarchy-ticktick checkin "Water" --value 3
omarchy-ticktick pomo status # today's focus stats + settings
omarchy-ticktick pomo log --minutes 50 # upload a finished focus block
omarchy-ticktick status # cache state as JSON
omarchy-ticktick logout
Every command prints JSON on stdout and errors on stderr, so it scripts and binds cleanly.
Settings
Configure in Setup > Plugins, or inline on the bar entry in
~/.config/omarchy/shell.json:
| Key | Default | What it does |
|---|---|---|
syncInterval |
5 minutes |
2 minutes, 5 minutes, 15 minutes, 1 hour, or Only when opened. |
horizon |
Today |
Today, Tomorrow, or Next 7 days. |
includeOverdue |
true |
Count and list work that is already late. |
showTasks |
true |
Show the task section. |
showHabits |
true |
Show the habit section. |
maxTasks |
12 |
Rows before the list is capped with a "+N more". |
barLabel |
Count |
Count, Next (next task's title, scrolling), or Icon. Right-click the widget to cycle it. |
showPomo |
true |
Focus section, and a live countdown in the bar. |
undoSeconds |
6 |
How long an action is held before sending. 0 disables undo. |
pomoMinutes |
0 |
Focus length. 0 follows your TickTick account. |
shortBreakMinutes |
0 |
Short break. 0 follows your account. |
longBreakMinutes |
0 |
Long break. 0 follows your account. |
longBreakInterval |
0 |
Long break every N blocks. 0 follows your account. |
{
"bar": {
"layout": {
"right": [
{ "id": "io.github.sotoaugusto.ticktick", "horizon": "Next 7 days", "maxTasks": 20 }
]
}
}
}
Keys and clicks
| Where | Input | Action |
|---|---|---|
| Bar | left | open the panel |
| Bar | middle | sync now |
| Bar | right | cycle the label: counts → next task → icon only |
| Panel | click the circle | complete the task / check the habit in |
| Panel | click the title | cycle the range (Today → Tomorrow → 7 days) |
| Panel | Open in TickTick › |
open the web app and close the panel |
| Panel | ↑ ↓ |
move between tasks and habits |
| Panel | enter |
complete the task / check the habit in |
| Panel | u |
undo the held action |
| Panel | a |
focus the quick-add field |
| Panel | e |
edit the selected task in that same field |
| Panel | r |
sync now |
| Panel | p |
start or pause focus |
| Panel | d / del |
discard the focus block (not logged) |
| Panel | g / G |
first / last row |
| Panel | tab |
next bar panel |
| Panel | v / V |
cycle the range forward / back |
| Panel | ? |
show or hide the shortcut list |
| Panel | esc |
back out one layer, then close |
The shortcut list is reachable both ways: ? from the keyboard, and the
? button in the panel header for the mouse. A shortcut list you can only
reach by shortcut helps the people who need it least.
IPC, for keybindings:
omarchy-shell io.github.sotoaugusto.ticktick toggle
omarchy-shell io.github.sotoaugusto.ticktick sync
omarchy-shell io.github.sotoaugusto.ticktick focus # start or pause a block
omarchy-shell io.github.sotoaugusto.ticktick focusStop # discard it
omarchy-shell io.github.sotoaugusto.ticktick cycleLabel # counts / next task / icon
toggle, open, close, show, and hide open the panel on the monitor
Hyprland currently has focused, not on whichever copy of the widget happens
to own the IPC target. sync goes to every instance, since refreshing is not
a place.
Tests
node --test tests/model.test.js
Model.js holds every piece of logic that can be wrong without being
visibly wrong — timezone handling on all-day due dates, streak counting
across a day that is still open, overdue sorting — so it is plain JS with no
QML imports and runs under node.
Background sync
The interval is a short list rather than a number field, because the useful range is narrow and the costs are not obvious:
| Option | Meaning |
|---|---|
2 minutes |
for a busy shared list; the most this should ever poll |
5 minutes |
the default |
15 minutes |
fine for a personal list |
1 hour |
you mostly add tasks rather than watch them |
Only when opened |
no background polling at all |
Opening the panel always syncs, and so do the sync button and r, so this
setting governs only the idle case — how fresh the bar's count is while you
are not looking at it.
Two things make a short list better than a free-form seconds box here. Each
tick is five HTTP requests, and a bar surface exists per monitor, so a
two-screen desktop fires the timer twice. To keep that from doubling the
traffic, a timer-driven sync passes --max-age: whichever instance gets
there first does the work, and the second sees a fresh cache and exits
without a request. Explicit syncs never skip, and neither does a sync with
anything queued in the outbox.
omarchy-ticktick sync --max-age 285 # what the timer runs
Only when opened still syncs once shortly after the shell starts —
otherwise the bar would show a stale count until you first clicked it.
Why writes feel immediate
A write costs a sync, and a full sync is five HTTP round trips — about two seconds. Adding a task cannot change your habits, your check-ins, or your pomodoro settings, so re-fetching them afterwards spends most of that second confirming that nothing happened.
Syncs are therefore scoped. A task write refreshes tasks only, a check-in refreshes habits only, and a finished focus block refreshes the pomodoro stats. Sections outside the scope keep their cached values.
omarchy-ticktick sync --scope tasks # ~0.6s, vs ~2.0s for full
On top of that, a quick-added task appears in the list the moment you press enter, before the request completes. The next cache write replaces it with the real one. The placeholder row is inert — it has no id yet, so it cannot be completed by accident.
Offline
A write made while TickTick is unreachable is not lost and not silently
dropped. It goes into an outbox at
~/.local/state/omarchy/ticktick/outbox.json, and the change is applied to
the local cache immediately — so the task appears in the list, the habit
shows checked, and both survive a shell restart rather than living only in
the panel's memory.
Every sync drains the queue before reading anything back, so what you get afterwards reflects your writes instead of contradicting them. The panel shows a cloud and a count while anything is waiting; clicking it retries.
This is safe to replay because every write carries a client-generated id — tasks, check-in entries, and pomodoro records alike. Sending a queued write twice updates the same record instead of creating a duplicate.
Replays resolve against current server state rather than being sent verbatim. A v2 update is a whole-object write, so a queued completion re-fetches the task and changes only its status; anything you edited on another device in the meantime survives. Queued check-ins re-query the day's entry before deciding whether to add or update it.
A write that TickTick actively rejects is dropped rather than retried forever, since replaying a rejection only earns another rejection. A write that never got a verdict — no network, or a rate limit — is kept and retried. When the queue stalls, the remaining entries stay in order instead of each one hammering a dead network.
omarchy-ticktick status # includes the queued count
Quick add
The field takes more than a title:
Renew the TLS cert #work !1 tomorrow
| Syntax | Does | Whose convention |
|---|---|---|
#tag |
attaches a tag, lowercased | TickTick's — # is what its apps use |
!1 !2 !3 |
priority: high, medium, low | this plugin's |
!high !med !low |
the same, spelled out | this plugin's |
trailing today / tomorrow / yesterday / 2026-09-01 |
sets the due date | TickTick parses dates from text too |
Everything not consumed becomes the title, so the line above creates Renew
the TLS cert, tagged work, high priority, due tomorrow. With no syntax at
all it is a plain task due today.
TickTick has no quick-add symbol for priority — it is still an open request
on their forum — so ! is defined here rather than borrowed. # and the
date words match what TickTick already taught you.
Three details worth knowing:
- A date word only counts at the end.
Plan today standupkeeps its word;Standup notes for todaydoes not, and the preposition goes with the date rather than being left dangling. - An unrecognised
!tokenis left alone in the title. - Tags are lowercased, because that is the key tasks reference them by.
The same syntax is listed under ? in the panel.
Editing uses the same line
Press e on the selected task and the field fills with the line that would
have created it — Renew the cert #work !1 tomorrow. Change it, press enter,
and the task becomes what the line says. Escape cancels.
So there is no separate editor and no second syntax: what you type to make a
task is what you edit to change it. Removal falls out of that — delete
#work from the line and the tag is gone, drop !1 and the priority clears.
A date is applied only when the line carries one, so an undated task stays
undated.
The view, and what it hides
The panel opens on the horizon you configured — Today by default, meaning
overdue work plus today's. That is the point: a bar widget should answer
"what now", not show a backlog.
But a task you just created must never be invisible, so:
- Click the title (or press
v) to cycle the range: Today → Tomorrow → Next 7 days → back to Today.Vsteps the other way. - Position dots beside the title (
● ○ ○) show how many ranges there are and which one you are on. They are deliberately not a chevron: a chevron promises a dropdown, and this control cycles. - The tooltip names where the next click lands rather than a direction, since the control wraps — "Show Tomorrow", then "Show Next 7 days", then "Back to Today".
- Adding something due later widens the view automatically. Type
Ship the release tomorrowfrom a Today view and the view moves to Tomorrow so you can see what you just made.
Widening is a look, not a setting. Closing the panel returns it to your
configured horizon, so the default never drifts.
Long titles
A title wider than the row is elided — until you point at it. The row under the mouse, or under the keyboard cursor, scrolls to reveal the rest, pauses at the end, and slides back.
Only that one row moves. A list where every long title animates at once cannot be scanned, and scanning is what the list is for. Nothing scrolls until you show interest in a specific row, and a row that stops mid-scroll returns home rather than sitting half off the edge.
The bar behaves the same way in Next mode, where the label is a task title:
it scrolls rather than being cut at 28 characters, and pauses at each end
instead of wrapping around. A continuous wrap — the way the media widget
scrolls a track name — leaves the bar slot looking empty for part of every
loop, which a track name can afford and a task reminder cannot. Reading speed
is constant in both places, so a longer title takes longer rather than moving
faster.
The slot keeps a fixed width in Next mode. A bar item that resized with
every task title would shove its neighbours sideways each time you completed
something.
Colours
TickTick stores a colour for tags and for projects, and this shows the tag colour as a dot beside the task title. That is the only colour in a task's data — there is no colour field on a task itself.
Overdue, today, and upcoming have no colour in the API. Every TickTick client paints that itself, so this one paints it from your Omarchy theme rather than hardcoding their palette: overdue takes the accent colour, today takes normal foreground, anything further out is muted. A fixed red would fight every theme you switch to.
Priority is the same story — the API gives an integer, not a colour — so a high-priority task is shown in bold rather than in TickTick's red.
Note that dots only appear on tasks that are both tagged and dated, since the panel lists dated tasks.
Undo, and why it is a delay
Completing a task or checking a habit in does not fire immediately. The
action is held for undoSeconds, an undo row appears, and only when the
window lapses is anything sent. Closing the panel sends everything still held
— closing is not a cancel.
Held actions are a stack, not a single slot. Ticking four things off in a
row is how a list actually gets cleared, and holding only the newest would
mean the first three were already gone by the time you noticed the mistake —
the undo window would fail exactly where mistakes cluster. Each action keeps
its own countdown, u takes back the most recent, and pressing it repeatedly
walks back through them. The row shows how many are behind the one on offer
(+2 more).
It works this way because the alternative does not work. Completing a
recurring task rolls it forward to its next occurrence, and a later
reopen does not put that back; you get a different task in a different
state. An undo that never sends the request is the only one that is
actually reversible.
Focus timer
TickTick's pomodoro is client-side. There is no server-side running clock to
join, so this plugin runs its own and uploads each completed block through
POST /batch/pomodoro — the same thing TickTick's apps do. Your durations,
break lengths, long-break interval, and daily goal are read from the account
(/user/preferences/pomodoro), so the rhythm matches the phone app.
Durations are settable per widget. Each of pomoMinutes,
shortBreakMinutes, longBreakMinutes, and longBreakInterval defaults to
0, meaning "use whatever TickTick says" — so the panel tracks your account
until you deliberately disagree with it, and only the fields you set are
overridden:
{ "id": "io.github.sotoaugusto.ticktick", "pomoMinutes": 25, "longBreakInterval": 3 }
While a block runs the bar shows the countdown instead of the task count. Stopping a block early does not log it: TickTick counts a pomodoro on completion, and banking partial blocks would inflate the statistics this is meant to keep honest.
Changes
CHANGELOG.md — note that 0.2.0 renames the
refreshIntervalSec setting to syncInterval.
Licence and acknowledgements
MIT — see LICENSE.
Built on Omarchy and Quickshell, and it speaks TickTick's private v2 API, which several other projects mapped out first. Who contributed what, and what this plugin owes them, is in ACKNOWLEDGEMENTS.md.
Unofficial and unaffiliated: not endorsed by or supported by TickTick (Appest Inc.). Please do not report breakage here to them.
Limitations
- Read plus the core writes. No editing titles, no rescheduling, no subtasks, no moving between projects — do those in TickTick.
- Habit check-ins are all-or-step. Arbitrary values need
--value. - One account.
- The focus timer lives in the shell process. Restarting the shell loses a running block; it is not persisted.
- A stopped focus block is discarded, never logged.