Build Your Own Advanced Offline Terminal Dictionary with Python, UV, NLTK, and SQLite
If you spend your entire day inside the terminal, you know how annoying it is to have to open a web browser just to check the precise definition, synonyms, or part of speech of an advanced English word.
In this article, we will build a modern terminal utility that is 100% offline, blazing fast, and features relational database persistenceβall built using cutting-edge tools in the Python ecosystem.
π οΈ The Tech Stack
For this project, we'll use a powerful and lightweight set of libraries:
- uv: The extremely fast package and virtual environment manager written in Rust.
- NLTK (WordNet): Princeton's computational lexical database for advanced semantic definitions without relying on external APIs.
- Rich: For rendering stylish panels, ASCII art, and formatted tables directly in the console.
- SQLite: Local structured storage to save search history and parsed results in relational tables.
- Difflib: For smart spelling correction suggestions ("Did you mean...?").
π 1. Project Structure
First, initialize an independent and clean project using uv:
bash
mkdir advanced_dictionary
cd advanced_dictionary
uv init --app
uv add nltk rich
Your project layout will look like this:
Plaintext
advanced_dictionary/
βββ .venv/
βββ pyproject.toml
βββ uv.lock
βββ dictionary_records.db (Generated automatically)
βββ main.py
π» 2. Source Code (main.py)
Create and edit the main.py file in your project root with the following complete code:
import sys
import sqlite3
from datetime import datetime
from difflib import get_close_matches
import nltk
from nltk.corpus import wordnet as wn
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich import box
from rich.text import Text
console = Console()
# Initialize lexical engine and cache lemmas for rapid suggestions
with console.status("[bold cyan]Initializing WordNet engine & spelling cache...[/bold cyan]", spinner="dots"):
try:
nltk.download('wordnet', quiet=True)
nltk.download('omw-1.4', quiet=True)
except Exception:
pass
ALL_LEMMAS = list(set(wn.all_lemma_names()))
# SQLite Configuration (Relational Tables)
DB_NAME = "dictionary_records.db"
def init_db():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
# Table 1: General search history
cursor.execute("""
CREATE TABLE IF NOT EXISTS searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL,
timestamp TEXT NOT NULL,
found BOOLEAN NOT NULL
)
""")
# Table 2: Detailed definitions associated with the search
cursor.execute("""
CREATE TABLE IF NOT EXISTS lookup_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
search_id INTEGER,
part_of_speech TEXT,
definition TEXT,
synonyms TEXT,
FOREIGN KEY(search_id) REFERENCES searches(id)
)
""")
conn.commit()
conn.close()
def save_search_to_db(word: str, found: bool, results: list):
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("INSERT INTO searches (word, timestamp, found) VALUES (?, ?, ?)", (word.lower(), timestamp, found))
search_id = cursor.lastrowid
if found and results:
for item in results:
syn_str = ", ".join(item["synonyms"])
cursor.execute("""
INSERT INTO lookup_results (search_id, part_of_speech, definition, synonyms)
VALUES (?, ?, ?, ?)
""", (search_id, item["pos"], item["definition"], syn_str))
conn.commit()
conn.close()
def show_history():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute("SELECT word, timestamp, found FROM searches ORDER BY id DESC LIMIT 10")
rows = cursor.fetchall()
conn.close()
if not rows:
console.print("[yellow]No search history found in SQLite yet.[/yellow]")
return
table = Table(title="[bold cyan]Recent Search History (SQLite)[/bold cyan]", box=box.ROUNDED)
table.add_column("Word", style="bold white", no_wrap=True)
table.add_column("Timestamp", style="dim")
table.add_column("Found", justify="center")
for r in rows:
status = "[green]Yes[/green]" if r[2] else "[red]No[/red]"
table.add_row(r[0], r[1], status)
console.print(table)
ASCII_BANNER = r"""
____ ___ ____ _____ ___ ___ _ _ _ ____ __ __
| _ \|_ _/ ___|_ _|_ _/ _ \| \ | | / \ | _ \ \ \ / /
| | | || | | | | | | | | | \| | / _ \ | |_) | \ V /
| |_| || | |___ | | | | |_| | |\ |/ ___ \| _ < | |
|____/|___\____| |_| |___\___/|_| \_/_/ \_\_| \_\ |_|
"""
def get_wordnet_definition(word: str):
clean_word = word.strip().lower()
synsets = wn.synsets(clean_word)
if not synsets:
suggestions = get_close_matches(clean_word, ALL_LEMMAS, n=3, cutoff=0.75)
return None, suggestions
results = []
for syn in synsets[:5]:
pos_map = {
'n': 'noun',
'v': 'verb',
'adj': 'adjective',
'adv': 'adverb',
's': 'adjective (satellite)'
}
pos = pos_map.get(syn.pos(), syn.pos())
definition = syn.definition()
examples = syn.examples()
synonyms = [lemma.name().replace('_', ' ') for lemma in syn.lemmas()]
synonyms = [s for s in synonyms if s.lower() != clean_word]
results.append({
"pos": pos,
"definition": definition,
"examples": examples,
"synonyms": list(set(synonyms))[:6]
})
return results, []
def format_definition(word, data, suggestions):
if not data:
msg = f"[bold red]No advanced definitions found for '{word}'.[/bold red]"
if suggestions:
sug_str = ", ".join([f"[bold cyan]{s}[/bold cyan]" for s in suggestions])
msg += f"\n\n[yellow]Did you mean:[/yellow] {sug_str}?"
return msg
content_lines = []
content_lines.append(f"[bold cyan]LEXICAL TARGET:[/bold cyan] {word.upper()}\n")
for i, item in enumerate(data, 1):
pos = item["pos"]
defn = item["definition"]
examples = item["examples"]
synonyms = item["synonyms"]
content_lines.append(f"[bold yellow]({pos})[/bold yellow]")
content_lines.append(f" [bold]{i}.[/bold] {defn}")
if examples:
for ex in examples:
content_lines.append(f" [italic green]Example:[/italic green] \"{ex}\"")
if synonyms:
syn_str = ", ".join(synonyms)
content_lines.append(f" [blue]Synonyms:[/blue] {syn_str}")
content_lines.append("")
return "\n".join(content_lines)
def main():
init_db()
console.clear()
banner_text = Text(ASCII_BANNER.strip(), style="bold bright_blue")
console.print(banner_text, justify="center")
console.print("[dim center]Advanced Offline English Dictionary | Type '.history' for logs or 'exit' to quit[/dim center]\n")
if len(sys.argv) > 1:
word = " ".join(sys.argv[1:])
data, suggestions = get_wordnet_definition(word)
found = data is not None
save_search_to_db(word, found, data if found else [])
result_content = format_definition(word, data, suggestions)
console.print(Panel(result_content, title=f"Result: {word}", border_style="cyan", box=box.ROUNDED))
return
while True:
try:
word = console.input("\n[bold green]π Enter word to look up > [/bold green]").strip()
if not word:
continue
if word.lower() in ("exit", "quit"):
console.print("\n[yellow]Goodbye! Your session has been safely logged.[/yellow]")
break
if word.lower() == ".history":
show_history()
continue
with console.status("[bold cyan]Consulting database & verifying records...[/bold cyan]", spinner="dots"):
data, suggestions = get_wordnet_definition(word)
found = data is not None
save_search_to_db(word, found, data if found else [])
result_content = format_definition(word, data, suggestions)
definition_panel = Panel(
result_content,
title=f"[bold white] Definition: {word.upper()} [/bold white]",
title_align="left",
border_style="bright_blue",
box=box.ROUNDED,
padding=(1, 2)
)
console.print(definition_panel)
except (KeyboardInterrupt, EOFError):
console.print("\n[yellow]Goodbye![/yellow]")
break
if __name__ == "__main__":
main()
π 3. Global Terminal Integration (Linux / macOS)
To invoke your dictionary by simply typing a quick command (like dict) from any directory on your system without contaminating your global environment, you can create a wrapper script:
Create the local binary folder if it doesn't exist:
mkdir -p ~/.local/bin
Create the executable file dict:
cat << 'EOF' > ~/.local/bin/dict
#!/bin/bash
cd /absolute/path/to/your/project/advanced_dictionary
uv run python main.py "$@"
EOF
Grant execution permissions:
chmod +x ~/.local/bin/dict
π― How to Use It
You now have a ready-to-use tool for your daily workflow:
Interactive Mode: Type dict in your terminal to open the stylized console with the ASCII banner.
Direct Lookup: Type dict serendipity to get the result immediately.
SQLite History: Inside the interactive mode, type .history to check a formatted table containing your latest locally tracked searches.
Top comments (0)