diff --git a/linux-requirements.txt b/linux-requirements.txt
index d4fd345..756b651 100644
--- a/linux-requirements.txt
+++ b/linux-requirements.txt
@@ -1,25 +1,36 @@
+bcrypt==4.0.1
+certifi==2022.12.7
+charset-normalizer==3.0.1
click==7.1.2
-dnspython==2.1.0
-eventlet==0.30.1
+dnspython==1.16.0
email-validator==1.1.2
+eventlet==0.30.1
Flask==1.1.2
+Flask-Bcrypt==0.7.1
Flask-Login==0.5.0
flask-oidc==1.4.0
-Flask-Bcrypt==0.7.1
Flask-SQLAlchemy==2.4.4
+Flask-Uploads==0.2.1
Flask-WTF==0.14.3
greenlet==1.0.0
gunicorn==20.0.4
httplib2==0.19.0
+idna==3.4
itsdangerous==1.1.0
Jinja2==2.11.3
MarkupSafe==1.1.1
oauth2client==4.1.3
+Pillow==9.4.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
+pycairo==1.23.0
+PyGObject==3.42.2
pyparsing==2.4.7
+pyxdg==0.28
+requests==2.28.2
rsa==4.7
six==1.15.0
SQLAlchemy==1.3.23
+urllib3==1.26.14
Werkzeug==1.0.1
-WTForms==2.3.3
\ No newline at end of file
+WTForms==2.3.3
diff --git a/src/core/__builtins__.py b/src/core/__builtins__.py
new file mode 100644
index 0000000..30e4e11
--- /dev/null
+++ b/src/core/__builtins__.py
@@ -0,0 +1,94 @@
+# Python imports
+import os
+import builtins
+import threading
+import re
+import secrets
+
+# Lib imports
+from flask import session
+
+# Application imports
+from core import app
+from core.utils import Logger
+
+
+
+# NOTE: Threads WILL NOT die with parent's destruction.
+def threaded_wrapper(fn):
+ def wrapper(*args, **kwargs):
+ threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
+ return wrapper
+
+# NOTE: Threads WILL die with parent's destruction.
+def daemon_threaded_wrapper(fn):
+ def wrapper(*args, **kwargs):
+ threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
+ return wrapper
+
+def sizeof_fmt_def(num, suffix="B"):
+ for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
+ if abs(num) < 1024.0:
+ return f"{num:3.1f} {unit}{suffix}"
+ num /= 1024.0
+ return f"{num:.1f} Yi{suffix}"
+
+
+def _get_file_size(file):
+ return "4K" if isdir(file) else sizeof_fmt_def(os.path.getsize(file))
+
+
+
+# NOTE: Just reminding myself we can add to builtins two different ways...
+# __builtins__.update({"event_system": Builtins()})
+builtins.app_name = "WebFM"
+builtins.threaded = threaded_wrapper
+builtins.daemon_threaded = daemon_threaded_wrapper
+builtins.sizeof_fmt = sizeof_fmt_def
+builtins.get_file_size = _get_file_size
+builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
+builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
+builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
+builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
+builtins.logger = Logger().get_logger()
+
+
+
+# NOTE: Need threads defined befor instantiating
+from core.utils.shellfm.windows.controller import WindowController # Get file manager controller
+window_controllers = {}
+def _get_view():
+ controller = None
+ try:
+ controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
+ except Exception as e:
+ id = secrets.token_hex(16)
+ controller = WindowController()
+ view = controller.create_window().create_tab()
+
+ try:
+ view.ABS_THUMBS_PTH = app.config['ABS_THUMBS_PTH']
+ except Exception as e:
+ print("No ABS_THUMBS_PTH set by WebFM...")
+
+ try:
+ view.REMUX_FOLDER = app.config['REMUX_FOLDER']
+ except Exception as e:
+ print("No REMUX_FOLDER set by WebFM...")
+
+ try:
+ view.FFMPG_THUMBNLR = app.config['FFMPG_THUMBNLR']
+ except Exception as e:
+ print("No FFMPG_THUMBNLR set by WebFM...")
+
+ view.logger = logger
+
+ session['win_controller_id'] = id
+ window_controllers.update( {id: controller } )
+ controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
+
+ return controller
+
+
+
+builtins.get_view = _get_view
diff --git a/src/core/__init__.py b/src/core/__init__.py
index f7d8ac5..fb9ce5b 100644
--- a/src/core/__init__.py
+++ b/src/core/__init__.py
@@ -1,6 +1,4 @@
# Python imports
-import os
-
# Lib imports
from flask import Flask
@@ -10,20 +8,18 @@ from flask_oidc import OpenIDConnect
from flask_bcrypt import Bcrypt
from flask_login import current_user, login_user, logout_user, LoginManager
-
-# Apoplication imports
-from core.utils import Logger
-
-
-ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
app = Flask(__name__)
app.config.from_object("core.config.ProductionConfig")
# app.config.from_object("core.config.DevelopmentConfig")
+# Apoplication imports
+from .__builtins__ import *
+
+
+
oidc = OpenIDConnect(app)
login_manager = LoginManager(app)
bcrypt = Bcrypt(app)
-logger = Logger().get_logger()
def oidc_loggedin():
return oidc.user_loggedin
diff --git a/src/core/routes/CRUD.py b/src/core/routes/CRUD.py
new file mode 100644
index 0000000..52c7ffd
--- /dev/null
+++ b/src/core/routes/CRUD.py
@@ -0,0 +1,119 @@
+# Python imports
+import os
+import re
+
+# Lib imports
+from flask import request
+
+from flask_uploads import ALL
+from flask_uploads import configure_uploads
+from flask_uploads import UploadSet
+
+
+# App imports
+ # Get from __init__
+from core import app
+from core import db
+from core import Favorites
+from core import oidc
+
+from core.utils import MessageHandler # Get simple message processor
+
+json_message = MessageHandler()
+
+
+
+@app.route('/api/delete/<_hash>', methods=['GET', 'POST'])
+def delete_item(_hash = None):
+ if request.method == 'POST':
+ msg = "Log in with an Admin privlidged user to delete files!"
+ if not oidc.user_loggedin:
+ return json_message.create("danger", msg)
+ elif oidc.user_loggedin:
+ isAdmin = oidc.user_getfield("isAdmin")
+ if isAdmin != "yes" :
+ return json_message.create("danger", msg)
+
+ view = get_view()
+ folder = view.get_current_directory()
+ file = view.get_path_part_from_hash(_hash)
+ fpath = os.path.join(folder, file)
+ try:
+ msg = f"[Success] Deleted the file/folder -->: {file} !"
+ view.delete_file(fpath)
+ return json_message.create("success", msg)
+ except Exception as e:
+ msg = "[Error] Unable to delete the file/folder...."
+ return json_message.create("danger", msg)
+
+
+
+@app.route('/api/create/<_type>', methods=['GET', 'POST'])
+def create_item(_type = None):
+ if request.method == 'POST':
+ msg = "Log in with an Admin privlidged user to upload files!"
+ if not oidc.user_loggedin:
+ return json_message.create("danger", msg)
+ elif oidc.user_loggedin:
+ isAdmin = oidc.user_getfield("isAdmin")
+ if isAdmin != "yes" :
+ return json_message.create("danger", msg)
+
+ TYPE = _type.strip()
+ if not TYPE in ["dir", "file"]:
+ msg = "Couldn't handle action type for api create..."
+ return json_message.create("danger", msg)
+
+ FNAME = str(request.values['fname']).strip()
+ if not re.fullmatch(valid_fname_pat, FNAME):
+ msg = "A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters..."
+ return json_message.create("danger", msg)
+
+ try:
+ view = get_view()
+ folder = view.get_current_directory()
+ new_item = f"{folder}/{FNAME}"
+ view.create_file(new_item, TYPE)
+ except Exception as e:
+ print(repr(e))
+ msg = "Couldn't create file/folder. An unexpected error occured..."
+ return json_message.create("danger", msg)
+
+
+ msg = "[Success] created the file/dir..."
+ return json_message.create("success", msg)
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
+
+
+@app.route('/api/upload', methods=['GET', 'POST'])
+def upload():
+ if request.method == 'POST' and len(request.files) > 0:
+ msg = "Log in with an Admin privlidged user to upload files!"
+ if not oidc.user_loggedin:
+ return json_message.create("danger", msg)
+ elif oidc.user_loggedin:
+ isAdmin = oidc.user_getfield("isAdmin")
+ if isAdmin != "yes" :
+ return json_message.create("danger", msg)
+
+ view = get_view()
+ folder = view.get_current_directory()
+ UPLOADS_PTH = f'{folder}/'
+ files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH)
+ configure_uploads(app, files)
+
+ try:
+ for file in request.files:
+ files.save(request.files[file])
+ except Exception as e:
+ print(repr(e))
+ msg = "[Error] Failed to upload some or all of the file(s)..."
+ return json_message.create("danger", msg)
+
+ msg = "[Success] Uploaded file(s)..."
+ return json_message.create("success", msg)
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Favorites.py b/src/core/routes/Favorites.py
new file mode 100644
index 0000000..5f76298
--- /dev/null
+++ b/src/core/routes/Favorites.py
@@ -0,0 +1,71 @@
+# Python imports
+
+# Lib imports
+from flask import request
+
+# App imports
+from core import app
+from core import db
+from core import Favorites # Get from __init__
+from core.utils import MessageHandler # Get simple message processor
+
+
+json_message = MessageHandler()
+
+
+
+@app.route('/api/list-favorites', methods=['GET', 'POST'])
+def listFavorites():
+ if request.method == 'POST':
+ list = db.session.query(Favorites).all()
+ faves = []
+ for fave in list:
+ faves.append([fave.link, fave.id])
+
+ return json_message.faves_list(faves)
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
+
+@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
+def loadFavorite(_id):
+ if request.method == 'POST':
+ try:
+ ID = int(_id)
+ fave = db.session.query(Favorites).filter_by(id = ID).first()
+ view = get_view()
+ view.set_path_with_sub_path(fave.link)
+ return '{"refresh": "true"}'
+ except Exception as e:
+ print(repr(e))
+ msg = "Incorrect Favorites ID..."
+ return json_message.create("danger", msg)
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
+
+
+@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
+def manageFavorites(_action):
+ if request.method == 'POST':
+ ACTION = _action.strip()
+ view = get_view()
+ sub_path = view.get_current_sub_path()
+
+ if ACTION == "add":
+ fave = Favorites(link = sub_path)
+ db.session.add(fave)
+ msg = "Added to Favorites successfully..."
+ elif ACTION == "delete":
+ fave = db.session.query(Favorites).filter_by(link = sub_path).first()
+ db.session.delete(fave)
+ msg = "Deleted from Favorites successfully..."
+ else:
+ msg = "Couldn't handle action for favorites item..."
+ return json_message.create("danger", msg)
+
+ db.session.commit()
+ return json_message.create("success", msg)
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Images.py b/src/core/routes/Images.py
new file mode 100644
index 0000000..fe832d7
--- /dev/null
+++ b/src/core/routes/Images.py
@@ -0,0 +1,92 @@
+# Python imports
+import os
+import requests
+import shutil
+
+# Lib imports
+from flask import request
+
+
+# App imports
+ # Get from __init__
+from core import app
+
+from core.utils import MessageHandler # Get simple message processor
+from core.utils.tmdbscraper import scraper # Get media art scraper
+
+
+json_message = MessageHandler()
+tmdb = scraper.get_tmdb_scraper()
+
+
+@app.route('/api/get-background-poster-trailer', methods=['GET', 'POST'])
+def getPosterTrailer():
+ if request.method == 'GET':
+ info = {}
+ view = get_view()
+ dot_dots = view.get_dot_dots()
+
+ sub_path = view.get_current_sub_path()
+ file = sub_path.split("/")[-1]
+ trailer = None
+ if "(" in file and ")" in file:
+ title = file.split("(")[0].strip()
+ startIndex = file.index('(') + 1
+ endIndex = file.index(')')
+ date = file[startIndex:endIndex]
+
+ try:
+ video_data = tmdb.search(title, date)[0]
+ video_id = video_data["id"]
+ background_url = video_data["backdrop_path"]
+ background_pth = f"{view.get_current_directory()}/000.jpg"
+
+ tmdb_videos = tmdb.tmdbapi.get_movie(str(video_id), append_to_response="videos")["videos"]["results"]
+ for tmdb_video in tmdb_videos:
+ if "YouTube" in tmdb_video["site"]:
+ trailer_key = tmdb_video["key"]
+ trailer = f"https://www.youtube-nocookie.com/embed/{trailer_key}?start=0&autoplay=1";
+
+ if not trailer:
+ raise Exception("No key found. Defering to none...")
+ except Exception as e:
+ print("No trailer found...")
+ trailer = None
+
+ if not os.path.isfile(background_pth):
+ r = requests.get(background_url, stream = True)
+
+ if r.status_code == 200:
+ r.raw.decode_content = True
+ with open(background_pth,'wb') as f:
+ shutil.copyfileobj(r.raw, f)
+
+ view.load_directory()
+ print('Cover Background Image sucessfully retreived...')
+ else:
+ print('Cover Background Image Couldn\'t be retreived...')
+
+ info.update({'trailer': trailer})
+ info.update({'poster': background_url})
+
+ return info
+
+
+@app.route('/backgrounds', methods=['GET', 'POST'])
+def backgrounds():
+ files = []
+ data = os.listdir(BG_IMGS_PATH)
+ for file in data:
+ if file.lower().endswith(BG_FILE_TYPE):
+ files.append(file)
+
+ return json_message.backgrounds(files)
+
+@app.route('/api/get-thumbnails', methods=['GET', 'POST'])
+def getThumbnails():
+ if request.method == 'GET':
+ view = get_view()
+ return json_message.thumbnails( view.get_video_icons() )
+ else:
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Routes.py b/src/core/routes/Routes.py
index 505e341..e162009 100644
--- a/src/core/routes/Routes.py
+++ b/src/core/routes/Routes.py
@@ -1,65 +1,31 @@
# Python imports
-import os, json, secrets, requests, shutil, re
+import os
# Lib imports
-from flask import request, session, render_template, send_from_directory, redirect
-from flask_uploads import UploadSet, configure_uploads, ALL
-from flask_login import current_user
-
+from flask import redirect
+from flask import request
+from flask import render_template
+from flask import send_from_directory
# App imports
-from core import app, logger, oidc, db, Favorites, ROOT_FILE_PTH # Get from __init__
-from core.utils import MessageHandler # Get simple message processor
-from core.utils.shellfm import WindowController # Get file manager controller
-from core.utils.tmdbscraper import scraper # Get media art scraper
+ # Get from __init__
+from core import app
+from core import db
+from core import Favorites
+from core import oidc
+
+from core.utils import MessageHandler # Get simple message processor
-BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
-BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
-msgHandler = MessageHandler()
-tmdb = scraper.get_tmdb_scraper()
-window_controllers = {}
-# valid_fname_pat = re.compile(r"/^[a-zA-Z0-9-_\[\]\(\)| ]+$/")
-valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
+json_message = MessageHandler()
-def get_window_controller():
- controller = None
- try:
- controller = window_controllers[ session["win_controller_id"] ]
- except Exception as e:
- id = secrets.token_hex(16)
- controller = WindowController()
- view = controller.get_window(1).get_view(0)
-
- try:
- view.ABS_THUMBS_PTH = app.config['ABS_THUMBS_PTH']
- except Exception as e:
- ...
-
- try:
- view.REMUX_FOLDER = app.config['REMUX_FOLDER']
- except Exception as e:
- ...
-
- try:
- view.FFMPG_THUMBNLR = app.config['FFMPG_THUMBNLR']
- except Exception as e:
- ...
-
- view.logger = logger
-
- session['win_controller_id'] = id
- window_controllers.update( {id: controller } )
- controller = window_controllers[ session["win_controller_id"] ]
-
- return controller
@app.route('/', methods=['GET', 'POST'])
def home():
if request.method == 'GET':
- view = get_window_controller().get_window(1).get_view(0)
+ view = get_view()
_dot_dots = view.get_dot_dots()
_current_directory = view.get_current_directory()
return render_template('pages/index.html', current_directory = _current_directory, dot_dots = _dot_dots)
@@ -68,20 +34,11 @@ def home():
message = 'Must use GET request type...')
-@app.route('/backgrounds', methods=['GET', 'POST'])
-def backgrounds():
- files = []
- data = os.listdir(BG_IMGS_PATH)
- for file in data:
- if file.lower().endswith(BG_FILE_TYPE):
- files.append(file)
-
- return '{ "backgrounds": ' + json.dumps(files) + '}'
@app.route('/api/list-files/<_hash>', methods=['GET', 'POST'])
def listFiles(_hash = None):
if request.method == 'POST':
- view = get_window_controller().get_window(1).get_view(0)
+ view = get_view()
dot_dots = view.get_dot_dots()
if dot_dots[0][1] == _hash: # Refresh
@@ -92,91 +49,41 @@ def listFiles(_hash = None):
msg = "Log in with an Admin privlidged user to view the requested path!"
is_locked = view.is_folder_locked(_hash)
if is_locked and not oidc.user_loggedin:
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("danger", msg)
elif is_locked and oidc.user_loggedin:
isAdmin = oidc.user_getfield("isAdmin")
if isAdmin != "yes" :
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("danger", msg)
if dot_dots[0][1] != _hash and dot_dots[1][1] != _hash:
path = view.get_path_part_from_hash(_hash)
view.push_to_path(path)
error_msg = view.get_error_message()
- if error_msg != None:
+ if error_msg:
view.unset_error_message()
- return msgHandler.create_JSON_message("danger", error_msg)
-
-
- sub_path = view.get_current_sub_path()
- current_directory = sub_path.split("/")[-1]
- trailer = None
- if "(" in current_directory and ")" in current_directory:
- title = current_directory.split("(")[0].strip()
- startIndex = current_directory.index('(') + 1
- endIndex = current_directory.index(')')
- date = current_directory[startIndex:endIndex]
- video_data = tmdb.search(title, date)[0]
- video_id = video_data["id"]
- background_url = video_data["backdrop_path"]
- background_pth = f"{view.get_current_directory()}/000.jpg"
-
- try:
- tmdb_videos = tmdb.tmdbapi.get_movie(str(video_id), append_to_response="videos")["videos"]["results"]
- for tmdb_video in tmdb_videos:
- if "YouTube" in tmdb_video["site"]:
- trailer_key = tmdb_video["key"]
- trailer = f"https://www.youtube-nocookie.com/embed/{trailer_key}?start=0&autoplay=1";
-
- if not trailer:
- raise Exception("No key found. Defering to none...")
- except Exception as e:
- print("No trailer found...")
- trailer = None
-
- if not os.path.isfile(background_pth):
- r = requests.get(background_url, stream = True)
-
- if r.status_code == 200:
- r.raw.decode_content = True
- with open(background_pth,'wb') as f:
- shutil.copyfileobj(r.raw, f)
-
- view.load_directory()
- print('Cover Background Image sucessfully retreived...')
- else:
- print('Cover Background Image Couldn\'t be retreived...')
-
+ return json_message.create("danger", error_msg)
+ sub_path = view.get_current_sub_path()
files = view.get_files_formatted()
fave = db.session.query(Favorites).filter_by(link = sub_path).first()
in_fave = "true" if fave else "false"
-
files.update({'in_fave': in_fave})
- files.update({'trailer': trailer})
return files
else:
msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("danger", msg)
+
-@app.route('/api/get-posters', methods=['GET', 'POST'])
-def getPosters():
- if request.method == 'POST':
- view = get_window_controller().get_window(1).get_view(0)
- videos = view.get_videos()
- return videos
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
def fileManagerAction(_type, _hash = None):
- view = get_window_controller().get_window(1).get_view(0)
+ view = get_view()
if _type == "reset-path" and _hash == "None":
view.set_to_home()
msg = "Returning to home directory..."
- return msgHandler.create_JSON_message("success", msg)
+ return json_message.create("success", msg)
folder = view.get_current_directory()
file = view.get_path_part_from_hash(_hash)
@@ -195,7 +102,7 @@ def fileManagerAction(_type, _hash = None):
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
else:
msg = "Remuxing: Remux failed or took too long; please, refresh the page and try again..."
- return msgHandler.create_JSON_message("success", msg)
+ return json_message.create("success", msg)
if _type == "remux":
stream_target = view.remux_video(_hash, fpath)
@@ -205,158 +112,14 @@ def fileManagerAction(_type, _hash = None):
# Be aware of ordering!
msg = "Log in with an Admin privlidged user to do this action!"
if not oidc.user_loggedin:
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("danger", msg)
elif oidc.user_loggedin:
isAdmin = oidc.user_getfield("isAdmin")
if isAdmin != "yes" :
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("danger", msg)
if _type == "run-locally":
msg = "Opened media..."
view.open_file_locally(fpath)
- return msgHandler.create_JSON_message("success", msg)
-
- if _type == "delete":
- try:
- msg = f"[Success] Deleted the file/folder -->: {file} !"
- if os.path.isfile(fpath):
- os.unlink(fpath)
- else:
- shutil.rmtree(fpath)
- return msgHandler.create_JSON_message("success", msg)
- except Exception as e:
- msg = "[Error] Unable to delete the file/folder...."
- return msgHandler.create_JSON_message("danger", msg)
-
-
-@app.route('/api/list-favorites', methods=['GET', 'POST'])
-def listFavorites():
- if request.method == 'POST':
- list = db.session.query(Favorites).all()
- faves = []
- for fave in list:
- faves.append([fave.link, fave.id])
-
- return '{"faves_list":' + json.dumps(faves) + '}'
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
-
-@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
-def loadFavorite(_id):
- if request.method == 'POST':
- try:
- ID = int(_id)
- fave = db.session.query(Favorites).filter_by(id = ID).first()
- view = get_window_controller().get_window(1).get_view(0)
- view.set_path_with_sub_path(fave.link)
- return '{"refresh": "true"}'
- except Exception as e:
- print(repr(e))
- msg = "Incorrect Favorites ID..."
- return msgHandler.create_JSON_message("danger", msg)
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
-
-
-@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
-def manageFavorites(_action):
- if request.method == 'POST':
- ACTION = _action.strip()
- view = get_window_controller().get_window(1).get_view(0)
- sub_path = view.get_current_sub_path()
-
- if ACTION == "add":
- fave = Favorites(link = sub_path)
- db.session.add(fave)
- msg = "Added to Favorites successfully..."
- elif ACTION == "delete":
- fave = db.session.query(Favorites).filter_by(link = sub_path).first()
- db.session.delete(fave)
- msg = "Deleted from Favorites successfully..."
- else:
- msg = "Couldn't handle action for favorites item..."
- return msgHandler.create_JSON_message("danger", msg)
-
- db.session.commit()
- return msgHandler.create_JSON_message("success", msg)
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
-
-
-@app.route('/api/create/<_type>', methods=['GET', 'POST'])
-def create_item(_type = None):
- if request.method == 'POST':
- msg = "Log in with an Admin privlidged user to upload files!"
- if not oidc.user_loggedin:
- return msgHandler.create_JSON_message("danger", msg)
- elif oidc.user_loggedin:
- isAdmin = oidc.user_getfield("isAdmin")
- if isAdmin != "yes" :
- return msgHandler.create_JSON_message("danger", msg)
-
- TYPE = _type.strip()
- FNAME = str(request.values['fname']).strip()
-
- if not re.fullmatch(valid_fname_pat, FNAME):
- msg = "A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters..."
- return msgHandler.create_JSON_message("danger", msg)
-
- view = get_window_controller().get_window(1).get_view(0)
- folder = view.get_current_directory()
- new_item = f"{folder}/{FNAME}"
-
- try:
- if TYPE == "dir":
- os.mkdir(new_item)
- elif TYPE == "file":
- open(f"{new_item}.txt", 'a').close()
- else:
- msg = "Couldn't handle action type for api create..."
- return msgHandler.create_JSON_message("danger", msg)
- except Exception as e:
- print(repr(e))
- msg = "Couldn't create file/folder. An unexpected error occured..."
- return msgHandler.create_JSON_message("danger", msg)
-
-
- msg = "[Success] created the file/dir..."
- return msgHandler.create_JSON_message("success", msg)
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
-
-
-@app.route('/upload', methods=['GET', 'POST'])
-def upload():
- if request.method == 'POST' and len(request.files) > 0:
- msg = "Log in with an Admin privlidged user to upload files!"
- if not oidc.user_loggedin:
- return msgHandler.create_JSON_message("danger", msg)
- elif oidc.user_loggedin:
- isAdmin = oidc.user_getfield("isAdmin")
- if isAdmin != "yes" :
- return msgHandler.create_JSON_message("danger", msg)
-
- view = get_window_controller().get_window(1).get_view(0)
- folder = view.get_current_directory()
- UPLOADS_PTH = f'{folder}/'
- files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH)
- configure_uploads(app, files)
-
- for file in request.files:
- try:
- files.save(request.files[file])
- except Exception as e:
- print(repr(e))
- msg = "[Error] Failed to upload some or all of the file(s)..."
- return msgHandler.create_JSON_message("danger", msg)
-
- msg = "[Success] Uploaded file(s)..."
- return msgHandler.create_JSON_message("success", msg)
- else:
- msg = "Can't manage the request type..."
- return msgHandler.create_JSON_message("danger", msg)
+ return json_message.create("success", msg)
diff --git a/src/core/routes/__init__.py b/src/core/routes/__init__.py
index 058bca0..03728b9 100644
--- a/src/core/routes/__init__.py
+++ b/src/core/routes/__init__.py
@@ -1,4 +1,7 @@
+from . import Images
+from . import CRUD
from . import Routes
+from . import Favorites
from .pages import Flask_Login
from .pages import Flask_Register
from .pages import OIDC_Login
diff --git a/src/core/static/ffmpegthumbnailer b/src/core/static/ffmpegthumbnailer
deleted file mode 100755
index 0b6e6e2..0000000
Binary files a/src/core/static/ffmpegthumbnailer and /dev/null differ
diff --git a/src/core/static/js/ajax.js b/src/core/static/js/ajax.js
index d3f02c0..523b14f 100644
--- a/src/core/static/js/ajax.js
+++ b/src/core/static/js/ajax.js
@@ -5,9 +5,14 @@ const goHomeAjax = async (hash) => {
doAjax("api/file-manager-action/reset-path/None", data, "reset-path");
}
+const createItemAjax = async (type, fname) => {
+ const data = "fname=" + fname;
+ doAjax("api/create/" + type, data, "create-file");
+}
+
const deleteItemAjax = async (hash) => {
const data = "empty=NULL";
- doAjax("api/file-manager-action/delete/" + hash, data, "delete-file");
+ doAjax("api/delete/" + hash, data, "delete-file");
}
const listFilesAjax = async (hash) => {
diff --git a/src/core/static/js/post-ajax.js b/src/core/static/js/post-ajax.js
index dab7819..aa6cf74 100644
--- a/src/core/static/js/post-ajax.js
+++ b/src/core/static/js/post-ajax.js
@@ -51,18 +51,6 @@ const updateHTMLDirList = async (data) => {
let isInFaves = data.in_fave;
let background_image = (images[0]) ? images[0][0] : "";
- if (data.hasOwnProperty("trailer")) {
- let trailerBttn = document.getElementById("trailer-btn");
- let trailerLink = document.getElementById("trailer-link");
- if (data.trailer !== null) {
- trailerBttn.style.display = "";
- trailerLink.href = `javascript: showFile( "Trailer", "${data.trailer}", "", "trailer" )`;
- } else {
- trailerBttn.style.display = "none";
- trailerLink.href = "#";
- }
- }
-
document.getElementById("path").innerText = data.path_head;
// Setup background if there is a 000.* in selection
if (background_image.match(/000\.(jpg|png|gif)\b/) != null) {
@@ -80,5 +68,8 @@ const updateHTMLDirList = async (data) => {
else
tggl_faves_btn.classList.remove("btn-info");
+
renderFilesList(data.list);
+ loadBackgroundPoster();
+ loadThumbnails();
}
diff --git a/src/core/static/js/privileged-logic.js b/src/core/static/js/privileged-logic.js
index 34d6f83..654a8cf 100644
--- a/src/core/static/js/privileged-logic.js
+++ b/src/core/static/js/privileged-logic.js
@@ -42,12 +42,12 @@ const uploadFiles = (files = null) => {
file = files[i];
name = file.name;
data = createFormDataFiles([file]);
- doAjaxUpload('upload', data, name, "upload-file");
+ doAjaxUpload('api/upload', data, name, "upload-file");
}
} else { // Single upload...
data = createFormDataFiles(files);
name = files[0].name;
- doAjaxUpload('upload', data, name, "upload-file");
+ doAjaxUpload('api/upload', data, name, "upload-file");
}
}
diff --git a/src/core/static/js/react-ui-logic.js b/src/core/static/js/react-ui-logic.js
index ffe7225..8221959 100644
--- a/src/core/static/js/react-ui-logic.js
+++ b/src/core/static/js/react-ui-logic.js
@@ -69,7 +69,7 @@ class FilesList extends React.Component {
if (filetype === "video") {
card_header = name;
card_body =
-
+
{fsize}
;
} else if (filetype === "image") {
@@ -163,6 +163,7 @@ const renderFilesList = (data = null) => {
React.createElement(FilesList, data),
filesListElm
)
+
videoPlaylist = document.body.querySelectorAll('[ftype="video"][value="Open"]'); // With attributes ftype and value
}
diff --git a/src/core/static/js/ui-logic.js b/src/core/static/js/ui-logic.js
index b59bc45..ba658cc 100644
--- a/src/core/static/js/ui-logic.js
+++ b/src/core/static/js/ui-logic.js
@@ -212,6 +212,38 @@ const clearSearch = () => {
}
}
+const loadThumbnails = () => {
+ fetchData("api/get-thumbnails").then((data) => {
+ const cards_imgs = document.body.querySelectorAll('.card-image');
+ const thumbnails = data["thumbnails"]
+ for (var i = 0; i < cards_imgs.length; i++) {
+ cards_imgs[i].src = "static/imgs/thumbnails/" + thumbnails[i][0] + ".jpg?d=" + Date.now();
+ }
+ });
+
+}
+
+const loadBackgroundPoster = () => {
+ fetchData("api/get-background-poster-trailer").then((data) => {
+ if (data.hasOwnProperty("trailer")) {
+ let trailerBttn = document.getElementById("trailer-btn");
+ let trailerLink = document.getElementById("trailer-link");
+ if (data.trailer !== null) {
+ trailerBttn.style.display = "";
+ trailerLink.href = `javascript: showFile( "Trailer", "${data.trailer}", "", "trailer" )`;
+ } else {
+ trailerBttn.style.display = "none";
+ trailerLink.href = "#";
+ }
+
+ if (data.poster !== null) {
+ background_image = "api/file-manager-action/files/000.jpg?d=" + Date.now();
+ updateBackground(background_image, false);
+ }
+ }
+ });
+}
+
const updateBackground = (srcLink, isvideo = true) => {
try {
diff --git a/src/core/templates/modals/options-modal.html b/src/core/templates/modals/options-modal.html
index 1b33d2a..dab3cfa 100644
--- a/src/core/templates/modals/options-modal.html
+++ b/src/core/templates/modals/options-modal.html
@@ -55,7 +55,7 @@
-
+
diff --git a/src/core/utils/MessageHandler.py b/src/core/utils/MessageHandler.py
index 9bf6368..45d6e3a 100644
--- a/src/core/utils/MessageHandler.py
+++ b/src/core/utils/MessageHandler.py
@@ -1,6 +1,7 @@
-# Gtk imports
-
# Python imports
+import json
+
+# Lib imports
# Application imports
@@ -10,5 +11,14 @@ class MessageHandler:
print("MessageHandler initialized...")
- def create_JSON_message(self, type, text):
+ def create(self, type, text):
return '{"message": { "type": "' + type + '", "text": "' + text + '" } }'
+
+ def backgrounds(self, files):
+ return '{ "backgrounds": ' + json.dumps(files) + '}'
+
+ def thumbnails(self, files):
+ return '{ "thumbnails": ' + json.dumps(files) + '}'
+
+ def faves_list(self, faves):
+ return '{"faves_list":' + json.dumps(faves) + '}'
diff --git a/src/core/utils/shellfm/__init__.py b/src/core/utils/shellfm/__init__.py
index 0c8b591..803a268 100644
--- a/src/core/utils/shellfm/__init__.py
+++ b/src/core/utils/shellfm/__init__.py
@@ -1 +1,3 @@
-from .windows import WindowController
+"""
+ Root of ShellFM
+"""
diff --git a/src/core/utils/shellfm/windows/Window.py b/src/core/utils/shellfm/windows/Window.py
deleted file mode 100644
index 5284e08..0000000
--- a/src/core/utils/shellfm/windows/Window.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from .view import View
-
-
-class Window:
- def __init__(self):
- self.name = ""
- self.nickname = ""
- self.id = 0
- self.views = []
-
- def create_view(self):
- view = View()
- self.views.append(view)
-
- def pop_view(self):
- self.views.pop()
-
- def delete_view(self, index):
- del self.views[index]
-
- def get_view(self, index):
- return self.views[index]
diff --git a/src/core/utils/shellfm/windows/WindowController.py b/src/core/utils/shellfm/windows/WindowController.py
deleted file mode 100644
index 4c81aa0..0000000
--- a/src/core/utils/shellfm/windows/WindowController.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from . import Window
-
-
-class WindowController:
- def __init__(self):
- self.windows = []
- self.add_window()
-
- def get_window(self, win_id):
- for window in self.windows:
- if window.id == win_id:
- return window
-
- raise(f"No Window by ID {win_id} found!")
-
- def get_windows(self):
- return self.windows
-
- def add_window(self):
- window = Window()
- window.id = len(self.windows) + 1
- window.name = f"window_{window.id}"
- window.create_view()
- self.windows.append(window)
-
- def add_view_for_window(self, win_id):
- for window in self.windows:
- if window.id == win_id:
- window.create_view()
- break
-
- def pop_window(self):
- self.windows.pop()
-
- def delete_window_by_id(self, win_id):
- i = 0
- for window in self.windows:
- if window.id == win_id:
- self.window.remove(win_id)
- break
- i += 1
-
- def set_window_nickname(self, win_id = None, nickname = ""):
- for window in self.windows:
- if window.id == win_id:
- window.nickname = nickname
-
- def list_windows(self):
- for window in self.windows:
- print("\n[ Window ]")
- print("ID: {window.id}")
- print("Name: {window.name}")
- print("Nickname: {window.nickname}")
- print("View Count: {len(window.views)}")
-
-
- def list_views_from_window(self, win_id):
- for window in self.windows:
- if window.id == win_id:
- for view in window.views:
- print(view.files)
- break
-
- def return_views_from_window(self, win_id):
- for window in self.windows:
- if window.id == win_id:
- return window.views
diff --git a/src/core/utils/shellfm/windows/__init__.py b/src/core/utils/shellfm/windows/__init__.py
index cd9f6ce..12abfa0 100644
--- a/src/core/utils/shellfm/windows/__init__.py
+++ b/src/core/utils/shellfm/windows/__init__.py
@@ -1,2 +1,3 @@
-from .Window import Window
-from .WindowController import WindowController
+"""
+ Window module
+"""
diff --git a/src/core/utils/shellfm/windows/controller.py b/src/core/utils/shellfm/windows/controller.py
new file mode 100644
index 0000000..667d399
--- /dev/null
+++ b/src/core/utils/shellfm/windows/controller.py
@@ -0,0 +1,179 @@
+# Python imports
+import json
+from os import path
+
+# Lib imports
+
+# Application imports
+from .window import Window
+
+
+
+
+class WindowController:
+ def __init__(self):
+ USER_HOME: str = path.expanduser('~')
+ CONFIG_PATH: str = f"{USER_HOME}/.config/{app_name.lower()}"
+ self._session_file: srr = f"{CONFIG_PATH}/session.json"
+
+ self._event_sleep_time: int = 1
+ self._active_window_id: str = ""
+ self._active_tab_id: str = ""
+ self._windows: list = []
+
+
+ def set_wid_and_tid(self, wid: int, tid: int) -> None:
+ self._active_window_id = str(wid)
+ self._active_tab_id = str(tid)
+
+ def get_active_wid_and_tid(self) -> list:
+ return self._active_window_id, self._active_tab_id
+
+ def create_window(self) -> Window:
+ window = Window()
+ window.set_nickname(f"window_{str(len(self._windows) + 1)}")
+ self._windows.append(window)
+ return window
+
+
+ def add_tab_for_window(self, win_id: str) -> None:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ return window.create_tab()
+
+ def add_tab_for_window_by_name(self, name: str) -> None:
+ for window in self._windows:
+ if window.get_name() == name:
+ return window.create_tab()
+
+ def add_tab_for_window_by_nickname(self, nickname: str) -> None:
+ for window in self._windows:
+ if window.get_nickname() == nickname:
+ return window.create_tab()
+
+ def pop_window(self) -> None:
+ self._windows.pop()
+
+ def delete_window_by_id(self, win_id: str) -> None:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ self._windows.remove(window)
+ break
+
+ def delete_window_by_name(self, name: str) -> str:
+ for window in self._windows:
+ if window.get_name() == name:
+ self._windows.remove(window)
+ break
+
+ def delete_window_by_nickname(self, nickname: str) -> str:
+ for window in self._windows:
+ if window.get_nickname() == nickname:
+ self._windows.remove(window)
+ break
+
+ def get_window_by_id(self, win_id: str) -> Window:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ return window
+
+ raise(f"No Window by ID {win_id} found!")
+
+ def get_window_by_name(self, name: str) -> Window:
+ for window in self._windows:
+ if window.get_name() == name:
+ return window
+
+ raise(f"No Window by Name {name} found!")
+
+ def get_window_by_nickname(self, nickname: str) -> Window:
+ for window in self._windows:
+ if window.get_nickname() == nickname:
+ return window
+
+ raise(f"No Window by Nickname {nickname} found!")
+
+ def get_window_by_index(self, index: int) -> Window:
+ return self._windows[index]
+
+ def get_all_windows(self) -> list:
+ return self._windows
+
+
+ def set_window_nickname(self, win_id: str = None, nickname: str = "") -> None:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ window.set_nickname(nickname)
+
+ def list_windows(self) -> None:
+ print("\n[ ---- Windows ---- ]\n")
+ for window in self._windows:
+ print(f"\nID: {window.get_id()}")
+ print(f"Name: {window.get_name()}")
+ print(f"Nickname: {window.get_nickname()}")
+ print(f"Is Hidden: {window.is_hidden()}")
+ print(f"Tab Count: {window.get_tabs_count()}")
+ print("\n-------------------------\n")
+
+
+
+ def list_files_from_tabs_of_window(self, win_id: str) -> None:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ window.list_files_from_tabs()
+ break
+
+ def get_tabs_count(self, win_id: str) -> int:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ return window.get_tabs_count()
+
+ def get_tabs_from_window(self, win_id: str) -> list:
+ for window in self._windows:
+ if window.get_id() == win_id:
+ return window.get_all_tabs()
+
+
+
+
+ def unload_tabs_and_windows(self) -> None:
+ for window in self._windows:
+ window.get_all_tabs().clear()
+
+ self._windows.clear()
+
+ def save_state(self, session_file: str = None) -> None:
+ if not session_file:
+ session_file = self._session_file
+
+ if len(self._windows) > 0:
+ windows = []
+ for window in self._windows:
+ tabs = []
+ for tab in window.get_all_tabs():
+ tabs.append(tab.get_current_directory())
+
+ windows.append(
+ {
+ 'window':{
+ "ID": window.get_id(),
+ "Name": window.get_name(),
+ "Nickname": window.get_nickname(),
+ "isHidden": f"{window.is_hidden()}",
+ 'tabs': tabs
+ }
+ }
+ )
+
+ with open(session_file, 'w') as outfile:
+ json.dump(windows, outfile, separators=(',', ':'), indent=4)
+ else:
+ raise Exception("Window data corrupted! Can not save session!")
+
+ def get_state_from_file(self, session_file: str = None) -> dict:
+ if not session_file:
+ session_file = self._session_file
+
+ if path.isfile(session_file):
+ with open(session_file) as infile:
+ return json.load(infile)
diff --git a/src/core/utils/shellfm/windows/tabs/__init__.py b/src/core/utils/shellfm/windows/tabs/__init__.py
new file mode 100644
index 0000000..657c1fe
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/__init__.py
@@ -0,0 +1,3 @@
+"""
+ Tabs module
+"""
diff --git a/src/core/utils/shellfm/windows/tabs/icons/__init__.py b/src/core/utils/shellfm/windows/tabs/icons/__init__.py
new file mode 100644
index 0000000..771182f
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/__init__.py
@@ -0,0 +1,3 @@
+"""
+ Icons module
+"""
diff --git a/src/core/utils/shellfm/windows/tabs/icons/icon.py b/src/core/utils/shellfm/windows/tabs/icons/icon.py
new file mode 100644
index 0000000..c06e2be
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/icon.py
@@ -0,0 +1,192 @@
+# Python imports
+import os
+from os.path import isfile
+import hashlib
+import threading
+
+# Lib imports
+import gi
+gi.require_version('GdkPixbuf', '2.0')
+from gi.repository import GLib
+from gi.repository import Gio
+from gi.repository import GdkPixbuf
+
+try:
+ from PIL import Image as PImage
+except ModuleNotFoundError as e:
+ PImage = None
+
+# Application imports
+from .mixins.videoiconmixin import VideoIconMixin
+from .mixins.meshsiconmixin import MeshsIconMixin
+from .mixins.desktopiconmixin import DesktopIconMixin
+
+
+
+class IconException(Exception):
+ ...
+
+
+
+class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin):
+ 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 = self._get_system_thumbnail_gtk_thread(full_path, self.sys_icon_wh[0])
+
+ if file.lower().endswith(self.fmeshs): # 3D Mesh icon
+ ...
+ if file.lower().endswith(self.fvideos): # Video icon
+ thumbnl = self.create_video_thumbnail(full_path)
+ elif file.lower().endswith(self.fimages): # Image Icon
+ thumbnl = self.create_scaled_image(full_path)
+ elif file.lower().endswith( (".blend",) ): # Blender icon
+ thumbnl = self.create_blender_thumbnail(full_path)
+ elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing
+ thumbnl = self.find_thumbnail_from_desktop_file(full_path)
+
+ if not thumbnl:
+ raise IconException("No known icons found.")
+
+ return thumbnl
+ except IconException:
+ ...
+
+ return self.get_generic_icon()
+
+ def create_blender_thumbnail(self, full_path, returnHashInstead=False):
+ try:
+ path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
+ if not path_exists:
+ self.generate_blender_thumbnail(full_path, hash_img_path)
+
+ if returnHashInstead:
+ return img_hash, hash_img_path
+
+ return self.create_scaled_image(hash_img_path, self.video_icon_wh)
+ except IconException as e:
+ print("Blender thumbnail generation issue:")
+ print( repr(e) )
+
+ return None
+
+ def create_video_thumbnail(self, full_path, scrub_percent = "65%", replace=False, returnHashInstead=False):
+ try:
+ path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
+ if path_exists and replace:
+ os.remove(hash_img_path)
+ path_exists = False
+
+ if not path_exists:
+ self.generate_video_thumbnail(full_path, hash_img_path, scrub_percent)
+
+ if returnHashInstead:
+ return img_hash, hash_img_path
+
+ return self.create_scaled_image(hash_img_path, self.video_icon_wh)
+ except IconException as e:
+ print("Image/Video thumbnail generation issue:")
+ print( repr(e) )
+
+ return None
+
+
+ def create_scaled_image(self, full_path, wxh = None):
+ if not wxh:
+ wxh = self.video_icon_wh
+
+ if full_path:
+ try:
+ if full_path.lower().endswith(".gif"):
+ return GdkPixbuf.PixbufAnimation.new_from_file(full_path) \
+ .get_static_image() \
+ .scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
+ elif full_path.lower().endswith(".webp") and PImage:
+ return self.image2pixbuf(full_path, wxh)
+
+ return GdkPixbuf.Pixbuf.new_from_file_at_scale(full_path, wxh[0], wxh[1], True)
+ except IconException as e:
+ print("Image Scaling Issue:")
+ print( repr(e) )
+
+ return None
+
+ def create_from_file(self, full_path):
+ try:
+ return GdkPixbuf.Pixbuf.new_from_file(full_path)
+ except IconException as e:
+ print("Image from file Issue:")
+ print( repr(e) )
+
+ return None
+
+ def _get_system_thumbnail_gtk_thread(self, full_path, size):
+ def _call_gtk_thread(event, result):
+ result.append( self.get_system_thumbnail(full_path, size) )
+ event.set()
+
+ result = []
+ event = threading.Event()
+ GLib.idle_add(_call_gtk_thread, event, result)
+ event.wait()
+ return result[0]
+
+
+ def get_system_thumbnail(self, full_path, size):
+ try:
+ gio_file = Gio.File.new_for_path(full_path)
+ info = gio_file.query_info('standard::icon' , 0, None)
+ icon = info.get_icon().get_names()[0]
+ data = settings.get_icon_theme().lookup_icon(icon , size , 0)
+
+ if data:
+ icon_path = data.get_filename()
+ return GdkPixbuf.Pixbuf.new_from_file(icon_path)
+
+ raise IconException("No system icon found...")
+ except IconException:
+ ...
+
+ return None
+
+ def get_generic_icon(self):
+ return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON)
+
+ def generate_hash_and_path(self, full_path):
+ img_hash = self.fast_hash(full_path)
+ hash_img_path = f"{self.ABS_THUMBS_PTH}/{img_hash}.jpg"
+ path_exists = True if isfile(hash_img_path) else False
+
+ return path_exists, img_hash, hash_img_path
+
+
+ def fast_hash(self, filename, hash_factory=hashlib.md5, chunk_num_blocks=128, i=1):
+ h = hash_factory()
+ with open(filename,'rb') as f:
+ f.seek(0, 2)
+ mid = int(f.tell() / 2)
+ f.seek(mid, 0)
+
+ while chunk := f.read(chunk_num_blocks*h.block_size):
+ h.update(chunk)
+ if (i == 12):
+ break
+
+ i += 1
+
+ return h.hexdigest()
+
+ def image2pixbuf(self, full_path, wxh):
+ """Convert Pillow image to GdkPixbuf"""
+ im = PImage.open(full_path)
+ data = im.tobytes()
+ data = GLib.Bytes.new(data)
+ w, h = im.size
+
+ pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
+ False, 8, w, h, w * 3)
+
+ return pixbuf.scale_simple(wxh[0], wxh[1], 2) # BILINEAR = 2
diff --git a/src/core/utils/shellfm/windows/tabs/icons/mixins/__init__.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/__init__.py
new file mode 100644
index 0000000..b6bd8ae
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/mixins/__init__.py
@@ -0,0 +1,3 @@
+"""
+ Icons mixins module
+"""
diff --git a/src/core/utils/shellfm/windows/tabs/icons/mixins/desktopiconmixin.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/desktopiconmixin.py
new file mode 100644
index 0000000..c1f6be0
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/mixins/desktopiconmixin.py
@@ -0,0 +1,76 @@
+# Python imports
+import os
+from os.path import isfile
+import subprocess
+import hashlib
+
+# Lib imports
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+from gi.repository import Gio
+
+# Application imports
+from .xdg.DesktopEntry import DesktopEntry
+
+
+
+
+class DesktopIconMixin:
+ def find_thumbnail_from_desktop_file(self, full_path):
+ try:
+ xdgObj = DesktopEntry(full_path)
+ icon = xdgObj.getIcon()
+ alt_icon_path = ""
+
+ if "steam" in icon:
+ return self.get_steam_img(xdgObj)
+ elif os.path.exists(icon):
+ return self.create_scaled_image(icon, self.sys_icon_wh)
+ else:
+ pixbuf, alt_icon_path = self.get_icon_from_traversal(icon)
+ if pixbuf:
+ return pixbuf
+
+ 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 get_steam_img(self, xdgObj):
+ name = xdgObj.getName()
+ file_hash = hashlib.sha256(str.encode(name)).hexdigest()
+ hash_img_pth = f"{self.STEAM_ICONS_PTH}/{file_hash}.jpg"
+
+ if not isfile(hash_img_pth):
+ exec_str = xdgObj.getExec()
+ parts = exec_str.split("steam://rungameid/")
+ id = parts[len(parts) - 1]
+ imageLink = f"{self.STEAM_CDN_URL}{id}/header.jpg"
+ proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink])
+ proc.wait()
+
+ return self.create_scaled_image(hash_img_pth, self.video_icon_wh)
+
+ def get_icon_from_traversal(self, icon):
+ gio_icon = Gio.Icon.new_for_string(icon)
+ gicon = Gtk.Image.new_from_gicon(gio_icon, 32)
+ pixbuf = gicon.get_pixbuf()
+
+ alt_icon_path = ""
+ for dir in self.ICON_DIRS:
+ alt_icon_path = self.traverse_icons_folder(dir, icon)
+ if alt_icon_path != "":
+ break
+
+ return pixbuf, alt_icon_path
+
+ def traverse_icons_folder(self, path, icon):
+ for (dirpath, dirnames, filenames) in os.walk(path):
+ for file in filenames:
+ appNM = "application-x-" + icon
+ if icon in file or appNM in file:
+ return f"{dirpath}/{file}"
diff --git a/src/core/utils/shellfm/windows/tabs/icons/mixins/meshsiconmixin.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/meshsiconmixin.py
new file mode 100644
index 0000000..0d62636
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/mixins/meshsiconmixin.py
@@ -0,0 +1,17 @@
+# Python imports
+import subprocess
+
+# Lib imports
+
+# Application imports
+
+
+
+
+class MeshsIconMixin:
+ def generate_blender_thumbnail(self, full_path, hash_img_path):
+ try:
+ proc = subprocess.Popen([self.BLENDER_THUMBNLR, full_path, hash_img_path])
+ proc.wait()
+ except Exception as e:
+ self.logger.debug(repr(e))
diff --git a/src/core/utils/shellfm/windows/tabs/icons/mixins/videoiconmixin.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/videoiconmixin.py
new file mode 100644
index 0000000..60fd16b
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/mixins/videoiconmixin.py
@@ -0,0 +1,55 @@
+# Python imports
+import subprocess
+
+# Lib imports
+
+# Application imports
+
+
+
+
+class VideoIconMixin:
+ def generate_video_thumbnail(self, full_path, hash_img_path, scrub_percent = "65%"):
+ try:
+ proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", scrub_percent, "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_path])
+ proc.wait()
+ except Exception as e:
+ self.logger.debug(repr(e))
+ self.ffprobe_generate_video_thumbnail(full_path, hash_img_path)
+
+
+ def ffprobe_generate_video_thumbnail(self, full_path, hash_img_path):
+ 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_path]
+ 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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/BaseDirectory.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/BaseDirectory.py
new file mode 100644
index 0000000..a7c31b1
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Config.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Config.py
new file mode 100644
index 0000000..3f5d654
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/DesktopEntry.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/DesktopEntry.py
new file mode 100644
index 0000000..803993e
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Exceptions.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Exceptions.py
new file mode 100644
index 0000000..7096b61
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/IconTheme.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/IconTheme.py
new file mode 100644
index 0000000..2ff3c05
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/IniFile.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/IniFile.py
new file mode 100644
index 0000000..74ab858
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Locale.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Locale.py
new file mode 100644
index 0000000..d0a70d2
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Menu.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Menu.py
new file mode 100644
index 0000000..fcf1ac1
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/MenuEditor.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/MenuEditor.py
new file mode 100644
index 0000000..2c68515
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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("""
+
+
+""" % 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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Mime.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/Mime.py
new file mode 100644
index 0000000..60c4efd
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/RecentFiles.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/RecentFiles.py
new file mode 100644
index 0000000..fbe608c
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/__init__.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/__init__.py
new file mode 100644
index 0000000..b5a117e
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/icons/mixins/xdg/util.py b/src/core/utils/shellfm/windows/tabs/icons/mixins/xdg/util.py
new file mode 100644
index 0000000..1637aa5
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/icons/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/core/utils/shellfm/windows/tabs/path.py b/src/core/utils/shellfm/windows/tabs/path.py
new file mode 100644
index 0000000..0322db5
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/path.py
@@ -0,0 +1,64 @@
+# Python imports
+import os
+
+# Lib imports
+
+# Application imports
+
+
+
+
+class Path:
+ def get_home(self) -> str:
+ return os.path.expanduser("~") + self.subpath
+
+ def get_path(self) -> str:
+ return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
+
+ def get_path_list(self) -> list:
+ return self.path
+
+ def push_to_path(self, dir: str):
+ self.path.append(dir)
+ self.load_directory()
+
+ def pop_from_path(self) -> None:
+ try:
+ self.path.pop()
+
+ if not self.go_past_home:
+ if self.get_home() not in self.get_path():
+ self.set_to_home()
+
+ self.load_directory()
+ except Exception as e:
+ pass
+
+ def set_path(self, path: str) -> bool:
+ if path == self.get_path():
+ return False
+
+ if os.path.isdir(path):
+ self.path = list( filter(None, path.replace("\\", "/").split('/')) )
+ self.load_directory()
+ return True
+
+ return False
+
+ def set_path_with_sub_path(self, sub_path: str) -> bool:
+ path = os.path.join(self.get_home(), sub_path)
+ if path == self.get_path():
+ return False
+
+ if os.path.isdir(path):
+ self.path = list( filter(None, path.replace("\\", "/").split('/')) )
+ self.load_directory()
+ return True
+
+ return False
+
+ def set_to_home(self) -> None:
+ home = os.path.expanduser("~") + self.subpath
+ path = list( filter(None, home.replace("\\", "/").split('/')) )
+ self.path = path
+ self.load_directory()
diff --git a/src/core/utils/shellfm/windows/tabs/tab.py b/src/core/utils/shellfm/windows/tabs/tab.py
new file mode 100644
index 0000000..6a05f28
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/tab.py
@@ -0,0 +1,288 @@
+# Python imports
+import os
+import hashlib
+import re
+from os import listdir
+from os.path import isdir
+from os.path import isfile
+from os.path import join
+from random import randint
+
+# Lib imports
+
+# Application imports
+from .utils.settings import Settings
+from .utils.launcher import Launcher
+from .utils.filehandler import FileHandler
+from .icons.icon import Icon
+from .path import Path
+
+
+try:
+ get_file_size("/")
+except Exception as e:
+ import os
+
+ def sizeof_fmt_def(num, suffix="B"):
+ for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
+ if abs(num) < 1024.0:
+ return f"{num:3.1f} {unit}{suffix}"
+ num /= 1024.0
+ return f"{num:.1f} Yi{suffix}"
+
+ def _get_file_size(file):
+ return "4K" if isdir(file) else sizeof_fmt_def(os.path.getsize(file))
+
+ get_file_size = _get_file_size
+
+
+
+class Tab(Settings, FileHandler, Launcher, Icon, Path):
+ def __init__(self):
+ self.logger = None
+ self._id_length: int = 10
+
+ self._id: str = ""
+ self._wid: str = None
+ self.error_message: str = None
+ self._dir_watcher = None
+ self._hide_hidden: bool = self.HIDE_HIDDEN_FILES
+ self._files: list = []
+ self._dirs: list = []
+ self._vids: list = []
+ self._images: list = []
+ self._desktop: list = []
+ self._ungrouped: list = []
+ self._hidden: list = []
+
+ self._generate_id()
+ self.set_to_home()
+
+ def load_directory(self) -> None:
+ path = self.get_path()
+ self._dirs = []
+ self._vids = []
+ self._images = []
+ self._desktop = []
+ self._ungrouped = []
+ self._hidden = []
+ self._files = []
+
+ if not isdir(path):
+ self._set_error_message("Path can not be accessed.")
+ self.set_to_home()
+ return ""
+
+ for f in listdir(path):
+ file = join(path, f)
+ if self._hide_hidden:
+ if f.startswith('.'):
+ self._hidden.append(f)
+ continue
+
+ if isfile(file):
+ lowerName = file.lower()
+ if lowerName.endswith(self.fvideos):
+ self._vids.append(f)
+ elif lowerName.endswith(self.fimages):
+ self._images.append(f)
+ elif lowerName.endswith((".desktop",)):
+ self._desktop.append(f)
+ else:
+ self._ungrouped.append(f)
+ else:
+ self._dirs.append(f)
+
+ self._dirs.sort(key=self._natural_keys)
+ self._vids.sort(key=self._natural_keys)
+ self._images.sort(key=self._natural_keys)
+ self._desktop.sort(key=self._natural_keys)
+ self._ungrouped.sort(key=self._natural_keys)
+
+ self._files = self._dirs + self._vids + self._images + self._desktop + self._ungrouped
+
+ def is_folder_locked(self, hash):
+ if self.lock_folder:
+ path_parts = self.get_path().split('/')
+ file = self.get_path_part_from_hash(hash)
+
+ # Insure chilren folders are locked too.
+ lockedFolderInPath = False
+ for folder in self.locked_folders:
+ if folder in path_parts:
+ lockedFolderInPath = True
+ break
+
+ return (file in self.locked_folders or lockedFolderInPath)
+ else:
+ return False
+
+
+ def get_not_hidden_count(self) -> int:
+ return len(self._files) + \
+ len(self._dirs) + \
+ len(self._vids) + \
+ len(self._images) + \
+ len(self._desktop) + \
+ len(self._ungrouped)
+
+ def get_hidden_count(self) -> int:
+ return len(self._hidden)
+
+ def get_files_count(self) -> int:
+ return len(self._files)
+
+ def get_path_part_from_hash(self, hash: str) -> str:
+ files = self.get_files()
+ file = None
+
+ for f in files:
+ if hash == f[1]:
+ file = f[0]
+ break
+
+ return file
+
+ def get_files_formatted(self) -> dict:
+ files = self._hash_set(self._files),
+ dirs = self._hash_set(self._dirs),
+ videos = self.get_videos(),
+ images = self._hash_set(self._images),
+ desktops = self._hash_set(self._desktop),
+ ungrouped = self._hash_set(self._ungrouped)
+ hidden = self._hash_set(self._hidden)
+
+ return {
+ 'path_head': self.get_path(),
+ 'list': {
+ 'files': files,
+ 'dirs': dirs,
+ 'videos': videos,
+ 'images': images,
+ 'desktops': desktops,
+ 'ungrouped': ungrouped,
+ 'hidden': hidden
+ }
+ }
+
+ def get_video_icons(self) -> list:
+ data = []
+ dir = self.get_current_directory()
+ for file in self._vids:
+ img_hash, hash_img_path = self.create_video_thumbnail(full_path=f"{dir}/{file}", returnHashInstead=True)
+ data.append([img_hash, hash_img_path])
+
+ return data
+
+ def get_pixbuf_icon_str_combo(self):
+ data = []
+ dir = self.get_current_directory()
+ for file in self._files:
+ icon = self.create_icon(dir, file).get_pixbuf()
+ data.append([icon, file])
+
+ return data
+
+ def get_gtk_icon_str_combo(self) -> list:
+ data = []
+ dir = self.get_current_directory()
+ for file in self._files:
+ icon = self.create_icon(dir, file)
+ data.append([icon, file[0]])
+
+ return data
+
+ def get_current_directory(self) -> str:
+ return self.get_path()
+
+ def get_current_sub_path(self) -> str:
+ path = self.get_path()
+ home = f"{self.get_home()}/"
+ return path.replace(home, "")
+
+ def get_end_of_path(self) -> str:
+ parts = self.get_current_directory().split("/")
+ size = len(parts)
+ return parts[size - 1]
+
+
+ def set_hiding_hidden(self, state: bool) -> None:
+ self._hide_hidden = state
+
+ def is_hiding_hidden(self) -> bool:
+ return self._hide_hidden
+
+ def get_dot_dots(self) -> list:
+ return self._hash_set(['.', '..'])
+
+ def get_files(self) -> list:
+ return self._hash_set(self._files)
+
+ def get_dirs(self) -> list:
+ return self._hash_set(self._dirs)
+
+ def get_videos(self) -> list:
+ return self._hash_set(self._vids)
+
+ def get_images(self) -> list:
+ return self._hash_set(self._images)
+
+ def get_desktops(self) -> list:
+ return self._hash_set(self._desktop)
+
+ def get_ungrouped(self) -> list:
+ return self._hash_set(self._ungrouped)
+
+ def get_hidden(self) -> list:
+ return self._hash_set(self._hidden)
+
+ def get_id(self) -> str:
+ return self._id
+
+ def set_wid(self, _wid: str) -> None:
+ self._wid = _wid
+
+ def get_wid(self) -> str:
+ return self._wid
+
+ def set_dir_watcher(self, watcher):
+ self._dir_watcher = watcher
+
+ def get_dir_watcher(self):
+ return self._dir_watcher
+
+ def get_error_message(self):
+ return self.error_message
+
+ def unset_error_message(self):
+ self.error_message = None
+
+ def _atoi(self, text):
+ return int(text) if text.isdigit() else text
+
+ def _natural_keys(self, text):
+ return [ self._atoi(c) for c in re.split('(\d+)',text) ]
+
+ def _hash_text(self, text) -> str:
+ return hashlib.sha256(str.encode(text)).hexdigest()[:18]
+
+ def _hash_set(self, arry: list) -> list:
+ path = self.get_current_directory()
+ data = []
+ for arr in arry:
+ file = f"{path}/{arr}"
+ size = get_file_size(file)
+ data.append([arr, self._hash_text(arr), size])
+ return data
+
+
+ def _random_with_N_digits(self, n: int) -> int:
+ range_start = 10**(n-1)
+ range_end = (10**n)-1
+ return randint(range_start, range_end)
+
+ def _generate_id(self) -> str:
+ self._id = str(self._random_with_N_digits(self._id_length))
+
+ def _set_error_message(self, text: str):
+ self.error_message = text
diff --git a/src/core/utils/shellfm/windows/tabs/utils/__init__.py b/src/core/utils/shellfm/windows/tabs/utils/__init__.py
new file mode 100644
index 0000000..a8e5edd
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/utils/__init__.py
@@ -0,0 +1,3 @@
+"""
+ Utils module
+"""
diff --git a/src/core/utils/shellfm/windows/tabs/utils/filehandler.py b/src/core/utils/shellfm/windows/tabs/utils/filehandler.py
new file mode 100644
index 0000000..750ed2c
--- /dev/null
+++ b/src/core/utils/shellfm/windows/tabs/utils/filehandler.py
@@ -0,0 +1,87 @@
+# Python imports
+import os
+import shutil
+
+# Lib imports
+
+# Application imports
+
+
+
+
+class FileHandler:
+ def create_file(self, nFile, type):
+ try:
+ if type == "dir":
+ os.mkdir(nFile)
+ elif type == "file":
+ open(nFile, 'a').close()
+ except Exception as e:
+ print("An error occured creating the file/dir:")
+ print(repr(e))
+ return False
+
+ return True
+
+ def update_file(self, oFile, nFile):
+ try:
+ print(f"Renaming: {oFile} --> {nFile}")
+ os.rename(oFile, nFile)
+ except Exception as e:
+ print("An error occured renaming the file:")
+ print(repr(e))
+ return False
+
+ return True
+
+ def delete_file(self, toDeleteFile):
+ try:
+ print(f"Deleting: {toDeleteFile}")
+ if os.path.exists(toDeleteFile):
+ if os.path.isfile(toDeleteFile):
+ os.remove(toDeleteFile)
+ elif os.path.isdir(toDeleteFile):
+ shutil.rmtree(toDeleteFile)
+ else:
+ print("An error occured deleting the file:")
+ return False
+ else:
+ print("The folder/file does not exist")
+ return False
+ except Exception as e:
+ print("An error occured deleting the file:")
+ print(repr(e))
+ return False
+
+ return True
+
+ def move_file(self, fFile, tFile):
+ try:
+ print(f"Moving: {fFile} --> {tFile}")
+ if os.path.exists(fFile) and not os.path.exists(tFile):
+ if not tFile.endswith("/"):
+ tFile += "/"
+
+ shutil.move(fFile, tFile)
+ else:
+ print("The folder/file does not exist")
+ return False
+ except Exception as e:
+ print("An error occured moving the file:")
+ print(repr(e))
+ return False
+
+ return True
+
+ def copy_file(self,fFile, tFile, symlinks=False, ignore=None):
+ try:
+ if os.path.isdir(fFile):
+ shutil.copytree(fFile, tFile, symlinks, ignore)
+ else:
+ shutil.copy2(fFile, tFile)
+ except Exception as e:
+ print("An error occured copying the file:")
+ print(repr(e))
+ return False
+
+ return True
diff --git a/src/core/utils/shellfm/windows/view/utils/Launcher.py b/src/core/utils/shellfm/windows/tabs/utils/launcher.py
similarity index 71%
rename from src/core/utils/shellfm/windows/view/utils/Launcher.py
rename to src/core/utils/shellfm/windows/tabs/utils/launcher.py
index cc09d61..ffe88de 100644
--- a/src/core/utils/shellfm/windows/view/utils/Launcher.py
+++ b/src/core/utils/shellfm/windows/tabs/utils/launcher.py
@@ -1,13 +1,15 @@
-# System import
-import os, subprocess, threading
-
+# Python imports
+import os
+import subprocess
+import shlex
# Lib imports
-
# Apoplication imports
+
+
class Launcher:
def open_file_locally(self, file):
lowerName = file.lower()
@@ -26,37 +28,41 @@ class Launcher:
command = [self.music_app, file]
elif lowerName.endswith(self.foffice):
command = [self.office_app, file]
+ elif lowerName.endswith(self.fcode):
+ command = [self.code_app, file]
elif lowerName.endswith(self.ftext):
command = [self.text_app, file]
elif lowerName.endswith(self.fpdf):
command = [self.pdf_app, file]
- else:
+ elif lowerName.endswith("placeholder-until-i-can-get-a-use-pref-fm-flag"):
command = [self.file_manager_app, file]
+ else:
+ command = ["xdg-open", file]
- self.logger.debug(command)
+ self.execute(command)
+
+
+ def execute(self, command, start_dir=os.getenv("HOME"), use_shell=False):
+ self.logger.debug(command)
+ subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=True, stdout=None, stderr=None, close_fds=True)
+
+ # TODO: Return stdout and in handlers along with subprocess instead of sinking to null
+ def execute_and_return_thread_handler(self, command, start_dir=os.getenv("HOME"), use_shell=False):
DEVNULL = open(os.devnull, 'w')
- subprocess.Popen(command, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True)
-
-
- def create_stream(self, hash, file):
- # ffmpeg -re -stream_loop -1 -i "