gui.py: one button per direction, "SAB file to SAX file" and "SAX file to SAB file". The button decides which extension the chooser offers and which way the conversion goes. The log names the chosen file and its folder before the work starts, then the output file and folder when it finishes, each value on its own line. Output lands next to the input under the timestamped name, so there is no save dialog and nothing is overwritten. Sedona home sits at the top with a Change... button that validates manifests/ and rewrites system.properties. run.py: no arguments opens the window, any argument goes to the command line converter. pythonw run.py for a shortcut with no console. config: NAME/VERSION are now "SAB - SAX Sedona Files Converter 0.0.0.002", carried by the window title, every dialog title and the CLI banner. Adds choose_sab_input() - the existing choose_sab_file() is the save dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""Configuration: sedona home lives in `system.properties` next to this file.
|
|
|
|
Same key sedonac itself uses, but written as a plain Windows path:
|
|
|
|
sedona.home=C:\\path\\to\\sedona
|
|
|
|
We only *read* this file, so there is no reason to escape anything. Java's
|
|
Properties (what sedonac uses) treats `\\` as an escape, so its own files double
|
|
them up -- `\\\\path\\\\to`. Both parse here: `\\\\` collapses to `\\`, and every
|
|
other backslash is kept literally, so `C:\\niagara` does not turn into a newline.
|
|
|
|
If the key is missing, or the directory no longer holds `manifests/`, a folder
|
|
chooser is shown and the answer is written back to `system.properties`.
|
|
"""
|
|
import os, sys
|
|
|
|
NAME = "SAB - SAX Sedona Files Converter"
|
|
VERSION = "0.0.0.002"
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
PROPS = os.path.join(HERE, "system.properties")
|
|
KEY = "sedona.home"
|
|
|
|
|
|
def title(text):
|
|
"""Every dialog carries the program name and version in its title bar."""
|
|
return "%s %s - %s" % (NAME, VERSION, text)
|
|
|
|
|
|
# ---------- .properties ----------
|
|
def _unescape(v):
|
|
"""Collapse `\\\\` to `\\`; leave any other backslash alone.
|
|
|
|
Accepts both our plain paths and java-escaped ones, and never mistakes a
|
|
path segment for an escape (`C:\\niagara` stays `C:\\niagara`).
|
|
"""
|
|
out, i = [], 0
|
|
while i < len(v):
|
|
if v[i] == "\\" and i + 1 < len(v) and v[i + 1] == "\\":
|
|
i += 1
|
|
out.append(v[i])
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
def read_props(path=PROPS):
|
|
props = {}
|
|
if not os.path.exists(path):
|
|
return props
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line[0] in "#!" or "=" not in line:
|
|
continue
|
|
k, v = line.split("=", 1)
|
|
props[k.strip()] = _unescape(v.strip()) # last wins
|
|
return props
|
|
|
|
|
|
def write_prop(key, value, path=PROPS):
|
|
"""Set `key`, keeping every other line of the file intact."""
|
|
lines = []
|
|
if os.path.exists(path):
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
lines = f.read().splitlines()
|
|
|
|
new = "%s=%s" % (key, value)
|
|
done = False
|
|
for i, line in enumerate(lines):
|
|
s = line.strip()
|
|
if s and s[0] not in "#!" and "=" in s and s.split("=", 1)[0].strip() == key:
|
|
lines[i] = new if not done else "#" + line # comment out later dupes
|
|
done = True
|
|
if not done:
|
|
lines.append(new)
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(lines) + "\n")
|
|
|
|
|
|
# ---------- dialogs ----------
|
|
def _tk():
|
|
"""A hidden root window, or None when no GUI is available."""
|
|
try:
|
|
import tkinter as tk
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
return root
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def ask_directory(text, initial=None):
|
|
root = _tk()
|
|
if root is None:
|
|
return None
|
|
try:
|
|
from tkinter import filedialog
|
|
return filedialog.askdirectory(title=title(text), initialdir=initial or HERE) or None
|
|
finally:
|
|
root.destroy()
|
|
|
|
|
|
def ask_open_file(text, filetypes, initial=None):
|
|
root = _tk()
|
|
if root is None:
|
|
return None
|
|
try:
|
|
from tkinter import filedialog
|
|
return filedialog.askopenfilename(title=title(text), filetypes=filetypes,
|
|
initialdir=initial or HERE) or None
|
|
finally:
|
|
root.destroy()
|
|
|
|
|
|
def ask_save_file(text, filetypes, defaultextension, initialfile=None, initial=None):
|
|
root = _tk()
|
|
if root is None:
|
|
return None
|
|
try:
|
|
from tkinter import filedialog
|
|
return filedialog.asksaveasfilename(title=title(text), filetypes=filetypes,
|
|
defaultextension=defaultextension,
|
|
initialfile=initialfile,
|
|
initialdir=initial or HERE) or None
|
|
finally:
|
|
root.destroy()
|
|
|
|
|
|
# ---------- api ----------
|
|
def is_sedona_home(path):
|
|
return bool(path) and os.path.isdir(os.path.join(path, "manifests"))
|
|
|
|
|
|
def get_sedona_home(interactive=True):
|
|
"""Path from system.properties, prompting (and saving) when unusable."""
|
|
home = read_props().get(KEY)
|
|
if is_sedona_home(home):
|
|
return home
|
|
|
|
if not interactive:
|
|
raise SystemExit("%s not set (or has no manifests/) in %s" % (KEY, PROPS))
|
|
|
|
why = "not set in" if not home else "invalid (%s), update" % home
|
|
print("sedona.home %s %s -- please choose the sedona installation folder" % (why, PROPS))
|
|
while True:
|
|
picked = ask_directory("Select Sedona home (the folder containing manifests/)",
|
|
home if home and os.path.isdir(home) else None)
|
|
if not picked:
|
|
raise SystemExit("no sedona home selected")
|
|
picked = os.path.normpath(picked)
|
|
if is_sedona_home(picked):
|
|
write_prop(KEY, picked)
|
|
print("saved %s=%s to %s" % (KEY, picked, PROPS))
|
|
return picked
|
|
print(" %s has no manifests/ subfolder -- try again" % picked)
|
|
|
|
|
|
SAX = ("Sedona app XML", "*.sax")
|
|
SAB = ("Sedona app binary", "*.sab")
|
|
ANY = ("All files", "*.*")
|
|
|
|
|
|
def choose_input_file(initial=None):
|
|
"""Pick the file to convert -- direction follows its extension."""
|
|
return ask_open_file("Select the .sax or .sab application file to convert",
|
|
[("Sedona app (*.sax, *.sab)", "*.sax *.sab"), SAX, SAB, ANY],
|
|
initial)
|
|
|
|
|
|
def choose_sax_file(initial=None):
|
|
"""Open a .sax -- the input of a sax -> sab conversion."""
|
|
return ask_open_file("Select the .sax application file to convert to .sab",
|
|
[SAX, ANY], initial)
|
|
|
|
|
|
def choose_sab_input(initial=None):
|
|
"""Open a .sab -- the input of a sab -> sax conversion."""
|
|
return ask_open_file("Select the .sab application file to convert to .sax",
|
|
[SAB, ANY], initial)
|
|
|
|
|
|
def choose_sab_file(initialfile=None, initial=None):
|
|
"""Save-as for the .sab produced from a .sax."""
|
|
return ask_save_file("Save the .sab application file", [SAB, ANY],
|
|
".sab", initialfile, initial)
|
|
|
|
|
|
def choose_sax_output(initialfile=None, initial=None):
|
|
"""Save-as for the .sax produced from a .sab."""
|
|
return ask_save_file("Save the .sax application file", [SAX, ANY],
|
|
".sax", initialfile, initial)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(get_sedona_home())
|