Welcome to StructuredDiff! In this project, we are building a tool to make code differences ("diffs") look beautiful and readable directly in your terminal.
Have you ever looked at a standard git diff? It often looks like a wall of text with simple + and - signs.
- const oldVal = 1;
+ const newVal = 2;
While functional, itβs hard to scan quickly. Modern code editors (like VS Code) and websites (like GitHub) use colors, line numbers, and distinct background bars to make changes obvious.
Our Goal: Bring that rich, visual experience into the command line.
To build a complex User Interface (UI) in the terminal, we use React.
You might be thinking: "Isn't React for websites?"
Usually, yes. But we use a library called ink. It takes React components (like <Box> and <Text>) and translates them into characters and colors that your terminal understands.
This chapter explains how we render the visual layer of our diff tool.
Our rendering logic has one main job: Translate abstract data into a grid of colored characters.
Here is the visual anatomy of a single line in our UI:
+, -).
Let's look at the entry point in Fallback.tsx. This component receives the patch data and decides how to draw it.
// Inside StructuredDiffFallback function
export function StructuredDiffFallback({ patch, dim, width }) {
// ... (calculation logic) ...
// Render a vertical column of boxes
return (
<Box flexDirection="column" flexGrow={1}>
{diff.map((node, i) => (
<Box key={i}>{node}</Box>
))}
</Box>
);
}
Explanation:
Box: Think of this like a <div> in HTML. flexDirection="column" stacks lines on top of each other.diff.map: We loop through every processed line of the diff and render it.Before we look at the detailed rendering code, let's visualize the flow of data when this component renders.
The core rendering happens in a function called formatDiff. It takes the raw lines and turns them into visual elements.
Here is a simplified view of how we handle a standard line of code:
// Inside formatDiff function loop
const sigil = type === 'add' ? '+' : type === 'remove' ? '-' : ' ';
const bgColor = type === 'add' ? 'diffAdded' : 'diffRemoved';
return (
<Box flexDirection="row">
{/* Left Side: Gutter */}
<Text backgroundColor={bgColor}>
{lineNumStr} {sigil}
</Text>
{/* Right Side: Code */}
<Text backgroundColor={bgColor}>
{line}
</Text>
</Box>
);
Explanation:
sigil (the symbol) based on whether the line was added, removed, or unchanged.bgColor (background color). We use theme names like 'diffAdded' (usually green) or 'diffRemoved' (usually red).row Box so they sit side-by-side.Terminals don't naturally have "background-color" properties that stretch to the edge of the window. We have to fake it using padding.
If your terminal is 80 characters wide, and your code is 20 characters long, we need to add 60 spaces of background color to fill the row.
// Calculating padding to fill the screen
const contentWidth = lineNumStr.length + 1 + stringWidth(line);
const padding = Math.max(0, width - contentWidth);
// Rendering the padding
<Text backgroundColor={bgColor}>
{line}
{' '.repeat(padding)} {/* <--- The Magic Padding */}
</Text>
Explanation:
width: The total width of the terminal window.contentWidth: How much space our text actually takes.' '.repeat(padding): We append empty spaces to the end of the line. Because these spaces share the backgroundColor, it creates a solid colored bar.
What happens if a line of code is longer than the terminal window? If we don't handle it, the visual layout breaks. We use a helper called wrapText.
// Calculate space available for code (Total width - Gutter width)
const availableContentWidth = Math.max(1, safeWidth - maxWidth - 3);
// Force text to wrap nicely
const wrappedText = wrapText(code, availableContentWidth, 'wrap');
const wrappedLines = wrappedText.split('\n');
Explanation:
To help the user focus, we often "dim" the lines that haven't changed, making the colorful changes pop out more.
<Text
dimColor={dim || type === 'nochange'}
backgroundColor={bgColor}
>
{/* Content */}
</Text>
Explanation:
dimColor: An Ink prop that makes the text gray or faint.dim setting is on, or automatically if the line type is nochange.In this chapter, we built the Presentation Layer. You learned:
<Box>) to align the gutter and code.
However, our rendering code assumed we already knew which lines were add, remove, or nochange. How does raw text get transformed into that structured data?
In the next chapter, we will look at the data structure that powers this view.
Generated by Code IQ