Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fulsomenko/kanban/llms.txt

Use this file to discover all available pages before exploring further.

Thank you for considering contributing to Kanban! This guide will help you get started with development.

Development Setup

Prerequisites

Rust 1.70+

Install via rustup

Nix (Recommended)

For reproducible environment

Getting Started

1

Clone the repository

git clone https://github.com/fulsomenko/kanban
cd kanban
2

Enter development shell

Using Nix (recommended):
nix develop
This provides:
  • Rust toolchain (stable, rust-analyzer, rust-src)
  • cargo-watch, cargo-edit, cargo-audit, cargo-tarpaulin
  • bacon (background compiler)
Or install dependencies manually:
rustup update stable
3

Build the project

cargo build
4

Run the application

cargo run

Common Commands

Building

cargo build            # Build all crates

Running

cargo run              # Launch TUI
cargo run -- tui       # Explicit TUI mode

Development Tools

cargo watch -x run     # Auto-rebuild on file changes

Testing

cargo test             # Run all tests

Code Style Guidelines

Rust Best Practices

Prefer &str over String for function parameters:
// Good
fn process_title(title: &str) -> String

// Avoid
fn process_title(title: String) -> String
Use impl Trait for return types when appropriate:
fn get_cards() -> impl Iterator<Item = Card>
Use Result<T, E> for recoverable errors, panic! only for unrecoverable:
pub fn find_card(&self, id: CardId) -> KanbanResult<&Card> {
    self.cards.iter()
        .find(|c| c.id == id)
        .ok_or(KanbanError::CardNotFound(id))
}
Keep functions small and focused (< 50 lines):
// Good: focused function
fn validate_card_title(title: &str) -> KanbanResult<()> {
    if title.is_empty() {
        return Err(KanbanError::InvalidInput("Title cannot be empty"));
    }
    Ok(())
}

Project-Specific Conventions

NO COMMENTS unless:
  • Documenting public APIs
  • Explaining complex algorithms
  • Required for safety/correctness
Let the code speak for itself through clear naming and structure.
Module Organization:
  • Each file should be < 300 lines
  • Extract reusable patterns into separate modules
  • Follow existing module structure:
    • app.rs - Application state and event handling
    • ui.rs - Rendering logic
    • events.rs - Event loop and input handling
    • input.rs - Input state management
    • dialog.rs - Dialog interaction patterns
    • editor.rs - External editor integration
Type Safety:
// Leverage newtype pattern
pub struct BoardId(Uuid);
pub struct CardId(Uuid);
pub struct ColumnId(Uuid);

// Use enums for state machines
pub enum AppMode {
    Normal,
    CreateCard,
    EditCard,
}

pub enum Focus {
    BoardList,
    CardList,
    CardDetail,
}
Error Handling:
  • All public APIs return KanbanResult<T>
  • Use thiserror for error definitions
  • Provide context in error messages
  • Log errors with tracing::error!
use tracing::error;

if let Err(e) = self.execute_command(cmd) {
    error!("Failed to create card: {}", e);
    self.show_error_dialog("Failed to create card");
    return;
}
Immutability:
// Prefer immutable data
let card = Card::new(column_id, title, position);

// Use &mut only when necessary
impl Card {
    pub fn update_title(&mut self, title: String) {
        self.title = title;
        self.updated_at = Utc::now();  // Update timestamp on mutation
    }
}

Architecture Principles

See the Architecture page for detailed information on:
  • SOLID principles applied to this codebase
  • Workspace structure and dependency flow
  • Design patterns (Command, Repository, Observer)
  • Crate descriptions and responsibilities

Development Workflow

Domain-First Approach

1

Define Domain Model

Start in kanban-domain:
// crates/kanban-domain/src/card.rs
pub struct Card {
    pub id: CardId,
    pub title: String,
    pub branch_name: Option<String>,  // New field
    // ...
}
2

Implement Behavior

Add methods to domain models:
impl Card {
    pub fn update_branch_name(&mut self, branch_name: Option<String>) {
        self.branch_name = branch_name;
        self.updated_at = Utc::now();
    }
}
3

Create Command

Implement command in kanban-domain/src/commands/:
pub struct UpdateCardBranch {
    pub card_id: CardId,
    pub branch_name: Option<String>,
}

