Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature windows support (WIP) #292

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion bin/variety
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import os
import sys

if os.geteuid() == 0:
if os.name != 'nt' and os.geteuid() == 0:
print(
'Variety is not supposed to run as root.\n'
'You should NEVER run desktop apps as root, unless they are supposed to make '
Expand Down
2 changes: 1 addition & 1 deletion data/config/variety.conf
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ clock_date_font = "Sans 30"
# A tutorial on "annotating" with ImageMagick that you may use as a reference: http://www.imagemagick.org/Usage/annotating/
# You can get a very uniquely looking clock with some of the more advanced techniques (e.g. circle-shaped text, interesting colors and shading, etc....).

clock_filter = "-density 100 -font `fc-match -f '%{file[0]}' '%CLOCK_FONT_NAME'` -pointsize %CLOCK_FONT_SIZE -gravity SouthEast -fill '#00000044' -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+108] '%H:%M' -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+110] '%H:%M' -font `fc-match -f '%{file[0]}' '%DATE_FONT_NAME'` -pointsize %DATE_FONT_SIZE -fill '#00000044' -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+58] '%A, %B %d' -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+60] '%A, %B %d'"
clock_filter = '-density 100 -font %CLOCK_FONT_FILE -pointsize %CLOCK_FONT_SIZE -gravity SouthEast -fill "#00000044" -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+108] "%H:%M" -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+110] "%H:%M" -font %DATE_FONT_FILE -pointsize %DATE_FONT_SIZE -fill "#00000044" -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+58] "%A, %B %d" -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+60] "%A, %B %d"'

# Quotes settings
# quotes_enabled = <True or False>
Expand Down
70 changes: 66 additions & 4 deletions variety/Util.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import random
import re
import shutil
import shlex
import string
import subprocess
import sys
Expand Down Expand Up @@ -739,8 +740,12 @@ def same_file_paths(f1, f2):

@staticmethod
def collapseuser(path):
home = os.path.expanduser("~") + "/"
return re.sub("^" + home, "~/", path)
# normpath used to avoid backslash mixing on windows
home = os.path.normpath(os.path.expanduser('~')) + os.sep
if os.name == 'nt':
return path
else:
return re.sub('^' + home, '~' + os.sep, path)

@staticmethod
def compare_versions(v1, v2):
Expand Down Expand Up @@ -774,6 +779,8 @@ def random_hash():

@staticmethod
def get_file_icon_name(path):
if os.name == 'nt':
return "folder"
try:
f = Gio.File.new_for_path(os.path.normpath(os.path.expanduser(path)))
query_info = f.query_info("standard::icon", Gio.FileQueryInfoFlags.NONE, None)
Expand Down Expand Up @@ -896,12 +903,67 @@ def copy_with_replace(from_path, to_path, search_replace_map):
with open(to_path + ".partial", "w") as file:
file.write(data)
file.flush()
os.rename(to_path + ".partial", to_path)
shutil.move(to_path + ".partial", to_path)

@staticmethod
def get_exec_path():
return os.path.abspath(sys.argv[0])

@staticmethod
def set_windows_registry_key(path, name, value, dtype=None, master='HKEY_CURRENT_USER'):
import winreg

# Type is only used if creating a new value
if not dtype or isinstance(value, str):
dtype = winreg.REG_SZ
elif isinstance(value, int):
dtype = winreg.REG_DWORD
else:
dtype = winreg.REG_SZ
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can probably just collapse this into one check for isinstance(value, int)?


# Translate key name to winreg key name
registry = winreg.HKEY_CURRENT_USER
if master == 'HKEY_CURRENT_USER':
registry = winreg.HKEY_CURRENT_USER
elif master == 'HKEY_LOCAL_MACHINE':
registry = winreg.HKEY_LOCAL_MACHINE
elif master == 'HKEY_USERS':
registry = winreg.HKEY_USERS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no hives other than HKEY_CURRENT_USER are used we can probably remove those cases. AFAICT HKEY_LOCAL_MACHINE shouldn't be writable from an unprivileged account anyways.


# This will throw FileNotFoundError if key does not exist - this is fatal for this function
key = winreg.OpenKey(
registry,
path,
0,
winreg.KEY_WRITE | winreg.KEY_READ
)

# Try getting the explicit value, catch if doesn't exist
try:
val, val_type = winreg.QueryValueEx(key, "WallpaperStyle")
# If the value is correct, ignore, otherwise set
if val == value:
pass
else:
winreg.SetValueEx(key, name, 0, val_type, value)
except FileNotFoundError:
# Couldn't find key, try creating
try:
winreg.SetValueEx(key, name, 0, dtype, value)
except Exception as e:
# Log failure here
pass
finally:
winreg.CloseKey(key)

@staticmethod
def shlex_quote(s):
if os.name != 'nt':
return shlex.quote(s)
else:
# shlex.quote does not play nicely with windows shell, so just always-quote
return '"%s"' % s

@staticmethod
def get_folder_size(start_path):
total_size = 0
Expand All @@ -916,7 +978,6 @@ def get_folder_size(start_path):
def get_screen_width():
return Gdk.Screen.get_default().get_width()


def on_gtk(f):
@functools.wraps(f)
def wrapped(*args):
Expand All @@ -925,6 +986,7 @@ def wrapped(*args):
return wrapped



def safe_print(text, ascii_text=None, file=sys.stdout):
"""
Python's print throws UnicodeEncodeError if the terminal encoding is borked. This version tries print, then logging, then printing the ascii text when one is present.
Expand Down
72 changes: 61 additions & 11 deletions variety/VarietyWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,14 +1264,14 @@ def build_imagemagick_filter_cmd(self, filename, target_file):

w = Gdk.Screen.get_default().get_width()
h = Gdk.Screen.get_default().get_height()
cmd = "convert %s -scale %dx%d^ " % (shlex.quote(filename), w, h)
cmd = "convert %s -scale %dx%d^ " % (Util.shlex_quote(filename), w, h)

logger.info(lambda: "Applying filter: " + filter)
cmd += filter + " "

cmd += shlex.quote(target_file)
cmd = cmd.replace("%FILEPATH%", shlex.quote(filename))
cmd = cmd.replace("%FILENAME%", shlex.quote(os.path.basename(filename)))
cmd += Util.shlex_quote(target_file)
cmd = cmd.replace("%FILEPATH%", Util.shlex_quote(filename))
cmd = cmd.replace("%FILENAME%", Util.shlex_quote(os.path.basename(filename)))

logger.info(lambda: "ImageMagick filter cmd: " + cmd)
return cmd.encode("utf-8")
Expand All @@ -1282,7 +1282,7 @@ def build_imagemagick_clock_cmd(self, filename, target_file):

w = Gdk.Screen.get_default().get_width()
h = Gdk.Screen.get_default().get_height()
cmd = "convert %s -scale %dx%d^ " % (shlex.quote(filename), w, h)
cmd = "convert %s -scale %dx%d^ " % (Util.shlex_quote(filename), w, h)

hoffset, voffset = Util.compute_trimmed_offsets(Util.get_size(filename), (w, h))
clock_filter = self.options.clock_filter
Expand All @@ -1296,7 +1296,7 @@ def build_imagemagick_clock_cmd(self, filename, target_file):

cmd += clock_filter
cmd += " "
cmd += shlex.quote(target_file)
cmd += Util.shlex_quote(target_file)
logger.info(lambda: "ImageMagick clock cmd: " + cmd)
return cmd.encode("utf-8")

Expand All @@ -1307,6 +1307,15 @@ def replace_clock_filter_fonts(self, clock_filter):
clock_filter = clock_filter.replace("%CLOCK_FONT_SIZE", clock_font_size)
clock_filter = clock_filter.replace("%DATE_FONT_NAME", date_font_name)
clock_filter = clock_filter.replace("%DATE_FONT_SIZE", date_font_size)
if os.name != 'nt':
clock_filter = clock_filter.replace("%CLOCK_FONT_FILE", "`fc-match -f \"%%{file[0]}\" \"%CLOCK_FONT_NAME\"`")
clock_filter = clock_filter.replace("%DATE_FONT_FILE", "`fc-match -f \"%%{file[0]}\" \"%DATE_FONT_NAME\"`")
else:
clock_font_file = subprocess.check_output(["fc-match.exe", "-f", "%{file[0]}", clock_font_name])
date_font_file = subprocess.check_output(["fc-match.exe", "-f", "%{file[0]}", date_font_name])
clock_filter = clock_filter.replace("%CLOCK_FONT_FILE", clock_font_file.decode())
clock_filter = clock_filter.replace("%DATE_FONT_FILE", date_font_file.decode())

return clock_filter

@staticmethod
Expand Down Expand Up @@ -1362,6 +1371,11 @@ def apply_filters(self, to_set, refresh_level):
)
cmd = self.build_imagemagick_filter_cmd(to_set, target_file)
if cmd:
if os.name == 'nt':
cmd = cmd.decode()
# Quick fix - brackets explicitly stop imagemagick from being parsed correctly on windows
cmd = cmd.replace("\(", "(")
cmd = cmd.replace("\)", ")")
result = os.system(cmd)
if result == 0: # success
to_set = target_file
Expand Down Expand Up @@ -1404,6 +1418,8 @@ def apply_clock(self, to_set):
self.wallpaper_folder, "wallpaper-clock-%s.jpg" % Util.random_hash()
)
cmd = self.build_imagemagick_clock_cmd(to_set, target_file)
if os.name == 'nt':
cmd = cmd.decode()
result = os.system(cmd)
if result == 0: # success
to_set = target_file
Expand Down Expand Up @@ -1747,6 +1763,7 @@ def on_rating_changed(self, file):

def image_ok(self, img, fuzziness):
try:
img = os.path.normpath(img)
if Util.is_animated_gif(img):
return False

Expand All @@ -1760,7 +1777,11 @@ def image_ok(self, img, fuzziness):
width = self.image_colors_cache[img][3]
height = self.image_colors_cache[img][4]
else:
i = PILImage.open(img)
try:
i = PILImage.open(img)
except Exception as e:
logger.debug("Could not PILImage.open image %s: moving on", img, exc_info=True)
raise
width = i.size[0]
height = i.size[1]

Expand Down Expand Up @@ -1837,7 +1858,10 @@ def open_file(self, widget=None, file=None):
if not file:
file = self.current
if file:
subprocess.Popen(["xdg-open", os.path.realpath(file)])
if os.name == 'nt':
subprocess.Popen(["explorer", os.path.realpath(file)])
else:
subprocess.Popen(["xdg-open", os.path.realpath(file)])

def on_show_origin(self, widget=None):
if self.url:
Expand Down Expand Up @@ -2146,6 +2170,10 @@ def on_quit(self, widget=None):
)

Util.start_force_exit_thread(15)

# On windows we need to destroy the indicator otherwise it won't disappear until hovered over
self.ind.destroy_indicator()

logger.debug(lambda: "OK, waiting for other loops to finish")
logger.debug(lambda: "Remaining threads: ")
for t in threading.enumerate():
Expand Down Expand Up @@ -2615,7 +2643,14 @@ def get_desktop_wallpaper(self):

file = None

if os.access(script, os.X_OK):
if os.name == 'nt':
import ctypes
# https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-systemparametersinfow#SPI_GETDESKWALLPAPER
SPI_GETDESKWALLPAPER = 0x0073
wallpaper = ctypes.create_unicode_buffer(200)
ctypes.windll.user32.SystemParametersInfoW(SPI_GETDESKWALLPAPER, 200, wallpaper, 0)
file = wallpaper.value
elif os.access(script, os.X_OK):
logger.debug(lambda: "Running get_wallpaper script")
try:
output = subprocess.check_output(script).decode().strip()
Expand Down Expand Up @@ -2662,15 +2697,28 @@ def cleanup_old_wallpapers(self, folder, prefix, new_wallpaper=None):

def set_desktop_wallpaper(self, wallpaper, original_file, refresh_level):
script = os.path.join(self.scripts_folder, "set_wallpaper")
if os.access(script, os.X_OK):
if os.name == 'nt':
import ctypes
# Windows code for setting a wallpaper
WINDOWS_WALLPAPER_STYLE_STRETCH = "2"
try:
Util.set_windows_registry_key(r"Control Panel\\Desktop", "WallpaperStyle", WINDOWS_WALLPAPER_STYLE_STRETCH)
Util.set_windows_registry_key(r"Control Panel\\Desktop", "WallPaper", wallpaper)
except FileNotFoundError as e:
logger.exception(lambda: "Exception when calling execute_set_desktop_wallpaper: %s" % str(e))

# https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-systemparametersinfow#SPI_SETDESKWALLPAPER
SPI_SETDESKWALLPAPER = 0x0014
ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, wallpaper , 0)
elif os.access(script, os.X_OK):
auto = (
"manual"
if not self.auto_changed
else ("auto" if refresh_level == VarietyWindow.RefreshLevel.ALL else "refresh")
)
logger.debug(
lambda: "Running set_wallpaper script with parameters: %s, %s, %s"
% (wallpaper, auto, original_file)
% (wallpaper, auto, original_file)
)
try:
subprocess.check_call(
Expand All @@ -2679,6 +2727,8 @@ def set_desktop_wallpaper(self, wallpaper, original_file, refresh_level):
except subprocess.CalledProcessError as e:
if e.returncode == 124:
logger.error(lambda: "Timeout while running set_wallpaper script, killed")
logger.exception(lambda: "Exception when calling set_wallpaper script: %d" % e.returncode)
except Exception as e:
logger.exception(
lambda: "Exception when calling set_wallpaper script: %d" % e.returncode
)
Expand Down
Loading