diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..a402c35
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,11 @@
+version: 2
+jobs:
+ build:
+ docker:
+ - image: circleci/python:3.6.4
+ steps:
+ - checkout
+ - run: sudo pip3 install pylint yapf
+ - run: make lint
+
+
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3003e90
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 4
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5b9ac68
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.pyc
+__pycache__
+.cache
+*.log
+.vscode
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..a846c3b
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,17 @@
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=locally-disabled,too-few-public-methods
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4ddcd5f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+
+EXT_NAME:=com.github.brpaz.ulauncher-docsearch
+EXT_DIR:=$(shell pwd)
+
+.PHONY: help lint format link unlink deps dev
+.DEFAULT_TARGET: help
+
+help: ## Show help menu
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+lint: ## Run Pylint
+ @find . -iname "*.py" | xargs pylint
+
+format: ## Format code using yapf
+ @yapf --in-place --recursive .
+
+link: ## Symlink the project source directory with Ulauncher extensions dir.
+ @ln -s ${EXT_DIR} ~/.cache/ulauncher_cache/extensions/${EXT_NAME}
+
+unlink: ## Unlink extension from Ulauncher
+ @rm -r ~/.cache/ulauncher_cache/extensions/${EXT_NAME}
+
+deps: ## Install Python Dependencies
+ @pip3 install -r requirements.txt
+
+dev: ## Runs ulauncher on development mode
+ ulauncher --no-extensions --dev -v
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e63af2c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# ulauncher-docsearch
+
+[![Ulauncher Extension](https://img.shields.io/badge/Ulauncher-Extension-green.svg?style=for-the-badge)](https://ext.ulauncher.io/-/github-brpaz-ulauncher-docsearch)
+[![CircleCI](https://img.shields.io/circleci/build/github/brpaz/ulauncher-docsearch.svg?style=for-the-badge)](https://circleci.com/gh/brpaz/ulauncher-docsearch)
+![License](https://img.shields.io/github/license/brpaz/ulauncher-docsearch.svg?style=for-the-badge)
+
+![Algolia](images/search-by-algolia-light-background.svg)
+
+> Full text search on Documentation sites, powered by [Algolia](https://www.algolia.com/).
+
+**This is a work in progress project and its not ready to be used yet.**
+
+## Motivation
+
+Searching documentation is a constant need but not an easy task. Switch contexts between your code editor to the browser, remember the documentation site url and then find what you need. Many times, I kwew exactly what I want to find and that I have seen before, but simple cant find it again.
+
+This extension, aims to make documentation search less painfull, allowing you to a full text search on popular documentation sites directly from directly from ulauncher.
+
+## Features
+
+This extension allows to easily search on popular documentation websites, using [Algolia DocSearch](https://community.algolia.com/docsearch/).
+
+
+
+## Usage
+
+![demo](demo.gif)
+
+## Requirements
+
+* Ulauncher V5
+* Python 3
+ * algoliasearch>=2.0,<3.0 (install with pip3)
+
+## Install
+
+Open ulauncher preferences window -> extensions -> add extension and paste the following url:
+
+```
+https://github.com/brpaz/ulauncher-docsearch
+```
+
+## Development
+
+```
+git clone https://github.com/brpaz/ulauncher-docsearch
+make link
+```
+
+The `make link` command will symlink the cloned repo into the appropriate location on the ulauncher extensions folder.
+
+To see your changes, stop ulauncher and run it from the command line with: `ulauncher -v`.
+
+## Contributing
+
+All contributions are welcome.
+
+## Show your support
+
+
+
+
+## License
+
+Copywright @ 2019 [Bruno Paz](https://github.com/brpaz)
+
+This project is [MIT](LLICENSE) Licensed.
+
diff --git a/data/docsets.json b/data/docsets.json
new file mode 100644
index 0000000..2595901
--- /dev/null
+++ b/data/docsets.json
@@ -0,0 +1,10 @@
+{
+ "react" : {
+ "name": "React",
+ "description": "A JavaScript library for building user interfaces",
+ "icon": "images/docs/react.png",
+ "algolia_index": "react",
+ "keyword": "reactdocs",
+ "url": "https://reactjs.org/docs/getting-started.html"
+ }
+}
diff --git a/demo.gif b/demo.gif
new file mode 100644
index 0000000..0858018
Binary files /dev/null and b/demo.gif differ
diff --git a/docs.py b/docs.py
new file mode 100644
index 0000000..c0f68a6
--- /dev/null
+++ b/docs.py
@@ -0,0 +1,92 @@
+"""
+Module responsible for search into docs
+"""
+
+import json
+import os
+from algoliasearch.search_client import SearchClient
+
+ALGOLIA_APPLICAITON_ID = 'BH4D9OD16A'
+ALGOLIA_API_KEY = '36221914cce388c46d0420343e0bb32e'
+
+DEFAULT_DOC_IMAGE = 'images/icon.png'
+
+class DocSearch:
+ """ Searches Docs """
+
+ def __init__(self):
+ """ Class constructor """
+
+ self.algolia_client = SearchClient.create(ALGOLIA_APPLICAITON_ID, ALGOLIA_API_KEY)
+ self.docsets = {}
+
+ with open(os.path.join(os.path.dirname(__file__), 'data', 'docsets.json'), 'r') as data:
+ self.docsets = json.load(data)
+
+ def get_available_docs(self, filter_term):
+ """ Returns a list of available docs """
+ docs = []
+ for key, value in self.docsets.items():
+ docs.append({
+ 'key': key,
+ 'name': value['name'],
+ 'description': value['description'],
+ 'icon': value['icon'],
+ 'url': value['url']
+ })
+
+ if filter_term:
+ docs = [x for x in docs if filter_term.lower() in x['name'].lower()]
+
+ return docs
+
+ def has_docset(self, key):
+ """ Checks if the specified docset exists """
+ return key in self.docsets
+
+ def get_docset(self, key):
+ """ Returns the details from a docset with the specified key passed as argument """
+
+ if key in self.docsets.keys():
+ return self.docsets[key]
+
+ return None
+
+ def search(self, docset, term):
+ """ Searches a term on a specific docset and return the results """
+
+ docset = self.get_docset(docset)
+
+ if not docset:
+ raise ValueError("The specified docset is not known")
+
+ index = self.algolia_client.init_index(docset['algolia_index'])
+ search_results = index.search(term)
+
+ if not search_results['hits']:
+ return []
+
+ items = []
+ for hit in search_results['hits']:
+ title, description = self.parse_item_description(hit)
+ items.append({
+ 'url': hit['url'],
+ 'title': title,
+ 'icon': docset['icon'],
+ 'category': description
+ })
+
+ return items
+
+ def parse_item_description(self, hit):
+ """ Returns the text to display as result item title """
+ hierarchy = hit['hierarchy'].values()
+
+ # Filter the list by removing the empty values
+ res = [i for i in hierarchy if i]
+
+ if len(res) < 2:
+ return res[0], ""
+
+ # The last element found will be the description and the previous one the title.
+ return res[-1], ' -> '.join(res[:-1])
diff --git a/images/docs/react.png b/images/docs/react.png
new file mode 100644
index 0000000..7dc4289
Binary files /dev/null and b/images/docs/react.png differ
diff --git a/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..9b12805
Binary files /dev/null and b/images/icon.png differ
diff --git a/images/search-by-algolia-light-background.svg b/images/search-by-algolia-light-background.svg
new file mode 100644
index 0000000..ab6ae5a
--- /dev/null
+++ b/images/search-by-algolia-light-background.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..119aa57
--- /dev/null
+++ b/main.py
@@ -0,0 +1,121 @@
+"""
+Doc search extension
+This extension provides full text search on popular documentation sites, powered by Algolia.
+"""
+import logging
+
+# pylint: disable=import-error
+from ulauncher.api.client.Extension import Extension
+from ulauncher.api.client.EventListener import EventListener
+from ulauncher.api.shared.event import KeywordQueryEvent
+from ulauncher.api.shared.item.ExtensionResultItem import ExtensionResultItem
+from ulauncher.api.shared.action.RenderResultListAction import RenderResultListAction
+from ulauncher.api.shared.action.OpenUrlAction import OpenUrlAction
+from ulauncher.api.shared.action.HideWindowAction import HideWindowAction
+from ulauncher.api.shared.action.SetUserQueryAction import SetUserQueryAction
+from docs import DocSearch
+
+LOGGING = logging.getLogger(__name__)
+
+class DocsearchExtension(Extension):
+ """ Main Extension Class """
+
+ def __init__(self):
+ """ Extension constructor"""
+ super(DocsearchExtension, self).__init__()
+ self.subscribe(KeywordQueryEvent, KeywordQueryEventListener())
+ self.searcher = DocSearch()
+
+ def show_available_docs(self, event, filter_term=None):
+ """ Displays a list of available docs """
+ docs = self.searcher.get_available_docs(filter_term)
+
+ items = []
+
+ if not docs:
+ return RenderResultListAction([
+ ExtensionResultItem(
+ icon='images/icon.png',
+ name='No results found',
+ on_enter=HideWindowAction()
+ )
+ ])
+
+ for doc in docs[:8]:
+ items.append(
+ ExtensionResultItem(
+ icon=doc['icon'],
+ name=doc['name'],
+ description=doc['description'],
+ on_alt_enter=OpenUrlAction(doc['url']),
+ on_enter=SetUserQueryAction("%s %s > " % (event.get_keyword(), doc['key']))
+ )
+ )
+
+ return RenderResultListAction(items)
+
+class KeywordQueryEventListener(EventListener):
+ """ Listener that handles the user input """
+
+ # pylint: disable=unused-argument,no-self-use
+ def on_event(self, event, extension):
+ """ Handles the event """
+
+ arg = event.get_argument() or ""
+
+ args_array = arg.split(">")
+ if len(args_array) != 2:
+ return extension.show_available_docs(event, arg)
+
+ try:
+
+ docset = args_array[0].strip()
+
+ search_term = args_array[1]
+
+ if len(search_term.strip()) < 3:
+ return RenderResultListAction([
+ ExtensionResultItem(
+ icon='images/icon.png',
+ name='Please type a minimum of 3 characters',
+ description='Searching ...',
+ on_enter=HideWindowAction()
+ )
+ ])
+
+ result = extension.searcher.search(docset, search_term)
+
+ items = []
+
+ if not result:
+ return RenderResultListAction([
+ ExtensionResultItem(
+ icon='images/icon.png',
+ name='No results matching your criteria',
+ on_enter=HideWindowAction()
+ )]
+ )
+
+ for i in result[:8]:
+ items.append(
+ ExtensionResultItem(icon=i['icon'],
+ name=i['title'],
+ description=i['category'],
+ highlightable=False,
+ on_enter=OpenUrlAction(i['url'])))
+
+ return RenderResultListAction(items)
+
+ except Exception as err:
+ LOGGING.error(err)
+ return RenderResultListAction([
+ ExtensionResultItem(
+ icon="images/icon.png",
+ name='An error ocurred when searching documentation',
+ description='err',
+ on_enter=HideWindowAction()
+ )
+ ])
+
+if __name__ == '__main__':
+ DocsearchExtension().run()
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..6a37769
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,17 @@
+{
+ "required_api_version": "^2.0.0",
+ "name": "DocSearch",
+ "description": "Full text search on Documentation sites, powered by Algolia",
+ "developer_name": "Bruno Paz",
+ "icon": "images/icon.png",
+ "options": {
+ "query_debounce": 0.1
+ },
+ "preferences": [{
+ "id": "kw",
+ "type": "keyword",
+ "name": "Documenation Search",
+ "default_value": "docs"
+ }
+ ]
+}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..93db6e1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+algoliasearch>=2.0,<3.0
diff --git a/versions.json b/versions.json
new file mode 100644
index 0000000..e7ff41c
--- /dev/null
+++ b/versions.json
@@ -0,0 +1,6 @@
+[
+ {
+ "required_api_version": "^2.0.0",
+ "commit": "master"
+ }
+]