vibefpmgui.new_project

File → New fpm Project…: fpm new with its options chosen in a dialog.

new_command makes the command, name_problem checks a name as fpm does; NewProjectDialog asks for them, showing the command it will run. fpm makes one of three kinds of project (its options for them can't be mixed): full (--full, the default here: an annotated fpm.toml and every folder), the folders chosen (--lib, --app…, with a minimal fpm.toml), or bare (--bare: fpm.toml and README.md only).

  1"""File → New fpm Project…: ``fpm new`` with its options chosen in a dialog.
  2
  3`new_command` makes the command, `name_problem` checks a name as fpm does;
  4`NewProjectDialog` asks for them, showing the command it will run. fpm
  5makes one of three kinds of project (its options for them can't be mixed):
  6*full* (``--full``, the default here: an annotated ``fpm.toml`` and every
  7folder), the *folders chosen* (``--lib``, ``--app``…, with a minimal
  8``fpm.toml``), or *bare* (``--bare``: ``fpm.toml`` and ``README.md`` only).
  9"""
 10
 11from __future__ import annotations
 12
 13import html
 14import re
 15from pathlib import Path
 16
 17from qtpy.QtCore import Qt
 18from qtpy.QtWidgets import (
 19    QButtonGroup,
 20    QCheckBox,
 21    QDialog,
 22    QDialogButtonBox,
 23    QFileDialog,
 24    QFormLayout,
 25    QHBoxLayout,
 26    QLabel,
 27    QLineEdit,
 28    QPushButton,
 29    QRadioButton,
 30    QVBoxLayout,
 31    QWidget,
 32)
 33
 34NO_NAME = "Give the project a name."
 35NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,62}$")  # (fpm's rule: up to 63, starting with a letter)
 36# The parts a project can be made with: (flag, what it is, its folder).
 37FULL, CHOSEN, BARE = "full", "chosen", "bare"  # the kinds of project
 38PARTS = (("--lib", "Library", "src/"), ("--app", "Application", "app/"), ("--test", "Tests", "test/"),
 39         ("--example", "Example", "example/"))
 40
 41
 42def name_problem(name: str, folder: Path, backfill: bool = False) -> str:
 43    """Why ``name`` can't be a new project in ``folder`` ("" if it can)."""
 44    if not name:
 45        return NO_NAME
 46    if not NAME_RE.match(name):
 47        return ("A name is up to 63 letters, digits, underscores or hyphens, starting with a letter "
 48                "(no spaces).")
 49    if not folder.is_dir():
 50        return f"There's no folder {folder}."
 51    target = folder / name
 52    if target.exists() and not backfill:
 53        return f"{target} already exists (Backfill adds only what's missing to it)."
 54    if target.exists() and not target.is_dir():
 55        return f"{target} is a file."
 56    return ""
 57
 58
 59def new_command(fpm: str, name: str, kind: str = FULL, parts: set[str] = frozenset(),
 60                backfill: bool = False) -> list[str]:
 61    """``fpm new NAME`` for a kind of project: --full, the ``parts`` chosen
 62    (their flags, in fpm's order), or --bare; and --backfill."""
 63    command = [fpm, "new", name]
 64    if kind == FULL:
 65        command.append("--full")
 66    elif kind == BARE:
 67        command.append("--bare")
 68    else:
 69        command += [flag for flag, _what, _folder in PARTS if flag in parts]
 70    if backfill:
 71        command.append("--backfill")
 72    return command
 73
 74
 75class NewProjectDialog(QDialog):
 76    """The new project's name, folder and parts; Create runs nothing itself
 77    (see ``command`` and ``project_folder``: the window runs fpm)."""
 78
 79    def __init__(self, fpm: str, folder: Path, parent=None):
 80        super().__init__(parent)
 81        self.fpm = fpm
 82        self.setWindowTitle("New fpm Project")
 83        self.name_edit = QLineEdit()
 84        self.name_edit.setPlaceholderText("my_project")
 85        self.folder_edit = QLineEdit(str(folder))
 86        browse = QPushButton("Browse…")
 87        browse.clicked.connect(self._browse)
 88        where = QHBoxLayout()
 89        where.addWidget(self.folder_edit, 1)
 90        where.addWidget(browse)
 91        # The kind of project (fpm's options for them can't be mixed).
 92        self.kind_buttons: dict[str, QRadioButton] = {}
 93        self.kinds = QButtonGroup(self)
 94        kinds = QVBoxLayout()
 95        kinds.setSpacing(2)
 96        for kind, text, tip in (
 97                (FULL, "Full: an annotated fpm.toml, and src/, app/, test/ and example/",
 98                 ("--full: a fpm.toml with every setting explained (and links to the documentation), and every "
 99                 "folder with a placeholder Fortran file")),
100                (CHOSEN, "Only the folders chosen, with a minimal fpm.toml:",
101                 "--lib, --app, --test, --example: the folders ticked below"),
102                (BARE, "Bare: fpm.toml and README.md only", "--bare: no folders or Fortran files")):
103            button = QRadioButton(text)
104            button.setToolTip(tip)
105            button.toggled.connect(self._update)
106            self.kinds.addButton(button)
107            self.kind_buttons[kind] = button
108            kinds.addWidget(button)
109            if kind == CHOSEN:
110                self.part_checks: dict[str, QCheckBox] = {}
111                for flag, what, folder_name in PARTS:
112                    check = QCheckBox(f"{what} ({folder_name})")
113                    check.setChecked(flag != "--example")  # (fpm's own choice without --full)
114                    check.setToolTip(f"{flag}: create {folder_name} with a placeholder Fortran file")
115                    check.toggled.connect(self._update)
116                    row = QHBoxLayout()
117                    row.setContentsMargins(22, 0, 0, 0)
118                    row.addWidget(check)
119                    kinds.addLayout(row)
120                    self.part_checks[flag] = check
121        self.kind_buttons[FULL].setChecked(True)
122        parts_box = QWidget()
123        parts_box.setLayout(kinds)
124        self.backfill_check = QCheckBox("Backfill an existing folder (add only what's missing)")
125        self.backfill_check.setToolTip("--backfill: the folder may exist already; only the files and folders "
126                                       "it doesn't have are created")
127        self.backfill_check.toggled.connect(self._update)
128        self.command_label = QLabel()
129        self.command_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
130        self.command_label.setWordWrap(True)
131        self.problem_label = QLabel()
132        self.problem_label.setWordWrap(True)
133        self.problem_label.setStyleSheet("color: #e03131;")
134        form = QFormLayout()
135        form.addRow("Name:", self.name_edit)
136        form.addRow("Create in:", where)
137        form.addRow("Make:", parts_box)
138        form.addRow("", self.backfill_check)
139        note = QLabel("<span style='color:gray'>fpm also makes it a git repository; it opens here once it's "
140                      "made.</span>")
141        note.setWordWrap(True)
142        self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
143        self.create_button = self.buttons.addButton("Create", QDialogButtonBox.AcceptRole)
144        self.buttons.accepted.connect(self.accept)
145        self.buttons.rejected.connect(self.reject)
146        layout = QVBoxLayout(self)
147        layout.addLayout(form)
148        layout.addWidget(note)
149        layout.addWidget(self.command_label)
150        layout.addWidget(self.problem_label)
151        layout.addWidget(self.buttons)
152        self.name_edit.textChanged.connect(self._update)
153        self.folder_edit.textChanged.connect(self._update)
154        self.setMinimumWidth(520)
155        self._update()
156
157    def _browse(self):
158        folder = QFileDialog.getExistingDirectory(self, "Create the project in", self.folder_edit.text())
159        if folder:
160            self.folder_edit.setText(folder)
161
162    def kind(self) -> str:
163        return next(kind for kind, button in self.kind_buttons.items() if button.isChecked())
164
165    def parts(self) -> set[str]:
166        return {flag for flag, check in self.part_checks.items() if check.isChecked()}
167
168    def project_folder(self) -> Path:
169        return Path(self.folder_edit.text().strip()).expanduser() / self.name_edit.text().strip()
170
171    def command(self) -> list[str]:
172        return new_command(self.fpm, self.name_edit.text().strip(), self.kind(), self.parts(),
173                           self.backfill_check.isChecked())
174
175    def problem(self) -> str:
176        """What's wrong (the name's first: it's the first field), or ""."""
177        problem = name_problem(self.name_edit.text().strip(), Path(self.folder_edit.text().strip()).expanduser(),
178                               self.backfill_check.isChecked())
179        if not problem and self.kind() == CHOSEN and not self.parts():
180            problem = "Choose at least one folder."
181        return problem
182
183    def _update(self, *_):
184        if not hasattr(self, "create_button"):
185            return  # (being built)
186        for check in self.part_checks.values():
187            check.setEnabled(self.kind() == CHOSEN)
188        problem = self.problem()
189        self.problem_label.setText("" if problem == NO_NAME else problem)  # (not before anything's typed)
190        self.create_button.setEnabled(not problem)
191        shown = " ".join(["fpm", *self.command()[1:]])
192        self.command_label.setText(f"<span style='color:gray'>Runs, in {html.escape(self.folder_edit.text().strip())}"
193                                   f":</span> <code>{html.escape(shown)}</code>")
NO_NAME = 'Give the project a name.'
NAME_RE = re.compile('^[A-Za-z][A-Za-z0-9_-]{0,62}$')
PARTS = (('--lib', 'Library', 'src/'), ('--app', 'Application', 'app/'), ('--test', 'Tests', 'test/'), ('--example', 'Example', 'example/'))
def name_problem(name: str, folder: pathlib.Path, backfill: bool = False) -> str:
43def name_problem(name: str, folder: Path, backfill: bool = False) -> str:
44    """Why ``name`` can't be a new project in ``folder`` ("" if it can)."""
45    if not name:
46        return NO_NAME
47    if not NAME_RE.match(name):
48        return ("A name is up to 63 letters, digits, underscores or hyphens, starting with a letter "
49                "(no spaces).")
50    if not folder.is_dir():
51        return f"There's no folder {folder}."
52    target = folder / name
53    if target.exists() and not backfill:
54        return f"{target} already exists (Backfill adds only what's missing to it)."
55    if target.exists() and not target.is_dir():
56        return f"{target} is a file."
57    return ""

