DEV Community

Cover image for Build Your Own Advanced Offline Terminal Dictionary with Python, UV, NLTK, and SQLite
Kernel Cero
Kernel Cero

Posted on

Build Your Own Advanced Offline Terminal Dictionary with Python, UV, NLTK, and SQLite

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.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)