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>")
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).
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.
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).
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.
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 "".