How Claude HUD Validates and Applies Element Order in the Expanded Layout
Claude HUD validates user-defined element sequences through validateElementOrder in src/config.ts and applies them in renderExpanded within src/render/index.ts, ensuring safe, deduplicated ordering with special handling for adjacent context and usage elements.
The jarrodwatts/claude-hud repository provides a customizable heads-up display for Claude, allowing users to rearrange status-line elements via the elementOrder field in ~/.claude/hud/config.json. This article examines how the codebase validates these custom sequences against known elements and applies them when rendering the expanded layout, ensuring type safety and preventing duplicate entries.
Validating the elementOrder Configuration
Before any custom ordering reaches the renderer, it undergoes strict validation to prevent malformed configurations from breaking the display.
The validateElementOrder Function
Located in src/config.ts (lines 53-75), the validateElementOrder function receives the raw value from the user's JSON configuration and enforces several constraints:
- Array validation: The input must be a non-empty array; otherwise, it falls back to
DEFAULT_ELEMENT_ORDER. - Element filtering: Each item must be a string present in the
KNOWN_ELEMENTSset (which equalsDEFAULT_ELEMENT_ORDER). Non-string or unknown values are silently discarded. - Deduplication: A
Settracks seen elements, preserving only the first occurrence of each valid element. - Fallback protection: If filtering results in an empty array, the function returns the default order.
// src/config.ts – validateElementOrder
function validateElementOrder(value: unknown): HudElement[] {
if (!Array.isArray(value) || value.length === 0) {
return [...DEFAULT_ELEMENT_ORDER];
}
const seen = new Set<HudElement>();
const elementOrder: HudElement[] = [];
for (const item of value) {
if (typeof item !== 'string' || !KNOWN_ELEMENTS.has(item as HudElement)) continue;
const element = item as HudElement;
if (seen.has(element)) continue;
seen.add(element);
elementOrder.push(element);
}
return elementOrder.length > 0 ? elementOrder : [...DEFAULT_ELEMENT_ORDER];
}
The mergeConfig function (line 333) invokes this validator when loading user settings, guaranteeing that hudConfig.elementOrder always contains a clean, deduplicated list of known elements.
Applying the Element Order in the Expanded Layout
Once validated, the element order drives the rendering pipeline in the expanded view.
Reading the Sanitized Configuration
In src/render/index.ts (line 62), the renderExpanded function retrieves the finalized order:
const elementOrder = ctx.config?.elementOrder ?? DEFAULT_ELEMENT_ORDER;
This expression ensures that even if configuration loading fails, the renderer has a valid sequence to work with.
Rendering with renderExpanded
The function iterates over elementOrder while maintaining a seen Set to skip any duplicates that might have slipped through. For each element, it calls renderElementLine, which delegates to specific line renderers like renderProjectLine, renderIdentityLine, or renderUsageLine located in src/render/lines/*.ts.
A special optimization occurs when context and usage appear consecutively (lines 73-92). Rather than rendering on separate lines, the code joins them side-by-side with a vertical bar separator:
// src/render/index.ts – renderExpanded
function renderExpanded(ctx: RenderContext): Array<{ line: string; isActivity: boolean }> {
const elementOrder = ctx.config?.elementOrder ?? DEFAULT_ELEMENT_ORDER;
const seen = new Set<HudElement>();
const lines: Array<{ line: string; isActivity: boolean }> = [];
for (let index = 0; index < elementOrder.length; index += 1) {
const element = elementOrder[index];
if (seen.has(element)) continue; // skip duplicates
// special handling for context ↔ usage adjacency
const nextElement = elementOrder[index + 1];
if ((element === 'context' && nextElement === 'usage' && !seen.has('usage')) ||
(element === 'usage' && nextElement === 'context' && !seen.has('context'))) {
seen.add(element);
seen.add(nextElement);
const firstLine = renderElementLine(ctx, element);
const secondLine = renderElementLine(ctx, nextElement);
if (firstLine && secondLine) {
lines.push({ line: `${firstLine} │ ${secondLine}`, isActivity: false });
} else if (firstLine) {
lines.push({ line: firstLine, isActivity: false });
} else if (secondLine) {
lines.push({ line: secondLine, isActivity: false });
}
continue;
}
seen.add(element);
const line = renderElementLine(ctx, element);
if (!line) continue;
lines.push({ line, isActivity: ACTIVITY_ELEMENTS.has(element) });
}
return lines;
}
The output array contains objects with line text and an isActivity boolean flag, which the terminal wrapper uses to apply appropriate styling before printing.
Practical Configuration Examples
Custom Element Sequence
To reorder elements in your ~/.claude/hud/config.json:
{
"elementOrder": ["context", "project", "usage", "todos"]
}
This configuration places the context indicator first, followed by project information, usage statistics, and finally the todos activity line.
Consuming the Configuration
When the renderer loads this configuration:
import { loadConfig } from './config.js';
import { render } from './render/index.js';
const ctx = { config: await loadConfig(), /* …other fields… */ };
render(ctx); // renderExpanded reads ctx.config.elementOrder
Because "context" and "usage" are adjacent in the array, renderExpanded combines them into a single line: Context ████░░░ 45% │ Usage ██░░░ 20% (1h 30m / 5h).
Resulting Terminal Output
[Opus] │ my-project
Context ████░░░ 45% │ Usage ██░░░ 20% (1h 30m / 5h)
▸ Fix auth bug (2/5)
The first line displays the project element, the second shows the combined context and usage block due to their adjacency, and the third renders the todos activity line.
Summary
- Validation occurs in
src/config.ts: ThevalidateElementOrderfunction filters unknown elements, removes duplicates, and falls back to defaults when necessary. - Application happens in
src/render/index.ts: TherenderExpandedfunction respects the validated order while optimizing the display of adjacent context and usage elements. - Safety is enforced at load time: The
mergeConfigfunction ensures only sanitized arrays reach the rendering pipeline. - Special layout rules apply: Consecutive context and usage elements render side-by-side regardless of their relative order in the configuration array.
Frequently Asked Questions
What happens if I include an unknown element in my elementOrder array?
The validateElementOrder function silently filters out any strings not present in the KNOWN_ELEMENTS set. Only valid elements appear in the final configuration, and if this filtering leaves the array empty, the system falls back to DEFAULT_ELEMENT_ORDER.
Can I place activity elements like "todos" before static elements?
Yes. The renderer processes the elementOrder array sequentially without separating activity and non-activity elements. However, the isActivity flag attached to each line allows the terminal wrapper to apply distinct styling to activity indicators regardless of their position in the sequence.
Why do context and usage appear on the same line even when I list them separately?
When renderExpanded detects that context and usage appear consecutively in the element order (in either direction), it joins them with a vertical bar separator (│) to conserve vertical space. This optimization occurs before the standard rendering logic and treats the pair as a single combined line entry.
Where does the default element order originate?
The DEFAULT_ELEMENT_ORDER constant, referenced in src/config.ts and used as the fallback in src/render/index.ts, defines the standard sequence. This constant likely resides in src/constants.ts or a similar configuration file within the repository, establishing the baseline order when users do not provide custom preferences.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →