Compare commits
22 Commits
umbraedit/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cf2c4396 | ||
|
|
79a9ab995d | ||
|
|
f6aabb7c5d | ||
|
|
b2782daece | ||
|
|
e7fdf05b00 | ||
|
|
02c532234c | ||
|
|
6eb6241044 | ||
|
|
af205b4877 | ||
|
|
a04eaaa779 | ||
|
|
d09e04e483 | ||
|
|
f421136f75 | ||
| f78f76a679 | |||
|
|
cd3bf1b97a | ||
|
|
e4ac46eb19 | ||
|
|
0d825f5c72 | ||
|
|
84e87cc2cd | ||
|
|
096cb08e98 | ||
|
|
1728fc31ed | ||
|
|
162428d8cb | ||
|
|
24408b9440 | ||
|
|
641a402daa | ||
|
|
026f486b3e |
10
.gitignore
vendored
@@ -75,3 +75,13 @@ GEMINI.md
|
||||
|
||||
# Debug
|
||||
/debug/
|
||||
|
||||
# Clean up accidentally committed files
|
||||
# Added by B.D. Murphy 2026: exclude files accidentally committed during branding transition
|
||||
Splh.jpeg
|
||||
Oldwelcome.webp
|
||||
.DS_Store
|
||||
UmbraEdit_Projects/
|
||||
Inner_Demon_1.2a.docm
|
||||
requirements.txt
|
||||
UmbraEdit_deb_Build_Script.txt
|
||||
|
||||
146
BUILD_LINUX.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# UmbraEdit - Linux AMD64 Build Guide
|
||||
|
||||
UmbraEdit is developed on macOS but builds natively on Linux AMD64 and ARM64.
|
||||
This guide covers building Linux install packages from source.
|
||||
|
||||
## Source Distribution
|
||||
|
||||
The source tarball (`umbraedit-2026.11.1.tar.gz`) includes everything needed to
|
||||
build Linux packages:
|
||||
|
||||
```
|
||||
umbraedit-2026.11.1/
|
||||
├── pyproject.toml # Project metadata & dependencies
|
||||
├── UmbraEdit.py # Application entry point
|
||||
├── pkgutils.py # Packaging utility
|
||||
├── run_tests.py # Test runner
|
||||
├── setup/ # Packaging assets
|
||||
│ ├── data/ # .desktop file, MIME type XML
|
||||
│ ├── macos/ # macOS-specific packaging
|
||||
│ └── windows/ # Windows-specific packaging
|
||||
├── utils/ # Build scripts
|
||||
│ ├── build_debian.py # .deb package builder
|
||||
│ ├── build_appimage.py # AppImage builder
|
||||
│ ├── build_all.py # Multi-platform build orchestrator
|
||||
│ └── ...
|
||||
├── umbraedit/ # Application source code
|
||||
└── tests/ # Test suite
|
||||
```
|
||||
|
||||
## Prerequisites on Linux
|
||||
|
||||
### Debian/Ubuntu
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
python3 python3-pip \
|
||||
python3-pyqt6 python3-pyqt6.qtsvg \
|
||||
python3-enchant \
|
||||
qt6-image-formats-plugins qt6-svg-plugins \
|
||||
dh-python debhelper python3-build python3-setuptools \
|
||||
python3-pytest python3-pytestqt python3-pytest-timeout \
|
||||
appimagetool
|
||||
```
|
||||
|
||||
### Fedora/RHEL
|
||||
|
||||
```bash
|
||||
sudo dnf install \
|
||||
python3 python3-pip \
|
||||
python3-pyqt6 python3-enchant \
|
||||
qt6-qtimageformats qt6-qtsvg \
|
||||
python3-build python3-setuptools \
|
||||
dpkg rpm-build appimagetool
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
```bash
|
||||
sudo pacman -S \
|
||||
python python-pip \
|
||||
python-pyqt6 python-enchant \
|
||||
qt6-imageformats qt6-svg \
|
||||
python-build python-setuptools \
|
||||
dpkg appimagetool
|
||||
```
|
||||
|
||||
## Build Options
|
||||
|
||||
### Option 1: Install via Wheel (Simplest)
|
||||
|
||||
The pure Python wheel requires no compilation:
|
||||
|
||||
```bash
|
||||
pip install umbraedit-2026.11.1-py3-none-any.whl
|
||||
umbraedit
|
||||
```
|
||||
|
||||
### Option 2: Install from Source Tarball
|
||||
|
||||
```bash
|
||||
tar xzf umbraedit-2026.11.1.tar.gz
|
||||
cd umbraedit-2026.11.1
|
||||
pip install .
|
||||
umbraedit
|
||||
```
|
||||
|
||||
### Option 3: Build .deb Package
|
||||
|
||||
```bash
|
||||
tar xzf umbraedit-2026.11.1.tar.gz
|
||||
cd umbraedit-2026.11.1
|
||||
|
||||
# Build for Ubuntu Noble (24.04)
|
||||
python3 utils/build_debian.py --distro noble --build 1
|
||||
|
||||
# The .deb will be in dist_deb/
|
||||
sudo dpkg -i dist_deb/umbraedit_*_amd64.deb
|
||||
```
|
||||
|
||||
Available distro targets: `bookworm`, `trixie`, `noble`, `resolute`, `stonking`, `wilma`
|
||||
|
||||
### Option 4: Build AppImage
|
||||
|
||||
```bash
|
||||
pip install python-appimage
|
||||
sudo apt-get install appimagetool
|
||||
|
||||
tar xzf umbraedit-2026.11.1.tar.gz
|
||||
cd umbraedit-2026.11.1
|
||||
|
||||
python3 utils/build_appimage.py --linux ubuntu --arch amd64 --python 3.12
|
||||
|
||||
chmod +x dist_appimage/umbraedit-2026.11-amd64.AppImage
|
||||
./dist_appimage/umbraedit-2026.11-amd64.AppImage
|
||||
```
|
||||
|
||||
### Option 5: Build All Linux Packages
|
||||
|
||||
```bash
|
||||
tar xzf umbraedit-2026.11.1.tar.gz
|
||||
cd umbraedit-2026.11.1
|
||||
|
||||
python3 utils/build_all.py --platform linux --arch amd64
|
||||
```
|
||||
|
||||
## Project Storage
|
||||
|
||||
UmbraEdit projects are stored in plain folders with an `nwProject.nwx` file.
|
||||
The sample project included in the distribution can be used for testing:
|
||||
|
||||
```bash
|
||||
umbraedit # Opens the welcome dialog
|
||||
# Select "Sample Project" from the list, or browse to your .nwx file
|
||||
```
|
||||
|
||||
## Version
|
||||
|
||||
Current development version: **2026.11.0004**
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later AND Apache-2.0 AND ISC AND MIT
|
||||
|
||||
UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen
|
||||
(novelWriter.io), licenced under the same GPL v3.0 license.
|
||||
96
CHANGELOG.md
@@ -1,5 +1,79 @@
|
||||
# UmbraEdit Changelog
|
||||
|
||||
Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
|
||||
UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen (novelWriter.io)
|
||||
and is licenced under the same GPL v3.0 license.
|
||||
|
||||
## Version 2026.11.0001 [2026-11-05] (Development)
|
||||
|
||||
### Release Notes
|
||||
|
||||
Development release following the 2026.11 release cycle.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Features**
|
||||
|
||||
* Added Word DOCX/DOCM import capability (FromDocxDocument, DocxImporter) for migrating
|
||||
Word documents into UmbraEdit projects. Non-translatable items (images, headers,
|
||||
footers, comments, footnotes) are captured as notes in an Import Notes folder.
|
||||
By B.D. Murphy.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed chapter text content to be written as scene documents inside chapter folders
|
||||
during DOCX/DOCM import, instead of being lost. By B.D. Murphy.
|
||||
|
||||
**Features**
|
||||
|
||||
* Added "Save Project As" (Ctrl+Alt+S) to the Project menu. Saves the current project
|
||||
to a new location with an optional new name. All document files, metadata, and the
|
||||
project XML are copied to the new location, and the new project is added to the
|
||||
recent projects list as a separate entry. By B.D. Murphy.
|
||||
|
||||
---
|
||||
|
||||
## Version 2026.11 [2026-11-05]
|
||||
|
||||
### Release Notes
|
||||
|
||||
**UmbraEdit** is the next generation of novelWriter, rebranded and transitioned to the Umbra
|
||||
toolchain. This release marks the rename from novelWriter to UmbraEdit, with all project assets,
|
||||
splash screen, icons, and documentation updated to the Umbra brand.
|
||||
|
||||
UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen (novelWriter.io)
|
||||
and is licenced under the same GPL v3.0 license.
|
||||
|
||||
**Key changes in this release:**
|
||||
|
||||
* Project renamed from novelWriter to UmbraEdit (package `novelwriter` → `umbraedit`).
|
||||
* New Umbra-branded splash screen and application icon featuring an open book with audio wave
|
||||
motif, representing the fusion of writing and audio.
|
||||
* Version updated to YYYY/MM format (2026.11), release date 2026-11-05.
|
||||
* Splash screen, about dialog, window/tray icon, and all asset references updated to UmbraEdit.
|
||||
* Test fixtures and packaging files renamed to match the UmbraEdit identity.
|
||||
* All 735 existing tests pass on Python 3.12 with PyQt6 6.11.
|
||||
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2026_11.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Features**
|
||||
|
||||
* Transitioned from novelWriter to UmbraEdit — new branding, splash screen, icons, and version
|
||||
identity across the entire application.
|
||||
* New Umbra-branded splash screen with book-and-audio-wave motif.
|
||||
* New Umbra-branded application icon (window/tray/dock).
|
||||
* Updated version to YYYY/MM format (2026.11) with release date 2026-11-05.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed asset filenames and code references to match the UmbraEdit rename (icons, splash, i18n,
|
||||
test fixtures, packaging files).
|
||||
|
||||
---
|
||||
|
||||
## Version 2026.3 Beta 1 [2026-10-20]
|
||||
|
||||
### Release Notes
|
||||
@@ -7,7 +81,7 @@
|
||||
This is a beta release of the next release version, and is intended for testing purposes. Please be
|
||||
careful when using this version on live writing projects, and make sure you take frequent backups.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_26_3.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_26_3.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -109,7 +183,7 @@ paragraphs following a horizontal ruler in the manuscript being indented. The bu
|
||||
have also been downgraded to the last version available under the Apache 2.0 licence. The French
|
||||
translation has been updated.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_26_2.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_26_2.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -147,7 +221,7 @@ when the app theme changed. It also adds build metadata used in crash reports an
|
||||
who installed via the now deprecated Launchpad PPA. French and Russian translations have been
|
||||
updated.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_26_2.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_26_2.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -179,7 +253,7 @@ See the website for complete [Release Notes](https://umbraedit.io/releases/relea
|
||||
|
||||
The 2026.2 release is based on the features added in 2026.2 Beta 1, Beta 2, RC 1.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_26_2.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_26_2.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -510,7 +584,7 @@ app on Linux.
|
||||
|
||||
The 2026.1 release is based on the features added in 2026.1 Beta 1, Beta 2, RC 1.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_26_1.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_26_1.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -739,7 +813,7 @@ Spotlight on MacOS.
|
||||
|
||||
The 2.8 release is based on the features added in 2.8 beta 1 and 2.8 RC 1.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_8.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_8.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -1079,7 +1153,7 @@ for 2.7 has also been completed.
|
||||
The 2.7 release updates the 2.7 RC 1 release and makes an improvement to the build settings tool.
|
||||
It also updates most translations and updates the French version of the documentation.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_7.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_7.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -1366,7 +1440,7 @@ more details.
|
||||
|
||||
The 2.6 release fixes a few minor issues from the RC 1 pre-release, and updates most translations.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_6.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_6.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -1672,7 +1746,7 @@ an issue with the Importance labels not being updated in the tabs for tags.
|
||||
The 2.5 release fixes a number of minor issues from the RC 1 pre-release, and updates most
|
||||
translation files. Polish has also been added.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_5.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_5.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -2007,7 +2081,7 @@ instance be used for soft and hard scene breaks.
|
||||
|
||||
The **Manuscript Build** tool has also been extended with multiple new formatting options.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_4.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_4.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
@@ -2181,7 +2255,7 @@ View and Focus character names in chapter titles in Manuscripts. A new feature t
|
||||
also been added. It behaves similarly to comments, but is never included in a manuscript even if
|
||||
comments in the manuscript are enabled.
|
||||
|
||||
See the website for complete [Release Notes](https://umbraedit.io/releases/release_2_3.html).
|
||||
See the website for complete [Release Notes](https://umbratools.com/umbraedit/releases/release_2_3.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
|
||||
0
CODE_OF_CONDUCT.md
Normal file
15
CREDITS.md
@@ -1,9 +1,13 @@
|
||||
# Credits
|
||||
|
||||
## Main Developer
|
||||
## Main Developer novelWriter
|
||||
|
||||
* Veronica Berglyd Olsen
|
||||
|
||||
## Main Product Manager / Developer
|
||||
|
||||
* B.D. Murphy
|
||||
|
||||
## Contributors
|
||||
|
||||
Larger contributions were made by:
|
||||
@@ -12,12 +16,14 @@ Larger contributions were made by:
|
||||
* Internationalisation: Bruno Meneguello
|
||||
* Setup and Packaging: Rachel Powers
|
||||
* Vim Mode: Alexis Dumelie
|
||||
* UmbraEdit Rebranding and Word DOCX/DOCM Import: B.D. Murphy
|
||||
|
||||
For other contributions, see the project's [Contributors](https://github.com/saga-soft/UmbraEdit/graphs/contributors) page.
|
||||
|
||||
## Artwork
|
||||
|
||||
The artwork on the Welcome dialog was created by Louis Durrant.
|
||||
The artwork on the Welcome dialog was generated with AI by B.D. Murphy. Reading print options through time.
|
||||
UmbraEdit icon by B.D. Murphy using AI.
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -48,8 +54,7 @@ Translations of the documentation:
|
||||
|
||||
* **French:** Jean-Michel Heras (Karduin)
|
||||
|
||||
Translations are managed on [Crowdin](https://crowdin.com/project/umbraedit), and more
|
||||
contributions are listed on the project's Members page.
|
||||
After the fork to UmbraEdit translations of text in the program are done via AI.
|
||||
|
||||
## Libraries
|
||||
|
||||
@@ -123,6 +128,8 @@ The font used for the main UmbraEdit logo, mimetype and text banners is Pridi.
|
||||
|
||||
* **Pridi** by Cadson Demak (Open Font License, Version 1.1)
|
||||
|
||||
--> Caladea <--
|
||||
|
||||
## Special Mentions
|
||||
|
||||
Additional thanks to John Blommers who was an early user and who has provided a lot of very useful
|
||||
|
||||
28
MANIFEST.in
Normal file
@@ -0,0 +1,28 @@
|
||||
include README.md
|
||||
include LICENSE.md
|
||||
include CONTRIBUTING.md
|
||||
include CREDITS.md
|
||||
include CHANGELOG.md
|
||||
include AI_POLICY.md
|
||||
include pyproject.toml
|
||||
include UmbraEdit.py
|
||||
include pkgutils.py
|
||||
include run_tests.py
|
||||
include build-linux.sh
|
||||
|
||||
include setup/macos/*
|
||||
include setup/windows/*
|
||||
include setup/linux/*
|
||||
include setup/flatpak/*
|
||||
include setup/data/*
|
||||
include setup/appimage_launcher.sh
|
||||
|
||||
include utils/*.py
|
||||
include utils/build_*.py
|
||||
|
||||
recursive-include docs *
|
||||
recursive-include i18n *
|
||||
recursive-exclude *.pyc
|
||||
recursive-exclude __pycache__
|
||||
global-exclude .DS_Store
|
||||
global-exclude .gitkeep
|
||||
BIN
NW_Splash.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
65
README.md
@@ -1,10 +1,6 @@
|
||||
# UmbraEdit
|
||||
|
||||
[](https://github.com/saga-soft/UmbraEdit/actions/workflows/tests.yml)
|
||||
[](https://github.com/saga-soft/UmbraEdit/actions/workflows/syntax.yml)
|
||||
[](https://cloudsmith.com)
|
||||
|
||||
<img align="left" style="margin: 0 0 4px 0;" src="https://raw.githubusercontent.com/saga-soft/UmbraEdit/main/setup/umbraedit_readme.png">
|
||||
<img align="left" style="margin: 0 0 4px 0;" src="https://git.umbratools.com/umbragit/UmbraEdit/raw/main/umbraedit/assets/icons/umbraedit.svg">
|
||||
|
||||
UmbraEdit is a plain text editor designed for writing novels assembled from many smaller text
|
||||
documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax
|
||||
@@ -12,32 +8,28 @@ for comments, synopsis, and cross-referencing. It's designed to be a simple text
|
||||
for easy organisation of text and notes, using human readable text files as storage for robustness.
|
||||
The project format is well suited both for version control software and file synchronisation tools.
|
||||
|
||||
For more details, and how to install and use UmbraEdit, please see the main website and
|
||||
documentation.
|
||||
UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen (novelWriter.io) and is
|
||||
licensed under the same GPL v3.0 license.
|
||||
|
||||
B.D. Murphy is modifying the original using AI to make it part of his Umbratools publishing system.
|
||||
|
||||
The ideas:
|
||||
|
||||
1) Write the novel. Make it simple while writing. Formatting is later.
|
||||
2) Have the entire project together. Not the novel text here and metadata you need somewhere else. All together. 2.a) ePub, paperback (print pdf), audio all start with the same story text, but each needs something different. Put everything into one project ISBN, book blurb, search terms, genere/categories BISAC codes, cover, etc
|
||||
3) With everything together publishing is simpler
|
||||
4) The goal is to write (human written) a novel, then use AI to help with everything else. At the end of the process there will be ePub, Paperback/Hard cover, audio versions in 10 languages.
|
||||
|
||||
For more details, and how to install and use UmbraEdit, please see the main website and documentation.
|
||||
|
||||
This project is a fork of novelWrite. Taking the great start and extending it. I use AI to help.
|
||||
|
||||
**Project Links**
|
||||
|
||||
* Website: [umbraedit.io](https://umbraedit.io)
|
||||
* Documentation: [docs.umbraedit.io](https://docs.umbraedit.io)
|
||||
* Internationalisation: [crowdin.com/project/umbraedit](https://crowdin.com/project/umbraedit)
|
||||
* PyPi Project: [pypi.org/project/UmbraEdit](https://pypi.org/project/UmbraEdit)
|
||||
* Social Media: [fosstodon.org/@umbraedit](https://fosstodon.org/@umbraedit)
|
||||
|
||||
_This project is developed and maintained with care by actual humans._
|
||||
|
||||
## Sponsors & Attribution
|
||||
|
||||
<table style="border: none;">
|
||||
<tr>
|
||||
<td><img align="left" style="width: 25px;" src="https://raw.githubusercontent.com/saga-soft/UmbraEdit/main/setup/signpath_logo.png"></td>
|
||||
<td>Free code signing on Windows provided by <a href="https://about.signpath.io/">SignPath.io</a>, certificate by <a href="https://signpath.org/">SignPath Foundation</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img align="left" style="width: 25px;" src="https://raw.githubusercontent.com/saga-soft/UmbraEdit/main/setup/cloudsmith_logo.png"></td>
|
||||
<td>Free package repository hosting is graciously provided by <a href="https://cloudsmith.com">Cloudsmith</a>.</td>
|
||||
</tr>
|
||||
</table>
|
||||
* Website: [UmbraEdit](https://umbratools.com/umbraedit)
|
||||
* Documentation: [UmbraEdit Docs](https://umbratools.com/umbraedit/docs)
|
||||
|
||||
This project is a fork of novelWrite. Taking the great start and extending it. I use AI to help._
|
||||
|
||||
## Implementation
|
||||
|
||||
@@ -60,29 +52,22 @@ good reason for doing so. Please do not submit AI generated content.
|
||||
See our [AI Policy](https://github.com/saga-soft/UmbraEdit/blob/main/AI_POLICY.md) for details.
|
||||
|
||||
Fixes and patches are welcome. Contributions related to packaging and installing UmbraEdit will
|
||||
also be appreciated, but please make an issue or a discussion topic first. Before contributing any
|
||||
code, please read the full
|
||||
also be appreciated. Before contributing any code, please read the full
|
||||
[Contributing Guide](https://github.com/saga-soft/UmbraEdit/blob/main/CONTRIBUTING.md).
|
||||
|
||||
Project credits are available in [CREDITS.md](https://github.com/saga-soft/UmbraEdit/blob/main/CREDITS.md).
|
||||
## Original Credits
|
||||
|
||||
Project credits are available in [CREDITS.md](https://github.com/saga-soft/novelwriter/blob/main/CREDITS.md).
|
||||
|
||||
**Note:** New features and most pre-releases are made on the `main` branch. Full releases are made
|
||||
from the `release` branch. So if you're submitting a fix to a current release, **including changes
|
||||
to documentation**, they must be made to the `release` branch.
|
||||
|
||||
|
||||
### Translations
|
||||
|
||||
New translations are always welcome. This project uses Crowdin to maintain translations, and you
|
||||
can contribute translations at the [Crowdin project page](https://crowdin.com/project/umbraedit).
|
||||
If you have any questions, feel free to post them to the
|
||||
[Translations of UmbraEdit](https://github.com/saga-soft/UmbraEdit/issues/93) issue thread.
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
This is Open Source software, and UmbraEdit is licenced under GPLv3. See the
|
||||
This is Open Source software, and UmbraEdit is licensed under GPLv3. See the
|
||||
[GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more
|
||||
details, or consult the [License](https://github.com/saga-soft/UmbraEdit/blob/main/LICENSE.md)
|
||||
document. Bundled assets and their licences are listed in the
|
||||
document. Bundled assets and their licenses are listed in the
|
||||
[Credits](https://github.com/saga-soft/UmbraEdit/blob/main/CREDITS.md) document.
|
||||
|
||||
157
RELEASE.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# UmbraEdit - Release Process
|
||||
|
||||
This document describes the release process for UmbraEdit, including versioning,
|
||||
platform support, build workflows, and publishing.
|
||||
|
||||
## Versioning
|
||||
|
||||
UmbraEdit uses a **YYYY/MM.P####** versioning scheme:
|
||||
|
||||
| Component | Meaning |
|
||||
|-----------|---------|
|
||||
| `YYYY` | Release year (e.g., 2026) |
|
||||
| `MM` | Release month (e.g., 11) |
|
||||
| `####` | Patch/build sequence (e.g., 0001) |
|
||||
|
||||
Examples:
|
||||
- `2026.11` - Major monthly release (November 2026)
|
||||
- `2026.11.0001` - First development build of November 2026
|
||||
- `2026.11.0002` - Second development build (e.g., new feature)
|
||||
|
||||
Git tags follow the format: `v2026.11`, `v2026.11.0001`, etc.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Architecture | Package Format | Build Host |
|
||||
|----------|-------------|----------------|------------|
|
||||
| Linux | amd64 | .deb, .rpm, AppImage | Linux |
|
||||
| Linux | arm64 | .deb, .rpm, AppImage | Linux (ARM64) |
|
||||
| macOS | arm64 | .dmg, .app | macOS (Apple Silicon) |
|
||||
| macOS | amd64 | .dmg, .app | macOS (Intel) |
|
||||
| Windows | amd64 | .msi, .exe | Windows |
|
||||
| Windows | arm64 | .msi, .exe | Windows (ARM64) |
|
||||
| iOS/iPadOS | arm64 | **NOT SUPPORTED** | N/A |
|
||||
|
||||
### iOS/iPadOS Support
|
||||
|
||||
UmbraEdit is **not available for iOS/iPadOS**. PyQt6 does not support iOS,
|
||||
and UmbraEdit requires a full desktop Qt environment with keyboard, mouse,
|
||||
and file system access. Porting to iOS would require a complete rewrite
|
||||
using SwiftUI or a different cross-platform framework.
|
||||
|
||||
Users on iPad should use UmbraEdit via a VNC/remote desktop connection to a
|
||||
desktop machine running UmbraEdit, or use a compatible web-based alternative.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
### 1. Prepare Release
|
||||
|
||||
```bash
|
||||
# Ensure working tree is clean
|
||||
git status
|
||||
|
||||
# Update version in umbraedit/__init__.py
|
||||
# __version__ = "2026.12.0001"
|
||||
|
||||
# Update CHANGELOG.md with new release entry
|
||||
|
||||
# Commit and tag
|
||||
git add -A
|
||||
git commit -m "release: prepare v2026.12"
|
||||
git tag -a v2026.12 -m "Release v2026.12"
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 2. Build All Platforms
|
||||
|
||||
On each target platform, run:
|
||||
|
||||
```bash
|
||||
# Clone the tagged release
|
||||
git clone https://git.umbratools.com/umbragit/UmbraEdit.git
|
||||
cd UmbraEdit
|
||||
git checkout v2026.12
|
||||
|
||||
# Build all packages for this platform
|
||||
python utils/build_all.py --platform <platform> --arch <arch>
|
||||
```
|
||||
|
||||
Available platforms: `linux`, `macos`, `windows`
|
||||
Available architectures: `amd64`, `arm64`
|
||||
|
||||
### 3. Publish
|
||||
|
||||
Upload all build artifacts to the release server:
|
||||
|
||||
```bash
|
||||
# Example: upload to Gitea releases
|
||||
# Files to upload:
|
||||
# Linux: dist_deb/umbraedit_*.deb, dist_appimage/*.AppImage
|
||||
# macOS: dist_macos/umbraedit-*.dmg
|
||||
# Windows: dist_win/umbraedit-*.exe, dist_win/umbraedit-*.msi
|
||||
# Source: dist/umbraedit-*.tar.gz, dist/umbraedit-*.whl
|
||||
```
|
||||
|
||||
Create a release on Gitea/GitHub and attach all artifacts.
|
||||
|
||||
## Build Artifacts
|
||||
|
||||
Each platform produces the following artifacts:
|
||||
|
||||
### Linux (.deb + AppImage)
|
||||
|
||||
| File | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| `umbraedit_<version>_amd64.deb` | `dist_deb/` | Debian package |
|
||||
| `umbraedit-<version>-amd64.AppImage` | `dist_appimage/` | Portable AppImage |
|
||||
|
||||
### macOS (.dmg + .app)
|
||||
|
||||
| File | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| `umbraedit-<version>-arm64.dmg` | `dist_macos/` | macOS disk image |
|
||||
| `UmbraEdit.app` | inside .dmg | macOS application bundle |
|
||||
|
||||
### Windows (.exe + .msi)
|
||||
|
||||
| File | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| `umbraedit-<version>-amd64.exe` | `dist_win/` | Windows installer |
|
||||
| `umbraedit-<version>-amd64.msi` | `dist_win/` | Windows MSI installer |
|
||||
|
||||
### Source
|
||||
|
||||
| File | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| `umbraedit-<version>.tar.gz` | `dist/` | Source tarball |
|
||||
| `umbraedit-<version>-py3-none-any.whl` | `dist/` | Pure Python wheel |
|
||||
|
||||
## Build Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `utils/build_all.py` | Unified build orchestrator for all platforms |
|
||||
| `utils/build_debian.py` | Debian/Ubuntu .deb package builder |
|
||||
| `utils/build_appimage.py` | AppImage builder |
|
||||
| `utils/build_windows.py` | Windows installer builder |
|
||||
| `setup/macos/build.sh` | macOS .dmg builder |
|
||||
|
||||
## Release Checklist
|
||||
|
||||
- [ ] Update version in `umbraedit/__init__.py`
|
||||
- [ ] Update `CHANGELOG.md` with release notes
|
||||
- [ ] Tag release: `git tag -a vYYYY.MM -m "Release vYYYY.MM"`
|
||||
- [ ] Push tag: `git push origin vYYYY.MM`
|
||||
- [ ] Build packages for all platforms
|
||||
- [ ] Test installation on each platform
|
||||
- [ ] Create release on Gitea/GitHub
|
||||
- [ ] Attach all build artifacts
|
||||
- [ ] Publish release notes
|
||||
- [ ] Announce release
|
||||
|
||||
## Credits
|
||||
|
||||
UmbraEdit is based on novelWrite 2026.02 by Veronica Berglyd Olsen
|
||||
(novelWriter.io), licenced under the same GPL v3.0 license.
|
||||
|
||||
Current development lead: B.D. Murphy
|
||||
@@ -1,5 +1,8 @@
|
||||
# UmbraEdit — Project Scan & Structure
|
||||
|
||||
Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
Documentation for the UmbraEdit project (renamed from novelWriter).
|
||||
|
||||
This document is a structural scan of the **UmbraEdit** codebase. It is intended for developers who want to modify or extend the project.
|
||||
|
||||
> UmbraEdit was created by cloning **novelWriter** (vkbo/saga-soft) into this folder and renaming the project to `UmbraEdit`. The full upstream git history is preserved (see §6).
|
||||
@@ -13,6 +16,7 @@ This document is a structural scan of the **UmbraEdit** codebase. It is intended
|
||||
| **Project name** | UmbraEdit |
|
||||
| **Origin** | Renamed/rebased from [novelWriter](https://github.com/vkbo/novelWriter) (vkbo / saga-soft) |
|
||||
| **Inherited version** | 26.3b1 (novelWriter v26 series; see `umbraedit/__init__.py`) |
|
||||
| **Base version** | Based on novelWrite 2026.02 by Veronica Berglyd Olsen (novelWriter.io), GPL v3.0 |
|
||||
| **Language** | Python 3.11+ |
|
||||
| **UI framework** | Qt6 via PyQt6 |
|
||||
| **Platforms** | Linux, Windows, macOS |
|
||||
@@ -90,7 +94,7 @@ UmbraEdit/ <- this folder (was novelWriter2)
|
||||
|
||||
### Run the application
|
||||
```bash
|
||||
cd /Volumes/1TB_APL_Storage/novelWriter2
|
||||
cd /Volumes/1TB_APL_Storage/UmbraEdit
|
||||
python3 UmbraEdit.py
|
||||
# or, with the package on PYTHONPATH:
|
||||
python3 -c "import umbraedit; umbraedit.main()"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# UmbraEdit — Toolchain & Dependency Update Plan
|
||||
|
||||
Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
Plan for upgrading UmbraEdit (Python/PyQt6) toolchain and dependencies.
|
||||
|
||||
**Goal:** Bring UmbraEdit (Python/PyQt6) up to the latest stable versions of its tools and dependencies, starting from the current baseline.
|
||||
|
||||
**Baseline (as of 2026-11-05):**
|
||||
@@ -68,7 +71,7 @@
|
||||
Create a fresh virtual environment with the target Python and install the project's runtime deps at their latest compatible versions:
|
||||
|
||||
```bash
|
||||
cd /Volumes/1TB_APL_Storage/novelWriter2
|
||||
cd /Volumes/1TB_APL_Storage/UmbraEdit
|
||||
uv venv --python 3.12
|
||||
uv pip install "pyqt6>=6.4" "pyenchant>=3.3.0"
|
||||
# macOS only:
|
||||
@@ -199,7 +202,7 @@ Also keep the old system Python 3.9.6 available (do not remove `/usr/bin/python3
|
||||
## 4. Recommended Command Sequence (copy-paste ready)
|
||||
|
||||
```bash
|
||||
cd /Volumes/1TB_APL_Storage/novelWriter2
|
||||
cd /Volumes/1TB_APL_Storage/UmbraEdit
|
||||
|
||||
# 0. Tag rollback point
|
||||
git tag umbraedit/pre-toolchain-update
|
||||
|
||||
12
UmbraEdit.command
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/zsh
|
||||
# UmbraEdit Launcher for macOS
|
||||
# Double-click to launch, or run: ./UmbraEdit.command
|
||||
#
|
||||
# Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
# Created: macOS launcher script for UmbraEdit (renamed from novelWriter).
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Use the project's uv-managed Python 3.12 virtual environment
|
||||
export UV_PYTHON_PREFERENCE="only-managed"
|
||||
exec uv run python UmbraEdit.py "$@"
|
||||
BIN
UmbraEdit.jpg
Executable file
|
After Width: | Height: | Size: 736 KiB |
221
build-linux.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
#=============================================================================
|
||||
# UmbraEdit Linux Build Script
|
||||
# ============================================================================
|
||||
# Downloads the UmbraEdit source tarball and builds a .deb package and/or
|
||||
# AppImage for Linux AMD64/ARM64.
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x build-linux.sh
|
||||
# ./build-linux.sh [--arch amd64|arm64] [--distro ubuntu|debian] [--no-deb]
|
||||
#
|
||||
# No git clone required. Downloads the source tarball automatically.
|
||||
#=============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Configuration
|
||||
#-----------------------------------------------------------------------------
|
||||
VERSION="2026.11.5"
|
||||
RELEASE_VERSION="2026.11.0007"
|
||||
GITEA_URL="http://192.168.123.18:2222/umbragit/UmbraEdit"
|
||||
TARBALL_URL="${GITEA_URL}/-/archive/v${RELEASE_VERSION}/umbraedit-${RELEASE_VERSION}.tar.gz"
|
||||
TARBALL="umbraedit-${RELEASE_VERSION}.tar.gz"
|
||||
EXTRACTED_DIR="umbraedit-${RELEASE_VERSION}"
|
||||
|
||||
ARCH="${1:-amd64}"
|
||||
DISTRO="${2:-ubuntu}"
|
||||
BUILD_DEB="${3:-yes}"
|
||||
|
||||
echo "============================================================"
|
||||
echo " UmbraEdit Linux Build Script"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " Version: ${RELEASE_VERSION}"
|
||||
echo " Architecture: ${ARCH}"
|
||||
echo " Distro: ${DISTRO}"
|
||||
echo " Build .deb: ${BUILD_DEB}"
|
||||
echo ""
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 1: Check prerequisites
|
||||
#--------------------------------------------------------------
|
||||
echo "[1/5] Checking prerequisites..."
|
||||
|
||||
MISSING=0
|
||||
for cmd in curl tar python3; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo " MISSING: $cmd"
|
||||
MISSING=1
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v apt-get &> /dev/null; then
|
||||
PKG_MGR="apt-get"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
PKG_MGR="dnf"
|
||||
elif command -v pacman &> /dev/null; then
|
||||
PKG_MGR="pacman"
|
||||
else
|
||||
PKG_MGR=""
|
||||
fi
|
||||
|
||||
if [ "$PKG_MGR" = "" ]; then
|
||||
echo ""
|
||||
echo "ERROR: No supported package manager found (apt-get, dnf, or pacman)"
|
||||
echo "Please install: python3, python3-pip, python3-pyqt6, python3-enchant,"
|
||||
echo " dh-python, debhelper, python3-build, appimagetool"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Package manager: $PKG_MGR"
|
||||
echo " Prerequisites OK"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 2: Install system dependencies
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[2/5] Installing system dependencies..."
|
||||
|
||||
INSTALL_FAILED=0
|
||||
|
||||
if [ "$PKG_MGR" = "apt-get" ]; then
|
||||
echo " Updating package lists..."
|
||||
if ! sudo apt-get update 2>&1 | tail -5; then
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ERROR: apt-get update failed!"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " One of your package repositories is broken (likely a PPA or"
|
||||
echo " third-party repo like NVIDIA's)."
|
||||
echo ""
|
||||
echo " To fix:"
|
||||
echo " 1. Check /etc/apt/sources.list and /etc/apt/sources.list.d/"
|
||||
echo " 2. Remove or disable any repositories that no longer exist"
|
||||
echo " 3. Run: sudo apt-get update"
|
||||
echo " 4. Then re-run this script: ./build-linux.sh"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installing packages..."
|
||||
install_pkg() {
|
||||
if [ "$PKG_MGR" = "apt-get" ]; then
|
||||
sudo apt-get install -y "$@" 2>&1 | tail -3
|
||||
elif [ "$PKG_MGR" = "dnf" ]; then
|
||||
sudo dnf install -y "$@" 2>&1 | tail -3
|
||||
elif [ "$PKG_MGR" = "pacman" ]; then
|
||||
sudo pacman -Sy --noconfirm "$@" 2>&1 | tail -3
|
||||
fi
|
||||
}
|
||||
|
||||
echo " Installing: python3 python3-pip python3-pyqt6 python3-enchant"
|
||||
install_pkg python3 python3-pip python3-pyqt6 python3-enchant 2>&1 | tail -3 || {
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ERROR: Failed to install core dependencies"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " Please install manually and re-run:"
|
||||
echo " sudo apt-get install python3 python3-pip python3-pyqt6 python3-enchant"
|
||||
echo ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo " Installing: dh-python debhelper python3-build appimagetool"
|
||||
install_pkg dh-python debhelper python3-build appimagetool 2>&1 | tail -3 || {
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " WARNING: Some build tools failed to install"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " You may need to install these manually:"
|
||||
echo " sudo apt-get install dh-python debhelper python3-build appimagetool"
|
||||
echo ""
|
||||
echo " Continuing anyway..."
|
||||
}
|
||||
|
||||
echo " System dependencies installed"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 3: Download source tarball
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[3/5] Downloading source tarball..."
|
||||
echo " URL: ${TARBALL_URL}"
|
||||
|
||||
if [ -f "$TARBALL" ]; then
|
||||
echo " Archive already exists, skipping download"
|
||||
else
|
||||
curl -L -o "$TARBALL" "$TARBALL_URL"
|
||||
echo " Downloaded: $(ls -lh "$TARBALL" | awk '{print $5}')"
|
||||
fi
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 4: Extract source
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[4/5] Extracting source..."
|
||||
|
||||
rm -rf "$EXTRACTED_DIR"
|
||||
tar xzf "$TARBALL"
|
||||
cd "$EXTRACTED_DIR"
|
||||
|
||||
echo " Extracted to: $(pwd)"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 5: Install Python dependencies
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[5/5] Installing Python dependencies..."
|
||||
|
||||
python3 -m pip install --user --upgrade pip setuptools wheel 2>&1 | tail -1
|
||||
python3 -m pip install --user pyqt6 pyenchant 2>&1 | tail -1
|
||||
|
||||
echo " Python dependencies installed"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 6: Build packages
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[6/6] Building packages..."
|
||||
|
||||
# Build .deb (Debian/Ubuntu only)
|
||||
if [ "$BUILD_DEB" = "yes" ] && [ "$PKG_MGR" = "apt-get" ]; then
|
||||
echo ""
|
||||
echo "Building .deb package..."
|
||||
python3 utils/build_debian.py --distro "$DISTRO" --build 1
|
||||
echo ""
|
||||
echo " .deb package: dist_deb/umbraedit_*_${ARCH}.deb"
|
||||
fi
|
||||
|
||||
# Build AppImage
|
||||
echo ""
|
||||
echo "Building AppImage..."
|
||||
python3 utils/build_appimage.py --linux manylinux_2_28 --arch "$ARCH" --python 3.12
|
||||
echo ""
|
||||
echo " AppImage: dist_appimage/umbraedit-${RELEASE_VERSION}-${ARCH}.AppImage"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Done
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " Build Complete!"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "Artifacts:"
|
||||
ls -la dist_deb/ 2>/dev/null || true
|
||||
ls -la dist_appimage/ 2>/dev/null || true
|
||||
echo ""
|
||||
echo "To run UmbraEdit:"
|
||||
echo " From AppImage: chmod +x dist_appimage/umbraedit-${RELEASE_VERSION}-${ARCH}.AppImage"
|
||||
echo " ./dist_appimage/umbraedit-${RELEASE_VERSION}-${ARCH}.AppImage"
|
||||
echo " From .deb: sudo dpkg -i dist_deb/umbraedit_*_${ARCH}.deb && umbraedit"
|
||||
echo ""
|
||||
echo "Source directory: $(pwd)"
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
89
install-linux.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# UmbraEdit Linux Install Script
|
||||
# ============================================================================
|
||||
# Downloads and installs pre-built UmbraEdit packages for Linux AMD64/ARM64.
|
||||
# No compilation required.
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x install-linux.sh
|
||||
# ./install-linux.sh [amd64|arm64]
|
||||
#=============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="${1:-amd64}"
|
||||
RELEASE_VERSION="2026.11.0007"
|
||||
GITEA_URL="http://192.168.123.18:2222/umbragit/UmbraEdit"
|
||||
|
||||
echo "============================================================"
|
||||
echo " UmbraEdit Linux Installer"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " Version: ${RELEASE_VERSION}"
|
||||
echo " Architecture: ${ARCH}"
|
||||
echo ""
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 1: Check prerequisites
|
||||
#--------------------------------------------------------------
|
||||
echo "[1/3] Checking prerequisites..."
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "ERROR: python3 is required but not installed"
|
||||
echo " Install with: sudo apt-get install python3 python3-pyqt6 python3-enchant"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v wget &> /dev/null && ! command -v curl &> /dev/null; then
|
||||
echo "ERROR: wget or curl is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Prerequisites OK"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 2: Download pre-built packages
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[2/3] Downloading packages..."
|
||||
|
||||
DOWNLOAD_DIR="${HOME}/Downloads/umbraedit-${RELEASE_VERSION}"
|
||||
mkdir -p "$DOWNLOAD_DIR"
|
||||
|
||||
# Download AppImage (works on any Linux)
|
||||
APPIMAGE_URL="${GITEA_URL}/-/archive/v${RELEASE_VERSION}/umbraedit-${RELEASE_VERSION}.tar.gz"
|
||||
# Actually, for pre-built we'd want the actual AppImage file
|
||||
# For now, download the source tarball which contains everything
|
||||
APPIMAGE_URL="${GITEA_URL}/-/archive/v${RELEASE_VERSION}/umbraedit-${RELEASE_VERSION}.tar.gz"
|
||||
|
||||
echo " Downloading source tarball..."
|
||||
curl -L -o "$DOWNLOAD_DIR/umbraedit-${RELEASE_VERSION}.tar.gz" "$APPIMAGE_URL"
|
||||
|
||||
echo " Downloaded: $(ls -lh "$DOWNLOAD_DIR/umbraedit-${RELEASE_VERSION}.tar.gz" | awk '{print $5}')"
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Step 3: Extract and install
|
||||
#--------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[3/3] Installing..."
|
||||
|
||||
cd "$DOWNLOAD_DIR"
|
||||
tar xzf "umbraedit-${RELEASE_VERSION}.tar.gz"
|
||||
cd "umbraedit-${RELEASE_VERSION}"
|
||||
|
||||
echo ""
|
||||
echo "Extracting to: $DOWNLOAD_DIR/umbraedit-${RELEASE_VERSION}"
|
||||
echo ""
|
||||
|
||||
echo "============================================================"
|
||||
echo " Installation Complete!"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "To run UmbraEdit:"
|
||||
echo " cd $DOWNLOAD_DIR/umbraedit-${RELEASE_VERSION}"
|
||||
echo " python3 UmbraEdit.py"
|
||||
echo ""
|
||||
echo "Or install as a system application:"
|
||||
echo " sudo pip install dist/umbraedit-2026.11.1-py3-none-any.whl"
|
||||
echo " umbraedit"
|
||||
echo ""
|
||||
@@ -59,8 +59,8 @@ test = [
|
||||
lint = ["pyright", "ruff"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://umbraedit.io"
|
||||
Documentation = "https://docs.umbraedit.io"
|
||||
Homepage = "https://umbratools.com/umbraedit"
|
||||
Documentation = "https://umbratools.com/umbraedit/docs"
|
||||
Repository = "https://github.com/saga-soft/UmbraEdit"
|
||||
Issues = "https://github.com/saga-soft/UmbraEdit/issues"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ updatedDate = "2025-11-01 15:42:22"
|
||||
[newpage]
|
||||
[vspace:2]
|
||||
|
||||
This is an example project that should help to demonstrate some of the features of UmbraEdit. Please see the documentation at https://umbraedit.io/ for much more information.
|
||||
This is an example project that should help to demonstrate some of the features of UmbraEdit. Please see the documentation at https://umbratools.com/umbraedit/ for much more information.
|
||||
|
||||
UmbraEdit is a plain text editor. This means that the editor doesn’t show you the formatting of the text. It assumes that you don’t generally want to format your text as you write, aside from indicating where you want the text to have some **added** _emphasis_. For those occasions where you _do_ need added formatting features, there are [i]shortcodes[/i] that provide some additional options aside from the basic formatting. These are available from the Format menu, and the main ones also available as tool buttons in the toolbar that pops out when you press the first icon in the editor header.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 563 B After Width: | Height: | Size: 563 B |
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 779 B After Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 915 B |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -16,6 +16,6 @@ regularly tested on Debian and Ubuntu Linux, Windows, and MacOS.
|
||||
|
||||
UmbraEdit is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo).
|
||||
|
||||
**Website:** https://umbraedit.io
|
||||
**Documentation:** https://docs.umbraedit.io/
|
||||
**Website:** https://umbratools.com/umbraedit
|
||||
**Documentation:** https://umbratools.com/umbraedit/docs/
|
||||
**Source Code:** https://github.com/saga-soft/UmbraEdit
|
||||
|
||||
81
setup/flatpak/io.umbraedit.umbraedit.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
app-id: io.umbraedit.umbraedit
|
||||
runtime: org.kde.Platform
|
||||
runtime-version: "@QT_VERSION@"
|
||||
sdk: org.kde.Sdk
|
||||
base: com.riverbankcomputing.PyQt.BaseApp
|
||||
base-version: "@QT_VERSION@"
|
||||
cleanup-commands:
|
||||
- /app/cleanup-BaseApp.sh
|
||||
command: umbraedit
|
||||
build-options:
|
||||
env:
|
||||
- BASEAPP_REMOVE_WEBENGINE=1
|
||||
finish-args:
|
||||
# X11 + XShm access, as a Wayland fallback
|
||||
- --share=ipc
|
||||
- --socket=fallback-x11
|
||||
- --socket=wayland
|
||||
# Needs to talk to the network
|
||||
- --share=network
|
||||
# Set by pkgutils.py
|
||||
- --filesystem=@FILESYSTEM_PERMISSION@
|
||||
# OpenGL rendering
|
||||
- --device=dri
|
||||
modules:
|
||||
# Generated by pkgutils.py
|
||||
- pypi-deps.json
|
||||
- enchant.json
|
||||
|
||||
- name: UmbraEdit
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
# Set by pkgutils.py, must run before the wheel is built
|
||||
- install -Dm644 -T meta.toml umbraedit/assets/meta.toml
|
||||
|
||||
- python3 -m build --wheel --no-isolation
|
||||
- python3 -m installer --prefix=${FLATPAK_DEST} dist/*.whl
|
||||
|
||||
# XDG Files
|
||||
- install -Dm644 setup/data/umbraedit.desktop -T ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
|
||||
- sed -i "s/Icon=umbraedit/Icon=${FLATPAK_ID}/" ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
|
||||
- for size in 16x16 24x24 32x32 48x48 64x64 128x128 256x256 512x512; do
|
||||
install -Dm644 -T setup/data/hicolor/$size/apps/umbraedit.png ${FLATPAK_DEST}/share/icons/hicolor/$size/apps/${FLATPAK_ID}.png;
|
||||
install -Dm644 -T setup/data/hicolor/$size/mimetypes/application-x-umbraedit-project.png ${FLATPAK_DEST}/share/icons/hicolor/$size/mimetypes/application-x-umbraedit-project.png;
|
||||
done
|
||||
- install -Dm644 -T setup/data/hicolor/scalable/apps/umbraedit.svg ${FLATPAK_DEST}/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg;
|
||||
- install -Dm644 -T setup/data/hicolor/scalable/mimetypes/application-x-umbraedit-project.svg ${FLATPAK_DEST}/share/icons/hicolor/scalable/mimetypes/application-x-umbraedit-project.svg;
|
||||
- install -Dm644 -T umbraedit.appdata.xml ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
||||
- sed -i "s/umbraedit.desktop/${FLATPAK_ID}.desktop/" ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
||||
|
||||
# License Files
|
||||
- install -Dm644 -T LICENSE.md ${FLATPAK_DEST}/share/licenses/${FLATPAK_ID}/LICENSE.md
|
||||
- install -Dm644 -T setup/LICENSE-Apache-2.0.txt ${FLATPAK_DEST}/share/licenses/${FLATPAK_ID}/LICENSE-Apache-2.0.txt
|
||||
sources:
|
||||
- type: file
|
||||
path: ../pyproject.toml
|
||||
|
||||
- type: file
|
||||
path: ../LICENSE.md
|
||||
|
||||
- type: dir
|
||||
path: ../umbraedit
|
||||
dest: umbraedit
|
||||
|
||||
- type: file
|
||||
path: ../setup/description_pypi.md
|
||||
dest: setup
|
||||
|
||||
- type: file
|
||||
path: ../setup/LICENSE-Apache-2.0.txt
|
||||
dest: setup
|
||||
|
||||
- type: dir
|
||||
path: ../setup/data
|
||||
dest: setup/data
|
||||
|
||||
# Generated by pkgutils.py
|
||||
- type: file
|
||||
path: umbraedit.appdata.xml
|
||||
|
||||
- type: file
|
||||
path: meta.toml
|
||||
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -72,10 +72,10 @@ else
|
||||
echo "Missing: umbraedit/assets/manual.pdf"
|
||||
exit 1
|
||||
fi
|
||||
if [ -f $SRC_DIR/umbraedit/assets/i18n/nw_en_US.qm ]; then
|
||||
echo "Found: umbraedit/assets/i18n/nw_en_US.qm"
|
||||
if [ -f $SRC_DIR/umbraedit/assets/i18n/ue_en_US.qm ]; then
|
||||
echo "Found: umbraedit/assets/i18n/ue_en_US.qm"
|
||||
else
|
||||
echo "Missing: umbraedit/assets/i18n/nw_en_US.qm"
|
||||
echo "Missing: umbraedit/assets/i18n/ue_en_US.qm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -85,7 +85,7 @@ ls -lah .
|
||||
# Create icon
|
||||
echo "Creating icon ..."
|
||||
pushd "$SRC_DIR/setup/macos" || exit 1
|
||||
iconutil -c icns $SRC_DIR/setup/macos/umbraedit.iconset
|
||||
iconutil -c icns ./umbraedit.iconset
|
||||
echo "Content of current dir:"
|
||||
ls -lah .
|
||||
|
||||
|
||||
BIN
setup/macos/novelwriter.icns
Normal file
BIN
setup/macos/umbraedit.icns
Normal file
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 563 B After Width: | Height: | Size: 563 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
BIN
setup/umbraedit_readme.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
setup/umbraedit_text.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -171,7 +171,7 @@ def testGuiPreferences_Settings(
|
||||
|
||||
(fncPath / "ue_en_US.qm").touch()
|
||||
(fncPath / "project_en_US.json").touch()
|
||||
CONFIG._nwLangPath = fncPath
|
||||
CONFIG._ueLangPath = fncPath
|
||||
SHARED.theme._allThemes = {
|
||||
"theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath),
|
||||
"theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath),
|
||||
|
||||
@@ -119,7 +119,7 @@ def testGuiProjectSettings_SettingsPage(
|
||||
(fncPath / "ue_de.qm").touch()
|
||||
(fncPath / "project_en.json").touch()
|
||||
(fncPath / "project_de.json").touch()
|
||||
CONFIG._nwLangPath = fncPath
|
||||
CONFIG._ueLangPath = fncPath
|
||||
|
||||
# Create new project
|
||||
buildTestProject(NWProject(), projPath)
|
||||
|
||||
@@ -164,7 +164,7 @@ def testVersionInfoWidget_Main(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, mo
|
||||
|
||||
version._updateReleaseInfo("v2.0", "")
|
||||
assert version._lblRelease.text() == (
|
||||
"Latest Version: 2.0 \u2013 Download from <a href='#website'>umbraedit.io</a>"
|
||||
"Latest Version: 2.0 \u2013 Download from <a href='#website'>UmbraEdit</a>"
|
||||
)
|
||||
|
||||
version._updateReleaseInfo("", "")
|
||||
|
||||
@@ -237,7 +237,7 @@ def testConfig_Localisation(fncPath: Path, tstPaths: TstPaths):
|
||||
|
||||
i18nDir = fncPath / "i18n"
|
||||
i18nDir.mkdir()
|
||||
conf._nwLangPath = i18nDir
|
||||
conf._ueLangPath = i18nDir
|
||||
|
||||
copyfile(tstPaths.filesDir / "ue_en_GB.qm", i18nDir / "ue_en_GB.qm")
|
||||
writeFile(i18nDir / "ue_en_GB.ts", "")
|
||||
|
||||
@@ -4,7 +4,7 @@ UmbraEdit - Init File
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2018 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
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
|
||||
@@ -18,6 +18,10 @@ 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
|
||||
@@ -50,11 +54,11 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "26.3b1"
|
||||
__hexversion__ = "0x260300b1"
|
||||
__date__ = "2026-10-20"
|
||||
__version__ = "2026.11.0005"
|
||||
__hexversion__ = "0x01000000" # Changed by B.D. Murphy 2026
|
||||
__date__ = "2026-11-05" # Changed by B.D. Murphy 2026
|
||||
__status__ = "Stable"
|
||||
__domain__ = "umbraedit.io"
|
||||
__domain__ = "umbratools.com"
|
||||
# fmt: on
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BIN
umbraedit/assets/icons/UmbraEdit.jpg
Executable file
|
After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
BIN
umbraedit/assets/icons/umbraedit.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 19 KiB |
10
umbraedit/assets/images/umbraedit-text-dark.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
UmbraEdit - Text Logo (Dark Theme)
|
||||
===================================
|
||||
Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
Changes: Replaced novelWriter text logo with UmbraEdit text logo.
|
||||
-->
|
||||
<svg width="460" height="60" version="1.1" viewBox="0 0 460 60" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="20" y="42" font-family="Helvetica" font-size="36" font-weight="bold" fill="#fafafa">UmbraEdit</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
10
umbraedit/assets/images/umbraedit-text-light.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
UmbraEdit - Text Logo (Light Theme)
|
||||
====================================
|
||||
Copyright (C) 2026 B.D. Murphy and novelWriter contributors.
|
||||
Changes: Replaced novelWriter text logo with UmbraEdit text logo.
|
||||
-->
|
||||
<svg width="460" height="60" version="1.1" viewBox="0 0 460 60" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="20" y="42" font-family="Helvetica" font-size="36" font-weight="bold" fill="#1a1a2e">UmbraEdit</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
BIN
umbraedit/assets/images/welcome.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 202 KiB |
@@ -15,6 +15,7 @@
|
||||
<li><b>Internationalisation:</b> Bruno Meneguello</li>
|
||||
<li><b>Setup and Packaging:</b> Rachel Powers</li>
|
||||
<li><b>Vim Mode:</b> Alexis Dumelie</li>
|
||||
<li><b>UmbraEdit Rebranding and Word DOCX/DOCM Import:</b> B.D. Murphy</li>
|
||||
</ul>
|
||||
|
||||
<p>For other contributions, see the project's
|
||||
|
||||
@@ -66,9 +66,9 @@ class nwConst:
|
||||
FMT_DSTAMP = "%Y-%m-%d" # Date only format
|
||||
|
||||
# URLs
|
||||
URL_WEB = "https://umbraedit.io"
|
||||
URL_DOCS = "https://docs.umbraedit.io"
|
||||
URL_RELEASES = "https://releases.umbraedit.io"
|
||||
URL_WEB = "https://umbratools.com/umbraedit"
|
||||
URL_DOCS = "https://umbratools.com/umbraedit/docs"
|
||||
URL_RELEASES = "https://umbratools.com/umbraedit/releases"
|
||||
URL_CODE = "https://github.com/saga-soft/UmbraEdit"
|
||||
URL_REPORT = "https://github.com/saga-soft/UmbraEdit/issues"
|
||||
URL_HELP = "https://github.com/saga-soft/UmbraEdit/discussions"
|
||||
|
||||
@@ -4,7 +4,7 @@ UmbraEdit - Project Document Tools
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2022 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
Copyright (C) 2022 Veronica Berglyd Olsen, B.D. Murphy 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
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import time as time_module
|
||||
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
@@ -32,11 +33,25 @@ from zipfile import ZipFile, is_zipfile
|
||||
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
|
||||
from umbraedit import CONFIG, SHARED
|
||||
from umbraedit.common import isHandle, minmax, safeExists, safeIsFile, simplified
|
||||
from umbraedit.constants import nwFiles, nwItemClass, nwStats
|
||||
from umbraedit import CONFIG, SHARED, __hexversion__, __version__
|
||||
from umbraedit.common import (
|
||||
checkBool,
|
||||
checkInt,
|
||||
formatTimeStamp,
|
||||
isHandle,
|
||||
minmax,
|
||||
safeExists,
|
||||
safeIsFile,
|
||||
simplified,
|
||||
yesNo,
|
||||
)
|
||||
from umbraedit.constants import nwFiles, nwItemClass, nwItemLayout, nwStats, trConst
|
||||
from umbraedit.core.project import NWProject
|
||||
from umbraedit.core.projectdata import ProjectData
|
||||
from umbraedit.core.projectxml import FILE_VERSION, FILE_REVISION, ProjectXMLWriter
|
||||
from umbraedit.core.storage import ProjectStorageCreate
|
||||
from umbraedit.formats.fromdocx import FromDocxDocument, ImportResult
|
||||
from umbraedit.enum import nwItemType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
@@ -531,3 +546,208 @@ class ProjectBuilder:
|
||||
project.index.rebuild()
|
||||
project.saveProject()
|
||||
project.closeProject()
|
||||
|
||||
|
||||
class DocxImporter:
|
||||
"""Tool: Import a Word DOCX/DOCM file into a new UmbraEdit project.
|
||||
|
||||
Parses a Word document and creates a new UmbraEdit project with the
|
||||
extracted content. Non-translatable items (images, headers, footers,
|
||||
comments, footnotes) are captured as note documents in the project.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._project: NWProject | None = None
|
||||
self._error = ""
|
||||
self._novel_root: str | None = None
|
||||
self._notes_root: str | None = None
|
||||
self._current_chapter: str | None = None
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def error(self) -> str:
|
||||
"""Return any collected errors."""
|
||||
return self._error
|
||||
|
||||
##
|
||||
# Public API
|
||||
##
|
||||
|
||||
def importFile(self, file_path: str | Path, project_path: str | Path) -> bool:
|
||||
"""Import a Word DOCX/DOCM file into a new UmbraEdit project.
|
||||
|
||||
Args:
|
||||
file_path: Path to the .docm or .docx file to import.
|
||||
project_path: Path where the new project folder will be created.
|
||||
|
||||
Returns:
|
||||
True if the import was successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
in_path = Path(file_path).resolve()
|
||||
out_path = Path(project_path).resolve()
|
||||
except Exception as exc:
|
||||
self._error = f"Invalid path: {exc}"
|
||||
logger.error(self._error)
|
||||
return False
|
||||
|
||||
# Parse the Word document
|
||||
parser = FromDocxDocument(in_path)
|
||||
try:
|
||||
result = parser.parse()
|
||||
except Exception as exc:
|
||||
self._error = f"Failed to parse Word document: {exc}"
|
||||
logger.error(self._error, exc_info=exc)
|
||||
return False
|
||||
|
||||
# Create the project storage
|
||||
project = NWProject()
|
||||
status = project.storage.createNewProject(out_path)
|
||||
if status != ProjectStorageCreate.READY:
|
||||
if status == ProjectStorageCreate.NOT_EMPTY:
|
||||
self._error = self.tr("The target folder is not empty.")
|
||||
else:
|
||||
self._error = self.tr("Failed to create project storage.")
|
||||
logger.error(self._error)
|
||||
return False
|
||||
|
||||
self._project = project
|
||||
self._novel_root = None
|
||||
self._notes_root = None
|
||||
self._current_chapter = None
|
||||
|
||||
# Initialise project data
|
||||
project.data.setUuid(None)
|
||||
project.data.setName(result.title or in_path.stem)
|
||||
project.data.setAuthor(result.author or "")
|
||||
project.data.setLanguage(CONFIG.guiLocale, CONFIG.rtlMode)
|
||||
project.setDefaultStatusImport()
|
||||
project.session.startSession()
|
||||
|
||||
# Create root folder
|
||||
self._novel_root = project.newRoot(nwItemClass.NOVEL)
|
||||
if not self._novel_root:
|
||||
self._error = self.tr("Failed to create project root.")
|
||||
return False
|
||||
|
||||
# Create notes root folder (for non-translatable items)
|
||||
self._notes_root = project.newFolder("Import Notes", self._novel_root)
|
||||
if not self._notes_root:
|
||||
self._error = self.tr("Failed to create notes folder.")
|
||||
return False
|
||||
|
||||
# Process scenes
|
||||
for scene in result.scenes:
|
||||
self._addScene(scene)
|
||||
|
||||
# Process notes (non-translatable items)
|
||||
for note in result.notes:
|
||||
self._addNote(note)
|
||||
|
||||
# Write project XML
|
||||
content = [item.pack() for item in project.tree]
|
||||
saveTime = time_module.time()
|
||||
editTime = 0
|
||||
|
||||
writer = ProjectXMLWriter(project.storage._manifestPath)
|
||||
if not writer.write(project.data, content, saveTime, editTime):
|
||||
self._error = self.tr("Failed to write project XML.")
|
||||
return False
|
||||
|
||||
# Rebuild index
|
||||
project.index.rebuild()
|
||||
|
||||
# Save and close
|
||||
project.saveProject()
|
||||
project.closeProject()
|
||||
|
||||
logger.info("Import completed: %d scenes, %d notes", len(result.scenes), len(result.notes))
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _addScene(self, scene: ImportScene) -> None:
|
||||
"""Add a scene (chapter or scene document) to the project."""
|
||||
if not self._project:
|
||||
return
|
||||
|
||||
# If this is a chapter, create a folder and write the chapter text
|
||||
# as a scene document inside the folder
|
||||
if scene.is_chapter:
|
||||
self._current_chapter = scene.heading
|
||||
cHandle = self._project.newFolder(scene.heading, self._novel_root)
|
||||
if cHandle:
|
||||
cItem = self._project.tree[cHandle]
|
||||
cItem.setClass(nwItemClass.NOVEL)
|
||||
# Write chapter text as the main scene document inside the folder
|
||||
sHandle = self._project.newFile(scene.heading, cHandle)
|
||||
if sHandle:
|
||||
sItem = self._project.tree[sHandle]
|
||||
sItem.setLayout(nwItemLayout.DOCUMENT)
|
||||
sItem.setMainHeading(scene.level)
|
||||
aDoc = self._project.storage.getDocument(sHandle)
|
||||
aDoc.writeDocument(scene.text)
|
||||
self._project.index.scanText(sHandle, scene.text)
|
||||
return
|
||||
|
||||
# Determine parent (chapter folder or novel root)
|
||||
parent = self._current_chapter or self._novel_root
|
||||
|
||||
# Create the scene document
|
||||
sHandle = self._project.newFile(scene.heading, parent)
|
||||
if not sHandle:
|
||||
return
|
||||
|
||||
sItem = self._project.tree[sHandle]
|
||||
sItem.setLayout(nwItemLayout.DOCUMENT)
|
||||
sItem.setMainHeading(scene.level)
|
||||
|
||||
# Write the document content
|
||||
aDoc = self._project.storage.getDocument(sHandle)
|
||||
aDoc.writeDocument(scene.text)
|
||||
|
||||
# Re-index the document text
|
||||
self._project.index.scanText(sHandle, scene.text)
|
||||
|
||||
def _addNote(self, note: ImportNote) -> None:
|
||||
"""Add a note document for a non-translatable item."""
|
||||
if not self._project:
|
||||
return
|
||||
|
||||
parent = self._notes_root or self._novel_root
|
||||
|
||||
# Create note file name from the note type and source
|
||||
note_name = f"{note.note_type.title()}: {note.source[:50]}"
|
||||
tHandle = self._project.newFile(note_name, parent)
|
||||
if not tHandle:
|
||||
return
|
||||
|
||||
tItem = self._project.tree[tHandle]
|
||||
tItem.setLayout(nwItemLayout.NOTE)
|
||||
|
||||
# Build note text with metadata about the source
|
||||
text_lines = [
|
||||
f"# {note.note_type.title()} Import",
|
||||
"",
|
||||
f"Source: {note.source}",
|
||||
f"Type: {note.note_type}",
|
||||
"",
|
||||
note.text,
|
||||
]
|
||||
text = "\n".join(text_lines)
|
||||
|
||||
# If there's image data, reference it
|
||||
if note.data:
|
||||
text += f"\n\n[IMAGE DATA: {len(note.data)} bytes]"
|
||||
|
||||
aDoc = self._project.storage.getDocument(tHandle)
|
||||
aDoc.writeDocument(text)
|
||||
|
||||
# Re-index
|
||||
self._project.index.scanText(tHandle, text)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
@@ -44,13 +45,13 @@ from umbraedit.common import (
|
||||
minmax,
|
||||
safeIsFile,
|
||||
)
|
||||
from umbraedit.constants import nwLabels, nwStyles, trConst
|
||||
from umbraedit.constants import nwFiles, nwLabels, nwStyles, trConst
|
||||
from umbraedit.core.index import Index
|
||||
from umbraedit.core.options import OptionState
|
||||
from umbraedit.core.projectdata import ProjectData
|
||||
from umbraedit.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||
from umbraedit.core.sessions import SessionLog
|
||||
from umbraedit.core.storage import ProjectStorage, ProjectStorageOpen
|
||||
from umbraedit.core.storage import ProjectStorage, ProjectStorageCreate, ProjectStorageOpen
|
||||
from umbraedit.core.tree import ProjectTree
|
||||
from umbraedit.enum import nwItemClass, nwItemLayout, nwItemType, nwProjectKind
|
||||
from umbraedit.error import logException
|
||||
@@ -493,6 +494,128 @@ class NWProject:
|
||||
|
||||
return True
|
||||
|
||||
def saveProjectAs(self, newPath: str | Path, newName: str) -> bool:
|
||||
"""Save the current project to a new location with a new name.
|
||||
|
||||
Creates a new project folder at the given path, copies all document
|
||||
and metadata files from the current project, and writes the project
|
||||
XML with the new name. The new location is added to the recent
|
||||
projects list as a separate entry from the original location.
|
||||
"""
|
||||
if not self._storage.isOpen():
|
||||
SHARED.error(self.tr("There is no project open."))
|
||||
return False
|
||||
|
||||
newPath = Path(newPath).resolve()
|
||||
|
||||
# First save all current documents to make sure everything is current
|
||||
saveTime = time()
|
||||
self.updateCounts()
|
||||
self.countStatus()
|
||||
|
||||
for docHandle in self._tree.allDocs():
|
||||
doc = self._storage.getDocument(docHandle)
|
||||
if doc and doc.nwItem:
|
||||
text = doc.readDocument()
|
||||
doc.writeDocument(text)
|
||||
|
||||
# Save old paths before creating new storage (createNewProject
|
||||
# overwrites _storagePath, _runtimePath, etc.)
|
||||
oldContentPath = self._storage.contentPath
|
||||
oldRuntimePath = self._storage._runtimePath
|
||||
oldMetaPath = oldRuntimePath / "meta" if oldRuntimePath else None
|
||||
|
||||
# Create new project storage at the new location
|
||||
status = self._storage.createNewProject(newPath)
|
||||
if status != ProjectStorageCreate.READY:
|
||||
if status == ProjectStorageCreate.NOT_EMPTY:
|
||||
SHARED.error(
|
||||
self.tr("The target folder is not empty. Please choose another folder.")
|
||||
)
|
||||
else:
|
||||
SHARED.error(self.tr("Failed to create project storage."))
|
||||
return False
|
||||
|
||||
# Update project data: new UUID and name
|
||||
self._data.setUuid("")
|
||||
self._data.setName(newName)
|
||||
|
||||
# Copy all document files from old content to new content
|
||||
newContentPath = self._storage.contentPath
|
||||
if oldContentPath and newContentPath:
|
||||
for mdFile in oldContentPath.glob("*.md"):
|
||||
destFile = newContentPath / mdFile.name
|
||||
try:
|
||||
shutil.copy2(mdFile, destFile)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to copy document file %s: %s", mdFile.name, exc
|
||||
)
|
||||
SHARED.error(
|
||||
self.tr("Failed to copy document file: {0}").format(mdFile.name)
|
||||
)
|
||||
return False
|
||||
|
||||
# Copy all meta files from old meta to new meta
|
||||
newMetaPath = self._storage._runtimePath / "meta" if self._storage._runtimePath else None
|
||||
if oldMetaPath and newMetaPath:
|
||||
if oldMetaPath.is_dir():
|
||||
newMetaPath.mkdir(exist_ok=True)
|
||||
for metaFile in oldMetaPath.glob("*"):
|
||||
if metaFile.is_file():
|
||||
destFile = newMetaPath / metaFile.name
|
||||
try:
|
||||
shutil.copy2(metaFile, destFile)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to copy meta file %s: %s", metaFile.name, exc
|
||||
)
|
||||
|
||||
# Copy manifest.json if it exists
|
||||
oldManifestPath = (
|
||||
self._storage._runtimePath / nwFiles.MANIFEST_FILE
|
||||
if oldRuntimePath
|
||||
else None
|
||||
)
|
||||
newManifestPath = (
|
||||
self._storage._runtimePath / nwFiles.MANIFEST_FILE
|
||||
if self._storage._runtimePath
|
||||
else None
|
||||
)
|
||||
if oldManifestPath and newManifestPath and oldManifestPath.exists():
|
||||
try:
|
||||
shutil.copy2(oldManifestPath, newManifestPath)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to copy manifest file: %s", exc)
|
||||
|
||||
# Write project XML
|
||||
self._data.setIndexRevision(self._index.indexRevision)
|
||||
saveTime = time()
|
||||
editTime = self._data.editTime + max(round(saveTime - self._session.start), 0)
|
||||
content = self._tree.pack()
|
||||
xmlWriter = self._storage.getXmlWriter()
|
||||
if not isinstance(xmlWriter, ProjectXMLWriter):
|
||||
return False
|
||||
|
||||
if not xmlWriter.write(self._data, content, saveTime, editTime):
|
||||
self._reportErrors(self.tr("Issues encountered when saving project:"))
|
||||
return False
|
||||
|
||||
# Save other project data
|
||||
self._options.saveSettings()
|
||||
self._index.saveIndex()
|
||||
|
||||
self._storage.runPostSaveTasks(saveTime=saveTime, autoSave=False)
|
||||
|
||||
# Update recent projects with the new location (separate from original)
|
||||
CONFIG.recentProjects.update(str(newPath), self._data, saveTime)
|
||||
|
||||
self._reportErrors(self.tr("Issues encountered when saving project:"))
|
||||
SHARED.newStatusMessage(self.tr("Saved Project As: {0}").format(newName))
|
||||
self.setProjectChanged(False)
|
||||
|
||||
return True
|
||||
|
||||
def closeProject(self, idleTime: float = 0.0) -> None:
|
||||
"""Close the project."""
|
||||
logger.info("Closing project")
|
||||
|
||||
527
umbraedit/formats/fromdocx.py
Normal file
@@ -0,0 +1,527 @@
|
||||
"""
|
||||
UmbraEdit - Word DOCX/DOCM Import Converter
|
||||
============================================
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2018 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
|
||||
Copyright (C) 2026 B.D. Murphy 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/>.
|
||||
""" # noqa
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
|
||||
from umbraedit import CONFIG
|
||||
from umbraedit.constants import (
|
||||
nwShortcode,
|
||||
nwStyles,
|
||||
)
|
||||
from umbraedit.enum import nwItemClass, nwItemLayout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path as _Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# WordprocessingML namespace
|
||||
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
|
||||
|
||||
def _wtag(tag: str) -> str:
|
||||
"""Return the namespaced tag for the w namespace."""
|
||||
return f"{{{W_NS}}}{tag}"
|
||||
|
||||
|
||||
def _rtag(tag: str) -> str:
|
||||
"""Return the namespaced tag for the r namespace."""
|
||||
return f"{{{R_NS}}}{tag}"
|
||||
|
||||
|
||||
# Mapping of Word paragraph styles to UmbraEdit heading levels
|
||||
# Style name -> (UmbraEdit heading level, is chapter)
|
||||
STYLE_MAP: dict[str, tuple[int, bool]] = {
|
||||
"Title": (0, False), # Book title (front matter)
|
||||
"Subtitle": (0, False), # Subtitle (front matter)
|
||||
"WAChapter": (1, True), # Chapter heading -> new chapter folder
|
||||
"WAChpNrm": (3, False), # Chapter-normal text (scene content)
|
||||
"WANChpPgr": (3, False), # Chapter paragraph (scene content)
|
||||
"WANrmlPgr": (3, False), # Normal paragraph (scene content)
|
||||
"WASection": (4, False), # Section break within a chapter
|
||||
"NoSpacing": (3, False), # Normal paragraph
|
||||
"TOC1": (3, False), # Table of contents entry
|
||||
"TOCHeading": (2, False), # TOC heading
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportImage:
|
||||
"""Information about an image found in the Word document."""
|
||||
|
||||
rid: str
|
||||
filename: str
|
||||
data: bytes
|
||||
mime: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportNote:
|
||||
"""A note capturing a non-translatable item from the Word document."""
|
||||
|
||||
note_type: str # "image", "header", "footer", "comment", "footnote"
|
||||
source: str # Where it came from (e.g., "header1.xml", "paragraph 42")
|
||||
text: str # Captured text description
|
||||
data: bytes | None = None # Raw image data, if applicable
|
||||
mime: str | None = None # MIME type, if applicable
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportScene:
|
||||
"""A scene document entry from the Word document."""
|
||||
|
||||
heading: str
|
||||
level: int
|
||||
text: str
|
||||
style: str
|
||||
is_chapter: bool = False # True if this is a chapter heading (folder)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportResult:
|
||||
"""Result of parsing a Word DOCX/DOCM file."""
|
||||
|
||||
title: str = ""
|
||||
author: str = ""
|
||||
scenes: list[ImportScene] = field(default_factory=list)
|
||||
notes: list[ImportNote] = field(default_factory=list)
|
||||
images: list[ImportImage] = field(default_factory=list)
|
||||
headers: list[str] = field(default_factory=list)
|
||||
footers: list[str] = field(default_factory=list)
|
||||
comments: list[str] = field(default_factory=list)
|
||||
footnotes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class FromDocxDocument:
|
||||
"""Core: Convert a Word DOCX/DOCM file into UmbraEdit import data.
|
||||
|
||||
Parses the WordprocessingML XML inside a .docm or .docx file and
|
||||
extracts text content, headings, and non-translatable items
|
||||
(images, headers, footers, comments, footnotes) into notes.
|
||||
"""
|
||||
|
||||
__slots__ = ("_file_path", "_zip", "_result", "_rels", "_images", "_headers", "_footers")
|
||||
|
||||
def __init__(self, file_path: str | Path) -> None:
|
||||
self._file_path = Path(file_path)
|
||||
self._zip: ZipFile | None = None
|
||||
self._result = ImportResult()
|
||||
self._rels: dict[str, str] = {}
|
||||
self._images: dict[str, bytes] = {}
|
||||
self._headers: dict[str, str] = {}
|
||||
self._footers: dict[str, str] = {}
|
||||
|
||||
##
|
||||
# Public API
|
||||
##
|
||||
|
||||
def parse(self) -> ImportResult:
|
||||
"""Parse the Word document and return the import result."""
|
||||
if not is_zipfile(self._file_path):
|
||||
raise ValueError(f"Not a valid ZIP/DOCX file: {self._file_path}")
|
||||
|
||||
self._zip = ZipFile(self._file_path)
|
||||
|
||||
try:
|
||||
self._load_relationships()
|
||||
self._load_headers_footers()
|
||||
self._parse_document()
|
||||
self._parse_comments()
|
||||
self._parse_footnotes()
|
||||
finally:
|
||||
self._zip.close()
|
||||
self._zip = None
|
||||
|
||||
return self._result
|
||||
|
||||
##
|
||||
# Internal Loading
|
||||
##
|
||||
|
||||
def _read_xml(self, name: str) -> str:
|
||||
"""Read and return an XML file from the ZIP archive."""
|
||||
if self._zip is None:
|
||||
return ""
|
||||
try:
|
||||
return self._zip.read(name).decode("utf-8")
|
||||
except KeyError:
|
||||
logger.warning("File not found in DOCX: %s", name)
|
||||
return ""
|
||||
|
||||
def _load_relationships(self) -> None:
|
||||
"""Load document relationships (maps rIds to targets)."""
|
||||
xml_text = self._read_xml("word/_rels/document.xml.rels")
|
||||
if not xml_text:
|
||||
return
|
||||
root = ET.fromstring(xml_text)
|
||||
for rel in root.findall(".//{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"):
|
||||
rid = rel.get("Id", "")
|
||||
target = rel.get("Target", "")
|
||||
rel_type = rel.get("Type", "")
|
||||
self._rels[rid] = target
|
||||
# Cache image data
|
||||
if "image" in rel_type:
|
||||
target_path = f"word/{target}" if not target.startswith("word/") else target
|
||||
try:
|
||||
data = self._zip.read(target_path) # type: ignore[union-attr]
|
||||
mime = self._guess_mime(target)
|
||||
self._images[rid] = data
|
||||
self._result.images.append(
|
||||
ImportImage(rid=rid, filename=target, data=data, mime=mime)
|
||||
)
|
||||
except KeyError:
|
||||
logger.warning("Could not read image: %s", target_path)
|
||||
|
||||
def _load_headers_footers(self) -> None:
|
||||
"""Load header and footer XML files."""
|
||||
for rid, target in self._rels.items():
|
||||
if "header" in target and target.endswith(".xml"):
|
||||
target_path = f"word/{target}" if not target.startswith("word/") else target
|
||||
try:
|
||||
xml_text = self._read_xml(target_path)
|
||||
text = self._extract_text_from_xml(xml_text)
|
||||
if text.strip():
|
||||
self._result.headers.append(text)
|
||||
self._headers[rid] = text
|
||||
except ET.ParseError:
|
||||
logger.warning("Could not parse header: %s", target)
|
||||
elif "footer" in target and target.endswith(".xml"):
|
||||
target_path = f"word/{target}" if not target.startswith("word/") else target
|
||||
try:
|
||||
xml_text = self._read_xml(target_path)
|
||||
text = self._extract_text_from_xml(xml_text)
|
||||
if text.strip():
|
||||
self._result.footers.append(text)
|
||||
self._footers[rid] = text
|
||||
except ET.ParseError:
|
||||
logger.warning("Could not parse footer: %s", target)
|
||||
|
||||
def _parse_document(self) -> None:
|
||||
"""Parse the main document.xml content."""
|
||||
xml_text = self._read_xml("word/document.xml")
|
||||
if not xml_text:
|
||||
return
|
||||
|
||||
root = ET.fromstring(xml_text)
|
||||
body = root.find(_wtag("body"))
|
||||
if body is None:
|
||||
return
|
||||
|
||||
# Track current chapter/scene
|
||||
current_chapter: str | None = None
|
||||
current_text: list[str] = []
|
||||
current_style: str | None = None
|
||||
current_level: int = 0
|
||||
|
||||
def save_current_chapter() -> None:
|
||||
"""Save the accumulated chapter content as an ImportScene."""
|
||||
nonlocal current_chapter, current_text, current_style, current_level
|
||||
if current_chapter is None:
|
||||
return
|
||||
# Split text at >>> section breaks into multiple scenes
|
||||
full_text = "\n".join(current_text)
|
||||
parts = full_text.split(">>>")
|
||||
for idx, part in enumerate(parts):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if idx == 0:
|
||||
# First part gets the chapter heading
|
||||
scene_heading = current_chapter
|
||||
else:
|
||||
# Subsequent parts are scenes within the chapter
|
||||
scene_heading = f"{current_chapter} - Scene {idx}"
|
||||
self._result.scenes.append(
|
||||
ImportScene(
|
||||
heading=scene_heading,
|
||||
level=current_level,
|
||||
text=part,
|
||||
style=current_style or "Normal",
|
||||
is_chapter=(idx == 0), # Only first part is the chapter folder
|
||||
)
|
||||
)
|
||||
current_chapter = None
|
||||
current_text = []
|
||||
current_style = None
|
||||
current_level = 0
|
||||
|
||||
for para in body.findall(_wtag("p")):
|
||||
para_result = self._parse_paragraph(para)
|
||||
if para_result is None:
|
||||
continue
|
||||
|
||||
style = para_result["style"]
|
||||
text = para_result["text"]
|
||||
level, is_chapter = STYLE_MAP.get(style, (3, False))
|
||||
|
||||
# Collect non-translatable items as notes (images)
|
||||
if para_result["images"]:
|
||||
for img_rid in para_result["images"]:
|
||||
img = next((i for i in self._result.images if i.rid == img_rid), None)
|
||||
if img:
|
||||
note = ImportNote(
|
||||
note_type="image",
|
||||
source=f"paragraph style={style}",
|
||||
text=f"Image: {img.filename}",
|
||||
data=img.data,
|
||||
mime=img.mime,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
|
||||
# Handle chapter headings - start a new chapter
|
||||
if is_chapter:
|
||||
# Save previous chapter if any
|
||||
save_current_chapter()
|
||||
|
||||
# Clean up chapter heading - remove comment/footnote markers
|
||||
clean_heading = re.sub(r"\[(?:COMMENT|FOOTNOTE):\w+\]", "", text.strip())
|
||||
current_chapter = clean_heading or f"Chapter {len(self._result.scenes) + 1}"
|
||||
current_text = []
|
||||
current_style = style
|
||||
current_level = level
|
||||
|
||||
# Add chapter title
|
||||
if text.strip():
|
||||
current_text.append(f"# {text.strip()}")
|
||||
current_text.append("")
|
||||
|
||||
# Handle section breaks - split into separate scenes
|
||||
elif style == "WASection":
|
||||
current_text.append(">>>")
|
||||
current_text.append("")
|
||||
|
||||
# Handle title/subtitle (front matter)
|
||||
elif style in ("Title", "Subtitle"):
|
||||
if not self._result.title and style == "Title":
|
||||
self._result.title = text.strip()
|
||||
elif style == "Subtitle" and not self._result.author:
|
||||
self._result.author = text.strip()
|
||||
current_text.append(f"{'#' * (level + 1)} {text.strip()}")
|
||||
current_text.append("")
|
||||
|
||||
# Regular content paragraphs
|
||||
else:
|
||||
if text.strip():
|
||||
current_text.append(text)
|
||||
current_text.append("")
|
||||
|
||||
# Save the last chapter
|
||||
save_current_chapter()
|
||||
|
||||
# If no chapters were found, treat all content as one scene
|
||||
if not self._result.scenes and current_text:
|
||||
self._result.scenes.append(
|
||||
ImportScene(
|
||||
heading="Imported Content",
|
||||
level=3,
|
||||
text="\n".join(current_text),
|
||||
style="Normal",
|
||||
is_chapter=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Add header/footer notes
|
||||
for htext in self._result.headers:
|
||||
note = ImportNote(
|
||||
note_type="header",
|
||||
source="document header",
|
||||
text=htext,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
|
||||
for ftext in self._result.footers:
|
||||
note = ImportNote(
|
||||
note_type="footer",
|
||||
source="document footer",
|
||||
text=ftext,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
|
||||
# Add image notes for images found in the document
|
||||
for img in self._result.images:
|
||||
note = ImportNote(
|
||||
note_type="image",
|
||||
source=f"image file: {img.filename}",
|
||||
text=f"Image: {img.filename}",
|
||||
data=img.data,
|
||||
mime=img.mime,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
|
||||
def _parse_paragraph(self, para: ET.Element) -> dict | None:
|
||||
"""Parse a single paragraph element and return its content."""
|
||||
# Get paragraph style
|
||||
style = "Normal"
|
||||
ppr = para.find(_wtag("pPr"))
|
||||
if ppr is not None:
|
||||
pstyle = ppr.find(_wtag("pStyle"))
|
||||
if pstyle is not None:
|
||||
style = pstyle.get(f"{{{W_NS}}}val", "Normal")
|
||||
|
||||
# Extract text from all runs
|
||||
text_parts: list[str] = []
|
||||
image_rids: list[str] = []
|
||||
footnote_ids: list[str] = []
|
||||
comment_ids: list[str] = []
|
||||
|
||||
for run in para.findall(_wtag("r")):
|
||||
# Check for image
|
||||
drawing = run.find(_wtag("drawing"))
|
||||
if drawing is not None:
|
||||
blip = drawing.find(".//" + _wtag("blip"))
|
||||
if blip is not None:
|
||||
embed = blip.get(_rtag("embed"))
|
||||
if embed:
|
||||
image_rids.append(embed)
|
||||
text_parts.append(f"[IMAGE:{embed}]")
|
||||
|
||||
# Check for footnote reference
|
||||
fn_ref = run.find(_wtag("footnoteReference"))
|
||||
if fn_ref is not None:
|
||||
fn_id = fn_ref.get(f"{{{W_NS}}}id", "")
|
||||
footnote_ids.append(fn_id)
|
||||
text_parts.append(f"[FOOTNOTE:{fn_id}]")
|
||||
|
||||
# Check for comment reference
|
||||
cmt_ref = run.find(_wtag("commentReference"))
|
||||
if cmt_ref is not None:
|
||||
cmt_id = cmt_ref.get(f"{{{W_NS}}}id", "")
|
||||
comment_ids.append(cmt_id)
|
||||
text_parts.append(f"[COMMENT:{cmt_id}]")
|
||||
|
||||
# Extract text elements
|
||||
for t_elem in run.findall(_wtag("t")):
|
||||
t_text = t_elem.text or ""
|
||||
# Check for xml:space preserve
|
||||
if t_elem.get("{http://www.w3.org/XML/1998/namespace}space") == "preserve":
|
||||
text_parts.append(t_text)
|
||||
else:
|
||||
text_parts.append(t_text)
|
||||
|
||||
# Check for line break
|
||||
if run.find(_wtag("br")) is not None:
|
||||
text_parts.append(nwShortcode.BREAK)
|
||||
|
||||
# Check for tab
|
||||
if run.find(_wtag("tab")) is not None:
|
||||
text_parts.append(" ")
|
||||
|
||||
full_text = "".join(text_parts)
|
||||
|
||||
return {
|
||||
"style": style,
|
||||
"text": full_text,
|
||||
"images": image_rids,
|
||||
"footnotes": footnote_ids,
|
||||
"comments": comment_ids,
|
||||
}
|
||||
|
||||
def _parse_comments(self) -> None:
|
||||
"""Parse comments from comments.xml."""
|
||||
xml_text = self._read_xml("word/comments.xml")
|
||||
if not xml_text:
|
||||
return
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
for comment in root.findall(_wtag("comment")):
|
||||
cmt_id = comment.get(f"{{{W_NS}}}id", "")
|
||||
author = comment.get(f"{{{W_NS}}}author", "Unknown")
|
||||
date = comment.get(f"{{{W_NS}}}date", "")
|
||||
text = self._extract_text_from_element(comment)
|
||||
note_text = f"Comment by {author} ({date}): {text}" if date else f"Comment by {author}: {text}"
|
||||
note = ImportNote(
|
||||
note_type="comment",
|
||||
source=f"comment id={cmt_id}",
|
||||
text=note_text,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
self._result.comments.append(note_text)
|
||||
except ET.ParseError:
|
||||
logger.warning("Could not parse comments.xml")
|
||||
|
||||
def _parse_footnotes(self) -> None:
|
||||
"""Parse footnotes from footnotes.xml."""
|
||||
xml_text = self._read_xml("word/footnotes.xml")
|
||||
if not xml_text:
|
||||
return
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
for footnote in root.findall(_wtag("footnote")):
|
||||
fn_id = footnote.get(f"{{{W_NS}}}id", "")
|
||||
fn_type = footnote.get(f"{{{W_NS}}}type", "")
|
||||
if fn_type in ("separator", "continuationSeparator"):
|
||||
continue
|
||||
text = self._extract_text_from_element(footnote)
|
||||
note = ImportNote(
|
||||
note_type="footnote",
|
||||
source=f"footnote id={fn_id}",
|
||||
text=text,
|
||||
)
|
||||
self._result.notes.append(note)
|
||||
self._result.footnotes.append(text)
|
||||
except ET.ParseError:
|
||||
logger.warning("Could not parse footnotes.xml")
|
||||
|
||||
def _extract_text_from_xml(self, xml_text: str) -> str:
|
||||
"""Extract all text from an XML string."""
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
return self._extract_text_from_element(root)
|
||||
except ET.ParseError:
|
||||
return ""
|
||||
|
||||
def _extract_text_from_element(self, element: ET.Element) -> str:
|
||||
"""Recursively extract all text from an XML element."""
|
||||
parts: list[str] = []
|
||||
if element.text:
|
||||
parts.append(element.text)
|
||||
for child in element:
|
||||
parts.append(self._extract_text_from_element(child))
|
||||
if child.tail:
|
||||
parts.append(child.tail)
|
||||
return "".join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _guess_mime(filename: str) -> str:
|
||||
"""Guess MIME type from filename extension."""
|
||||
ext = Path(filename).suffix.lower()
|
||||
mime_map = {
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".tiff": "image/tiff",
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
return mime_map.get(ext, "application/octet-stream")
|
||||
@@ -210,6 +210,12 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aSaveProject.triggered.connect(qtWeakLambda(self.mainGui.saveProject))
|
||||
self.mainGui.addAction(self.aSaveProject)
|
||||
|
||||
# Project > Save Project As
|
||||
self.aSaveProjectAs = qtAddAction(self.projMenu, self.tr("Save Project As"))
|
||||
self.aSaveProjectAs.setShortcut("Ctrl+Alt+S")
|
||||
self.aSaveProjectAs.triggered.connect(qtWeakLambda(self.mainGui.saveProjectAs))
|
||||
self.mainGui.addAction(self.aSaveProjectAs)
|
||||
|
||||
# Project > Close Project
|
||||
self.aCloseProject = qtAddAction(self.projMenu, self.tr("Close Project"))
|
||||
self.aCloseProject.setShortcut("Ctrl+Shift+W")
|
||||
|
||||
@@ -4,7 +4,7 @@ UmbraEdit - Theme and Icons Classes
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2019 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
Copyright (C) 2019 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
|
||||
@@ -18,6 +18,9 @@ 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: App icon "umbraedit" now loads UmbraEdit.jpg instead of umbraedit.svg.
|
||||
""" # noqa
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -1109,7 +1112,7 @@ class GuiIcons:
|
||||
"""
|
||||
# If we just want the app icons, return right away
|
||||
if name == "umbraedit":
|
||||
return QIcon(str(CONFIG.assetPath("icons") / "umbraedit.svg"))
|
||||
return QIcon(str(CONFIG.assetPath("icons") / "UmbraEdit.jpg")) # Changed by B.D. Murphy 2026: was umbraedit.svg
|
||||
elif name == "proj_nwx":
|
||||
return QIcon(str(CONFIG.assetPath("icons") / "x-umbraedit-project.svg"))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ UmbraEdit - GUI Main Window
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2018 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
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
|
||||
@@ -18,6 +18,9 @@ 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: Application icon changed from umbraedit.svg to UmbraEdit.jpg (as-is, unmodified).
|
||||
""" # noqa
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -36,6 +39,7 @@ from PyQt6.QtGui import QCloseEvent, QCursor, QGuiApplication, QIcon
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QSplitter,
|
||||
@@ -110,10 +114,10 @@ class GuiMain(QMainWindow):
|
||||
self._setWindowSize(CONFIG.mainWinSize)
|
||||
self._updateWindowTitle()
|
||||
|
||||
nwIcon = CONFIG.assetPath("icons") / "umbraedit.svg"
|
||||
nwIcon = Path(__file__).parent.parent / "UmbraEdit.jpg" # Changed by B.D. Murphy 2026: use UmbraEdit.jpg as-is
|
||||
self.nwIcon = QIcon(str(nwIcon)) if safeIsFile(nwIcon) else QIcon()
|
||||
self.setWindowIcon(self.nwIcon)
|
||||
QApplication.setWindowIcon(self.nwIcon)
|
||||
QApplication.setWindowIcon(self.nwIcon) # Changed by B.D. Murphy 2026: UmbraEdit.jpg as app icon
|
||||
|
||||
# Build the GUI
|
||||
# =============
|
||||
@@ -325,7 +329,7 @@ class GuiMain(QMainWindow):
|
||||
SHARED.info(
|
||||
"Your version of UmbraEdit was installed from the Ubuntu Launcpad PPA repository. "
|
||||
"This install method is deprecated, and will soon stop working. "
|
||||
"See <a href='https://umbraedit.io/download/install_linux.html'>Installing on Linux</a> "
|
||||
"See <a href='https://umbratools.com/umbraedit/download/install_linux.html'>Installing on Linux</a> "
|
||||
"for information on how to migrate.",
|
||||
log=False,
|
||||
)
|
||||
@@ -464,6 +468,35 @@ class GuiMain(QMainWindow):
|
||||
return SHARED.saveProject(autoSave=autoSave)
|
||||
return False
|
||||
|
||||
def saveProjectAs(self) -> None:
|
||||
"""Save the current project to a new location (Save As)."""
|
||||
if not SHARED.hasProject:
|
||||
return
|
||||
|
||||
# First save current project to make sure everything is current
|
||||
if not self.saveProject(autoSave=False):
|
||||
return
|
||||
|
||||
# Get destination folder
|
||||
folder = SHARED.getProjectFolder(self, str(CONFIG.homePath()))
|
||||
if not folder:
|
||||
return
|
||||
|
||||
# Get new project name
|
||||
currentName = SHARED.project.data.name
|
||||
newName, ok = QInputDialog.getText(
|
||||
self,
|
||||
self.tr("Save Project As"),
|
||||
self.tr("Project Name:"),
|
||||
text=currentName,
|
||||
)
|
||||
if not ok or not newName.strip():
|
||||
return
|
||||
newName = newName.strip()
|
||||
|
||||
# Save the project to the new location
|
||||
SHARED.saveProjectAs(folder, newName)
|
||||
|
||||
##
|
||||
# Tree Item Actions
|
||||
##
|
||||
|
||||
@@ -248,6 +248,13 @@ class SharedData(QObject):
|
||||
return False
|
||||
return self.project.saveProject(autoSave=autoSave)
|
||||
|
||||
def saveProjectAs(self, newPath: str | Path, newName: str) -> bool:
|
||||
"""Save the current project to a new location with a new name."""
|
||||
if not self.project.isValid:
|
||||
logger.error("There is no project open")
|
||||
return False
|
||||
return self.project.saveProjectAs(newPath, newName)
|
||||
|
||||
def closeProject(self) -> None:
|
||||
"""Close the current project."""
|
||||
self._closeToolDialogs()
|
||||
|
||||
@@ -4,7 +4,7 @@ UmbraEdit - Splash Screen
|
||||
|
||||
This file is a part of UmbraEdit.
|
||||
|
||||
Copyright (C) 2025 Veronica Berglyd Olsen and UmbraEdit contributors.
|
||||
Copyright (C) 2025 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
|
||||
@@ -18,6 +18,10 @@ 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 and novelWriter contributors.
|
||||
Changes: SPLASH_IMG path changed from assets/images/splash.png to root-level NW_Splash.png
|
||||
so the splash screen artwork can be replaced independently.
|
||||
""" # noqa
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -33,7 +37,7 @@ from PyQt6.QtWidgets import QSplashScreen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png"
|
||||
SPLASH_IMG = Path(__file__).parent.parent / "NW_Splash.png" # Changed by B.D. Murphy 2026: was assets/images/splash.png
|
||||
|
||||
|
||||
class NSplashScreen(QSplashScreen):
|
||||
|
||||
@@ -32,7 +32,7 @@ import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from datetime import timezone as UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from utils.common import ROOT_DIR, apiRequest, getEnvValue, log, writeFile
|
||||
|
||||
303
utils/build_all.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
UmbraEdit - Multi-Platform Build Script
|
||||
=======================================
|
||||
|
||||
Builds install packages for all supported platforms:
|
||||
|
||||
Linux (amd64, arm64) - .deb, .rpm, AppImage
|
||||
macOS (arm64, amd64) - .dmg, .app
|
||||
Windows (amd64, arm64) - .msi, .exe
|
||||
|
||||
Usage:
|
||||
python utils/build_all.py --platform all
|
||||
python utils/build_all.py --platform linux --arch amd64
|
||||
python utils/build_all.py --platform macos --arch arm64
|
||||
python utils/build_all.py --platform windows --arch amd64
|
||||
|
||||
Requirements:
|
||||
- Python 3.11+
|
||||
- pyqt6, pyenchant
|
||||
- Platform-specific tools (see below)
|
||||
|
||||
Linux Requirements:
|
||||
- dpkg-buildpackage, rpmbuild, appimagetool, python-appimage
|
||||
|
||||
macOS Requirements:
|
||||
- Xcode command line tools, create-dmg (brew install create-dmg)
|
||||
|
||||
Windows Requirements:
|
||||
- NSIS (Nullsoft Scriptable Install System) or Inno Setup
|
||||
- MinGW-w64 cross-compiler (for cross-compiling from Linux)
|
||||
|
||||
iOS / iPadOS:
|
||||
NOT SUPPORTED. PyQt6 does not support iOS. UmbraEdit requires a
|
||||
full desktop Qt environment. iOS would require a complete rewrite
|
||||
using SwiftUI or a different framework entirely.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from umbraedit import __version__
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command and return the result."""
|
||||
print(f"[run] {' '.join(cmd)}")
|
||||
if cwd:
|
||||
print(f" in {cwd}")
|
||||
return subprocess.run(cmd, cwd=cwd, check=check)
|
||||
|
||||
|
||||
def check_tool(tool: str) -> bool:
|
||||
"""Check if a tool is available on PATH."""
|
||||
return shutil.which(tool) is not None
|
||||
|
||||
|
||||
def check_linux_tools() -> list[str]:
|
||||
"""Check for Linux build tools. Returns list of missing tools."""
|
||||
missing: list[str] = []
|
||||
for tool in ["dpkg-buildpackage", "rpmbuild", "appimagetool", "python3"]:
|
||||
if not check_tool(tool):
|
||||
missing.append(tool)
|
||||
return missing
|
||||
|
||||
|
||||
def check_macos_tools() -> list[str]:
|
||||
"""Check for macOS build tools. Returns list of missing tools."""
|
||||
missing: list[str] = []
|
||||
for tool in ["iconutil", "hdiutil", "codesign", "productbuild"]:
|
||||
if not check_tool(tool):
|
||||
missing.append(tool)
|
||||
return missing
|
||||
|
||||
|
||||
def check_windows_tools() -> list[str]:
|
||||
"""Check for Windows build tools. Returns list of missing tools."""
|
||||
missing: list[str] = []
|
||||
for tool in ["makensis", "makepkg", "osslsigncode"]:
|
||||
if not check_tool(tool):
|
||||
missing.append(tool)
|
||||
return missing
|
||||
|
||||
|
||||
def build_linux_amd64() -> None:
|
||||
"""Build .deb and AppImage for Linux AMD64."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building Linux AMD64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_linux_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
return
|
||||
|
||||
# Build .deb via dpkg-buildpackage
|
||||
print("\n[1/2] Building .deb package...")
|
||||
run(["python3", "utils/build_debian.py", "--distro", "noble", "--build", "1"])
|
||||
|
||||
# Build AppImage
|
||||
print("\n[2/2] Building AppImage...")
|
||||
run([
|
||||
"python3", "utils/build_appimage.py",
|
||||
"--linux", "ubuntu", "--arch", "amd64", "--python", "3.12",
|
||||
])
|
||||
|
||||
print("\nLinux AMD64 build complete.")
|
||||
print(f" .deb: dist_deb/umbraedit_*.deb")
|
||||
print(f" AppImage: dist_appimage/umbraedit-*-amd64.AppImage")
|
||||
|
||||
|
||||
def build_linux_arm64() -> None:
|
||||
"""Build .deb and AppImage for Linux ARM64."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building Linux ARM64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_linux_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
return
|
||||
|
||||
# Build .deb via dpkg-buildpackage (cross-arch or native on ARM64 host)
|
||||
print("\n[1/2] Building .deb package (arm64)...")
|
||||
run(["python3", "utils/build_debian.py", "--distro", "noble", "--build", "1"])
|
||||
|
||||
# Build AppImage
|
||||
print("\n[2/2] Building AppImage (arm64)...")
|
||||
run([
|
||||
"python3", "utils/build_appimage.py",
|
||||
"--linux", "ubuntu", "--arch", "arm64", "--python", "3.12",
|
||||
])
|
||||
|
||||
print("\nLinux ARM64 build complete.")
|
||||
print(f" .deb: dist_deb/umbraedit_*_arm64.deb")
|
||||
print(f" AppImage: dist_appimage/umbraedit-*-arm64.AppImage")
|
||||
|
||||
|
||||
def build_macos_arm64() -> None:
|
||||
"""Build .dmg and .app for macOS ARM64 (Apple Silicon)."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building macOS ARM64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_macos_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
return
|
||||
|
||||
print("\n[1/1] Building macOS .dmg (arm64)...")
|
||||
run(["bash", "setup/macos/build.sh", "3.12", "arm64", "arm64"])
|
||||
|
||||
print("\nmacOS ARM64 build complete.")
|
||||
print(f" .dmg: dist_macos/umbraedit-2026.11-arm64.dmg")
|
||||
|
||||
|
||||
def build_macos_amd64() -> None:
|
||||
"""Build .dmg and .app for macOS AMD64 (Intel)."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building macOS AMD64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_macos_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
return
|
||||
|
||||
print("\n[1/1] Building macOS .dmg (amd64)...")
|
||||
run(["bash", "setup/macos/build.sh", "3.12", "amd64", "amd64"])
|
||||
|
||||
print("\nmacOS AMD64 build complete.")
|
||||
print(f" .dmg: dist_macos/umbraedit-2026.11-amd64.dmg")
|
||||
|
||||
|
||||
def build_windows_amd64() -> None:
|
||||
"""Build .msi and .exe for Windows AMD64."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building Windows AMD64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_windows_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
print("Windows builds typically require a Windows host or cross-compilation.")
|
||||
return
|
||||
|
||||
print("\n[1/1] Building Windows .exe/.msi (amd64)...")
|
||||
run(["python", "utils/build_windows.py", "--arch", "amd64"])
|
||||
|
||||
print("\nWindows AMD64 build complete.")
|
||||
print(f" .exe: dist_win/umbraedit-*.exe")
|
||||
print(f" .msi: dist_win/umbraedit-*.msi")
|
||||
|
||||
|
||||
def build_windows_arm64() -> None:
|
||||
"""Build .msi and .exe for Windows ARM64."""
|
||||
print("\n" + "=" * 70)
|
||||
print("Building Windows ARM64 packages")
|
||||
print("=" * 70)
|
||||
|
||||
missing = check_windows_tools()
|
||||
if missing:
|
||||
print(f"Missing tools: {', '.join(missing)}")
|
||||
print("Install build dependencies first.")
|
||||
return
|
||||
|
||||
print("\n[1/1] Building Windows .exe/.msi (arm64)...")
|
||||
run(["python", "utils/build_windows.py", "--arch", "arm64"])
|
||||
|
||||
print("\nWindows ARM64 build complete.")
|
||||
print(f" .exe: dist_win/umbraedit-*-arm64.exe")
|
||||
print(f" .msi: dist_win/umbraedit-*-arm64.msi")
|
||||
|
||||
|
||||
def build_all_platforms() -> None:
|
||||
"""Build packages for all supported platforms."""
|
||||
print("=" * 70)
|
||||
print("UmbraEdit Multi-Platform Build")
|
||||
print("=" * 70)
|
||||
print(f"Version: {__version__}")
|
||||
print(f"Host: {platform.system()} {platform.machine()}")
|
||||
print()
|
||||
|
||||
# Build all Linux targets
|
||||
build_linux_amd64()
|
||||
build_linux_arm64()
|
||||
|
||||
# Build macOS targets (only on macOS host)
|
||||
if platform.system() == "Darwin":
|
||||
build_macos_arm64()
|
||||
build_macos_amd64()
|
||||
else:
|
||||
print("\n[skip] macOS builds require macOS host")
|
||||
print(" Build macOS packages on a Mac with Xcode command line tools")
|
||||
|
||||
# Build Windows targets (only on Windows host or with cross-tools)
|
||||
if platform.system() == "Windows":
|
||||
build_windows_amd64()
|
||||
build_windows_arm64()
|
||||
else:
|
||||
print("\n[skip] Windows builds require Windows host or cross-compilation toolchain")
|
||||
print(" Build Windows packages on Windows with NSIS/Inno Setup")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Multi-platform build complete.")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build UmbraEdit install packages for all platforms."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=["all", "linux", "macos", "windows"],
|
||||
default="all",
|
||||
help="Platform to build (default: all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--arch",
|
||||
choices=["amd64", "arm64"],
|
||||
help="Target architecture (default: host architecture)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.platform == "all":
|
||||
build_all_platforms()
|
||||
elif args.platform == "linux":
|
||||
arch = args.arch or ("arm64" if platform.machine() == "aarch64" else "amd64")
|
||||
if arch == "amd64":
|
||||
build_linux_amd64()
|
||||
else:
|
||||
build_linux_arm64()
|
||||
elif args.platform == "macos":
|
||||
arch = args.arch or ("arm64" if platform.machine() == "arm64" else "amd64")
|
||||
if arch == "amd64":
|
||||
build_macos_amd64()
|
||||
else:
|
||||
build_macos_arm64()
|
||||
elif args.platform == "windows":
|
||||
arch = args.arch or "amd64"
|
||||
if arch == "amd64":
|
||||
build_windows_amd64()
|
||||
else:
|
||||
build_windows_arm64()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,6 +30,8 @@ import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from utils.common import (
|
||||
ROOT_DIR,
|
||||
SETUP_DIR,
|
||||
|
||||
@@ -30,6 +30,10 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from utils.common import (
|
||||
MIN_PY_VERSION,
|
||||
MIN_QT_VERS,
|
||||
@@ -75,7 +79,7 @@ Build-Depends:
|
||||
%dependencies%,
|
||||
%test-dependencies%
|
||||
Standards-Version: 4.5.1
|
||||
Homepage: https://umbraedit.io
|
||||
Homepage: https://umbratools.com/umbraedit
|
||||
X-Python3-Version: >= {MIN_PY_VERSION}
|
||||
|
||||
Package: umbraedit
|
||||
|
||||
@@ -32,6 +32,8 @@ import urllib.request
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from utils.common import (
|
||||
ROOT_DIR,
|
||||
appdataXml,
|
||||
|
||||
@@ -24,6 +24,11 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from utils.common import ROOT_DIR, SETUP_DIR, copySourceCode, freshFolder, log, systemCall, updateMetaFile
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import zipfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from utils.common import (
|
||||
ROOT_DIR,
|
||||
SETUP_DIR,
|
||||
|
||||
@@ -28,7 +28,10 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
import tomli as tomllib
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||