bridges-cli 0.1.0 → 0.2.0

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.
@@ -30,12 +30,18 @@ type HashiGridProps = {
30
30
  hasSolution?: boolean
31
31
  /** Whether to show the solution */
32
32
  showSolution?: boolean
33
+ /** Whether to enable the solution feature */
34
+ enableSolutions?: boolean
33
35
  /** Current selection state for highlighting */
34
36
  selectionState?: SelectionState
35
37
  /** Minimum number value in the puzzle */
36
38
  minNumber?: number
37
39
  /** Maximum number value in the puzzle */
38
40
  maxNumber?: number
41
+ /** Whether the puzzle solution has been reached */
42
+ solutionReached?: boolean
43
+ /** Whether the grid is not fully connected but all nodes are filled */
44
+ gridNotConnected?: boolean
39
45
  }
40
46
 
41
47
  export default function HashiGrid({
@@ -47,9 +53,12 @@ export default function HashiGrid({
47
53
  isCustomPuzzle = false,
48
54
  hasSolution = false,
49
55
  showSolution = false,
56
+ enableSolutions = false,
50
57
  selectionState,
51
58
  minNumber,
52
59
  maxNumber,
60
+ solutionReached = false,
61
+ gridNotConnected = false,
53
62
  }: HashiGridProps) {
54
63
  validateGrid({ rows, numNodes })
55
64
 
@@ -85,11 +94,18 @@ export default function HashiGrid({
85
94
  rowIndex={i}
86
95
  highlightedNode={selectionState?.selectedNumber ?? undefined}
87
96
  selectionState={selectionState}
97
+ showSolution={showSolution}
88
98
  />
89
99
  ))}
90
100
  </Box>
91
101
  {showInstructions ? (
92
- <Messages hasSolution={hasSolution} selectionState={selectionState} />
102
+ <Messages
103
+ hasSolution={hasSolution}
104
+ enableSolutions={enableSolutions}
105
+ selectionState={selectionState}
106
+ solutionReached={solutionReached}
107
+ gridNotConnected={gridNotConnected}
108
+ />
93
109
  ) : null}
94
110
  </Box>
95
111
  )
@@ -5,6 +5,7 @@ import type { HashiNodeData, SelectionState } from '../types.ts'
5
5
  import {
6
6
  constructNode,
7
7
  getDisplayMode,
8
+ getNodeFilledState,
8
9
  NODE_WIDTH,
9
10
  OUTER_PADDING,
10
11
  ROW_HEIGHT,
@@ -16,6 +17,7 @@ type HashiRowProps = {
16
17
  highlightedNode?: number
17
18
  rowIndex: number
18
19
  selectionState?: SelectionState
20
+ showSolution?: boolean
19
21
  }
20
22
 
21
23
  export default function HashiRow({
@@ -23,6 +25,7 @@ export default function HashiRow({
23
25
  highlightedNode,
24
26
  rowIndex,
25
27
  selectionState,
28
+ showSolution,
26
29
  }: HashiRowProps) {
27
30
  // Each row consists of multiple lines of terminal output
28
31
  const lines: React.ReactNode[] = []
@@ -60,7 +63,17 @@ export default function HashiRow({
60
63
  selectionState?.mode
61
64
  )
62
65
  const label = disambiguationMap[i]
63
- rowItems.push(constructNode(node, line as 0 | 1 | 2, displayMode, label))
66
+ const filledState = getNodeFilledState(node)
67
+ rowItems.push(
68
+ constructNode(
69
+ node,
70
+ line as 0 | 1 | 2,
71
+ displayMode,
72
+ label,
73
+ filledState,
74
+ showSolution
75
+ )
76
+ )
64
77
 
65
78
  // Add space between columns except the last
66
79
  if (i < nodes.length - 1) {
@@ -8,27 +8,60 @@ type LegendItem = {
8
8
  disabled?: boolean
9
9
  }
10
10
 
11
- export function legendItems(hasSolution: boolean, isSelecting: boolean): LegendItem[] {
12
- return [
11
+ export function legendItems(
12
+ hasSolution: boolean,
13
+ enableSolutions: boolean,
14
+ isSelecting: boolean
15
+ ): LegendItem[] {
16
+ const items: LegendItem[] = [
13
17
  { key: 'p', description: 'Previous puzzle', disabled: isSelecting },
14
18
  { key: 'n', description: 'Next puzzle', disabled: isSelecting },
15
- { key: 's', description: 'Show solution', disabled: isSelecting || !hasSolution },
16
- { key: 'q', description: 'Quit' },
17
19
  ]
20
+
21
+ if (enableSolutions) {
22
+ items.push({
23
+ key: 's',
24
+ description: 'Show solution',
25
+ disabled: isSelecting || !hasSolution,
26
+ })
27
+ }
28
+
29
+ items.push({ key: 'q', description: 'Quit' })
30
+
31
+ return items
18
32
  }
19
33
 
20
34
  type MessagesProps = {
21
35
  hasSolution?: boolean
36
+ enableSolutions?: boolean
22
37
  selectionState?: SelectionState
38
+ solutionReached?: boolean
39
+ gridNotConnected?: boolean
23
40
  }
24
41
 
25
- export default function Messages({ hasSolution = false, selectionState }: MessagesProps) {
42
+ export default function Messages({
43
+ hasSolution = false,
44
+ enableSolutions = false,
45
+ selectionState,
46
+ solutionReached = false,
47
+ gridNotConnected = false,
48
+ }: MessagesProps) {
26
49
  const isSelecting = selectionState !== undefined && selectionState.mode !== 'idle'
27
50
 
28
51
  return (
29
52
  <Box flexDirection="column" marginTop={1}>
53
+ {solutionReached ? (
54
+ <Text bold color="green">
55
+ Solution reached!
56
+ </Text>
57
+ ) : null}
58
+ {gridNotConnected ? (
59
+ <Text bold color="yellow">
60
+ Grid is not fully connected
61
+ </Text>
62
+ ) : null}
30
63
  <Text bold>Controls:</Text>
31
- {legendItems(hasSolution, isSelecting).map(item => (
64
+ {legendItems(hasSolution, enableSolutions, isSelecting).map(item => (
32
65
  <Box key={item.key}>
33
66
  <Text bold color={item.disabled ? 'gray' : undefined}>
34
67
  {item.key}
@@ -22,9 +22,9 @@ describe('HashiRow component', () => {
22
22
  rowIndex={0}
23
23
  />
24
24
  )
25
- expect(lastFrame()).toEqual(` ╭───╮ ╭───╮
26
- │ 1 ├─────┤ 3 │
27
- ╰───╯ ╰───╯`)
25
+ expect(lastFrame()).toEqual(` \x1b[32m╭───╮\x1b[39m ╭───╮
26
+ \x1b[32m│ 1 ├\x1b[39m─────┤ 3 │
27
+ \x1b[32m╰───╯\x1b[39m ╰───╯`)
28
28
  })
29
29
 
30
30
  it('renders a vertical node', () => {
@@ -140,4 +140,29 @@ describe('HashiRow component', () => {
140
140
  )
141
141
  })
142
142
  })
143
+
144
+ describe('validation state colors', () => {
145
+ it('shows green on valid nodes in selected mode', () => {
146
+ const selectionState: SelectionState = {
147
+ mode: 'selected',
148
+ selectedNumber: 1,
149
+ direction: 'h',
150
+ matchingNodes: [{ row: 0, col: 0 }],
151
+ disambiguationLabels: [],
152
+ selectedNode: { row: 0, col: 0 },
153
+ }
154
+ const { lastFrame } = render(
155
+ <HashiRow
156
+ nodes={[{ value: 1, lineRight: 1 }, { value: ' ' }, { value: 1, lineLeft: 1 }]}
157
+ rowIndex={0}
158
+ selectionState={selectionState}
159
+ />
160
+ )
161
+ expect(lastFrame()).toEqual(
162
+ ` \x1b[32m\x1b[1m╭───╮\x1b[22m\x1b[39m \x1b[32m\x1b[2m╭───╮\x1b[22m\x1b[39m
163
+ \x1b[32m\x1b[1m│ 1 ├\x1b[22m\x1b[39m \x1b[32m\x1b[2m┤ 1 │\x1b[22m\x1b[39m
164
+ \x1b[32m\x1b[1m╰───╯\x1b[22m\x1b[39m \x1b[32m\x1b[2m╰───╯\x1b[22m\x1b[39m`
165
+ )
166
+ })
167
+ })
143
168
  })
@@ -4,8 +4,17 @@ import { describe, expect, it } from 'vitest'
4
4
  import Messages, { legendItems } from '../Messages.tsx'
5
5
 
6
6
  describe('Messages', () => {
7
- it('renders the legend', () => {
8
- const { lastFrame } = render(<Messages />)
7
+ it('does not show solution option when enableSolutions is false', () => {
8
+ const { lastFrame } = render(<Messages enableSolutions={false} />)
9
+ expect(lastFrame()).toContain('Controls:')
10
+ expect(lastFrame()).toContain('p: Previous puzzle')
11
+ expect(lastFrame()).toContain('n: Next puzzle')
12
+ expect(lastFrame()).not.toContain('s: Show solution')
13
+ expect(lastFrame()).toContain('q: Quit')
14
+ })
15
+
16
+ it('shows solution option when enableSolutions is true', () => {
17
+ const { lastFrame } = render(<Messages enableSolutions={true} />)
9
18
  expect(lastFrame()).toContain('Controls:')
10
19
  expect(lastFrame()).toContain('p: Previous puzzle')
11
20
  expect(lastFrame()).toContain('n: Next puzzle')
@@ -13,47 +22,61 @@ describe('Messages', () => {
13
22
  expect(lastFrame()).toContain('q: Quit')
14
23
  })
15
24
 
16
- describe('disabled state in selection mode', () => {
17
- it('shows n/p/s as enabled when idle', () => {
18
- const items = legendItems(true, false)
19
- const p = items.find(i => i.key === 'p')
20
- const n = items.find(i => i.key === 'n')
21
- const s = items.find(i => i.key === 's')
22
- expect(p?.disabled).toBe(false)
23
- expect(n?.disabled).toBe(false)
24
- expect(s?.disabled).toBe(false)
25
- })
25
+ describe('legendItems', () => {
26
+ describe('when enableSolutions is false', () => {
27
+ it('shows n/p as enabled when idle', () => {
28
+ const items = legendItems(true, false, false)
29
+ const p = items.find(i => i.key === 'p')
30
+ const n = items.find(i => i.key === 'n')
31
+ const s = items.find(i => i.key === 's')
32
+ expect(p?.disabled).toBe(false)
33
+ expect(n?.disabled).toBe(false)
34
+ expect(s).toBeUndefined()
35
+ })
26
36
 
27
- it('shows n/p/s as disabled in selecting-node mode', () => {
28
- const items = legendItems(true, true)
29
- const p = items.find(i => i.key === 'p')
30
- const n = items.find(i => i.key === 'n')
31
- const s = items.find(i => i.key === 's')
32
- expect(p?.disabled).toBe(true)
33
- expect(n?.disabled).toBe(true)
34
- expect(s?.disabled).toBe(true)
37
+ it('shows n/p as disabled in selecting-node mode', () => {
38
+ const items = legendItems(true, false, true)
39
+ const p = items.find(i => i.key === 'p')
40
+ const n = items.find(i => i.key === 'n')
41
+ const s = items.find(i => i.key === 's')
42
+ expect(p?.disabled).toBe(true)
43
+ expect(n?.disabled).toBe(true)
44
+ expect(s).toBeUndefined()
45
+ })
35
46
  })
36
47
 
37
- it('shows n/p/s as disabled in disambiguation mode', () => {
38
- const items = legendItems(true, true)
39
- const p = items.find(i => i.key === 'p')
40
- const n = items.find(i => i.key === 'n')
41
- const s = items.find(i => i.key === 's')
42
- expect(p?.disabled).toBe(true)
43
- expect(n?.disabled).toBe(true)
44
- expect(s?.disabled).toBe(true)
45
- })
48
+ describe('when enableSolutions is true', () => {
49
+ it('shows n/p/s as enabled when idle', () => {
50
+ const items = legendItems(true, true, false)
51
+ const p = items.find(i => i.key === 'p')
52
+ const n = items.find(i => i.key === 'n')
53
+ const s = items.find(i => i.key === 's')
54
+ expect(p?.disabled).toBe(false)
55
+ expect(n?.disabled).toBe(false)
56
+ expect(s?.disabled).toBe(false)
57
+ })
46
58
 
47
- it('shows s as disabled when no solution exists', () => {
48
- const items = legendItems(false, false)
49
- const s = items.find(i => i.key === 's')
50
- expect(s?.disabled).toBe(true)
51
- })
59
+ it('shows n/p/s as disabled in selecting-node mode', () => {
60
+ const items = legendItems(true, true, true)
61
+ const p = items.find(i => i.key === 'p')
62
+ const n = items.find(i => i.key === 'n')
63
+ const s = items.find(i => i.key === 's')
64
+ expect(p?.disabled).toBe(true)
65
+ expect(n?.disabled).toBe(true)
66
+ expect(s?.disabled).toBe(true)
67
+ })
68
+
69
+ it('shows s as disabled when no solution exists', () => {
70
+ const items = legendItems(false, true, false)
71
+ const s = items.find(i => i.key === 's')
72
+ expect(s?.disabled).toBe(true)
73
+ })
52
74
 
53
- it('shows s as enabled when solution exists', () => {
54
- const items = legendItems(true, false)
55
- const s = items.find(i => i.key === 's')
56
- expect(s?.disabled).toBe(false)
75
+ it('shows s as enabled when solution exists', () => {
76
+ const items = legendItems(true, true, false)
77
+ const s = items.find(i => i.key === 's')
78
+ expect(s?.disabled).toBe(false)
79
+ })
57
80
  })
58
81
  })
59
82
  })
package/src/index.tsx CHANGED
@@ -12,6 +12,7 @@ const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'
12
12
 
13
13
  type CliOptions = {
14
14
  puzzle: string | undefined
15
+ enableSolutions: boolean
15
16
  }
16
17
 
17
18
  const program = new Command()
@@ -38,6 +39,7 @@ program
38
39
  Example (3x3 with corner islands):
39
40
  --puzzle "3x3:1a2.c.1a2"`
40
41
  )
42
+ .option('--enable-solutions', 'Enable the solution viewing feature', false)
41
43
  .parse(process.argv)
42
44
 
43
45
  const options = program.opts<CliOptions>()
@@ -49,4 +51,10 @@ if (options.puzzle) {
49
51
  puzzles = [{ encoding: options.puzzle }, ...samplePuzzles]
50
52
  }
51
53
 
52
- render(<Game puzzles={puzzles} hasCustomPuzzle={hasCustomPuzzle} />)
54
+ render(
55
+ <Game
56
+ puzzles={puzzles}
57
+ hasCustomPuzzle={hasCustomPuzzle}
58
+ enableSolutions={options.enableSolutions}
59
+ />
60
+ )
@@ -1,5 +1,7 @@
1
1
  import type { HashiNodeData, HashiNodeDisplayMode } from '../types.ts'
2
2
 
3
+ export type NodeFilledState = 'valid' | 'invalid' | 'incomplete'
4
+
3
5
  export const ROW_HEIGHT = 3
4
6
  export const NODE_WIDTH = 5
5
7
  export const SPACE_BETWEEN = 0
@@ -50,6 +52,170 @@ export function validateGrid({ rows, numNodes }: HashiGridValidationProps): void
50
52
  }
51
53
  }
52
54
 
55
+ /**
56
+ * Check if the puzzle is solved - all numbered nodes have the correct number of bridges
57
+ * and all nodes form a connected graph.
58
+ */
59
+ export function checkSolution(rows: HashiNodeData[][]): boolean {
60
+ for (const row of rows) {
61
+ for (const node of row) {
62
+ const state = getNodeFilledState(node)
63
+ if (state !== null && state !== 'valid') {
64
+ return false
65
+ }
66
+ }
67
+ }
68
+
69
+ if (!isGraphConnected(rows)) {
70
+ return false
71
+ }
72
+
73
+ return true
74
+ }
75
+
76
+ /**
77
+ * Check if all numbered nodes have the correct number of bridges (regardless of connectivity).
78
+ */
79
+ export function areAllNodesFilled(rows: HashiNodeData[][]): boolean {
80
+ for (const row of rows) {
81
+ for (const node of row) {
82
+ const state = getNodeFilledState(node)
83
+ if (state !== null && state !== 'valid') {
84
+ return false
85
+ }
86
+ }
87
+ }
88
+ return true
89
+ }
90
+
91
+ /**
92
+ * Check if the graph is connected (regardless of whether nodes have correct bridge counts).
93
+ */
94
+ export function isConnected(rows: HashiNodeData[][]): boolean {
95
+ return isGraphConnected(rows)
96
+ }
97
+
98
+ /**
99
+ * Check if all numbered nodes form a connected graph using BFS.
100
+ */
101
+ function isGraphConnected(rows: HashiNodeData[][]): boolean {
102
+ const numRows = rows.length
103
+ if (numRows === 0) return true
104
+ const numCols = rows[0].length
105
+
106
+ const numberedNodes: [number, number][] = []
107
+ for (let r = 0; r < numRows; r++) {
108
+ for (let c = 0; c < numCols; c++) {
109
+ if (typeof rows[r][c].value === 'number') {
110
+ numberedNodes.push([r, c])
111
+ }
112
+ }
113
+ }
114
+
115
+ if (numberedNodes.length === 0) return true
116
+
117
+ const visited = new Set<string>()
118
+ const queue: [number, number][] = [[numberedNodes[0][0], numberedNodes[0][1]]]
119
+ visited.add(`${numberedNodes[0][0]},${numberedNodes[0][1]}`)
120
+
121
+ while (queue.length > 0) {
122
+ const next = queue.shift()
123
+ if (!next) continue
124
+ const [r, c] = next
125
+ const node = rows[r]?.[c]
126
+ if (!node) continue
127
+
128
+ if (node.lineRight === 1 || node.lineRight === 2) {
129
+ const dest = findNodeInDirection(rows, r, c, 0, 1)
130
+ if (dest) {
131
+ const key = `${dest[0]},${dest[1]}`
132
+ if (!visited.has(key)) {
133
+ visited.add(key)
134
+ queue.push(dest)
135
+ }
136
+ }
137
+ }
138
+
139
+ if (node.lineLeft === 1 || node.lineLeft === 2) {
140
+ const dest = findNodeInDirection(rows, r, c, 0, -1)
141
+ if (dest) {
142
+ const key = `${dest[0]},${dest[1]}`
143
+ if (!visited.has(key)) {
144
+ visited.add(key)
145
+ queue.push(dest)
146
+ }
147
+ }
148
+ }
149
+
150
+ if (node.lineDown === 1 || node.lineDown === 2) {
151
+ const dest = findNodeInDirection(rows, r, c, 1, 0)
152
+ if (dest) {
153
+ const key = `${dest[0]},${dest[1]}`
154
+ if (!visited.has(key)) {
155
+ visited.add(key)
156
+ queue.push(dest)
157
+ }
158
+ }
159
+ }
160
+
161
+ if (node.lineUp === 1 || node.lineUp === 2) {
162
+ const dest = findNodeInDirection(rows, r, c, -1, 0)
163
+ if (dest) {
164
+ const key = `${dest[0]},${dest[1]}`
165
+ if (!visited.has(key)) {
166
+ visited.add(key)
167
+ queue.push(dest)
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return visited.size === numberedNodes.length
174
+ }
175
+
176
+ function findNodeInDirection(
177
+ rows: HashiNodeData[][],
178
+ startR: number,
179
+ startC: number,
180
+ dRow: number,
181
+ dCol: number
182
+ ): [number, number] | null {
183
+ const numRows = rows.length
184
+ const numCols = rows[0].length
185
+ let r = startR + dRow
186
+ let c = startC + dCol
187
+
188
+ while (r >= 0 && r < numRows && c >= 0 && c < numCols) {
189
+ if (typeof rows[r][c].value === 'number') {
190
+ return [r, c]
191
+ }
192
+ r += dRow
193
+ c += dCol
194
+ }
195
+
196
+ return null
197
+ }
198
+
199
+ /**
200
+ * Determines if a node has the correct number of bridges, too many, or too few.
201
+ */
202
+ export function getNodeFilledState(node: HashiNodeData): NodeFilledState | null {
203
+ if (typeof node.value !== 'number') {
204
+ return null
205
+ }
206
+
207
+ const bridges =
208
+ (node.lineUp ?? 0) + (node.lineDown ?? 0) + (node.lineLeft ?? 0) + (node.lineRight ?? 0)
209
+
210
+ if (bridges === node.value) {
211
+ return 'valid'
212
+ }
213
+ if (bridges > node.value) {
214
+ return 'invalid'
215
+ }
216
+ return 'incomplete'
217
+ }
218
+
53
219
  /**
54
220
  * Determines the display mode for a node based on the highlighted node value.
55
221
  */
@@ -96,14 +262,35 @@ export function constructNode(
96
262
  node: HashiNodeData,
97
263
  line: 0 | 1 | 2,
98
264
  displayMode: HashiNodeDisplayMode = 'normal',
99
- disambiguationLabel?: string
265
+ disambiguationLabel?: string,
266
+ validationState?: NodeFilledState | null,
267
+ showSolution?: boolean
100
268
  ): string {
269
+ // Determine color prefix based on validation state
270
+ const getColorPrefix = (): string => {
271
+ if (validationState === 'valid') {
272
+ return '\x1b[32m' // green
273
+ }
274
+ if (validationState === 'invalid') {
275
+ return '\x1b[31m' // red
276
+ }
277
+ return ''
278
+ }
279
+
280
+ const colorReset = '\x1b[39m'
281
+ const colorPrefix = displayMode === 'dim' ? '' : getColorPrefix()
282
+ const useColor = colorPrefix !== ''
283
+
101
284
  // Horizontal line
102
285
  if (node.value === '-') {
103
286
  if (displayMode === 'dim') {
104
287
  return line === MIDDLE_ROW ? `\x1b[2m─────\x1b[22m` : ' '.repeat(NODE_WIDTH)
105
288
  }
106
- return line === MIDDLE_ROW ? '─────' : ' '.repeat(NODE_WIDTH)
289
+ const content = line === MIDDLE_ROW ? '─────' : ' '.repeat(NODE_WIDTH)
290
+ if (showSolution) {
291
+ return `\x1b[32m${content}\x1b[39m`
292
+ }
293
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
107
294
  }
108
295
 
109
296
  // Double horizontal line
@@ -111,7 +298,11 @@ export function constructNode(
111
298
  if (displayMode === 'dim') {
112
299
  return line === MIDDLE_ROW ? `\x1b[2m═════\x1b[22m` : ' '.repeat(NODE_WIDTH)
113
300
  }
114
- return line === MIDDLE_ROW ? '═════' : ' '.repeat(NODE_WIDTH)
301
+ const content = line === MIDDLE_ROW ? '═════' : ' '.repeat(NODE_WIDTH)
302
+ if (showSolution) {
303
+ return `\x1b[32m${content}\x1b[39m`
304
+ }
305
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
115
306
  }
116
307
 
117
308
  // Vertical line
@@ -119,7 +310,11 @@ export function constructNode(
119
310
  if (displayMode === 'dim') {
120
311
  return `\x1b[2m │ \x1b[22m`
121
312
  }
122
- return ' │ '
313
+ const content = ' │ '
314
+ if (showSolution) {
315
+ return `\x1b[32m${content}\x1b[39m`
316
+ }
317
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
123
318
  }
124
319
 
125
320
  // Double vertical line
@@ -127,7 +322,11 @@ export function constructNode(
127
322
  if (displayMode === 'dim') {
128
323
  return `\x1b[2m ║ \x1b[22m`
129
324
  }
130
- return ' ║ '
325
+ const content = ' ║ '
326
+ if (showSolution) {
327
+ return `\x1b[32m${content}\x1b[39m`
328
+ }
329
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
131
330
  }
132
331
 
133
332
  // Node with value to render
@@ -137,32 +336,57 @@ export function constructNode(
137
336
  const label = disambiguationLabel ? disambiguationLabel : '─'
138
337
  const border = `╭${label}${up}─╮`
139
338
  if (displayMode === 'highlight') {
140
- return `\x1b[1m${border}\x1b[22m`
339
+ const highlighted = `\x1b[1m${border}\x1b[22m`
340
+ if (validationState === 'valid' || validationState === 'invalid') {
341
+ return `${getColorPrefix()}${highlighted}${colorReset}`
342
+ }
343
+ return highlighted
141
344
  }
142
345
  if (displayMode === 'dim') {
143
- return `\x1b[2m${border}\x1b[22m`
346
+ const dimmedBorder = `\x1b[2m${border}\x1b[22m`
347
+ if (validationState === 'valid' || validationState === 'invalid') {
348
+ return `${getColorPrefix()}${dimmedBorder}${colorReset}`
349
+ }
350
+ return dimmedBorder
144
351
  }
145
- return border
352
+ return useColor ? `${getColorPrefix()}${border}${colorReset}` : border
146
353
  } else if (line === MIDDLE_ROW) {
147
354
  const left = node.lineLeft === 2 ? '╡' : node.lineLeft === 1 ? '┤' : '│'
148
355
  const right = node.lineRight === 2 ? '╞' : node.lineRight === 1 ? '├' : '│'
356
+ const content = `${left} ${node.value} ${right}`
149
357
  if (displayMode === 'highlight') {
150
- return `\x1b[1m${left} ${node.value} ${right}\x1b[22m`
358
+ const highlighted = `\x1b[1m${content}\x1b[22m`
359
+ if (validationState === 'valid' || validationState === 'invalid') {
360
+ return `${getColorPrefix()}${highlighted}${colorReset}`
361
+ }
362
+ return highlighted
151
363
  }
152
364
  if (displayMode === 'dim') {
153
- return `\x1b[2m${left} ${node.value} ${right}\x1b[22m`
365
+ const dimmedContent = `\x1b[2m${content}\x1b[22m`
366
+ if (validationState === 'valid' || validationState === 'invalid') {
367
+ return `${getColorPrefix()}${dimmedContent}${colorReset}`
368
+ }
369
+ return dimmedContent
154
370
  }
155
- return `${left} ${node.value} ${right}`
371
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
156
372
  } else if (line === BOTTOM_ROW) {
157
373
  const down = node.lineDown === 2 ? '╥' : node.lineDown === 1 ? '┬' : '─'
158
374
  const border = `╰─${down}─╯`
159
375
  if (displayMode === 'highlight') {
160
- return `\x1b[1m${border}\x1b[22m`
376
+ const highlighted = `\x1b[1m${border}\x1b[22m`
377
+ if (validationState === 'valid' || validationState === 'invalid') {
378
+ return `${getColorPrefix()}${highlighted}${colorReset}`
379
+ }
380
+ return highlighted
161
381
  }
162
382
  if (displayMode === 'dim') {
163
- return `\x1b[2m${border}\x1b[22m`
383
+ const dimmedBorder = `\x1b[2m${border}\x1b[22m`
384
+ if (validationState === 'valid' || validationState === 'invalid') {
385
+ return `${getColorPrefix()}${dimmedBorder}${colorReset}`
386
+ }
387
+ return dimmedBorder
164
388
  }
165
- return border
389
+ return useColor ? `${getColorPrefix()}${border}${colorReset}` : border
166
390
  }
167
391
  }
168
392