Claude Code for simple-term-menu: Interactive CLI Menus in Python — Claude Skills 360 Blog
Blog / AI / Claude Code for simple-term-menu: Interactive CLI Menus in Python
AI

Claude Code for simple-term-menu: Interactive CLI Menus in Python

Published: March 25, 2028
Read time: 5 min read
By: Claude Skills 360

simple-term-menu renders interactive keyboard-navigable terminal menus. pip install simple-term-menu. Basic: from simple_term_menu import TerminalMenu; menu = TerminalMenu(["Option A", "Option B", "Quit"]); idx = menu.show(); print(idx). Title: TerminalMenu(entries, title="Choose action\n"). Cursor: menu_cursor="▶ ". Style: menu_cursor_style=("fg_red","bold"). Hightlight: menu_highlight_style=("bg_blue","fg_white"). Multi-select: TerminalMenu(entries, multi_select=True, show_multi_select_hint=True)menu.show() returns tuple. Multi accept: multi_select_select_on_accept=False — must press Space to select items, Enter to confirm. Search: show_search_hint=True → type to fuzzy-filter entries. Preview: preview_command="cat {}" or preview_command=fn(entry); preview_size=0.25. Keys: accept_keys=("enter","right"). quit_keys=("q","escape"). Cycle: cycle_cursor=True — wraps around at top/bottom. Shortcuts: prefix entries "[a] Option A". Clear screen: clear_screen=True. Raise: raise_error_on_interrupt=True → KeyboardInterrupt on Ctrl+C. Nested: return to parent menu by re-calling parent_menu.show(). Status bar: status_bar="Press / to search". TerminalMenu(entries, menu_entry_count_limit=10) for long lists. show_search_hint_text="type to search". Result: menu.chosen_menu_entry (str) and menu.chosen_menu_index (int). For multi: menu.chosen_menu_entries and menu.chosen_menu_indices. Claude Code generates simple-term-menu navigation systems, config editors, and interactive CLI selectors.

CLAUDE.md for simple-term-menu

## simple-term-menu Stack
- Version: simple-term-menu >= 1.6 | pip install simple-term-menu
- Basic: TerminalMenu(["A","B","C"]).show() → int index | None if cancelled
- Multi: TerminalMenu(entries, multi_select=True).show() → tuple of indices
- Search: show_search_hint=True — type to fuzzy-filter entries in real time
- Preview: preview_command="cat {}" or fn(entry) → right panel preview
- Result: menu.chosen_menu_entry (str) | menu.chosen_menu_entries (tuple)
- Nested: call show() in a loop, return None to go up a level

simple-term-menu Interactive CLI Pipeline

# app/menus.py — simple-term-menu navigation, multi-select, and nested menus
from __future__ import annotations

import math
import os
import sys
from typing import Any, Callable

from simple_term_menu import TerminalMenu


# ─────────────────────────────────────────────────────────────────────────────
# 1. Factory helpers
# ─────────────────────────────────────────────────────────────────────────────

def make_menu(
    entries: list[str],
    title: str = "",
    multi_select: bool = False,
    search: bool = False,
    preview_fn: Callable[[str], str] | None = None,
    preview_command: str | None = None,
    cursor: str = "▶ ",
    cursor_style: tuple[str, ...] = ("fg_cyan", "bold"),
    highlight_style: tuple[str, ...] = ("bg_blue", "fg_white"),
    cycle: bool = True,
    clear_screen: bool = False,
    status_bar: str = "",
) -> TerminalMenu:
    """
    Create a TerminalMenu with consistent defaults.
    preview_fn: callable(entry) → str used as preview text.
    preview_command: shell command string — "{}" replaced with entry.
    """
    kwargs: dict[str, Any] = {
        "menu_cursor":            cursor,
        "menu_cursor_style":      cursor_style,
        "menu_highlight_style":   highlight_style,
        "cycle_cursor":           cycle,
        "clear_screen":           clear_screen,
        "raise_error_on_interrupt": True,
    }
    if title:
        kwargs["title"] = title + "\n"
    if multi_select:
        kwargs["multi_select"] = True
        kwargs["show_multi_select_hint"] = True
        kwargs["multi_select_select_on_accept"] = False
    if search:
        kwargs["show_search_hint"] = True
    if preview_fn:
        kwargs["preview_command"] = preview_fn
        kwargs["preview_size"] = 0.35
    elif preview_command:
        kwargs["preview_command"] = preview_command
        kwargs["preview_size"] = 0.35
    if status_bar:
        kwargs["status_bar"] = status_bar

    return TerminalMenu(entries, **kwargs)


