diff --git a/.archive/inhibit_sleep/__init__.py b/.archive/inhibit_sleep/__init__.py
new file mode 100644
index 00000000..76829305
--- /dev/null
+++ b/.archive/inhibit_sleep/__init__.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
+
+"""
+Provides an item 'Inhibit sleep' which can be used to temporarily disable system suspension.
+
+This is a prototype using `systemd-inhibit`. A sophisticated implementation would probably use the systemd D-Bus \
+interface documented [here](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html).
+"""
+
+from albert import *
+from subprocess import Popen, TimeoutExpired
+
+md_iid = '2.3'
+md_version = '1.2'
+md_name = 'Inhibit sleep'
+md_description = 'Inhibit system sleep mode.'
+md_license = "MIT"
+md_url = 'https://github.com/albertlauncher/python/tree/main/inhibit_sleep'
+md_authors = "@manuelschneid3r"
+md_bin_dependencies = ['systemd-inhibit', "sleep"]
+
+
+class Plugin(PluginInstance, GlobalQueryHandler):
+
+ def __init__(self):
+ PluginInstance.__init__(self)
+ GlobalQueryHandler.__init__(
+ self, self.id, self.name, self.description,
+ defaultTrigger='is '
+ )
+ self.proc = None
+
+ def finalize(self):
+ if self.proc:
+ self.toggle()
+
+ def toggle(self):
+ if self.proc:
+ self.proc.terminate()
+ try:
+ self.proc.wait(timeout=1)
+ except TimeoutExpired:
+ self.proc.kill()
+ self.proc = None
+ else:
+ self.proc = Popen(["systemd-inhibit",
+ "--what=idle:sleep", "--who=Albert", "--why=User",
+ "sleep", "infinity"])
+ info(str(self.proc))
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ 'widget_properties': {
+ 'textFormat': 'Qt::MarkdownText'
+ }
+ }
+ ]
+
+ def handleGlobalQuery(self, query):
+ stripped = query.string.strip().lower()
+ if stripped in "inhibit sleep":
+ return [
+ RankItem(
+ StandardItem(
+ id=md_name,
+ text=md_name,
+ subtext=f"{'Enable' if self.proc else 'Disable'} sleep mode",
+ iconUrls=[f"gen:?text=💤"],
+ actions=[Action("inhibit", "Toggle", self.toggle)]
+ ),
+ len(stripped)/len(md_name))
+
+ ]
+ return []
diff --git a/mathematica_eval/__init__.py b/.archive/mathematica_eval/__init__.py
similarity index 100%
rename from mathematica_eval/__init__.py
rename to .archive/mathematica_eval/__init__.py
diff --git a/timer/__init__.py b/.archive/timer/__init__.py
similarity index 88%
rename from timer/__init__.py
rename to .archive/timer/__init__.py
index ab49cb5c..5a22e160 100644
--- a/timer/__init__.py
+++ b/.archive/timer/__init__.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
+# # Copyright (c) 2018-2024 Manuel Schneider
+# # Copyright (c) 2020 Andreas Dominik Preikschat
"""
Takes arguments in the form of '`[[hrs:]mins:]secs [name]`'. Empty fields resolve to `0`. \
@@ -17,13 +19,13 @@
from albert import *
-md_iid = '2.0'
-md_version = "1.7"
+md_iid = '2.3'
+md_version = "1.8"
md_name = "Timer"
md_description = "Set up timers"
-md_license = "BSD-2"
-md_url = "https://github.com/albertlauncher/python/tree/master/timer"
-md_maintainers = ["@manuelschneid3r", "@googol42", "@uztnus"]
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/timer"
+md_authors = ["@manuelschneid3r", "@googol42"]
class Timer(threading.Timer):
@@ -46,7 +48,7 @@ def __init__(self):
description=md_description,
synopsis='[[hrs:]mins:]secs [name]',
defaultTrigger='timer ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
self.iconUrls = [f"file:{Path(__file__).parent}/time.svg"]
self.soundPath = Path(__file__).parent / "bing.wav"
self.timers = []
@@ -71,6 +73,15 @@ def onTimerTimeout(self, timer):
)
self.deleteTimer(timer)
+ def configWidget(self):
+ return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ 'widget_properties': { 'textFormat': 'Qt::MarkdownText' }
+ }
+ ]
+
def handleTriggerQuery(self, query):
if not query.isValid:
return
diff --git a/vpn/__init__.py b/.archive/vpn/__init__.py
similarity index 77%
rename from vpn/__init__.py
rename to .archive/vpn/__init__.py
index 7c7bc02d..f529bc93 100644
--- a/vpn/__init__.py
+++ b/.archive/vpn/__init__.py
@@ -1,17 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 janeklb
+# Copyright (c) 2023 Bierchermuesli
+# Copyright (c) 2020-2024 Manuel Schneider
+
import subprocess
from collections import namedtuple
from albert import *
-md_iid = '2.0'
-md_version = "1.4"
-md_id = "vpn"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "VPN"
md_description = "Manage NetworkManager VPN connections"
md_license = "MIT"
-md_url = "https://github.com/albertlauncher/python"
-md_maintainers = ["@Bierchermuesli"]
-md_credits = ["@janeklb"]
+md_url = "https://github.com/albertlauncher/python/tree/main/vpn"
+md_authors = ["@janeklb", "@Bierchermuesli", "@manuelschneid3r"]
md_bin_dependencies = ["nmcli"]
@@ -20,12 +23,11 @@ class Plugin(PluginInstance, TriggerQueryHandler):
VPNConnection = namedtuple('VPNConnection', ['name', 'connected'])
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='vpn ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ def defaultTrigger(self):
+ return "vpn "
def getVPNConnections(self):
consStr = subprocess.check_output(
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 4b860dc1..00000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: Bug report
-about: Please check the existing issues and discuss your issue in the chats before creating a new one.
----
-
-
-
-#### Description
-A brief description of the issue. If applicable, add screenshots.
-
-#### Expected behavior
-A brief description of what you expected to happen
-
-#### Steps to reproduce
-Steps to reproduce the behavior
-
-#### Source
-e.g. ppa:name, repository, built from source, etc …
-
-#### Debug output
-
-
-
-```
-Output of `albert -d` when run in a terminal
-```
-
-
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index bf083087..00000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-blank_issues_enabled: false
-contact_links:
- - name: Start a discussion
- url: https://github.com/orgs/albertlauncher/discussions/new/choose
- about: Post the link to the discussion in the chats to get more attention.
- - name: Chat on Discord
- url: https://discord.gg/t8G2EkvRZh
- about: Bridged community chat.
- - name: Chat on Telegram
- url: https://telegram.me/albert_launcher_community
- about: Bridged community chat.
- - name: Chat on on IRC (libera#albertlauncher)
- url: https://web.libera.chat/#albertlauncher
- about: Bridged community chat.
diff --git a/.gitignore b/.gitignore
index 1d4436e8..bef49833 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
__pycache__
/.idea
/.vscode
-/.venv
\ No newline at end of file
+/.venv
+albert.pyi
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..4742ff5a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,16 @@
+## How to contribute to this repository
+
+### Do you have an issue?
+
+* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/albertlauncher/albert/issues).
+* Create a new issue using the templates provided.
+* Ping the authors of the related plugin.
+
+### Do you want to contribute code?
+
+* Add a copyright notice, otherwise the code is in public domain.
+* You agree to publish your contribution under the MIT license.
+* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
+* Changes that do not add anything substantial to the stability, functionality, or testability will generally not be accepted.
+
+Thanks! :heart:
diff --git a/README.md b/README.md
index 81e3ce5b..95b590ae 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,3 @@
-### This is the official repository for python plugins.
+# ⚠️ ARCHIVED ⚠️
-This repository is shipped with albert. If you want to have bleeding edge plugins or share your work clone the repository. Check the [docs on Python plugins](https://github.com/albertlauncher/plugins/blob/master/python/README.md). To install the plugins in user space type the following in your terminal:
-
-```shell
-git clone https://github.com/albertlauncher/python.git ~/.local/share/albert/python/plugins
-```
-
-Credits go to our contributors
-
-
-
-
+The plugins in this repository have been moved to [dedicated repositories](https://github.com/orgs/albertlauncher/repositories) using `history_to_submodules.sh`.
diff --git a/albert.pyi b/albert.pyi
deleted file mode 100644
index 3283f1d7..00000000
--- a/albert.pyi
+++ /dev/null
@@ -1,418 +0,0 @@
-"""
-
-# Albert Python interface v2.1
-
-
-The Python interface is a subset of the internal C++ interface exposed to Python with some minor adjustments. A Python
-plugin is required to contain the mandatory metadata and a plugin class, both described below. To get started read the
-top level classes and function names in this file. Most of them are self explanatory. In case of questions see the C++
-documentation at https://albertlauncher.github.io/reference/namespacealbert.html
-
-
-## Mandatory metadata variables
-
-md_iid: str | Interface version (.)
-md_version: str | Plugin version (.)
-md_name: str | Human readable name
-md_description: str | A brief, imperative description. (Like "Launch apps" or "Open files")
-
-
-## Optional metadata variables:
-
-md_id | Identifier overwrite. [a-zA-Z0-9_]. Defaults to module name.
-__doc__ | The docstring of the module is used as long description/readme of the extension.
-md_license: str | Short form e.g. BSD-2-Clause or GPL-3.0
-md_url: str | Browsable source, issues etc
-md_maintainers: [str|List(str)] | Active maintainer(s). Preferrably using mentionable Github usernames.
-md_bin_dependencies: [str|List(str)] | Required executable(s). Have to match the name of the executable in $PATH.
-md_lib_dependencies: [str|List(str)] | Required Python package(s). Have to match the PyPI package name.
-md_credits: [str|List(str)] | Third party credit(s) and license notes
-
-
-## The Plugin class
-
-The plugin class is the entry point for a Python plugin. It is instantiated on plugin initialization and has to subclass
-PluginInstance. Implement extension(s) by subclassing _one_ extension class (TriggerQueryHandler etc…) provided by the
-built-in `albert` module and pass the list of your extensions to the PluginInstance init function. Due to the
-differences in type systems multiple inheritance of extensions is not supported. (Python does not support virtual
-inheritance, which is used in the C++ space to inherit from 'Extension'). For more details see
-
-"""
-
-
-from abc import abstractmethod, ABC
-from enum import Enum
-from typing import Any
-from typing import Callable
-from typing import List
-from typing import Optional
-from typing import Union
-from typing import overload
-
-
-class PluginInstance(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_plugin_instance.html"""
-
- def __init__(self, extensions: List[Extension] = []):
- ...
-
- @property
- def id(self) -> str:
- ...
-
- @property
- def name(self) -> str:
- ...
-
- @property
- def description(self) -> str:
- ...
-
- @property
- def cacheLocation(self) -> pathlib.Path:
- ...
-
- @property
- def configLocation(self) -> pathlib.Path:
- ...
-
- @property
- def dataLocation(self) -> pathlib.Path:
- ...
-
- @property
- def extensions(self) -> List[Extension]:
- ...
-
- def initialize(self):
- ...
-
- def finalize(self):
- ...
-
- def readConfig(self, key: str, type: type[str|int|float|bool]) -> str|int|float|bool|None:
- """
- Read a config value from the Albert settings.
- Note: Due to limitations of QSettings on some platforms the type may be lost, therefore the type expected has to
- be passed.
-
- Returns:
- The requested value or None if the value does not exist or errors occurred.
- """
-
- def writeConfig(self, key: str, value: str|int|float|bool):
- """Write a config value to the Albert settings."""
-
- def configWidget(self) -> List[dict]:
- """
- Descriptive config widget factory.
-
- Define a static config widget using a list of dicts, each defining a row in the resulting form layout.
- Supported keys are:
-
- - 'property' The name of the property that will be set upon editing the forms.
- - 'label' The text displayed in front of the the editor widget.
- - 'type' The type of editor widget used. See the supported types below.
- - 'items' The list of strings used for 'type': 'combobox'.
- - 'widget_properties' Dict setting the widget properties of the editor widget.
- See the links along the editor types below (but also the base classes) to find available properties.
- Note that due to the restricted type conversion only properties of type str|int|float|bool are settable.
-
- The supported editor widget types are:
-
- * 'checkbox' for boolean properties (See https://doc.qt.io/qt-6/qcheckbox.html)
- * 'spinbox' for integer properties. (See https://doc.qt.io/qt-6/qspinbox.html)
- * 'doublespinbox' for float properties. (See https://doc.qt.io/qt-6/qdoublespinbox.html)
- * 'lineedit' if you want the user to input any string. (See https://doc.qt.io/qt-6/qlineedit.html)
- * 'combobox' if you want the user to choose a string. (See https://doc.qt.io/qt-6/qcombobox.html)
-
- Returns:
- A list of dicts, describing a form layout as defined above.
- """
-
-class Action:
- """https://albertlauncher.github.io/reference/classalbert_1_1_action.html"""
-
- def __init__(self,
- id: str,
- text: str,
- callable: Callable):
- ...
-
-
-class Item(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_item.html"""
-
- @abstractmethod
- def id(self) -> str:
- ...
-
- @abstractmethod
- def text(self) -> str:
- ...
-
- @abstractmethod
- def subtext(self) -> str:
- ...
-
- @abstractmethod
- def inputActionText(self) -> str:
- ...
-
- @abstractmethod
- def iconUrls(self) -> List[str]:
- """See https://albertlauncher.github.io/reference/classalbert_1_1_icon_provider.html"""
-
- @abstractmethod
- def actions(self) -> List[Action]:
- ...
-
-
-class StandardItem(Item):
- """https://albertlauncher.github.io/reference/structalbert_1_1_standard_item.html"""
-
- def __init__(self,
- id: str = '',
- text: str = '',
- subtext: str = '',
- iconUrls: List[str] = [],
- actions: List[Action] = [],
- inputActionText: Optional[str] = ''):
- ...
-
- id: str
- text: str
- subtext: str
- iconUrls: List[str]
- actions: List[Action]
- inputActionText: str
-
-
-class Extension(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_extension.html"""
-
- @property
- def id(self) -> str:
- ...
-
- @property
- def name(self) -> str:
- ...
-
- @property
- def description(self) -> str:
- ...
-
-
-class FallbackHandler(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_fallback_handler.html"""
-
- @abstractmethod
- def fallbacks(self, query: str ) ->List[Item]:
- ...
-
-
-class TriggerQuery(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_trigger_query_handler_1_1_trigger_query.html"""
-
- @property
- def trigger(self) -> str:
- ...
-
- @property
- def string(self) -> str:
- ...
-
- @property
- def isValid(self) -> bool:
- ...
-
- @overload
- def add(self, item: Item):
- ...
-
- @overload
- def add(self, item: List[Item]):
- ...
-
-
-class TriggerQueryHandler(Extension):
- """https://albertlauncher.github.io/reference/classalbert_1_1_trigger_query_handler.html"""
-
- def __init__(self,
- id: str,
- name: str,
- description: str,
- synopsis: str = '',
- defaultTrigger: str = f'{id} ',
- allowTriggerRemap: str = true,
- supportsFuzzyMatching: bool = False):
- ...
-
- @property
- def synopsis(self) -> str:
- ...
-
- @property
- def trigger(self) -> str:
- ...
-
- @property
- def defaultTrigger(self) -> str:
- ...
-
- @property
- def allowTriggerRemap(self) -> bool:
- ...
-
- @property
- def supportsFuzzyMatching(self) -> bool:
- ...
-
- @property
- def fuzzyMatching(self) -> bool:
- ...
-
- @fuzzyMatching.setter
- def setFuzzyMatching(self, enabled: bool):
- ...
-
- @abstractmethod
- def handleTriggerQuery(self, query: TriggerQuery):
- ...
-
-
-class RankItem:
- """https://albertlauncher.github.io/reference/classalbert_1_1_rank_item.html"""
-
- def __init__(self, item: Item, score: float):
- ...
-
- item: Item
- score: float
-
-
-class GlobalQuery(ABC):
- """https://albertlauncher.github.io/reference/classalbert_1_1_global_query_handler_1_1_global_query.html"""
-
- @property
- def string(self) -> str:
- ...
-
- @property
- def isValid(self) -> bool:
- ...
-
-
-class GlobalQueryHandler(TriggerQueryHandler):
- """https://albertlauncher.github.io/reference/classalbert_1_1_global_query_handler.html"""
-
- def __init__(self,
- id: str,
- name: str,
- description: str,
- synopsis: str = '',
- defaultTrigger: str = f'{id} ',
- allowTriggerRemap: str = true,
- supportsFuzzyMatching: bool = False):
- ...
-
- @abstractmethod
- def handleGlobalQuery(self, query: GlobalQuery) -> List[RankItem]:
- ...
-
- def applyUsageScore(self, rank_items: List[RankItem]):
- ...
-
- def handleTriggerQuery(self, query: TriggerQuery):
- ...
-
-
-class IndexItem:
- """https://albertlauncher.github.io/reference/classalbert_1_1_index_item.html"""
-
- def __init__(self, item: AbstractItem, string: str):
- ...
-
- item: AbstractItem
- string: str
-
-
-class IndexQueryHandler(GlobalQueryHandler):
- """https://albertlauncher.github.io/reference/classalbert_1_1_index_query_handler.html"""
-
- @abstractmethod
- def updateIndexItems(self):
- ...
-
- def setIndexItems(self, indexItems: List[RankItem]):
- ...
-
- def handleGlobalQuery(self, query: GlobalQuery) -> List[RankItem]:
- ...
-
-
-class Notification:
-
- def __init__(self, title: str, subtitle: str = '', text: str = ''):
- ...
-
-
-def debug(arg: Any):
- """Module attached attribute"""
-
-
-def info(arg: Any):
- """Module attached attribute"""
-
-
-def warning(arg: Any):
- """Module attached attribute"""
-
-
-def critical(arg: Any):
- """Module attached attribute"""
-
-
-def setClipboardText(text: str=''):
- """
- Set the system clipboard text.
- Args:
- text: The text used to set the clipboard
- """
-
-
-def setClipboardTextAndPaste(text: str=''):
- """
- Set the system clipboard text and paste to the front-most window
- Args:
- text: The text used to set the clipboard
- """
-
-
-def openUrl(url: str = ''):
- """
- Open an URL using QDesktopServices::openUrl.
- Args:
- url: The URL to open
- """
-
-
-def runDetachedProcess(cmdln: List[str] = [], workdir: str = ''):
- """
- Run a detached process.
- Args:
- cmdln: The commandline to run in the terminal (argv)
- workdir: The working directory used to run the terminal
- """
-
-
-def runTerminal(script: str = '', workdir: str = '', close_on_exit: bool = False):
- """
- Run a script in the users shell and terminal.
- Args:
- script: The script to be executed.
- workdir: The working directory used to run the process
- close_on_exit: Close the terminal on exit. Otherwise exec $SHELL.
- """
-
diff --git a/arch_wiki/__init__.py b/arch_wiki/__init__.py
index cc70f54e..c24c2476 100644
--- a/arch_wiki/__init__.py
+++ b/arch_wiki/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
import json
from pathlib import Path
@@ -7,12 +8,13 @@
from albert import *
-md_iid = '2.0'
-md_version = "1.4"
-md_name = "ArchLinux Wiki"
-md_description = "Search ArchLinux Wiki articles"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/awiki"
+md_iid = "3.0"
+md_version = '2.0'
+md_name = "Arch Linux Wiki"
+md_description = "Search Arch Linux Wiki articles"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/arch_wiki"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
@@ -20,15 +22,14 @@ class Plugin(PluginInstance, TriggerQueryHandler):
baseurl = 'https://wiki.archlinux.org/api.php'
search_url = "https://wiki.archlinux.org/index.php?search=%s"
user_agent = "org.albert.extension.python.archwiki"
+ iconUrls = [f"file:{Path(__file__).parent}/arch.svg"]
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='awiki ')
- PluginInstance.__init__(self, extensions=[self])
- self.iconUrls = [f"file:{Path(__file__).parent}/arch.svg"]
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ def defaultTrigger(self):
+ return 'awiki '
def handleTriggerQuery(self, query):
stripped = query.string.strip()
@@ -60,7 +61,7 @@ def handleTriggerQuery(self, query):
summary = data[2][i]
url = data[3][i]
- results.append(StandardItem(id=md_id,
+ results.append(StandardItem(id=self.id(),
text=title,
subtext=summary if summary else url,
iconUrls=self.iconUrls,
@@ -71,14 +72,15 @@ def handleTriggerQuery(self, query):
if results:
query.add(results)
else:
- query.add(StandardItem(id=md_id,
+ query.add(StandardItem(id=self.id(),
text="Search '%s'" % query.string,
subtext="No results. Start online search on Arch Wiki",
iconUrls=self.iconUrls,
- actions=[Action("search", "Open search", lambda s=query.string: self.search_url % s)]))
+ actions=[Action("search", "Open search",
+ lambda s=query.string: openUrl(self.search_url % s))]))
else:
- query.add(StandardItem(id=md_id,
+ query.add(StandardItem(id=self.id(),
text=md_name,
iconUrls=self.iconUrls,
subtext="Enter a query to search on the Arch Wiki"))
diff --git a/arch_wiki/arch.svg b/arch_wiki/arch.svg
index 61f55ed9..b95bef86 100644
--- a/arch_wiki/arch.svg
+++ b/arch_wiki/arch.svg
@@ -1,5 +1 @@
-
-
\ No newline at end of file
diff --git a/aur/__init__.py b/aur/__init__.py
index a2bcb1e2..0ca8917a 100644
--- a/aur/__init__.py
+++ b/aur/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2022-2023 Manuel Schneider
+# Copyright (c) 2024 Manuel Schneider
"""
Search for packages and open their URLs. This extension is also intended to be used to \
@@ -15,29 +15,24 @@
from albert import *
-md_iid = '2.0'
-md_version = "1.8"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "AUR"
md_description = "Query and install AUR packages"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/aur"
-# md_platforms = ["Linux"]
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/aur"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
aur_url = "https://aur.archlinux.org/packages/"
baseurl = 'https://aur.archlinux.org/rpc/'
+ iconUrls = [f"file:{Path(__file__).parent}/arch.svg"]
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='aur ')
- PluginInstance.__init__(self, extensions=[self])
-
- self.iconUrls = [f"file:{Path(__file__).parent}/arch.svg"]
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
if which("yaourt"):
self.install_cmdline = "yaourt -S aur/%s"
@@ -51,6 +46,17 @@ def __init__(self):
info("No supported AUR helper found.")
self.install_cmdline = None
+ def defaultTrigger(self):
+ return 'aur '
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip()
+ }
+ ]
+
def handleTriggerQuery(self, query):
for _ in range(50):
sleep(0.01)
@@ -72,7 +78,7 @@ def handleTriggerQuery(self, query):
data = json.loads(response.read().decode())
if data['type'] == "error":
query.add(StandardItem(
- id=md_id,
+ id=self.id(),
text="Error",
subtext=data['error'],
iconUrls=self.iconUrls
@@ -86,7 +92,7 @@ def handleTriggerQuery(self, query):
for entry in results_json:
name = entry['Name']
item = StandardItem(
- id=md_id,
+ id=self.id(),
iconUrls=self.iconUrls,
text=f"{entry['Name']} {entry['Version']}"
)
@@ -107,16 +113,14 @@ def handleTriggerQuery(self, query):
id="inst",
text="Install using %s" % pacman,
callable=lambda n=name: runTerminal(
- script=self.install_cmdline % n,
- close_on_exit=False
+ script=self.install_cmdline % n + " ; exec $SHELL"
)
))
actions.append(Action(
id="instnc",
text="Install using %s (noconfirm)" % pacman,
callable=lambda n=name: runTerminal(
- script=self.install_cmdline % n + " --noconfirm",
- close_on_exit=False
+ script=self.install_cmdline % n + " --noconfirm ; exec $SHELL"
)
))
@@ -133,7 +137,7 @@ def handleTriggerQuery(self, query):
query.add(results)
else:
query.add(StandardItem(
- id=md_id,
+ id=self.id(),
text=md_name,
subtext="Enter a query to search the AUR",
iconUrls=self.iconUrls,
diff --git a/aur/arch.svg b/aur/arch.svg
index 61f55ed9..b95bef86 100644
--- a/aur/arch.svg
+++ b/aur/arch.svg
@@ -1,5 +1 @@
-
-
\ No newline at end of file
diff --git a/bitwarden/__init__.py b/bitwarden/__init__.py
index 78d39f51..87afe63a 100644
--- a/bitwarden/__init__.py
+++ b/bitwarden/__init__.py
@@ -1,106 +1,92 @@
# -*- coding: utf-8 -*-
+import time
+from dataclasses import dataclass
+from enum import Enum
from pathlib import Path
-from subprocess import run, CalledProcessError
+from subprocess import CalledProcessError, run
from albert import *
-md_iid = '2.0'
-md_version = "1.3"
+md_iid = "3.0"
+md_version = "3.1"
md_name = "Bitwarden"
md_description = "'rbw' wrapper extension"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python"
-md_maintainers = "@ovitor"
-md_credits = "Original author: @tylio"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/bitwarden"
+md_authors = ["@ovitor", "@daviddeadly", "@manuelschneid3r"]
md_bin_dependencies = ["rbw"]
+MAX_MINUTES_CACHE_TIMEOUT = 60
+DEFAULT_MINUTE_CACHE_TIMEOUT = 5
+
+
+@dataclass(frozen=True)
+class ConfigKeys:
+ CACHE_TIMEOUT = "cache_timeout"
+
class Plugin(PluginInstance, TriggerQueryHandler):
+ _cached_items = None
+ _last_fetch_time = 0
+
+ iconUrls = [f"file:{Path(__file__).parent}/bw.svg"]
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='bw ')
- PluginInstance.__init__(self, extensions=[self])
- self.iconUrls = [f"file:{Path(__file__).parent}/bw.svg"]
-
- def _get_passwords(self):
- field_names = ["id", "name", "user", "folder"]
- p = run(
- ["rbw", "list", "--fields", ",".join(field_names)],
- capture_output=True,
- encoding="utf-8",
- check=True,
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ self.cache_timeout = (
+ self.readConfig(ConfigKeys.CACHE_TIMEOUT, int)
+ or DEFAULT_MINUTE_CACHE_TIMEOUT
)
- passwords = []
- for l in p.stdout.splitlines():
- fields = l.split("\t")
- d = dict(zip(field_names, fields))
- if d["folder"]:
- d["path"] = d["folder"] + "/" + d["name"]
- else:
- d["path"] = d["name"]
- passwords.append(d)
- return passwords
+ def defaultTrigger(self):
+ return "bw "
+
+ @property
+ def cache_timeout(self):
+ return int(self._cache_timeout / 60)
+
+ @cache_timeout.setter
+ def cache_timeout(self, value):
+ self._cache_timeout = int(value * 60)
+ self.writeConfig(ConfigKeys.CACHE_TIMEOUT, value)
+
+ def configWidget(self):
+ return [
+ {
+ "type": "label",
+ "text": "Cache (result of `rbw list`) duration",
+ },
+ {
+ "type": "spinbox",
+ "property": ConfigKeys.CACHE_TIMEOUT,
+ "label": f"Minutes: (max: {MAX_MINUTES_CACHE_TIMEOUT}, disable: 0)",
+ "widget_properties": {"maximum": MAX_MINUTES_CACHE_TIMEOUT},
+ },
+ ]
def handleTriggerQuery(self, query):
- if query.string.strip().lower() == "unlock":
- query.add(
+ results = []
+ if query.string.strip().lower() == "sync":
+ results.append(
StandardItem(
- id="unlock",
- text="Unlock Bitwarden Vault",
+ id="sync",
+ text="Sync Bitwarden Vault",
iconUrls=self.iconUrls,
actions=[
Action(
- id="unlock",
- text="Unlocking Bitwarden Vault",
- callable=lambda: runTerminal(
- script="rbw stop-agent && rbw unlock",
- close_on_exit=True
- )
+ id="sync",
+ text="Syncing Bitwarden Vault",
+ callable=lambda: self._sync_vault(),
)
- ]
+ ],
)
)
- passwords = self._get_passwords()
- filtered_passwords = []
- search_fields = ["path", "user"]
- words = query.string.strip().lower().split()
- for p in passwords:
- all_matches = True
- for w in words:
- for k in search_fields:
- if w in p[k].lower():
- break
- else:
- all_matches = False
- break
-
- if all_matches:
- filtered_passwords.append(p)
-
- for p in filtered_passwords:
- pw = run(
- ["rbw", "get", p["id"]],
- capture_output=True,
- encoding="utf-8",
- check=True
- )
- try:
- code = run(
- ["rbw", "code", p["id"]],
- capture_output=True,
- encoding="utf-8",
- check=True
- )
- except CalledProcessError as err:
- code = run (["echo"], capture_output=True,encoding="utf-8", check=True)
- query.add(
+ for p in self._filter_items(query):
+ results.append(
StandardItem(
id=p["id"],
text=p["path"],
@@ -110,24 +96,124 @@ def handleTriggerQuery(self, query):
Action(
id="copy",
text="Copy password to clipboard",
- callable=lambda password=pw.stdout.strip(): setClipboardText(
- text=password)
+ callable=lambda item=p: self._password_to_clipboard(item),
),
Action(
id="copy-auth",
text="Copy auth code to clipboard",
- callable=lambda code=code.stdout.strip(): setClipboardText(
- text=code)
+ callable=lambda item=p: self._code_to_clipboard(item),
+ ),
+ Action(
+ id="copy-username",
+ text="Copy username to clipboard",
+ callable=lambda username=p["user"]: setClipboardText(
+ text=username
+ ),
),
Action(
id="edit",
text="Edit entry in terminal",
- callable=lambda pid=p['id']: runTerminal(
- script=f"rbw edit {pid}",
- workdir="~",
- close_on_exit=False
- )
- )
- ]
+ callable=lambda item=p: self._edit_entry(item),
+ ),
+ ],
)
- )
\ No newline at end of file
+ )
+
+ query.add(results)
+
+ def _get_items(self):
+ not_first_time = self._cached_items is not None
+
+ time_passed = time.time() - self._last_fetch_time
+ is_chache_fresh = time_passed < self._cache_timeout
+
+ if not_first_time and is_chache_fresh:
+ return self._cached_items
+
+ field_names = ["id", "name", "user", "folder"]
+ raw_items = run(
+ ["rbw", "list", "--fields", ",".join(field_names)],
+ capture_output=True,
+ encoding="utf-8",
+ check=True,
+ )
+
+ items = []
+
+ for line in raw_items.stdout.splitlines():
+ fields = line.split("\t")
+ item = dict(zip(field_names, fields))
+
+ if item["folder"]:
+ item["path"] = item["folder"] + "/" + item["name"]
+ else:
+ item["path"] = item["name"]
+
+ items.append(item)
+
+ self._cached_items = items
+ self._last_fetch_time = time.time()
+
+ return items
+
+ def _filter_items(self, query):
+ passwords = self._get_items() or []
+ search_fields = ["path", "user"]
+ # Use a set for faster membership tests
+ words = set(query.string.strip().lower().split())
+
+ filtered_passwords = []
+
+ for p in passwords:
+ match_all_words_with_any_field = all(
+ any(word in p[field].lower() for field in search_fields)
+ for word in words
+ )
+
+ if match_all_words_with_any_field:
+ filtered_passwords.append(p)
+
+ return filtered_passwords
+
+ def _sync_vault(self):
+ run(["rbw", "sync"], check=True)
+
+ self._cached_items = None
+ self._last_fetch_time = 0
+
+ @staticmethod
+ def _password_to_clipboard(item):
+ rbw_id = item["id"]
+
+ password = run(
+ ["rbw", "get", rbw_id], capture_output=True, encoding="utf-8", check=True
+ ).stdout.strip()
+
+ setClipboardText(text=password)
+
+ @staticmethod
+ def _code_to_clipboard(item):
+ rbw_id = item["id"]
+
+ try:
+ code = run(
+ ["rbw", "code", rbw_id],
+ capture_output=True,
+ encoding="utf-8",
+ check=True,
+ ).stdout.strip()
+ except CalledProcessError as err:
+ code = run(
+ ["echo", err.__str__()],
+ capture_output=True,
+ encoding="utf-8",
+ check=True,
+ ).stdout.strip()
+
+ setClipboardText(text=code)
+
+ @staticmethod
+ def _edit_entry(item):
+ rbw_id = item["id"]
+
+ runTerminal(script=f"rbw edit {rbw_id}")
diff --git a/coingecko/__init__.py b/coingecko/__init__.py
index 602d0684..df23f20d 100644
--- a/coingecko/__init__.py
+++ b/coingecko/__init__.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
-
-"""Show and access crypto currencies on CoinGecko.com."""
+# Copyright (c) 2024 Manuel Schneider
from albert import *
from time import time
@@ -9,12 +8,13 @@
from pathlib import Path
from threading import Thread, Event
-md_iid = "2.0"
-md_version = "1.1"
+md_iid = "3.0"
+md_version = "2.1"
md_name = "CoinGecko"
md_description = "Access CoinGecko"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/coingecko"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/coingecko"
+md_authors = "@manuelschneid3r"
class CoinFetcherThread(Thread):
@@ -71,9 +71,9 @@ def __init__(self,
iconUrls=Plugin.iconUrls,
actions=[
Action("show", f"Show {name} on CoinGecko",
- lambda id=identifier: openUrl(Plugin.coinsUrl + id)),
+ lambda coin_id=identifier: openUrl(Plugin.coinsUrl + coin_id)),
Action("url", "Copy URL to clipboard",
- lambda id=identifier: setClipboardText(Plugin.coinsUrl + id))
+ lambda coin_id=identifier: setClipboardText(Plugin.coinsUrl + coin_id))
]
)
self.name = name
@@ -86,23 +86,27 @@ class Plugin(PluginInstance, IndexQueryHandler):
iconUrls = [f"file:{Path(__file__).parent}/coingecko.png"]
def __init__(self):
- IndexQueryHandler.__init__(
- self, md_id, md_name, md_description,
- defaultTrigger='cg ',
- synopsis='< symbol | name >'
- )
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ IndexQueryHandler.__init__(self)
self.items = []
self.mtime = 0
- self.coinCacheFilePath = self.cacheLocation / "coins.json"
+ cache_location = self.cacheLocation()
+ cache_location.mkdir(parents=True, exist_ok=True)
+ self.coinCacheFilePath = cache_location / "coins.json"
self.thread = CoinFetcherThread(self.updateIndexItems, self.coinCacheFilePath)
self.thread.start()
- def finalize(self):
+ def __del__(self):
self.thread.stop()
self.thread.join()
+ def defaultTrigger(self):
+ return 'cg '
+
+ def synopsis(self, query):
+ return "< symbol | name >"
+
def updateIndexItems(self):
if self.coinCacheFilePath.is_file() and (mtime := self.coinCacheFilePath.lstat().st_mtime) > self.mtime:
self.mtime = mtime
@@ -128,7 +132,5 @@ def updateIndexItems(self):
# override default trigger handling to sort by rank
def handleTriggerQuery(self, query):
- qs = query.string.strip().lower()
- for item in self.items:
- if qs in item.name.lower() or qs in item.symbol.lower():
- query.add(item)
+ m = Matcher(query.string)
+ query.add([item for item in self.items if m.match(item.symbol, item.name)])
diff --git a/color/__init__.py b/color/__init__.py
index 10bfa2f9..482f709f 100644
--- a/color/__init__.py
+++ b/color/__init__.py
@@ -1,43 +1,39 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
"""
-Displays a color parsed from name, which may be in one of these formats:
+Displays a color parsed from a code, which may be in one of these formats:
* #RGB (each of R, G, and B is a single hex digit)
* #RRGGBB
* #AARRGGBB
* #RRRGGGBBB
* #RRRRGGGGBBBB
-* A name from the list of colors defined in the list of SVG color keyword
-names provided by the World Wide Web Consortium; for example, "steelblue"
-or "gainsboro".
-Note: This extension started as a prototype to test the internal color pixmap
-generator. However it may serve as a starting point for people having a real
-need for color workflows. PR's welcome.
+Note: This extension started as a prototype to test the internal color pixmap generator. However it may serve as a \
+starting point for people having a real need for color workflows. PR's welcome.
"""
from albert import *
-from urllib.parse import quote_plus
from string import hexdigits
-md_iid = '2.0'
-md_version = '1.0'
-md_name = 'Color'
-md_description = 'Display color for color codes'
-md_license = 'MIT'
-md_url = 'https://github.com/albertlauncher/python/color'
+md_iid = "3.0"
+md_version = "2.0"
+md_name = "Color"
+md_description = "Display color for color codes"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/color"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, GlobalQueryHandler):
def __init__(self):
- GlobalQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='#')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ GlobalQueryHandler.__init__(self)
+
+ def defaultTrigger(self):
+ return '#'
def handleGlobalQuery(self, query):
rank_items = []
@@ -51,7 +47,7 @@ def handleGlobalQuery(self, query):
rank_items.append(
RankItem(
StandardItem(
- id=md_id,
+ id=self.id(),
text=s,
subtext="The color for this code.",
iconUrls=[f"gen:?background=%23{s}"],
@@ -61,3 +57,7 @@ def handleGlobalQuery(self, query):
)
return rank_items
+
+ def configWidget(self):
+ return [{ 'type': 'label', 'text': __doc__.strip() }]
+
\ No newline at end of file
diff --git a/copyq/__init__.py b/copyq/__init__.py
index ae418eb5..cd21adb7 100644
--- a/copyq/__init__.py
+++ b/copyq/__init__.py
@@ -1,18 +1,20 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2024 Manuel Schneider
+# Copyright (c) 2023 Oskar Haarklou Veileborg (@BarrensZeppelin)
import json
import subprocess
from albert import *
-md_iid = '2.0'
-md_version = "1.4"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "CopyQ"
md_description = "Access CopyQ clipboard"
md_license = "BSD-2-Clause"
-md_url = "https://github.com/albertlauncher/python"
+md_url = "https://github.com/albertlauncher/python/tree/main/copyq"
+md_authors = ["@ManuelSchneid3r", "@BarrensZeppelin"]
md_bin_dependencies = ["copyq"]
-md_maintainers = "@BarrensZeppelin"
copyq_script_getAll = r"""
@@ -48,19 +50,15 @@
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis="",
- defaultTrigger='cq ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ def defaultTrigger(self):
+ return "cp "
def handleTriggerQuery(self, query):
items = []
- q_string = query.string
-
- script = copyq_script_getMatches % q_string if q_string else copyq_script_getAll
+ script = copyq_script_getMatches % query.string if query.string else copyq_script_getAll
proc = subprocess.run(["copyq", "-"], input=script.encode(), stdout=subprocess.PIPE)
json_arr = json.loads(proc.stdout.decode())
@@ -72,12 +70,12 @@ def handleTriggerQuery(self, query):
else:
text = " ".join(filter(None, text.replace("\n", " ").split(" ")))
- act = lambda script, row=row: (
- lambda: runDetachedProcess(["copyq", script % row])
+ act = lambda s=script, r=row: (
+ lambda: runDetachedProcess(["copyq", s % r])
)
items.append(
StandardItem(
- id=md_id,
+ id=self.id(),
iconUrls=["xdg:copyq"],
text=text,
subtext="%s: %s" % (row, ", ".join(json_obj["mimetypes"])),
diff --git a/dice_roll/README.md b/dice_roll/README.md
deleted file mode 100644
index 955d6c3c..00000000
--- a/dice_roll/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Dice Roll
-
-Extension for rolling dice
-
-
-
-## Usage
-
-Roll any number of dice using the format `_d_`.
-
-Synopsis: `d [d ...]`
-
-Example: `"roll 2d6 3d8 1d20"`
diff --git a/dice_roll/__init__.py b/dice_roll/__init__.py
index 95f95031..deb9bc2c 100644
--- a/dice_roll/__init__.py
+++ b/dice_roll/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Jonah Lawrence
from __future__ import annotations
@@ -14,13 +15,13 @@
Example: "roll 2d6 3d8 1d20"
"""
-md_iid = '2.0'
-md_version = "1.3"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Dice Roll"
md_description = "Roll any number of dice"
md_license = "MIT"
-md_url = "https://github.com/albertlauncher/python"
-md_maintainers = "@DenverCoder1"
+md_url = "https://github.com/albertlauncher/python/tree/main/dice_roll"
+md_authors = "@DenverCoder1"
def get_icon_path(num_sides: int | None) -> str:
@@ -131,21 +132,24 @@ class Plugin(albert.PluginInstance, albert.TriggerQueryHandler):
"""A plugin to roll dice"""
def __init__(self):
- albert.TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis="d [d ...]",
- defaultTrigger="roll ",
- )
- albert.PluginInstance.__init__(self, extensions=[self])
+ albert.PluginInstance.__init__(self)
+ albert.TriggerQueryHandler.__init__(self)
+
+ def synopsis(self, query):
+ return "d [d ...]"
+
+ def defaultTrigger(self):
+ return "roll "
+
+ def configWidget(self):
+ return [{ 'type': 'label', 'text': __doc__.strip() }]
def handleTriggerQuery(self, query: albert.Query) -> None:
query_string = query.string.strip()
try:
items = get_items(query_string)
query.add(items)
- except Exception as e:
+ except Exception:
query.add([albert.StandardItem(
id="error",
iconUrls=[get_icon_path(None)],
diff --git a/docker/__init__.py b/docker/__init__.py
index f925e913..f6639c03 100644
--- a/docker/__init__.py
+++ b/docker/__init__.py
@@ -1,42 +1,54 @@
-"""
-Docker wrapper (prototype)
-"""
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
from pathlib import Path
import docker
from albert import *
-md_iid = "2.0"
-md_version = "1.6"
+md_iid = "3.0"
+md_version = "4.0"
md_name = "Docker"
md_description = "Manage docker images and containers"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/docker"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/docker"
+md_authors = "@manuelschneid3r"
md_bin_dependencies = "docker"
md_lib_dependencies = "docker"
-class Plugin(PluginInstance, GlobalQueryHandler):
+class Plugin(PluginInstance, TriggerQueryHandler):
+ # Global query handler not applicable, queries take seconds sometimes
def __init__(self):
- GlobalQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='d ',
- synopsis='')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.icon_urls_running = [f"file:{Path(__file__).parent}/running.png"]
self.icon_urls_stopped = [f"file:{Path(__file__).parent}/stopped.png"]
self.client = None
- def handleGlobalQuery(self, query):
- rank_items = []
- try:
- if not self.client:
+ def synopsis(self, query):
+ return ""
+
+ def defaultTrigger(self):
+ return "d "
+
+ def handleTriggerQuery(self, query):
+ items = []
+
+ if not self.client:
+ try:
self.client = docker.from_env()
+ except Exception as e:
+ items.append(StandardItem(
+ id='except',
+ text="Failed starting docker client",
+ subtext=str(e),
+ iconUrls=self.icon_urls_running,
+ ))
+ return items
+ try:
for container in self.client.containers.list(all=True):
if query.string in container.name:
# Create dynamic actions
@@ -47,41 +59,37 @@ def handleGlobalQuery(self, query):
actions = [Action("start", "Start container", lambda c=container: c.start())]
actions.extend([
Action("logs", "Logs",
- lambda c=container.id: runTerminal("docker logs -f %s" % c, close_on_exit=False)),
+ lambda c=container.id: runTerminal("docker logs -f %s ; exec $SHELL" % c)),
Action("remove", "Remove (forced, with volumes)",
lambda c=container: c.remove(v=True, force=True)),
Action("copy-id", "Copy id to clipboard",
- lambda id=container.id: setClipboardText(id))
+ lambda cid=container.id: setClipboardText(cid))
])
- rank_items.append(RankItem(
- item=StandardItem(
- id=container.id,
- text="%s (%s)" % (container.name, ", ".join(container.image.tags)),
- subtext="Container: %s" % container.id,
- iconUrls=self.icon_urls_running if container.status == 'running' else self.icon_urls_stopped,
- actions=actions
- ),
- score=len(query.string)/len(container.name)
+ items.append(StandardItem(
+ id=container.id,
+ text="%s (%s)" % (container.name, ", ".join(container.image.tags)),
+ subtext="Container: %s" % container.id,
+ iconUrls=self.icon_urls_running if container.status == 'running' else self.icon_urls_stopped,
+ actions=actions
))
for image in reversed(self.client.images.list()):
for tag in sorted(image.tags, key=len): # order by resulting score
if query.string in tag:
- rank_items.append(RankItem(
- item=StandardItem(
- id=image.short_id,
- text=", ".join(image.tags),
- subtext="Image: %s" % image.id,
- iconUrls=self.icon_urls_stopped,
- actions=[Action("run", "Run with command: %s" % query.string,
- lambda i=image, s=query.string: client.containers.run(i, s)),
- Action("rmi", "Remove image", lambda i=image: i.remove())]
- ),
- score=len(query.string)/len(tag)
+ items.append(StandardItem(
+ id=image.short_id,
+ text=", ".join(image.tags),
+ subtext="Image: %s" % image.id,
+ iconUrls=self.icon_urls_stopped,
+ actions=[
+ # Action("run", "Run with command: %s" % query.string,
+ # lambda i=image, s=query.string: client.containers.run(i, s)),
+ Action("rmi", "Remove image", lambda i=image: i.remove())
+ ]
))
except Exception as e:
- warning(e)
+ warning(str(e))
self.client = None
- return rank_items
+ query.add(items)
diff --git a/duckduckgo/__init__.py b/duckduckgo/__init__.py
index 2f9174e9..3666115e 100644
--- a/duckduckgo/__init__.py
+++ b/duckduckgo/__init__.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
+
"""
Inline DuckDuckGo web search using the 'duckduckgo-search' library.
"""
@@ -8,34 +11,34 @@
from itertools import islice
from time import sleep
-md_iid = '2.0'
-md_version = '1.0'
+md_iid = "3.0"
+md_version = "2.0"
md_name = 'DuckDuckGo'
md_description = 'Inline DuckDuckGo web search'
-md_url = 'https://github.com/albertlauncher/python/duckduckgo'
+md_license = "MIT"
+md_url = 'https://github.com/albertlauncher/python/tree/main/duckduckgo'
md_lib_dependencies = "duckduckgo-search"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis="",
- defaultTrigger='ddg ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.ddg = DDGS()
self.iconUrls = [f"file:{Path(__file__).parent}/duckduckgo.svg"]
+ def defaultTrigger(self):
+ return "ddg "
+
def handleTriggerQuery(self, query):
stripped = query.string.strip()
if stripped:
# dont flood
- for number in range(25):
+ for _ in range(25):
sleep(0.01)
if not query.isValid:
return
@@ -43,7 +46,7 @@ def handleTriggerQuery(self, query):
for r in islice(self.ddg.text(stripped, safesearch='off'), 10):
query.add(
StandardItem(
- id=md_id,
+ id=self.id(),
text=r['title'],
subtext=r['body'],
iconUrls=self.iconUrls,
diff --git a/emoji/__init__.py b/emoji/__init__.py
index fe918e6d..49bfa0a3 100644
--- a/emoji/__init__.py
+++ b/emoji/__init__.py
@@ -1,43 +1,43 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
import json
import re
import threading
import urllib.request
-from itertools import product
+import builtins
from locale import getdefaultlocale
from pathlib import Path
from albert import *
-md_iid = '2.1'
-md_version = "2.1"
+md_iid = "3.0"
+md_version = "3.1"
md_name = "Emoji"
md_description = "Find and copy emojis by name"
md_license = "MIT"
-md_url = "https://github.com/albertlauncher/python/tree/master/emoji"
+md_url = "https://github.com/albertlauncher/python/tree/main/emoji"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, IndexQueryHandler):
def __init__(self):
- IndexQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger=':',
- synopsis='')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ IndexQueryHandler.__init__(self)
self.thread = None
self._use_derived = self.readConfig('use_derived', bool)
if self._use_derived is None:
self._use_derived = False
- def finalize(self):
- if self.thread.is_alive():
+ def __del__(self):
+ if self.thread and self.thread.is_alive():
self.thread.join()
+ def defaultTrigger(self):
+ return ':'
+
@property
def use_derived(self):
return self._use_derived
@@ -65,19 +65,19 @@ def updateIndexItems(self):
def update_index_items_task(self):
- def download_file(url: str, path: str) -> bool:
+ def download_file(url: str, path: Path):
debug(f"Downloading {url}.")
headers = {'User-Agent': 'Mozilla/5.0'} # otherwise github returns html
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=3) as response:
if response.getcode() == 200:
debug(f"Success. Storing to {path}.")
- with open(path, 'wb') as file:
+ with builtins.open(path, 'wb') as file:
file.write(response.read())
else:
raise RuntimeError(f"Failed to download {url}. Status code: {response.getcode()}")
- def get_fully_qualified_emojis(cache_path: str) -> list:
+ def get_fully_qualified_emojis(cache_path: Path) -> list:
"""Returns fully qualified emoji strings"""
def convert_to_unicode_char(hex_code: str):
@@ -122,7 +122,7 @@ def convert_to_unicode_str(hex_codes: str):
return fully_qualified
- def get_annotations(cache_path: str, use_derived: bool) -> dict:
+ def get_annotations(cache_path: Path, use_derived: bool) -> dict:
# determine locale
@@ -134,7 +134,7 @@ def get_annotations(cache_path: str, use_derived: bool) -> dict:
# fetch localized cldr annotations 'full'
- path_full = cache_path / 'emoji_annotations_full.json'
+ path_full = cache_path / f'emoji_annotations_full_{lang}.json'
if not path_full.is_file():
url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \
'cldr-annotations-full/annotations/%s/annotations.json' % lang
@@ -148,7 +148,7 @@ def get_annotations(cache_path: str, use_derived: bool) -> dict:
# fetch localized cldr annotations 'derived'
- path_derived = cache_path / 'emoji_annotations_derived.json'
+ path_derived = cache_path / f'emoji_annotations_derived_{lang}.json'
if not path_derived.is_file():
url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \
'cldr-annotations-derived-full/annotationsDerived/%s/annotations.json' % lang
@@ -160,8 +160,10 @@ def get_annotations(cache_path: str, use_derived: bool) -> dict:
json_derived = json.load(file_derived)['annotationsDerived']['annotations']
return json_full | json_derived
- emojis = get_fully_qualified_emojis(self.cacheLocation)
- annotations = get_annotations(self.cacheLocation, self.use_derived)
+ cache_location = self.cacheLocation()
+ cache_location.mkdir(parents=True, exist_ok=True)
+ emojis = get_fully_qualified_emojis(cache_location)
+ annotations = get_annotations(cache_location, self.use_derived)
def remove_redundancy(sentences):
sets_of_words = [set(sentence.lower().split()) for sentence in sentences]
@@ -184,27 +186,34 @@ def remove_redundancy(sentences):
non_rgi_emoji = emoji.replace('\uFE0F', '')
ann = annotations[non_rgi_emoji]
except KeyError as e:
- # debug(f"Found no translation for {e}. Emoji will not be available.")
+ debug(f"Found no translation for {e}. Emoji will not be available.")
continue
title = ann['tts'][0]
aliases = remove_redundancy([title.replace(':', '').replace(',', ''), *ann['default']])
+ actions = []
+ if havePasteSupport():
+ actions.append(
+ Action(
+ "paste", "Copy and paste to front-most window",
+ lambda emj=emoji: setClipboardTextAndPaste(emj)
+ )
+ )
+
+ actions.append(
+ Action(
+ "copy", "Copy to clipboard",
+ lambda emj=emoji: setClipboardText(emj)
+ )
+ )
+
item = StandardItem(
id=emoji,
text=title.capitalize(),
subtext=", ".join([a.capitalize() for a in aliases]),
iconUrls=[f"gen:?text={emoji}"],
- actions=[
- Action(
- "paste", "Copy and paste to front-most window",
- lambda emj=emoji: setClipboardTextAndPaste(emj)
- ),
- Action(
- "copy", "Copy to clipboard",
- lambda emj=emoji: setClipboardText(emj)
- ),
- ]
+ actions=actions
)
for alias in aliases:
diff --git a/goldendict/__init__.py b/goldendict/__init__.py
index 80126cf6..c25c72bb 100644
--- a/goldendict/__init__.py
+++ b/goldendict/__init__.py
@@ -1,37 +1,56 @@
-from albert import Action, StandardItem, TriggerQuery, PluginInstance, TriggerQueryHandler, runDetachedProcess # pylint: disable=import-error
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2024 Manuel Schneider
-md_iid = '2.0'
-md_version = '1.3'
-md_name = 'GoldenDict'
-md_description = 'Searches in GoldenDict'
-md_url = 'https://github.com/albertlauncher/python/'
-md_maintainers = '@stevenxxiu'
-md_bin_dependencies = ['goldendict']
+import os
+import shutil
+
+from albert import *
+
+md_iid = "3.0"
+md_version = "2.0"
+md_name = "GoldenDict"
+md_description = "Quick access to GoldenDict"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/goldendict"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='query',
- defaultTrigger='gd ')
- PluginInstance.__init__(self, extensions=[self])
- self.iconUrls = ["xdg:goldendict"]
-
- def handleTriggerQuery(self, query: TriggerQuery) -> None:
- query_str = query.string.strip()
- if not query_str:
- return
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ commands = [
+ '/var/lib/flatpak/exports/bin/org.goldendict.GoldenDict', # flatpak
+ '/var/lib/flatpak/exports/bin/io.github.xiaoyifang.goldendict_ng', # flatpak ng
+ 'goldendict', # native
+ 'goldendict-ng', # native ng
+ ]
+
+ executables = [e for e in [shutil.which(c) for c in commands] if e]
+
+ if not executables:
+ raise RuntimeError(f'None of the GoldenDict distributions found.')
+
+ self.executable = executables[0]
+ self.iconUrls = [f'xdg:{os.path.basename(self.executable)}']
+
+ if len(executables) > 1:
+ warning(f"Multiple GoldenDict commands found: {', '.join(executables)}")
+ warning(f"Using {self.executable}")
+
+ def defaultTrigger(self):
+ return "gd "
+ def handleTriggerQuery(self, query):
+ q = query.string.strip()
query.add(
StandardItem(
id=md_name,
text=md_name,
- subtext=f'Look up {query_str} using GoldenDict',
+ subtext=f"Look up '{q}' in GoldenDict",
iconUrls=self.iconUrls,
- actions=[Action(md_name, md_name, lambda: runDetachedProcess(['goldendict', query_str]))],
+ actions=[Action(md_name, md_name, lambda e=self.executable: runDetachedProcess([e, q]))],
)
)
diff --git a/history_to_submodules.sh b/history_to_submodules.sh
new file mode 100755
index 00000000..054cc79c
--- /dev/null
+++ b/history_to_submodules.sh
@@ -0,0 +1,106 @@
+#! /usr/bin/env bash
+
+#set -x
+set -e
+
+# Find history dirs
+# git log --pretty=format: --name-only | grep -E '^(.archive/)?[^/]+' -o | sort -u | uniq
+
+git clone --bare git@github.com:albertlauncher/python.git .bare || true
+
+function filter (
+
+ if [[ $# -lt 2 ]]; then
+ echo "Error: At least two arguments required."
+ return 1
+ fi
+
+ local repo_name="$(echo $1 | tr '_' '-')"
+ local filter_path="$2"
+ shift 2
+
+ declare -a additional_filter_paths
+ if [[ $# -gt 0 ]]; then
+ additional_filter_paths=$(printf -- "--path %s " "$@")
+ fi
+
+ echo "ℹ️" $repo_name $filter_path $additional_filter_paths
+
+ git clone .bare $repo_name
+ cd $repo_name
+ git filter-repo --subdirectory-filter ${filter_path} ${additional_filter_paths[@]}
+
+ gh repo create "albertlauncher/albert-plugin-python-$repo_name" --public --disable-wiki
+ git remote add origin "git@github.com:albertlauncher/albert-plugin-python-$repo_name.git"
+ git push -f --set-upstream origin main
+ open "https://github.com/albertlauncher/albert-plugin-python-$repo_name"
+
+ cd ..
+)
+
+
+filter arch_wiki arch_wiki .archive/arch_wiki ArchWiki
+filter atom_projects atom_projects atom_projects.py .archive/atom_projects AtomProjects.py
+filter aur aur .archive/aur AUR
+filter base_converter base_converter base_converter.py .archive/base_converter BaseConverter.py
+filter binance binance .archive/binance Binance
+filter bitfinex bitfinex .archive/bitfinex Bitfinex
+filter bitwarden bitwarden bitwarden.py .archive/bitwarden
+filter coingecko coingecko
+filter coinmarketcap coinmarketcap .archive/coinmarketcap CoinMarketCap
+filter color color
+filter copyq copyq copyq.py .archive/copyq CopyQ.py
+filter currency_converter currency_converter currency_converter.py .archive/currency_converter Currency.py
+filter dango_emoji dango_emoji .archive/dango_emoji dangoemoji
+filter dango_kao dango_kao .archive/dango_kao dangokao
+filter datetime datetime datetime.py .archive/datetime DateTime.py epoch Epoch.py
+filter dice_roll dice_roll .archive/dice_roll
+filter docker docker .archive/docker
+filter duckduckgo duckduckgo
+filter emoji emoji EmojiPicker unicode_emoji .archive/unicode_emoji
+filter find find find .archive/find
+filter fortune fortune fortune.py .archive/fortune Fortune.py
+filter gnome_dictionary gnome_dictionary gnome_dictionary.py .archive/gnome_dictionary GnomeDictionary.py
+filter gnote gnote gnote.py .archive/gnote Gnote.py
+filter goldendict goldendict goldendict.py .archive/goldendict GoldenDict.py
+filter google_translate google_translate google_translate.py .archive/google_translate GoogleTranslate.py
+filter googletrans googletrans .archive/googletrans
+filter inhibit_sleep inhibit_sleep .archive/inhibit_sleep
+filter ip ip ip.py .archive/ip Ip.py
+filter jetbrains_projects jetbrains_projects jetbrains-projects.py .archive/jetbrains_projects JetbrainsProjects
+filter kill kill kill.py .archive/kill Kill.py
+filter locate locate locate.py .archive/locate Locate.py
+filter lpass lpass .archive/lpass
+filter mathematica_eval mathematica_eval mathematica_eval.py .archive/mathematica_eval
+filter multi_google_translate multi_google_translate multi_google_translate.py .archive/multi_google_translate MultiGoogleTranslate.py
+filter node_eval node_eval .archive/node_eval
+filter npm npm .archive/npm Npm
+filter packagist packagist .archive/packagist Packagist
+filter pacman pacman pacman.py .archive/pacman Pacman.py
+filter pass pass pass.py .archive/pass Pass.py
+filter php_eval php_eval .archive/php_eval
+filter pidgin pidgin pidgin.py .archive/pidgin Pidgin.py
+filter pomodoro pomodoro .archive/pomodoro Pomodoro
+filter python_eval python_eval python Python
+filter rand rand .archive/rand
+filter scrot scrot scrot.py Scrot.py .archive/scrot
+filter syncthing syncthing
+filter tex_to_unicode tex_to_unicode tex_to_unicode.py .archive/tex_to_unicode
+filter texdoc texdoc .archive/texdoc
+filter timer timer Timer .archive/timer
+filter tomboy tomboy tomboy.py Tomboy.py .archive/tomboy
+filter translators translators
+filter trash trash trash.py Trash.py .archive/trash
+filter unit_converter unit_converter .archive/unit_converter
+filter units units units.py Units.py .archive/units
+filter virtualbox virtualbox virtualbox.py VirtualBox.py .archive/virtualbox
+filter vpn vpn vpn.py .archive/vpn
+filter vscode_projects vscode_projects
+filter wikipedia wikipedia Wikipedia Wikipedia.py
+filter x_window_switcher x_window_switcher window_switcher window_switcher.py WindowSwitcher.py SwitchApp.py .archive/window_switcher
+filter xkcd xkcd .archive/xkcd
+filter youtube youtube youtube.py Youtube.py .archive/youtube
+filter zeal zeal zeal.py Zeal.py .archive/zeal
+
+
+
diff --git a/jetbrains_projects/LICENSE b/jetbrains_projects/LICENSE
deleted file mode 100644
index f288702d..00000000
--- a/jetbrains_projects/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/jetbrains_projects/README.md b/jetbrains_projects/README.md
deleted file mode 100644
index 08f9b880..00000000
--- a/jetbrains_projects/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Jetbrains-albert-plugin
-
-DISCLAIMER: This plugin has no affiliation with JetBrains s.r.o.. The icons are used under the terms specified [here](https://www.jetbrains.com/company/brand/#brand-guidelines).
-
-The plugin itself (the python file(s)) is licensed under the GPLv3 license.
-
-## How to use:
-
-Type `jb ` in the prompt, followed by your search term. Albert should show you a list of matching projects, scraped from your "recently edited" list of your Jetbrains IDEs. It tries to match the path of your project directory (not e.g. any files or contents).
-
-## Creating the project launcher:
-
-For this plugin to find the editors, you need to create a command line launcher for them. This is done by opening the IDE (for example PyCharm) and then going to `Tools -> Create Command-line Launcher...`. This will create a launcher (for example, `charm`) that this plugin will be able to find.
-
-## Authors:
-
-- Thomas Queste (@tomsquest)
-- @mqus
-
-## Contributors:
-
-- @iyzana
-- @Sharsie
-- @dsager
diff --git a/jetbrains_projects/__init__.py b/jetbrains_projects/__init__.py
index d8dc59f6..06229396 100644
--- a/jetbrains_projects/__init__.py
+++ b/jetbrains_projects/__init__.py
@@ -1,27 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018-2023 Thomas Queste
+# Copyright (c) 2023 Valentin Maerten
+
"""
-Supported IDEs:
+This plugin allows you to quickly open projects of the Jetbrains IDEs
-Android Studio, CLion, DataGrip, DataSpell, GoLand, IntelliJ IDEA, PhpStorm, PyCharm, Rider, RubyMine, WebStorm.
+- Android Studio
+- Aqua
+- CLion
+- DataGrip
+- DataSpell
+- GoLand
+- IntelliJ IDEA
+- PhpStorm
+- PyCharm
+- Rider
+- RubyMine
+- RustRover
+- WebStorm
+- Writerside.
-Note: To open projects the command-line launcher is required. If your IDE has no \
-command-line launcher in $PATH, use `Tools` > `Create Command-line Launcher`.
+Note that for this plugin to find the IDEs, a commandline launcher in $PATH is required.
+Open the IDE and click Tools -> Create Command-line Launcher to add one.
"""
from dataclasses import dataclass
from pathlib import Path
-from typing import Union
+from typing import Union, List
from shutil import which
from sys import platform
from xml.etree import ElementTree
from albert import *
-md_iid = '2.0'
-md_version = "1.5"
+md_iid = "3.0"
+md_version = "4.1"
md_name = "Jetbrains projects"
md_description = "Open your JetBrains projects"
-md_license = "GPL-3"
-md_url = "https://github.com/albertlauncher/python/"
-md_maintainers = ["@mqus", "@tomsquest"]
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/jetbrains_projects"
+md_authors = ["@tomsquest", "@vmaerten", "@manuelschneid3r", "@d3v2a"]
@dataclass
@@ -38,19 +55,30 @@ class Editor:
config_dir_prefix: str
binary: str
- def __init__(self, name: str, icon: Path, config_dir_prefix: str, binaries: list[str]):
+ # Rider calls recentProjects.xml -> recentSolutions.xml and in it RecentProjectsManager -> RiderRecentProjectsManager
+ is_rider: bool
+
+ def __init__(
+ self,
+ name: str,
+ icon: Path,
+ config_dir_prefix: str,
+ binaries: list[str],
+ is_rider = False):
self.name = name
self.icon = icon
self.config_dir_prefix = config_dir_prefix
self.binary = self._find_binary(binaries)
+ self.is_rider = is_rider
- def _find_binary(self, binaries: list[str]) -> Union[str, None]:
+ @staticmethod
+ def _find_binary(binaries: list[str]) -> Union[str, None]:
for binary in binaries:
if which(binary):
return binary
return None
- def list_projects(self) -> list[Project]:
+ def list_projects(self) -> List[Project]:
config_dir = Path.home() / ".config"
if platform == "darwin":
config_dir = Path.home() / "Library" / "Application Support"
@@ -59,24 +87,38 @@ def list_projects(self) -> list[Project]:
if not dirs:
return []
latest = sorted(dirs)[-1]
- return self._parse_recent_projects(Path(latest) / "options" / "recentProjects.xml")
+ if not self.is_rider:
+ recent_projects_xml = "recentProjects.xml"
+ else:
+ recent_projects_xml = "recentSolutions.xml"
+ return self._parse_recent_projects(Path(latest) / "options" / recent_projects_xml)
def _parse_recent_projects(self, recent_projects_file: Path) -> list[Project]:
try:
root = ElementTree.parse(recent_projects_file).getroot()
- entries = root.findall(".//component[@name='RecentProjectsManager']//entry[@key]")
+ if not self.is_rider:
+ entries = root.findall(".//component[@name='RecentProjectsManager']//entry[@key]")
+ else:
+ entries = root.findall(".//component[@name='RiderRecentProjectsManager']//entry[@key]")
projects = []
for entry in entries:
- project_path = entry.attrib["key"].replace("$USER_HOME$", str(Path.home()))
-
+ project_path = entry.attrib["key"]
+ project_path = project_path.replace("$USER_HOME$", str(Path.home()))
+ project_name = Path(project_path).name
+ files = Path(project_path + "/.idea").glob("*.iml")
tag_opened = entry.find(".//option[@name='projectOpenTimestamp']")
last_opened = tag_opened.attrib["value"] if tag_opened is not None and "value" in tag_opened.attrib else None
if project_path and last_opened:
projects.append(
- Project(name=Path(project_path).name, path=project_path, last_opened=int(last_opened))
+ Project(name=project_name, path=project_path, last_opened=int(last_opened))
)
+ for file in files:
+ name = file.name.replace(".iml", "")
+ if name != project_name:
+ projects.append(Project(name=name, path=project_path, last_opened=int(last_opened)))
+
return projects
except (ElementTree.ParseError, FileNotFoundError):
return []
@@ -87,96 +129,139 @@ class Plugin(PluginInstance, TriggerQueryHandler):
executables = []
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='project name',
- defaultTrigger='jb ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ self.fuzzy = False
+
+ self._match_path = self.readConfig('match_path', bool)
+ if self._match_path is None:
+ self._match_path = False
plugin_dir = Path(__file__).parent
editors = [
Editor(
name="Android Studio",
- icon=plugin_dir / "androidstudio.svg",
+ icon=plugin_dir / "icons" / "androidstudio.svg",
config_dir_prefix="Google/AndroidStudio",
binaries=["studio", "androidstudio", "android-studio", "android-studio-canary", "jdk-android-studio",
"android-studio-system-jdk"]),
+ Editor(
+ name="Aqua",
+ icon=plugin_dir / "icons" / "aqua.svg",
+ config_dir_prefix="JetBrains/Aqua",
+ binaries=["aqua", "aqua-eap"]),
Editor(
name="CLion",
- icon=plugin_dir / "clion.svg",
+ icon=plugin_dir / "icons" / "clion.svg",
config_dir_prefix="JetBrains/CLion",
binaries=["clion", "clion-eap"]),
Editor(
name="DataGrip",
- icon=plugin_dir / "datagrip.svg",
+ icon=plugin_dir / "icons" / "datagrip.svg",
config_dir_prefix="JetBrains/DataGrip",
binaries=["datagrip", "datagrip-eap"]),
Editor(
name="DataSpell",
- icon=plugin_dir / "dataspell.svg",
+ icon=plugin_dir / "icons" / "dataspell.svg",
config_dir_prefix="JetBrains/DataSpell",
binaries=["dataspell", "dataspell-eap"]),
Editor(
name="GoLand",
- icon=plugin_dir / "goland.svg",
+ icon=plugin_dir / "icons" / "goland.svg",
config_dir_prefix="JetBrains/GoLand",
binaries=["goland", "goland-eap"]),
Editor(
name="IntelliJ IDEA",
- icon=plugin_dir / "idea.svg",
+ icon=plugin_dir / "icons" / "idea.svg",
config_dir_prefix="JetBrains/IntelliJIdea",
binaries=["idea", "idea.sh", "idea-ultimate", "idea-ce-eap", "idea-ue-eap", "intellij-idea-ce",
"intellij-idea-ce-eap", "intellij-idea-ue-bundled-jre", "intellij-idea-ultimate-edition",
"intellij-idea-community-edition-jre", "intellij-idea-community-edition-no-jre"]),
Editor(
name="PhpStorm",
- icon=plugin_dir / "phpstorm.svg",
+ icon=plugin_dir / "icons" / "phpstorm.svg",
config_dir_prefix="JetBrains/PhpStorm",
binaries=["phpstorm", "phpstorm-eap"]),
Editor(
name="PyCharm",
- icon=plugin_dir / "pycharm.svg",
+ icon=plugin_dir / "icons" / "pycharm.svg",
config_dir_prefix="JetBrains/PyCharm",
- binaries=["charm", "pycharm", "pycharm-eap"]),
+ binaries=["charm", "pycharm", "pycharm-eap", "pycharm-professional"]),
Editor(
name="Rider",
- icon=plugin_dir / "rider.svg",
+ icon=plugin_dir / "icons" / "rider.svg",
config_dir_prefix="JetBrains/Rider",
- binaries=["rider", "rider-eap"]),
+ binaries=["rider", "rider-eap"],
+ is_rider=True),
Editor(
name="RubyMine",
- icon=plugin_dir / "rubymine.svg",
+ icon=plugin_dir / "icons" / "rubymine.svg",
config_dir_prefix="JetBrains/RubyMine",
binaries=["rubymine", "rubymine-eap", "jetbrains-rubymine", "jetbrains-rubymine-eap"]),
+ Editor(
+ name="RustRover",
+ icon=plugin_dir / "icons" / "rustrover.svg",
+ config_dir_prefix="JetBrains/RustRover",
+ binaries=["rustrover", "rustrover-eap"]),
Editor(
name="WebStorm",
- icon=plugin_dir / "webstorm.svg",
+ icon=plugin_dir / "icons" / "webstorm.svg",
config_dir_prefix="JetBrains/WebStorm",
binaries=["webstorm", "webstorm-eap"]),
+ Editor(
+ name="Writerside",
+ icon=plugin_dir / "icons" / "writerside.svg",
+ config_dir_prefix="JetBrains/Writerside",
+ binaries=["writerside", "writerside-eap"]),
]
self.editors = [e for e in editors if e.binary is not None]
- def handleTriggerQuery(self, query: TriggerQuery):
+ @property
+ def match_path(self):
+ return self._match_path
+
+ @match_path.setter
+ def match_path(self, value):
+ self._match_path = value
+ self.writeConfig('match_path', value)
+
+ def supportsFuzzyMatching(self):
+ return True
+
+ def setFuzzyMatching(self, enabled):
+ self.fuzzy = enabled
+
+ def defaultTrigger(self):
+ return "jb "
+
+ def handleTriggerQuery(self, query: Query):
editor_project_pairs = []
+
+ m = Matcher(query.string, MatchConfig(fuzzy=self.fuzzy))
+
for editor in self.editors:
- projects = editor.list_projects()
- projects = [p for p in projects if Path(p.path).exists()]
- projects = [p for p in projects if query.string.lower() in p.name.lower()]
- editor_project_pairs.extend([(editor, p) for p in projects])
+ for project in editor.list_projects():
+ if Path(project.path).exists():
+ if self._match_path:
+ if m.match(project.name, project.path):
+ editor_project_pairs.append((editor, project))
+ else:
+ if m.match(project.name):
+ editor_project_pairs.append((editor, project))
# sort by last opened
editor_project_pairs.sort(key=lambda pair: pair[1].last_opened, reverse=True)
- query.add([self._make_item(editor, project, query) for editor, project in editor_project_pairs])
+ query.add([self._make_item(editor, project) for editor, project in editor_project_pairs])
- def _make_item(self, editor: Editor, project: Project, query: TriggerQuery) -> Item:
+ @staticmethod
+ def _make_item(editor: Editor, project: Project) -> Item:
return StandardItem(
id="%s-%s-%s" % (editor.binary, project.path, project.last_opened),
text=project.name,
subtext=project.path,
- inputActionText=query.trigger + project.name,
+ inputActionText=project.name,
iconUrls=["file:" + str(editor.icon)],
actions=[
Action(
@@ -188,3 +273,19 @@ def _make_item(self, editor: Editor, project: Project, query: TriggerQuery) -> I
)
],
)
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'checkbox',
+ 'property': 'match_path',
+ 'label': 'Match path'
+ },
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ 'widget_properties': {
+ 'textFormat': 'Qt::MarkdownText'
+ }
+ }
+ ]
diff --git a/jetbrains_projects/androidstudio.svg b/jetbrains_projects/icons/androidstudio.svg
similarity index 100%
rename from jetbrains_projects/androidstudio.svg
rename to jetbrains_projects/icons/androidstudio.svg
diff --git a/jetbrains_projects/icons/aqua.svg b/jetbrains_projects/icons/aqua.svg
new file mode 100644
index 00000000..a2d7e161
--- /dev/null
+++ b/jetbrains_projects/icons/aqua.svg
@@ -0,0 +1,171 @@
+
diff --git a/jetbrains_projects/clion.svg b/jetbrains_projects/icons/clion.svg
similarity index 100%
rename from jetbrains_projects/clion.svg
rename to jetbrains_projects/icons/clion.svg
diff --git a/jetbrains_projects/datagrip.svg b/jetbrains_projects/icons/datagrip.svg
similarity index 100%
rename from jetbrains_projects/datagrip.svg
rename to jetbrains_projects/icons/datagrip.svg
diff --git a/jetbrains_projects/dataspell.svg b/jetbrains_projects/icons/dataspell.svg
similarity index 100%
rename from jetbrains_projects/dataspell.svg
rename to jetbrains_projects/icons/dataspell.svg
diff --git a/jetbrains_projects/goland.svg b/jetbrains_projects/icons/goland.svg
similarity index 100%
rename from jetbrains_projects/goland.svg
rename to jetbrains_projects/icons/goland.svg
diff --git a/jetbrains_projects/idea.svg b/jetbrains_projects/icons/idea.svg
similarity index 100%
rename from jetbrains_projects/idea.svg
rename to jetbrains_projects/icons/idea.svg
diff --git a/jetbrains_projects/phpstorm.svg b/jetbrains_projects/icons/phpstorm.svg
similarity index 100%
rename from jetbrains_projects/phpstorm.svg
rename to jetbrains_projects/icons/phpstorm.svg
diff --git a/jetbrains_projects/pycharm.svg b/jetbrains_projects/icons/pycharm.svg
similarity index 100%
rename from jetbrains_projects/pycharm.svg
rename to jetbrains_projects/icons/pycharm.svg
diff --git a/jetbrains_projects/rider.svg b/jetbrains_projects/icons/rider.svg
similarity index 100%
rename from jetbrains_projects/rider.svg
rename to jetbrains_projects/icons/rider.svg
diff --git a/jetbrains_projects/rubymine.svg b/jetbrains_projects/icons/rubymine.svg
similarity index 100%
rename from jetbrains_projects/rubymine.svg
rename to jetbrains_projects/icons/rubymine.svg
diff --git a/jetbrains_projects/icons/rustrover.svg b/jetbrains_projects/icons/rustrover.svg
new file mode 100644
index 00000000..bd5621c5
--- /dev/null
+++ b/jetbrains_projects/icons/rustrover.svg
@@ -0,0 +1,132 @@
+
diff --git a/jetbrains_projects/webstorm.svg b/jetbrains_projects/icons/webstorm.svg
similarity index 100%
rename from jetbrains_projects/webstorm.svg
rename to jetbrains_projects/icons/webstorm.svg
diff --git a/jetbrains_projects/icons/writerside.svg b/jetbrains_projects/icons/writerside.svg
new file mode 100644
index 00000000..087eed26
--- /dev/null
+++ b/jetbrains_projects/icons/writerside.svg
@@ -0,0 +1,9 @@
+
diff --git a/kill/__init__.py b/kill/__init__.py
index 0b259a23..c9c69231 100644
--- a/kill/__init__.py
+++ b/kill/__init__.py
@@ -1,28 +1,30 @@
-"""Unix 'kill' wrapper extension."""
+# -*- coding: utf-8 -*-
+# Copyright (c) 2022 Manuel Schneider
+# Copyright (c) 2022 Benedict Dudel
+# Copyright (c) 2022 Pete Hamlin
+
import os
from signal import SIGKILL, SIGTERM
from albert import *
-md_iid = '2.0'
-md_version = "1.3"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Kill Process"
md_description = "Kill processes"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/kill"
-md_maintainers = "@Pete-Hamlin"
-md_credits = "Original idea by Benedict Dudel & Manuel Schneider"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/kill"
+md_authors = ["@Pete-Hamlin", "@BenedictDwudel", "@ManuelSchneid3r"]
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='kill ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ def defaultTrigger(self):
+ return "kill "
def handleTriggerQuery(self, query):
if not query.isValid:
diff --git a/locate/__init__.py b/locate/__init__.py
index 29ed4c4c..55015ba8 100644
--- a/locate/__init__.py
+++ b/locate/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2022 Manuel Schneider
+# Copyright (c) 2022-2024 Manuel Schneider
"""
`locate` wrapper. Note that it is up to you to ensure that the locate database is \
@@ -13,25 +13,21 @@
from albert import *
-md_iid = '2.0'
-md_version = "1.9"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Locate"
md_description = "Find and open files using locate"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/locate"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/locate"
md_bin_dependencies = "locate"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='',
- defaultTrigger="'")
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.iconUrls = [
"xdg:preferences-system-search",
@@ -41,6 +37,12 @@ def __init__(self):
f"file:{Path(__file__).parent}/locate.svg"
]
+ def synopsis(self, query):
+ return ""
+
+ def defaultTrigger(self):
+ return "'"
+
def handleTriggerQuery(self, query):
if len(query.string) > 2:
diff --git a/pacman/__init__.py b/pacman/__init__.py
index 51cb829d..eb0e3e14 100644
--- a/pacman/__init__.py
+++ b/pacman/__init__.py
@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*-
-
-"""
-This plugin is a `pacman` (Arch Linux Package Manager) wrapper. You can update, search, install and remove \
-packages.
-"""
+# Copyright (c) 2024 Manuel Schneider
import subprocess
from time import sleep
@@ -11,12 +7,13 @@
from albert import Action, StandardItem, PluginInstance, TriggerQueryHandler, runTerminal, openUrl
-md_iid = '2.0'
-md_version = "1.8"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "PacMan"
md_description = "Search, install and remove packages"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/pacman"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/pacman"
+md_authors = "@ManuelSchneid3r"
md_bin_dependencies = ["pacman", "expac"]
@@ -25,26 +22,27 @@ class Plugin(PluginInstance, TriggerQueryHandler):
pkgs_url = "https://www.archlinux.org/packages/"
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='',
- defaultTrigger='pac ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.iconUrls = [
"xdg:archlinux-logo",
"xdg:system-software-install",
f"file:{pathlib.Path(__file__).parent}/arch.svg"
]
+ def synopsis(self, query):
+ return ""
+
+ def defaultTrigger(self):
+ return "pac "
+
def handleTriggerQuery(self, query):
stripped = query.string.strip()
# Update item on empty queries
if not stripped:
query.add(StandardItem(
- id="%s-update" % md_id,
+ id="%s-update" % self.id,
text="Pacman package manager",
subtext="Enter the package you are looking for or hit enter to update.",
iconUrls=self.iconUrls,
@@ -58,7 +56,7 @@ def handleTriggerQuery(self, query):
return
# avoid rate limiting
- for number in range(50):
+ for _ in range(50):
sleep(0.01)
if not query.isValid:
return
@@ -87,13 +85,14 @@ def handleTriggerQuery(self, query):
])
else:
actions.append(Action("inst", "Install", lambda n=pkg_name: runTerminal("sudo pacman -S %s" % n)))
+
actions.append(Action("pkg_url", "Show on packages.archlinux.org",
- lambda r=pkg_repo, n=pkg_name: openUrl(f"{r}/x86_64/{n}/")))
+ lambda r=pkg_repo, n=pkg_name: openUrl(f"{self.pkgs_url}{r}/x86_64/{n}/")))
if pkg_purl:
actions.append(Action("proj_url", "Show project website", lambda u=pkg_purl: openUrl(u)))
item = StandardItem(
- id="%s_%s_%s" % (md_id, pkg_repo, pkg_name),
+ id="%s_%s_%s" % (self.id, pkg_repo, pkg_name),
iconUrls=self.iconUrls,
text="%s %s [%s]" % (pkg_name, pkg_vers, pkg_repo),
subtext=f"{pkg_desc} [Installed]" if pkg_installed else f"{pkg_desc}",
@@ -106,7 +105,7 @@ def handleTriggerQuery(self, query):
query.add(items)
else:
query.add(StandardItem(
- id="%s-empty" % md_id,
+ id="%s-empty" % self.id,
text="Search on archlinux.org",
subtext="No results found in the local database",
iconUrls=self.iconUrls,
diff --git a/pacman/arch.svg b/pacman/arch.svg
index 61f55ed9..b95bef86 100644
--- a/pacman/arch.svg
+++ b/pacman/arch.svg
@@ -1,5 +1 @@
-
-
\ No newline at end of file
diff --git a/pass/__init__.py b/pass/__init__.py
index 5da2d5fc..2a0ce20a 100644
--- a/pass/__init__.py
+++ b/pass/__init__.py
@@ -1,16 +1,20 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2017 Benedict Dudel
+# Copyright (c) 2023 Max
+# Copyright (c) 2023 Pete-Hamlin
import fnmatch
import os
from albert import *
-md_iid = "2.1"
-md_version = "1.5"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Pass"
md_description = "Manage passwords in pass"
-md_bin_dependencies = ["pass"]
-md_maintainers = ["@Pete-Hamlin"]
md_license = "BSD-3"
+md_url = "https://github.com/albertlauncher/python/tree/main/pass"
+md_authors = ["@benedictdudel", "@maxmil", "@Pete-Hamlin"]
+md_bin_dependencies = ["pass"]
HOME_DIR = os.environ["HOME"]
PASS_DIR = os.environ.get("PASSWORD_STORE_DIR", os.path.join(HOME_DIR, ".password-store/"))
@@ -18,15 +22,8 @@
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(
- self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis="",
- defaultTrigger="pass ",
- )
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.iconUrls = ["xdg:dialog-password"]
self._use_otp = self.readConfig("use_otp", bool) or False
self._otp_glob = self.readConfig("otp_glob", str) or "*-otp.gpg"
@@ -51,6 +48,12 @@ def otp_glob(self, value):
self._otp_glob = value
self.writeConfig("otp_glob", value)
+ def defaultTrigger(self):
+ return "pass "
+
+ def synopsis(self, query):
+ return ""
+
def configWidget(self):
return [
{"type": "checkbox", "property": "use_otp", "label": "Enable pass OTP extension"},
diff --git a/pomodoro/__init__.py b/pomodoro/__init__.py
index a18b4455..39dc9b35 100644
--- a/pomodoro/__init__.py
+++ b/pomodoro/__init__.py
@@ -1,24 +1,23 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2022 Manuel Schneider
+# Copyright (c) 2024 Manuel Schneider
"""
-https://en.wikipedia.org/wiki/Pomodoro_Technique
+Wiki: [Pomodoro_Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique).
"""
-import subprocess
import threading
import time
from pathlib import Path
from albert import *
-md_iid = '2.0'
-md_version = "1.3"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Pomodoro"
md_description = "Set up a Pomodoro timer"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/pomodoro"
-md_maintainers = "@manuelschneid3r"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/pomodoro"
+md_authors = "@manuelschneid3r"
class PomodoroTimer:
@@ -26,22 +25,29 @@ class PomodoroTimer:
def __init__(self):
self.isBreak = True
self.timer = None
+ self.notification = None
+ self.remainingTillLongBreak = 0
+ self.endTime = 0
+ self.pomodoroDuration = 0
+ self.breakDuration = 0
+ self.longBreakDuration = 0
+ self.count = 0
def timeout(self):
if self.isBreak:
duration = self.pomodoroDuration * 60
self.timer = threading.Timer(duration, self.timeout)
self.endTime = time.time() + duration
- sendTrayNotification("PomodoroTimer", "Let's go to work!")
+ self.notification = Notification("PomodoroTimer", "Let's go to work!")
self.timer.start()
else:
self.remainingTillLongBreak -= 1
if self.remainingTillLongBreak == 0:
self.remainingTillLongBreak = self.count
- sendTrayNotification("PomodoroTimer", "Take a long break (%s min)" % self.longBreakDuration)
+ self.notification = Notification("PomodoroTimer", "Take a long break (%s min)" % self.longBreakDuration)
duration = self.longBreakDuration * 60
else:
- sendTrayNotification("PomodoroTimer", "Take a short break (%s min)" % self.breakDuration)
+ self.notification = Notification("PomodoroTimer", "Take a short break (%s min)" % self.breakDuration)
duration = self.breakDuration * 60
self.endTime = time.time() + duration
self.timer = threading.Timer(duration, self.timeout)
@@ -75,44 +81,56 @@ class Plugin(PluginInstance, TriggerQueryHandler):
default_pomodoro_count = 4
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='[duration [break duration [long break duration [count]]]]',
- defaultTrigger='pomo ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.pomodoro = PomodoroTimer()
self.iconUrls = [f"file:{Path(__file__).parent}/pomodoro.svg"]
+ def defaultTrigger(self):
+ return 'pomo '
+
+ def synopsis(self, query):
+ return '[duration [break duration [long break duration [count]]]]'
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ 'widget_properties': {'textFormat': 'Qt::MarkdownText'}
+ }
+ ]
+
def handleTriggerQuery(self, query):
item = StandardItem(
- id=md_id,
+ id=self.id(),
iconUrls=self.iconUrls,
- text=md_name
)
if self.pomodoro.isActive():
- item.actions = [Action("stop", "Stop", lambda p=self.pomodoro: p.stop())]
+ item.text = "Stop Pomodoro"
+ item.actions = [Action("stop", "Stop", lambda pomo=self.pomodoro: pomo.stop())]
if self.pomodoro.isBreak:
whatsNext = "Pomodoro"
else:
whatsNext = "Long break" if self.pomodoro.remainingTillLongBreak == 1 else "Short break"
- item.subtext = "Stop pomodoro (Next: %s at %s)" \
- % (whatsNext, time.strftime("%X", time.localtime(self.pomodoro.endTime)))
+ item.subtext = "%s at %s" % (whatsNext, time.strftime("%X", time.localtime(self.pomodoro.endTime)))
query.add(item)
else:
tokens = query.string.split()
if len(tokens) > 4 or not all([t.isdigit() for t in tokens]):
- item.subtext = "Invalid parameters. Use %s" % self.synopsis()
+ item.text = "Invalid parameters"
+ item.subtext = "Use %s" % self.synopsis
query.add(item)
else:
p = int(tokens[0]) if len(tokens) > 0 else self.default_pomodoro_duration
b = int(tokens[1]) if len(tokens) > 1 else self.default_break_duration
lb = int(tokens[2]) if len(tokens) > 2 else self.default_longbreak_duration
c = int(tokens[3]) if len(tokens) > 3 else self.default_pomodoro_count
- item.subtext = "Start new pomodoro timer (%s min, break %s min, long break %s min, count %s)"\
- % (p, b, lb, c)
- item.actions = [Action("start", "Start", lambda p=p, b=b, lb=lb, c=c: self.pomodoro.start(p, b, lb, c))]
+
+ item.text = "Start Pomodoro"
+ item.subtext = f"{p} min, break {b} min, long break {lb} min, count {c}"
+ item.actions = [Action("start", "Start",
+ lambda _p=p, _b=b, _lb=lb, _c=c: self.pomodoro.start(_p, _b, _lb, _c))]
query.add(item)
diff --git a/pomodoro/pomodoro.svg b/pomodoro/pomodoro.svg
index 29cc53cf..fea164ad 100644
--- a/pomodoro/pomodoro.svg
+++ b/pomodoro/pomodoro.svg
@@ -1,5 +1 @@
-
-
\ No newline at end of file
diff --git a/python_eval/__init__.py b/python_eval/__init__.py
index 86f86d16..1acce6c4 100644
--- a/python_eval/__init__.py
+++ b/python_eval/__init__.py
@@ -1,31 +1,32 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2014 Manuel Schneider
-from builtins import pow
-from math import *
from pathlib import Path
from albert import *
-md_iid = '2.0'
-md_version = "1.5"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "Python Eval"
md_description = "Evaluate Python code"
md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/python_eval"
+md_url = "https://github.com/albertlauncher/python/tree/main/python_eval"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='',
- defaultTrigger='py ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.iconUrls = [f"file:{Path(__file__).parent}/python.svg"]
+ def synopsis(self, query):
+ return ""
+
+ def defaultTrigger(self):
+ return "py "
+
def handleTriggerQuery(self, query):
stripped = query.string.strip()
if stripped:
@@ -37,7 +38,7 @@ def handleTriggerQuery(self, query):
result_str = str(result)
query.add(StandardItem(
- id=md_id,
+ id=self.id(),
text=result_str,
subtext=type(result).__name__,
inputActionText=query.trigger + result_str,
diff --git a/syncthing/__init__.py b/syncthing/__init__.py
new file mode 100644
index 00000000..6fde0df1
--- /dev/null
+++ b/syncthing/__init__.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
+
+import json
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+from albert import *
+
+md_iid = "3.0"
+md_version = "3.0"
+md_name = "Syncthing"
+md_description = "Control the local Syncthing instance."
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/syncthing"
+md_authors = "@manuelschneid3r"
+
+
+# https://docs.syncthing.net/dev/rest.html
+class Syncthing:
+ def __init__(self, api_key, base_url="http://localhost:8384"):
+ self.api_key = api_key
+ self.base_url = base_url
+
+ def _request(self, method, endpoint, data=None) -> dict:
+ url = f"{self.base_url}{endpoint}"
+ headers = {
+ "X-API-Key": self.api_key,
+ "Content-Type": "application/json"
+ }
+ body = json.dumps(data).encode("utf-8") if data else None
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
+
+ try:
+ with urllib.request.urlopen(req) as resp:
+ if not (200 <= resp.status < 300):
+ raise Exception(f"Unexpected status {resp.status}")
+ content = resp.read().decode()
+ return json.loads(content) if content else {}
+ except urllib.error.HTTPError as e:
+ raise Exception(f"HTTP {e.code}: {e.read().decode()}")
+
+ def _get(self, endpoint):
+ return self._request("GET", endpoint)
+
+ def _post(self, endpoint, data=None):
+ return self._request("POST", endpoint, data)
+
+ def _patch(self, endpoint, data=None):
+ return self._request("PATCH", endpoint, data)
+
+ def config(self):
+ return self._get('/rest/config')
+
+ def resumeDevice(self, device_id:str):
+ return self._patch(f'/rest/config/devices/{device_id}', {'paused': False})
+
+ def pauseDevice(self, device_id:str):
+ return self._patch(f'/rest/config/devices/{device_id}', {'paused': True})
+
+ def resumeFolder(self, folder_id:str):
+ return self._patch(f'/rest/config/folders/{folder_id}', {'paused': False})
+
+ def pauseFolder(self, folder_id:str):
+ return self._patch(f'/rest/config/folders/{folder_id}', {'paused': True})
+
+ def scanFolder(self, folder_id:str):
+ return self._post(f'/rest/db/scan?folder={folder_id}')
+
+
+class Plugin(PluginInstance, GlobalQueryHandler):
+
+ config_key = 'syncthing_api_key'
+ icon_urls_active = [f"file:{Path(__file__).parent}/syncthing_active.svg"]
+ icon_urls_inactive = [f"file:{Path(__file__).parent}/syncthing_inactive.svg"]
+
+ def __init__(self):
+ PluginInstance.__init__(self)
+ GlobalQueryHandler.__init__(self)
+ self.st = Syncthing(self.readConfig(self.config_key, str) or '')
+
+ def defaultTrigger(self):
+ return 'st '
+
+ @property
+ def api_key(self) -> str:
+ return self.st.api_key
+
+ @api_key.setter
+ def api_key(self, value: str):
+ if self.st.api_key != value:
+ self.st.api_key = value
+ self.writeConfig(self.config_key, value)
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'lineedit',
+ 'property': 'api_key',
+ 'label': 'API key',
+ 'widget_properties': {'tooltip': 'You can find the API key using the web frontend.'}
+ }
+ ]
+
+ def handleTriggerQuery(self, query):
+ try:
+ super().handleTriggerQuery(query)
+ except Exception as e:
+ query.add(StandardItem(id="err", text="Error", subtext=str(e), iconUrls=self.icon_urls_active))
+
+ def handleGlobalQuery(self, query):
+
+ config = self.st.config()
+
+ devices = dict()
+ for d in config['devices']:
+ if not d['name']:
+ d['name'] = d['deviceID']
+ d['_shared_folders'] = {}
+ devices[d['deviceID']] = d
+
+ folders = dict()
+ for f in config['folders']:
+ if not f['label']:
+ f['label'] = f['id']
+ for d in f['devices']:
+ devices[d['deviceID']]['_shared_folders'][f['id']] = f
+ folders[f['id']] = f
+
+ results = []
+ matcher = Matcher(query.string)
+
+ # create device items
+ for device_id, d in devices.items():
+ device_name = d['name']
+
+ if match := matcher.match(device_name):
+ device_folders = ", ".join([f['label'] for f in d['_shared_folders'].values()])
+
+ actions = []
+ if d['paused']:
+ actions.append(Action("resume", "Resume", lambda did=device_id: self.st.resumeDevice(did)))
+ else:
+ actions.append(Action("pause", "Pause", lambda did=device_id: self.st.pauseDevice(did)))
+
+ item = StandardItem(
+ id=device_id,
+ text=f"{device_name}",
+ subtext=f"{'PAUSED · ' if d['paused'] else ''}Device · "
+ f"Shared: {device_folders if device_folders else 'Nothing'}.",
+ iconUrls=self.icon_urls_inactive if d['paused'] else self.icon_urls_active,
+ actions=actions
+ )
+
+ results.append(RankItem(item, match))
+
+ # create folder items
+ for folder_id, f in folders.items():
+ folder_name = f['label']
+ if match := matcher.match(folder_name):
+ folders_devices = ", ".join([devices[d['deviceID']]['name'] for d in f['devices']])
+
+ actions = []
+ if f['paused']:
+ actions.append(Action("resume", "Resume", lambda fid=folder_id: self.st.resumeFolder(fid)))
+ else:
+ actions.append(Action("pause", "Pause", lambda fid=folder_id: self.st.pauseFolder(fid)))
+ actions.append(Action("open", "Open", lambda p=f['path']: openFile(p)))
+ actions.append(Action("scan", "Scan", lambda fid=folder_id: self.st.scanFolder(fid)))
+
+ item = StandardItem(
+ id=folder_id,
+ text=folder_name,
+ subtext=f"{'PAUSED · ' if f['paused'] else ''}Folder · {f['path']} · "
+ f"Shared with {folders_devices if folders_devices else 'nobody'}.",
+ iconUrls=self.icon_urls_inactive if f['paused'] else self.icon_urls_active,
+ actions=actions
+ )
+ results.append(RankItem(item, match))
+
+ return results
diff --git a/syncthing/syncthing_active.svg b/syncthing/syncthing_active.svg
new file mode 100644
index 00000000..b56b7cbc
--- /dev/null
+++ b/syncthing/syncthing_active.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/syncthing/syncthing_inactive.svg b/syncthing/syncthing_inactive.svg
new file mode 100644
index 00000000..247b4d43
--- /dev/null
+++ b/syncthing/syncthing_inactive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tex_to_unicode/__init__.py b/tex_to_unicode/__init__.py
index f28fabdd..629e4584 100644
--- a/tex_to_unicode/__init__.py
+++ b/tex_to_unicode/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-
-# Copyright (c) 2022 Manuel Schneider
+# Copyright (c) 2022 Jonah Lawrence
+# Copyright (c) 2024 Manuel Schneider
import re
import unicodedata
@@ -9,30 +9,25 @@
from albert import *
-md_iid = '2.0'
-md_version = "1.2"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "TeX to Unicode"
md_description = "Convert TeX mathmode commands to unicode characters"
-md_license = "GPL-3.0"
-md_url = "https://github.com/albertlauncher/python/"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/tex_to_unicode"
+md_authors = ["@DenverCoder1", "@manuelschneid3r"]
md_lib_dependencies = "pylatexenc"
-md_maintainers = "@DenverCoder1"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='',
- defaultTrigger='tex ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.COMBINING_LONG_SOLIDUS_OVERLAY = "\u0338"
- self.iconUrls = [f"file:{Path(__file__).parent}/tex.png"]
+ self.iconUrls = [f"file:{Path(__file__).parent}/tex.svg"]
- def _create_item(self, text: str, subtext: str, can_copy: bool) -> Item:
+ def _create_item(self, text: str, subtext: str, can_copy: bool):
actions = []
if can_copy:
actions.append(
@@ -43,14 +38,17 @@ def _create_item(self, text: str, subtext: str, can_copy: bool) -> Item:
)
)
return StandardItem(
- id=md_id,
+ id=self.id(),
text=text,
subtext=subtext,
iconUrls=self.iconUrls,
actions=actions,
)
- def handleTriggerQuery(self, query: TriggerQuery) -> None:
+ def defaultTrigger(self):
+ return "tex "
+
+ def handleTriggerQuery(self, query):
stripped = query.string.strip()
if not stripped:
diff --git a/tex_to_unicode/tex.png b/tex_to_unicode/tex.png
deleted file mode 100644
index 49379b81..00000000
Binary files a/tex_to_unicode/tex.png and /dev/null differ
diff --git a/tex_to_unicode/tex.svg b/tex_to_unicode/tex.svg
new file mode 100644
index 00000000..86ddf19b
--- /dev/null
+++ b/tex_to_unicode/tex.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/timer/time.svg b/timer/time.svg
deleted file mode 100644
index 0f675ad2..00000000
--- a/timer/time.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/translators/__init__.py b/translators/__init__.py
index 3f1f9b3d..45541e8b 100644
--- a/translators/__init__.py
+++ b/translators/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
"""
Translates text using the python package translators. See https://pypi.org/project/translators/
@@ -11,25 +12,22 @@
from albert import *
import translators as ts
-md_iid = '2.0'
-md_version = "1.3"
+md_iid = "3.0"
+md_version = "2.1"
md_name = "Translator"
-md_description = "Translate sentences using 'translators' package"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/translators"
+md_description = "Translate text using online translators"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/translators"
+md_authors = "@manuelschneid3r"
md_lib_dependencies = "translators"
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis="[[from] to] text",
- defaultTrigger='tr ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
self.iconUrls = [f"file:{Path(__file__).parent}/google_translate.png"]
self._translator = self.readConfig('translator', str)
@@ -68,8 +66,15 @@ def lang(self, value):
self._lang = value
self.writeConfig('lang', value)
+ def defaultTrigger(self):
+ return 'tr '
+
def configWidget(self):
return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ },
{
'type': 'combobox',
'property': 'translator',
@@ -83,6 +88,9 @@ def configWidget(self):
}
]
+ def synopsis(self, s):
+ return "[[from] to] text"
+
def handleTriggerQuery(self, query):
stripped = query.string.strip()
if stripped:
@@ -99,22 +107,42 @@ def handleTriggerQuery(self, query):
else:
src, dst, text = 'auto', self.lang, stripped
- translation = ts.translate_text(query_text=text,
- translator=self.translator,
- from_language=src,
- to_language=dst)
-
- query.add(StandardItem(
- id=md_id,
- text=translation,
- subtext=f"{src.upper()} > {dst.upper()}",
- iconUrls=self.iconUrls,
- actions=[
- Action(
- "paste", "Copy to clipboard and paste to front-most window",
- lambda t=translation: setClipboardTextAndPaste(t)
- ),
+ try:
+ translation = ts.translate_text(query_text=text,
+ translator=self.translator,
+ from_language=src,
+ to_language=dst,
+ timeout=5)
+
+ actions = []
+ if havePasteSupport():
+ actions.append(
+ Action(
+ "paste", "Copy to clipboard and paste to front-most window",
+ lambda t=translation: setClipboardTextAndPaste(t)
+ )
+ )
+
+ actions.append(
Action("copy", "Copy to clipboard",
lambda t=translation: setClipboardText(t))
- ]
- ))
+ )
+
+ query.add(StandardItem(
+ id=self.id(),
+ text=translation,
+ subtext=f"{src.upper()} > {dst.upper()}",
+ iconUrls=self.iconUrls,
+ actions=actions
+ ))
+
+ except Exception as e:
+
+ query.add(StandardItem(
+ id=self.id(),
+ text="Error",
+ subtext=str(e),
+ iconUrls=self.iconUrls
+ ))
+
+ warning(str(e))
\ No newline at end of file
diff --git a/.archive/unit_converter/__init__.py b/unit_converter/__init__.py
similarity index 85%
rename from .archive/unit_converter/__init__.py
rename to unit_converter/__init__.py
index 462dad4f..cd26cf22 100644
--- a/.archive/unit_converter/__init__.py
+++ b/unit_converter/__init__.py
@@ -12,31 +12,27 @@
- `convert 100 USD to EUR`
"""
-
-from __future__ import annotations
-
import json
import re
import traceback
from datetime import datetime
from pathlib import Path
-from typing import Any
+from typing import Any, Optional
from urllib.error import URLError
from urllib.request import urlopen
-import albert
import inflect
import pint
+from albert import *
-
-md_iid = '2.0'
-md_version = "1.4"
+md_iid = "3.0"
+md_version = "1.8"
md_name = "Unit Converter"
md_description = "Convert between units"
md_license = "MIT"
-md_url = "https://github.com/albertlauncher/python"
+md_url = "https://github.com/albertlauncher/python/tree/main/unit_converter"
md_lib_dependencies = ["pint", "inflect"]
-md_maintainers = "@DenverCoder1"
+md_authors = ["@DenverCoder1", "@Pete-Hamlin"]
class ConversionResult:
@@ -97,7 +93,8 @@ def __display_unit_name(self, amount: float, unit: str) -> str:
unit = self.__pluralize_unit(unit) if amount != 1 else unit
return self.display_names.get(unit, unit)
- def __format_float(self, num: float) -> str:
+ @staticmethod
+ def __format_float(num: float) -> str:
"""Format a float to remove trailing zeros and avoid scientific notation
Args:
@@ -129,7 +126,7 @@ def formatted_from(self) -> str:
def icon(self) -> str:
"""Return the icon for the result's dimensionality"""
# strip characters from the dimensionality if not alphanumeric or underscore
- dimensionality = re.sub(r"[^\w]", "", self.dimensionality)
+ dimensionality = re.sub(r"\W", "", self.dimensionality)
return f"{dimensionality}.svg"
def __repr__(self):
@@ -250,15 +247,15 @@ def _get_currencies(self) -> dict[str, float]:
with urlopen(self.API_URL) as response:
data = json.loads(response.read().decode("utf-8"))
if not data or "rates" not in data:
- albert.info("No currencies found")
+ info("No currencies found")
return {}
- albert.info(f'Currencies updated')
+ info(f"Currencies updated")
return data["rates"]
except URLError as error:
- albert.warning(f"Error getting currencies: {error}")
+ warning(f"Error getting currencies: {error}")
return {}
- def get_currency(self, currency: str) -> str | None:
+ def get_currency(self, currency: str) -> Optional[str]:
"""Get the currency name normalized using aliases and capitalization
Args:
@@ -309,14 +306,9 @@ def convert(self, amount: float, from_unit: str, to_unit: str) -> ConversionResu
)
-class Plugin(albert.TriggerQueryHandler):
+class Plugin(PluginInstance, GlobalQueryHandler):
"""The plugin class"""
- unit_convert_regex = re.compile(
- r"(?P-?\d+\.?\d*)\s?(?P.*)\s(?:to|in)\s(?P.*)",
- re.I,
- )
-
config: dict[str, Any] = {
# Maximum number of decimal places for precision
"precision": 12,
@@ -351,47 +343,50 @@ class Plugin(albert.TriggerQueryHandler):
},
}
- def initialize(self):
+ def __init__(self):
+ PluginInstance.__init__(self)
+ GlobalQueryHandler.__init__(self)
+
+ self.unit_convert_regex = re.compile(
+ r"(?P-?\d+\.?\d*)\s?(?P.*)\s(?:to|in)\s(?P.*)",
+ re.I,
+ )
self.unit_converter = StandardUnitConverter()
self.currency_converter = CurrencyConverter()
- def id(self) -> str:
- return md_id
-
- def name(self) -> str:
- return md_name
-
- def description(self) -> str:
- return md_description
+ def defaultTrigger(self):
+ return "convert "
- def synopsis(self) -> str:
+ def synopsis(self, query):
return " to "
- def defaultTrigger(self) -> str:
- return "convert "
+ def handleTriggerQuery(self, query: Query) -> None:
+ if query_string := query.string.strip():
+ items = self.match_query(query_string)
+ query.add(items)
+
+ def handleGlobalQuery(self, query):
+ return [RankItem(item=item, score=1) for item in self.match_query(query.string.strip())]
- def handleTriggerQuery(self, query: albert.TriggerQuery) -> None:
- query_string = query.string.strip()
+ def match_query(self, query_string: str):
match = self.unit_convert_regex.fullmatch(query_string)
if match:
- albert.info(f"Matched {query_string}")
try:
- items = self._get_items(
+ return self._get_items(
float(match.group("from_amount")),
match.group("from_unit").strip(),
match.group("to_unit").strip(),
)
- query.add(items)
except Exception as error:
- albert.warning(f"Error: {error}")
- tb = "".join(
- traceback.format_exception(error.__class__, error, error.__traceback__)
- )
- albert.warning(tb)
- albert.info("Something went wrong. Make sure you're using the correct format.")
+ warning(f"Error: {error}")
+ tb = "".join(traceback.format_exception(error.__class__, error, error.__traceback__))
+ warning(tb)
+ info("Something went wrong. Make sure you're using the correct format.")
+ return []
- def _create_item(self, text: str, subtext: str, icon: str = "") -> albert.Item:
- """Create an albert.Item from a text and subtext
+ @staticmethod
+ def _create_item(text: str, subtext: str, icon: str = "") -> Item:
+ """Create an Item from a text and subtext
Args:
text (str): The text to display
@@ -399,22 +394,22 @@ def _create_item(self, text: str, subtext: str, icon: str = "") -> albert.Item:
icon (Optional[str]): The icon to display. If not specified, the default icon will be used
Returns:
- albert.Item: The item to be added to the list of results
+ Item: The item to be added to the list of results
"""
icon_path = Path(__file__).parent / "icons" / icon
if not icon or not icon_path.exists():
- albert.warning(f"Icon {icon} does not exist")
+ warning(f"Icon {icon} does not exist")
icon_path = Path(__file__).parent / "icons" / "unit_converter.svg"
- return albert.StandardItem(
+ return StandardItem(
id=str(icon_path),
iconUrls=["file:" + str(icon_path)],
text=text,
subtext=subtext,
actions=[
- albert.Action(
+ Action(
id="copy",
text="Copy result to clipboard",
- callable=lambda: albert.setClipboardText(text=text),
+ callable=lambda: setClipboardText(text=text),
)
],
)
@@ -436,7 +431,7 @@ def _get_converter(self, from_unit: str, to_unit: str) -> UnitConverter:
return self.currency_converter
return self.unit_converter
- def _get_items(self, amount: float, from_unit: str, to_unit: str) -> list[albert.Item]:
+ def _get_items(self, amount: float, from_unit: str, to_unit: str) -> list[Item]:
"""Generate the Albert items to display for the query
Args:
@@ -445,7 +440,7 @@ def _get_items(self, amount: float, from_unit: str, to_unit: str) -> list[albert
to_unit (str): The unit to convert to
Returns:
- List[albert.Item]: The list of items to display
+ List[Item]: The list of items to display
"""
try:
converter = self._get_converter(from_unit, to_unit)
@@ -459,13 +454,11 @@ def _get_items(self, amount: float, from_unit: str, to_unit: str) -> list[albert
)
]
except pint.errors.DimensionalityError as e:
- albert.warning(f"DimensionalityError: {e}")
- return [
- self._create_item(f"Unable to convert {amount} {from_unit} to {to_unit}", str(e))
- ]
+ warning(f"DimensionalityError: {e}")
+ return [self._create_item(f"Unable to convert {amount} {from_unit} to {to_unit}", str(e))]
except pint.errors.UndefinedUnitError as e:
- albert.warning(f"UndefinedUnitError: {e}")
+ warning(f"UndefinedUnitError: {e}")
return []
except UnknownCurrencyError as e:
- albert.warning(f"UnknownCurrencyError: {e}")
+ warning(f"UnknownCurrencyError: {e}")
return []
diff --git a/.archive/unit_converter/icons/currency.svg b/unit_converter/icons/currency.svg
similarity index 100%
rename from .archive/unit_converter/icons/currency.svg
rename to unit_converter/icons/currency.svg
diff --git a/.archive/unit_converter/icons/current.svg b/unit_converter/icons/current.svg
similarity index 100%
rename from .archive/unit_converter/icons/current.svg
rename to unit_converter/icons/current.svg
diff --git a/.archive/unit_converter/icons/length.svg b/unit_converter/icons/length.svg
similarity index 100%
rename from .archive/unit_converter/icons/length.svg
rename to unit_converter/icons/length.svg
diff --git a/.archive/unit_converter/icons/lengthtime.svg b/unit_converter/icons/lengthtime.svg
similarity index 100%
rename from .archive/unit_converter/icons/lengthtime.svg
rename to unit_converter/icons/lengthtime.svg
diff --git a/.archive/unit_converter/icons/luminosity.svg b/unit_converter/icons/luminosity.svg
similarity index 100%
rename from .archive/unit_converter/icons/luminosity.svg
rename to unit_converter/icons/luminosity.svg
diff --git a/.archive/unit_converter/icons/mass.svg b/unit_converter/icons/mass.svg
similarity index 100%
rename from .archive/unit_converter/icons/mass.svg
rename to unit_converter/icons/mass.svg
diff --git a/.archive/unit_converter/icons/printing_unit.svg b/unit_converter/icons/printing_unit.svg
similarity index 100%
rename from .archive/unit_converter/icons/printing_unit.svg
rename to unit_converter/icons/printing_unit.svg
diff --git a/.archive/unit_converter/icons/substance.svg b/unit_converter/icons/substance.svg
similarity index 100%
rename from .archive/unit_converter/icons/substance.svg
rename to unit_converter/icons/substance.svg
diff --git a/.archive/unit_converter/icons/temperature.svg b/unit_converter/icons/temperature.svg
similarity index 100%
rename from .archive/unit_converter/icons/temperature.svg
rename to unit_converter/icons/temperature.svg
diff --git a/.archive/unit_converter/icons/time.svg b/unit_converter/icons/time.svg
similarity index 100%
rename from .archive/unit_converter/icons/time.svg
rename to unit_converter/icons/time.svg
diff --git a/.archive/unit_converter/icons/unit_converter.svg b/unit_converter/icons/unit_converter.svg
similarity index 100%
rename from .archive/unit_converter/icons/unit_converter.svg
rename to unit_converter/icons/unit_converter.svg
diff --git a/virtualbox/__init__.py b/virtualbox/__init__.py
index 94b21015..ad39ebbf 100644
--- a/virtualbox/__init__.py
+++ b/virtualbox/__init__.py
@@ -1,17 +1,23 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
+"""
+This plugin is based on [virtualbox-python](https://pypi.org/project/virtualbox/) and needs the 'vboxapi' module which
+is part of the VirtualBox SDK. Some distributions package the SDK, e.g. Arch has
+[virtualbox-sdk](https://archlinux.org/packages/extra/x86_64/virtualbox-sdk/).
+"""
import virtualbox
from virtualbox.library import LockType, MachineState
from albert import *
-md_iid = '2.0'
-md_version = "1.5"
+md_iid = "3.0"
+md_version = "2.0"
md_name = "VirtualBox"
md_description = "Manage your VirtualBox machines"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/virtualbox"
-md_maintainers = "@manuelschneid3r"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/virtualbox"
+md_authors = "@manuelschneid3r"
md_lib_dependencies = ['virtualbox']
@@ -57,15 +63,27 @@ def pauseVm(vm):
class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- synopsis='',
- defaultTrigger='vbox ')
- PluginInstance.__init__(self, extensions=[self])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
self.iconUrls = ["xdg:virtualbox", ":unknown"]
+ def defaultTrigger(self):
+ return 'vbox '
+
+ def synopsis(self, query):
+ return ""
+
+ def configWidget(self):
+ return [
+ {
+ 'type': 'label',
+ 'text': __doc__.strip(),
+ 'widget_properties': {
+ 'textFormat': 'Qt::MarkdownText'
+ }
+ }
+ ]
+
def handleTriggerQuery(self, query):
items = []
pattern = query.string.strip().lower()
@@ -73,17 +91,17 @@ def handleTriggerQuery(self, query):
for vm in filter(lambda vm: pattern in vm.name.lower(), virtualbox.VirtualBox().machines):
actions = []
if vm.state == MachineState.powered_off or vm.state == MachineState.aborted: # 1 # 4
- actions.append(Action("startvm", "Start virtual machine", lambda vm=vm: startVm(vm)))
+ actions.append(Action("startvm", "Start virtual machine", lambda m=vm: startVm(m)))
if vm.state == MachineState.saved: # 2
- actions.append(Action("restorevm", "Start saved virtual machine", lambda vm=vm: startVm(vm)))
- actions.append(Action("discardvm", "Discard saved state", lambda vm=vm: discardSavedVm(vm)))
+ actions.append(Action("restorevm", "Start saved virtual machine", lambda m=vm: startVm(m)))
+ actions.append(Action("discardvm", "Discard saved state", lambda m=vm: discardSavedVm(m)))
if vm.state == MachineState.running: # 5
- actions.append(Action("savevm", "Save virtual machine", lambda vm=vm: saveVm(vm)))
- actions.append(Action("poweroffvm", "Power off via ACPI event (Power button)", lambda vm=vm: acpiPowerVm(vm)))
- actions.append(Action("stopvm", "Turn off virtual machine", lambda vm=vm: stopVm(vm)))
- actions.append(Action("pausevm", "Pause virtual machine", lambda vm=vm: pauseVm(vm)))
+ actions.append(Action("savevm", "Save virtual machine", lambda m=vm: saveVm(m)))
+ actions.append(Action("poweroffvm", "Power off via ACPI event (Power button)", lambda m=vm: acpiPowerVm(m)))
+ actions.append(Action("stopvm", "Turn off virtual machine", lambda m=vm: stopVm(m)))
+ actions.append(Action("pausevm", "Pause virtual machine", lambda m=vm: pauseVm(m)))
if vm.state == MachineState.paused: # 6
- actions.append(Action("resumevm", "Resume virtual machine", lambda vm=vm: resumeVm(vm)))
+ actions.append(Action("resumevm", "Resume virtual machine", lambda m=vm: resumeVm(m)))
items.append(
StandardItem(
diff --git a/vscode_projects/__init__.py b/vscode_projects/__init__.py
new file mode 100644
index 00000000..7ed263c9
--- /dev/null
+++ b/vscode_projects/__init__.py
@@ -0,0 +1,550 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Sharsie
+
+import os
+import json
+from pathlib import Path
+from dataclasses import dataclass
+from albert import *
+
+md_iid = "3.0"
+md_version = "1.10"
+md_name = "VSCode projects"
+md_description = "Open VSCode projects"
+md_url = "https://github.com/albertlauncher/python/tree/master/vscode_projects"
+md_license = "MIT"
+md_bin_dependencies = ["code"]
+md_authors = ["@Sharsie"]
+
+@dataclass
+class Project:
+ displayName: str
+ name: str
+ path: str
+ tags: list[str]
+
+
+@dataclass
+class SearchResult:
+ project: Project
+ # priority is used to sort returned results
+ priority: int
+ # sortIndex is a decision maker when two search results have same priority
+ sortIndex: int
+
+
+@dataclass
+class CachedConfig:
+ projects: list[Project]
+ mTime: float
+
+
+class Plugin(PluginInstance, TriggerQueryHandler):
+ # Possible locations for Code configuration
+ _configStoragePaths = [
+ os.path.join(os.environ["HOME"], ".config/Code/storage.json"),
+ os.path.join(os.environ["HOME"],
+ ".config/Code/User/globalStorage/storage.json"),
+ ]
+
+ # Possible locations for Project Manager extension configuration
+ _configProjectManagerPaths = [
+ os.path.join(
+ os.environ["HOME"], ".config/Code/User/globalStorage/alefragnani.project-manager/projects.json")
+ ]
+
+ # Indicates whether results from the Recent list in VSCode should be searched
+ _recentEnabled = True
+
+ # Indicates whether projects from Project Manager extension should be searched
+ _projectManagerEnabled = False
+
+ # Defines sorting priorities for results
+ _sortPriority = {
+ "PMName": 1,
+ "PMPath": 5,
+ "PMTag": 10,
+ "Recent": 15
+ }
+
+ # Holds cached data from the json configurations
+ _configCache: dict[str, CachedConfig] = {}
+
+ # Overrides the command to open projects
+ _terminalCommand = ""
+
+ # Setting indicating whether results from the Recent list in VSCode should be searched
+ @property
+ def recentEnabled(self):
+ return self._recentEnabled
+
+ @recentEnabled.setter
+ def recentEnabled(self, value):
+ self._recentEnabled = value
+ self.writeConfig("recentEnabled", value)
+
+ # Setting indicating whether projects in Project Manager extension should be searched
+ @property
+ def projectManagerEnabled(self):
+ return self._projectManagerEnabled
+
+ @projectManagerEnabled.setter
+ def projectManagerEnabled(self, value):
+ self._projectManagerEnabled = value
+ self.writeConfig("projectManagerEnabled", value)
+
+ found = False
+ for p in self._configProjectManagerPaths:
+ if os.path.exists(p):
+ found = True
+ break
+
+ if found == False:
+ warning(
+ "Project Manager search was enabled, but configuration file was not found")
+ notif = Notification(
+ title=self.name,
+ text=f"Configuration file was not found for the Project Manager extension. Please make sure the extension is installed."
+ )
+ notif.send()
+
+ # Priority settings for project manager results using name search
+ @property
+ def priorityPMName(self):
+ return self._sortPriority["PMName"]
+
+ @priorityPMName.setter
+ def priorityPMName(self, value):
+ self._sortPriority["PMName"] = value
+ self.writeConfig("priorityPMName", value)
+
+ # Priority settings for project manager results using path search
+ @property
+ def priorityPMPath(self):
+ return self._sortPriority["PMPath"]
+
+ @priorityPMPath.setter
+ def priorityPMPath(self, value):
+ self._sortPriority["PMPath"] = value
+ self.writeConfig("priorityPMPath", value)
+
+ # Priority settings for project manager results using tag search
+ @property
+ def priorityPMTag(self):
+ return self._sortPriority["PMTag"]
+
+ @priorityPMTag.setter
+ def priorityPMTag(self, value):
+ self._sortPriority["PMTag"] = value
+ self.writeConfig("priorityPMTag", value)
+
+ # Priority settings for recently opened files
+ @property
+ def priorityRecent(self):
+ return self._sortPriority["Recent"]
+
+ @priorityRecent.setter
+ def priorityRecent(self, value):
+ self._sortPriority["Recent"] = value
+ self.writeConfig("priorityRecent", value)
+
+ # Setting for custom command when opening resulted items
+ @property
+ def terminalCommand(self):
+ return self._terminalCommand
+
+ @terminalCommand.setter
+ def terminalCommand(self, value):
+ self._terminalCommand = value
+ self.writeConfig("terminalCommand", value)
+
+ def defaultTrigger(self):
+ return "code "
+
+ def synopsis(self, query):
+ return "project name or path"
+
+ def __init__(self):
+ self.iconUrls = [f"file:{Path(__file__).parent}/icon.svg"]
+
+ PluginInstance.__init__(self)
+
+ TriggerQueryHandler.__init__(self)
+
+ configFound = False
+
+ for p in self._configStoragePaths:
+ if os.path.exists(p):
+ configFound = True
+ break
+
+ if not configFound:
+ warning("Could not find any VSCode configuration directory")
+
+ self._initConfiguration()
+
+ def configWidget(self):
+ return [
+ {
+ "type": "label",
+ "text": """Recent files are sorted in order found in the VSCode configuration.
+Sort order with Project Manager can be adjusted, lower number = higher priority = displays first.
+With all priorities equal, PM results will take precedence over recents."""
+ },
+ {
+ "type": "label",
+ "text": """
+PM extension: https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager
+"""
+ },
+
+ {
+ "type": "checkbox",
+ "property": "recentEnabled",
+ "label": "Search in Recent files"
+ },
+ {
+ "type": "checkbox",
+ "property": "projectManagerEnabled",
+ "label": "Search in Project Manager extension"
+ },
+ {
+ "type": "spinbox",
+ "property": "priorityPMName",
+ "label": "Priority: Project Manager entries matched by name",
+ "widget_properties": {
+ "minimum": 1,
+ "maximum": 99,
+ },
+ },
+ {
+ "type": "spinbox",
+ "property": "priorityPMPath",
+ "label": "Priority: Project Manager entries matched by path",
+ "widget_properties": {
+ "minimum": 1,
+ "maximum": 99,
+ },
+ },
+ {
+ "type": "spinbox",
+ "property": "priorityPMTag",
+ "label": "Priority: Project Manager entries matched by tag",
+ "widget_properties": {
+ "minimum": 1,
+ "maximum": 99,
+ },
+ },
+ {
+ "type": "spinbox",
+ "property": "priorityRecent",
+ "label": "Priority: Recent entries",
+ "widget_properties": {
+ "minimum": 1,
+ "maximum": 99,
+ },
+ },
+ {
+ "type": "label",
+ "text": """
+The way VSCode is opened can be overriden through terminal command.
+Terminal will enter the working directory of the project upon selection, execute the command and then close itself.
+
+Usecase with direnv - To load direnv environment before opening VSCode, enter the following custom command: direnv exec . code .
+
+Usecase with single VSCode instance - To reuse the VSCode window instead of opening a new one, enter the following custom command: code -r ."""
+ },
+ {
+ "type": "lineedit",
+ "property": "terminalCommand",
+ "label": "Run custom command in the workdir of selected item"
+ },
+ ]
+
+ def _initConfiguration(self):
+ # Recent search
+ recentEnabled = self.readConfig('recentEnabled', bool)
+ if recentEnabled is None:
+ self._recentEnabled = True
+ self.writeConfig("recentEnabled", True)
+ else:
+ self._recentEnabled = recentEnabled
+
+ # Project Manager search
+ foundPM = False
+ for p in self._configProjectManagerPaths:
+ if os.path.exists(p):
+ foundPM = True
+ break
+
+ projectManagerEnabled = self.readConfig('projectManagerEnabled', bool)
+ if projectManagerEnabled is None:
+ # If not configured, check if the project manager configuration file exists and if so, enable PM search
+ if foundPM:
+ self._projectManagerEnabled = True
+ self.writeConfig("projectManagerEnabled", True)
+ else:
+ self._projectManagerEnabled = False
+ else:
+ self._projectManagerEnabled = projectManagerEnabled
+
+ # Priority settings
+ for p in self._sortPriority:
+ prio = self.readConfig(f"priority{p}", int)
+ if prio is None:
+ self.writeConfig(f"priority{p}", self._sortPriority[p])
+ else:
+ self._sortPriority[p] = prio
+
+ # Terminal command setting
+ terminalCommand = self.readConfig('terminalCommand', str)
+ if terminalCommand is not None:
+ self._terminalCommand = terminalCommand
+
+ def handleTriggerQuery(self, query):
+ if not query.isValid:
+ return
+
+ if query.string == "":
+ return
+
+ matcher = Matcher(query.string)
+
+ results: dict[str, SearchResult] = {}
+
+ if self.recentEnabled:
+ results = self._searchInRecentFiles(matcher, results)
+
+ if self.projectManagerEnabled:
+ results = self._searchInProjectManager(matcher, results)
+
+ sortedItems = sorted(results.values(), key=lambda item: "%s_%s_%s" % (
+ '{:03d}'.format(item.priority), '{:03d}'.format(item.sortIndex), item.project.name), reverse=False)
+
+ items: list[StandardItem] = []
+ for i in sortedItems:
+ items.append(self._createItem(i.project, query))
+
+ query.add(items)
+
+ # Creates an item for the query based on the project and plugin settings
+ def _createItem(self, project: Project, query: Query) -> StandardItem:
+ actions: list[Action] = []
+
+ if self.terminalCommand != "":
+ actions.append(
+ Action(
+ id="open-terminal",
+ text=f"Run terminal command in project's workdir: {self.terminalCommand}",
+ callable=lambda: runTerminal(f"cd {project.path} && {self.terminalCommand}")
+ )
+ )
+
+ actions.append(
+ Action(
+ id="open-code",
+ text="Open with VSCode",
+ callable=lambda: runDetachedProcess(
+ ["code", project.path]),
+ )
+ )
+
+ subtext = ""
+
+ if len(project.tags) > 0:
+ subtext = "<" + ",".join(project.tags) + "> "
+
+ return StandardItem(
+ id=project.path,
+ text=project.displayName,
+ subtext=f"{subtext}{project.path}",
+ iconUrls=self.iconUrls,
+ inputActionText=project.displayName,
+ actions=actions,
+ )
+
+ def _searchInRecentFiles(self, matcher: Matcher, results: dict[str, SearchResult]) -> dict[str, SearchResult]:
+ sortIndex = 1
+
+ for path in self._configStoragePaths:
+ c = self._getStorageConfig(path)
+ for proj in c.projects:
+ # Resolve sym links to get unique results
+ resolvedPath = str(Path(proj.path).resolve())
+ if matcher.match(proj.name) or matcher.match(proj.path) or matcher.match(resolvedPath):
+ results[resolvedPath] = self._getHigherPriorityResult(
+ SearchResult(
+ project=proj,
+ priority=self.priorityRecent,
+ sortIndex=sortIndex
+ ),
+ results.get(resolvedPath),
+ )
+
+ if results.get(resolvedPath) is not None:
+ sortIndex += 1
+
+ return results
+
+ def _searchInProjectManager(self, matcher: Matcher, results: dict[str, SearchResult]) -> dict[str, SearchResult]:
+ for path in self._configProjectManagerPaths:
+ c = self._getProjectManagerConfig(path)
+ for proj in c.projects:
+ # Resolve sym links to get unique results
+ resolvedPath = str(Path(proj.path).resolve())
+ if matcher.match(proj.name):
+ results[resolvedPath] = self._getHigherPriorityResult(
+ SearchResult(
+ project=proj,
+ priority=self.priorityPMName,
+ sortIndex=0 if matcher.match(proj.name).isExactMatch() else 1
+ ),
+ results.get(resolvedPath),
+ )
+
+ if matcher.match(proj.path) or matcher.match(resolvedPath):
+ results[resolvedPath] = self._getHigherPriorityResult(
+ SearchResult(
+ project=proj,
+ priority=self.priorityPMPath,
+ sortIndex=1
+ ),
+ results.get(resolvedPath),
+ )
+
+ for tag in proj.tags:
+ if matcher.match(tag):
+ results[resolvedPath] = self._getHigherPriorityResult(
+ SearchResult(
+ project=proj,
+ priority=self.priorityPMTag,
+ sortIndex=1
+ ),
+ results.get(resolvedPath),
+ )
+ break
+
+ return results
+
+ # Compares the search results to return the one with higher priority
+ # For nitpickers: higher priorty = lower number
+ def _getHigherPriorityResult(self, current: SearchResult, prev: SearchResult | None) -> SearchResult:
+ if prev is None or current.priority < prev.priority or (current.priority == prev.priority and current.sortIndex < prev.sortIndex):
+ return current
+
+ return prev
+
+ def _getStorageConfig(self, path: str) -> CachedConfig:
+ c: CachedConfig = self._configCache.get(path, CachedConfig([], 0))
+
+ if not os.path.exists(path):
+ return c
+
+ mTime = os.stat(path).st_mtime
+
+ if mTime == c.mTime:
+ return c
+
+ c.mTime = mTime
+
+ with open(path) as configFile:
+ # Load the storage json
+ storageConfig = json.loads(configFile.read())
+
+ if (
+ "lastKnownMenubarData" in storageConfig
+ and "menus" in storageConfig["lastKnownMenubarData"]
+ and "File" in storageConfig["lastKnownMenubarData"]["menus"]
+ and "items" in storageConfig["lastKnownMenubarData"]["menus"]["File"]
+ ):
+ # These are all the menu items in File dropdown
+ for menuItem in storageConfig["lastKnownMenubarData"]["menus"]["File"]["items"]:
+ # Cannot safely detect proper menu item, as menu item IDs change over time
+ # Instead we will search all submenus and check for IDs inside the submenu items
+ if (
+ not "id" in menuItem
+ or not "submenu" in menuItem
+ or not "items" in menuItem["submenu"]
+ ):
+ continue
+
+ for submenuItem in menuItem["submenu"]["items"]:
+ # Check of submenu item with id "openRecentFolder" and make sure it contains necessarry keys
+ if (
+ not "id" in submenuItem
+ or submenuItem['id'] != "openRecentFolder"
+ or not "enabled" in submenuItem
+ or submenuItem["enabled"] != True
+ or not "label" in submenuItem
+ or not "uri" in submenuItem
+ or not "path" in submenuItem["uri"]
+ ):
+ continue
+
+ # Get the full path to the project
+ recentPath = submenuItem["uri"]["path"]
+ if not os.path.exists(recentPath):
+ continue
+
+ displayName = recentPath.split("/")[-1]
+
+ # Inject the project
+ c.projects.append(Project(
+ displayName=displayName,
+ name=displayName,
+ path=recentPath,
+ tags=[],
+ ))
+
+ self._configCache[path] = c
+
+ return c
+
+ def _getProjectManagerConfig(self, path: str) -> CachedConfig:
+ c = self._configCache.get(path, CachedConfig([], 0))
+
+ if not os.path.exists(path):
+ return c
+
+ mTime = os.stat(path).st_mtime
+
+ if mTime == c.mTime:
+ return c
+
+ c.mTime = mTime
+
+ with open(path) as configFile:
+ configuredProjects = json.loads(configFile.read())
+
+ for p in configuredProjects:
+ # Make sure we have necessarry keys
+ if (
+ not "rootPath" in p
+ or not "name" in p
+ or not "enabled" in p
+ or p["enabled"] != True
+ ):
+ continue
+
+ # Grab the path to the project
+ rootPath = p["rootPath"]
+ if os.path.exists(rootPath) == False:
+ continue
+
+ project = Project(
+ displayName=p["name"],
+ name=p["name"],
+ path=rootPath,
+ tags=[],
+ )
+
+ # Search against the query string
+ if "tags" in p:
+ for tag in p["tags"]:
+ project.tags.append(tag)
+
+ c.projects.append(project)
+
+ self._configCache[path] = c
+
+ return c
diff --git a/vscode_projects/icon.svg b/vscode_projects/icon.svg
new file mode 100644
index 00000000..c453e633
--- /dev/null
+++ b/vscode_projects/icon.svg
@@ -0,0 +1,41 @@
+
diff --git a/wikipedia/__init__.py b/wikipedia/__init__.py
index 24809292..af92e2f6 100644
--- a/wikipedia/__init__.py
+++ b/wikipedia/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
-# Copyright (c) 2022-2023 Manuel Schneider
from albert import *
from locale import getdefaultlocale
@@ -10,25 +10,13 @@
import json
from pathlib import Path
-md_iid = '2.0'
-md_version = "1.10"
+md_iid = "3.0"
+md_version = "3.0"
md_name = "Wikipedia"
md_description = "Search Wikipedia articles"
-md_license = "BSD-3"
-md_url = "https://github.com/albertlauncher/python/tree/master/wikipedia"
-
-
-class WikiFallbackHandler(FallbackHandler):
- def __init__(self):
- FallbackHandler.__init__(self,
- id=f"{md_id}_fb",
- name=f"{md_name} fallback",
- description="Wikipedia fallback search")
-
- def fallbacks(self, query_string):
- stripped = query_string.strip()
- return [Plugin.createFallbackItem(query_string)] if stripped else []
-
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/wikipedia"
+md_authors = "@manuelschneid3r"
class Plugin(PluginInstance, TriggerQueryHandler):
@@ -39,13 +27,18 @@ class Plugin(PluginInstance, TriggerQueryHandler):
iconUrls = [f"file:{Path(__file__).parent}/wikipedia.png"]
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='wiki ')
- self.wiki_fb = WikiFallbackHandler()
- PluginInstance.__init__(self, extensions=[self, self.wiki_fb])
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ self.fbh = FBH(self)
+ self.fuzzy = False
+
+ self.local_lang_code = getdefaultlocale()[0]
+ if self.local_lang_code:
+ self.local_lang_code = self.local_lang_code[0:2]
+ else:
+ self.local_lang_code = 'en'
+ warning("Failed getting language code. Using 'en'.")
params = {
'action': 'query',
@@ -55,13 +48,6 @@ def __init__(self):
'format': 'json'
}
- Plugin.local_lang_code = getdefaultlocale()[0]
- if Plugin.local_lang_code:
- Plugin.local_lang_code = Plugin.local_lang_code[0:2]
- else:
- Plugin.local_lang_code = 'en'
- warning("Failed getting language code. Using 'en'.")
-
get_url = "%s?%s" % (self.baseurl, parse.urlencode(params))
req = request.Request(get_url, headers={'User-Agent': self.user_agent})
try:
@@ -75,9 +61,21 @@ def __init__(self):
except Exception as error:
warning('Error getting languages (%s). Defaulting to EN.' % error)
+ def extensions(self):
+ return [self, self.fbh]
+
+ def defaultTrigger(self):
+ return "wiki "
+
+ def supportsFuzzyMatching(self):
+ return True
+
+ def setFuzzyMatching(self, enabled: bool):
+ self.fuzzy = enabled
+
def handleTriggerQuery(self, query):
- stripped = query.string.strip()
- if stripped:
+ if stripped := query.string.strip():
+
# avoid rate limiting
for _ in range(50):
sleep(0.01)
@@ -91,7 +89,8 @@ def handleTriggerQuery(self, query):
'search': stripped,
'limit': self.limit,
'utf8': 1,
- 'format': 'json'
+ 'format': 'json',
+ 'profile': 'fuzzy' if self.fuzzy else 'normal'
}
get_url = "%s?%s" % (self.baseurl, parse.urlencode(params))
req = request.Request(get_url, headers={'User-Agent': self.user_agent})
@@ -105,7 +104,7 @@ def handleTriggerQuery(self, query):
url = data[3][i]
results.append(
StandardItem(
- id=md_id,
+ id=self.id(),
text=title,
subtext=summary if summary else url,
iconUrls=self.iconUrls,
@@ -117,28 +116,46 @@ def handleTriggerQuery(self, query):
)
if not results:
- results.append(Plugin.createFallbackItem(stripped))
+ results.append(self.createFallbackItem(stripped))
query.add(results)
else:
query.add(
StandardItem(
- id=md_id,
- text=md_name,
+ id=self.id(),
+ text=self.name(),
subtext="Enter a query to search on Wikipedia",
iconUrls=self.iconUrls
)
)
- @staticmethod
- def createFallbackItem(query_string):
+ def createFallbackItem(self, q: str) -> Item:
return StandardItem(
- id=md_id,
- text=md_name,
- subtext="Search '%s' on Wiki" % query_string,
- iconUrls=Plugin.iconUrls,
+ id=self.id(),
+ text=self.name(),
+ subtext="Search '%s' on Wikipedia" % q,
+ iconUrls=self.iconUrls,
actions=[
Action("wiki_search", "Search on Wikipedia",
- lambda url=Plugin.searchUrl % (Plugin.local_lang_code, query_string): openUrl(url))
+ lambda url=self.searchUrl % (self.local_lang_code, q): openUrl(url))
]
)
+
+
+class FBH(FallbackHandler):
+
+ def __init__(self, p: Plugin):
+ FallbackHandler.__init__(self)
+ self.plugin = p
+
+ def id(self):
+ return "wikipedia.fallbacks"
+
+ def name(self):
+ return md_name
+
+ def description(self):
+ return md_description
+
+ def fallbacks(self, q :str):
+ return [self.plugin.createFallbackItem(q)]
diff --git a/x_window_switcher/__init__.py b/x_window_switcher/__init__.py
new file mode 100644
index 00000000..00c180b9
--- /dev/null
+++ b/x_window_switcher/__init__.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+import subprocess
+from collections import namedtuple
+from albert import *
+
+md_iid = "3.0"
+md_version = "0.6.0"
+md_name = "X Window Switcher"
+md_description = "Switch X11 Windows"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/x_window_switcher"
+md_bin_dependencies = "wmctrl"
+md_authors = ["Ed Perez", "Manuel S.", "dshoreman", "nopsqi"]
+
+Window = namedtuple("Window", ["wid", "desktop", "wm_class", "host", "wm_name"])
+
+
+class Plugin(PluginInstance, TriggerQueryHandler):
+ def __init__(self):
+ PluginInstance.__init__(self)
+ TriggerQueryHandler.__init__(self)
+
+ # Check for X session and wmctrl availability
+ try:
+ subprocess.check_call(["wmctrl", "-m"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except FileNotFoundError:
+ raise Exception("wmctrl not found. Please install wmctrl.")
+ except subprocess.CalledProcessError:
+ raise Exception("Unable to communicate with X11 window manager. This plugin requires a running X session.")
+
+ def defaultTrigger(self):
+ return 'w '
+
+ def handleTriggerQuery(self, query):
+ try:
+ for line in subprocess.check_output(['wmctrl', '-l', '-x']).splitlines():
+ win = Window(*parseWindow(line))
+
+ if win.desktop == "-1":
+ continue
+
+ win_instance, win_class = win.wm_class.replace(' ', '-').split('.', 1)
+
+ m = Matcher(query.string)
+ if not query.string or m.match(win_instance + ' ' + win_class + ' ' + win.wm_name):
+ query.add(StandardItem(
+ id="%s%s" % (md_name, win.wm_class),
+ iconUrls=["xdg:%s" % win_instance],
+ text="%s - Desktop %s" % (win_class.replace('-', ' '), win.desktop),
+ subtext=win.wm_name,
+ actions=[Action("switch",
+ "Switch Window",
+ lambda w=win: runDetachedProcess(["wmctrl", '-i', '-a', w.wid])),
+ Action("move",
+ "Move window to this desktop",
+ lambda w=win: runDetachedProcess(["wmctrl", '-i', '-R', w.wid])),
+ Action("close",
+ "Close the window gracefully.",
+ lambda w=win: runDetachedProcess(["wmctrl", '-c', w.wid]))]
+ ))
+ except subprocess.CalledProcessError as e:
+ warning(f"Error executing wmctrl: {str(e)}")
+
+
+def parseWindow(line):
+ win_id, desktop, rest = line.decode().split(None, 2)
+ win_class, rest = rest.split(' ', 1)
+ host, title = rest.strip().split(None, 1)
+
+ return [win_id, desktop, win_class, host, title]
diff --git a/zeal/__init__.py b/zeal/__init__.py
index 843eee45..1339a7d9 100644
--- a/zeal/__init__.py
+++ b/zeal/__init__.py
@@ -1,33 +1,55 @@
-"""Search in Zeal offline docs."""
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Manuel Schneider
-from albert import *
+import albert
-md_iid = '2.0'
-md_version = '1.2'
-md_name = 'Zeal'
-md_description = 'Search in Zeal docs'
-md_url = 'https://github.com/albertlauncher/python/zeal'
+md_iid = "3.0"
+md_version = "3.0"
+md_name = "Zeal"
+md_description = "Search in Zeal docs"
+md_license = "MIT"
+md_url = "https://github.com/albertlauncher/python/tree/main/zeal"
+md_authors = "@manuelschneid3r"
md_bin_dependencies = ['zeal']
+def createItem(query: str):
+ return albert.StandardItem(
+ id=md_name,
+ text=md_name,
+ subtext=f"Search '{query}' in Zeal",
+ iconUrls=["xdg:zeal"],
+ actions=[albert.Action("zeal", "Search in Zeal",
+ lambda q=query: albert.runDetachedProcess(['zeal', q]))]
+ )
+
+class FBH(albert.FallbackHandler):
+
+ def id(self):
+ return "zeal_fbh"
+
+ def name(self):
+ return md_name
+
+ def description(self):
+ return md_description
+
+ def fallbacks(self, s):
+ return [createItem(s)] if s else []
+
+
+class Plugin(albert.PluginInstance, albert.TriggerQueryHandler):
-class Plugin(PluginInstance, TriggerQueryHandler):
def __init__(self):
- TriggerQueryHandler.__init__(self,
- id=md_id,
- name=md_name,
- description=md_description,
- defaultTrigger='z ')
- PluginInstance.__init__(self, extensions=[self])
+ albert.PluginInstance.__init__(self)
+ albert.TriggerQueryHandler.__init__(self)
+ self.fbh = FBH()
+
+ def defaultTrigger(self):
+ return "z "
+
+ def extensions(self):
+ return [self, self.fbh]
def handleTriggerQuery(self, query):
- stripped = query.string.strip()
- if stripped:
- query.add(
- StandardItem(
- id=md_name,
- text=md_name,
- subtext=f"Search '{stripped}' in Zeal",
- iconUrls=["xdg:zeal"],
- actions=[Action("zeal", "Search in Zeal", lambda s=stripped: runDetachedProcess(['zeal', s]))]
- )
- )
+ if stripped := query.string.strip():
+ query.add(createItem(stripped))