Notemac++ follows a layered architecture with strict separation of concerns. This document covers the design decisions, patterns, and conventions used throughout the codebase.
┌─────────────────────────────────────────┐
│ ViewPresenters (UI) │ React components — rendering only
├─────────────────────────────────────────┤
│ Controllers │ Business logic, coordination
├─────────────────────────────────────────┤
│ Models │ Runtime state and data
├─────────────────────────────────────────┤
│ Configs │ Static settings, configuration
└─────────────────────────────────────────┘
Configs hold static data and settings. They expose data through explicit Get/Set methods — never auto-properties or public fields.
Models hold runtime state. Plain classes with no framework dependencies. Each domain has its own model: TabModel, SearchModel, MacroModel, UIModel, FileTreeModel.
Controllers contain business logic. They coordinate between models, configs, and the UI layer.
ViewPresenters handle all UI and presentation logic. They receive data from controllers and never contain business logic.
State is managed with Zustand and Immer, split into composable slices:
| Slice | Responsibility |
|---|---|
tabSlice |
Tab lifecycle, ordering, pinning, coloring, content, navigation |
searchSlice |
Find/replace state, marks, bookmarks |
macroSlice |
Recording state, action log, saved macros |
uiSlice |
Sidebar, zoom, split view, dialogs, settings, clipboard history |
fileTreeSlice |
File tree nodes, expansion state, workspace root |
pluginSlice |
Installed plugins, enabled state, plugin metadata, registry cache |
compileRunSlice |
Execution state, output, history, runtime cache statuses, run configurations |
Each slice is independently testable and follows the same patterns: initial state, action creators with Immer drafts, and explicit getter methods.
Dependencies are resolved through a lightweight service locator — no constructor-based DI or third-party IoC frameworks.
Rules:
- Services register on initialization and unregister on teardown.
- Consumers access services via inline accessors (expression-bodied properties or lazy getters). Service references are never cached in fields — always resolved dynamically.
- Both framework-level and project-specific services can be registered.
A custom typed event dispatcher handles cross-component communication. Components subscribe, dispatch, and unsubscribe explicitly — no implicit wiring or string-based keys.
Frequently created objects are pooled rather than repeatedly instantiated and destroyed. Pooled objects are accessed via a factory/pool manager registered in the service locator, referenced by string ID.
A dedicated persistence service handles saving and loading data, accessed through the service locator. All stored values use string-key constants with category prefixes (DB_ for persistence keys, UI_ for interface identifiers).
src/Notemac/
├── Commons/ # Constants, enums, shared types
├── Configs/ # EditorConfig, ThemeConfig
├── Controllers/ # Business logic controllers
├── Model/ # Data models (runtime state slices)
├── Services/ # Platform bridge, runtime adapters
│ └── Runtimes/ # Language command map, Desktop/Web/WASM adapters, cache service
└── UI/ # ViewPresenter components
└── Params/ # Parameter/DTO classes for views
src/Shared/
├── DependencyInjection/ # Service locator
├── EventDispatcher/ # Typed pub/sub event system
├── Helpers/ # FileHelpers, IdHelpers, HexHelpers
├── Persistence/ # Save/load services
├── Pooling/ # Object pool management
└── Git/ # Git integration adapter
The Shared/ library is reusable infrastructure. Project-specific logic never goes here.
| Type | Pattern | Example |
|---|---|---|
| Controllers | <Name>Controller |
GameController |
| Configs | <Name>Config |
EditorConfig |
| Models | <Name>Model |
TabModel |
| ViewPresenters | <Name>ViewPresenter |
MainScreenViewPresenter |
| Item views | <Name>UIItemViewPresenter |
GoalPairUIItemViewPresenter |
| View params | <ViewName>Params |
ScrollUIPieceItemViewPresenterParams |
| Extensions | <Type>Extensions |
ListExtensions |
| Helpers | <Name>Helpers |
FileHelpers |
Fields: private fields use camelCase, model fields use PascalCase, constants use UPPER_SNAKE_CASE with category prefix.
Methods: explicit Get<Property>()/Set<Property>() on configs and models. Boolean getters use Is<Name>()/Has<Name>().
- Braces: Allman style (opening brace on its own line).
- Single-statement if/else: no braces, statement on next line.
- Yoda conditions: literal on the left (
if (null == instance)). - Loop count caching:
for (int i = 0, maxCount = list.Count; i < maxCount; i++). - Collections: prefer native methods (
Find,filter,map) over heavy query frameworks. - Tuples: use value tuples for lightweight groupings.
Code execution uses a unified RuntimeAdapter interface with platform-specific backends, orchestrated by CompileRunController.
CompileRunController (orchestrator)
├── DetectPlatform() → 'tauri' | 'electron' | 'web'
├── SelectAdapter() → RuntimeAdapter
│ ├── DesktopRuntimeAdapter (Electron/Tauri — OS process spawning)
│ ├── CloudRuntimeAdapter (Piston API — cloud execution for 40+ languages)
│ └── SelectWebAdapter()
│ ├── WebJsRuntimeAdapter (sandboxed iframe for JS/TS/CoffeeScript)
│ ├── WebValidationAdapter (JSON/XML/YAML validation, HTML/CSS/MD preview)
│ └── WasmRuntimeAdapter (CDN-loaded WASM runtimes: Pyodide, Wasmoon, sql.js)
└── CompileRunModel (Zustand slice — execution state, output, history)
Languages are categorized in LanguageCommandMap.ts across 5 web runtime types: js-sandbox (Category A), wasm (B — existing ports), emscripten (C — future Emscripten builds), self-hosted (D — compilers-in-JS), and interpreter (E — custom TS interpreters).
RuntimeCacheService provides background WASM caching via IndexedDB with predictive preloading based on open file types. A Service Worker intercepts fetch requests for WASM files and serves from cache.
A three-tier encryption system provides platform-specific and secure credential handling:
Layer 1: SecureEncryptionService
- Core AES-GCM encryption for web (via
crypto.subtle) - Supports key derivation and secure erasure
Layer 2: SafeStorageService
- Uses Electron's
safeStorageon desktop (OS keychain) - Falls back to in-memory storage on web
Layer 3: CredentialStorageService
- Session-only mode by default (no disk persistence)
- Opt-in encrypted persistence with auto-expiry
- Silent migration from plaintext storage
- Credentials: AI keys (24h), Git tokens (8h)
LLMController coordinates with provider adapters:
- OpenAI, Anthropic, Google Gemini, Mistral, Groq, custom
- Configurable models, temperature, token limits
- Chat panel for interactive discussion
- Inline completions with ghost text
- Code actions: explain, refactor, fix, document, test
GitController uses isomorphic-git for cross-platform operations:
- Clone, commit, push, pull, branch management
- Visual diff viewer
- GitHub OAuth Device Flow authentication
- Status indicators in sidebar and status bar
BreadcrumbController manages breadcrumb navigation:
- Maintains current file path and symbol stack
- Updates breadcrumb state as cursor moves
- Provides click-to-navigate handlers for file, folder, and symbol navigation
- Caches breadcrumb data per file for performance
StickyScrollController manages pinned scroll context:
- Tracks function and class headers
- Positions and updates sticky header overlays
- Synchronizes with editor scroll events
- Configurable sticky region height
FormattingController manages document and selection formatting:
- Integrates with Prettier formatter
- Provides format-on-save via file save hooks
- Supports multiple formatters (JavaScript, TypeScript, HTML, CSS, JSON, Markdown)
- Format document and selection commands
DiagnosticsController manages inline errors and warnings:
- Integrates with Monaco's diagnostics system
- Provides Problems panel view
- Implements go-to-next/previous-error navigation
- Groups diagnostics by file and severity
EmmetController manages HTML/CSS abbreviation expansion:
- Registers as Monaco completion provider
- Expands abbreviations in supported file types
- Supports HTML, CSS, JSX, TSX, SCSS, LESS, XML
- Real-time expansion with preview
PrintController manages document printing:
- Formats document with syntax highlighting for print
- Provides print preview dialog
- Configurable print options (line numbers, font size, word wrap, headers/footers)
- Print document and selection commands
GitBlameController manages blame annotations:
- Fetches commit history per file
- Displays author, date, hash, message per line
- Caches blame data per file
- Toggle blame on/off from View menu
GitStashController manages stash lifecycle:
- Stash current changes with optional message
- List all stashes with metadata
- Pop (apply + remove), apply (keep), or drop stashes
- Auto re-index stash entries after operations
MergeConflictController manages merge conflict resolution:
- Detects conflict markers automatically
- Displays inline resolution controls per conflict
- Accept Current, Accept Incoming, Accept Both actions
- Bulk resolve-all actions
CollaborationController manages real-time multi-user editing:
- Yjs CRDT integration for conflict-free collaboration
- WebRTC peer-to-peer communication
- Session creation and joining with room IDs
- Live peer cursors with colored labels and avatars
- Connection status and peer count in status bar
- Dependencies: yjs, y-webrtc, y-monaco
HexEditorController manages binary file viewing and editing:
- Detects binary content and switches view mode
- Handles byte-level editing with validation
- Manages Go To Offset navigation
- Supports 8/16 bytes-per-row toggle
- Integrates with FileController for file operations
HexHelpers.ts (Shared/Helpers) provides binary utilities:
- Byte-to-hex and hex-to-byte conversion
- Offset calculation and validation
- ASCII representation generation
- Binary content detection algorithm
UI Components:
- HexEditorViewPresenter.tsx: Main virtualized hex editor display with three-column layout
- GoToHexOffsetDialogViewPresenter.tsx: Offset navigation dialog with input validation
Shortcut Support: Keyboard commands registered for hex operations:
view-as-hexandview-as-text: Toggle hex/text viewhex-goto-offset: Open Go To Offset dialoghex-toggle-bytes-per-row: Switch 8 ↔ 16 bytes per row
ShortcutEditorController manages custom keyboard binding:
- Loads default shortcuts from ShortcutConfig
- Applies user overrides from localStorage
- Detects keyboard conflicts
- Validates shortcut syntax
- Exports/imports shortcuts as JSON
- Dynamic command dispatch with NormalizeKeyboardEvent
- Manages preset switching via
GetAvailablePresets(),SetActivePreset(),GetActivePresetShortcuts()
ShortcutModel.ts holds keyboard mapping state:
- Default shortcut definitions by category
- Custom override dictionary
- Conflict detection and tracking
- Active preset ID (
activePresetId) with localStorage persistence
ShortcutPresets.ts defines shortcut mapping presets:
ShortcutMappingPresetinterface:{ id, name, description, shortcuts }- Built-in presets: Notemac++ Default, ReSharper
GetPresetById(),LoadActivePresetId(),SaveActivePresetId()- Resolution order: Active Preset (base) → User Overrides →
GetEffectiveShortcuts()merges
ShortcutConfig Extensions:
GetEffectiveShortcuts(overrides?, baseShortcuts?): Merges preset base with custom overridesFindConflict(shortcut, excludeAction, overrides?, baseShortcuts?): Detects shortcut collisions with preset awarenessNormalizeKeyboardEvent(): Cross-platform key event normalization- localStorage persistence with key
notemac-custom-shortcuts
Plugin Presets:
- Plugins register presets via
PluginContributions.presets(partial overrides merged with defaults) PluginModelmanagespluginPresetsstate with register/unregister lifecycle
AppController Refactor:
- Migrated from if-else chains to dynamic lookup map for command dispatch
- Commands resolved by action ID from shortcut registry
- Resolves base shortcuts from active preset via
GetPresetById() - Enables extensibility for plugin-registered commands
The plugin system provides a secure, extensible framework for third-party functionality.
Plugin Lifecycle
- Discovery: Plugins are discovered from the
plugins/directory and remote registry - Loading: Manifest-based loading with JS bundles dynamically imported via Blob URLs
- Activation:
activate(context)hook called with sandboxed PluginContext - Deactivation:
deactivate()hook called for cleanup - Error Handling: Plugin render errors caught by error boundaries; misbehaving plugins can be disabled
Plugin Services
- PluginLoaderService: Loads plugins from disk/registry, manages manifest parsing, handles bundle imports
- PluginAPIService: Creates sandboxed PluginContext with scoped interfaces (editor, events, UI, commands, themes, languages, storage)
- PluginRegistryService: Remote registry integration with search, install, uninstall, and update checking
- PluginController: Orchestrates loading, activation, deactivation, and error handling
- PluginModel: Persistent storage of plugin state (installed plugins, enabled/disabled status, metadata)
- PluginSlice: Zustand store slice for plugin state management
Plugin UI Components
- PluginErrorBoundary: React error boundary for isolating and reporting plugin render errors
- PluginManagerViewPresenter: Two-tab dialog for browsing/installing and managing plugins
- PluginSidebarPanelViewPresenter: Renders registered sidebar panels from plugins with theme passthrough
- PluginStatusBarViewPresenter: Renders status bar items registered by plugins with error isolation
- PluginSettingsSectionViewPresenter: Renders plugin-registered settings in Settings dialog
- PluginDialogViewPresenter: Generic modal wrapper for plugin-provided dialogs
Sandboxing & Security
- Plugins run in the main thread but are isolated through PluginContext (no direct DOM access)
- Blob URLs prevent direct file system access to plugin bundles
- Each plugin gets namespaced storage (localStorage with
plugin:${pluginName}prefix) - Error boundaries prevent plugin crashes from affecting the main application
| Package | Version | Purpose |
|---|---|---|
| React | 18.3 | UI rendering |
| Monaco Editor | 0.45 | Code editor engine |
| Zustand | 4.5 | State management |
| Immer | 10.0 | Immutable state updates |
| Electron | 28.1 | Desktop shell |
| TypeScript | 5.6 | Type safety |
| Vite | 6.0 | Build tooling |
| isomorphic-git | 1.37 | Git operations |
| iconv-lite | 0.6 | Character encoding |
| jschardet | 3.1 | Charset detection |
| js-md5/sha1/sha256/sha512 | — | Hash generation |
| react-icons | 5.3 | Icon library |