Feature description
often have multiple widgets open (several terminals plus a browser) and I rely heavily on keyboard navigation. I’d like built‑in shortcuts to move focus between widgets (e.g., “next/previous widget” and maybe direct focus like “widget 1/2/3”).
Important details:
– I frequently change layouts (full screen, tiled, different monitors), so hacks based on fixed/relative screen coordinates aren’t reliable.
– I’m specifically looking for a layout‑independent, app‑native solution (like `Cmd+`` to cycle windows in one app), not global automation tools.
This would make Wave much more usable for keyboard‑driven workflows.
Implementation Suggestion
I don’t know Wave’s actual codebase, so I can’t give drop‑in code, but here’s a concrete, framework‑agnostic sketch that should be easy to adapt if you are using React + a central store (Redux/Zustand, etc.) and an Electron/desktop shell.
Data model (central store)
ts
// types
type WidgetId = string;
interface Widget {
id: WidgetId;
title: string;
// type: "terminal" | "browser" | "docs" | ...
// layout info, etc.
}
interface WidgetsState {
orderedIds: WidgetId[]; // visual order left‑to‑right / top‑to‑bottom
focusedId: WidgetId | null; // currently focused widget
mruStack: WidgetId[]; // [mostRecent, nextMostRecent, ...]
}
Reducers / actions
ts
// action creators
const focusWidget = (id: WidgetId) => ({ type: "FOCUS_WIDGET", id });
const focusNextWidget = () => ({ type: "FOCUS_NEXT_WIDGET" });
const focusPrevWidget = () => ({ type: "FOCUS_PREV_WIDGET" });
const focusLastWidget = () => ({ type: "FOCUS_LAST_WIDGET" }); // MRU toggle
const setWidgetOrder = (orderedIds: WidgetId[]) => ({
type: "SET_WIDGET_ORDER",
orderedIds,
});
// helpers
function updateMruStack(mru: WidgetId[], id: WidgetId): WidgetId[] {
// remove id if already present, then unshift
const filtered = mru.filter(wid => wid !== id);
filtered.unshift(id);
// optional: cap length
return filtered.slice(0, 16);
}
function focusByOffset(state: WidgetsState, delta: number): WidgetsState {
const { orderedIds, focusedId } = state;
if (orderedIds.length === 0) return state;
const currentIndex = focusedId
? orderedIds.indexOf(focusedId)
: 0;
const idx = ((currentIndex + delta) % orderedIds.length + orderedIds.length)
% orderedIds.length;
const nextId = orderedIds[idx];
return {
...state,
focusedId: nextId,
mruStack: updateMruStack(state.mruStack, nextId),
};
}
// reducer
function widgetsReducer(state: WidgetsState, action: any): WidgetsState {
switch (action.type) {
case "FOCUS_WIDGET": {
if (!state.orderedIds.includes(action.id)) return state;
return {
...state,
focusedId: action.id,
mruStack: updateMruStack(state.mruStack, action.id),
};
}
case "FOCUS_NEXT_WIDGET":
return focusByOffset(state, +1);
case "FOCUS_PREV_WIDGET":
return focusByOffset(state, -1);
case "FOCUS_LAST_WIDGET": {
const [current, last] = state.mruStack;
if (!last || last === current) return state;
return {
...state,
focusedId: last,
mruStack: updateMruStack(state.mruStack, last),
};
}
case "SET_WIDGET_ORDER": {
const orderedIds: WidgetId[] = action.orderedIds;
// keep focusedId if still present; otherwise pick first
const focusedId =
state.focusedId && orderedIds.includes(state.focusedId)
? state.focusedId
: orderedIds[0] ?? null;
// filter MRU to only still‑present ids
const mruStack = state.mruStack.filter(id =>
orderedIds.includes(id)
);
return {
...state,
orderedIds,
focusedId,
mruStack: focusedId
? updateMruStack(mruStack, focusedId)
: mruStack,
};
}
default:
return state;
}
}
Keyboard bindings (app shell)
At the Electron/window level, hook global shortcuts while the Wave window is focused:
ts
// pseudo‑code; adapt to actual key handling (Electron, React hotkeys, etc.)
registerShortcut("Ctrl+Tab", () => {
dispatch(focusNextWidget());
});
registerShortcut("Ctrl+Shift+Tab", () => {
dispatch(focusPrevWidget());
});
registerShortcut("Ctrl+`", () => {
dispatch(focusLastWidget()); // MRU toggle
});
Applying focus in the UI
Each widget container subscribes to focusedId and sets DOM focus when it becomes active:
tsx
function WidgetContainer({ widget }: { widget: Widget }) {
const focusedId = useSelector(s => s.widgets.focusedId);
const dispatch = useDispatch();
const ref = useRef<HTMLDivElement | null>(null);
const isFocused = focusedId === widget.id;
useEffect(() => {
if (isFocused && ref.current) {
ref.current.focus(); // or focus inner terminal/browser view
}
}, [isFocused]);
return (
<div
ref={ref}
tabIndex={0}
onMouseDown={() => dispatch(focusWidget(widget.id))}
className={isFocused ? "widget focused" : "widget"}
>
{/* widget content */}
);
}
Maintaining orderedIds
Wherever the layout engine already computes the visual ordering of widgets (e.g., when you:
create/close a widget
split/merge panes
drag‑reorder),
make sure it calls setWidgetOrder(newOrderedIds) with the current visible order (left‑to‑right / top‑to‑bottom). No coordinate hacks are needed; this is purely internal, layout‑aware ordering.
Behavior this gives:
Ctrl+Tab / Ctrl+Shift+Tab: cycle focus through widgets in the current layout order (independent of window size/monitor).
`Ctrl+``: toggle between the last two focused widgets (MRU stack).
Mouse clicks still move focus and feed the MRU stack.
Layout changes keep the current focus if possible; otherwise fall back gracefully.
You can extend the same store to add “focus widget N” (Alt+1…9) by indexing into orderedIds[N-1] and dispatching focusWidget.
Anything else?
No response
Feature description
often have multiple widgets open (several terminals plus a browser) and I rely heavily on keyboard navigation. I’d like built‑in shortcuts to move focus between widgets (e.g., “next/previous widget” and maybe direct focus like “widget 1/2/3”).
Important details:
– I frequently change layouts (full screen, tiled, different monitors), so hacks based on fixed/relative screen coordinates aren’t reliable.
– I’m specifically looking for a layout‑independent, app‑native solution (like `Cmd+`` to cycle windows in one app), not global automation tools.
This would make Wave much more usable for keyboard‑driven workflows.
Implementation Suggestion
I don’t know Wave’s actual codebase, so I can’t give drop‑in code, but here’s a concrete, framework‑agnostic sketch that should be easy to adapt if you are using React + a central store (Redux/Zustand, etc.) and an Electron/desktop shell.
Data model (central store)
ts
// types
type WidgetId = string;
interface Widget {
id: WidgetId;
title: string;
// type: "terminal" | "browser" | "docs" | ...
// layout info, etc.
}
interface WidgetsState {
orderedIds: WidgetId[]; // visual order left‑to‑right / top‑to‑bottom
focusedId: WidgetId | null; // currently focused widget
mruStack: WidgetId[]; // [mostRecent, nextMostRecent, ...]
}
Reducers / actions
ts
// action creators
const focusWidget = (id: WidgetId) => ({ type: "FOCUS_WIDGET", id });
const focusNextWidget = () => ({ type: "FOCUS_NEXT_WIDGET" });
const focusPrevWidget = () => ({ type: "FOCUS_PREV_WIDGET" });
const focusLastWidget = () => ({ type: "FOCUS_LAST_WIDGET" }); // MRU toggle
const setWidgetOrder = (orderedIds: WidgetId[]) => ({
type: "SET_WIDGET_ORDER",
orderedIds,
});
// helpers
function updateMruStack(mru: WidgetId[], id: WidgetId): WidgetId[] {
// remove id if already present, then unshift
const filtered = mru.filter(wid => wid !== id);
filtered.unshift(id);
// optional: cap length
return filtered.slice(0, 16);
}
function focusByOffset(state: WidgetsState, delta: number): WidgetsState {
const { orderedIds, focusedId } = state;
if (orderedIds.length === 0) return state;
const currentIndex = focusedId
? orderedIds.indexOf(focusedId)
: 0;
const idx = ((currentIndex + delta) % orderedIds.length + orderedIds.length)
% orderedIds.length;
const nextId = orderedIds[idx];
return {
...state,
focusedId: nextId,
mruStack: updateMruStack(state.mruStack, nextId),
};
}
// reducer
function widgetsReducer(state: WidgetsState, action: any): WidgetsState {
switch (action.type) {
case "FOCUS_WIDGET": {
if (!state.orderedIds.includes(action.id)) return state;
return {
...state,
focusedId: action.id,
mruStack: updateMruStack(state.mruStack, action.id),
};
}
}
}
Keyboard bindings (app shell)
At the Electron/window level, hook global shortcuts while the Wave window is focused:
ts
// pseudo‑code; adapt to actual key handling (Electron, React hotkeys, etc.)
registerShortcut("Ctrl+Tab", () => {
dispatch(focusNextWidget());
});
registerShortcut("Ctrl+Shift+Tab", () => {
dispatch(focusPrevWidget());
});
registerShortcut("Ctrl+`", () => {
dispatch(focusLastWidget()); // MRU toggle
});
Applying focus in the UI
Each widget container subscribes to focusedId and sets DOM focus when it becomes active:
tsx
function WidgetContainer({ widget }: { widget: Widget }) {
const focusedId = useSelector(s => s.widgets.focusedId);
const dispatch = useDispatch();
const ref = useRef<HTMLDivElement | null>(null);
const isFocused = focusedId === widget.id;
useEffect(() => {
if (isFocused && ref.current) {
ref.current.focus(); // or focus inner terminal/browser view
}
}, [isFocused]);
return (
<div
ref={ref}
tabIndex={0}
onMouseDown={() => dispatch(focusWidget(widget.id))}
className={isFocused ? "widget focused" : "widget"}
>
{/* widget content */}
);
}
Maintaining orderedIds
Wherever the layout engine already computes the visual ordering of widgets (e.g., when you:
create/close a widget
split/merge panes
drag‑reorder),
make sure it calls setWidgetOrder(newOrderedIds) with the current visible order (left‑to‑right / top‑to‑bottom). No coordinate hacks are needed; this is purely internal, layout‑aware ordering.
Behavior this gives:
Ctrl+Tab / Ctrl+Shift+Tab: cycle focus through widgets in the current layout order (independent of window size/monitor).
`Ctrl+``: toggle between the last two focused widgets (MRU stack).
Mouse clicks still move focus and feed the MRU stack.
Layout changes keep the current focus if possible; otherwise fall back gracefully.
You can extend the same store to add “focus widget N” (Alt+1…9) by indexing into orderedIds[N-1] and dispatching focusWidget.
Anything else?
No response