diff --git a/bin/pytop-0-0-1-x64.deb b/bin/pytop-0-0-1-x64.deb index f28be12..b8ac406 100644 Binary files a/bin/pytop-0-0-1-x64.deb and b/bin/pytop-0-0-1-x64.deb differ diff --git a/src/Pytop/__builtins__.py b/src/Pytop/__builtins__.py new file mode 100644 index 0000000..3eea8aa --- /dev/null +++ b/src/Pytop/__builtins__.py @@ -0,0 +1,74 @@ +import builtins + +# Python imports +import builtins + +# Lib imports + +# Application imports +from ipc_server import IPCServer + + + +class EventSystem(IPCServer): + """ Inheret IPCServerMixin. Create an pub/sub systems. """ + + def __init__(self): + super(EventSystem, self).__init__() + + # NOTE: The format used is list of [type, target, (data,)] Where: + # type is useful context for control flow, + # target is the method to call, + # data is the method parameters to give + # Where data may be any kind of data + self._gui_events = [] + self._module_events = [] + + + # Makeshift fake "events" type system FIFO + def _pop_gui_event(self): + if len(self._gui_events) > 0: + return self._gui_events.pop(0) + return None + + def _pop_module_event(self): + if len(self._module_events) > 0: + return self._module_events.pop(0) + return None + + + def push_gui_event(self, event): + if len(event) == 3: + self._gui_events.append(event) + return None + + raise Exception("Invald event format! Please do: [type, target, (data,)]") + + def push_module_event(self, event): + if len(event) == 3: + self._module_events.append(event) + return None + + raise Exception("Invald event format! Please do: [type, target, (data,)]") + + def read_gui_event(self): + return self._gui_events[0] + + def read_module_event(self): + return self._module_events[0] + + def consume_gui_event(self): + return self._pop_gui_event() + + def consume_module_event(self): + return self._pop_module_event() + + + +# NOTE: Just reminding myself we can add to builtins two different ways... +# __builtins__.update({"event_system": Builtins()}) +builtins.app_name = "Pytop" +builtins.event_system = EventSystem() +builtins.event_sleep_time = 0.2 +builtins.debug = False +builtins.trace_debug = False diff --git a/src/Pytop/__init__.py b/src/Pytop/__init__.py old mode 100755 new mode 100644 index 054b613..90dc8da --- a/src/Pytop/__init__.py +++ b/src/Pytop/__init__.py @@ -1,73 +1,3 @@ -#!/usr/bin/python3 - -# Python imports -import inspect - -from setproctitle import setproctitle - -# Gtk imports -import gi, faulthandler, signal -gi.require_version('Gtk', '3.0') - -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk -from gi.repository import GLib - -# Application imports -from utils import Settings -from signal_classes import Signals - - -class Main: - def __init__(self, args): - setproctitle('Pytop') - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit) - faulthandler.enable() # For better debug info - - builder = gtk.Builder() - settings = Settings() - settings.attachBuilder(builder) - self.connectBuilder(settings, builder) - - window = settings.createWindow() - window.fullscreen() - window.show() - - monitors = settings.returnMonitorsInfo() - i = 1 - if len(monitors) > 1: - for mon in monitors[1:]: - subBuilder = gtk.Builder() - subSettings = Settings(i) - subSettings.attachBuilder(subBuilder) - self.connectBuilder(subSettings, subBuilder) - - win = subSettings.createWindow() - win.set_default_size(mon.width, mon.height) - win.set_size_request(mon.width, mon.height) - win.set_resizable(False) - win.move(mon.x, mon.y) - - win.show() - i += 1 - - - def connectBuilder(self, settings, builder): - # Gets the methods from the classes and sets to handler. - # Then, builder connects to any signals it needs. - classes = [Signals(settings)] - - handlers = {} - for c in classes: - methods = inspect.getmembers(c, predicate=inspect.ismethod) - handlers.update(methods) - - builder.connect_signals(handlers) - - -if __name__ == "__main__": - try: - main = Main() - gtk.main() - except Exception as e: - print( repr(e) ) +""" + Start of package. +""" diff --git a/src/Pytop/__main__.py b/src/Pytop/__main__.py index 06514d9..3c99175 100644 --- a/src/Pytop/__main__.py +++ b/src/Pytop/__main__.py @@ -2,18 +2,18 @@ # Python imports -import argparse +import argparse, faulthandler, traceback import pdb # For trace debugging from setproctitle import setproctitle -# Gtk imports +# Lib imports import gi, faulthandler, signal gi.require_version('Gtk', '3.0') -from gi.repository import Gtk as gtk +from gi.repository import Gtk from gi.repository import GLib # Application imports -from __init__ import Main +from main import Main @@ -21,15 +21,16 @@ if __name__ == "__main__": try: # pdb.set_trace() setproctitle('Pytop') - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit) faulthandler.enable() # For better debug info + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit) + parser = argparse.ArgumentParser() # Add long and short arguments parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.") - # Read arguments (If any...) - args = parser.parse_args() - main = Main(args) - gtk.main() + args, unknownargs = parser.parse_known_args() + + main = Main(args, unknownargs) + Gtk.main() except Exception as e: - print( repr(e) ) + traceback.print_exc() diff --git a/src/Pytop/context/__init__.py b/src/Pytop/context/__init__.py new file mode 100644 index 0000000..0d42d90 --- /dev/null +++ b/src/Pytop/context/__init__.py @@ -0,0 +1,3 @@ +""" +Context module +""" diff --git a/src/Pytop/context/controller.py b/src/Pytop/context/controller.py new file mode 100644 index 0000000..ad367d3 --- /dev/null +++ b/src/Pytop/context/controller.py @@ -0,0 +1,21 @@ +# Python imports +from datetime import datetime +import os + +# Gtk imports + + +# Application imports +from .controller_data import Controller_Data +from .mixins.main_menu_mixin import MainMenuMixin +from .mixins.taskbar_mixin import TaskbarMixin +from .mixins.cpu_draw_mixin import CPUDrawMixin +from .mixins.grid_mixin import GridMixin + + + + + +class Controller(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin, Controller_Data): + def __init__(self, _settings): + self.setup_controller_data(_settings) diff --git a/src/Pytop/signal_classes/Signals.py b/src/Pytop/context/controller_data.py similarity index 55% rename from src/Pytop/signal_classes/Signals.py rename to src/Pytop/context/controller_data.py index 03b79d4..65613f0 100644 --- a/src/Pytop/signal_classes/Signals.py +++ b/src/Pytop/context/controller_data.py @@ -1,22 +1,25 @@ # Python imports -from datetime import datetime -import os - -# Gtk imports +import os, signal +# Lib imports +from gi.repository import GLib # Application imports -from .mixins import CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin -from widgets import Grid -from widgets import Icon -from utils import FileHandler +from plugins.plugins import Plugins + +from widgets.grid import Grid +from widgets.icon import Icon +from utils.file_handler import FileHandler +class Controller_Data: + ''' Controller_Data contains most of the state of the app at ay given time. It also has some support methods. ''' -class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): - def __init__(self, settings): - self.settings = settings - self.builder = self.settings.returnBuilder() + def setup_controller_data(self, _settings): + self.plugins = Plugins(_settings) + + self.settings = _settings + self.builder = self.settings.get_builder() self.timeLabel = self.builder.get_object("timeLabel") self.drawArea = self.builder.get_object("drawArea") @@ -28,10 +31,6 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): self.setPagerWidget() self.setTasklistWidget() - # Must be after pager and task list inits - self.displayclock() - self.startClock() - # CPUDrawMixin Parts self.cpu_percents = [] self.doDrawBackground = False @@ -53,14 +52,13 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): # GridMixin Parts - self.filehandler = FileHandler(settings) + self.filehandler = FileHandler(self.settings) - self.builder = self.settings.returnBuilder() self.gridObj = self.builder.get_object("Desktop") selectDirDialog = self.builder.get_object("selectDirDialog") filefilter = self.builder.get_object("Folders") - self.currentPath = self.settings.returnSettings()[0] + self.currentPath = self.settings.getSettings()[0] self.copyCutArry = [] self.selectedFiles = [] self.gridClss = Grid(self.gridObj, self.settings) @@ -81,9 +79,39 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): self.iconFactory = Icon(self.settings) self.grpDefault = "Accessories" self.progGroup = self.grpDefault - HOME_APPS = os.path.expanduser('~') + "/.local/share/applications/" + HOME_APPS = f"{self.settings.get_user_home()}/.local/share/applications/" paths = ["/opt/", "/usr/share/applications/", HOME_APPS] self.menuData = self.getDesktopFilesInfo(paths) self.desktopObjs = [] - self.getSubgroup() - self.generateListView() + + + + def clear_console(self): + ''' Clears the terminal screen. ''' + os.system('cls' if os.name == 'nt' else 'clear') + + def call_method(self, _method_name, data = None): + ''' + Calls a method from scope of class. + + Parameters: + a (obj): self + b (str): method name to be called + c (*): Data (if any) to be passed to the method. + Note: It must be structured according to the given methods requirements. + + Returns: + Return data is that which the calling method gives. + ''' + method_name = str(_method_name) + method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") + return method(data) if data else method() + + def has_method(self, obj, name): + ''' Checks if a given method exists. ''' + return callable(getattr(obj, name, None)) + + def clear_children(self, widget): + ''' Clear children of a gtk widget. ''' + for child in widget.get_children(): + widget.remove(child) diff --git a/src/Pytop/context/mixins/__init__.py b/src/Pytop/context/mixins/__init__.py new file mode 100644 index 0000000..5043d7d --- /dev/null +++ b/src/Pytop/context/mixins/__init__.py @@ -0,0 +1,3 @@ +""" +Mixins module +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/CPUDrawMixin.py b/src/Pytop/context/mixins/cpu_draw_mixin.py old mode 100644 new mode 100755 similarity index 97% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/CPUDrawMixin.py rename to src/Pytop/context/mixins/cpu_draw_mixin.py index e921777..b5a5d7b --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/CPUDrawMixin.py +++ b/src/Pytop/context/mixins/cpu_draw_mixin.py @@ -1,17 +1,12 @@ -#!/usr/bin/python3 - - -# Python Imports +# Python imports from __future__ import division import cairo, psutil - -# GTK Imports +# Lib imports from gi.repository import GObject from gi.repository import GLib - - +# Application imports class CPUDrawMixin: @@ -54,7 +49,7 @@ class CPUDrawMixin: self.brush.set_source_rgba(rgba[0], rgba[1], rgba[2], rgba[3]) # Movbe to prev. point if any - if oldP is not 0.0 and oldX is not 0.0: + if oldP != 0.0 and oldX != 0.0: x = oldX y = float(self.ah) - (oldP * self.yStep) self.brush.move_to(x, y) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/GridMixin.py b/src/Pytop/context/mixins/grid_mixin.py similarity index 99% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/GridMixin.py rename to src/Pytop/context/mixins/grid_mixin.py index a10cf42..3f2428f 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/GridMixin.py +++ b/src/Pytop/context/mixins/grid_mixin.py @@ -1,7 +1,7 @@ -# Gtk imports - # Python imports +# Lib imports + # Application imports diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/MainMenuMixin.py b/src/Pytop/context/mixins/main_menu_mixin.py similarity index 89% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/MainMenuMixin.py rename to src/Pytop/context/mixins/main_menu_mixin.py index 85c786f..1027d3d 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/MainMenuMixin.py +++ b/src/Pytop/context/mixins/main_menu_mixin.py @@ -12,8 +12,8 @@ from os import listdir import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk as gtk -from gi.repository import GLib as glib +from gi.repository import Gtk +from gi.repository import GLib from xdg.DesktopEntry import DesktopEntry @@ -35,9 +35,9 @@ class MainMenuMixin: posY = pos[1] + 72 if self.menuWindow.get_visible() == False: self.menuWindow.move(posX, posY) - glib.idle_add(self.menuWindow.show_all) + GLib.idle_add(self.menuWindow.show_all) else: - glib.idle_add(self.menuWindow.hide) + GLib.idle_add(self.menuWindow.hide) def setListGroup(self, widget): @@ -59,29 +59,41 @@ class MainMenuMixin: self.generateListView() - @threaded def generateListView(self): widget = self.builder.get_object("programListBttns") # Should have this as a useful method...But, I don't want to import Glib everywhere children = widget.get_children() for child in children: - glib.idle_add(widget.remove, (child)) + GLib.idle_add(widget.remove, (child)) for obj in self.desktopObjs: title = obj[0] dirPath = obj[1] if self.showIcons: - image = self.iconFactory.parseDesktopFiles(dirPath) # .get_pixbuf() - self.addToProgramListView(widget, title, image) + self.update_view(widget, title, dirPath) else: - self.addToProgramListViewAsText(widget, title) + self.update_view(widget, title, dirPath) @threaded - def addToProgramListView(self, widget, title, icon): - button = gtk.Button(label=title) - button.set_image(icon) + def update_view(self, widget, title, dirPath): + image = self.iconFactory.parse_desktop_files(dirPath) # .get_pixbuf() + if self.showIcons: + GLib.idle_add(self.addToProgramListView, *(widget, title, image, self.showIcons,)) + else: + GLib.idle_add(self.addToProgramListView, *(widget, title, image, self.showIcons,)) + + + def addToProgramListView(self, widget, title, image, show_image=True): + icon = Gtk.Image().new_from_pixbuf(image) + button = Gtk.Button(label=title) + + if show_image: + button.set_image(icon) + button.set_always_show_image(True) + pass + button.connect("clicked", self.executeProgram) children = button.get_children() @@ -96,23 +108,7 @@ class MainMenuMixin: label.set_size_request(640, 64) button.show_all() - glib.idle_add(widget.add, (button)) - - @threaded - def addToProgramListViewAsText(self, widget, title): - button = gtk.Button(label=title) - button.connect("clicked", self.executeProgram) - - children = button.get_children() - label = children[0] - - label.set_halign(1) - label.set_line_wrap(True) - label.set_max_width_chars(38) - label.set_size_request(640, 64) - - button.show_all() - glib.idle_add(widget.add, (button)) + widget.add(button) def executeProgram(self, widget): diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/TaskbarMixin.py b/src/Pytop/context/mixins/taskbar_mixin.py similarity index 90% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/TaskbarMixin.py rename to src/Pytop/context/mixins/taskbar_mixin.py index f8fee3b..df315b2 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/TaskbarMixin.py +++ b/src/Pytop/context/mixins/taskbar_mixin.py @@ -2,15 +2,15 @@ import threading from datetime import datetime -# Gtk imports +# Lib imports import gi gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') gi.require_version('Wnck', '3.0') -from gi.repository import Wnck as wnck -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk +from gi.repository import Wnck +from gi.repository import Gtk +from gi.repository import Gdk from gi.repository import GLib from gi.repository import GObject @@ -43,11 +43,11 @@ class TaskbarMixin: def showSystemStats(self, widget, eve): - if eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON: + if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON: self.builder.get_object('systemStats').popup() def setPagerWidget(self): - pager = wnck.Pager() + pager = Wnck.Pager() if self.orientation == 0: self.builder.get_object('taskBarWorkspacesHor').add(pager) @@ -58,7 +58,7 @@ class TaskbarMixin: def setTasklistWidget(self): - tasklist = wnck.Tasklist() + tasklist = Wnck.Tasklist() tasklist.set_scroll_enabled(False) tasklist.set_button_relief(2) # 0 = normal relief, 2 = no relief tasklist.set_grouping(1) # 0 = mever group, 1 auto group, 2 = always group diff --git a/src/Pytop/ipc_server.py b/src/Pytop/ipc_server.py new file mode 100644 index 0000000..589a906 --- /dev/null +++ b/src/Pytop/ipc_server.py @@ -0,0 +1,71 @@ +# Python imports +import threading, socket, time +from multiprocessing.connection import Listener, Client + +# Lib imports + +# Application imports + + +def threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() + return wrapper + + + + +class IPCServer: + ''' Create a listener so that other instances send requests back to existing instance. ''' + def __init__(self): + self.is_ipc_alive = False + self.ipc_authkey = b'pytop-ipc' + self.ipc_address = '127.0.0.1' + self.ipc_port = 8888 + self.ipc_timeout = 15.0 + + @threaded + def create_ipc_server(self): + listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) + self.is_ipc_alive = True + while True: + conn = listener.accept() + start_time = time.time() + + print(f"New Connection: {listener.last_accepted}") + while True: + msg = conn.recv() + if debug: + print(msg) + + if "FILE|" in msg: + file = msg.split("FILE|")[1].strip() + if file: + event_system.push_gui_event([None, "handle_file_from_ipc", (file,)]) + + conn.close() + break + + + if msg == 'close connection': + conn.close() + break + if msg == 'close server': + conn.close() + break + + # NOTE: Not perfect but insures we don't lockup the connection for too long. + end_time = time.time() + if (end - start) > self.ipc_timeout: + conn.close() + + listener.close() + + + def send_ipc_message(self, message="Empty Data..."): + try: + conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) + conn.send(message) + conn.send('close connection') + except Exception as e: + print(repr(e)) diff --git a/src/Pytop/main.py b/src/Pytop/main.py new file mode 100755 index 0000000..6c1166d --- /dev/null +++ b/src/Pytop/main.py @@ -0,0 +1,47 @@ +# Python imports +import inspect + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from utils.settings import Settings +from context.controller import Controller +from __builtins__ import EventSystem + + +class Main(EventSystem): + def __init__(self, args, unknownargs): + settings = Settings() + settings.set_window_data(Gtk.Window()) + monitors = settings.get_monitor_info() + for i, mon in enumerate(monitors): + sub_builder = Gtk.Builder() + sub_settings = Settings(i) + sub_settings.attach_builder(sub_builder) + self.connect_builder(sub_settings, sub_builder) + + window = sub_settings.create_window() + window.set_default_size(mon.width, mon.height) + window.set_size_request(mon.width, mon.height) + window.set_resizable(False) + window.resize(mon.width, mon.height) + window.move(mon.x, mon.y) + window.show() + + + + def connect_builder(self, settings, builder): + # Gets the methods from the classes and sets to handler. + # Then, builder connects to any signals it needs. + classes = [Controller(settings)] + + handlers = {} + for c in classes: + methods = inspect.getmembers(c, predicate=inspect.ismethod) + handlers.update(methods) + + builder.connect_signals(handlers) diff --git a/src/Pytop/plugins/__init__.py b/src/Pytop/plugins/__init__.py new file mode 100644 index 0000000..5624b32 --- /dev/null +++ b/src/Pytop/plugins/__init__.py @@ -0,0 +1,3 @@ +""" + Gtk Bound Plugins Module +""" diff --git a/src/Pytop/plugins/plugins.py b/src/Pytop/plugins/plugins.py new file mode 100644 index 0000000..b77ab32 --- /dev/null +++ b/src/Pytop/plugins/plugins.py @@ -0,0 +1,78 @@ +# Python imports +import os, sys, importlib, traceback +from os.path import join, isdir + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gio + +# Application imports + + +class Plugin: + name = None + module = None + reference = None + + +class Plugins: + """Plugins controller""" + + def __init__(self, settings): + self._settings = settings + self._builder = self._settings.get_builder() + self._plugins_path = self._settings.get_plugins_path() + self._plugins_dir_watcher = None + self._plugin_collection = [] + + + def launch_plugins(self): + self._set_plugins_watcher() + self.load_plugins() + + def _set_plugins_watcher(self): + self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \ + .monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable()) + self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ()) + + def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None): + if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, + Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, + Gio.FileMonitorEvent.MOVED_OUT]: + self.reload_plugins(file) + + def load_plugins(self, file=None): + print(f"Loading plugins...") + parent_path = os.getcwd() + + for file in os.listdir(self._plugins_path): + try: + path = join(self._plugins_path, file) + if isdir(path): + os.chdir(path) + + sys.path.insert(0, path) + spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py")) + app = importlib.util.module_from_spec(spec) + spec.loader.exec_module(app) + + plugin_reference = app.Plugin(self._builder, event_system) + plugin = Plugin() + plugin.name = plugin_reference.get_plugin_name() + plugin.module = path + plugin.reference = plugin_reference + + self._plugin_collection.append(plugin) + except Exception as e: + print("Malformed plugin! Not loading!") + traceback.print_exc() + + os.chdir(parent_path) + + + def reload_plugins(self, file=None): + print(f"Reloading plugins...") + + def set_message_on_plugin(self, type, data): + print("Trying to send message to plugin...") diff --git a/src/Pytop/signal_classes/__init__.py b/src/Pytop/signal_classes/__init__.py deleted file mode 100644 index f5b8431..0000000 --- a/src/Pytop/signal_classes/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .mixins import CPUDrawMixin -from .mixins import TaskbarMixin -from .mixins import GridMixin -from signal_classes.Signals import Signals diff --git a/src/Pytop/signal_classes/mixins/__init__.py b/src/Pytop/signal_classes/mixins/__init__.py deleted file mode 100644 index 7df5562..0000000 --- a/src/Pytop/signal_classes/mixins/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .MainMenuMixin import MainMenuMixin -from .TaskbarMixin import TaskbarMixin -from .CPUDrawMixin import CPUDrawMixin -from .GridMixin import GridMixin diff --git a/src/Pytop/utils/Settings.py b/src/Pytop/utils/Settings.py deleted file mode 100644 index c9c4174..0000000 --- a/src/Pytop/utils/Settings.py +++ /dev/null @@ -1,180 +0,0 @@ -# Gtk imports -import gi, cairo -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') - -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk - -# Python imports -import os, json - -# Application imports - - -class Settings: - def __init__(self, monIndex = 0): - self.builder = None - self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" - - # 'Filters' - self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', - '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') - self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', - '.mpeg', '.mp4', '.webm') - self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf') - self.music = ('.psf', '.mp3', '.ogg' , '.flac') - self.images = ('.png', '.jpg', '.jpeg', '.gif') - self.pdf = ('.pdf') - - self.hideHiddenFiles = True - self.ColumnSize = 8 - self.usrHome = os.path.expanduser('~') - self.desktopPath = self.usrHome + "/Desktop" - self.iconContainerWxH = [128, 128] - self.systemIconImageWxH = [56, 56] - self.viIconWxH = [256, 128] - self.monitors = None - - self.DEFAULTCOLOR = gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000 - self.MOUSEOVERCOLOR = gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff - self.SELECTEDCOLOR = gdk.RGBA(0.4, 0.5, 0.1, 0.84) - self.TRASHFOLDER = os.path.expanduser('~') + "/.local/share/Trash/" - self.TRASHFILESFOLDER = self.TRASHFOLDER + "files/" - self.TRASHINFOFOLDER = self.TRASHFOLDER + "info/" - self.THUMB_GENERATOR = "ffmpegthumbnailer" - self.MEDIAPLAYER = "mpv"; - self.IMGVIEWER = "mirage"; - self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef"; - self.OFFICEPROG = "libreoffice"; - self.TEXTVIEWER = "leafpad"; - self.PDFVIEWER = "evince"; - self.FILEMANAGER = "spacefm"; - self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% "; - self.MPV_WH = " -geometry 50%:50% "; - self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) - - configFolder = os.path.expanduser('~') + "/.config/pytop/" - self.configFile = configFolder + "mon_" + str(monIndex) + "_settings.ini" - - if os.path.isdir(configFolder) == False: - os.mkdir(configFolder) - - if os.path.isdir(self.TRASHFOLDER) == False: - os.mkdir(TRASHFILESFOLDER) - os.mkdir(TRASHINFOFOLDER) - - if os.path.isdir(self.TRASHFILESFOLDER) == False: - os.mkdir(TRASHFILESFOLDER) - - if os.path.isdir(self.TRASHINFOFOLDER) == False: - os.mkdir(TRASHINFOFOLDER) - - if os.path.isfile(self.configFile) == False: - open(self.configFile, 'a').close() - self.saveSettings(self.desktopPath) - - - def attachBuilder(self, builder): - self.builder = builder - self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade") - - def createWindow(self): - # Get window and connect signals - window = self.builder.get_object("Window") - window.connect("delete-event", gtk.main_quit) - self.setWindowData(window) - return window - - def setWindowData(self, window): - screen = window.get_screen() - visual = screen.get_rgba_visual() - if visual != None and screen.is_composited(): - window.set_visual(visual) - - # bind css file - cssProvider = gtk.CssProvider() - cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css') - screen = gdk.Screen.get_default() - styleContext = gtk.StyleContext() - styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER) - - window.set_app_paintable(True) - self.monitors = self.getMonitorData(screen) - window.resize(self.monitors[0].width, self.monitors[0].height) - - def getMonitorData(self, screen): - monitors = [] - for m in range(screen.get_n_monitors()): - monitors.append(screen.get_monitor_geometry(m)) - - for monitor in monitors: - print(str(monitor.width) + "+" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y)) - - return monitors - - - def returnMonitorsInfo(self): - return self.monitors - - - def saveSettings(self, startPath): - data = {} - data['pytop_settings'] = [] - - data['pytop_settings'].append({ - 'startPath' : startPath - }) - - with open(self.configFile, 'w') as outfile: - json.dump(data, outfile) - - - def returnSettings(self): - returnData = [] - - with open(self.configFile) as infile: - try: - data = json.load(infile) - for obj in data['pytop_settings']: - returnData = [obj['startPath']] - except Exception as e: - returnData = ['~/Desktop/'] - - - if returnData[0] == '': - returnData[0] = '~/Desktop/' - - return returnData - - - def returnBuilder(self): return self.builder - def returnUserHome(self): return self.usrHome - def returnDesktopPath(self): return self.usrHome + "/Desktop" - def returnColumnSize(self): return self.ColumnSize - def returnContainerWH(self): return self.iconContainerWxH - def returnSystemIconImageWH(self): return self.systemIconImageWxH - def returnVIIconWH(self): return self.viIconWxH - def isHideHiddenFiles(self): return self.hideHiddenFiles - - # Filter returns - def returnOfficeFilter(self): return self.office - def returnVidsFilter(self): return self.vids - def returnTextFilter(self): return self.txt - def returnMusicFilter(self): return self.music - def returnImagesFilter(self): return self.images - def returnPdfFilter(self): return self.pdf - - def returnIconImagePos(self): return self.GTK_ORIENTATION - def getThumbnailGenerator(self): return self.THUMB_GENERATOR - def returnMediaProg(self): return self.MEDIAPLAYER - def returnImgVwrProg(self): return self.IMGVIEWER - def returnMusicProg(self): return self.MUSICPLAYER - def returnOfficeProg(self): return self.OFFICEPROG - def returnTextProg(self): return self.TEXTVIEWER - def returnPdfProg(self): return self.PDFVIEWER - def returnFileMngrProg(self): return self.FILEMANAGER - def returnMplyrWH(self): return self.MPLAYER_WH - def returnMpvWHProg(self): return self.MPV_WH - def returnTrshFilesPth(self): return self.TRASHFILESFOLDER - def returnTrshInfoPth(self): return self.TRASHINFOFOLDER diff --git a/src/Pytop/utils/__init__.py b/src/Pytop/utils/__init__.py index 548f6d4..4247336 100644 --- a/src/Pytop/utils/__init__.py +++ b/src/Pytop/utils/__init__.py @@ -1,4 +1,3 @@ -from utils.Dragging import Dragging -from .Logger import Logger -from utils.FileHandler import FileHandler -from utils.Settings import Settings +""" +Utils modile +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Dragging.py b/src/Pytop/utils/dragging.py similarity index 99% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Dragging.py rename to src/Pytop/utils/dragging.py index abf0a03..0f11663 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Dragging.py +++ b/src/Pytop/utils/dragging.py @@ -1,4 +1,7 @@ -# Gtk imports +# Python imports +import os + +# Lib imports import gi gi.require_version('Gdk', '3.0') @@ -6,8 +9,6 @@ gi.require_version('Gdk', '3.0') from gi.repository import Gdk from gi.repository import GObject -# Python imports -import os # Application imports diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/FileHandler.py b/src/Pytop/utils/file_handler.py similarity index 82% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/utils/FileHandler.py rename to src/Pytop/utils/file_handler.py index 3c8045d..ffda606 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/FileHandler.py +++ b/src/Pytop/utils/file_handler.py @@ -12,27 +12,29 @@ def threaded(fn): return wrapper class FileHandler: - def __init__(self, settings): + def __init__(self, _settings): + self.settings = _settings + # 'Filters' - self.office = settings.returnOfficeFilter() - self.vids = settings.returnVidsFilter() - self.txt = settings.returnTextFilter() - self.music = settings.returnMusicFilter() - self.images = settings.returnImagesFilter() - self.pdf = settings.returnPdfFilter() + self.office = self.settings.getOfficeFilter() + self.vids = self.settings.getVidsFilter() + self.txt = self.settings.getTextFilter() + self.music = self.settings.getMusicFilter() + self.images = self.settings.getImagesFilter() + self.pdf = self.settings.getPdfFilter() # Args - self.MEDIAPLAYER = settings.returnMediaProg() - self.IMGVIEWER = settings.returnImgVwrProg() - self.MUSICPLAYER = settings.returnMusicProg() - self.OFFICEPROG = settings.returnOfficeProg() - self.TEXTVIEWER = settings.returnTextProg() - self.PDFVIEWER = settings.returnPdfProg() - self.FILEMANAGER = settings.returnFileMngrProg() - self.MPLAYER_WH = settings.returnMplyrWH() - self.MPV_WH = settings.returnMpvWHProg() - self.TRASHFILESFOLDER = settings.returnTrshFilesPth() - self.TRASHINFOFOLDER = settings.returnTrshInfoPth() + self.MEDIAPLAYER = self.settings.getMediaProg() + self.IMGVIEWER = self.settings.getImgVwrProg() + self.MUSICPLAYER = self.settings.getMusicProg() + self.OFFICEPROG = self.settings.getOfficeProg() + self.TEXTVIEWER = self.settings.getTextProg() + self.PDFVIEWER = self.settings.getPdfProg() + self.FILEMANAGER = self.settings.getFileMngrProg() + self.MPLAYER_WH = self.settings.getMplyrWH() + self.MPV_WH = self.settings.getMpvWHProg() + self.TRASHFILESFOLDER = self.settings.getTrshFilesPth() + self.TRASHINFOFOLDER = self.settings.getTrshInfoPth() def openFile(self, file): diff --git a/src/Pytop/utils/Logger.py b/src/Pytop/utils/logger.py similarity index 89% rename from src/Pytop/utils/Logger.py rename to src/Pytop/utils/logger.py index c8dc0db..ba66789 100644 --- a/src/Pytop/utils/Logger.py +++ b/src/Pytop/utils/logger.py @@ -1,12 +1,14 @@ # Python imports import os, logging +# Lib imports + # Application imports class Logger: - def __init__(self): - self.USER_HOME = os.path.expanduser("~") + def __init__(self, home): + self._USER_HOME = home def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True): """ @@ -41,8 +43,8 @@ class Logger: log.addHandler(ch) if createFile: - folder = self.USER_HOME + ".config/pytop/logs" - file = folder + "/application.log" + folder = f"{self._USER_HOME}/.config/{app_name.lower()}/logs" + file = f"{folder}/application.log" if not os.path.exists(folder): os.mkdir(folder) diff --git a/src/Pytop/utils/settings.py b/src/Pytop/utils/settings.py new file mode 100644 index 0000000..ba46229 --- /dev/null +++ b/src/Pytop/utils/settings.py @@ -0,0 +1,199 @@ +# Gtk imports +import gi, cairo +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +from gi.repository import Gtk +from gi.repository import Gdk + +# Python imports +import os, json + +# Application imports +from .logger import Logger + + +class Settings: + def __init__(self, monIndex = 0): + + self._USR_PATH = f"/usr/share/{app_name.lower()}" + self._USER_HOME = os.path.expanduser('~') + self._SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + self._DESKTOP_PATH = f"{self._USER_HOME}/Desktop" + self._CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}" + self._CONFIG_FILE = f"{self._CONFIG_PATH}/mon_{str(monIndex)}_settings.ini" + self._PLUGINS_PATH = f"{self._CONFIG_PATH}/plugins" + self._LOGGER = Logger(self._USER_HOME) + + self._DEFAULT_ICONS = f"{self._CONFIG_PATH}/icons" + self._INTERNAL_ICON_PTH = f"{self._DEFAULT_ICONS}/bin.png" + self._ABS_THUMBS_PTH = f"{self._USER_HOME}/.thumbnails/normal" + self._STEAM_ICONS_PTH = f"{self._USER_HOME}/.thumbnails/steam_icons" + self._ICON_DIRS = ["/usr/share/icons", f"{self._USER_HOME}/.local/share/icons"] + + self.DEFAULTCOLOR = Gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000 + self.MOUSEOVERCOLOR = Gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff + self.SELECTEDCOLOR = Gdk.RGBA(0.4, 0.5, 0.1, 0.84) + self._TRASHFOLDER = f"{self._USER_HOME}/.local/share/Trash" + self._TRASH_FILES_FOLDER = f"{self._TRASHFOLDER}/files/" + self._TRASH_INFO_FOLDER = f"{self._TRASHFOLDER}/info/" + self.THUMB_GENERATOR = "ffmpegthumbnailer" + self.MEDIAPLAYER = "mpv"; + self.IMGVIEWER = "mirage"; + self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef"; + self.OFFICEPROG = "libreoffice"; + self.TEXTVIEWER = "leafpad"; + self.PDFVIEWER = "evince"; + self.FILEMANAGER = "spacefm"; + self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% "; + self.MPV_WH = " -geometry 50%:50% "; + self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) + + # 'Filters' + self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', + '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') + self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', + '.mpeg', '.mp4', '.webm') + self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf') + self.music = ('.psf', '.mp3', '.ogg' , '.flac') + self.images = ('.png', '.jpg', '.jpeg', '.gif') + self.pdf = ('.pdf') + + self.hideHiddenFiles = True + self.ColumnSize = 8 + self.iconContainerWxH = [128, 128] + self.systemIconImageWxH = [56, 56] + self.viIconWxH = [256, 128] + self.monitors = None + self.builder = None + + if os.path.isdir(self._CONFIG_PATH) == False: + os.mkdir(self._CONFIG_PATH) + + if os.path.isdir(self._TRASHFOLDER) == False: + os.mkdir(TRASHFILESFOLDER) + os.mkdir(TRASHINFOFOLDER) + + if os.path.isdir(self._TRASH_FILES_FOLDER) == False: + os.mkdir(TRASHFILESFOLDER) + + if os.path.isdir(self._TRASH_INFO_FOLDER) == False: + os.mkdir(TRASHINFOFOLDER) + + if os.path.isfile(self._CONFIG_FILE) == False: + open(self._CONFIG_FILE, 'a').close() + self.saveSettings(self._DESKTOP_PATH) + + + def attach_builder(self, builder): + self.builder = builder + self.builder.add_from_file(f"{self._CONFIG_PATH}/Main_Window.glade") + + def create_window(self): + # Get window and connect signals + window = self.builder.get_object("Window") + window.connect("delete-event", Gtk.main_quit) + self.set_window_data(window) + return window + + def set_window_data(self, window): + screen = window.get_screen() + visual = screen.get_rgba_visual() + if visual != None and screen.is_composited(): + window.set_visual(visual) + + # bind css file + cssProvider = Gtk.CssProvider() + cssProvider.load_from_path(f'{self._CONFIG_PATH}/stylesheet.css') + screen = Gdk.Screen.get_default() + styleContext = Gtk.StyleContext() + styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + + window.set_app_paintable(True) + self.monitors = self.get_monitor_data(screen) + + def get_monitor_data(self, screen): + monitors = [] + for m in range(screen.get_n_monitors()): + monitors.append(screen.get_monitor_geometry(m)) + + for monitor in monitors: + print(str(monitor.width) + "+" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y)) + + return monitors + + + def get_monitor_info(self): + return self.monitors + + + def saveSettings(self, startPath): + data = {} + data['pytop_settings'] = [] + + data['pytop_settings'].append({ + 'startPath' : startPath + }) + + with open(self._CONFIG_FILE, 'w') as outfile: + json.dump(data, outfile) + + + def getSettings(self): + returnData = [] + + with open(self._CONFIG_FILE) as infile: + try: + data = json.load(infile) + for obj in data['pytop_settings']: + returnData = [obj['startPath']] + except Exception as e: + returnData = [f'{self._DESKTOP_PATH}'] + + + if returnData[0] == '': + returnData[0] = f'{self._DESKTOP_PATH}' + + return returnData + + + def get_builder(self): return self.builder + def get_user_home(self): return self._USER_HOME + + def get_desktop_path(self): return self._DESKTOP_PATH + def get_config_path(self): return self._CONFIG_PATH + def get_plugins_path(self): return self._PLUGINS_PATH + + def getColumnSize(self): return self.ColumnSize + def getContainerWH(self): return self.iconContainerWxH + def getSystemIconImageWH(self): return self.systemIconImageWxH + def getVIIconWH(self): return self.viIconWxH + def isHideHiddenFiles(self): return self.hideHiddenFiles + + # Filter returns + def getOfficeFilter(self): return self.office + def getVidsFilter(self): return self.vids + def getTextFilter(self): return self.txt + def getMusicFilter(self): return self.music + def getImagesFilter(self): return self.images + def getPdfFilter(self): return self.pdf + + def getIconImagePos(self): return self.GTK_ORIENTATION + def getThumbnailGenerator(self): return self.THUMB_GENERATOR + def getMediaProg(self): return self.MEDIAPLAYER + def getImgVwrProg(self): return self.IMGVIEWER + def getMusicProg(self): return self.MUSICPLAYER + def getOfficeProg(self): return self.OFFICEPROG + def getTextProg(self): return self.TEXTVIEWER + def getPdfProg(self): return self.PDFVIEWER + def getFileMngrProg(self): return self.FILEMANAGER + def getMplyrWH(self): return self.MPLAYER_WH + def getMpvWHProg(self): return self.MPV_WH + def getTrshFilesPth(self): return self._TRASH_FILES_FOLDER + def getTrshInfoPth(self): return self._TRASH_INFO_FOLDER + + def getDefaultIcon(self): return self._INTERNAL_ICON_PTH + def getInternalIconsPth(self): return self._DEFAULT_ICONS + def getAbsThumbsPth(self): return self._ABS_THUMBS_PTH + def getSteamIconsPth(self): return self._STEAM_ICONS_PTH + def getIconDirs(self): return self._ICON_DIRS diff --git a/src/Pytop/widgets/Icon.py b/src/Pytop/widgets/Icon.py deleted file mode 100644 index 55ff4b8..0000000 --- a/src/Pytop/widgets/Icon.py +++ /dev/null @@ -1,199 +0,0 @@ -# Python Imports -import os, subprocess, hashlib, threading -from os.path import isdir, isfile, join - - -# Gtk imports -import gi -gi.require_version('Gdk', '3.0') - -from gi.repository import GdkPixbuf -from xdg.DesktopEntry import DesktopEntry - - -# Application imports - - - - -def threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs).start() - return wrapper - -class Icon: - def __init__(self, settings): - self.settings = settings - self.thubnailGen = settings.getThumbnailGenerator() - self.vidsList = settings.returnVidsFilter() - self.imagesList = settings.returnImagesFilter() - self.GTK_ORIENTATION = settings.returnIconImagePos() - self.usrHome = settings.returnUserHome() - self.iconContainerWH = settings.returnContainerWH() - self.systemIconImageWH = settings.returnSystemIconImageWH() - self.viIconWH = settings.returnVIIconWH() - self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" - self.INTERNAL_ICON_PTH = self.SCRIPT_PTH + "../resources/icons/text.png" - - - def createIcon(self, dir, file): - fullPath = dir + "/" + file - return self.getIconImage(file, fullPath) - - def createThumbnail(self, dir, file): - fullPath = dir + "/" + file - try: - # Video thumbnail - if file.lower().endswith(self.vidsList): - fileHash = hashlib.sha256(str.encode(fullPath)).hexdigest() - hashImgPth = self.usrHome + "/.thumbnails/normal/" + fileHash + ".png" - - if isfile(hashImgPth) == False: - self.generateVideoThumbnail(fullPath, hashImgPth) - - thumbnl = self.createScaledImage(hashImgPth, self.viIconWH) - if thumbnl == None: # If no icon whatsoever, return internal default - thumbnl = GdkPixbuf.Pixbuf.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - - return thumbnl - except Exception as e: - print("Thumbnail generation issue:") - print( repr(e) ) - return GdkPixbuf.Pixbuf.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - - - def getIconImage(self, file, fullPath): - try: - thumbnl = None - - # Video icon - if file.lower().endswith(self.vidsList): - thumbnl = GdkPixbuf.Pixbuf.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - # Image Icon - elif file.lower().endswith(self.imagesList): - thumbnl = self.createScaledImage(fullPath, self.viIconWH) - # .desktop file parsing - elif fullPath.lower().endswith( ('.desktop',) ): - thumbnl = self.parseDesktopFiles(fullPath) - - return thumbnl - except Exception as e: - print("Icon generation issue:") - print( repr(e) ) - return GdkPixbuf.Pixbuf.new_from_file(self.INTERNAL_ICON_PTH) - - def parseDesktopFiles(self, fullPath): - try: - xdgObj = DesktopEntry(fullPath) - icon = xdgObj.getIcon() - altIconPath = "" - - if "steam" in icon: - steamIconsDir = self.usrHome + "/.thumbnails/steam_icons/" - name = xdgObj.getName() - fileHash = hashlib.sha256(str.encode(name)).hexdigest() - - if isdir(steamIconsDir) == False: - os.mkdir(steamIconsDir) - - hashImgPth = steamIconsDir + fileHash + ".jpg" - if isfile(hashImgPth) == True: - # Use video sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) - - execStr = xdgObj.getExec() - parts = execStr.split("steam://rungameid/") - id = parts[len(parts) - 1] - imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg" - proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink]) - proc.wait() - - # Use video thumbnail sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) - elif os.path.exists(icon): - return self.createScaledImage(icon, self.systemIconImageWH) - else: - iconsDirs = ["/usr/share/pixmaps", "/usr/share/icons", self.usrHome + "/.icons" ,] - altIconPath = "" - - for iconsDir in iconsDirs: - altIconPath = self.traverseIconsFolder(iconsDir, icon) - if altIconPath != "": - break - - return self.createScaledImage(altIconPath, self.systemIconImageWH) - except Exception as e: - print(".desktop icon generation issue:") - print( repr(e) ) - return None - - - def traverseIconsFolder(self, path, icon): - altIconPath = "" - - for (dirpath, dirnames, filenames) in os.walk(path): - for file in filenames: - appNM = "application-x-" + icon - if icon in file or appNM in file: - altIconPath = dirpath + "/" + file - break - - return altIconPath - - - def createScaledImage(self, path, wxh): - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) - scaledPixBuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default - return scaledPixBuf - except Exception as e: - print("Image Scaling Issue:") - print( repr(e) ) - return None - - def createFromFile(self, path): - try: - return GdkPixbuf.Pixbuf.new_from_file(path) - except Exception as e: - print("Image from file Issue:") - print( repr(e) ) - return None - - def returnGenericIcon(self): - return GdkPixbuf.Pixbuf.new_from_file(self.INTERNAL_ICON_PTH) - - - def generateVideoThumbnail(self, fullPath, hashImgPth): - proc = None - try: - # Stream duration - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Stream duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Get frame roughly 35% through video - grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) - command = ["ffmpeg", "-ss", grabTime, "-an", "-i", fullPath, "-s", "320x180", "-vframes", "1", hashImgPth] - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - proc.wait() - except Exception as e: - print("Video thumbnail generation issue in thread:") - print( repr(e) ) diff --git a/src/Pytop/widgets/__init__.py b/src/Pytop/widgets/__init__.py index 148c91c..896cbb1 100644 --- a/src/Pytop/widgets/__init__.py +++ b/src/Pytop/widgets/__init__.py @@ -1,2 +1,3 @@ -from widgets.Grid import Grid -from widgets.Icon import Icon +""" +Widgets module +""" diff --git a/src/Pytop/widgets/Grid.py b/src/Pytop/widgets/grid.py similarity index 85% rename from src/Pytop/widgets/Grid.py rename to src/Pytop/widgets/grid.py index 7f7de0d..392d443 100644 --- a/src/Pytop/widgets/Grid.py +++ b/src/Pytop/widgets/grid.py @@ -4,7 +4,7 @@ from os.path import isdir, isfile, join from os import listdir -# Gtk imports +# Lib imports import gi gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') @@ -17,8 +17,8 @@ from gi.repository import GdkPixbuf # Application imports -from .Icon import Icon -from utils.FileHandler import FileHandler +from .icon import Icon +from utils.file_handler import FileHandler def threaded(fn): @@ -28,19 +28,19 @@ def threaded(fn): class Grid: - def __init__(self, grid, settings): - self.grid = grid - self.settings = settings + def __init__(self, _grid, _settings): + self.grid = _grid + self.settings = _settings self.fileHandler = FileHandler(self.settings) - self.store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - self.usrHome = settings.returnUserHome() - self.hideHiddenFiles = settings.isHideHiddenFiles() - self.builder = settings.returnBuilder() - self.ColumnSize = settings.returnColumnSize() - self.vidsFilter = settings.returnVidsFilter() - self.imagesFilter = settings.returnImagesFilter() - self.iconFactory = Icon(settings) + self.store = Gtk.ListStore(GdkPixbuf.Pixbuf or None, str) + self.usrHome = self.settings.get_user_home() + self.hideHiddenFiles = self.settings.isHideHiddenFiles() + self.builder = self.settings.get_builder() + self.ColumnSize = self.settings.getColumnSize() + self.vidsFilter = self.settings.getVidsFilter() + self.imagesFilter = self.settings.getImagesFilter() + self.iconFactory = Icon(self.settings) self.selectedFiles = [] self.currentPath = "" @@ -90,16 +90,15 @@ class Grid: def generateGridIcons(self, dir, files): - icon = GdkPixbuf.Pixbuf.new_from_file(self.iconFactory.INTERNAL_ICON_PTH) for i, file in enumerate(files): - self.store.append([icon, file]) + self.store.append([None, file]) self.create_icon(i, dir, file) @threaded def create_icon(self, i, dir, file): - icon = self.iconFactory.createIcon(dir, file) - fpath = dir + "/" + file + icon = self.iconFactory.create_icon(dir, file) + fpath = f"{dir}/{file}" GLib.idle_add(self.update_store, (i, icon, fpath,)) def update_store(self, item): @@ -107,7 +106,7 @@ class Grid: itr = self.store.get_iter(i) if not icon: - icon = self.get_system_thumbnail(fpath, self.iconFactory.systemIconImageWH[0]) + icon = self.get_system_thumbnail(fpath, self.iconFactory.SYS_ICON_WH[0]) if not icon: if fpath.endswith(".gif"): icon = GdkPixbuf.PixbufAnimation.get_static_image(fpath) @@ -150,11 +149,11 @@ class Grid: parentDir = os.path.abspath(os.path.join(dir, os.pardir)) self.currentPath = parentDir self.setNewDirectory(parentDir) - self.settings.saveSettings(parentDir) + self.self.settings.saveSettings(parentDir) elif isdir(file): self.currentPath = file self.setNewDirectory(self.currentPath) - self.settings.saveSettings(self.currentPath) + self.self.settings.saveSettings(self.currentPath) elif isfile(file): self.fileHandler.openFile(file) except Exception as e: @@ -162,7 +161,7 @@ class Grid: def iconSingleClick(self, widget, eve, rclicked_icon): try: - if eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == 1: + if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: self.selectedFiles.clear() items = widget.get_selected_items() model = widget.get_model() @@ -175,7 +174,7 @@ class Grid: file = dir + "/" + fileName self.selectedFiles.append(file) # Used for return to caller - elif eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == 3: + elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: input = self.builder.get_object("filenameInput") controls = self.builder.get_object("iconControlsWindow") iconsButtonBox = self.builder.get_object("iconsButtonBox") diff --git a/src/Pytop/widgets/icon.py b/src/Pytop/widgets/icon.py new file mode 100644 index 0000000..99af6ab --- /dev/null +++ b/src/Pytop/widgets/icon.py @@ -0,0 +1,93 @@ +# Python Imports +import os, subprocess, threading, hashlib +from os.path import isfile + +# Gtk imports +from gi.repository import GdkPixbuf + +# Application imports + +from .mixins.video_icon_mixin import VideoIconMixin +from .mixins.desktop_icon_mixin import DesktopIconMixin + + + +def threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs).start() + return wrapper + + +class Icon(DesktopIconMixin, VideoIconMixin): + def __init__(self, _settings): + self.settings = _settings + + self.FFMPG_THUMBNLR = self.settings.getThumbnailGenerator() + self.DEFAULT_ICONS = self.settings.getInternalIconsPth() + self.INTERNAL_ICON_PTH = self.settings.getDefaultIcon() + self.STEAM_ICONS_PTH = self.settings.getSteamIconsPth() + self.ABS_THUMBS_PTH = self.settings.getAbsThumbsPth() + self.ICON_DIRS = self.settings.getIconDirs() + self.VIDEO_ICON_WH = self.settings.getVIIconWH() + self.SYS_ICON_WH = self.settings.getSystemIconImageWH() + self.fvideos = self.settings.getVidsFilter() + self.fimages = self.settings.getImagesFilter() + + + def create_icon(self, dir, file): + full_path = f"{dir}/{file}" + return self.get_icon_image(dir, file, full_path) + + def get_icon_image(self, dir, file, full_path): + try: + thumbnl = None + + if file.lower().endswith(self.fvideos): # Video icon + thumbnl = self.create_thumbnail(dir, file) + elif file.lower().endswith(self.fimages): # Image Icon + thumbnl = self.create_scaled_image(full_path, self.VIDEO_ICON_WH) + elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing + thumbnl = self.parse_desktop_files(full_path) + + return thumbnl + except Exception as e: + print(repr(e)) + return None + + def create_thumbnail(self, dir, file): + full_path = f"{dir}/{file}" + try: + file_hash = hashlib.sha256(str.encode(full_path)).hexdigest() + hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg" + if isfile(hash_img_pth) == False: + self.generate_video_thumbnail(full_path, hash_img_pth) + + thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + if thumbnl == None: # If no icon whatsoever, return internal default + thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") + + return thumbnl + except Exception as e: + print("Thumbnail generation issue:") + print( repr(e) ) + return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") + + + def create_scaled_image(self, path, wxh): + try: + return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True) + except Exception as e: + print("Image Scaling Issue:") + print( repr(e) ) + return None + + def create_from_file(self, path): + try: + return GdkPixbuf.Pixbuf.new_from_file(path) + except Exception as e: + print("Image from file Issue:") + print( repr(e) ) + return None + + def return_generic_icon(self): + return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON) diff --git a/src/Pytop/widgets/mixins/__init__.py b/src/Pytop/widgets/mixins/__init__.py new file mode 100644 index 0000000..a6ce60d --- /dev/null +++ b/src/Pytop/widgets/mixins/__init__.py @@ -0,0 +1 @@ +from . import xdg diff --git a/src/Pytop/widgets/mixins/desktop_icon_mixin.py b/src/Pytop/widgets/mixins/desktop_icon_mixin.py new file mode 100644 index 0000000..2d3c30b --- /dev/null +++ b/src/Pytop/widgets/mixins/desktop_icon_mixin.py @@ -0,0 +1,65 @@ +# Python Imports +import os, subprocess, hashlib +from os.path import isfile + +# Gtk imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from .xdg.DesktopEntry import DesktopEntry + + +class DesktopIconMixin: + def parse_desktop_files(self, full_path): + try: + xdgObj = DesktopEntry(full_path) + icon = xdgObj.getIcon() + alt_icon_path = "" + + if "steam" in icon: + name = xdgObj.getName() + file_hash = hashlib.sha256(str.encode(name)).hexdigest() + hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg" + + if isfile(hash_img_pth) == True: + # Use video sizes since headers are bigger + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + + exec_str = xdgObj.getExec() + parts = exec_str.split("steam://rungameid/") + id = parts[len(parts) - 1] + imageLink = self.STEAM_BASE_URL + id + "/header.jpg" + proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink]) + proc.wait() + + # Use video thumbnail sizes since headers are bigger + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + elif os.path.exists(icon): + return self.create_scaled_image(icon, self.SYS_ICON_WH) + else: + alt_icon_path = "" + + for dir in self.ICON_DIRS: + alt_icon_path = self.traverse_icons_folder(dir, icon) + if alt_icon_path != "": + break + + return self.create_scaled_image(alt_icon_path, self.SYS_ICON_WH) + except Exception as e: + print(".desktop icon generation issue:") + print( repr(e) ) + return None + + def traverse_icons_folder(self, path, icon): + alt_icon_path = "" + + for (dirpath, dirnames, filenames) in os.walk(path): + for file in filenames: + appNM = "application-x-" + icon + if icon in file or appNM in file: + alt_icon_path = dirpath + "/" + file + break + + return alt_icon_path diff --git a/src/Pytop/widgets/mixins/video_icon_mixin.py b/src/Pytop/widgets/mixins/video_icon_mixin.py new file mode 100644 index 0000000..fc35e9d --- /dev/null +++ b/src/Pytop/widgets/mixins/video_icon_mixin.py @@ -0,0 +1,53 @@ +# Python Imports +import subprocess + +# Gtk imports + +# Application imports + + +class VideoIconMixin: + def generate_video_thumbnail(self, full_path, hash_img_pth): + try: + proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_pth]) + proc.wait() + except Exception as e: + self.logger.debug(repr(e)) + self.ffprobe_generate_video_thumbnail(full_path, hash_img_pth) + + + def ffprobe_generate_video_thumbnail(self, full_path, hash_img_pth): + proc = None + try: + # Stream duration + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Stream duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Get frame roughly 35% through video + grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) + command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_pth] + proc = subprocess.Popen(command, stdout=subprocess.PIPE) + proc.wait() + except Exception as e: + print("Video thumbnail generation issue in thread:") + print( repr(e) ) + self.logger.debug(repr(e)) diff --git a/src/Pytop/widgets/mixins/xdg/BaseDirectory.py b/src/Pytop/widgets/mixins/xdg/BaseDirectory.py new file mode 100644 index 0000000..a7c31b1 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/BaseDirectory.py @@ -0,0 +1,160 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log + +The freedesktop.org Base Directory specification provides a way for +applications to locate shared data and configuration: + + http://standards.freedesktop.org/basedir-spec/ + +(based on version 0.6) + +This module can be used to load and save from and to these directories. + +Typical usage: + + from rox import basedir + + for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): + print "Load settings from", dir + + dir = basedir.save_config_path('mydomain.org', 'MyProg') + print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" + +Note: see the rox.Options module for a higher-level API for managing options. +""" + +import os, stat + +_home = os.path.expanduser('~') +xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ + os.path.join(_home, '.local', 'share') + +xdg_data_dirs = [xdg_data_home] + \ + (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':') + +xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ + os.path.join(_home, '.config') + +xdg_config_dirs = [xdg_config_home] + \ + (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':') + +xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \ + os.path.join(_home, '.cache') + +xdg_data_dirs = [x for x in xdg_data_dirs if x] +xdg_config_dirs = [x for x in xdg_config_dirs if x] + +def save_config_path(*resource): + """Ensure ``$XDG_CONFIG_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application. Use this + when saving configuration settings. + """ + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_config_home, resource) + if not os.path.isdir(path): + os.makedirs(path, 0o700) + return path + +def save_data_path(*resource): + """Ensure ``$XDG_DATA_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application or a shared + resource. Use this when saving or updating application data. + """ + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_data_home, resource) + if not os.path.isdir(path): + os.makedirs(path) + return path + +def save_cache_path(*resource): + """Ensure ``$XDG_CACHE_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application or a shared + resource.""" + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_cache_home, resource) + if not os.path.isdir(path): + os.makedirs(path) + return path + +def load_config_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + configuration search path. Information provided by earlier directories should + take precedence over later ones, and the user-specific config dir comes + first.""" + resource = os.path.join(*resource) + for config_dir in xdg_config_dirs: + path = os.path.join(config_dir, resource) + if os.path.exists(path): yield path + +def load_first_config(*resource): + """Returns the first result from load_config_paths, or None if there is nothing + to load.""" + for x in load_config_paths(*resource): + return x + return None + +def load_data_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + application data search path. Information provided by earlier directories + should take precedence over later ones.""" + resource = os.path.join(*resource) + for data_dir in xdg_data_dirs: + path = os.path.join(data_dir, resource) + if os.path.exists(path): yield path + +def get_runtime_dir(strict=True): + """Returns the value of $XDG_RUNTIME_DIR, a directory path. + + This directory is intended for 'user-specific non-essential runtime files + and other file objects (such as sockets, named pipes, ...)', and + 'communication and synchronization purposes'. + + As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not + set, with ``strict=True`` (the default), a KeyError is raised. With + ``strict=False``, PyXDG will create a fallback under /tmp for the current + user. This fallback does *not* provide the same guarantees as the + specification requires for the runtime directory. + + The strict default is deliberately conservative, so that application + developers can make a conscious decision to allow the fallback. + """ + try: + return os.environ['XDG_RUNTIME_DIR'] + except KeyError: + if strict: + raise + + import getpass + fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() + create = False + + try: + # This must be a real directory, not a symlink, so attackers can't + # point it elsewhere. So we use lstat to check it. + st = os.lstat(fallback) + except OSError as e: + import errno + if e.errno == errno.ENOENT: + create = True + else: + raise + else: + # The fallback must be a directory + if not stat.S_ISDIR(st.st_mode): + os.unlink(fallback) + create = True + # Must be owned by the user and not accessible by anyone else + elif (st.st_uid != os.getuid()) \ + or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)): + os.rmdir(fallback) + create = True + + if create: + os.mkdir(fallback, 0o700) + + return fallback diff --git a/src/Pytop/widgets/mixins/xdg/Config.py b/src/Pytop/widgets/mixins/xdg/Config.py new file mode 100644 index 0000000..3f5d654 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/Config.py @@ -0,0 +1,39 @@ +""" +Functions to configure Basic Settings +""" + +language = "C" +windowmanager = None +icon_theme = "hicolor" +icon_size = 48 +cache_time = 5 +root_mode = False + +def setWindowManager(wm): + global windowmanager + windowmanager = wm + +def setIconTheme(theme): + global icon_theme + icon_theme = theme + import xdg.IconTheme + xdg.IconTheme.themes = [] + +def setIconSize(size): + global icon_size + icon_size = size + +def setCacheTime(time): + global cache_time + cache_time = time + +def setLocale(lang): + import locale + lang = locale.normalize(lang) + locale.setlocale(locale.LC_ALL, lang) + import xdg.Locale + xdg.Locale.update(lang) + +def setRootMode(boolean): + global root_mode + root_mode = boolean diff --git a/src/Pytop/widgets/mixins/xdg/DesktopEntry.py b/src/Pytop/widgets/mixins/xdg/DesktopEntry.py new file mode 100644 index 0000000..803993e --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/DesktopEntry.py @@ -0,0 +1,435 @@ +""" +Complete implementation of the XDG Desktop Entry Specification +http://standards.freedesktop.org/desktop-entry-spec/ + +Not supported: +- Encoding: Legacy Mixed +- Does not check exec parameters +- Does not check URL's +- Does not completly validate deprecated/kde items +- Does not completly check categories +""" + +from .IniFile import IniFile +from . import Locale + +from .IniFile import is_ascii + +from .Exceptions import ParsingError +from .util import which +import os.path +import re +import warnings + +class DesktopEntry(IniFile): + "Class to parse and validate Desktop Entries" + + defaultGroup = 'Desktop Entry' + + def __init__(self, filename=None): + """Create a new DesktopEntry. + + If filename exists, it will be parsed as a desktop entry file. If not, + or if filename is None, a blank DesktopEntry is created. + """ + self.content = dict() + if filename and os.path.exists(filename): + self.parse(filename) + elif filename: + self.new(filename) + + def __str__(self): + return self.getName() + + def parse(self, file): + """Parse a desktop entry file. + + This can raise :class:`~xdg.Exceptions.ParsingError`, + :class:`~xdg.Exceptions.DuplicateGroupError` or + :class:`~xdg.Exceptions.DuplicateKeyError`. + """ + IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) + + def findTryExec(self): + """Looks in the PATH for the executable given in the TryExec field. + + Returns the full path to the executable if it is found, None if not. + Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present. + """ + tryexec = self.get('TryExec', strict=True) + return which(tryexec) + + # start standard keys + def getType(self): + return self.get('Type') + def getVersion(self): + """deprecated, use getVersionString instead """ + return self.get('Version', type="numeric") + def getVersionString(self): + return self.get('Version') + def getName(self): + return self.get('Name', locale=True) + def getGenericName(self): + return self.get('GenericName', locale=True) + def getNoDisplay(self): + return self.get('NoDisplay', type="boolean") + def getComment(self): + return self.get('Comment', locale=True) + def getIcon(self): + return self.get('Icon', locale=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getOnlyShowIn(self): + return self.get('OnlyShowIn', list=True) + def getNotShowIn(self): + return self.get('NotShowIn', list=True) + def getTryExec(self): + return self.get('TryExec') + def getExec(self): + return self.get('Exec') + def getPath(self): + return self.get('Path') + def getTerminal(self): + return self.get('Terminal', type="boolean") + def getMimeType(self): + """deprecated, use getMimeTypes instead """ + return self.get('MimeType', list=True, type="regex") + def getMimeTypes(self): + return self.get('MimeType', list=True) + def getCategories(self): + return self.get('Categories', list=True) + def getStartupNotify(self): + return self.get('StartupNotify', type="boolean") + def getStartupWMClass(self): + return self.get('StartupWMClass') + def getURL(self): + return self.get('URL') + # end standard keys + + # start kde keys + def getServiceTypes(self): + return self.get('ServiceTypes', list=True) + def getDocPath(self): + return self.get('DocPath') + def getKeywords(self): + return self.get('Keywords', list=True, locale=True) + def getInitialPreference(self): + return self.get('InitialPreference') + def getDev(self): + return self.get('Dev') + def getFSType(self): + return self.get('FSType') + def getMountPoint(self): + return self.get('MountPoint') + def getReadonly(self): + return self.get('ReadOnly', type="boolean") + def getUnmountIcon(self): + return self.get('UnmountIcon', locale=True) + # end kde keys + + # start deprecated keys + def getMiniIcon(self): + return self.get('MiniIcon', locale=True) + def getTerminalOptions(self): + return self.get('TerminalOptions') + def getDefaultApp(self): + return self.get('DefaultApp') + def getProtocols(self): + return self.get('Protocols', list=True) + def getExtensions(self): + return self.get('Extensions', list=True) + def getBinaryPattern(self): + return self.get('BinaryPattern') + def getMapNotify(self): + return self.get('MapNotify') + def getEncoding(self): + return self.get('Encoding') + def getSwallowTitle(self): + return self.get('SwallowTitle', locale=True) + def getSwallowExec(self): + return self.get('SwallowExec') + def getSortOrder(self): + return self.get('SortOrder', list=True) + def getFilePattern(self): + return self.get('FilePattern', type="regex") + def getActions(self): + return self.get('Actions', list=True) + # end deprecated keys + + # desktop entry edit stuff + def new(self, filename): + """Make this instance into a new, blank desktop entry. + + If filename has a .desktop extension, Type is set to Application. If it + has a .directory extension, Type is Directory. Other extensions will + cause :class:`~xdg.Exceptions.ParsingError` to be raised. + """ + if os.path.splitext(filename)[1] == ".desktop": + type = "Application" + elif os.path.splitext(filename)[1] == ".directory": + type = "Directory" + else: + raise ParsingError("Unknown extension", filename) + + self.content = dict() + self.addGroup(self.defaultGroup) + self.set("Type", type) + self.filename = filename + # end desktop entry edit stuff + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Desktop Entry": + self.warnings.append('[KDE Desktop Entry]-Header is deprecated') + + # file extension + if self.fileExtension == ".kdelnk": + self.warnings.append("File extension .kdelnk is deprecated") + elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": + self.warnings.append('Unknown File extension') + + # Type + try: + self.type = self.content[self.defaultGroup]["Type"] + except KeyError: + self.errors.append("Key 'Type' is missing") + + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \ + or (re.match("^X-", group) and is_ascii(group))): + self.errors.append("Invalid Group name: %s" % group) + else: + #OnlyShowIn and NotShowIn + if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): + self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") + + def checkKey(self, key, value, group): + # standard keys + if key == "Type": + if value == "ServiceType" or value == "Service" or value == "FSDevice": + self.warnings.append("Type=%s is a KDE extension" % key) + elif value == "MimeType": + self.warnings.append("Type=MimeType is deprecated") + elif not (value == "Application" or value == "Link" or value == "Directory"): + self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) + + if self.fileExtension == ".directory" and not value == "Directory": + self.warnings.append("File extension is .directory, but Type is '%s'" % value) + elif self.fileExtension == ".desktop" and value == "Directory": + self.warnings.append("Files with Type=Directory should have the extension .directory") + + if value == "Application": + if "Exec" not in self.content[group]: + self.warnings.append("Type=Application needs 'Exec' key") + if value == "Link": + if "URL" not in self.content[group]: + self.warnings.append("Type=Link needs 'URL' key") + + elif key == "Version": + self.checkValue(key, value) + + elif re.match("^Name"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^GenericName"+xdg.Locale.regex+"$", key): + pass # locale string + + elif key == "NoDisplay": + self.checkValue(key, value, type="boolean") + + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^Icon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + + elif key == "OnlyShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "NotShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "TryExec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Exec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Path": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Terminal": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "Actions": + self.checkValue(key, value, list=True) + self.checkType(key, "Application") + + elif key == "MimeType": + self.checkValue(key, value, list=True) + self.checkType(key, "Application") + + elif key == "Categories": + self.checkValue(key, value) + self.checkType(key, "Application") + self.checkCategories(value) + + elif re.match("^Keywords"+xdg.Locale.regex+"$", key): + self.checkValue(key, value, type="localestring", list=True) + self.checkType(key, "Application") + + elif key == "StartupNotify": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "StartupWMClass": + self.checkType(key, "Application") + + elif key == "URL": + self.checkValue(key, value) + self.checkType(key, "URL") + + # kde extensions + elif key == "ServiceTypes": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "DocPath": + self.checkValue(key, value) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "InitialPreference": + self.checkValue(key, value, type="numeric") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "Dev": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "FSType": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "MountPoint": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "ReadOnly": + self.checkValue(key, value, type="boolean") + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + # deprecated keys + elif key == "Encoding": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "TerminalOptions": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "DefaultApp": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Protocols": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Extensions": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "BinaryPattern": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "MapNotify": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SwallowExec": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "FilePattern": + self.checkValue(key, value, type="regex", list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SortOrder": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + # "X-" extensions + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + + else: + self.errors.append("Invalid key: %s" % key) + + def checkType(self, key, type): + if not self.getType() == type: + self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) + + def checkOnlyShowIn(self, value): + values = self.getList(value) + valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", + "XFCE", "Old"] + for item in values: + if item not in valid and item[0:2] != "X-": + self.errors.append("'%s' is not a registered OnlyShowIn value" % item); + + def checkCategories(self, value): + values = self.getList(value) + + main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] + if not any(item in main for item in values): + self.errors.append("Missing main category") + + additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly'] + allcategories = additional + main + + for item in values: + if item not in allcategories and not item.startswith("X-"): + self.errors.append("'%s' is not a registered Category" % item); + + def checkCategorie(self, value): + """Deprecated alias for checkCategories - only exists for backwards + compatibility. + """ + warnings.warn("checkCategorie is deprecated, use checkCategories", + DeprecationWarning) + return self.checkCategories(value) diff --git a/src/Pytop/widgets/mixins/xdg/Exceptions.py b/src/Pytop/widgets/mixins/xdg/Exceptions.py new file mode 100644 index 0000000..7096b61 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/Exceptions.py @@ -0,0 +1,84 @@ +""" +Exception Classes for the xdg package +""" + +debug = False + +class Error(Exception): + """Base class for exceptions defined here.""" + def __init__(self, msg): + self.msg = msg + Exception.__init__(self, msg) + def __str__(self): + return self.msg + +class ValidationError(Error): + """Raised when a file fails to validate. + + The filename is the .file attribute. + """ + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) + +class ParsingError(Error): + """Raised when a file cannot be parsed. + + The filename is the .file attribute. + """ + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) + +class NoKeyError(Error): + """Raised when trying to access a nonexistant key in an INI-style file. + + Attributes are .key, .group and .file. + """ + def __init__(self, key, group, file): + Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + self.file = file + +class DuplicateKeyError(Error): + """Raised when the same key occurs twice in an INI-style file. + + Attributes are .key, .group and .file. + """ + def __init__(self, key, group, file): + Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + self.file = file + +class NoGroupError(Error): + """Raised when trying to access a nonexistant group in an INI-style file. + + Attributes are .group and .file. + """ + def __init__(self, group, file): + Error.__init__(self, "No group: %s in file %s" % (group, file)) + self.group = group + self.file = file + +class DuplicateGroupError(Error): + """Raised when the same key occurs twice in an INI-style file. + + Attributes are .group and .file. + """ + def __init__(self, group, file): + Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) + self.group = group + self.file = file + +class NoThemeError(Error): + """Raised when trying to access a nonexistant icon theme. + + The name of the theme is the .theme attribute. + """ + def __init__(self, theme): + Error.__init__(self, "No such icon-theme: %s" % theme) + self.theme = theme diff --git a/src/Pytop/widgets/mixins/xdg/IconTheme.py b/src/Pytop/widgets/mixins/xdg/IconTheme.py new file mode 100644 index 0000000..2ff3c05 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/IconTheme.py @@ -0,0 +1,445 @@ +""" +Complete implementation of the XDG Icon Spec +http://standards.freedesktop.org/icon-theme-spec/ +""" + +import os, time +import re + +from . import IniFile, Config +from .IniFile import is_ascii +from .BaseDirectory import xdg_data_dirs +from .Exceptions import NoThemeError, debug + + +class IconTheme(IniFile): + "Class to parse and validate IconThemes" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + return self.name + + def parse(self, file): + IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) + self.dir = os.path.dirname(file) + (nil, self.name) = os.path.split(self.dir) + + def getDir(self): + return self.dir + + # Standard Keys + def getName(self): + return self.get('Name', locale=True) + def getComment(self): + return self.get('Comment', locale=True) + def getInherits(self): + return self.get('Inherits', list=True) + def getDirectories(self): + return self.get('Directories', list=True) + def getScaledDirectories(self): + return self.get('ScaledDirectories', list=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getExample(self): + return self.get('Example') + + # Per Directory Keys + def getSize(self, directory): + return self.get('Size', type="integer", group=directory) + def getContext(self, directory): + return self.get('Context', group=directory) + def getType(self, directory): + value = self.get('Type', group=directory) + if value: + return value + else: + return "Threshold" + def getMaxSize(self, directory): + value = self.get('MaxSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getMinSize(self, directory): + value = self.get('MinSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getThreshold(self, directory): + value = self.get('Threshold', type="integer", group=directory) + if value or value == 0: + return value + else: + return 2 + + def getScale(self, directory): + value = self.get('Scale', type="integer", group=directory) + return value or 1 + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Icon Theme": + self.warnings.append('[KDE Icon Theme]-Header is deprecated') + + # file extension + if self.fileExtension == ".theme": + pass + elif self.fileExtension == ".desktop": + self.warnings.append('.desktop fileExtension is deprecated') + else: + self.warnings.append('Unknown File extension') + + # Check required keys + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + # Comment + try: + self.comment = self.content[self.defaultGroup]["Comment"] + except KeyError: + self.errors.append("Key 'Comment' is missing") + + # Directories + try: + self.directories = self.content[self.defaultGroup]["Directories"] + except KeyError: + self.errors.append("Key 'Directories' is missing") + + def checkGroup(self, group): + # check if group header is valid + if group == self.defaultGroup: + try: + self.name = self.content[group]["Name"] + except KeyError: + self.errors.append("Key 'Name' in Group '%s' is missing" % group) + try: + self.name = self.content[group]["Comment"] + except KeyError: + self.errors.append("Key 'Comment' in Group '%s' is missing" % group) + elif group in self.getDirectories(): + try: + self.type = self.content[group]["Type"] + except KeyError: + self.type = "Threshold" + try: + self.name = self.content[group]["Size"] + except KeyError: + self.errors.append("Key 'Size' in Group '%s' is missing" % group) + elif not (re.match(r"^\[X-", group) and is_ascii(group)): + self.errors.append("Invalid Group name: %s" % group) + + def checkKey(self, key, value, group): + # standard keys + if group == self.defaultGroup: + if re.match("^Name"+xdg.Locale.regex+"$", key): + pass + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass + elif key == "Inherits": + self.checkValue(key, value, list=True) + elif key == "Directories": + self.checkValue(key, value, list=True) + elif key == "ScaledDirectories": + self.checkValue(key, value, list=True) + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + elif key == "Example": + self.checkValue(key, value) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + elif group in self.getDirectories(): + if key == "Size": + self.checkValue(key, value, type="integer") + elif key == "Context": + self.checkValue(key, value) + elif key == "Type": + self.checkValue(key, value) + if value not in ["Fixed", "Scalable", "Threshold"]: + self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) + elif key == "MaxSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) + elif key == "MinSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) + elif key == "Threshold": + self.checkValue(key, value, type="integer") + if self.type != "Threshold": + self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) + elif key == "Scale": + self.checkValue(key, value, type="integer") + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + +class IconData(IniFile): + "Class to parse and validate IconData Files" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + displayname = self.getDisplayName() + if displayname: + return "" % displayname + else: + return "" + + def parse(self, file): + IniFile.parse(self, file, ["Icon Data"]) + + # Standard Keys + def getDisplayName(self): + """Retrieve the display name from the icon data, if one is specified.""" + return self.get('DisplayName', locale=True) + def getEmbeddedTextRectangle(self): + """Retrieve the embedded text rectangle from the icon data as a list of + numbers (x0, y0, x1, y1), if it is specified.""" + return self.get('EmbeddedTextRectangle', type="integer", list=True) + def getAttachPoints(self): + """Retrieve the anchor points for overlays & emblems from the icon data, + as a list of co-ordinate pairs, if they are specified.""" + return self.get('AttachPoints', type="point", list=True) + + # validation stuff + def checkExtras(self): + # file extension + if self.fileExtension != ".icon": + self.warnings.append('Unknown File extension') + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or (re.match(r"^\[X-", group) and is_ascii(group))): + self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) + + def checkKey(self, key, value, group): + # standard keys + if re.match("^DisplayName"+xdg.Locale.regex+"$", key): + pass + elif key == "EmbeddedTextRectangle": + self.checkValue(key, value, type="integer", list=True) + elif key == "AttachPoints": + self.checkValue(key, value, type="point", list=True) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + + +icondirs = [] +for basedir in xdg_data_dirs: + icondirs.append(os.path.join(basedir, "icons")) + icondirs.append(os.path.join(basedir, "pixmaps")) +icondirs.append(os.path.expanduser("~/.icons")) + +# just cache variables, they give a 10x speed improvement +themes = [] +theme_cache = {} +dir_cache = {} +icon_cache = {} + +def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): + """Get the path to a specified icon. + + size : + Icon size in pixels. Defaults to ``xdg.Config.icon_size``. + theme : + Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't + found in the specified theme, it will be looked up in the basic 'hicolor' + theme. + extensions : + List of preferred file extensions. + + Example:: + + >>> getIconPath("inkscape", 32) + '/usr/share/icons/hicolor/32x32/apps/inkscape.png' + """ + + global themes + + if size == None: + size = xdg.Config.icon_size + if theme == None: + theme = xdg.Config.icon_theme + + # if we have an absolute path, just return it + if os.path.isabs(iconname): + return iconname + + # check if it has an extension and strip it + if os.path.splitext(iconname)[1][1:] in extensions: + iconname = os.path.splitext(iconname)[0] + + # parse theme files + if (themes == []) or (themes[0].name != theme): + themes = list(__get_themes(theme)) + + # more caching (icon looked up in the last 5 seconds?) + tmp = (iconname, size, theme, tuple(extensions)) + try: + timestamp, icon = icon_cache[tmp] + except KeyError: + pass + else: + if (time.time() - timestamp) >= xdg.Config.cache_time: + del icon_cache[tmp] + else: + return icon + + for thme in themes: + icon = LookupIcon(iconname, size, thme, extensions) + if icon: + icon_cache[tmp] = (time.time(), icon) + return icon + + # cache stuff again (directories looked up in the last 5 seconds?) + for directory in icondirs: + if (directory not in dir_cache \ + or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \ + and dir_cache[directory][2] < os.path.getmtime(directory))) \ + and os.path.isdir(directory): + dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory)) + + for dir, values in dir_cache.items(): + for extension in extensions: + try: + if iconname + "." + extension in values[0]: + icon = os.path.join(dir, iconname + "." + extension) + icon_cache[tmp] = [time.time(), icon] + return icon + except UnicodeDecodeError as e: + if debug: + raise e + else: + pass + + # we haven't found anything? "hicolor" is our fallback + if theme != "hicolor": + icon = getIconPath(iconname, size, "hicolor") + icon_cache[tmp] = [time.time(), icon] + return icon + +def getIconData(path): + """Retrieve the data from the .icon file corresponding to the given file. If + there is no .icon file, it returns None. + + Example:: + + getIconData("/usr/share/icons/Tango/scalable/places/folder.svg") + """ + if os.path.isfile(path): + icon_file = os.path.splitext(path)[0] + ".icon" + if os.path.isfile(icon_file): + data = IconData() + data.parse(icon_file) + return data + +def __get_themes(themename): + """Generator yielding IconTheme objects for a specified theme and any themes + from which it inherits. + """ + for dir in icondirs: + theme_file = os.path.join(dir, themename, "index.theme") + if os.path.isfile(theme_file): + break + theme_file = os.path.join(dir, themename, "index.desktop") + if os.path.isfile(theme_file): + break + else: + if debug: + raise NoThemeError(themename) + return + + theme = IconTheme() + theme.parse(theme_file) + yield theme + for subtheme in theme.getInherits(): + for t in __get_themes(subtheme): + yield t + +def LookupIcon(iconname, size, theme, extensions): + # look for the cache + if theme.name not in theme_cache: + theme_cache[theme.name] = [] + theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup + theme_cache[theme.name].append(0) # [1] mtime + theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] + + # cache stuff (directory lookuped up the in the last 5 seconds?) + if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time: + theme_cache[theme.name][0] = time.time() + for subdir in theme.getDirectories(): + for directory in icondirs: + dir = os.path.join(directory,theme.name,subdir) + if (dir not in theme_cache[theme.name][2] \ + or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ + and subdir != "" \ + and os.path.isdir(dir): + theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)] + theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) + + for dir, values in theme_cache[theme.name][2].items(): + if DirectoryMatchesSize(values[0], size, theme): + for extension in extensions: + if iconname + "." + extension in values[1]: + return os.path.join(dir, iconname + "." + extension) + + minimal_size = 2**31 + closest_filename = "" + for dir, values in theme_cache[theme.name][2].items(): + distance = DirectorySizeDistance(values[0], size, theme) + if distance < minimal_size: + for extension in extensions: + if iconname + "." + extension in values[1]: + closest_filename = os.path.join(dir, iconname + "." + extension) + minimal_size = distance + + return closest_filename + +def DirectoryMatchesSize(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return Size == iconsize + elif Type == "Scaleable": + return MinSize <= iconsize <= MaxSize + elif Type == "Threshold": + return Size - Threshold <= iconsize <= Size + Threshold + +def DirectorySizeDistance(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return abs(Size - iconsize) + elif Type == "Scalable": + if iconsize < MinSize: + return MinSize - iconsize + elif iconsize > MaxSize: + return MaxSize - iconsize + return 0 + elif Type == "Threshold": + if iconsize < Size - Threshold: + return MinSize - iconsize + elif iconsize > Size + Threshold: + return iconsize - MaxSize + return 0 diff --git a/src/Pytop/widgets/mixins/xdg/IniFile.py b/src/Pytop/widgets/mixins/xdg/IniFile.py new file mode 100644 index 0000000..74ab858 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/IniFile.py @@ -0,0 +1,419 @@ +""" +Base Class for DesktopEntry, IconTheme and IconData +""" + +import re, os, stat, io +from .Exceptions import (ParsingError, DuplicateGroupError, NoGroupError, + NoKeyError, DuplicateKeyError, ValidationError, + debug) +# import xdg.Locale +from . import Locale +from .util import u + +def is_ascii(s): + """Return True if a string consists entirely of ASCII characters.""" + try: + s.encode('ascii', 'strict') + return True + except UnicodeError: + return False + +class IniFile: + defaultGroup = '' + fileExtension = '' + + filename = '' + + tainted = False + + def __init__(self, filename=None): + self.content = dict() + if filename: + self.parse(filename) + + def __cmp__(self, other): + return cmp(self.content, other.content) + + def parse(self, filename, headers=None): + '''Parse an INI file. + + headers -- list of headers the parser will try to select as a default header + ''' + # for performance reasons + content = self.content + + if not os.path.isfile(filename): + raise ParsingError("File not found", filename) + + try: + # The content should be UTF-8, but legacy files can have other + # encodings, including mixed encodings in one file. We don't attempt + # to decode them, but we silence the errors. + fd = io.open(filename, 'r', encoding='utf-8', errors='replace') + except IOError as e: + if debug: + raise e + else: + return + + # parse file + for line in fd: + line = line.strip() + # empty line + if not line: + continue + # comment + elif line[0] == '#': + continue + # new group + elif line[0] == '[': + currentGroup = line.lstrip("[").rstrip("]") + if debug and self.hasGroup(currentGroup): + raise DuplicateGroupError(currentGroup, filename) + else: + content[currentGroup] = {} + # key + else: + try: + key, value = line.split("=", 1) + except ValueError: + raise ParsingError("Invalid line: " + line, filename) + + key = key.strip() # Spaces before/after '=' should be ignored + try: + if debug and self.hasKey(key, currentGroup): + raise DuplicateKeyError(key, currentGroup, filename) + else: + content[currentGroup][key] = value.strip() + except (IndexError, UnboundLocalError): + raise ParsingError("Parsing error on key, group missing", filename) + + fd.close() + + self.filename = filename + self.tainted = False + + # check header + if headers: + for header in headers: + if header in content: + self.defaultGroup = header + break + else: + raise ParsingError("[%s]-Header missing" % headers[0], filename) + + # start stuff to access the keys + def get(self, key, group=None, locale=False, type="string", list=False, strict=False): + # set default group + if not group: + group = self.defaultGroup + + # return key (with locale) + if (group in self.content) and (key in self.content[group]): + if locale: + value = self.content[group][self.__addLocale(key, group)] + else: + value = self.content[group][key] + else: + if strict or debug: + if group not in self.content: + raise NoGroupError(group, self.filename) + elif key not in self.content[group]: + raise NoKeyError(key, group, self.filename) + else: + value = "" + + if list == True: + values = self.getList(value) + result = [] + else: + values = [value] + + for value in values: + if type == "boolean": + value = self.__getBoolean(value) + elif type == "integer": + try: + value = int(value) + except ValueError: + value = 0 + elif type == "numeric": + try: + value = float(value) + except ValueError: + value = 0.0 + elif type == "regex": + value = re.compile(value) + elif type == "point": + x, y = value.split(",") + value = int(x), int(y) + + if list == True: + result.append(value) + else: + result = value + + return result + # end stuff to access the keys + + # start subget + def getList(self, string): + if re.search(r"(? 0: + key = key + "[" + xdg.Locale.langs[0] + "]" + + try: + self.content[group][key] = value + except KeyError: + raise NoGroupError(group, self.filename) + + self.tainted = (value == self.get(key, group)) + + def addGroup(self, group): + if self.hasGroup(group): + if debug: + raise DuplicateGroupError(group, self.filename) + else: + self.content[group] = {} + self.tainted = True + + def removeGroup(self, group): + existed = group in self.content + if existed: + del self.content[group] + self.tainted = True + else: + if debug: + raise NoGroupError(group, self.filename) + return existed + + def removeKey(self, key, group=None, locales=True): + # set default group + if not group: + group = self.defaultGroup + + try: + if locales: + for name in list(self.content[group]): + if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: + del self.content[group][name] + value = self.content[group].pop(key) + self.tainted = True + return value + except KeyError as e: + if debug: + if e == group: + raise NoGroupError(group, self.filename) + else: + raise NoKeyError(key, group, self.filename) + else: + return "" + + # misc + def groups(self): + return self.content.keys() + + def hasGroup(self, group): + return group in self.content + + def hasKey(self, key, group=None): + # set default group + if not group: + group = self.defaultGroup + + return key in self.content[group] + + def getFileName(self): + return self.filename diff --git a/src/Pytop/widgets/mixins/xdg/Locale.py b/src/Pytop/widgets/mixins/xdg/Locale.py new file mode 100644 index 0000000..d0a70d2 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/Locale.py @@ -0,0 +1,79 @@ +""" +Helper Module for Locale settings + +This module is based on a ROX module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log +""" + +import os +from locale import normalize + +regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?" + +def _expand_lang(locale): + locale = normalize(locale) + COMPONENT_CODESET = 1 << 0 + COMPONENT_MODIFIER = 1 << 1 + COMPONENT_TERRITORY = 1 << 2 + # split up the locale into its base components + mask = 0 + pos = locale.find('@') + if pos >= 0: + modifier = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_MODIFIER + else: + modifier = '' + pos = locale.find('.') + codeset = '' + if pos >= 0: + locale = locale[:pos] + pos = locale.find('_') + if pos >= 0: + territory = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_TERRITORY + else: + territory = '' + language = locale + ret = [] + for i in range(mask+1): + if not (i & ~mask): # if all components for this combo exist ... + val = language + if i & COMPONENT_TERRITORY: val += territory + if i & COMPONENT_CODESET: val += codeset + if i & COMPONENT_MODIFIER: val += modifier + ret.append(val) + ret.reverse() + return ret + +def expand_languages(languages=None): + # Get some reasonable defaults for arguments that were not supplied + if languages is None: + languages = [] + for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): + val = os.environ.get(envar) + if val: + languages = val.split(':') + break + #if 'C' not in languages: + # languages.append('C') + + # now normalize and expand the languages + nelangs = [] + for lang in languages: + for nelang in _expand_lang(lang): + if nelang not in nelangs: + nelangs.append(nelang) + return nelangs + +def update(language=None): + global langs + if language: + langs = expand_languages([language]) + else: + langs = expand_languages() + +langs = [] +update() diff --git a/src/Pytop/widgets/mixins/xdg/Menu.py b/src/Pytop/widgets/mixins/xdg/Menu.py new file mode 100644 index 0000000..fcf1ac1 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/Menu.py @@ -0,0 +1,1125 @@ +""" +Implementation of the XDG Menu Specification +http://standards.freedesktop.org/menu-spec/ + +Example code: + +from xdg.Menu import parse, Menu, MenuEntry + +def print_menu(menu, tab=0): + for submenu in menu.Entries: + if isinstance(submenu, Menu): + print ("\t" * tab) + unicode(submenu) + print_menu(submenu, tab+1) + elif isinstance(submenu, MenuEntry): + print ("\t" * tab) + unicode(submenu.DesktopEntry) + +print_menu(parse()) +""" + +import os +import locale +import subprocess +import ast +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from .BaseDirectory import xdg_data_dirs, xdg_config_dirs +from . import DesktopEntry, Locale, Config +from .Exceptions import ParsingError +from .util import PY3 + + +def _strxfrm(s): + """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. + + See Python bug #2481. + """ + if (not PY3) and isinstance(s, unicode): + s = s.encode('utf-8') + return locale.strxfrm(s) + + +DELETED = "Deleted" +NO_DISPLAY = "NoDisplay" +HIDDEN = "Hidden" +EMPTY = "Empty" +NOT_SHOW_IN = "NotShowIn" +NO_EXEC = "NoExec" + + +class Menu: + """Menu containing sub menus under menu.Entries + + Contains both Menu and MenuEntry items. + """ + def __init__(self): + # Public stuff + self.Name = "" + self.Directory = None + self.Entries = [] + self.Doc = "" + self.Filename = "" + self.Depth = 0 + self.Parent = None + self.NotInXml = False + + # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN + self.Show = True + self.Visible = 0 + + # Private stuff, only needed for parsing + self.AppDirs = [] + self.DefaultLayout = None + self.Deleted = None + self.Directories = [] + self.DirectoryDirs = [] + self.Layout = None + self.MenuEntries = [] + self.Moves = [] + self.OnlyUnallocated = None + self.Rules = [] + self.Submenus = [] + + def __str__(self): + return self.Name + + def __add__(self, other): + for dir in other.AppDirs: + self.AppDirs.append(dir) + + for dir in other.DirectoryDirs: + self.DirectoryDirs.append(dir) + + for directory in other.Directories: + self.Directories.append(directory) + + if other.Deleted is not None: + self.Deleted = other.Deleted + + if other.OnlyUnallocated is not None: + self.OnlyUnallocated = other.OnlyUnallocated + + if other.Layout: + self.Layout = other.Layout + + if other.DefaultLayout: + self.DefaultLayout = other.DefaultLayout + + for rule in other.Rules: + self.Rules.append(rule) + + for move in other.Moves: + self.Moves.append(move) + + for submenu in other.Submenus: + self.addSubmenu(submenu) + + return self + + # FIXME: Performance: cache getName() + def __cmp__(self, other): + return locale.strcoll(self.getName(), other.getName()) + + def _key(self): + """Key function for locale-aware sorting.""" + return _strxfrm(self.getName()) + + def __lt__(self, other): + try: + other = other._key() + except AttributeError: + pass + return self._key() < other + + def __eq__(self, other): + try: + return self.Name == unicode(other) + except NameError: # unicode() becomes str() in Python 3 + return self.Name == str(other) + + """ PUBLIC STUFF """ + def getEntries(self, show_hidden=False): + """Interator for a list of Entries visible to the user.""" + for entry in self.Entries: + if show_hidden: + yield entry + elif entry.Show is True: + yield entry + + # FIXME: Add searchEntry/seaqrchMenu function + # search for name/comment/genericname/desktopfileid + # return multiple items + + def getMenuEntry(self, desktopfileid, deep=False): + """Searches for a MenuEntry with a given DesktopFileID.""" + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID == desktopfileid: + return menuentry + if deep: + for submenu in self.Submenus: + submenu.getMenuEntry(desktopfileid, deep) + + def getMenu(self, path): + """Searches for a Menu with a given path.""" + array = path.split("/", 1) + for submenu in self.Submenus: + if submenu.Name == array[0]: + if len(array) > 1: + return submenu.getMenu(array[1]) + else: + return submenu + + def getPath(self, org=False, toplevel=False): + """Returns this menu's path in the menu structure.""" + parent = self + names = [] + while 1: + if org: + names.append(parent.Name) + else: + names.append(parent.getName()) + if parent.Depth > 0: + parent = parent.Parent + else: + break + names.reverse() + path = "" + if not toplevel: + names.pop(0) + for name in names: + path = os.path.join(path, name) + return path + + def getName(self): + """Returns the menu's localised name.""" + try: + return self.Directory.DesktopEntry.getName() + except AttributeError: + return self.Name + + def getGenericName(self): + """Returns the menu's generic name.""" + try: + return self.Directory.DesktopEntry.getGenericName() + except AttributeError: + return "" + + def getComment(self): + """Returns the menu's comment text.""" + try: + return self.Directory.DesktopEntry.getComment() + except AttributeError: + return "" + + def getIcon(self): + """Returns the menu's icon, filename or simple name""" + try: + return self.Directory.DesktopEntry.getIcon() + except AttributeError: + return "" + + def sort(self): + self.Entries = [] + self.Visible = 0 + + for submenu in self.Submenus: + submenu.sort() + + _submenus = set() + _entries = set() + + for order in self.Layout.order: + if order[0] == "Filename": + _entries.add(order[1]) + elif order[0] == "Menuname": + _submenus.add(order[1]) + + for order in self.Layout.order: + if order[0] == "Separator": + separator = Separator(self) + if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): + separator.Show = False + self.Entries.append(separator) + elif order[0] == "Filename": + menuentry = self.getMenuEntry(order[1]) + if menuentry: + self.Entries.append(menuentry) + elif order[0] == "Menuname": + submenu = self.getMenu(order[1]) + if submenu: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + elif order[0] == "Merge": + if order[1] == "files" or order[1] == "all": + self.MenuEntries.sort() + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID not in _entries: + self.Entries.append(menuentry) + elif order[1] == "menus" or order[1] == "all": + self.Submenus.sort() + for submenu in self.Submenus: + if submenu.Name not in _submenus: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + + # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec + for entry in self.Entries: + entry.Show = True + self.Visible += 1 + if isinstance(entry, Menu): + if entry.Deleted is True: + entry.Show = DELETED + self.Visible -= 1 + elif isinstance(entry.Directory, MenuEntry): + if entry.Directory.DesktopEntry.getNoDisplay(): + entry.Show = NO_DISPLAY + self.Visible -= 1 + elif entry.Directory.DesktopEntry.getHidden(): + entry.Show = HIDDEN + self.Visible -= 1 + elif isinstance(entry, MenuEntry): + if entry.DesktopEntry.getNoDisplay(): + entry.Show = NO_DISPLAY + self.Visible -= 1 + elif entry.DesktopEntry.getHidden(): + entry.Show = HIDDEN + self.Visible -= 1 + elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): + entry.Show = NO_EXEC + self.Visible -= 1 + elif xdg.Config.windowmanager: + if (entry.DesktopEntry.OnlyShowIn != [] and ( + xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn + ) + ) or ( + xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn + ): + entry.Show = NOT_SHOW_IN + self.Visible -= 1 + elif isinstance(entry, Separator): + self.Visible -= 1 + # remove separators at the beginning and at the end + if len(self.Entries) > 0: + if isinstance(self.Entries[0], Separator): + self.Entries[0].Show = False + if len(self.Entries) > 1: + if isinstance(self.Entries[-1], Separator): + self.Entries[-1].Show = False + + # show_empty tag + for entry in self.Entries[:]: + if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: + entry.Show = EMPTY + self.Visible -= 1 + if entry.NotInXml is True: + self.Entries.remove(entry) + + """ PRIVATE STUFF """ + def addSubmenu(self, newmenu): + for submenu in self.Submenus: + if submenu == newmenu: + submenu += newmenu + break + else: + self.Submenus.append(newmenu) + newmenu.Parent = self + newmenu.Depth = self.Depth + 1 + + # inline tags + def merge_inline(self, submenu): + """Appends a submenu's entries to this menu + See the section of the spec about the "inline" attribute + """ + if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: + menuentry = submenu.Entries[0] + menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) + menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) + menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) + self.Entries.append(menuentry) + elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: + if submenu.Layout.inline_header: + header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) + self.Entries.append(header) + for entry in submenu.Entries: + self.Entries.append(entry) + else: + self.Entries.append(submenu) + + +class Move: + "A move operation" + def __init__(self, old="", new=""): + self.Old = old + self.New = new + + def __cmp__(self, other): + return cmp(self.Old, other.Old) + + +class Layout: + "Menu Layout class" + def __init__(self, show_empty=False, inline=False, inline_limit=4, + inline_header=True, inline_alias=False): + self.show_empty = show_empty + self.inline = inline + self.inline_limit = inline_limit + self.inline_header = inline_header + self.inline_alias = inline_alias + self._order = [] + self._default_order = [ + ['Merge', 'menus'], + ['Merge', 'files'] + ] + + @property + def order(self): + return self._order if self._order else self._default_order + + @order.setter + def order(self, order): + self._order = order + + +class Rule: + """Include / Exclude Rules Class""" + + TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 + + @classmethod + def fromFilename(cls, type, filename): + tree = ast.Expression( + body=ast.Compare( + left=ast.Str(filename), + ops=[ast.Eq()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='DesktopFileID', + ctx=ast.Load() + )] + ), + lineno=1, col_offset=0 + ) + ast.fix_missing_locations(tree) + rule = Rule(type, tree) + return rule + + def __init__(self, type, expression): + # Type is TYPE_INCLUDE or TYPE_EXCLUDE + self.Type = type + # expression is ast.Expression + self.expression = expression + self.code = compile(self.expression, '', 'eval') + + def __str__(self): + return ast.dump(self.expression) + + def apply(self, menuentries, run): + for menuentry in menuentries: + if run == 2 and (menuentry.MatchedInclude is True or + menuentry.Allocated is True): + continue + if eval(self.code): + if self.Type is Rule.TYPE_INCLUDE: + menuentry.Add = True + menuentry.MatchedInclude = True + else: + menuentry.Add = False + return menuentries + + +class MenuEntry: + "Wrapper for 'Menu Style' Desktop Entries" + + TYPE_USER = "User" + TYPE_SYSTEM = "System" + TYPE_BOTH = "Both" + + def __init__(self, filename, dir="", prefix=""): + # Create entry + self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) + self.setAttributes(filename, dir, prefix) + + # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC + self.Show = True + + # Semi-Private + self.Original = None + self.Parents = [] + + # Private Stuff + self.Allocated = False + self.Add = False + self.MatchedInclude = False + + # Caching + self.Categories = self.DesktopEntry.getCategories() + + def save(self): + """Save any changes to the desktop entry.""" + if self.DesktopEntry.tainted: + self.DesktopEntry.write() + + def getDir(self): + """Return the directory containing the desktop entry file.""" + return self.DesktopEntry.filename.replace(self.Filename, '') + + def getType(self): + """Return the type of MenuEntry, System/User/Both""" + if not xdg.Config.root_mode: + if self.Original: + return self.TYPE_BOTH + elif xdg_data_dirs[0] in self.DesktopEntry.filename: + return self.TYPE_USER + else: + return self.TYPE_SYSTEM + else: + return self.TYPE_USER + + def setAttributes(self, filename, dir="", prefix=""): + self.Filename = filename + self.Prefix = prefix + self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") + + if not os.path.isabs(self.DesktopEntry.filename): + self.__setFilename() + + def updateAttributes(self): + if self.getType() == self.TYPE_SYSTEM: + self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) + self.__setFilename() + + def __setFilename(self): + if not xdg.Config.root_mode: + path = xdg_data_dirs[0] + else: + path = xdg_data_dirs[1] + + if self.DesktopEntry.getType() == "Application": + dir_ = os.path.join(path, "applications") + else: + dir_ = os.path.join(path, "desktop-directories") + + self.DesktopEntry.filename = os.path.join(dir_, self.Filename) + + def __cmp__(self, other): + return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) + + def _key(self): + """Key function for locale-aware sorting.""" + return _strxfrm(self.DesktopEntry.getName()) + + def __lt__(self, other): + try: + other = other._key() + except AttributeError: + pass + return self._key() < other + + def __eq__(self, other): + if self.DesktopFileID == str(other): + return True + else: + return False + + def __repr__(self): + return self.DesktopFileID + + +class Separator: + "Just a dummy class for Separators" + def __init__(self, parent): + self.Parent = parent + self.Show = True + + +class Header: + "Class for Inline Headers" + def __init__(self, name, generic_name, comment): + self.Name = name + self.GenericName = generic_name + self.Comment = comment + + def __str__(self): + return self.Name + + +TYPE_DIR, TYPE_FILE = 0, 1 + + +def _check_file_path(value, filename, type): + path = os.path.dirname(filename) + if not os.path.isabs(value): + value = os.path.join(path, value) + value = os.path.abspath(value) + if not os.path.exists(value): + return False + if type == TYPE_DIR and os.path.isdir(value): + return value + if type == TYPE_FILE and os.path.isfile(value): + return value + return False + + +def _get_menu_file_path(filename): + dirs = list(xdg_config_dirs) + if xdg.Config.root_mode is True: + dirs.pop(0) + for d in dirs: + menuname = os.path.join(d, "menus", filename) + if os.path.isfile(menuname): + return menuname + + +def _to_bool(value): + if isinstance(value, bool): + return value + return value.lower() == "true" + + +# remove duplicate entries from a list +def _dedupe(_list): + _set = {} + _list.reverse() + _list = [_set.setdefault(e, e) for e in _list if e not in _set] + _list.reverse() + return _list + + +class XMLMenuBuilder(object): + + def __init__(self, debug=False): + self.debug = debug + + def parse(self, filename=None): + """Load an applications.menu file. + + filename : str, optional + The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. + """ + # convert to absolute path + if filename and not os.path.isabs(filename): + filename = _get_menu_file_path(filename) + # use default if no filename given + if not filename: + candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" + filename = _get_menu_file_path(candidate) + if not filename: + raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) + # check if it is a .menu file + if not filename.endswith(".menu"): + raise ParsingError('Not a .menu file', filename) + # create xml parser + try: + tree = etree.parse(filename) + except: + raise ParsingError('Not a valid .menu file', filename) + + # parse menufile + self._merged_files = set() + self._directory_dirs = set() + self.cache = MenuEntryCache() + + menu = self.parse_menu(tree.getroot(), filename) + menu.tree = tree + menu.filename = filename + + self.handle_moves(menu) + self.post_parse(menu) + + # generate the menu + self.generate_not_only_allocated(menu) + self.generate_only_allocated(menu) + + # and finally sort + menu.sort() + + return menu + + def parse_menu(self, node, filename): + menu = Menu() + self.parse_node(node, filename, menu) + return menu + + def parse_node(self, node, filename, parent=None): + num_children = len(node) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == 'Menu': + menu = self.parse_menu(child, filename) + parent.addSubmenu(menu) + elif tag == 'AppDir' and text: + self.parse_app_dir(text, filename, parent) + elif tag == 'DefaultAppDirs': + self.parse_default_app_dir(filename, parent) + elif tag == 'DirectoryDir' and text: + self.parse_directory_dir(text, filename, parent) + elif tag == 'DefaultDirectoryDirs': + self.parse_default_directory_dir(filename, parent) + elif tag == 'Name' and text: + parent.Name = text + elif tag == 'Directory' and text: + parent.Directories.append(text) + elif tag == 'OnlyUnallocated': + parent.OnlyUnallocated = True + elif tag == 'NotOnlyUnallocated': + parent.OnlyUnallocated = False + elif tag == 'Deleted': + parent.Deleted = True + elif tag == 'NotDeleted': + parent.Deleted = False + elif tag == 'Include' or tag == 'Exclude': + parent.Rules.append(self.parse_rule(child)) + elif tag == 'MergeFile': + if child.attrib.get("type", None) == "parent": + self.parse_merge_file("applications.menu", child, filename, parent) + elif text: + self.parse_merge_file(text, child, filename, parent) + elif tag == 'MergeDir' and text: + self.parse_merge_dir(text, child, filename, parent) + elif tag == 'DefaultMergeDirs': + self.parse_default_merge_dirs(child, filename, parent) + elif tag == 'Move': + parent.Moves.append(self.parse_move(child)) + elif tag == 'Layout': + if num_children > 1: + parent.Layout = self.parse_layout(child) + elif tag == 'DefaultLayout': + if num_children > 1: + parent.DefaultLayout = self.parse_layout(child) + elif tag == 'LegacyDir' and text: + self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) + elif tag == 'KDELegacyDirs': + self.parse_kde_legacy_dirs(filename, parent) + + def parse_layout(self, node): + layout = Layout( + show_empty=_to_bool(node.attrib.get("show_empty", False)), + inline=_to_bool(node.attrib.get("inline", False)), + inline_limit=int(node.attrib.get("inline_limit", 4)), + inline_header=_to_bool(node.attrib.get("inline_header", True)), + inline_alias=_to_bool(node.attrib.get("inline_alias", False)) + ) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Menuname" and text: + layout.order.append([ + "Menuname", + text, + _to_bool(child.attrib.get("show_empty", False)), + _to_bool(child.attrib.get("inline", False)), + int(child.attrib.get("inline_limit", 4)), + _to_bool(child.attrib.get("inline_header", True)), + _to_bool(child.attrib.get("inline_alias", False)) + ]) + elif tag == "Separator": + layout.order.append(['Separator']) + elif tag == "Filename" and text: + layout.order.append(["Filename", text]) + elif tag == "Merge": + layout.order.append([ + "Merge", + child.attrib.get("type", "all") + ]) + return layout + + def parse_move(self, node): + old, new = "", "" + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Old" and text: + old = text + elif tag == "New" and text: + new = text + return Move(old, new) + + # ---------- parsing + + def parse_rule(self, node): + type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE + tree = ast.Expression(lineno=1, col_offset=0) + expr = self.parse_bool_op(node, ast.Or()) + if expr: + tree.body = expr + else: + tree.body = ast.Name('False', ast.Load()) + ast.fix_missing_locations(tree) + return Rule(type, tree) + + def parse_bool_op(self, node, operator): + values = [] + for child in node: + rule = self.parse_rule_node(child) + if rule: + values.append(rule) + num_values = len(values) + if num_values > 1: + return ast.BoolOp(operator, values) + elif num_values == 1: + return values[0] + return None + + def parse_rule_node(self, node): + tag = node.tag + if tag == 'Or': + return self.parse_bool_op(node, ast.Or()) + elif tag == 'And': + return self.parse_bool_op(node, ast.And()) + elif tag == 'Not': + expr = self.parse_bool_op(node, ast.Or()) + return ast.UnaryOp(ast.Not(), expr) if expr else None + elif tag == 'All': + return ast.Name('True', ast.Load()) + elif tag == 'Category': + category = node.text + return ast.Compare( + left=ast.Str(category), + ops=[ast.In()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='Categories', + ctx=ast.Load() + )] + ) + elif tag == 'Filename': + filename = node.text + return ast.Compare( + left=ast.Str(filename), + ops=[ast.Eq()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='DesktopFileID', + ctx=ast.Load() + )] + ) + + # ---------- App/Directory Dir Stuff + + def parse_app_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.AppDirs.append(value) + + def parse_default_app_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_app_dir(os.path.join(d, "applications"), filename, parent) + + def parse_directory_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.DirectoryDirs.append(value) + + def parse_default_directory_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) + + # ---------- Merge Stuff + + def parse_merge_file(self, value, child, filename, parent): + if child.attrib.get("type", None) == "parent": + for d in xdg_config_dirs: + rel_file = filename.replace(d, "").strip("/") + if rel_file != filename: + for p in xdg_config_dirs: + if d == p: + continue + if os.path.isfile(os.path.join(p, rel_file)): + self.merge_file(os.path.join(p, rel_file), child, parent) + break + else: + value = _check_file_path(value, filename, TYPE_FILE) + if value: + self.merge_file(value, child, parent) + + def parse_merge_dir(self, value, child, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + for item in os.listdir(value): + try: + if item.endswith(".menu"): + self.merge_file(os.path.join(value, item), child, parent) + except UnicodeDecodeError: + continue + + def parse_default_merge_dirs(self, child, filename, parent): + basename = os.path.splitext(os.path.basename(filename))[0] + for d in reversed(xdg_config_dirs): + self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) + + def merge_file(self, filename, child, parent): + # check for infinite loops + if filename in self._merged_files: + if self.debug: + raise ParsingError('Infinite MergeFile loop detected', filename) + else: + return + self._merged_files.add(filename) + # load file + try: + tree = etree.parse(filename) + except IOError: + if self.debug: + raise ParsingError('File not found', filename) + else: + return + except: + if self.debug: + raise ParsingError('Not a valid .menu file', filename) + else: + return + root = tree.getroot() + self.parse_node(root, filename, parent) + + # ---------- Legacy Dir Stuff + + def parse_legacy_dir(self, dir_, prefix, filename, parent): + m = self.merge_legacy_dir(dir_, prefix, filename, parent) + if m: + parent += m + + def merge_legacy_dir(self, dir_, prefix, filename, parent): + dir_ = _check_file_path(dir_, filename, TYPE_DIR) + if dir_ and dir_ not in self._directory_dirs: + self._directory_dirs.add(dir_) + m = Menu() + m.AppDirs.append(dir_) + m.DirectoryDirs.append(dir_) + m.Name = os.path.basename(dir_) + m.NotInXml = True + + for item in os.listdir(dir_): + try: + if item == ".directory": + m.Directories.append(item) + elif os.path.isdir(os.path.join(dir_, item)): + m.addSubmenu(self.merge_legacy_dir( + os.path.join(dir_, item), + prefix, + filename, + parent + )) + except UnicodeDecodeError: + continue + + self.cache.add_menu_entries([dir_], prefix, True) + menuentries = self.cache.get_menu_entries([dir_], False) + + for menuentry in menuentries: + categories = menuentry.Categories + if len(categories) == 0: + r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) + m.Rules.append(r) + if not dir_ in parent.AppDirs: + categories.append("Legacy") + menuentry.Categories = categories + + return m + + def parse_kde_legacy_dirs(self, filename, parent): + try: + proc = subprocess.Popen( + ['kde-config', '--path', 'apps'], + stdout=subprocess.PIPE, + universal_newlines=True + ) + output = proc.communicate()[0].splitlines() + except OSError: + # If kde-config doesn't exist, ignore this. + return + try: + for dir_ in output[0].split(":"): + self.parse_legacy_dir(dir_, "kde", filename, parent) + except IndexError: + pass + + def post_parse(self, menu): + # unallocated / deleted + if menu.Deleted is None: + menu.Deleted = False + if menu.OnlyUnallocated is None: + menu.OnlyUnallocated = False + + # Layout Tags + if not menu.Layout or not menu.DefaultLayout: + if menu.DefaultLayout: + menu.Layout = menu.DefaultLayout + elif menu.Layout: + if menu.Depth > 0: + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.DefaultLayout = Layout() + else: + if menu.Depth > 0: + menu.Layout = menu.Parent.DefaultLayout + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.Layout = Layout() + menu.DefaultLayout = Layout() + + # add parent's app/directory dirs + if menu.Depth > 0: + menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs + menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs + + # remove duplicates + menu.Directories = _dedupe(menu.Directories) + menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) + menu.AppDirs = _dedupe(menu.AppDirs) + + # go recursive through all menus + for submenu in menu.Submenus: + self.post_parse(submenu) + + # reverse so handling is easier + menu.Directories.reverse() + menu.DirectoryDirs.reverse() + menu.AppDirs.reverse() + + # get the valid .directory file out of the list + for directory in menu.Directories: + for dir in menu.DirectoryDirs: + if os.path.isfile(os.path.join(dir, directory)): + menuentry = MenuEntry(directory, dir) + if not menu.Directory: + menu.Directory = menuentry + elif menuentry.Type == MenuEntry.TYPE_SYSTEM: + if menu.Directory.Type == MenuEntry.TYPE_USER: + menu.Directory.Original = menuentry + if menu.Directory: + break + + # Finally generate the menu + def generate_not_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_not_only_allocated(submenu) + + if menu.OnlyUnallocated is False: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) + + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + menuentry.Add = False + menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def generate_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_only_allocated(submenu) + + if menu.OnlyUnallocated is True: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + # menuentry.Add = False + # menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def handle_moves(self, menu): + for submenu in menu.Submenus: + self.handle_moves(submenu) + # parse move operations + for move in menu.Moves: + move_from_menu = menu.getMenu(move.Old) + if move_from_menu: + # FIXME: this is assigned, but never used... + move_to_menu = menu.getMenu(move.New) + + menus = move.New.split("/") + oldparent = None + while len(menus) > 0: + if not oldparent: + oldparent = menu + newmenu = oldparent.getMenu(menus[0]) + if not newmenu: + newmenu = Menu() + newmenu.Name = menus[0] + if len(menus) > 1: + newmenu.NotInXml = True + oldparent.addSubmenu(newmenu) + oldparent = newmenu + menus.pop(0) + + newmenu += move_from_menu + move_from_menu.Parent.Submenus.remove(move_from_menu) + + +class MenuEntryCache: + "Class to cache Desktop Entries" + def __init__(self): + self.cacheEntries = {} + self.cacheEntries['legacy'] = [] + self.cache = {} + + def add_menu_entries(self, dirs, prefix="", legacy=False): + for dir_ in dirs: + if not dir_ in self.cacheEntries: + self.cacheEntries[dir_] = [] + self.__addFiles(dir_, "", prefix, legacy) + + def __addFiles(self, dir_, subdir, prefix, legacy): + for item in os.listdir(os.path.join(dir_, subdir)): + if item.endswith(".desktop"): + try: + menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) + except ParsingError: + continue + + self.cacheEntries[dir_].append(menuentry) + if legacy: + self.cacheEntries['legacy'].append(menuentry) + elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: + self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) + + def get_menu_entries(self, dirs, legacy=True): + entries = [] + ids = set() + # handle legacy items + appdirs = dirs[:] + if legacy: + appdirs.append("legacy") + # cache the results again + key = "".join(appdirs) + try: + return self.cache[key] + except KeyError: + pass + for dir_ in appdirs: + for menuentry in self.cacheEntries[dir_]: + try: + if menuentry.DesktopFileID not in ids: + ids.add(menuentry.DesktopFileID) + entries.append(menuentry) + elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: + # FIXME: This is only 99% correct, but still... + idx = entries.index(menuentry) + entry = entries[idx] + if entry.getType() == MenuEntry.TYPE_USER: + entry.Original = menuentry + except UnicodeDecodeError: + continue + self.cache[key] = entries + return entries + + +def parse(filename=None, debug=False): + """Helper function. + Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) + """ + return XMLMenuBuilder(debug).parse(filename) diff --git a/src/Pytop/widgets/mixins/xdg/MenuEditor.py b/src/Pytop/widgets/mixins/xdg/MenuEditor.py new file mode 100644 index 0000000..2c68515 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/MenuEditor.py @@ -0,0 +1,541 @@ +""" CLass to edit XDG Menus """ +import os +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from .Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder +from .BaseDirectory import xdg_config_dirs, xdg_data_dirs +from .Exceptions import ParsingError +from .Config import setRootMode + +# XML-Cleanups: Move / Exclude +# FIXME: proper reverte/delete +# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions +# FIXME: catch Exceptions +# FIXME: copy functions +# FIXME: More Layout stuff +# FIXME: unod/redo function / remove menu... +# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile +# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs + + +class MenuEditor(object): + + def __init__(self, menu=None, filename=None, root=False): + self.menu = None + self.filename = None + self.tree = None + self.parser = XMLMenuBuilder() + self.parse(menu, filename, root) + + # fix for creating two menus with the same name on the fly + self.filenames = [] + + def parse(self, menu=None, filename=None, root=False): + if root: + setRootMode(True) + + if isinstance(menu, Menu): + self.menu = menu + elif menu: + self.menu = self.parser.parse(menu) + else: + self.menu = self.parser.parse() + + if root: + self.filename = self.menu.Filename + elif filename: + self.filename = filename + else: + self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) + + try: + self.tree = etree.parse(self.filename) + except IOError: + root = etree.fromtring(""" + + + Applications + %s + +""" % self.menu.Filename) + self.tree = etree.ElementTree(root) + except ParsingError: + raise ParsingError('Not a valid .menu file', self.filename) + + #FIXME: is this needed with etree ? + self.__remove_whitespace_nodes(self.tree) + + def save(self): + self.__saveEntries(self.menu) + self.__saveMenu() + + def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): + menuentry = MenuEntry(self.__getFileName(name, ".desktop")) + menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) + + self.__addEntry(parent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): + menu = Menu() + + menu.Parent = parent + menu.Depth = parent.Depth + 1 + menu.Layout = parent.DefaultLayout + menu.DefaultLayout = parent.DefaultLayout + + menu = self.editMenu(menu, name, genericname, comment, icon) + + self.__addEntry(parent, menu, after, before) + + self.menu.sort() + + return menu + + def createSeparator(self, parent, after=None, before=None): + separator = Separator(parent) + + self.__addEntry(parent, separator, after, before) + + self.menu.sort() + + return separator + + def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menuentry, after, before) + self.__addEntry(newparent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def moveMenu(self, menu, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menu, after, before) + self.__addEntry(newparent, menu, after, before) + + root_menu = self.__getXmlMenu(self.menu.Name) + if oldparent.getPath(True) != newparent.getPath(True): + self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) + + self.menu.sort() + + return menu + + def moveSeparator(self, separator, parent, after=None, before=None): + self.__deleteEntry(parent, separator, after, before) + self.__addEntry(parent, separator, after, before) + + self.menu.sort() + + return separator + + def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__addEntry(newparent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): + deskentry = menuentry.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale=True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale=True) + if genericname: + if not deskentry.hasKey("GenericName"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale=True) + if command: + deskentry.set("Exec", command) + if icon: + deskentry.set("Icon", icon) + + if terminal: + deskentry.set("Terminal", "true") + elif not terminal: + deskentry.set("Terminal", "false") + + if nodisplay is True: + deskentry.set("NoDisplay", "true") + elif nodisplay is False: + deskentry.set("NoDisplay", "false") + + if hidden is True: + deskentry.set("Hidden", "true") + elif hidden is False: + deskentry.set("Hidden", "false") + + menuentry.updateAttributes() + + if len(menuentry.Parents) > 0: + self.menu.sort() + + return menuentry + + def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): + # Hack for legacy dirs + if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") + menu.Directory.setAttributes(menu.Name + ".directory") + # Hack for New Entries + elif not isinstance(menu.Directory, MenuEntry): + if not name: + name = menu.Name + filename = self.__getFileName(name, ".directory").replace("/", "") + if not menu.Name: + menu.Name = filename.replace(".directory", "") + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', filename) + menu.Directory = MenuEntry(filename) + + deskentry = menu.Directory.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale=True) + if genericname: + if not deskentry.hasKey("GenericName"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale=True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale=True) + if icon: + deskentry.set("Icon", icon) + + if nodisplay is True: + deskentry.set("NoDisplay", "true") + elif nodisplay is False: + deskentry.set("NoDisplay", "false") + + if hidden is True: + deskentry.set("Hidden", "true") + elif hidden is False: + deskentry.set("Hidden", "false") + + menu.Directory.updateAttributes() + + if isinstance(menu.Parent, Menu): + self.menu.sort() + + return menu + + def hideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay=True) + + def unhideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay=False, hidden=False) + + def hideMenu(self, menu): + self.editMenu(menu, nodisplay=True) + + def unhideMenu(self, menu): + self.editMenu(menu, nodisplay=False, hidden=False) + xml_menu = self.__getXmlMenu(menu.getPath(True, True), False) + deleted = xml_menu.findall('Deleted') + not_deleted = xml_menu.findall('NotDeleted') + for node in deleted + not_deleted: + xml_menu.remove(node) + + def deleteMenuEntry(self, menuentry): + if self.getAction(menuentry) == "delete": + self.__deleteFile(menuentry.DesktopEntry.filename) + for parent in menuentry.Parents: + self.__deleteEntry(parent, menuentry) + self.menu.sort() + return menuentry + + def revertMenuEntry(self, menuentry): + if self.getAction(menuentry) == "revert": + self.__deleteFile(menuentry.DesktopEntry.filename) + menuentry.Original.Parents = [] + for parent in menuentry.Parents: + index = parent.Entries.index(menuentry) + parent.Entries[index] = menuentry.Original + index = parent.MenuEntries.index(menuentry) + parent.MenuEntries[index] = menuentry.Original + menuentry.Original.Parents.append(parent) + self.menu.sort() + return menuentry + + def deleteMenu(self, menu): + if self.getAction(menu) == "delete": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + self.__deleteEntry(menu.Parent, menu) + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + parent = self.__get_parent_node(xml_menu) + parent.remove(xml_menu) + self.menu.sort() + return menu + + def revertMenu(self, menu): + if self.getAction(menu) == "revert": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + menu.Directory = menu.Directory.Original + self.menu.sort() + return menu + + def deleteSeparator(self, separator): + self.__deleteEntry(separator.Parent, separator, after=True) + + self.menu.sort() + + return separator + + """ Private Stuff """ + def getAction(self, entry): + if isinstance(entry, Menu): + if not isinstance(entry.Directory, MenuEntry): + return "none" + elif entry.Directory.getType() == "Both": + return "revert" + elif entry.Directory.getType() == "User" and ( + len(entry.Submenus) + len(entry.MenuEntries) + ) == 0: + return "delete" + + elif isinstance(entry, MenuEntry): + if entry.getType() == "Both": + return "revert" + elif entry.getType() == "User": + return "delete" + else: + return "none" + + return "none" + + def __saveEntries(self, menu): + if not menu: + menu = self.menu + if isinstance(menu.Directory, MenuEntry): + menu.Directory.save() + for entry in menu.getEntries(hidden=True): + if isinstance(entry, MenuEntry): + entry.save() + elif isinstance(entry, Menu): + self.__saveEntries(entry) + + def __saveMenu(self): + if not os.path.isdir(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + self.tree.write(self.filename, encoding='utf-8') + + def __getFileName(self, name, extension): + postfix = 0 + while 1: + if postfix == 0: + filename = name + extension + else: + filename = name + "-" + str(postfix) + extension + if extension == ".desktop": + dir = "applications" + elif extension == ".directory": + dir = "desktop-directories" + if not filename in self.filenames and not os.path.isfile( + os.path.join(xdg_data_dirs[0], dir, filename) + ): + self.filenames.append(filename) + break + else: + postfix += 1 + + return filename + + def __getXmlMenu(self, path, create=True, element=None): + # FIXME: we should also return the menu's parent, + # to avoid looking for it later on + # @see Element.getiterator() + if not element: + element = self.tree + + if "/" in path: + (name, path) = path.split("/", 1) + else: + name = path + path = "" + + found = None + for node in element.findall("Menu"): + name_node = node.find('Name') + if name_node.text == name: + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + if found: + break + if not found and create: + node = self.__addXmlMenuElement(element, name) + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + + return found + + def __addXmlMenuElement(self, element, name): + menu_node = etree.SubElement('Menu', element) + name_node = etree.SubElement('Name', menu_node) + name_node.text = name + return menu_node + + def __addXmlTextElement(self, element, name, text): + node = etree.SubElement(name, element) + node.text = text + return node + + def __addXmlFilename(self, element, filename, type_="Include"): + # remove old filenames + includes = element.findall('Include') + excludes = element.findall('Exclude') + rules = includes + excludes + for rule in rules: + #FIXME: this finds only Rules whose FIRST child is a Filename element + if rule[0].tag == "Filename" and rule[0].text == filename: + element.remove(rule) + # shouldn't it remove all occurences, like the following: + #filename_nodes = rule.findall('.//Filename'): + #for fn in filename_nodes: + #if fn.text == filename: + ##element.remove(rule) + #parent = self.__get_parent_node(fn) + #parent.remove(fn) + + # add new filename + node = etree.SubElement(type_, element) + self.__addXmlTextElement(node, 'Filename', filename) + return node + + def __addXmlMove(self, element, old, new): + node = etree.SubElement("Move", element) + self.__addXmlTextElement(node, 'Old', old) + self.__addXmlTextElement(node, 'New', new) + return node + + def __addXmlLayout(self, element, layout): + # remove old layout + for node in element.findall("Layout"): + element.remove(node) + + # add new layout + node = etree.SubElement("Layout", element) + for order in layout.order: + if order[0] == "Separator": + child = etree.SubElement("Separator", node) + elif order[0] == "Filename": + child = self.__addXmlTextElement(node, "Filename", order[1]) + elif order[0] == "Menuname": + child = self.__addXmlTextElement(node, "Menuname", order[1]) + elif order[0] == "Merge": + child = etree.SubElement("Merge", node) + child.attrib["type"] = order[1] + return node + + def __addLayout(self, parent): + layout = Layout() + layout.order = [] + layout.show_empty = parent.Layout.show_empty + layout.inline = parent.Layout.inline + layout.inline_header = parent.Layout.inline_header + layout.inline_alias = parent.Layout.inline_alias + layout.inline_limit = parent.Layout.inline_limit + + layout.order.append(["Merge", "menus"]) + for entry in parent.Entries: + if isinstance(entry, Menu): + layout.parseMenuname(entry.Name) + elif isinstance(entry, MenuEntry): + layout.parseFilename(entry.DesktopFileID) + elif isinstance(entry, Separator): + layout.parseSeparator() + layout.order.append(["Merge", "files"]) + + parent.Layout = layout + + return layout + + def __addEntry(self, parent, entry, after=None, before=None): + if after or before: + if after: + index = parent.Entries.index(after) + 1 + elif before: + index = parent.Entries.index(before) + parent.Entries.insert(index, entry) + else: + parent.Entries.append(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + parent.MenuEntries.append(entry) + entry.Parents.append(parent) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") + elif isinstance(entry, Menu): + parent.addSubmenu(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteEntry(self, parent, entry, after=None, before=None): + parent.Entries.remove(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + entry.Parents.remove(parent) + parent.MenuEntries.remove(entry) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") + elif isinstance(entry, Menu): + parent.Submenus.remove(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteFile(self, filename): + try: + os.remove(filename) + except OSError: + pass + try: + self.filenames.remove(filename) + except ValueError: + pass + + def __remove_whitespace_nodes(self, node): + for child in node: + text = child.text.strip() + if not text: + child.text = '' + tail = child.tail.strip() + if not tail: + child.tail = '' + if len(child): + self.__remove_whilespace_nodes(child) + + def __get_parent_node(self, node): + # elements in ElementTree doesn't hold a reference to their parent + for parent, child in self.__iter_parent(): + if child is node: + return child + + def __iter_parent(self): + for parent in self.tree.getiterator(): + for child in parent: + yield parent, child diff --git a/src/Pytop/widgets/mixins/xdg/Mime.py b/src/Pytop/widgets/mixins/xdg/Mime.py new file mode 100644 index 0000000..60c4efd --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/Mime.py @@ -0,0 +1,780 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log + +This module provides access to the shared MIME database. + +types is a dictionary of all known MIME types, indexed by the type name, e.g. +types['application/x-python'] + +Applications can install information about MIME types by storing an +XML file as /packages/.xml and running the +update-mime-database command, which is provided by the freedesktop.org +shared mime database package. + +See http://www.freedesktop.org/standards/shared-mime-info-spec/ for +information about the format of these files. + +(based on version 0.13) +""" + +import os +import re +import stat +import sys +import fnmatch + +from . import BaseDirectory, Locale + +from .dom import minidom, XML_NAMESPACE +from collections import defaultdict + +FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' + +types = {} # Maps MIME names to type objects + +exts = None # Maps extensions to types +globs = None # List of (glob, type) pairs +literals = None # Maps liternal names to types +magic = None + +PY3 = (sys.version_info[0] >= 3) + +def _get_node_data(node): + """Get text of XML node""" + return ''.join([n.nodeValue for n in node.childNodes]).strip() + +def lookup(media, subtype = None): + """Get the MIMEtype object for the given type. + + This remains for backwards compatibility; calling MIMEtype now does + the same thing. + + The name can either be passed as one part ('text/plain'), or as two + ('text', 'plain'). + """ + return MIMEtype(media, subtype) + +class MIMEtype(object): + """Class holding data about a MIME type. + + Calling the class will return a cached instance, so there is only one + instance for each MIME type. The name can either be passed as one part + ('text/plain'), or as two ('text', 'plain'). + """ + def __new__(cls, media, subtype=None): + if subtype is None and '/' in media: + media, subtype = media.split('/', 1) + assert '/' not in subtype + media = media.lower() + subtype = subtype.lower() + + try: + return types[(media, subtype)] + except KeyError: + mtype = super(MIMEtype, cls).__new__(cls) + mtype._init(media, subtype) + types[(media, subtype)] = mtype + return mtype + + # If this is done in __init__, it is automatically called again each time + # the MIMEtype is returned by __new__, which we don't want. So we call it + # explicitly only when we construct a new instance. + def _init(self, media, subtype): + self.media = media + self.subtype = subtype + self._comment = None + + def _load(self): + "Loads comment for current language. Use get_comment() instead." + resource = os.path.join('mime', self.media, self.subtype + '.xml') + for path in BaseDirectory.load_data_paths(resource): + doc = minidom.parse(path) + if doc is None: + continue + for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): + lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' + goodness = 1 + (lang in xdg.Locale.langs) + if goodness > self._comment[0]: + self._comment = (goodness, _get_node_data(comment)) + if goodness == 2: return + + # FIXME: add get_icon method + def get_comment(self): + """Returns comment for current language, loading it if needed.""" + # Should we ever reload? + if self._comment is None: + self._comment = (0, str(self)) + self._load() + return self._comment[1] + + def canonical(self): + """Returns the canonical MimeType object if this is an alias.""" + update_cache() + s = str(self) + if s in aliases: + return lookup(aliases[s]) + return self + + def inherits_from(self): + """Returns a set of Mime types which this inherits from.""" + update_cache() + return set(lookup(t) for t in inheritance[str(self)]) + + def __str__(self): + return self.media + '/' + self.subtype + + def __repr__(self): + return 'MIMEtype(%r, %r)' % (self.media, self.subtype) + + def __hash__(self): + return hash(self.media) ^ hash(self.subtype) + +class UnknownMagicRuleFormat(ValueError): + pass + +class DiscardMagicRules(Exception): + "Raised when __NOMAGIC__ is found, and caught to discard previous rules." + pass + +class MagicRule: + also = None + + def __init__(self, start, value, mask, word, range): + self.start = start + self.value = value + self.mask = mask + self.word = word + self.range = range + + rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$') + + @classmethod + def from_file(cls, f): + """Read a rule from the binary magics file. Returns a 2-tuple of + the nesting depth and the MagicRule.""" + line = f.readline() + #print line + + # [indent] '>' + nest_depth, line = line.split(b'>', 1) + nest_depth = int(nest_depth) if nest_depth else 0 + + # start-offset '=' + start, line = line.split(b'=', 1) + start = int(start) + + if line == b'__NOMAGIC__\n': + raise DiscardMagicRules + + # value length (2 bytes, big endian) + if sys.version_info[0] >= 3: + lenvalue = int.from_bytes(line[:2], byteorder='big') + else: + lenvalue = (ord(line[0])<<8)+ord(line[1]) + line = line[2:] + + # value + # This can contain newlines, so we may need to read more lines + while len(line) <= lenvalue: + line += f.readline() + value, line = line[:lenvalue], line[lenvalue:] + + # ['&' mask] + if line.startswith(b'&'): + # This can contain newlines, so we may need to read more lines + while len(line) <= lenvalue: + line += f.readline() + mask, line = line[1:lenvalue+1], line[lenvalue+1:] + else: + mask = None + + # ['~' word-size] ['+' range-length] + ending = cls.rule_ending_re.match(line) + if not ending: + # Per the spec, this will be caught and ignored, to allow + # for future extensions. + raise UnknownMagicRuleFormat(repr(line)) + + word, range = ending.groups() + word = int(word) if (word is not None) else 1 + range = int(range) if (range is not None) else 1 + + return nest_depth, cls(start, value, mask, word, range) + + def maxlen(self): + l = self.start + len(self.value) + self.range + if self.also: + return max(l, self.also.maxlen()) + return l + + def match(self, buffer): + if self.match0(buffer): + if self.also: + return self.also.match(buffer) + return True + + def match0(self, buffer): + l=len(buffer) + lenvalue = len(self.value) + for o in range(self.range): + s=self.start+o + e=s+lenvalue + if l [(priority, rule), ...] + + def merge_file(self, fname): + """Read a magic binary file, and add its rules to this MagicDB.""" + with open(fname, 'rb') as f: + line = f.readline() + if line != b'MIME-Magic\0\n': + raise IOError('Not a MIME magic file') + + while True: + shead = f.readline().decode('ascii') + #print(shead) + if not shead: + break + if shead[0] != '[' or shead[-2:] != ']\n': + raise ValueError('Malformed section heading', shead) + pri, tname = shead[1:-2].split(':') + #print shead[1:-2] + pri = int(pri) + mtype = lookup(tname) + try: + rule = MagicMatchAny.from_file(f) + except DiscardMagicRules: + self.bytype.pop(mtype, None) + rule = MagicMatchAny.from_file(f) + if rule is None: + continue + #print rule + + self.bytype[mtype].append((pri, rule)) + + def finalise(self): + """Prepare the MagicDB for matching. + + This should be called after all rules have been merged into it. + """ + maxlen = 0 + self.alltypes = [] # (priority, mimetype, rule) + + for mtype, rules in self.bytype.items(): + for pri, rule in rules: + self.alltypes.append((pri, mtype, rule)) + maxlen = max(maxlen, rule.maxlen()) + + self.maxlen = maxlen # Number of bytes to read from files + self.alltypes.sort(key=lambda x: x[0], reverse=True) + + def match_data(self, data, max_pri=100, min_pri=0, possible=None): + """Do magic sniffing on some bytes. + + max_pri & min_pri can be used to specify the maximum & minimum priority + rules to look for. possible can be a list of mimetypes to check, or None + (the default) to check all mimetypes until one matches. + + Returns the MIMEtype found, or None if no entries match. + """ + if possible is not None: + types = [] + for mt in possible: + for pri, rule in self.bytype[mt]: + types.append((pri, mt, rule)) + types.sort(key=lambda x: x[0]) + else: + types = self.alltypes + + for priority, mimetype, rule in types: + #print priority, max_pri, min_pri + if priority > max_pri: + continue + if priority < min_pri: + break + + if rule.match(data): + return mimetype + + def match(self, path, max_pri=100, min_pri=0, possible=None): + """Read data from the file and do magic sniffing on it. + + max_pri & min_pri can be used to specify the maximum & minimum priority + rules to look for. possible can be a list of mimetypes to check, or None + (the default) to check all mimetypes until one matches. + + Returns the MIMEtype found, or None if no entries match. Raises IOError + if the file can't be opened. + """ + with open(path, 'rb') as f: + buf = f.read(self.maxlen) + return self.match_data(buf, max_pri, min_pri, possible) + + def __repr__(self): + return '' % len(self.alltypes) + +class GlobDB(object): + def __init__(self): + """Prepare the GlobDB. It can't actually be used until .finalise() is + called, but merge_file() can be used to add data before that. + """ + # Maps mimetype to {(weight, glob, flags), ...} + self.allglobs = defaultdict(set) + + def merge_file(self, path): + """Loads name matching information from a globs2 file."""# + allglobs = self.allglobs + with open(path) as f: + for line in f: + if line.startswith('#'): continue # Comment + + fields = line[:-1].split(':') + weight, type_name, pattern = fields[:3] + weight = int(weight) + mtype = lookup(type_name) + if len(fields) > 3: + flags = fields[3].split(',') + else: + flags = () + + if pattern == '__NOGLOBS__': + # This signals to discard any previous globs + allglobs.pop(mtype, None) + continue + + allglobs[mtype].add((weight, pattern, tuple(flags))) + + def finalise(self): + """Prepare the GlobDB for matching. + + This should be called after all files have been merged into it. + """ + self.exts = defaultdict(list) # Maps extensions to [(type, weight),...] + self.cased_exts = defaultdict(list) + self.globs = [] # List of (regex, type, weight) triplets + self.literals = {} # Maps literal names to (type, weight) + self.cased_literals = {} + + for mtype, globs in self.allglobs.items(): + mtype = mtype.canonical() + for weight, pattern, flags in globs: + + cased = 'cs' in flags + + if pattern.startswith('*.'): + # *.foo -- extension pattern + rest = pattern[2:] + if not ('*' in rest or '[' in rest or '?' in rest): + if cased: + self.cased_exts[rest].append((mtype, weight)) + else: + self.exts[rest.lower()].append((mtype, weight)) + continue + + if ('*' in pattern or '[' in pattern or '?' in pattern): + # Translate the glob pattern to a regex & compile it + re_flags = 0 if cased else re.I + pattern = re.compile(fnmatch.translate(pattern), flags=re_flags) + self.globs.append((pattern, mtype, weight)) + else: + # No wildcards - literal pattern + if cased: + self.cased_literals[pattern] = (mtype, weight) + else: + self.literals[pattern.lower()] = (mtype, weight) + + # Sort globs by weight & length + self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) ) + + def first_match(self, path): + """Return the first match found for a given path, or None if no match + is found.""" + try: + return next(self._match_path(path))[0] + except StopIteration: + return None + + def all_matches(self, path): + """Return a list of (MIMEtype, glob weight) pairs for the path.""" + return list(self._match_path(path)) + + def _match_path(self, path): + """Yields pairs of (mimetype, glob weight).""" + leaf = os.path.basename(path) + + # Literals (no wildcards) + if leaf in self.cased_literals: + yield self.cased_literals[leaf] + + lleaf = leaf.lower() + if lleaf in self.literals: + yield self.literals[lleaf] + + # Extensions + ext = leaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p + 1:] + if ext in self.cased_exts: + for res in self.cased_exts[ext]: + yield res + ext = lleaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p+1:] + if ext in self.exts: + for res in self.exts[ext]: + yield res + + # Other globs + for (regex, mime_type, weight) in self.globs: + if regex.match(leaf): + yield (mime_type, weight) + +# Some well-known types +text = lookup('text', 'plain') +octet_stream = lookup('application', 'octet-stream') +inode_block = lookup('inode', 'blockdevice') +inode_char = lookup('inode', 'chardevice') +inode_dir = lookup('inode', 'directory') +inode_fifo = lookup('inode', 'fifo') +inode_socket = lookup('inode', 'socket') +inode_symlink = lookup('inode', 'symlink') +inode_door = lookup('inode', 'door') +app_exe = lookup('application', 'executable') + +_cache_uptodate = False + +def _cache_database(): + global globs, magic, aliases, inheritance, _cache_uptodate + + _cache_uptodate = True + + aliases = {} # Maps alias Mime types to canonical names + inheritance = defaultdict(set) # Maps to sets of parent mime types. + + # Load aliases + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')): + with open(path, 'r') as f: + for line in f: + alias, canonical = line.strip().split(None, 1) + aliases[alias] = canonical + + # Load filename patterns (globs) + globs = GlobDB() + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')): + globs.merge_file(path) + globs.finalise() + + # Load magic sniffing data + magic = MagicDB() + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): + magic.merge_file(path) + magic.finalise() + + # Load subclasses + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): + with open(path, 'r') as f: + for line in f: + sub, parent = line.strip().split(None, 1) + inheritance[sub].add(parent) + +def update_cache(): + if not _cache_uptodate: + _cache_database() + +def get_type_by_name(path): + """Returns type of file by its name, or None if not known""" + update_cache() + return globs.first_match(path) + +def get_type_by_contents(path, max_pri=100, min_pri=0): + """Returns type of file by its contents, or None if not known""" + update_cache() + + return magic.match(path, max_pri, min_pri) + +def get_type_by_data(data, max_pri=100, min_pri=0): + """Returns type of the data, which should be bytes.""" + update_cache() + + return magic.match_data(data, max_pri, min_pri) + +def _get_type_by_stat(st_mode): + """Match special filesystem objects to Mimetypes.""" + if stat.S_ISDIR(st_mode): return inode_dir + elif stat.S_ISCHR(st_mode): return inode_char + elif stat.S_ISBLK(st_mode): return inode_block + elif stat.S_ISFIFO(st_mode): return inode_fifo + elif stat.S_ISLNK(st_mode): return inode_symlink + elif stat.S_ISSOCK(st_mode): return inode_socket + return inode_door + +def get_type(path, follow=True, name_pri=100): + """Returns type of file indicated by path. + + This function is *deprecated* - :func:`get_type2` is more accurate. + + :param path: pathname to check (need not exist) + :param follow: when reading file, follow symbolic links + :param name_pri: Priority to do name matches. 100=override magic + + This tries to use the contents of the file, and falls back to the name. It + can also handle special filesystem objects like directories and sockets. + """ + update_cache() + + try: + if follow: + st = os.stat(path) + else: + st = os.lstat(path) + except: + t = get_type_by_name(path) + return t or text + + if stat.S_ISREG(st.st_mode): + # Regular file + t = get_type_by_contents(path, min_pri=name_pri) + if not t: t = get_type_by_name(path) + if not t: t = get_type_by_contents(path, max_pri=name_pri) + if t is None: + if stat.S_IMODE(st.st_mode) & 0o111: + return app_exe + else: + return text + return t + else: + return _get_type_by_stat(st.st_mode) + +def get_type2(path, follow=True): + """Find the MIMEtype of a file using the XDG recommended checking order. + + This first checks the filename, then uses file contents if the name doesn't + give an unambiguous MIMEtype. It can also handle special filesystem objects + like directories and sockets. + + :param path: file path to examine (need not exist) + :param follow: whether to follow symlinks + + :rtype: :class:`MIMEtype` + + .. versionadded:: 1.0 + """ + update_cache() + + try: + st = os.stat(path) if follow else os.lstat(path) + except OSError: + return get_type_by_name(path) or octet_stream + + if not stat.S_ISREG(st.st_mode): + # Special filesystem objects + return _get_type_by_stat(st.st_mode) + + mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True) + if mtypes: + max_weight = mtypes[0][1] + i = 1 + for mt, w in mtypes[1:]: + if w < max_weight: + break + i += 1 + mtypes = mtypes[:i] + if len(mtypes) == 1: + return mtypes[0][0] + + possible = [mt for mt,w in mtypes] + else: + possible = None # Try all magic matches + + try: + t = magic.match(path, possible=possible) + except IOError: + t = None + + if t: + return t + elif mtypes: + return mtypes[0][0] + elif stat.S_IMODE(st.st_mode) & 0o111: + return app_exe + else: + return text if is_text_file(path) else octet_stream + +def is_text_file(path): + """Guess whether a file contains text or binary data. + + Heuristic: binary if the first 32 bytes include ASCII control characters. + This rule may change in future versions. + + .. versionadded:: 1.0 + """ + try: + f = open(path, 'rb') + except IOError: + return False + + with f: + return _is_text(f.read(32)) + +if PY3: + def _is_text(data): + return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data) +else: + def _is_text(data): + return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \ + for b in data) + +_mime2ext_cache = None +_mime2ext_cache_uptodate = False + +def get_extensions(mimetype): + """Retrieve the set of filename extensions matching a given MIMEtype. + + Extensions are returned without a leading dot, e.g. 'py'. If no extensions + are registered for the MIMEtype, returns an empty set. + + The extensions are stored in a cache the first time this is called. + + .. versionadded:: 1.0 + """ + global _mime2ext_cache, _mime2ext_cache_uptodate + update_cache() + if not _mime2ext_cache_uptodate: + _mime2ext_cache = defaultdict(set) + for ext, mtypes in globs.exts.items(): + for mtype, prio in mtypes: + _mime2ext_cache[mtype].add(ext) + _mime2ext_cache_uptodate = True + + return _mime2ext_cache[mimetype] + + +def install_mime_info(application, package_file): + """Copy 'package_file' as ``~/.local/share/mime/packages/.xml.`` + If package_file is None, install ``/.xml``. + If already installed, does nothing. May overwrite an existing + file with the same name (if the contents are different)""" + application += '.xml' + + new_data = open(package_file).read() + + # See if the file is already installed + package_dir = os.path.join('mime', 'packages') + resource = os.path.join(package_dir, application) + for x in BaseDirectory.load_data_paths(resource): + try: + old_data = open(x).read() + except: + continue + if old_data == new_data: + return # Already installed + + global _cache_uptodate + _cache_uptodate = False + + # Not already installed; add a new copy + # Create the directory structure... + new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application) + + # Write the file... + open(new_file, 'w').write(new_data) + + # Update the database... + command = 'update-mime-database' + if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')): + os.unlink(new_file) + raise Exception("The '%s' command returned an error code!\n" \ + "Make sure you have the freedesktop.org shared MIME package:\n" \ + "http://standards.freedesktop.org/shared-mime-info/" % command) diff --git a/src/Pytop/widgets/mixins/xdg/RecentFiles.py b/src/Pytop/widgets/mixins/xdg/RecentFiles.py new file mode 100644 index 0000000..fbe608c --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/RecentFiles.py @@ -0,0 +1,181 @@ +""" +Implementation of the XDG Recent File Storage Specification +http://standards.freedesktop.org/recent-file-spec +""" + +import xml.dom.minidom, xml.sax.saxutils +import os, time, fcntl +from .Exceptions import ParsingError + +class RecentFiles: + def __init__(self): + self.RecentFiles = [] + self.filename = "" + + def parse(self, filename=None): + """Parse a list of recently used files. + + filename defaults to ``~/.recently-used``. + """ + if not filename: + filename = os.path.join(os.getenv("HOME"), ".recently-used") + + try: + doc = xml.dom.minidom.parse(filename) + except IOError: + raise ParsingError('File not found', filename) + except xml.parsers.expat.ExpatError: + raise ParsingError('Not a valid .menu file', filename) + + self.filename = filename + + for child in doc.childNodes: + if child.nodeType == xml.dom.Node.ELEMENT_NODE: + if child.tagName == "RecentFiles": + for recent in child.childNodes: + if recent.nodeType == xml.dom.Node.ELEMENT_NODE: + if recent.tagName == "RecentItem": + self.__parseRecentItem(recent) + + self.sort() + + def __parseRecentItem(self, item): + recent = RecentFile() + self.RecentFiles.append(recent) + + for attribute in item.childNodes: + if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: + if attribute.tagName == "URI": + recent.URI = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Mime-Type": + recent.MimeType = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Timestamp": + recent.Timestamp = int(attribute.childNodes[0].nodeValue) + elif attribute.tagName == "Private": + recent.Prviate = True + elif attribute.tagName == "Groups": + + for group in attribute.childNodes: + if group.nodeType == xml.dom.Node.ELEMENT_NODE: + if group.tagName == "Group": + recent.Groups.append(group.childNodes[0].nodeValue) + + def write(self, filename=None): + """Write the list of recently used files to disk. + + If the instance is already associated with a file, filename can be + omitted to save it there again. + """ + if not filename and not self.filename: + raise ParsingError('File not found', filename) + elif not filename: + filename = self.filename + + f = open(filename, "w") + fcntl.lockf(f, fcntl.LOCK_EX) + f.write('\n') + f.write("\n") + + for r in self.RecentFiles: + f.write(" \n") + f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) + f.write(" %s\n" % r.MimeType) + f.write(" %s\n" % r.Timestamp) + if r.Private == True: + f.write(" \n") + if len(r.Groups) > 0: + f.write(" \n") + for group in r.Groups: + f.write(" %s\n" % group) + f.write(" \n") + f.write(" \n") + + f.write("\n") + fcntl.lockf(f, fcntl.LOCK_UN) + f.close() + + def getFiles(self, mimetypes=None, groups=None, limit=0): + """Get a list of recently used files. + + The parameters can be used to filter by mime types, by group, or to + limit the number of items returned. By default, the entire list is + returned, except for items marked private. + """ + tmp = [] + i = 0 + for item in self.RecentFiles: + if groups: + for group in groups: + if group in item.Groups: + tmp.append(item) + i += 1 + elif mimetypes: + for mimetype in mimetypes: + if mimetype == item.MimeType: + tmp.append(item) + i += 1 + else: + if item.Private == False: + tmp.append(item) + i += 1 + if limit != 0 and i == limit: + break + + return tmp + + def addFile(self, item, mimetype, groups=None, private=False): + """Add a recently used file. + + item should be the URI of the file, typically starting with ``file:///``. + """ + # check if entry already there + if item in self.RecentFiles: + index = self.RecentFiles.index(item) + recent = self.RecentFiles[index] + else: + # delete if more then 500 files + if len(self.RecentFiles) == 500: + self.RecentFiles.pop() + # add entry + recent = RecentFile() + self.RecentFiles.append(recent) + + recent.URI = item + recent.MimeType = mimetype + recent.Timestamp = int(time.time()) + recent.Private = private + if groups: + recent.Groups = groups + + self.sort() + + def deleteFile(self, item): + """Remove a recently used file, by URI, from the list. + """ + if item in self.RecentFiles: + self.RecentFiles.remove(item) + + def sort(self): + self.RecentFiles.sort() + self.RecentFiles.reverse() + + +class RecentFile: + def __init__(self): + self.URI = "" + self.MimeType = "" + self.Timestamp = "" + self.Private = False + self.Groups = [] + + def __cmp__(self, other): + return cmp(self.Timestamp, other.Timestamp) + + def __lt__ (self, other): + return self.Timestamp < other.Timestamp + + def __eq__(self, other): + return self.URI == str(other) + + def __str__(self): + return self.URI diff --git a/src/Pytop/widgets/mixins/xdg/__init__.py b/src/Pytop/widgets/mixins/xdg/__init__.py new file mode 100644 index 0000000..b5a117e --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/__init__.py @@ -0,0 +1,3 @@ +__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] + +__version__ = "0.26" diff --git a/src/Pytop/widgets/mixins/xdg/util.py b/src/Pytop/widgets/mixins/xdg/util.py new file mode 100644 index 0000000..1637aa5 --- /dev/null +++ b/src/Pytop/widgets/mixins/xdg/util.py @@ -0,0 +1,75 @@ +import sys + +PY3 = sys.version_info[0] >= 3 + +if PY3: + def u(s): + return s +else: + # Unicode-like literals + def u(s): + return s.decode('utf-8') + +try: + # which() is available from Python 3.3 + from shutil import which +except ImportError: + import os + # This is a copy of which() from Python 3.3 + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if not os.curdir in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if not normdir in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/__builtins__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/__builtins__.py new file mode 100644 index 0000000..3eea8aa --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/__builtins__.py @@ -0,0 +1,74 @@ +import builtins + +# Python imports +import builtins + +# Lib imports + +# Application imports +from ipc_server import IPCServer + + + +class EventSystem(IPCServer): + """ Inheret IPCServerMixin. Create an pub/sub systems. """ + + def __init__(self): + super(EventSystem, self).__init__() + + # NOTE: The format used is list of [type, target, (data,)] Where: + # type is useful context for control flow, + # target is the method to call, + # data is the method parameters to give + # Where data may be any kind of data + self._gui_events = [] + self._module_events = [] + + + # Makeshift fake "events" type system FIFO + def _pop_gui_event(self): + if len(self._gui_events) > 0: + return self._gui_events.pop(0) + return None + + def _pop_module_event(self): + if len(self._module_events) > 0: + return self._module_events.pop(0) + return None + + + def push_gui_event(self, event): + if len(event) == 3: + self._gui_events.append(event) + return None + + raise Exception("Invald event format! Please do: [type, target, (data,)]") + + def push_module_event(self, event): + if len(event) == 3: + self._module_events.append(event) + return None + + raise Exception("Invald event format! Please do: [type, target, (data,)]") + + def read_gui_event(self): + return self._gui_events[0] + + def read_module_event(self): + return self._module_events[0] + + def consume_gui_event(self): + return self._pop_gui_event() + + def consume_module_event(self): + return self._pop_module_event() + + + +# NOTE: Just reminding myself we can add to builtins two different ways... +# __builtins__.update({"event_system": Builtins()}) +builtins.app_name = "Pytop" +builtins.event_system = EventSystem() +builtins.event_sleep_time = 0.2 +builtins.debug = False +builtins.trace_debug = False diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/__init__.py index 054b613..90dc8da 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/__init__.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/__init__.py @@ -1,73 +1,3 @@ -#!/usr/bin/python3 - -# Python imports -import inspect - -from setproctitle import setproctitle - -# Gtk imports -import gi, faulthandler, signal -gi.require_version('Gtk', '3.0') - -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk -from gi.repository import GLib - -# Application imports -from utils import Settings -from signal_classes import Signals - - -class Main: - def __init__(self, args): - setproctitle('Pytop') - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit) - faulthandler.enable() # For better debug info - - builder = gtk.Builder() - settings = Settings() - settings.attachBuilder(builder) - self.connectBuilder(settings, builder) - - window = settings.createWindow() - window.fullscreen() - window.show() - - monitors = settings.returnMonitorsInfo() - i = 1 - if len(monitors) > 1: - for mon in monitors[1:]: - subBuilder = gtk.Builder() - subSettings = Settings(i) - subSettings.attachBuilder(subBuilder) - self.connectBuilder(subSettings, subBuilder) - - win = subSettings.createWindow() - win.set_default_size(mon.width, mon.height) - win.set_size_request(mon.width, mon.height) - win.set_resizable(False) - win.move(mon.x, mon.y) - - win.show() - i += 1 - - - def connectBuilder(self, settings, builder): - # Gets the methods from the classes and sets to handler. - # Then, builder connects to any signals it needs. - classes = [Signals(settings)] - - handlers = {} - for c in classes: - methods = inspect.getmembers(c, predicate=inspect.ismethod) - handlers.update(methods) - - builder.connect_signals(handlers) - - -if __name__ == "__main__": - try: - main = Main() - gtk.main() - except Exception as e: - print( repr(e) ) +""" + Start of package. +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/__main__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/__main__.py index 06514d9..3c99175 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/__main__.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/__main__.py @@ -2,18 +2,18 @@ # Python imports -import argparse +import argparse, faulthandler, traceback import pdb # For trace debugging from setproctitle import setproctitle -# Gtk imports +# Lib imports import gi, faulthandler, signal gi.require_version('Gtk', '3.0') -from gi.repository import Gtk as gtk +from gi.repository import Gtk from gi.repository import GLib # Application imports -from __init__ import Main +from main import Main @@ -21,15 +21,16 @@ if __name__ == "__main__": try: # pdb.set_trace() setproctitle('Pytop') - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit) faulthandler.enable() # For better debug info + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit) + parser = argparse.ArgumentParser() # Add long and short arguments parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.") - # Read arguments (If any...) - args = parser.parse_args() - main = Main(args) - gtk.main() + args, unknownargs = parser.parse_known_args() + + main = Main(args, unknownargs) + Gtk.main() except Exception as e: - print( repr(e) ) + traceback.print_exc() diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/context/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/__init__.py new file mode 100644 index 0000000..0d42d90 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/__init__.py @@ -0,0 +1,3 @@ +""" +Context module +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller.py new file mode 100644 index 0000000..ad367d3 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller.py @@ -0,0 +1,21 @@ +# Python imports +from datetime import datetime +import os + +# Gtk imports + + +# Application imports +from .controller_data import Controller_Data +from .mixins.main_menu_mixin import MainMenuMixin +from .mixins.taskbar_mixin import TaskbarMixin +from .mixins.cpu_draw_mixin import CPUDrawMixin +from .mixins.grid_mixin import GridMixin + + + + + +class Controller(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin, Controller_Data): + def __init__(self, _settings): + self.setup_controller_data(_settings) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/Signals.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller_data.py similarity index 55% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/Signals.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller_data.py index 97dd312..65613f0 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/Signals.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/controller_data.py @@ -1,22 +1,25 @@ # Python imports -from datetime import datetime -import os - -# Gtk imports +import os, signal +# Lib imports +from gi.repository import GLib # Application imports -from .mixins import CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin -from widgets import Grid -from widgets import Icon -from utils import FileHandler +from plugins.plugins import Plugins + +from widgets.grid import Grid +from widgets.icon import Icon +from utils.file_handler import FileHandler +class Controller_Data: + ''' Controller_Data contains most of the state of the app at ay given time. It also has some support methods. ''' -class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): - def __init__(self, settings): - self.settings = settings - self.builder = self.settings.returnBuilder() + def setup_controller_data(self, _settings): + self.plugins = Plugins(_settings) + + self.settings = _settings + self.builder = self.settings.get_builder() self.timeLabel = self.builder.get_object("timeLabel") self.drawArea = self.builder.get_object("drawArea") @@ -28,10 +31,6 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): self.setPagerWidget() self.setTasklistWidget() - # Must be after pager and task list inits - self.displayclock() - self.startClock() - # CPUDrawMixin Parts self.cpu_percents = [] self.doDrawBackground = False @@ -53,14 +52,13 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): # GridMixin Parts - self.filehandler = FileHandler(settings) + self.filehandler = FileHandler(self.settings) - self.builder = self.settings.returnBuilder() self.gridObj = self.builder.get_object("Desktop") selectDirDialog = self.builder.get_object("selectDirDialog") filefilter = self.builder.get_object("Folders") - self.currentPath = self.settings.returnSettings()[0] + self.currentPath = self.settings.getSettings()[0] self.copyCutArry = [] self.selectedFiles = [] self.gridClss = Grid(self.gridObj, self.settings) @@ -76,14 +74,44 @@ class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin): # Program Menu Parts self.menuWindow = self.builder.get_object("menuWindow") self.menuWindow.set_keep_above(True); - self.showIcons = False + self.showIcons = True self.iconFactory = Icon(self.settings) self.grpDefault = "Accessories" self.progGroup = self.grpDefault - HOME_APPS = os.path.expanduser('~') + "/.local/share/applications/" + HOME_APPS = f"{self.settings.get_user_home()}/.local/share/applications/" paths = ["/opt/", "/usr/share/applications/", HOME_APPS] self.menuData = self.getDesktopFilesInfo(paths) self.desktopObjs = [] - self.getSubgroup() - self.generateListView() + + + + def clear_console(self): + ''' Clears the terminal screen. ''' + os.system('cls' if os.name == 'nt' else 'clear') + + def call_method(self, _method_name, data = None): + ''' + Calls a method from scope of class. + + Parameters: + a (obj): self + b (str): method name to be called + c (*): Data (if any) to be passed to the method. + Note: It must be structured according to the given methods requirements. + + Returns: + Return data is that which the calling method gives. + ''' + method_name = str(_method_name) + method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") + return method(data) if data else method() + + def has_method(self, obj, name): + ''' Checks if a given method exists. ''' + return callable(getattr(obj, name, None)) + + def clear_children(self, widget): + ''' Clear children of a gtk widget. ''' + for child in widget.get_children(): + widget.remove(child) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/__init__.py new file mode 100644 index 0000000..5043d7d --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/__init__.py @@ -0,0 +1,3 @@ +""" +Mixins module +""" diff --git a/src/Pytop/signal_classes/mixins/CPUDrawMixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/cpu_draw_mixin.py old mode 100755 new mode 100644 similarity index 97% rename from src/Pytop/signal_classes/mixins/CPUDrawMixin.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/cpu_draw_mixin.py index e921777..b5a5d7b --- a/src/Pytop/signal_classes/mixins/CPUDrawMixin.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/cpu_draw_mixin.py @@ -1,17 +1,12 @@ -#!/usr/bin/python3 - - -# Python Imports +# Python imports from __future__ import division import cairo, psutil - -# GTK Imports +# Lib imports from gi.repository import GObject from gi.repository import GLib - - +# Application imports class CPUDrawMixin: @@ -54,7 +49,7 @@ class CPUDrawMixin: self.brush.set_source_rgba(rgba[0], rgba[1], rgba[2], rgba[3]) # Movbe to prev. point if any - if oldP is not 0.0 and oldX is not 0.0: + if oldP != 0.0 and oldX != 0.0: x = oldX y = float(self.ah) - (oldP * self.yStep) self.brush.move_to(x, y) diff --git a/src/Pytop/signal_classes/mixins/GridMixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/grid_mixin.py similarity index 99% rename from src/Pytop/signal_classes/mixins/GridMixin.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/grid_mixin.py index a10cf42..3f2428f 100644 --- a/src/Pytop/signal_classes/mixins/GridMixin.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/grid_mixin.py @@ -1,7 +1,7 @@ -# Gtk imports - # Python imports +# Lib imports + # Application imports diff --git a/src/Pytop/signal_classes/mixins/MainMenuMixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/main_menu_mixin.py similarity index 89% rename from src/Pytop/signal_classes/mixins/MainMenuMixin.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/main_menu_mixin.py index 2b6ad2b..1027d3d 100644 --- a/src/Pytop/signal_classes/mixins/MainMenuMixin.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/main_menu_mixin.py @@ -12,8 +12,8 @@ from os import listdir import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk as gtk -from gi.repository import GLib as glib +from gi.repository import Gtk +from gi.repository import GLib from xdg.DesktopEntry import DesktopEntry @@ -35,9 +35,9 @@ class MainMenuMixin: posY = pos[1] + 72 if self.menuWindow.get_visible() == False: self.menuWindow.move(posX, posY) - glib.idle_add(self.menuWindow.show_all) + GLib.idle_add(self.menuWindow.show_all) else: - glib.idle_add(self.menuWindow.hide) + GLib.idle_add(self.menuWindow.hide) def setListGroup(self, widget): @@ -65,7 +65,7 @@ class MainMenuMixin: # Should have this as a useful method...But, I don't want to import Glib everywhere children = widget.get_children() for child in children: - glib.idle_add(widget.remove, (child)) + GLib.idle_add(widget.remove, (child)) for obj in self.desktopObjs: title = obj[0] @@ -78,19 +78,22 @@ class MainMenuMixin: @threaded def update_view(self, widget, title, dirPath): - image = self.iconFactory.parseDesktopFiles(dirPath) # .get_pixbuf() + image = self.iconFactory.parse_desktop_files(dirPath) # .get_pixbuf() if self.showIcons: - glib.idle_add(self.addToProgramListView, (widget, title, image,)) + GLib.idle_add(self.addToProgramListView, *(widget, title, image, self.showIcons,)) else: - glib.idle_add(self.addToProgramListViewAsText, (widget, title, image,)) + GLib.idle_add(self.addToProgramListView, *(widget, title, image, self.showIcons,)) - def addToProgramListView(self, data): - widget, title, img = data - icon = gtk.Image().new_from_pixbuf(img) - button = gtk.Button(label=title) + def addToProgramListView(self, widget, title, image, show_image=True): + icon = Gtk.Image().new_from_pixbuf(image) + button = Gtk.Button(label=title) + + if show_image: + button.set_image(icon) + button.set_always_show_image(True) + pass - button.set_image(icon) button.connect("clicked", self.executeProgram) children = button.get_children() @@ -107,22 +110,6 @@ class MainMenuMixin: button.show_all() widget.add(button) - def addToProgramListViewAsText(self, data): - widget, title, icon = data - button = gtk.Button(label=title) - button.connect("clicked", self.executeProgram) - - children = button.get_children() - label = children[0] - - label.set_halign(1) - label.set_line_wrap(True) - label.set_max_width_chars(38) - label.set_size_request(640, 64) - - button.show_all() - glib.idle_add(widget.add, (button)) - def executeProgram(self, widget): """ diff --git a/src/Pytop/signal_classes/mixins/TaskbarMixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/taskbar_mixin.py similarity index 90% rename from src/Pytop/signal_classes/mixins/TaskbarMixin.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/taskbar_mixin.py index f8fee3b..df315b2 100644 --- a/src/Pytop/signal_classes/mixins/TaskbarMixin.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/context/mixins/taskbar_mixin.py @@ -2,15 +2,15 @@ import threading from datetime import datetime -# Gtk imports +# Lib imports import gi gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') gi.require_version('Wnck', '3.0') -from gi.repository import Wnck as wnck -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk +from gi.repository import Wnck +from gi.repository import Gtk +from gi.repository import Gdk from gi.repository import GLib from gi.repository import GObject @@ -43,11 +43,11 @@ class TaskbarMixin: def showSystemStats(self, widget, eve): - if eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON: + if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON: self.builder.get_object('systemStats').popup() def setPagerWidget(self): - pager = wnck.Pager() + pager = Wnck.Pager() if self.orientation == 0: self.builder.get_object('taskBarWorkspacesHor').add(pager) @@ -58,7 +58,7 @@ class TaskbarMixin: def setTasklistWidget(self): - tasklist = wnck.Tasklist() + tasklist = Wnck.Tasklist() tasklist.set_scroll_enabled(False) tasklist.set_button_relief(2) # 0 = normal relief, 2 = no relief tasklist.set_grouping(1) # 0 = mever group, 1 auto group, 2 = always group diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/ipc_server.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/ipc_server.py new file mode 100644 index 0000000..589a906 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/ipc_server.py @@ -0,0 +1,71 @@ +# Python imports +import threading, socket, time +from multiprocessing.connection import Listener, Client + +# Lib imports + +# Application imports + + +def threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() + return wrapper + + + + +class IPCServer: + ''' Create a listener so that other instances send requests back to existing instance. ''' + def __init__(self): + self.is_ipc_alive = False + self.ipc_authkey = b'pytop-ipc' + self.ipc_address = '127.0.0.1' + self.ipc_port = 8888 + self.ipc_timeout = 15.0 + + @threaded + def create_ipc_server(self): + listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) + self.is_ipc_alive = True + while True: + conn = listener.accept() + start_time = time.time() + + print(f"New Connection: {listener.last_accepted}") + while True: + msg = conn.recv() + if debug: + print(msg) + + if "FILE|" in msg: + file = msg.split("FILE|")[1].strip() + if file: + event_system.push_gui_event([None, "handle_file_from_ipc", (file,)]) + + conn.close() + break + + + if msg == 'close connection': + conn.close() + break + if msg == 'close server': + conn.close() + break + + # NOTE: Not perfect but insures we don't lockup the connection for too long. + end_time = time.time() + if (end - start) > self.ipc_timeout: + conn.close() + + listener.close() + + + def send_ipc_message(self, message="Empty Data..."): + try: + conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) + conn.send(message) + conn.send('close connection') + except Exception as e: + print(repr(e)) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/main.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/main.py new file mode 100644 index 0000000..6c1166d --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/main.py @@ -0,0 +1,47 @@ +# Python imports +import inspect + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from utils.settings import Settings +from context.controller import Controller +from __builtins__ import EventSystem + + +class Main(EventSystem): + def __init__(self, args, unknownargs): + settings = Settings() + settings.set_window_data(Gtk.Window()) + monitors = settings.get_monitor_info() + for i, mon in enumerate(monitors): + sub_builder = Gtk.Builder() + sub_settings = Settings(i) + sub_settings.attach_builder(sub_builder) + self.connect_builder(sub_settings, sub_builder) + + window = sub_settings.create_window() + window.set_default_size(mon.width, mon.height) + window.set_size_request(mon.width, mon.height) + window.set_resizable(False) + window.resize(mon.width, mon.height) + window.move(mon.x, mon.y) + window.show() + + + + def connect_builder(self, settings, builder): + # Gets the methods from the classes and sets to handler. + # Then, builder connects to any signals it needs. + classes = [Controller(settings)] + + handlers = {} + for c in classes: + methods = inspect.getmembers(c, predicate=inspect.ismethod) + handlers.update(methods) + + builder.connect_signals(handlers) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/__init__.py new file mode 100644 index 0000000..5624b32 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/__init__.py @@ -0,0 +1,3 @@ +""" + Gtk Bound Plugins Module +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/plugins.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/plugins.py new file mode 100644 index 0000000..b77ab32 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/plugins/plugins.py @@ -0,0 +1,78 @@ +# Python imports +import os, sys, importlib, traceback +from os.path import join, isdir + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gio + +# Application imports + + +class Plugin: + name = None + module = None + reference = None + + +class Plugins: + """Plugins controller""" + + def __init__(self, settings): + self._settings = settings + self._builder = self._settings.get_builder() + self._plugins_path = self._settings.get_plugins_path() + self._plugins_dir_watcher = None + self._plugin_collection = [] + + + def launch_plugins(self): + self._set_plugins_watcher() + self.load_plugins() + + def _set_plugins_watcher(self): + self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \ + .monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable()) + self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ()) + + def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None): + if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, + Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, + Gio.FileMonitorEvent.MOVED_OUT]: + self.reload_plugins(file) + + def load_plugins(self, file=None): + print(f"Loading plugins...") + parent_path = os.getcwd() + + for file in os.listdir(self._plugins_path): + try: + path = join(self._plugins_path, file) + if isdir(path): + os.chdir(path) + + sys.path.insert(0, path) + spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py")) + app = importlib.util.module_from_spec(spec) + spec.loader.exec_module(app) + + plugin_reference = app.Plugin(self._builder, event_system) + plugin = Plugin() + plugin.name = plugin_reference.get_plugin_name() + plugin.module = path + plugin.reference = plugin_reference + + self._plugin_collection.append(plugin) + except Exception as e: + print("Malformed plugin! Not loading!") + traceback.print_exc() + + os.chdir(parent_path) + + + def reload_plugins(self, file=None): + print(f"Reloading plugins...") + + def set_message_on_plugin(self, type, data): + print("Trying to send message to plugin...") diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/__init__.py deleted file mode 100644 index f5b8431..0000000 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .mixins import CPUDrawMixin -from .mixins import TaskbarMixin -from .mixins import GridMixin -from signal_classes.Signals import Signals diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/__init__.py deleted file mode 100644 index 7df5562..0000000 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/signal_classes/mixins/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .MainMenuMixin import MainMenuMixin -from .TaskbarMixin import TaskbarMixin -from .CPUDrawMixin import CPUDrawMixin -from .GridMixin import GridMixin diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Settings.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Settings.py deleted file mode 100644 index c9c4174..0000000 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Settings.py +++ /dev/null @@ -1,180 +0,0 @@ -# Gtk imports -import gi, cairo -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') - -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk - -# Python imports -import os, json - -# Application imports - - -class Settings: - def __init__(self, monIndex = 0): - self.builder = None - self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" - - # 'Filters' - self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', - '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') - self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', - '.mpeg', '.mp4', '.webm') - self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf') - self.music = ('.psf', '.mp3', '.ogg' , '.flac') - self.images = ('.png', '.jpg', '.jpeg', '.gif') - self.pdf = ('.pdf') - - self.hideHiddenFiles = True - self.ColumnSize = 8 - self.usrHome = os.path.expanduser('~') - self.desktopPath = self.usrHome + "/Desktop" - self.iconContainerWxH = [128, 128] - self.systemIconImageWxH = [56, 56] - self.viIconWxH = [256, 128] - self.monitors = None - - self.DEFAULTCOLOR = gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000 - self.MOUSEOVERCOLOR = gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff - self.SELECTEDCOLOR = gdk.RGBA(0.4, 0.5, 0.1, 0.84) - self.TRASHFOLDER = os.path.expanduser('~') + "/.local/share/Trash/" - self.TRASHFILESFOLDER = self.TRASHFOLDER + "files/" - self.TRASHINFOFOLDER = self.TRASHFOLDER + "info/" - self.THUMB_GENERATOR = "ffmpegthumbnailer" - self.MEDIAPLAYER = "mpv"; - self.IMGVIEWER = "mirage"; - self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef"; - self.OFFICEPROG = "libreoffice"; - self.TEXTVIEWER = "leafpad"; - self.PDFVIEWER = "evince"; - self.FILEMANAGER = "spacefm"; - self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% "; - self.MPV_WH = " -geometry 50%:50% "; - self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) - - configFolder = os.path.expanduser('~') + "/.config/pytop/" - self.configFile = configFolder + "mon_" + str(monIndex) + "_settings.ini" - - if os.path.isdir(configFolder) == False: - os.mkdir(configFolder) - - if os.path.isdir(self.TRASHFOLDER) == False: - os.mkdir(TRASHFILESFOLDER) - os.mkdir(TRASHINFOFOLDER) - - if os.path.isdir(self.TRASHFILESFOLDER) == False: - os.mkdir(TRASHFILESFOLDER) - - if os.path.isdir(self.TRASHINFOFOLDER) == False: - os.mkdir(TRASHINFOFOLDER) - - if os.path.isfile(self.configFile) == False: - open(self.configFile, 'a').close() - self.saveSettings(self.desktopPath) - - - def attachBuilder(self, builder): - self.builder = builder - self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade") - - def createWindow(self): - # Get window and connect signals - window = self.builder.get_object("Window") - window.connect("delete-event", gtk.main_quit) - self.setWindowData(window) - return window - - def setWindowData(self, window): - screen = window.get_screen() - visual = screen.get_rgba_visual() - if visual != None and screen.is_composited(): - window.set_visual(visual) - - # bind css file - cssProvider = gtk.CssProvider() - cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css') - screen = gdk.Screen.get_default() - styleContext = gtk.StyleContext() - styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER) - - window.set_app_paintable(True) - self.monitors = self.getMonitorData(screen) - window.resize(self.monitors[0].width, self.monitors[0].height) - - def getMonitorData(self, screen): - monitors = [] - for m in range(screen.get_n_monitors()): - monitors.append(screen.get_monitor_geometry(m)) - - for monitor in monitors: - print(str(monitor.width) + "+" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y)) - - return monitors - - - def returnMonitorsInfo(self): - return self.monitors - - - def saveSettings(self, startPath): - data = {} - data['pytop_settings'] = [] - - data['pytop_settings'].append({ - 'startPath' : startPath - }) - - with open(self.configFile, 'w') as outfile: - json.dump(data, outfile) - - - def returnSettings(self): - returnData = [] - - with open(self.configFile) as infile: - try: - data = json.load(infile) - for obj in data['pytop_settings']: - returnData = [obj['startPath']] - except Exception as e: - returnData = ['~/Desktop/'] - - - if returnData[0] == '': - returnData[0] = '~/Desktop/' - - return returnData - - - def returnBuilder(self): return self.builder - def returnUserHome(self): return self.usrHome - def returnDesktopPath(self): return self.usrHome + "/Desktop" - def returnColumnSize(self): return self.ColumnSize - def returnContainerWH(self): return self.iconContainerWxH - def returnSystemIconImageWH(self): return self.systemIconImageWxH - def returnVIIconWH(self): return self.viIconWxH - def isHideHiddenFiles(self): return self.hideHiddenFiles - - # Filter returns - def returnOfficeFilter(self): return self.office - def returnVidsFilter(self): return self.vids - def returnTextFilter(self): return self.txt - def returnMusicFilter(self): return self.music - def returnImagesFilter(self): return self.images - def returnPdfFilter(self): return self.pdf - - def returnIconImagePos(self): return self.GTK_ORIENTATION - def getThumbnailGenerator(self): return self.THUMB_GENERATOR - def returnMediaProg(self): return self.MEDIAPLAYER - def returnImgVwrProg(self): return self.IMGVIEWER - def returnMusicProg(self): return self.MUSICPLAYER - def returnOfficeProg(self): return self.OFFICEPROG - def returnTextProg(self): return self.TEXTVIEWER - def returnPdfProg(self): return self.PDFVIEWER - def returnFileMngrProg(self): return self.FILEMANAGER - def returnMplyrWH(self): return self.MPLAYER_WH - def returnMpvWHProg(self): return self.MPV_WH - def returnTrshFilesPth(self): return self.TRASHFILESFOLDER - def returnTrshInfoPth(self): return self.TRASHINFOFOLDER diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/__init__.py index 548f6d4..4247336 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/__init__.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/__init__.py @@ -1,4 +1,3 @@ -from utils.Dragging import Dragging -from .Logger import Logger -from utils.FileHandler import FileHandler -from utils.Settings import Settings +""" +Utils modile +""" diff --git a/src/Pytop/utils/Dragging.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/dragging.py similarity index 99% rename from src/Pytop/utils/Dragging.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/utils/dragging.py index abf0a03..0f11663 100644 --- a/src/Pytop/utils/Dragging.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/dragging.py @@ -1,4 +1,7 @@ -# Gtk imports +# Python imports +import os + +# Lib imports import gi gi.require_version('Gdk', '3.0') @@ -6,8 +9,6 @@ gi.require_version('Gdk', '3.0') from gi.repository import Gdk from gi.repository import GObject -# Python imports -import os # Application imports diff --git a/src/Pytop/utils/FileHandler.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/file_handler.py similarity index 82% rename from src/Pytop/utils/FileHandler.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/utils/file_handler.py index 3c8045d..ffda606 100644 --- a/src/Pytop/utils/FileHandler.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/file_handler.py @@ -12,27 +12,29 @@ def threaded(fn): return wrapper class FileHandler: - def __init__(self, settings): + def __init__(self, _settings): + self.settings = _settings + # 'Filters' - self.office = settings.returnOfficeFilter() - self.vids = settings.returnVidsFilter() - self.txt = settings.returnTextFilter() - self.music = settings.returnMusicFilter() - self.images = settings.returnImagesFilter() - self.pdf = settings.returnPdfFilter() + self.office = self.settings.getOfficeFilter() + self.vids = self.settings.getVidsFilter() + self.txt = self.settings.getTextFilter() + self.music = self.settings.getMusicFilter() + self.images = self.settings.getImagesFilter() + self.pdf = self.settings.getPdfFilter() # Args - self.MEDIAPLAYER = settings.returnMediaProg() - self.IMGVIEWER = settings.returnImgVwrProg() - self.MUSICPLAYER = settings.returnMusicProg() - self.OFFICEPROG = settings.returnOfficeProg() - self.TEXTVIEWER = settings.returnTextProg() - self.PDFVIEWER = settings.returnPdfProg() - self.FILEMANAGER = settings.returnFileMngrProg() - self.MPLAYER_WH = settings.returnMplyrWH() - self.MPV_WH = settings.returnMpvWHProg() - self.TRASHFILESFOLDER = settings.returnTrshFilesPth() - self.TRASHINFOFOLDER = settings.returnTrshInfoPth() + self.MEDIAPLAYER = self.settings.getMediaProg() + self.IMGVIEWER = self.settings.getImgVwrProg() + self.MUSICPLAYER = self.settings.getMusicProg() + self.OFFICEPROG = self.settings.getOfficeProg() + self.TEXTVIEWER = self.settings.getTextProg() + self.PDFVIEWER = self.settings.getPdfProg() + self.FILEMANAGER = self.settings.getFileMngrProg() + self.MPLAYER_WH = self.settings.getMplyrWH() + self.MPV_WH = self.settings.getMpvWHProg() + self.TRASHFILESFOLDER = self.settings.getTrshFilesPth() + self.TRASHINFOFOLDER = self.settings.getTrshInfoPth() def openFile(self, file): diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Logger.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/logger.py similarity index 89% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Logger.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/utils/logger.py index c8dc0db..ba66789 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/Logger.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/logger.py @@ -1,12 +1,14 @@ # Python imports import os, logging +# Lib imports + # Application imports class Logger: - def __init__(self): - self.USER_HOME = os.path.expanduser("~") + def __init__(self, home): + self._USER_HOME = home def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True): """ @@ -41,8 +43,8 @@ class Logger: log.addHandler(ch) if createFile: - folder = self.USER_HOME + ".config/pytop/logs" - file = folder + "/application.log" + folder = f"{self._USER_HOME}/.config/{app_name.lower()}/logs" + file = f"{folder}/application.log" if not os.path.exists(folder): os.mkdir(folder) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/settings.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/settings.py new file mode 100644 index 0000000..ba46229 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/utils/settings.py @@ -0,0 +1,199 @@ +# Gtk imports +import gi, cairo +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +from gi.repository import Gtk +from gi.repository import Gdk + +# Python imports +import os, json + +# Application imports +from .logger import Logger + + +class Settings: + def __init__(self, monIndex = 0): + + self._USR_PATH = f"/usr/share/{app_name.lower()}" + self._USER_HOME = os.path.expanduser('~') + self._SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + self._DESKTOP_PATH = f"{self._USER_HOME}/Desktop" + self._CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}" + self._CONFIG_FILE = f"{self._CONFIG_PATH}/mon_{str(monIndex)}_settings.ini" + self._PLUGINS_PATH = f"{self._CONFIG_PATH}/plugins" + self._LOGGER = Logger(self._USER_HOME) + + self._DEFAULT_ICONS = f"{self._CONFIG_PATH}/icons" + self._INTERNAL_ICON_PTH = f"{self._DEFAULT_ICONS}/bin.png" + self._ABS_THUMBS_PTH = f"{self._USER_HOME}/.thumbnails/normal" + self._STEAM_ICONS_PTH = f"{self._USER_HOME}/.thumbnails/steam_icons" + self._ICON_DIRS = ["/usr/share/icons", f"{self._USER_HOME}/.local/share/icons"] + + self.DEFAULTCOLOR = Gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000 + self.MOUSEOVERCOLOR = Gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff + self.SELECTEDCOLOR = Gdk.RGBA(0.4, 0.5, 0.1, 0.84) + self._TRASHFOLDER = f"{self._USER_HOME}/.local/share/Trash" + self._TRASH_FILES_FOLDER = f"{self._TRASHFOLDER}/files/" + self._TRASH_INFO_FOLDER = f"{self._TRASHFOLDER}/info/" + self.THUMB_GENERATOR = "ffmpegthumbnailer" + self.MEDIAPLAYER = "mpv"; + self.IMGVIEWER = "mirage"; + self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef"; + self.OFFICEPROG = "libreoffice"; + self.TEXTVIEWER = "leafpad"; + self.PDFVIEWER = "evince"; + self.FILEMANAGER = "spacefm"; + self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% "; + self.MPV_WH = " -geometry 50%:50% "; + self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1) + + # 'Filters' + self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', + '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') + self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', + '.mpeg', '.mp4', '.webm') + self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf') + self.music = ('.psf', '.mp3', '.ogg' , '.flac') + self.images = ('.png', '.jpg', '.jpeg', '.gif') + self.pdf = ('.pdf') + + self.hideHiddenFiles = True + self.ColumnSize = 8 + self.iconContainerWxH = [128, 128] + self.systemIconImageWxH = [56, 56] + self.viIconWxH = [256, 128] + self.monitors = None + self.builder = None + + if os.path.isdir(self._CONFIG_PATH) == False: + os.mkdir(self._CONFIG_PATH) + + if os.path.isdir(self._TRASHFOLDER) == False: + os.mkdir(TRASHFILESFOLDER) + os.mkdir(TRASHINFOFOLDER) + + if os.path.isdir(self._TRASH_FILES_FOLDER) == False: + os.mkdir(TRASHFILESFOLDER) + + if os.path.isdir(self._TRASH_INFO_FOLDER) == False: + os.mkdir(TRASHINFOFOLDER) + + if os.path.isfile(self._CONFIG_FILE) == False: + open(self._CONFIG_FILE, 'a').close() + self.saveSettings(self._DESKTOP_PATH) + + + def attach_builder(self, builder): + self.builder = builder + self.builder.add_from_file(f"{self._CONFIG_PATH}/Main_Window.glade") + + def create_window(self): + # Get window and connect signals + window = self.builder.get_object("Window") + window.connect("delete-event", Gtk.main_quit) + self.set_window_data(window) + return window + + def set_window_data(self, window): + screen = window.get_screen() + visual = screen.get_rgba_visual() + if visual != None and screen.is_composited(): + window.set_visual(visual) + + # bind css file + cssProvider = Gtk.CssProvider() + cssProvider.load_from_path(f'{self._CONFIG_PATH}/stylesheet.css') + screen = Gdk.Screen.get_default() + styleContext = Gtk.StyleContext() + styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + + window.set_app_paintable(True) + self.monitors = self.get_monitor_data(screen) + + def get_monitor_data(self, screen): + monitors = [] + for m in range(screen.get_n_monitors()): + monitors.append(screen.get_monitor_geometry(m)) + + for monitor in monitors: + print(str(monitor.width) + "+" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y)) + + return monitors + + + def get_monitor_info(self): + return self.monitors + + + def saveSettings(self, startPath): + data = {} + data['pytop_settings'] = [] + + data['pytop_settings'].append({ + 'startPath' : startPath + }) + + with open(self._CONFIG_FILE, 'w') as outfile: + json.dump(data, outfile) + + + def getSettings(self): + returnData = [] + + with open(self._CONFIG_FILE) as infile: + try: + data = json.load(infile) + for obj in data['pytop_settings']: + returnData = [obj['startPath']] + except Exception as e: + returnData = [f'{self._DESKTOP_PATH}'] + + + if returnData[0] == '': + returnData[0] = f'{self._DESKTOP_PATH}' + + return returnData + + + def get_builder(self): return self.builder + def get_user_home(self): return self._USER_HOME + + def get_desktop_path(self): return self._DESKTOP_PATH + def get_config_path(self): return self._CONFIG_PATH + def get_plugins_path(self): return self._PLUGINS_PATH + + def getColumnSize(self): return self.ColumnSize + def getContainerWH(self): return self.iconContainerWxH + def getSystemIconImageWH(self): return self.systemIconImageWxH + def getVIIconWH(self): return self.viIconWxH + def isHideHiddenFiles(self): return self.hideHiddenFiles + + # Filter returns + def getOfficeFilter(self): return self.office + def getVidsFilter(self): return self.vids + def getTextFilter(self): return self.txt + def getMusicFilter(self): return self.music + def getImagesFilter(self): return self.images + def getPdfFilter(self): return self.pdf + + def getIconImagePos(self): return self.GTK_ORIENTATION + def getThumbnailGenerator(self): return self.THUMB_GENERATOR + def getMediaProg(self): return self.MEDIAPLAYER + def getImgVwrProg(self): return self.IMGVIEWER + def getMusicProg(self): return self.MUSICPLAYER + def getOfficeProg(self): return self.OFFICEPROG + def getTextProg(self): return self.TEXTVIEWER + def getPdfProg(self): return self.PDFVIEWER + def getFileMngrProg(self): return self.FILEMANAGER + def getMplyrWH(self): return self.MPLAYER_WH + def getMpvWHProg(self): return self.MPV_WH + def getTrshFilesPth(self): return self._TRASH_FILES_FOLDER + def getTrshInfoPth(self): return self._TRASH_INFO_FOLDER + + def getDefaultIcon(self): return self._INTERNAL_ICON_PTH + def getInternalIconsPth(self): return self._DEFAULT_ICONS + def getAbsThumbsPth(self): return self._ABS_THUMBS_PTH + def getSteamIconsPth(self): return self._STEAM_ICONS_PTH + def getIconDirs(self): return self._ICON_DIRS diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Icon.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Icon.py deleted file mode 100644 index 2562a9b..0000000 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Icon.py +++ /dev/null @@ -1,228 +0,0 @@ -# Python Imports -import os, subprocess, hashlib, threading -from os.path import isdir, isfile, join - - -# Gtk imports -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') - -from gi.repository import Gtk as gtk -from gi.repository import Gio as gio -from xdg.DesktopEntry import DesktopEntry - - -# Application imports - - - - -def threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs).start() - return wrapper - -class Icon: - def __init__(self, settings): - self.settings = settings - self.thubnailGen = settings.getThumbnailGenerator() - self.vidsList = settings.returnVidsFilter() - self.imagesList = settings.returnImagesFilter() - self.GTK_ORIENTATION = settings.returnIconImagePos() - self.usrHome = settings.returnUserHome() - self.iconContainerWH = settings.returnContainerWH() - self.systemIconImageWH = settings.returnSystemIconImageWH() - self.viIconWH = settings.returnVIIconWH() - self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" - self.INTERNAL_ICON_PTH = self.SCRIPT_PTH + "../resources/icons/text.png" - - - def createIcon(self, dir, file): - fullPath = dir + "/" + file - return self.getIconImage(file, fullPath) - - def createThumbnail(self, dir, file): - fullPath = dir + "/" + file - try: - # Video thumbnail - if file.lower().endswith(self.vidsList): - fileHash = hashlib.sha256(str.encode(fullPath)).hexdigest() - hashImgPth = self.usrHome + "/.thumbnails/normal/" + fileHash + ".png" - - if isfile(hashImgPth) == False: - self.generateVideoThumbnail(fullPath, hashImgPth) - - thumbnl = self.createScaledImage(hashImgPth, self.viIconWH) - if thumbnl == None: # If no icon whatsoever, return internal default - thumbnl = gtk.Image.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - - return thumbnl - except Exception as e: - print("Thumbnail generation issue:") - print( repr(e) ) - return gtk.Image.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - - - def getIconImage(self, file, fullPath): - try: - thumbnl = None - - # Video icon - if file.lower().endswith(self.vidsList): - thumbnl = gtk.Image.new_from_file(self.SCRIPT_PTH + "../resources/icons/video.png") - # Image Icon - elif file.lower().endswith(self.imagesList): - thumbnl = self.createScaledImage(fullPath, self.viIconWH) - # .desktop file parsing - elif fullPath.lower().endswith( ('.desktop',) ): - thumbnl = self.parseDesktopFiles(fullPath) - # System icons - else: - thumbnl = self.getSystemThumbnail(fullPath, self.systemIconImageWH[0]) - - if thumbnl == None: # If no icon whatsoever, return internal default - thumbnl = gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) - - return thumbnl - except Exception as e: - print("Icon generation issue:") - print( repr(e) ) - return gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) - - def parseDesktopFiles(self, fullPath): - try: - xdgObj = DesktopEntry(fullPath) - icon = xdgObj.getIcon() - altIconPath = "" - - if "steam" in icon: - steamIconsDir = self.usrHome + "/.thumbnails/steam_icons/" - name = xdgObj.getName() - fileHash = hashlib.sha256(str.encode(name)).hexdigest() - - if isdir(steamIconsDir) == False: - os.mkdir(steamIconsDir) - - hashImgPth = steamIconsDir + fileHash + ".jpg" - if isfile(hashImgPth) == True: - # Use video sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) - - execStr = xdgObj.getExec() - parts = execStr.split("steam://rungameid/") - id = parts[len(parts) - 1] - imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg" - proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink]) - proc.wait() - - # Use video thumbnail sizes since headers are bigger - return self.createScaledImage(hashImgPth, self.viIconWH) - elif os.path.exists(icon): - return self.createScaledImage(icon, self.systemIconImageWH) - else: - iconsDirs = ["/usr/share/pixmaps", "/usr/share/icons", self.usrHome + "/.icons" ,] - altIconPath = "" - - for iconsDir in iconsDirs: - altIconPath = self.traverseIconsFolder(iconsDir, icon) - if altIconPath is not "": - break - - return self.createScaledImage(altIconPath, self.systemIconImageWH) - except Exception as e: - print(".desktop icon generation issue:") - print( repr(e) ) - return None - - - def traverseIconsFolder(self, path, icon): - altIconPath = "" - - for (dirpath, dirnames, filenames) in os.walk(path): - for file in filenames: - appNM = "application-x-" + icon - if icon in file or appNM in file: - altIconPath = dirpath + "/" + file - break - - return altIconPath - - - def getSystemThumbnail(self, filename, size): - try: - if os.path.exists(filename): - gioFile = gio.File.new_for_path(filename) - info = gioFile.query_info('standard::icon' , 0, gio.Cancellable()) - icon = info.get_icon().get_names()[0] - iconTheme = gtk.IconTheme.get_default() - iconData = iconTheme.lookup_icon(icon , size , 0) - if iconData: - iconPath = iconData.get_filename() - return gtk.Image.new_from_file(iconPath) # This seems to cause a lot of core dump issues... - else: - return None - else: - return None - except Exception as e: - print("system icon generation issue:") - print( repr(e) ) - return None - - - def createScaledImage(self, path, wxh): - try: - pixbuf = gtk.Image.new_from_file(path).get_pixbuf() - scaledPixBuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default - return gtk.Image.new_from_pixbuf(scaledPixBuf) - except Exception as e: - print("Image Scaling Issue:") - print( repr(e) ) - return None - - def createFromFile(self, path): - try: - return gtk.Image.new_from_file(path) - except Exception as e: - print("Image from file Issue:") - print( repr(e) ) - return None - - def returnGenericIcon(self): - return gtk.Image.new_from_file(self.INTERNAL_ICON_PTH) - - - def generateVideoThumbnail(self, fullPath, hashImgPth): - proc = None - try: - # Stream duration - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Stream duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command, stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Format (container) duration type: image2 - if "N/A" in duration: - command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", fullPath] - data = subprocess.run(command , stdout=subprocess.PIPE) - duration = data.stdout.decode('utf-8') - - # Get frame roughly 35% through video - grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) - command = ["ffmpeg", "-ss", grabTime, "-an", "-i", fullPath, "-s", "320x180", "-vframes", "1", hashImgPth] - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - proc.wait() - except Exception as e: - print("Video thumbnail generation issue in thread:") - print( repr(e) ) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/__init__.py index 148c91c..896cbb1 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/__init__.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/__init__.py @@ -1,2 +1,3 @@ -from widgets.Grid import Grid -from widgets.Icon import Icon +""" +Widgets module +""" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Grid.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/grid.py similarity index 64% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Grid.py rename to src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/grid.py index ae67fb7..392d443 100644 --- a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/Grid.py +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/grid.py @@ -4,20 +4,21 @@ from os.path import isdir, isfile, join from os import listdir -# Gtk imports +# Lib imports import gi gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk -from gi.repository import GLib as glib +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import Gio from gi.repository import GdkPixbuf # Application imports -from .Icon import Icon -from utils.FileHandler import FileHandler +from .icon import Icon +from utils.file_handler import FileHandler def threaded(fn): @@ -27,19 +28,19 @@ def threaded(fn): class Grid: - def __init__(self, grid, settings): - self.grid = grid - self.settings = settings + def __init__(self, _grid, _settings): + self.grid = _grid + self.settings = _settings self.fileHandler = FileHandler(self.settings) - self.store = gtk.ListStore(GdkPixbuf.Pixbuf, str) - self.usrHome = settings.returnUserHome() - self.hideHiddenFiles = settings.isHideHiddenFiles() - self.builder = settings.returnBuilder() - self.ColumnSize = settings.returnColumnSize() - self.vidsFilter = settings.returnVidsFilter() - self.imagesFilter = settings.returnImagesFilter() - self.iconFactory = Icon(settings) + self.store = Gtk.ListStore(GdkPixbuf.Pixbuf or None, str) + self.usrHome = self.settings.get_user_home() + self.hideHiddenFiles = self.settings.isHideHiddenFiles() + self.builder = self.settings.get_builder() + self.ColumnSize = self.settings.getColumnSize() + self.vidsFilter = self.settings.getVidsFilter() + self.imagesFilter = self.settings.getImagesFilter() + self.iconFactory = Icon(self.settings) self.selectedFiles = [] self.currentPath = "" @@ -86,45 +87,53 @@ class Grid: files = dirPaths + vids + images + desktop + files self.generateGridIcons(path, files) - self.fillVideoIcons(path, vids, len(dirPaths)) + + + def generateGridIcons(self, dir, files): + for i, file in enumerate(files): + self.store.append([None, file]) + self.create_icon(i, dir, file) @threaded - def generateGridIcons(self, dirPath, files): - for file in files: - image = self.iconFactory.createIcon(dirPath, file).get_pixbuf() - glib.idle_add(self.addToGrid, (image, file,)) + def create_icon(self, i, dir, file): + icon = self.iconFactory.create_icon(dir, file) + fpath = f"{dir}/{file}" + GLib.idle_add(self.update_store, (i, icon, fpath,)) + def update_store(self, item): + i, icon, fpath = item + itr = self.store.get_iter(i) - @threaded - def fillVideoIcons(self, dirPath, files, start): - model = self.grid.get_model() + if not icon: + icon = self.get_system_thumbnail(fpath, self.iconFactory.SYS_ICON_WH[0]) + if not icon: + if fpath.endswith(".gif"): + icon = GdkPixbuf.PixbufAnimation.get_static_image(fpath) + else: + icon = GdkPixbuf.Pixbuf.new_from_file(self.iconFactory.INTERNAL_ICON_PTH) - # Wait till we have a proper index... - while len(self.store) < (start + 1): - time.sleep(.650) + self.store.set_value(itr, 0, icon) - i = start - for file in files: - self.updateGrid(model, dirPath, file, i) - i += 1 - - @threaded - def updateGrid(self, model, dirPath, file, i): + def get_system_thumbnail(self, filename, size): try: - image = self.iconFactory.createThumbnail(dirPath, file).get_pixbuf() - iter = model.get_iter_from_string(str(i)) - glib.idle_add(self.replaceInGrid, (iter, image,)) + if os.path.exists(filename): + gioFile = Gio.File.new_for_path(filename) + info = gioFile.query_info('standard::icon' , 0, Gio.Cancellable()) + icon = info.get_icon().get_names()[0] + iconTheme = Gtk.IconTheme.get_default() + iconData = iconTheme.lookup_icon(icon , size , 0) + if iconData: + iconPath = iconData.get_filename() + return GdkPixbuf.Pixbuf.new_from_file(iconPath) + else: + return None + else: + return None except Exception as e: - # Errors seem to happen when fillVideoIcons index wait check is to low - print("widgets/Grid.py sinking errors on updateGrid method...") - - def addToGrid(self, dataSet): - self.store.append([dataSet[0], dataSet[1]]) - - def replaceInGrid(self, dataSet): - # Iter, row column, new pixbuf... - self.store.set_value(dataSet[0], 0 , dataSet[1]) + print("System icon generation issue:") + print( repr(e) ) + return None def iconDblLeftClick(self, widget, item): @@ -140,11 +149,11 @@ class Grid: parentDir = os.path.abspath(os.path.join(dir, os.pardir)) self.currentPath = parentDir self.setNewDirectory(parentDir) - self.settings.saveSettings(parentDir) + self.self.settings.saveSettings(parentDir) elif isdir(file): self.currentPath = file self.setNewDirectory(self.currentPath) - self.settings.saveSettings(self.currentPath) + self.self.settings.saveSettings(self.currentPath) elif isfile(file): self.fileHandler.openFile(file) except Exception as e: @@ -152,7 +161,7 @@ class Grid: def iconSingleClick(self, widget, eve, rclicked_icon): try: - if eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == 1: + if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: self.selectedFiles.clear() items = widget.get_selected_items() model = widget.get_model() @@ -165,7 +174,7 @@ class Grid: file = dir + "/" + fileName self.selectedFiles.append(file) # Used for return to caller - elif eve.type == gdk.EventType.BUTTON_RELEASE and eve.button == 3: + elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: input = self.builder.get_object("filenameInput") controls = self.builder.get_object("iconControlsWindow") iconsButtonBox = self.builder.get_object("iconsButtonBox") diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/icon.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/icon.py new file mode 100644 index 0000000..99af6ab --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/icon.py @@ -0,0 +1,93 @@ +# Python Imports +import os, subprocess, threading, hashlib +from os.path import isfile + +# Gtk imports +from gi.repository import GdkPixbuf + +# Application imports + +from .mixins.video_icon_mixin import VideoIconMixin +from .mixins.desktop_icon_mixin import DesktopIconMixin + + + +def threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs).start() + return wrapper + + +class Icon(DesktopIconMixin, VideoIconMixin): + def __init__(self, _settings): + self.settings = _settings + + self.FFMPG_THUMBNLR = self.settings.getThumbnailGenerator() + self.DEFAULT_ICONS = self.settings.getInternalIconsPth() + self.INTERNAL_ICON_PTH = self.settings.getDefaultIcon() + self.STEAM_ICONS_PTH = self.settings.getSteamIconsPth() + self.ABS_THUMBS_PTH = self.settings.getAbsThumbsPth() + self.ICON_DIRS = self.settings.getIconDirs() + self.VIDEO_ICON_WH = self.settings.getVIIconWH() + self.SYS_ICON_WH = self.settings.getSystemIconImageWH() + self.fvideos = self.settings.getVidsFilter() + self.fimages = self.settings.getImagesFilter() + + + def create_icon(self, dir, file): + full_path = f"{dir}/{file}" + return self.get_icon_image(dir, file, full_path) + + def get_icon_image(self, dir, file, full_path): + try: + thumbnl = None + + if file.lower().endswith(self.fvideos): # Video icon + thumbnl = self.create_thumbnail(dir, file) + elif file.lower().endswith(self.fimages): # Image Icon + thumbnl = self.create_scaled_image(full_path, self.VIDEO_ICON_WH) + elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing + thumbnl = self.parse_desktop_files(full_path) + + return thumbnl + except Exception as e: + print(repr(e)) + return None + + def create_thumbnail(self, dir, file): + full_path = f"{dir}/{file}" + try: + file_hash = hashlib.sha256(str.encode(full_path)).hexdigest() + hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg" + if isfile(hash_img_pth) == False: + self.generate_video_thumbnail(full_path, hash_img_pth) + + thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + if thumbnl == None: # If no icon whatsoever, return internal default + thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") + + return thumbnl + except Exception as e: + print("Thumbnail generation issue:") + print( repr(e) ) + return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") + + + def create_scaled_image(self, path, wxh): + try: + return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True) + except Exception as e: + print("Image Scaling Issue:") + print( repr(e) ) + return None + + def create_from_file(self, path): + try: + return GdkPixbuf.Pixbuf.new_from_file(path) + except Exception as e: + print("Image from file Issue:") + print( repr(e) ) + return None + + def return_generic_icon(self): + return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/__init__.py new file mode 100644 index 0000000..a6ce60d --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/__init__.py @@ -0,0 +1 @@ +from . import xdg diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/desktop_icon_mixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/desktop_icon_mixin.py new file mode 100644 index 0000000..2d3c30b --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/desktop_icon_mixin.py @@ -0,0 +1,65 @@ +# Python Imports +import os, subprocess, hashlib +from os.path import isfile + +# Gtk imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from .xdg.DesktopEntry import DesktopEntry + + +class DesktopIconMixin: + def parse_desktop_files(self, full_path): + try: + xdgObj = DesktopEntry(full_path) + icon = xdgObj.getIcon() + alt_icon_path = "" + + if "steam" in icon: + name = xdgObj.getName() + file_hash = hashlib.sha256(str.encode(name)).hexdigest() + hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg" + + if isfile(hash_img_pth) == True: + # Use video sizes since headers are bigger + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + + exec_str = xdgObj.getExec() + parts = exec_str.split("steam://rungameid/") + id = parts[len(parts) - 1] + imageLink = self.STEAM_BASE_URL + id + "/header.jpg" + proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink]) + proc.wait() + + # Use video thumbnail sizes since headers are bigger + return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) + elif os.path.exists(icon): + return self.create_scaled_image(icon, self.SYS_ICON_WH) + else: + alt_icon_path = "" + + for dir in self.ICON_DIRS: + alt_icon_path = self.traverse_icons_folder(dir, icon) + if alt_icon_path != "": + break + + return self.create_scaled_image(alt_icon_path, self.SYS_ICON_WH) + except Exception as e: + print(".desktop icon generation issue:") + print( repr(e) ) + return None + + def traverse_icons_folder(self, path, icon): + alt_icon_path = "" + + for (dirpath, dirnames, filenames) in os.walk(path): + for file in filenames: + appNM = "application-x-" + icon + if icon in file or appNM in file: + alt_icon_path = dirpath + "/" + file + break + + return alt_icon_path diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/video_icon_mixin.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/video_icon_mixin.py new file mode 100644 index 0000000..fc35e9d --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/video_icon_mixin.py @@ -0,0 +1,53 @@ +# Python Imports +import subprocess + +# Gtk imports + +# Application imports + + +class VideoIconMixin: + def generate_video_thumbnail(self, full_path, hash_img_pth): + try: + proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_pth]) + proc.wait() + except Exception as e: + self.logger.debug(repr(e)) + self.ffprobe_generate_video_thumbnail(full_path, hash_img_pth) + + + def ffprobe_generate_video_thumbnail(self, full_path, hash_img_pth): + proc = None + try: + # Stream duration + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Stream duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command, stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Format (container) duration type: image2 + if "N/A" in duration: + command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path] + data = subprocess.run(command , stdout=subprocess.PIPE) + duration = data.stdout.decode('utf-8') + + # Get frame roughly 35% through video + grabTime = str( int( float( duration.split(".")[0] ) * 0.35) ) + command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_pth] + proc = subprocess.Popen(command, stdout=subprocess.PIPE) + proc.wait() + except Exception as e: + print("Video thumbnail generation issue in thread:") + print( repr(e) ) + self.logger.debug(repr(e)) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/BaseDirectory.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/BaseDirectory.py new file mode 100644 index 0000000..a7c31b1 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/BaseDirectory.py @@ -0,0 +1,160 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log + +The freedesktop.org Base Directory specification provides a way for +applications to locate shared data and configuration: + + http://standards.freedesktop.org/basedir-spec/ + +(based on version 0.6) + +This module can be used to load and save from and to these directories. + +Typical usage: + + from rox import basedir + + for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): + print "Load settings from", dir + + dir = basedir.save_config_path('mydomain.org', 'MyProg') + print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" + +Note: see the rox.Options module for a higher-level API for managing options. +""" + +import os, stat + +_home = os.path.expanduser('~') +xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ + os.path.join(_home, '.local', 'share') + +xdg_data_dirs = [xdg_data_home] + \ + (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':') + +xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ + os.path.join(_home, '.config') + +xdg_config_dirs = [xdg_config_home] + \ + (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':') + +xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \ + os.path.join(_home, '.cache') + +xdg_data_dirs = [x for x in xdg_data_dirs if x] +xdg_config_dirs = [x for x in xdg_config_dirs if x] + +def save_config_path(*resource): + """Ensure ``$XDG_CONFIG_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application. Use this + when saving configuration settings. + """ + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_config_home, resource) + if not os.path.isdir(path): + os.makedirs(path, 0o700) + return path + +def save_data_path(*resource): + """Ensure ``$XDG_DATA_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application or a shared + resource. Use this when saving or updating application data. + """ + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_data_home, resource) + if not os.path.isdir(path): + os.makedirs(path) + return path + +def save_cache_path(*resource): + """Ensure ``$XDG_CACHE_HOME//`` exists, and return its path. + 'resource' should normally be the name of your application or a shared + resource.""" + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_cache_home, resource) + if not os.path.isdir(path): + os.makedirs(path) + return path + +def load_config_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + configuration search path. Information provided by earlier directories should + take precedence over later ones, and the user-specific config dir comes + first.""" + resource = os.path.join(*resource) + for config_dir in xdg_config_dirs: + path = os.path.join(config_dir, resource) + if os.path.exists(path): yield path + +def load_first_config(*resource): + """Returns the first result from load_config_paths, or None if there is nothing + to load.""" + for x in load_config_paths(*resource): + return x + return None + +def load_data_paths(*resource): + """Returns an iterator which gives each directory named 'resource' in the + application data search path. Information provided by earlier directories + should take precedence over later ones.""" + resource = os.path.join(*resource) + for data_dir in xdg_data_dirs: + path = os.path.join(data_dir, resource) + if os.path.exists(path): yield path + +def get_runtime_dir(strict=True): + """Returns the value of $XDG_RUNTIME_DIR, a directory path. + + This directory is intended for 'user-specific non-essential runtime files + and other file objects (such as sockets, named pipes, ...)', and + 'communication and synchronization purposes'. + + As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not + set, with ``strict=True`` (the default), a KeyError is raised. With + ``strict=False``, PyXDG will create a fallback under /tmp for the current + user. This fallback does *not* provide the same guarantees as the + specification requires for the runtime directory. + + The strict default is deliberately conservative, so that application + developers can make a conscious decision to allow the fallback. + """ + try: + return os.environ['XDG_RUNTIME_DIR'] + except KeyError: + if strict: + raise + + import getpass + fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() + create = False + + try: + # This must be a real directory, not a symlink, so attackers can't + # point it elsewhere. So we use lstat to check it. + st = os.lstat(fallback) + except OSError as e: + import errno + if e.errno == errno.ENOENT: + create = True + else: + raise + else: + # The fallback must be a directory + if not stat.S_ISDIR(st.st_mode): + os.unlink(fallback) + create = True + # Must be owned by the user and not accessible by anyone else + elif (st.st_uid != os.getuid()) \ + or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)): + os.rmdir(fallback) + create = True + + if create: + os.mkdir(fallback, 0o700) + + return fallback diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Config.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Config.py new file mode 100644 index 0000000..3f5d654 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Config.py @@ -0,0 +1,39 @@ +""" +Functions to configure Basic Settings +""" + +language = "C" +windowmanager = None +icon_theme = "hicolor" +icon_size = 48 +cache_time = 5 +root_mode = False + +def setWindowManager(wm): + global windowmanager + windowmanager = wm + +def setIconTheme(theme): + global icon_theme + icon_theme = theme + import xdg.IconTheme + xdg.IconTheme.themes = [] + +def setIconSize(size): + global icon_size + icon_size = size + +def setCacheTime(time): + global cache_time + cache_time = time + +def setLocale(lang): + import locale + lang = locale.normalize(lang) + locale.setlocale(locale.LC_ALL, lang) + import xdg.Locale + xdg.Locale.update(lang) + +def setRootMode(boolean): + global root_mode + root_mode = boolean diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/DesktopEntry.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/DesktopEntry.py new file mode 100644 index 0000000..803993e --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/DesktopEntry.py @@ -0,0 +1,435 @@ +""" +Complete implementation of the XDG Desktop Entry Specification +http://standards.freedesktop.org/desktop-entry-spec/ + +Not supported: +- Encoding: Legacy Mixed +- Does not check exec parameters +- Does not check URL's +- Does not completly validate deprecated/kde items +- Does not completly check categories +""" + +from .IniFile import IniFile +from . import Locale + +from .IniFile import is_ascii + +from .Exceptions import ParsingError +from .util import which +import os.path +import re +import warnings + +class DesktopEntry(IniFile): + "Class to parse and validate Desktop Entries" + + defaultGroup = 'Desktop Entry' + + def __init__(self, filename=None): + """Create a new DesktopEntry. + + If filename exists, it will be parsed as a desktop entry file. If not, + or if filename is None, a blank DesktopEntry is created. + """ + self.content = dict() + if filename and os.path.exists(filename): + self.parse(filename) + elif filename: + self.new(filename) + + def __str__(self): + return self.getName() + + def parse(self, file): + """Parse a desktop entry file. + + This can raise :class:`~xdg.Exceptions.ParsingError`, + :class:`~xdg.Exceptions.DuplicateGroupError` or + :class:`~xdg.Exceptions.DuplicateKeyError`. + """ + IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) + + def findTryExec(self): + """Looks in the PATH for the executable given in the TryExec field. + + Returns the full path to the executable if it is found, None if not. + Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present. + """ + tryexec = self.get('TryExec', strict=True) + return which(tryexec) + + # start standard keys + def getType(self): + return self.get('Type') + def getVersion(self): + """deprecated, use getVersionString instead """ + return self.get('Version', type="numeric") + def getVersionString(self): + return self.get('Version') + def getName(self): + return self.get('Name', locale=True) + def getGenericName(self): + return self.get('GenericName', locale=True) + def getNoDisplay(self): + return self.get('NoDisplay', type="boolean") + def getComment(self): + return self.get('Comment', locale=True) + def getIcon(self): + return self.get('Icon', locale=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getOnlyShowIn(self): + return self.get('OnlyShowIn', list=True) + def getNotShowIn(self): + return self.get('NotShowIn', list=True) + def getTryExec(self): + return self.get('TryExec') + def getExec(self): + return self.get('Exec') + def getPath(self): + return self.get('Path') + def getTerminal(self): + return self.get('Terminal', type="boolean") + def getMimeType(self): + """deprecated, use getMimeTypes instead """ + return self.get('MimeType', list=True, type="regex") + def getMimeTypes(self): + return self.get('MimeType', list=True) + def getCategories(self): + return self.get('Categories', list=True) + def getStartupNotify(self): + return self.get('StartupNotify', type="boolean") + def getStartupWMClass(self): + return self.get('StartupWMClass') + def getURL(self): + return self.get('URL') + # end standard keys + + # start kde keys + def getServiceTypes(self): + return self.get('ServiceTypes', list=True) + def getDocPath(self): + return self.get('DocPath') + def getKeywords(self): + return self.get('Keywords', list=True, locale=True) + def getInitialPreference(self): + return self.get('InitialPreference') + def getDev(self): + return self.get('Dev') + def getFSType(self): + return self.get('FSType') + def getMountPoint(self): + return self.get('MountPoint') + def getReadonly(self): + return self.get('ReadOnly', type="boolean") + def getUnmountIcon(self): + return self.get('UnmountIcon', locale=True) + # end kde keys + + # start deprecated keys + def getMiniIcon(self): + return self.get('MiniIcon', locale=True) + def getTerminalOptions(self): + return self.get('TerminalOptions') + def getDefaultApp(self): + return self.get('DefaultApp') + def getProtocols(self): + return self.get('Protocols', list=True) + def getExtensions(self): + return self.get('Extensions', list=True) + def getBinaryPattern(self): + return self.get('BinaryPattern') + def getMapNotify(self): + return self.get('MapNotify') + def getEncoding(self): + return self.get('Encoding') + def getSwallowTitle(self): + return self.get('SwallowTitle', locale=True) + def getSwallowExec(self): + return self.get('SwallowExec') + def getSortOrder(self): + return self.get('SortOrder', list=True) + def getFilePattern(self): + return self.get('FilePattern', type="regex") + def getActions(self): + return self.get('Actions', list=True) + # end deprecated keys + + # desktop entry edit stuff + def new(self, filename): + """Make this instance into a new, blank desktop entry. + + If filename has a .desktop extension, Type is set to Application. If it + has a .directory extension, Type is Directory. Other extensions will + cause :class:`~xdg.Exceptions.ParsingError` to be raised. + """ + if os.path.splitext(filename)[1] == ".desktop": + type = "Application" + elif os.path.splitext(filename)[1] == ".directory": + type = "Directory" + else: + raise ParsingError("Unknown extension", filename) + + self.content = dict() + self.addGroup(self.defaultGroup) + self.set("Type", type) + self.filename = filename + # end desktop entry edit stuff + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Desktop Entry": + self.warnings.append('[KDE Desktop Entry]-Header is deprecated') + + # file extension + if self.fileExtension == ".kdelnk": + self.warnings.append("File extension .kdelnk is deprecated") + elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": + self.warnings.append('Unknown File extension') + + # Type + try: + self.type = self.content[self.defaultGroup]["Type"] + except KeyError: + self.errors.append("Key 'Type' is missing") + + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \ + or (re.match("^X-", group) and is_ascii(group))): + self.errors.append("Invalid Group name: %s" % group) + else: + #OnlyShowIn and NotShowIn + if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): + self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") + + def checkKey(self, key, value, group): + # standard keys + if key == "Type": + if value == "ServiceType" or value == "Service" or value == "FSDevice": + self.warnings.append("Type=%s is a KDE extension" % key) + elif value == "MimeType": + self.warnings.append("Type=MimeType is deprecated") + elif not (value == "Application" or value == "Link" or value == "Directory"): + self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) + + if self.fileExtension == ".directory" and not value == "Directory": + self.warnings.append("File extension is .directory, but Type is '%s'" % value) + elif self.fileExtension == ".desktop" and value == "Directory": + self.warnings.append("Files with Type=Directory should have the extension .directory") + + if value == "Application": + if "Exec" not in self.content[group]: + self.warnings.append("Type=Application needs 'Exec' key") + if value == "Link": + if "URL" not in self.content[group]: + self.warnings.append("Type=Link needs 'URL' key") + + elif key == "Version": + self.checkValue(key, value) + + elif re.match("^Name"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^GenericName"+xdg.Locale.regex+"$", key): + pass # locale string + + elif key == "NoDisplay": + self.checkValue(key, value, type="boolean") + + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass # locale string + + elif re.match("^Icon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + + elif key == "OnlyShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "NotShowIn": + self.checkValue(key, value, list=True) + self.checkOnlyShowIn(value) + + elif key == "TryExec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Exec": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Path": + self.checkValue(key, value) + self.checkType(key, "Application") + + elif key == "Terminal": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "Actions": + self.checkValue(key, value, list=True) + self.checkType(key, "Application") + + elif key == "MimeType": + self.checkValue(key, value, list=True) + self.checkType(key, "Application") + + elif key == "Categories": + self.checkValue(key, value) + self.checkType(key, "Application") + self.checkCategories(value) + + elif re.match("^Keywords"+xdg.Locale.regex+"$", key): + self.checkValue(key, value, type="localestring", list=True) + self.checkType(key, "Application") + + elif key == "StartupNotify": + self.checkValue(key, value, type="boolean") + self.checkType(key, "Application") + + elif key == "StartupWMClass": + self.checkType(key, "Application") + + elif key == "URL": + self.checkValue(key, value) + self.checkType(key, "URL") + + # kde extensions + elif key == "ServiceTypes": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "DocPath": + self.checkValue(key, value) + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "InitialPreference": + self.checkValue(key, value, type="numeric") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "Dev": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "FSType": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "MountPoint": + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif key == "ReadOnly": + self.checkValue(key, value, type="boolean") + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.checkType(key, "FSDevice") + self.warnings.append("Key '%s' is a KDE extension" % key) + + # deprecated keys + elif key == "Encoding": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "TerminalOptions": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "DefaultApp": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Protocols": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "Extensions": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "BinaryPattern": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "MapNotify": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SwallowExec": + self.checkValue(key, value) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "FilePattern": + self.checkValue(key, value, type="regex", list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + elif key == "SortOrder": + self.checkValue(key, value, list=True) + self.warnings.append("Key '%s' is deprecated" % key) + + # "X-" extensions + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + + else: + self.errors.append("Invalid key: %s" % key) + + def checkType(self, key, type): + if not self.getType() == type: + self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) + + def checkOnlyShowIn(self, value): + values = self.getList(value) + valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", + "XFCE", "Old"] + for item in values: + if item not in valid and item[0:2] != "X-": + self.errors.append("'%s' is not a registered OnlyShowIn value" % item); + + def checkCategories(self, value): + values = self.getList(value) + + main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] + if not any(item in main for item in values): + self.errors.append("Missing main category") + + additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly'] + allcategories = additional + main + + for item in values: + if item not in allcategories and not item.startswith("X-"): + self.errors.append("'%s' is not a registered Category" % item); + + def checkCategorie(self, value): + """Deprecated alias for checkCategories - only exists for backwards + compatibility. + """ + warnings.warn("checkCategorie is deprecated, use checkCategories", + DeprecationWarning) + return self.checkCategories(value) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Exceptions.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Exceptions.py new file mode 100644 index 0000000..7096b61 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Exceptions.py @@ -0,0 +1,84 @@ +""" +Exception Classes for the xdg package +""" + +debug = False + +class Error(Exception): + """Base class for exceptions defined here.""" + def __init__(self, msg): + self.msg = msg + Exception.__init__(self, msg) + def __str__(self): + return self.msg + +class ValidationError(Error): + """Raised when a file fails to validate. + + The filename is the .file attribute. + """ + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) + +class ParsingError(Error): + """Raised when a file cannot be parsed. + + The filename is the .file attribute. + """ + def __init__(self, msg, file): + self.msg = msg + self.file = file + Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) + +class NoKeyError(Error): + """Raised when trying to access a nonexistant key in an INI-style file. + + Attributes are .key, .group and .file. + """ + def __init__(self, key, group, file): + Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + self.file = file + +class DuplicateKeyError(Error): + """Raised when the same key occurs twice in an INI-style file. + + Attributes are .key, .group and .file. + """ + def __init__(self, key, group, file): + Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) + self.key = key + self.group = group + self.file = file + +class NoGroupError(Error): + """Raised when trying to access a nonexistant group in an INI-style file. + + Attributes are .group and .file. + """ + def __init__(self, group, file): + Error.__init__(self, "No group: %s in file %s" % (group, file)) + self.group = group + self.file = file + +class DuplicateGroupError(Error): + """Raised when the same key occurs twice in an INI-style file. + + Attributes are .group and .file. + """ + def __init__(self, group, file): + Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) + self.group = group + self.file = file + +class NoThemeError(Error): + """Raised when trying to access a nonexistant icon theme. + + The name of the theme is the .theme attribute. + """ + def __init__(self, theme): + Error.__init__(self, "No such icon-theme: %s" % theme) + self.theme = theme diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IconTheme.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IconTheme.py new file mode 100644 index 0000000..2ff3c05 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IconTheme.py @@ -0,0 +1,445 @@ +""" +Complete implementation of the XDG Icon Spec +http://standards.freedesktop.org/icon-theme-spec/ +""" + +import os, time +import re + +from . import IniFile, Config +from .IniFile import is_ascii +from .BaseDirectory import xdg_data_dirs +from .Exceptions import NoThemeError, debug + + +class IconTheme(IniFile): + "Class to parse and validate IconThemes" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + return self.name + + def parse(self, file): + IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) + self.dir = os.path.dirname(file) + (nil, self.name) = os.path.split(self.dir) + + def getDir(self): + return self.dir + + # Standard Keys + def getName(self): + return self.get('Name', locale=True) + def getComment(self): + return self.get('Comment', locale=True) + def getInherits(self): + return self.get('Inherits', list=True) + def getDirectories(self): + return self.get('Directories', list=True) + def getScaledDirectories(self): + return self.get('ScaledDirectories', list=True) + def getHidden(self): + return self.get('Hidden', type="boolean") + def getExample(self): + return self.get('Example') + + # Per Directory Keys + def getSize(self, directory): + return self.get('Size', type="integer", group=directory) + def getContext(self, directory): + return self.get('Context', group=directory) + def getType(self, directory): + value = self.get('Type', group=directory) + if value: + return value + else: + return "Threshold" + def getMaxSize(self, directory): + value = self.get('MaxSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getMinSize(self, directory): + value = self.get('MinSize', type="integer", group=directory) + if value or value == 0: + return value + else: + return self.getSize(directory) + def getThreshold(self, directory): + value = self.get('Threshold', type="integer", group=directory) + if value or value == 0: + return value + else: + return 2 + + def getScale(self, directory): + value = self.get('Scale', type="integer", group=directory) + return value or 1 + + # validation stuff + def checkExtras(self): + # header + if self.defaultGroup == "KDE Icon Theme": + self.warnings.append('[KDE Icon Theme]-Header is deprecated') + + # file extension + if self.fileExtension == ".theme": + pass + elif self.fileExtension == ".desktop": + self.warnings.append('.desktop fileExtension is deprecated') + else: + self.warnings.append('Unknown File extension') + + # Check required keys + # Name + try: + self.name = self.content[self.defaultGroup]["Name"] + except KeyError: + self.errors.append("Key 'Name' is missing") + + # Comment + try: + self.comment = self.content[self.defaultGroup]["Comment"] + except KeyError: + self.errors.append("Key 'Comment' is missing") + + # Directories + try: + self.directories = self.content[self.defaultGroup]["Directories"] + except KeyError: + self.errors.append("Key 'Directories' is missing") + + def checkGroup(self, group): + # check if group header is valid + if group == self.defaultGroup: + try: + self.name = self.content[group]["Name"] + except KeyError: + self.errors.append("Key 'Name' in Group '%s' is missing" % group) + try: + self.name = self.content[group]["Comment"] + except KeyError: + self.errors.append("Key 'Comment' in Group '%s' is missing" % group) + elif group in self.getDirectories(): + try: + self.type = self.content[group]["Type"] + except KeyError: + self.type = "Threshold" + try: + self.name = self.content[group]["Size"] + except KeyError: + self.errors.append("Key 'Size' in Group '%s' is missing" % group) + elif not (re.match(r"^\[X-", group) and is_ascii(group)): + self.errors.append("Invalid Group name: %s" % group) + + def checkKey(self, key, value, group): + # standard keys + if group == self.defaultGroup: + if re.match("^Name"+xdg.Locale.regex+"$", key): + pass + elif re.match("^Comment"+xdg.Locale.regex+"$", key): + pass + elif key == "Inherits": + self.checkValue(key, value, list=True) + elif key == "Directories": + self.checkValue(key, value, list=True) + elif key == "ScaledDirectories": + self.checkValue(key, value, list=True) + elif key == "Hidden": + self.checkValue(key, value, type="boolean") + elif key == "Example": + self.checkValue(key, value) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + elif group in self.getDirectories(): + if key == "Size": + self.checkValue(key, value, type="integer") + elif key == "Context": + self.checkValue(key, value) + elif key == "Type": + self.checkValue(key, value) + if value not in ["Fixed", "Scalable", "Threshold"]: + self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) + elif key == "MaxSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) + elif key == "MinSize": + self.checkValue(key, value, type="integer") + if self.type != "Scalable": + self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) + elif key == "Threshold": + self.checkValue(key, value, type="integer") + if self.type != "Threshold": + self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) + elif key == "Scale": + self.checkValue(key, value, type="integer") + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + +class IconData(IniFile): + "Class to parse and validate IconData Files" + def __init__(self): + IniFile.__init__(self) + + def __repr__(self): + displayname = self.getDisplayName() + if displayname: + return "" % displayname + else: + return "" + + def parse(self, file): + IniFile.parse(self, file, ["Icon Data"]) + + # Standard Keys + def getDisplayName(self): + """Retrieve the display name from the icon data, if one is specified.""" + return self.get('DisplayName', locale=True) + def getEmbeddedTextRectangle(self): + """Retrieve the embedded text rectangle from the icon data as a list of + numbers (x0, y0, x1, y1), if it is specified.""" + return self.get('EmbeddedTextRectangle', type="integer", list=True) + def getAttachPoints(self): + """Retrieve the anchor points for overlays & emblems from the icon data, + as a list of co-ordinate pairs, if they are specified.""" + return self.get('AttachPoints', type="point", list=True) + + # validation stuff + def checkExtras(self): + # file extension + if self.fileExtension != ".icon": + self.warnings.append('Unknown File extension') + + def checkGroup(self, group): + # check if group header is valid + if not (group == self.defaultGroup \ + or (re.match(r"^\[X-", group) and is_ascii(group))): + self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) + + def checkKey(self, key, value, group): + # standard keys + if re.match("^DisplayName"+xdg.Locale.regex+"$", key): + pass + elif key == "EmbeddedTextRectangle": + self.checkValue(key, value, type="integer", list=True) + elif key == "AttachPoints": + self.checkValue(key, value, type="point", list=True) + elif re.match("^X-[a-zA-Z0-9-]+", key): + pass + else: + self.errors.append("Invalid key: %s" % key) + + + +icondirs = [] +for basedir in xdg_data_dirs: + icondirs.append(os.path.join(basedir, "icons")) + icondirs.append(os.path.join(basedir, "pixmaps")) +icondirs.append(os.path.expanduser("~/.icons")) + +# just cache variables, they give a 10x speed improvement +themes = [] +theme_cache = {} +dir_cache = {} +icon_cache = {} + +def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): + """Get the path to a specified icon. + + size : + Icon size in pixels. Defaults to ``xdg.Config.icon_size``. + theme : + Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't + found in the specified theme, it will be looked up in the basic 'hicolor' + theme. + extensions : + List of preferred file extensions. + + Example:: + + >>> getIconPath("inkscape", 32) + '/usr/share/icons/hicolor/32x32/apps/inkscape.png' + """ + + global themes + + if size == None: + size = xdg.Config.icon_size + if theme == None: + theme = xdg.Config.icon_theme + + # if we have an absolute path, just return it + if os.path.isabs(iconname): + return iconname + + # check if it has an extension and strip it + if os.path.splitext(iconname)[1][1:] in extensions: + iconname = os.path.splitext(iconname)[0] + + # parse theme files + if (themes == []) or (themes[0].name != theme): + themes = list(__get_themes(theme)) + + # more caching (icon looked up in the last 5 seconds?) + tmp = (iconname, size, theme, tuple(extensions)) + try: + timestamp, icon = icon_cache[tmp] + except KeyError: + pass + else: + if (time.time() - timestamp) >= xdg.Config.cache_time: + del icon_cache[tmp] + else: + return icon + + for thme in themes: + icon = LookupIcon(iconname, size, thme, extensions) + if icon: + icon_cache[tmp] = (time.time(), icon) + return icon + + # cache stuff again (directories looked up in the last 5 seconds?) + for directory in icondirs: + if (directory not in dir_cache \ + or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \ + and dir_cache[directory][2] < os.path.getmtime(directory))) \ + and os.path.isdir(directory): + dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory)) + + for dir, values in dir_cache.items(): + for extension in extensions: + try: + if iconname + "." + extension in values[0]: + icon = os.path.join(dir, iconname + "." + extension) + icon_cache[tmp] = [time.time(), icon] + return icon + except UnicodeDecodeError as e: + if debug: + raise e + else: + pass + + # we haven't found anything? "hicolor" is our fallback + if theme != "hicolor": + icon = getIconPath(iconname, size, "hicolor") + icon_cache[tmp] = [time.time(), icon] + return icon + +def getIconData(path): + """Retrieve the data from the .icon file corresponding to the given file. If + there is no .icon file, it returns None. + + Example:: + + getIconData("/usr/share/icons/Tango/scalable/places/folder.svg") + """ + if os.path.isfile(path): + icon_file = os.path.splitext(path)[0] + ".icon" + if os.path.isfile(icon_file): + data = IconData() + data.parse(icon_file) + return data + +def __get_themes(themename): + """Generator yielding IconTheme objects for a specified theme and any themes + from which it inherits. + """ + for dir in icondirs: + theme_file = os.path.join(dir, themename, "index.theme") + if os.path.isfile(theme_file): + break + theme_file = os.path.join(dir, themename, "index.desktop") + if os.path.isfile(theme_file): + break + else: + if debug: + raise NoThemeError(themename) + return + + theme = IconTheme() + theme.parse(theme_file) + yield theme + for subtheme in theme.getInherits(): + for t in __get_themes(subtheme): + yield t + +def LookupIcon(iconname, size, theme, extensions): + # look for the cache + if theme.name not in theme_cache: + theme_cache[theme.name] = [] + theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup + theme_cache[theme.name].append(0) # [1] mtime + theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] + + # cache stuff (directory lookuped up the in the last 5 seconds?) + if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time: + theme_cache[theme.name][0] = time.time() + for subdir in theme.getDirectories(): + for directory in icondirs: + dir = os.path.join(directory,theme.name,subdir) + if (dir not in theme_cache[theme.name][2] \ + or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ + and subdir != "" \ + and os.path.isdir(dir): + theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)] + theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) + + for dir, values in theme_cache[theme.name][2].items(): + if DirectoryMatchesSize(values[0], size, theme): + for extension in extensions: + if iconname + "." + extension in values[1]: + return os.path.join(dir, iconname + "." + extension) + + minimal_size = 2**31 + closest_filename = "" + for dir, values in theme_cache[theme.name][2].items(): + distance = DirectorySizeDistance(values[0], size, theme) + if distance < minimal_size: + for extension in extensions: + if iconname + "." + extension in values[1]: + closest_filename = os.path.join(dir, iconname + "." + extension) + minimal_size = distance + + return closest_filename + +def DirectoryMatchesSize(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return Size == iconsize + elif Type == "Scaleable": + return MinSize <= iconsize <= MaxSize + elif Type == "Threshold": + return Size - Threshold <= iconsize <= Size + Threshold + +def DirectorySizeDistance(subdir, iconsize, theme): + Type = theme.getType(subdir) + Size = theme.getSize(subdir) + Threshold = theme.getThreshold(subdir) + MinSize = theme.getMinSize(subdir) + MaxSize = theme.getMaxSize(subdir) + if Type == "Fixed": + return abs(Size - iconsize) + elif Type == "Scalable": + if iconsize < MinSize: + return MinSize - iconsize + elif iconsize > MaxSize: + return MaxSize - iconsize + return 0 + elif Type == "Threshold": + if iconsize < Size - Threshold: + return MinSize - iconsize + elif iconsize > Size + Threshold: + return iconsize - MaxSize + return 0 diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IniFile.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IniFile.py new file mode 100644 index 0000000..74ab858 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/IniFile.py @@ -0,0 +1,419 @@ +""" +Base Class for DesktopEntry, IconTheme and IconData +""" + +import re, os, stat, io +from .Exceptions import (ParsingError, DuplicateGroupError, NoGroupError, + NoKeyError, DuplicateKeyError, ValidationError, + debug) +# import xdg.Locale +from . import Locale +from .util import u + +def is_ascii(s): + """Return True if a string consists entirely of ASCII characters.""" + try: + s.encode('ascii', 'strict') + return True + except UnicodeError: + return False + +class IniFile: + defaultGroup = '' + fileExtension = '' + + filename = '' + + tainted = False + + def __init__(self, filename=None): + self.content = dict() + if filename: + self.parse(filename) + + def __cmp__(self, other): + return cmp(self.content, other.content) + + def parse(self, filename, headers=None): + '''Parse an INI file. + + headers -- list of headers the parser will try to select as a default header + ''' + # for performance reasons + content = self.content + + if not os.path.isfile(filename): + raise ParsingError("File not found", filename) + + try: + # The content should be UTF-8, but legacy files can have other + # encodings, including mixed encodings in one file. We don't attempt + # to decode them, but we silence the errors. + fd = io.open(filename, 'r', encoding='utf-8', errors='replace') + except IOError as e: + if debug: + raise e + else: + return + + # parse file + for line in fd: + line = line.strip() + # empty line + if not line: + continue + # comment + elif line[0] == '#': + continue + # new group + elif line[0] == '[': + currentGroup = line.lstrip("[").rstrip("]") + if debug and self.hasGroup(currentGroup): + raise DuplicateGroupError(currentGroup, filename) + else: + content[currentGroup] = {} + # key + else: + try: + key, value = line.split("=", 1) + except ValueError: + raise ParsingError("Invalid line: " + line, filename) + + key = key.strip() # Spaces before/after '=' should be ignored + try: + if debug and self.hasKey(key, currentGroup): + raise DuplicateKeyError(key, currentGroup, filename) + else: + content[currentGroup][key] = value.strip() + except (IndexError, UnboundLocalError): + raise ParsingError("Parsing error on key, group missing", filename) + + fd.close() + + self.filename = filename + self.tainted = False + + # check header + if headers: + for header in headers: + if header in content: + self.defaultGroup = header + break + else: + raise ParsingError("[%s]-Header missing" % headers[0], filename) + + # start stuff to access the keys + def get(self, key, group=None, locale=False, type="string", list=False, strict=False): + # set default group + if not group: + group = self.defaultGroup + + # return key (with locale) + if (group in self.content) and (key in self.content[group]): + if locale: + value = self.content[group][self.__addLocale(key, group)] + else: + value = self.content[group][key] + else: + if strict or debug: + if group not in self.content: + raise NoGroupError(group, self.filename) + elif key not in self.content[group]: + raise NoKeyError(key, group, self.filename) + else: + value = "" + + if list == True: + values = self.getList(value) + result = [] + else: + values = [value] + + for value in values: + if type == "boolean": + value = self.__getBoolean(value) + elif type == "integer": + try: + value = int(value) + except ValueError: + value = 0 + elif type == "numeric": + try: + value = float(value) + except ValueError: + value = 0.0 + elif type == "regex": + value = re.compile(value) + elif type == "point": + x, y = value.split(",") + value = int(x), int(y) + + if list == True: + result.append(value) + else: + result = value + + return result + # end stuff to access the keys + + # start subget + def getList(self, string): + if re.search(r"(? 0: + key = key + "[" + xdg.Locale.langs[0] + "]" + + try: + self.content[group][key] = value + except KeyError: + raise NoGroupError(group, self.filename) + + self.tainted = (value == self.get(key, group)) + + def addGroup(self, group): + if self.hasGroup(group): + if debug: + raise DuplicateGroupError(group, self.filename) + else: + self.content[group] = {} + self.tainted = True + + def removeGroup(self, group): + existed = group in self.content + if existed: + del self.content[group] + self.tainted = True + else: + if debug: + raise NoGroupError(group, self.filename) + return existed + + def removeKey(self, key, group=None, locales=True): + # set default group + if not group: + group = self.defaultGroup + + try: + if locales: + for name in list(self.content[group]): + if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: + del self.content[group][name] + value = self.content[group].pop(key) + self.tainted = True + return value + except KeyError as e: + if debug: + if e == group: + raise NoGroupError(group, self.filename) + else: + raise NoKeyError(key, group, self.filename) + else: + return "" + + # misc + def groups(self): + return self.content.keys() + + def hasGroup(self, group): + return group in self.content + + def hasKey(self, key, group=None): + # set default group + if not group: + group = self.defaultGroup + + return key in self.content[group] + + def getFileName(self): + return self.filename diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Locale.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Locale.py new file mode 100644 index 0000000..d0a70d2 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Locale.py @@ -0,0 +1,79 @@ +""" +Helper Module for Locale settings + +This module is based on a ROX module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log +""" + +import os +from locale import normalize + +regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?" + +def _expand_lang(locale): + locale = normalize(locale) + COMPONENT_CODESET = 1 << 0 + COMPONENT_MODIFIER = 1 << 1 + COMPONENT_TERRITORY = 1 << 2 + # split up the locale into its base components + mask = 0 + pos = locale.find('@') + if pos >= 0: + modifier = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_MODIFIER + else: + modifier = '' + pos = locale.find('.') + codeset = '' + if pos >= 0: + locale = locale[:pos] + pos = locale.find('_') + if pos >= 0: + territory = locale[pos:] + locale = locale[:pos] + mask |= COMPONENT_TERRITORY + else: + territory = '' + language = locale + ret = [] + for i in range(mask+1): + if not (i & ~mask): # if all components for this combo exist ... + val = language + if i & COMPONENT_TERRITORY: val += territory + if i & COMPONENT_CODESET: val += codeset + if i & COMPONENT_MODIFIER: val += modifier + ret.append(val) + ret.reverse() + return ret + +def expand_languages(languages=None): + # Get some reasonable defaults for arguments that were not supplied + if languages is None: + languages = [] + for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): + val = os.environ.get(envar) + if val: + languages = val.split(':') + break + #if 'C' not in languages: + # languages.append('C') + + # now normalize and expand the languages + nelangs = [] + for lang in languages: + for nelang in _expand_lang(lang): + if nelang not in nelangs: + nelangs.append(nelang) + return nelangs + +def update(language=None): + global langs + if language: + langs = expand_languages([language]) + else: + langs = expand_languages() + +langs = [] +update() diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Menu.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Menu.py new file mode 100644 index 0000000..fcf1ac1 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Menu.py @@ -0,0 +1,1125 @@ +""" +Implementation of the XDG Menu Specification +http://standards.freedesktop.org/menu-spec/ + +Example code: + +from xdg.Menu import parse, Menu, MenuEntry + +def print_menu(menu, tab=0): + for submenu in menu.Entries: + if isinstance(submenu, Menu): + print ("\t" * tab) + unicode(submenu) + print_menu(submenu, tab+1) + elif isinstance(submenu, MenuEntry): + print ("\t" * tab) + unicode(submenu.DesktopEntry) + +print_menu(parse()) +""" + +import os +import locale +import subprocess +import ast +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from .BaseDirectory import xdg_data_dirs, xdg_config_dirs +from . import DesktopEntry, Locale, Config +from .Exceptions import ParsingError +from .util import PY3 + + +def _strxfrm(s): + """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. + + See Python bug #2481. + """ + if (not PY3) and isinstance(s, unicode): + s = s.encode('utf-8') + return locale.strxfrm(s) + + +DELETED = "Deleted" +NO_DISPLAY = "NoDisplay" +HIDDEN = "Hidden" +EMPTY = "Empty" +NOT_SHOW_IN = "NotShowIn" +NO_EXEC = "NoExec" + + +class Menu: + """Menu containing sub menus under menu.Entries + + Contains both Menu and MenuEntry items. + """ + def __init__(self): + # Public stuff + self.Name = "" + self.Directory = None + self.Entries = [] + self.Doc = "" + self.Filename = "" + self.Depth = 0 + self.Parent = None + self.NotInXml = False + + # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN + self.Show = True + self.Visible = 0 + + # Private stuff, only needed for parsing + self.AppDirs = [] + self.DefaultLayout = None + self.Deleted = None + self.Directories = [] + self.DirectoryDirs = [] + self.Layout = None + self.MenuEntries = [] + self.Moves = [] + self.OnlyUnallocated = None + self.Rules = [] + self.Submenus = [] + + def __str__(self): + return self.Name + + def __add__(self, other): + for dir in other.AppDirs: + self.AppDirs.append(dir) + + for dir in other.DirectoryDirs: + self.DirectoryDirs.append(dir) + + for directory in other.Directories: + self.Directories.append(directory) + + if other.Deleted is not None: + self.Deleted = other.Deleted + + if other.OnlyUnallocated is not None: + self.OnlyUnallocated = other.OnlyUnallocated + + if other.Layout: + self.Layout = other.Layout + + if other.DefaultLayout: + self.DefaultLayout = other.DefaultLayout + + for rule in other.Rules: + self.Rules.append(rule) + + for move in other.Moves: + self.Moves.append(move) + + for submenu in other.Submenus: + self.addSubmenu(submenu) + + return self + + # FIXME: Performance: cache getName() + def __cmp__(self, other): + return locale.strcoll(self.getName(), other.getName()) + + def _key(self): + """Key function for locale-aware sorting.""" + return _strxfrm(self.getName()) + + def __lt__(self, other): + try: + other = other._key() + except AttributeError: + pass + return self._key() < other + + def __eq__(self, other): + try: + return self.Name == unicode(other) + except NameError: # unicode() becomes str() in Python 3 + return self.Name == str(other) + + """ PUBLIC STUFF """ + def getEntries(self, show_hidden=False): + """Interator for a list of Entries visible to the user.""" + for entry in self.Entries: + if show_hidden: + yield entry + elif entry.Show is True: + yield entry + + # FIXME: Add searchEntry/seaqrchMenu function + # search for name/comment/genericname/desktopfileid + # return multiple items + + def getMenuEntry(self, desktopfileid, deep=False): + """Searches for a MenuEntry with a given DesktopFileID.""" + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID == desktopfileid: + return menuentry + if deep: + for submenu in self.Submenus: + submenu.getMenuEntry(desktopfileid, deep) + + def getMenu(self, path): + """Searches for a Menu with a given path.""" + array = path.split("/", 1) + for submenu in self.Submenus: + if submenu.Name == array[0]: + if len(array) > 1: + return submenu.getMenu(array[1]) + else: + return submenu + + def getPath(self, org=False, toplevel=False): + """Returns this menu's path in the menu structure.""" + parent = self + names = [] + while 1: + if org: + names.append(parent.Name) + else: + names.append(parent.getName()) + if parent.Depth > 0: + parent = parent.Parent + else: + break + names.reverse() + path = "" + if not toplevel: + names.pop(0) + for name in names: + path = os.path.join(path, name) + return path + + def getName(self): + """Returns the menu's localised name.""" + try: + return self.Directory.DesktopEntry.getName() + except AttributeError: + return self.Name + + def getGenericName(self): + """Returns the menu's generic name.""" + try: + return self.Directory.DesktopEntry.getGenericName() + except AttributeError: + return "" + + def getComment(self): + """Returns the menu's comment text.""" + try: + return self.Directory.DesktopEntry.getComment() + except AttributeError: + return "" + + def getIcon(self): + """Returns the menu's icon, filename or simple name""" + try: + return self.Directory.DesktopEntry.getIcon() + except AttributeError: + return "" + + def sort(self): + self.Entries = [] + self.Visible = 0 + + for submenu in self.Submenus: + submenu.sort() + + _submenus = set() + _entries = set() + + for order in self.Layout.order: + if order[0] == "Filename": + _entries.add(order[1]) + elif order[0] == "Menuname": + _submenus.add(order[1]) + + for order in self.Layout.order: + if order[0] == "Separator": + separator = Separator(self) + if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): + separator.Show = False + self.Entries.append(separator) + elif order[0] == "Filename": + menuentry = self.getMenuEntry(order[1]) + if menuentry: + self.Entries.append(menuentry) + elif order[0] == "Menuname": + submenu = self.getMenu(order[1]) + if submenu: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + elif order[0] == "Merge": + if order[1] == "files" or order[1] == "all": + self.MenuEntries.sort() + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID not in _entries: + self.Entries.append(menuentry) + elif order[1] == "menus" or order[1] == "all": + self.Submenus.sort() + for submenu in self.Submenus: + if submenu.Name not in _submenus: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + + # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec + for entry in self.Entries: + entry.Show = True + self.Visible += 1 + if isinstance(entry, Menu): + if entry.Deleted is True: + entry.Show = DELETED + self.Visible -= 1 + elif isinstance(entry.Directory, MenuEntry): + if entry.Directory.DesktopEntry.getNoDisplay(): + entry.Show = NO_DISPLAY + self.Visible -= 1 + elif entry.Directory.DesktopEntry.getHidden(): + entry.Show = HIDDEN + self.Visible -= 1 + elif isinstance(entry, MenuEntry): + if entry.DesktopEntry.getNoDisplay(): + entry.Show = NO_DISPLAY + self.Visible -= 1 + elif entry.DesktopEntry.getHidden(): + entry.Show = HIDDEN + self.Visible -= 1 + elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): + entry.Show = NO_EXEC + self.Visible -= 1 + elif xdg.Config.windowmanager: + if (entry.DesktopEntry.OnlyShowIn != [] and ( + xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn + ) + ) or ( + xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn + ): + entry.Show = NOT_SHOW_IN + self.Visible -= 1 + elif isinstance(entry, Separator): + self.Visible -= 1 + # remove separators at the beginning and at the end + if len(self.Entries) > 0: + if isinstance(self.Entries[0], Separator): + self.Entries[0].Show = False + if len(self.Entries) > 1: + if isinstance(self.Entries[-1], Separator): + self.Entries[-1].Show = False + + # show_empty tag + for entry in self.Entries[:]: + if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: + entry.Show = EMPTY + self.Visible -= 1 + if entry.NotInXml is True: + self.Entries.remove(entry) + + """ PRIVATE STUFF """ + def addSubmenu(self, newmenu): + for submenu in self.Submenus: + if submenu == newmenu: + submenu += newmenu + break + else: + self.Submenus.append(newmenu) + newmenu.Parent = self + newmenu.Depth = self.Depth + 1 + + # inline tags + def merge_inline(self, submenu): + """Appends a submenu's entries to this menu + See the section of the spec about the "inline" attribute + """ + if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: + menuentry = submenu.Entries[0] + menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) + menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) + menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) + self.Entries.append(menuentry) + elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: + if submenu.Layout.inline_header: + header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) + self.Entries.append(header) + for entry in submenu.Entries: + self.Entries.append(entry) + else: + self.Entries.append(submenu) + + +class Move: + "A move operation" + def __init__(self, old="", new=""): + self.Old = old + self.New = new + + def __cmp__(self, other): + return cmp(self.Old, other.Old) + + +class Layout: + "Menu Layout class" + def __init__(self, show_empty=False, inline=False, inline_limit=4, + inline_header=True, inline_alias=False): + self.show_empty = show_empty + self.inline = inline + self.inline_limit = inline_limit + self.inline_header = inline_header + self.inline_alias = inline_alias + self._order = [] + self._default_order = [ + ['Merge', 'menus'], + ['Merge', 'files'] + ] + + @property + def order(self): + return self._order if self._order else self._default_order + + @order.setter + def order(self, order): + self._order = order + + +class Rule: + """Include / Exclude Rules Class""" + + TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 + + @classmethod + def fromFilename(cls, type, filename): + tree = ast.Expression( + body=ast.Compare( + left=ast.Str(filename), + ops=[ast.Eq()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='DesktopFileID', + ctx=ast.Load() + )] + ), + lineno=1, col_offset=0 + ) + ast.fix_missing_locations(tree) + rule = Rule(type, tree) + return rule + + def __init__(self, type, expression): + # Type is TYPE_INCLUDE or TYPE_EXCLUDE + self.Type = type + # expression is ast.Expression + self.expression = expression + self.code = compile(self.expression, '', 'eval') + + def __str__(self): + return ast.dump(self.expression) + + def apply(self, menuentries, run): + for menuentry in menuentries: + if run == 2 and (menuentry.MatchedInclude is True or + menuentry.Allocated is True): + continue + if eval(self.code): + if self.Type is Rule.TYPE_INCLUDE: + menuentry.Add = True + menuentry.MatchedInclude = True + else: + menuentry.Add = False + return menuentries + + +class MenuEntry: + "Wrapper for 'Menu Style' Desktop Entries" + + TYPE_USER = "User" + TYPE_SYSTEM = "System" + TYPE_BOTH = "Both" + + def __init__(self, filename, dir="", prefix=""): + # Create entry + self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) + self.setAttributes(filename, dir, prefix) + + # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC + self.Show = True + + # Semi-Private + self.Original = None + self.Parents = [] + + # Private Stuff + self.Allocated = False + self.Add = False + self.MatchedInclude = False + + # Caching + self.Categories = self.DesktopEntry.getCategories() + + def save(self): + """Save any changes to the desktop entry.""" + if self.DesktopEntry.tainted: + self.DesktopEntry.write() + + def getDir(self): + """Return the directory containing the desktop entry file.""" + return self.DesktopEntry.filename.replace(self.Filename, '') + + def getType(self): + """Return the type of MenuEntry, System/User/Both""" + if not xdg.Config.root_mode: + if self.Original: + return self.TYPE_BOTH + elif xdg_data_dirs[0] in self.DesktopEntry.filename: + return self.TYPE_USER + else: + return self.TYPE_SYSTEM + else: + return self.TYPE_USER + + def setAttributes(self, filename, dir="", prefix=""): + self.Filename = filename + self.Prefix = prefix + self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") + + if not os.path.isabs(self.DesktopEntry.filename): + self.__setFilename() + + def updateAttributes(self): + if self.getType() == self.TYPE_SYSTEM: + self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) + self.__setFilename() + + def __setFilename(self): + if not xdg.Config.root_mode: + path = xdg_data_dirs[0] + else: + path = xdg_data_dirs[1] + + if self.DesktopEntry.getType() == "Application": + dir_ = os.path.join(path, "applications") + else: + dir_ = os.path.join(path, "desktop-directories") + + self.DesktopEntry.filename = os.path.join(dir_, self.Filename) + + def __cmp__(self, other): + return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) + + def _key(self): + """Key function for locale-aware sorting.""" + return _strxfrm(self.DesktopEntry.getName()) + + def __lt__(self, other): + try: + other = other._key() + except AttributeError: + pass + return self._key() < other + + def __eq__(self, other): + if self.DesktopFileID == str(other): + return True + else: + return False + + def __repr__(self): + return self.DesktopFileID + + +class Separator: + "Just a dummy class for Separators" + def __init__(self, parent): + self.Parent = parent + self.Show = True + + +class Header: + "Class for Inline Headers" + def __init__(self, name, generic_name, comment): + self.Name = name + self.GenericName = generic_name + self.Comment = comment + + def __str__(self): + return self.Name + + +TYPE_DIR, TYPE_FILE = 0, 1 + + +def _check_file_path(value, filename, type): + path = os.path.dirname(filename) + if not os.path.isabs(value): + value = os.path.join(path, value) + value = os.path.abspath(value) + if not os.path.exists(value): + return False + if type == TYPE_DIR and os.path.isdir(value): + return value + if type == TYPE_FILE and os.path.isfile(value): + return value + return False + + +def _get_menu_file_path(filename): + dirs = list(xdg_config_dirs) + if xdg.Config.root_mode is True: + dirs.pop(0) + for d in dirs: + menuname = os.path.join(d, "menus", filename) + if os.path.isfile(menuname): + return menuname + + +def _to_bool(value): + if isinstance(value, bool): + return value + return value.lower() == "true" + + +# remove duplicate entries from a list +def _dedupe(_list): + _set = {} + _list.reverse() + _list = [_set.setdefault(e, e) for e in _list if e not in _set] + _list.reverse() + return _list + + +class XMLMenuBuilder(object): + + def __init__(self, debug=False): + self.debug = debug + + def parse(self, filename=None): + """Load an applications.menu file. + + filename : str, optional + The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. + """ + # convert to absolute path + if filename and not os.path.isabs(filename): + filename = _get_menu_file_path(filename) + # use default if no filename given + if not filename: + candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" + filename = _get_menu_file_path(candidate) + if not filename: + raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) + # check if it is a .menu file + if not filename.endswith(".menu"): + raise ParsingError('Not a .menu file', filename) + # create xml parser + try: + tree = etree.parse(filename) + except: + raise ParsingError('Not a valid .menu file', filename) + + # parse menufile + self._merged_files = set() + self._directory_dirs = set() + self.cache = MenuEntryCache() + + menu = self.parse_menu(tree.getroot(), filename) + menu.tree = tree + menu.filename = filename + + self.handle_moves(menu) + self.post_parse(menu) + + # generate the menu + self.generate_not_only_allocated(menu) + self.generate_only_allocated(menu) + + # and finally sort + menu.sort() + + return menu + + def parse_menu(self, node, filename): + menu = Menu() + self.parse_node(node, filename, menu) + return menu + + def parse_node(self, node, filename, parent=None): + num_children = len(node) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == 'Menu': + menu = self.parse_menu(child, filename) + parent.addSubmenu(menu) + elif tag == 'AppDir' and text: + self.parse_app_dir(text, filename, parent) + elif tag == 'DefaultAppDirs': + self.parse_default_app_dir(filename, parent) + elif tag == 'DirectoryDir' and text: + self.parse_directory_dir(text, filename, parent) + elif tag == 'DefaultDirectoryDirs': + self.parse_default_directory_dir(filename, parent) + elif tag == 'Name' and text: + parent.Name = text + elif tag == 'Directory' and text: + parent.Directories.append(text) + elif tag == 'OnlyUnallocated': + parent.OnlyUnallocated = True + elif tag == 'NotOnlyUnallocated': + parent.OnlyUnallocated = False + elif tag == 'Deleted': + parent.Deleted = True + elif tag == 'NotDeleted': + parent.Deleted = False + elif tag == 'Include' or tag == 'Exclude': + parent.Rules.append(self.parse_rule(child)) + elif tag == 'MergeFile': + if child.attrib.get("type", None) == "parent": + self.parse_merge_file("applications.menu", child, filename, parent) + elif text: + self.parse_merge_file(text, child, filename, parent) + elif tag == 'MergeDir' and text: + self.parse_merge_dir(text, child, filename, parent) + elif tag == 'DefaultMergeDirs': + self.parse_default_merge_dirs(child, filename, parent) + elif tag == 'Move': + parent.Moves.append(self.parse_move(child)) + elif tag == 'Layout': + if num_children > 1: + parent.Layout = self.parse_layout(child) + elif tag == 'DefaultLayout': + if num_children > 1: + parent.DefaultLayout = self.parse_layout(child) + elif tag == 'LegacyDir' and text: + self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) + elif tag == 'KDELegacyDirs': + self.parse_kde_legacy_dirs(filename, parent) + + def parse_layout(self, node): + layout = Layout( + show_empty=_to_bool(node.attrib.get("show_empty", False)), + inline=_to_bool(node.attrib.get("inline", False)), + inline_limit=int(node.attrib.get("inline_limit", 4)), + inline_header=_to_bool(node.attrib.get("inline_header", True)), + inline_alias=_to_bool(node.attrib.get("inline_alias", False)) + ) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Menuname" and text: + layout.order.append([ + "Menuname", + text, + _to_bool(child.attrib.get("show_empty", False)), + _to_bool(child.attrib.get("inline", False)), + int(child.attrib.get("inline_limit", 4)), + _to_bool(child.attrib.get("inline_header", True)), + _to_bool(child.attrib.get("inline_alias", False)) + ]) + elif tag == "Separator": + layout.order.append(['Separator']) + elif tag == "Filename" and text: + layout.order.append(["Filename", text]) + elif tag == "Merge": + layout.order.append([ + "Merge", + child.attrib.get("type", "all") + ]) + return layout + + def parse_move(self, node): + old, new = "", "" + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Old" and text: + old = text + elif tag == "New" and text: + new = text + return Move(old, new) + + # ---------- parsing + + def parse_rule(self, node): + type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE + tree = ast.Expression(lineno=1, col_offset=0) + expr = self.parse_bool_op(node, ast.Or()) + if expr: + tree.body = expr + else: + tree.body = ast.Name('False', ast.Load()) + ast.fix_missing_locations(tree) + return Rule(type, tree) + + def parse_bool_op(self, node, operator): + values = [] + for child in node: + rule = self.parse_rule_node(child) + if rule: + values.append(rule) + num_values = len(values) + if num_values > 1: + return ast.BoolOp(operator, values) + elif num_values == 1: + return values[0] + return None + + def parse_rule_node(self, node): + tag = node.tag + if tag == 'Or': + return self.parse_bool_op(node, ast.Or()) + elif tag == 'And': + return self.parse_bool_op(node, ast.And()) + elif tag == 'Not': + expr = self.parse_bool_op(node, ast.Or()) + return ast.UnaryOp(ast.Not(), expr) if expr else None + elif tag == 'All': + return ast.Name('True', ast.Load()) + elif tag == 'Category': + category = node.text + return ast.Compare( + left=ast.Str(category), + ops=[ast.In()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='Categories', + ctx=ast.Load() + )] + ) + elif tag == 'Filename': + filename = node.text + return ast.Compare( + left=ast.Str(filename), + ops=[ast.Eq()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='DesktopFileID', + ctx=ast.Load() + )] + ) + + # ---------- App/Directory Dir Stuff + + def parse_app_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.AppDirs.append(value) + + def parse_default_app_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_app_dir(os.path.join(d, "applications"), filename, parent) + + def parse_directory_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.DirectoryDirs.append(value) + + def parse_default_directory_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) + + # ---------- Merge Stuff + + def parse_merge_file(self, value, child, filename, parent): + if child.attrib.get("type", None) == "parent": + for d in xdg_config_dirs: + rel_file = filename.replace(d, "").strip("/") + if rel_file != filename: + for p in xdg_config_dirs: + if d == p: + continue + if os.path.isfile(os.path.join(p, rel_file)): + self.merge_file(os.path.join(p, rel_file), child, parent) + break + else: + value = _check_file_path(value, filename, TYPE_FILE) + if value: + self.merge_file(value, child, parent) + + def parse_merge_dir(self, value, child, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + for item in os.listdir(value): + try: + if item.endswith(".menu"): + self.merge_file(os.path.join(value, item), child, parent) + except UnicodeDecodeError: + continue + + def parse_default_merge_dirs(self, child, filename, parent): + basename = os.path.splitext(os.path.basename(filename))[0] + for d in reversed(xdg_config_dirs): + self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) + + def merge_file(self, filename, child, parent): + # check for infinite loops + if filename in self._merged_files: + if self.debug: + raise ParsingError('Infinite MergeFile loop detected', filename) + else: + return + self._merged_files.add(filename) + # load file + try: + tree = etree.parse(filename) + except IOError: + if self.debug: + raise ParsingError('File not found', filename) + else: + return + except: + if self.debug: + raise ParsingError('Not a valid .menu file', filename) + else: + return + root = tree.getroot() + self.parse_node(root, filename, parent) + + # ---------- Legacy Dir Stuff + + def parse_legacy_dir(self, dir_, prefix, filename, parent): + m = self.merge_legacy_dir(dir_, prefix, filename, parent) + if m: + parent += m + + def merge_legacy_dir(self, dir_, prefix, filename, parent): + dir_ = _check_file_path(dir_, filename, TYPE_DIR) + if dir_ and dir_ not in self._directory_dirs: + self._directory_dirs.add(dir_) + m = Menu() + m.AppDirs.append(dir_) + m.DirectoryDirs.append(dir_) + m.Name = os.path.basename(dir_) + m.NotInXml = True + + for item in os.listdir(dir_): + try: + if item == ".directory": + m.Directories.append(item) + elif os.path.isdir(os.path.join(dir_, item)): + m.addSubmenu(self.merge_legacy_dir( + os.path.join(dir_, item), + prefix, + filename, + parent + )) + except UnicodeDecodeError: + continue + + self.cache.add_menu_entries([dir_], prefix, True) + menuentries = self.cache.get_menu_entries([dir_], False) + + for menuentry in menuentries: + categories = menuentry.Categories + if len(categories) == 0: + r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) + m.Rules.append(r) + if not dir_ in parent.AppDirs: + categories.append("Legacy") + menuentry.Categories = categories + + return m + + def parse_kde_legacy_dirs(self, filename, parent): + try: + proc = subprocess.Popen( + ['kde-config', '--path', 'apps'], + stdout=subprocess.PIPE, + universal_newlines=True + ) + output = proc.communicate()[0].splitlines() + except OSError: + # If kde-config doesn't exist, ignore this. + return + try: + for dir_ in output[0].split(":"): + self.parse_legacy_dir(dir_, "kde", filename, parent) + except IndexError: + pass + + def post_parse(self, menu): + # unallocated / deleted + if menu.Deleted is None: + menu.Deleted = False + if menu.OnlyUnallocated is None: + menu.OnlyUnallocated = False + + # Layout Tags + if not menu.Layout or not menu.DefaultLayout: + if menu.DefaultLayout: + menu.Layout = menu.DefaultLayout + elif menu.Layout: + if menu.Depth > 0: + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.DefaultLayout = Layout() + else: + if menu.Depth > 0: + menu.Layout = menu.Parent.DefaultLayout + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.Layout = Layout() + menu.DefaultLayout = Layout() + + # add parent's app/directory dirs + if menu.Depth > 0: + menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs + menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs + + # remove duplicates + menu.Directories = _dedupe(menu.Directories) + menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) + menu.AppDirs = _dedupe(menu.AppDirs) + + # go recursive through all menus + for submenu in menu.Submenus: + self.post_parse(submenu) + + # reverse so handling is easier + menu.Directories.reverse() + menu.DirectoryDirs.reverse() + menu.AppDirs.reverse() + + # get the valid .directory file out of the list + for directory in menu.Directories: + for dir in menu.DirectoryDirs: + if os.path.isfile(os.path.join(dir, directory)): + menuentry = MenuEntry(directory, dir) + if not menu.Directory: + menu.Directory = menuentry + elif menuentry.Type == MenuEntry.TYPE_SYSTEM: + if menu.Directory.Type == MenuEntry.TYPE_USER: + menu.Directory.Original = menuentry + if menu.Directory: + break + + # Finally generate the menu + def generate_not_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_not_only_allocated(submenu) + + if menu.OnlyUnallocated is False: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) + + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + menuentry.Add = False + menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def generate_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_only_allocated(submenu) + + if menu.OnlyUnallocated is True: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + # menuentry.Add = False + # menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def handle_moves(self, menu): + for submenu in menu.Submenus: + self.handle_moves(submenu) + # parse move operations + for move in menu.Moves: + move_from_menu = menu.getMenu(move.Old) + if move_from_menu: + # FIXME: this is assigned, but never used... + move_to_menu = menu.getMenu(move.New) + + menus = move.New.split("/") + oldparent = None + while len(menus) > 0: + if not oldparent: + oldparent = menu + newmenu = oldparent.getMenu(menus[0]) + if not newmenu: + newmenu = Menu() + newmenu.Name = menus[0] + if len(menus) > 1: + newmenu.NotInXml = True + oldparent.addSubmenu(newmenu) + oldparent = newmenu + menus.pop(0) + + newmenu += move_from_menu + move_from_menu.Parent.Submenus.remove(move_from_menu) + + +class MenuEntryCache: + "Class to cache Desktop Entries" + def __init__(self): + self.cacheEntries = {} + self.cacheEntries['legacy'] = [] + self.cache = {} + + def add_menu_entries(self, dirs, prefix="", legacy=False): + for dir_ in dirs: + if not dir_ in self.cacheEntries: + self.cacheEntries[dir_] = [] + self.__addFiles(dir_, "", prefix, legacy) + + def __addFiles(self, dir_, subdir, prefix, legacy): + for item in os.listdir(os.path.join(dir_, subdir)): + if item.endswith(".desktop"): + try: + menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) + except ParsingError: + continue + + self.cacheEntries[dir_].append(menuentry) + if legacy: + self.cacheEntries['legacy'].append(menuentry) + elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: + self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) + + def get_menu_entries(self, dirs, legacy=True): + entries = [] + ids = set() + # handle legacy items + appdirs = dirs[:] + if legacy: + appdirs.append("legacy") + # cache the results again + key = "".join(appdirs) + try: + return self.cache[key] + except KeyError: + pass + for dir_ in appdirs: + for menuentry in self.cacheEntries[dir_]: + try: + if menuentry.DesktopFileID not in ids: + ids.add(menuentry.DesktopFileID) + entries.append(menuentry) + elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: + # FIXME: This is only 99% correct, but still... + idx = entries.index(menuentry) + entry = entries[idx] + if entry.getType() == MenuEntry.TYPE_USER: + entry.Original = menuentry + except UnicodeDecodeError: + continue + self.cache[key] = entries + return entries + + +def parse(filename=None, debug=False): + """Helper function. + Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) + """ + return XMLMenuBuilder(debug).parse(filename) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/MenuEditor.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/MenuEditor.py new file mode 100644 index 0000000..2c68515 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/MenuEditor.py @@ -0,0 +1,541 @@ +""" CLass to edit XDG Menus """ +import os +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from .Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder +from .BaseDirectory import xdg_config_dirs, xdg_data_dirs +from .Exceptions import ParsingError +from .Config import setRootMode + +# XML-Cleanups: Move / Exclude +# FIXME: proper reverte/delete +# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions +# FIXME: catch Exceptions +# FIXME: copy functions +# FIXME: More Layout stuff +# FIXME: unod/redo function / remove menu... +# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile +# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs + + +class MenuEditor(object): + + def __init__(self, menu=None, filename=None, root=False): + self.menu = None + self.filename = None + self.tree = None + self.parser = XMLMenuBuilder() + self.parse(menu, filename, root) + + # fix for creating two menus with the same name on the fly + self.filenames = [] + + def parse(self, menu=None, filename=None, root=False): + if root: + setRootMode(True) + + if isinstance(menu, Menu): + self.menu = menu + elif menu: + self.menu = self.parser.parse(menu) + else: + self.menu = self.parser.parse() + + if root: + self.filename = self.menu.Filename + elif filename: + self.filename = filename + else: + self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) + + try: + self.tree = etree.parse(self.filename) + except IOError: + root = etree.fromtring(""" + + + Applications + %s + +""" % self.menu.Filename) + self.tree = etree.ElementTree(root) + except ParsingError: + raise ParsingError('Not a valid .menu file', self.filename) + + #FIXME: is this needed with etree ? + self.__remove_whitespace_nodes(self.tree) + + def save(self): + self.__saveEntries(self.menu) + self.__saveMenu() + + def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): + menuentry = MenuEntry(self.__getFileName(name, ".desktop")) + menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) + + self.__addEntry(parent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): + menu = Menu() + + menu.Parent = parent + menu.Depth = parent.Depth + 1 + menu.Layout = parent.DefaultLayout + menu.DefaultLayout = parent.DefaultLayout + + menu = self.editMenu(menu, name, genericname, comment, icon) + + self.__addEntry(parent, menu, after, before) + + self.menu.sort() + + return menu + + def createSeparator(self, parent, after=None, before=None): + separator = Separator(parent) + + self.__addEntry(parent, separator, after, before) + + self.menu.sort() + + return separator + + def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menuentry, after, before) + self.__addEntry(newparent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def moveMenu(self, menu, oldparent, newparent, after=None, before=None): + self.__deleteEntry(oldparent, menu, after, before) + self.__addEntry(newparent, menu, after, before) + + root_menu = self.__getXmlMenu(self.menu.Name) + if oldparent.getPath(True) != newparent.getPath(True): + self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) + + self.menu.sort() + + return menu + + def moveSeparator(self, separator, parent, after=None, before=None): + self.__deleteEntry(parent, separator, after, before) + self.__addEntry(parent, separator, after, before) + + self.menu.sort() + + return separator + + def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): + self.__addEntry(newparent, menuentry, after, before) + + self.menu.sort() + + return menuentry + + def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): + deskentry = menuentry.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale=True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale=True) + if genericname: + if not deskentry.hasKey("GenericName"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale=True) + if command: + deskentry.set("Exec", command) + if icon: + deskentry.set("Icon", icon) + + if terminal: + deskentry.set("Terminal", "true") + elif not terminal: + deskentry.set("Terminal", "false") + + if nodisplay is True: + deskentry.set("NoDisplay", "true") + elif nodisplay is False: + deskentry.set("NoDisplay", "false") + + if hidden is True: + deskentry.set("Hidden", "true") + elif hidden is False: + deskentry.set("Hidden", "false") + + menuentry.updateAttributes() + + if len(menuentry.Parents) > 0: + self.menu.sort() + + return menuentry + + def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): + # Hack for legacy dirs + if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") + menu.Directory.setAttributes(menu.Name + ".directory") + # Hack for New Entries + elif not isinstance(menu.Directory, MenuEntry): + if not name: + name = menu.Name + filename = self.__getFileName(name, ".directory").replace("/", "") + if not menu.Name: + menu.Name = filename.replace(".directory", "") + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + self.__addXmlTextElement(xml_menu, 'Directory', filename) + menu.Directory = MenuEntry(filename) + + deskentry = menu.Directory.DesktopEntry + + if name: + if not deskentry.hasKey("Name"): + deskentry.set("Name", name) + deskentry.set("Name", name, locale=True) + if genericname: + if not deskentry.hasKey("GenericName"): + deskentry.set("GenericName", genericname) + deskentry.set("GenericName", genericname, locale=True) + if comment: + if not deskentry.hasKey("Comment"): + deskentry.set("Comment", comment) + deskentry.set("Comment", comment, locale=True) + if icon: + deskentry.set("Icon", icon) + + if nodisplay is True: + deskentry.set("NoDisplay", "true") + elif nodisplay is False: + deskentry.set("NoDisplay", "false") + + if hidden is True: + deskentry.set("Hidden", "true") + elif hidden is False: + deskentry.set("Hidden", "false") + + menu.Directory.updateAttributes() + + if isinstance(menu.Parent, Menu): + self.menu.sort() + + return menu + + def hideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay=True) + + def unhideMenuEntry(self, menuentry): + self.editMenuEntry(menuentry, nodisplay=False, hidden=False) + + def hideMenu(self, menu): + self.editMenu(menu, nodisplay=True) + + def unhideMenu(self, menu): + self.editMenu(menu, nodisplay=False, hidden=False) + xml_menu = self.__getXmlMenu(menu.getPath(True, True), False) + deleted = xml_menu.findall('Deleted') + not_deleted = xml_menu.findall('NotDeleted') + for node in deleted + not_deleted: + xml_menu.remove(node) + + def deleteMenuEntry(self, menuentry): + if self.getAction(menuentry) == "delete": + self.__deleteFile(menuentry.DesktopEntry.filename) + for parent in menuentry.Parents: + self.__deleteEntry(parent, menuentry) + self.menu.sort() + return menuentry + + def revertMenuEntry(self, menuentry): + if self.getAction(menuentry) == "revert": + self.__deleteFile(menuentry.DesktopEntry.filename) + menuentry.Original.Parents = [] + for parent in menuentry.Parents: + index = parent.Entries.index(menuentry) + parent.Entries[index] = menuentry.Original + index = parent.MenuEntries.index(menuentry) + parent.MenuEntries[index] = menuentry.Original + menuentry.Original.Parents.append(parent) + self.menu.sort() + return menuentry + + def deleteMenu(self, menu): + if self.getAction(menu) == "delete": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + self.__deleteEntry(menu.Parent, menu) + xml_menu = self.__getXmlMenu(menu.getPath(True, True)) + parent = self.__get_parent_node(xml_menu) + parent.remove(xml_menu) + self.menu.sort() + return menu + + def revertMenu(self, menu): + if self.getAction(menu) == "revert": + self.__deleteFile(menu.Directory.DesktopEntry.filename) + menu.Directory = menu.Directory.Original + self.menu.sort() + return menu + + def deleteSeparator(self, separator): + self.__deleteEntry(separator.Parent, separator, after=True) + + self.menu.sort() + + return separator + + """ Private Stuff """ + def getAction(self, entry): + if isinstance(entry, Menu): + if not isinstance(entry.Directory, MenuEntry): + return "none" + elif entry.Directory.getType() == "Both": + return "revert" + elif entry.Directory.getType() == "User" and ( + len(entry.Submenus) + len(entry.MenuEntries) + ) == 0: + return "delete" + + elif isinstance(entry, MenuEntry): + if entry.getType() == "Both": + return "revert" + elif entry.getType() == "User": + return "delete" + else: + return "none" + + return "none" + + def __saveEntries(self, menu): + if not menu: + menu = self.menu + if isinstance(menu.Directory, MenuEntry): + menu.Directory.save() + for entry in menu.getEntries(hidden=True): + if isinstance(entry, MenuEntry): + entry.save() + elif isinstance(entry, Menu): + self.__saveEntries(entry) + + def __saveMenu(self): + if not os.path.isdir(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + self.tree.write(self.filename, encoding='utf-8') + + def __getFileName(self, name, extension): + postfix = 0 + while 1: + if postfix == 0: + filename = name + extension + else: + filename = name + "-" + str(postfix) + extension + if extension == ".desktop": + dir = "applications" + elif extension == ".directory": + dir = "desktop-directories" + if not filename in self.filenames and not os.path.isfile( + os.path.join(xdg_data_dirs[0], dir, filename) + ): + self.filenames.append(filename) + break + else: + postfix += 1 + + return filename + + def __getXmlMenu(self, path, create=True, element=None): + # FIXME: we should also return the menu's parent, + # to avoid looking for it later on + # @see Element.getiterator() + if not element: + element = self.tree + + if "/" in path: + (name, path) = path.split("/", 1) + else: + name = path + path = "" + + found = None + for node in element.findall("Menu"): + name_node = node.find('Name') + if name_node.text == name: + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + if found: + break + if not found and create: + node = self.__addXmlMenuElement(element, name) + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node + + return found + + def __addXmlMenuElement(self, element, name): + menu_node = etree.SubElement('Menu', element) + name_node = etree.SubElement('Name', menu_node) + name_node.text = name + return menu_node + + def __addXmlTextElement(self, element, name, text): + node = etree.SubElement(name, element) + node.text = text + return node + + def __addXmlFilename(self, element, filename, type_="Include"): + # remove old filenames + includes = element.findall('Include') + excludes = element.findall('Exclude') + rules = includes + excludes + for rule in rules: + #FIXME: this finds only Rules whose FIRST child is a Filename element + if rule[0].tag == "Filename" and rule[0].text == filename: + element.remove(rule) + # shouldn't it remove all occurences, like the following: + #filename_nodes = rule.findall('.//Filename'): + #for fn in filename_nodes: + #if fn.text == filename: + ##element.remove(rule) + #parent = self.__get_parent_node(fn) + #parent.remove(fn) + + # add new filename + node = etree.SubElement(type_, element) + self.__addXmlTextElement(node, 'Filename', filename) + return node + + def __addXmlMove(self, element, old, new): + node = etree.SubElement("Move", element) + self.__addXmlTextElement(node, 'Old', old) + self.__addXmlTextElement(node, 'New', new) + return node + + def __addXmlLayout(self, element, layout): + # remove old layout + for node in element.findall("Layout"): + element.remove(node) + + # add new layout + node = etree.SubElement("Layout", element) + for order in layout.order: + if order[0] == "Separator": + child = etree.SubElement("Separator", node) + elif order[0] == "Filename": + child = self.__addXmlTextElement(node, "Filename", order[1]) + elif order[0] == "Menuname": + child = self.__addXmlTextElement(node, "Menuname", order[1]) + elif order[0] == "Merge": + child = etree.SubElement("Merge", node) + child.attrib["type"] = order[1] + return node + + def __addLayout(self, parent): + layout = Layout() + layout.order = [] + layout.show_empty = parent.Layout.show_empty + layout.inline = parent.Layout.inline + layout.inline_header = parent.Layout.inline_header + layout.inline_alias = parent.Layout.inline_alias + layout.inline_limit = parent.Layout.inline_limit + + layout.order.append(["Merge", "menus"]) + for entry in parent.Entries: + if isinstance(entry, Menu): + layout.parseMenuname(entry.Name) + elif isinstance(entry, MenuEntry): + layout.parseFilename(entry.DesktopFileID) + elif isinstance(entry, Separator): + layout.parseSeparator() + layout.order.append(["Merge", "files"]) + + parent.Layout = layout + + return layout + + def __addEntry(self, parent, entry, after=None, before=None): + if after or before: + if after: + index = parent.Entries.index(after) + 1 + elif before: + index = parent.Entries.index(before) + parent.Entries.insert(index, entry) + else: + parent.Entries.append(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + parent.MenuEntries.append(entry) + entry.Parents.append(parent) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") + elif isinstance(entry, Menu): + parent.addSubmenu(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteEntry(self, parent, entry, after=None, before=None): + parent.Entries.remove(entry) + + xml_parent = self.__getXmlMenu(parent.getPath(True, True)) + + if isinstance(entry, MenuEntry): + entry.Parents.remove(parent) + parent.MenuEntries.remove(entry) + self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") + elif isinstance(entry, Menu): + parent.Submenus.remove(entry) + + if after or before: + self.__addLayout(parent) + self.__addXmlLayout(xml_parent, parent.Layout) + + def __deleteFile(self, filename): + try: + os.remove(filename) + except OSError: + pass + try: + self.filenames.remove(filename) + except ValueError: + pass + + def __remove_whitespace_nodes(self, node): + for child in node: + text = child.text.strip() + if not text: + child.text = '' + tail = child.tail.strip() + if not tail: + child.tail = '' + if len(child): + self.__remove_whilespace_nodes(child) + + def __get_parent_node(self, node): + # elements in ElementTree doesn't hold a reference to their parent + for parent, child in self.__iter_parent(): + if child is node: + return child + + def __iter_parent(self): + for parent in self.tree.getiterator(): + for child in parent: + yield parent, child diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Mime.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Mime.py new file mode 100644 index 0000000..60c4efd --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/Mime.py @@ -0,0 +1,780 @@ +""" +This module is based on a rox module (LGPL): + +http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log + +This module provides access to the shared MIME database. + +types is a dictionary of all known MIME types, indexed by the type name, e.g. +types['application/x-python'] + +Applications can install information about MIME types by storing an +XML file as /packages/.xml and running the +update-mime-database command, which is provided by the freedesktop.org +shared mime database package. + +See http://www.freedesktop.org/standards/shared-mime-info-spec/ for +information about the format of these files. + +(based on version 0.13) +""" + +import os +import re +import stat +import sys +import fnmatch + +from . import BaseDirectory, Locale + +from .dom import minidom, XML_NAMESPACE +from collections import defaultdict + +FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' + +types = {} # Maps MIME names to type objects + +exts = None # Maps extensions to types +globs = None # List of (glob, type) pairs +literals = None # Maps liternal names to types +magic = None + +PY3 = (sys.version_info[0] >= 3) + +def _get_node_data(node): + """Get text of XML node""" + return ''.join([n.nodeValue for n in node.childNodes]).strip() + +def lookup(media, subtype = None): + """Get the MIMEtype object for the given type. + + This remains for backwards compatibility; calling MIMEtype now does + the same thing. + + The name can either be passed as one part ('text/plain'), or as two + ('text', 'plain'). + """ + return MIMEtype(media, subtype) + +class MIMEtype(object): + """Class holding data about a MIME type. + + Calling the class will return a cached instance, so there is only one + instance for each MIME type. The name can either be passed as one part + ('text/plain'), or as two ('text', 'plain'). + """ + def __new__(cls, media, subtype=None): + if subtype is None and '/' in media: + media, subtype = media.split('/', 1) + assert '/' not in subtype + media = media.lower() + subtype = subtype.lower() + + try: + return types[(media, subtype)] + except KeyError: + mtype = super(MIMEtype, cls).__new__(cls) + mtype._init(media, subtype) + types[(media, subtype)] = mtype + return mtype + + # If this is done in __init__, it is automatically called again each time + # the MIMEtype is returned by __new__, which we don't want. So we call it + # explicitly only when we construct a new instance. + def _init(self, media, subtype): + self.media = media + self.subtype = subtype + self._comment = None + + def _load(self): + "Loads comment for current language. Use get_comment() instead." + resource = os.path.join('mime', self.media, self.subtype + '.xml') + for path in BaseDirectory.load_data_paths(resource): + doc = minidom.parse(path) + if doc is None: + continue + for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): + lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' + goodness = 1 + (lang in xdg.Locale.langs) + if goodness > self._comment[0]: + self._comment = (goodness, _get_node_data(comment)) + if goodness == 2: return + + # FIXME: add get_icon method + def get_comment(self): + """Returns comment for current language, loading it if needed.""" + # Should we ever reload? + if self._comment is None: + self._comment = (0, str(self)) + self._load() + return self._comment[1] + + def canonical(self): + """Returns the canonical MimeType object if this is an alias.""" + update_cache() + s = str(self) + if s in aliases: + return lookup(aliases[s]) + return self + + def inherits_from(self): + """Returns a set of Mime types which this inherits from.""" + update_cache() + return set(lookup(t) for t in inheritance[str(self)]) + + def __str__(self): + return self.media + '/' + self.subtype + + def __repr__(self): + return 'MIMEtype(%r, %r)' % (self.media, self.subtype) + + def __hash__(self): + return hash(self.media) ^ hash(self.subtype) + +class UnknownMagicRuleFormat(ValueError): + pass + +class DiscardMagicRules(Exception): + "Raised when __NOMAGIC__ is found, and caught to discard previous rules." + pass + +class MagicRule: + also = None + + def __init__(self, start, value, mask, word, range): + self.start = start + self.value = value + self.mask = mask + self.word = word + self.range = range + + rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$') + + @classmethod + def from_file(cls, f): + """Read a rule from the binary magics file. Returns a 2-tuple of + the nesting depth and the MagicRule.""" + line = f.readline() + #print line + + # [indent] '>' + nest_depth, line = line.split(b'>', 1) + nest_depth = int(nest_depth) if nest_depth else 0 + + # start-offset '=' + start, line = line.split(b'=', 1) + start = int(start) + + if line == b'__NOMAGIC__\n': + raise DiscardMagicRules + + # value length (2 bytes, big endian) + if sys.version_info[0] >= 3: + lenvalue = int.from_bytes(line[:2], byteorder='big') + else: + lenvalue = (ord(line[0])<<8)+ord(line[1]) + line = line[2:] + + # value + # This can contain newlines, so we may need to read more lines + while len(line) <= lenvalue: + line += f.readline() + value, line = line[:lenvalue], line[lenvalue:] + + # ['&' mask] + if line.startswith(b'&'): + # This can contain newlines, so we may need to read more lines + while len(line) <= lenvalue: + line += f.readline() + mask, line = line[1:lenvalue+1], line[lenvalue+1:] + else: + mask = None + + # ['~' word-size] ['+' range-length] + ending = cls.rule_ending_re.match(line) + if not ending: + # Per the spec, this will be caught and ignored, to allow + # for future extensions. + raise UnknownMagicRuleFormat(repr(line)) + + word, range = ending.groups() + word = int(word) if (word is not None) else 1 + range = int(range) if (range is not None) else 1 + + return nest_depth, cls(start, value, mask, word, range) + + def maxlen(self): + l = self.start + len(self.value) + self.range + if self.also: + return max(l, self.also.maxlen()) + return l + + def match(self, buffer): + if self.match0(buffer): + if self.also: + return self.also.match(buffer) + return True + + def match0(self, buffer): + l=len(buffer) + lenvalue = len(self.value) + for o in range(self.range): + s=self.start+o + e=s+lenvalue + if l [(priority, rule), ...] + + def merge_file(self, fname): + """Read a magic binary file, and add its rules to this MagicDB.""" + with open(fname, 'rb') as f: + line = f.readline() + if line != b'MIME-Magic\0\n': + raise IOError('Not a MIME magic file') + + while True: + shead = f.readline().decode('ascii') + #print(shead) + if not shead: + break + if shead[0] != '[' or shead[-2:] != ']\n': + raise ValueError('Malformed section heading', shead) + pri, tname = shead[1:-2].split(':') + #print shead[1:-2] + pri = int(pri) + mtype = lookup(tname) + try: + rule = MagicMatchAny.from_file(f) + except DiscardMagicRules: + self.bytype.pop(mtype, None) + rule = MagicMatchAny.from_file(f) + if rule is None: + continue + #print rule + + self.bytype[mtype].append((pri, rule)) + + def finalise(self): + """Prepare the MagicDB for matching. + + This should be called after all rules have been merged into it. + """ + maxlen = 0 + self.alltypes = [] # (priority, mimetype, rule) + + for mtype, rules in self.bytype.items(): + for pri, rule in rules: + self.alltypes.append((pri, mtype, rule)) + maxlen = max(maxlen, rule.maxlen()) + + self.maxlen = maxlen # Number of bytes to read from files + self.alltypes.sort(key=lambda x: x[0], reverse=True) + + def match_data(self, data, max_pri=100, min_pri=0, possible=None): + """Do magic sniffing on some bytes. + + max_pri & min_pri can be used to specify the maximum & minimum priority + rules to look for. possible can be a list of mimetypes to check, or None + (the default) to check all mimetypes until one matches. + + Returns the MIMEtype found, or None if no entries match. + """ + if possible is not None: + types = [] + for mt in possible: + for pri, rule in self.bytype[mt]: + types.append((pri, mt, rule)) + types.sort(key=lambda x: x[0]) + else: + types = self.alltypes + + for priority, mimetype, rule in types: + #print priority, max_pri, min_pri + if priority > max_pri: + continue + if priority < min_pri: + break + + if rule.match(data): + return mimetype + + def match(self, path, max_pri=100, min_pri=0, possible=None): + """Read data from the file and do magic sniffing on it. + + max_pri & min_pri can be used to specify the maximum & minimum priority + rules to look for. possible can be a list of mimetypes to check, or None + (the default) to check all mimetypes until one matches. + + Returns the MIMEtype found, or None if no entries match. Raises IOError + if the file can't be opened. + """ + with open(path, 'rb') as f: + buf = f.read(self.maxlen) + return self.match_data(buf, max_pri, min_pri, possible) + + def __repr__(self): + return '' % len(self.alltypes) + +class GlobDB(object): + def __init__(self): + """Prepare the GlobDB. It can't actually be used until .finalise() is + called, but merge_file() can be used to add data before that. + """ + # Maps mimetype to {(weight, glob, flags), ...} + self.allglobs = defaultdict(set) + + def merge_file(self, path): + """Loads name matching information from a globs2 file."""# + allglobs = self.allglobs + with open(path) as f: + for line in f: + if line.startswith('#'): continue # Comment + + fields = line[:-1].split(':') + weight, type_name, pattern = fields[:3] + weight = int(weight) + mtype = lookup(type_name) + if len(fields) > 3: + flags = fields[3].split(',') + else: + flags = () + + if pattern == '__NOGLOBS__': + # This signals to discard any previous globs + allglobs.pop(mtype, None) + continue + + allglobs[mtype].add((weight, pattern, tuple(flags))) + + def finalise(self): + """Prepare the GlobDB for matching. + + This should be called after all files have been merged into it. + """ + self.exts = defaultdict(list) # Maps extensions to [(type, weight),...] + self.cased_exts = defaultdict(list) + self.globs = [] # List of (regex, type, weight) triplets + self.literals = {} # Maps literal names to (type, weight) + self.cased_literals = {} + + for mtype, globs in self.allglobs.items(): + mtype = mtype.canonical() + for weight, pattern, flags in globs: + + cased = 'cs' in flags + + if pattern.startswith('*.'): + # *.foo -- extension pattern + rest = pattern[2:] + if not ('*' in rest or '[' in rest or '?' in rest): + if cased: + self.cased_exts[rest].append((mtype, weight)) + else: + self.exts[rest.lower()].append((mtype, weight)) + continue + + if ('*' in pattern or '[' in pattern or '?' in pattern): + # Translate the glob pattern to a regex & compile it + re_flags = 0 if cased else re.I + pattern = re.compile(fnmatch.translate(pattern), flags=re_flags) + self.globs.append((pattern, mtype, weight)) + else: + # No wildcards - literal pattern + if cased: + self.cased_literals[pattern] = (mtype, weight) + else: + self.literals[pattern.lower()] = (mtype, weight) + + # Sort globs by weight & length + self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) ) + + def first_match(self, path): + """Return the first match found for a given path, or None if no match + is found.""" + try: + return next(self._match_path(path))[0] + except StopIteration: + return None + + def all_matches(self, path): + """Return a list of (MIMEtype, glob weight) pairs for the path.""" + return list(self._match_path(path)) + + def _match_path(self, path): + """Yields pairs of (mimetype, glob weight).""" + leaf = os.path.basename(path) + + # Literals (no wildcards) + if leaf in self.cased_literals: + yield self.cased_literals[leaf] + + lleaf = leaf.lower() + if lleaf in self.literals: + yield self.literals[lleaf] + + # Extensions + ext = leaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p + 1:] + if ext in self.cased_exts: + for res in self.cased_exts[ext]: + yield res + ext = lleaf + while 1: + p = ext.find('.') + if p < 0: break + ext = ext[p+1:] + if ext in self.exts: + for res in self.exts[ext]: + yield res + + # Other globs + for (regex, mime_type, weight) in self.globs: + if regex.match(leaf): + yield (mime_type, weight) + +# Some well-known types +text = lookup('text', 'plain') +octet_stream = lookup('application', 'octet-stream') +inode_block = lookup('inode', 'blockdevice') +inode_char = lookup('inode', 'chardevice') +inode_dir = lookup('inode', 'directory') +inode_fifo = lookup('inode', 'fifo') +inode_socket = lookup('inode', 'socket') +inode_symlink = lookup('inode', 'symlink') +inode_door = lookup('inode', 'door') +app_exe = lookup('application', 'executable') + +_cache_uptodate = False + +def _cache_database(): + global globs, magic, aliases, inheritance, _cache_uptodate + + _cache_uptodate = True + + aliases = {} # Maps alias Mime types to canonical names + inheritance = defaultdict(set) # Maps to sets of parent mime types. + + # Load aliases + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')): + with open(path, 'r') as f: + for line in f: + alias, canonical = line.strip().split(None, 1) + aliases[alias] = canonical + + # Load filename patterns (globs) + globs = GlobDB() + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')): + globs.merge_file(path) + globs.finalise() + + # Load magic sniffing data + magic = MagicDB() + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): + magic.merge_file(path) + magic.finalise() + + # Load subclasses + for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): + with open(path, 'r') as f: + for line in f: + sub, parent = line.strip().split(None, 1) + inheritance[sub].add(parent) + +def update_cache(): + if not _cache_uptodate: + _cache_database() + +def get_type_by_name(path): + """Returns type of file by its name, or None if not known""" + update_cache() + return globs.first_match(path) + +def get_type_by_contents(path, max_pri=100, min_pri=0): + """Returns type of file by its contents, or None if not known""" + update_cache() + + return magic.match(path, max_pri, min_pri) + +def get_type_by_data(data, max_pri=100, min_pri=0): + """Returns type of the data, which should be bytes.""" + update_cache() + + return magic.match_data(data, max_pri, min_pri) + +def _get_type_by_stat(st_mode): + """Match special filesystem objects to Mimetypes.""" + if stat.S_ISDIR(st_mode): return inode_dir + elif stat.S_ISCHR(st_mode): return inode_char + elif stat.S_ISBLK(st_mode): return inode_block + elif stat.S_ISFIFO(st_mode): return inode_fifo + elif stat.S_ISLNK(st_mode): return inode_symlink + elif stat.S_ISSOCK(st_mode): return inode_socket + return inode_door + +def get_type(path, follow=True, name_pri=100): + """Returns type of file indicated by path. + + This function is *deprecated* - :func:`get_type2` is more accurate. + + :param path: pathname to check (need not exist) + :param follow: when reading file, follow symbolic links + :param name_pri: Priority to do name matches. 100=override magic + + This tries to use the contents of the file, and falls back to the name. It + can also handle special filesystem objects like directories and sockets. + """ + update_cache() + + try: + if follow: + st = os.stat(path) + else: + st = os.lstat(path) + except: + t = get_type_by_name(path) + return t or text + + if stat.S_ISREG(st.st_mode): + # Regular file + t = get_type_by_contents(path, min_pri=name_pri) + if not t: t = get_type_by_name(path) + if not t: t = get_type_by_contents(path, max_pri=name_pri) + if t is None: + if stat.S_IMODE(st.st_mode) & 0o111: + return app_exe + else: + return text + return t + else: + return _get_type_by_stat(st.st_mode) + +def get_type2(path, follow=True): + """Find the MIMEtype of a file using the XDG recommended checking order. + + This first checks the filename, then uses file contents if the name doesn't + give an unambiguous MIMEtype. It can also handle special filesystem objects + like directories and sockets. + + :param path: file path to examine (need not exist) + :param follow: whether to follow symlinks + + :rtype: :class:`MIMEtype` + + .. versionadded:: 1.0 + """ + update_cache() + + try: + st = os.stat(path) if follow else os.lstat(path) + except OSError: + return get_type_by_name(path) or octet_stream + + if not stat.S_ISREG(st.st_mode): + # Special filesystem objects + return _get_type_by_stat(st.st_mode) + + mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True) + if mtypes: + max_weight = mtypes[0][1] + i = 1 + for mt, w in mtypes[1:]: + if w < max_weight: + break + i += 1 + mtypes = mtypes[:i] + if len(mtypes) == 1: + return mtypes[0][0] + + possible = [mt for mt,w in mtypes] + else: + possible = None # Try all magic matches + + try: + t = magic.match(path, possible=possible) + except IOError: + t = None + + if t: + return t + elif mtypes: + return mtypes[0][0] + elif stat.S_IMODE(st.st_mode) & 0o111: + return app_exe + else: + return text if is_text_file(path) else octet_stream + +def is_text_file(path): + """Guess whether a file contains text or binary data. + + Heuristic: binary if the first 32 bytes include ASCII control characters. + This rule may change in future versions. + + .. versionadded:: 1.0 + """ + try: + f = open(path, 'rb') + except IOError: + return False + + with f: + return _is_text(f.read(32)) + +if PY3: + def _is_text(data): + return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data) +else: + def _is_text(data): + return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \ + for b in data) + +_mime2ext_cache = None +_mime2ext_cache_uptodate = False + +def get_extensions(mimetype): + """Retrieve the set of filename extensions matching a given MIMEtype. + + Extensions are returned without a leading dot, e.g. 'py'. If no extensions + are registered for the MIMEtype, returns an empty set. + + The extensions are stored in a cache the first time this is called. + + .. versionadded:: 1.0 + """ + global _mime2ext_cache, _mime2ext_cache_uptodate + update_cache() + if not _mime2ext_cache_uptodate: + _mime2ext_cache = defaultdict(set) + for ext, mtypes in globs.exts.items(): + for mtype, prio in mtypes: + _mime2ext_cache[mtype].add(ext) + _mime2ext_cache_uptodate = True + + return _mime2ext_cache[mimetype] + + +def install_mime_info(application, package_file): + """Copy 'package_file' as ``~/.local/share/mime/packages/.xml.`` + If package_file is None, install ``/.xml``. + If already installed, does nothing. May overwrite an existing + file with the same name (if the contents are different)""" + application += '.xml' + + new_data = open(package_file).read() + + # See if the file is already installed + package_dir = os.path.join('mime', 'packages') + resource = os.path.join(package_dir, application) + for x in BaseDirectory.load_data_paths(resource): + try: + old_data = open(x).read() + except: + continue + if old_data == new_data: + return # Already installed + + global _cache_uptodate + _cache_uptodate = False + + # Not already installed; add a new copy + # Create the directory structure... + new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application) + + # Write the file... + open(new_file, 'w').write(new_data) + + # Update the database... + command = 'update-mime-database' + if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')): + os.unlink(new_file) + raise Exception("The '%s' command returned an error code!\n" \ + "Make sure you have the freedesktop.org shared MIME package:\n" \ + "http://standards.freedesktop.org/shared-mime-info/" % command) diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/RecentFiles.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/RecentFiles.py new file mode 100644 index 0000000..fbe608c --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/RecentFiles.py @@ -0,0 +1,181 @@ +""" +Implementation of the XDG Recent File Storage Specification +http://standards.freedesktop.org/recent-file-spec +""" + +import xml.dom.minidom, xml.sax.saxutils +import os, time, fcntl +from .Exceptions import ParsingError + +class RecentFiles: + def __init__(self): + self.RecentFiles = [] + self.filename = "" + + def parse(self, filename=None): + """Parse a list of recently used files. + + filename defaults to ``~/.recently-used``. + """ + if not filename: + filename = os.path.join(os.getenv("HOME"), ".recently-used") + + try: + doc = xml.dom.minidom.parse(filename) + except IOError: + raise ParsingError('File not found', filename) + except xml.parsers.expat.ExpatError: + raise ParsingError('Not a valid .menu file', filename) + + self.filename = filename + + for child in doc.childNodes: + if child.nodeType == xml.dom.Node.ELEMENT_NODE: + if child.tagName == "RecentFiles": + for recent in child.childNodes: + if recent.nodeType == xml.dom.Node.ELEMENT_NODE: + if recent.tagName == "RecentItem": + self.__parseRecentItem(recent) + + self.sort() + + def __parseRecentItem(self, item): + recent = RecentFile() + self.RecentFiles.append(recent) + + for attribute in item.childNodes: + if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: + if attribute.tagName == "URI": + recent.URI = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Mime-Type": + recent.MimeType = attribute.childNodes[0].nodeValue + elif attribute.tagName == "Timestamp": + recent.Timestamp = int(attribute.childNodes[0].nodeValue) + elif attribute.tagName == "Private": + recent.Prviate = True + elif attribute.tagName == "Groups": + + for group in attribute.childNodes: + if group.nodeType == xml.dom.Node.ELEMENT_NODE: + if group.tagName == "Group": + recent.Groups.append(group.childNodes[0].nodeValue) + + def write(self, filename=None): + """Write the list of recently used files to disk. + + If the instance is already associated with a file, filename can be + omitted to save it there again. + """ + if not filename and not self.filename: + raise ParsingError('File not found', filename) + elif not filename: + filename = self.filename + + f = open(filename, "w") + fcntl.lockf(f, fcntl.LOCK_EX) + f.write('\n') + f.write("\n") + + for r in self.RecentFiles: + f.write(" \n") + f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) + f.write(" %s\n" % r.MimeType) + f.write(" %s\n" % r.Timestamp) + if r.Private == True: + f.write(" \n") + if len(r.Groups) > 0: + f.write(" \n") + for group in r.Groups: + f.write(" %s\n" % group) + f.write(" \n") + f.write(" \n") + + f.write("\n") + fcntl.lockf(f, fcntl.LOCK_UN) + f.close() + + def getFiles(self, mimetypes=None, groups=None, limit=0): + """Get a list of recently used files. + + The parameters can be used to filter by mime types, by group, or to + limit the number of items returned. By default, the entire list is + returned, except for items marked private. + """ + tmp = [] + i = 0 + for item in self.RecentFiles: + if groups: + for group in groups: + if group in item.Groups: + tmp.append(item) + i += 1 + elif mimetypes: + for mimetype in mimetypes: + if mimetype == item.MimeType: + tmp.append(item) + i += 1 + else: + if item.Private == False: + tmp.append(item) + i += 1 + if limit != 0 and i == limit: + break + + return tmp + + def addFile(self, item, mimetype, groups=None, private=False): + """Add a recently used file. + + item should be the URI of the file, typically starting with ``file:///``. + """ + # check if entry already there + if item in self.RecentFiles: + index = self.RecentFiles.index(item) + recent = self.RecentFiles[index] + else: + # delete if more then 500 files + if len(self.RecentFiles) == 500: + self.RecentFiles.pop() + # add entry + recent = RecentFile() + self.RecentFiles.append(recent) + + recent.URI = item + recent.MimeType = mimetype + recent.Timestamp = int(time.time()) + recent.Private = private + if groups: + recent.Groups = groups + + self.sort() + + def deleteFile(self, item): + """Remove a recently used file, by URI, from the list. + """ + if item in self.RecentFiles: + self.RecentFiles.remove(item) + + def sort(self): + self.RecentFiles.sort() + self.RecentFiles.reverse() + + +class RecentFile: + def __init__(self): + self.URI = "" + self.MimeType = "" + self.Timestamp = "" + self.Private = False + self.Groups = [] + + def __cmp__(self, other): + return cmp(self.Timestamp, other.Timestamp) + + def __lt__ (self, other): + return self.Timestamp < other.Timestamp + + def __eq__(self, other): + return self.URI == str(other) + + def __str__(self): + return self.URI diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/__init__.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/__init__.py new file mode 100644 index 0000000..b5a117e --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/__init__.py @@ -0,0 +1,3 @@ +__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] + +__version__ = "0.26" diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/util.py b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/util.py new file mode 100644 index 0000000..1637aa5 --- /dev/null +++ b/src/debs/pytop-0-0-1-x64/opt/Pytop/widgets/mixins/xdg/util.py @@ -0,0 +1,75 @@ +import sys + +PY3 = sys.version_info[0] >= 3 + +if PY3: + def u(s): + return s +else: + # Unicode-like literals + def u(s): + return s.decode('utf-8') + +try: + # which() is available from Python 3.3 + from shutil import which +except ImportError: + import os + # This is a copy of which() from Python 3.3 + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if not os.curdir in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if not normdir in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/Main_Window.glade b/src/debs/pytop-0-0-1-x64/usr/share/pytop/Main_Window.glade similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/Main_Window.glade rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/Main_Window.glade diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/archive.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/archive.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/archive.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/archive.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/audio.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/audio.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/audio.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/audio.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/bin.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/bin.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/bin.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/bin.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/dir.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/dir.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/dir.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/dir.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/doc.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/doc.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/doc.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/doc.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/pdf.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/pdf.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/pdf.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/pdf.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/presentation.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/presentation.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/presentation.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/presentation.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/spreadsheet.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/spreadsheet.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/spreadsheet.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/spreadsheet.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/text.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/text.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/text.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/text.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/trash.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/trash.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/trash.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/trash.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/video.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/video.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/video.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/video.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/web.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/web.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/icons/web.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/icons/web.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_128x128.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_128x128.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_128x128.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_128x128.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_256x256.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_256x256.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_256x256.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_256x256.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_32x32.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_32x32.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_32x32.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_32x32.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_64x64.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_64x64.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_64x64.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_64x64.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_72x72.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_72x72.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_72x72.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_72x72.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_96x96.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_96x96.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon2_96x96.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon2_96x96.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_128x128.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_128x128.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_128x128.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_128x128.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_256x256.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_256x256.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_256x256.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_256x256.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_32x32.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_32x32.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_32x32.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_32x32.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_64x64.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_64x64.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_64x64.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_64x64.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_72x72.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_72x72.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_72x72.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_72x72.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_96x96.png b/src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_96x96.png similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/start_menu_icons/start_menu_icon_96x96.png rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/start_menu_icons/start_menu_icon_96x96.png diff --git a/src/debs/pytop-0-0-1-x64/opt/Pytop/resources/stylesheet.css b/src/debs/pytop-0-0-1-x64/usr/share/pytop/stylesheet.css similarity index 100% rename from src/debs/pytop-0-0-1-x64/opt/Pytop/resources/stylesheet.css rename to src/debs/pytop-0-0-1-x64/usr/share/pytop/stylesheet.css diff --git a/user_config/usr/share/pytop/Main_Window.glade b/user_config/usr/share/pytop/Main_Window.glade new file mode 100644 index 0000000..f0222ae --- /dev/null +++ b/user_config/usr/share/pytop/Main_Window.glade @@ -0,0 +1,849 @@ + + + + + + + inode/directory + + + + 1 + 100 + 1 + 1 + 10 + + + + True + False + gtk-new + + + 64 + 64 + True + False + start_menu_icons/start_menu_icon2_32x32.png + + + False + 800 + 600 + desktop + False + center + + + + + + 256 + True + False + vertical + + + True + False + + + Menu + 64 + 64 + True + True + True + menuImage + True + + + + False + True + 0 + + + + + + + + True + False + start + + + + + + + + + + False + True + end + 3 + + + + + True + False + select-folder + Folders + Directory Chooser + + + + False + True + end + 2 + + + + + False + True + 0 + + + + + True + False + + + True + False + vertical + + + True + True + in + + + True + True + 6 + multiple + + + + + True + True + 0 + + + + + True + True + 0 + + + + + 180 + 64 + True + True + never + in + 225 + + + True + False + + + True + False + vertical + + + + + + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + False + + + 64 + True + True + never + in + + + True + False + + + True + False + + + + + + + + + + True + True + 0 + + + + + True + False + start + + + + + + False + True + 1 + + + + + True + False + True + + + + 126 + True + False + 10 + 10 + center + False + + + + + + + + False + True + 2 + + + + + False + True + 3 + + + + + + + 420 + 225 + False + timeLabelEveBox + False + + + True + True + 2020 + 3 + 22 + + + + + 500 + 0 + False + taskBarWorkspacesVer + bottom + + + True + False + + + True + True + True + True + rgb(138,226,52) + + + + False + True + 1 + + + + + 60 + True + False + + + + + True + True + 2 + + + + + True + True + brushSizeProp + + + False + True + 2 + + + + + + + 600 + 400 + False + 4 + mouse + True + True + True + False + False + center + True + + + + + + True + False + vertical + + + True + True + True + True + True + edit-find-symbolic + False + False + + + + False + True + 0 + + + + + True + False + + + True + False + vertical + start + + + Accessories + True + True + True + + + + True + True + 0 + + + + + Multimedia + True + True + True + + + + True + True + 1 + + + + + Graphics + True + True + True + + + + True + True + 2 + + + + + Game + True + True + True + + + + True + True + 3 + + + + + Office + True + True + True + + + + True + True + 4 + + + + + Development + True + True + True + + + + True + True + 5 + + + + + Internet + True + True + True + + + + True + True + 6 + + + + + Settings + True + True + True + + + + True + True + 7 + + + + + System + True + True + True + + + + True + True + 8 + + + + + Wine + True + True + True + + + + True + True + 9 + + + + + Other + True + True + True + + + + True + True + 10 + + + + + False + True + 0 + + + + + True + True + True + never + in + + + True + False + + + True + False + vertical + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + + True + False + icons/trash.png + + + False + False + True + center + True + splashscreen + True + True + False + False + center + + + + + + + 500 + True + False + vertical + + + 500 + 26 + True + True + gtk-edit + + + + False + True + 0 + + + + + True + False + + + gtk-copy + True + True + True + Copy... + True + True + + + + False + True + 0 + + + + + gtk-cut + True + True + True + Cut... + True + True + + + + False + True + 1 + + + + + gtk-delete + True + True + True + Delete... + True + True + + + + False + True + 4 + + + + + Trash + True + True + True + Move to Trash... + trashImage + True + + + + False + True + end + 3 + + + + + False + True + 1 + + + + + True + False + vertical + + + True + False + + + True + False + 15 + Folder + + + + + + True + True + 0 + + + + + True + False + 15 + File + + + + + + True + True + 1 + + + + + False + True + 0 + + + + + True + True + File/Folder + True + + + False + True + 1 + + + + + Create + True + True + True + Create File/Folder... + createImage + True + + + + False + True + 2 + + + + + gtk-paste + True + True + True + Paste... + True + True + + + + False + True + 3 + + + + + False + True + 2 + + + + + + diff --git a/user_config/usr/share/pytop/icons/archive.png b/user_config/usr/share/pytop/icons/archive.png new file mode 100644 index 0000000..7943e4e Binary files /dev/null and b/user_config/usr/share/pytop/icons/archive.png differ diff --git a/user_config/usr/share/pytop/icons/audio.png b/user_config/usr/share/pytop/icons/audio.png new file mode 100644 index 0000000..c010134 Binary files /dev/null and b/user_config/usr/share/pytop/icons/audio.png differ diff --git a/user_config/usr/share/pytop/icons/bin.png b/user_config/usr/share/pytop/icons/bin.png new file mode 100644 index 0000000..d6954e3 Binary files /dev/null and b/user_config/usr/share/pytop/icons/bin.png differ diff --git a/user_config/usr/share/pytop/icons/dir.png b/user_config/usr/share/pytop/icons/dir.png new file mode 100644 index 0000000..a9b5e9f Binary files /dev/null and b/user_config/usr/share/pytop/icons/dir.png differ diff --git a/user_config/usr/share/pytop/icons/doc.png b/user_config/usr/share/pytop/icons/doc.png new file mode 100644 index 0000000..f838826 Binary files /dev/null and b/user_config/usr/share/pytop/icons/doc.png differ diff --git a/user_config/usr/share/pytop/icons/pdf.png b/user_config/usr/share/pytop/icons/pdf.png new file mode 100644 index 0000000..9f40122 Binary files /dev/null and b/user_config/usr/share/pytop/icons/pdf.png differ diff --git a/user_config/usr/share/pytop/icons/presentation.png b/user_config/usr/share/pytop/icons/presentation.png new file mode 100644 index 0000000..3a339af Binary files /dev/null and b/user_config/usr/share/pytop/icons/presentation.png differ diff --git a/user_config/usr/share/pytop/icons/spreadsheet.png b/user_config/usr/share/pytop/icons/spreadsheet.png new file mode 100644 index 0000000..710efa6 Binary files /dev/null and b/user_config/usr/share/pytop/icons/spreadsheet.png differ diff --git a/user_config/usr/share/pytop/icons/text.png b/user_config/usr/share/pytop/icons/text.png new file mode 100644 index 0000000..2546fcd Binary files /dev/null and b/user_config/usr/share/pytop/icons/text.png differ diff --git a/user_config/usr/share/pytop/icons/trash.png b/user_config/usr/share/pytop/icons/trash.png new file mode 100644 index 0000000..c6514b9 Binary files /dev/null and b/user_config/usr/share/pytop/icons/trash.png differ diff --git a/user_config/usr/share/pytop/icons/video.png b/user_config/usr/share/pytop/icons/video.png new file mode 100644 index 0000000..55afa98 Binary files /dev/null and b/user_config/usr/share/pytop/icons/video.png differ diff --git a/user_config/usr/share/pytop/icons/web.png b/user_config/usr/share/pytop/icons/web.png new file mode 100644 index 0000000..17017ce Binary files /dev/null and b/user_config/usr/share/pytop/icons/web.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_128x128.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_128x128.png new file mode 100644 index 0000000..d565a6a Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_128x128.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_256x256.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_256x256.png new file mode 100644 index 0000000..29810bb Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_256x256.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_32x32.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_32x32.png new file mode 100644 index 0000000..ba53a44 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_32x32.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_64x64.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_64x64.png new file mode 100644 index 0000000..07eae0a Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_64x64.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_72x72.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_72x72.png new file mode 100644 index 0000000..221cca5 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_72x72.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_96x96.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_96x96.png new file mode 100644 index 0000000..74020a7 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon2_96x96.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_128x128.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_128x128.png new file mode 100644 index 0000000..f11fb1b Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_128x128.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_256x256.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_256x256.png new file mode 100644 index 0000000..f7b5320 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_256x256.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_32x32.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_32x32.png new file mode 100644 index 0000000..f5abcf9 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_32x32.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_64x64.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_64x64.png new file mode 100644 index 0000000..7250d7c Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_64x64.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_72x72.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_72x72.png new file mode 100644 index 0000000..2d4e7c1 Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_72x72.png differ diff --git a/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_96x96.png b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_96x96.png new file mode 100644 index 0000000..9ea489d Binary files /dev/null and b/user_config/usr/share/pytop/start_menu_icons/start_menu_icon_96x96.png differ diff --git a/user_config/usr/share/pytop/stylesheet.css b/user_config/usr/share/pytop/stylesheet.css new file mode 100644 index 0000000..d8263f5 --- /dev/null +++ b/user_config/usr/share/pytop/stylesheet.css @@ -0,0 +1,96 @@ +/* Menu css properties */ +.menu_scroller label { + font-size: 120%; +} + + +/* Grid css properties */ + +viewport, +treeview, +treeview > header, +notebook > stack, +notebook > header { + background-color: rgba(0, 0, 0, 0.24); +} + + +notebook > header { + background-color: rgba(0, 0, 0, 0.24); + border-color: rgba(0, 232, 255, 0.64); +} + +box, +iconview { + background-color: rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.2); +} + +treeview, +treeview.view { + background: rgba(0, 0, 0, 0.2); + background-color: rgba(0, 0, 0, 0.2); +} + +cell { + margin: 0em; + padding: 0em; + /* float: left; */ +} + +cell:focus { + outline-style: solid; + outline-color: rgba(0, 232, 255, 0.64); +} + + +/* Ivonview and children default color */ +.view { + background-color: rgba(0, 0, 0, 0.22); + color: #ebebeb; +} + + +/* Hover over color when not selected */ +.view:hover { + box-shadow: inset 0 0 0 9999px alpha(rgba(0, 232, 255, 0.64), 0.54); +} + +/* Handles the icon selection hover and selected hover color. */ +.view:selected, +.view:selected:hover { + box-shadow: inset 0 0 0 9999px rgba(15, 134, 13, 0.49); +} + +/* Rubberband coloring */ +.rubberband, +rubberband, +flowbox rubberband, +treeview.view rubberband, +.content-view rubberband, +.content-view .rubberband, +XfdesktopIconView.view .rubberband { + border: 1px solid #6c6c6c; + background-color: rgba(21, 158, 167, 0.57); +} + +XfdesktopIconView.view:active { + background-color: rgba(172, 102, 21, 1); +} + + +XfdesktopIconView.view { + border-radius: 4px; + background-color: transparent; + color: white; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} + +XfdesktopIconView.view:active { + box-shadow: none; + text-shadow: none; +} + +XfdesktopIconView.view .rubberband { + border-radius: 0; +}