def choose(
    entries: list[str],
    title: str = "Choose an option",
    search: bool = False,
) -> str | None:
    """
    Show a menu and return the chosen entry string, or None if cancelled.
    """
    if not sys.stdout.isatty():
        return entries[0] if entries else None
    try:
        menu = make_menu(entries, title=title, search=search)
        idx = menu.show()
        return menu.chosen_menu_entry if idx is not None else None
    except KeyboardInterrupt:
        return None


def choose_multi(
    entries: list[str],
    title: str = "Select items (Space to toggle, Enter to confirm)",
    search: bool = False,
) -> list[str]:
    """
    Show a multi-select menu. Returns list of chosen entry strings.
    """
    if not sys.stdout.isatty():
        return entries
    try:
        menu = make_menu(entries, title=title, multi_select=True, search=search)
        menu.show()
        return list(menu.chosen_menu_entries or [])
    except KeyboardInterrupt:
        return []


# ─────────────────────────────────────────────────────────────────────────────
# 2. Navigation helpers
# ─────────────────────────────────────────────────────────────────────────────

def confirm(
    prompt: str = "Confirm?",
    default: bool = True,
) -> bool:
    """Simple Yes / No confirmation menu."""
    entries = ["Yes", "No"] if default else ["No", "Yes"]
    result = choose(entries, title=prompt)
    return result == "Yes"


def choose_from_dict(
    options: dict[str, Any],
    title: str = "Choose",
) -> tuple[str, Any] | tuple[None, None]:
    """
    Show a menu with dict keys as labels.
    Returns (key, value) of chosen entry, or (None, None) on cancel.
    """
    keys = list(options.keys())
    result = choose(keys, title=title)
    if result is None:
        return None, None
    return result, options[result]


def paginate(
    entries: list[str],
    page_size: int = 10,
    title: str = "Select",
) -> str | None:
    """
    Show a paginated menu — next/previous page entries added automatically.
    """
    page = 0
    n_pages = max(1, math.ceil(len(entries) / page_size))

    while True:
        start = page * page_size
        chunk = entries[start: start + page_size]
        nav = []
        if page > 0:
            nav.append("← Previous page")
        if page < n_pages - 1:
            nav.append("→ Next page")
        nav.append("✕ Cancel")

        all_entries = chunk + nav
        hdr = f"{title} (page {page+1}/{n_pages})"
        result = choose(all_entries, title=hdr)

        if result == "← Previous page":
            page -= 1
        elif result == "→ Next page":
            page += 1
        elif result == "✕ Cancel" or result is None:
            return None
        else:
            return result



# ─────────────────────────────────────────────────────────────────────────────
# 3. Nested menu system
# ─────────────────────────────────────────────────────────────────────────────

class MenuNode:
    """
    A node in a recursive menu tree.

    Usage:
        root = MenuNode("Main Menu", {
            "Deploy":   MenuNode("Deploy", {
                "Production": lambda: deploy("prod"),
                "Staging":    lambda: deploy("staging"),
            }),
            "Config":   MenuNode("Config", {
                "Edit .env":  edit_env,
                "Reset":      reset_config,
            }),
            "Exit": None,
        })
        root.run()
    """

    def __init__(
        self,
        title: str,
        children: dict[str, "MenuNode | Callable | None"],
        back_label: str = "← Back",
        search: bool = False,
    ):
        self.title = title
        self.children = children
        self.back_label = back_label
        self.search = search

    def run(self, depth: int = 0) -> bool:
        """
        Show the menu. Returns False if user chose Back/Exit.
        """
        keys = list(self.children.keys()) + [self.back_label]
        while True:
            try:
                menu = make_menu(
                    keys,
                    title=self.title,
                    search=self.search,
                )
                idx = menu.show()
                if idx is None:
                    return False
                label = menu.chosen_menu_entry
                if label == self.back_label:
                    return False

                child = self.children[label]
                if child is None:
                    return False
                elif isinstance(child, MenuNode):
                    child.run(depth=depth + 1)
                elif callable(child):
                    child()
            except KeyboardInterrupt:
                return False


# ─────────────────────────────────────────────────────────────────────────────
# 4. File picker
# ─────────────────────────────────────────────────────────────────────────────

