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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ ShellGenius reads color settings from `~/.config/lmt/config.json`. If the file i

These `lmterminal` compatibility keys apply across tools:

* `code_block_theme` — any [Pygments style](https://pygments.org/styles/) name, plus the built-in `alabaster` and `alabaster-shellgenius` themes.
* `code_block_theme` — any [Pygments style](https://pygments.org/styles/) name, plus the built-in `alabaster` theme.
* `inline_code_theme` — any Rich style string, such as `"#325cc0 on #f0f0f0"`.

### ShellGenius-only overrides

Add a `shellgenius` block to change ShellGenius without affecting other tools:

* `theme` — ShellGenius's own preset. Built-in values: `default`, `alabaster`, `alabaster-shellgenius`. Any Pygments theme name also works for fenced code blocks.
* `theme` — ShellGenius's own preset. Built-in values: `default`, `alabaster`. Any Pygments theme name also works for fenced code blocks.
* `styles` — override individual Rich semantic styles (`markdown.h1`, `markdown.code`, `markdown.code_block`). `markdown.code` overrides the top-level `inline_code_theme` for ShellGenius only; `markdown.code_block` controls the command-block background in TTY output.

Example:
Expand All @@ -144,7 +144,7 @@ Example:

Invalid `styles` entries are ignored individually, so one bad override does not discard the rest. The built-in `alabaster` preset keeps the upstream `#f8f8f8` syntax background; for a darker command block, add `{"markdown.code_block": "on #f0f0f0"}` under `styles`.

Legacy top-level `code_block_theme: "alabaster-shellgenius"` is still honored, but new config should prefer `shellgenius.theme`. These settings affect Rich output only; `--raw` and `--cmd` output is unchanged.
These settings affect Rich output only; `--raw` and `--cmd` output is unchanged.

## License

Expand Down
4 changes: 4 additions & 0 deletions changelog.d/remove-alabaster-shellgenius.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Removed

* Remove the unsupported `alabaster-shellgenius` theme name from ShellGenius config. The built-in theme surface is now just `alabaster` plus standard Pygments theme names.
* Remove the hidden `--plain`, `-p`, and `--command-only` CLI aliases. Use `--raw` for plain text and `--cmd` for command-only output.
3 changes: 3 additions & 0 deletions changelog.d/tighten-key-env-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Changed

* `~/.config/lmt/key.env` is now parsed as a dedicated ShellGenius key file. ShellGenius still accepts `OPENAI_API_KEY=...`, quoted values, and `export OPENAI_API_KEY=...`, but no longer accepts a bare key line or unrelated env assignments in that file.
58 changes: 21 additions & 37 deletions shellgenius/api_key.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

import os
import re
import stat
from pathlib import Path

import click

KEY_FILE_PATH = Path.home() / ".config" / "lmt" / "key.env"
EXPORT_KEY_LINE_RE = re.compile(r"^export[ \t]+OPENAI_API_KEY=(.*)$")


def get_api_key_path() -> Path:
Expand All @@ -19,58 +21,40 @@ def _parse_key_file(path: Path) -> str:
Accepted formats:
* ``OPENAI_API_KEY=sk-...``
* ``export OPENAI_API_KEY=sk-...``
* A single bare key on its own line.
* Quoted values for those assignments.

The file is dedicated to this single key. Blank lines and comments are
allowed, but any other non-comment content makes the file invalid.
"""
try:
text = path.read_text(encoding="utf-8").strip()
text = path.read_text(encoding="utf-8")
except (FileNotFoundError, UnicodeDecodeError, OSError):
return ""

if not text:
return ""

bare_key = ""
saw_other_content = False
key = ""

for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue

# export OPENAI_API_KEY=...
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
saw_other_content = True
continue

# OPENAI_API_KEY=...
if line.startswith("OPENAI_API_KEY="):
match = EXPORT_KEY_LINE_RE.match(line)
if match:
value = match.group(1)
elif line.startswith("OPENAI_API_KEY="):
value = line[len("OPENAI_API_KEY=") :]
# Strip optional surrounding quotes.
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1]
return value.strip()

if line == "OPENAI_API_KEY":
saw_other_content = True
continue

if "=" in line:
saw_other_content = True
continue

if bare_key or saw_other_content:
saw_other_content = True
continue
else:
return ""

# Single bare key on its own non-comment line.
bare_key = line
# Strip optional surrounding quotes.
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1]

if bare_key and not saw_other_content:
return bare_key
if key:
return ""
key = value.strip()

return ""
return key


def get_api_key() -> str:
Expand Down
5 changes: 0 additions & 5 deletions shellgenius/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,7 @@ def key_edit():
is_flag=True,
help="Force Rich formatting in a TTY; fall back to plain text otherwise.",
)
@click.option("--plain", "-p", is_flag=True, hidden=True)
@click.option("--cmd", "command_only", is_flag=True, help="Print only the generated command.")
@click.option("--command-only", "command_only", is_flag=True, hidden=True)
@click.option(
"--tokens", is_flag=True, help="Print prompt token count and estimated cost, then exit."
)
Expand All @@ -482,7 +480,6 @@ def prompt(
no_stream,
raw,
rich_flag,
plain,
command_only,
tokens,
):
Expand All @@ -494,8 +491,6 @@ def prompt(
click.echo(ctx.get_help())
return

raw = raw or plain

tty_state = get_tty_state()

if raw and rich_flag:
Expand Down
64 changes: 2 additions & 62 deletions shellgenius/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,45 +77,8 @@ class AlabasterStyle(_PygmentsStyle):
}


class AlabasterShellGeniusStyle(_PygmentsStyle):
"""Demo-friendly light style with more visible shell-command coloring."""

name = "alabaster-shellgenius"
background_color = "#ececec"

styles = {
Token: "#000000",
Token.Text: "#000000",
Comment: "#aa3731",
Comment.Preproc: "#aa3731",
String: "#448c27",
Number: "#7a3e9d",
Keyword: "#7a3e9d",
Keyword.Type: "#000000",
Name: "#325cc0",
Name.Function: "#325cc0",
Name.Class: "#325cc0",
Name.Decorator: "#325cc0",
Name.Tag: "#007acc",
Name.Attribute: "#325cc0",
Name.Builtin: "#325cc0",
Name.Variable: "#7a3e9d",
Operator: "#000000",
Punctuation: "#777777",
Generic.Heading: "#325cc0",
Generic.Subheading: "#325cc0",
Generic.Deleted: "#aa3731",
Generic.Inserted: "#448c27",
Generic.Error: "#aa3731",
Generic.Emph: "italic",
Generic.Strong: "bold",
Error: "#aa3731",
}


_CUSTOM_CODE_THEMES: dict[str, type[_PygmentsStyle]] = {
"alabaster": AlabasterStyle,
"alabaster-shellgenius": AlabasterShellGeniusStyle,
}

_SHELLGENIUS_THEME_STYLES: dict[str, dict[str, str]] = {
Expand All @@ -130,21 +93,10 @@ class AlabasterShellGeniusStyle(_PygmentsStyle):
"markdown.hr": "#c7c7c7",
"status.spinner": "#325cc0",
},
"alabaster-shellgenius": {
"markdown.h1": "bold #005faf",
"markdown.h2": "bold #5f00d7",
"markdown.h3": "bold #008700",
"markdown.item.bullet": "#d70000",
"markdown.link": "#005faf",
"markdown.link_url": "underline #005faf",
"markdown.hr": "#c5cbd3",
"status.spinner": "#008700",
},
}

_SHELLGENIUS_COMMAND_BLOCK_STYLES: dict[str, str] = {
"alabaster": f"on {AlabasterStyle.background_color}",
"alabaster-shellgenius": f"on {AlabasterShellGeniusStyle.background_color}",
}


Expand Down Expand Up @@ -260,13 +212,6 @@ def _theme_from_preset(name: str | None) -> LmtTheme:
)


def _legacy_shellgenius_theme(code_block_theme: str | None) -> LmtTheme:
if code_block_theme != "alabaster-shellgenius":
return LmtTheme()

return _theme_from_preset(code_block_theme)


def _resolve_code_theme(name: str) -> str | PygmentsSyntaxTheme:
"""Return a value suitable for ``Markdown(code_theme=...)``."""
custom = _CUSTOM_CODE_THEMES.get(name)
Expand Down Expand Up @@ -295,12 +240,9 @@ def load_lmt_theme() -> LmtTheme:

shellgenius_config = data.get("shellgenius")
if not isinstance(shellgenius_config, dict):
return _merge_theme(theme, _legacy_shellgenius_theme(theme.code_block_theme))
return theme

shellgenius_theme = _theme_from_preset(_validated_theme_value(shellgenius_config, "theme"))
if shellgenius_theme.shellgenius_theme is None:
shellgenius_theme = _legacy_shellgenius_theme(theme.code_block_theme)

theme = _merge_theme(theme, shellgenius_theme)
return _merge_theme(
theme,
Expand Down Expand Up @@ -388,9 +330,7 @@ def _command_block_style(theme: LmtTheme) -> str:
if override:
return override

return (
theme.shellgenius_command_block_style or f"on {AlabasterShellGeniusStyle.background_color}"
)
return theme.shellgenius_command_block_style or f"on {AlabasterStyle.background_color}"


def _style_for_shell_token(token, value: str, *, in_parameter_expansion: bool) -> str:
Expand Down
40 changes: 33 additions & 7 deletions tests/test_api_key.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import stat

import pytest
from click.testing import CliRunner

import shellgenius.api_key as api_key_module
Expand All @@ -20,10 +21,17 @@ def test_parse_key_file_with_export(tmp_path):
assert api_key_module._parse_key_file(key_file) == "sk-test456"


def test_parse_key_file_with_bare_key(tmp_path):
@pytest.mark.parametrize(
("line", "expected"),
[
("export OPENAI_API_KEY=sk-spaces\n", "sk-spaces"),
("export\tOPENAI_API_KEY=sk-tab\n", "sk-tab"),
],
)
def test_parse_key_file_with_export_and_flexible_whitespace(tmp_path, line, expected):
key_file = tmp_path / "key.env"
key_file.write_text("sk-barekey789\n")
assert api_key_module._parse_key_file(key_file) == "sk-barekey789"
key_file.write_text(line)
assert api_key_module._parse_key_file(key_file) == expected


def test_parse_key_file_with_quoted_value(tmp_path):
Expand All @@ -44,10 +52,22 @@ def test_parse_key_file_skips_comments(tmp_path):
assert api_key_module._parse_key_file(key_file) == "sk-after-comment"


def test_parse_key_file_ignores_other_assignments_before_key(tmp_path):
def test_parse_key_file_allows_comments_after_key(tmp_path):
key_file = tmp_path / "key.env"
key_file.write_text("OPENAI_API_KEY=sk-before-comment\n# trailing comment\n")
assert api_key_module._parse_key_file(key_file) == "sk-before-comment"


def test_parse_key_file_rejects_other_assignments_before_key(tmp_path):
key_file = tmp_path / "key.env"
key_file.write_text("export PATH=/usr/bin\nOPENAI_API_KEY=sk-after-path\n")
assert api_key_module._parse_key_file(key_file) == "sk-after-path"
assert api_key_module._parse_key_file(key_file) == ""


def test_parse_key_file_rejects_other_assignments_after_key(tmp_path):
key_file = tmp_path / "key.env"
key_file.write_text("OPENAI_API_KEY=sk-before-path\nexport PATH=/usr/bin\n")
assert api_key_module._parse_key_file(key_file) == ""


def test_parse_key_file_rejects_non_key_assignment_as_bare_key(tmp_path):
Expand All @@ -68,9 +88,15 @@ def test_parse_key_file_rejects_bare_key_variable_name(tmp_path):
assert api_key_module._parse_key_file(key_file) == ""


def test_parse_key_file_rejects_bare_key_when_file_has_other_content(tmp_path):
def test_parse_key_file_rejects_bare_key_line(tmp_path):
key_file = tmp_path / "key.env"
key_file.write_text("sk-barekey789\n")
assert api_key_module._parse_key_file(key_file) == ""


def test_parse_key_file_rejects_multiple_key_assignments(tmp_path):
key_file = tmp_path / "key.env"
key_file.write_text("export PATH=/usr/bin\nsk-barekey789\n")
key_file.write_text("OPENAI_API_KEY=sk-first\nOPENAI_API_KEY=sk-second\n")
assert api_key_module._parse_key_file(key_file) == ""


Expand Down
Loading
Loading