core/cli: config open via VISUAL/EDITOR or system default

Prefer $VISUAL then $EDITOR (blocking). Without either, fall back to
xdg-open / open / startfile (non-blocking) for config paths only.
/edit stays blocking-only so the temp buffer can be re-read.
This commit is contained in:
2026-07-19 22:33:02 +08:00
parent d528f65c4c
commit 58ff646034
6 changed files with 245 additions and 66 deletions
+86 -12
View File
@@ -15,10 +15,13 @@ from plyngent.cli.editor import (
)
def test_get_editor(monkeypatch: pytest.MonkeyPatch) -> None:
def test_get_editor_prefers_visual(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("EDITOR", raising=False)
monkeypatch.delenv("VISUAL", raising=False)
assert get_editor() is None
monkeypatch.setenv("EDITOR", " codium --wait ")
monkeypatch.setenv("EDITOR", " nano ")
assert get_editor() == "nano"
monkeypatch.setenv("VISUAL", " codium --wait ")
assert get_editor() == "codium --wait"
@@ -49,7 +52,8 @@ def test_open_in_editor_splits_args(tmp_path: Path, monkeypatch: pytest.MonkeyPa
return Result()
monkeypatch.setattr("plyngent.cli.editor.subprocess.run", fake_run)
open_in_editor(path, editor="codium --wait")
outcome = open_in_editor(path, editor="codium --wait")
assert outcome == "waited"
assert len(calls) == 1
assert calls[0][0] == "codium"
assert calls[0][1] == "--wait"
@@ -57,10 +61,33 @@ def test_open_in_editor_splits_args(tmp_path: Path, monkeypatch: pytest.MonkeyPa
assert path.is_file()
def test_open_in_editor_missing_editor(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def test_open_in_editor_system_fallback(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("EDITOR", raising=False)
with pytest.raises(Exception, match="EDITOR"):
open_in_editor(tmp_path / "x.toml", editor=None)
monkeypatch.delenv("VISUAL", raising=False)
path = tmp_path / "x.toml"
opened: list[str] = []
def fake_system(p: Path) -> None:
opened.append(str(p))
monkeypatch.setattr("plyngent.cli.editor._open_with_system_default", fake_system)
outcome = open_in_editor(path, editor=None, allow_system_open=True)
assert outcome == "system"
assert opened == [str(path)]
assert path.is_file()
def test_open_in_editor_no_fallback_requires_editor(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("EDITOR", raising=False)
monkeypatch.delenv("VISUAL", raising=False)
with pytest.raises(Exception, match=r"VISUAL|EDITOR"):
open_in_editor(tmp_path / "x.toml", editor=None, allow_system_open=False)
def test_edit_text_in_editor(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -99,9 +126,13 @@ def test_edit_text_empty_cancels(monkeypatch: pytest.MonkeyPatch) -> None:
assert edit_text_in_editor("") is None
def test_prompt_edit_no_editor(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def test_edit_text_no_system_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
from plyngent.cli.editor import edit_text_in_editor
monkeypatch.delenv("EDITOR", raising=False)
assert prompt_edit_config(tmp_path / "p.toml") is False
monkeypatch.delenv("VISUAL", raising=False)
with pytest.raises(Exception, match=r"VISUAL|EDITOR"):
edit_text_in_editor("x")
def test_prompt_edit_declined(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -111,7 +142,7 @@ def test_prompt_edit_declined(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -
return False
monkeypatch.setattr("click.confirm", _confirm)
assert prompt_edit_config(tmp_path / "p.toml", reason="empty") is False
assert prompt_edit_config(tmp_path / "p.toml", reason="empty") is None
def test_prompt_edit_accepted(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -124,15 +155,40 @@ def test_prompt_edit_accepted(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -
monkeypatch.setattr("click.confirm", _confirm)
opened: list[Path] = []
def fake_open(p: Path, *, editor: str | None = None) -> None:
del editor
def fake_open(
p: Path,
*,
editor: str | None = None,
ensure_exists: bool = True,
allow_system_open: bool = True,
) -> str:
del editor, ensure_exists, allow_system_open
opened.append(p)
return "waited"
monkeypatch.setattr("plyngent.cli.editor.open_in_editor", fake_open)
assert prompt_edit_config(path, reason="No providers.") is True
assert prompt_edit_config(path, reason="No providers.") == "waited"
assert opened == [path]
def test_prompt_edit_without_editor_can_system_open(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("EDITOR", raising=False)
monkeypatch.delenv("VISUAL", raising=False)
def _confirm(*_a: object, **_k: object) -> bool:
return True
monkeypatch.setattr("click.confirm", _confirm)
monkeypatch.setattr(
"plyngent.cli.editor.open_in_editor",
lambda *a, **k: "system",
)
assert prompt_edit_config(tmp_path / "p.toml") == "system"
def test_config_path_command(tmp_path: Path) -> None:
runner = CliRunner()
result = runner.invoke(main, ["config", "path", "--config", str(tmp_path / "a.toml")])
@@ -159,3 +215,21 @@ def test_config_edit_command(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
result = runner.invoke(main, ["config", "edit", "--config", str(path)])
assert result.exit_code == 0
assert calls and calls[0][-1] == str(path)
assert "edited" in result.output
def test_config_edit_system_fallback_message(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
path = tmp_path / "edit.toml"
monkeypatch.delenv("EDITOR", raising=False)
monkeypatch.delenv("VISUAL", raising=False)
monkeypatch.setattr(
"plyngent.cli.editor._open_with_system_default",
lambda p: None,
)
runner = CliRunner()
result = runner.invoke(main, ["config", "edit", "--config", str(path)])
assert result.exit_code == 0
assert "system default" in result.output.lower() or "system default" in (result.stderr or "").lower()