├── test2.sh
├── test.sh
├── test.yaml
├── browser_tabs.json
├── mixcase.rb
├── shellsnip.rb
├── calendar.rb
├── shorten_amazon.rb
├── imdb.rb
├── custom-google.rb
├── manpage.rb
├── stretchlink.rb
├── browser_tabs.js
├── weather.rb
├── README.md
├── lyrics.rb
└── makeadate.rb
/test2.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{ "url": "https://brettterpstra.com", "title": "Script Test", "link_text": "Boogie" }'
4 | exit 0
5 |
--------------------------------------------------------------------------------
/test.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: test shell script
3 | trigger: ^scr$
4 | searches:
5 | - [test, "Test Shell Script Search"]
6 | script: test.sh
7 |
--------------------------------------------------------------------------------
/browser_tabs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browsertab",
3 | "searches": [
4 | ["tabs", "Search Safari tabs"],
5 | ["tabsf", "Frontmost safari tab"],
6 | ["tabc", "Search Chrome tabs"],
7 | ["tabcf", "Frontmost Chrome tab"],
8 | ["tabef", "Frontmost Edge tab"],
9 | ["tabe", "Search Edge tabs"],
10 | ["tabbf", "Frontmost Brave tab"],
11 | ["tabb", "Search Brave tabs"]
12 | ],
13 | "trigger": "^tab[csceb]f?$",
14 | "script": "browser_tabs.js"
15 | }
16 |
--------------------------------------------------------------------------------
/mixcase.rb:
--------------------------------------------------------------------------------
1 | # Always start with module SL
2 | module SL
3 | # Give it a unique class name
4 | class MixCaps
5 | class << self
6 | # Settings block is required with `trigger` and `searches`
7 | def settings
8 | {
9 | trigger: 'mix',
10 | searches: [
11 | ['mix', 'Mix Casing']
12 | ]
13 | }
14 | end
15 |
16 | def search(search_type, search_terms, link_text)
17 | ['embed', mix_case(search_terms), link_text]
18 | end
19 |
20 | def mix_case(string)
21 | string.split(//).map { |s| rand(20000) % 2 == 1 ? s.upcase : s.downcase }
22 | end
23 | end
24 |
25 |
26 | SL::Searches.register 'mix', :search, self
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/shellsnip.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'shellwords'
4 |
5 | module SL
6 | # Inserts a shell command result as an embed
7 | class ShellSnip
8 | class << self
9 | SCRIPTS_PATH = File.expand_path('~/.local/searchlink/scripts/').freeze
10 |
11 | def settings
12 | {
13 | trigger: 'shell',
14 | searches: [
15 | ['shell', 'Inserts Shell-Skript']
16 | ]
17 | }
18 | end
19 |
20 | def search(_, search_terms, link_text)
21 | result = execute_shell_script(search_terms)
22 | result ? ['embed', result, link_text] : false
23 | end
24 |
25 | def execute_shell_script(search_terms)
26 | params = Shellwords.split(search_terms)
27 | script_path = File.join(SCRIPTS_PATH, Shellwords.escape(params.shift))
28 | params.map! { |w| Shellwords.escape(w) }
29 |
30 | full_command = "#{Shellwords.escape(script_path)} #{params.join(' ')}"
31 |
32 | `#{full_command} 2>&1`
33 | end
34 | end
35 |
36 | SL::Searches.register 'shell', :search, self
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/calendar.rb:
--------------------------------------------------------------------------------
1 | module SL
2 | class Calendar
3 | class << self
4 |
5 | def settings
6 | {
7 | trigger: '(cal|days?)',
8 | searches: [
9 | ['cal', 'Calendar'],
10 | ['days', 'Days in month']
11 | ]
12 | }
13 | end
14 |
15 | def search(search_type, search_terms, link_text)
16 | ['embed', calendar(search_type, search_terms), link_text]
17 | end
18 |
19 | def markdownify_line(line)
20 | "|#{line.scan(/.{3}/).join('|')}|"
21 | end
22 |
23 | def calendar(type, string)
24 | if type =~ /cal/
25 | raw = cal(type, string)
26 | lines = raw.split(/\n/)[0..-2]
27 | output = []
28 | header = "[#{lines.shift.strip}]"
29 | output << markdownify_line(lines.shift)
30 | output << '|---|---|---|---|---|---|---|'
31 | output.concat(lines.map { |l| markdownify_line(l) })
32 | output << header
33 | output.join("\n")
34 | else
35 | cal(type, string)
36 | end
37 | end
38 |
39 | def cal(type, string)
40 | case type
41 | when /days?/
42 | case string
43 | when /1?[0-9] \d{4}$/
44 | `cal -h #{string} | awk 'NF {DAYS = $NF}; END {print DAYS}'`
45 | else
46 | `cal -h | awk 'NF {DAYS = $NF}; END {print DAYS}'`
47 | end
48 | when /cal/
49 | case string
50 | when /1?[0-9] \d{4}$/
51 | `cal -h #{string}`
52 | else
53 | `cal -h`
54 | end
55 | end
56 | end
57 | end
58 |
59 | SL::Searches.register 'calendar', :search, self
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/shorten_amazon.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true Shorten Amazon Plugin Takes an
2 | # Amazon product url and shortens it to the minimal
3 | # information needed to work
4 | #
5 | # Optional config:
6 | #
7 | # ```yaml
8 | # stretchlink:
9 | # clean_url: true
10 | # ```
11 | #
12 | # clean_url: removes tracking information from the url
13 | #
14 | module SL
15 | # Amazon Shorten Plugin
16 | class AmazonShorten
17 | class << self
18 | def settings
19 | {
20 | trigger: "sa",
21 | searches: [
22 | ["sa", "Shorten Amazon URLs"],
23 | ],
24 | }
25 | end
26 |
27 | def search(_, search_terms, link_text)
28 | return [search_terms, nil, link_text] unless SL::URL.url?(search_terms) && search_terms =~ /amazon\.com/
29 |
30 | settings = if SL.config.key?("stretchlink")
31 | SL.config["stretchlink"]
32 | else
33 | { "clean_url" => true, "tidy_amazon" => false }
34 | end
35 | query = [
36 | "url=#{ERB::Util.url_encode(search_terms)}",
37 | "clean=#{settings["clean_url"] ? "true" : "false"}",
38 | "tidy_amazon=true",
39 | "output=json",
40 | ]
41 |
42 | res = Curl::Json.new(%(https://stretchlink.cc/api/1?#{query.join("&")}))
43 | json = res.json
44 | if json.nil? || json.empty?
45 | return [search_terms, nil, link_text]
46 | end
47 |
48 | if json["error"]
49 | SL.error("StretchLink Error: #{json["error"]}")
50 | return [search_terms, nil, link_text]
51 | end
52 |
53 | rtitle = SL::URL.title(search_terms) || res["title"]
54 | if link_text.nil? || link_text.empty? || link_text == search_terms
55 | link_text = rtitle
56 | end
57 |
58 | [json["expanded_url"], rtitle, link_text]
59 | end
60 | end
61 |
62 | SL::Searches.register "shorten_amazon", :search, self
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/imdb.rb:
--------------------------------------------------------------------------------
1 | module SL
2 | # Requires google_api_key in ~/.searchlink
3 | class IMDBSearch
4 | class << self
5 | attr_reader :api_key
6 |
7 | def settings
8 | {
9 | trigger: 'imdb[ta]?',
10 | searches: [
11 | ['imdb', 'IMDB.com search'],
12 | ['imdba', 'IMDB.com actor search'],
13 | ['imdbt', 'IMDB.com title search']
14 | ]
15 | }
16 | end
17 |
18 | def test_for_key
19 | return false unless SL.config.key?('google_api_key') && SL.config['google_api_key']
20 |
21 | key = SL.config['google_api_key']
22 | return false if key =~ /^(x{4,})?$/i
23 |
24 | @api_key = key
25 |
26 | true
27 | end
28 |
29 | def search(search_type, search_terms, link_text)
30 | search_id = case search_type
31 | when /a$/
32 | '72c6aaca9f3144c20'
33 | when /t$/
34 | '97e8c7f9186d54bd1'
35 | else
36 | '03c787fbdb87449d1'
37 | end
38 |
39 | unless test_for_key
40 | SL.add_error('api key', 'Missing Google API Key')
41 | return false
42 | end
43 |
44 | url = "https://customsearch.googleapis.com/customsearch/v1?cx=#{search_id}&q=#{ERB::Util.url_encode(search_terms)}&num=1&key=#{@api_key}"
45 | body = `curl -SsL '#{url}'`
46 | json = JSON.parse(body)
47 |
48 | return SL.ddg(terms, link_text) unless json['queries']['request'][0]['totalResults'].to_i.positive?
49 |
50 | result = json['items'][0]
51 | return false if result.nil?
52 |
53 | output_url = result['link']
54 | output_title = result['title']
55 | output_title.remove_seo!(output_url) if SL.config['remove_seo']
56 | [output_url, output_title, link_text]
57 | rescue StandardError
58 | SL.ddg(search_terms, link_text)
59 | end
60 | end
61 |
62 | SL::Searches.register 'brettterpstra', :search, self
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/custom-google.rb:
--------------------------------------------------------------------------------
1 | module SL
2 | # Custom Google Search
3 | # This plugin allows use of a Custom Google Search engine
4 | # 1. Make sure you have a google_api_key configured in ~/.searchlink
5 | # 2. Give the plugin a unique class name.
6 | # 3. Create an engine at https://programmablesearchengine.google.com/controlpanel/all
7 | # and get the ID for it, and insert it into the @search_id variable at line 16
8 | # 4. Modify the trigger regex and search definition (lines 17-21)
9 | # 5. Change the plugin name on line 59
10 | # This example uses a Custom Search Engine that searches only BrettTerpstra.com
11 | class CustomGoogleSearch
12 | class << self
13 | attr_reader :api_key, :search_id
14 |
15 | def settings
16 | {
17 | trigger: "gbt",
18 | searches: [
19 | ["gbt", "BrettTerpstra.com search"],
20 | ],
21 | }
22 | end
23 |
24 | def test_for_key
25 | return false unless SL.config.key?("google_api_key") && SL.config["google_api_key"]
26 |
27 | key = SL.config["google_api_key"]
28 | return false if key =~ /^(x{4,})?$/i
29 |
30 | @api_key = key
31 |
32 | true
33 | end
34 |
35 | def search(_, search_terms, link_text)
36 | search_id = "84d644cd1af424b7a"
37 |
38 | unless test_for_key
39 | SL.add_error("api key", "Missing Google API Key")
40 | return false
41 | end
42 |
43 | url = "https://customsearch.googleapis.com/customsearch/v1?cx=#{search_id}&q=#{ERB::Util.url_encode(search_terms)}&num=1&key=#{@api_key}"
44 | json = Curl::Json.new(url).json
45 |
46 | return SL.ddg(terms, false) unless json["queries"]["request"][0]["totalResults"].to_i.positive?
47 |
48 | result = json["items"][0]
49 | return false if result.nil?
50 |
51 | output_url = result["link"]
52 | output_title = result["title"]
53 | output_title.remove_seo!(output_url) if SL.config["remove_seo"]
54 | [output_url, output_title, link_text]
55 | rescue StandardError
56 | SL.ddg(search_terms, link_text)
57 | end
58 | end
59 |
60 | SL::Searches.register "brettterpstra", :search, self
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/manpage.rb:
--------------------------------------------------------------------------------
1 | module SL
2 | # Man Page Search
3 | class ManPageSearch
4 | class << self
5 | # Returns a hash of settings for the search
6 | #
7 | # @return [Hash] the settings for the search
8 | #
9 | def settings
10 | {
11 | trigger: "x?man",
12 | searches: [
13 | ["xman", "x-man-page url"],
14 | ["man", "Man Page from manpages.org"],
15 | ],
16 | }
17 | end
18 |
19 | # Searches for a man page for given command. Only the first word of the
20 | # search is used. If no man page is found on manpages.org, ss64.com is
21 | # used
22 | #
23 | # @param search_type [String] Search type, unused
24 | # @param search_terms [String] the terms to search for
25 | # @param link_text [String] the text to use for the link
26 | #
27 | # @return [Array] the url, title, and link text for the search
28 | #
29 | def search(search_type, search_terms, link_text)
30 | return ["x-man-page://#{search_terms.split(" ").first}", "#{search_terms} man page", link_text] if search_type == "xman"
31 |
32 | url, title = find_man_page(search_terms)
33 | if url
34 | [url, title, link_text]
35 | else
36 | SL.site_search("ss64.com", search_terms, link_text)
37 | end
38 | end
39 |
40 | # Uses manpages.org autocomplete to validate command
41 | # name. Shortest match returned.
42 | #
43 | # @param term [String] the terms to search for
44 | # @return [Array] the url and title for the search
45 | #
46 | def find_man_page(terms)
47 | terms.split(/ /).each do |term|
48 | autocomplete = "https://manpages.org/pagenames/autocomplete_page_name_name?term=#{ERB::Util.url_encode(term)}"
49 | data = Curl::Json.new(autocomplete).json
50 | next if data.count.zero?
51 |
52 | data.delete_if { |d| d["locale"] != "en" }
53 | shortest = data.min_by { |d| d["permalink"].length }
54 | man = shortest["permalink"]
55 | cat = shortest["category_id"]
56 | url = "https://manpages.org/#{man}/#{cat}"
57 | return [url, get_man_title(url)]
58 | end
59 | [false, false]
60 | rescue StandardError
61 | false
62 | end
63 |
64 | def get_man_title(url)
65 | body = `/usr/bin/curl -sSL '#{url}'`
66 | body.match(%r{(?<=
).*?(?=)})[0].sub(/^man /, "")
67 | end
68 | end
69 |
70 | # Registers the search with the SL::Searches module
71 | SL::Searches.register "manpage", :search, self
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/stretchlink.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # StretchLink Plugin
4 | # Takes a shortened url and expands it
5 | #
6 | # Optional config:
7 | #
8 | # ```yaml
9 | # stretchlink:
10 | # clean_url: true
11 | # tidy_amazon: true
12 | # ```
13 | # clean_url: removes tracking information from the url
14 | # tidy_amazon: reduces amazon url to minimal information
15 | #
16 | module SL
17 | # StretchLink Plugin
18 | class StretchLink
19 | class << self
20 | def settings
21 | {
22 | trigger: "str(?:etch)?",
23 | searches: [
24 | ["stretch", "Expand URLs with stretchlink.cc"],
25 | ["str", "Expand URLs with stretchlink.cc"],
26 | ],
27 | config: [
28 | {
29 | key: "stretchlink",
30 | value: { "clean_url" => false, "tidy_amazon" => false },
31 | required: true,
32 | description: "StretchLink plugin config",
33 | },
34 | ],
35 | }
36 | end
37 |
38 | def search(_, search_terms, link_text)
39 | return [search_terms, nil, link_text] unless SL::URL.url?(search_terms)
40 |
41 | settings = if SL.config.key?("stretchlink")
42 | SL.config["stretchlink"]
43 | else
44 | { "clean_url" => true, "tidy_amazon" => false }
45 | end
46 | query = [
47 | "url=#{ERB::Util.url_encode(search_terms)}",
48 | "clean=#{settings["clean_url"] ? "true" : "false"}",
49 | "tidy_amazon=false",
50 | "output=json",
51 | ]
52 |
53 | res = Curl::Json.new(%(https://stretchlink.cc/api/1?#{query.join("&")}))
54 | json = res.json
55 | return [search_terms, nil, link_text] if json.nil? || json.empty?
56 |
57 | if json["error"]
58 | SL.error("StretchLink Error: #{json["error"]}")
59 | return [search_terms, nil, link_text]
60 | end
61 |
62 | url = if settings["tidy_amazon"] && json["expanded_url"] =~ /amazon/
63 | shorten_amazon(json["expanded_url"])
64 | else
65 | json["expanded_url"]
66 | end
67 |
68 | rtitle = SL::URL.title(url) || res["title"]
69 | link_text = rtitle if link_text.nil? || link_text.empty? || link_text == search_terms
70 |
71 | [url, rtitle, link_text]
72 | end
73 |
74 | def shorten_amazon(url)
75 | uri = URI.parse(url)
76 |
77 | # Return nil if not an Amazon URL
78 | return url unless uri.host.include?("amazon")
79 |
80 | # Extract the product ID from the path
81 | match = uri.path.match(%r{/dp/([^/]*)})
82 | product_id = match ? match[1] : nil
83 |
84 | # Construct the shortened URL if the product ID is found
85 | clean_url = product_id ? "https://www.amazon.com/dp/#{product_id}" : nil
86 |
87 | clean_url + uri.query.to_s.sub(/^&?/, "?")
88 | end
89 | end
90 |
91 | SL::Searches.register "stretchlink", :search, self
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/browser_tabs.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/osascript -l JavaScript
2 |
3 | ObjC.import('stdlib');
4 | ObjC.import('Foundation');
5 |
6 | const stdout = function (msg) {
7 | $.NSFileHandle.fileHandleWithStandardOutput.writeData(
8 | $.NSString.alloc.initWithString(String(msg))
9 | .dataUsingEncoding($.NSUTF8StringEncoding)
10 | )
11 | }
12 |
13 | const stderr = function (msg) {
14 | $.NSFileHandle.fileHandleWithStandardError.writeData(
15 | $.NSString.alloc.initWithString(String(msg))
16 | .dataUsingEncoding($.NSUTF8StringEncoding)
17 | )
18 | }
19 |
20 | const args = $.NSProcessInfo.processInfo.arguments;
21 | // args[0..3] are filename, "/usr/bin/osascript", "-l", "JavaScript"
22 | if (args.count > 4) {
23 | var search_type = args.js[4].js;
24 | var search_terms = args.js[5].js;
25 | var link_text = args.js[6].js;
26 | }
27 |
28 | let saf;
29 | switch (search_type) {
30 | case 'tabb':
31 | case 'tabbf':
32 | saf = Application('Brave');
33 | break;
34 | case 'tabe':
35 | case 'tabef':
36 | saf = Application('Microsoft Edge');
37 | break;
38 | case 'tabc':
39 | case 'tabcf':
40 | saf = Application('Google Chrome');
41 | break;
42 | case 'tabs':
43 | case 'tabsf':
44 | saf = Application('Safari');
45 | }
46 |
47 | saf.includeStandardAdditions = true;
48 |
49 |
50 | String.prototype.to_rx = function(distance) {
51 | return this.split('').join(`.{0,${distance}}`);
52 | }
53 |
54 | if (search_type.endsWith('f')) {
55 | let current_tab_title;
56 | let current_tab_url;
57 | if (search_type.startsWith('tabs')) {
58 | current_tab_title = saf.windows[0].currentTab.name()
59 | current_tab_url = saf.windows[0].currentTab.url()
60 | } else {
61 | current_tab_title = saf.windows[0].activeTab.name()
62 | current_tab_url = saf.windows[0].activeTab.url()
63 | }
64 |
65 | data = {
66 | "url": current_tab_url,
67 | "title": current_tab_title,
68 | "link_text": link_text
69 | }
70 | } else {
71 | var tabs = []
72 |
73 | saf.windows().forEach(function(win) {
74 | win.tabs().forEach(function(tab) {
75 | tabs.push({
76 | 'title': tab.name(),
77 | 'url': tab.url()
78 | });
79 | });
80 | });
81 | tabs.forEach((tab) => stderr(tab['title']));
82 | var terms = search_terms.split(/ +/);
83 | var regex = '';
84 |
85 | var res = null;
86 | var matches = [];
87 | var distance = 0;
88 | while (distance < 5) {
89 | var terms_rx = terms.map(term => term.to_rx(distance));
90 | var rx = new RegExp(terms_rx.join('.*?'), 'i');
91 |
92 | matches = tabs.filter((tab) => { return (rx.test(tab['title']) || rx.test(tab['url'])) });
93 | if (matches.length == 0) {
94 | distance += 1;
95 | } else {
96 | break;
97 | }
98 | }
99 |
100 | if (matches.length > 0) {
101 | res = matches[0];
102 | data = {
103 | 'url': res['url'],
104 | 'title': res['title'],
105 | 'link_text': link_text
106 | }
107 | } else {
108 | data = {
109 | 'url': false,
110 | 'title': null,
111 | 'link_text': link_text
112 | }
113 | }
114 | }
115 |
116 | stdout(JSON.stringify(data));
117 |
--------------------------------------------------------------------------------
/weather.rb:
--------------------------------------------------------------------------------
1 | # Always start with module SL
2 | module SL
3 | # Weather and forecast plugin
4 | # Needs a weatherapi.com API key in config as `weather_api_key: xxxxx`
5 | # Provide a zip or city/region as the search term
6 | class WeatherSearch
7 | class << self
8 | def settings
9 | searches = []
10 | %w[weather current weat wea curr cur].each { |s| searches << [s, 'Embed Current Weather'] }
11 | %w[forecast fore for].each { |s| searches << [s, 'Embed Weather Forecast'] }
12 | {
13 | trigger: '(for(e(cast)?)?|cur(r(ent)?)?|wea(t(her)?)?)',
14 | searches: searches
15 | }
16 | end
17 |
18 | def search(search_type, search_terms, link_text)
19 | api_key = SL.config['weather_api_key']
20 | temp_in = SL.config['weather_temp_in'] =~ /^c/ ? 'temp_c' : 'temp_f'
21 | zip = search_terms.url_encode
22 | url = "http://api.weatherapi.com/v1/forecast.json?key=#{api_key}&q=#{zip}&aqi=no"
23 | res = Curl::Json.new(url)
24 |
25 | raise StandardError, 'Invalid JSON response' unless res
26 |
27 | data = res.json
28 |
29 | loc = "#{data['location']['name']}, #{data['location']['region']}"
30 | clock = Time.parse(data['location']['localtime'])
31 | date = clock.strftime('%Y-%m-%d')
32 | time = clock.strftime('%I:%M %p')
33 |
34 | raise StandardError, 'Missing conditions' unless data['current']
35 |
36 | curr_temp = data['current'][temp_in]
37 | curr_condition = data['current']['condition']['text']
38 |
39 | raise StandardError, 'Missing forecast' unless data['forecast']
40 |
41 | forecast = data['forecast']['forecastday'][0]
42 |
43 | day = forecast['date']
44 | high = forecast['day']["max#{temp_in}"]
45 | low = forecast['day']["min#{temp_in}"]
46 | condition = forecast['day']['condition']['text']
47 |
48 | output = []
49 |
50 | case search_type
51 | when /^[wc]/
52 | output << "Weather for #{loc} on #{date} at #{time}: #{curr_temp} and #{curr_condition} "
53 | else
54 | output << "Forecast for #{loc} on #{day}: #{condition} #{high}/#{low}"
55 | output << "Currently: #{curr_temp} and #{curr_condition}"
56 | output << ''
57 |
58 | output.concat(forecast_to_markdown(forecast['hour'], temp_in))
59 | end
60 |
61 | output.empty? ? false : ['embed', output.join("\n"), link_text]
62 | end
63 |
64 | def forecast_to_markdown(hours, temp_in)
65 | output = []
66 | temps = [
67 | { temp: hours[8][temp_in], condition: hours[8]['condition']['text'] },
68 | { temp: hours[10][temp_in], condition: hours[10]['condition']['text'] },
69 | { temp: hours[12][temp_in], condition: hours[12]['condition']['text'] },
70 | { temp: hours[14][temp_in], condition: hours[14]['condition']['text'] },
71 | { temp: hours[16][temp_in], condition: hours[16]['condition']['text'] },
72 | { temp: hours[18][temp_in], condition: hours[18]['condition']['text'] },
73 | { temp: hours[19][temp_in], condition: hours[20]['condition']['text'] }
74 | ]
75 |
76 | # Hours
77 | hours_text = %w[8am 10am 12pm 2pm 4pm 6pm 8pm]
78 | step_out = ['|']
79 | hours_text.each_with_index do |h, i|
80 | width = temps[i][:condition].length + 1
81 | step_out << format("%#{width}s |", h)
82 | end
83 |
84 | output << step_out.join('')
85 |
86 | # table separator
87 | step_out = ['|']
88 | temps.each do |temp|
89 | width = temp[:condition].length + 1
90 | step_out << "#{'-' * width}-|"
91 | end
92 |
93 | output << step_out.join('')
94 |
95 | # Conditions
96 | step_out = ['|']
97 | temps.each do |temp|
98 | step_out << format(' %s |', temp[:condition])
99 | end
100 |
101 | output << step_out.join('')
102 |
103 | # Temps
104 | step_out = ['|']
105 | temps.each do |temp|
106 | width = temp[:condition].length + 1
107 | step_out << format("%#{width}s |", temp[:temp])
108 | end
109 |
110 | output << step_out.join('')
111 | end
112 | end
113 |
114 | SL::Searches.register 'weather', :search, self
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SearchLink Plugins
2 |
3 |
4 | A collection of plugins for [SearchLink](https://brettterpstra.com/projects/searchlink/ "SearchLink").
5 |
6 | ## Installation
7 |
8 | Just save any of these plugins to `~/.local/searchlink/plugins` and SearchLink will add them as valid searches. You could, if you wanted to, clone the entire project to that folder with:
9 |
10 | ```
11 | $ mkdir -p ~/.local/searchlink
12 | $ cd ~/.local/searchlink
13 | $ git clone git@github.com:ttscoff/searchlink-plugins.git plugins
14 | ```
15 |
16 | > If you intend to develop your own plugins or make modifications to existing ones, fork the repo first and close that to your local drive, allowing you to make pull requests from your fork.
17 |
18 | ## About Plugins
19 |
20 | All of [SearchLink's searches](https://github.com/ttscoff/searchlink/tree/main/lib/searchlink/searches) are defined as [plugins](https://github.com/ttscoff/searchlink/wiki/Plugins). You can duplicate any of them into the plugins folder and override default functionality, or use them as examples for developing your own.
21 |
22 | ## Plugins
23 |
24 | ### Calendar
25 |
26 | Another example of a text filter. This one can insert a Markdown calendar for any month and year. You can define the month and year like `!cal 5 2024` to get a calendar for May, 2024. If you use `!cal now` it will insert a calendar for the current month and year. It can also print how many days are in a month with `!days 2 2024` to show how many days are in February, 2024. Silly, and would probably be better as a TextExpander snippet, but I'm just experimenting with extending SearchLink.
27 |
28 | ### Custom Google
29 |
30 | This plugin demonstrates adding your own custom search engine through Google. Create a [custom engine here](https://programmablesearchengine.google.com/controlpanel/all) to search specific pages/sites, then modify the plugin according to the comments at the top to create your own custom site search.
31 |
32 | Requires a [Google API key](https://github.com/ttscoff/searchlink/wiki/Using-Google-Search).
33 |
34 | ### IMDB
35 |
36 | This plugin uses a custom site search from Google Programmable Search to search IMDB, actors, or titles. Just another demo of how to use custom engines to create plugins. Requires a [Google API key](https://github.com/ttscoff/searchlink/wiki/Using-Google-Search).
37 |
38 | Adds searches `!imdb`, `!imdba` (actor search), and `!imdbt` (tv/movie title search).
39 |
40 | ### Lyrics
41 |
42 | This plugin will search for a song, returning either a link (`!lyric`) or embedding the actual lyrics (`!lyrice`). It demonstrates both search and embed functionality, and is fully commented to serve as an example plugin.
43 |
44 | ### MakeADate
45 |
46 | This is a port of a TextExpander snippet I use. It takes a natural language date and inserts a formatted date. It provides the following formats:
47 |
48 | | Abbr | Result |
49 | | ---------------------- | -------------------------------------- |
50 | | `!ddate tomorrow 8am` | 2023-11-02 8:00am |
51 | | `!dshort tomorrow 8am` | 2023-11-2 8:00am |
52 | | `!diso tomorrow 8am` | 2023-11-02 08:00 |
53 | | `!dlong tomorrow 8am` | Thursday, November 2nd, 2023 at 8:00am |
54 |
55 | ***This plugin requires that PHP be installed on the system, either with the Apple Command Line Utilties (I think), or with Homebrew (`brew install php`).***
56 |
57 | ### ManPage
58 |
59 | This plugin searches mapages.org for a matching command and returns the url, including command name and man section, e.g. `https://manpages.org/lsof/8`, as well as a title with a brief description of the command. So running SearchLink on `!man lsof` would return `[lsof](https://manpages.org/lsof/8 "lsof (8): list open files")`. If the search term contains multiple words, they'll be parsed individually until one of the words gets at least a partial search result, the shortest of which will be used.
60 |
61 | ### MixCase
62 |
63 | This plugin is a text filter that will turn `!mix A string of text` into `A STRInG of TExT`, randomly capitalizing characters. It's just to demonstrate how easily a text filter can be implemented.
64 |
65 | ### Setapp affiliate linking
66 |
67 | There are two plugins, bitly_setapp and isgd_setapp that will take an app name that's avaialable on Setapp and turn it into an affiliate link so you can make some money on recommendations. Links are shortened with either bit.ly or is.gd, depending on the plugin. Bit.ly shortening requires an access token.
68 |
69 | ### StretchLink and Amazon URL tidying
70 |
71 | The stretchlink and shorten_amazon plugins will use [StretchLink.cc](https://stretchlink.cc) to expand shortened URLs and tidy up Amazon URLs.
72 |
73 | ### Weather
74 |
75 | Can output a current conditions with searches !weather and !current, and a forecast with !forecast. All output in Markdown format. Requires a weatherapi.com API key.
76 |
77 | ## Contributing
78 |
79 | Use the sample code in `lyrics.rb` ([documented in the SearchLink wiki](https://github.com/ttscoff/searchlink/wiki/Plugins)) to generate your own plugins. Feel free to fork and submit a PR to this repository if you create something you'd like to share!
80 |
--------------------------------------------------------------------------------
/lyrics.rb:
--------------------------------------------------------------------------------
1 | # Always start with module SL
2 | module SL
3 | # Give it a unique class name
4 | class LyricsSearch
5 | class << self
6 | # Settings block is required with `trigger` and `searches`
7 | def settings
8 | {
9 | # `trigger` is A regular expression that will trigger this plugin
10 | # when used with a bang. The one below will trigger on !lyrics or
11 | # !lyricse.
12 | trigger: 'lyrics?(e|e?js)?',
13 | # Every search that the plugin should execute should be individually
14 | # listed and described in the searches array. This is used for
15 | # completion and help generation. Do not include the bang (!) in the
16 | # search keyword.
17 | searches: [
18 | ['lyric', 'Song Lyrics Search'],
19 | ['lyrice', 'Song Lyrics Embed'],
20 | ['lyricjs', 'Song Lyrics JS Embed']
21 | ]
22 | }
23 | end
24 |
25 | # Every plugin must contain a #search method that takes 3 arguments:
26 | #
27 | # - `search_type` will contain the !search trigger that was used (minus the !)
28 | # - `search_terms` will include everything that came after the !search
29 | # - `link_text` will contain the text that will be used for the linked
30 | # text portion of the link. This can usually remain untouched but must
31 | # be passed back at the end of the function.
32 | def search(search_type, search_terms, link_text)
33 | # You can branch to multiple searches by testing the search_type
34 | case search_type
35 | when /e$/
36 | url, title = SL.ddg("site:genius.com #{search_terms}", link_text)
37 | if url
38 | title = get_lyrics(url)
39 | # To return an embed, set url (first parameter in the return
40 | # array) to 'embed', and put the embed contents in the second
41 | # parameter.
42 | title ? ['embed', title, link_text] : false
43 | else
44 | # Use `SL#add_error(title, text)` to add errors to the HTML
45 | # report. The report will only be shown if errors have been added.
46 | SL.add_error('No lyrics found', "Song lyrics for #{search_terms} not found")
47 | false
48 | end
49 | when /js$/
50 | url, title = SL.ddg("site:genius.com #{search_terms}", link_text)
51 | if url
52 | title = js_embed(url)
53 | title ? ['embed', title, link_text] : false
54 | else
55 | SL.add_error('No lyrics found', "Song lyrics for #{search_terms} not found")
56 | false
57 | end
58 | else
59 | # You can perform a DuckDuckGo search using SL#ddg, passing the
60 | # search terms and link_text. It will return url, title, and
61 | # link_text. SL#ddg will add its own errors, and if it returns false
62 | # that will automatically be tested for, no additional error
63 | # handling is required.
64 | url, title, link_text = SL.ddg("site:genius.com #{search_terms}", link_text)
65 | # Always return an array containing the resulting URL, the title,
66 | # and the link_text variable that was passed in, even if it's
67 | # unmodified.
68 | [url, title, link_text]
69 | end
70 | end
71 |
72 | def js_embed(url)
73 | if SL::URL.valid_link?(url)
74 | body = Curl::Html.new(url).body
75 | api_path = body.match(%r{\\"apiPath\\":\\"(/songs/.*?)\\"})[1]
76 | id = api_path.sub(/.*?(\d+)$/, '\1')
77 | title = body.match(/_sf_async_config.title = '(.*?) \| Genius Lyrics'/)[1].gsub(/\\/, '').sub(/ Lyrics$/, '').scrub
78 |
79 | <<~EOEMBED
80 |
83 |
84 | EOEMBED
85 | else
86 | false
87 | end
88 | end
89 |
90 | # Any additional helper methods can be defined after #search
91 | def get_lyrics(url)
92 | if SL::URL.valid_link?(url)
93 | # You can use Ruby's net/http methods for retrieving pages, but
94 | # `curl -SsL` is faster and easier. Curl::Html.new(url) returns a
95 | # new object containing :body
96 | body = Curl::Html.new(url).body
97 | title = body.match(/_sf_async_config.title = '(.*?) \| Genius Lyrics'/)[1].gsub(/\\/, '').sub(/ Lyrics$/, '').scrub
98 |
99 | matches = body.scan(%r{class="Lyrics-\w+-.*?>(.*?)