termsearch 0.3.9 → 0.3.11

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.
@@ -286,6 +286,27 @@ const ENGINE_PRESETS = [
286
286
  { id: 'github', label: 'GitHub Focus', engines: ['github-api', 'github', 'duckduckgo', 'wikipedia'] },
287
287
  ];
288
288
 
289
+ // ─── Engine availability (requires config) ────────────────────────────────
290
+ const SEARXNG_ROUTED = new Set([
291
+ 'bing', 'google', 'yahoo', 'startpage', 'qwant',
292
+ 'youtube', 'reddit', 'hackernews', 'sepiasearch',
293
+ 'wikidata', 'crossref', 'openalex', 'openlibrary',
294
+ 'mastodon users', 'mastodon hashtags', 'tootfinder',
295
+ 'lemmy communities', 'lemmy posts',
296
+ 'piratebay', '1337x', 'nyaa',
297
+ ]);
298
+
299
+ function isEngineAvailable(engine) {
300
+ const cfg = state.config || {};
301
+ if (engine === 'brave') return Boolean(cfg.brave?.enabled && cfg.brave?.api_key);
302
+ if (engine === 'mojeek') return Boolean(cfg.mojeek?.enabled && cfg.mojeek?.api_key);
303
+ if (engine === 'yandex') return cfg.yandex?.enabled !== false;
304
+ if (engine === 'ahmia') return cfg.ahmia?.enabled !== false;
305
+ if (engine === 'marginalia') return cfg.marginalia?.enabled !== false;
306
+ if (SEARXNG_ROUTED.has(engine)) return Boolean(cfg.searxng?.enabled && cfg.searxng?.url);
307
+ return true; // duckduckgo, wikipedia, github, github-api — sempre disponibili
308
+ }
309
+
289
310
  function detectPresetFromBase(base) {
290
311
  const raw = String(base || '').toLowerCase();
291
312
  if (!raw) return 'custom';
@@ -316,8 +337,11 @@ function LangPicker() {
316
337
 
317
338
  function EnginePicker(opts = {}) {
318
339
  const compact = Boolean(opts.compact);
340
+ // [] = ALL (no filter sent to backend)
341
+ const isAll = state.selectedEngines.length === 0;
342
+ const selectedCount = isAll ? 0 : state.selectedEngines.length;
343
+
319
344
  const details = el('details', { className: `engine-picker${compact ? ' engine-picker-compact' : ''}` });
320
- const selectedCount = state.selectedEngines.length;
321
345
  const summary = compact
322
346
  ? el('summary', { className: 'engine-picker-summary engine-picker-summary-icon', title: 'Search engines' },
323
347
  iconEl('filter', 'engine-filter-icon'),
@@ -329,45 +353,64 @@ function EnginePicker(opts = {}) {
329
353
  );
330
354
 
331
355
  const body = el('div', { className: 'engine-picker-body' });
356
+
357
+ // Preset buttons
332
358
  const presetRow = el('div', { className: 'engine-preset-row' });
333
359
  ENGINE_PRESETS.forEach((preset) => {
360
+ const isActive = preset.id === 'all' ? isAll
361
+ : preset.engines.length > 0 && preset.engines.every(e => state.selectedEngines.includes(e)) && state.selectedEngines.length === preset.engines.length;
334
362
  presetRow.append(el('button', {
335
- className: `btn ${preset.id === 'balanced' ? 'btn-primary' : ''}`,
363
+ className: `btn ${isActive || preset.id === 'balanced' ? 'btn-primary' : ''}`,
336
364
  type: 'button',
337
365
  onClick: () => {
338
- setSelectedEngines(preset.engines);
339
- details.open = false;
340
- if (state.query) doSearch(state.query, state.category);
341
- else renderApp();
366
+ // 'all' preset → clear filter (backend uses all configured providers)
367
+ setSelectedEngines(preset.id === 'all' ? [] : preset.engines);
368
+ // visually check/uncheck all available chips
369
+ [...details.querySelectorAll('.engine-chip input:not(:disabled)')].forEach((input) => {
370
+ input.checked = preset.id === 'all' || preset.engines.includes(input.closest('.engine-chip')?.querySelector('span')?.textContent?.trim().toLowerCase() || '');
371
+ });
342
372
  },
343
373
  }, preset.label));
344
374
  });
345
375
  body.append(presetRow);
346
376
 
377
+ // Engine chips per group
347
378
  ENGINE_GROUPS.forEach((group) => {
348
379
  const card = el('div', { className: 'engine-group' });
349
380
  card.append(el('div', { className: 'engine-group-title' }, group.label));
350
381
  const list = el('div', { className: 'engine-chip-wrap' });
351
382
  group.items.forEach((engine) => {
352
- const checked = state.selectedEngines.includes(engine);
383
+ const available = isEngineAvailable(engine);
384
+ // checked = explicitly selected, OR in "all" mode ([] = all available)
385
+ const checked = available && (isAll || state.selectedEngines.includes(engine));
353
386
  const id = `engine-${engine.replace(/[^a-z0-9]+/g, '-')}-${Math.random().toString(36).slice(2, 6)}`;
354
- const input = el('input', { id, type: 'checkbox', ...(checked ? { checked: '' } : {}) });
355
- const label = el('label', { className: 'engine-chip', for: id }, input, el('span', {}, engine));
387
+ const inputAttrs = { id, type: 'checkbox', ...(checked ? { checked: '' } : {}), ...(available ? {} : { disabled: '' }) };
388
+ const input = el('input', inputAttrs);
389
+ const chipClass = `engine-chip${available ? '' : ' engine-chip-unavailable'}`;
390
+ const title = available ? engine : `${engine} — not configured (Settings)`;
391
+ const label = el('label', { className: chipClass, for: id, title },
392
+ input,
393
+ el('span', {}, engine),
394
+ );
356
395
  list.append(label);
357
396
  });
358
397
  card.append(list);
359
398
  body.append(card);
360
399
  });
361
400
 
401
+ // Apply / Reset
362
402
  body.append(el('div', { className: 'engine-actions' },
363
403
  el('button', {
364
404
  className: 'btn btn-primary',
365
405
  type: 'button',
366
406
  onClick: () => {
367
- const selected = [...details.querySelectorAll('.engine-chip input:checked')]
368
- .map((node) => node.parentElement?.textContent?.trim().toLowerCase())
407
+ const checked = [...details.querySelectorAll('.engine-chip input:not(:disabled):checked')]
408
+ .map((node) => node.closest('.engine-chip')?.querySelector('span')?.textContent?.trim().toLowerCase())
369
409
  .filter(Boolean);
370
- setSelectedEngines(selected);
410
+ const availableAll = ENGINE_GROUPS.flatMap(g => g.items).filter(isEngineAvailable);
411
+ // If all available engines are checked, send [] (no filter)
412
+ const allChecked = availableAll.every(e => checked.includes(e));
413
+ setSelectedEngines(allChecked ? [] : checked);
371
414
  details.open = false;
372
415
  if (state.query) doSearch(state.query, state.category);
373
416
  else renderApp();
@@ -396,7 +439,6 @@ function EnginePicker(opts = {}) {
396
439
  document.removeEventListener('click', onClickOutside, true);
397
440
  }
398
441
  };
399
- // Defer to avoid catching the same click that opened it
400
442
  requestAnimationFrame(() => document.addEventListener('click', onClickOutside, true));
401
443
  });
402
444
 
@@ -1165,11 +1207,34 @@ function addToBrowser() {
1165
1207
  el('div', { className: 'add-browser-label' }, 'Search URL (paste in browser settings):'),
1166
1208
  urlBox,
1167
1209
  );
1168
- document.querySelector('.home-actions')?.after(hint);
1210
+ document.querySelector('.footer')?.after(hint);
1169
1211
  setTimeout(() => hint.remove(), 20000);
1170
1212
  }
1171
1213
 
1172
1214
  function renderHome(app) {
1215
+ // Top-right controls
1216
+ const topBar = el('div', { className: 'home-topbar' },
1217
+ LangPicker(),
1218
+ el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
1219
+ el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
1220
+ );
1221
+
1222
+ // History below search (if present)
1223
+ const historyEl = (state.historyEnabled && state.searchHistory.length > 0)
1224
+ ? el('div', { className: 'home-history' },
1225
+ ...state.searchHistory.slice(0, 8).map((q) =>
1226
+ el('button', {
1227
+ className: 'home-history-item',
1228
+ type: 'button',
1229
+ onClick: () => { state.query = q; state.category = 'web'; doSearch(q, 'web'); },
1230
+ },
1231
+ el('span', { className: 'home-history-icon', html: ICONS.search }),
1232
+ el('span', {}, q),
1233
+ ),
1234
+ ),
1235
+ )
1236
+ : null;
1237
+
1173
1238
  const home = el('div', { className: 'home' },
1174
1239
  el('div', { className: 'home-logo' }, 'Term', el('strong', {}, 'Search')),
1175
1240
  el('div', { className: 'home-tagline' },
@@ -1177,23 +1242,20 @@ function renderHome(app) {
1177
1242
  el('span', { className: 'tagline-mobile' }, 'Private local search'),
1178
1243
  ),
1179
1244
  el('div', { className: 'home-search' }, SearchForm('', (q) => { state.query = q; state.category = 'web'; doSearch(q, 'web'); })),
1180
- el('div', { className: 'home-actions' },
1181
- LangPicker(),
1182
- el('button', { className: 'btn', onClick: () => navigate('#/settings') }, iconEl('settings'), ' Settings'),
1183
- el('button', { className: 'btn', onClick: toggleTheme }, iconEl('theme'), ' Theme'),
1184
- el('button', { className: 'btn', onClick: addToBrowser, title: 'Add as browser search engine' }, iconEl('search'), ' Add to browser'),
1185
- el('a', { className: 'btn', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' }, iconEl('github'), ' GitHub'),
1186
- ),
1245
+ historyEl,
1187
1246
  );
1188
1247
 
1189
1248
  const footer = el('div', { className: 'footer' },
1190
1249
  el('span', { className: 'footer-link' }, '© 2026 DioNanos'),
1191
- el('a', { className: 'footer-link', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener' },
1192
- iconEl('github'), 'GitHub',
1250
+ el('a', { className: 'footer-link', href: 'https://github.com/DioNanos/termsearch', target: '_blank', rel: 'noopener noreferrer' },
1251
+ iconEl('github'), ' GitHub',
1252
+ ),
1253
+ el('button', { className: 'footer-link footer-add-btn', onClick: addToBrowser, title: 'Add as default search engine' },
1254
+ iconEl('search'), ' Add to browser',
1193
1255
  ),
1194
1256
  );
1195
1257
 
1196
- app.append(home, footer);
1258
+ app.append(topBar, home, footer);
1197
1259
  }
1198
1260
 
1199
1261
  // ─── Settings ─────────────────────────────────────────────────────────────
@@ -97,7 +97,7 @@ a:hover { color: var(--link-h); }
97
97
  .pulse { animation: pulse 1.5s ease-in-out infinite; }
98
98
 
99
99
  /* ─── Layout ──────────────────────────────────────────────────────────────── */
100
- #app { min-height: 100vh; display: flex; flex-direction: column; }
100
+ #app { min-height: 100vh; display: flex; flex-direction: column; position: relative; }
101
101
 
102
102
  /* ─── Header ──────────────────────────────────────────────────────────────── */
103
103
  .header {
@@ -281,6 +281,17 @@ a:hover { color: var(--link-h); }
281
281
  .engine-chip input {
282
282
  margin: 0;
283
283
  }
284
+ .engine-chip-unavailable {
285
+ opacity: 0.35;
286
+ cursor: not-allowed;
287
+ border-style: dashed;
288
+ pointer-events: none;
289
+ }
290
+ .engine-chip-unavailable span {
291
+ font-weight: 700;
292
+ text-decoration: line-through;
293
+ text-decoration-color: rgba(255,255,255,0.25);
294
+ }
284
295
  .engine-actions {
285
296
  display: flex;
286
297
  gap: 8px;
@@ -289,13 +300,23 @@ a:hover { color: var(--link-h); }
289
300
  }
290
301
 
291
302
  /* ─── Homepage ────────────────────────────────────────────────────────────── */
303
+ .home-topbar {
304
+ position: absolute;
305
+ top: 12px;
306
+ right: 16px;
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 4px;
310
+ z-index: 10;
311
+ }
312
+
292
313
  .home {
293
314
  flex: 1;
294
315
  display: flex;
295
316
  flex-direction: column;
296
317
  align-items: center;
297
318
  justify-content: center;
298
- padding: 48px 20px 80px;
319
+ padding: 48px 20px 40px;
299
320
  gap: 0;
300
321
  }
301
322
 
@@ -326,14 +347,44 @@ a:hover { color: var(--link-h); }
326
347
  .tagline-mobile { display: none; }
327
348
  .tagline-desktop { display: inline; }
328
349
 
329
- .home-search { width: 100%; max-width: 560px; margin-bottom: 14px; }
350
+ .home-search { width: 100%; max-width: 560px; margin-bottom: 0; }
330
351
 
331
- .home-actions {
352
+ /* History below search */
353
+ .home-history {
354
+ width: 100%;
355
+ max-width: 560px;
356
+ margin-top: 6px;
357
+ display: flex;
358
+ flex-direction: column;
359
+ gap: 1px;
360
+ border: 1px solid var(--border);
361
+ border-radius: var(--radius);
362
+ overflow: hidden;
363
+ background: var(--bg2);
364
+ }
365
+ .home-history-item {
332
366
  display: flex;
333
367
  align-items: center;
334
- gap: 8px;
335
- flex-wrap: wrap;
336
- justify-content: center;
368
+ gap: 10px;
369
+ padding: 8px 14px;
370
+ font-size: 13px;
371
+ color: var(--text2);
372
+ text-align: left;
373
+ border-bottom: 1px solid var(--border2);
374
+ cursor: pointer;
375
+ transition: background 0.1s;
376
+ background: transparent;
377
+ border-left: none;
378
+ border-right: none;
379
+ border-top: none;
380
+ }
381
+ .home-history-item:last-child { border-bottom: none; }
382
+ .home-history-item:hover { background: var(--bg3); color: var(--text); }
383
+ .home-history-icon {
384
+ opacity: 0.35;
385
+ flex-shrink: 0;
386
+ display: inline-flex;
387
+ align-items: center;
337
388
  }
338
389
 
339
390
  /* ─── Search Bar ──────────────────────────────────────────────────────────── */
@@ -1017,12 +1068,18 @@ a:hover { color: var(--link-h); }
1017
1068
  /* ─── Footer ──────────────────────────────────────────────────────────────── */
1018
1069
  .footer {
1019
1070
  border-top: 1px solid var(--border2);
1020
- padding: 12px 16px;
1071
+ padding: 10px 16px;
1021
1072
  display: flex;
1022
1073
  align-items: center;
1023
1074
  justify-content: center;
1024
1075
  gap: 16px;
1025
1076
  }
1077
+ .footer-add-btn {
1078
+ background: none;
1079
+ border: none;
1080
+ cursor: pointer;
1081
+ padding: 0;
1082
+ }
1026
1083
  .footer-link {
1027
1084
  color: var(--text3);
1028
1085
  font-size: 10px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termsearch",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Personal search engine for Termux/Linux/macOS — zero-config, privacy-first, AI-optional",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,7 +50,7 @@ function termuxStatus() {
50
50
  function termuxEnable() {
51
51
  fs.mkdirSync(TERMUX_BOOT_DIR, { recursive: true });
52
52
  const bin = findBin();
53
- const sh = `#!/data/data/com.termux/files/usr/bin/sh\n# TermSearch autostart\n${bin} &\n`;
53
+ const sh = `#!/data/data/com.termux/files/usr/bin/sh\n# TermSearch autostart\n${bin} start --fg &\n`;
54
54
  fs.writeFileSync(TERMUX_BOOT_FILE, sh, { mode: 0o755 });
55
55
  }
56
56
 
@@ -103,7 +103,7 @@ function linuxEnable() {
103
103
  '',
104
104
  '[Service]',
105
105
  'Type=simple',
106
- `ExecStart=${bin}`,
106
+ `ExecStart=${bin} start --fg`,
107
107
  'Restart=on-failure',
108
108
  'RestartSec=5',
109
109
  '',
@@ -156,6 +156,8 @@ function macosEnable() {
156
156
  <key>ProgramArguments</key>
157
157
  <array>
158
158
  <string>${bin}</string>
159
+ <string>start</string>
160
+ <string>--fg</string>
159
161
  </array>
160
162
  <key>RunAtLoad</key>
161
163
  <true/>