Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e93b45a
Add basic skeleton with welcome screen
jfy133 May 25, 2024
266a163
Update changelgo
jfy133 May 25, 2024
71a1475
Fix linting failure
jfy133 May 25, 2024
a583018
Linting
jfy133 May 25, 2024
5e1dc52
Move common util functions/classes to common location
jfy133 May 26, 2024
03eaa52
Move textual CSS to common place
jfy133 May 26, 2024
b82b6fb
Add config type question
jfy133 May 26, 2024
5f6b2a4
Start adding basic details screen. Missing: validation. Not working: …
jfy133 May 26, 2024
e8233f5
Merge branch 'dev' into configbuilder2
jfy133 Jun 1, 2024
17971e6
Fix function calling due to move to generic location
jfy133 Jun 1, 2024
15f5be8
Copy over @mirpedrol 's writing functions
jfy133 Jun 1, 2024
4af6e99
Start making config writing function actually write nextflow configs
jfy133 Jun 2, 2024
2d7863a
Add URL saving, start adding validation: problem unless everything fi…
jfy133 Jun 8, 2024
edc2d8f
Merge branch 'dev' into configbuilder2
jfy133 Jun 30, 2024
e01c486
Merge branch 'dev' into configbuilder2
jfy133 Jun 30, 2024
f0cb5e9
Small debugging, now know the issue
jfy133 Jun 30, 2024
66227b7
Fixing writing of parameters to the input file when no input from user
jfy133 Jun 30, 2024
f8ad151
Merge branch 'dev' of https://github.com/nf-core/tools into configbui…
mirpedrol Mar 31, 2025
46c4c0a
Add back configs create command
mirpedrol Apr 2, 2025
dc22d8f
update to new version of textual and rename CreateConfig classes
mirpedrol Apr 2, 2025
544fd89
add screen asking if the config is nf-core
mirpedrol Apr 2, 2025
33e88dd
add validation of basicdetails
mirpedrol Apr 2, 2025
227abc2
conditional validation for nf-core configs
mirpedrol Apr 2, 2025
954b263
Merge branch 'dev' into configbuilder2
mirpedrol May 5, 2025
48ce7a5
remove config author and url from pipeline configs
mirpedrol May 5, 2025
078520a
fix custom fields using hide class and add pipeline name or path
mirpedrol May 5, 2025
a55d26b
add screen to select if config is for an HPC or not
mirpedrol May 5, 2025
b143b30
Merge branch 'dev' of https://github.com/mirpedrol/tools into configb…
mirpedrol Sep 5, 2025
b0ad759
use 'local' for no HPC
mirpedrol Sep 5, 2025
d557970
add HPC configuration screen
mirpedrol Sep 5, 2025
580598b
add screen for final infrastructure config details - containers cache…
mirpedrol Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ We also enabled to install subworkflows with modules from different remotes.
| `-p` / `--parallel-downloads` | `-d` / `--parallel-downloads` |
| new parameter | `-p` / (`--platform`) |

### Configs

- New command: `nf-core configs create wizard` for generating configs for nf-core pipelines ([#3001](https://github.com/nf-core/tools/pull/3001))

### General

- Change default branch to `main` for the nf-core/tools repository
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ include nf_core/assets/logo/nf-core-repo-logo-base-lightbg.png
include nf_core/assets/logo/nf-core-repo-logo-base-darkbg.png
include nf_core/assets/logo/placeholder_logo.svg
include nf_core/assets/logo/MavenPro-Bold.ttf
include nf_core/pipelines/create/create.tcss
include nf_core/textual.tcss
include nf_core/pipelines/create/template_features.yml
65 changes: 60 additions & 5 deletions nf_core/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@
from nf_core.components.constants import NF_CORE_MODULES_REMOTE
from nf_core.pipelines.download import DownloadError
from nf_core.pipelines.list import autocomplete_pipelines
from nf_core.utils import check_if_outdated, nfcore_logo, rich_force_colors, setup_nfcore_dir
from nf_core.utils import (
check_if_outdated,
nfcore_logo,
rich_force_colors,
setup_nfcore_dir,
)

# Set up logging as the root logger
# Submodules should all traverse back to this
Expand All @@ -79,7 +84,7 @@
"nf-core": [
{
"name": "Commands",
"commands": ["pipelines", "modules", "subworkflows", "test-datasets", "interface"],
"commands": ["pipelines", "modules", "subworkflows", "configs", "test-datasets", "interface"],
},
],
"nf-core pipelines": [
Expand All @@ -89,7 +94,15 @@
},
{
"name": "For developers",
"commands": ["create", "lint", "bump-version", "sync", "schema", "rocrate", "create-logo"],
"commands": [
"create",
"lint",
"bump-version",
"sync",
"schema",
"rocrate",
"create-logo",
],
},
],
"nf-core modules": [
Expand All @@ -112,13 +125,18 @@
"commands": ["create", "lint", "test"],
},
],
"nf-core configs": [
{
"name": "Config commands",
"commands": ["create"],
},
],
"nf-core pipelines schema": [{"name": "Schema commands", "commands": ["validate", "build", "lint", "docs"]}],
"nf-core test-datasets": [{"name": "For developers", "commands": ["search", "list", "list-branches"]}],
}
click.rich_click.OPTION_GROUPS = {
"nf-core modules list local": [{"options": ["--dir", "--json", "--help"]}],
}

