Files
2026-07-13 13:34:48 +08:00

231 lines
6.7 KiB
TypeScript

import chalk from 'chalk'
export interface Task<T = any> {
id: string
title: string
status: 'pending' | 'in-progress' | 'done'
preview?: string // Optional preview for completed tasks
statusText?: string // Generic status description
value?: T
}
export interface TaskGroup<T> {
task: Task<T>
subtasks: Task<T>[]
}
// Animation frames for in-progress indicator
const progressFrames = ['◐', '◓', '◑', '◒']
let frameIndex = 0
export function displayTaskList(taskGroups: TaskGroup<any>[]): void {
console.log() // Add some spacing
taskGroups.forEach((group, groupIndex) => {
// Display main task
displaySingleTask(group.task, groupIndex + 1, 0)
// Determine if this group should be expanded
const shouldExpand = shouldExpandGroup(group)
if (shouldExpand) {
// Display subtasks with indentation
group.subtasks.forEach((subtask, subtaskIndex) => {
displaySingleTask(subtask, subtaskIndex + 1, 1, true)
})
} else {
// Show collapsed indicator with appropriate color
const collapsedCount = group.subtasks.length
if (collapsedCount > 0) {
const indent = ' '.repeat(1)
const allCompleted = group.subtasks.every((subtask) => subtask.status === 'done')
const collapsedColor = allCompleted ? chalk.green : chalk.gray
const collapsedText = collapsedColor(
`${indent}└─ ${collapsedCount} subtask${collapsedCount > 1 ? 's' : ''} (collapsed)`
)
console.log(collapsedText)
}
}
// Add spacing between task groups
if (groupIndex < taskGroups.length - 1) {
console.log()
}
})
// Display progress bar at the bottom
displayProgressBar(taskGroups)
console.log() // Add spacing after the list
}
// Display progress bar showing overall completion
function displayProgressBar(taskGroups: TaskGroup<any>[]): void {
// Calculate total tasks and completed tasks
let totalTasks = 0
let completedTasks = 0
taskGroups.forEach((group) => {
// Count main task
totalTasks++
if (group.task.status === 'done') completedTasks++
// Count subtasks
group.subtasks.forEach((subtask) => {
totalTasks++
if (subtask.status === 'done') completedTasks++
})
})
const percentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
const progressWidth = 30
const filledWidth = Math.round((percentage / 100) * progressWidth)
const emptyWidth = progressWidth - filledWidth
const filledBar = chalk.green('█'.repeat(filledWidth))
const emptyBar = chalk.gray('░'.repeat(emptyWidth))
const progressBar = `[${filledBar}${emptyBar}]`
const statusText = `${completedTasks}/${totalTasks} tasks completed (${percentage}%)`
console.log()
console.log(`${progressBar} ${statusText}`)
}
// Determine if a task group should be expanded
function shouldExpandGroup(group: TaskGroup<any>): boolean {
// Expand if main task is in-progress
if (group.task.status === 'in-progress') {
return true
}
// Expand if any subtask is in-progress
if (group.subtasks.some((subtask) => subtask.status === 'in-progress')) {
return true
}
// Collapse if all tasks are pending or all are done
return false
}
function displaySingleTask(task: Task, number: number, indentLevel: number, isSubtask: boolean = false): void {
// Create indentation
const indent = ' '.repeat(indentLevel)
const prefix = isSubtask ? '└─' : ''
// Status indicator with color coding
let statusIcon: string
let statusColor: (text: string) => string
switch (task.status) {
case 'pending':
statusIcon = '○'
statusColor = chalk.gray
break
case 'in-progress':
statusIcon = progressFrames[frameIndex % progressFrames.length]
statusColor = chalk.yellow
break
case 'done':
statusIcon = '●'
statusColor = chalk.green
break
}
// Task number formatting
const taskNumber = chalk.dim(`${number}.`)
// Title formatting
const title = task.status === 'done' ? chalk.dim(task.title) : task.title
// Status text formatting - keep all status on same line
let statusInfo = ''
if (task.statusText) {
if (task.status === 'in-progress') {
statusInfo = ` ${chalk.dim(`[ ${task.statusText} ]`)}`
} else if (task.status === 'done') {
statusInfo = chalk.dim(` ${task.statusText}`)
}
}
// Main task line
console.log(`${indent}${prefix} ${taskNumber} ${statusColor(statusIcon)} ${title}${statusInfo}`)
}
// Animated display function that updates in-progress indicators
export function displayTaskListAnimated(taskGroups: TaskGroup<any>[], updateInterval: number = 500): () => void {
let intervalId: ReturnType<typeof setInterval> | null = null
const updateDisplay = () => {
// Clear the entire screen and move cursor to top
process.stdout.write('\x1b[2J') // Clear entire screen
process.stdout.write('\x1b[H') // Move cursor to home position (0,0)
// Increment animation frame
frameIndex = (frameIndex + 1) % progressFrames.length
// Redraw the task list
displayTaskList(taskGroups)
const allDone = taskGroups.every((group) => group.task.status === 'done')
if (allDone) {
clearInterval(intervalId!)
}
}
// Initial display - clear screen first
process.stdout.write('\x1b[2J') // Clear entire screen
process.stdout.write('\x1b[H') // Move cursor to home position
displayTaskList(taskGroups)
intervalId = setInterval(updateDisplay, updateInterval)
// Return cleanup function
return () => {
if (intervalId) {
clearInterval(intervalId)
}
}
}
export function updateTaskStatus<T>({
status,
taskGroups,
taskId,
preview,
statusText,
value,
}: {
taskGroups: TaskGroup<T>[]
taskId: string
status: Task['status']
preview?: string
statusText?: string
value?: T
}) {
for (const group of taskGroups) {
if (group.task.id === taskId) {
group.task.status = status
if (preview) group.task.preview = preview
if (statusText) group.task.statusText = statusText
if (value !== undefined) group.task.value = value
}
for (const subtask of group.subtasks) {
if (subtask.id === taskId) {
subtask.status = status
if (preview) subtask.preview = preview
if (statusText) subtask.statusText = statusText
if (value !== undefined) subtask.value = value
}
}
if (group.task.status !== 'done' && group.subtasks.every((sub) => sub.status === 'done')) {
group.task.status = 'done' // Mark main task as done if all subtasks are done
}
if (group.task.status !== 'pending' && group.subtasks.some((sub) => sub.status === 'in-progress')) {
group.task.status = 'in-progress' // Mark main task as in-progress if any subtask is in-progress
}
}
}