Contributing to Focust

January 29, 2026 · View on GitHub

Thank you for your interest in contributing to Focust! Whether you're fixing bugs, adding features, improving documentation, or helping with translations, your contributions are warmly welcomed.

Table of Contents


Getting Started

Prerequisites

Before you start contributing, make sure you have:

  1. Git - Download Git
  2. Node.js (v18+) or Bun (recommended) - Download Bun
  3. Rust (latest stable) - Install Rustup
  4. Just (optional but recommended) - cargo install just
  5. Platform-specific dependencies - See README.md

Fork and Clone

  1. Fork the repository on GitHub
  2. Clone your fork:
    git clone https://github.com/YOUR_USERNAME/Focust.git
    cd Focust
    
  3. Add upstream remote:
    git remote add upstream https://github.com/pilgrimlyieu/Focust.git
    

Initial Setup

# Install dependencies
bun install

# Setup Rust dependencies
cd src-tauri
cargo check
cd ..

# Or use Just
just setup

Development Environment

Visual Studio Code with the following extensions:

Running the Development Server

# Start with hot-reload
bun run tauri dev

# Or use Just
just dev

The settings window should be opened in tray tab. To test break windows:

  1. In the settings UI, adjust break intervals to short durations (e.g., 30 seconds)
  2. Wait for the break to trigger, or use the "Trigger Break" test command in development

Project Structure

Focust/
├── src/                    # Frontend (Vue 3 + TypeScript)
│   ├── components/         # Vue components (settings, UI, icons)
│   ├── stores/             # Pinia state management
│   ├── views/              # Main application views
│   ├── i18n/               # Internationalization
│   ├── types/              # Type definitions (including generated/)
│   └── utils/              # Utility functions

├── src-tauri/              # Backend (Rust + Tauri)
│   ├── src/
│   │   ├── cmd/            # Tauri command handlers
│   │   ├── config/         # Configuration system
│   │   ├── core/           # Business logic (audio, schedule, theme, etc.)
│   │   ├── scheduler/      # Break scheduling engine
│   │   ├── monitors/       # Environment monitoring (idle, DND, etc.)
│   │   ├── platform/       # Platform integrations (tray, hotkeys, notifications)
│   │   └── utils/          # Utility functions
│   └── assets/sounds/      # Built-in audio files

├── docs/                   # Documentation
│   ├── ARCHITECTURE.md     # System architecture
│   ├── CONFIGURATION.md    # Configuration reference
│   └── QUICKSTART.md       # Quick start guide

├── justfile                # Command definitions
└── README.md               # Project readme

For detailed architecture information, see docs/ARCHITECTURE.md.


Development Workflow

Creating a New Branch

Always create a new branch for your work:

# Update your fork
git checkout main
git pull upstream main

# Create a feature branch
git checkout -b feature/your-feature-name
# or
git checkout -b fix/issue-number-description

Syncing with Upstream

Regularly sync your fork with the upstream repository:

git fetch upstream
git checkout main
git merge upstream/main
git push origin main

Running Tests

Before submitting changes, run all tests:

# Run all tests
just test-all

# Run frontend tests only
just test-front-all

# Run backend tests only
just test-back-all

# Run specific test
just test-back scheduler_test

Code Quality Checks

Before committing, ensure your code meets quality standards:

# Format code
just format

# Run linters
just lint

# Type checking
just check

# Or run all at once
just pre-commit

Coding Standards

General Principles

  1. Write clean, readable code: Prioritize clarity over cleverness
  2. Comment complex logic: Explain the "why", not just the "what"
  3. Keep functions small: Each function should do one thing well
  4. Use meaningful names: Variables, functions, and types should be self-documenting
  5. Test your code: Add tests for new features and bug fixes

Language Standards

Rust Code

Style:

Naming Conventions:

// Types: PascalCase
struct AppConfig { }
enum AudioSource { }

// Functions and variables: snake_case
fn load_config() -> Result<AppConfig> { }
let user_name = "Alice";

// Constants: SCREAMING_SNAKE_CASE
const MAX_RETRIES: u32 = 3;

// Private fields: prefix with underscore if unused
struct Internal {
    _unused_field: String,
}

Error Handling:

// Use Result<T, E> for recoverable errors
fn parse_config(path: &str) -> Result<AppConfig, ConfigError> {
    // ...
}

