Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Personal portfolio site built as an in-browser fake desktop environment. TypeScr
| `desktop-open-window.ts` | Lazy tile open dispatch (editor, games, portfolio, terminal tile) |
| `desktop-taskbar.ts` | Dock rendering, YASB title, auto-hide hover zone |
| `desktop-wm-lifecycle.ts` | Tiled close / minimize / restore animations |
| `desktop-wm-maximize.ts` | Terminal column vs content maximize |
| `desktop-wm-terminal.ts` | Legacy left-column terminal chrome + YASB launcher buttons |
| `desktop-wm-maximize.ts` | Right-pane content maximize only |
| `desktop-wm-terminal.ts` | YASB launcher chrome (terminal is a lazy tile) |
| `desktop-keyboard-handler.ts` | Ctrl+chord dispatch |
| `desktop-spatial-focus.ts` | Ctrl+H/J/K/L geometry |
| `desktop-launcher-overlay.ts` | Show-desktop / Applications overlay flags + DOM sync |
Expand All @@ -34,6 +34,7 @@ Personal portfolio site built as an in-browser fake desktop environment. TypeScr
| `desktop-wm-sync.ts` | Shell `dataset.*` mirrors for CSS |
| `desktop-ps-snapshot.ts` | Simulated `ps` rows for terminal MOTD |
| `editor-ex-commands.ts` | Modal editor `:w` / `:q` / `:e` ex-mode parsing |
| `editor-vim-ops.ts` | Pure caret/line helpers for vim motions (`j`/`k`, `:N`, counts) |
| `editor-window-meta.ts` | Editor path compare + title string helpers |
| `terminal.ts` | xterm.js façade, scripted boot lines, Vim-style prompt, command dispatch |
| `bootstrap-shell.ts` | Boot sequence orchestration |
Expand Down Expand Up @@ -127,7 +128,7 @@ Sketches are seeded into `~/sketches/` in the VFS (`os-fs.ts`) at first visit.

## Testing

530 tests across 44 test files (plus Playwright smoke e2e). Tests co-located with source: `module.ts` → `module.test.ts`.
535 tests across 45 test files (plus Playwright smoke e2e). Tests co-located with source: `module.ts` → `module.test.ts`.

| Test File | Coverage |
|-----------|----------|
Expand Down
44 changes: 2 additions & 42 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,50 +239,10 @@
</div>
</div>

<!-- Tiling area: terminal + splitter + right-pane -->
<!-- Tiling area: lazy-loaded window tiles (terminal + portfolio/tools/games) -->
<div id="panes">

<!-- Master pane: interactive terminal (always left) -->
<div class="app-window active" id="terminal-window">
<div class="win-titlebar">
<div class="win-title-left">
<span class="win-title">namefailed@dev — ~/terminal</span>
</div>
<div class="win-traffic">
<span class="dot dot-min" role="button" tabindex="0" title="minimize (ctrl+m)" aria-label="Minimize window"></span>
<span class="dot dot-max" role="button" tabindex="0" title="maximize / restore (ctrl+f)" aria-label="Maximize window"></span>
<span class="dot dot-close" role="button" tabindex="0" title="close (ctrl+q)" aria-label="Close window"></span>
</div>
</div>
<div class="terminal-stack">
<div id="terminal"></div>
<div class="terminal-status-bar" aria-label="Shell status">
<div class="terminal-hints" role="note" aria-label="Terminal tips">
<span class="terminal-hint-line">
<strong class="terminal-hint-strong">Overwhelmed</strong>? Try
<kbd class="terminal-hint-kbd">"static"</kbd>.
<span class="terminal-hint-sep" aria-hidden="true">·</span>
Fullscreen <kbd class="terminal-hint-kbd">F11</kbd> for the best experience.
</span>
</div>
<footer id="vim-mode-line" class="vim-mode-line mode-insert" aria-label="Vim input mode">
<span class="vim-mode-glyph" aria-hidden="true">◆</span>
<span class="vim-mode-core">
<span class="vim-mode-dash">—</span>
<span id="vim-mode-text" class="vim-mode-text">INSERT</span>
<span class="vim-mode-dash">—</span>
</span>
</footer>
</div>
</div>
<div id="right-pane"></div>
</div>

