import sys import traceback import os import json import time import webbrowser import urllib.parse from datetime import date import logging import logging.handlers from pathlib import Path from typing import Union from functools import wraps, cached_property from tempfile import gettempdir from .launcher import Launcher from .browser import Browser from .settings import Settings PLUGIN_MANIFEST = 'plugin.json' FLOW_LAUNCHER_DIR_NAME = "FlowLauncher" SCOOP_FLOW_LAUNCHER_DIR_NAME = "flow-launcher" WOX_DIR_NAME = "Wox" FLOW_API = 'Flow.Launcher' WOX_API = 'Wox' APP_DIR = None USER_DIR = None LOCALAPPDATA = Path(os.getenv('LOCALAPPDATA')) APPDATA = Path(os.getenv('APPDATA')) FILE_PATH = os.path.dirname(os.path.abspath(__file__)) CURRENT_WORKING_DIR = Path().cwd() LAUNCHER_NOT_FOUND_MSG = f"Unable to locate Launcher directory\nCurrent working directory: {CURRENT_WORKING_DIR}" launcher_dir = None path = CURRENT_WORKING_DIR if SCOOP_FLOW_LAUNCHER_DIR_NAME.lower() in str(path).lower(): launcher_name = SCOOP_FLOW_LAUNCHER_DIR_NAME API = FLOW_API elif FLOW_LAUNCHER_DIR_NAME.lower() in str(path).lower(): launcher_name = FLOW_LAUNCHER_DIR_NAME API = FLOW_API elif WOX_DIR_NAME.lower() in str(path).lower(): launcher_name = WOX_DIR_NAME API = WOX_API else: raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) while True: if len(path.parts) == 1: raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) if path.joinpath('Settings').exists(): USER_DIR = path if USER_DIR.name == 'UserData': APP_DIR = USER_DIR.parent elif str(CURRENT_WORKING_DIR).startswith(str(APPDATA)): APP_DIR = LOCALAPPDATA.joinpath(launcher_name) else: raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) break path = path.parent APP_ICONS = APP_DIR.joinpath("Images") ICON_APP = APP_DIR.joinpath('app.png') ICON_APP_ERROR = APP_DIR.joinpath(APP_ICONS, 'app_error.png') ICON_BROWSER = APP_DIR.joinpath(APP_ICONS, 'browser.png') ICON_CALCULATOR = APP_DIR.joinpath(APP_ICONS, 'calculator.png') ICON_CANCEL = APP_DIR.joinpath(APP_ICONS, 'cancel.png') ICON_CLOSE = APP_DIR.joinpath(APP_ICONS, 'close.png') ICON_CMD = APP_DIR.joinpath(APP_ICONS, 'cmd.png') ICON_COLOR = APP_DIR.joinpath('color.png') ICON_CONTROL_PANEL = APP_DIR.joinpath('ControlPanel.png') ICON_COPY = APP_DIR.joinpath('copy.png') ICON_DELETE_FILE_FOLDER = APP_DIR.joinpath('deletefilefolder.png') ICON_DISABLE = APP_DIR.joinpath('disable.png') ICON_DOWN = APP_DIR.joinpath('down.png') ICON_EXE = APP_DIR.joinpath('exe.png') ICON_FILE = APP_DIR.joinpath('file.png') ICON_FIND = APP_DIR.joinpath('find.png') ICON_FOLDER = APP_DIR.joinpath('folder.png') ICON_HISTORY = APP_DIR.joinpath('history.png') ICON_IMAGE = APP_DIR.joinpath('image.png') ICON_LOCK = APP_DIR.joinpath('lock.png') ICON_LOGOFF = APP_DIR.joinpath('logoff.png') ICON_OK = APP_DIR.joinpath('ok.png') ICON_OPEN = APP_DIR.joinpath('open.png') ICON_PICTURES = APP_DIR.joinpath('pictures.png') ICON_PLUGIN = APP_DIR.joinpath('plugin.png') ICON_PROGRAM = APP_DIR.joinpath('program.png') ICON_RECYCLEBIN = APP_DIR.joinpath('recyclebin.png') ICON_RESTART = APP_DIR.joinpath('restart.png') ICON_SEARCH = APP_DIR.joinpath('search.png') ICON_SETTINGS = APP_DIR.joinpath('settings.png') ICON_SHELL = APP_DIR.joinpath('shell.png') ICON_SHUTDOWN = APP_DIR.joinpath('shutdown.png') ICON_SLEEP = APP_DIR.joinpath('sleep.png') ICON_UP = APP_DIR.joinpath('up.png') ICON_UPDATE = APP_DIR.joinpath('update.png') ICON_URL = APP_DIR.joinpath('url.png') ICON_USER = APP_DIR.joinpath('user.png') ICON_WARNING = APP_DIR.joinpath('warning.png') ICON_WEB_SEARCH = APP_DIR.joinpath('web_search.png') ICON_WORK = APP_DIR.joinpath('work.png') class Flox(Launcher): def __init_subclass__(cls, api=API, app_dir=APP_DIR, user_dir=USER_DIR): cls._debug = False cls.appdir = APP_DIR cls.user_dir = USER_DIR cls.api = api cls._start = time.time() cls._results = [] cls._settings = None cls.font_family = '/Resources/#Segoe Fluent Icons' cls.issue_item_title = 'Report Issue' cls.issue_item_subtitle = 'Report this issue to the developer' @cached_property def browser(self): return Browser(self.app_settings) def exception(self, exception): self.exception_item(exception) self.issue_item(exception) def _query(self, query): self.args = query.lower() self.query(query) def _context_menu(self, data): self.context_menu(data) def exception_item(self, exception): self.add_item( title=exception.__class__.__name__, subtitle=str(exception), icon=ICON_APP_ERROR, method=self.change_query, dont_hide=True ) def issue_item(self, e): trace = ''.join(traceback.format_exception(type(e), value=e, tb=e.__traceback__)).replace('\n', '%0A') self.add_item( title=self.issue_item_title, subtitle=self.issue_item_subtitle, icon=ICON_BROWSER, method=self.create_github_issue, parameters=[e.__class__.__name__, trace], ) def create_github_issue(self, title, trace, log=None): url = self.manifest['Website'] if 'github' in url.lower(): issue_body = f"Please+type+any+relevant+information+here%0A%0A%0A%0A%0A%0A%3Cdetails open%3E%3Csummary%3ETrace+Log%3C%2Fsummary%3E%0A%3Cp%3E%0A%0A%60%60%60%0A{trace}%0A%60%60%60%0A%3C%2Fp%3E%0A%3C%2Fdetails%3E" url = f"{url}/issues/new?title={title}&body={issue_body}" webbrowser.open(url) def add_item(self, title:str, subtitle:str='', icon:str=None, method:Union[str, callable]=None, parameters:list=None, context:list=None, glyph:str=None, score:int=0, **kwargs): icon = icon or self.icon if not Path(icon).is_absolute(): icon = Path(self.plugindir, icon) item = { "Title": str(title), "SubTitle": str(subtitle), "IcoPath": str(icon), "ContextData": context, "Score": score, "JsonRPCAction": {} } auto_complete_text = kwargs.pop("auto_complete_text", None) item["AutoCompleteText"] = auto_complete_text or f'{self.user_keyword} {title}'.replace('* ', '') if method: item['JsonRPCAction']['method'] = getattr(method, "__name__", method) item['JsonRPCAction']['parameters'] = parameters or [] item['JsonRPCAction']['dontHideAfterAction'] = kwargs.pop("dont_hide", False) if glyph: item['Glyph'] = {} item['Glyph']['Glyph'] = glyph font_family = kwargs.pop("font_family", self.font_family) if font_family.startswith("#"): font_family = str(Path(self.plugindir).joinpath(font_family)) item['Glyph']['FontFamily'] = font_family for kw in kwargs: item[kw] = kwargs[kw] self._results.append(item) return self._results[-1] @cached_property def plugindir(self): potential_paths = [ os.path.abspath(os.getcwd()), os.path.dirname(os.path.abspath(os.path.dirname(__file__))) ] for path in potential_paths: while True: if os.path.exists(os.path.join(path, PLUGIN_MANIFEST)): return path elif os.path.ismount(path): return os.getcwd() path = os.path.dirname(path) @cached_property def manifest(self): with open(os.path.join(self.plugindir, PLUGIN_MANIFEST), 'r', encoding='utf-8') as f: return json.load(f) @cached_property def id(self): return self.manifest['ID'] @cached_property def icon(self): return self.manifest['IcoPath'] @cached_property def action_keyword(self): return self.manifest['ActionKeyword'] @cached_property def version(self): return self.manifest['Version'] @cached_property def appdata(self): # Userdata should be up two directories from plugin root return os.path.dirname(os.path.dirname(self.plugindir)) @property def app_settings(self): with open(os.path.join(self.appdata, 'Settings', 'Settings.json'), 'r', encoding='utf-8') as f: return json.load(f) @property def query_search_precision(self): return self.app_settings.get('QuerySearchPrecision', 'Regular') @cached_property def user_keywords(self): return self.app_settings['PluginSettings']['Plugins'].get(self.id, {}).get('UserKeywords', [self.action_keyword]) @cached_property def user_keyword(self): return self.user_keywords[0] @cached_property def appicon(self, icon): return os.path.join(self.appdir, 'images', icon + '.png') @property def applog(self): today = date.today().strftime('%Y-%m-%d') file = f"{today}.txt" return os.path.join(self.appdata, 'Logs', self.appversion, file) @cached_property def appversion(self): return os.path.basename(self.appdir).replace('app-', '') @cached_property def logfile(self): file = "plugin.log" return os.path.join(self.plugindir, file) @cached_property def logger(self): logger = logging.getLogger('') formatter = logging.Formatter( '%(asctime)s %(levelname)s (%(filename)s): %(message)s', datefmt='%H:%M:%S') logfile = logging.handlers.RotatingFileHandler( self.logfile, maxBytes=1024 * 2024, backupCount=1) logfile.setFormatter(formatter) logger.addHandler(logfile) logger.setLevel(logging.WARNING) return logger def logger_level(self, level): if level == "info": self.logger.setLevel(logging.INFO) elif level == "debug": self.logger.setLevel(logging.DEBUG) elif level == "warning": self.logger.setLevel(logging.WARNING) elif level == "error": self.logger.setLevel(logging.ERROR) elif level == "critical": self.logger.setLevel(logging.CRITICAL) @cached_property def api(self): launcher = os.path.basename(os.path.dirname(self.appdir)) if launcher == 'FlowLauncher': return FLOW_API else: return WOX_API @cached_property def name(self): return self.manifest['Name'] @cached_property def author(self): return self.manifest['Author'] @cached_property def settings_path(self): dirname = self.name setting_file = "Settings.json" return os.path.join(self.appdata, 'Settings', 'Plugins', dirname, setting_file) @cached_property def settings(self): if not os.path.exists(os.path.dirname(self.settings_path)): os.mkdir(os.path.dirname(self.settings_path)) return Settings(self.settings_path) def browser_open(self, url): self.browser.open(url) @cached_property def python_dir(self): return self.app_settings["PluginSettings"]["PythonDirectory"] def log(self): return self.logger