# Scholar (sch)

Scholar (`sch`) is a programmable bookmark and pseudo-search engine heavily inspired
by [YubNub](https://yubnub.org/). It is a  lightweight HTTP service for executing
scripts which generate URLs. These URLs are then issued to the user via an HTTP
redirect.

The main use case of sch is as a search engine in a browser toolbar, with
a predefined set of commands and search proxies.

For a hands-on explanation and demonstration of Scholar as a tool, a public hosted
instance is available: https://sch.luphy.net/sch?s=sch_tree

As well as a general use doc: https://sch.luphy.net/sch?s=sch_help

That being said, a key difference is that Scholar is intended to be run locally
on the machine if possible. Information on hosting a public instance can be
found in the "Advanced Usage" section below.

# Install & Run
## TL; DR

```bash
# Install scholar and pandoc
pip install scholar-search
brew install pandoc

# Clone and run sch against the example codex
git clone https://github.com/adammillerio/sch.git
sch --app example_codex run
```

And go to http://localhost:5000/sch?s=sch_help for usage info.

## Detailed

Scholar can be installed via pip:

```bash
pip install scholar-search
```

Alternatively, it can be installed from a local clone of this repository:
```bash
pip install -e .
```

Rendering text pages depends on [`pandoc`](https://pandoc.org/), which can be
installed via homebrew:
```bash
brew install pandoc
```

Or via native package management. Refer to the [Pandoc Manual](https://pandoc.org/installing.html)
for more info.

Commands are loaded into Scholar via a "Codex", which is just a Python file that
has a Flask app factory defined. This file can import other files or define
commands directly.

A basic hello world example (`hello_world.py`):

```python
#!/usr/bin/env python3
from sch import codex


@codex.command(name="hello")
def hello() -> str:
    return "https://github.com/adammillerio/sch"


# Flask Application Factory
# Run with sch --app hello_world run
def create_app():
    return codex.create_app()

```

An extension of the flask CLI, `sch` is provided, which can be used both to run
an HTTP server and to perform completions via the CLI:
```bash
sch --app hello_world search hello                    
https://github.com/adammillerio/sch
```

An example codex with some basic commands is provided at [example_codex.py](example_codex.py).

To start the sch webserver, run the example codex from the root of this repository:
```bash
sch --app example_codex run
 * Serving Flask app 'hello_world'
 * Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
INFO:werkzeug:Press CTRL+C to quit
```

And go to http://localhost:5000/sch?s=sch_help for usage info.

# Usage
`sch` is best explained via hands on examples. This will demonstrate adding a 
single command to `sch`.

## The Goal
Write a scholar command, `gh`, which performs the following actions:

* `gh` - Go to GitHub homepage
    * `https://github.com`
* `gh {repo}` - View a GitHub repo
    * `https://github.com/{repo}`
* `gh search {repo} {query}` - Search a GitHub repo
    * `https://github.com/search?type=code&q=repo:{repo}+{query}`
* `gh search all {query}` - Search all of GitHub
    * `https://github.com/search?type=code&q={query}`

## Running Commands

Scholar commands can also be run interactively via the command line, which use a 
Flask test request context to execute the same HTTP request flow and display the 
generated redirect URL:

```bash
sch --app example_codex search gh
https://github.com
sch --app example_codex search gh search adammillerio/sch test search
https://github.com/search?type=code&q=repo:adammillerio%2Fsch+test+search
```

## Defining Commands
Commands are organized in Python source files which register commands to the root codex.

An example root codex definition file looks like this:

```python
#!/usr/bin/env python3
from sch import codex


@codex.command("gh")
def github(*args: str) -> str:
    """go to github"""

    return "https://github.com"


# Flask Application Factory
# Run with sch --app hello_world run
def create_app():
    return codex.create_app()

```

This leverages Flask [Application Factories](https://flask.palletsprojects.com/en/2.3.x/patterns/appfactories/),
which are used to generate the final Flask implementation at the end of the root
codex. Above this, the Codex can be composed via importing commands or defining
them directly as neecessary.

All commands have the same signature, any amount of strings as arguments, which 
generates another string. All actual HTTP redirection is handled by Flask. In this 
case, this is just a bookmark that makes `gh` go to the home page of GitHub:

```bash
sch --app example_codex search gh
https://github.com
```

This pattern is common enough that there is an `add_bookmark` command for quick 
registration:
```python
from sch import codex

codex.add_bookmark("gh", "https://github.com", "github git host")
```

## Accessing Arguments

All arguments to commands are strings, but they can be arbitrarily named as well
as dynamic:
```python
from sch import codex

@codex.command("gh")
def github(repo: str, *args: str) -> str:
    """go to github or view a repo"""

    if repo:
        return f"https://github.com/{repo}"
    else:
        return "https://github.com"
```

Now supplying a repo goes to the repo page as expected:

```bash
sch --app example_codex search gh adammillerio/sch
https://github.com/adammillerio/sch
```

## Sub-Commands
Commands can have other Commands registered under them in order to handle specific 
named parameters:

```python
from sch import query_args

@github.command("search")
def github_search(repo: str, *args: str) -> str:
    """search a github repo

    return https://github.com/search?type=code&q=repo:{repo}+{*args}
    """

    return f"https://github.com/search?type=code&q=repo:{query_args(repo, *args)}"


@github_search.command("all")
def github_search_all(*args: str) -> str:
    """search all of github

    return https://github.com/search?type=code&q={*args}
    """

    return f"https://github.com/search?type=code&q={query_args(*args)}"
```

This registers specific handlers for `gh search` and `gh search all`, which are reflected in the tree:
```bash
sch --app example_codex search gh sch_tree
gh - go to github or a view a repo
+-- search - search a github repo
   +-- all - search all of github
```

And also implements the last two search commands:
```bash
sch --app example_codex search gh search adammillerio/sch search query
https://github.com/search?type=code&q=repo:adammillerio%2Fsch+search+query
./sch.sh gh search all search query
sch --app example_codex search gh search all search query
https://github.com/search?type=code&q=search+query
```

These are search "proxies", which are basically just commands which take up all
the unused arguments as `*args` and sends them off as a search to a specific
place, such as GitHub. This is done via the `query_args` utility function.

# Advanced Usage
## Composing Commands
Commands can be composed without being registered to Scholar via the "generic" 
command decorator. For example, to make a factory for code bookmarks that generates 
GitHub and hosted doc links:

```python
from sch import codex, command, Command, format_doc

def repo_command(repo: str, docs: str) -> Command:
    @command(tags=["code"])
    @format_doc(repo=repo)
    def code_repo() -> str:
        """go to {repo} on github"""

        return f"https://github.com/{repo}"

    @code_repo.command("docs")
    @format_doc(repo=repo)
    def code_repo_docs() -> str:
        """go to hosted docs for {repo}"""

        return docs
    
    return code_repo


codex.add_command(
    repo_command("pallets/click", "https://click.palletsprojects.com/en/8.1.x"),
    "click",
)
codex.add_command(
    repo_command("pallets/flask", "https://flask.palletsprojects.com/en/3.0.x/"),
    "flask",
)
```

Which registers code bookmark sets for click and flask:
```bash
sch --app example_codex search click
https://github.com/pallets/click
./sch.sh click docs
sch --app example_codex search click docs
https://click.palletsprojects.com/en/8.1.x
sch --app example_codex search flask
https://github.com/pallets/flask
sch --app example_codex search flask docs
https://flask.palletsprojects.com/en/3.0.x/
```

Additionally, the `format_doc` utility decorator can be used to format the 
Command's docstring after definition to provide context specific `sch_help` 
and `sch_tree` information.

## Longform Command Help

To provide longform command help, a docstring can be provided:
```python
from sch import codex

@codex.command("gh")
def github(repo: str, *args: str) -> str:
    """go to github or a view a repo
    
    If a repo is provided, go to the repo on GitHub.

    If no repo is provided, go to the GitHub homepage.
    """

    if repo:
        return f"https://github.com/{repo}"
    else:
        return "https://github.com"
```

The first line of the docstring will become the command short help, which will 
display in the sch_tree:
```bash
sch --app example_codex search gh sch_tree
gh - go to github or a view a repo
+-- search - search a github repo
   +--all - search all of github
```

The entire docstring will be printed if `sch_help` is the first argument to any command:
```bash
sch --app example_codex search gh sch_help
def sch gh(repo: Optional[str] = None) -> str:

        go to github or a view a repo
        
        if repo:
            return https://github.com/{repo}
        else:
            return https://github.com
```

## Exposing to the Internet
Sometimes it is useful to expose sch to the internet, ie to be used with mobile 
devices. To do this, first install a production WSGI server like Waitress:

```bash
pip install waitress
```

Then use the `waitress-serve` CLI to load and serve the Flask server:

```bash
waitress-serve --port 5000 --call example_codex:create_app
```

More info on using Flask with Waitress is available in the [Flask Docs](https://flask.palletsprojects.com/en/2.3.x/deploying/waitress/)

The WSGI server itself can then be exposed to the internet via reverse proxy via
https. Be sure to change the URL scheme when doing this or generated URLs will
be incorrect:

```bash
waitress-serve --port 5000 --url-scheme https --call example_codex:create_app
```