def pick_file(
    directory: str = ".",
    pattern: str = "*",
    title: str = "Select a file",
    preview: bool = True,
) -> str | None:
    """
    Browse files in a directory with optional preview.
    pattern: glob pattern to filter files.
    """
    import glob as _glob
    import pathlib

    path = pathlib.Path(directory).resolve()
    files = sorted(str(f) for f in path.glob(pattern) if f.is_file())
    if not files:
        return None

    short_names = [os.path.relpath(f, directory) for f in files]
    name_to_path = dict(zip(short_names, files))

    preview_fn = None
    if preview:
        def preview_fn(entry: str) -> str:
            full = name_to_path.get(entry, "")
            try:
                with open(full, encoding="utf-8", errors="replace") as fh:
                    return fh.read(2000)
            except Exception:
                return "(binary or unreadable)"

    result = choose(short_names, title=title, search=True) if not preview else None
    if preview:
        try:
            menu = make_menu(
                short_names,
                title=title,
                search=True,
                preview_fn=preview_fn,
            )
            idx = menu.show()
            result = menu.chosen_menu_entry if idx is not None else None
        except KeyboardInterrupt:
            result = None

    return name_to_path.get(result) if result else None


# ─────────────────────────────────────────────────────────────────────────────
# 5. Config editor
# ─────────────────────────────────────────────────────────────────────────────

def edit_config(
    config: dict[str, Any],
    title: str = "Edit configuration",
    editor: str | None = None,
) -> dict[str, Any]:
    """
    Interactive config editor: pick a key, then set a new value.
    Returns the (possibly modified) config dict.
    """
    import json
    import subprocess
    import tempfile

    while True:
        entries = [f"{k} = {v!r}" for k, v in config.items()] + ["← Done"]
        result = choose(entries, title=title)
        if result is None or result == "← Done":
            break

        key = result.split(" = ")[0]
        old_val = config[key]
        val_type = type(old_val)

        if isinstance(old_val, (dict, list)):
            # Open in editor for complex types
            ed = editor or os.environ.get("EDITOR", "nano")
            with tempfile.NamedTemporaryFile(
                mode="w", suffix=".json", delete=False
            ) as tmp:
                json.dump(old_val, tmp, indent=2)
                tmp_path = tmp.name
            subprocess.run([ed, tmp_path])
            try:
                with open(tmp_path) as fh:
                    config[key] = json.load(fh)
            except Exception:
                pass
            os.unlink(tmp_path)
        else:
            # Inline: show current + prompt for new value
            raw = input(f"  {key} [{old_val!r}] > ").strip()
            if raw:
                try:
                    config[key] = val_type(raw)
                except Exception:
                    config[key] = raw

    return config


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    if not sys.stdout.isatty():
        print("Demo requires a TTY — run in a real terminal.")
        sys.exit(0)

    print("=== Single select ===")
    action = choose(
        ["Deploy to production", "Deploy to staging", "Run tests", "Exit"],
        title="What do you want to do?",
    )
    print(f"  Chosen: {action!r}")

    print("\n=== Multi-select ===")
    services = choose_multi(
        ["api", "worker", "scheduler", "redis", "postgres"],
        title="Select services to restart",
    )
    print(f"  Selected: {services}")

    print("\n=== Confirmation ===")
    ok = confirm("Deploy to production?")
    print(f"  Confirmed: {ok}")

    print("\n=== Nested menu ===")
    def _action(name: str):
        def _fn():
            print(f"  → Executed: {name}")
        return _fn

    root = MenuNode("Main Menu", {
        "Build": MenuNode("Build", {
            "Docker image": _action("docker build"),
            "Assets":       _action("npm run build"),
        }),
        "Deploy": MenuNode("Deploy", {
            "Production": _action("deploy prod"),
            "Staging":    _action("deploy staging"),
        }),
        "Exit": None,
    })
    root.run()

For the questionary alternative — questionary offers a richer set of prompt types (text input, password, checkbox, autocomplete, path, and select) rendered with arrow-key navigation and styled with prompt_toolkit; simple-term-menu is narrower in scope — list selection only — but adds file preview panels with preview_command and multi-select checkboxes, which questionary’s select doesn’t include natively; use simple-term-menu when you need file pickers and preview panels, questionary when you need mixed prompt types in a single flow. For the pick alternative — pick is a zero-dependency single-module selector using curses (or Windows console); simple-term-menu is larger but adds multi-select, search filtering, preview panels, and custom keyboard shortcuts that pick doesn’t support. The Claude Skills 360 bundle includes simple-term-menu skill sets covering TerminalMenu() constructor, make_menu() factory, choose() and choose_multi() helpers, confirm() yes/no dialog, choose_from_dict() dict picker, paginate() paged navigation, MenuNode recursive nested menus, pick_file() file browser with preview, edit_config() interactive config editor, multi_select with Space+Enter workflow, show_search_hint fuzzy filtering, and preview_command/preview_fn panel. Start with the free tier to try interactive terminal menu code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free