tasuku 2.3.0 → 3.0.0-beta.1
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.
- package/README.md +733 -260
- package/dist/create.d.mts +11 -0
- package/dist/create.mjs +1 -0
- package/dist/default-BEjMJA3x.mjs +1 -0
- package/dist/default-CFIL_U8K.d.mts +5 -0
- package/dist/index-DliZudNt.mjs +2 -0
- package/dist/index.d.mts +6 -157
- package/dist/index.mjs +1 -22
- package/dist/inline-SSRUlOZv.mjs +3 -0
- package/dist/inline.d.mts +9 -0
- package/dist/inline.mjs +1 -0
- package/dist/patch-console-PhLhxtc5.mjs +12 -0
- package/dist/pinned-DCoOgkO1.mjs +6 -0
- package/dist/themes/blink.d.mts +7 -0
- package/dist/themes/blink.mjs +1 -0
- package/dist/themes/claude.d.mts +7 -0
- package/dist/themes/claude.mjs +1 -0
- package/dist/themes/codex.d.mts +7 -0
- package/dist/themes/codex.mjs +1 -0
- package/dist/{index.d.cts → types-BC9OrTGg.d.mts} +60 -67
- package/package.json +51 -9
- package/dist/index.cjs +0 -22
package/README.md
CHANGED
|
@@ -1,239 +1,226 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<p align="center" demo>
|
|
2
|
+
<img src=".github/media/script.gif" width="500" alt="Terminal showing three build-pipeline tasks completing sequentially with title updates">
|
|
3
|
+
|
|
4
|
+
<!-- @vhs
|
|
5
|
+
Set Height 440
|
|
6
|
+
Hide
|
|
7
|
+
Type "node {file}"
|
|
8
|
+
Enter
|
|
9
|
+
Show
|
|
10
|
+
Sleep 5s
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
```js
|
|
15
|
+
import { setTimeout } from 'node:timers/promises'
|
|
16
|
+
import task from 'tasuku'
|
|
17
|
+
|
|
18
|
+
await task.group(task => [
|
|
19
|
+
task('Resolving dependencies', async ({ setTitle }) => {
|
|
20
|
+
await setTimeout(1000)
|
|
21
|
+
setTitle('Resolved 148 dependencies')
|
|
22
|
+
}),
|
|
23
|
+
task('Running tests', async ({ setTitle }) => {
|
|
24
|
+
await setTimeout(1500)
|
|
25
|
+
setTitle('42 tests passed')
|
|
26
|
+
}),
|
|
27
|
+
task('Building project', async ({ setTitle }) => {
|
|
28
|
+
await setTimeout(1200)
|
|
29
|
+
setTitle('Built in 1.2s')
|
|
30
|
+
}),
|
|
31
|
+
])
|
|
32
|
+
```
|
|
33
|
+
-->
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<h1 align="center">タスク</h1>
|
|
37
|
+
<p align="center">
|
|
38
|
+
<i>The minimalist's task runner for Node.js</i>
|
|
5
39
|
</p>
|
|
6
40
|
|
|
41
|
+
|
|
7
42
|
### Features
|
|
8
43
|
- Task list with dynamic states
|
|
9
44
|
- Parallel & nestable tasks
|
|
10
|
-
-
|
|
45
|
+
- Customizable themes (icons, colors, spinners)
|
|
46
|
+
- Two renderers: [pinned](#pinned-default) (animated) and [inline](#inline) (sequential)
|
|
47
|
+
- Zero runtime dependencies
|
|
48
|
+
- Renders to stderr — stdout stays clean for program output
|
|
11
49
|
- Type-safe
|
|
12
50
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<sub>Found this package useful? Show your support & appreciation by [sponsoring](https://github.com/sponsors/privatenumber)! ❤️</sub>
|
|
51
|
+
> [!TIP]
|
|
52
|
+
> [Try it out online](https://stackblitz.com/edit/tasuku-demo?file=index.js&devtoolsheight=50&view=editor)
|
|
16
53
|
|
|
17
54
|
## Install
|
|
18
55
|
```sh
|
|
19
56
|
npm i tasuku
|
|
20
57
|
```
|
|
21
58
|
|
|
22
|
-
##
|
|
23
|
-
タスク (Tasuku) is a minimal task runner for Node.js. You can use it to label any task/function so that its loading, success, and error states are rendered in the terminal.
|
|
59
|
+
## Quick start
|
|
24
60
|
|
|
25
|
-
|
|
61
|
+
タスク (Tasuku) is a minimal task runner for Node.js. Call `task()` from anywhere to display loading, success, and error states in the terminal:
|
|
26
62
|
|
|
27
63
|
```ts
|
|
28
|
-
import { copyFile } from 'node:fs/promises'
|
|
29
64
|
import task from 'tasuku'
|
|
30
65
|
|
|
31
|
-
task('Copying
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
setTitle('Successfully copied file from path A to B!')
|
|
66
|
+
await task('Copying files', async () => {
|
|
67
|
+
await copyFiles(source, destination)
|
|
35
68
|
})
|
|
36
69
|
```
|
|
37
70
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<img src=".github/media/basic.gif">
|
|
71
|
+
Tasks can be grouped, nested, run in parallel, and cleared — all with a simple functional API. Read on for the full usage guide.
|
|
41
72
|
|
|
42
73
|
## Usage
|
|
43
|
-
### Task list
|
|
44
|
-
Call `task(taskTitle, taskFunction)` to start a task and display it in a task list in the terminal.
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
import task from 'tasuku'
|
|
75
|
+
### Nesting
|
|
48
76
|
|
|
49
|
-
task(
|
|
50
|
-
await someAsyncTask()
|
|
51
|
-
})
|
|
77
|
+
Tasks can be nested indefinitely. Any `task()` call inside a task function automatically becomes a child task via async context tracking.
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
<p align="center" demo>
|
|
80
|
+
<img src=".github/media/nested.gif" width="600" alt="Terminal showing three levels of nested tasks">
|
|
81
|
+
<details>
|
|
82
|
+
<summary>View code</summary>
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
<!-- @vhs
|
|
85
|
+
Set Height 440
|
|
86
|
+
Hide
|
|
87
|
+
Type "node {file}"
|
|
88
|
+
Enter
|
|
89
|
+
Show
|
|
90
|
+
Sleep 3s
|
|
91
|
+
-->
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
```js
|
|
94
|
+
import { setTimeout } from 'node:timers/promises'
|
|
95
|
+
import task from 'tasuku'
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- **🔅 Loading** The task is running
|
|
67
|
-
- **⚠️ Warning** The task completed with a warning
|
|
68
|
-
- **❌ Error** The task exited with an error
|
|
69
|
-
- **✅ Success** The task completed without error
|
|
97
|
+
await task('Deploy', async () => {
|
|
98
|
+
await setTimeout(1000)
|
|
70
99
|
|
|
71
|
-
|
|
100
|
+
await task('Run migrations', async () => {
|
|
101
|
+
await setTimeout(1000)
|
|
72
102
|
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
await task('Seed database', async () => {
|
|
104
|
+
await setTimeout(1000)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
```
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
</details>
|
|
111
|
+
</p>
|
|
77
112
|
|
|
78
|
-
|
|
113
|
+
Since nesting is based on async context, task functions are composable across modules:
|
|
79
114
|
```ts
|
|
115
|
+
// db.ts
|
|
80
116
|
import task from 'tasuku'
|
|
81
117
|
|
|
82
|
-
task('
|
|
83
|
-
await
|
|
118
|
+
export const migrate = (directory: string) => task('Running migrations', async () => {
|
|
119
|
+
await runMigrations(directory)
|
|
84
120
|
})
|
|
85
121
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
someOtherCode()
|
|
89
|
-
|
|
90
|
-
// ...
|
|
91
|
-
|
|
92
|
-
task('Task 2', async ({ setTitle }) => {
|
|
93
|
-
await someAsyncTask()
|
|
94
|
-
|
|
95
|
-
setTitle('Task 2 complete')
|
|
122
|
+
export const seed = (count: number) => task('Seeding data', async () => {
|
|
123
|
+
await seedDatabase(count)
|
|
96
124
|
})
|
|
97
125
|
```
|
|
98
|
-
|
|
99
|
-
<img src=".github/media/set-title.gif">
|
|
100
|
-
|
|
101
|
-
### Task return values
|
|
102
|
-
The return value of a task will be stored in the output `.result` property.
|
|
103
|
-
|
|
104
|
-
If using TypeScript, the type of `.result` will be inferred from the task function.
|
|
105
|
-
|
|
106
126
|
```ts
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// deploy.ts
|
|
128
|
+
import { migrate, seed } from './db.js'
|
|
129
|
+
import task from 'tasuku'
|
|
109
130
|
|
|
110
|
-
|
|
131
|
+
await task('Deploy', async () => {
|
|
132
|
+
await migrate('./migrations') // automatically nested under "Deploy"
|
|
133
|
+
await seed(1000)
|
|
111
134
|
})
|
|
112
|
-
|
|
113
|
-
console.log(myTask.result) // 'Success'
|
|
114
135
|
```
|
|
115
136
|
|
|
116
|
-
###
|
|
117
|
-
Tasks can be nested indefinitely. Nested tasks will be stacked hierarchically in the task list.
|
|
118
|
-
```ts
|
|
119
|
-
await task('Do task', async ({ task }) => {
|
|
120
|
-
await someAsyncTask()
|
|
137
|
+
### Collapsing
|
|
121
138
|
|
|
122
|
-
|
|
123
|
-
await someAsyncTask()
|
|
139
|
+
Call `.clear()` on the task promise to collapse the nested task. `.clear()` returns the promise, so you can chain it.
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
130
|
-
```
|
|
141
|
+
<p align="center" demo>
|
|
142
|
+
<img src=".github/media/collapse.gif" width="600" alt="Terminal showing a nested task that collapses after completion">
|
|
143
|
+
<details>
|
|
144
|
+
<summary>View code</summary>
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
<!-- @vhs
|
|
147
|
+
Set Height 300
|
|
148
|
+
Hide
|
|
149
|
+
Type "node {file}"
|
|
150
|
+
Enter
|
|
151
|
+
Show
|
|
152
|
+
Sleep 3s
|
|
153
|
+
-->
|
|
133
154
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await task('Do task', async ({ task }) => {
|
|
138
|
-
await someAsyncTask()
|
|
155
|
+
```js
|
|
156
|
+
import { setTimeout } from 'node:timers/promises'
|
|
157
|
+
import task from 'tasuku'
|
|
139
158
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
})
|
|
159
|
+
await task('Deploy', async () => {
|
|
160
|
+
await setTimeout(500)
|
|
143
161
|
|
|
144
|
-
|
|
162
|
+
// .clear() collapses the nested task on completion
|
|
163
|
+
await task('Run migrations', async () => {
|
|
164
|
+
await setTimeout(500)
|
|
165
|
+
}).clear()
|
|
145
166
|
})
|
|
146
167
|
```
|
|
147
168
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
### Grouped tasks
|
|
151
|
-
Tasks can be grouped with `task.group()`. Pass in a function that returns an array of tasks to run them sequentially.
|
|
169
|
+
</details>
|
|
170
|
+
</p>
|
|
152
171
|
|
|
153
|
-
|
|
172
|
+
### task.group
|
|
154
173
|
|
|
155
|
-
|
|
156
|
-
const groupedTasks = await task.group(task => [
|
|
157
|
-
task('Task 1', async () => {
|
|
158
|
-
await someAsyncTask()
|
|
174
|
+
Group tasks with `task.group()` to display a queue and control execution. Pass a function that returns an array of tasks. Set `concurrency` to run tasks in parallel — queued tasks show as pending until their turn.
|
|
159
175
|
|
|
160
|
-
|
|
161
|
-
|
|
176
|
+
<p align="center" demo>
|
|
177
|
+
<img src=".github/media/grouped-parallel.gif" width="600" alt="Terminal showing four tasks running with concurrency 2, two at a time">
|
|
178
|
+
<details>
|
|
179
|
+
<summary>View code</summary>
|
|
162
180
|
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
<!-- @vhs
|
|
182
|
+
Set Height 400
|
|
183
|
+
Hide
|
|
184
|
+
Type "node {file}"
|
|
185
|
+
Enter
|
|
186
|
+
Show
|
|
187
|
+
Sleep 5s
|
|
188
|
+
-->
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
```js
|
|
191
|
+
import { setTimeout } from 'node:timers/promises'
|
|
192
|
+
import task from 'tasuku'
|
|
169
193
|
|
|
170
|
-
|
|
194
|
+
await task.group(task => [
|
|
195
|
+
task('Lint', async () => {
|
|
196
|
+
await setTimeout(1000)
|
|
197
|
+
}),
|
|
198
|
+
task('Type check', async () => {
|
|
199
|
+
await setTimeout(1500)
|
|
200
|
+
}),
|
|
201
|
+
task('Unit tests', async () => {
|
|
202
|
+
await setTimeout(2000)
|
|
203
|
+
}),
|
|
204
|
+
task('Build', async () => {
|
|
205
|
+
await setTimeout(2500)
|
|
171
206
|
})
|
|
172
|
-
|
|
173
|
-
// ...
|
|
174
|
-
])
|
|
175
|
-
|
|
176
|
-
console.log(groupedTasks) // [{ result: 'one' }, { result: 'two' }]
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
<img src=".github/media/grouped.gif">
|
|
180
|
-
|
|
181
|
-
### Running tasks in parallel
|
|
182
|
-
You can run tasks in parallel by passing in `{ concurrency: n }` as the second argument in `task.group()`.
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
const api = await task.group(task => [
|
|
186
|
-
task(
|
|
187
|
-
'Task 1',
|
|
188
|
-
async () => await someAsyncTask()
|
|
189
|
-
),
|
|
190
|
-
|
|
191
|
-
task(
|
|
192
|
-
'Task 2',
|
|
193
|
-
async () => await someAsyncTask()
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
// ...
|
|
197
|
-
], {
|
|
198
|
-
concurrency: 2 // Number of tasks to run at a time
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
api.clear() // Clear output
|
|
207
|
+
], { concurrency: 2 })
|
|
202
208
|
```
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Alternatively, you can also use the native `Promise.all()` if you prefer. The advantage of using `task.group()` is that you can limit concurrency, displays queued tasks as pending, and it returns an API to easily clear the results.
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
// No API
|
|
210
|
-
await Promise.all([
|
|
211
|
-
task(
|
|
212
|
-
'Task 1',
|
|
213
|
-
async () => await someAsyncTask()
|
|
214
|
-
),
|
|
215
|
-
|
|
216
|
-
task(
|
|
217
|
-
'Task 2',
|
|
218
|
-
async () => await someAsyncTask()
|
|
219
|
-
)
|
|
210
|
+
</details>
|
|
211
|
+
</p>
|
|
220
212
|
|
|
221
|
-
|
|
222
|
-
])
|
|
223
|
-
```
|
|
213
|
+
Alternatively, use `Promise.all()` if you prefer. The advantage of `task.group()` is concurrency control, pending state for queued tasks, and `.clear()` on the group promise.
|
|
224
214
|
|
|
225
215
|
## API
|
|
226
216
|
|
|
227
|
-
### task(
|
|
217
|
+
### task(title, taskFunction, options?)
|
|
228
218
|
|
|
229
|
-
Returns a Promise that resolves with
|
|
219
|
+
Returns a `TaskPromise<T>` — a Promise that resolves to `T` (the task function's return value) with additional properties:
|
|
230
220
|
```ts
|
|
231
|
-
type
|
|
232
|
-
// Result from taskFunction
|
|
233
|
-
result: unknown
|
|
234
|
-
|
|
221
|
+
type TaskPromise<T> = Promise<T> & {
|
|
235
222
|
// State of the task
|
|
236
|
-
state: 'error' | 'warning' | 'success'
|
|
223
|
+
state: 'loading' | 'error' | 'warning' | 'success'
|
|
237
224
|
|
|
238
225
|
// Warning message if state is 'warning', otherwise undefined
|
|
239
226
|
warning: string | undefined
|
|
@@ -241,88 +228,195 @@ type TaskAPI = {
|
|
|
241
228
|
// Error message if state is 'error', otherwise undefined
|
|
242
229
|
error: string | undefined
|
|
243
230
|
|
|
244
|
-
//
|
|
245
|
-
|
|
231
|
+
// Clear the task from the terminal. Returns the promise for chaining.
|
|
232
|
+
// If the task is still running, clears automatically on completion.
|
|
233
|
+
clear: () => TaskPromise<T>
|
|
246
234
|
}
|
|
247
235
|
```
|
|
248
236
|
|
|
249
|
-
|
|
250
|
-
|
|
237
|
+
The return value is the resolved value of the task function. If using TypeScript, the type is inferred:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const result = await task('Fetch data', async () => {
|
|
241
|
+
const response = await fetch(apiUrl)
|
|
242
|
+
return response.json()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
console.log(result) // typed as the return value
|
|
246
|
+
```
|
|
251
247
|
|
|
252
|
-
|
|
248
|
+
### Task inner API
|
|
253
249
|
|
|
254
|
-
The
|
|
250
|
+
The task function receives an API object for controlling the task display:
|
|
255
251
|
|
|
256
|
-
#### taskFunction
|
|
257
|
-
Type:
|
|
258
252
|
```ts
|
|
259
|
-
type TaskFunction = (
|
|
260
|
-
|
|
253
|
+
type TaskFunction = (api: {
|
|
254
|
+
signal: AbortSignal
|
|
261
255
|
setTitle(title: string): void
|
|
262
256
|
setStatus(status?: string): void
|
|
263
257
|
setOutput(output: string | { message: string }): void
|
|
264
258
|
setWarning(warning?: Error | string | false | null): void
|
|
265
259
|
setError(error?: Error | string | false | null): void
|
|
266
|
-
streamPreview: Writable
|
|
260
|
+
streamPreview: Writable & { clear(): void }
|
|
267
261
|
startTime(): void
|
|
268
262
|
stopTime(): number
|
|
269
263
|
}) => Promise<unknown>
|
|
270
264
|
```
|
|
271
265
|
|
|
272
|
-
|
|
266
|
+
#### signal
|
|
267
|
+
|
|
268
|
+
An `AbortSignal` that the task can use to respond to cancellation. The signal is cooperative — it only cancels work if you pass it to an API that respects it (like `fetch()`, streams, or `setTimeout` from `timers/promises`). Tasks that don't use the signal will continue running normally.
|
|
269
|
+
|
|
270
|
+
The signal is aborted automatically when:
|
|
271
|
+
- **In `task.group()`**: a sibling task fails (when `stopOnError` is `true`, the default)
|
|
272
|
+
- **In nested tasks**: the parent task throws an error
|
|
273
|
+
|
|
274
|
+
The error that caused the abort is available on `signal.reason`.
|
|
275
|
+
|
|
276
|
+
Many APIs like `fetch()` accept a signal and cancel automatically. For multi-step work, use `signal.throwIfAborted()` between steps to bail out early:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
await task('Deploy', async ({ signal }) => {
|
|
280
|
+
const artifact = await build({ signal })
|
|
281
|
+
|
|
282
|
+
signal.throwIfAborted() // stop here if aborted during build
|
|
283
|
+
|
|
284
|
+
await upload(artifact, { signal })
|
|
285
|
+
|
|
286
|
+
signal.throwIfAborted() // stop here if aborted during upload
|
|
287
|
+
|
|
288
|
+
await notifySlack('Deployed!')
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
##### Aborting by throwing
|
|
293
|
+
|
|
294
|
+
Throwing an error from a task aborts the signal for all child and sibling tasks:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// Nested tasks: parent throw aborts children
|
|
298
|
+
await task('Deploy', async () => {
|
|
299
|
+
task('Upload assets', async ({ signal }) => {
|
|
300
|
+
await upload(files, { signal })
|
|
301
|
+
}).catch(() => {})
|
|
302
|
+
|
|
303
|
+
throw new Error('deploy failed')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Group tasks: sibling throw aborts siblings
|
|
307
|
+
await task.group(task => [
|
|
308
|
+
task('Upload A', async ({ signal }) => {
|
|
309
|
+
// signal.aborted becomes true when B fails, but only
|
|
310
|
+
// cancels work if you pass it to an API that respects it
|
|
311
|
+
await upload(fileA, { signal })
|
|
312
|
+
}),
|
|
313
|
+
task('Upload B', async () => {
|
|
314
|
+
throw new Error('network error')
|
|
315
|
+
})
|
|
316
|
+
], { concurrency: 2 })
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
##### Aborting with an external signal
|
|
273
320
|
|
|
274
|
-
|
|
321
|
+
Pass an `AbortController` signal via `options.signal` to cancel from outside:
|
|
275
322
|
|
|
323
|
+
```ts
|
|
324
|
+
// Nested tasks — abort after a timeout
|
|
325
|
+
await task('Deploy', async () => {
|
|
326
|
+
await task('Long upload', async ({ signal }) => {
|
|
327
|
+
await upload(files, { signal })
|
|
328
|
+
}, { signal: AbortSignal.timeout(5000) })
|
|
329
|
+
})
|
|
276
330
|
|
|
277
|
-
|
|
278
|
-
|
|
331
|
+
// Group tasks — abort after a timeout
|
|
332
|
+
await task.group(task => [
|
|
333
|
+
task('Upload A', async ({ signal }) => {
|
|
334
|
+
await upload(fileA, { signal })
|
|
335
|
+
}),
|
|
336
|
+
task('Upload B', async ({ signal }) => {
|
|
337
|
+
await upload(fileB, { signal })
|
|
338
|
+
})
|
|
339
|
+
], { signal: AbortSignal.timeout(5000) })
|
|
340
|
+
```
|
|
279
341
|
|
|
280
342
|
#### setTitle()
|
|
281
|
-
|
|
343
|
+
|
|
344
|
+
Change the task title.
|
|
282
345
|
|
|
283
346
|
#### setStatus()
|
|
284
|
-
|
|
347
|
+
|
|
348
|
+
Set dimmed metadata after the title.
|
|
285
349
|
|
|
286
350
|
#### setOutput()
|
|
287
|
-
Call with a string to set the output of the task.
|
|
288
351
|
|
|
289
|
-
|
|
352
|
+
Set static output below the task.
|
|
290
353
|
|
|
291
354
|
#### streamPreview
|
|
292
|
-
A `Writable` stream for displaying live output below the task. Pipe a child process or any readable stream into it to show a scrolling preview of the output.
|
|
293
355
|
|
|
294
|
-
|
|
356
|
+
A `Writable` stream for displaying live output below the task. Pipe a child process or any readable stream into it to show a scrolling preview.
|
|
295
357
|
|
|
296
|
-
|
|
358
|
+
Handles both `\n` (newline) and `\r` (carriage return) — programs like `wget` that use `\r` for in-place progress bars work out of the box.
|
|
359
|
+
|
|
360
|
+
<p align="center" demo>
|
|
361
|
+
<img src=".github/media/stream-preview.gif" width="600" alt="Terminal showing a task with a wget progress bar streamed below it">
|
|
362
|
+
<details>
|
|
363
|
+
<summary>View code</summary>
|
|
364
|
+
|
|
365
|
+
<!-- @vhs
|
|
366
|
+
Set Width 1790
|
|
367
|
+
Set Height 300
|
|
368
|
+
Hide
|
|
369
|
+
Type "node {file}"
|
|
370
|
+
Enter
|
|
371
|
+
Show
|
|
372
|
+
Sleep 5s
|
|
373
|
+
-->
|
|
374
|
+
|
|
375
|
+
```js
|
|
297
376
|
import { spawn } from 'node:child_process'
|
|
298
377
|
import { pipeline } from 'node:stream/promises'
|
|
378
|
+
import task from 'tasuku'
|
|
299
379
|
|
|
300
|
-
await task('Download', async ({ streamPreview }) => {
|
|
301
|
-
const child = spawn('
|
|
380
|
+
await task('Download TypeScript', async ({ setTitle, streamPreview }) => {
|
|
381
|
+
const child = spawn('wget', [
|
|
382
|
+
'-q',
|
|
383
|
+
'--show-progress',
|
|
384
|
+
'--progress=bar:force',
|
|
385
|
+
'--limit-rate=2M',
|
|
386
|
+
'-O',
|
|
387
|
+
'/dev/null',
|
|
388
|
+
'https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz'
|
|
389
|
+
])
|
|
302
390
|
await pipeline(child.stderr, streamPreview)
|
|
391
|
+
setTitle('Downloaded TypeScript')
|
|
303
392
|
})
|
|
304
393
|
```
|
|
305
394
|
|
|
306
|
-
|
|
395
|
+
</details>
|
|
396
|
+
</p>
|
|
307
397
|
|
|
308
398
|
By default, shows the last 5 lines. Use the `previewLines` option to change this. When there are more lines than the limit, a `(+ N lines)` indicator is shown.
|
|
309
399
|
|
|
400
|
+
Call `streamPreview.clear()` to remove the preview output. Useful for cleaning up verbose output after a task succeeds.
|
|
401
|
+
|
|
310
402
|
> [!NOTE]
|
|
311
403
|
> `setOutput()` and `streamPreview` render independently. If both are used, static output appears above the stream preview.
|
|
312
404
|
|
|
313
405
|
#### setWarning()
|
|
314
|
-
|
|
406
|
+
|
|
407
|
+
Call with a string or Error to put the task in a warning state. Call with no argument (or a falsy value) to revert to loading state.
|
|
315
408
|
|
|
316
409
|
#### setError()
|
|
317
|
-
Call with a string or Error instance to put the task in an error state. Call with no argument (or a falsy value) to revert to loading state. Tasks automatically go into an error state when it catches an error in the task.
|
|
318
410
|
|
|
319
|
-
|
|
411
|
+
Call with a string or Error to put the task in an error state. Call with no argument (or a falsy value) to revert to loading state. Tasks automatically enter error state when an uncaught error is thrown.
|
|
320
412
|
|
|
321
413
|
#### startTime()
|
|
414
|
+
|
|
322
415
|
Start or restart the elapsed time counter. Calling again resets to 0. Time is displayed after the status: `⠋ Task [status] (3s)`
|
|
323
416
|
|
|
324
417
|
#### stopTime()
|
|
325
|
-
|
|
418
|
+
|
|
419
|
+
Stop the elapsed time counter and return the elapsed milliseconds. The displayed time freezes at the stopped value.
|
|
326
420
|
|
|
327
421
|
```ts
|
|
328
422
|
await task('Multi-phase', async ({ startTime, stopTime, setStatus }) => {
|
|
@@ -339,130 +433,509 @@ await task('Multi-phase', async ({ startTime, stopTime, setStatus }) => {
|
|
|
339
433
|
})
|
|
340
434
|
```
|
|
341
435
|
|
|
436
|
+
Time format: `(Xs)` under a minute, `(Xm Ys)` under an hour, `(Xh Ym)` for longer. Not shown if elapsed < 1 second.
|
|
437
|
+
|
|
342
438
|
#### options
|
|
343
|
-
Type: `{ showTime?: boolean, previewLines?: number }`
|
|
344
439
|
|
|
345
|
-
|
|
440
|
+
##### showTime
|
|
441
|
+
|
|
442
|
+
Type: `boolean`
|
|
443
|
+
|
|
444
|
+
Automatically start the elapsed time counter when the task begins. Equivalent to calling `startTime()` at the start of the task function.
|
|
346
445
|
|
|
347
446
|
##### previewLines
|
|
447
|
+
|
|
348
448
|
Type: `number`
|
|
349
449
|
|
|
350
450
|
Default: `5`
|
|
351
451
|
|
|
352
|
-
Maximum
|
|
353
|
-
|
|
354
|
-
##### showTime
|
|
355
|
-
When `true`, automatically starts the elapsed time counter when the task begins. Equivalent to calling `startTime()` at the start of the task function.
|
|
452
|
+
Maximum lines to display in `streamPreview` output (minimum 1). When the stream produces more lines, older lines scroll off and a `(+ N lines)` indicator shows the total.
|
|
356
453
|
|
|
357
|
-
|
|
358
|
-
await task('Building', async () => {
|
|
359
|
-
await build()
|
|
360
|
-
}, { showTime: true })
|
|
361
|
-
// Output: ✔ Building (3s)
|
|
362
|
-
```
|
|
454
|
+
##### signal
|
|
363
455
|
|
|
364
|
-
|
|
456
|
+
Type: `AbortSignal`
|
|
365
457
|
|
|
366
|
-
|
|
367
|
-
- Format: `(Xs)` for under a minute, `(Xm Ys)` for under an hour, `(Xh Ym)` for longer
|
|
368
|
-
- Not shown if elapsed < 1 second
|
|
369
|
-
- Freezes at final value when task completes
|
|
458
|
+
An external abort signal to cancel the task. The signal is exposed via the task inner API's `signal` property. When used in a group, the effective signal is a combination of both the external and group-internal signals.
|
|
370
459
|
|
|
371
|
-
|
|
372
|
-
### task.group(createTaskFunctions, options)
|
|
373
|
-
Returns a Promise that resolves with object:
|
|
374
460
|
```ts
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// State of the task
|
|
381
|
-
state: 'error' | 'warning' | 'success'
|
|
461
|
+
task('Upload', async ({ signal }) => {
|
|
462
|
+
await fetch(url, { signal })
|
|
463
|
+
}, { signal: AbortSignal.timeout(5000) })
|
|
464
|
+
```
|
|
382
465
|
|
|
383
|
-
|
|
384
|
-
clear: () => void
|
|
385
|
-
}[] & {
|
|
466
|
+
### task.group(createTasks, options?)
|
|
386
467
|
|
|
387
|
-
|
|
388
|
-
|
|
468
|
+
Returns a `TaskGroupPromise` — a Promise that resolves to an array of return values with a `.clear()` method:
|
|
469
|
+
```ts
|
|
470
|
+
type TaskGroupPromise<Results> = Promise<Results> & {
|
|
471
|
+
// Clear ALL task results from the terminal. Returns the promise for chaining.
|
|
472
|
+
clear: () => TaskGroupPromise<Results>
|
|
389
473
|
}
|
|
390
474
|
```
|
|
391
475
|
|
|
392
|
-
####
|
|
393
|
-
Type: `(task) => Task[]`
|
|
476
|
+
#### createTasks
|
|
394
477
|
|
|
395
|
-
|
|
478
|
+
Type: `(task) => Task[]`
|
|
396
479
|
|
|
397
|
-
A function that returns all the tasks
|
|
480
|
+
A function that returns all the tasks to group in an array.
|
|
398
481
|
|
|
399
482
|
#### options
|
|
400
483
|
|
|
401
|
-
Directly passed into [`p-map`](https://github.com/sindresorhus/p-map).
|
|
402
|
-
|
|
403
484
|
##### concurrency
|
|
404
|
-
|
|
485
|
+
|
|
486
|
+
Type: `number`
|
|
405
487
|
|
|
406
488
|
Default: `1`
|
|
407
489
|
|
|
408
490
|
Number of tasks to run at a time.
|
|
409
491
|
|
|
410
492
|
##### stopOnError
|
|
493
|
+
|
|
411
494
|
Type: `boolean`
|
|
412
495
|
|
|
413
496
|
Default: `true`
|
|
414
497
|
|
|
415
|
-
When
|
|
498
|
+
When `false`, instead of stopping when a task fails, waits for all tasks to finish and rejects with an aggregated error.
|
|
416
499
|
|
|
417
|
-
#####
|
|
500
|
+
##### signal
|
|
501
|
+
|
|
502
|
+
Type: `AbortSignal`
|
|
418
503
|
|
|
419
|
-
|
|
504
|
+
Abort signal to cancel pending tasks.
|
|
505
|
+
|
|
506
|
+
In addition to this external signal, `task.group()` creates an internal signal that auto-aborts all running tasks when one fails (when `stopOnError` is `true`, the default). This signal is passed to each task's inner API as `signal`, so task functions can react to sibling failures:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
await task.group(task => [
|
|
510
|
+
task('Upload A', async ({ signal }) => {
|
|
511
|
+
await upload(fileA, { signal }) // aborted when B fails
|
|
512
|
+
}),
|
|
513
|
+
task('Upload B', async () => {
|
|
514
|
+
throw new Error('network error') // triggers abort of A
|
|
515
|
+
})
|
|
516
|
+
], { concurrency: 2 })
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
##### maxVisible
|
|
420
520
|
|
|
421
521
|
Type: `number | ((terminalHeight: number) => number)`
|
|
422
522
|
|
|
423
523
|
Default: Responsive to terminal height (rows - 2, minimum 5)
|
|
424
524
|
|
|
425
|
-
Maximum number of lines to display in the task list. When there are more task lines than this limit, remaining tasks are hidden with a state breakdown (e.g., "(+ 3 loading, 5 queued, 4 completed)"). Active tasks are always prioritized over pending and completed ones.
|
|
525
|
+
Maximum number of lines to display in the task list. When there are more task lines than this limit, remaining tasks are hidden with a state breakdown (e.g., "(+ 3 loading, 5 queued, 4 completed)"). Active tasks are always prioritized over pending and completed ones.
|
|
426
526
|
|
|
427
|
-
Can be a fixed number or a function called on each render for responsive limits.
|
|
527
|
+
Can be a fixed number or a function called on each render for responsive limits. By default, the limit is automatically lifted when all tasks complete and `.clear()` is called.
|
|
428
528
|
|
|
429
|
-
|
|
529
|
+
<p align="center" demo>
|
|
530
|
+
<img src=".github/media/max-visible.gif" width="600" alt="Terminal showing a task group with maxVisible limiting displayed tasks">
|
|
531
|
+
<details>
|
|
532
|
+
<summary>View code</summary>
|
|
430
533
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
534
|
+
<!-- @vhs
|
|
535
|
+
Set Height 660
|
|
536
|
+
Hide
|
|
537
|
+
Type "node {file}"
|
|
538
|
+
Enter
|
|
539
|
+
Show
|
|
540
|
+
Sleep 8s
|
|
541
|
+
-->
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
import { setTimeout } from 'node:timers/promises'
|
|
545
|
+
import task from 'tasuku'
|
|
546
|
+
|
|
547
|
+
await task.group(
|
|
548
|
+
task => Array.from(
|
|
549
|
+
{ length: 10 },
|
|
550
|
+
(_, i) => task(
|
|
551
|
+
`Task ${i + 1}`,
|
|
552
|
+
() => setTimeout(500 + Math.random() * 1200)
|
|
553
|
+
)
|
|
554
|
+
),
|
|
555
|
+
{
|
|
556
|
+
concurrency: 2,
|
|
557
|
+
maxVisible: 8
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
</details>
|
|
563
|
+
</p>
|
|
564
|
+
|
|
565
|
+
### Task anatomy
|
|
566
|
+
|
|
567
|
+
<p align="center" demo>
|
|
568
|
+
<img src=".github/media/task-anatomy.gif" width="600" alt="Terminal showing task API methods being called: setStatus, setOutput, and setTitle">
|
|
569
|
+
<details>
|
|
570
|
+
<summary>View code</summary>
|
|
571
|
+
|
|
572
|
+
<!-- @vhs
|
|
573
|
+
Set Height 340
|
|
574
|
+
Hide
|
|
575
|
+
Type "node {file}"
|
|
576
|
+
Enter
|
|
577
|
+
Show
|
|
578
|
+
Sleep 7s
|
|
579
|
+
-->
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
import { setTimeout } from 'node:timers/promises'
|
|
583
|
+
import task from 'tasuku'
|
|
584
|
+
|
|
585
|
+
await task('my title', async ({ setTitle, setStatus, setOutput }) => {
|
|
586
|
+
await setTimeout(1000)
|
|
587
|
+
|
|
588
|
+
setStatus('my status')
|
|
589
|
+
await setTimeout(1500)
|
|
590
|
+
|
|
591
|
+
setOutput('my output')
|
|
592
|
+
await setTimeout(1500)
|
|
593
|
+
|
|
594
|
+
setTitle('updated title')
|
|
595
|
+
await setTimeout(1000)
|
|
596
|
+
})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
</details>
|
|
600
|
+
</p>
|
|
601
|
+
|
|
602
|
+
#### Task states
|
|
603
|
+
|
|
604
|
+
| State | Icon | Description |
|
|
605
|
+
| :--- | :---: | :--- |
|
|
606
|
+
| Pending | ◼ | Queued, not yet started |
|
|
607
|
+
| Loading | ⠋ | Running (animated spinner) |
|
|
608
|
+
| Success | ✔ | Completed without error |
|
|
609
|
+
| Warning | ⚠ | Completed with a warning |
|
|
610
|
+
| Error | ✖ | Exited with an error |
|
|
611
|
+
|
|
612
|
+
<p align="center" demo>
|
|
613
|
+
<img src=".github/media/task-states.gif" width="600" alt="Terminal showing all five task states: success, warning, error, loading, and pending">
|
|
614
|
+
<details>
|
|
615
|
+
<summary>View code</summary>
|
|
616
|
+
|
|
617
|
+
<!-- @vhs
|
|
618
|
+
Set Height 480
|
|
619
|
+
Set TypingSpeed 0
|
|
620
|
+
Hide
|
|
621
|
+
Type "node {file}"
|
|
622
|
+
Enter
|
|
623
|
+
Sleep 800ms
|
|
624
|
+
Show
|
|
625
|
+
Sleep 3s
|
|
626
|
+
-->
|
|
627
|
+
|
|
628
|
+
```js
|
|
629
|
+
import { setTimeout } from 'node:timers/promises'
|
|
630
|
+
import task from 'tasuku'
|
|
631
|
+
|
|
632
|
+
const tasks = task.group(task => [
|
|
633
|
+
task('Success task', async () => {
|
|
634
|
+
await setTimeout(100)
|
|
635
|
+
}),
|
|
636
|
+
|
|
637
|
+
task('Warning task', async ({ setWarning }) => {
|
|
638
|
+
await setTimeout(100)
|
|
639
|
+
setWarning('Something might be wrong')
|
|
640
|
+
}),
|
|
641
|
+
|
|
642
|
+
task('Error task', async ({ setError }) => {
|
|
643
|
+
await setTimeout(100)
|
|
644
|
+
setError(new Error('Something went wrong'))
|
|
645
|
+
}),
|
|
646
|
+
|
|
647
|
+
task('Loading task', async () => {
|
|
648
|
+
await setTimeout(5000)
|
|
649
|
+
}),
|
|
650
|
+
|
|
651
|
+
task('Pending task', async () => {
|
|
652
|
+
await setTimeout(100)
|
|
653
|
+
})
|
|
654
|
+
], {
|
|
655
|
+
concurrency: 1,
|
|
435
656
|
maxVisible: 10
|
|
436
657
|
})
|
|
658
|
+
await tasks
|
|
659
|
+
|
|
660
|
+
tasks.clear()
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
</details>
|
|
664
|
+
</p>
|
|
665
|
+
|
|
666
|
+
## Renderers
|
|
667
|
+
|
|
668
|
+
Tasuku ships two renderers that control how task output appears in the terminal. The default export uses `pinned`, but you can switch to `inline` via `tasuku/inline`.
|
|
669
|
+
|
|
670
|
+
### Pinned (default)
|
|
671
|
+
|
|
672
|
+
The pinned renderer keeps the task list fixed at the bottom of the terminal using cursor save/restore. Spinner animations update in-place, and `console.log` output is moved above the task area. This is the default behavior.
|
|
673
|
+
|
|
674
|
+
### Inline
|
|
675
|
+
|
|
676
|
+
The inline renderer writes output sequentially — each task result is appended as a new line, and `console.log` output appears exactly where it was called, interleaved with task results.
|
|
677
|
+
|
|
678
|
+
Use this when:
|
|
679
|
+
- You want `console.log` and task output in natural order ([#16](https://github.com/privatenumber/tasuku/issues/16))
|
|
680
|
+
- You're logging to a file or piping output
|
|
681
|
+
- You want minimal terminal manipulation
|
|
682
|
+
|
|
683
|
+
<p align="center" demo>
|
|
684
|
+
<img src=".github/media/inline.gif" width="600" alt="Terminal showing inline renderer with console.log interleaved between tasks">
|
|
685
|
+
<details>
|
|
686
|
+
<summary>View code</summary>
|
|
687
|
+
|
|
688
|
+
<!-- @vhs
|
|
689
|
+
Set Height 440
|
|
690
|
+
Hide
|
|
691
|
+
Type "node {file}"
|
|
692
|
+
Enter
|
|
693
|
+
Show
|
|
694
|
+
Sleep 7s
|
|
695
|
+
-->
|
|
696
|
+
|
|
697
|
+
```js
|
|
698
|
+
import { setTimeout } from 'node:timers/promises'
|
|
699
|
+
import task from 'tasuku/inline'
|
|
700
|
+
|
|
701
|
+
console.log('Starting build pipeline...')
|
|
702
|
+
|
|
703
|
+
await task('Resolving dependencies', async ({ setTitle }) => {
|
|
704
|
+
await setTimeout(1500)
|
|
705
|
+
setTitle('Resolved 148 dependencies')
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
console.log('Dependencies locked ✓')
|
|
709
|
+
|
|
710
|
+
await task('Running tests', async ({ setTitle }) => {
|
|
711
|
+
await setTimeout(2000)
|
|
712
|
+
setTitle('42 tests passed')
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
console.log('All checks passed — ready to deploy')
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
</details>
|
|
719
|
+
</p>
|
|
720
|
+
|
|
721
|
+
On TTY, the inline renderer tracks each task line by its offset from the cursor and updates it in-place using `CSI n A` (cursor up) and `CSI n B` (cursor down). On non-TTY (piped output, CI), only the final state is written — no spinner frames or cursor sequences.
|
|
722
|
+
|
|
723
|
+
> [!NOTE]
|
|
724
|
+
> The inline renderer does not support `maxVisible` since tasks are written to scrollback immediately.
|
|
725
|
+
|
|
726
|
+
> [!IMPORTANT]
|
|
727
|
+
> `CSI n A` clamps at row 1 of the visible viewport — it cannot enter the scrollback buffer. Tasks that scroll above the visible terminal window can no longer be updated in-place. This is a terminal limitation, not a software one. If you have more concurrent tasks than terminal rows, consider using the [pinned](#pinned-default) renderer instead.
|
|
728
|
+
|
|
729
|
+
## Themes
|
|
730
|
+
|
|
731
|
+
### Default
|
|
732
|
+
|
|
733
|
+
The built-in theme with braille spinner and standard terminal colors.
|
|
734
|
+
|
|
735
|
+
<p align="center" demo>
|
|
736
|
+
<img src=".github/media/theme-default.gif" width="600" alt="Terminal showing the default theme with braille spinner">
|
|
737
|
+
<details>
|
|
738
|
+
<summary>View code</summary>
|
|
739
|
+
|
|
740
|
+
<!-- @vhs
|
|
741
|
+
Set Height 260
|
|
742
|
+
Hide
|
|
743
|
+
Type "node {file}"
|
|
744
|
+
Enter
|
|
745
|
+
Show
|
|
746
|
+
Sleep 6s
|
|
747
|
+
-->
|
|
748
|
+
|
|
749
|
+
```js
|
|
750
|
+
import { setTimeout } from 'node:timers/promises'
|
|
751
|
+
import task from 'tasuku'
|
|
752
|
+
|
|
753
|
+
await task('Building project', async ({ setTitle }) => {
|
|
754
|
+
await setTimeout(3000)
|
|
755
|
+
setTitle('Build complete')
|
|
756
|
+
})
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
</details>
|
|
760
|
+
</p>
|
|
761
|
+
|
|
762
|
+
### Claude
|
|
763
|
+
|
|
764
|
+
Claude Code-inspired theme with truecolor palette, dingbat star spinner, and shimmer title animation. Import the theme from `tasuku/theme/claude`.
|
|
765
|
+
|
|
766
|
+
<p align="center" demo>
|
|
767
|
+
<img src=".github/media/theme-claude.gif" width="600" alt="Terminal showing the Claude theme with star spinner and shimmer animation">
|
|
768
|
+
<details>
|
|
769
|
+
<summary>View code</summary>
|
|
770
|
+
|
|
771
|
+
<!-- @vhs
|
|
772
|
+
Set Height 260
|
|
773
|
+
Hide
|
|
774
|
+
Type "node {file}"
|
|
775
|
+
Enter
|
|
776
|
+
Show
|
|
777
|
+
Sleep 6s
|
|
778
|
+
-->
|
|
779
|
+
|
|
780
|
+
```js
|
|
781
|
+
import { setTimeout } from 'node:timers/promises'
|
|
782
|
+
import task from 'tasuku/theme/claude'
|
|
783
|
+
|
|
784
|
+
await task('Building project', async ({ setTitle }) => {
|
|
785
|
+
await setTimeout(5000)
|
|
786
|
+
setTitle('Build complete')
|
|
787
|
+
})
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
</details>
|
|
791
|
+
</p>
|
|
792
|
+
|
|
793
|
+
### Blink
|
|
794
|
+
|
|
795
|
+
Reduced-motion theme inspired by Claude Code's accessibility mode. The `⏺` indicator pulses between bright and dim on a 2-second cycle. Import the theme from `tasuku/theme/blink`.
|
|
796
|
+
|
|
797
|
+
<p align="center" demo>
|
|
798
|
+
<img src=".github/media/theme-blink.gif" width="600" alt="Terminal showing the Blink reduced-motion theme with pulsing indicator">
|
|
799
|
+
<details>
|
|
800
|
+
<summary>View code</summary>
|
|
801
|
+
|
|
802
|
+
<!-- @vhs
|
|
803
|
+
Set Height 260
|
|
804
|
+
Hide
|
|
805
|
+
Type "node {file}"
|
|
806
|
+
Enter
|
|
807
|
+
Show
|
|
808
|
+
Sleep 6s
|
|
809
|
+
-->
|
|
810
|
+
|
|
811
|
+
```js
|
|
812
|
+
import { setTimeout } from 'node:timers/promises'
|
|
813
|
+
import task from 'tasuku/theme/blink'
|
|
814
|
+
|
|
815
|
+
await task('Building project', async ({ setTitle }) => {
|
|
816
|
+
await setTimeout(5000)
|
|
817
|
+
setTitle('Build complete')
|
|
818
|
+
})
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
</details>
|
|
822
|
+
</p>
|
|
823
|
+
|
|
824
|
+
### Codex
|
|
825
|
+
|
|
826
|
+
OpenAI Codex CLI-inspired theme with cosine-based shimmer gradient and monochrome palette. Import the theme from `tasuku/theme/codex`.
|
|
827
|
+
|
|
828
|
+
<p align="center" demo>
|
|
829
|
+
<img src=".github/media/theme-codex.gif" width="600" alt="Terminal showing the Codex theme with shimmer gradient">
|
|
830
|
+
<details>
|
|
831
|
+
<summary>View code</summary>
|
|
832
|
+
|
|
833
|
+
<!-- @vhs
|
|
834
|
+
Set Height 260
|
|
835
|
+
Hide
|
|
836
|
+
Type "node {file}"
|
|
837
|
+
Enter
|
|
838
|
+
Show
|
|
839
|
+
Sleep 6s
|
|
840
|
+
-->
|
|
841
|
+
|
|
842
|
+
```js
|
|
843
|
+
import { setTimeout } from 'node:timers/promises'
|
|
844
|
+
import task from 'tasuku/theme/codex'
|
|
845
|
+
|
|
846
|
+
await task('Building project', async ({ setTitle }) => {
|
|
847
|
+
await setTimeout(5000)
|
|
848
|
+
setTitle('Build complete')
|
|
849
|
+
})
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
</details>
|
|
853
|
+
</p>
|
|
854
|
+
|
|
855
|
+
### Custom themes
|
|
856
|
+
|
|
857
|
+
Create your own theme with `createTasuku()`. Each call returns an independent task runner with its own renderer.
|
|
858
|
+
|
|
859
|
+
The `createTasuku` export from `tasuku` (or `tasuku/inline`) accepts partial overrides — renderer and theme default to the entry point's built-in values. You can also use any built-in theme as a base:
|
|
860
|
+
|
|
861
|
+
```ts
|
|
862
|
+
import { rgb } from 'ansis'
|
|
863
|
+
import { createTasuku, theme } from 'tasuku'
|
|
864
|
+
|
|
865
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
866
|
+
const rainbow = frames.map((frame, i) => {
|
|
867
|
+
const hue = (i / frames.length) * 360
|
|
868
|
+
const [r, g, b] = hslToRgb(hue, 100, 50)
|
|
869
|
+
return rgb(r, g, b)(frame)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const task = createTasuku({
|
|
873
|
+
theme: {
|
|
874
|
+
...theme,
|
|
875
|
+
spinner: rainbow
|
|
876
|
+
}
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
await task('Custom task', async () => {
|
|
880
|
+
await someAsyncTask()
|
|
881
|
+
})
|
|
882
|
+
```
|
|
437
883
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
884
|
+
For full control over both renderer and theme, use the raw factory from `tasuku/create`:
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
import { createTasuku, pinned } from 'tasuku/create'
|
|
888
|
+
import { theme } from 'tasuku/theme/claude'
|
|
889
|
+
|
|
890
|
+
const task = createTasuku({
|
|
891
|
+
renderer: pinned,
|
|
892
|
+
theme
|
|
442
893
|
})
|
|
443
894
|
```
|
|
444
895
|
|
|
445
|
-
|
|
896
|
+
#### Theme object
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
type TasukuTheme = {
|
|
900
|
+
spinner: string[] // Pre-colored spinner frames
|
|
901
|
+
spinnerInterval?: number // ms between frames (default: 80)
|
|
902
|
+
icons: {
|
|
903
|
+
pending: string // Pre-colored icon strings
|
|
904
|
+
success: string
|
|
905
|
+
error: string
|
|
906
|
+
warning: string
|
|
907
|
+
parent: string // Parent task with children
|
|
908
|
+
parentError: string // Parent task in error state
|
|
909
|
+
}
|
|
910
|
+
colors: {
|
|
911
|
+
title?: (text: string, state: State, frame: number) => string
|
|
912
|
+
dim: (text: string) => string // Status, elapsed time
|
|
913
|
+
secondary: (text: string) => string // Output text, stream preview
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
The `title` color function receives the task state and animation frame counter, enabling per-frame effects like shimmer animations.
|
|
919
|
+
|
|
920
|
+
#### renderer
|
|
446
921
|
|
|
447
|
-
|
|
448
|
-
_Tasuku_ or タスク is the phonetic Japanese pronounciation of the word "task".
|
|
922
|
+
Type: `RendererFactory`
|
|
449
923
|
|
|
924
|
+
Required. The [renderer](#renderers) to use. Import `pinned` or `inline` from `tasuku/create`.
|
|
450
925
|
|
|
451
|
-
|
|
926
|
+
#### outputStream
|
|
452
927
|
|
|
453
|
-
|
|
928
|
+
Type: `NodeJS.WriteStream`
|
|
454
929
|
|
|
455
|
-
|
|
930
|
+
Default: `process.stderr`
|
|
456
931
|
|
|
457
|
-
|
|
932
|
+
The stream to render task UI to. Defaults to stderr so that stdout stays clean for program output (e.g. `mytool | jq`). `console.log` output goes to stdout unaffected.
|
|
458
933
|
|
|
459
|
-
###
|
|
460
|
-
Yes, but it should be fine as you don't need access to other `task` functions aside from the immediate one.
|
|
934
|
+
### Contributing a theme
|
|
461
935
|
|
|
462
|
-
|
|
463
|
-
- `"no-shadow": ["error", { "allow": ["task"] }]`
|
|
464
|
-
- `"@typescript-eslint/no-shadow": ["error", { "allow": ["task"] }]`
|
|
936
|
+
Have a theme you're proud of? We'd love to see it. Open a PR to add it as a built-in theme.
|
|
465
937
|
|
|
938
|
+
We hold themes to a high design standard — they should be elegant, versatile, and visually cohesive across all task states. We may decline themes that don't meet this bar, so don't take it personally.
|
|
466
939
|
|
|
467
940
|
## Sponsors
|
|
468
941
|
<p align="center">
|