summon-open 1.0.0 → 1.1.1
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 +93 -19
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.1",
|
|
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
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
Bookmarks, search, clipboard, reveal-in-file-manager, dry-run, multiple targets, and stdin — all in a tool small enough to forget it's there. No more remembering `xdg-open` vs `start` vs `open`.
|
|
15
15
|
|
|
16
16
|
[](https://www.npmjs.com/package/summon-open)
|
|
17
|
+
[](https://www.npmjs.com/package/summon-open)
|
|
18
|
+
[](https://packagephobia.com/result?p=summon-open)
|
|
17
19
|
[](https://nodejs.org)
|
|
18
20
|
[](license)
|
|
19
21
|
[](https://github.com/Aditya060806/summon/actions)
|
|
@@ -21,9 +23,10 @@ Bookmarks, search, clipboard, reveal-in-file-manager, dry-run, multiple targets,
|
|
|
21
23
|
</div>
|
|
22
24
|
|
|
23
25
|
<!--
|
|
24
|
-
DEMO:
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
DEMO: a ready-to-run VHS script lives at media/demo.tape.
|
|
27
|
+
1. Install VHS: https://github.com/charmbracelet/vhs
|
|
28
|
+
2. From the repo root, run: vhs media/demo.tape
|
|
29
|
+
3. It writes media/demo.gif — then uncomment the line below.
|
|
27
30
|
<div align="center"><img src="media/demo.gif" alt="summon demo" width="700"></div>
|
|
28
31
|
-->
|
|
29
32
|
|
|
@@ -54,7 +57,7 @@ $ cat diagram.png | summon # pipe raw bytes straight in
|
|
|
54
57
|
- [Web search](#web-search)
|
|
55
58
|
- [Clipboard](#clipboard)
|
|
56
59
|
- [Reveal in file manager](#reveal-in-file-manager)
|
|
57
|
-
- [Recent &
|
|
60
|
+
- [Recent & fuzzy picker](#recent--fuzzy-picker)
|
|
58
61
|
- [Dry run](#dry-run)
|
|
59
62
|
- [Choosing the app](#choosing-the-app)
|
|
60
63
|
- [Stdin](#stdin)
|
|
@@ -65,6 +68,7 @@ $ cat diagram.png | summon # pipe raw bytes straight in
|
|
|
65
68
|
- [Comparison](#comparison)
|
|
66
69
|
- [Feature matrix](#feature-matrix)
|
|
67
70
|
- [Same task, side by side](#same-task-side-by-side)
|
|
71
|
+
- [vs other npm openers](#vs-other-npm-openers)
|
|
68
72
|
- [Efficiency](#efficiency)
|
|
69
73
|
- [Exit codes](#exit-codes)
|
|
70
74
|
- [Platform support](#platform-support)
|
|
@@ -107,10 +111,10 @@ summon "$url"
|
|
|
107
111
|
- **Multiple targets** — `summon a.pdf b.png https://x.com` opens them all.
|
|
108
112
|
- **Smart URL normalization** — `summon github.com` just works; the `https://` is added for you.
|
|
109
113
|
- **Bookmarks** — save aliases (`summon @docs`) and turn `summon` into a daily driver.
|
|
110
|
-
- **Web search** — `summon -s "query"`
|
|
114
|
+
- **Web search** — `summon -s "query"` searches with a configurable engine (`-e mdn`, `-e npm`, …).
|
|
111
115
|
- **Clipboard mode** — `summon -c` opens whatever URL/path you just copied.
|
|
112
116
|
- **Reveal** — `summon -r file` highlights it in Finder/Explorer/your file manager.
|
|
113
|
-
- **Recent +
|
|
117
|
+
- **Recent + fuzzy picker** — re-open recent items or fuzzy-search a menu of bookmarks/history.
|
|
114
118
|
- **Dry run** — `--dry-run` prints exactly what would happen without doing it.
|
|
115
119
|
- **Pick the app** — force a specific app and pass it arguments.
|
|
116
120
|
- **Pipe-friendly** — stream data over stdin; the file type is auto-detected.
|
|
@@ -130,12 +134,19 @@ This installs the `summon` command. Or run it once without installing:
|
|
|
130
134
|
npx summon-open https://github.com
|
|
131
135
|
```
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
<summary>Other install channels</summary>
|
|
137
|
+
**Homebrew** (macOS/Linux):
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
```sh
|
|
140
|
+
brew install --formula https://raw.githubusercontent.com/Aditya060806/summon/main/dist/homebrew/summon-open.rb
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Scoop** (Windows):
|
|
137
144
|
|
|
138
|
-
|
|
145
|
+
```powershell
|
|
146
|
+
scoop install https://raw.githubusercontent.com/Aditya060806/summon/main/dist/scoop/summon-open.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Both channels install the same `summon` command and depend on Node.js. The manifests live in [`dist/`](dist).
|
|
139
150
|
|
|
140
151
|
## Quick start
|
|
141
152
|
|
|
@@ -191,6 +202,8 @@ The whole tool at a glance — task on the left, command on the right.
|
|
|
191
202
|
| List bookmarks | `summon --bookmarks` |
|
|
192
203
|
| Remove a bookmark | `summon --remove-bookmark docs` |
|
|
193
204
|
| Search the web | `summon -s "query here"` |
|
|
205
|
+
| Search with a specific engine | `summon -s "flatMap" -e mdn` |
|
|
206
|
+
| List search engines | `summon --engines` |
|
|
194
207
|
| Open clipboard URL | `summon -c` |
|
|
195
208
|
| Reveal in file manager | `summon report.pdf -r` |
|
|
196
209
|
| Re-open something recent | `summon --recent` |
|
|
@@ -214,6 +227,8 @@ $ summon --help
|
|
|
214
227
|
--extension File extension for when stdin file type cannot be detected
|
|
215
228
|
--dry-run, -n Print what would be opened without opening it
|
|
216
229
|
--search, -s Treat the input as a search query
|
|
230
|
+
--engine, -e Search engine to use with --search (see --engines)
|
|
231
|
+
--engines List available search engines
|
|
217
232
|
--clipboard, -c Open the URL/path currently on the clipboard
|
|
218
233
|
--reveal, -r Reveal the file/folder in your file manager
|
|
219
234
|
--recent Pick from recently opened items
|
|
@@ -223,12 +238,13 @@ $ summon --help
|
|
|
223
238
|
|
|
224
239
|
Examples
|
|
225
240
|
$ summon https://sindresorhus.com
|
|
226
|
-
$ summon github.com
|
|
241
|
+
$ summon github.com # scheme added automatically
|
|
227
242
|
$ summon report.pdf photo.png notes.txt
|
|
228
243
|
$ summon https://github.com -- 'google chrome' --incognito
|
|
229
|
-
$ summon @docs
|
|
244
|
+
$ summon @docs # open a saved bookmark
|
|
230
245
|
$ summon https://docs.example.com --save docs
|
|
231
246
|
$ summon -s "rust async traits"
|
|
247
|
+
$ summon -s "flatMap" -e mdn
|
|
232
248
|
$ echo '<h1>Hi</h1>' | summon --extension=html
|
|
233
249
|
$ summon report.pdf --reveal
|
|
234
250
|
$ summon --recent
|
|
@@ -243,6 +259,8 @@ $ summon --help
|
|
|
243
259
|
| `--extension` | | string | auto | Extension to use for piped stdin when the type can't be auto-detected. |
|
|
244
260
|
| `--dry-run` | `-n` | boolean | `false` | Print what would be opened without launching anything. |
|
|
245
261
|
| `--search` | `-s` | boolean | `false` | Treat the input as a web search query. |
|
|
262
|
+
| `--engine <name>` | `-e` | string | config | Search engine to use with `--search`. See `--engines`. |
|
|
263
|
+
| `--engines` | | boolean | `false` | List available search engines (`*` marks the default). |
|
|
246
264
|
| `--clipboard` | `-c` | boolean | `false` | Open the URL/path currently on the clipboard. |
|
|
247
265
|
| `--reveal` | `-r` | boolean | `false` | Reveal/highlight the file or folder in the file manager. |
|
|
248
266
|
| `--recent` | | boolean | `false` | Interactively pick from recently opened items. |
|
|
@@ -292,7 +310,15 @@ Bookmarks live in your [config file](#configuration) and can also be edited by h
|
|
|
292
310
|
summon -s "how to exit vim"
|
|
293
311
|
```
|
|
294
312
|
|
|
295
|
-
|
|
313
|
+
Pick an engine per search with `--engine`/`-e`, and list what's available with `--engines`:
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
summon -s "flatMap" -e mdn
|
|
317
|
+
summon -s "left-pad" -e npm
|
|
318
|
+
summon --engines
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
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
322
|
|
|
297
323
|
### Clipboard
|
|
298
324
|
|
|
@@ -318,7 +344,7 @@ summon report.pdf --reveal
|
|
|
318
344
|
- **Windows** — `explorer /select,` (selects the file in Explorer)
|
|
319
345
|
- **Linux/other** — opens the containing folder
|
|
320
346
|
|
|
321
|
-
### Recent &
|
|
347
|
+
### Recent & fuzzy picker
|
|
322
348
|
|
|
323
349
|
`summon` remembers what you open. Re-open something recent:
|
|
324
350
|
|
|
@@ -326,7 +352,7 @@ summon report.pdf --reveal
|
|
|
326
352
|
summon --recent
|
|
327
353
|
```
|
|
328
354
|
|
|
329
|
-
Run `summon` with no arguments in a terminal and it shows an interactive
|
|
355
|
+
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
356
|
|
|
331
357
|
### Dry run
|
|
332
358
|
|
|
@@ -436,14 +462,19 @@ You can override it with the `SUMMON_CONFIG_DIR` environment variable (also hand
|
|
|
436
462
|
"docs": "https://docs.example.com",
|
|
437
463
|
"repo": "https://github.com/Aditya060806/summon"
|
|
438
464
|
},
|
|
439
|
-
"
|
|
465
|
+
"defaultSearchEngine": "ddg",
|
|
466
|
+
"searchEngines": {
|
|
467
|
+
"work": "https://intranet.example.com/search?q=%s",
|
|
468
|
+
"scholar": "https://scholar.google.com/scholar?q=%s"
|
|
469
|
+
}
|
|
440
470
|
}
|
|
441
471
|
```
|
|
442
472
|
|
|
443
473
|
- `bookmarks` — map of alias → target. Managed via `--save` / `--remove-bookmark`, or edited by hand.
|
|
444
|
-
- `
|
|
445
|
-
|
|
446
|
-
|
|
474
|
+
- `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.
|
|
475
|
+
- `defaultSearchEngine` — the engine `--search` uses when `--engine` isn't given (default: `google`).
|
|
476
|
+
|
|
477
|
+
> A legacy single `"searchEngine": "…"` string is still honored and appears as the `custom` engine.
|
|
447
478
|
|
|
448
479
|
**`history.json`** stores your most recently opened targets (capped, de-duplicated) and powers `--recent`.
|
|
449
480
|
|
|
@@ -528,6 +559,8 @@ How `summon` stacks up against common alternatives:
|
|
|
528
559
|
| Auto `https://` for bare domains | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
529
560
|
| Bookmarks / aliases | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
530
561
|
| Web search shortcut | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
562
|
+
| Configurable search engines | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
563
|
+
| Fuzzy interactive picker | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
531
564
|
| Open from clipboard | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
532
565
|
| Reveal in file manager | ✅ | ❌ | ⚠️ `-R` | ⚠️ | ❌ |
|
|
533
566
|
| Recent / interactive picker | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
@@ -556,6 +589,33 @@ The value shows up most when you compare the actual commands for the same job.
|
|
|
556
589
|
| Preview without launching | `summon x.com --dry-run` | no native equivalent |
|
|
557
590
|
| Pipe + auto-detect type | `cat f \| summon` | no native equivalent |
|
|
558
591
|
|
|
592
|
+
### vs other npm openers
|
|
593
|
+
|
|
594
|
+
The npm ecosystem has a few ways to open things. `summon` builds on the excellent [`open`](https://github.com/sindresorhus/open) library (the programmatic API) and starts from [`open-cli`](https://github.com/sindresorhus/open-cli), then adds a workflow layer on top.
|
|
595
|
+
|
|
596
|
+
| Capability | **summon** | [`open-cli`](https://www.npmjs.com/package/open-cli) | [`opener`](https://www.npmjs.com/package/opener) | [`open`](https://www.npmjs.com/package/open) |
|
|
597
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
598
|
+
| Ships a CLI command | ✅ | ✅ | ✅ | ❌ (library) |
|
|
599
|
+
| Cross-platform | ✅ | ✅ | ✅ | ✅ |
|
|
600
|
+
| Open URL / file / folder / app | ✅ | ✅ | ✅ | ✅ (API) |
|
|
601
|
+
| Choose app + pass args (`-- app …`) | ✅ | ✅ | ❌ | ✅ (API) |
|
|
602
|
+
| Wait / background | ✅ | ✅ | ❌ | ✅ (API) |
|
|
603
|
+
| Stdin + file-type detection | ✅ | ✅ | ❌ | ❌ |
|
|
604
|
+
| Multiple targets in one call | ✅ | ❌ | ❌ | ❌ |
|
|
605
|
+
| Auto `https://` for bare domains | ✅ | ❌ | ❌ | ❌ |
|
|
606
|
+
| Bookmarks / aliases | ✅ | ❌ | ❌ | ❌ |
|
|
607
|
+
| Web search + configurable engines | ✅ | ❌ | ❌ | ❌ |
|
|
608
|
+
| Open from clipboard | ✅ | ❌ | ❌ | ❌ |
|
|
609
|
+
| Reveal in file manager | ✅ | ❌ | ❌ | ❌ |
|
|
610
|
+
| Recent + fuzzy picker | ✅ | ❌ | ❌ | ❌ |
|
|
611
|
+
| Dry-run preview | ✅ | ❌ | ❌ | ❌ |
|
|
612
|
+
| Typed exit codes + friendly errors | ✅ | ⚠️ | ⚠️ | n/a |
|
|
613
|
+
| Shell completions | ✅ | ❌ | ❌ | n/a |
|
|
614
|
+
|
|
615
|
+
✅ yes · ⚠️ basic/partial · ❌ no · n/a not applicable
|
|
616
|
+
|
|
617
|
+
**In short:** reach for [`open`](https://www.npmjs.com/package/open) when you need a programmatic API inside Node code; reach for `open-cli` or `opener` for a bare "open this one thing" command; reach for **summon** when you want that same reliability *plus* multiple targets, bookmarks, search, clipboard, reveal, a fuzzy picker, and dry-run in day-to-day terminal use.
|
|
618
|
+
|
|
559
619
|
## Efficiency
|
|
560
620
|
|
|
561
621
|
`summon` is designed to add as little overhead as possible on top of the native OS handler.
|
|
@@ -641,6 +701,20 @@ git config --global core.editor "summon --wait"
|
|
|
641
701
|
**A non-existent `report.pdf` didn't open a website — good?**
|
|
642
702
|
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
703
|
|
|
704
|
+
## Releasing
|
|
705
|
+
|
|
706
|
+
Maintainers: releases publish to npm automatically when a version tag is pushed (see [`.github/workflows/release.yml`](.github/workflows/release.yml)).
|
|
707
|
+
|
|
708
|
+
1. One-time: add an npm automation token as the `NPM_TOKEN` repository secret.
|
|
709
|
+
2. Cut a release:
|
|
710
|
+
|
|
711
|
+
```sh
|
|
712
|
+
npm version patch # or minor / major — bumps package.json and tags
|
|
713
|
+
git push --follow-tags
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The workflow runs the tests, then publishes with npm provenance. See the [changelog](changelog.md) for release history.
|
|
717
|
+
|
|
644
718
|
## Related
|
|
645
719
|
|
|
646
720
|
- [open](https://github.com/sindresorhus/open) — the programmatic API that powers this CLI.
|