From dc2cb7cbc742beb7a63b3074774f7cea5a0c1545 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Sun, 31 May 2026 21:32:16 -0500 Subject: [PATCH] built auto save extension for LibreOffice --- .gitignore | 27 ++ build/Jobs.xcu | 22 ++ build/META-INF/manifest.xml | 5 + build/__pycache__/autosave.cpython-314.pyc | Bin 0 -> 4090 bytes build/autosave.py | 107 +++++++ build/description.xml | 8 + build_oxt.py | 32 ++ deploy_and_test.ps1 | 33 +++ poetry.lock | 273 ++++++++++++++++++ pyproject.toml | 22 ++ src/Jobs.xcu | 22 ++ src/META-INF/manifest.xml | 5 + src/__pycache__/autosave.cpython-314.pyc | Bin 0 -> 7224 bytes src/autosave.py | 107 +++++++ src/description.xml | 8 + ...test_autosave.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 7601 bytes tests/test_autosave.py | 112 +++++++ 17 files changed, 783 insertions(+) create mode 100644 .gitignore create mode 100644 build/Jobs.xcu create mode 100644 build/META-INF/manifest.xml create mode 100644 build/__pycache__/autosave.cpython-314.pyc create mode 100644 build/autosave.py create mode 100644 build/description.xml create mode 100644 build_oxt.py create mode 100644 deploy_and_test.ps1 create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/Jobs.xcu create mode 100644 src/META-INF/manifest.xml create mode 100644 src/__pycache__/autosave.cpython-314.pyc create mode 100644 src/autosave.py create mode 100644 src/description.xml create mode 100644 tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/test_autosave.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b607e54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +.antigravitycli/ +.pytest_cache/ +dist/ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/Jobs.xcu b/build/Jobs.xcu new file mode 100644 index 0000000..8eef9fb --- /dev/null +++ b/build/Jobs.xcu @@ -0,0 +1,22 @@ + + + + + + org.libreoffice.extensions.autosave.AutoSaveJob + + + + + + + + + + + + + + + + diff --git a/build/META-INF/manifest.xml b/build/META-INF/manifest.xml new file mode 100644 index 0000000..81cf6bf --- /dev/null +++ b/build/META-INF/manifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/build/__pycache__/autosave.cpython-314.pyc b/build/__pycache__/autosave.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0e97deb4ac3341be9c775d8dfe172d1db801428 GIT binary patch literal 4090 zcmb6cZEO?Cb=JFUZ`O{TV3Oc`V21<;UrC@LBGd=mK?o!#F8CHM^we;x&3cn8alG5v zH6;81tAEN>r*ixPe!*3hfL{fv68vnd1NEmJBQ{z%38^QY`Y%AFI~A$wo3+>Z1k{oC z&6}C`-n^MN?_+-vtmP53pZ?Nvc1r-Ef02RPycOj2bwKjyOC(Uk$Sn7iNx|4N>=~j> zI$EA%Ova&MO%{53*gNDieNoha+L6FCA%Q)^rs`bb%l#Q&A5$KgHG;PZnfw`k4`up+ zddo7Ei?yS^3ToC0R0m4<{!PXs3Cd;z-(^-e(Y=yY9{%sTCKEK9k#fJRZ))d~ry1Kp)Y60}XHHQ8pv1{0kz@fy2cnN{SRsfdO&LBKi1W(`xq=3Ng4XF@isoKv!B1E>4A4xxN1Y#v(nr0M^RX*J_Rt^&F!fS5M)6`$gWx9e{A%OS_(< z(=F&dx`0owZOgj2c1?rEaA&P9WooO7E%)u{bcefd^lk5O`!c zLU%U_F|lz;p3dp92?>bO$=Jw+tt6~3uT5etmrXnCIjkgOiEuezgp8c7YuGeSD)=01 zXZxolo|N=7sX9q)9FX@GA#KEU z-%|yd zA6W=)vVuE{!JUQf;eYNa1yAIUE%5agzrDzBFY#UZfggSR^Ki$lfqTZ2@Q0VV12Zp*k5+!gIIvQh<6#}t{@mw)H77`4Ap**TAm^P8LImpbuTfysn|Dj*DhYYc23jAxfJ7 literal 0 HcmV?d00001 diff --git a/build/autosave.py b/build/autosave.py new file mode 100644 index 0000000..187e68e --- /dev/null +++ b/build/autosave.py @@ -0,0 +1,107 @@ +import os +import shutil +import traceback +from threading import Timer +from datetime import datetime +import uno +import unohelper +from com.sun.star.util import XModifyListener +from com.sun.star.task import XJob, XJobExecutor + +LOG_FILE = "C:\\Users\\jasonross\\workspace\\LibreOfficeAutoSave\\autosave_debug.log" + +def log_debug(msg): + try: + with open(LOG_FILE, "a") as f: + f.write(f"[{datetime.now().isoformat()}] {msg}\n") + except Exception: + pass + +log_debug("autosave.py module is being loaded by LibreOffice!") + +class AutoSaveListener(unohelper.Base, XModifyListener): + def __init__(self, doc, delay_seconds=1.0): + self.doc = doc + self.delay = delay_seconds + self.timer = None + log_debug(f"AutoSaveListener initialized for document: {doc}") + + def modified(self, event): + if self.timer: + self.timer.cancel() + + self.timer = Timer(self.delay, self.execute_safe_save) + self.timer.start() + + def execute_safe_save(self): + try: + log_debug("Timer expired, attempting save...") + if self.doc.supportsService("com.sun.star.document.OfficeDocument"): + if self.doc.hasLocation() and not self.doc.isReadonly(): + file_url = self.doc.getURL() + log_debug(f"File has location: {file_url}") + + if file_url.startswith("file:///"): + system_path = uno.fileUrlToSystemPath(file_url) + bak_path = os.path.join(os.path.dirname(system_path), f".{os.path.basename(system_path)}.bak") + try: + if os.path.exists(system_path): + shutil.copy2(system_path, bak_path) + log_debug(f"Created backup at {bak_path}") + except Exception as e: + log_debug(f"Backup failed: {e}") + + self.doc.store() + log_debug("Document successfully saved.") + else: + log_debug("Document is readonly or has no location (not saved yet).") + except Exception as e: + log_debug(f"Auto-save exception: {e}\n{traceback.format_exc()}") + + def disposing(self, event): + if self.timer: + self.timer.cancel() + log_debug("Listener disposed.") + +g_listeners = {} + +class AutoSaveJob(unohelper.Base, XJob, XJobExecutor): + def __init__(self, ctx=None): + self.ctx = ctx + log_debug("AutoSaveJob instantiated!") + + def execute(self, arguments): + log_debug(f"AutoSaveJob execute() called with args: {arguments}") + self._attach_listener() + return () + + def trigger(self, args): + log_debug(f"AutoSaveJob trigger() called with args: {args}") + self._attach_listener() + + def _attach_listener(self): + try: + ctx = uno.getComponentContext() + smgr = ctx.ServiceManager + desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) + doc = desktop.getCurrentComponent() + + if doc: + if doc in g_listeners: + log_debug("Listener already attached to this document.") + return + listener = AutoSaveListener(doc, delay_seconds=1.0) + doc.addModifyListener(listener) + g_listeners[doc] = listener + log_debug("Successfully attached listener to current document.") + else: + log_debug("No active document found.") + except Exception as e: + log_debug(f"Error in AutoSaveJob attach: {e}\n{traceback.format_exc()}") + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation( + AutoSaveJob, + "org.libreoffice.extensions.autosave.AutoSaveJob", + ("com.sun.star.task.Job",), +) diff --git a/build/description.xml b/build/description.xml new file mode 100644 index 0000000..e89d342 --- /dev/null +++ b/build/description.xml @@ -0,0 +1,8 @@ + + + + + + AutoSave + + diff --git a/build_oxt.py b/build_oxt.py new file mode 100644 index 0000000..0026e6c --- /dev/null +++ b/build_oxt.py @@ -0,0 +1,32 @@ +import os +import zipfile +import shutil + +def build_oxt(): + build_dir = "build" + dist_dir = "dist" + oxt_name = "AutoSave.oxt" + + # Clean previous builds + if os.path.exists(build_dir): + shutil.rmtree(build_dir) + if not os.path.exists(dist_dir): + os.makedirs(dist_dir) + + # Copy source files to build directory + shutil.copytree("src", build_dir) + + # Create the .oxt file (which is a zip archive) + oxt_path = os.path.join(dist_dir, oxt_name) + with zipfile.ZipFile(oxt_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(build_dir): + for file in files: + file_path = os.path.join(root, file) + # The archive root should be the contents of the 'build' dir + arcname = os.path.relpath(file_path, build_dir).replace("\\", "/") + zipf.write(file_path, arcname) + + print(f"Successfully built extension: {oxt_path}") + +if __name__ == "__main__": + build_oxt() diff --git a/deploy_and_test.ps1 b/deploy_and_test.ps1 new file mode 100644 index 0000000..34794c4 --- /dev/null +++ b/deploy_and_test.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = "Stop" + +$LO_PATH = "C:\Program Files\LibreOffice\program" +$UNOPKG = Join-Path $LO_PATH "unopkg.com" +$SOFFICE = Join-Path $LO_PATH "soffice.exe" +$EXT_ID = "org.libreoffice.extensions.autosave" + +Write-Host "1. Building the extension..." -ForegroundColor Cyan +python build_oxt.py + +Write-Host "2. Killing open LibreOffice instances..." -ForegroundColor Cyan +# Kill both the launcher and the main binary, silently ignoring if they aren't running +Stop-Process -Name "soffice" -Force -ErrorAction SilentlyContinue +Stop-Process -Name "soffice.bin" -Force -ErrorAction SilentlyContinue + +# Give the OS a moment to fully release file locks on the UNO packages +Start-Sleep -Seconds 2 + +Write-Host "3. Uninstalling existing extension..." -ForegroundColor Cyan +# We catch and ignore errors here because 'unopkg remove' will throw an error if the extension isn't currently installed +try { + & $UNOPKG remove $EXT_ID *>&1 | Out-Null +} catch { + # Ignore "package not found" errors +} + +Write-Host "4. Installing the new extension..." -ForegroundColor Cyan +& $UNOPKG add -f dist\AutoSave.oxt + +Write-Host "5. Starting LibreOffice Writer..." -ForegroundColor Cyan +Start-Process -FilePath $SOFFICE -ArgumentList "--writer" + +Write-Host "Deployment complete! Writer is launching in the background." -ForegroundColor Green diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..98f5738 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,273 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "black" +version = "26.5.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893"}, + {file = "black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90"}, + {file = "black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4"}, + {file = "black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef"}, + {file = "black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22"}, + {file = "black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c"}, + {file = "black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7"}, + {file = "black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59"}, + {file = "black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3"}, + {file = "black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe"}, + {file = "black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8"}, + {file = "black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217"}, + {file = "black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d"}, + {file = "black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264"}, + {file = "black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418"}, + {file = "black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3"}, + {file = "black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0"}, + {file = "black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294"}, + {file = "black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a"}, + {file = "black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52"}, + {file = "black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168"}, + {file = "black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3"}, + {file = "black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18"}, + {file = "black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50"}, + {file = "black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae"}, + {file = "black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2"}, + {file = "black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.4.0,<0.5.0" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""] + +[[package]] +name = "click" +version = "8.4.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, + {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "isort" +version = "8.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.10.0" +groups = ["dev"] +files = [ + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, +] + +[package.extras] +colors = ["colorama"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + +[[package]] +name = "platformdirs" +version = "4.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, + {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.14" +content-hash = "677487ac55f2b53800a1f496f6b30a79dcec9f89aae9c147f61e95914fd22dd9" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9f3ce84 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "libreoffice-autosave" +version = "0.1.0" +description = "LibreOffice AutoSave Extension" +authors = [ + {name = "Jason Ross",email = "jason.ross841@gmail.com"} +] +requires-python = ">=3.14" +dependencies = [ +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[dependency-groups] +dev = [ + "pytest (>=9.0.3,<10.0.0)", + "black (>=26.5.1,<27.0.0)", + "isort (>=8.0.1,<9.0.0)" +] diff --git a/src/Jobs.xcu b/src/Jobs.xcu new file mode 100644 index 0000000..8eef9fb --- /dev/null +++ b/src/Jobs.xcu @@ -0,0 +1,22 @@ + + + + + + org.libreoffice.extensions.autosave.AutoSaveJob + + + + + + + + + + + + + + + + diff --git a/src/META-INF/manifest.xml b/src/META-INF/manifest.xml new file mode 100644 index 0000000..81cf6bf --- /dev/null +++ b/src/META-INF/manifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/__pycache__/autosave.cpython-314.pyc b/src/__pycache__/autosave.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..499e46a60e9689f98e06c35881dc357f99c3bd55 GIT binary patch literal 7224 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 + + + + + AutoSave + + diff --git a/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9429f118bc98368624abe8af72e0725d5d9747c8 GIT binary patch literal 7601 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 K*&`hkBKU9C!-g3E literal 0 HcmV?d00001 diff --git a/tests/test_autosave.py b/tests/test_autosave.py new file mode 100644 index 0000000..91121ae --- /dev/null +++ b/tests/test_autosave.py @@ -0,0 +1,112 @@ +import sys +import os +import time +from unittest.mock import MagicMock, patch + +# Add the project root to sys.path so we can import src +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Mock the uno and unohelper modules before importing our module +sys.modules['uno'] = MagicMock() + +class MockBase: pass +mock_unohelper = MagicMock() +mock_unohelper.Base = MockBase +sys.modules['unohelper'] = mock_unohelper + +class MockListener: pass +class MockXJob: pass +class MockXJobExecutor: pass + +mock_util = MagicMock() +mock_util.XModifyListener = MockListener +sys.modules['com.sun.star.util'] = mock_util + +mock_task = MagicMock() +mock_task.XJob = MockXJob +mock_task.XJobExecutor = MockXJobExecutor +sys.modules['com.sun.star.task'] = mock_task + +# Now we can safely import our module +import src.autosave as autosave +from src.autosave import AutoSaveListener, AutoSaveJob + +def setup_function(): + # Clear the global listeners dictionary before each test + autosave.g_listeners.clear() + +def test_debounce_timer(): + mock_doc = MagicMock() + listener = AutoSaveListener(mock_doc, delay_seconds=0.1) + + # Simulate multiple rapid modifications + listener.modified(None) + first_timer = listener.timer + + listener.modified(None) + second_timer = listener.timer + + # The first timer should be cancelled, the second should be active + assert first_timer is not second_timer + + # Ensure execute_safe_save has not executed synchronously + mock_doc.store.assert_not_called() + +@patch('src.autosave.os.path.exists') +@patch('src.autosave.shutil.copy2') +def test_execute_safe_save_success(mock_copy2, mock_exists): + mock_doc = MagicMock() + mock_doc.supportsService.return_value = True + mock_doc.hasLocation.return_value = True + mock_doc.isReadonly.return_value = False + mock_doc.getURL.return_value = "file:///dummy/path/test.odt" + + # Mock uno.fileUrlToSystemPath to just return a string + autosave.uno.fileUrlToSystemPath.return_value = "/dummy/path/test.odt" + mock_exists.return_value = True + + listener = AutoSaveListener(mock_doc, delay_seconds=0.1) + listener.execute_safe_save() + + # Assert copy2 was called to create the backup + mock_copy2.assert_called_once() + # Assert store() was called to save the document + mock_doc.store.assert_called_once() + +def test_execute_safe_save_no_location(): + mock_doc = MagicMock() + mock_doc.supportsService.return_value = True + mock_doc.hasLocation.return_value = False # Document not saved yet + + listener = AutoSaveListener(mock_doc) + listener.execute_safe_save() + + # store() should NOT be called + mock_doc.store.assert_not_called() + +def test_autosave_job_execution(): + job = AutoSaveJob() + + # Mock context and document + mock_ctx = MagicMock() + autosave.uno.getComponentContext.return_value = mock_ctx + + mock_smgr = MagicMock() + mock_ctx.ServiceManager = mock_smgr + + mock_desktop = MagicMock() + mock_smgr.createInstanceWithContext.return_value = mock_desktop + + mock_doc = MagicMock() + mock_desktop.getCurrentComponent.return_value = mock_doc + + # Execute the job + job.execute(()) + + # The listener should be created and attached + assert mock_doc in autosave.g_listeners + mock_doc.addModifyListener.assert_called_once() + + # Re-executing should not attach a duplicate listener + job.execute(()) + assert mock_doc.addModifyListener.call_count == 1