Source code for pygra.main

"""
main.py — CLI entry point for PyGRA.
Handles --help before importing PyQt5 so it works without a display.
"""

import sys

HELP_TEXT = """
PyGRA — interactive scientific data plotter

Usage:
  pygra [file ...] [options]
  pygra --file FILE [--x COL] [--y COL] [--dx COL] [--dy COL] [--file FILE ...] [options]

Positional arguments:
  file                  One or more data files. The shell expands glob
                        patterns, e.g.:  pygra *wham_TI.dat

Options:
  -f, --file FILE       Load a data file (repeatable, alternative to positional)
  --x COL               x column index (0-based) for the preceding --file.
                        If given after all files, applies to all. Default: 0
  --y COL               y column index (0-based) for the preceding --file.
                        If given after all files, applies to all. Default: 1
  --dx COL              x error bar column index (0-based) for the preceding
                        --file. If given after all files, applies to all.
                        Default: 0 (no x error bars)
  --dy COL              y error bar column index (0-based) for the preceding
                        --file. If given after all files, applies to all.
                        Default: 0 (no y error bars)
  -s, --downsampling N  Load every N-th row from each file (default: 1, no downsampling)
  -l, --load FILE       Load a previously saved session (.json)
  -h, --help            Show this help message and exit

Examples:
  # open GUI with no files
  pygra

  # positional — shell expands the glob
  pygra *file*.dat

  # same columns for all files
  pygra file1.dat file2.dat --x 0 --y 3

  # per-file column specification
  pygra --file file1.dat --x 0 --y 3 --file file2.dat --x 0 --y 5

  # specify error bars
  pygra --file data.dat --x 0 --y 1 --dy 2

  # load every 100th row (useful for large files)
  pygra idx_*.csv --x 3 --y 4 --downsampling 100

  # load a saved session
  pygra --load session.json
"""


def _parse_interleaved(argv: list) -> dict:
    """
    Parse the command-line argument list into a structured dict.

    Supports interleaved ``--file``/``--x``/``--y``/``--dx``/``--dy`` groups so that each
    ``--file`` can carry its own column specification, and also handles
    positional file arguments and trailing column options that apply
    to all files.  Prints :data:`HELP_TEXT` and exits if ``-h`` or
    ``--help`` is present.

    Parameters
    ----------
    argv : list of str
        Argument strings, typically ``sys.argv[1:]``.

    Returns
    -------
    dict
        ``{"files": list[dict], "load": str | None, "downsampling": int}``
        where each file dict has keys ``"path"`` (str), ``"xcol"`` (int),
        ``"ycol"`` (int), ``"dxcol"`` (int), and ``"dycol"`` (int).
    """
    if any(tok in ("-h", "--help") for tok in argv):
        print(HELP_TEXT)
        sys.exit(0)

    files = []
    load = None
    downsampling = 1
    global_cols = {"xcol": None, "ycol": None, "dxcol": None, "dycol": None}
    col_options = {
        "--x": "xcol",
        "--y": "ycol",
        "--dx": "dxcol",
        "--dy": "dycol",
    }
    defaults = {"xcol": 0, "ycol": 1, "dxcol": 0, "dycol": 0}

    def _has_subsequent_file(start: int) -> bool:
        """Return True if argv[start:] contains another file argument."""
        j = start
        while j < len(argv):
            nxt = argv[j]
            if nxt in ("--file", "-f"):
                return True
            if nxt in ("--load", "-l", "--x", "--y", "--dx", "--dy",
                       "--downsampling", "-s"):
                j += 2
                continue
            if not nxt.startswith("-"):
                return True
            j += 1
        return False

    i = 0
    while i < len(argv):
        tok = argv[i]
        if tok in ("--load", "-l"):
            i += 1
            load = argv[i] if i < len(argv) else None
        elif tok in ("--downsampling", "-s"):
            i += 1
            try:
                downsampling = max(1, int(argv[i]))
            except (ValueError, IndexError):
                pass
        elif tok in ("--file", "-f"):
            i += 1
            if i < len(argv):
                files.append(
                    {
                        "path": argv[i],
                        "xcol": None,
                        "ycol": None,
                        "dxcol": None,
                        "dycol": None,
                    }
                )
        elif tok in col_options and files:
            i += 1
            try:
                value = int(argv[i])
            except (ValueError, IndexError):
                value = None
            if value is not None:
                key = col_options[tok]
                if _has_subsequent_file(i + 1):
                    files[-1][key] = value
                else:
                    global_cols[key] = value
        elif not tok.startswith("-"):
            files.append(
                {"path": tok, "xcol": None, "ycol": None, "dxcol": None, "dycol": None}
            )
        i += 1

    for f in files:
        for key, default in defaults.items():
            if f[key] is None:
                f[key] = global_cols[key] if global_cols[key] is not None else default

    return {"files": files, "load": load, "downsampling": downsampling}


[docs] def main(): """ Entry point for the ``pygra`` command-line interface. Parses ``sys.argv``, creates the Qt application and :class:`~pygra.mainwindow.MainWindow`, optionally restores a saved session or pre-loads data files, then starts the Qt event loop. """ args = _parse_interleaved(sys.argv[1:]) from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QIcon from .mainwindow import MainWindow import os app = QApplication(sys.argv) app.setStyle("Fusion") # search for icon in logo/ subfolder first, then package root pkg_root = os.path.dirname(os.path.dirname(__file__)) logo = None for candidate in [ os.path.join(pkg_root, "logo", "pygra_dock_icon.png"), os.path.join(pkg_root, "logo", "pygra_logo.png"), os.path.join(pkg_root, "pygra_logo.png"), ]: if os.path.exists(candidate): logo = candidate break if logo: icon = QIcon(logo) app.setWindowIcon(icon) # on Linux, also set the taskbar/dock icon via the window icon # (requires a .desktop file for full dock integration — see README) win = MainWindow() if args["load"]: try: win._load_state_from_path(args["load"]) except Exception as e: print(f"Warning: could not load session: {e}", file=sys.stderr) for f in args["files"]: win._load_file( f["path"], xcol=f["xcol"], ycol=f["ycol"], dxcol=f["dxcol"], dycol=f["dycol"], step=args["downsampling"], ) win.show() sys.exit(app.exec_())