PyMacroRecord icon indicating copy to clipboard operation
PyMacroRecord copied to clipboard

Trigger playback via Python or CLI?

Open cameronweibel opened this issue 7 months ago • 3 comments

Hi, is there a way to run this with playback via Python or via CLI?

Like:

pymacrorecord.playback(PATH_TO_JSON) or

PyMacroRecord_1,3,0-portable.exe PATH_TO_JSON

If so, please let me know and I'll donate to the project!

cameronweibel avatar May 20 '25 14:05 cameronweibel

Hello, no it's not implemented and it's not planned unfortunately. PyMacroRecord is on stand-by at the moment because I'm working on another big project and I don't have much time to dedicate time to PyMacroRecord.

LOUDO56 avatar May 20 '25 21:05 LOUDO56

Let me know if I should PR. I uploaded your code to Gemini and got a working version that allows for PyMacroRecord-portable.exe test.pmr, just need to change main_app.py and build:

import json
import sys
from tkinter import *
from tkinter import messagebox  # Added messagebox import
from os import path

from utils.not_windows import NotWindows
from windows.window import Window
from windows.main.menu_bar import MenuBar
from utils.user_settings import UserSettings
from utils.get_file import resource_path
from utils.warning_pop_up_save import confirm_save
from utils.record_file_management import RecordFileManagement
from utils.version import Version
from windows.others.new_ver_avalaible import NewVerAvailable
from hotkeys.hotkeys_manager import HotkeysManager
from macro import Macro
from sys import platform, argv
from pystray import Icon, MenuItem
from PIL import Image
from threading import Thread
from json import load  # json.load is used directly
from time import time
import copy

if platform.lower() == "win32":
    from tkinter.ttk import *


def deepcopy_dict_missing_entries(dst: dict, src: dict):
    # recursively copy entries that are in src but not in dst
    for k, v in src.items():
        if k not in dst:
            dst[k] = copy.deepcopy(v)
        elif isinstance(v, dict):
            deepcopy_dict_missing_entries(dst[k], v)


