Files
UmbraEdit/umbraedit/__init__.py
B.D. Murphy e4ac46eb19
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
feat: establish YYYY/MM versioning and add NovelWriter attribution
- 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
2026-11-05 10:58:28 -08:00

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>&nbsp;-&nbsp;{0}</p>"
"<p>Shutting down ...</p>"
).format("<br>&nbsp;-&nbsp;".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