utils.py 12.1 KB
Newer Older
1
# Copyright (c) 2011 John Stowers
2 3
# SPDX-License-Identifier: GPL-3.0+
# License-Filename: LICENSES/GPL-3.0
4 5

import os.path
6
import logging
7 8
import tempfile
import shutil
9
import subprocess
10
import glob
11
import itertools
12

13 14
import gi
gi.require_version("Notify", "0.7")
15
from gi.repository import GObject
16
from gi.repository import GLib
Alex Muñoz's avatar
Alex Muñoz committed
17
from gi.repository import Gio
18 19
from gi.repository import Notify

20 21
import gtweak

22 23 24 25 26 27 28 29 30 31 32 33 34
def singleton(cls):
    """
    Singleton decorator that works with GObject derived types. The 'recommended'
    python one - http://wiki.python.org/moin/PythonDecoratorLibrary#Singleton
    does not (interacts badly with GObjectMeta
    """
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

35
def make_combo_list_with_default(opts, default, title=True, default_text=None):
36 37 38 39 40 41 42 43 44
    """
    Turns a list of values into a list of value,name (where name is the
    display name a user will see in a combo box). If a value is opt is
    equal to that supplied in default the display name for that value is
    modified to "value <i>(default)</i>"

    @opts: a list of value
    @returns: a list of 2-tuples (value, name)
    """
45 46
    themes = []
    for t in opts:
47
        if t.lower() == "default" and t != default:
48 49
            #some themes etc are actually called default. Ick. Dont show them if they
            #are not the actual default value
50 51
            continue

52
        if title and len(t):
53 54 55 56 57
            name = t[0].upper() + t[1:]
        else:
            name = t

        if t == default:
58
            #indicates the default theme, e.g Adwaita (default)
John Stowers's avatar
John Stowers committed
59
            name = default_text or _("%s <i>(default)</i>") % name
60 61 62 63

        themes.append((t, name))
    return themes

64 65 66 67 68 69 70 71 72 73 74 75
def walk_directories(dirs, filter_func):
    valid = []
    try:
        for thdir in dirs:
            if os.path.isdir(thdir):
                for t in os.listdir(thdir):
                    if filter_func(os.path.join(thdir, t)):
                         valid.append(t)
    except:
        logging.critical("Error parsing directories", exc_info=True)
    return valid

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
def extract_zip_file(z, members_path, dest):
    """ returns (true_if_extracted_ok, true_if_updated) """
    tmp = tempfile.mkdtemp()
    tmpdest = os.path.join(tmp, members_path)

    ok = True
    updated = False
    try:
        if os.path.exists(dest):
            shutil.rmtree(dest)
            updated = True
        z.extractall(tmp)
        shutil.copytree(tmpdest, dest)
    except OSError:
        ok = False
        logging.warning("Error extracting zip", exc_info=True)

    if ok:
        logging.info("Extracted zip to %s, copied to %s" % (tmpdest, dest))

    return ok, updated

98 99 100
def execute_subprocess(cmd_then_args, block=True):
    p = subprocess.Popen(
            cmd_then_args,
101 102
            stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True,
            universal_newlines=True)
103 104 105 106
    if block:
        stdout, stderr = p.communicate()
        return stdout, stderr, p.returncode

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
def get_resource_dirs(resource):
    """Returns a list of all known resource dirs for a given resource.

    :param str resource:
        Name of the resource (e.g. "themes")
    :return:
        A list of resource dirs
    """
    dirs = [os.path.join(dir, resource)
            for dir in itertools.chain(GLib.get_system_data_dirs(),
                                       (gtweak.DATA_DIR,
                                        GLib.get_user_data_dir()))]
    dirs += [os.path.join(os.path.expanduser("~"), ".{}".format(resource))]

    return [dir for dir in dirs if os.path.isdir(dir)]

123
@singleton
124
class AutostartManager:
125 126 127 128 129 130 131 132 133

    @staticmethod
    def get_desktop_files():
        return [a.get_filename() for a in Gio.app_info_get_all()]

    @staticmethod
    def get_user_autostart_files():
        return glob.glob(
                    os.path.join(
134
                        GLib.get_user_config_dir(), "autostart", "*.desktop"))
135 136 137 138 139 140 141 142 143 144

    @staticmethod
    def get_system_autostart_files():
        f = []
        for d in GLib.get_system_config_dirs():
            f.extend( glob.glob(os.path.join(d, "autostart", "*.desktop")) )
        return f

