Interfaces and extension points for the portfolio OS. For step-by-step recipes see DEVELOPMENT.md and AGENTS.md.
Contract between shell commands and the desktop tile system (appwindow.ts).
interface WindowSpec {
/** Unique tile id — same CLI again focuses or toggles this window */
command: string
title: string
content: string[]
editorPath?: string // edit / vim / editor
explorerPath?: string // explorer starting directory
browserUrl?: string // browse initial URL
resumeSkills?: string[] // resume tile aside
resumeLead?: string[]
resumeBody?: string[]
projectCards?: readonly PortfolioProjectEntry[]
p5SketchPath?: string // p5 viewer VFS path
}Build specs from command names: windowSpecForCommand() in desktop-window-spec.ts.
Flow:
- Terminal runs command →
app-commandsreturns[] terminal.tscallsgetDesktopRef().openWindow(spec)desktop.ts→dispatchOpenWindow()indesktop-open-window.ts- Dynamic
import('./foo-window')→ mount viaBspLayout
Do not instantiate tiles from command handlers directly.
interface WindowChromeOptions {
title: string
onClose: () => void
onMinimize: () => void
onMaximize: () => void
onFocus?: () => void
focusOnTitlebar?: boolean // default true
}
interface WindowChromeElements {
el: HTMLElement
titlebar: HTMLElement
titleEl: HTMLElement
btnClose: HTMLElement
btnMin: HTMLElement
btnMax: HTMLElement
}Factory: createWindowChrome() in window-chrome.ts.
Union of all tile classes (desktop-open-window.ts):
type TiledWin =
| AppWindow | EditorWindow | FileExplorerWindow | BrowserWindow
| PaintWindow | SnakeWindow | PongWindow | RubikWindow | P5Window
| TerminalWindowEach tile exposes: el, command, onFocus, setActive(), setMinimized(), isMaximized(), etc.
interface Command {
description: string
run: (args: string[]) => string[]
hidden?: boolean
loadMs?: number // fake delay for tile commands
}Registry: commands/index.ts merges vfsCommands, systemCommands, appCommands.
// commands/system-commands.ts
export const systemCommands = {
hello: {
description: 'Says hello',
run: args => [`Hello, ${args[0] ?? 'world'}!`],
},
}// commands/app-commands.ts
myapp: {
description: 'Opens my app tile',
loadMs: 400,
run: () => [], // required — desktop opens the tile
},Then add dispatch in desktop-open-window.ts and launcher entries in launcher-catalog.ts.
type FsDir = { t: 'd'; c: Record<string, FsNode> }
type FsFile = { t: 'f'; body: string }
type FsNode = FsDir | FsFileStorage key: portfolio-vfs-v8-namefailed-home (os-fs.ts).
vfsNormalize(input: string): string
vfsPwd(): string
vfsCd(path: string): string | null
vfsLs(path?: string): string[]
vfsLsLong(path?: string): VfsLongEntry[]
vfsCat(path: string): string | null
vfsTouch(path: string): string | null
vfsWrite(path: string, body: string): string | null
vfsReadRaw(path: string): { ok: true; body: string } | { ok: false; err: string }
vfsFormatPath(abs: string): string // /home/namefailed → ~
vfsMkdir(path: string): string | nullDebounced save: 150 ms after mutations.
Always use storage.ts — never raw localStorage in new code.
storageGet(key: string): string | null
storageSet(key: string, value: string): boolean
storageRemove(key: string): boolean
storageGetJson<T>(key: string, fallback: T): T
storageSetJson<T>(key: string, value: T): boolean
storageGetNumber(key: string, fallback: number, min?: number, max?: number): number
storageGetBool(key: string, fallback: boolean): boolean
storageSetBool(key: string, value: boolean): boolean| Key | Module |
|---|---|
portfolio-vfs-v8-namefailed-home |
os-fs.ts |
mrgrey-theme |
theme-control.ts |
mrgrey-os-sound / mrgrey-os-volume |
os-sound.ts |
mrgrey-retro-fx |
retro-fx.ts |
mrgrey-matrix-bg |
matrix-bg.ts |
mrgrey-wallpaper |
wallpaper.ts |
mrgrey-desktop-tile-positions-v6 |
desktop-tiles.ts |
portfolio-fe-prefs-v1 |
file-explorer-window.ts |
mrgrey-browser-iframe-tip-dismiss |
browser-window.ts |
mrgrey-boot-seen |
boot-splash.ts |
mrgrey-guide-seen |
welcome-guide.ts |
mrgrey-p5-tip-seen |
p5-window.ts |
mrgrey-pkgs-v1 |
os-packages.ts |
Full table: AGENTS.md.
interface ThemePack {
id: string
label: string
terminal: ITheme // xterm.js palette
matrixRain: string[] // 8 hex colours
css: Record<string, string> // all --th-* properties
}applyTheme(id: string): boolean
initThemeFromStorage(): void
getActivePack(): ThemePack
getThemeId(): string
listThemeSummaries(): ReadonlyArray<{ id: string; label: string }>Adding a pack: THEMING.md.
vim.ts — one-line shell editor (separate from editor tile).
type VimMode = 'insert' | 'normal' | 'visual'
type InputAction =
| { type: 'none' }
| { type: 'rendered' }
| { type: 'submit'; value: string }
| { type: 'history'; dir: 'up' | 'down' }
| { type: 'complete' }
| { type: 'interrupt' }
| { type: 'clear' }
class VimInput {
mode: VimMode
handleKey(ev: KeyboardEvent): InputAction
getValue(): string
setBuffer(text: string): void
// ...
}Split pure modules — safe to unit test without DOM:
| Module | Exports |
|---|---|
editor-vim-motions.ts |
Caret positions, word/find motions — no buffer mutation |
editor-vim-edits.ts |
{ text, pos } mutations — BufferEditResult contract |
editor-vim-ops.ts |
Barrel re-export of both |
editor-buffer.ts |
applyBufferEditToState, runIndentBufferEdit — shared apply layer |
editor-normal-handlers.ts |
EDITOR_NORMAL_KEY_HANDLERS, dispatchEditorNormalKey |
// Motions (editor-vim-motions.ts)
moveVertPos / moveVertRepeat / moveHorizPos
wordForwardPos / wordBackPos / wordEndForwardPos
findNextOnLine / repeatFindPos / reverseFindKind
lineEndCaretPos / appendLineEndPos / firstNonBlankOnLine
// Edits (editor-vim-edits.ts)
indentLinesText / unindentLinesText // >> / <<
toggleCaseRunText // ~
substituteCharsText // s
deleteCharForwardText / deleteCharBackwardText // x / X
deleteLineBlockText / yankLineBlockText / yankToEOLText
deleteThroughEOLText // D / C
joinLinesText / openLineBelowText / openLineAboveText
applyReplaceRunsText / pasteYankText
// ...Key chords: editor-vim-keys.ts — insertModeKeyAction, tryAppendCountDigit.
Multi-key chords (gg, dd, >>, find-await) remain in editor-window.ts.
Ex commands: parseEditorExCommand() in editor-ex-commands.ts.
ansiToHtml(input: string): string
ansiToHtmlWithLinks(input: string): string
stripAnsi(input: string): string
const c = { pink, blue, green, yellow, red, cyan, white, dim, bold, reset }| Event | Detail |
|---|---|
mrgrey-theme-change |
none |
mrgrey-wallpaper-change |
string | null URL |
mrgrey-first-window |
none |
mrgrey-terminal-cmd |
none |
playOsSound('focus' | 'click' | 'notify' | 'boot')pushToast(message: string, timeoutMs?: number) // 0 = persistentHost interface for desktop-keyboard-handler.ts:
interface DesktopKeyboardHost {
openTerminal(): void
focusTaskbarIndex(index: number): void
focusSpatial(dir: 'h' | 'j' | 'k' | 'l'): void
closeFocusedOrTerminal(): void
minimizeFocusedOrTerminal(): void
toggleMaximizeFocused(): void
toggleShowDesktop(): void
}Chord table: USER_GUIDE.md.
Breaks circular import between terminal and desktop:
// os-registry.ts
setDesktopRef(desktop: Desktop): void
getDesktopRef(): Desktop | nullCalled once from Desktop constructor.
- ARCHITECTURE.md — module map
- DEVELOPMENT.md — setup & workflows
- AGENTS.md — machine-oriented guide
- THEMING.md — CSS token catalogue