bdtui 0.7.2 → 0.8.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.
@@ -1,218 +1,231 @@
1
- import blessed from 'blessed';
2
- import { state } from '../state.js';
3
-
4
- const STATUS_COLOR = {
5
- open: 'white', in_progress: 'cyan', blocked: 'red',
6
- deferred: 'yellow', closed: 'gray',
7
- };
8
-
9
- // Dep-row palette: tuned for at-a-glance scanning in the dependency list.
10
- // open / ready → green (go)
11
- // in_progress → yellow (active)
12
- // blocked → red (stop)
13
- // deferred → gray (parked)
14
- // closed → gray (done, recede)
15
- const DEP_STATUS_COLOR = {
16
- open: 'green', in_progress: 'yellow', blocked: 'red',
17
- deferred: 'gray', closed: 'gray',
18
- };
19
-
20
- const STATUS_ICON = {
21
- open: '○', in_progress: '◐', blocked: '●',
22
- closed: '✓', deferred: '❄',
23
- };
24
- const PRIORITY_COLOR = ['red', 'yellow', 'green', 'cyan', 'gray'];
25
- const PRIORITY_LABEL = ['Critical', 'High', 'Normal', 'Low', 'Trivial'];
26
-
27
- // Single pass: a two-step .replace(/\{/).replace(/\}/) would re-process the "}"
28
- // it just inserted into "{open}", corrupting any text containing a literal "}"
29
- // (e.g. a JSON/code snippet in a description) into a stray "{open}" token.
30
- function esc(s) { return String(s ?? '').replace(/[{}]/g, (c) => (c === '{' ? '{open}' : '{close}')); }
31
- function t(color, text) { return `{${color}-fg}${text}{/}`; }
32
- function bold(text) { return `{bold}${text}{/bold}`; }
33
- function fmtDate(iso) { return iso ? iso.slice(0, 10) : '—'; }
34
-
35
- function field(label, value, labelWidth = 10) {
36
- return `${t('gray', (label + ':').padEnd(labelWidth))} ${value}`;
37
- }
38
-
39
- function visLen(s) { return s.replace(/\{[^}]+\}/g, '').length; }
40
- function padRight(s, n) { const p = n - visLen(s); return p > 0 ? s + ' '.repeat(p) : s; }
41
-
42
- function fieldRow(lLabel, lVal, rLabel, rVal, lValWidth = 14) {
43
- const left = t('gray', (lLabel + ':').padEnd(10)) + ' ' + padRight(lVal, lValWidth);
44
- const right = ' ' + t('gray', (rLabel + ':').padEnd(10)) + ' ' + rVal;
45
- return left + ' ' + t('gray', '│') + right;
46
- }
47
-
48
- function sectionHeader(title) {
49
- return `\n${bold(t('cyan', title))}\n${t('gray', '─'.repeat(44))}`;
50
- }
51
-
52
- function depLine(dep) {
53
- const rawId = esc(dep.id || String(dep));
54
- const rawTitle = dep.title ? esc(dep.title) : '';
55
- const issueType = dep.issue_type ? esc(dep.issue_type) : null;
56
-
57
- // Effective status mirrors how `bd blocked` derives the Blocked tab:
58
- // an open bead whose blockers aren't all closed reads as "blocked",
59
- // not "open". `bd dep list` only returns the raw stored status.
60
- const effectiveStatus = (dep.status === 'open' && state.blockedIds.has(dep.id))
61
- ? 'blocked'
62
- : dep.status;
63
-
64
- const statusColor = DEP_STATUS_COLOR[effectiveStatus] || 'white';
65
- const icon = STATUS_ICON[effectiveStatus] || ' ';
66
- const idText = `{bold}${rawId}{/bold}`;
67
- const typeTag = issueType ? ` [${issueType}]` : '';
68
- const titleText = rawTitle ? ` — ${rawTitle}` : '';
69
-
70
- const row = `${icon} ${idText}${typeTag}${titleText}`;
71
- return ` {${statusColor}-fg}${row}{/${statusColor}-fg}`;
72
- }
73
-
74
- // Apply inline markdown styling to already-escaped text. Order matters:
75
- // inline code is captured first to placeholders so its content is not
76
- // mangled by bold/italic/link regexes.
77
- function applyInline(s) {
78
- const codes = [];
79
- let out = s.replace(/`([^`]+)`/g, (_, c) => {
80
- codes.push(c);
81
- return `\x00C${codes.length - 1}\x00`;
82
- });
83
- out = out.replace(/\*\*([^*]+)\*\*/g, '{bold}{white-fg}$1{/white-fg}{/bold}');
84
- out = out.replace(/(^|[\s(])\*([^*\s][^*]*?)\*(?=[\s.,;:!?)]|$)/g, '$1{italic}$2{/italic}');
85
- out = out.replace(/~~([^~]+)~~/g, '{strikethrough}$1{/strikethrough}');
86
- out = out.replace(
87
- /\[([^\]]+)\]\(([^)]+)\)/g,
88
- '{underline}{cyan-fg}$1{/cyan-fg}{/underline} {gray-fg}($2){/gray-fg}',
89
- );
90
- out = out.replace(/\x00C(\d+)\x00/g, (_, i) => t('yellow', '`' + codes[+i] + '`'));
91
- return out;
92
- }
93
-
94
- function renderDescription(lines, text) {
95
- const src = text.split('\n');
96
- let inCode = false;
97
- for (const raw of src) {
98
- const l = raw;
99
-
100
- if (l.trim().startsWith('```')) {
101
- inCode = !inCode;
102
- lines.push(` ${t('gray', '────')}`);
103
- continue;
104
- }
105
- if (inCode) {
106
- lines.push(` ${t('green', esc(l))}`);
107
- continue;
108
- }
109
-
110
- if (l.startsWith('### ')) lines.push(` ${bold(t('blue', applyInline(esc(l.slice(4)))))}`);
111
- else if (l.startsWith('## ')) lines.push(` ${bold(t('cyan', applyInline(esc(l.slice(3)))))}`);
112
- else if (l.startsWith('# ')) lines.push(` ${bold(t('white', applyInline(esc(l.slice(2)))))}`);
113
- else if (l.match(/^[-*] /)) lines.push(` ${t('gray', '•')} ${applyInline(esc(l.slice(2)))}`);
114
- else if (l.match(/^\s*\d+\.\s+/)) {
115
- const m = l.match(/^(\s*)(\d+)\.\s+(.*)$/);
116
- lines.push(` ${m[1]}${t('gray', m[2] + '.')} ${applyInline(esc(m[3]))}`);
117
- }
118
- else if (l.startsWith('> ')) lines.push(` ${t('gray', '│')} ${t('gray', applyInline(esc(l.slice(2))))}`);
119
- else lines.push(` ${applyInline(esc(l))}`);
120
- }
121
- }
122
-
123
- let lastId = null;
124
-
125
- export function createDetail(screen) {
126
- return blessed.box({
127
- parent: screen,
128
- top: 1,
129
- left: '40%',
130
- width: '60%',
131
- bottom: 1,
132
- border: { type: 'line' },
133
- label: ' Detail ',
134
- tags: true,
135
- scrollable: true,
136
- alwaysScroll: true,
137
- keys: true,
138
- mouse: true,
139
- scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
140
- style: {
141
- border: { fg: 'gray' },
142
- label: { fg: 'white', bold: true },
143
- },
144
- });
145
- }
146
-
147
- export function renderDetail(box) {
148
- const id = state.selectedId;
149
- if (!id) { box.setContent(t('gray', 'No bead selected.')); return; }
150
-
151
- const b = state.beadsById.get(id);
152
- if (!b) { box.setContent(t('gray', `Loading ${id}…`)); return; }
153
-
154
- const resetScroll = id !== lastId;
155
- lastId = id;
156
-
157
- const prio = b.priority ?? 2;
158
- const prioColor = PRIORITY_COLOR[prio] || 'white';
159
- const prioLabel = PRIORITY_LABEL[prio] || String(prio);
160
- const statColor = STATUS_COLOR[b.status] || 'white';
161
-
162
- const lines = [];
163
-
164
- // ── Header ─────────────────────────────────────────────────────────────────
165
- lines.push(`{bold}{white-fg}${esc(b.id)}{/} {gray-fg}—{/} ${esc(b.title || '')}`);
166
- lines.push(t('gray', '═'.repeat(50)));
167
- lines.push('');
168
-
169
- // ── Fields ─────────────────────────────────────────────────────────────────
170
- lines.push(fieldRow(
171
- 'Status', `{${statColor}-fg}{bold}${esc(b.status || '—')}{/bold}{/}`,
172
- 'Priority', `{${prioColor}-fg}{bold}P${prio} ${prioLabel}{/bold}{/}`,
173
- ));
174
- lines.push(fieldRow(
175
- 'Type', t('white', esc(b.issue_type || '—')),
176
- 'Owner', t('white', esc(b.owner || '—')),
177
- ));
178
- lines.push(fieldRow(
179
- 'Created', t('gray', fmtDate(b.created_at)),
180
- 'Updated', t('gray', fmtDate(b.updated_at)),
181
- ));
182
-
183
- if (b.labels?.length > 0) {
184
- lines.push(field('Labels', b.labels.map((l) => t('blue', esc(l))).join(' ')));
185
- }
186
-
187
- // ── Dependencies ───────────────────────────────────────────────────────────
188
- if (b.depsDown?.length > 0) {
189
- lines.push(sectionHeader(`Depends on (${b.depsDown.length})`));
190
- for (const d of b.depsDown) lines.push(depLine(d));
191
- }
192
- if (b.depsUp?.length > 0) {
193
- lines.push(sectionHeader(`Needed by (${b.depsUp.length})`));
194
- for (const d of b.depsUp) lines.push(depLine(d));
195
- }
196
-
197
- // ── Description ────────────────────────────────────────────────────────────
198
- if (b.description) {
199
- lines.push(sectionHeader('Description'));
200
- lines.push('');
201
- renderDescription(lines, b.description);
202
- }
203
-
204
- if (b.acceptance) {
205
- lines.push(sectionHeader('Acceptance'));
206
- lines.push('');
207
- renderDescription(lines, b.acceptance);
208
- }
209
-
210
- if (b.notes) {
211
- lines.push(sectionHeader('Notes'));
212
- lines.push('');
213
- renderDescription(lines, b.notes);
214
- }
215
-
216
- box.setContent(lines.join('\n'));
217
- if (resetScroll) box.scrollTo(0);
218
- }
1
+ import blessed from 'blessed';
2
+ import { state } from '../state.js';
3
+
4
+ const STATUS_COLOR = {
5
+ open: 'white', in_progress: 'cyan', blocked: 'red',
6
+ deferred: 'yellow', closed: 'gray',
7
+ };
8
+
9
+ // Dep-row palette: tuned for at-a-glance scanning in the dependency list.
10
+ // open / ready → green (go)
11
+ // in_progress → yellow (active)
12
+ // blocked → red (stop)
13
+ // deferred → gray (parked)
14
+ // closed → gray (done, recede)
15
+ const DEP_STATUS_COLOR = {
16
+ open: 'green', in_progress: 'yellow', blocked: 'red',
17
+ deferred: 'gray', closed: 'gray',
18
+ };
19
+
20
+ const STATUS_ICON = {
21
+ open: '○', in_progress: '◐', blocked: '●',
22
+ closed: '✓', deferred: '❄',
23
+ };
24
+ const PRIORITY_COLOR = ['red', 'yellow', 'green', 'cyan', 'gray'];
25
+ const PRIORITY_LABEL = ['Critical', 'High', 'Normal', 'Low', 'Trivial'];
26
+
27
+ // Single pass: a two-step .replace(/\{/).replace(/\}/) would re-process the "}"
28
+ // it just inserted into "{open}", corrupting any text containing a literal "}"
29
+ // (e.g. a JSON/code snippet in a description) into a stray "{open}" token.
30
+ function esc(s) { return String(s ?? '').replace(/[{}]/g, (c) => (c === '{' ? '{open}' : '{close}')); }
31
+ function t(color, text) { return `{${color}-fg}${text}{/}`; }
32
+ function bold(text) { return `{bold}${text}{/bold}`; }
33
+ function fmtDate(iso) { return iso ? iso.slice(0, 10) : '—'; }
34
+
35
+ function field(label, value, labelWidth = 10) {
36
+ return `${t('gray', (label + ':').padEnd(labelWidth))} ${value}`;
37
+ }
38
+
39
+ function visLen(s) { return s.replace(/\{[^}]+\}/g, '').length; }
40
+ function padRight(s, n) { const p = n - visLen(s); return p > 0 ? s + ' '.repeat(p) : s; }
41
+
42
+ function fieldRow(lLabel, lVal, rLabel, rVal, lValWidth = 14) {
43
+ const left = t('gray', (lLabel + ':').padEnd(10)) + ' ' + padRight(lVal, lValWidth);
44
+ const right = ' ' + t('gray', (rLabel + ':').padEnd(10)) + ' ' + rVal;
45
+ return left + ' ' + t('gray', '│') + right;
46
+ }
47
+
48
+ function sectionHeader(title) {
49
+ return `\n${bold(t('cyan', title))}\n${t('gray', '─'.repeat(44))}`;
50
+ }
51
+
52
+ function depLine(dep) {
53
+ const rawId = esc(dep.id || String(dep));
54
+ const rawTitle = dep.title ? esc(dep.title) : '';
55
+ const issueType = dep.issue_type ? esc(dep.issue_type) : null;
56
+
57
+ // Effective status mirrors how `bd blocked` derives the Blocked tab:
58
+ // an open bead whose blockers aren't all closed reads as "blocked",
59
+ // not "open". `bd dep list` only returns the raw stored status.
60
+ const effectiveStatus = (dep.status === 'open' && state.blockedIds.has(dep.id))
61
+ ? 'blocked'
62
+ : dep.status;
63
+
64
+ const statusColor = DEP_STATUS_COLOR[effectiveStatus] || 'white';
65
+ const icon = STATUS_ICON[effectiveStatus] || ' ';
66
+ const idText = `{bold}${rawId}{/bold}`;
67
+ const typeTag = issueType ? ` [${issueType}]` : '';
68
+ const titleText = rawTitle ? ` — ${rawTitle}` : '';
69
+
70
+ const row = `${icon} ${idText}${typeTag}${titleText}`;
71
+ return ` {${statusColor}-fg}${row}{/${statusColor}-fg}`;
72
+ }
73
+
74
+ // Apply inline markdown styling to already-escaped text. Order matters:
75
+ // inline code is captured first to placeholders so its content is not
76
+ // mangled by bold/italic/link regexes.
77
+ function applyInline(s) {
78
+ const codes = [];
79
+ let out = s.replace(/`([^`]+)`/g, (_, c) => {
80
+ codes.push(c);
81
+ return `\x00C${codes.length - 1}\x00`;
82
+ });
83
+ out = out.replace(/\*\*([^*]+)\*\*/g, '{bold}{white-fg}$1{/white-fg}{/bold}');
84
+ out = out.replace(/(^|[\s(])\*([^*\s][^*]*?)\*(?=[\s.,;:!?)]|$)/g, '$1{italic}$2{/italic}');
85
+ out = out.replace(/~~([^~]+)~~/g, '{strikethrough}$1{/strikethrough}');
86
+ out = out.replace(
87
+ /\[([^\]]+)\]\(([^)]+)\)/g,
88
+ '{underline}{cyan-fg}$1{/cyan-fg}{/underline} {gray-fg}($2){/gray-fg}',
89
+ );
90
+ out = out.replace(/\x00C(\d+)\x00/g, (_, i) => t('yellow', '`' + codes[+i] + '`'));
91
+ return out;
92
+ }
93
+
94
+ function renderDescription(lines, text) {
95
+ const src = text.split('\n');
96
+ let inCode = false;
97
+ for (const raw of src) {
98
+ const l = raw;
99
+
100
+ if (l.trim().startsWith('```')) {
101
+ inCode = !inCode;
102
+ lines.push(` ${t('gray', '────')}`);
103
+ continue;
104
+ }
105
+ if (inCode) {
106
+ lines.push(` ${t('green', esc(l))}`);
107
+ continue;
108
+ }
109
+
110
+ if (l.startsWith('### ')) lines.push(` ${bold(t('blue', applyInline(esc(l.slice(4)))))}`);
111
+ else if (l.startsWith('## ')) lines.push(` ${bold(t('cyan', applyInline(esc(l.slice(3)))))}`);
112
+ else if (l.startsWith('# ')) lines.push(` ${bold(t('white', applyInline(esc(l.slice(2)))))}`);
113
+ else if (l.match(/^[-*] /)) lines.push(` ${t('gray', '•')} ${applyInline(esc(l.slice(2)))}`);
114
+ else if (l.match(/^\s*\d+\.\s+/)) {
115
+ const m = l.match(/^(\s*)(\d+)\.\s+(.*)$/);
116
+ lines.push(` ${m[1]}${t('gray', m[2] + '.')} ${applyInline(esc(m[3]))}`);
117
+ }
118
+ else if (l.startsWith('> ')) lines.push(` ${t('gray', '│')} ${t('gray', applyInline(esc(l.slice(2))))}`);
119
+ else lines.push(` ${applyInline(esc(l))}`);
120
+ }
121
+ }
122
+
123
+ let lastId = null;
124
+
125
+ export function createDetail(screen) {
126
+ return blessed.box({
127
+ parent: screen,
128
+ top: 1,
129
+ left: '40%',
130
+ width: '60%',
131
+ bottom: 1,
132
+ border: { type: 'line' },
133
+ label: ' Detail ',
134
+ tags: true,
135
+ scrollable: true,
136
+ alwaysScroll: true,
137
+ keys: true,
138
+ vi: true,
139
+ mouse: true,
140
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
141
+ style: {
142
+ border: { fg: 'gray' },
143
+ label: { fg: 'white', bold: true },
144
+ },
145
+ });
146
+ }
147
+
148
+ export function renderDetail(box) {
149
+ const id = state.selectedId;
150
+ if (!id) { box.setContent(t('gray', 'No bead selected.')); return; }
151
+
152
+ const b = state.beadsById.get(id);
153
+ if (!b) { box.setContent(t('gray', `Loading ${id}…`)); return; }
154
+
155
+ const resetScroll = id !== lastId;
156
+ lastId = id;
157
+
158
+ const prio = b.priority ?? 2;
159
+ const prioColor = PRIORITY_COLOR[prio] || 'white';
160
+ const prioLabel = PRIORITY_LABEL[prio] || String(prio);
161
+ const statColor = STATUS_COLOR[b.status] || 'white';
162
+
163
+ const lines = [];
164
+
165
+ // ── Header ─────────────────────────────────────────────────────────────────
166
+ lines.push(`{bold}{white-fg}${esc(b.id)}{/} {gray-fg}—{/} ${esc(b.title || '')}`);
167
+ lines.push(t('gray', '═'.repeat(50)));
168
+ lines.push('');
169
+
170
+ // ── Fields ─────────────────────────────────────────────────────────────────
171
+ lines.push(fieldRow(
172
+ 'Status', `{${statColor}-fg}{bold}${esc(b.status || '—')}{/bold}{/}`,
173
+ 'Priority', `{${prioColor}-fg}{bold}P${prio} ${prioLabel}{/bold}{/}`,
174
+ ));
175
+ lines.push(fieldRow(
176
+ 'Type', t('white', esc(b.issue_type || '—')),
177
+ 'Owner', t('white', esc(b.owner || '—')),
178
+ ));
179
+ lines.push(fieldRow(
180
+ 'Created', t('gray', fmtDate(b.created_at)),
181
+ 'Updated', t('gray', fmtDate(b.updated_at)),
182
+ ));
183
+
184
+ if (b.labels?.length > 0) {
185
+ lines.push(field('Labels', b.labels.map((l) => t('blue', esc(l))).join(' ')));
186
+ }
187
+
188
+ // ── Close reason (closed beads) ─────────────────────────────────────────────
189
+ // bd stores the text from `bd close --reason` in `close_reason`; loadDetail
190
+ // already spreads the full `bd show` object onto the bead, so it's available
191
+ // here. Rendered high in the pane (before deps) so a closed bead's resolution
192
+ // is visible without scrolling. Truthy guard → open beads show nothing.
193
+ if (b.close_reason) {
194
+ const when = b.closed_at ? ` · ${fmtDate(b.closed_at)}` : '';
195
+ lines.push(sectionHeader(`Close Reason${when}`));
196
+ lines.push('');
197
+ renderDescription(lines, b.close_reason);
198
+ }
199
+
200
+ // ── Dependencies ───────────────────────────────────────────────────────────
201
+ if (b.depsDown?.length > 0) {
202
+ lines.push(sectionHeader(`Depends on (${b.depsDown.length})`));
203
+ for (const d of b.depsDown) lines.push(depLine(d));
204
+ }
205
+ if (b.depsUp?.length > 0) {
206
+ lines.push(sectionHeader(`Needed by (${b.depsUp.length})`));
207
+ for (const d of b.depsUp) lines.push(depLine(d));
208
+ }
209
+
210
+ // ── Description ────────────────────────────────────────────────────────────
211
+ if (b.description) {
212
+ lines.push(sectionHeader('Description'));
213
+ lines.push('');
214
+ renderDescription(lines, b.description);
215
+ }
216
+
217
+ if (b.acceptance) {
218
+ lines.push(sectionHeader('Acceptance'));
219
+ lines.push('');
220
+ renderDescription(lines, b.acceptance);
221
+ }
222
+
223
+ if (b.notes) {
224
+ lines.push(sectionHeader('Notes'));
225
+ lines.push('');
226
+ renderDescription(lines, b.notes);
227
+ }
228
+
229
+ box.setContent(lines.join('\n'));
230
+ if (resetScroll) box.scrollTo(0);
231
+ }
package/src/views/list.js CHANGED
@@ -1,99 +1,99 @@
1
- import blessed from 'blessed';
2
- import { state } from '../state.js';
3
-
4
- const STATUS_SHORT = {
5
- open: 'open ',
6
- in_progress: 'in_prog',
7
- blocked: 'blocked',
8
- deferred: 'deferrd',
9
- closed: 'closed ',
10
- };
11
-
12
- const PRIORITY_COLOR = ['red', 'yellow', 'green', 'cyan', 'gray'];
13
- const STATUS_COLOR = {
14
- open: 'white', in_progress: 'cyan', blocked: 'red',
15
- deferred: 'yellow', closed: 'gray',
16
- };
17
-
18
- const TYPE_BADGE = {
19
- epic: '{magenta-fg}E{/}',
20
- task: '{blue-fg}T{/}',
21
- chore: '{gray-fg}C{/}',
22
- bug: '{red-fg}B{/}',
23
- story: '{cyan-fg}S{/}',
24
- feature: '{green-fg}F{/}',
25
- };
26
-
27
- function t(color, text) {
28
- return `{${color}-fg}${text}{/}`;
29
- }
30
-
31
- function formatRow(bead, depth = 0, isLast = false) {
32
- const id = bead.id.padEnd(14);
33
- const p = `P${bead.priority ?? 2}`;
34
- // Effective status mirrors how `bd blocked` derives the Blocked tab:
35
- // an open bead whose blockers aren't all closed reads as "blocked",
36
- // not "open". The raw `bd list` rows only carry the stored status.
37
- const stat = (bead.status === 'open' && state.blockedIds.has(bead.id))
38
- ? 'blocked'
39
- : (bead.status || 'open');
40
- const s = (STATUS_SHORT[stat] || stat.slice(0, 7)).padEnd(7);
41
- const badge = TYPE_BADGE[bead.issue_type] ?? t('gray', '?');
42
- const prefix = depth > 0 ? t('gray', isLast ? ' └ ' : ' ├ ') : ' ';
43
- const title = bead.title || '';
44
-
45
- const idCol = t('gray', id);
46
- const pCol = t(PRIORITY_COLOR[bead.priority ?? 2] ?? 'white', p);
47
- const sCol = t(STATUS_COLOR[stat] ?? 'white', s);
48
-
49
- return `${idCol} ${pCol} ${sCol} ${badge}${prefix}${title}`;
50
- }
51
-
52
- export function createList(screen) {
53
- return blessed.list({
54
- parent: screen,
55
- top: 1,
56
- left: 0,
57
- width: '40%',
58
- bottom: 1,
59
- border: { type: 'line' },
60
- label: ' Beads ',
61
- tags: true,
62
- keys: true,
63
- vi: true,
64
- mouse: true,
65
- scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
66
- style: {
67
- selected: { bg: 'blue', fg: 'white', bold: true },
68
- border: { fg: 'gray' },
69
- label: { fg: 'white', bold: true },
70
- },
71
- });
72
- }
73
-
74
- export function renderList(list) {
75
- const total = state.listOrder.length;
76
- // In paged ("load more") mode only the first visibleCount rows are rendered;
77
- // everything else renders the full list.
78
- const shown = state.visibleCount > 0 && state.visibleCount < total
79
- ? state.visibleCount
80
- : total;
81
- const ids = state.listOrder.slice(0, shown);
82
-
83
- const items = ids.map((id) => {
84
- const b = state.beadsById.get(id);
85
- const { depth = 0, isLast = false } = state.treeMeta.get(id) ?? {};
86
- return b ? formatRow(b, depth, isLast) : t('gray', id);
87
- });
88
-
89
- const selIdx = state.selectedId
90
- ? Math.max(0, ids.indexOf(state.selectedId))
91
- : 0;
92
-
93
- const label = shown < total
94
- ? ` Beads ${t('gray', `(showing ${shown} of ${total})`)} `
95
- : ` Beads ${t('gray', `(${total})`)} `;
96
- list.setLabel(label);
97
- list.setItems(items);
98
- if (ids.length > 0) list.select(Math.min(selIdx, ids.length - 1));
99
- }
1
+ import blessed from 'blessed';
2
+ import { state } from '../state.js';
3
+
4
+ const STATUS_SHORT = {
5
+ open: 'open ',
6
+ in_progress: 'in_prog',
7
+ blocked: 'blocked',
8
+ deferred: 'deferrd',
9
+ closed: 'closed ',
10
+ };
11
+
12
+ const PRIORITY_COLOR = ['red', 'yellow', 'green', 'cyan', 'gray'];
13
+ const STATUS_COLOR = {
14
+ open: 'white', in_progress: 'cyan', blocked: 'red',
15
+ deferred: 'yellow', closed: 'gray',
16
+ };
17
+
18
+ const TYPE_BADGE = {
19
+ epic: '{magenta-fg}E{/}',
20
+ task: '{blue-fg}T{/}',
21
+ chore: '{gray-fg}C{/}',
22
+ bug: '{red-fg}B{/}',
23
+ story: '{cyan-fg}S{/}',
24
+ feature: '{green-fg}F{/}',
25
+ };
26
+
27
+ function t(color, text) {
28
+ return `{${color}-fg}${text}{/}`;
29
+ }
30
+
31
+ function formatRow(bead, depth = 0, isLast = false) {
32
+ const id = bead.id.padEnd(14);
33
+ const p = `P${bead.priority ?? 2}`;
34
+ // Effective status mirrors how `bd blocked` derives the Blocked tab:
35
+ // an open bead whose blockers aren't all closed reads as "blocked",
36
+ // not "open". The raw `bd list` rows only carry the stored status.
37
+ const stat = (bead.status === 'open' && state.blockedIds.has(bead.id))
38
+ ? 'blocked'
39
+ : (bead.status || 'open');
40
+ const s = (STATUS_SHORT[stat] || stat.slice(0, 7)).padEnd(7);
41
+ const badge = TYPE_BADGE[bead.issue_type] ?? t('gray', '?');
42
+ const prefix = depth > 0 ? t('gray', isLast ? ' └ ' : ' ├ ') : ' ';
43
+ const title = bead.title || '';
44
+
45
+ const idCol = t('gray', id);
46
+ const pCol = t(PRIORITY_COLOR[bead.priority ?? 2] ?? 'white', p);
47
+ const sCol = t(STATUS_COLOR[stat] ?? 'white', s);
48
+
49
+ return `${idCol} ${pCol} ${sCol} ${badge}${prefix}${title}`;
50
+ }
51
+
52
+ export function createList(screen) {
53
+ return blessed.list({
54
+ parent: screen,
55
+ top: 1,
56
+ left: 0,
57
+ width: '40%',
58
+ bottom: 1,
59
+ border: { type: 'line' },
60
+ label: ' Beads ',
61
+ tags: true,
62
+ keys: true,
63
+ vi: true,
64
+ mouse: true,
65
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
66
+ style: {
67
+ selected: { bg: 'blue', fg: 'white', bold: true },
68
+ border: { fg: 'gray' },
69
+ label: { fg: 'white', bold: true },
70
+ },
71
+ });
72
+ }
73
+
74
+ export function renderList(list) {
75
+ const total = state.listOrder.length;
76
+ // In paged ("load more") mode only the first visibleCount rows are rendered;
77
+ // everything else renders the full list.
78
+ const shown = state.visibleCount > 0 && state.visibleCount < total
79
+ ? state.visibleCount
80
+ : total;
81
+ const ids = state.listOrder.slice(0, shown);
82
+
83
+ const items = ids.map((id) => {
84
+ const b = state.beadsById.get(id);
85
+ const { depth = 0, isLast = false } = state.treeMeta.get(id) ?? {};
86
+ return b ? formatRow(b, depth, isLast) : t('gray', id);
87
+ });
88
+
89
+ const selIdx = state.selectedId
90
+ ? Math.max(0, ids.indexOf(state.selectedId))
91
+ : 0;
92
+
93
+ const label = shown < total
94
+ ? ` Beads ${t('gray', `(showing ${shown} of ${total})`)} `
95
+ : ` Beads ${t('gray', `(${total})`)} `;
96
+ list.setLabel(label);
97
+ list.setItems(items);
98
+ if (ids.length > 0) list.select(Math.min(selIdx, ids.length - 1));
99
+ }