summon-open 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +85 -35
- package/lib/config.js +54 -3
- package/lib/fuzzy.js +68 -0
- package/lib/picker.js +52 -18
- package/package.json +1 -1
- package/readme.md +56 -15
package/cli.js
CHANGED
|
@@ -8,12 +8,13 @@ import clipboard from 'clipboardy';
|
|
|
8
8
|
import {temporaryWrite} from 'tempy';
|
|
9
9
|
import {fileTypeFromBuffer} from 'file-type';
|
|
10
10
|
import {
|
|
11
|
-
readConfig,
|
|
12
11
|
readBookmarks,
|
|
13
12
|
saveBookmark,
|
|
14
13
|
removeBookmark,
|
|
15
14
|
readHistory,
|
|
16
15
|
addHistory,
|
|
16
|
+
readSearchEngines,
|
|
17
|
+
defaultSearchEngineName,
|
|
17
18
|
} from './lib/config.js';
|
|
18
19
|
import {
|
|
19
20
|
isUrl,
|
|
@@ -38,6 +39,8 @@ const cli = meow(`
|
|
|
38
39
|
--extension File extension for when stdin file type cannot be detected
|
|
39
40
|
--dry-run, -n Print what would be opened without opening it
|
|
40
41
|
--search, -s Treat the input as a search query
|
|
42
|
+
--engine, -e Search engine to use with --search (see --engines)
|
|
43
|
+
--engines List available search engines
|
|
41
44
|
--clipboard, -c Open the URL/path currently on the clipboard
|
|
42
45
|
--reveal, -r Reveal the file/folder in your file manager
|
|
43
46
|
--recent Pick from recently opened items
|
|
@@ -53,6 +56,7 @@ const cli = meow(`
|
|
|
53
56
|
$ summon @docs # open a saved bookmark
|
|
54
57
|
$ summon https://docs.example.com --save docs
|
|
55
58
|
$ summon -s "rust async traits"
|
|
59
|
+
$ summon -s "flatMap" -e mdn
|
|
56
60
|
$ echo '<h1>Hi</h1>' | summon --extension=html
|
|
57
61
|
$ summon report.pdf --reveal
|
|
58
62
|
$ summon --recent
|
|
@@ -64,6 +68,8 @@ const cli = meow(`
|
|
|
64
68
|
extension: {type: 'string'},
|
|
65
69
|
dryRun: {type: 'boolean', default: false, shortFlag: 'n'},
|
|
66
70
|
search: {type: 'boolean', default: false, shortFlag: 's'},
|
|
71
|
+
engine: {type: 'string', shortFlag: 'e'},
|
|
72
|
+
engines: {type: 'boolean', default: false},
|
|
67
73
|
clipboard: {type: 'boolean', default: false, shortFlag: 'c'},
|
|
68
74
|
reveal: {type: 'boolean', default: false, shortFlag: 'r'},
|
|
69
75
|
recent: {type: 'boolean', default: false},
|
|
@@ -94,20 +100,43 @@ const stderr = message => process.stderr.write(`${message}\n`);
|
|
|
94
100
|
const stdout = message => process.stdout.write(`${message}\n`);
|
|
95
101
|
|
|
96
102
|
async function main() {
|
|
97
|
-
|
|
103
|
+
if (handleManagementCommand()) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bookmarks = readBookmarks();
|
|
108
|
+
const plan = await collectInputs(bookmarks);
|
|
109
|
+
if (plan.done) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (plan.stdinMode) {
|
|
114
|
+
await handleStdin();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await act(plan.rawInputs, bookmarks);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle commands that never open anything. Returns true when one ran.
|
|
122
|
+
function handleManagementCommand() {
|
|
98
123
|
if (flags.bookmarks) {
|
|
99
124
|
listBookmarks();
|
|
100
|
-
return;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (flags.engines) {
|
|
129
|
+
listSearchEngines();
|
|
130
|
+
return true;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
if (flags.removeBookmark !== undefined) {
|
|
104
|
-
|
|
105
|
-
if (!removed) {
|
|
134
|
+
if (!removeBookmark(flags.removeBookmark)) {
|
|
106
135
|
throw new SummonError(`No such bookmark: ${flags.removeBookmark}`, 3);
|
|
107
136
|
}
|
|
108
137
|
|
|
109
138
|
stdout(`Removed bookmark: ${flags.removeBookmark}`);
|
|
110
|
-
return;
|
|
139
|
+
return true;
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
if (flags.save !== undefined) {
|
|
@@ -118,14 +147,14 @@ async function main() {
|
|
|
118
147
|
const value = resolveTarget(targets[0], readBookmarks());
|
|
119
148
|
saveBookmark(flags.save, value);
|
|
120
149
|
stdout(`Saved bookmark ${flags.save} → ${value}`);
|
|
121
|
-
return;
|
|
150
|
+
return true;
|
|
122
151
|
}
|
|
123
152
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let rawInputs;
|
|
127
|
-
let stdinMode = false;
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
128
155
|
|
|
156
|
+
// Decide what to open. Returns {rawInputs} | {stdinMode:true} | {done:true}.
|
|
157
|
+
async function collectInputs(bookmarks) {
|
|
129
158
|
if (flags.clipboard) {
|
|
130
159
|
const clipboardText = await clipboard.read();
|
|
131
160
|
const text = clipboardText.trim();
|
|
@@ -133,39 +162,46 @@ async function main() {
|
|
|
133
162
|
throw new SummonError('Clipboard is empty', 4);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
|
-
rawInputs
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
165
|
+
return {rawInputs: text.split(/\s+/v).filter(Boolean)};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (flags.search) {
|
|
169
|
+
return {rawInputs: [searchUrlFromTargets()]};
|
|
170
|
+
}
|
|
141
171
|
|
|
142
|
-
|
|
143
|
-
} else if (flags.recent) {
|
|
172
|
+
if (flags.recent) {
|
|
144
173
|
const chosen = await pick('Recently opened', readHistory().map(item => ({label: item, value: item})));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
174
|
+
return chosen ? {rawInputs: [chosen]} : {done: true};
|
|
175
|
+
}
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
177
|
+
if (targets.length > 0) {
|
|
178
|
+
return {rawInputs: targets};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (process.stdin.isTTY) {
|
|
153
182
|
const chosen = await interactivePicker(bookmarks);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
183
|
+
return chosen ? {rawInputs: [chosen]} : {done: true};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {stdinMode: true};
|
|
187
|
+
}
|
|
157
188
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
function searchUrlFromTargets() {
|
|
190
|
+
if (targets.length === 0) {
|
|
191
|
+
throw new SummonError('Provide a search query, e.g. `summon -s "hello world"`', 4);
|
|
161
192
|
}
|
|
162
193
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
const engines = readSearchEngines();
|
|
195
|
+
const engineName = flags.engine ?? defaultSearchEngineName();
|
|
196
|
+
const template = engines[engineName];
|
|
197
|
+
if (!template) {
|
|
198
|
+
throw new SummonError(`Unknown search engine: ${engineName}. Available: ${Object.keys(engines).toSorted().join(', ')}`, 4);
|
|
166
199
|
}
|
|
167
200
|
|
|
168
|
-
|
|
201
|
+
return buildSearchUrl(targets.join(' '), template);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function act(rawInputs, bookmarks) {
|
|
169
205
|
const expanded = rawInputs.map(input => expandBookmark(input, bookmarks));
|
|
170
206
|
|
|
171
207
|
if (flags.reveal) {
|
|
@@ -249,6 +285,20 @@ function listBookmarks() {
|
|
|
249
285
|
}
|
|
250
286
|
}
|
|
251
287
|
|
|
288
|
+
function listSearchEngines() {
|
|
289
|
+
const engines = readSearchEngines();
|
|
290
|
+
const defaultName = defaultSearchEngineName();
|
|
291
|
+
const names = Object.keys(engines).toSorted();
|
|
292
|
+
const width = Math.max(...names.map(name => name.length));
|
|
293
|
+
|
|
294
|
+
for (const name of names) {
|
|
295
|
+
const marker = name === defaultName ? '*' : ' ';
|
|
296
|
+
stdout(`${marker} ${name.padEnd(width)} ${engines[name]}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
stdout('\n* = default. Choose another with: summon -s "query" -e <name>');
|
|
300
|
+
}
|
|
301
|
+
|
|
252
302
|
async function interactivePicker(bookmarks) {
|
|
253
303
|
const items = [
|
|
254
304
|
...Object.entries(bookmarks).map(([name, target]) => ({label: `@${name} → ${target}`, value: `@${name}`})),
|
package/lib/config.js
CHANGED
|
@@ -10,11 +10,30 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const HISTORY_LIMIT = 50;
|
|
12
12
|
|
|
13
|
+
// Built-in search engines. Each value is a URL template where `%s` is replaced
|
|
14
|
+
// by the URL-encoded query. Users can add/override these in config.json.
|
|
15
|
+
const DEFAULT_SEARCH_ENGINES = {
|
|
16
|
+
google: 'https://www.google.com/search?q=%s',
|
|
17
|
+
ddg: 'https://duckduckgo.com/?q=%s',
|
|
18
|
+
bing: 'https://www.bing.com/search?q=%s',
|
|
19
|
+
brave: 'https://search.brave.com/search?q=%s',
|
|
20
|
+
npm: 'https://www.npmjs.com/search?q=%s',
|
|
21
|
+
gh: 'https://github.com/search?q=%s&type=repositories',
|
|
22
|
+
mdn: 'https://developer.mozilla.org/en-US/search?q=%s',
|
|
23
|
+
so: 'https://stackoverflow.com/search?q=%s',
|
|
24
|
+
yt: 'https://www.youtube.com/results?search_query=%s',
|
|
25
|
+
wiki: 'https://en.wikipedia.org/w/index.php?search=%s',
|
|
26
|
+
};
|
|
27
|
+
|
|
13
28
|
const DEFAULT_CONFIG = {
|
|
14
29
|
// Map of alias -> target. Resolved when you run `summon @alias` or `summon alias`.
|
|
15
30
|
bookmarks: {},
|
|
16
|
-
//
|
|
17
|
-
|
|
31
|
+
// Named search engines, merged with the built-ins above.
|
|
32
|
+
searchEngines: {},
|
|
33
|
+
// Which engine `--search` uses when `--engine` isn't given.
|
|
34
|
+
defaultSearchEngine: 'google',
|
|
35
|
+
// Legacy single-engine field, still honored as a fallback.
|
|
36
|
+
searchEngine: undefined,
|
|
18
37
|
};
|
|
19
38
|
|
|
20
39
|
/**
|
|
@@ -68,12 +87,44 @@ function writeJson(filePath, data) {
|
|
|
68
87
|
/**
|
|
69
88
|
Read the full config, merged with defaults.
|
|
70
89
|
|
|
71
|
-
@returns {{bookmarks: Record<string, string>,
|
|
90
|
+
@returns {{bookmarks: Record<string, string>, searchEngines: Record<string, string>, defaultSearchEngine: string, searchEngine?: string}}
|
|
72
91
|
*/
|
|
73
92
|
export function readConfig() {
|
|
74
93
|
return {...DEFAULT_CONFIG, ...readJson(configPath(), {})};
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
/**
|
|
97
|
+
All available search engines: built-ins merged with the user's `searchEngines`.
|
|
98
|
+
A legacy top-level `searchEngine` is exposed as the `custom` engine.
|
|
99
|
+
|
|
100
|
+
@returns {Record<string, string>}
|
|
101
|
+
*/
|
|
102
|
+
export function readSearchEngines() {
|
|
103
|
+
const config = readConfig();
|
|
104
|
+
const engines = {...DEFAULT_SEARCH_ENGINES, ...config.searchEngines};
|
|
105
|
+
|
|
106
|
+
if (typeof config.searchEngine === 'string' && config.searchEngine.length > 0) {
|
|
107
|
+
engines.custom = config.searchEngine;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return engines;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
The name of the engine `--search` uses when `--engine` isn't provided.
|
|
115
|
+
|
|
116
|
+
@returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function defaultSearchEngineName() {
|
|
119
|
+
const config = readConfig();
|
|
120
|
+
|
|
121
|
+
if (typeof config.searchEngine === 'string' && config.searchEngine.length > 0 && !config.defaultSearchEngine) {
|
|
122
|
+
return 'custom';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return config.defaultSearchEngine ?? 'google';
|
|
126
|
+
}
|
|
127
|
+
|
|
77
128
|
/** @returns {Record<string, string>} */
|
|
78
129
|
export function readBookmarks() {
|
|
79
130
|
return readConfig().bookmarks ?? {};
|
package/lib/fuzzy.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Word-boundary characters that earn a scoring bonus when a match starts there.
|
|
2
|
+
const BOUNDARY = /[\s\-_\/.@:]/v;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
Score how well `query` fuzzy-matches `target`.
|
|
6
|
+
|
|
7
|
+
Returns a non-negative score (higher is better) when every character of the
|
|
8
|
+
query appears in order within the target, or `-1` when there's no match.
|
|
9
|
+
Consecutive matches and matches at word boundaries score higher.
|
|
10
|
+
|
|
11
|
+
@param {string} query
|
|
12
|
+
@param {string} target
|
|
13
|
+
@returns {number}
|
|
14
|
+
*/
|
|
15
|
+
export function fuzzyScore(query, target) {
|
|
16
|
+
const q = query.toLowerCase();
|
|
17
|
+
const t = target.toLowerCase();
|
|
18
|
+
|
|
19
|
+
if (q === '') {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let score = 0;
|
|
24
|
+
let index = 0;
|
|
25
|
+
let consecutive = 0;
|
|
26
|
+
|
|
27
|
+
for (const char of q) {
|
|
28
|
+
const found = t.indexOf(char, index);
|
|
29
|
+
if (found === -1) {
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let bonus = 1;
|
|
34
|
+
|
|
35
|
+
if (found === index && index > 0) {
|
|
36
|
+
consecutive += 1;
|
|
37
|
+
bonus += consecutive * 2;
|
|
38
|
+
} else {
|
|
39
|
+
consecutive = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (found === 0 || BOUNDARY.test(t[found - 1])) {
|
|
43
|
+
bonus += 3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
score += bonus;
|
|
47
|
+
index = found + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return score;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
Filter and rank items by fuzzy match against a label.
|
|
55
|
+
|
|
56
|
+
@template T
|
|
57
|
+
@param {string} query
|
|
58
|
+
@param {T[]} items
|
|
59
|
+
@param {(item: T) => string} toLabel
|
|
60
|
+
@returns {T[]} Matching items, best first.
|
|
61
|
+
*/
|
|
62
|
+
export function fuzzyFilter(query, items, toLabel = String) {
|
|
63
|
+
return items
|
|
64
|
+
.map(item => ({item, score: fuzzyScore(query, toLabel(item))}))
|
|
65
|
+
.filter(entry => entry.score >= 0)
|
|
66
|
+
.toSorted((a, b) => b.score - a.score)
|
|
67
|
+
.map(entry => entry.item);
|
|
68
|
+
}
|
package/lib/picker.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import readline from 'node:readline/promises';
|
|
3
|
+
import {fuzzyFilter} from './fuzzy.js';
|
|
4
|
+
|
|
5
|
+
const PAGE_SIZE = 15;
|
|
6
|
+
|
|
7
|
+
function render(title, items) {
|
|
8
|
+
process.stderr.write(`\n${title}\n\n`);
|
|
9
|
+
|
|
10
|
+
const shown = items.slice(0, PAGE_SIZE);
|
|
11
|
+
for (const [index, item] of shown.entries()) {
|
|
12
|
+
process.stderr.write(` ${String(index + 1).padStart(2)}. ${item.label}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (items.length > shown.length) {
|
|
16
|
+
process.stderr.write(` … ${items.length - shown.length} more (type to filter)\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.stderr.write('\n');
|
|
20
|
+
return shown;
|
|
21
|
+
}
|
|
3
22
|
|
|
4
23
|
/**
|
|
5
|
-
Show
|
|
24
|
+
Show an interactive fuzzy picker and return the chosen value.
|
|
25
|
+
|
|
26
|
+
Type to fuzzy-filter, enter a number to select from the visible list, or press
|
|
27
|
+
Enter on an empty line to cancel. When a filter narrows results to one, it's
|
|
28
|
+
selected automatically.
|
|
6
29
|
|
|
7
30
|
@param {string} title
|
|
8
31
|
@param {Array<{label: string, value: string}>} items
|
|
9
|
-
@returns {Promise<string | undefined>}
|
|
32
|
+
@returns {Promise<string | undefined>}
|
|
10
33
|
*/
|
|
11
34
|
export async function pick(title, items) {
|
|
12
35
|
if (items.length === 0) {
|
|
@@ -14,29 +37,40 @@ export async function pick(title, items) {
|
|
|
14
37
|
}
|
|
15
38
|
|
|
16
39
|
const rl = readline.createInterface({input: process.stdin, output: process.stderr});
|
|
40
|
+
let current = items;
|
|
17
41
|
|
|
18
42
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
for (;;) {
|
|
44
|
+
const shown = render(title, current);
|
|
45
|
+
// eslint-disable-next-line no-await-in-loop
|
|
46
|
+
const response = await rl.question('Type to filter, number to select, Enter to cancel: ');
|
|
47
|
+
const answer = response.trim();
|
|
23
48
|
|
|
24
|
-
|
|
49
|
+
if (answer === '') {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
if (/^\d+$/v.test(answer)) {
|
|
54
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
55
|
+
if (index >= 0 && index < shown.length) {
|
|
56
|
+
return shown[index].value;
|
|
57
|
+
}
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
process.stderr.write('Invalid selection.\n');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
if (Number.isInteger(index) && index >= 0 && index < items.length) {
|
|
35
|
-
return items[index].value;
|
|
36
|
-
}
|
|
63
|
+
const filtered = fuzzyFilter(answer, items, item => item.label);
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
if (filtered.length === 0) {
|
|
66
|
+
process.stderr.write('No matches.\n');
|
|
67
|
+
current = items;
|
|
68
|
+
} else if (filtered.length === 1) {
|
|
69
|
+
return filtered[0].value;
|
|
70
|
+
} else {
|
|
71
|
+
current = filtered;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
40
74
|
} finally {
|
|
41
75
|
rl.close();
|
|
42
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summon-open",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Open URLs, files, folders, and apps from one cross-platform command — with bookmarks, search, clipboard, reveal, dry-run, and stdin support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/readme.md
CHANGED
|
@@ -21,9 +21,10 @@ Bookmarks, search, clipboard, reveal-in-file-manager, dry-run, multiple targets,
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!--
|
|
24
|
-
DEMO:
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
DEMO: a ready-to-run VHS script lives at media/demo.tape.
|
|
25
|
+
1. Install VHS: https://github.com/charmbracelet/vhs
|
|
26
|
+
2. From the repo root, run: vhs media/demo.tape
|
|
27
|
+
3. It writes media/demo.gif — then uncomment the line below.
|
|
27
28
|
<div align="center"><img src="media/demo.gif" alt="summon demo" width="700"></div>
|
|
28
29
|
-->
|
|
29
30
|
|
|
@@ -107,10 +108,10 @@ summon "$url"
|
|
|
107
108
|
- **Multiple targets** — `summon a.pdf b.png https://x.com` opens them all.
|
|
108
109
|
- **Smart URL normalization** — `summon github.com` just works; the `https://` is added for you.
|
|
109
110
|
- **Bookmarks** — save aliases (`summon @docs`) and turn `summon` into a daily driver.
|
|
110
|
-
- **Web search** — `summon -s "query"`
|
|
111
|
+
- **Web search** — `summon -s "query"` searches with a configurable engine (`-e mdn`, `-e npm`, …).
|
|
111
112
|
- **Clipboard mode** — `summon -c` opens whatever URL/path you just copied.
|
|
112
113
|
- **Reveal** — `summon -r file` highlights it in Finder/Explorer/your file manager.
|
|
113
|
-
- **Recent +
|
|
114
|
+
- **Recent + fuzzy picker** — re-open recent items or fuzzy-search a menu of bookmarks/history.
|
|
114
115
|
- **Dry run** — `--dry-run` prints exactly what would happen without doing it.
|
|
115
116
|
- **Pick the app** — force a specific app and pass it arguments.
|
|
116
117
|
- **Pipe-friendly** — stream data over stdin; the file type is auto-detected.
|
|
@@ -130,12 +131,19 @@ This installs the `summon` command. Or run it once without installing:
|
|
|
130
131
|
npx summon-open https://github.com
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
<summary>Other install channels</summary>
|
|
134
|
+
**Homebrew** (macOS/Linux):
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
```sh
|
|
137
|
+
brew install --formula https://raw.githubusercontent.com/Aditya060806/summon/main/dist/homebrew/summon-open.rb
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Scoop** (Windows):
|
|
141
|
+
|
|
142
|
+
```powershell
|
|
143
|
+
scoop install https://raw.githubusercontent.com/Aditya060806/summon/main/dist/scoop/summon-open.json
|
|
144
|
+
```
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
Both channels install the same `summon` command and depend on Node.js. The manifests live in [`dist/`](dist).
|
|
139
147
|
|
|
140
148
|
## Quick start
|
|
141
149
|
|
|
@@ -191,6 +199,8 @@ The whole tool at a glance — task on the left, command on the right.
|
|
|
191
199
|
| List bookmarks | `summon --bookmarks` |
|
|
192
200
|
| Remove a bookmark | `summon --remove-bookmark docs` |
|
|
193
201
|
| Search the web | `summon -s "query here"` |
|
|
202
|
+
| Search with a specific engine | `summon -s "flatMap" -e mdn` |
|
|
203
|
+
| List search engines | `summon --engines` |
|
|
194
204
|
| Open clipboard URL | `summon -c` |
|
|
195
205
|
| Reveal in file manager | `summon report.pdf -r` |
|
|
196
206
|
| Re-open something recent | `summon --recent` |
|
|
@@ -243,6 +253,8 @@ $ summon --help
|
|
|
243
253
|
| `--extension` | | string | auto | Extension to use for piped stdin when the type can't be auto-detected. |
|
|
244
254
|
| `--dry-run` | `-n` | boolean | `false` | Print what would be opened without launching anything. |
|
|
245
255
|
| `--search` | `-s` | boolean | `false` | Treat the input as a web search query. |
|
|
256
|
+
| `--engine <name>` | `-e` | string | config | Search engine to use with `--search`. See `--engines`. |
|
|
257
|
+
| `--engines` | | boolean | `false` | List available search engines (`*` marks the default). |
|
|
246
258
|
| `--clipboard` | `-c` | boolean | `false` | Open the URL/path currently on the clipboard. |
|
|
247
259
|
| `--reveal` | `-r` | boolean | `false` | Reveal/highlight the file or folder in the file manager. |
|
|
248
260
|
| `--recent` | | boolean | `false` | Interactively pick from recently opened items. |
|
|
@@ -292,7 +304,15 @@ Bookmarks live in your [config file](#configuration) and can also be edited by h
|
|
|
292
304
|
summon -s "how to exit vim"
|
|
293
305
|
```
|
|
294
306
|
|
|
295
|
-
|
|
307
|
+
Pick an engine per search with `--engine`/`-e`, and list what's available with `--engines`:
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
summon -s "flatMap" -e mdn
|
|
311
|
+
summon -s "left-pad" -e npm
|
|
312
|
+
summon --engines
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Built-in engines include `google` (default), `ddg`, `bing`, `brave`, `npm`, `gh`, `mdn`, `so`, `yt`, and `wiki`. Add your own or change the default in [Configuration](#configuration).
|
|
296
316
|
|
|
297
317
|
### Clipboard
|
|
298
318
|
|
|
@@ -326,7 +346,7 @@ summon report.pdf --reveal
|
|
|
326
346
|
summon --recent
|
|
327
347
|
```
|
|
328
348
|
|
|
329
|
-
Run `summon` with no arguments in a terminal and it shows an interactive
|
|
349
|
+
Run `summon` with no arguments in a terminal and it shows an interactive **fuzzy picker** of your bookmarks and recent items. Just start typing to filter (fuzzy, so `ghb` matches `github`), enter a number to select from the visible list, or press Enter on an empty line to cancel. When your filter narrows to a single match, it's selected automatically.
|
|
330
350
|
|
|
331
351
|
### Dry run
|
|
332
352
|
|
|
@@ -436,14 +456,19 @@ You can override it with the `SUMMON_CONFIG_DIR` environment variable (also hand
|
|
|
436
456
|
"docs": "https://docs.example.com",
|
|
437
457
|
"repo": "https://github.com/Aditya060806/summon"
|
|
438
458
|
},
|
|
439
|
-
"
|
|
459
|
+
"defaultSearchEngine": "ddg",
|
|
460
|
+
"searchEngines": {
|
|
461
|
+
"work": "https://intranet.example.com/search?q=%s",
|
|
462
|
+
"scholar": "https://scholar.google.com/scholar?q=%s"
|
|
463
|
+
}
|
|
440
464
|
}
|
|
441
465
|
```
|
|
442
466
|
|
|
443
467
|
- `bookmarks` — map of alias → target. Managed via `--save` / `--remove-bookmark`, or edited by hand.
|
|
444
|
-
- `
|
|
445
|
-
|
|
446
|
-
|
|
468
|
+
- `searchEngines` — map of engine name → URL template, where `%s` is replaced by the URL-encoded query. These are **merged** with the built-ins (`google`, `ddg`, `bing`, `brave`, `npm`, `gh`, `mdn`, `so`, `yt`, `wiki`), so you can add new ones or override existing ones.
|
|
469
|
+
- `defaultSearchEngine` — the engine `--search` uses when `--engine` isn't given (default: `google`).
|
|
470
|
+
|
|
471
|
+
> A legacy single `"searchEngine": "…"` string is still honored and appears as the `custom` engine.
|
|
447
472
|
|
|
448
473
|
**`history.json`** stores your most recently opened targets (capped, de-duplicated) and powers `--recent`.
|
|
449
474
|
|
|
@@ -528,6 +553,8 @@ How `summon` stacks up against common alternatives:
|
|
|
528
553
|
| Auto `https://` for bare domains | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
529
554
|
| Bookmarks / aliases | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
530
555
|
| Web search shortcut | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
556
|
+
| Configurable search engines | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
557
|
+
| Fuzzy interactive picker | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
531
558
|
| Open from clipboard | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
532
559
|
| Reveal in file manager | ✅ | ❌ | ⚠️ `-R` | ⚠️ | ❌ |
|
|
533
560
|
| Recent / interactive picker | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
@@ -641,6 +668,20 @@ git config --global core.editor "summon --wait"
|
|
|
641
668
|
**A non-existent `report.pdf` didn't open a website — good?**
|
|
642
669
|
Yes. Names ending in a known file extension are treated as files, so typos give a clear "not found" (exit code 2) instead of launching a browser.
|
|
643
670
|
|
|
671
|
+
## Releasing
|
|
672
|
+
|
|
673
|
+
Maintainers: releases publish to npm automatically when a version tag is pushed (see [`.github/workflows/release.yml`](.github/workflows/release.yml)).
|
|
674
|
+
|
|
675
|
+
1. One-time: add an npm automation token as the `NPM_TOKEN` repository secret.
|
|
676
|
+
2. Cut a release:
|
|
677
|
+
|
|
678
|
+
```sh
|
|
679
|
+
npm version patch # or minor / major — bumps package.json and tags
|
|
680
|
+
git push --follow-tags
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
The workflow runs the tests, then publishes with npm provenance. See the [changelog](changelog.md) for release history.
|
|
684
|
+
|
|
644
685
|
## Related
|
|
645
686
|
|
|
646
687
|
- [open](https://github.com/sindresorhus/open) — the programmatic API that powers this CLI.
|