// Use ? operator for error propagation
let config = load_file(path)?;

// Avoid unwrap() in library code
// In tests and examples, unwrap() is acceptable
#[cfg(test)]
fn parsing_works() {
    let config = parse_config("test.toml").unwrap();
}

Async Code:

// Use async/await for asynchronous operations
async fn fetch_data() -> Result<Data> {
    // Implementation
}

Type Exports for TypeScript:

use serde::{Deserialize, Serialize};
use ts_rs::TS;

#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]  // JavaScript convention
#[ts(export, rename_all = "camelCase")]
pub struct AppConfig {
    pub autostart: bool,
    pub theme_mode: String,
}

Documentation:

/// Loads the application configuration from disk.
///
/// Loads from platform-specific config directory, or creates default if not found.
///
/// # Arguments
/// * `app_handle` - Handle to the Tauri application
///
/// # Returns
/// The loaded or default configuration
pub async fn load_config(app_handle: &AppHandle) -> AppConfig {
    // Implementation
}

TypeScript/Vue Code

Style:

  • Use Biome for formatting and linting (configured in biome.json)
  • Use TypeScript strict mode

Naming Conventions:

// Types and Interfaces: PascalCase
interface UserConfig {
  userName: string;
}
type AudioSource = "builtin" | "file";

// Functions and variables: camelCase
function loadConfig(): UserConfig { }
const userName = "Alice";

// Constants: SCREAMING_SNAKE_CASE
const MAX_RETRIES = 3;

// Components: PascalCase (Vue SFC files)
// SettingsPanel.vue, BreakWindow.vue

Vue 3 Composition API:

<script setup lang="ts">
import { ref, computed } from "vue";
import type { AppConfig } from "@/types";

// Define props with TypeScript
interface Props {
  config: AppConfig;
  readonly?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  readonly: false,
});

// Reactive state and computed values
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>

<template>
  <div>
    <p>{{ count }} × 2 = {{ doubled }}</p>
  </div>
</template>

State Management (Pinia):

import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useConfigStore = defineStore("config", () => {
  // State
  const config = ref<AppConfig | null>(null);

  // Getters (computed values)
  const isDarkMode = computed(() => config.value?.themeMode === "dark");

  // Actions (functions that modify state)
  async function loadConfig() {
    config.value = await invoke<AppConfig>("get_config");
  }

  return { config, isDarkMode, loadConfig };
});

Error Handling:

// Handle errors gracefully with try-catch
try {
  await invoke("save_config", { config });
  showToast("success", "Settings saved");
} catch (error) {
  console.error("Failed to save config:", error);
  showToast("error", "Failed to save settings");
}

// Use type guards for runtime type checking
import { isPromptPayload } from "@/types";

if (isPromptPayload(data)) {
  startBreak(data); // TypeScript knows data type
}

Documentation:

/**
 * Loads the user configuration from the backend.
 *
 * @returns A promise that resolves to the user configuration
 * @throws Error if configuration cannot be loaded
 */
export async function loadUserConfig(): Promise<UserConfig> {
  return await invoke<UserConfig>("get_user_config");
}

Testing Standards

Key Points:

  • Add tests for new features and bug fixes
  • Test edge cases and error handling
  • Use descriptive test names
  • Keep tests simple and focused
  • Check existing tests for patterns

Rust Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn basic_basic_functionality_works() {
        let result = calculate(5);
        assert_eq!(result, expected_value);
    }

    // For async tests, use tokio::test
    #[tokio::test]
    async fn async_function_works() {
        let data = fetch_data().await.unwrap();
        assert!(!data.is_empty());
    }
}

TypeScript/Vue Tests

import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import MyComponent from "@/components/MyComponent.vue";

describe("MyComponent", () => {
  it("renders with props", () => {
    const wrapper = mount(MyComponent, { props: { title: "Test" } });
    expect(wrapper.text()).toContain("Test");
  });

  it("emits events on interaction", async () => {
    const wrapper = mount(MyComponent);
    await wrapper.find("button").trigger("click");
    expect(wrapper.emitted("submit")).toBeTruthy();
  });
});

Commit Messages

We follow the Conventional Commits specification:

<type>(<scope>): <subject>

<body>

<footer>

Types

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code formatting (no logic changes)
  • refactor: Code refactoring
  • perf: Performance improvements
  • test: Test additions or modifications
  • chore: Build process, dependencies, tooling