class AutostartFile:
    def __init__(self, appinfo, autostart_desktop_filename="", exec_cmd="", extra_exec_args=""):
145 146 147 148 149 150 151 152 153 154 155
        if appinfo:
            self._desktop_file = appinfo.get_filename()
            self._autostart_desktop_filename = autostart_desktop_filename or os.path.basename(self._desktop_file)
            self._create_file = False
        elif autostart_desktop_filename:
            self._desktop_file = None
            self._autostart_desktop_filename = autostart_desktop_filename
            self._create_file = True
        else:
            raise Exception("Need either an appinfo or a file name")

156
        self._exec_cmd = exec_cmd
157 158 159 160
        if extra_exec_args:
            self._extra_exec_args = " %s\n" % extra_exec_args
        else:
            self._extra_exec_args = "\n"
161

162
        user_autostart_dir = os.path.join(GLib.get_user_config_dir(), "autostart")
163
        os.makedirs(user_autostart_dir, exist_ok=True)
164 165 166

        self._user_autostart_file = os.path.join(user_autostart_dir, self._autostart_desktop_filename)

167 168
        if self._desktop_file:
            logging.debug("Found desktop file: %s" % self._desktop_file)
169 170
        logging.debug("User autostart desktop file: %s" % self._user_autostart_file)

171 172 173 174 175 176
    def _create_user_autostart_file(self):
        f = open(self._user_autostart_file, "w")
        f.write("[Desktop Entry]\nType=Application\nName=%s\nExec=%s\n" %
                (self._autostart_desktop_filename[0:-len('.desktop')], self._exec_cmd + self._extra_exec_args))
        f.close()

177
    def is_start_at_login_enabled(self):
178 179
        if os.path.exists(self._user_autostart_file):
            #prefer user directories first
180 181 182
            #if it contains X-GNOME-Autostart-enabled=false then it has
            #has been disabled by the user in the session applet, otherwise
            #it is enabled
183
            return open(self._user_autostart_file).read().find("X-GNOME-Autostart-enabled=false") == -1
184
        else:
185
            #check the system directories
186 187
            for f in AutostartManager().get_system_autostart_files():
                if os.path.basename(f) == self._autostart_desktop_filename:
188 189
                    return True
        return False
190 191 192

    def update_start_at_login(self, update):

193 194 195
        if os.path.exists(self._user_autostart_file):
            logging.info("Removing user autostart file %s" % self._user_autostart_file)
            os.remove(self._user_autostart_file)
196 197

        if update:
198
            if (not self._desktop_file) or (not os.path.exists(self._desktop_file)):
199 200 201 202
                if self._create_file:
                    self._create_user_autostart_file()
                else:
                    logging.critical("Could not find desktop file: %s" % self._desktop_file)
203 204
                return

205
            logging.info("Adding autostart %s" % self._user_autostart_file)
206 207
            #copy the original file to the new file, but add the extra exec args
            old = open(self._desktop_file, "r")
208
            new = open(self._user_autostart_file, "w")
209

210
            for l in old.readlines():
211
                if l.startswith("Exec="):
212 213 214 215 216
                    if self._exec_cmd:
                        new.write("Exec=%s\n" % self._exec_cmd)
                    else:
                        new.write(l[0:-1])
                        new.write(self._extra_exec_args)
217 218 219 220 221
                else:
                    new.write(l)

            old.close()
            new.close()
222

Alex Muñoz's avatar
Alex Muñoz committed
223 224 225 226 227 228 229 230
class SchemaList:

    __list = None

    def __init__(self):

        if SchemaList.__list == None:
            SchemaList.__list = []
231

Alex Muñoz's avatar
Alex Muñoz committed
232 233
    def get(self):
        return SchemaList.__list
234

Alex Muñoz's avatar
Alex Muñoz committed
235 236 237
    def insert(self, key_name, schema_name):
        v = [key_name, schema_name]
        SchemaList.__list.append(v)
238

Alex Muñoz's avatar
Alex Muñoz committed
239 240 241 242
    def reset(self):
        for i in SchemaList.__list:
            s = Gio.Settings(i[1])
            s.reset(i[0])
243 244
@singleton
class DisableExtension(GObject.GObject):
245

246
    __gsignals__ = {
247
        "disable-extension": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,()),
