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()