Files
bal-electrum-plugin/bal/gui/qt/calendar.py
GenSpark AI Developer 4198a5145b BAL - Bitcoin After Life Electrum plugin (v0.2.8)
Behavior-preserving refactor of the original BAL plugin with clean separation
of business logic from the PyQt GUI.

Layout:
  bal/core/   GUI-free logic (util, plugin_base, heirs, will, willexecutors)
  bal/gui/qt/ PyQt6 presentation (theme, common, widgets, calendar, dialogs,
              lists, window, plugin)
  bal/qt.py   Qt entry-point shim (works as internal and external zip plugin)
  bal/manifest.json  standard-conforming metadata

Tooling:
  build_zip.py            deterministic, zipimport-friendly archive builder
  tests/smoke_test.py     imports + behavior regression test
  tests/external_zip_test.py  reproduces Electrum's external-zip loading

Targets Electrum 4.7.2 + PyQt6. Logic kept byte-identical where possible.
2026-06-07 09:22:34 +00:00

81 lines
2.6 KiB
Python

"""
bal.gui.qt.calendar
===================
iCalendar (.ics) generation and "open with default calendar app" helper.
When a will is built, the plugin can create a calendar event reminding the user
to "check in" before the locktime expires. This module turns the event data
into an RFC-5545 .ics file and opens it with the OS default application.
"""
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
class BalCalendar:
@staticmethod
def write_temp_ics(content):
fd, path = tempfile.mkstemp(prefix="event_", suffix=".ics")
with os.fdopen(fd, "wb") as f:
f.write(content.encode("utf-8"))
return path
@staticmethod
def open_with_default_app(calendar_app, path):
_logger.debug("opening calendar app")
try:
subprocess.check_call([calendar_app, path])
return True
except Exception as e:
_logger.error(f"starting calendar app {e}")
return False
@staticmethod
def format_time(time):
return time.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
#return time.astimezone(timezone.utc).strftime("%Y%m%d")
@staticmethod
def ical_escape(text: str) -> str:
# escape per RFC5545: backslash, ; , newlines
text = text.encode("utf-8")
text = (
text.replace(b"\\", b"\\\\")
.replace(b";", b"\\;")
.replace(b",", b"\\,")
)
out =""
temp=text.split(b"\r\n")
for s in temp:
encoded= s
cut =0
while len(encoded) >75:
cut+=5
encoded=f"{s[:len(s)-cut]}"
if encoded[-1]==b"\\" and encoded[-2]!=b"\\\\":
cut += 1
encoded=f"{s[:len(s)-cut]}"
encoded=f"{encoded}...\r\n".encode("utf-8")
if cut>0:
out+=str(f"{s[:len(s)-cut].decode()}...\r\n")
else:
out+=str(f"{s.decode()}\r\n")
return out[:-2]
@staticmethod
def fold_ical_line(line: str, limit: int = 75) -> str:
# ritorna linee separate da CRLF e folding con spazio iniziale sulle righe successive
encoded = line.encode("utf-8")
parts = []
while len(encoded) > limit:
# taglia senza spezzare byte UTF-8
cut = limit
while (encoded[cut] & 0xC0) == 0x80: # byte di continuazione UTF-8
cut -= 1
parts.append(encoded[:cut].decode("utf-8"))
encoded = encoded[cut:]
parts.append(encoded.decode("utf-8"))
return "\r\n ".join(parts)