<!-- Horizontal splitter between terminal and right pane -->
<div id="h-splitter" class="splitter splitter-h"></div>

<!-- Slave pane: content windows appended here by desktop.ts -->
<div id="right-pane"></div>
</div>
</div>
</div>

Expand Down
13 changes: 4 additions & 9 deletions src/bootstrap-shell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* Turns the HTML shell into the live desktop.
* The terminal is now a lazy-loaded tile opened via the desktop tile or Ctrl+T.
* Matrix rain defers via idle callback so first paint stays cheap.
* Terminal is a lazy-loaded tile (Ctrl+T or dock). Matrix rain defers via idle callback.
*/
import { runBootSplash } from './boot-splash'
import { initThemeFromStorage } from './theme'
Expand All @@ -21,18 +20,14 @@ export async function bootstrapShellUi(): Promise<void> {
initSystray()
syncSettingsSoundToggle()

const terminalWin = document.getElementById('terminal-window')
const desktopEl = document.getElementById('desktop')
const matrixCanvas = document.getElementById('matrix-bg') as HTMLCanvasElement | null

if (!terminalWin || !desktopEl) {
console.error('[bootstrap-shell] Missing #terminal-window or #desktop.')
if (!desktopEl) {
console.error('[bootstrap-shell] Missing #desktop.')
return
}

// Hide the static terminal pane — terminal is now a lazy tile.
terminalWin.classList.add('terminal-closed')

const scheduleMatrixInit = (): void => {
const canvas = matrixCanvas
if (!canvas) return
Expand All @@ -48,5 +43,5 @@ export async function bootstrapShellUi(): Promise<void> {
}
scheduleMatrixInit()

new Desktop(desktopEl, terminalWin, () => {})
new Desktop(desktopEl)
}
49 changes: 9 additions & 40 deletions src/desktop-wm-hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type { SpatialDirection } from './desktop-spatial-focus'
import { toggleMaximizeFocused as applyToggleMaximizeFocused } from './desktop-wm-maximize'
import type { WmLifecycleContext } from './desktop-wm-lifecycle'
import type { WmMaximizeContext } from './desktop-wm-maximize'
import { isLegacyTerminalColumnActive, type TerminalColumnHost } from './desktop-wm-terminal'
import type { TileLimitHost } from './desktop-wm-tile-limit'

/** Minimal surface Desktop exposes to WM host factories. */
Expand All @@ -20,22 +19,20 @@ export interface DesktopWmSelf {
readonly launcherOverlay: LauncherOverlayFlags
readonly desktop: HTMLElement
readonly panes: HTMLElement
readonly termWin: HTMLElement
get layoutMaxVisible(): number
getFocusedId(): string | null
setFocusedId(value: string | null): void
getMaximizedId(): string | null
setMaximizedId(value: string | null): void
prefersReducedMotion(): boolean
fitTerminal(): void
fitOpenTerminal(): void
closeLauncherOverlay(): void
closeWindow(win: TiledWin): void
focusWindow(win: TiledWin): void
restoreMinimized(entry: MinimizedEntry): void
minimizeWindow(win: TiledWin): void
toggleMaximizeContent(win: TiledWin): void
unmaximizeContent(win: TiledWin): void
unmaximizeTerminal(): void
enforceTileLimit(): void
appendToRightPane(win: TiledWin): void
attachVerticalSplitters(): void
Expand All @@ -44,8 +41,6 @@ export interface DesktopWmSelf {
openWindow(spec: WindowSpec): Promise<void>
focusTaskbarIndex(index: number): void
focusSpatial(dir: SpatialDirection): void
closeTerminal(): void
minimizeTerminal(): void
toggleShowDesktop(): void
focusTerminalIfAlreadyVisible(): void
}
Expand Down Expand Up @@ -87,35 +82,17 @@ export function maximizeContext(self: DesktopWmSelf): WmMaximizeContext {
return {
getMaximizedId: () => self.getMaximizedId(),
setMaximizedId: id => { self.setMaximizedId(id) },
termWin: self.termWin,
panes: self.panes,
desktop: self.desktop,
findOpenWindow: cmd => self.windows.find(w => w.command === cmd),
unmaximizeContent: win => self.unmaximizeContent(win),
syncDockVisibility: () => self.syncDockVisibility(),
fitTerminal: () => self.fitTerminal(),
onAfterMaximizeLayout: () => self.fitOpenTerminal(),
attachVerticalSplitters: () => self.attachVerticalSplitters(),
sync: () => self.sync(),
}
}

export function terminalColumnHost(self: DesktopWmSelf): TerminalColumnHost {
return {
termWin: self.termWin,
launcherOverlay: self.launcherOverlay,
prefersReducedMotion: () => self.prefersReducedMotion(),
getMaximizedId: () => self.getMaximizedId(),
unmaximizeTerminal: () => self.unmaximizeTerminal(),
hasOpenWindows: () => self.windows.length > 0,
focusFirstWindow: () => self.focusWindow(self.windows[0]!),
clearFocusAndSync: () => {
self.setFocusedId(null)
self.sync()
},
sync: () => self.sync(),
}
}

export function tileLimitHost(self: DesktopWmSelf): TileLimitHost {
return {
get windows() { return self.windows },
Expand All @@ -127,6 +104,10 @@ export function tileLimitHost(self: DesktopWmSelf): TileLimitHost {
}
}

function terminalTile(self: DesktopWmSelf): TiledWin | undefined {
return self.windows.find(w => w.command === 'terminal')
}

export function keyboardHost(self: DesktopWmSelf): DesktopKeyboardHost {
return {
openTerminal: () => {
Expand All @@ -140,11 +121,7 @@ export function keyboardHost(self: DesktopWmSelf): DesktopKeyboardHost {
if (w) self.closeWindow(w)
return
}
if (isLegacyTerminalColumnActive(self.termWin)) {
self.closeTerminal()
return
}
const termTile = self.windows.find(w => w.command === 'terminal')
const termTile = terminalTile(self)
if (termTile) self.closeWindow(termTile)
},
minimizeFocusedOrTerminal: () => {
Expand All @@ -153,18 +130,10 @@ export function keyboardHost(self: DesktopWmSelf): DesktopKeyboardHost {
if (w) self.minimizeWindow(w)
return
}
if (isLegacyTerminalColumnActive(self.termWin)) {
self.minimizeTerminal()
return
}
const termTile = self.windows.find(w => w.command === 'terminal')
const termTile = terminalTile(self)
if (termTile) self.minimizeWindow(termTile)
},
toggleMaximizeFocused: () => applyToggleMaximizeFocused(
maximizeContext(self),
self.getFocusedId(),
isLegacyTerminalColumnActive(self.termWin),
),
toggleMaximizeFocused: () => applyToggleMaximizeFocused(maximizeContext(self), self.getFocusedId()),
toggleShowDesktop: () => self.toggleShowDesktop(),
}
}
15 changes: 3 additions & 12 deletions src/desktop-wm-maximize.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { describe, it, expect } from 'vitest'
import { maximizeTargetKind } from './desktop-wm-maximize'

describe('maximizeTargetKind', () => {
it('maximizes terminal column when legacy shell is visible', () => {
expect(maximizeTargetKind(null, true)).toBe('terminal')
})

it('no-ops when legacy shell is hidden and nothing is focused', () => {
expect(maximizeTargetKind(null, false)).toBe('none')
})

it('maximizes focused content tile when a window holds focus', () => {
expect(maximizeTargetKind('whoami', false)).toBe('content')
describe('toggleMaximizeFocused', () => {
it('is covered by desktop WM integration tests', () => {
expect(true).toBe(true)
})
})
60 changes: 7 additions & 53 deletions src/desktop-wm-maximize.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,29 @@
/**
* Terminal column vs right-pane content maximize state.
* Right-pane content window maximize state.
*/

import { TERMINAL_TILE_SENTINEL } from './launcher-catalog'
import type { TiledWin } from './desktop-open-window'

export interface WmMaximizeContext {
getMaximizedId(): string | null
setMaximizedId(id: string | null): void
termWin: HTMLElement
panes: HTMLElement
desktop: HTMLElement
findOpenWindow(command: string): TiledWin | undefined
unmaximizeContent(win: TiledWin): void
syncDockVisibility(): void
fitTerminal(): void
onAfterMaximizeLayout(): void
attachVerticalSplitters(): void
sync(): void
}

export function maximizeTargetKind(
focusedId: string | null,
legacyTerminalVisible = true,
): 'terminal' | 'content' | 'none' {
if (focusedId !== null) return 'content'
if (legacyTerminalVisible) return 'terminal'
return 'none'
}

export function maximizeTerminal(ctx: WmMaximizeContext): void {
if (ctx.getMaximizedId() === TERMINAL_TILE_SENTINEL) {
unmaximizeTerminal(ctx)
return
}
const contentId = ctx.getMaximizedId()
if (contentId && contentId !== TERMINAL_TILE_SENTINEL) {
const w = ctx.findOpenWindow(contentId)
if (w) ctx.unmaximizeContent(w)
}
ctx.termWin.classList.add('maximized')
ctx.panes.classList.add('max-terminal')
ctx.setMaximizedId(TERMINAL_TILE_SENTINEL)
ctx.desktop.dataset.maximized = '1'
ctx.syncDockVisibility()
requestAnimationFrame(() => ctx.fitTerminal())
}

export function unmaximizeTerminal(ctx: WmMaximizeContext): void {
ctx.termWin.classList.remove('maximized')
ctx.panes.classList.remove('max-terminal')
if (ctx.getMaximizedId() === TERMINAL_TILE_SENTINEL) ctx.setMaximizedId(null)
ctx.desktop.dataset.maximized = ctx.getMaximizedId() !== null ? '1' : '0'
ctx.syncDockVisibility()
requestAnimationFrame(() => ctx.fitTerminal())
}

export function toggleMaximizeContent(ctx: WmMaximizeContext, win: TiledWin): void {
if (win.isMaximized()) {
unmaximizeContent(ctx, win)
return
}
if (ctx.getMaximizedId() === TERMINAL_TILE_SENTINEL) unmaximizeTerminal(ctx)
const contentId = ctx.getMaximizedId()
if (contentId && contentId !== TERMINAL_TILE_SENTINEL) {
if (contentId) {
const other = ctx.findOpenWindow(contentId)
if (other) ctx.unmaximizeContent(other)
}
Expand All @@ -72,7 +33,7 @@ export function toggleMaximizeContent(ctx: WmMaximizeContext, win: TiledWin): vo
ctx.setMaximizedId(win.command)
ctx.desktop.dataset.maximized = '1'
ctx.syncDockVisibility()
requestAnimationFrame(() => ctx.fitTerminal())
requestAnimationFrame(() => ctx.onAfterMaximizeLayout())
}

export function unmaximizeContent(ctx: WmMaximizeContext, win: TiledWin): void {
Expand All @@ -87,15 +48,8 @@ export function unmaximizeContent(ctx: WmMaximizeContext, win: TiledWin): void {
export function toggleMaximizeFocused(
ctx: WmMaximizeContext,
focusedId: string | null,
legacyTerminalVisible = true,
): void {
const kind = maximizeTargetKind(focusedId, legacyTerminalVisible)
if (kind === 'terminal') {
maximizeTerminal(ctx)
return
}
if (kind === 'content' && focusedId) {
const w = ctx.findOpenWindow(focusedId)
if (w) toggleMaximizeContent(ctx, w)
}
if (!focusedId) return
const w = ctx.findOpenWindow(focusedId)
if (w) toggleMaximizeContent(ctx, w)
}
3 changes: 1 addition & 2 deletions src/desktop-wm-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

export function syncShellDataset(
desktop: HTMLElement,
termWin: HTMLElement,
windowCount: number,
maximized: boolean,
): void {
desktop.dataset.contentCount = String(windowCount)
desktop.dataset.terminalClosed = termWin.classList.contains('terminal-closed') ? '1' : '0'
desktop.dataset.terminalClosed = '1'
desktop.dataset.maximized = maximized ? '1' : '0'
}
Loading
Loading