class MainApp(Window):
    """Main windows of the application"""

    def __init__(self):
        super().__init__("PyMacroRecord", 350, 200)
        self.attributes("-topmost", 1)
        if platform == "win32":
            self.iconbitmap(resource_path(path.join("assets", "logo.ico")))

        self.settings = UserSettings(self)
        self.load_language()

        # For save message purpose
        self.macro_saved = False
        self.macro_recorded = False
        self.current_file = None
        self.prevent_record = False

        self.version = Version(self.settings.get_config(), self)
        self.menu = MenuBar(self)  # Menu Bar
        self.macro = Macro(self)
        self.validate_cmd = self.register(self.validate_input)
        self.hotkeyManager = HotkeysManager(self)

        self.status_text = Label(self, text="", relief=SUNKEN, anchor=W)
        if self.settings.get_config()["Recordings"]["Show_Events_On_Status_Bar"]:
            self.status_text.pack(side=BOTTOM, fill=X)

        # Initialize image assets
        self.playImg = PhotoImage(
            file=resource_path(path.join("assets", "button", "play.png"))
        )
        self.recordImg = PhotoImage(
            file=resource_path(path.join("assets", "button", "record.png"))
        )
        self.stopImg = PhotoImage(
            file=resource_path(path.join("assets", "button", "stop.png"))
        )

        # Initialize center frame for buttons
        self.center_frame = Frame(self)
        self.center_frame.pack(expand=True, fill=BOTH)

        # Create Play and Record buttons - their commands might be updated by CLI logic
        self.playBtn = Button(self.center_frame, image=self.playImg)
        self.playBtn.pack(side=LEFT, padx=50)

        self.recordBtn = Button(
            self.center_frame, image=self.recordImg, command=self.macro.start_record
        )
        self.recordBtn.pack(side=RIGHT, padx=50)

        cli_playback_initiated = False

        # Handle command-line argument for auto-playback
        if len(argv) > 1:
            file_path_arg = argv[1]
            if path.isfile(file_path_arg) and (
                file_path_arg.lower().endswith(".pmr")
                or file_path_arg.lower().endswith(".json")
            ):
                try:
                    self.update_idletasks()  # Ensure UI is somewhat ready

                    with open(file_path_arg, "r") as record_file_content:
                        loaded_content = load(record_file_content)

                    self.macro.import_record(loaded_content)
                    self.macro_recorded = True
                    self.current_file = file_path_arg
                    self.macro_saved = True  # Considered saved as it's loaded

                    self.playBtn.configure(
                        command=self.macro.start_playback
                    )  # Configure command
                    self.macro.start_playback()  # Automatically start playback
                    cli_playback_initiated = True

                except FileNotFoundError:
                    messagebox.showerror(
                        "Error", f"Macro file not found: {file_path_arg}"
                    )
                except json.JSONDecodeError:
                    messagebox.showerror(
                        "Error", f"Error decoding JSON from macro file: {file_path_arg}"
                    )
                except Exception as e:
                    messagebox.showerror(
                        "Error",
                        f"Failed to load or play macro: {e}\nFile: {file_path_arg}",
                    )
            elif not (
                file_path_arg.lower().endswith(".pmr")
                or file_path_arg.lower().endswith(".json")
            ):
                messagebox.showwarning(
                    "PyMacroRecord",
                    f"Invalid file type: {file_path_arg}\nPlease provide a .pmr or .json file.",
                )
            else:  # File does not exist
                messagebox.showwarning(
                    "PyMacroRecord", f"File not found: {file_path_arg}"
                )

        # Set default playBtn command if not handled by CLI playback
        if not cli_playback_initiated:
            self.playBtn.configure(
                command=self.macro.start_playback if self.macro_recorded else None
            )

        record_management = RecordFileManagement(self, self.menu)

        self.bind("<Control-Shift-S>", record_management.save_macro_as)
        self.bind("<Control-s>", record_management.save_macro)
        self.bind("<Control-l>", record_management.load_macro)
        self.bind("<Control-n>", record_management.new_macro)

        self.protocol("WM_DELETE_WINDOW", self.quit_software)
        if platform.lower() != "darwin":
            Thread(target=self.systemTray).start()

        self.attributes("-topmost", 0)

        if platform != "win32" and self.settings.first_time:
            NotWindows(self)

        if self.settings.get_config()["Others"]["Check_update"]:
            if (
                self.version.new_version != ""
                and self.version.version != self.version.new_version
            ):
                if time() > self.settings.get_config()["Others"]["Remind_new_ver_at"]:
                    NewVerAvailable(self, self.version.new_version)
        self.mainloop()

    def load_language(self):
        self.lang = self.settings.get_config()["Language"]
        # Make sure text_content is initialized even if file loading fails
        self.text_content = {}
        try:
            with open(
                resource_path(path.join("langs", self.lang + ".json")), encoding="utf-8"
            ) as f:
                loaded_lang_data = json.load(f)
            self.text_content = loaded_lang_data.get(
                "content", {}
            )  # Use .get for safety

            if self.lang != "en":
                with open(
                    resource_path(path.join("langs", "en.json")), encoding="utf-8"
                ) as f:
                    en_data = json.load(f)
                deepcopy_dict_missing_entries(
                    self.text_content, en_data.get("content", {})
                )
        except FileNotFoundError:
            # Fallback or error handling if a language file is missing
            # For now, text_content might remain empty or partially filled
            # Consider loading English as a default more robustly here
            if self.lang != "en":  # Attempt to load English if primary lang failed
                try:
                    with open(
                        resource_path(path.join("langs", "en.json")), encoding="utf-8"
                    ) as f:
                        en_data = json.load(f)
                    self.text_content = en_data.get("content", {})
                except Exception:
                    # If English also fails, text_content remains empty; app might have issues
                    pass  # Or raise an error / show messagebox
        except Exception:
            # Handle other potential errors during language loading
            pass

    def systemTray(self):
        """Just to show little icon on system tray"""
        try:
            image = Image.open(resource_path(path.join("assets", "logo.ico")))
            menu = (
                MenuItem("Show", action=self.deiconify, default=True),
                MenuItem(
                    "Quit", action=self.quit_software_from_tray
                ),  # Added quit option
            )
            self.icon = Icon("PyMacroRecord", image, "PyMacroRecord", menu)
            self.icon.run()
        except Exception:
            # Handle cases where icon cannot be created or run (e.g., headless environment)
            pass

    def quit_software_from_tray(self):
        """Safely quits the application from the system tray menu."""
        if hasattr(self, "icon") and self.icon:
            self.icon.stop()
        self.quit_software(
            force=True
        )  # Force quit to bypass save dialog if quitting from tray

    def validate_input(self, action, value_if_allowed):
        """Prevents from adding letters on an Entry label"""
        if action == "1":  # Insert
            try:
                float(value_if_allowed)
                return True
            except ValueError:
                return False
        return True

    def quit_software(self, force=False):
        if not force and not self.macro_saved and self.macro_recorded:
            wantToSave = confirm_save(self)  # confirm_save uses self.text_content
            if wantToSave:
                RecordFileManagement(self, self.menu).save_macro()
            elif wantToSave is None:  # User cancelled the save dialog
                return

        if platform.lower() != "darwin":
            if hasattr(self, "icon") and self.icon:
                self.icon.stop()

        # self.destroy() # This can cause issues if quit() is also called.
        # Tk.quit() is generally preferred for stopping the mainloop and allowing cleanup.
        self.quit()
        # sys.exit() # Can be used if immediate exit is needed after cleanup

cameronweibel avatar May 21 '25 08:05 cameronweibel

No I don't want a contribution fully did by AI, I'll probably do it soon by myself as I got time.

LOUDO56 avatar May 29 '25 12:05 LOUDO56