Flashlight to nieoficjalne API dla Spotlight (błyskawicznej wyszukiwarki dla OSX), umożliwiające programowe przetwarzanie zapytań i dodawanie własnych wyników wyszukiwania. Bliższy opis modułu można znaleźć na GitHubie. W tym wpisie przybliżę temat tworzenia własnego pluginu dla Flashlight. Artykuł jest inspirowany instrukcją opublikowaną na wiki projektu.

Zasada działania

Każdy plugin rejestruje wyrażenia, na które będzie reagował. W ramach artykułu stworzymy plugin pozwalający wysłać mail z ustawionego jako domyślny klienta pocztowego. Pełny kod pluginu dostępny jest również w moim repozytorium GitHub.

Struktura pluginu

Każdy plugin musi znajdować się w katalogu ~/Library/FlashlightPlugins (w preferencjach Findera należy włączyć widoczność folderu Biblioteki). Tworząc nowy plugin należy utworzyć katalog nazwa_pluginu.bundle przy czym rozszerzenie .bundle jest konieczne. Katalog powinien zawierać następujące pliki

  • info.json - zawierający informacje na temat pluginu takie jak nazwa, opis, autora czy kategorię, do której należy plugin,
  • examples.txt - określenie przykłady może być nieco mylące; plik ten zawiera listę wyrażeń (umieszczonych w kolejnych liniach), na które reagować będzie plugin,
  • plugin.py - rdzeń pluginu; musi zawierać dwie główne funkcje: results() oraz run(), których rolę przybliżymy poniżej

Realizacja

Tworzenie pluginu rozpoczniemy od pliku info.json, proponuję następujący kod:

{
    "name": "mpmail",
    "displayName": "Mail with default client",
    "description": "Sends mail with your default mail client",
    "examples": ["nowy mail", "mail do john@example.net"],
    "categories": ["Utilities"],
    "creator_name": "MichalP",
    "creator_url": "michalp.net"
}

Role powyższych pól są dość oczywiste. Warto jedynie zaznaczyć, że przykłady (examples) oraz kategorie (categories) są listami. Dostępne kategorie to: Information, Search, Art, Language, Media, Other, System, Utilities, Weather.

Kolejnym krokiem jest utworzenie pliku examples.txt:

nowy mail
mail do ~addr(john@example.net)
napisz do ~addr(john@example.net)
wiadomość do ~addr(john@example.net)

Zawiera on kilka elementów wartych uwagi. Charakterystycznym elementem, powtarzającym się w większości linii jest wyrażenie ~addr(john@example.net). Najważniejsza uwaga: użytkownik nie wprowadza tekstu ~addr, natomiast wyrażenie john@example.net może być dowolne. Jego rola to prezentacja możliwie reprezentatywnego argumentu. Wyrażenia, które rozpoczynają się od ~ mają kluczowe zadanie w wyrażeniach. Zostaną one przekazane do skryptu w postaci słownika argumentów.

Dygresja - alternatywny przykład

Chcąc lepiej zrozumieć tę ideę przytoczmy inny przykład. Powiedzmy, że tworzymy wyszukiwarkę produktów na allegro. Jeśli chcemy, aby nasz plugin wyszukiwał ofert we wskazanej kategorii możemy zdefiniować następujący wpis w pliku examples.txt

allegro ~thing(przedmiot) w kategorii ~cat(kategoria)

Wpisanie w spotlight frazy

allegro laptop w kategorii elektronika

Przekaże do skryptu następujący słownik:

{
    "~thing" : "laptop",
    "~cat" : "elektronika"
}

Koniec dygresji

Sposób wykorzystania tych danych opiszemy w pliku plugin.py. Proponuję następującą formę:

# -*- coding: utf-8 -*-
def results(fields, original_query):
    addr = ""
    taddr = ""
    if len(fields):
        addr = fields["~addr"]
        taddr = "do "+addr
    return {
        "title": "Napisz wiadomość {0}".format(taddr),
        "run_args": [addr]
    }

def run(addr):
    import os
    os.system('open mailto:"{0}"'.format(addr))

Przed przystąpieniem do omówienia samego kodu zdefiniujmy rolę funkcji results() oraz run():

results()

