Skip to content

Commit 6ff0acf

Browse files
feat: refactor navigation to use visual rows
1 parent 5177db8 commit 6ff0acf

File tree

3 files changed

+98
-37
lines changed

3 files changed

+98
-37
lines changed

src/DmuxApp.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ import UpdatingIndicator from "./components/indicators/UpdatingIndicator.js"
6060
import FooterHelp from "./components/ui/FooterHelp.js"
6161
import TmuxHooksPromptDialog from "./components/dialogs/TmuxHooksPromptDialog.js"
6262
import { PaneEventService } from "./services/PaneEventService.js"
63-
import { buildProjectActionLayout } from "./utils/projectActions.js"
63+
import {
64+
buildProjectActionLayout,
65+
buildVisualNavigationRows,
66+
} from "./utils/projectActions.js"
6467

6568
const DmuxApp: React.FC<DmuxAppProps> = ({
6669
panesFile,
@@ -409,12 +412,17 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
409412
() => buildProjectActionLayout(panes, sessionProjectRoot, projectName),
410413
[panes, sessionProjectRoot, projectName]
411414
)
415+
const navigationRows = useMemo(
416+
() => isLoading
417+
? projectActionLayout.groups.flatMap((group) =>
418+
group.panes.map((entry) => [entry.index])
419+
)
420+
: buildVisualNavigationRows(projectActionLayout),
421+
[isLoading, projectActionLayout]
422+
)
412423

413424
// Navigation logic moved to hook
414-
const { getCardGridPosition, findCardInDirection } = useNavigation(
415-
terminalWidth,
416-
isLoading ? panes.length : projectActionLayout.totalItems
417-
)
425+
const { getCardGridPosition, findCardInDirection } = useNavigation(navigationRows)
418426

419427
// findCardInDirection provided by useNavigation
420428

@@ -892,9 +900,8 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
892900
toastQueuePosition={toastQueuePosition}
893901
gridInfo={(() => {
894902
if (!process.env.DEBUG_DMUX) return undefined
895-
const cols = Math.max(1, Math.floor(terminalWidth / 37))
896-
const totalCards = isLoading ? panes.length : projectActionLayout.totalItems
897-
const rows = Math.ceil(totalCards / cols)
903+
const rows = navigationRows.length
904+
const cols = Math.max(1, ...navigationRows.map((row) => row.length))
898905
const pos = getCardGridPosition(selectedIndex)
899906
return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`
900907
})()}

src/hooks/useNavigation.ts

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,59 @@
11
import { useMemo } from 'react';
22

3-
export default function useNavigation(terminalWidth: number, totalItems: number) {
4-
const cardWidth = 35 + 2; // Card width + gap
5-
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
3+
export default function useNavigation(navigationRows: number[][]) {
4+
const indexToPosition = useMemo(() => {
5+
const map = new Map<number, { row: number; col: number }>();
6+
navigationRows.forEach((rowItems, row) => {
7+
rowItems.forEach((itemIndex, col) => {
8+
map.set(itemIndex, { row, col });
9+
});
10+
});
11+
return map;
12+
}, [navigationRows]);
613

714
const getCardGridPosition = useMemo(() => {
815
return (index: number): { row: number; col: number } => {
9-
const row = Math.floor(index / cardsPerRow);
10-
const col = index % cardsPerRow;
11-
return { row, col };
16+
return indexToPosition.get(index) || { row: 0, col: 0 };
1217
};
13-
}, [cardsPerRow]);
18+
}, [indexToPosition]);
1419

1520
const findCardInDirection = useMemo(() => {
1621
return (currentIndex: number, direction: 'up' | 'down' | 'left' | 'right'): number | null => {
17-
const currentPos = getCardGridPosition(currentIndex);
18-
let targetIndex: number | null = null;
22+
const currentPos = indexToPosition.get(currentIndex);
23+
if (!currentPos) return null;
1924

2025
switch (direction) {
2126
case 'up':
22-
if (currentPos.row > 0) {
23-
targetIndex = (currentPos.row - 1) * cardsPerRow + currentPos.col;
24-
if (targetIndex >= totalItems) {
25-
targetIndex = Math.min((currentPos.row - 1) * cardsPerRow + cardsPerRow - 1, totalItems - 1);
26-
}
27+
if (currentPos.row > 0 && navigationRows[currentPos.row - 1]) {
28+
const targetRow = navigationRows[currentPos.row - 1];
29+
const targetCol = Math.min(currentPos.col, targetRow.length - 1);
30+
return targetRow[targetCol] ?? null;
2731
}
2832
break;
2933
case 'down':
30-
targetIndex = (currentPos.row + 1) * cardsPerRow + currentPos.col;
31-
if (targetIndex >= totalItems) {
32-
if (currentIndex < totalItems - 1) targetIndex = totalItems - 1; else targetIndex = null;
34+
if (currentPos.row < navigationRows.length - 1 && navigationRows[currentPos.row + 1]) {
35+
const targetRow = navigationRows[currentPos.row + 1];
36+
const targetCol = Math.min(currentPos.col, targetRow.length - 1);
37+
return targetRow[targetCol] ?? null;
3338
}
3439
break;
3540
case 'left':
3641
if (currentPos.col > 0) {
37-
targetIndex = currentIndex - 1;
38-
} else if (currentPos.row > 0) {
39-
targetIndex = currentPos.row * cardsPerRow - 1;
40-
if (targetIndex >= totalItems) targetIndex = totalItems - 1;
42+
const row = navigationRows[currentPos.row];
43+
return row?.[currentPos.col - 1] ?? null;
4144
}
4245
break;
4346
case 'right':
44-
if (currentPos.col < cardsPerRow - 1 && currentIndex < totalItems - 1) {
45-
targetIndex = currentIndex + 1;
46-
} else if ((currentPos.row + 1) * cardsPerRow < totalItems) {
47-
targetIndex = (currentPos.row + 1) * cardsPerRow;
47+
if (navigationRows[currentPos.row] && currentPos.col < navigationRows[currentPos.row].length - 1) {
48+
const row = navigationRows[currentPos.row];
49+
return row?.[currentPos.col + 1] ?? null;
4850
}
4951
break;
5052
}
5153

52-
if (targetIndex !== null && targetIndex >= 0 && targetIndex < totalItems) {
53-
return targetIndex;
54-
}
5554
return null;
5655
};
57-
}, [cardsPerRow, totalItems, getCardGridPosition]);
56+
}, [indexToPosition, navigationRows]);
5857

5958
return { getCardGridPosition, findCardInDirection };
6059
}

src/utils/projectActions.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,58 @@ export function getProjectActionByIndex(
9494
): ProjectActionItem | undefined {
9595
return actionItems.find((item) => item.index === index);
9696
}
97+
98+
/**
99+
* Build visual navigation rows in rendered order.
100+
*
101+
* Each inner array represents one visible row of selectable cards/buttons.
102+
* This is the canonical source for arrow-key navigation.
103+
*/
104+
export function buildVisualNavigationRows(
105+
layout: ProjectActionLayout
106+
): number[][] {
107+
const rows: number[][] = [];
108+
const actionByProject = new Map<
109+
string,
110+
{ newAgent?: ProjectActionItem; terminal?: ProjectActionItem }
111+
>();
112+
113+
for (const action of layout.actionItems) {
114+
const entry = actionByProject.get(action.projectRoot) || {};
115+
if (action.kind === 'new-agent') {
116+
entry.newAgent = action;
117+
} else {
118+
entry.terminal = action;
119+
}
120+
actionByProject.set(action.projectRoot, entry);
121+
}
122+
123+
if (!layout.multiProjectMode) {
124+
for (const group of layout.groups) {
125+
for (const entry of group.panes) {
126+
rows.push([entry.index]);
127+
}
128+
}
129+
130+
const first = layout.actionItems[0];
131+
const second = layout.actionItems[1];
132+
if (first && second) {
133+
rows.push([first.index, second.index]);
134+
}
135+
136+
return rows;
137+
}
138+
139+
for (const group of layout.groups) {
140+
for (const entry of group.panes) {
141+
rows.push([entry.index]);
142+
}
143+
144+
const actions = actionByProject.get(group.projectRoot);
145+
if (actions?.newAgent && actions.terminal) {
146+
rows.push([actions.newAgent.index, actions.terminal.index]);
147+
}
148+
}
149+
150+
return rows;
151+
}

0 commit comments

Comments
 (0)