248 249
    }

250 251
    def __init__(self):
        GObject.GObject.__init__(self)
252

253 254
    def disable(self):
        self.emit("disable-extension")
Alex Muñoz's avatar
Alex Muñoz committed
255

256 257 258 259 260 261
@singleton
class XSettingsOverrides:

    VARIANT_TYPES = {
        'Gtk/ShellShowsAppMenu': GLib.Variant.new_int32,
        'Gtk/EnablePrimaryPaste': GLib.Variant.new_int32,
262
        'Gdk/WindowScalingFactor': GLib.Variant.new_int32,
263 264 265
    }

    def __init__(self):
266
        self._settings = Gio.Settings(schema='org.gnome.settings-daemon.plugins.xsettings')
267 268 269 270
        self._variant = self._settings.get_value("overrides")

    def _dup_variant_as_dict(self):
        items = {}
271
        for k in list(self._variant.keys()):
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
            try:
                #variant override doesnt support .items()
                v = self._variant[k]
                items[k] = self.VARIANT_TYPES[k](v)
            except KeyError:
                pass
        return items

    def _dup_variant(self):
        return GLib.Variant('a{sv}', self._dup_variant_as_dict())

    def _set_override(self, name, v):
        items = self._dup_variant_as_dict()
        items[name] = self.VARIANT_TYPES[name](v)
        n = GLib.Variant('a{sv}', items)
        self._settings.set_value('overrides', n)
288
        self._variant = self._settings.get_value("overrides")
289 290 291 292 293 294 295 296 297 298 299 300 301

    def _get_override(self, name, default):
        try:
            return self._variant[name]
        except KeyError:
            return default

    #while I could store meta type information in the VARIANT_TYPES
    #dict, its easiest to do default value handling and missing value
    #checks in dedicated functions
    def set_shell_shows_app_menu(self, v):
        self._set_override('Gtk/ShellShowsAppMenu', int(v))
    def get_shell_shows_app_menu(self):
302
        return self._get_override('Gtk/ShellShowsAppMenu', True)
303 304 305
    def set_enable_primary_paste(self, v):
        self._set_override('Gtk/EnablePrimaryPaste', int(v))
    def get_enable_primary_paste(self):
306
        return self._get_override('Gtk/EnablePrimaryPaste', True)
307 308 309 310
    def set_window_scaling_factor(self, v):
        self._set_override('Gdk/WindowScalingFactor', int(v))
    def get_window_scaling_factor(self):
        return self._get_override('Gdk/WindowScalingFactor', 1)
311

312 313
class Notification:
    def __init__(self, summary, body):
314
        if Notify.is_initted() or Notify.init(_("GNOME Tweaks")):
315 316 317
            self.notification = Notify.Notification.new(
                                    summary,
                                    body,
318
                                    'gnome-tweaks'
319 320 321
            )
            self.notification.set_hint(
                                "desktop-entry",
322
                                GLib.Variant('s', 'org.gnome.tweaks'))
323 324 325 326 327
            self.notification.show()
        else:
            raise Exception("Not Supported")

@singleton
328 329
class LogoutNotification:
    def __init__(self):
Alberto Fanjul's avatar
Alberto Fanjul committed
330
        if Notify.is_initted() or Notify.init(_("GNOME Tweaks")):
331
            self.notification = Notify.Notification.new(
332 333
                                _("Configuration changes require restart"),
                                _("Your session needs to be restarted for settings to take effect"),
334
                                'gnome-tweaks')
335 336
            self.notification.add_action(
                                "restart",
337
                                _("Restart Session"),
338
                                self._logout, None, None)
339 340
            self.notification.set_hint(
                                "desktop-entry",
341
                                GLib.Variant('s', 'org.gnome.tweaks'))
342
            self.notification.show()
343
        else:
344
            raise Exception("Not Supported")
345

346
    def _logout(self, btn, action, user_data, unknown):
Alex Muñoz's avatar
Alex Muñoz committed
347 348 349
        d = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        proxy = Gio.DBusProxy.new_sync(
                       d,Gio.DBusProxyFlags.NONE, None,
350 351
                       'org.gnome.SessionManager',
                       '/org/gnome/SessionManager',
Alex Muñoz's avatar
Alex Muñoz committed
352 353 354
                       'org.gnome.SessionManager',
                       None)
        proxy.Logout('(u)', 0)