"""
novelWriter – Init File
=======================
File History:
Created: 2018-09-22 [0.0.1] main
This file is a part of novelWriter
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
""" # noqa
from __future__ import annotations
import getopt
import logging
import os
import sys
from typing import TYPE_CHECKING
from PyQt6.QtWidgets import QApplication, QErrorMessage
from novelwriter.config import Config
from novelwriter.error import exceptionHandler
from novelwriter.shared import SharedData
from novelwriter.splash import NSplashScreen
if TYPE_CHECKING:
from novelwriter.guimain import GuiMain
# Package Meta
# ============
__package__ = "novelwriter"
__copyright__ = "Copyright 2018-2025 Veronica Berglyd Olsen"
__license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.8a3"
__hexversion__ = "0x020800a3"
__date__ = "2025-10-18"
__status__ = "Stable"
__domain__ = "novelwriter.io"
logger = logging.getLogger(__name__)
##
# Main Program
##
# Globals Singletons
CONFIG = Config()
SHARED = SharedData()
# ANSI Colours
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
WHITE = "\033[97m"
END = "\033[0m"
# Log Format Components
TIME = "[{asctime:}]"
FILE = "{filename:>18}"
LINE = "{lineno:<4d}"
LVLP = "{levelname:8}"
LVLC = "{levelname:17}"
TEXT = "{message:}"
# Read Environment
FORCE_COLOR = bool(os.environ.get("FORCE_COLOR"))
NO_COLOR = bool(os.environ.get("NO_COLOR"))
def main(sysArgs: list | None = None) -> GuiMain | None:
"""Parse command line, set up logging, and launch main GUI."""
if sysArgs is None:
sysArgs = sys.argv[1:]
# Valid Input Options
shortOpt = "hvidc"
longOpt = [
"help",
"version",
"info",
"debug",
"color",
"style=",
"config=",
"data=",
"meminfo"
]
helpMsg = (
f"novelWriter {__version__} ({__date__})\n"
f"{__copyright__}\n"
"\n"
"This program is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU General Public License as published by\n"
"the Free Software Foundation, either version 3 of the License, or\n"
"(at your option) any later version.\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public Licence for more details.\n"
"\n"
"You should have received a copy of the GNU General Public License\n"
"along with this program. If not, see .\n"
"\n"
"Usage:\n"
" -h, --help Print this message.\n"
" -v, --version Print program version and exit.\n"
" -i, --info Print additional runtime information.\n"
" -d, --debug Print debug output. Includes --info.\n"
" -c, --color Add ANSI colors to log output.\n"
" --meminfo Show memory usage information in the status bar.\n"
" --style= Sets Qt style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
)
# Defaults
logLevel = logging.WARN
fmtColor = FORCE_COLOR
fmtLong = False
confPath = None
dataPath = None
qtStyle = "Fusion"
cmdOpen = None
# Parse Options
try:
inOpts, inRemain = getopt.getopt(sysArgs, shortOpt, longOpt)
except getopt.GetoptError as exc:
print(helpMsg)
print(f"ERROR: {exc!s}")
sys.exit(2)
if len(inRemain) > 0:
cmdOpen = inRemain[0]
for inOpt, inArg in inOpts:
if inOpt in ("-h", "--help"):
print(helpMsg)
sys.exit(0)
elif inOpt in ("-v", "--version"):
print(f"novelWriter Version {__version__} [{__date__}]")
sys.exit(0)
elif inOpt in ("-i", "--info"):
logLevel = logging.INFO
elif inOpt in ("-d", "--debug"):
CONFIG.isDebug = True
fmtLong = True
logLevel = logging.DEBUG
elif inOpt in ("-c", "--color"):
fmtColor = not NO_COLOR
elif inOpt == "--meminfo":
CONFIG.memInfo = True
elif inOpt == "--style":
qtStyle = inArg
elif inOpt == "--config":
confPath = inArg
elif inOpt == "--data":
dataPath = inArg
if fmtColor:
# This will overwrite the default level names, and also ensure that
# they can be converted back to integer levels
logging.addLevelName(logging.DEBUG, f"{BLUE}DEBUG{END}")
logging.addLevelName(logging.INFO, f"{GREEN}INFO{END}")
logging.addLevelName(logging.WARNING, f"{YELLOW}WARNING{END}")
logging.addLevelName(logging.ERROR, f"{RED}ERROR{END}")
logging.addLevelName(logging.CRITICAL, f"{RED}CRITICAL{END}")
logTxt = f"{LVLC} {TEXT}" if fmtColor else f"{LVLP} {TEXT}"
logPos = f"{BLUE}{FILE}{END}:{WHITE}{LINE}{END}" if fmtColor else f"{FILE}:{LINE}"
logFmt = f"{TIME} {logPos} {logTxt}" if fmtLong else logTxt
# Setup Logging
pkgLogger = logging.getLogger(__package__)
pkgLogger.setLevel(logLevel)
if len(pkgLogger.handlers) == 0:
# Make sure we only create one logger (mostly an issue with tests)
cHandle = logging.StreamHandler()
cHandle.setFormatter(logging.Formatter(fmt=logFmt, style="{"))
pkgLogger.addHandler(cHandle)
logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__)
# Check Packages and Versions
errorData = []
errorCode = 0
if sys.hexversion < 0x030b00f0:
errorData.append(
f"At least Python 3.11 is required, found {CONFIG.verPyString}"
)
errorCode |= 0x04
if CONFIG.verQtValue < 0x060400:
errorData.append(
f"At least Qt6 version 6.4 is required, found {CONFIG.verQtString}"
)
errorCode |= 0x08
if CONFIG.verPyQtValue < 0x060400:
errorData.append(
f"At least PyQt6 version 6.4 is required, found {CONFIG.verPyQtString}"
)
errorCode |= 0x10
if errorData:
errApp = QApplication([])
errDlg = QErrorMessage()
errDlg.resize(500, 300)
errDlg.showMessage((
"
A critical error was encountered
"
"novelWriter cannot start due to the following issues:
"
"
- {0}
"
"Shutting down ...
"
).format(
"
- ".join(errorData)
))
for errLine in errorData:
logger.critical(errLine)
errApp.exec()
sys.exit(errorCode)
# Finish initialising config
CONFIG.initConfig(confPath, dataPath)
if sys.platform == "darwin":
try:
from Foundation import NSBundle # type: ignore
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info["CFBundleName"] = "novelWriter"
except Exception:
pass # Quietly ignore error
elif sys.platform == "win32":
try:
import ctypes
appID = f"io.novelwriter.{__version__}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appID) # type: ignore
except Exception:
pass # Quietly ignore error
# Import GUI (after dependency checks), and launch
from novelwriter.gui.theme import GuiTheme
from novelwriter.guimain import GuiMain
# Create App
app = _createApp(qtStyle)
# Connect the exception handler before making the main GUI
sys.excepthook = exceptionHandler
splash = NSplashScreen()
splash.show()
splash.showStatus("")
splash.showStatus("Starting novelWriter ...")
# Run Config steps that require the QApplication
CONFIG.loadConfig(splash)
CONFIG.initLocalisation(app)
SHARED.initTheme(GuiTheme())
# Launch main GUI
nwGUI = GuiMain()
nwGUI.showNormal()
splash.finish(nwGUI)
CONFIG.finishStartup()
del splash
nwGUI.postLaunchTasks(cmdOpen)
sys.exit(app.exec())
def _createApp(style: str) -> QApplication:
"""Create the app. This is done in a function to make it easier to
block app creation during testing.
"""
app = QApplication([CONFIG.appName])
app.setStyle(style)
app.setApplicationName(CONFIG.appName)
app.setApplicationVersion(__version__)
app.setOrganizationDomain(__domain__)
app.setOrganizationName(__domain__)
app.setDesktopFileName(CONFIG.appName)
return app