Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ crates/
│ ├── api_client.rs # HTTP client for server communication
│ ├── auth.rs # Register, login, logout, status commands
│ ├── credentials.rs # Encryption key and token management
│ ├── config.rs # ~/.taskbook.json configuration (with sync section)
│ ├── config.rs # config file (XDG path, with sync section)
│ ├── directory.rs # Taskbook directory resolution
│ ├── render.rs # Terminal output with colored formatting
│ ├── editor.rs # External editor support for notes
Expand Down Expand Up @@ -110,9 +110,13 @@ crates/
5. **Directory Resolution Priority**:
- `--taskbook-dir` CLI flag (highest)
- `TASKBOOK_DIR` environment variable
- `~/.taskbook.json` config file
- config file (lowest)
- Default `~/.taskbook/` (lowest)
Comment on lines 110 to 114

The config file is resolved as `~/.config/taskbook/taskbook.json`
(honoring `$XDG_CONFIG_HOME`), falling back to the legacy
`~/.taskbook.json` when it exists and the XDG file does not.

6. **Storage Structure (Local)**:
```
~/.taskbook/
Expand Down Expand Up @@ -167,7 +171,8 @@ tb --mcp # Run as MCP stdio server
`tb --mcp` runs a Model Context Protocol server over stdio (newline-delimited
JSON-RPC 2.0, no extra dependencies). It uses the configured storage backend:
the remote server storage (with client-side encryption) when `sync.enabled =
true` in `~/.taskbook.json`, local files otherwise.
true` in the config file (`~/.config/taskbook/taskbook.json`), local files
otherwise.

Register it in Claude Code:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ See [Server Setup](docs/server.md) for running your own server.

## Configuration

Configuration is stored in `~/.taskbook.json`:
Configuration is stored in `~/.config/taskbook/taskbook.json` (honoring `$XDG_CONFIG_HOME`; a legacy `~/.taskbook.json` is still used if present):

