Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Development Setup and Guide

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

Table of Contents

  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

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

  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:

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

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 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 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 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.

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

  1. Create Feature Branch
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
angreal test unit
cd crates/mimir/frontend && npm test
  1. Commit and Push
git add .
git commit -m "Add feature description"
git push origin feature/my-feature

Bug Fix Workflow

  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

Adding a New Page

  1. Create Vue Component
# 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>

Adding a New Tauri Command

  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):

#![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))
}
}
  1. Register Command
#![allow(unused)]
fn main() {
// 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 });
}

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_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.

#![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

#![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

  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}"
    }
  ]
}

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