Why name can't be a new project in folder ("" if it can).

def new_command( fpm: str, name: str, kind: str = 'full', parts: set[str] = frozenset(), backfill: bool = False) -> list[str]:
60def new_command(fpm: str, name: str, kind: str = FULL, parts: set[str] = frozenset(),
61                backfill: bool = False) -> list[str]:
62    """``fpm new NAME`` for a kind of project: --full, the ``parts`` chosen
63    (their flags, in fpm's order), or --bare; and --backfill."""
64    command = [fpm, "new", name]
65    if kind == FULL:
66        command.append("--full")
67    elif kind == BARE:
68        command.append("--bare")
69    else:
70        command += [flag for flag, _what, _folder in PARTS if flag in parts]
71    if backfill:
72        command.append("--backfill")
73    return command

fpm new NAME for a kind of project: --full, the parts chosen (their flags, in fpm's order), or --bare; and --backfill.

class NewProjectDialog(PySide6.QtWidgets.QDialog):
 76class NewProjectDialog(QDialog):
 77    """The new project's name, folder and parts; Create runs nothing itself
 78    (see ``command`` and ``project_folder``: the window runs fpm)."""
 79
 80    def __init__(self, fpm: str, folder: Path, parent=None):
 81        super().__init__(parent)
 82        self.fpm = fpm
 83        self.setWindowTitle("New fpm Project")
 84        self.name_edit = QLineEdit()
 85        self.name_edit.setPlaceholderText("my_project")
 86        self.folder_edit = QLineEdit(str(folder))
 87        browse = QPushButton("Browse…")
 88        browse.clicked.connect(self._browse)
 89        where = QHBoxLayout()
 90        where.addWidget(self.folder_edit, 1)
 91        where.addWidget(browse)
 92        # The kind of project (fpm's options for them can't be mixed).
 93        self.kind_buttons: dict[str, QRadioButton] = {}
 94        self.kinds = QButtonGroup(self)
 95        kinds = QVBoxLayout()
 96        kinds.setSpacing(2)
 97        for kind, text, tip in (
 98                (FULL, "Full: an annotated fpm.toml, and src/, app/, test/ and example/",
 99                 ("--full: a fpm.toml with every setting explained (and links to the documentation), and every "
100                 "folder with a placeholder Fortran file")),
101                (CHOSEN, "Only the folders chosen, with a minimal fpm.toml:",
102                 "--lib, --app, --test, --example: the folders ticked below"),
103                (BARE, "Bare: fpm.toml and README.md only", "--bare: no folders or Fortran files")):
104            button = QRadioButton(text)
105            button.setToolTip(tip)
106            button.toggled.connect(self._update)
107            self.kinds.addButton(button)
108            self.kind_buttons[kind] = button
109            kinds.addWidget(button)
110            if kind == CHOSEN:
111                self.part_checks: dict[str, QCheckBox] = {}
112                for flag, what, folder_name in PARTS:
113                    check = QCheckBox(f"{what} ({folder_name})")
114                    check.setChecked(flag != "--example")  # (fpm's own choice without --full)
115                    check.setToolTip(f"{flag}: create {folder_name} with a placeholder Fortran file")
116                    check.toggled.connect(self._update)
117                    row = QHBoxLayout()
118                    row.setContentsMargins(22, 0, 0, 0)
119                    row.addWidget(check)
120                    kinds.addLayout(row)
121                    self.part_checks[flag] = check
122        self.kind_buttons[FULL].setChecked(True)
123        parts_box = QWidget()
124        parts_box.setLayout(kinds)
125        self.backfill_check = QCheckBox("Backfill an existing folder (add only what's missing)")
126        self.backfill_check.setToolTip("--backfill: the folder may exist already; only the files and folders "
127                                       "it doesn't have are created")
128        self.backfill_check.toggled.connect(self._update)
129        self.command_label = QLabel()
130        self.command_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
131        self.command_label.setWordWrap(True)
132        self.problem_label = QLabel()
133        self.problem_label.setWordWrap(True)
134        self.problem_label.setStyleSheet("color: #e03131;")
135        form = QFormLayout()
136        form.addRow("Name:", self.name_edit)
137        form.addRow("Create in:", where)
138        form.addRow("Make:", parts_box)
139        form.addRow("", self.backfill_check)
140        note = QLabel("<span style='color:gray'>fpm also makes it a git repository; it opens here once it's "
141                      "made.</span>")
142        note.setWordWrap(True)
143        self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
144        self.create_button = self.buttons.addButton("Create", QDialogButtonBox.AcceptRole)
145        self.buttons.accepted.connect(self.accept)
146        self.buttons.rejected.connect(self.reject)
147        layout = QVBoxLayout(self)
148        layout.addLayout(form)
149        layout.addWidget(note)
150        layout.addWidget(self.command_label)
151        layout.addWidget(self.problem_label)
152        layout.addWidget(self.buttons)
153        self.name_edit.textChanged.connect(self._update)
154        self.folder_edit.textChanged.connect(self._update)
155        self.setMinimumWidth(520)
156        self._update()
157
158    def _browse(self):
159        folder = QFileDialog.getExistingDirectory(self, "Create the project in", self.folder_edit.text())
160        if folder:
161            self.folder_edit.setText(folder)
162
163    def kind(self) -> str:
164        return next(kind for kind, button in self.kind_buttons.items() if button.isChecked())
165
166    def parts(self) -> set[str]:
167        return {flag for flag, check in self.part_checks.items() if check.isChecked()}
168
169    def project_folder(self) -> Path:
170        return Path(self.folder_edit.text().strip()).expanduser() / self.name_edit.text().strip()
171
172    def command(self) -> list[str]:
173        return new_command(self.fpm, self.name_edit.text().strip(), self.kind(), self.parts(),
174                           self.backfill_check.isChecked())
175
176    def problem(self) -> str:
177        """What's wrong (the name's first: it's the first field), or ""."""
178        problem = name_problem(self.name_edit.text().strip(), Path(self.folder_edit.text().strip()).expanduser(),
179                               self.backfill_check.isChecked())
180        if not problem and self.kind() == CHOSEN and not self.parts():
181            problem = "Choose at least one folder."
182        return problem
183
184    def _update(self, *_):
185        if not hasattr(self, "create_button"):
186            return  # (being built)
187        for check in self.part_checks.values():
188            check.setEnabled(self.kind() == CHOSEN)
189        problem = self.problem()
190        self.problem_label.setText("" if problem == NO_NAME else problem)  # (not before anything's typed)
191        self.create_button.setEnabled(not problem)
192        shown = " ".join(["fpm", *self.command()[1:]])
193        self.command_label.setText(f"<span style='color:gray'>Runs, in {html.escape(self.folder_edit.text().strip())}"
194                                   f":</span> <code>{html.escape(shown)}</code>")

The new project's name, folder and parts; Create runs nothing itself (see command and project_folder: the window runs fpm).

NewProjectDialog(fpm: str, folder: pathlib.Path, parent=None)
 80    def __init__(self, fpm: str, folder: Path, parent=None):
 81        super().__init__(parent)
 82        self.fpm = fpm
 83        self.setWindowTitle("New fpm Project")
 84        self.name_edit = QLineEdit()
 85        self.name_edit.setPlaceholderText("my_project")
 86        self.folder_edit = QLineEdit(str(folder))
 87        browse = QPushButton("Browse…")
 88        browse.clicked.connect(self._browse)
 89        where = QHBoxLayout()
 90        where.addWidget(self.folder_edit, 1)
 91        where.addWidget(browse)
 92        # The kind of project (fpm's options for them can't be mixed).
 93        self.kind_buttons: dict[str, QRadioButton] = {}
 94        self.kinds = QButtonGroup(self)
 95        kinds = QVBoxLayout()
 96        kinds.setSpacing(2)
 97        for kind, text, tip in (
 98                (FULL, "Full: an annotated fpm.toml, and src/, app/, test/ and example/",
 99                 ("--full: a fpm.toml with every setting explained (and links to the documentation), and every "
100                 "folder with a placeholder Fortran file")),
101                (CHOSEN, "Only the folders chosen, with a minimal fpm.toml:",
102                 "--lib, --app, --test, --example: the folders ticked below"),
103                (BARE, "Bare: fpm.toml and README.md only", "--bare: no folders or Fortran files")):
104            button = QRadioButton(text)
105            button.setToolTip(tip)
106            button.toggled.connect(self._update)
107            self.kinds.addButton(button)
108            self.kind_buttons[kind] = button
109            kinds.addWidget(button)
110            if kind == CHOSEN:
111                self.part_checks: dict[str, QCheckBox] = {}
112                for flag, what, folder_name in PARTS:
113                    check = QCheckBox(f"{what} ({folder_name})")
114                    check.setChecked(flag != "--example")  # (fpm's own choice without --full)
115                    check.setToolTip(f"{flag}: create {folder_name} with a placeholder Fortran file")
116                    check.toggled.connect(self._update)
117                    row = QHBoxLayout()
118                    row.setContentsMargins(22, 0, 0, 0)
119                    row.addWidget(check)
120                    kinds.addLayout(row)
121                    self.part_checks[flag] = check
122        self.kind_buttons[FULL].setChecked(True)
123        parts_box = QWidget()
124        parts_box.setLayout(kinds)
125        self.backfill_check = QCheckBox("Backfill an existing folder (add only what's missing)")
126        self.backfill_check.setToolTip("--backfill: the folder may exist already; only the files and folders "
127                                       "it doesn't have are created")
128        self.backfill_check.toggled.connect(self._update)
129        self.command_label = QLabel()
130        self.command_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
131        self.command_label.setWordWrap(True)
132        self.problem_label = QLabel()
133        self.problem_label.setWordWrap(True)
134        self.problem_label.setStyleSheet("color: #e03131;")
135        form = QFormLayout()
136        form.addRow("Name:", self.name_edit)
137        form.addRow("Create in:", where)
138        form.addRow("Make:", parts_box)
139        form.addRow("", self.backfill_check)
140        note = QLabel("<span style='color:gray'>fpm also makes it a git repository; it opens here once it's "
141                      "made.</span>")
142        note.setWordWrap(True)
143        self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
144        self.create_button = self.buttons.addButton("Create", QDialogButtonBox.AcceptRole)
145        self.buttons.accepted.connect(self.accept)
146        self.buttons.rejected.connect(self.reject)
147        layout = QVBoxLayout(self)
148        layout.addLayout(form)
149        layout.addWidget(note)
150        layout.addWidget(self.command_label)
151        layout.addWidget(self.problem_label)
152        layout.addWidget(self.buttons)
153        self.name_edit.textChanged.connect(self._update)
154        self.folder_edit.textChanged.connect(self._update)
155        self.setMinimumWidth(520)
156        self._update()

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

fpm
name_edit
folder_edit
kind_buttons: dict[str, PySide6.QtWidgets.QRadioButton]
kinds
backfill_check
command_label
problem_label
buttons
create_button
def kind(self) -> str:
163    def kind(self) -> str:
164        return next(kind for kind, button in self.kind_buttons.items() if button.isChecked())
def parts(self) -> set[str]:
166    def parts(self) -> set[str]:
167        return {flag for flag, check in self.part_checks.items() if check.isChecked()}
def project_folder(self) -> pathlib.Path:
169    def project_folder(self) -> Path:
170        return Path(self.folder_edit.text().strip()).expanduser() / self.name_edit.text().strip()
def command(self) -> list[str]:
172    def command(self) -> list[str]:
173        return new_command(self.fpm, self.name_edit.text().strip(), self.kind(), self.parts(),
174                           self.backfill_check.isChecked())
def problem(self) -> str:
176    def problem(self) -> str:
177        """What's wrong (the name's first: it's the first field), or ""."""
178        problem = name_problem(self.name_edit.text().strip(), Path(self.folder_edit.text().strip()).expanduser(),
179                               self.backfill_check.isChecked())
180        if not problem and self.kind() == CHOSEN and not self.parts():
181            problem = "Choose at least one folder."
182        return problem

What's wrong (the name's first: it's the first field), or "".

staticMetaObject = PySide6.QtCore.QMetaObject("NewProjectDialog" inherits "QDialog": )