# Set up rich stderr console
stderr = rich.console.Console(stderr=True, force_terminal=rich_force_colors())
stdout = rich.console.Console(force_terminal=rich_force_colors())
Expand Down Expand Up @@ -262,7 +280,13 @@ def pipelines(ctx):
@click.option("-d", "--description", type=str, help="A short description of your pipeline")
@click.option("-a", "--author", type=str, help="Name of the main author(s)")
@click.option("--version", type=str, default="1.0.0dev", help="The initial version number to use")
@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite output directory if it already exists")
@click.option(
"-f",
"--force",
is_flag=True,
default=False,
help="Overwrite output directory if it already exists",
)
@click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)")
@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template")
@click.option(
Expand Down Expand Up @@ -1877,6 +1901,18 @@ def command_subworkflows_update(
)


# nf-core configs subcommands
@nf_core_cli.group()
@click.pass_context
def configs(ctx):
"""
Commands to manage nf-core configs.
"""
# ensure that ctx.obj exists and is a dict (in case `cli()` is called
# by means other than the `if` block below)
ctx.ensure_object(dict)


# nf-core test-dataset subcommands
@nf_core_cli.group()
@click.pass_context
Expand All @@ -1889,6 +1925,25 @@ def test_datasets(ctx):
ctx.ensure_object(dict)


# nf-core configs create
@configs.command("create")
@click.pass_context
def create_configs(ctx):
"""
Command to interactively create a nextflow or nf-core config
"""
from nf_core.configs.create import ConfigsCreateApp

try:
log.info("Launching interactive nf-core configs creation tool.")
app = ConfigsCreateApp()
app.run()
sys.exit(app.return_code or 0)
except UserWarning as e:
log.error(e)
sys.exit(1)


# nf-core test-dataset search
@test_datasets.command("search", short_help="Search files in the nf-core/test-datasets repository")
@click.pass_context
Expand Down
1 change: 1 addition & 0 deletions nf_core/configs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .create import ConfigsCreateApp
116 changes: 116 additions & 0 deletions nf_core/configs/create/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""A Textual app to create a config."""

import logging

import click
from rich.logging import RichHandler

## Textual objects
from textual.app import App
from textual.widgets import Button

from nf_core.configs.create import utils

## nf-core question page (screen) imports
from nf_core.configs.create.basicdetails import BasicDetails
from nf_core.configs.create.configtype import ChooseConfigType
from nf_core.configs.create.final import FinalScreen
from nf_core.configs.create.finalinfradetails import FinalInfraDetails
from nf_core.configs.create.hpccustomisation import HpcCustomisation
from nf_core.configs.create.hpcquestion import ChooseHpc
from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig
from nf_core.configs.create.welcome import WelcomeScreen

## General utilities
from nf_core.utils import LoggingConsole

## Logging
logger = logging.getLogger(__name__)
rich_log_handler = RichHandler(
console=LoggingConsole(classes="log_console"),
level=logging.INFO,
rich_tracebacks=True,
show_time=False,
show_path=False,
markup=True,
tracebacks_suppress=[click],
)
logger.addHandler(rich_log_handler)


## Main workflow
class ConfigsCreateApp(App[utils.ConfigsCreateConfig]):
"""A Textual app to create nf-core configs."""

CSS_PATH = "../../textual.tcss"
TITLE = "nf-core configs create"
SUB_TITLE = "Create a new nextflow config with an interactive interface"
BINDINGS = [
("d", "toggle_dark", "Toggle dark mode"),
("q", "quit", "Quit"),
]

## New question screens (sections) loaded here
SCREENS = {
"welcome": WelcomeScreen,
"choose_type": ChooseConfigType,
"nfcore_question": ChooseNfcoreConfig,
"basic_details": BasicDetails,
"final": FinalScreen,
"hpc_question": ChooseHpc,
"hpc_customisation": HpcCustomisation,
"final_infra_details": FinalInfraDetails,
}

# Initialise config as empty
TEMPLATE_CONFIG = utils.ConfigsCreateConfig()

# Tracking variables
CONFIG_TYPE = None
NFCORE_CONFIG = True

# Log handler
LOG_HANDLER = rich_log_handler
# Logging state
LOGGING_STATE = None

## Question dialogue order defined here
def on_mount(self) -> None:
self.push_screen("welcome")

def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle all button pressed events."""
if event.button.id == "lets_go":
self.push_screen("choose_type")
elif event.button.id == "type_infrastructure":
self.CONFIG_TYPE = "infrastructure"
utils.CONFIG_ISINFRASTRUCTURE_GLOBAL = True
self.push_screen("nfcore_question")
elif event.button.id == "type_nfcore":
self.NFCORE_CONFIG = True
utils.NFCORE_CONFIG_GLOBAL = True
self.push_screen("basic_details")
elif event.button.id == "type_pipeline":
self.CONFIG_TYPE = "pipeline"
utils.CONFIG_ISINFRASTRUCTURE_GLOBAL = False
self.push_screen("nfcore_question")
elif event.button.id == "type_custom":
self.NFCORE_CONFIG = False
utils.NFCORE_CONFIG_GLOBAL = False
self.push_screen("basic_details")
elif event.button.id == "type_hpc":
self.push_screen("hpc_customisation")
elif event.button.id == "toconfiguration":
self.push_screen("final_infra_details")
elif event.button.id == "finish":
self.push_screen("final")
## General options
if event.button.id == "close_app":
self.exit(return_code=0)
if event.button.id == "back":
self.pop_screen()

## User theme options
def action_toggle_dark(self) -> None:
"""An action to toggle dark mode."""
self.theme: str = "textual-dark" if self.theme == "textual-light" else "textual-light"
130 changes: 130 additions & 0 deletions nf_core/configs/create/basicdetails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Get basic contact information to set in params to help with debugging. By
displaying such info in the pipeline run header on run execution"""

from textwrap import dedent

from textual import on
from textual.app import ComposeResult
from textual.containers import Center, Horizontal
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Input, Markdown

from nf_core.configs.create.utils import (
ConfigsCreateConfig,
TextInput,
) ## TODO Move somewhere common?
from nf_core.utils import add_hide_class, remove_hide_class

config_exists_warn = """
> ⚠️ **The config file you are trying to create already exists.**
>
> If you continue, you will **overwrite** the existing config.
> Please change the config name to create a different config!.
"""


class BasicDetails(Screen):
"""Name, description, author, etc."""

def compose(self) -> ComposeResult:
yield Header()
yield Footer()
yield Markdown(
dedent(
"""
# Basic details
"""
)
)
## TODO Add validation, <config_name>.conf already exists?
yield TextInput(
"general_config_name",
"custom",
"Config Name. Used for naming resulting file.",
"",
classes="column",
)
with Horizontal():
yield TextInput(
"config_profile_contact",
"Boaty McBoatFace",
"Author full name.",
classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column",
)
yield TextInput(
"config_profile_handle",
"@BoatyMcBoatFace",
"Author Git(Hub) handle.",
classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column",
)
yield TextInput(
"config_pipeline_name",
"Pipeline name",
"The pipeline name you want to create the config for.",
classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "",
)
yield TextInput(
"config_pipeline_path",
"Pipeline path",
"The path to the pipeline you want to create the config for.",
classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or self.parent.NFCORE_CONFIG else "",
)

yield TextInput(
"config_profile_description",
"Description",
"A short description of your config.",
)
yield TextInput(
"config_profile_url",
"https://nf-co.re",
"URL of infrastructure website or owning institution (infrastructure configs only).",
classes="hide" if self.parent.CONFIG_TYPE == "pipeline" else "",
)
yield Center(
Button("Back", id="back", variant="default"),
Button("Next", id="next", variant="success"),
classes="cta",
)

## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs
@on(Button.Pressed)
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Save fields to the config."""
config = {}
for text_input in self.query("TextInput"):
this_input = text_input.query_one(Input)
validation_result = this_input.validate(this_input.value)
config[text_input.field_id] = this_input.value
if not validation_result.is_valid:
text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions))
else:
text_input.query_one(".validation_msg").update("")
try:
self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config)
if event.button.id == "next":
if self.parent.CONFIG_TYPE == "infrastructure":
self.parent.push_screen("hpc_question")
elif self.parent.CONFIG_TYPE == "pipeline":
self.parent.push_screen("final")
except ValueError:
pass

def on_screen_resume(self):
"""Show or hide form fields on resume depending on config type."""
if self.parent.CONFIG_TYPE == "pipeline":
add_hide_class(self.parent, "config_profile_contact")
add_hide_class(self.parent, "config_profile_handle")
add_hide_class(self.parent, "config_profile_url")
if self.parent.NFCORE_CONFIG:
remove_hide_class(self.parent, "config_pipeline_name")
add_hide_class(self.parent, "config_pipeline_path")
else:
remove_hide_class(self.parent, "config_pipeline_path")
add_hide_class(self.parent, "config_pipeline_name")
if self.parent.CONFIG_TYPE == "infrastructure":
remove_hide_class(self.parent, "config_profile_contact")
remove_hide_class(self.parent, "config_profile_handle")
remove_hide_class(self.parent, "config_profile_url")
add_hide_class(self.parent, "config_pipeline_name")
add_hide_class(self.parent, "config_pipeline_path")
Loading