# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

from __future__ import annotations

import os
import re
import textwrap
from enum import Enum
from typing import TYPE_CHECKING

from gitfourchette.localization import *
from gitfourchette.porcelain import *

if TYPE_CHECKING:
    from gitfourchette.toolbox.gitutils import PatchPurpose


_userCommandsGuideUrl = "https://gitfourchette.org/guide/commands"


def _tokenReferenceTable(table):
    markup = "<table>"
    for token, caption in table.items():
        markup += f"<tr><td><b>{token} </b></td><td> {caption}</td></tr>\n"
    markup += "</table>"
    return markup


class TrTables:
    _enums                      : dict[type[Enum], dict[Enum, str]] = {}
    _exceptionNames             : dict[str, str] = {}
    _prefKeys                   : dict[str, str] = {}
    _diffStatusChars            : dict[str, str] = {}
    _shortFileModes             : dict[FileMode, str] = {}
    _patchPurposesPastTense     : dict[PatchPurpose, str] = {}

    @classmethod
    def init(cls):
        if not cls._exceptionNames:
            cls.retranslate()

    @classmethod
    def retranslate(cls):
        cls._enums = cls._init_enums()
        cls._exceptionNames = cls._init_exceptionNames()
        cls._prefKeys = cls._init_prefKeys()
        cls._diffStatusChars = cls._init_diffStatusChars()
        cls._shortFileModes = cls._init_shortFileModes()
        cls._patchPurposesPastTense = cls._init_patchPurposesPastTense()

    @classmethod
    def enum(cls, enumValue: Enum) -> str:
        try:
            return cls._enums[type(enumValue)][enumValue]
        except KeyError:
            return enumValue.name

    @classmethod
    def exceptionName(cls, exc: BaseException):
        name = type(exc).__name__
        return cls._exceptionNames.get(name, name)

    @classmethod
    def prefKey(cls, key: str):
        return cls._prefKeys.get(key, str(key))

    @classmethod
    def prefKeyNoDefault(cls, key: str):
        return cls._prefKeys.get(key, "")

    @classmethod
    def diffStatusChar(cls, c: str):
        return cls._diffStatusChars.get(c, c)

    @classmethod
    def shortFileModes(cls, m: FileMode):
        try:
            return cls._shortFileModes[m]
        except KeyError:
            return f"{m:o}"

    @classmethod
    def patchPurposePastTense(cls, purpose: PatchPurpose):
        return cls._patchPurposesPastTense.get(purpose, "???")

    @staticmethod
    def _init_exceptionNames():
        return {
            "ConnectionRefusedError": _("Connection refused"),
            "FileNotFoundError": _("File not found"),
            "PermissionError": _("Permission denied"),
            "GitError": _("Git error"),
            "NotImplementedError": _("Unsupported feature"),
            "InterruptedError": _("Operation interrupted"),
        }

    @staticmethod
    def _init_enums():
        from gitfourchette.gitdriver import GitConflictSides
        from gitfourchette.porcelain import FileMode, NameValidationError
        from gitfourchette.toolbox import toLengthVariants
        from gitfourchette.sidebar.sidebarmodel import SidebarItem
        from gitfourchette.settings import GraphRowHeight, QtApiNames, GraphRefBoxWidth, RefSort, TabBarClick, FileListClick
        from gitfourchette.toolbox import PatchPurpose, PathDisplayStyle, AuthorDisplayStyle
        from gitfourchette.repomodel import GpgStatus

        NVERule = NameValidationError.Rule

        return {
            NVERule: {
                NVERule.ILLEGAL_NAME            : _("Illegal name."),
                NVERule.ILLEGAL_SUFFIX          : _("Illegal suffix."),
                NVERule.ILLEGAL_PREFIX          : _("Illegal prefix."),
                NVERule.CONTAINS_ILLEGAL_SEQ    : _("Contains illegal character sequence."),
                NVERule.CONTAINS_ILLEGAL_CHAR   : _("Contains illegal character."),
                NVERule.CANNOT_BE_EMPTY         : _("Cannot be empty."),
                NVERule.NOT_WINDOWS_FRIENDLY    : _("This name is discouraged for compatibility with Windows."),
                NVERule.NAME_TAKEN_BY_REF       : _("This name is already taken."),
                NVERule.NAME_TAKEN_BY_FOLDER    : _("This name is already taken by a folder."),
                NVERule.NOT_A_FOLDER            : _("The folder in this path clashes with an existing ref that isn’t a folder."),
            },

            FileMode: {
                0                       : _p("FileMode", "deleted"),
                FileMode.BLOB           : _p("FileMode", "regular file"),
                FileMode.BLOB_EXECUTABLE: _p("FileMode", "executable file"),
                FileMode.LINK           : _p("FileMode", "symbolic link"),
                FileMode.TREE           : _p("FileMode", "subtree"),
                FileMode.COMMIT         : _p("FileMode", "subtree commit"),
            },

            RepositoryState: {
                RepositoryState.NONE                    : _p("RepositoryState", "None"),
                RepositoryState.MERGE                   : _p("RepositoryState", "Merging"),
                RepositoryState.REVERT                  : _p("RepositoryState", "Reverting"),
                RepositoryState.REVERT_SEQUENCE         : _p("RepositoryState", "Reverting (sequence)"),
                RepositoryState.CHERRYPICK              : _p("RepositoryState", "Cherry-picking"),
                RepositoryState.CHERRYPICK_SEQUENCE     : _p("RepositoryState", "Cherry-picking (sequence)"),
                RepositoryState.BISECT                  : _p("RepositoryState", "Bisecting"),
                RepositoryState.REBASE                  : _p("RepositoryState", "Rebasing"),
                RepositoryState.REBASE_INTERACTIVE      : _p("RepositoryState", "Rebasing (interactive)"),
                RepositoryState.REBASE_MERGE            : _p("RepositoryState", "Rebasing (merging)"),
                RepositoryState.APPLY_MAILBOX           : "Apply Mailbox",  # intentionally untranslated
                RepositoryState.APPLY_MAILBOX_OR_REBASE : "Apply Mailbox or Rebase",  # intentionally untranslated
            },

            GitConflictSides: {
                GitConflictSides.BothDeleted    : _p("ConflictSides", "deleted by both sides"),
                GitConflictSides.AddedByUs      : _p("ConflictSides", "added by us"),
                GitConflictSides.DeletedByThem  : _p("ConflictSides", "deleted by them"),
                GitConflictSides.AddedByThem    : _p("ConflictSides", "added by them"),
                GitConflictSides.DeletedByUs    : _p("ConflictSides", "deleted by us"),
                GitConflictSides.BothAdded      : _p("ConflictSides", "added by both sides"),
                GitConflictSides.BothModified   : _p("ConflictSides", "modified by both sides"),
            },

            PatchPurpose: {
                PatchPurpose.Stage                          : _p("PatchPurpose", "Stage"),
                PatchPurpose.Unstage                        : _p("PatchPurpose", "Unstage"),
                PatchPurpose.Discard                        : _p("PatchPurpose", "Discard"),
                PatchPurpose.Lines | PatchPurpose.Stage     : _p("PatchPurpose", "Stage lines"),
                PatchPurpose.Lines | PatchPurpose.Unstage   : _p("PatchPurpose", "Unstage lines"),
                PatchPurpose.Lines | PatchPurpose.Discard   : _p("PatchPurpose", "Discard lines"),
                PatchPurpose.Hunk | PatchPurpose.Stage      : _p("PatchPurpose", "Stage hunk"),
                PatchPurpose.Hunk | PatchPurpose.Unstage    : _p("PatchPurpose", "Unstage hunk"),
                PatchPurpose.Hunk | PatchPurpose.Discard    : _p("PatchPurpose", "Discard hunk"),
                PatchPurpose.File | PatchPurpose.Stage      : _p("PatchPurpose", "Stage file"),
                PatchPurpose.File | PatchPurpose.Unstage    : _p("PatchPurpose", "Unstage file"),
                PatchPurpose.File | PatchPurpose.Discard    : _p("PatchPurpose", "Discard file"),
            },

            SidebarItem: {
                SidebarItem.UncommittedChanges  : toLengthVariants(_p("SidebarModel", "Working Directory|Workdir")),
                SidebarItem.LocalBranchesHeader : toLengthVariants(_p("SidebarModel", "Local Branches|Branches")),
                SidebarItem.StashesHeader       : _p("SidebarModel", "Stashes"),
                SidebarItem.RemotesHeader       : _p("SidebarModel", "Remotes"),
                SidebarItem.TagsHeader          : _p("SidebarModel", "Tags"),
                SidebarItem.SubmodulesHeader    : _p("SidebarModel", "Submodules"),
                SidebarItem.LocalBranch         : _p("SidebarModel", "Local branch"),
                SidebarItem.DetachedHead        : _p("SidebarModel", "Detached HEAD"),
                SidebarItem.UnbornHead          : _p("SidebarModel", "Unborn HEAD"),
                SidebarItem.RemoteBranch        : _p("SidebarModel", "Remote branch"),
                SidebarItem.Stash               : _p("SidebarModel", "Stash"),
                SidebarItem.Remote              : _p("SidebarModel", "Remote"),
                SidebarItem.Tag                 : _p("SidebarModel", "Tag"),
                SidebarItem.Submodule           : _p("SidebarModel", "Submodules"),
                SidebarItem.Spacer              : "---",
            },

            PathDisplayStyle: {
                PathDisplayStyle.FullPaths      : _("Full paths"),
                PathDisplayStyle.AbbreviateDirs : _("Abbreviate directories"),
                PathDisplayStyle.FileNameOnly   : _("Filename only"),
                PathDisplayStyle.FileNameFirst  : _("Filename first"),
            },

            AuthorDisplayStyle: {
                AuthorDisplayStyle.FullName     : _("Full name"),
                AuthorDisplayStyle.FirstName    : _("First name"),
                AuthorDisplayStyle.LastName     : _("Last name"),
                AuthorDisplayStyle.Initials     : _("Initials"),
                AuthorDisplayStyle.FullEmail    : _("Full email"),
                AuthorDisplayStyle.EmailUserName: _("Abbreviated email"),
            },

            GraphRowHeight: {
                GraphRowHeight.Cramped          : _p("row spacing", "Cramped"),
                GraphRowHeight.Tight            : _p("row spacing", "Tight"),
                GraphRowHeight.Relaxed          : _p("row spacing", "Relaxed"),
                GraphRowHeight.Roomy            : _p("row spacing", "Roomy"),
                GraphRowHeight.Spacious         : _p("row spacing", "Spacious"),
            },

            QtApiNames: {
                QtApiNames.Automatic            : _p("Qt binding", "Automatic (recommended)"),
                QtApiNames.PySide6              : "PySide6",
                QtApiNames.PyQt6                : "PyQt6",
                QtApiNames.PyQt5                : "PyQt5"
            },

            GraphRefBoxWidth: {
                GraphRefBoxWidth.IconsOnly      : _("Icons Only"),
                GraphRefBoxWidth.Standard       : _("Truncate long ref names"),
                GraphRefBoxWidth.Wide           : _("Show full ref names"),
            },

            RefSort: {
                RefSort.TimeDesc                : _p("sort refs by date of latest commit, descending", "Date, Newest First"),
                RefSort.TimeAsc                 : _p("sort refs by date of latest commit, ascending", "Date, Oldest First"),
                RefSort.AlphaAsc                : _p("sort refs alphabetically, ascending", "Name, A-Z"),
                RefSort.AlphaDesc               : _p("sort refs alphabetically, descending", "Name, Z-A"),
                RefSort.UseGlobalPref           : "",
            },

            GpgStatus: {
                GpgStatus.Unsigned              : _("Not signed"),
                GpgStatus.Pending               : _("Signature not verified yet"),
                GpgStatus.CantCheck             : _("Unable to verify signature"),
                GpgStatus.MissingKey            : _("Can’t verify signature; Key not in your keyring"),
                GpgStatus.GoodTrusted           : _("Good signature; Key trusted"),
                GpgStatus.GoodUntrusted         : _("Good signature; Key not fully trusted"),
                GpgStatus.ExpiredSig            : _("Good signature; Signature expired"),
                GpgStatus.ExpiredKey            : _("Good signature; Key expired"),
                GpgStatus.RevokedKey            : _("Good signature; Key revoked"),
                GpgStatus.Bad                   : _("Bad signature"),
                GpgStatus.ProcessError          : _("Failed to start verification process"),
            },

            TabBarClick: {
                TabBarClick.Nothing             : _("Do nothing"),
                TabBarClick.Folder              : _("Open repo folder"),
                TabBarClick.Terminal            : _("Open repo in terminal"),
                TabBarClick.Close               : _("Close tab"),
            },

            FileListClick: {
                FileListClick.Nothing           : _("Do nothing"),
                FileListClick.Stage             : _("Stage/unstage file"),
                FileListClick.Blame             : _("Blame file"),
                FileListClick.Edit              : _("Open file in external editor"),
                FileListClick.DiffTool          : _("Open file in external diff tool"),
                FileListClick.Folder            : _("Show file in folder"),
            },
        }

    @staticmethod
    def _init_diffStatusChars():
        # see git_diff_status_char (diff_print.c)
        return {
            "A": _p("FileStatus", "added"),
            "Z": _p("FileStatus", "added"),
            "C": _p("FileStatus", "copied"),
            "D": _p("FileStatus", "deleted"),
            "I": _p("FileStatus", "ignored"),
            "M": _p("FileStatus", "modified"),
            "R": _p("FileStatus", "renamed"),
            "T": _p("FileStatus", "file type changed"),
            "U": _p("FileStatus", "merge conflict"),  # "updated but unmerged"
            "X": _p("FileStatus", "unreadable"),
            "?": _p("FileStatus", "untracked"),
        }

    @staticmethod
    def _init_shortFileModes():
        return {
            0: "",
            FileMode.BLOB: "",
            FileMode.BLOB_EXECUTABLE: "+x",
            FileMode.LINK: _("link"),
            FileMode.TREE: _("new subtree"),
            FileMode.COMMIT: _("commit in subtree"),
        }

    @staticmethod
    def _init_patchPurposesPastTense():
        from gitfourchette.toolbox.gitutils import PatchPurpose as pp
        return {
            pp.Stage                : _p("PatchPurpose", "Staged."),
            pp.Unstage              : _p("PatchPurpose", "Unstaged."),
            pp.Discard              : _p("PatchPurpose", "Discarded."),
            pp.Lines | pp.Stage     : _p("PatchPurpose", "Lines staged."),
            pp.Lines | pp.Unstage   : _p("PatchPurpose", "Lines unstaged."),
            pp.Lines | pp.Discard   : _p("PatchPurpose", "Lines discarded."),
            pp.Hunk | pp.Stage      : _p("PatchPurpose", "Hunk staged."),
            pp.Hunk | pp.Unstage    : _p("PatchPurpose", "Hunk unstaged."),
            pp.Hunk | pp.Discard    : _p("PatchPurpose", "Hunk discarded."),
            pp.File | pp.Stage      : _p("PatchPurpose", "File staged."),
            pp.File | pp.Unstage    : _p("PatchPurpose", "File unstaged."),
            pp.File | pp.Discard    : _p("PatchPurpose", "File discarded."),
        }

    @staticmethod
    def _timeFormatTable():
        from gitfourchette.qt import QLocale, QDateTime, QDate, QTime

        locale = QLocale()
        firstDay = QDateTime(QDate(2000, 1, 1), QTime(0, 0))
        lastDay = QDateTime(QDate(2099, 12, 31), QTime(23, 59, 59))
        monday = QDateTime(QDate(2024, 12, 23), QTime(12, 0))
        sunday = QDateTime(QDate(2024, 12, 29), QTime(12, 0))

        def row(fmt: str, caption="", date1: QDateTime | None = firstDay, date2=lastDay):
            sample = ""
            if date1 is not None:
                f1 = locale.toString(date1, fmt)
                f2 = locale.toString(date2, fmt)
                sample = f1 + "–" + f2
                if caption:
                    sample = f", {sample}"
            return f"\n<code>{fmt:>4} </code> {caption}{sample}"

        return (
            "<html style='white-space: pre'>"
            + _p("date/time formats", "Available formats:")
            + "<p>"
            + row("yy", _("year"))
            + row("yyyy", _("year") + f", {QDate.currentDate().year()}", None)
            + "</p><p>"
            + row("M", _("month"))
            + row("MM", _("month"))
            + row("MMM")
            + row("MMMM")
            + "</p><p>"
            + row("d", _("day"))
            + row("dd", _("day"))
            + row("ddd", "", monday, sunday)
            + row("dddd", "", monday, sunday)
            + "</p><p>"
            + row("h", _("hour") + ", 0–23/1–12", None)
            + row("hh", _("hour") + ", 00–23/01–12", None)
            + row("mm", _("minute"))
            + row("ss", _("second"))
            + "</p><p>"
            + row("a")
            + row("A")
            + "</p>")

    @staticmethod
    def _init_prefKeys():
        from gitfourchette.toolbox.textutils import paragraphs, tquo, escape
        from gitfourchette.exttools.usercommand import UserCommand
        from gitfourchette.appconsts import APP_DISPLAY_NAME
        from gitfourchette.repomodel import GpgStatus

        sshAuthSock = os.environ.get("SSH_AUTH_SOCK", "")
        if sshAuthSock:
            sshAuthSockHelp = paragraphs([
                _("Note: Per {k}, your system is providing an ssh-agent ({v}). "
                  "It’s recommended to use this one."),
                _("If your system’s agent isn’t saving any passphrases, "
                  "make sure you’ve enabled {c} in your SSH configuration."),
            ])
        else:
            sshAuthSockHelp = _("Note: Per {k}, no ssh-agent seems to be running on your system.")
        sshAuthSockHelp = "<blockquote>" + sshAuthSockHelp.format(
            k="SSH_AUTH_SOCK", v=escape(sshAuthSock), c="AddKeysToAgent")

        return {
            "general": _p("Prefs", "General"),
            "diff": _p("Prefs", "Code"),
            "imageDiff": _p("Prefs", "Images"),
            "tabs": _p("Prefs", "Tabs"),
            "graph": _p("Prefs", "Commit History"),
            "trash": _p("Prefs", "Trash"),
            "git": _p("Prefs", "Git Integration"),
            "external": _p("Prefs", "External Tools"),
            "advanced": _p("Prefs", "Advanced"),
            "userCommands": _p("Prefs", "Custom Commands"),

            "language": _("Language"),
            "qtStyle": _("Qt style"),
            "shortHashChars": _("Shorten hashes to # characters"),
            "shortTimeFormat": _("Date/time format"),
            "shortTimeFormat_help": TrTables._timeFormatTable(),
            "pathDisplayStyle": _("Path display style"),
            "authorDisplayStyle": _("Author display style"),
            "maxRecentRepos": _("Remember up to # recent repositories"),
            "showStatusBar": _("Show status bar"),
            "showToolBar": _("Show toolbar"),
            "showMenuBar": _("Show menu bar"),
            "showMenuBar_help": _("When the menu bar is hidden, press the Alt key to show it again."),
            "resetDontShowAgain": _("Restore all “don’t show this again” messages"),
            "pygmentsPlugins": _("Allow third-party Pygments plugins"),
            "pygmentsPlugins_help": "<p>" + _("Let {app} load third-party Pygments plugins installed on your system. "
                                              "These plugins extend syntax highlighting with new languages "
                                              "and color schemes. <b>May incur significant slowdowns.</b>"),
            "refSort": _("Sort branches && tags by"),
            "refSort_help": paragraphs(
                _("The default sorting mode for local branches, remote branches, and tags in the sidebar."),
                _("You can fine-tune this setting in each repo by right-clicking Branches, Remotes, or Tags "
                  "in the sidebar. (Note that changing the default setting here will clear per-repo tweaks.)")),

            "font": _("Font"),
            "tabSpaces": _("One tab is # spaces"),
            "contextLines": _("Show up to # context lines"),
            "contextLines_help": _("Amount of unmodified lines to show around red or green lines in a diff."),
            "largeFileThresholdKB": _("Load diffs up to # KB"),
            "imageFileThresholdKB": _("Load images up to # KB"),
            "syntaxHighlighting": _("Syntax highlighting"),
            "wordWrap": _("Word wrap"),
            "showStrayCRs": _("Display alien line endings (CRLF)"),
            "colorblind": _("“-/+” colors"),
            "colorblind_help": _("Background colors for deleted (-) and added (+) lines."),
            "renderSvg": _("SVG files"),
            "renderSvg_false": _("Display as text"),
            "renderSvg_true": _("Display as images"),

            "tabCloseButton": _("Show tab close button"),
            "expandingTabs": _("Expand tabs to available width"),
            "autoHideTabs": _("Auto-hide tabs if only one repo is open"),

            "chronologicalOrder": _("Sort commits"),
            "chronologicalOrder_true": _("Chronologically"),
            "chronologicalOrder_false": _("Topologically"),
            "chronologicalOrder_help": paragraphs(
                _("<b>Chronological mode</b> lets you stay on top of the latest activity in the repository. "
                  "The most recent commits always show up at the top of the graph. "
                  "However, the graph can get messy when multiple branches receive commits in the same timeframe."),
                _("<b>Topological mode</b> makes the graph easier to read. It attempts to present sequences of "
                  "commits within a branch in a linear fashion. Since this is not a strictly chronological "
                  "mode, you may have to do more scrolling to see the latest changes in various branches."),
            ),
            "graphRowHeight": _("Row spacing"),
            "flattenLanes": _("Avoid gaps between branches in the graph"),
            "authorDiffAsterisk": _("Mark author/committer signature differences"),
            "authorDiffAsterisk_help": paragraphs(
                _("The commit history displays information about a commit’s <b>author</b>—"
                  "their name and the date at which they made the commit. But in some cases, a commit "
                  "might have been revised by someone else than the original author—"
                  "this person is called the <b>committer</b>."),
                _("If you tick this option, an asterisk (*) will appear after the author’s name "
                  "and/or date if they differ from the committer’s for any given commit."),
                _("Note that you can always hover over the author’s name or date to obtain "
                  "detailed information about the author and the committer."),
            ),
            "maxCommits": _("Load up to # commits in the history"),
            "maxCommits_help": _("Set to 0 to always load the full commit history."),
            "alternatingRowColors": _("Draw rows using alternating background colors"),
            "refBoxMaxWidth": _("Ref indicators"),
            "refBoxMaxWidth_help": _("You can always hover over an indicator to display the full name of the ref."),
            "verifyGpgOnTheFly": _("Verify signed commits on the fly"),
            "verifyGpgOnTheFly_help": _("As commits scroll into view, call {0} automatically to verify their signatures. "
                                        "The verification status is materialized by a seal icon next to the author’s name:", tquo("git verify-commit")
                                        ) + _tokenReferenceTable({
                GpgStatus.Pending.iconHtml()        : _("Verification pending"),
                GpgStatus.CantCheck.iconHtml()      : _("Verification failed (e.g. missing key)"),
                GpgStatus.GoodTrusted.iconHtml()    : _("Good signature; Key trusted"),
                GpgStatus.GoodUntrusted.iconHtml()  : _("Good signature; Key not fully trusted"),
                GpgStatus.ExpiredSig.iconHtml()     : _("Good signature; Key or signature expired"),
                GpgStatus.Bad.iconHtml()            : _("Key revoked or bad signature"),
            }) + "<br>" + _("(No seal = Commit isn’t signed)"),

            "maxTrashFiles": _("The trash keeps up to # discarded patches"),
            "maxTrashFileKB": _("Patches bigger than # KB won’t be salvaged"),
            "trash_HEADER": _(
                "When you discard changes from the working directory, {app} keeps a temporary copy in a hidden "
                "“trash” folder. This gives you a last resort to rescue changes that you have discarded by mistake. "
                "You can look around this trash folder via <i>“Help &rarr; Open Trash”</i>."),

            "verbosity": _("Logging verbosity"),
            "autoRefresh": _("Auto-refresh when app regains focus"),
            "autoRefresh_help": paragraphs(
                _("When you return to {app} from another application, it automatically "
                  "scans for changes in the working directory and local branches. "
                  "This keeps the interface in sync with the state of your repo on disk."),
                _("If you turn this off, you will need to hit {key} to "
                  "perform this refresh manually.", key="F5"),
                "<b>" + _("We strongly recommend to keep this setting enabled.") + "</b>"),
            "autoFetchMinutes": _("Auto-fetch remotes every # minutes"),
            "animations": _("Animation effects in sidebar"),
            "smoothScroll": _("Smooth scrolling (where applicable)"),
            "forceQtApi": _("Preferred Qt binding"),
            "forceQtApi_help": paragraphs(
                _("After restarting, {app} will use this Qt binding if available."),
                _("You can also pass the name of a Qt binding via the “QT_API” environment variable."),
            ),
            "condensedFonts": _("Use condensed fonts"),
            "condensedFonts_help": "<p>" + _(
                "When a branch name or author name is too long to fit in its allotted space, "
                "condense the font before truncating the text."),

            "externalEditor": _("Text editor"),
            "externalDiff": _("Diff tool"),
            "externalDiff_help":
                "<p style='white-space: pre'>" + _("Argument placeholders:") + "\n" + _tokenReferenceTable({
                    "$L": _("Old / Left"),
                    "$R": _("New / Right"),
                }),
            "externalMerge": _("Merge tool"),
            "externalMerge_help":
                "<p style='white-space: pre'>" + _("Argument placeholders:") + "\n" + _tokenReferenceTable({
                    "$B": _("Ancestor / Base / Center"),
                    "$L": _("Ours / Local / Left"),
                    "$R": _("Theirs / Remote / Right"),
                    "$M": _("Merged / Output / Result"),
                }),
            "terminal": _("Terminal"),
            "terminal_help": paragraphs(
                _("Argument placeholders:"),
                _tokenReferenceTable({"$COMMAND": _("Command to execute after launching the terminal")}),
                _("The {0} placeholder is mandatory. It is automatically substituted for a wrapper script that "
                  "enters your working directory and optionally starts one of your Custom Commands.",
                  "$COMMAND")),
            "gitPath": "git",
            "ownAskpass": _("Have OpenSSH ask for passphrases via {app}", app=APP_DISPLAY_NAME),
            "ownAskpass_help": paragraphs(
                _("Tick this to have OpenSSH use {app} to ask for passphrases."),
                _("Untick this if you’ve set up another program in the {0} environment variable (such as {1}).", tquo("SSH_ASKPASS"), tquo("ksshaskpass"))),
            "ownSshAgent": "ssh-agent",
            "ownSshAgent_false": _("Use ssh-agent provided by the system") + " " + "" if sshAuthSock else _("(not detected)"),
            "ownSshAgent_true": _("Have {app} manage its own ssh-agent", app=APP_DISPLAY_NAME),
            "ownSshAgent_help": paragraphs(
                _("“ssh-agent” can save your SSH credentials so you don’t have to retype the same passphrase over and over. "
                  "Some Linux distributions set up an ssh-agent for you."),
                _("You can also have {app} start its own instance of ssh-agent "
                  "for the duration of your session and have it remember passphrases."),
                sshAuthSockHelp,
            ),
            "lfsAware": _("Parse LFS pointers"),
            "lfsAware_help": paragraphs(
                _("Tick this to display the real contents from LFS files."),
                _("Untick to display the raw text in LFS pointers."),
            ),

            "mouseShortcuts": _("Mouse Shortcuts"),
            "mouseShortcuts_HEADER": _("Tip: If your mouse has side buttons, you can use them to navigate back/forward in the repo."),
            "tabBarClicks": _("Repository tabs:"),
            "doubleClickTabBar": _("Double-click tab"),
            "middleClickTabBar": _("Middle-click tab"),
            "fileListClicks": _("File lists:"),
            "doubleClickFileList": _("Double-click file"),
            "middleClickFileList": _("Middle-click file"),
            "diffViewClicks": _("Diff view:"),
            "middleClickStageLines": _("Middle-click selection"),
            "middleClickStageLines_true": _("Stage/unstage selected lines"),
            "middleClickStageLines_false": _("Do nothing"),

            "userCommands_GUIDE": TrTables.userCommandsGuide(),
            "commands": "",
            "confirmCommands": _("Ask for confirmation before running any command"),
            "confirmCommands_help": _(
                "If you untick this, you can still force a prompt to appear for "
                "specific commands by prepending them with {0}. For example: {1}",
                tquo(f"<tt>{UserCommand.AlwaysConfirmPrefix}</tt>"),
                "<pre>?git stash</pre>"),
        }

    @staticmethod
    def userCommandsGuide():
        from gitfourchette.toolbox.textutils import paragraphs, linkify, stripAccelerators, tquo
        from gitfourchette.exttools.usercommand import UserCommand

        def cmdName(s: str):
            s = re.sub(r"&(.)", r"<u>&amp;\1</u>", s)
            return f"<com># {s}</com>"

        markup = textwrap.dedent("""\
        <style>
        body {background-color: palette(window); }
        pre { font-size: small; white-space: pre-wrap; margin: 0px; margin-left: 16px; }
        com { color: gray; font-style: italic; }
        tok { font-weight: bold; }
        </style><body>""")
        markup += paragraphs(_("Feel free to copy the sample below and paste it into the text box. "
                               "Then, click OK, and explore {menu} in the menu bar.",
                               menu=tquo(stripAccelerators(_("&Commands")))))
        markup += textwrap.dedent(f"""\
        <pre>
        git rebase -i <tok>$COMMIT</tok>   {cmdName(_("&Interactive Rebase"))}
        git rebase --continue   {cmdName(_("&Continue Rebase"))}
        <tok>?</tok> git rebase --abort    {cmdName(_("&Abort Rebase"))}
        git diff <tok>$COMMIT</tok> HEAD   {cmdName(_("Diff Commit With &HEAD"))}
        </pre>""")
        markup += paragraphs(_("You may use the following placeholders in your commands:"))
        markup += _tokenReferenceTable(UserCommand.tokenHelpTable())
        markup += paragraphs(linkify(_("For advanced usage tips, please visit [the user’s guide]."
                                       ), _userCommandsGuideUrl))
        return markup
