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
5 changes: 3 additions & 2 deletions docs/completion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ your shell:

nnote --install-completion

This does **not** modify any shell config file. The script is placed in:
This does **not** modify any shell config file. The script is placed in an
appropriate completion directory for your shell:

- **bash** — ``~/.local/share/bash-completion/completions/nnote``
- **zsh** — ``~/.local/share/zsh/site-functions/_nnote``
- **zsh** — the first user-writable directory already in your ``$fpath``
- **fish** — ``~/.config/fish/completions/nnote.fish``

To inspect the script before installing, use:
Expand Down
28 changes: 27 additions & 1 deletion nnote/completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,43 @@ def show_completion_callback(ctx, param, value):
ctx.exit()


def _zsh_install_path():
import subprocess

result = subprocess.run(
["zsh", "-i", "-c", "print -l $fpath"],
capture_output=True,
text=True,
)
home = Path.home()
for line in result.stdout.splitlines():
p = Path(line.strip())
if not p.is_absolute():
continue
try:
p.relative_to(home)
except ValueError:
continue
if p.exists() and p.is_dir():
return p / "_nnote"
return _COMPLETION_FILE["zsh"].expanduser()


def install_completion_callback(ctx, param, value):
if not value or ctx.resilient_parsing:
return
shell = _detect_shell()
script_file = _COMPLETION_FILE[shell].expanduser()
if shell == "zsh":
script_file = _zsh_install_path()
else:
script_file = _COMPLETION_FILE[shell].expanduser()
if script_file.exists():
click.echo(f"Completion already installed in {script_file}")
ctx.exit()
script_file.parent.mkdir(parents=True, exist_ok=True)
script_file.write_text(_generate_script(shell, ctx.command))
click.echo(f"Completion installed in {script_file}")
click.echo("Restart your shell for the change to take effect.")
ctx.exit()


Expand Down
52 changes: 48 additions & 4 deletions tests/test_completions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from pathlib import Path
from unittest.mock import patch
from unittest.mock import patch, MagicMock

import pytest
from click.shell_completion import CompletionItem
from click.testing import CliRunner

from nnote.cli import cli
from nnote.completions import complete_note_titles, complete_directories
from nnote.completions import (
complete_note_titles,
complete_directories,
_zsh_install_path,
)
from nnote.config import Config


Expand Down Expand Up @@ -127,7 +131,7 @@ def test_show_completion():
def test_install_completion(tmp_path):
script_file = tmp_path / "_nnote"
with patch("nnote.completions._detect_shell", return_value="zsh"):
with patch("nnote.completions._COMPLETION_FILE", {"zsh": script_file}):
with patch("nnote.completions._zsh_install_path", return_value=script_file):
result = CliRunner().invoke(cli, ["--install-completion"])
assert result.exit_code == 0
assert script_file.exists()
Expand All @@ -138,8 +142,48 @@ def test_install_completion_idempotent(tmp_path):
script_file = tmp_path / "_nnote"
script_file.write_text("# existing script\n")
with patch("nnote.completions._detect_shell", return_value="zsh"):
with patch("nnote.completions._COMPLETION_FILE", {"zsh": script_file}):
with patch("nnote.completions._zsh_install_path", return_value=script_file):
result = CliRunner().invoke(cli, ["--install-completion"])
assert result.exit_code == 0
assert "already installed" in result.output
assert script_file.read_text() == "# existing script\n"


def test_install_completion_bash(tmp_path):
script_file = tmp_path / "nnote"
with patch("nnote.completions._detect_shell", return_value="bash"):
with patch("nnote.completions._COMPLETION_FILE", {"bash": script_file}):
result = CliRunner().invoke(cli, ["--install-completion"])
assert result.exit_code == 0
assert script_file.exists()
assert "_NNOTE_COMPLETE" in script_file.read_text()


# --- _zsh_install_path ---


def test_zsh_install_path_picks_first_writable_home_fpath_dir(tmp_path):
user_dir = tmp_path / "zsh" / "functions"
user_dir.mkdir(parents=True)
system_dir = Path("/usr/share/zsh/functions")

mock_result = MagicMock()
mock_result.stdout = f"{system_dir}\n{user_dir}\n"

with patch("subprocess.run", return_value=mock_result):
with patch("nnote.completions.Path.home", return_value=tmp_path):
path = _zsh_install_path()

assert path == user_dir / "_nnote"


def test_zsh_install_path_falls_back_to_xdg_when_no_home_fpath(tmp_path):
mock_result = MagicMock()
mock_result.stdout = (
"/usr/share/zsh/functions\n/usr/local/share/zsh/site-functions\n"
)

with patch("subprocess.run", return_value=mock_result):
path = _zsh_install_path()

assert path == Path("~/.local/share/zsh/site-functions/_nnote").expanduser()