Scope

Optional, indicates the area of change:

  • config: Configuration system
  • scheduler: Break scheduler
  • ui: User interface
  • audio: Audio system
  • tray: System tray
  • i18n: Internationalization

Examples

feat(scheduler): add postpone break functionality

Implement the ability to postpone breaks by a configurable duration.
Users can now use the postpone button or global hotkey to delay
the next break.

Closes #42

---

fix(tray): fix tray menu not updating on pause

The tray menu was not reflecting the paused state correctly.
Fixed by adding proper state synchronization.

Fixes #38

---

docs(architecture): update backend architecture documentation

Added details about the event-driven scheduler and improved
the module organization diagrams.

---

chore(deps): update tauri to 2.9.2

Updated Tauri and related plugins to the latest versions.

Pull Request Process

Before Submitting

  1. Update your branch with the latest upstream changes
  2. Run all tests and ensure they pass
  3. Format and lint your code
  4. Update documentation if needed
  5. Add tests for new features or bug fixes

Submitting a PR

  1. Push your branch to your fork:

    git push origin feature/your-feature
    
  2. Open a Pull Request on GitHub:

    • Use a clear, descriptive title
    • Reference related issues (e.g., "Closes #123", "Fixes #456")
    • Describe your changes in detail
    • Add screenshots for UI changes
    • List any breaking changes
  3. Fill out the PR template:

    • The PR template will automatically appear when you create a pull request
    • Complete all relevant sections (description, type of change, testing, checklist)
    • Check all applicable boxes in the checklist before requesting review
    • See .github/pull_request_template.md for the full template

Review Process

  1. Automated Checks: CI/CD will run tests and linting
  2. Code Review: Maintainers will review your code
  3. Feedback: Address any requested changes
  4. Approval: Once approved, your PR will be merged

After Merging

  1. Delete your branch (optional):

    git branch -d feature/your-feature
    git push origin --delete feature/your-feature
    
  2. Update your local main:

    git checkout main
    git pull upstream main
    

Areas Needing Help

High Priority

  • Platform Testing: Test on macOS and Linux, report platform-specific issues
  • Bug Fixes: Check open issues labeled bug
  • Documentation: Improve existing docs, add missing documentation

Medium Priority

  • Translations: Add support for new languages
  • UI/UX Improvements: Design enhancements, accessibility improvements
  • Performance: Profile and optimize slow operations
  • Test Coverage: Add tests for untested code paths

Feature Requests

Check open issues labeled enhancement or feature-request. Feel free to propose new features via discussions.


Getting Help

Resources

Communication

  • GitHub Issues: Report bugs or request features
  • GitHub Discussions: Ask questions, share ideas
  • Code Comments: Check inline comments for context
  • PR Comments: Ask questions during code review

Tips for New Contributors

  1. Start small: Fix typos, improve documentation, add tests
  2. Read existing code: Understand the patterns and conventions
  3. Ask questions: Don't hesitate to ask for help
  4. Be patient: Code review takes time
  5. Learn from feedback: Use reviews as learning opportunities

Development Tips

Quick Commands (Just)

# Pre-commit checks (format, lint, check)
just pre-commit-checks

# Watch mode for tests
bun run test:ui         # Frontend tests with UI
cargo watch -x test     # Backend tests (requires cargo-watch)

# Clean and rebuild
just clean
just build

Debugging

Rust Backend:

// Add debug logs
tracing::debug!("Variable value: {value:?}");
tracing::info!("Processing started");
tracing::warn!("Potential issue detected");
tracing::error!("Operation failed: {error}");

Frontend:

// Console debugging
console.log("[Component] State:", state);
console.warn("[Store] Invalid data:", data);

// Vue Devtools
// Install Vue Devtools browser extension for reactive debugging

Common Issues

Issue: Types out of sync

# Regenerate TypeScript types from Rust
cd src-tauri
cargo test export_bindings

Issue: Build fails after pulling changes

# Clean and reinstall dependencies
just clean
bun install
cargo clean
just setup

Issue: Hot reload not working

# Restart dev server
# Kill any running tauri dev processes
just dev

License

By contributing to Focust, you agree that your contributions will be licensed under the same license as the project (GPL-3.0 License).


Thank you for contributing to Focust! Your efforts help make break reminders better for everyone. 🎉