impl Command for UpdateCardBranch {
    fn execute(&mut self, ctx: &mut CommandContext) -> KanbanResult<()> {
        let card = ctx.find_card_mut(self.card_id)?;
        card.update_branch_name(self.branch_name.clone());
        Ok(())
    }
}
4

Update Application State

Add handler in kanban-tui/src/app.rs:
pub fn handle_update_branch(&mut self) {
    let (card_id, branch_name) = { /* collect data */ };
    
    let cmd = Box::new(UpdateCardBranch {
        card_id,
        branch_name,
    });
    
    if let Err(e) = self.execute_command(cmd) {
        tracing::error!("Failed to update branch: {}", e);
    }
}
5

Implement UI

Add rendering in kanban-tui/src/ui.rs:
fn render_card_branch(f: &mut Frame, card: &Card, area: Rect) {
    if let Some(branch) = &card.branch_name {
        let text = format!("Branch: {}", branch);
        let paragraph = Paragraph::new(text);
        f.render_widget(paragraph, area);
    }
}
6

Wire Up Events

Add keyboard shortcut:
KeyCode::Char('b') => self.handle_update_branch(),
Update help text in footer.

State Management & Persistence

The application uses a command pattern for all state mutations: Flow:
  1. Event Handler (kanban-tui): Processes keyboard input
  2. Command (kanban-domain): Encapsulates mutation
  3. StateManager (kanban-tui): Executes command via CommandContext
  4. CommandContext: Applies mutation to data vectors
  5. Dirty Flag: StateManager marks state as dirty
  6. Progressive Save: Auto-saves immediately via async channel
Example Pattern:
pub fn handle_create_card_key(&mut self) {
    // Collect immutable data before command execution
    let (board_id, column_id) = {
        let Some(board) = self.get_active_board() else { return };
        let Some(column) = board.columns.get(self.column_index) else { return };
        (board.id, column.id)
    };

    // Create command
    let cmd = Box::new(CreateCard {
        board_id,
        column_id,
        title: self.input.as_str().to_string(),
        priority: CardPriority::Medium,
    });

    // Execute (sets dirty flag automatically)
    if let Err(e) = self.execute_command(cmd) {
        tracing::error!("Failed to create card: {}", e);
        return;
    }
    
    // StateManager handles persistence automatically
}
Persistence Features:
  • Progressive Auto-Save: Changes saved immediately after each operation
  • Conflict Detection: Multi-instance changes detected via file metadata
  • Format Versioning: Automatic V1→V2 migration with backup creation
  • Atomic Writes: Crash-safe pattern prevents corruption
StateManager handles dirty flags and persistence automatically. You just execute commands!

Testing

Writing Tests

Tests go in the same file as implementation:
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_card_completion_toggle() {
        let mut card = Card::new(
            column_id,
            "Test".to_string(),
            0
        );
        assert_eq!(card.status, CardStatus::Todo);

        card.update_status(CardStatus::Done);
        assert_eq!(card.status, CardStatus::Done);
    }
}

Branching and Release Workflow

Branch Strategy

develop → master release workflow:
1

Feature Development

Create feature branch from develop:
git checkout develop
git pull origin develop
git checkout -b MVP-123/my-feature
2

Make Changes

Make small, atomic commits:
git commit -m "feat: add sprint filtering"
git commit -m "refactor: extract dialog logic"
3

Create Changeset

Before submitting PR:
# Auto-generate from commits (default: patch)
./scripts/create-changeset.sh

# Or specify bump type and description
./scripts/create-changeset.sh minor "Add sprint support"
4

Submit PR

Create PR to develop:
  • PR checks for changeset presence
  • Changesets accumulate in develop
5

Periodic Release

From developmaster:
  • All accumulated changesets consumed
  • Single version bump (highest precedence)
  • Automatic publish to crates.io
  • GitHub release created

Commit Messages

Use semantic commit format:
<type>: <description>

[optional body]
Types:
  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • refactor: Code refactoring
  • test: Adding/updating tests
  • chore: Maintenance tasks
  • ci: CI/CD changes
Examples:
feat: add sprint filtering to task view
fix: handle empty board state correctly
docs: update keyboard shortcuts in README
refactor: extract dialog rendering logic
Make small, atomic commits that contain one functionally related change. Each commit should compile and pass tests.
Good commit strategy:
 refactor: add handlers module
 refactor: extract navigation handlers
 refactor: extract board handlers
 refactor: simplify handle_key_event to use handlers
