Skip to content

Commit 08793ab

Browse files
committed
Improve type safety and low-level elegance
- Extract isValidStatusData() type guard for stdin JSON validation - Decompose findOptimalAssignment into decodeAssignment/computeRowWidths/scoreAssignment - Fix last tautological effort test with CLAUDE_CONFIG_DIR mock
1 parent 30102e8 commit 08793ab

4 files changed

Lines changed: 68 additions & 58 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-statusblocks",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"description": "Opinionated, adaptive block-based status line for Claude Code with bin-packing layout, rate limit tracking, and campaign monitoring",
55
"type": "module",
66
"bin": {

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@ import type { StatusLineData } from './types.js';
55
import { loadConfig } from './config.js';
66
import { render } from './layout.js';
77

8+
/** Validate that parsed JSON has the minimum shape of StatusLineData */
9+
function isValidStatusData(v: unknown): v is StatusLineData {
10+
if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
11+
const obj = v as Record<string, unknown>;
12+
if (!obj.model || typeof obj.model !== 'object') return false;
13+
if (!obj.context_window || typeof obj.context_window !== 'object') return false;
14+
if (!obj.workspace || typeof obj.workspace !== 'object') return false;
15+
return true;
16+
}
17+
818
// Read all of stdin
919
let input = '';
1020
process.stdin.setEncoding('utf8');
1121
process.stdin.on('data', (chunk: string) => { input += chunk; });
1222
process.stdin.on('end', () => {
1323
try {
1424
const parsed = JSON.parse(input);
15-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.model || !parsed.context_window) {
25+
if (!isValidStatusData(parsed)) {
1626
process.stdout.write('\n');
1727
return;
1828
}
19-
const data = parsed as StatusLineData;
29+
const data = parsed;
2030
const config = loadConfig();
2131
// Detect terminal width — stty on parent's TTY, then tput, then fallback
2232
let termWidth = process.stderr.columns || process.stdout.columns || 0;

src/layout.ts

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -74,72 +74,72 @@ function renderRow(group: RowGroup): string[] {
7474
* Rows are sorted by width ascending (pyramid shape).
7575
* Returns the best assignment as a row-index-per-block array, or null.
7676
*/
77+
/** Decode a combo number into per-block row assignments and check all rows are used */
78+
function decodeAssignment(
79+
combo: number, blockCount: number, targetRows: number,
80+
blockRow: number[], rowUsed: boolean[],
81+
): boolean {
82+
let encoded = combo;
83+
for (let b = 0; b < blockCount; b++) {
84+
blockRow[b] = encoded % targetRows;
85+
encoded = Math.floor(encoded / targetRows);
86+
}
87+
rowUsed.fill(false, 0, targetRows);
88+
for (let b = 0; b < blockCount; b++) rowUsed[blockRow[b]!] = true;
89+
for (let r = 0; r < targetRows; r++) { if (!rowUsed[r]) return false; }
90+
return true;
91+
}
92+
93+
/** Compute the total rendered width of each row from block assignments */
94+
function computeRowWidths(
95+
blockRow: number[], blockCount: number, targetRows: number,
96+
blockWidths: number[], rowWidths: number[],
97+
): void {
98+
for (let r = 0; r < targetRows; r++) rowWidths[r] = 0;
99+
for (let b = 0; b < blockCount; b++) rowWidths[blockRow[b]!] += blockWidths[b]! + BOX_CHROME;
100+
for (let r = 0; r < targetRows; r++) {
101+
let count = 0;
102+
for (let b = 0; b < blockCount; b++) { if (blockRow[b] === r) count++; }
103+
if (count > 1) rowWidths[r] += (count - 1) * GAP;
104+
}
105+
}
106+
107+
/** Check rows fit within width limits (pyramid order) and return a score, or -Infinity if invalid */
108+
function scoreAssignment(
109+
targetRows: number, rowWidths: number[], row1MaxWidth: number, maxRowWidth: number,
110+
): number {
111+
// Sort by width ascending — narrowest row displayed first (pyramid)
112+
const displayOrder: number[] = [];
113+
for (let r = 0; r < targetRows; r++) displayOrder.push(r);
114+
displayOrder.sort((a, b) => rowWidths[a]! - rowWidths[b]!);
115+
116+
for (let pos = 0; pos < displayOrder.length; pos++) {
117+
const limit = pos === 0 ? row1MaxWidth : maxRowWidth;
118+
if (rowWidths[displayOrder[pos]!]! > limit) return -Infinity;
119+
}
120+
121+
// Row count dominates (1e9), widest row is tiebreaker (1e3)
122+
let widest = 0;
123+
for (let r = 0; r < targetRows; r++) { if (rowWidths[r]! > widest) widest = rowWidths[r]!; }
124+
return -(targetRows * 1e9) - (widest * 1e3);
125+
}
126+
77127
export function findOptimalAssignment(
78128
blockCount: number, blockWidths: number[], row1MaxWidth: number, maxRowWidth: number,
79129
): number[] | null {
80130
let bestAssignment: number[] | null = null;
81131
let bestScore = -Infinity;
82132

83-
// Reusable buffers to avoid per-iteration allocations
84133
const blockRow = new Array<number>(blockCount);
85134
const rowUsed = new Array<boolean>(blockCount);
86135
const rowWidths = new Array<number>(blockCount);
87136

88137
for (let targetRows = 1; targetRows <= blockCount; targetRows++) {
89138
const totalCombinations = targetRows ** blockCount;
90-
91139
for (let combo = 0; combo < totalCombinations; combo++) {
92-
// Decode combo into per-block row assignments (base-targetRows digits)
93-
let encoded = combo;
94-
for (let block = 0; block < blockCount; block++) {
95-
blockRow[block] = encoded % targetRows;
96-
encoded = Math.floor(encoded / targetRows);
97-
}
98-
99-
// Verify all target rows are occupied
100-
rowUsed.fill(false, 0, targetRows);
101-
for (let block = 0; block < blockCount; block++) rowUsed[blockRow[block]!] = true;
102-
let allRowsOccupied = true;
103-
for (let row = 0; row < targetRows; row++) {
104-
if (!rowUsed[row]) { allRowsOccupied = false; break; }
105-
}
106-
if (!allRowsOccupied) continue;
107-
108-
// Compute total width per row (block widths + chrome + gaps)
109-
for (let row = 0; row < targetRows; row++) rowWidths[row] = 0;
110-
for (let block = 0; block < blockCount; block++) {
111-
rowWidths[blockRow[block]!] += blockWidths[block]! + BOX_CHROME;
112-
}
113-
for (let row = 0; row < targetRows; row++) {
114-
let blocksInRow = 0;
115-
for (let block = 0; block < blockCount; block++) {
116-
if (blockRow[block] === row) blocksInRow++;
117-
}
118-
if (blocksInRow > 1) rowWidths[row] += (blocksInRow - 1) * GAP;
119-
}
120-
121-
// Sort rows by width ascending (pyramid: narrowest on top)
122-
const displayOrder: number[] = [];
123-
for (let row = 0; row < targetRows; row++) displayOrder.push(row);
124-
displayOrder.sort((a, b) => rowWidths[a]! - rowWidths[b]!);
125-
126-
// Validate: narrowest row (displayed first) uses tighter width limit
127-
let valid = true;
128-
for (let pos = 0; pos < displayOrder.length; pos++) {
129-
const widthLimit = pos === 0 ? row1MaxWidth : maxRowWidth;
130-
if (rowWidths[displayOrder[pos]!]! > widthLimit) { valid = false; break; }
131-
}
132-
if (!valid) continue;
133-
134-
// Score: row count dominates (1e9 weight), widest row is tiebreaker (1e3).
135-
// Higher score = better layout. Both terms are negative so fewer rows
136-
// and smaller widest row both increase the score.
137-
let widestRow = 0;
138-
for (let row = 0; row < targetRows; row++) {
139-
if (rowWidths[row]! > widestRow) widestRow = rowWidths[row]!;
140-
}
141-
const score = -(targetRows * 1e9) - (widestRow * 1e3);
142-
140+
if (!decodeAssignment(combo, blockCount, targetRows, blockRow, rowUsed)) continue;
141+
computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths);
142+
const score = scoreAssignment(targetRows, rowWidths, row1MaxWidth, maxRowWidth);
143143
if (score > bestScore) {
144144
bestScore = score;
145145
bestAssignment = blockRow.slice(0, blockCount);

0 commit comments

Comments
 (0)