29Orchestrator::Orchestrator() { register_commands(); }
31void Orchestrator::run_version()
const {
ui::info(PROJECT_VERSION); }
33void Orchestrator::run_help()
const {
40 std::cout << app_.get_formatter()->make_help(
41 &app_, app_.get_name(), CLI::AppFormatMode::Normal);
44void Orchestrator::run_info()
const {
55 std::string(
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
56 "━━━━━━━━━━━━━━━━━━━┓"));
61 std::string(
"┃ ░███ ░███ "
64 std::string(
"┃ ░██ ░███ ░██ ░██ "
67 std::string(
"┃ ░██ ░████ ░██ ░██ "
70 std::string(
"┃ ░██ ░██░██ ░██ ░██ "
73 std::string(
"┃ ░███ ░██ ░██ ░██ ░██ "
76 std::string(
"┃ ░██ ░██ ░██░██ ░██ "
79 std::string(
"┃ ░██ ░██ ░████ ░██ "
82 std::string(
"┃ ░██ ░██ ░███ ░██ "
85 std::string(
"┃ ░███ ░███ "
91 std::string(
"┃ ~ N O T E X ~ "
93 ui::info(std::string(
"┃ ") + PROJECT_DESCRIPTION +
101 ui::info(std::string(
"┃ Copyright (c) 2026 ") +
102 PROJECT_AUTHOR + std::string(
" ┃"));
105 std::string(
" License ┃"));
109 ui::info(std::string(
"┃ ") + PROJECT_HOMEPAGE_URL +
115 std::string(
"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
116 "━━━━━━━━━━━━━━━━━━━┛"));
120 const Environment environment;
121 ui::info(std::string(
" TEXMFHOME: ") +
122 environment.texmf_home().string());
123 ui::info(std::string(
" Global class dir: ") +
124 environment.global_latex_dir().string());
125 ui::info(std::string(
" Local class dir: ") +
126 environment.local_latex_dir().string());
127 ui::info(std::string(
" Installation: ") +
128 std::string(
to_string(environment.installation_type())));
129 }
catch (
const EnvironmentError& e) {
130 PLOG_WARNING << e.what();
136 const Manager manager;
137 const ProjectConfig& config = manager.config();
139 manager.root_dir().string());
141 std::string(
" Project type: ") +
142 (config.project_type.empty() ?
"(unknown)" : config.project_type));
143 ui::info(std::string(
" Main file: ") + config.main_file);
144 ui::info(std::string(
" Installation type: ") +
145 (config.installation_type.empty() ?
"(unknown)"
146 : config.installation_type));
148 (config.theme.empty() ?
"(none)" : config.theme));
149 }
catch (
const ProjectNotFoundError&) {
150 ui::info(
" Not inside a NoTeX project (.notex/ directory not found).");
154void Orchestrator::run_clean()
const {
155 const std::filesystem::path path(clean_path_);
158 PLOG_WARNING <<
"'clean' invoked outside a NoTeX project; cleaning '"
159 << path.string() <<
"' directly.";
161 ui::info(
"Cleaning inside '" + path.string() +
"' directly.");
166 const std::string verb = clean_dry_run_ ?
"Would remove: " :
"Removed: ";
167 for (
const auto& file : report.removed_files) {
170 for (
const auto& directory : report.removed_directories) {
171 ui::step(verb + directory.string());
174 if (report.total_removed() == 0) {
175 PLOG_INFO <<
"Clean found nothing to remove in '" << path.string()
179 const std::string count = std::to_string(report.total_removed());
180 PLOG_INFO << count <<
" item(s) "
181 << (clean_dry_run_ ?
"would be removed from '"
183 << path.string() <<
"'.";
185 (clean_dry_run_ ?
"would be removed." :
"removed."));
189void Orchestrator::run_install_global()
const {
190 const Environment environment;
192 PLOG_INFO <<
"Installed NoTeX globally into '"
193 << environment.global_latex_dir().string() <<
"'.";
195 environment.global_latex_dir().string() +
"'.");
199void Orchestrator::run_install_local()
const {
200 const std::filesystem::path target_dir(install_path_);
202 PLOG_INFO <<
"Installed NoTeX locally into '"
203 << (target_dir /
"settings").
string() <<
"'.";
205 "Installed NoTeX locally into '" +
206 (target_dir /
"settings").
string() +
207 "'. Documents should refer to \\documentclass{settings/notex}.");
211void Orchestrator::run_init()
const {
217 std::string path =
".";
219 if (!init_path_arg_.empty()) {
221 if (!parsed.has_value()) {
222 throw UsageError(
"'" + init_type_arg_ +
223 "' is not a valid project type; expected "
224 "'mono' or 'multi'.");
226 project_type = *parsed;
227 path = init_path_arg_;
228 }
else if (!init_type_arg_.empty()) {
229 if (
const auto parsed =
231 parsed.has_value()) {
232 project_type = *parsed;
234 path = init_type_arg_;
238 const Manager manager =
239 Manager::init(std::filesystem::path(path), project_type, init_force_);
241 <<
"-file NoTeX project in '" << manager.root_dir().string()
245 "-file NoTeX project in '" + manager.root_dir().string() +
249void Orchestrator::run_theme()
const {
251 manager.set_theme(theme_name_);
252 PLOG_INFO <<
"Theme set to '" << theme_name_ <<
"' for project at '"
253 << manager.root_dir().string() <<
"'.";
254 ui::success(
"Theme set to '" + theme_name_ +
"'.");
257void Orchestrator::run_add_section()
const {
259 manager.add_section(section_title_);
260 PLOG_INFO <<
"Added section '" << section_title_ <<
"' to project at '"
261 << manager.root_dir().string() <<
"'.";
262 ui::success(
"Added section '" + section_title_ +
"'.");
265void Orchestrator::run_remove_section()
const {
267 if (manager.remove_section(section_number_)) {
268 PLOG_INFO <<
"Removed section " << section_number_
269 <<
" from project at '" << manager.root_dir().string()
271 ui::success(
"Removed section " + std::to_string(section_number_) +
276void Orchestrator::run_add_bib()
const {
278 manager.add_bibliography();
279 PLOG_INFO <<
"Bibliography ready: '" << manager.config().bibliography_file
281 ui::success(
"Bibliography ready: '" + manager.config().bibliography_file +
285void Orchestrator::run_remove_bib()
const {
287 manager.remove_bibliography();
288 PLOG_INFO <<
"Removed the bibliography from project at '"
289 << manager.root_dir().string() <<
"'.";
293void Orchestrator::run_get()
const {
294 const auto print_names = [] {
300 if (get_name_.empty()) {
306 const std::vector<reference::Snippet> snippets =
308 if (snippets.empty()) {
309 PLOG_WARNING <<
"'" << get_name_ <<
"' is not a known snippet name.";
310 ui::warning(
"'" + get_name_ +
"' is not a known snippet name.");
311 std::vector<std::string_view> suggestions;
313 if (name.find(get_name_) != std::string_view::npos) {
314 suggestions.push_back(name);
317 ui::step(suggestions.empty() ?
"Available snippets" :
"Did you mean");
318 if (suggestions.empty()) {
321 for (
const std::string_view name : suggestions) {
328 for (
const reference::Snippet& snippet : snippets) {
329 if (snippets.size() > 1) {
ui::step(std::string(snippet.name)); }
330 std::cout << snippet.content;
334void Orchestrator::run_checkhealth()
const {
336 const auto check = [&healthy](
bool ok,
const std::string& message) {
338 PLOG_INFO << message;
341 PLOG_ERROR << message;
350 check(
true,
"kpsewhich is available.");
351 }
catch (
const NotexError&) {
352 check(
false,
"kpsewhich is not available on PATH.");
356 const Environment environment;
357 check(
true,
"TEXMFHOME resolved: " + environment.texmf_home().string());
361 std::string(
"Installation detected: ") +
366 environment.local_fonts_dir())) {
367 PLOG_WARNING <<
"The local installation differs from the "
368 "embedded template (customised install?).";
369 ui::warning(
"The local installation differs from the embedded "
370 "template (customised install?).");
373 environment.global_latex_dir(),
374 environment.global_fonts_dir())) {
375 PLOG_WARNING <<
"The global installation differs from the "
376 "embedded template (customised install?).";
377 ui::warning(
"The global installation differs from the embedded "
378 "template (customised install?).");
380 }
catch (
const EnvironmentError& e) { check(
false, e.what()); }
384 const Manager manager;
385 check(
true,
"Project metadata parses.");
387 const std::filesystem::path main_path =
388 manager.root_dir() / manager.config().main_file;
389 check(std::filesystem::exists(main_path),
390 "Main file exists: " + manager.config().main_file);
392 if (!manager.config().theme.empty()) {
394 check(std::find(themes.begin(), themes.end(),
395 manager.config().theme) != themes.end(),
396 "Theme is valid: " + manager.config().theme);
399 const std::vector<std::string> orphans = manager.orphan_sections();
400 if (orphans.empty()) {
401 PLOG_INFO <<
"Every section file is referenced from the main "
403 ui::success(
"Every section file is referenced from the main "
406 for (
const std::string& stem : orphans) {
407 PLOG_WARNING <<
"Section file '" << stem
408 <<
"' is not referenced by any \\subfile line.";
410 "' is not referenced by any \\subfile line.");
413 }
catch (
const ProjectNotFoundError&) {
414 ui::info(
"Not inside a NoTeX project; skipping project checks.");
415 }
catch (
const ConfigError& e) { check(
false, e.what()); }
418 throw NotexError(
"checkhealth found one or more problems.",
421 PLOG_INFO <<
"checkhealth: everything looks healthy.";
425void Orchestrator::run_set()
const {
427 manager.set_config_value(set_key_, set_value_);
428 PLOG_INFO <<
"Set '" << set_key_ <<
"' to '" << set_value_
429 <<
"' for project at '" << manager.root_dir().string() <<
"'.";
430 ui::success(
"Set '" + set_key_ +
"' to '" + set_value_ +
"'.");
433void Orchestrator::run_ls()
const {
434 const Manager manager{std::filesystem::path(ls_path_)};
435 const std::vector<std::filesystem::path> files = manager.project_files();
438 ui::info(
"No project files found.");
442 const std::filesystem::path main_path =
443 manager.root_dir() / manager.config().main_file;
444 for (
const std::filesystem::path& file : files) {
445 if (file == main_path) {
447 }
else if (file.extension() ==
".bib") {
448 ui::info(file.string() +
" (bibliography)");
455void Orchestrator::run_uninstall_global()
const {
456 const Environment environment;
458 PLOG_INFO <<
"Uninstalled the global NoTeX template.";
459 ui::success(
"Uninstalled the global NoTeX template.");
463void Orchestrator::run_uninstall_local()
const {
464 const std::filesystem::path target_dir(uninstall_path_);
466 PLOG_INFO <<
"Uninstalled the local NoTeX template from '"
467 << target_dir.string() <<
"'.";
468 ui::success(
"Uninstalled the local NoTeX template from '" +
469 target_dir.string() +
"'.");
473void Orchestrator::run_reset()
const {
475 const std::string main_file = manager.config().main_file;
476 if (manager.reset(reset_force_)) {
477 PLOG_INFO <<
"Regenerated '" << main_file
478 <<
"' from the template for project at '"
479 << manager.root_dir().string() <<
"'.";
480 ui::success(
"Regenerated '" + main_file +
"' from the template.");
484void Orchestrator::run_delete()
const {
486 const std::filesystem::path root = manager.root_dir();
487 if (manager.delete_scaffolding(delete_all_, delete_force_)) {
488 PLOG_INFO << (delete_all_ ?
"Deleted the project at '"
489 :
"Removed NoTeX's scaffolding from '")
490 << root.string() <<
"'.";
491 ui::success((delete_all_ ?
"Deleted the project at '"
492 :
"Removed NoTeX's scaffolding from '") +
493 root.string() +
"'.");
497void Orchestrator::register_commands() {
509 app_.require_subcommand(0, 1);
511 app_.set_version_flag(
"--version", std::string(PROJECT_VERSION));
517 app_.add_flag(
"-y,--yes", assume_yes_,
518 "Assume 'yes' to every confirmation prompt")
520 app_.add_flag(
"-v,--verbose", verbose_,
"Enable verbose output");
525 app_.add_subcommand(
"version",
"Show version information and exit")
526 ->callback([
this] { run_version(); });
527 app_.add_subcommand(
"help",
"Show this help message and exit")
528 ->callback([
this] { run_help(); });
530 app_.add_subcommand(
"info",
531 "Show information about the project, the "
532 "installation, and the template")
533 ->callback([
this] { run_info(); });
535 auto* clean_command = app_.add_subcommand(
536 "clean",
"Remove build artefacts recursively from a directory");
537 clean_command->add_option(
"path", clean_path_,
538 "Directory to clean (default: current "
540 clean_command->add_flag(
"--dry-run", clean_dry_run_,
541 "Report what would be removed without "
542 "deleting anything");
543 clean_command->callback([
this] { run_clean(); });
545 auto* install_command = app_.add_subcommand(
546 "install",
"Install the NoTeX template globally, or locally when "
548 auto* install_path_option = install_command->add_option(
549 "path", install_path_,
550 "Install locally into this directory instead of globally");
551 install_command->add_flag(
552 "--force", install_force_,
553 "Overwrite a differing existing installation without asking");
554 install_command->callback([
this, install_path_option] {
555 if (install_path_option->count() > 0) {
558 run_install_global();
562 auto* init_command = app_.add_subcommand(
563 "init",
"Initialize a new NoTeX project (defaults to multi-file "
564 "in the current directory)");
565 init_command->add_option(
566 "type", init_type_arg_,
567 "'mono' or 'multi' (default: multi); may be omitted, in which "
568 "case a lone positional argument is treated as the path instead");
569 init_command->add_option(
570 "path", init_path_arg_,
571 "Directory to initialize the project in (default: current "
573 init_command->add_flag(
574 "--force", init_force_,
575 "Overwrite an existing project instead of refusing to");
576 init_command->callback([
this] { run_init(); });
578 auto* theme_command =
579 app_.add_subcommand(
"theme",
"Switch the project's theme");
581 ->add_option(
"name", theme_name_,
582 "Theme to switch to, e.g. 'light', 'dark', 'tokyo', "
585 theme_command->callback([
this] { run_theme(); });
587 app_.add_subcommand(
"checkhealth",
588 "Check the installation and project state")
589 ->callback([
this] { run_checkhealth(); });
591 auto* set_command = app_.add_subcommand(
592 "set",
"Change a project configuration value");
594 ->add_option(
"key", set_key_,
595 "Configuration key to change: 'main_file', 'theme', "
596 "or 'bibliography_file'")
598 set_command->add_option(
"value", set_value_,
"New value")->required();
599 set_command->callback([
this] { run_set(); });
601 auto* ls_command = app_.add_subcommand(
602 "ls",
"List the files that belong to a project");
603 ls_command->add_option(
"path", ls_path_,
604 "Directory to list the project files of "
605 "(default: current directory)");
606 ls_command->callback([
this] { run_ls(); });
608 auto* get_command = app_.add_subcommand(
609 "get",
"Print a ready-to-paste template snippet");
610 get_command->add_option(
"name", get_name_,
611 "Snippet name (omit to list every available "
613 get_command->callback([
this] { run_get(); });
618 for (
const char* name : {
"uninstall",
"prune"}) {
619 auto* command = app_.add_subcommand(
620 name,
"Remove the NoTeX template installation, globally or "
621 "(given a path) locally");
622 auto* path_option = command->add_option(
623 "path", uninstall_path_,
624 "Uninstall from this directory instead of globally");
625 command->add_flag(
"--force", uninstall_force_,
626 "Skip the confirmation prompt");
627 command->callback([
this, path_option] {
628 if (path_option->count() > 0) {
629 run_uninstall_local();
631 run_uninstall_global();
636 auto* reset_command = app_.add_subcommand(
637 "reset",
"Regenerate the project's main file from the template");
638 reset_command->add_flag(
"--force", reset_force_,
639 "Skip the confirmation prompt");
640 reset_command->callback([
this] { run_reset(); });
642 auto* delete_command = app_.add_subcommand(
643 "delete",
"Remove NoTeX's own scaffolding from a project");
644 delete_command->add_flag(
645 "--all", delete_all_,
646 "Also remove every user-authored file (the whole project)");
647 delete_command->add_flag(
"--force", delete_force_,
648 "Skip the confirmation prompt");
649 delete_command->callback([
this] { run_delete(); });
651 auto* add_command = app_.add_subcommand(
652 "add",
"Add a section or a bibliography to the project");
653 auto* add_section_command = add_command->add_subcommand(
654 "section",
"Add a section to the project");
655 add_section_command->add_option(
"title", section_title_,
"Section title")
657 add_section_command->callback([
this] { run_add_section(); });
658 add_command->add_subcommand(
"bib",
"Add a bibliography to the project")
659 ->callback([
this] { run_add_bib(); });
661 auto* remove_command = app_.add_subcommand(
662 "remove",
"Remove a section or the bibliography from the project");
663 auto* remove_section_command = remove_command->add_subcommand(
664 "section",
"Remove a section from the project");
665 remove_section_command
666 ->add_option(
"number", section_number_,
667 "Number of the section to remove, as scanned from "
670 remove_section_command->callback([
this] { run_remove_section(); });
672 ->add_subcommand(
"bib",
"Remove the bibliography from the project")
673 ->callback([
this] { run_remove_bib(); });
678 app_.parse(argc, argv);
679 }
catch (
const CLI::ParseError& e) {
683 const int code = app_.exit(e);
684 PLOG_DEBUG <<
"CLI parsing exited with code " << code <<
": "
688 PLOG_ERROR << e.what();
690 return e.exit_code();
691 }
catch (
const std::exception& e) {
692 PLOG_ERROR << e.what();
697 if (app_.get_subcommands().empty()) {
698 std::cout << app_.help() << std::flush;
static bool install_local(const std::filesystem::path &target_dir, bool force=false)
Installs the template locally into target_dir.
static bool installation_differs(const std::filesystem::path &latex_dir, const std::filesystem::path &fonts_dir)
Reports whether an installation at latex_dir/fonts_dir differs from what would be freshly installed.
static bool uninstall_global(const Environment &environment, bool force=false)
Removes a global installation: every embedded LaTeX and font file's target directory inside environme...
static bool uninstall_local(const std::filesystem::path &target_dir, bool force=false)
Removes a local installation: target_dir's settings/ and fonts/ directories, then clears installation...
static bool install_global(const Environment &environment, bool force=false)
Installs the template globally, using environment to resolve the target directories inside the TeX tr...
static CleanReport clean(const std::filesystem::path &start_dir, bool dry_run=false)
Recursively removes known LaTeX build artefacts from start_dir.
static std::optional< std::filesystem::path > find_project_root(const std::filesystem::path &start_dir)
Searches upward from start_dir for a .notex/ directory, without requiring one to exist.
static std::vector< std::string > available_themes()
static Manager init(const std::filesystem::path &target_dir, templates::ProjectType project_type=templates::ProjectType::MULTI, bool force=false)
Creates a new NoTeX project inside target_dir.
Base class for every exception raised by the NoTeX core library.
ExitCode run(int argc, char **argv)
Parses the command line and dispatches to the requested command, catching every exception at this sin...
Environment: discovers where TeX lives and whether NoTeX is installed.
Installer: deploys the NoTeX LaTeX template, globally into the TeX tree or locally into a project.
Logging utility with plog initialization.
Manager: represents and operates on a single NoTeX project.
std::vector< std::string_view > snippet_names()
std::vector< Snippet > find_snippets(std::string_view name)
Looks up every snippet registered under name, whether as its canonical name or as an alias.
std::string run_command(const std::string &command)
Runs command through the shell and returns everything it wrote to standard output (and standard error...
@ MULTI
main.tex plus a sections/ directory of subfiles.
std::optional< ProjectType > project_type_from_string(std::string_view name)
Parses a project type name.
std::string_view to_string(ProjectType type)
void error(std::string_view message)
Prints an error message, prefixed with a red cross, to stderr.
void info(std::string_view message)
Prints a plain informational message to stdout.
void success(std::string_view message)
Prints a success message, prefixed with a green checkmark, to stdout.
void warning(std::string_view message)
void set_assume_yes(bool assume_yes)
Sets whether confirm() should answer "yes" without prompting.
void step(std::string_view message)
Prints a step or progress message, prefixed with an arrow, to stdout.
ExitCode
Process exit codes returned by the notex executable.
@ FAILURE
An unexpected, unclassified error.
@ USAGE_ERROR
The command line was invalid.
@ SUCCESS
The command completed successfully.
@ LOCAL
A local installation exists (takes precedence over global).
@ GLOBAL
Only a global installation exists.
@ NONE
Neither a local nor a global installation was found.
std::string_view to_string(InstallationType type)
Declares Orchestrator, the application layer that turns command-line input into calls on the notex_co...
Styled terminal output and interactive confirmation prompts.
reference: the table of ready-to-paste snippets served by notex get.
Process execution and environment-variable access.
templates: encoded skeletons used to scaffold new NoTeX projects.