feat: expose task status symbol in tasks table (#207)

* feat: expose task status symbol in tasks table

Adds a 'status' column to the tasks table that contains the raw
checkbox character (e.g. ' ', 'x', '-', '/'). This allows querying
by specific task states beyond just completed/incomplete.

* feat: adding tasks statuses support
This commit is contained in:
Kacper 2026-03-29 20:31:15 +01:00 committed by GitHub
parent 03dfcd78b2
commit c8567131cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 138 additions and 6 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": minor
---
Adding tasks statuses support

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View file

@ -44,6 +44,7 @@ Tasks table consists of the following columns:
| --------------- |--------------------------------------------------------------------------------------------------------------------------| ------------- |
| `task` | Content of the task (text) | |
| `completed` | 0 if not completed, 1 if completed | |
| `status` | The raw checkbox character (` ` for unchecked, `x` for completed, `/` for in-progress, `-` for cancelled, etc.) | 0.40.0 |
| `path` | Full path of the file the tag belongs to | 0.24.1 |
| `filePath` | (deprecated) same like `path`. Name changed for compatibility with other tables. Will get removed in the future versions | |
| `checkbox` | Interactive checkbox for the task that can be clicked to toggle completion state | 0.29.0 |
@ -88,3 +89,9 @@ WHERE target = @path
AND json_extract(position, '$.frontmatterKey') = 'type'
```
## Task Status Field
The `status` column in the `tasks` table contains the raw checkbox character from your markdown tasks. This enables querying tasks by their specific states beyond just completed/uncompleted. This is functionality used commonly in plugins like Tasks and Obsidian natively supports them too. Many themes also render these fields correctly by default. You can read more about it in [the Tasks documentation](https://publish.obsidian.md/tasks/Getting+Started/Statuses#Credit%20Sytone%20and%20the%20'Tasks%20SQL%20Powered'%20plugin).
### Example
![Tasks Statuses](tasks-statuses.png)

View file

@ -0,0 +1,96 @@
/**
* Tests for the `status` column added to the tasks table (main-2xf).
*
* `getFileTasks()` is an instance method that calls `this.app` and `this.db`,
* so we test the pure data-shaping logic by replicating it here rather than
* mocking the full Obsidian API surface.
*/
interface ListItemMock {
task: string // raw checkbox char: ' ', 'x', '/', '-', '<', '>', etc.
position: { start: { line: number } }
}
/** Mirrors the core logic of getFileTasks() in tasksTable.ts */
function shapeTask(listItem: ListItemMock, lineContent: string, filePath: string) {
if (!listItem.task) return undefined
const status = listItem.task !== ' '
const taskContent = lineContent.substring(lineContent.indexOf(']') + 1).trim()
return {
filePath,
path: filePath,
task: taskContent,
completed: status ? 1 : 0,
status: listItem.task,
position: listItem.position.start.line,
}
}
describe('tasks table — status column (main-2xf)', () => {
describe('status value reflects the raw checkbox character', () => {
it('is " " (space) for an unchecked task', () => {
const item: ListItemMock = { task: ' ', position: { start: { line: 0 } } }
const row = shapeTask(item, '- [ ] Buy groceries', 'note.md')
expect(row?.status).toBe(' ')
})
it('is "x" for a standard completed task', () => {
const item: ListItemMock = { task: 'x', position: { start: { line: 1 } } }
const row = shapeTask(item, '- [x] Done', 'note.md')
expect(row?.status).toBe('x')
})
it('is "/" for an in-progress task (Tasks plugin custom status)', () => {
const item: ListItemMock = { task: '/', position: { start: { line: 2 } } }
const row = shapeTask(item, '- [/] In progress', 'note.md')
expect(row?.status).toBe('/')
})
it('is "-" for a cancelled task', () => {
const item: ListItemMock = { task: '-', position: { start: { line: 3 } } }
const row = shapeTask(item, '- [-] Cancelled', 'note.md')
expect(row?.status).toBe('-')
})
it('is ">" for a forwarded/rescheduled task', () => {
const item: ListItemMock = { task: '>', position: { start: { line: 4 } } }
const row = shapeTask(item, '- [>] Rescheduled', 'note.md')
expect(row?.status).toBe('>')
})
})
describe('completed column is unaffected', () => {
it('completed=0 when status is space', () => {
const item: ListItemMock = { task: ' ', position: { start: { line: 0 } } }
expect(shapeTask(item, '- [ ] Pending', 'note.md')?.completed).toBe(0)
})
it('completed=1 when status is "x"', () => {
const item: ListItemMock = { task: 'x', position: { start: { line: 0 } } }
expect(shapeTask(item, '- [x] Done', 'note.md')?.completed).toBe(1)
})
it('completed=1 for custom non-space chars (any char != space counts as complete)', () => {
// This matches the existing semantics: status !== ' ' → completed
const item: ListItemMock = { task: '/', position: { start: { line: 0 } } }
expect(shapeTask(item, '- [/] In progress', 'note.md')?.completed).toBe(1)
})
})
describe('non-task list items are excluded', () => {
it('returns undefined when listItem.task is empty string (plain list item)', () => {
const item = { task: '', position: { start: { line: 0 } } }
expect(shapeTask(item, '- plain list item', 'note.md')).toBeUndefined()
})
})
describe('task content extraction is unaffected', () => {
it('strips the checkbox syntax and returns just the content', () => {
const item: ListItemMock = { task: 'x', position: { start: { line: 0 } } }
const row = shapeTask(item, '- [x] Buy milk', 'note.md')
expect(row?.task).toBe('Buy milk')
})
})
})

View file

@ -47,6 +47,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
checked: status,
path: file.path,
task: taskContent,
status: listItem.task,
position: {
line: listItem.position.start.line,
lineContent: lineContent
@ -63,6 +64,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
path: file.path,
task: taskContent,
completed: status ? 1 : 0,
status: listItem.task,
position: listItem.position.start.line,
checkbox: `SQLSEALCUSTOM(${JSON.stringify(checkboxData)})`,
heading: heading ? heading.heading : undefined,
@ -72,7 +74,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
}
async onInit(): Promise<void> {
await this.db.createTableNoTypes('tasks', ['checkbox', 'task', 'completed', 'filePath', 'path', 'position', 'heading', 'heading_level'])
await this.db.createTableNoTypes('tasks', ['checkbox', 'task', 'completed', 'status', 'filePath', 'path', 'position', 'heading', 'heading_level'])
// Indexes
const toIndex = ['filePath']

View file

@ -11,7 +11,8 @@ interface CheckboxProps {
line: number,
lineContent: string
},
task: string
task: string,
status?: string
}
const isCheckboxProp = (arg: any): arg is CheckboxProps => {
@ -45,7 +46,8 @@ export class CheckboxParser implements CellFunction<Args> {
const el = createEl('input', {
type: 'checkbox',
attr: {
checked: !!(values && values.checked) ? true : null
checked: !!(values && values.checked) ? true : null,
'data-task': values.status || ' '
}
})
@ -73,11 +75,26 @@ export class CheckboxParser implements CellFunction<Args> {
// Get the line containing the task
const lineIndex = values.position?.line
if (lineIndex >= 0 && lineIndex < lines.length) {
// Get current status
const currentStatus = values.status || (values.checked ? 'x' : ' ')
// Escape special regex characters
const escapedStatus = currentStatus.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// Update the task status
if (el.checked) {
lines[lineIndex] = lines[lineIndex].replace('- [ ]', '- [x]')
// Mark as completed
lines[lineIndex] = lines[lineIndex].replace(
new RegExp(`- \\[${escapedStatus}\\]`),
'- [x]'
)
} else {
lines[lineIndex] = lines[lineIndex].replace('- [x]', '- [ ]')
// Restore original status
const originalStatus = currentStatus === 'x' ? ' ' : currentStatus
lines[lineIndex] = lines[lineIndex].replace(
'- [x]',
`- [${originalStatus}]`
)
}
// Write back to the file
@ -98,10 +115,15 @@ export class CheckboxParser implements CellFunction<Args> {
}
return '[ ]'
} else {
// Use status if available
if (values.status) {
return `[${values.status}]`
}
// Fall back to checked boolean
if (values.checked) {
return '[x]'
} else {
return '[ ] '
return '[ ]'
}
}
}