Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] - 2026-06-17

### Added

- New `cheznav info` subcommand for quickly diagnosing your setup. It prints the cheznav and chezmoi versions, whether chezmoi is installed, the detected chezmoi config file, and whether a source directory is initialized. It runs non-interactively (never launches the TUI), which also gives packagers a real command to smoke-test the install against.

## [0.3.0] - 2026-06-15

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ yay -S cheznav
cheznav # normal
cheznav --dry-run # pass -n to all chezmoi calls
cheznav --chezmoi-config ~/.config/chezmoi/work.toml # use custom chezmoi config file
cheznav info # print cheznav/chezmoi versions and config status, then exit
```

<details>
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cheznav"
version = "0.3.0"
version = "0.4.0"
description = "TUI for chezmoi"
requires-python = ">=3.14"
dependencies = [
Expand Down
54 changes: 54 additions & 0 deletions src/cheznav/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Non-interactive `cheznav info` command.

Prints the cheznav and chezmoi versions and chezmoi's config status without
launching the TUI, so it is safe to run in scripts, CI, and packaging smoke
tests (e.g. Homebrew).
"""

import os
import shutil
import subprocess
from pathlib import Path

from cheznav import __version__


def _find_chezmoi_config() -> tuple[Path | None, Path]:
"""Locate chezmoi's config file in its default config directory."""
xdg = os.environ.get("XDG_CONFIG_HOME")
search_dir = (Path(xdg) if xdg else Path.home() / ".config") / "chezmoi"
for name in ("chezmoi.toml", "chezmoi.yaml", "chezmoi.yml", "chezmoi.json", "chezmoi.jsonc"):
candidate = search_dir / name
if candidate.is_file():
return candidate, search_dir
return None, search_dir


def print_info(config_path: str | None = None) -> None:
"""Print cheznav/chezmoi versions and chezmoi's config status, then return."""
print(f"cheznav {__version__}")

chezmoi_bin = shutil.which("chezmoi")
if chezmoi_bin is None:
print("chezmoi: not found on PATH (install from https://www.chezmoi.io)")
return

version_proc = subprocess.run([chezmoi_bin, "--version"], capture_output=True, text=True, check=False)
version_line = next(iter(version_proc.stdout.splitlines()), "").strip() or "unknown version"
print(f"chezmoi: {version_line} ({chezmoi_bin})")

if config_path:
config = Path(config_path)
config_file = config if config.is_file() else None
search_dir = config
else:
config_file, search_dir = _find_chezmoi_config()
print(f"config: {config_file}" if config_file else f"config: none found (looked in {search_dir})")

prefix = [chezmoi_bin, "--config", config_path] if config_path else [chezmoi_bin]
source_proc = subprocess.run([*prefix, "source-path"], capture_output=True, text=True, check=False)
source = Path(source_proc.stdout.strip()) if source_proc.returncode == 0 and source_proc.stdout.strip() else None
if source and source.is_dir():
print(f"source: {source} ({'git repo' if (source / '.git').exists() else 'directory'})")
else:
print("source: not initialized")
9 changes: 9 additions & 0 deletions src/cheznav/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from cheznav import THEME, chezmoi
from cheznav.chezmoi import ManagedEntry
from cheznav.info import print_info
from cheznav.utils import is_binary, is_binary_content
from cheznav.widgets import (
ActionMenu,
Expand Down Expand Up @@ -818,8 +819,16 @@ def main() -> None:
metavar="PATH",
help="Use a custom chezmoi config file (equivalent to chezmoi --config PATH)",
)
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("info", help="Print cheznav/chezmoi versions and config status, then exit")
args = parser.parse_args()

config_path = str(Path(args.chezmoi_config).expanduser()) if args.chezmoi_config else None

if args.command == "info":
print_info(config_path)
return

chezmoi.set_config_path(config_path)
CheznavApp(dry_run=args.dry_run).run()

Expand Down
230 changes: 115 additions & 115 deletions tests/__snapshots__/test_snapshots/test_add_flags.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 112 additions & 112 deletions tests/__snapshots__/test_snapshots/test_command_palette.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 112 additions & 112 deletions tests/__snapshots__/test_snapshots/test_context_menu_home.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 112 additions & 112 deletions tests/__snapshots__/test_snapshots/test_context_menu_managed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
214 changes: 107 additions & 107 deletions tests/__snapshots__/test_snapshots/test_git_ahead_behind_header.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 106 additions & 106 deletions tests/__snapshots__/test_snapshots/test_git_dirty_indicators.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 106 additions & 106 deletions tests/__snapshots__/test_snapshots/test_git_uncommitted_header.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
222 changes: 111 additions & 111 deletions tests/__snapshots__/test_snapshots/test_help_screen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 106 additions & 106 deletions tests/__snapshots__/test_snapshots/test_main_view.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
214 changes: 107 additions & 107 deletions tests/__snapshots__/test_snapshots/test_managed_expand_dir.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 106 additions & 106 deletions tests/__snapshots__/test_snapshots/test_managed_pane.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions tests/test_cli_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import subprocess
from unittest.mock import patch

from cheznav.main import main


def test_info_does_not_launch_tui_when_chezmoi_missing(capsys):
with (
patch("cheznav.main.CheznavApp") as app_cls,
patch("cheznav.info.shutil.which", return_value=None),
patch("sys.argv", ["cheznav", "info"]),
):
main()

app_cls.assert_not_called()
out = capsys.readouterr().out
assert "cheznav" in out
assert "chezmoi: not found" in out


def test_info_reports_versions_config_and_source(capsys, tmp_path, monkeypatch):
source = tmp_path / "source"
(source / ".git").mkdir(parents=True)
config_dir = tmp_path / "config" / "chezmoi"
config_dir.mkdir(parents=True)
(config_dir / "chezmoi.toml").write_text('sourceDir = "x"\n')
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))

def fake_run(cmd, **_kwargs):
if cmd[-1] == "--version":
return subprocess.CompletedProcess(cmd, 0, stdout="chezmoi version v2.50.0\nrest\n", stderr="")
if cmd[-1] == "source-path":
return subprocess.CompletedProcess(cmd, 0, stdout=f"{source}\n", stderr="")
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")

with (
patch("cheznav.main.CheznavApp") as app_cls,
patch("cheznav.info.shutil.which", return_value="/usr/bin/chezmoi"),
patch("cheznav.info.subprocess.run", side_effect=fake_run),
patch("sys.argv", ["cheznav", "info"]),
):
main()

app_cls.assert_not_called()
out = capsys.readouterr().out
assert "chezmoi version v2.50.0" in out
assert "chezmoi version v2.50.0\nrest" not in out # only the first line
assert str(config_dir / "chezmoi.toml") in out
assert "git repo" in out


def test_info_reports_uninitialized_source_and_no_config(capsys, tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "empty"))

def fake_run(cmd, **_kwargs):
if cmd[-1] == "--version":
return subprocess.CompletedProcess(cmd, 0, stdout="chezmoi version v2.50.0\n", stderr="")
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="chezmoi: source dir not found\n")

with (
patch("cheznav.info.shutil.which", return_value="/usr/bin/chezmoi"),
patch("cheznav.info.subprocess.run", side_effect=fake_run),
patch("sys.argv", ["cheznav", "info"]),
):
main()

out = capsys.readouterr().out
assert "none found" in out
assert "source: not initialized" in out
Loading