Skip to content

Development Setup and Guide

This comprehensive guide covers everything you need to know about developing Mimir, from initial setup to advanced development workflows.

  1. Environment Setup
  2. Building and Running
  3. Project Architecture
  4. Development Workflows
  5. Testing Strategy
  6. Debugging Techniques
  7. Database Management
  8. Frontend Development
  9. Tauri-Specific Considerations
  10. Common Tasks
  • macOS: 10.15 (Catalina) or later
  • Windows: Windows 10 or later
  • Linux: Any modern distribution with glibc 2.31+
Terminal window
# Install rustup (Rust version manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version
Terminal window
# 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
Terminal window
# 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
Terminal window
# 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
Terminal window
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"
  1. Install Visual Studio Build Tools
  2. Install Git for Windows
  3. Install Node.js
  4. Install Rust via rustup-init.exe

Angreal is the project’s task runner and the recommended way to run, test, and document Mimir. It requires Python and pip:

Terminal window
pip install 'angreal>=2'

Key tasks (run from the project root):

Terminal window
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 docs site locally with live reload

The full angreal docs task list: build, serve, clean (see .angreal/task_docs.py). The docs site is Astro Starlight in docs/; it needs Node 22 or later.

Terminal window
# Tauri CLI (faster startup for dev mode)
cargo install tauri-cli
# Diesel CLI (database migrations)
cargo install diesel_cli --no-default-features --features sqlite
# GitHub CLI (for PR management)
brew install gh # macOS
# or download from https://cli.github.com/

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"
}
}
  • Install Rust plugin
  • Install Vue.js plugin
  • Enable Tauri support
Terminal window
# 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 ../../..

The recommended way to run in development mode is via the angreal task:

Terminal window
angreal dev launch

This handles the Vite dev server and Tauri app startup correctly.

Known issue: cargo tauri dev has a bug where it runs npm run dev from crates/ instead of crates/mimir/frontend, causing the Vite dev server to fail. Use the workaround below.

Terminal window
# 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

The MCP sidecar binary must exist before building the mimir crate (required by externalBin in tauri.conf.json):

Terminal window
# 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 to opt-level = 3 in the dev profile (workspace Cargo.toml) because their pixel loops are 10-50x slower unoptimized, which would make map extraction unusable in debug builds.

Terminal window
# 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/
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

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
User Interaction
↓
Vue Component
↓
Pinia Store / API Service
↓
Tauri Command (IPC)
↓
Command Handler
↓
Core Service
↓
Repository (DAL)
↓
Database
  1. Create Feature Branch
Terminal window
git checkout -b feature/my-feature
  1. Implement Feature
  • Add backend logic in mimir-core
  • Add Tauri command in mimir/src/commands
  • Add frontend UI in mimir/frontend/src
  1. Test Locally
Terminal window
angreal test unit
cd crates/mimir/frontend && npm test
  1. Commit and Push
Terminal window
git add .
git commit -m "Add feature description"
git push origin feature/my-feature
  1. Reproduce the Bug
  • Create a failing test
  • Document reproduction steps
  1. Fix the Issue
  • Implement fix
  • Verify test passes
  1. Test Edge Cases
  • Add additional tests
  • Manual testing
  1. Submit PR
  1. Create Vue Component
Terminal window
# In crates/mimir/frontend/src/views/
touch MyNewView.vue
  1. Add Route
// In crates/mimir/frontend/src/app/router/index.ts
{
path: '/my-new-page',
name: 'MyNewPage',
component: () => import('@/views/MyNewView.vue')
}
  1. Add Navigation
<!-- In layout component -->
<router-link to="/my-new-page">My New Page</router-link>
  1. 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):

// 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))
}
  1. Register Command
// In crates/mimir/src/main.rs
.invoke_handler(tauri::generate_handler![
commands::my_new_command,
// ... other commands
])
  1. 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 });
}
// 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 live in each crate’s tests/ directory. The real targets are:

  • mimir-core: catalog_import, srd_smoke_test
  • mimir-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.

// 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
}

The recommended entry point is angreal:

Terminal window
# 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:

Terminal window
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:

Terminal window
# 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
// 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');
});
});
Terminal window
cd crates/mimir/frontend
# Run tests
npm test
# Watch mode
npm test -- --watch
# Coverage
npm run test:coverage
# UI mode
npm run test:ui
use tracing::{info, debug, warn, error};
debug!("Variable value: {:?}", my_var);
info!("Operation started");
warn!("Unusual condition: {}", condition);
error!("Operation failed: {}", err);
  1. Install CodeLLDB extension
  2. 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}"
}
]
}
  • Right-click in app → “Inspect Element”
  • Console tab for logs
  • Network tab for Tauri commands
  • Vue DevTools for component inspection
console.log('Debug:', variable);
console.warn('Warning:', condition);
console.error('Error:', error);
Terminal window
# 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
Terminal window
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
Terminal window
# Apply migrations
diesel migration run
# Rollback last migration
diesel migration revert
# Test migration cycle
diesel migration redo
  • 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
Terminal window
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.

Changes to Vue components, TypeScript, and CSS hot reload automatically:

  • Save file
  • Browser refreshes automatically
  • State may reset depending on change
// 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();
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);
}

Commands run in separate thread from UI:

  • Always async
  • Data must be serializable
  • Large data may have performance impact

Tauri v2 resolves paths via the app handle’s path resolver (see crates/mimir/src/main.rs):

// Inside .setup(|app| { ... })
let data_dir = app
.path()
.app_data_dir()
.expect("Failed to get app data directory");

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.

Terminal window
# Rust dependencies
cargo update
# Frontend dependencies
cd crates/mimir/frontend
npm update
Terminal window
# Rust
cargo fmt
# TypeScript/Vue
cd crates/mimir/frontend
npm run lint
Terminal window
cargo clippy --all-targets --all-features
# Fix auto-fixable issues
cargo clippy --fix
Terminal window
cd crates/mimir/frontend
npm run type-check

Build in release mode and use an external profiler:

Terminal window
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
  • Use Vue DevTools performance tab
  • Check bundle size: npm run build -- --analyze
  • Lazy load routes and heavy components
  • Check CONTRIBUTING.md for contribution guidelines
  • Search GitHub Issues
  • Review crate READMEs for detailed architecture info
  • Ask questions in pull request comments