-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathweekly.js
executable file
·245 lines (231 loc) · 7.93 KB
/
weekly.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
#!/usr/bin/env node
const { execSync } = require('child_process')
const inquirer = require('inquirer')
const chalk = require('chalk')
const boxen = require('boxen')
const moment = require('moment')
// Wrappers around taskwarrior
const pendingOrWaiting = '\\( status:pending or status:waiting \\)'
const task = (args, opts = {stdio: 'inherit'}) => {
const cmd = `task ${args}`.replace(/\n/g, ' ').replace(/ +/g, ' ')
console.log(chalk`{gray => ${cmd}}`)
return execSync(cmd, opts)
}
const captureTask = (args) => task(args, {}).toString()
const taskLines = (args) => captureTask(args).split('\n').map(l => l.trim()).filter(l => l.length > 0)
const printTasks = (ids) => task(`rc.report.next.filter= rc.verbose=label,sync next ${ids.join(',')}`)
const loadTasks = (args) => JSON.parse(captureTask(args + ' export'))
// Step logic implementations
const pause = (prompt) => prompt({
type: 'input',
name: 'pause',
message: 'Press Return when done'
})
const projectReview = async (prompt, stepHeader) => {
const projects = taskLines(`${pendingOrWaiting} _unique project`)
for (const [index, project] of projects.entries()) {
const projectHeader = chalk`{gray (${index + 1}/${projects.length})} Project: {white.bold ${project}}`
console.log(boxen(
chalk`${stepHeader}\n${projectHeader}`,
{borderStyle: 'round'}
))
if (process.env.WEEKLYJS_OLD_COLUMNS) {
task(
`rc.report.next.filter='status:pending or status:waiting'
rc.report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,wait.remaining,due,until.remaining,description,urgency
rc.report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Wait,Due,Until,Description,Urg
rc.verbose=label,sync
next "project.is:${project}"
`
)
} else {
task(
`rc.report.next.filter='status:pending or status:waiting'
rc.report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,wait.remaining,due.relative,until.remaining,description,urgency
rc.report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Wait,Due,Until,Description,Urg
rc.verbose=label,sync
next "project.is:${project}"
`
)
}
await prompt({
type: 'input',
name: 'pause',
suffix: boxen(chalk`{gray ^^^} ${projectHeader} {gray ^^^}`),
message: 'Press Return once all tasks for the project updated\n'
})
}
}
const setNextTask = async (prompt, taskIds, message) => {
const formatDate = (dateStr) => moment(dateStr, 'YYYYMMDD[T]HHmmss[Z]Z').fromNow()
const taskChoiceString = (task) => {
let str = chalk`{gray ${task.id}}`
str += chalk` [{yellow Urg} ${Math.round(task.urgency * 100) / 100}]`
if (task.tags) {
str += chalk`[{yellow Tag} ${task.tags.join(' ')}]`
}
if (task.due) {
str += chalk`[{yellow Due} ${formatDate(task.due)}]`
}
if (str[str.length - 1] !== ' ') {
str += ' '
}
str += task.description
return str
}
const tasks = loadTasks(taskIds.join(','))
tasks.sort((a, b) => b.urgency - a.urgency)
const result = await prompt({
name: 'chooseNext',
type: 'list',
message: message,
prefix: chalk`{red !}`,
choices: tasks.map(t => ({
name: taskChoiceString(t),
short: t.description,
value: t.id}))
})
const nextId = result.chooseNext
const rest = taskIds.filter(id => id !== nextId)
if (rest.length > 0) {
task(`${rest.join(',')} rc.recurrence.confirmation=no mod -next`)
}
task(`${nextId} rc.recurrence.confirmation=no mod +next`)
}
const nextReviewSingleProject = (prompt, project, projects) => {
const subprojects = projects.filter(other => other.indexOf(`${project}.`) === 0)
const isLeafProject = subprojects.length === 0
const taskIds = taskLines(`${pendingOrWaiting} project.is:${project} _unique id`)
const nextTaskIds = taskLines(`${pendingOrWaiting} project.is:${project} +next _unique id`)
// Non-leaf projects should have no tasks
if (!isLeafProject) {
if (taskIds.length > 0) {
return async () => {
console.info(chalk`{red Non-leaf project "${project}" must have no tasks}`)
console.info(`Child projects: ${subprojects.join(', ')}`)
printTasks(taskIds)
await pause(prompt)
}
}
}
// For leaf projects, there should be exactly one next task.
// That means that there isn't more than one...
if (nextTaskIds.length > 1) {
return async () => {
await setNextTask(
prompt,
nextTaskIds,
chalk`Leaf project "${project}" must have no more than one +next task, has multiple. Choose one.`
)
}
}
// And also that there is at least one
if (nextTaskIds.length === 0) {
return async () => {
await setNextTask(
prompt,
taskIds,
`Leaf project "${project}" must have a +next task, has none. Choose one.`
)
}
}
// All is well
return null
}
const nextReview = async (prompt, stepHeader) => {
let done = false
while (!done) {
done = true
const projects = taskLines(`${pendingOrWaiting} _unique project`)
for (const project of projects) {
const solveProblem = nextReviewSingleProject(prompt, project, projects)
if (solveProblem !== null) {
done = false
await solveProblem()
break
}
}
}
}
const reviewSomeday = async (prompt) => {
try {
task('list +someday')
await pause(prompt)
} catch (err) {
console.info(chalk`{gray Skipping, no +someday tasks}`)
}
}
const processIn = async (prompt) => {
while (true) {
const inTasks = loadTasks('status:pending +in')
if (inTasks.length > 0) {
printTasks(inTasks.map(t => t.id))
await prompt({
name: '+in',
type: 'input',
prefix: chalk`{red !}`,
message: 'Press Return after you have processed the above +in items'
})
} else {
return
}
}
}
// Weekly review steps
const stepDefinitions = {
'Mini mind sweep': {
description: 'Take a minute to mentally review if you have any stuff you have not yet captured. Create +in items about them (in another terminal).',
run: pause
},
'Project review': {
description: ` * Record any tasks not in the system for each project
* Mark any done tasks as such`,
run: projectReview
},
'+next review': {
description: 'Make sure all projects have exactly one +next item',
run: nextReview
},
'Process e-mail': {
description: `Ensure all e-mails in your inbox are handled. This can mean
* archiving them and adding an entry (+in) to Taskwarrior on another terminal
* organizing them into your e-mail based system - but make sure reminders get an item in Taskwarrior`,
run: pause
},
'Check last two weeks and next two weeks in calendars': {
description: 'This can often trigger ideas for new +in items',
run: pause
},
'Review +someday list': {
description: 'Is there anything else that should be on here? Is it time to activate one of these projects?',
run: reviewSomeday
},
'Process +in': {
description: 'Now is the time to turn all +in items into projects or actionable tasks',
run: processIn
}
}
// Preferred order of steps
const stepOrder = [
'Mini mind sweep',
'Project review',
'+next review',
'Process e-mail',
'Check last two weeks and next two weeks in calendars',
'Review +someday list',
'Process +in'
]
// Do the thing!
async function main () {
const prompt = inquirer.createPromptModule()
const steps = stepOrder.map(name => Object.assign({name}, stepDefinitions[name]))
for (const [index, step] of steps.entries()) {
const number = index + 1
const header = chalk`{gray [${number}/${steps.length}]} {bold.white.underline ${step.name}}`
console.log(boxen(header + '\n' + step.description, {borderColor: 'magenta', borderStyle: 'round', padding: 1}))
await step.run(prompt, header)
console.log(chalk`${header} {green done}`)
}
task('sync')
}
main().catch(console.error)