#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
use crate::integrations::fzf::is_fzf_available;
use crate::integrations::gh::{GhClient, RealGhClient};
use crate::integrations::tmux::{RealTmuxLauncher, TmuxLauncher};
use crate::integrations::zoxide::is_zoxide_available;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct TemplateContext {
pub gh_available: bool,
pub zoxide_available: bool,
pub fzf_available: bool,
pub tmux_available: bool,
}
impl TemplateContext {
pub fn detect() -> Self {
Self {
gh_available: RealGhClient.is_available(),
zoxide_available: is_zoxide_available(),
fzf_available: is_fzf_available(),
tmux_available: RealTmuxLauncher.detect().is_ok(),
}
}
pub fn generate_global(&self) -> String {
let zoxide_section = if self.zoxide_available {
"[integration.zoxide]
# Enable automatic zoxide integration
# When true, new worktrees are automatically added to zoxide database
enabled = true"
} else {
"[integration.zoxide]
# Enable automatic zoxide integration
# zoxide not detected - install from https://github.com/ajeetdsouza/zoxide
enabled = false"
};
let fzf_section = if self.fzf_available {
"[integration.fzf]
# Enable fzf integration for interactive worktree selection
# When enabled, running `ofsht cd` or `ofsht rm` without arguments
# will launch fzf for interactive selection
enabled = true
# Additional fzf command-line options (optional)
# options = [\"--height=50%\", \"--border\", \"--reverse\"]"
} else {
"[integration.fzf]
# Enable fzf integration for interactive worktree selection
# fzf not detected - install from https://github.com/junegunn/fzf
enabled = false"
};
let tmux_section = if self.tmux_available {
"[integration.tmux]
# Configure tmux integration behavior
# behavior: \"auto\" (use --tmux flag), \"always\" (always enabled), \"never\" (disabled)
behavior = \"auto\"
# Determines what to create: a new window or split pane
create = \"window\" # \"window\" or \"pane\""
} else {
"[integration.tmux]
# Configure tmux integration behavior
# tmux not detected - install from https://github.com/tmux/tmux
behavior = \"never\"
create = \"window\""
};
let gh_section = if self.gh_available {
"[integration.gh]
# Enable GitHub CLI (gh) integration
# When enabled, `ofsht add #123` will create worktrees from GitHub issues/PRs
# Requires the gh CLI to be installed (https://cli.github.com/)
enabled = true"
} else {
"[integration.gh]
# Enable GitHub CLI (gh) integration
# gh CLI not detected - install from https://cli.github.com/
enabled = false"
};
format!(
r#"# ofsht global configuration
# This file contains default settings applied to all repositories.
# Project-specific settings in .ofsht.toml will override these values.
[worktree]
# Directory template for new worktrees
# Variables: {{repo}} = repository name, {{branch}} = branch name
# Relative paths are resolved from the main repository root
dir = "../{{repo}}-worktrees/{{branch}}"
[hooks.create]
# Commands to run after creating a worktree (executed in worktree directory)
run = [
# "pnpm install",
]
# Files to copy from main repository to new worktree
copy = [
# ".env.local",
# ".vscode/settings.json",
]
# Files to symlink from main repository to new worktree
# Supports glob patterns: "*.env", "config/**/*.json"
link = [
# ".claude/settings.local.json",
]
[hooks.delete]
# Commands to run before deleting a worktree (executed in worktree directory)
run = [
# "pnpm store prune",
]
{zoxide_section}
{fzf_section}
{tmux_section}
{gh_section}
"#
)
}
#[allow(clippy::unused_self)]
pub fn generate_local(&self) -> String {
r#"# ofsht project configuration
# Location: .ofsht.toml (main repository root)
#
# This file is ALWAYS loaded from the main repository root, even when
# running ofsht commands from worktrees. This ensures consistent behavior
# across all worktrees.
#
# This file overrides global settings for this specific repository.
# Add this file to .gitignore if settings are user-specific,
# or commit it if settings should be shared with the team.
[hooks.create]
# Commands to run after creating a worktree
run = [
# "pnpm install",
]
# Files to copy from main repository
copy = [
# ".env.local",
]
# Files to symlink (supports glob patterns)
link = [
# ".claude/settings.local.json",
]
[hooks.delete]
# Commands to run before deleting a worktree
run = []
"#
.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_all_tools_available() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
assert!(ctx.gh_available);
assert!(ctx.zoxide_available);
assert!(ctx.fzf_available);
assert!(ctx.tmux_available);
}
#[test]
fn test_detect_some_tools_unavailable() {
let ctx = TemplateContext {
gh_available: false,
zoxide_available: true,
fzf_available: false,
tmux_available: true,
};
assert!(!ctx.gh_available);
assert!(ctx.zoxide_available);
assert!(!ctx.fzf_available);
assert!(ctx.tmux_available);
}
#[test]
fn test_generate_global_all_enabled() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_global();
assert!(template.contains("[integration.zoxide]"));
assert!(template.contains("[integration.fzf]"));
assert!(template.contains("[integration.tmux]"));
assert!(template.contains("[integration.gh]"));
assert!(template.contains("enabled = true"));
assert!(template.contains(".claude/settings.local.json"));
assert!(!template.contains("node_modules"));
assert!(!template.contains("Note: copy and link actions are not supported in delete hooks"));
}
#[test]
fn test_generate_global_gh_disabled() {
let ctx = TemplateContext {
gh_available: false,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_global();
assert!(template.contains("[integration.gh]"));
assert!(template.contains("enabled = false"));
assert!(template.contains("https://cli.github.com"));
assert!(template.contains("[integration.zoxide]"));
assert!(template.contains("enabled = true"));
}
#[test]
fn test_generate_global_uses_claude_example() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_global();
assert!(template.contains(".claude/settings.local.json"));
assert!(!template.contains("# \"node_modules\""));
}
#[test]
fn test_generate_local_no_worktree_section() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_local();
assert!(!template.contains("[worktree]"));
}
#[test]
fn test_generate_local_has_hooks_only() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_local();
assert!(template.contains("[hooks.create]"));
assert!(template.contains("[hooks.delete]"));
assert!(!template.contains("[integration.zoxide]"));
assert!(!template.contains("[integration.fzf]"));
assert!(!template.contains("[integration.tmux]"));
assert!(!template.contains("[integration.gh]"));
}
#[test]
fn test_generate_local_uses_claude_example() {
let ctx = TemplateContext {
gh_available: true,
zoxide_available: true,
fzf_available: true,
tmux_available: true,
};
let template = ctx.generate_local();
assert!(template.contains(".claude/settings.local.json"));
assert!(!template.contains("# \"node_modules\""));
}
}