orgwarrior 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orgwarrior/__init__.py +7 -0
- orgwarrior/__main__.py +3 -0
- orgwarrior/api.py +372 -0
- orgwarrior/cli.py +930 -0
- orgwarrior/config.py +189 -0
- orgwarrior/emacs.py +112 -0
- orgwarrior/help.py +471 -0
- orgwarrior/manpage.py +129 -0
- orgwarrior/orgwarrior.el +3187 -0
- orgwarrior/skill.py +116 -0
- orgwarrior-0.1.0.dist-info/METADATA +248 -0
- orgwarrior-0.1.0.dist-info/RECORD +15 -0
- orgwarrior-0.1.0.dist-info/WHEEL +4 -0
- orgwarrior-0.1.0.dist-info/entry_points.txt +2 -0
- orgwarrior-0.1.0.dist-info/licenses/LICENSE +674 -0
orgwarrior/__init__.py
ADDED
orgwarrior/__main__.py
ADDED
orgwarrior/api.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""orgwarrior's engine for other programs: the command line's views and
|
|
2
|
+
actions, as plain data.
|
|
3
|
+
|
|
4
|
+
from orgwarrior.api import Orgwarrior
|
|
5
|
+
ow = Orgwarrior() # the user's config.toml and daemon
|
|
6
|
+
ow.view("wait +@phone") # the same words `orgwarrior` takes
|
|
7
|
+
|
|
8
|
+
A view is a list of lines, each {"text", "id", "spans"}: `id` is null on
|
|
9
|
+
headers and separators; a date header also has "date" ("2026-10-12"); `spans` are [start, end, role, keyword] for
|
|
10
|
+
coloring, with roles as in config.ROLES. A habit's line also has "habit":
|
|
11
|
+
org-habit's consistency graph, {"text", "spans", "colors"}, a day per
|
|
12
|
+
character ("*" done, "!" today), its roles the days' states, "colors" each
|
|
13
|
+
role's background as Emacs draws it (#rrggbb), for when the config sets none. Every failure is an
|
|
14
|
+
OrgwarriorError whose message is fit to show the user.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from orgwarrior import OrgwarriorError, cli, config as configmod, emacs
|
|
20
|
+
|
|
21
|
+
__all__ = ["Orgwarrior", "OrgwarriorError"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Orgwarrior:
|
|
25
|
+
def __init__(self, config: configmod.Config | None = None, socket: str | None = None):
|
|
26
|
+
self.config = config or configmod.load()
|
|
27
|
+
self.socket = socket or os.environ.get("ORGWARRIOR_SOCKET") or self.config.socket
|
|
28
|
+
|
|
29
|
+
def _call(self, request: dict, **kw):
|
|
30
|
+
return emacs.call(request, self.socket, **kw)
|
|
31
|
+
|
|
32
|
+
def views(self) -> list[str]:
|
|
33
|
+
"""View names to offer: the built-ins, then the config's [views]."""
|
|
34
|
+
return [*cli.BUILTIN_VIEWS, *(v for v in self.config.views if v not in cli.BUILTIN_VIEWS)]
|
|
35
|
+
|
|
36
|
+
def view(self, query: str = "", width: int | None = None, archive: bool = False) -> list[dict]:
|
|
37
|
+
"""The view `orgwarrior QUERY` shows ("" is the default view). What the
|
|
38
|
+
CLI would take as a task ID or a command (`bed`, `add`) is a text
|
|
39
|
+
search here: a front end's query box only shows views. WIDTH is
|
|
40
|
+
what org right-aligns tags to with tags = "right". ARCHIVE (or
|
|
41
|
+
--archive in QUERY) takes in the archive files too."""
|
|
42
|
+
try:
|
|
43
|
+
command, _ = cli.parse(query.split(), self.config)
|
|
44
|
+
except cli.UsageError:
|
|
45
|
+
command = None
|
|
46
|
+
if not isinstance(command, cli.View):
|
|
47
|
+
command = cli.View("search", query.replace("--archive", "").strip().strip('"'),
|
|
48
|
+
archive="--archive" in query.split())
|
|
49
|
+
command.archive = command.archive or archive
|
|
50
|
+
return self._call(cli.view_request(command, self.config, width))
|
|
51
|
+
|
|
52
|
+
def show(self, task_id: str, html: bool = False) -> dict:
|
|
53
|
+
"""{"id", "file", "path", "parents", "prev", "next", "heading", "title",
|
|
54
|
+
"tags", "inherited", "scheduled", "deadline", "keyword", "done", "body",
|
|
55
|
+
"lines", "links", "checkboxes", "notes", "priority", "from", "keywords",
|
|
56
|
+
"keyword_keys", "done_keywords", "children", "clocks", "history",
|
|
57
|
+
"log_drawers"}: `orgwarrior ID`'s
|
|
58
|
+
content. parents are the headings above it, {"id", "title"},
|
|
59
|
+
outermost first; prev and next its siblings' titles, or null. `tags` are the heading's own;
|
|
60
|
+
`inherited` come from the file's #+filetags and parent headings;
|
|
61
|
+
scheduled and deadline are org's timestamps ("<2026-10-16 Fri
|
|
62
|
+
+1w>"), or null, closed the CLOSED one ("[2026-10-20 Tue 09:16]"); lines are {"text", "spans", "links"}, the text as
|
|
63
|
+
Emacs shows it, its links {"start", "end", "url"} being what each
|
|
64
|
+
covers of it; links are {"type", "url", "text"}; checkboxes are {"line", "text", "state"}, line an index into
|
|
65
|
+
lines, state "on", "off" or "trans" (see check); children are
|
|
66
|
+
the direct child headings, {"id", "heading", "text", "tags",
|
|
67
|
+
"keyword", "done"}, text being the heading without keyword and
|
|
68
|
+
tags; notes are the log items with text, newest first, {"time"
|
|
69
|
+
("[2026-10-15 Thu 14:02]"), "state" (a state change's new
|
|
70
|
+
keyword, or null), "kind" (org's purpose: "note", "state",
|
|
71
|
+
"done", ...), "text"}; priority is the cookie's value ("A"), or
|
|
72
|
+
null; from is the FROM property (who added it, someone else), or
|
|
73
|
+
null; keywords and done_keywords are its file's (a #+TODO line
|
|
74
|
+
makes them differ from file to file); clocks its own, as clocks()
|
|
75
|
+
has them; with HTML, "html": its text as org exports it (body only,
|
|
76
|
+
drawers and planning left out, source blocks colored by face name:
|
|
77
|
+
`org-keyword`...); untrusted where others wrote it, so sanitize it
|
|
78
|
+
before a page shows it; history every log item, notes or not, newest first, {"n"
|
|
79
|
+
(as note_edit takes it), "time", "state", "from" (a state change's
|
|
80
|
+
old keyword), "kind", "heading" (its first line), "text", "raw"};
|
|
81
|
+
log_drawers the log and clock drawers holding only those and
|
|
82
|
+
CLOCK lines, which a page showing history and clocks can leave out;
|
|
83
|
+
properties its own, {"name", "value"}, in its drawer's order;
|
|
84
|
+
effort its Effort, effort_minutes that in minutes (null: none, or
|
|
85
|
+
not a duration)."""
|
|
86
|
+
return self._call({"op": "show", "id": task_id, "html": html} if html else {"op": "show", "id": task_id})
|
|
87
|
+
|
|
88
|
+
def keywords(self, file: str | None = None) -> dict:
|
|
89
|
+
"""{"keywords": [...], "done": [...], "keys": [...], "priorities": [...],
|
|
90
|
+
"warning_days"}: the TODO keywords, done ones too, their fast keys (C-c
|
|
91
|
+
C-t's: `TODO(t)`, else org's choice; null for none), the priorities,
|
|
92
|
+
highest first, and org-deadline-warning-days (a deadline without a -3d
|
|
93
|
+
of its own shows that many days ahead).
|
|
94
|
+
Org's own, or FILE's (its #+TODO line, if it has one); show's
|
|
95
|
+
keyword_keys are its file's."""
|
|
96
|
+
return self._call({"op": "keywords", "file": file} if file else {"op": "keywords"})
|
|
97
|
+
|
|
98
|
+
def files(self) -> list[str]:
|
|
99
|
+
"""The agenda files, as IDs and the other calls name them
|
|
100
|
+
("projects.org": relative to org-directory)."""
|
|
101
|
+
return self._call({"op": "files"})
|
|
102
|
+
|
|
103
|
+
def ping(self, timeout: float = 3) -> None:
|
|
104
|
+
"""Raise OrgwarriorError unless the daemon answers within TIMEOUT
|
|
105
|
+
seconds with orgwarrior.el loaded: a health check's question."""
|
|
106
|
+
self._call({"op": "keywords"}, timeout=timeout)
|
|
107
|
+
|
|
108
|
+
def tags(self) -> dict:
|
|
109
|
+
"""For a tag picker: {"groups": [{"exclusive", "tags"}], "other": [...],
|
|
110
|
+
"keys": [[tag, key], ...]}, keys being C-c C-q's (#+TAGS: @home(h), the
|
|
111
|
+
rest as org assigns them).
|
|
112
|
+
Groups are as the agenda files' #+TAGS declare them (an exclusive one
|
|
113
|
+
is a { } group: setting one of its tags removes the others); "other"
|
|
114
|
+
are tags in use but not declared, filetags left out."""
|
|
115
|
+
return self._call({"op": "tags"})
|
|
116
|
+
|
|
117
|
+
def calendar(self, days: int = 365) -> list[dict]:
|
|
118
|
+
"""Every dated entry, for a calendar: {"id" (the whole hash, which
|
|
119
|
+
any ID taking command takes), "title" (as the agenda shows it, no
|
|
120
|
+
keyword, tags or timestamps), "keyword", "done", "habit", "tags",
|
|
121
|
+
"inherited" (as show has them), "category", "stamps"}. A stamp is {"kind":
|
|
122
|
+
"scheduled", "deadline" or "timestamp", "start", "start_time",
|
|
123
|
+
"end", "end_time", "repeat", "warning"}, dates "2026-10-14",
|
|
124
|
+
times "14:00" or null, end being the range's end or the start
|
|
125
|
+
again; repeat {"type": "+", "++" or ".+", "value", "unit": "h", "d",
|
|
126
|
+
"w", "m" or "y"} and warning {"value", "unit"} as org parsed them,
|
|
127
|
+
or null. Diary sexps (`%%(...)` lines, kind "sexp", and `<%%(...)>`
|
|
128
|
+
timestamps) are evaluated for each of the next DAYS days instead:
|
|
129
|
+
{"kind", "dates": [{"date", "text"}]}, text being what the sexp
|
|
130
|
+
returned ("Robin is 46"), or null. Done tasks and habits are
|
|
131
|
+
included: what to show is the caller's to decide."""
|
|
132
|
+
return self._call({"op": "calendar", "days": days})
|
|
133
|
+
|
|
134
|
+
def refile_targets(self, ids: list[str]) -> list[str]:
|
|
135
|
+
"""Where IDS can be refiled, as labels ("tasks.org › Tasks") that
|
|
136
|
+
act(ids, "refile", [label]) takes as is."""
|
|
137
|
+
return self._call({"op": "refile-targets", "ids": list(ids)})
|
|
138
|
+
|
|
139
|
+
def refile_places(self, ids: list[str]) -> list[str]:
|
|
140
|
+
"""Where a new heading for IDS can go (act's NEW): the top level of
|
|
141
|
+
each file in org-refile-targets ("projects.org"), then under each
|
|
142
|
+
target, as labels act takes as is."""
|
|
143
|
+
return self._call({"op": "refile-places", "ids": list(ids)})
|
|
144
|
+
|
|
145
|
+
def outline(self, file: str) -> dict:
|
|
146
|
+
"""{"file", "children"}: FILE's (an agenda file as show names it,
|
|
147
|
+
"projects.org") top-level headings, in file order, each as show's
|
|
148
|
+
children are, with its own "children" the same way, all the way
|
|
149
|
+
down (a foldable tree)."""
|
|
150
|
+
return self._call({"op": "outline", "file": file})
|
|
151
|
+
|
|
152
|
+
def reorder(self, ids: list[str], parent: str | None = None, file: str | None = None) -> list[dict]:
|
|
153
|
+
"""Put the children of PARENT (a task ID), or FILE's top-level
|
|
154
|
+
headings, in the order of IDS, which must be all of them (else
|
|
155
|
+
they changed since they were shown: refused). Each moves with its
|
|
156
|
+
subtree. Returns the children as they now are."""
|
|
157
|
+
return self._call({"op": "reorder", "ids": list(ids), "parent": parent, "file": file})
|
|
158
|
+
|
|
159
|
+
def wants_note(self, ids: list[str], verb: str) -> bool:
|
|
160
|
+
"""Whether org would ask for a note when VERB (a keyword) is applied to IDS."""
|
|
161
|
+
return self._call({"op": "wants-note", "ids": ids, "verb": verb})
|
|
162
|
+
|
|
163
|
+
def check(self, task_id: str, n: int, text: str) -> dict:
|
|
164
|
+
"""Toggle checkbox N (from 1, as show lists them), as C-c C-c does:
|
|
165
|
+
cookies follow. TEXT is the item's text as shown, and must still
|
|
166
|
+
match, so a list that changed underneath is refused. For a front
|
|
167
|
+
end that shows boxes in place; the CLI has no such verb."""
|
|
168
|
+
return self._call({"op": "check", "id": task_id, "n": n, "text": text})
|
|
169
|
+
|
|
170
|
+
def set_entry(self, task_id: str, text: str, old: str) -> dict:
|
|
171
|
+
"""Replace the task's text below its heading (show's "body":
|
|
172
|
+
planning line, drawers, body; children stay) with TEXT, saved
|
|
173
|
+
with one blank line after the planning line and drawers. OLD is
|
|
174
|
+
the body as shown, and must still match, so an entry that changed
|
|
175
|
+
underneath is refused. A line org would read as a heading is
|
|
176
|
+
refused too."""
|
|
177
|
+
return self._call({"op": "set-entry", "id": task_id, "text": text, "old": old})
|
|
178
|
+
|
|
179
|
+
def act(self, ids: list[str], verb: str, args: list[str] = (), note: str | None = None,
|
|
180
|
+
new: str | None = None) -> list[dict]:
|
|
181
|
+
"""Apply VERB (a keyword or nokeyword, sched, due, tag, pri, note,
|
|
182
|
+
prop (ARGS: name, value; value "none" removes it), effort (a
|
|
183
|
+
duration, one of Effort_ALL's where set, or "none"), rename, refile,
|
|
184
|
+
up, down, promote, demote, archive, kill) to IDS.
|
|
185
|
+
No confirmation here: kill deletes at once. Results
|
|
186
|
+
of refile and rename carry "old", the ID before. NEW (refile only)
|
|
187
|
+
is the title of a plain heading to make and refile into, as the
|
|
188
|
+
last child of the place ARGS names (one of refile_places, default
|
|
189
|
+
config's [refile] new_in)."""
|
|
190
|
+
return self._call(cli.act_request(cli.Act(list(ids), verb, list(args), note, new=new),
|
|
191
|
+
self.config))
|
|
192
|
+
|
|
193
|
+
def add(self, text: str | None = None, *, title: str | None = None,
|
|
194
|
+
keyword: str | None = None, priority: str | None = None,
|
|
195
|
+
tags: list[str] = (), scheduled: str | None = None,
|
|
196
|
+
deadline: str | None = None, body: str | None = None,
|
|
197
|
+
properties: dict[str, str] | None = None, where: str | None = None,
|
|
198
|
+
captured: bool | None = None, to: str | None = None,
|
|
199
|
+
under: str | None = None, effort: str | None = None,
|
|
200
|
+
closed: str | None = None) -> dict:
|
|
201
|
+
"""Add a task where config's [add] says: from `orgwarrior add` syntax
|
|
202
|
+
(TEXT), or from fields (TITLE etc.), where nothing in the title is
|
|
203
|
+
read as syntax. KEYWORD "" adds a plain heading; PRIORITY is one
|
|
204
|
+
of keywords()'s priorities; EFFORT a duration, as the effort verb takes
|
|
205
|
+
it. Dates are org-read-date strings; BODY goes below
|
|
206
|
+
the drawer, as set_entry lays it out; PROPERTIES go in the drawer
|
|
207
|
+
(beside :CAPTURED:). WHERE ("task" or "event") files it where
|
|
208
|
+
config says that kind goes; CAPTURED, if given, overrides config's
|
|
209
|
+
:CAPTURED: stamp. TO puts it elsewhere, a place from add_places
|
|
210
|
+
(or any name refile takes); UNDER under a heading, by its ID."""
|
|
211
|
+
if title is None:
|
|
212
|
+
return self._call(self._placed(cli.add_request(text, self.config), where, captured, to, under))
|
|
213
|
+
return self._call(self._placed(cli.add_request(None, self.config, {
|
|
214
|
+
"title": title, "keyword": keyword, "priority": priority, "tags": list(tags),
|
|
215
|
+
"scheduled": scheduled, "deadline": deadline, "body": body, "effort": effort,
|
|
216
|
+
"closed": closed, "properties": properties or {}}), where, captured, to, under))
|
|
217
|
+
|
|
218
|
+
def _placed(self, request: dict, where: str | None, captured: bool | None = None,
|
|
219
|
+
to: str | None = None, under: str | None = None) -> dict:
|
|
220
|
+
"""REQUEST filed where config puts a task (WHERE "task") or an event
|
|
221
|
+
("event") instead of where its own kind goes; CAPTURED overriding
|
|
222
|
+
whether it's stamped."""
|
|
223
|
+
if where not in (None, "task", "event"):
|
|
224
|
+
raise ValueError(f"where is task or event, not {where!r}")
|
|
225
|
+
if where:
|
|
226
|
+
place = cli.add_request(None, self.config, event=where == "event")
|
|
227
|
+
request.update({k: place[k] for k in ("file", "heading", "captured")})
|
|
228
|
+
if captured is not None:
|
|
229
|
+
request["captured"] = captured
|
|
230
|
+
return {**request, "to": to, "under": under}
|
|
231
|
+
|
|
232
|
+
def usual_target(self, labels: list[str]) -> str | None:
|
|
233
|
+
"""Which of LABELS (refile targets) config's [refile] usual names:
|
|
234
|
+
a heading of that name in a file of it (`tasks` is tasks.org › Tasks,
|
|
235
|
+
not inbox.org › Tasks), then in any file (as refile matches), then a
|
|
236
|
+
file of it, else a heading containing it. None without one, or no
|
|
237
|
+
match."""
|
|
238
|
+
want = (self.config.refile_usual or "").strip().lower()
|
|
239
|
+
if not want:
|
|
240
|
+
return None
|
|
241
|
+
last = lambda l: l.split(" › ")[-1].lower()
|
|
242
|
+
for test in (lambda l: l.lower().startswith(f"{want}.org › ") and last(l) == want,
|
|
243
|
+
lambda l: " › " in l and last(l) == want,
|
|
244
|
+
lambda l: l.lower() in (want, f"{want}.org"),
|
|
245
|
+
lambda l: " › " in l and want in last(l)):
|
|
246
|
+
if hit := next((l for l in labels if test(l)), None):
|
|
247
|
+
return hit
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def add_places(self, event: bool = False) -> dict:
|
|
251
|
+
"""Where add (add_event, with EVENT) can put something:
|
|
252
|
+
{"places": [{"label", "file"}], "keywords": {file: [open keywords]}}.
|
|
253
|
+
A label is what TO takes: where config says first, then the
|
|
254
|
+
refile targets as seen from that file, each file's top level
|
|
255
|
+
included. The keywords are those files' and the agenda files'
|
|
256
|
+
(a #+TODO line makes a file's its own)."""
|
|
257
|
+
place = cli.add_request(None, self.config, event=event)
|
|
258
|
+
r = self._call({"op": "add-places", "file": place["file"], "heading": place["heading"]})
|
|
259
|
+
return {"places": r["places"], "keywords": {k["file"]: k["open"] for k in r["keywords"]}}
|
|
260
|
+
|
|
261
|
+
def add_event(self, text: str | None = None, *, title: str | None = None,
|
|
262
|
+
tags: list[str] = (), when: str | None = None, body: str | None = None,
|
|
263
|
+
properties: dict[str, str] | None = None, where: str | None = None,
|
|
264
|
+
captured: bool | None = None, to: str | None = None,
|
|
265
|
+
under: str | None = None) -> dict:
|
|
266
|
+
"""Add an event (no keyword, a plain timestamp) where config's
|
|
267
|
+
[add.event] says: from `orgwarrior add event` syntax (TEXT, the date
|
|
268
|
+
as @DATE), or from fields. WHEN is a date as sched takes it,
|
|
269
|
+
ranges and repeaters too. PROPERTIES and WHERE as for add: WHERE
|
|
270
|
+
"task" files it with the tasks (an inbox to triage); TO and UNDER
|
|
271
|
+
as for add."""
|
|
272
|
+
if title is None:
|
|
273
|
+
return self._call(self._placed(cli.add_request(text, self.config, event=True), where,
|
|
274
|
+
captured, to, under))
|
|
275
|
+
return self._call(self._placed(cli.add_request(None, self.config, {
|
|
276
|
+
"title": title, "tags": list(tags), "when": when, "body": body,
|
|
277
|
+
"properties": properties or {}}, event=True), where, captured, to, under))
|
|
278
|
+
|
|
279
|
+
def clock(self) -> list[dict]:
|
|
280
|
+
"""The clocks running: the open CLOCK lines in the agenda files, not
|
|
281
|
+
Emacs's memory, oldest first. Usually one; two when two machines
|
|
282
|
+
clocked in. Each {"id", "heading", "title", "file", "start"
|
|
283
|
+
("2026-10-20 14:05"), "end" (null), "minutes", "stale" (running past
|
|
284
|
+
4 hours: likely forgotten)}."""
|
|
285
|
+
return self._call({"op": "clock"})
|
|
286
|
+
|
|
287
|
+
def clock_in(self, task_id: str, time: str | None = None) -> dict:
|
|
288
|
+
"""Clock in on the task, as org-clock-in does, at TIME (None: now):
|
|
289
|
+
"17:30", "5pm", "-15m", "yesterday 17:30", never later than now.
|
|
290
|
+
Whatever clock is running is closed at the same time. {"clock": as
|
|
291
|
+
clock() gives it, "closed": those closed, as clock_out gives them}."""
|
|
292
|
+
return self._call({"op": "clock-in", "id": task_id, "time": time})
|
|
293
|
+
|
|
294
|
+
def clock_out(self, task_id: str | None = None, time: str | None = None) -> dict:
|
|
295
|
+
"""Clock out at TIME (as clock_in takes it, later than now too:
|
|
296
|
+
"+10m"; None: now): the task's
|
|
297
|
+
clock, else the one running (several: refused, they're listed). As
|
|
298
|
+
clock() gives it, with "end" and "minutes", and "removed" when org
|
|
299
|
+
removed a zero-time line (org-clock-out-remove-zero-time-clocks)."""
|
|
300
|
+
return self._call({"op": "clock-out", "id": task_id, "time": time})
|
|
301
|
+
|
|
302
|
+
def clock_cancel(self, task_id: str | None = None) -> dict:
|
|
303
|
+
"""Remove the running clock's line, as org-clock-cancel does: the
|
|
304
|
+
task's, else the one running."""
|
|
305
|
+
return self._call({"op": "clock-cancel", "id": task_id})
|
|
306
|
+
|
|
307
|
+
def clocks(self, task_id: str) -> dict:
|
|
308
|
+
"""The task's own clocks, newest first: {"id", "heading", "minutes"
|
|
309
|
+
(all of them), "clocks": [{"n" (from 1), "start", "end" (null while
|
|
310
|
+
running), "minutes", "text" (the line, for clock_edit's OLD)}]}."""
|
|
311
|
+
return self._call({"op": "clocks", "id": task_id})
|
|
312
|
+
|
|
313
|
+
def clock_edit(self, task_id: str, n: int, start: str | None = None,
|
|
314
|
+
end: str | None = None, old: str | None = None) -> dict:
|
|
315
|
+
"""Change clock N's START and/or END (as clock_in and clock_out take
|
|
316
|
+
them, so an end may be later than now; a
|
|
317
|
+
time of day is on the clock's own date, an end before the start
|
|
318
|
+
the next day's). OLD, the clock's text as shown, must still match.
|
|
319
|
+
Org recomputes its total. The task's clocks, as clocks() gives them."""
|
|
320
|
+
return self._call({"op": "clock-edit", "id": task_id, "n": n, "start": start,
|
|
321
|
+
"end": end, "old": old})
|
|
322
|
+
|
|
323
|
+
def clock_add(self, task_id: str, start: str, end: str | None = None,
|
|
324
|
+
duration: str | None = None) -> dict:
|
|
325
|
+
"""Log time already spent: a finished clock from START (as clock_in
|
|
326
|
+
takes it) to END (a time of day on START's date) or for DURATION
|
|
327
|
+
("1h30m", "45m", "1:30"), not later than now. A running clock is
|
|
328
|
+
left alone. The task's clocks after, as clocks() gives them."""
|
|
329
|
+
return self._call({"op": "clock-add", "id": task_id, "start": start, "end": end,
|
|
330
|
+
"duration": duration})
|
|
331
|
+
|
|
332
|
+
def clock_remove(self, task_id: str, n: int, old: str | None = None) -> dict:
|
|
333
|
+
"""Remove clock N (a running one too: as clock_cancel). OLD as for
|
|
334
|
+
clock_edit. The task's clocks after."""
|
|
335
|
+
return self._call({"op": "clock-edit", "id": task_id, "n": n, "old": old, "remove": True})
|
|
336
|
+
|
|
337
|
+
def note_edit(self, task_id: str, n: int, text: str, old: str | None = None) -> list[dict]:
|
|
338
|
+
"""Set log item N's text (as show's history numbers them; "" leaves
|
|
339
|
+
just its first line, which a plain note can't do). OLD, its "raw" as
|
|
340
|
+
shown, must still match. The task's history after."""
|
|
341
|
+
return self._call({"op": "note-edit", "id": task_id, "n": n, "text": text, "old": old})
|
|
342
|
+
|
|
343
|
+
def note_remove(self, task_id: str, n: int, old: str | None = None) -> list[dict]:
|
|
344
|
+
"""Remove log item N, as note_edit numbers and guards it; an emptied
|
|
345
|
+
log drawer goes too. The task's history after."""
|
|
346
|
+
return self._call({"op": "note-edit", "id": task_id, "n": n, "old": old, "remove": True})
|
|
347
|
+
|
|
348
|
+
def report(self, words: str = "", archive: bool = False) -> dict:
|
|
349
|
+
"""Time clocked, as org's clocktable counts it, over the agenda files:
|
|
350
|
+
`orgwarrior clocked WORDS` (a span: today, yesterday, week, lastweek,
|
|
351
|
+
month, lastmonth, year, 2026-10-20, 2026-W43, 2026-10, 2026, or
|
|
352
|
+
2026-10-01..2026-10-15; default today; then a tag match). {"label",
|
|
353
|
+
"kind" (day, week, month, year, range), "start", "end" (its last day),
|
|
354
|
+
"prev", "next" (the spans either side, null for a range), "match",
|
|
355
|
+
"minutes", "files": [{"file", "minutes", "rows": [{"depth", "id",
|
|
356
|
+
"title", "minutes", "effort_minutes"}]}]}: every heading holding
|
|
357
|
+
time, by depth, in file order. ARCHIVE counts the archive files too."""
|
|
358
|
+
return self._call({"op": "report", "words": words, "archive": archive})
|
|
359
|
+
|
|
360
|
+
def props(self, task_id: str | None = None, name: str | None = None) -> dict:
|
|
361
|
+
"""For a property editor: {"names": the property names in use in the
|
|
362
|
+
agenda files, "values": NAME's allowed values for the task (its
|
|
363
|
+
NAME_ALL, a #+PROPERTY line or a parent's), or null}."""
|
|
364
|
+
return self._call({"op": "props", "id": task_id, "name": name})
|
|
365
|
+
|
|
366
|
+
def entries_with(self, prop: str, value: str) -> list[dict]:
|
|
367
|
+
"""The entries whose own PROP is VALUE (FROM = robin: what someone
|
|
368
|
+
added), archived and commented ones left out: {"title", "keyword",
|
|
369
|
+
"done", "scheduled", "deadline", "timestamp" (the first plain one,
|
|
370
|
+
an event's), "closed", "captured"}, the dates as org writes them,
|
|
371
|
+
or null."""
|
|
372
|
+
return self._call({"op": "entries", "property": prop, "value": value})
|