```json
{
Expand Down
6 changes: 5 additions & 1 deletion crates/taskbook-client/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ pub fn migrate(taskbook_dir: Option<PathBuf>) -> Result<()> {
);
println!(
"{}",
"To enable sync, set sync.enabled = true in ~/.taskbook.json".dimmed()
format!(
"To enable sync, set sync.enabled = true in {}",
crate::config::Config::config_file_path().display()
)
.dimmed()
);

Ok(())
Expand Down
121 changes: 114 additions & 7 deletions crates/taskbook-client/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use crate::error::Result;
use crate::tui::ViewMode;
Expand Down Expand Up @@ -266,17 +267,57 @@ impl Default for Config {
}

impl Config {
/// Get the config file path (~/.taskbook.json)
fn config_file_path() -> PathBuf {
dirs::home_dir()
.expect("Could not find home directory")
.join(".taskbook.json")
/// Resolve the config file path.
///
/// Prefers the XDG location `~/.config/taskbook/taskbook.json` (honoring
/// `$XDG_CONFIG_HOME`). Falls back to the legacy `~/.taskbook.json` when it
/// exists and the XDG file does not, so existing installs keep working.
/// New installs use the XDG location.
pub fn config_file_path() -> PathBuf {
let home = dirs::home_dir().expect("Could not find home directory");
let xdg = Self::xdg_config_path(&home, std::env::var_os("XDG_CONFIG_HOME").as_deref());
let legacy = Self::legacy_config_path(&home);
Self::select_config_path(xdg, legacy)
}

/// Ensure the config file exists, creating it with defaults if not
/// XDG config path: `$XDG_CONFIG_HOME/taskbook/taskbook.json`, or
/// `~/.config/taskbook/taskbook.json` when the env var is unset/empty.
///
/// `~/.config` is used on every platform (not the OS-native config dir) to
/// match the conventional Linux location users expect.
fn xdg_config_path(home: &Path, xdg_env: Option<&OsStr>) -> PathBuf {
let base = match xdg_env.filter(|v| !v.is_empty()) {
Some(v) => PathBuf::from(v),
None => home.join(".config"),
};
base.join("taskbook").join("taskbook.json")
}
Comment on lines +288 to +294

/// Legacy pre-XDG config path: `~/.taskbook.json`.
fn legacy_config_path(home: &Path) -> PathBuf {
home.join(".taskbook.json")
}

/// Choose between the XDG and legacy paths: prefer XDG, fall back to an
/// existing legacy file, otherwise default to XDG for new installs.
fn select_config_path(xdg: PathBuf, legacy: PathBuf) -> PathBuf {
if xdg.exists() {
xdg
} else if legacy.exists() {
legacy
} else {
xdg
}
}
Comment on lines +303 to +311

/// Ensure the config file (and any parent directory) exists, creating it
/// with defaults if not.
fn ensure_config_file() -> Result<()> {
let config_path = Self::config_file_path();
if !config_path.exists() {
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
Comment on lines 315 to +319
}
let default_config = Config::default();
let data = serde_json::to_string_pretty(&default_config)?;
fs::write(&config_path, data)?;
Expand Down Expand Up @@ -337,6 +378,9 @@ impl Config {
/// Save the configuration to file
pub fn save(&self) -> Result<()> {
let config_path = Self::config_file_path();
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let data = serde_json::to_string_pretty(self)?;
fs::write(&config_path, data)?;
Ok(())
Expand Down Expand Up @@ -388,4 +432,67 @@ mod tests {
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.default_view, ViewMode::Board);
}

#[test]
fn xdg_path_defaults_to_dot_config() {
let home = Path::new("/home/alice");
assert_eq!(
Config::xdg_config_path(home, None),
PathBuf::from("/home/alice/.config/taskbook/taskbook.json")
);
}

#[test]
fn xdg_path_honors_env_var() {
let home = Path::new("/home/alice");
assert_eq!(
Config::xdg_config_path(home, Some(OsStr::new("/custom/xdg"))),
PathBuf::from("/custom/xdg/taskbook/taskbook.json")
);
}

#[test]
fn xdg_path_ignores_empty_env_var() {
let home = Path::new("/home/alice");
assert_eq!(
Config::xdg_config_path(home, Some(OsStr::new(""))),
PathBuf::from("/home/alice/.config/taskbook/taskbook.json")
);
}

#[test]
fn legacy_path_is_dot_taskbook_json() {
let home = Path::new("/home/alice");
assert_eq!(
Config::legacy_config_path(home),
PathBuf::from("/home/alice/.taskbook.json")
);
}

#[test]
fn select_config_path_resolution() {
let dir = std::env::temp_dir().join(format!("tb_cfg_select_{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let xdg = dir.join("xdg.json");
let legacy = dir.join("legacy.json");
fs::write(&xdg, "{}").unwrap();
fs::write(&legacy, "{}").unwrap();
let missing_xdg = dir.join("missing_xdg.json");
let missing_legacy = dir.join("missing_legacy.json");

// XDG present wins, even when legacy also exists.
assert_eq!(Config::select_config_path(xdg.clone(), legacy.clone()), xdg);
// XDG absent but legacy present -> legacy (existing installs keep working).
assert_eq!(
Config::select_config_path(missing_xdg.clone(), legacy.clone()),
legacy
);
// Neither present -> XDG location for new installs.
assert_eq!(
Config::select_config_path(missing_xdg.clone(), missing_legacy),
missing_xdg
);

fs::remove_dir_all(&dir).unwrap();
}
}
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ tb --begin 2
└── archive/
└── archive.json # Archived items

~/.taskbook.json # Configuration file
~/.config/taskbook/taskbook.json # Configuration file (legacy: ~/.taskbook.json)
~/.taskbook/credentials.json # Server credentials (when using sync)
```

Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Configuration

Client configuration is stored in `~/.taskbook.json`. The file is created automatically with default values on first run.
Client configuration is stored in `~/.config/taskbook/taskbook.json` (honoring `$XDG_CONFIG_HOME`). The file is created automatically with default values on first run.

For backward compatibility, if a legacy `~/.taskbook.json` exists and the XDG file does not, the legacy file is used. To migrate, move `~/.taskbook.json` to `~/.config/taskbook/taskbook.json`.
Comment on lines +3 to +5

## Configuration File

Expand Down
2 changes: 1 addition & 1 deletion docs/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ This is by design - the server cannot decrypt your data without your key.

## Disabling Sync Temporarily

To work locally without syncing, edit `~/.taskbook.json`:
To work locally without syncing, edit `~/.config/taskbook/taskbook.json` (or the legacy `~/.taskbook.json` if you still use it):

```json
{
Expand Down
Loading