Some checks failed
Syntax / Linting and Type Checking (push) Has been cancelled
Tests / Linux Python 3.11 (push) Has been cancelled
Tests / Linux Python 3.12 (push) Has been cancelled
Tests / Linux Python 3.13 (push) Has been cancelled
Tests / Linux Python 3.14 (push) Has been cancelled
Tests / Linux Python 3.15 (push) Has been cancelled
Tests / MacOS Python 3.14 (push) Has been cancelled
Tests / Windows Python 3.14 (push) Has been cancelled
Tests / PR Coverage Report (push) Has been cancelled
Tests / Action Coverage Report (push) Has been cancelled
Tests / Ubuntu 24.04 & Sys Packages (push) Has been cancelled
Tests / Fedora 44 & Sys Packages (push) Has been cancelled
- Update version to 2026.11.0001 (YYYY/MM.#### development format) - Add NovelWriter attribution: UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen (novelWriter.io), GPL v3.0 - Update CHANGELOG.md with new version format and development entry - Add B.D. Murphy to CREDITS.md and credits_en.htm - Update all copyright headers to use 'novelWriter contributors' format - Add B.D. Murphy copyright to new files (fromdocx.py, SVGs, docs) - Tag release 2026.11 and development 2026.11.0001
303 lines
9.5 KiB
Python
303 lines
9.5 KiB
Python
"""
|
|
UmbraEdit - Init File
|
|
=======================
|
|
|
|
This file is a part of UmbraEdit.
|
|
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
Additional Copyright (C) 2026 B.D. Murphy.
|
|
Changes: Project renamed from novelWriter to UmbraEdit; version updated to YYYY/MM format;
|
|
__hexversion__ updated; __date__ updated.
|
|
""" # noqa
|
|
|
|
from __future__ import annotations
|
|
|
|
import gc
|
|
import getopt
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from PyQt6.QtWidgets import QApplication, QErrorMessage
|
|
|
|
from umbraedit.config import Config
|
|
from umbraedit.error import exceptionHandler
|
|
from umbraedit.shared import SharedData
|
|
from umbraedit.splash import NSplashScreen
|
|
|
|
if TYPE_CHECKING:
|
|
from umbraedit.guimain import GuiMain
|
|
|
|
# Package Meta
|
|
# ============
|
|
|
|
# fmt: off
|
|
__package__ = "umbraedit"
|
|
__copyright__ = "Copyright 2018-2026 Veronica Berglyd Olsen"
|
|
__license__ = "GPLv3"
|
|
__author__ = "Veronica Berglyd Olsen"
|
|
__maintainer__ = "Veronica Berglyd Olsen"
|
|
__email__ = "code@vkbo.net"
|
|
__version__ = "2026.11.0001" # Changed by B.D. Murphy 2026: YYYY/MM.#### development version
|
|
__hexversion__ = "0x01000000" # Changed by B.D. Murphy 2026
|
|
__date__ = "2026-11-05" # Changed by B.D. Murphy 2026
|
|
__status__ = "Stable"
|
|
__domain__ = "umbraedit.io"
|
|
# fmt: on
|
|
|
|
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", "config=", "data=", "meminfo"]
|
|
|
|
helpMsg = (
|
|
f"UmbraEdit {__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 <https://www.gnu.org/licenses/>.\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"
|
|
" --config= Alternative config file.\n"
|
|
" --data= Alternative user data path.\n"
|
|
)
|
|
|
|
# Defaults
|
|
logLevel = logging.WARNING
|
|
fmtColor = FORCE_COLOR
|
|
fmtLong = False
|
|
confPath = None
|
|
dataPath = None
|
|
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"UmbraEdit 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 == "--config":
|
|
confPath = inArg
|
|
elif inOpt == "--data":
|
|
dataPath = inArg
|
|
else: # pragma: no cover
|
|
pass
|
|
|
|
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 UmbraEdit %s (%s) %s", __version__, __hexversion__, __date__)
|
|
|
|
# Disable GC, running in GuiMain instead (#2927)
|
|
gc.disable()
|
|
|
|
# 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(
|
|
(
|
|
"<h3>A critical error was encountered</h3>"
|
|
"<p>UmbraEdit cannot start due to the following issues:<p>"
|
|
"<p> - {0}</p>"
|
|
"<p>Shutting down ...</p>"
|
|
).format("<br> - ".join(errorData))
|
|
)
|
|
for errLine in errorData:
|
|
logger.critical(errLine)
|
|
errApp.exec()
|
|
sys.exit(errorCode)
|
|
|
|
# Finish initialising config
|
|
CONFIG.initConfig(confPath, dataPath)
|
|
|
|
if sys.platform == "linux":
|
|
QApplication.setDesktopFileName(CONFIG.appHandle)
|
|
elif sys.platform == "darwin":
|
|
try:
|
|
from Foundation import NSBundle # type: ignore
|
|
|
|
bundle = NSBundle.mainBundle()
|
|
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
|
info["CFBundleName"] = "UmbraEdit"
|
|
except Exception:
|
|
pass # Quietly ignore error
|
|
elif sys.platform == "win32":
|
|
try:
|
|
import ctypes
|
|
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("io.umbraedit.app") # type: ignore
|
|
except Exception:
|
|
pass # Quietly ignore error
|
|
|
|
# Import GUI (after dependency checks), and launch
|
|
from umbraedit.gui.theme import GuiTheme
|
|
from umbraedit.guimain import GuiMain
|
|
|
|
# Create App
|
|
app = _createApp()
|
|
|
|
# Connect the exception handler before making the main GUI
|
|
sys.excepthook = exceptionHandler
|
|
|
|
splash = NSplashScreen()
|
|
splash.show()
|
|
|
|
splash.showStatus("")
|
|
splash.showStatus("Starting UmbraEdit ...")
|
|
|
|
# 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)
|
|
|
|
code = app.exec()
|
|
logger.info("UmbraEdit exited with code %d", code)
|
|
sys.exit(code)
|
|
|
|
|
|
def _createApp() -> 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("Fusion")
|
|
app.setApplicationName(CONFIG.appName)
|
|
app.setApplicationVersion(__version__)
|
|
app.setOrganizationDomain(__domain__)
|
|
app.setOrganizationName(__domain__)
|
|
|
|
return app
|