Skip to content

Commit e962d6e

Browse files
author
shmuel hizmi
committed
master
1 parent e67b7dd commit e962d6e

7 files changed

Lines changed: 156 additions & 89 deletions

File tree

packages/wmux-client/src/components/CommandPalette.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ function CategoryIcon({ icon, color }: { readonly icon?: string | undefined; rea
5454
return <span className="w-2 h-2 rounded-sm shrink-0" style={{ background: color }} />;
5555
}
5656

57+
function selectAndClose(action: () => void, onClose: () => void): void {
58+
action();
59+
onClose();
60+
}
61+
5762
function CategoriesGroup({
5863
categories,
5964
onSelectCategory,
@@ -69,7 +74,7 @@ function CategoriesGroup({
6974
<Command.Item
7075
key={`cat-${category.name}`}
7176
value={`category ${category.name}`}
72-
onSelect={() => { onSelectCategory(category.name); onClose(); }}
77+
onSelect={() => selectAndClose(() => onSelectCategory(category.name), onClose)}
7378
className={ITEM_CLASS}
7479
>
7580
<CategoryIcon icon={category.icon} color={category.color} />
@@ -104,7 +109,7 @@ function ProcessesGroup({
104109
<Command.Item
105110
key={`tab-${tab.id}`}
106111
value={`process ${tab.name} ${category.name} ${tab.description ?? ""}`}
107-
onSelect={() => { onSelectCategory(category.name); onSelectTab(tab.id); onClose(); }}
112+
onSelect={() => selectAndClose(() => { onSelectCategory(category.name); onSelectTab(tab.id); }, onClose)}
108113
className={ITEM_CLASS}
109114
>
110115
<CategoryIcon icon={tab.icon} color={category.color} />
@@ -138,7 +143,7 @@ function FilesGroup({
138143
<Command.Item
139144
key={`file-${file.path}`}
140145
value={`file ${file.name} ${file.path} ${file.category}`}
141-
onSelect={() => { onSelectCategory(file.category); onSelectTab(`file::${file.path}`); onOpenFile(file.path); onClose(); }}
146+
onSelect={() => selectAndClose(() => { onSelectCategory(file.category); onSelectTab(`file::${file.path}`); onOpenFile(file.path); }, onClose)}
142147
className={ITEM_CLASS}
143148
>
144149
<span className="text-muted-foreground/40 text-[12px]">📄</span>
@@ -170,16 +175,16 @@ function ActionsGroup({
170175
return (
171176
<Command.Group heading="Actions" className={GROUP_HEADING_CLASS}>
172177
{!isRunning && (
173-
<Command.Item value="start process" onSelect={() => { onStartProcess(activeTabId); onClose(); }} className={ITEM_CLASS}>
178+
<Command.Item value="start process" onSelect={() => selectAndClose(() => onStartProcess(activeTabId), onClose)} className={ITEM_CLASS}>
174179
<span className="text-success">Start</span><span className="text-muted-foreground/40">{activeTab.name}</span>
175180
</Command.Item>
176181
)}
177182
{isRunning && (
178183
<>
179-
<Command.Item value="restart process" onSelect={() => { onRestartProcess(activeTabId); onClose(); }} className={ITEM_CLASS}>
184+
<Command.Item value="restart process" onSelect={() => selectAndClose(() => onRestartProcess(activeTabId), onClose)} className={ITEM_CLASS}>
180185
<span className="text-warning">Restart</span><span className="text-muted-foreground/40">{activeTab.name}</span>
181186
</Command.Item>
182-
<Command.Item value="stop process" onSelect={() => { onStopProcess(activeTabId); onClose(); }} className={ITEM_CLASS}>
187+
<Command.Item value="stop process" onSelect={() => selectAndClose(() => onStopProcess(activeTabId), onClose)} className={ITEM_CLASS}>
183188
<span className="text-destructive">Stop</span><span className="text-muted-foreground/40">{activeTab.name}</span>
184189
</Command.Item>
185190
</>

packages/wmux-client/src/components/FileViewer.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,34 @@ const EXT_COLORS: Record<string, string> = {
1515
py: "#3572A5", rs: "#dea584", go: "#00ADD8", sh: "#89e051",
1616
};
1717

18-
function extColor(name: string): string {
19-
const ext = name.split(".").pop()?.toLowerCase() ?? "";
20-
return EXT_COLORS[ext] ?? "#71717a";
18+
function extensionColor(name: string): string {
19+
const fileExtension = name.split(".").pop()?.toLowerCase() ?? "";
20+
return EXT_COLORS[fileExtension] ?? "#71717a";
21+
}
22+
23+
function handleEntryClick(entry: FileEntry, onToggleDir: (path: string) => void, onOpenFile: (path: string) => void): void {
24+
if (entry.isDir) { onToggleDir(entry.path); return; }
25+
onOpenFile(entry.path);
26+
}
27+
28+
function DirectoryIcon({ isExpanded }: { readonly isExpanded: boolean }): ReactElement {
29+
return (
30+
<>
31+
<span className="w-3 h-3 flex items-center justify-center shrink-0 text-muted-foreground/30">
32+
{isExpanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
33+
</span>
34+
<Folder size={12} className="shrink-0 text-muted-foreground/40" />
35+
</>
36+
);
37+
}
38+
39+
function FileIcon({ name }: { readonly name: string }): ReactElement {
40+
return (
41+
<>
42+
<span className="w-3 h-3 shrink-0" />
43+
<File size={12} className="shrink-0" style={{ color: extensionColor(name) }} />
44+
</>
45+
);
2146
}
2247

2348
export function FileTree({
@@ -34,23 +59,14 @@ export function FileTree({
3459
{entries.map((entry) => (
3560
<button
3661
key={entry.path}
37-
onClick={() => entry.isDir ? onToggleDir(entry.path) : onOpenFile(entry.path)}
62+
onClick={() => handleEntryClick(entry, onToggleDir, onOpenFile)}
3863
className="flex items-center gap-1.5 py-[3px] pr-2 text-left bg-transparent border-none cursor-pointer text-[12px] leading-tight hover:bg-card/60 transition-colors text-muted-foreground/60 hover:text-foreground/70"
3964
style={{ paddingLeft: `${entry.depth * 12 + 8}px` }}
4065
>
41-
{entry.isDir ? (
42-
<>
43-
<span className="w-3 h-3 flex items-center justify-center shrink-0 text-muted-foreground/30">
44-
{entry.isExpanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
45-
</span>
46-
<Folder size={12} className="shrink-0 text-muted-foreground/40" />
47-
</>
48-
) : (
49-
<>
50-
<span className="w-3 h-3 shrink-0" />
51-
<File size={12} className="shrink-0" style={{ color: extColor(entry.name) }} />
52-
</>
53-
)}
66+
{entry.isDir
67+
? <DirectoryIcon isExpanded={entry.isExpanded} />
68+
: <FileIcon name={entry.name} />
69+
}
5470
<span className="truncate">{entry.name}</span>
5571
</button>
5672
))}

packages/wmux-client/src/components/Sidebar.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ function CategorySection({
143143
}): ReactElement {
144144
const isFiles = category.type === "files";
145145

146+
const handleToggleDir = (path: string): void => {
147+
selectCategoryAndAct(isActive, onSelectCategory, () => onToggleDir(path));
148+
};
149+
150+
const handleOpenFile = (path: string): void => {
151+
selectCategoryAndAct(isActive, onSelectCategory, () => onOpenFile(path));
152+
};
153+
154+
const handleSelectTab = (tabId: string): void => {
155+
selectCategoryAndAct(isActive, onSelectCategory, () => onSelectTab(tabId));
156+
};
157+
146158
return (
147159
<div
148160
className="border-b border-border/10 last:border-b-0 border-l-2"
@@ -165,8 +177,8 @@ function CategorySection({
165177
<div className="max-h-[50vh] overflow-y-auto pb-1">
166178
<FileTree
167179
entries={category.fileEntries}
168-
onToggleDir={(path) => selectCategoryAndAct(isActive, onSelectCategory, () => onToggleDir(path))}
169-
onOpenFile={(path) => selectCategoryAndAct(isActive, onSelectCategory, () => onOpenFile(path))}
180+
onToggleDir={handleToggleDir}
181+
onOpenFile={handleOpenFile}
170182
/>
171183
</div>
172184
)}
@@ -178,7 +190,7 @@ function CategorySection({
178190
key={tab.id}
179191
tab={tab}
180192
isActive={tab.id === activeTabId && isActive}
181-
onSelect={() => selectCategoryAndAct(isActive, onSelectCategory, () => onSelectTab(tab.id))}
193+
onSelect={() => handleSelectTab(tab.id)}
182194
/>
183195
))}
184196
</div>

packages/wmux-client/src/components/TabBar.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,26 @@ function reorderIds(ids: readonly string[], sourceId: string, targetId: string):
8484
return [...withoutSource.slice(0, targetIndex), sourceId, ...withoutSource.slice(targetIndex)];
8585
}
8686

87+
function handleDragEnd(
88+
event: Parameters<NonNullable<Parameters<typeof DragDropProvider>[0]["onDragEnd"]>>[0],
89+
tabs: readonly Tab[],
90+
onReorder: ((ids: string[]) => void) | undefined,
91+
): void {
92+
if (!onReorder || !event.operation.source || !event.operation.target) return;
93+
const sourceId = String(event.operation.source.id);
94+
const targetId = String(event.operation.target.id);
95+
if (sourceId === targetId) return;
96+
onReorder(reorderIds(tabs.map((t) => t.id), sourceId, targetId));
97+
}
98+
8799
export function TabBar({ tabs, activeId, categoryColor, onSelect, onClose, onReorder, processActions }: TabBarProps): ReactElement {
88100
return (
89101
<div
90102
className="flex items-center h-8 border-b border-border/20 shrink-0 overflow-x-auto scrollbar-hide"
91103
style={{ backgroundColor: `color-mix(in srgb, ${categoryColor} 4%, var(--color-background))` }}
92104
>
93105
<DragDropProvider
94-
onDragEnd={(event) => {
95-
if (!onReorder || !event.operation.source || !event.operation.target) return;
96-
const sourceId = String(event.operation.source.id);
97-
const targetId = String(event.operation.target.id);
98-
if (sourceId === targetId) return;
99-
onReorder(reorderIds(tabs.map((t) => t.id), sourceId, targetId));
100-
}}
106+
onDragEnd={(event) => handleDragEnd(event, tabs, onReorder)}
101107
>
102108
{tabs.map((tab, i) => (
103109
<SortableTab

packages/wmux-client/src/components/WmuxApp.tsx

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,19 @@ interface SidebarNavigationItem {
6262
readonly tabId: string;
6363
}
6464

65+
function isNavigableCategory(category: CategoryInfo, collapsedCategories: ReadonlySet<string>): boolean {
66+
return !collapsedCategories.has(category.name) && category.type !== "files";
67+
}
68+
6569
function buildSidebarNavigationItems(
6670
categories: ReadonlyArray<CategoryInfo>,
6771
collapsedCategories: ReadonlySet<string>,
6872
): readonly SidebarNavigationItem[] {
69-
const items: SidebarNavigationItem[] = [];
70-
for (const category of categories) {
71-
if (collapsedCategories.has(category.name) || category.type === "files") continue;
72-
for (const tab of category.tabs) {
73-
items.push({ categoryName: category.name, tabId: tab.id });
74-
}
75-
}
76-
return items;
73+
return categories
74+
.filter((category) => isNavigableCategory(category, collapsedCategories))
75+
.flatMap((category) =>
76+
category.tabs.map((tab) => ({ categoryName: category.name, tabId: tab.id })),
77+
);
7778
}
7879

7980
function buildTabList(activeCategory: CategoryInfo | undefined): Tab[] {
@@ -101,6 +102,69 @@ function toggleSetItem<T>(source: ReadonlySet<T>, item: T): Set<T> {
101102
return new Set([...source, item]);
102103
}
103104

105+
function handleCommandPaletteKey(
106+
event: KeyboardEvent,
107+
isModifierPressed: boolean,
108+
commandPaletteOpen: boolean,
109+
setCommandPaletteOpen: (open: boolean | ((prev: boolean) => boolean)) => void,
110+
): boolean {
111+
if (isModifierPressed && event.key === "k") { event.preventDefault(); setCommandPaletteOpen((prev: boolean) => !prev); return true; }
112+
if (event.key === "Escape" && commandPaletteOpen) { setCommandPaletteOpen(false); return true; }
113+
return false;
114+
}
115+
116+
function handleCategorySwitchKey(
117+
event: KeyboardEvent,
118+
isModifierPressed: boolean,
119+
categories: ReadonlyArray<CategoryInfo>,
120+
selectCategory: (name: string) => void,
121+
): boolean {
122+
if (!isModifierPressed || event.key < "1" || event.key > "9") return false;
123+
event.preventDefault();
124+
const categoryIndex = parseInt(event.key, 10) - 1;
125+
if (categoryIndex < categories.length) selectCategory(categories[categoryIndex]!.name);
126+
return true;
127+
}
128+
129+
function handleTabSwitchKey(
130+
event: KeyboardEvent,
131+
isModifierPressed: boolean,
132+
orderedTabs: readonly Tab[],
133+
activeTabId: string,
134+
selectTab: (id: string) => void,
135+
): boolean {
136+
if (!isModifierPressed || (event.key !== "[" && event.key !== "]")) return false;
137+
event.preventDefault();
138+
if (orderedTabs.length === 0) return true;
139+
const currentIndex = orderedTabs.findIndex((t) => t.id === activeTabId);
140+
const nextIndex = event.key === "]"
141+
? (currentIndex + 1) % orderedTabs.length
142+
: (currentIndex - 1 + orderedTabs.length) % orderedTabs.length;
143+
selectTab(orderedTabs[nextIndex]!.id);
144+
return true;
145+
}
146+
147+
function handleArrowNavigation(
148+
event: KeyboardEvent,
149+
commandPaletteOpen: boolean,
150+
sidebarItems: readonly SidebarNavigationItem[],
151+
activeTabId: string,
152+
activeCategory: string,
153+
selectCategory: (name: string) => void,
154+
selectTab: (id: string) => void,
155+
): boolean {
156+
const isArrowKey = event.key === "ArrowUp" || event.key === "ArrowDown";
157+
if (!isArrowKey || isTerminalFocused() || commandPaletteOpen || sidebarItems.length === 0) return false;
158+
event.preventDefault();
159+
const currentIndex = sidebarItems.findIndex((item) => item.tabId === activeTabId);
160+
const delta = event.key === "ArrowDown" ? 1 : -1;
161+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + delta + sidebarItems.length) % sidebarItems.length;
162+
const nextItem = sidebarItems[nextIndex]!;
163+
if (nextItem.categoryName !== activeCategory) selectCategory(nextItem.categoryName);
164+
selectTab(nextItem.tabId);
165+
return true;
166+
}
167+
104168
function useKeyboardShortcuts({
105169
commandPaletteOpen,
106170
setCommandPaletteOpen,
@@ -125,43 +189,10 @@ function useKeyboardShortcuts({
125189
useEffect(() => {
126190
const handler = (event: KeyboardEvent): void => {
127191
const isModifierPressed = event.metaKey || event.ctrlKey;
128-
129-
if (isModifierPressed && event.key === "k") { event.preventDefault(); setCommandPaletteOpen((prev: boolean) => !prev); return; }
130-
if (event.key === "Escape" && commandPaletteOpen) { setCommandPaletteOpen(false); return; }
131-
132-
if (isModifierPressed && event.key >= "1" && event.key <= "9") {
133-
event.preventDefault();
134-
const categoryIndex = parseInt(event.key, 10) - 1;
135-
if (categoryIndex < categories.length) selectCategory(categories[categoryIndex]!.name);
136-
return;
137-
}
138-
139-
if (isModifierPressed && (event.key === "[" || event.key === "]")) {
140-
event.preventDefault();
141-
if (orderedTabs.length === 0) return;
142-
const currentIndex = orderedTabs.findIndex((t) => t.id === activeTabId);
143-
const nextIndex = event.key === "]"
144-
? (currentIndex + 1) % orderedTabs.length
145-
: (currentIndex - 1 + orderedTabs.length) % orderedTabs.length;
146-
selectTab(orderedTabs[nextIndex]!.id);
147-
return;
148-
}
149-
150-
const isArrowNavigation = (event.key === "ArrowUp" || event.key === "ArrowDown")
151-
&& !isTerminalFocused()
152-
&& !commandPaletteOpen
153-
&& sidebarItems.length > 0;
154-
if (!isArrowNavigation) return;
155-
156-
event.preventDefault();
157-
const currentIndex = sidebarItems.findIndex((item) => item.tabId === activeTabId);
158-
const delta = event.key === "ArrowDown" ? 1 : -1;
159-
const nextIndex = currentIndex === -1
160-
? 0
161-
: (currentIndex + delta + sidebarItems.length) % sidebarItems.length;
162-
const nextItem = sidebarItems[nextIndex]!;
163-
if (nextItem.categoryName !== activeCategory) selectCategory(nextItem.categoryName);
164-
selectTab(nextItem.tabId);
192+
if (handleCommandPaletteKey(event, isModifierPressed, commandPaletteOpen, setCommandPaletteOpen)) return;
193+
if (handleCategorySwitchKey(event, isModifierPressed, categories, selectCategory)) return;
194+
if (handleTabSwitchKey(event, isModifierPressed, orderedTabs, activeTabId, selectTab)) return;
195+
handleArrowNavigation(event, commandPaletteOpen, sidebarItems, activeTabId, activeCategory, selectCategory, selectTab);
165196
};
166197

167198
window.addEventListener("keydown", handler);

packages/wmux-client/src/components/WmuxFileContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ function detectLanguage(name: string): string {
5757
const lower = name.toLowerCase();
5858
if (lower === "dockerfile") return "dockerfile";
5959
if (lower === "makefile") return "makefile";
60-
const ext = lower.split(".").pop() ?? "";
61-
return EXT_TO_LANG[ext] ?? "plaintext";
60+
const fileExtension = lower.split(".").pop() ?? "";
61+
return EXT_TO_LANG[fileExtension] ?? "plaintext";
6262
}
6363

6464
function defineWmuxDarkTheme(monaco: Parameters<NonNullable<Parameters<typeof Editor>[0]["beforeMount"]>>[0]): void {

packages/wmux-client/src/index.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,16 @@ function Spinner(): React.ReactElement {
4040
return <span className="text-muted-foreground/40 inline-block w-[4ch]">{chars[frame]}</span>;
4141
}
4242

43+
function extractHost(url: string): string {
44+
try { return new URL(url).host; } catch { return url; }
45+
}
46+
4347
function ConnectingScreen({ wsUrl }: { readonly wsUrl: string }): React.ReactElement {
44-
const host = (() => {
45-
try { return new URL(wsUrl).host; } catch { return wsUrl; }
46-
})();
48+
const host = extractHost(wsUrl);
4749

4850
return (
4951
<div className="flex items-center justify-center h-screen w-screen bg-background font-sans">
5052
<div className="flex flex-col items-center gap-6 wmux-fade-in">
51-
{/* Logo / brand */}
5253
<div className="flex items-center gap-2.5">
5354
<div className="w-8 h-8 rounded-lg bg-card border border-border/40 flex items-center justify-center wmux-pulse-border">
5455
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-foreground/80">
@@ -59,7 +60,6 @@ function ConnectingScreen({ wsUrl }: { readonly wsUrl: string }): React.ReactEle
5960
<span className="text-foreground/90 text-[15px] font-medium tracking-tight">wmux</span>
6061
</div>
6162

62-
{/* Terminal-style log */}
6363
<div className="bg-card/50 border border-border/30 rounded-lg px-5 py-4 w-[340px] overflow-hidden">
6464
<TerminalLine text={`$ connecting to ${host}`} delay={0} />
6565
<TerminalLine text=" resolving endpoint..." delay={300} dimmed />
@@ -80,10 +80,7 @@ function ErrorScreen({ message, wsUrl, onRetry }: {
8080
readonly wsUrl?: string;
8181
readonly onRetry?: () => void;
8282
}): React.ReactElement {
83-
const host = (() => {
84-
if (!wsUrl) return null;
85-
try { return new URL(wsUrl).host; } catch { return wsUrl; }
86-
})();
83+
const host = wsUrl ? extractHost(wsUrl) : null;
8784

8885
return (
8986
<div className="flex items-center justify-center h-screen w-screen bg-background font-sans">

0 commit comments

Comments
 (0)