Development Setup and Guide
This comprehensive guide covers everything you need to know about developing Mimir, from initial setup to advanced development workflows.
Quick Links
- Developer Documentation Home - Overview of developer docs
- Contributing Guide - How to contribute to the project
- Architecture - Technical architecture overview
- GitHub Repository
Table of Contents
- Environment Setup
- Building and Running
- Project Architecture
- Development Workflows
- Testing Strategy
- Debugging Techniques
- Database Management
- Frontend Development
- Tauri-Specific Considerations
- Common Tasks
Environment Setup
System Requirements
- macOS: 10.15 (Catalina) or later
- Windows: Windows 10 or later
- Linux: Any modern distribution with glibc 2.31+
Required Tools
Rust Toolchain
# Install rustup (Rust version manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version
Node.js and npm
# macOS (via Homebrew)
brew install node
# Windows (via installer)
# Download from https://nodejs.org
# Linux (via package manager)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify installation
node --version # Should be v18 or higher
npm --version
Platform-Specific Dependencies
macOS Setup
# Install Xcode Command Line Tools
xcode-select --install
# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Optional: Install additional development tools
brew install git gh
Linux Setup (Ubuntu/Debian)
# Update package lists
sudo apt-get update
# Install required dependencies
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf
# Optional: Install git and gh
sudo apt-get install -y git gh
Linux Setup (Fedora)
sudo dnf install \
webkit2gtk4.1-devel \
openssl-devel \
curl \
wget \
file \
libappindicator-gtk3-devel \
librsvg2-devel \
patchelf
sudo dnf groupinstall "C Development Tools and Libraries"
Windows Setup
- Install Visual Studio Build Tools
- Install Git for Windows
- Install Node.js
- Install Rust via rustup-init.exe
Angreal (Recommended Workflow)
Angreal is the project’s task runner and the recommended way to run, test, and document Mimir. It requires Python and pip:
pip install 'angreal>=2'
Key tasks (run from the project root):
angreal dev launch # Start Vite dev server + Tauri app (builds sidecar if missing)
angreal dev reset # Delete the dev database and dev assets (never touches production)
angreal test unit # Run Rust tests for core crates (excludes the Tauri app crate)
angreal test coverage # Coverage via cargo-tarpaulin (config: tarpaulin.toml)
angreal docs serve # Serve the mdBook docs locally with hot reload
The full angreal docs task list: build, serve, watch, clean, test, check, init, production (see .angreal/task_docs.py).
Optional Development Tools
# Tauri CLI (faster startup for dev mode)
cargo install tauri-cli
# Diesel CLI (database migrations)
cargo install diesel_cli --no-default-features --features sqlite
# mdBook (documentation, used by angreal docs tasks)
cargo install mdbook mdbook-mermaid
# GitHub CLI (for PR management)
brew install gh # macOS
# or download from https://cli.github.com/
IDE Setup
VS Code (Recommended)
Install these extensions:
- rust-analyzer - Rust language support
- Tauri - Tauri development tools
- Volar - Vue 3 language support
- ESLint - JavaScript/TypeScript linting
- Error Lens - Inline error messages
Settings:
{
"rust-analyzer.checkOnSave.command": "clippy",
"editor.formatOnSave": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
}
}
IntelliJ IDEA / CLion
- Install Rust plugin
- Install Vue.js plugin
- Enable Tauri support
Building and Running
Initial Build
# Clone repository
git clone https://github.com/mimir-dm/mimir.git
cd mimir
# Build the MCP sidecar FIRST — tauri.conf.json declares it as externalBin,
# so building the mimir crate fails if the binary is missing
bash scripts/build-sidecar.sh # on Windows: .\scripts\build-sidecar.ps1
# Build Rust workspace
cargo build
# Install frontend dependencies
cd crates/mimir/frontend
npm install
cd ../../..
Development Mode
The recommended way to run in development mode is via the angreal task:
angreal dev launch
This handles the Vite dev server and Tauri app startup correctly.
Manual Development Mode
Known issue:
cargo tauri devhas a bug where it runsnpm run devfromcrates/instead ofcrates/mimir/frontend, causing the Vite dev server to fail. Use the workaround below.
# Terminal 1: Start Vite dev server manually
cd crates/mimir/frontend
npm run dev
# Terminal 2: Run Rust backend (without Tauri's frontend handling)
cargo run -p mimir --no-default-features
Sidecar Binary
The MCP sidecar binary must exist before building the mimir crate (required by externalBin in tauri.conf.json):
# Build the sidecar (on Windows: .\scripts\build-sidecar.ps1)
./scripts/build-sidecar.sh
# Output: crates/mimir/binaries/mimir-mcp-{target-triple}
Build performance: the image-processing dependencies (
image,png,fdeflate,zune-jpeg) are pinned toopt-level = 3in the dev profile (workspaceCargo.toml) because their pixel loops are 10-50x slower unoptimized, which would make map extraction unusable in debug builds.
Production Build
# Build optimized release
cd crates/mimir
cargo tauri build
# Output locations:
# macOS: target/release/bundle/dmg/
# Windows: target/release/bundle/msi/
# Linux: target/release/bundle/deb/ and target/release/bundle/appimage/
Project Architecture
Workspace Structure
mimir/
├── Cargo.toml # Workspace configuration
├── crates/
│ ├── mimir/ # Main application
│ ├── mimir-core/ # Business logic
│ ├── mimir-mcp/ # MCP server for Claude Code
│ ├── mimir-mapgen/ # Procedural map generation
│ └── mimir-print/ # PDF generation
├── docs/ # Documentation
├── data/ # D&D reference data
└── .metis/ # Project management
Architecture Layers
Frontend Layer (Vue 3 + TypeScript)
- Views: Page-level components
- Components: Reusable UI components
- Stores: Pinia state management
- Services: API wrappers for Tauri commands
Application Layer (Tauri + Rust)
- Commands: IPC handlers
- Services: Application-level business logic
- State: Application state management
Domain Layer (mimir-core)
- Models: Domain entities
- Services: Business logic
- DAL: Data access layer
- Migrations: Database schema
Infrastructure Layer
- Database: SQLite with Diesel ORM
- File System: Campaign data storage
Data Flow
User Interaction
↓
Vue Component
↓
Pinia Store / API Service
↓
Tauri Command (IPC)
↓
Command Handler
↓
Core Service
↓
Repository (DAL)
↓
Database
Development Workflows
Feature Development
- Create Feature Branch
git checkout -b feature/my-feature
- Implement Feature
- Add backend logic in
mimir-core - Add Tauri command in
mimir/src/commands - Add frontend UI in
mimir/frontend/src
- Test Locally
angreal test unit
cd crates/mimir/frontend && npm test
- Commit and Push
git add .
git commit -m "Add feature description"
git push origin feature/my-feature
Bug Fix Workflow
- Reproduce the Bug
- Create a failing test
- Document reproduction steps
- Fix the Issue
- Implement fix
- Verify test passes
- Test Edge Cases
- Add additional tests
- Manual testing
- Submit PR
Adding a New Page
- Create Vue Component
# In crates/mimir/frontend/src/views/
touch MyNewView.vue
- Add Route
// In crates/mimir/frontend/src/app/router/index.ts
{
path: '/my-new-page',
name: 'MyNewPage',
component: () => import('@/views/MyNewView.vue')
}
- Add Navigation
<!-- In layout component -->
<router-link to="/my-new-page">My New Page</router-link>
Adding a New Tauri Command
- Create Command Function
Commands take State<'_, AppState> and open an on-demand connection with state.connect(), returning an ApiResponse (mirroring crates/mimir/src/commands/campaign.rs):
#![allow(unused)]
fn main() {
// In crates/mimir/src/commands/my_commands.rs
use tauri::State;
use crate::state::AppState;
use super::{to_api_response, ApiResponse};
#[tauri::command]
pub fn my_new_command(state: State<'_, AppState>, param: String) -> ApiResponse<String> {
let mut db = match state.connect() {
Ok(db) => db,
Err(e) => return ApiResponse::err(e),
};
// Business logic here, e.g. via a mimir-core service:
// let result = CampaignService::new(&mut db).list(false);
// to_api_response(result)
ApiResponse::ok(format!("Processed: {}", param))
}
}
- Register Command
#![allow(unused)]
fn main() {
// In crates/mimir/src/main.rs
.invoke_handler(tauri::generate_handler![
commands::my_new_command,
// ... other commands
])
}
- Call from Frontend
// In frontend service
import { invoke } from '@tauri-apps/api/core';
export async function myNewCommand(param: string): Promise<string> {
return await invoke<string>('my_new_command', { param });
}
Testing Strategy
Rust Testing
Unit Tests
#![allow(unused)]
fn main() {
// In same file as implementation
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_feature() {
let result = my_function();
assert_eq!(result, expected);
}
}
}
Integration Tests
Integration tests live in each crate’s tests/ directory. The real targets are:
mimir-core:catalog_import,srd_smoke_testmimir-mapgen:cli_integration,format_roundtrip,polygon_snapshots
SRD test fixtures are generated by scripts/extract-srd-fixtures.py (SRD/OGL-safe content only) into crates/mimir/frontend/__tests__/fixtures and crates/mimir-core/tests/fixtures.
#![allow(unused)]
fn main() {
// In crates/mimir-core/tests/catalog_import.rs (for example)
use mimir_core::*;
#[test]
fn test_database_workflow() {
let mut conn = create_connection(":memory:").unwrap();
run_migrations(&mut conn).unwrap();
// Test full workflow
}
}
Running Rust Tests
The recommended entry point is angreal:
# Rust tests for core crates (recommended)
angreal test unit
# With coverage (cargo-tarpaulin, config in tarpaulin.toml)
angreal test coverage
angreal test unit runs the real underlying command:
cargo test --workspace --exclude mimir -- --test-threads=1
The mimir (Tauri) crate is excluded because it cannot build without the sidecar binary, and --test-threads=1 is required to avoid SQLite locking issues. Avoid bare cargo test --workspace.
Targeted runs:
# Specific crate
cargo test -p mimir-core -- --test-threads=1
# Specific integration test target
cargo test -p mimir-core --test catalog_import -- --test-threads=1
cargo test -p mimir-mapgen --test cli_integration
# Specific test by name
cargo test -p mimir-core test_campaign_creation -- --test-threads=1
# With output
cargo test -p mimir-core -- --test-threads=1 --nocapture
Frontend Testing
Component Tests
// In crates/mimir/frontend/src/components/__tests__/
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import MyComponent from '../MyComponent.vue';
describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = mount(MyComponent, {
props: { title: 'Test' }
});
expect(wrapper.text()).toContain('Test');
});
});
Running Frontend Tests
cd crates/mimir/frontend
# Run tests
npm test
# Watch mode
npm test -- --watch
# Coverage
npm run test:coverage
# UI mode
npm run test:ui
Debugging Techniques
Rust Debugging
Print Debugging
#![allow(unused)]
fn main() {
use tracing::{info, debug, warn, error};
debug!("Variable value: {:?}", my_var);
info!("Operation started");
warn!("Unusual condition: {}", condition);
error!("Operation failed: {}", err);
}
VS Code Debugging
- Install CodeLLDB extension
- Add
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug Tauri",
"cargo": {
"args": ["build", "--manifest-path=crates/mimir/Cargo.toml"]
},
"cwd": "${workspaceFolder}"
}
]
}
Frontend Debugging
Browser DevTools
- Right-click in app → “Inspect Element”
- Console tab for logs
- Network tab for Tauri commands
- Vue DevTools for component inspection
Console Logging
console.log('Debug:', variable);
console.warn('Warning:', condition);
console.error('Error:', error);
Database Debugging
# Open development database (macOS)
sqlite3 ~/Library/Application\ Support/com.mimir.app/dev/data/mimir.db
# Common commands
.tables # List tables
.schema campaigns # Show table structure
SELECT * FROM campaigns; # Query data
.quit # Exit
Database Management
Migrations
Creating a Migration
cd crates/mimir-core
diesel migration generate add_new_feature
# Edit migrations/YYYY-MM-DD-HHMMSS_add_new_feature/up.sql
# Edit migrations/YYYY-MM-DD-HHMMSS_add_new_feature/down.sql
Running Migrations
# Apply migrations
diesel migration run
# Rollback last migration
diesel migration revert
# Test migration cycle
diesel migration redo
Development Database Location
- macOS:
~/Library/Application Support/com.mimir.app/dev/data/mimir.db - Linux:
~/.local/share/com.mimir.app/dev/data/mimir.db - Windows:
%APPDATA%\com.mimir.app\dev\data\mimir.db
Resetting Development Database
angreal dev reset
This deletes ONLY the dev database (.../com.mimir.app/dev/data/mimir.db) and clears dev assets — it never touches the production database (see .angreal/task_dev.py). Restart the app (e.g. angreal dev launch) to recreate a fresh database.
Frontend Development
Hot Reload
Changes to Vue components, TypeScript, and CSS hot reload automatically:
- Save file
- Browser refreshes automatically
- State may reset depending on change
State Management (Pinia)
// Define store
import { defineStore } from 'pinia';
export const useCampaignStore = defineStore('campaign', () => {
const campaigns = ref<Campaign[]>([]);
async function loadCampaigns() {
campaigns.value = await invoke('list_campaigns');
}
return { campaigns, loadCampaigns };
});
// Use in component
const campaignStore = useCampaignStore();
await campaignStore.loadCampaigns();
Calling Tauri Commands
import { invoke } from '@tauri-apps/api/core';
// Basic invocation
const result = await invoke<string>('command_name', { param: value });
// With error handling
try {
const data = await invoke<Campaign>('get_campaign', { id: campaignId });
} catch (error) {
console.error('Failed to get campaign:', error);
}
Tauri-Specific Considerations
IPC Communication
Commands run in separate thread from UI:
- Always async
- Data must be serializable
- Large data may have performance impact
File System Access
Tauri v2 resolves paths via the app handle’s path resolver (see crates/mimir/src/main.rs):
#![allow(unused)]
fn main() {
// Inside .setup(|app| { ... })
let data_dir = app
.path()
.app_data_dir()
.expect("Failed to get app data directory");
}
Window Management
Frontend uses @tauri-apps/api v2:
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
await appWindow.setTitle('New Title');
await appWindow.maximize();
For creating secondary windows, see crates/mimir/frontend/src/utils/windows.ts, which uses WebviewWindow from @tauri-apps/api/webviewWindow.
Common Tasks
Updating Dependencies
# Rust dependencies
cargo update
# Frontend dependencies
cd crates/mimir/frontend
npm update
Formatting Code
# Rust
cargo fmt
# TypeScript/Vue
cd crates/mimir/frontend
npm run lint
Running Clippy
cargo clippy --all-targets --all-features
# Fix auto-fixable issues
cargo clippy --fix
Type Checking Frontend
cd crates/mimir/frontend
npm run type-check
Performance Optimization
Profiling Rust Code
Build in release mode and use an external profiler:
cargo build --release
# Example: samply (cargo install samply)
samply record ./target/release/mimir-mapgen generate --preset forest --output /tmp/map.dungeondraft_map
# On macOS, Instruments (Xcode) also works against the release binary
Frontend Performance
- Use Vue DevTools performance tab
- Check bundle size:
npm run build -- --analyze - Lazy load routes and heavy components
Additional Resources
Getting Help
- Check CONTRIBUTING.md for contribution guidelines
- Search GitHub Issues
- Review crate READMEs for detailed architecture info
- Ask questions in pull request comments