Skip to content

Commit d429063

Browse files
committed
feat: CLI + hooks.
1 parent 946c3c8 commit d429063

File tree

12 files changed

+630
-9
lines changed

12 files changed

+630
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ build/
88
*.lock
99
.ruff_cache/
1010
.mypy_cache/
11+
.DS_Store

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.8.6
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.PHONY: dev install lint format check
2+
3+
dev:
4+
uv run -- python -m uvicorn linguee_api.main:app --reload
5+
6+
install:
7+
uv venv && uv pip install -e ".[dev]"
8+
9+
lint:
10+
uv run ruff check src/
11+
12+
format:
13+
uv run ruff format src/
14+
15+
check: lint
16+
uv run ruff format --check src/

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Unofficial JSON API proxy for [Linguee](https://www.linguee.com). Scrapes Linguee HTML and returns structured JSON for translations, examples, external sources, and autocompletions.
44

5+
![screenshot](img/img.png)
6+
57
## Quick Start
68

79
```bash
@@ -25,6 +27,31 @@ All endpoints accept `query`, `src`, and `dst` language code parameters.
2527

2628
**Supported languages**: bg, cs, da, de, el, en, es, et, fi, fr, hu, it, ja, lt, lv, mt, nl, pl, pt, ro, ru, sk, sl, sv, zh
2729

30+
## CLI
31+
32+
German-English dictionary TUI with clickable words, history, and bookmarks.
33+
34+
```bash
35+
linguee
36+
```
37+
38+
Install globally with `uv tool install .` or run from the project with `uv run linguee`.
39+
40+
Use `linguee --no-tui` for a simple REPL without the TUI.
41+
42+
| Key | Action |
43+
|---|---|
44+
| `ctrl+o` / `ctrl+i` | Back / forward in history |
45+
| `ctrl+n` / `ctrl+p` | Navigate lists |
46+
| `ctrl+d` | Flip direction (de↔en) |
47+
| `ctrl+s` | Bookmark current word |
48+
| `ctrl+b` | Show bookmarks |
49+
| `ctrl+u` + vowel | Type umlauts (ä, ö, ü, ß) |
50+
| `ctrl+l` | Focus search |
51+
| `escape` | Close panel |
52+
53+
History and bookmarks are stored in `~/.local/share/linguee/`. Lookups are cached in `~/.cache/linguee/`.
54+
2855
## Configuration
2956

3057
Environment variables (prefix `LINGUEE_`):

img/img.png

883 KB
Loading

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ dependencies = [
1818
"redis[hiredis]>=5.2",
1919
"slowapi>=0.1.9",
2020
"tenacity>=9.0",
21+
"textual>=1.0",
2122
]
2223

2324
[project.optional-dependencies]
2425
sentry = ["sentry-sdk[fastapi]>=2.19"]
25-
dev = ["ruff>=0.8"]
26+
dev = ["ruff>=0.8", "pre-commit>=4.0"]
2627

2728
[project.scripts]
2829
linguee = "linguee_api.cli:main"

src/linguee_api/cache.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import hashlib
2+
import time
3+
from pathlib import Path
24
from typing import Protocol
35

46
import structlog
@@ -58,6 +60,26 @@ async def create_cache() -> Cache:
5860
return MemoryCache()
5961

6062

63+
class DiskCache:
64+
def __init__(self, ttl: int = 86400) -> None:
65+
self._dir = Path.home() / ".cache" / "linguee"
66+
self._dir.mkdir(parents=True, exist_ok=True)
67+
self._ttl = ttl
68+
69+
async def get(self, key: str) -> str | None:
70+
p = self._dir / key
71+
if not p.exists():
72+
return None
73+
if time.time() - p.stat().st_mtime > self._ttl:
74+
p.unlink(missing_ok=True)
75+
return None
76+
return p.read_text()
77+
78+
async def set(self, key: str, value: str, ttl: int) -> None:
79+
p = self._dir / key
80+
p.write_text(value)
81+
82+
6183
async def cached_fetch(cache: Cache, key: str, fetcher) -> str:
6284
cached = await cache.get(key)
6385
if cached is not None:

src/linguee_api/cli.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import asyncio
2-
import sys
32

43
import httpx
54

65
from linguee_api.client import CaptchaError, LingueeError, fetch_search
6+
from linguee_api.logging import setup_logging
77
from linguee_api.models import Correction, NotFound, ParseError
88
from linguee_api.parser import parse_search_result
99

10-
from linguee_api.logging import setup_logging
11-
1210
SRC = "de"
1311
DST = "en"
1412

@@ -57,7 +55,10 @@ async def lookup(client: httpx.AsyncClient, word: str) -> None:
5755
for t in lemma.translations:
5856
freq = ""
5957
if t.usage_frequency:
60-
freq = f" {GREEN}{RESET}" if t.usage_frequency.value == "almost_always" else f" {GREEN}{RESET}"
58+
if t.usage_frequency.value == "almost_always":
59+
freq = f" {GREEN}{RESET}"
60+
else:
61+
freq = f" {GREEN}{RESET}"
6162
t_pos = f" {DIM}{t.pos}{RESET}" if t.pos else ""
6263
print(f" {CYAN}{RESET} {t.text}{t_pos}{freq}")
6364

@@ -94,10 +95,21 @@ async def repl() -> None:
9495

9596

9697
def main() -> None:
97-
try:
98-
asyncio.run(repl())
99-
except KeyboardInterrupt:
100-
pass
98+
import argparse
99+
100+
parser = argparse.ArgumentParser(description="Linguee dictionary")
101+
parser.add_argument("--no-tui", action="store_true", help="use simple REPL instead of TUI")
102+
args = parser.parse_args()
103+
104+
if args.no_tui:
105+
import contextlib
106+
107+
with contextlib.suppress(KeyboardInterrupt):
108+
asyncio.run(repl())
109+
else:
110+
from linguee_api.tui.app import LingueeApp
111+
112+
LingueeApp().run()
101113

102114

103115
if __name__ == "__main__":

src/linguee_api/tui/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)