<div align="center">
# doti
### A fast, interactive TUI for managing your dotfiles
Copy-based sync · Secret scanning · No symlink mess
[](https://crates.io/crates/doti)
[](LICENSE)
<br>
<img src="assets/demo.gif" alt="doti demo" width="720">
<br>
</div>
## Why doti?
Most dotfile managers either scatter symlinks across your system or require you to learn a DSL. **doti** takes a different approach:
- Your configs live in a `home/` directory that mirrors `$HOME`
- Changes are **copied**, never symlinked -- your system files stay untouched until you say so
- An interactive TUI lets you browse, diff, and sync files with a few keystrokes
- A built-in secret scanner catches leaked keys before you push
## Features
| **Interactive TUI** | Browse tracked and untracked files in a tree view with live diffs |
| **Copy-based sync** | No symlinks, no magic -- explicit file copies in either direction |
| **Inline diffs** | Side-by-side diff of repo vs system changes, right in the terminal |
| **Secret scanning** | Detects API keys, tokens, passwords, and private keys before they leak |
| **Lazy loading** | Untracked directories scanned on demand for fast startup |
| **Backup on apply** | Optionally back up system files before overwriting |
| **Run from anywhere** | Config file remembers your repo path after `doti init` |
## Installation
### From source
> **Requires:** [Rust toolchain](https://rustup.rs) (1.85+)
```bash
git clone https://github.com/volcmen/doti.git
cd doti
cargo install --path .
```
### From crates.io
```bash
cargo install doti
```
## Quick start
```bash
# 1. Go to your dotfiles repo and initialize
cd ~/my-dotfiles
doti init
# 2. Launch the TUI (works from anywhere after init)
doti
```
`doti init` creates a config at `~/.config/doti/config.toml` and ensures the `home/` directory exists in your repo.
## Usage
### Commands
| `doti` | Launch the interactive TUI |
| `doti init` | Set up config (run from your dotfiles repo) |
| `doti scan` | Scan for leaked secrets |
| `doti help` | Show help |
### TUI overview
The TUI has two tabs:
**Tracked** -- files in your repo, compared against the system:
| `✓` | Synced | Repo and system files are identical |
| `⚠` | Differs | Content has changed -- diff shown in panel |
| `✗` | Absent | In repo but missing from system |
**Untracked** -- system files not yet in the repo. Directories load lazily when expanded.
### Key bindings
<details>
<summary>Full key reference (press <code>?</code> in TUI)</summary>
#### Navigation
| `j` / `k` , `↑` / `↓` | Move cursor |
| `g` / `G` | Jump to top / bottom |
| `h` / `←` | Collapse directory or go to parent |
| `→` / `Space` | Expand directory |
| `Enter` | Expand directory / open action popup |
| `Tab` | Switch between Tracked and Untracked |
#### Tracked tab
| `l` | **Apply** -- copy repo to system |
| `p` | **Pull** -- copy system to repo |
| `b` | **Backup** system file, then apply |
| `d` | **Delete** from repo (system untouched) |
#### Untracked tab
| `a` | **Add** -- copy system file into repo |
#### Panel & general
| `J` / `K` | Scroll diff / preview |
| `PgDn` / `PgUp` | Scroll faster |
| `r` | Refresh all |
| `?` | Toggle help overlay |
| `q` / `Esc` | Quit |
</details>
## How it works
Your dotfiles repo has a `home/` directory that mirrors `$HOME`:
```
my-dotfiles/
└── home/
├── .config/
│ ├── hypr/hyprland.conf → ~/.config/hypr/hyprland.conf
│ ├── kitty/kitty.conf → ~/.config/kitty/kitty.conf
│ └── fish/config.fish → ~/.config/fish/config.fish
├── .bashrc → ~/.bashrc
└── .gitconfig → ~/.gitconfig
```
| **Add** | system → repo | Untouched |
| **Pull** | system → repo | Untouched |
| **Apply** | repo → system | Overwritten |
| **Backup + Apply** | repo → system | Backed up first |
| **Delete** | removes from repo | Untouched |
## Configuration
Config location: `~/.config/doti/config.toml`
```toml
[repo]
path = "~/dotfiles"
```
If you run `doti` inside a git repo, it uses that repo directly. Otherwise it falls back to the config file. This lets you run `doti` from anywhere after `doti init`.
## Secret scanning
```bash
doti scan
```
Scans all git-tracked files for:
- Private keys (`-----BEGIN ... PRIVATE KEY`)
- AWS access keys (`AKIA...`)
- GitHub / GitLab tokens
- OpenAI / Anthropic API keys
- Slack tokens
- Generic secrets (passwords, API keys with assigned values)
Binary files and `.gitignore`'d paths are skipped automatically.
Exits with code `1` if secrets are found -- useful in CI or as a pre-commit hook.
## Project structure
```
src/
├── main.rs Entry point & CLI routing
├── lib.rs Library root
├── config.rs Config file loading & saving
├── link.rs File sync operations (copy, compare)
├── scan.rs Directory scanning & tracking
├── tree.rs Tree data structure for UI
├── ui.rs Ratatui TUI rendering
├── secrets.rs Secret scanning rules
└── app/
├── mod.rs Core application state & event loop
├── input.rs Keyboard input handling
├── ops.rs File operations (add, apply, pull, delete)
├── panel.rs Diff panel logic
└── popup.rs Action popup handling
```
## Contributing
Contributions welcome! Feel free to open issues or submit pull requests.
```bash
cargo run -- help # Show CLI help
cargo run # Launch TUI in dev mode
cargo run -- scan # Test secret scanning
```
## License
[MIT](LICENSE)