Script to create Timers - Update 1

I made a little script in python to create timers, it's not perfect but it does the job, I share it with you :

The script:

import gi
import subprocess
import os
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Pango

class ScriptTimer:
    def __init__(self):
        self.window = Gtk.Window(title="Script Timer")
        self.window.set_default_size(1200, 340)  # Adjusted window size
        self.window.connect("destroy", Gtk.main_quit)

        # Paned to divide the window into two parts
        self.paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        self.window.add(self.paned)

        # Initial position of the separator to widen the log window
        self.paned.set_position(420)

        # Vertical box for main controls
        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.paned.add1(self.vbox)

        # Horizontal box for script path and selection button
        self.script_path_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        self.vbox.pack_start(self.script_path_hbox, False, False, 0)

        self.script_path_label = Gtk.Label(label="Script Path:")
        self.script_path_hbox.pack_start(self.script_path_label, False, False, 5)

        self.script_path_entry = Gtk.Entry()
        self.script_path_entry.connect("changed", self.on_script_path_changed)
        self.script_path_hbox.pack_start(self.script_path_entry, True, True, 5)

        self.script_path_button = Gtk.Button(label="Browse...")
        self.script_path_button.connect("clicked", self.on_script_path_button_clicked)
        self.script_path_hbox.pack_start(self.script_path_button, False, False, 5)

        # Button to test the script
        self.test_script_button = Gtk.Button(label="Test Script")
        self.test_script_button.connect("clicked", self.run_bash_script)
        self.vbox.pack_start(self.test_script_button, False, False, 5)

        # Grid for hour checkboxes
        self.grid = Gtk.Grid(column_spacing=2, row_spacing=0, margin=0)
        self.vbox.pack_start(self.grid, True, True, 0)

        self.time_checkboxes = {}
        row = 0
        col = 0
        for hour in range(0, 24):
            checkbox = Gtk.CheckButton(label=f"{hour:02d}:00")
            self.time_checkboxes[hour] = checkbox
            self.grid.attach(checkbox, col, row, 1, 1)
            col += 1
            if col == 6:  # 6 columns per row
                col = 0
                row += 1

        # Button to create the service and timer
        self.create_service_button = Gtk.Button(label="Create Service and Timer")
        self.create_service_button.connect("clicked", self.create_service_and_timer)
        self.vbox.pack_start(self.create_service_button, False, False, 2)

        # Button to choose the icon
        self.choose_icon_button = Gtk.Button(label="Choose Icon")
        self.choose_icon_button.connect("clicked", self.on_choose_icon_button_clicked)
        self.vbox.pack_start(self.choose_icon_button, False, False, 2)

        # Button to create the .desktop file
        self.create_desktop_button = Gtk.Button(label="Create .desktop File")
        self.create_desktop_button.connect("clicked", self.create_desktop_file)
        self.vbox.pack_start(self.create_desktop_button, False, False, 2)

        # Button to delete the .desktop file
        self.delete_desktop_button = Gtk.Button(label="Delete .desktop File")
        self.delete_desktop_button.connect("clicked", self.delete_desktop_file)
        self.vbox.pack_start(self.delete_desktop_button, False, False, 2)

        self.activate_timer_button = Gtk.Button(label="Activate Timer")
        self.activate_timer_button.connect("clicked", self.activate_timer)
        self.vbox.pack_start(self.activate_timer_button, False, False, 2)

        self.deactivate_timer_button = Gtk.Button(label="Deactivate Timer")
        self.deactivate_timer_button.connect("clicked", self.deactivate_timer)
        self.vbox.pack_start(self.deactivate_timer_button, False, False, 2)

        self.delete_service_button = Gtk.Button(label="Delete Timer and Service")
        self.delete_service_button.connect("clicked", self.delete_service_and_timer)
        self.vbox.pack_start(self.delete_service_button, False, False, 2)

        self.show_timer_status_button = Gtk.Button(label="Show Timer Status")
        self.show_timer_status_button.connect("clicked", self.show_timer_status)
        self.vbox.pack_start(self.show_timer_status_button, False, False, 2)

        self.list_timers_button = Gtk.Button(label="List User Timers")
        self.list_timers_button.connect("clicked", self.list_user_timers)
        self.vbox.pack_start(self.list_timers_button, False, False, 2)

        # Terminal window for logs
        self.log_view = Gtk.TextView()
        self.log_view.set_editable(False)
        self.log_view.set_cursor_visible(False)
        self.log_view.set_wrap_mode(Gtk.WrapMode.WORD)
        self.log_buffer = self.log_view.get_buffer()

        # ScrolledWindow to allow scrolling
        self.scrolled_window_for_log = Gtk.ScrolledWindow()
        self.scrolled_window_for_log.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.scrolled_window_for_log.add(self.log_view)

        # Add the ScrolledWindow to the paned
        self.paned.add2(self.scrolled_window_for_log)

        self.timeout_id = None
        self.timer_active = False
        self.timer_name = ""
        self.service_name = ""
        self.icon_path = ""  # Variable to store the chosen icon path

        self.window.show_all()
        self.update_buttons()

    def log(self, message):
        # Add a message to the log
        end_iter = self.log_buffer.get_end_iter()
        self.log_buffer.insert(end_iter, message + "\n")
        # Automatically scroll to the bottom
        self.log_view.scroll_to_iter(end_iter, 0.0, False, 0.0, 0.0)

    def run_bash_script(self, widget=None):
        try:
            bash_script_path = self.script_path_entry.get_text()
            subprocess.run(["bash", bash_script_path], check=True)
            self.log("Script executed successfully.")
        except subprocess.CalledProcessError as e:
            self.log(f"Failed to execute Bash script: {e}")

    def create_service_and_timer(self, widget):
        script_path = self.script_path_entry.get_text()
        selected_hours = [hour for hour, checkbox in self.time_checkboxes.items() if checkbox.get_active()]

        if not selected_hours:
            self.log("Please select at least one hour.")
            return

        # Extract the script name without path or extension
        script_name = os.path.splitext(os.path.basename(script_path))[0]
        self.timer_name = f"{script_name}.timer"
        self.service_name = f"{script_name}.service"

        # Directory for user systemd units
        systemd_user_dir = os.path.expanduser("~/.config/systemd/user/")

        # Create the directory if it doesn't exist
        os.makedirs(systemd_user_dir, exist_ok=True)

        # Content of the .service file
        service_content = f"""
        [Unit]
        Description={script_name}
        Wants=network-online.target graphical-session.target
        After=network-online.target graphical-session.target

        [Service]
        Type=simple
        ExecStart={script_path}
        """

        # Content of the .timer file
        timer_content = f"""
        [Unit]
        Description={script_name} Timer

        [Timer]
        OnCalendar=*-*-* {','.join(f'{h}' for h in selected_hours)}:00:00
        Persistent=true
        Unit={self.service_name}

        [Install]
        WantedBy=default.target
        """

        # Write the .service file
        service_path = os.path.join(systemd_user_dir, self.service_name)
        with open(service_path, "w") as service_file:
            service_file.write(service_content)

        # Write the .timer file
        timer_path = os.path.join(systemd_user_dir, self.timer_name)
        with open(timer_path, "w") as timer_file:
            timer_file.write(timer_content)

        self.log(f"Service created: {service_path}")
        self.log(f"Timer created: {timer_path}")
        self.log("To activate the timer, use the following commands:")
        self.log(f"systemctl --user enable {self.timer_name}")
        self.log(f"systemctl --user start {self.timer_name}")

        self.update_buttons()

    def create_desktop_file(self, widget):
        script_path = self.script_path_entry.get_text()
        script_name = os.path.splitext(os.path.basename(script_path))[0]

        # Directory for .desktop files
        desktop_dir = os.path.expanduser("~/.local/share/applications/")
        os.makedirs(desktop_dir, exist_ok=True)

        # Check if the .desktop file already exists
        desktop_path = os.path.join(desktop_dir, f"{script_name}.desktop")
        if os.path.exists(desktop_path):
            self.log(f"Existing .desktop file: {desktop_path}")
            self.create_desktop_button.set_sensitive(False)
            return

        # Check if the service and timer exist
        systemd_user_dir = os.path.expanduser("~/.config/systemd/user/")
        service_path = os.path.join(systemd_user_dir, f"{script_name}.service")
        timer_path = os.path.join(systemd_user_dir, f"{script_name}.timer")

        if not (os.path.exists(service_path) and os.path.exists(timer_path)):
            self.log("Service or timer missing. Please create them first.")
            return

        # Check if an icon has been chosen
        if not self.icon_path:
            self.log("Please choose an icon.")
            return

        # Content of the .desktop file
        desktop_content = f"""
        [Desktop Entry]
        Name={script_name.capitalize()}
        Comment=Fetch a new background image from {script_name}
        Keywords=background;wallpaper;
        Exec=systemctl start --user {script_name}.service
        Icon={self.icon_path}
        Terminal=false
        Type=Application
        StartupNotify=false
        X-GNOME-UsesNotifications=true
        """

        # Write the .desktop file
        with open(desktop_path, "w") as desktop_file:
            desktop_file.write(desktop_content)

        self.log(f".desktop file created: {desktop_path}")

    def delete_desktop_file(self, widget):
        script_path = self.script_path_entry.get_text()
        script_name = os.path.splitext(os.path.basename(script_path))[0]

        # Directory for .desktop files
        desktop_dir = os.path.expanduser("~/.local/share/applications/")
        desktop_path = os.path.join(desktop_dir, f"{script_name}.desktop")

        try:
            os.remove(desktop_path)
            self.log(f".desktop file deleted: {desktop_path}")
        except FileNotFoundError:
            self.log(f".desktop file not found: {desktop_path}")
        except Exception as e:
            self.log(f"Failed to delete .desktop file: {e}")

    def activate_timer(self, widget):
        try:
            subprocess.run(["systemctl", "--user", "enable", self.timer_name], check=True)
            subprocess.run(["systemctl", "--user", "start", self.timer_name], check=True)
            self.log("Timer activated and started.")
        except subprocess.CalledProcessError as e:
            self.log(f"Failed to activate timer: {e}")
        finally:
            self.update_buttons()

    def deactivate_timer(self, widget):
        try:
            subprocess.run(["systemctl", "--user", "disable", self.timer_name], check=True)
            subprocess.run(["systemctl", "--user", "stop", self.timer_name], check=True)
            self.log("Timer deactivated and stopped.")
        except subprocess.CalledProcessError as e:
            self.log(f"Failed to deactivate timer: {e}")
        finally:
            self.update_buttons()

    def delete_service_and_timer(self, widget):
        systemd_user_dir = os.path.expanduser("~/.config/systemd/user/")
        service_path = os.path.join(systemd_user_dir, self.service_name)
        timer_path = os.path.join(systemd_user_dir, self.timer_name)

        try:
            os.remove(service_path)
            os.remove(timer_path)
            subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
            self.log(f"Service and timer deleted: {service_path}, {timer_path}")
            self.update_buttons()
        except Exception as e:
            self.log(f"Failed to delete service and timer: {e}")

    def show_timer_status(self, widget):
        script_path = self.script_path_entry.get_text()
        script_name = os.path.splitext(os.path.basename(script_path))[0]
        timer_name = f"{script_name}.timer"
        try:
            result = subprocess.run(["systemctl", "--user", "status", timer_name], capture_output=True, text=True, check=True)
            self.log("Timer status:")
            self.log(result.stdout)
        except subprocess.CalledProcessError as e:
            self.log(f"Failed to retrieve timer status: {e}")

    def on_script_path_button_clicked(self, widget):
        dialog = Gtk.FileChooserDialog(
            title="Select a Script",
            parent=self.window,
            action=Gtk.FileChooserAction.OPEN
        )
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)

        filter_bash = Gtk.FileFilter()
        filter_bash.set_name("Bash scripts")
        filter_bash.add_pattern("*.sh")
        dialog.add_filter(filter_bash)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            self.script_path_entry.set_text(dialog.get_filename())
        dialog.destroy()
        self.update_buttons()

    def on_choose_icon_button_clicked(self, widget):
        dialog = Gtk.FileChooserDialog(
            title="Choose an Icon",
            parent=self.window,
            action=Gtk.FileChooserAction.OPEN
        )
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)

        filter_images = Gtk.FileFilter()
        filter_images.set_name("Images")
        filter_images.add_mime_type("image/*")
        dialog.add_filter(filter_images)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            self.icon_path = dialog.get_filename()
            self.log(f"Icon chosen: {self.icon_path}")
        dialog.destroy()
        self.update_buttons()

    def on_script_path_changed(self, widget):
        self.update_buttons()

    def update_buttons(self):
        script_path = self.script_path_entry.get_text()
        if script_path:
            script_name = os.path.splitext(os.path.basename(script_path))[0]
            self.timer_name = f"{script_name}.timer"
            self.service_name = f"{script_name}.service"

            # Check if the service and timer already exist
            systemd_user_dir = os.path.expanduser("~/.config/systemd/user/")
            service_path = os.path.join(systemd_user_dir, self.service_name)
            timer_path = os.path.join(systemd_user_dir, self.timer_name)

            service_exists = os.path.exists(service_path)
            timer_exists = os.path.exists(timer_path)

            # Check if the .desktop file already exists
            desktop_dir = os.path.expanduser("~/.local/share/applications/")
            desktop_path = os.path.join(desktop_dir, f"{script_name}.desktop")
            desktop_exists = os.path.exists(desktop_path)

            # Check the timer status
            try:
                result = subprocess.run(["systemctl", "--user", "is-active", self.timer_name], capture_output=True, text=True, check=True)
                timer_active = result.stdout.strip() == "active"
            except subprocess.CalledProcessError:
                timer_active = False

            self.create_service_button.set_sensitive(not (service_exists and timer_exists))
            self.create_desktop_button.set_sensitive(service_exists and timer_exists and not desktop_exists and self.icon_path)
            self.activate_timer_button.set_sensitive(timer_exists and not timer_active)
            self.deactivate_timer_button.set_sensitive(timer_exists and timer_active)
            self.show_timer_status_button.set_sensitive(timer_exists)

    def list_user_timers(self, widget):
        try:
            result = subprocess.run(["systemctl", "--user", "list-timers"], capture_output=True, text=True, check=True)
            self.log("User timers:")
            self.log(result.stdout)
        except subprocess.CalledProcessError as e:
            self.log(f"Failed to retrieve user timers: {e}")

if __name__ == "__main__":
    app = ScriptTimer()
    Gtk.main()


2 Likes

Changes Made:

  1. Added a button choose_icon_button to select the icon.
  2. Added a variable self.icon_path to store the chosen icon path.
  3. Modified the update_buttons method to disable the "Create .desktop File" button if no icon has been chosen.
  4. Added the on_choose_icon_button_clicked method to handle icon selection.

With these changes, the "Create .desktop File" button will be disabled until an icon is chosen.

2 Likes

Maybe you should start a repository over on Codeberg, GitLab or GitHub?

Might help to keep track of changes made, and share the code. Good job, btw, thanks for sharing :+1:

1 Like

Yeah I've thought about it but I'm too lazy for that :wink:

1 Like