Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.so
cmd/elephant/elephant
tmp
.vscode
.direnv
5 changes: 5 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
gcc
protobuf
protoc-gen-go
air
wl-clipboard
libqalculate
imagemagick
bluez
];
};
});
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/yalue/native_endian v1.0.2 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/text v0.31.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
Expand Down
3 changes: 3 additions & 0 deletions internal/providers/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ var (
QueryProviders map[uint32][]string
libDirs = []string{
"/usr/lib/elephant",
"/usr/lib64/elephant",
"/usr/local/lib/elephant",
"/usr/local/lib64/elephant",
"/lib/elephant",
"/lib64/elephant",
}
)

Expand Down
66 changes: 64 additions & 2 deletions internal/providers/websearch/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,74 @@
### Elephant Websearch
### Elephant Websearch Provider

Search the web with custom defined search engines.

#### Example entry
#### Features

- Opening URLs directly
- Define custom search engines
- Search with custom prefixes
- Auto complete suggestions from any JSON API
- Search multiple engines simultaneously with deduplicated suggestions
- Engine finder to search configured engines

#### Example Config

```toml
max_api_items = 10
engine_finder_default_single = false

# name and url are required
[[entries]]
default = true
name = "Example"
url = "https://www.example.com/search?q=%TERM%"

[[entries]]
default = true
default_single = true
name = "DuckDuckGo"
icon = "duckduckgo"
prefix = "@d"
url = "https://duckduckgo.com/?q=%TERM%"
suggestions_url = "https://ac.duckduckgo.com/ac/?q=%TERM%"
suggestions_path = "#.phrase"

[[entries]]
name = "Google"
icon = "google"
prefix = "@g"
url = "https://www.google.com/search?q=%TERM%"
suggestions_url = "https://suggestqueries.google.com/complete/search?client=firefox&q=%TERM%"
suggestions_path = "1"
```

#### Tips

1. The engine URL and suggestions API don't need to match

```toml
[[entries]]
name = "Crunchyroll"
prefix = "@anime"
url = "https://www.crunchyroll.com/search?q=%TERM%"
# Suggestions from MyAnimeList for Crunchyroll search
suggestions_url = "https://myanimelist.net/search/prefix.json?type=all&keyword=%TERM%&v=1"
suggestions_path = "categories.#(type==\"anime\").items.#.name"
```

2. Give multiple engines the same prefix to query them simultaneously

```toml
[[entries]]
name = "Amazon"
icon = "amazon"
prefix = "@shop"
url = "https://www.amazon.ca/s?k=%TERM%"

[[entries]]
name = "Newegg"
prefix = "@shop"
url = "https://www.newegg.ca/p/pl?d=%TERM%"
suggestions_url = "https://www.newegg.ca/api/SearchKeyword?keyword=%TERM%"
suggestions_path = "suggestion.keywords.#.keyword"
```
119 changes: 119 additions & 0 deletions internal/providers/websearch/activate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"fmt"
"log/slog"
"net"
"net/url"
"os"
"os/exec"
"strings"
"syscall"

"al.essio.dev/pkg/shellescape"
"github.com/abenz1267/elephant/v2/pkg/common"
"github.com/abenz1267/elephant/v2/pkg/common/history"
)

const (
ActionSearch = "search"
ActionSearchSuggestion = "search_suggestion"
ActionOpenURL = "open_url"
)

func Activate(single bool, identifier, action string, query string, args string, format uint8, conn net.Conn) {
switch action {
case ActionOpenURL:
address := query
if !strings.Contains(address, "://") {
address = fmt.Sprintf("https://%s", query)
}

openURL(address)

case ActionSearch:
engine := engineIdentifierMap[identifier]

if args == "" {
args = query
}

_, args = splitEnginePrefix(args)

address := expandSubstitutions(engine.URL, args)
run(query, identifier, address)

case ActionSearchSuggestion:
currentSuggestionsMutex.RLock()
s, ok := currentSuggestions[identifier]
currentSuggestionsMutex.RUnlock()
if !ok {
slog.Error(Name, "activate", "missing suggestion", "id", identifier)
return
}

url := expandSubstitutions(s.Engine.URL, s.Content)
run(query, s.Engine.Identifier, url)

case history.ActionDelete:
h.Remove(identifier)

default:
if !config.EnginesAsActions {
slog.Error(Name, "activate", fmt.Sprintf("unknown action: %s", action))
return
}

if args == "" {
args = query
}

engine := engineNameMap[action]
if engine == nil {
slog.Error(Name, "activate", "unknown engine", "action", action)
return
}

q := engine.URL
q = expandSubstitutions(q, args)
run(query, identifier, q)
}
}

func expandSubstitutions(format string, args string) string {
result := format
if strings.Contains(format, "%CLIPBOARD%") {
clipboardText := common.ClipboardText()
if clipboardText == "" {
slog.Error(Name, "activate", "empty clipboard")
}

result = strings.ReplaceAll(os.ExpandEnv(format), "%CLIPBOARD%", url.QueryEscape(clipboardText))
} else if strings.Contains(format, "%TERM%") {
result = strings.ReplaceAll(os.ExpandEnv(format), "%TERM%", url.QueryEscape(args))
}

return result
}

func run(query, identifier, url string) {
openURL(url)

if config.History {
h.Save(query, identifier)
}
}

func openURL(url string) {
cmdStr := fmt.Sprintf("%s %s %s", common.LaunchPrefix(""), config.Command, shellescape.Quote(url))
cmd := exec.Command("sh", "-c", strings.TrimSpace(cmdStr))

cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}

if err := cmd.Start(); err != nil {
slog.Error(Name, "executeCommand", err)
return
}

go cmd.Wait()
}
Loading