Bad commit strategy:
 refactor: extract all handlers and simplify app.rs (too large)
 fix: fix bugs (vague, multiple unrelated fixes)

Monorepo Versioning

All crates in this workspace maintain synchronized versions.
  • Root Cargo.toml defines workspace version: version = "X.Y.Z"
  • All crates reference via version.workspace = true
  • Cross-crate dependencies use path-only: { path = "../kanban-core" } (no version)
  • Prevents version skew during publishing
Publishing Order:
  1. kanban-core (no internal dependencies)
  2. kanban-domain (depends on kanban-core)
  3. kanban-persistence (depends on kanban-domain)
  4. kanban-tui (depends on kanban-persistence)
  5. kanban-cli (depends on all others)
  6. kanban-mcp (depends on kanban-domain)

Release Validation

Before publishing, validate the release:
# Using Nix
nix run .#validate-release

# Or directly
bash scripts/validate-release.sh
Checks:
  1. All crates use workspace versioning
  2. No hardcoded versions in path dependencies
  3. Entire workspace builds correctly
  4. Dry-run publish for each crate
  5. Dependency resolution validation
This runs automatically in CI on PRs to develop and master.

Pull Request Guidelines

Before Submitting

  • Run cargo fmt --all to format code
  • Run cargo clippy --all-targets --all-features -- -D warnings
  • Run cargo test and ensure all tests pass
  • Test manually with cargo run
  • Create changeset with ./scripts/create-changeset.sh
  • Update README.md if adding user-facing features
  • Update CLAUDE.md if changing architecture/conventions

Changesets

Create .changeset/<descriptive-name>.md:
---
bump: patch
---

Description of changes

- List of changes
Bump types:
  • patch - Bug fixes, small changes (0.1.0 → 0.1.1)
  • minor - New features, backwards compatible (0.1.0 → 0.2.0)
  • major - Breaking changes (0.1.0 → 1.0.0)
On merge to master:
  • Version automatically bumps based on changeset
  • CHANGELOG.md updates with your description
  • New version publishes to crates.io
  • GitHub release created with tag

Code Review Process

1

Automated Checks

CI runs format, clippy, tests, and validation
2

Maintainer Review

Code review and feedback from maintainers
3

Address Feedback

Update PR based on review comments
4

Merge

Once approved, maintainer merges to develop

Areas for Contribution

UI Improvements

Enhance TUI rendering, add color themes, improve layouts

Features

New metadata fields, filtering options, search improvements

Testing

Increase test coverage, add integration tests

Documentation

Improve docs, add examples, tutorials

Performance

Optimize rendering, reduce allocations, profiling

Refactoring

Extract patterns, improve modularity, simplify code

CI/CD and GitHub Secrets

Required Secrets

Configure these secrets in GitHub repository settings:
Required for: Publishing to crates.ioHow to obtain:
  1. Login to crates.io with GitHub account
  2. Go to Account Settings → API Tokens
  3. Create new token with “publish-update” scope
  4. Add to GitHub: Settings → Secrets → Actions → New repository secret
Required for: Automated git commits and tag pushesHow to generate:
ssh-keygen -t ed25519 -C "github-actions@kanban" -f deploy_key -N ""
  • Add public key (deploy_key.pub) to GitHub: Settings → Deploy keys → Add (with write access)
  • Add private key (deploy_key) to GitHub: Settings → Secrets → Actions → New repository secret

CI/CD Workflows

ci.yml - Runs on all pushes and PRs:
  • Format check (cargo fmt)
  • Linter (cargo clippy)
  • Tests (cargo test)
  • Build validation
  • Changeset validation (PRs to develop)
release.yml - Runs on push to master:
  • Checks for changesets (skips if none)
  • Bumps version based on changesets
  • Updates CHANGELOG.md
  • Publishes to crates.io
  • Creates GitHub release with tag

Questions?

Bug Reports

Open an issue for bugs

Feature Requests

Suggest new features

Discussions

Ask design questions

License

By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.
Check existing issues and discussions before starting work to avoid duplication!

Build docs developers (and LLMs) love