Over the weekend, I picked up a project via Reddit involving a plugin for the Flow Launcher. I created a fzf and rofi version for my Ubuntu Linux environment, and then thought, how hard can it be to port it to uLauncher?
Here I document what I did.
1. Inside ~/.local/share/ulauncher/extensions/
create a new dir. In my case, I created ~/.local/share/ulauncher/extensions/com.github.ubuntupunk.ulauncher-vim
2. Touch the following files:
├── images
│ └── icon.png
├── versions.json
├── manifest.json
└── main.py
3. In versions.json
place the following boilerplate:
[
{"required_api_version": "2", "commit": "master"}
]
4. In manifest.json
{
"required_api_version": "2",
"name": "Demo extension",
"description": "Extension Description",
"developer_name": "John Doe",
"icon": "images/icon.png",
"options": {
"query_debounce": 0.1
},
"preferences": [
{
"id": "demo_kw",
"type": "keyword",
"name": "Demo",
"description": "Demo extension",
"default_value": "dm"
}
]
}
5. In main.py
from ulauncher.api.client.Extension import Extension
from ulauncher.api.client.EventListener import EventListener
from ulauncher.api.shared.event import KeywordQueryEvent, ItemEnterEvent
from ulauncher.api.shared.item.ExtensionResultItem import ExtensionResultItem
from ulauncher.api.shared.action.RenderResultListAction import RenderResultListAction
from ulauncher.api.shared.action.HideWindowAction import HideWindowAction
class DemoExtension(Extension):
def __init__(self):
super().__init__()
self.subscribe(KeywordQueryEvent, KeywordQueryEventListener())
class KeywordQueryEventListener(EventListener):
def on_event(self, event, extension):
items = []
for i in range(5):
items.append(ExtensionResultItem(icon='images/icon.png',
name='Item %s' % i,
description='Item description %s' % i,
on_enter=HideWindowAction()))
return RenderResultListAction(items)
if __name__ == '__main__':
DemoExtension().run()
6. Now edit manifest.json
{
"required_api_version": "2",
"name": "Vim Prompter",
"description": "Vim cheatsheet helper",
"developer_name": "David Robert Lewis",
"icon": "images/icon.png",
"options": {
"query_debounce": 0.1
},
"preferences": [
{
"id": "vm_kw",
"type": "keyword",
"name": "Vim",
"description": "Search for Vim commands",
"default_value": "vm"
}
]
7. Add command loading function to main.py
class VmExtension(Extension):
def load_vim_commands(self):
"""Load Vim commands from JSON file."""
package_dir = os.path.dirname(os.path.abspath(__file__))
full_path = os.path.join(package_dir, 'db', 'commands.json')
with open(full_path, 'r') as file:
return json.load(file)
def __init__(self):
super().__init__()
self.vim_commands = self.load_vim_commands()
self.subscribe(KeywordQueryEvent, KeywordQueryEventListener())
8. Create a db folder with the following:
commands.json
example structure:
{
"categories": {
"navigation": {
"name": "Navigation",
"patterns": [
"scroll",
"jump",
"goto",
"position"
],
"subcategories": {
"cursor": {
"name": "Cursor Movement",
"patterns": [
"move[s]? cursor",
"^[hjkl]$",
"^[HJKL]$",
"^[wWeEbB]$"
]
},
You can see the entire commands.json here.
9. Modify the KeywordQueryEventListener
to implement the search functionality.
class KeywordQueryEventListener(EventListener):
def on_event(self, event, extension):
query = event.get_argument() or ""
items = []
# If no query, show all commands (limited to first 8)
commands_to_show = extension.vim_commands
# If there's a query, filter commands
if query:
commands_to_show = [
cmd for cmd in extension.vim_commands
if query.lower() in cmd['command'].lower() or
query.lower() in cmd['description'].lower()
]
# Limit results to first 8 matches
for cmd in commands_to_show[:8]:
items.append(ExtensionResultItem(
icon='images/icon.png',
name=cmd['command'],
description=f"{cmd['name']} - {cmd['description']}",
on_enter=HideWindowAction()
))
return RenderResultListAction(items)
10. Add the URL opening functionality. We'll need to import webbrowser and modify the on_enter action to open the Vim command URL
from ulauncher.api.shared.action.OpenUrlAction import OpenUrlAction
class KeywordQueryEventListener(EventListener):
def on_event(self, event, extension):
query = event.get_argument() or ""
items = []
commands_to_show = extension.vim_commands
if query:
commands_to_show = [
cmd for cmd in extension.vim_commands
if query.lower() in cmd['command'].lower() or
query.lower() in cmd['description'].lower()
]
for cmd in commands_to_show[:8]:
url = f"https://vim.rtorr.com/#:~:text={cmd['rtorr_description']}"
items.append(ExtensionResultItem(
icon='images/icon.png',
name=cmd['command'],
description=f"{cmd['name']} - {cmd['description']}",
on_enter=OpenUrlAction(url)
))
return RenderResultListAction(items)
11. Key changes are:
- Added OpenUrlAction import
- Replaced HideWindowAction with OpenUrlAction
- Constructed the URL using the command's rtorr_description
12. The full project code can be viewed here:
and the ulauncher extension here
Top comments (0)