diff --git a/README.md b/README.md index bb30bbe..56157b8 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,46 @@ runtime lib). Verified byte-identical to `sedonac.exe` in both directions on a 189-component / 217-link app across 18 kits. -## Usage +## Run it - python sab2sax.py [in.sax|in.sab] [out] + python run.py the window + pythonw run.py the window with no console behind it + python run.py app.sab [out] straight to the command line converter + +`run.py` is the single entry point: no arguments opens the GUI, any argument is handed to the +command line converter. Double-click it (or a shortcut to `pythonw run.py`) and you get the window. + +## The window + + python run.py (or: python gui.py) + +Title bar carries the name and version (`SAB - SAX Sedona Files Converter 0.0.0.002`). One button +per direction — **SAB file to SAX file** and **SAX file to SAB file** — and the button you press +decides which extension the file chooser offers. The log shows the chosen file and its folder +*before* the conversion runs, then the output file and its folder when it finishes: + + sab -> sax + INPUT FILE: + app.sab + INPUT PATH: + C:\apps + OUTPUT FILE: + app-sab2sax-20260728-140609.sax + OUTPUT PATH: + C:\apps + wrote 61294 bytes + +The result is written next to the input under that timestamped name, so nothing is overwritten and +no save dialog gets in the way. Sedona home is shown at the top with a `Change...` button that +rewrites `system.properties`. + +## The command line + + python run.py [in.sax|in.sab] [out] (or: python sab2sax.py ...) Like `sedonac`, the direction follows the input's extension: feed it a `.sax` and you get a `.sab`, -feed it a `.sab` and you get a `.sax`. Both arguments are optional, so you can run it by -double-click: +feed it a `.sab` and you get a `.sax`. Both arguments are optional — `sab2sax.py` on its own asks for +what it needs: * no input → a file chooser opens, listing `.sax` and `.sab`; * no output → a save dialog opens next to the input, pre-filled with a timestamped name: @@ -63,10 +96,11 @@ set the key by hand instead. ## Version -`config.NAME` / `config.VERSION` — currently `sab2sax 0.0.0.001`. Every dialog title bar reads -`sab2sax 0.0.0.001 - Select the .sax or .sab application file to convert`, and the CLI prints the -same line when it starts, so it is obvious which build produced a file. Bump it in -[config.py](config.py) and both follow. +`config.NAME` / `config.VERSION` — currently `SAB - SAX Sedona Files Converter 0.0.0.002`. The window +title and every dialog title bar carry it; a chooser reads `SAB - SAX Sedona Files Converter +0.0.0.002 - Select the .sax or .sab application file to convert`, and the CLI prints the same line +when it starts, so it is obvious which build produced a file. Bump it in [config.py](config.py) and +all of them follow. ## Verify @@ -100,6 +134,8 @@ Exit code is 0 on pass, 1 on failure. A malformed file fails with a one-line rea ## Files + run.py entry point: window with no arguments, CLI with them + gui.py the window: a button per direction sab2sax.py the CLI: dispatches on the input extension sab.py sax -> sab encoder sax.py sab -> sax decoder diff --git a/config.py b/config.py index 6706e5e..e055a7d 100644 --- a/config.py +++ b/config.py @@ -14,8 +14,8 @@ chooser is shown and the answer is written back to `system.properties`. """ import os, sys -NAME = "sab2sax" -VERSION = "0.0.0.001" +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") @@ -169,7 +169,15 @@ def choose_input_file(initial=None): def choose_sax_file(initial=None): - return ask_open_file("Select the .sax application file", [SAX, ANY], initial) + """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): diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..2d94c7a --- /dev/null +++ b/gui.py @@ -0,0 +1,166 @@ +"""A window with two buttons, one per direction. + + python gui.py + +Pick a direction, pick a file; the log shows what was chosen before the +conversion runs and where the result landed afterwards. Output goes next to the +input under a timestamped name, so nothing is ever overwritten. +""" +import os, sys, traceback +import tkinter as tk +from tkinter import ttk, messagebox + +import config, sab, sax + + +class App: + def __init__(self, root): + self.root = root + root.title("%s %s" % (config.NAME, config.VERSION)) + root.minsize(620, 340) + + frame = ttk.Frame(root, padding=12) + frame.pack(fill="both", expand=True) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(3, weight=1) + + # sedona home + home_box = ttk.LabelFrame(frame, text="Sedona home", padding=(8, 4)) + home_box.grid(row=0, column=0, sticky="ew") + home_box.columnconfigure(0, weight=1) + self.home_var = tk.StringVar(value="(not set)") + ttk.Label(home_box, textvariable=self.home_var).grid(row=0, column=0, sticky="w") + ttk.Button(home_box, text="Change...", command=self.change_home).grid(row=0, column=1, padx=(8, 0)) + + # the two directions + buttons = ttk.Frame(frame, padding=(0, 12)) + buttons.grid(row=1, column=0, sticky="ew") + buttons.columnconfigure(0, weight=1) + buttons.columnconfigure(1, weight=1) + self.b_sab2sax = ttk.Button(buttons, text="SAB file to SAX file", padding=10, + command=lambda: self.run("sab")) + self.b_sab2sax.grid(row=0, column=0, sticky="ew", padx=(0, 6)) + self.b_sax2sab = ttk.Button(buttons, text="SAX file to SAB file", padding=10, + command=lambda: self.run("sax")) + self.b_sax2sab.grid(row=0, column=1, sticky="ew", padx=(6, 0)) + + self.status = tk.StringVar(value="Choose a direction.") + ttk.Label(frame, textvariable=self.status).grid(row=2, column=0, sticky="w") + + # log + log_box = ttk.Frame(frame) + log_box.grid(row=3, column=0, sticky="nsew", pady=(8, 0)) + log_box.columnconfigure(0, weight=1) + log_box.rowconfigure(0, weight=1) + self.log_text = tk.Text(log_box, height=10, wrap="word", state="disabled") + self.log_text.grid(row=0, column=0, sticky="nsew") + bar = ttk.Scrollbar(log_box, orient="vertical", command=self.log_text.yview) + bar.grid(row=0, column=1, sticky="ns") + self.log_text.configure(yscrollcommand=bar.set) + + self.home = None + root.after(50, self.first_home) + + # ---------- helpers ---------- + def log(self, line=""): + self.log_text.configure(state="normal") + self.log_text.insert("end", line + "\n") + self.log_text.see("end") + self.log_text.configure(state="disabled") + self.root.update_idletasks() # so it is on screen before the work starts + + def set_home(self, path): + self.home = path + self.home_var.set(path or "(not set)") + + def first_home(self): + """Resolve sedona home at startup; the folder chooser opens if it isn't set.""" + try: + self.set_home(config.get_sedona_home()) + self.log("sedona home: %s" % self.home) + except SystemExit as e: + self.log("no sedona home: %s" % e) + messagebox.showwarning(config.title("Sedona home"), + "No sedona home selected.\n\n" + "Set it with Change... before converting.") + + def change_home(self): + picked = config.ask_directory("Select Sedona home (the folder containing manifests/)", + self.home) + if not picked: + return + picked = os.path.normpath(picked) + if not config.is_sedona_home(picked): + messagebox.showerror(config.title("Sedona home"), + "%s has no manifests/ subfolder." % picked) + return + config.write_prop(config.KEY, picked) + self.set_home(picked) + self.log("sedona home: %s (saved to %s)" % (picked, config.PROPS)) + + # ---------- conversion ---------- + def run(self, direction): + """direction: 'sab' means sab -> sax, 'sax' means sax -> sab.""" + if not config.is_sedona_home(self.home): + messagebox.showerror(config.title("Sedona home"), + "Set a valid sedona home first (Change...).") + return + + src = config.choose_sab_input() if direction == "sab" else config.choose_sax_file() + if not src: + self.status.set("Cancelled.") + return + src = os.path.abspath(src) + + # show what was chosen, before doing anything with it + self.log() + self.log("%s -> %s" % (direction, "sax" if direction == "sab" else "sab")) + self.log(" INPUT FILE:") + self.log(" %s" % os.path.basename(src)) + self.log(" INPUT PATH:") + self.log(" %s" % os.path.dirname(src)) + self.status.set("Converting %s ..." % os.path.basename(src)) + + if direction == "sab": + out = os.path.join(os.path.dirname(src), sax.default_sax_name(src)) + work = lambda: sax.decode(src, self.home, out) + else: + out = os.path.join(os.path.dirname(src), sab.default_sab_name(src)) + work = lambda: sab.encode(src, self.home, out) + + self.busy(True) + try: + n = work() + except Exception as e: + self.busy(False) + self.log(" FAILED: %s" % e) + self.status.set("Failed.") + traceback.print_exc() + messagebox.showerror(config.title("Conversion failed"), str(e)) + return + self.busy(False) + + self.log(" OUTPUT FILE:") + self.log(" %s" % os.path.basename(out)) + self.log(" OUTPUT PATH:") + self.log(" %s" % os.path.dirname(out)) + self.log(" wrote %d bytes" % n) + self.status.set("Done: %s" % os.path.basename(out)) + + def busy(self, on): + state = "disabled" if on else "normal" + self.b_sab2sax.configure(state=state) + self.b_sax2sab.configure(state=state) + self.root.configure(cursor="watch" if on else "") + self.root.update_idletasks() + + +def main(): + root = tk.Tk() + App(root) + root.mainloop() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/run.py b/run.py new file mode 100644 index 0000000..030dca0 --- /dev/null +++ b/run.py @@ -0,0 +1,22 @@ +"""Start here. + + python run.py the window + pythonw run.py the window, without a console behind it + python run.py app.sab [out] straight to the command line converter + +No arguments means the GUI; any argument is passed through to sab2sax.py, so +the same entry point serves a double-click and a script. +""" +import sys + + +def main(argv): + if len(argv) > 1: + import sab2sax + return sab2sax.main(argv) or 0 + import gui + return gui.main() + + +if __name__ == "__main__": + sys.exit(main(sys.argv))