Kiedy użytkownik wpisze do spotlight wyrażenie pasujące do którejś z definicji utworzonych przez nas w example.txt zostanie załadowany nasz plik plugin.py i zostanie wywołana funkcja results(). Jej argumenty to kolejno

  • fields słownik pól, które zdefiniowane zostały dla danego wyrażenia w *example.txt; jego forma została omówiona w przykładzie allegro,
  • original_query ciąg znaków, który został wpisany przez użytkownika

Funkcja zwraca słownik informacji dla Spotlight dotyczący wpisanych danych. Pełna lista kluczy zwracanego słownika oraz ich znaczenie:

  • title tytuł polecenia prezentowany w spotlight
  • html podpowiedź dla wpisanego hasła sformatowana w html
  • run_args lista argumentów przekazywanych do funkcji run() kiedy użytkownik zatwierdzi wpis. Dane muszą mieć postać zdatną do serializacji JSON
  • webview_links_open_in_browser sprawia, że klikanie linków w Spotligt otwiera przeglądarkę
  • dont_force_top_hit nie wymagaj, aby ten wynik był na pierwszym miejscu w Spotlight
  • webview_user_agent zmodyfikowany nagłówek user agent dla podglądu (użyteczne gdy chcemy wczytać stronę w trybie mobilnym, gdyż okno jest małe)
  • webview_transparent_background ustawia tło podglądu webview na przezroczyste
  • pass_result_of_output_function_as_first_run_arg wywołuje funkcję output() napisaną w JavaScript w ramach podglądu webview i przekazuje jej wynik (zdatny do serializacji JSON) jako pierwszy argument funkcji run()

run()

Zatwierdzenie przez użytkownika wpisanego tekstu również wiąże się z obsługą zdarzenia. Takie właśnie zadanie spełnia funkcja run(). Ilość parametrów funkcji zależy bepośrednio od parametru run_args słownika zwracanego przez results(). Tworząc funkcję run() należy pamiętać o jednym, bardzo istotnym fakcie. Cytując dokumentację:

Called with all arguments from run_args. This process has about ~20 seconds to run before being killed; spawn a new process for long-running work.

Co oznacza, że proces odpowiedzialny za wywołanie naszej funkcji zostaje zabity po około 20 sekundach. W tym artykule nie będziemy jednak skupiać się na metodach unikania timeoutu. Być może poświęce temu któryś z kolejnych wpisów.

Poza czasem i uprawnieniami skryptu, zakres wykonywanych przez niego czynności jest nieograniczony.

Analiza kodu

Rozpoczynając od pierwszej linii kodu, dyrektywa # -*- coding: utf-8 -*- informuje środowisko, że skrypt korzysta z kodowania UTF-8. Dzięki niej możemy stosować polskie znaki diakrytyczne.

results()

Pamiętając, że w pliku example.txt uwzględniliśmy tworzenie maila bez zdefiniowanego odbiorcy, logikę projektujemy tak, aby sprawdzała czy wystąpiło pole ~addr. Funkcja results wywoływana jest kiedy tylko ciąg wprowadzony przez użytkownika jest zgodny ze zdefiniowanym przez nas, zatem to właśnie results() odpowiada za weryfikację rozmiaru słownika fields. Weryfikacja ta ma miejsce w linii: if len(fields):

Na podstawie ilości parametrów przechwytujemy ewentualny adres odbiorcy oraz formułujemy ciąg, który dokleimy do parametru title.

Wartość zwracana zgodnie z dokumentacją jest słownikiem. Warto zauważyć, że pomimo przekazywania tylko jednej zmiennej do run(), jest ona nadal ujmowana w liście: "run_args": [addr].

run()

Treść funkcji jest bardzo prosta. Import modułu os pozwala na wykonywanie komend linii poleceń systemu operacyjnego. W kolejnej linii dokonujemy wywołania systemowego za pomocą os.system('open mailto:"{0}"'.format(addr)).

Dygresja

Wywołanie systemowe sformułowanie w ten sposób nie jest bezpieczne, ponieważ nie weryfikujemy danych wprowadzonych przez użytkownika. Wpisanie

mail do ; rm -r ~/

Spowoduje usunięcie zawartości katalogu użytkownika. Użyłem tej metody aby zbędnie nie komplikować kodu i nie uciekać zbytnio od tematu.