mirror of
https://github.com/kittywitch/konawall-py.git
synced 2026-02-09 12:29:19 -08:00
macOS changes
This commit is contained in:
parent
b2cf9c4870
commit
6cc517c163
5 changed files with 224 additions and 80 deletions
41
README.md
41
README.md
|
|
@ -1,3 +1,44 @@
|
||||||
# konawall-py
|
# konawall-py
|
||||||
|
|
||||||
A rewrite of konawall-rs in Python so that image manipulation is easier and cross-platform support is sort-of easier.
|
A rewrite of konawall-rs in Python so that image manipulation is easier and cross-platform support is sort-of easier.
|
||||||
|
|
||||||
|
## To-do
|
||||||
|
|
||||||
|
- [ ] Ensure that platforms all work with single and multiple-monitor setting:
|
||||||
|
- [x] macOS
|
||||||
|
- [x] Windows
|
||||||
|
- [ ] Linux
|
||||||
|
- Current progress: need to set up VM
|
||||||
|
- [ ] GNOME
|
||||||
|
- [ ] KDE
|
||||||
|
- [ ] XFCE
|
||||||
|
- [ ] Cinnamon
|
||||||
|
- [ ] MATE
|
||||||
|
- [ ] LXDE / LXQT
|
||||||
|
- [ ] Openbox
|
||||||
|
- [ ] i3
|
||||||
|
- [ ] sway
|
||||||
|
- [ ] Provide Nix packaging
|
||||||
|
- [ ] Provide installer packages for macOS and Windows, including on-startup functionality
|
||||||
|
- [ ] macOS
|
||||||
|
- [ ] Windows
|
||||||
|
- [ ] Sources
|
||||||
|
- [ ] Turn the current konachan source into a eneralized booru plugin
|
||||||
|
- [ ] Refactor, allow custom HTTP headers and a specified URL within config
|
||||||
|
- [ ] Test with e621
|
||||||
|
- [ ] Test with gelbooru
|
||||||
|
- [ ] Test with konachan
|
||||||
|
- [ ] Randomized sources and wallpapers mapping
|
||||||
|
- Implementation thoughts: To do this, there would need to be an intermediary between a source instance with a set of tags and another copy of that source instance with a set of tags as far as "presets" to be randomly selected from goes.
|
||||||
|
- [ ] Allow for a system in a different source is used to fetch a wallpaper or wallpaper(s) for each monitor
|
||||||
|
- [ ] Allow for a system in which a random source can be used to fetch a wallpaper or wallpaper(s) for each instantiation
|
||||||
|
- [ ] Allow multiple tag sets to be utilized even within one source, chosen at random
|
||||||
|
- [ ] Replace logging system with a local data store for the history
|
||||||
|
- Implementation thoughts: The data store ought to keep a note of the source and tags used to request it and any reasonable data that the API returned, e.g. tags, rating, other metadata...
|
||||||
|
- [ ] Replace current file downloading system
|
||||||
|
- [ ] Provide temporary directory as an option via config
|
||||||
|
- [ ] Provide a permanent directory as an option via config
|
||||||
|
- [ ] Provide a maximum number of wallpapers / history to keep via config
|
||||||
|
- [ ] Provide tooling for browsing the history
|
||||||
|
- [ ] Proper gallery grid UI, open in browser option, ...
|
||||||
|
- [ ] CLI tooling for searching the history
|
||||||
|
|
@ -10,4 +10,5 @@ This sets wallpapers on Darwin.
|
||||||
def set_wallpapers(files: list, displays: list):
|
def set_wallpapers(files: list, displays: list):
|
||||||
for i, file in enumerate(files):
|
for i, file in enumerate(files):
|
||||||
# Run osascript to set the wallpaper for each monitor
|
# Run osascript to set the wallpaper for each monitor
|
||||||
subprocess.run(["osascript", "-e", f'tell application "System Events" to set picture of desktop {i} file "{file}"'])
|
command = f'tell application "System Events" to set picture of desktop {i} to "{file}"'
|
||||||
|
subprocess.run(["osascript", "-e", command])
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import ctypes
|
import ctypes
|
||||||
import logging
|
import logging
|
||||||
import winreg
|
|
||||||
from imager import combine_to_viewport
|
from imager import combine_to_viewport
|
||||||
from module_loader import add_environment
|
from module_loader import add_environment
|
||||||
|
|
||||||
|
|
@ -20,6 +19,7 @@ This sets wallpapers on Windows.
|
||||||
"""
|
"""
|
||||||
@add_environment("win32_setter")
|
@add_environment("win32_setter")
|
||||||
def set_wallpapers(files: list, displays: list):
|
def set_wallpapers(files: list, displays: list):
|
||||||
|
import winreg
|
||||||
if len(files) > 1:
|
if len(files) > 1:
|
||||||
logging.debug("Several monitors detected, going the hard route")
|
logging.debug("Several monitors detected, going the hard route")
|
||||||
desktop = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, winreg.KEY_ALL_ACCESS)
|
desktop = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, winreg.KEY_ALL_ACCESS)
|
||||||
|
|
|
||||||
244
gui.py
244
gui.py
|
|
@ -11,70 +11,89 @@ import subprocess
|
||||||
from environment import set_environment_wallpapers, detect_environment
|
from environment import set_environment_wallpapers, detect_environment
|
||||||
from module_loader import import_dir, environment_handlers, source_handlers
|
from module_loader import import_dir, environment_handlers, source_handlers
|
||||||
from custom_print import kv_print
|
from custom_print import kv_print
|
||||||
|
from humanfriendly import format_timespan
|
||||||
|
|
||||||
def create_icon():
|
class Konawall(wx.adv.TaskBarIcon):
|
||||||
|
def __init__(self, file_logger):
|
||||||
|
# Prevents it from closing before it has done any work on macOS
|
||||||
|
if wx.Platform == "__WXMAC__":
|
||||||
|
self.hidden_frame = wx.Frame(None)
|
||||||
|
self.hidden_frame.Hide()
|
||||||
|
|
||||||
|
self.wallpaper_rotation_counter = 0
|
||||||
|
self.file_logger = file_logger
|
||||||
|
self.loaded_before = False
|
||||||
|
|
||||||
|
# Call the super function, make sure that the type is the statusitem for macOS
|
||||||
|
wx.adv.TaskBarIcon.__init__(self, wx.adv.TBI_CUSTOM_STATUSITEM)
|
||||||
|
|
||||||
|
# Detect environment and timer settings
|
||||||
|
self.environment = detect_environment()
|
||||||
|
self.toggle_wallpaper_rotation_item = None
|
||||||
|
self.wallpaper_rotation_timer = wx.Timer(self, wx.ID_ANY)
|
||||||
|
|
||||||
|
# Reload (actually load) the config and modules.
|
||||||
|
self.reload()
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# Start the timer to run every second
|
||||||
|
self.wallpaper_rotation_timer.Start(1000)
|
||||||
|
|
||||||
|
# Set up the taskbar icon, menu, bindings, ...
|
||||||
|
icon = self.generate_icon()
|
||||||
|
self.SetIcon(icon, "Konawall")
|
||||||
|
self.create_menu()
|
||||||
|
self.create_bindings()
|
||||||
|
|
||||||
|
# Run the first time, manually
|
||||||
|
self.run(None)
|
||||||
|
|
||||||
|
# wxPython requires a wx.Bitmap, so we generate one from a PIL.Image
|
||||||
|
def generate_icon(self):
|
||||||
width = 128
|
width = 128
|
||||||
height = 128
|
height = 128
|
||||||
# Missing texture
|
|
||||||
|
# Missing texture style, magenta and black checkerboard
|
||||||
image = Image.new('RGB', (width, height), (0, 0, 0))
|
image = Image.new('RGB', (width, height), (0, 0, 0))
|
||||||
dc = ImageDraw.Draw(image)
|
dc = ImageDraw.Draw(image)
|
||||||
dc.rectangle((0, 0, width//2, height//2), fill=(255, 0, 255))
|
dc.rectangle((0, 0, width//2, height//2), fill=(255, 0, 255))
|
||||||
dc.rectangle((width//2, height//2, width, height), fill=(255, 0, 255))
|
dc.rectangle((width//2, height//2, width, height), fill=(255, 0, 255))
|
||||||
|
if "wxMSW" in wx.PlatformInfo:
|
||||||
|
image = image.Scale(16, 16)
|
||||||
|
elif "wxGTK" in wx.PlatformInfo:
|
||||||
|
image = image.Scale(22, 22)
|
||||||
|
|
||||||
# Write image to temporary file
|
# Write image to temporary file
|
||||||
temp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
temp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
||||||
image.save(temp.name)
|
image.save(temp.name)
|
||||||
|
|
||||||
|
# Convert to wxPython icon
|
||||||
icon = wx.Icon()
|
icon = wx.Icon()
|
||||||
icon.CopyFromBitmap(wx.Bitmap(temp.name))
|
icon.CopyFromBitmap(wx.Bitmap(temp.name))
|
||||||
return icon
|
return icon
|
||||||
|
|
||||||
|
def toggle_wallpaper_rotation_status(self):
|
||||||
|
return f"{'Dis' if self.rotate else 'En'}able Timer"
|
||||||
|
|
||||||
def create_menu_item(menu, label, func):
|
# Load in our source and environment handlers
|
||||||
item = wx.MenuItem(menu, -1, label)
|
|
||||||
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
|
|
||||||
menu.Append(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
class Konawall(wx.adv.TaskBarIcon):
|
|
||||||
def __init__(self, file_logger):
|
|
||||||
self.file_logger = file_logger
|
|
||||||
#self.console_logger = console_logger
|
|
||||||
self.loaded = False
|
|
||||||
wx.adv.TaskBarIcon.__init__(self)
|
|
||||||
# Pre-setup initialization
|
|
||||||
self.environment = detect_environment()
|
|
||||||
self.automatic_item = None
|
|
||||||
self.automatic_timer = wx.Timer(self, wx.ID_ANY)
|
|
||||||
# Reload (actually load) the config
|
|
||||||
self.reload()
|
|
||||||
self.load_modules()
|
|
||||||
|
|
||||||
self.automatic_timer.Start(self.seconds * 1000)
|
|
||||||
# Set up the taskbar icon
|
|
||||||
self.SetIcon(create_icon(), "Konawall")
|
|
||||||
self.create_menu()
|
|
||||||
self.create_bindings()
|
|
||||||
|
|
||||||
def automatic_item_state(self):
|
|
||||||
if self.automatic:
|
|
||||||
return "Disable Automatic"
|
|
||||||
else:
|
|
||||||
return "Enable Automatic"
|
|
||||||
|
|
||||||
def load_modules(self):
|
def load_modules(self):
|
||||||
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "sources"))
|
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "sources"))
|
||||||
kv_print("Loaded source handlers", ", ".join(source_handlers), level="debug")
|
kv_print("Loaded source handlers", ", ".join(source_handlers), level="debug")
|
||||||
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "environments"))
|
import_dir(os.path.join(os.path.dirname(os.path.abspath( __file__ )), "environments"))
|
||||||
kv_print("Loaded environment handlers", ", ".join(environment_handlers), level="debug")
|
kv_print("Loaded environment handlers", ", ".join(environment_handlers), level="debug")
|
||||||
|
|
||||||
|
# Load a TOML file's key-value pairs into our class
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
# check if config file exists
|
|
||||||
if os.path.isfile("config.toml"):
|
if os.path.isfile("config.toml"):
|
||||||
|
# If the config file exists, load it as a dictionary into the config variable.
|
||||||
with open("config.toml", "rb") as f:
|
with open("config.toml", "rb") as f:
|
||||||
config = tomllib.load(f)
|
config = tomllib.load(f)
|
||||||
|
# for every key-value pair in the config variable , set the corresponding attribute of our class to it
|
||||||
for k, v in config.items():
|
for k, v in config.items():
|
||||||
kv_print(f"Loaded {k}", v)
|
kv_print(f"Loaded {k}", v)
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
else:
|
else:
|
||||||
|
# If there is no config file, get complainy.
|
||||||
dialog = wx.MessageDialog(
|
dialog = wx.MessageDialog(
|
||||||
None,
|
None,
|
||||||
"No config file found, using defaults.",
|
"No config file found, using defaults.",
|
||||||
|
|
@ -83,17 +102,70 @@ class Konawall(wx.adv.TaskBarIcon):
|
||||||
)
|
)
|
||||||
dialog.ShowModal()
|
dialog.ShowModal()
|
||||||
dialog.Destroy()
|
dialog.Destroy()
|
||||||
self.automatic = True
|
# Set some arbitrary defaults
|
||||||
self.seconds = 10*60
|
self.rotate = True
|
||||||
|
self.interval = 10*60
|
||||||
self.tags = ["rating:s"]
|
self.tags = ["rating:s"]
|
||||||
|
self.logging = {}
|
||||||
|
self.logging["file"] = "INFO"
|
||||||
|
|
||||||
|
# Create the menu
|
||||||
def create_menu(self):
|
def create_menu(self):
|
||||||
self.menu = wx.Menu()
|
# Make it easier to define menu items
|
||||||
create_menu_item(self.menu, "Run", self.run)
|
def create_menu_item(menu, label, func, help="", kind=wx.ITEM_NORMAL):
|
||||||
self.automatic_item = create_menu_item(self.menu, self.automatic_item_state(), self.toggle_automatic)
|
item = wx.MenuItem(menu, wx.ID_ANY, label)
|
||||||
create_menu_item(self.menu, "Edit Config", self.edit_config)
|
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
|
||||||
self.menu.Append(wx.ID_EXIT, "Exit")
|
menu.Append(item)
|
||||||
|
return item
|
||||||
|
def create_separator(menu):
|
||||||
|
item = wx.MenuItem(menu, id=wx.ID_SEPARATOR, kind=wx.ITEM_SEPARATOR)
|
||||||
|
menu.Append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
# Create our Menu object
|
||||||
|
self.menu = wx.Menu()
|
||||||
|
|
||||||
|
# Time remaining for automatic wallpaper rotation
|
||||||
|
self.wallpaper_rotation_status = wx.MenuItem(self.menu, -1, "Time remaining")
|
||||||
|
self.wallpaper_rotation_status.Enable(False)
|
||||||
|
self.menu.Append(self.wallpaper_rotation_status)
|
||||||
|
|
||||||
|
create_separator(self.menu)
|
||||||
|
|
||||||
|
# Change wallpapers
|
||||||
|
create_menu_item(
|
||||||
|
self.menu,
|
||||||
|
"Rotate Wallpapers",
|
||||||
|
self.run,
|
||||||
|
"Fetch new wallpapers and set them as your wallpapers"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Toggle automatic wallpaper rotation
|
||||||
|
self.toggle_wallpaper_rotation_item = create_menu_item(
|
||||||
|
self.menu,
|
||||||
|
self.toggle_wallpaper_rotation_status(),
|
||||||
|
self.toggle_wallpaper_rotation,
|
||||||
|
"Toggle the automatic wallpaper rotation timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
create_separator(self.menu)
|
||||||
|
|
||||||
|
# Interactive config editing
|
||||||
|
create_menu_item(
|
||||||
|
self.menu,
|
||||||
|
"Edit Config",
|
||||||
|
self.edit_config,
|
||||||
|
"Interactively edit the config file"
|
||||||
|
)
|
||||||
|
# Exit
|
||||||
|
create_menu_item(
|
||||||
|
self.menu,
|
||||||
|
"Exit",
|
||||||
|
self.Destroy,
|
||||||
|
"Quit the application"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Interactively edit the config file
|
||||||
def edit_config(self, event):
|
def edit_config(self, event):
|
||||||
kv_print("User is editing", "config.toml")
|
kv_print("User is editing", "config.toml")
|
||||||
# Check if we're on Windows, if so use Notepad
|
# Check if we're on Windows, if so use Notepad
|
||||||
|
|
@ -107,58 +179,82 @@ class Konawall(wx.adv.TaskBarIcon):
|
||||||
kv_print("User has edited", "config.toml")
|
kv_print("User has edited", "config.toml")
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
|
# Reload the application
|
||||||
def reload(self):
|
def reload(self):
|
||||||
if self.loaded:
|
kv_print(f"{'Rel' if self.loaded_before else 'L'}oading config from", "config.toml")
|
||||||
kv_print("Reloading config from", "config.toml")
|
|
||||||
else:
|
|
||||||
kv_print("Loading config from", "config.toml")
|
|
||||||
self.read_config()
|
self.read_config()
|
||||||
|
|
||||||
# Handle finding the log level
|
# Handle finding the log level
|
||||||
if "file" in self.logging:
|
if "file" in self.logging:
|
||||||
file_log_level = getattr(logging, self.logging["file"])
|
file_log_level = getattr(logging, self.logging["file"])
|
||||||
else:
|
else:
|
||||||
file_log_level = logging.INFO
|
file_log_level = logging.INFO
|
||||||
self.file_logger.setLevel(file_log_level)
|
self.file_logger.setLevel(file_log_level)
|
||||||
#if "console" in self.logging:
|
|
||||||
# console_log_level = getattr(logging, self.logging["console"])
|
if self.loaded_before == True:
|
||||||
#else:
|
# If we're reloading, we need to make sure the timer and menu item reflect our current state.
|
||||||
# console_log_level = logging.INFO
|
self.respect_wallpaper_rotation_toggle()
|
||||||
#self.console_logger.setLevel(console_log_level)
|
|
||||||
# Finished loading
|
# Finished loading
|
||||||
self.loaded = True
|
self.loaded_before = True
|
||||||
|
|
||||||
# Handle the automatic timer
|
|
||||||
if self.automatic and self.automatic_timer.IsRunning():
|
|
||||||
self.automatic_timer.Stop()
|
|
||||||
self.automatic_timer.Start(self.seconds * 1000)
|
|
||||||
self.menu.SetLabel(self.automatic_item.Id, self.automatic_item_state())
|
|
||||||
elif not self.automatic and self.automatic_timer.IsRunning():
|
|
||||||
self.automatic_timer.Stop()
|
|
||||||
self.menu.SetLabel(self.automatic_item.Id, self.automatic_item_state())
|
|
||||||
|
|
||||||
def toggle_automatic(self, event):
|
# Set whether to rotate wallpapers automatically or not
|
||||||
self.automatic = not self.automatic
|
def toggle_wallpaper_rotation(self, event):
|
||||||
if self.automatic:
|
self.rotate = not self.rotate
|
||||||
self.automatic_timer.Start(self.seconds * 1000)
|
self.respect_wallpaper_rotation_toggle()
|
||||||
else:
|
|
||||||
self.automatic_timer.Stop()
|
|
||||||
self.menu.SetLabel(self.automatic_item.Id, self.automatic_item_state())
|
|
||||||
|
|
||||||
def show_menu(self, event):
|
# Update the timer and the menu item to reflect our current state
|
||||||
self.PopupMenu(self.menu)
|
def respect_wallpaper_rotation_toggle(self):
|
||||||
|
if self.rotate and not self.wallpaper_rotation_timer.IsRunning():
|
||||||
|
self.wallpaper_rotation_timer.Start(1000)
|
||||||
|
elif not self.rotate and self.wallpaper_rotation_timer.IsRunning():
|
||||||
|
self.wallpaper_rotation_timer.Stop()
|
||||||
|
# Set the time left counter to show that it is disabled
|
||||||
|
self.wallpaper_rotation_status.SetItemLabel(f"Automatic wallpaper rotation disabled")
|
||||||
|
|
||||||
|
# Update the menu item for the toggle
|
||||||
|
self.toggle_wallpaper_rotation_item.SetItemLabel(self.toggle_wallpaper_rotation_status())
|
||||||
|
|
||||||
|
# Update wallpaper rotation time left counter
|
||||||
|
def respect_wallpaper_rotation_status(self):
|
||||||
|
self.wallpaper_rotation_status.SetItemLabel(f"{format_timespan(self.interval - self.wallpaper_rotation_counter)} remaining")
|
||||||
|
|
||||||
|
# Perform the purpose of the application; get new wallpaper media and set 'em.
|
||||||
def run(self, event):
|
def run(self, event):
|
||||||
displays = screeninfo.get_monitors()
|
displays = screeninfo.get_monitors()
|
||||||
count = len(displays)
|
count = len(displays)
|
||||||
files = source_handlers[self.source](count, self.tags)
|
files = source_handlers[self.source](count, self.tags)
|
||||||
set_environment_wallpapers(self.environment, files, displays)
|
set_environment_wallpapers(self.environment, files, displays)
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
def CreatePopupMenu(self):
|
||||||
|
self.PopupMenu(self.menu)
|
||||||
|
|
||||||
|
# For everybody else who has bindable events
|
||||||
|
def show_menu(self, event):
|
||||||
|
self.PopupMenu(self.menu)
|
||||||
|
|
||||||
|
# Every second, check if the wallpaper rotation timer has ticked over
|
||||||
|
def handle_timer_tick(self, event):
|
||||||
|
if self.wallpaper_rotation_counter >= self.interval:
|
||||||
|
# If it has, run the fetch and set mechanism
|
||||||
|
self.run(None)
|
||||||
|
self.wallpaper_rotation_counter = 0
|
||||||
|
else:
|
||||||
|
self.wallpaper_rotation_counter += 1
|
||||||
|
# Update the time left counter
|
||||||
|
self.respect_wallpaper_rotation_status()
|
||||||
|
|
||||||
|
# Bind application events
|
||||||
def create_bindings(self):
|
def create_bindings(self):
|
||||||
self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.run)
|
self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.run)
|
||||||
self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.show_menu)
|
self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.show_menu)
|
||||||
self.Bind(wx.EVT_TIMER, self.run, self.automatic_timer)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# Implement the wallpaper rotation timer
|
||||||
|
self.Bind(wx.EVT_TIMER, self.handle_timer_tick, self.wallpaper_rotation_timer)
|
||||||
|
|
||||||
|
def main():
|
||||||
file_logger = logging.FileHandler("app.log", mode="a")
|
file_logger = logging.FileHandler("app.log", mode="a")
|
||||||
#console_logger = logging.StreamHandler()
|
#console_logger = logging.StreamHandler()
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -169,6 +265,10 @@ if __name__ == "__main__":
|
||||||
file_logger,
|
file_logger,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
app = wx.App(False)
|
app = wx.App(redirect=False)
|
||||||
|
app.SetExitOnFrameDelete(False)
|
||||||
Konawall(file_logger)
|
Konawall(file_logger)
|
||||||
app.MainLoop()
|
app.MainLoop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -2,3 +2,5 @@ Pillow
|
||||||
screeninfo
|
screeninfo
|
||||||
requests
|
requests
|
||||||
termcolor
|
termcolor
|
||||||
|
wxpython
|
||||||
|
humanfriendly
|
||||||
Loading…
Add table
Add a link
Reference in a new issue