github-search-cli 9.9.0 → 9.10.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/dist/cli.js CHANGED
@@ -1569,6 +1569,7 @@ async function launchPackageBrowser() {
1569
1569
  renderer.keyInput.on("keypress", (key) => {
1570
1570
  const name = key.name || "";
1571
1571
  if (name === "q" || name === "escape") {
1572
+ renderer.destroy();
1572
1573
  process.exit(0);
1573
1574
  return;
1574
1575
  }
@@ -2153,7 +2154,7 @@ var cached = null;
2153
2154
  function getVersion() {
2154
2155
  if (cached) return cached;
2155
2156
  if (true) {
2156
- cached = "9.9.0";
2157
+ cached = "9.10.1";
2157
2158
  return cached;
2158
2159
  }
2159
2160
  try {
@@ -2322,6 +2323,7 @@ function reportError(err, context = {
2322
2323
 
2323
2324
  // src/output.ts
2324
2325
  import { writeFileSync as writeFileSync2 } from "fs";
2326
+ var PIPE_TARGETS = ["open", "clone"];
2325
2327
  var FORMATS = /* @__PURE__ */ new Map([
2326
2328
  // Export formats
2327
2329
  ["json", formatJson],
@@ -2335,6 +2337,7 @@ var FORMATS = /* @__PURE__ */ new Map([
2335
2337
  ["clone-commands", formatCloneCommands],
2336
2338
  ["ids", formatIds]
2337
2339
  ]);
2340
+ var FORMAT_VALUES = [...FORMATS.keys()];
2338
2341
  function formatJson(repos) {
2339
2342
  return JSON.stringify(repos, null, 2);
2340
2343
  }
@@ -2407,6 +2410,13 @@ async function pipeExec(repos, target) {
2407
2410
  }
2408
2411
 
2409
2412
  // src/watch.ts
2413
+ function resolveTrendingSince(opts) {
2414
+ if (opts.since === "weekly" || opts.since === "monthly") return opts.since;
2415
+ if (opts.since === "daily") return "daily";
2416
+ if (opts.query === "weekly") return "weekly";
2417
+ if (opts.query === "monthly") return "monthly";
2418
+ return "daily";
2419
+ }
2410
2420
  var MAX_FAILS = 5;
2411
2421
  async function runWatch(opts, onChange, onError) {
2412
2422
  let previous = [];
@@ -2419,7 +2429,7 @@ async function runWatch(opts, onChange, onError) {
2419
2429
  try {
2420
2430
  let repos;
2421
2431
  if (opts.trending) {
2422
- const since = opts.query === "weekly" ? "weekly" : opts.query === "monthly" ? "monthly" : "daily";
2432
+ const since = resolveTrendingSince(opts);
2423
2433
  const trendingSearch = createTrendingSearch();
2424
2434
  const response = await trendingSearch.search(trendingQuery(), {
2425
2435
  limit: opts.limit,
@@ -3874,6 +3884,135 @@ if (import.meta.main && false) {
3874
3884
  launchTrending();
3875
3885
  }
3876
3886
 
3887
+ // src/tui/mouse.ts
3888
+ var DOUBLE_CLICK_MS = 400;
3889
+ function hitIndexForY(geometry, screenY) {
3890
+ const pitch = geometry.linesPerItem;
3891
+ if (pitch <= 0) return -1;
3892
+ if (geometry.optionCount <= 0) return -1;
3893
+ const localY = screenY - geometry.screenY;
3894
+ if (localY < 0 || localY >= geometry.height) return -1;
3895
+ const row = Math.floor(localY / pitch);
3896
+ const index = geometry.scrollOffset + row;
3897
+ const last = geometry.optionCount - 1;
3898
+ if (index < 0) return 0;
3899
+ return index > last ? last : index;
3900
+ }
3901
+ function selectRowGeometry(select) {
3902
+ const internal = select;
3903
+ return {
3904
+ screenY: select.screenY,
3905
+ height: select.height,
3906
+ optionCount: select.options.length,
3907
+ scrollOffset: internal.scrollOffset ?? 0,
3908
+ linesPerItem: internal.linesPerItem ?? 0
3909
+ };
3910
+ }
3911
+ function attachClickToSelect(renderer, select, options = {}) {
3912
+ const wheelStep = options.wheelStep ?? 3;
3913
+ let lastClickIndex = -1;
3914
+ let lastClickAt = 0;
3915
+ let lastHovered = -1;
3916
+ select.onMouseDown = (event) => {
3917
+ event.stopPropagation();
3918
+ if (options.keepFocus) event.preventDefault();
3919
+ const index = hitIndexForY(selectRowGeometry(select), event.y);
3920
+ if (index < 0) {
3921
+ lastClickIndex = -1;
3922
+ options.onClickMiss?.();
3923
+ return;
3924
+ }
3925
+ const now = Date.now();
3926
+ const alreadySelected = select.getSelectedIndex() === index;
3927
+ const isDouble = alreadySelected && index === lastClickIndex && now - lastClickAt < DOUBLE_CLICK_MS;
3928
+ lastClickIndex = index;
3929
+ lastClickAt = now;
3930
+ if (isDouble) {
3931
+ lastClickIndex = -1;
3932
+ options.onOpen?.();
3933
+ return;
3934
+ }
3935
+ select.setSelectedIndex(index);
3936
+ };
3937
+ select.onMouseScroll = (event) => {
3938
+ event.stopPropagation();
3939
+ const direction = event.scroll?.direction;
3940
+ if (direction === "up") select.moveUp(wheelStep);
3941
+ else if (direction === "down") select.moveDown(wheelStep);
3942
+ else return;
3943
+ renderer.requestRender();
3944
+ };
3945
+ attachHoverHighlight(renderer, select, {
3946
+ onHover: (index) => {
3947
+ lastHovered = index;
3948
+ options.onHoverRow?.(index);
3949
+ },
3950
+ // A stale row must never outlive the options it pointed at.
3951
+ onLeave: () => {
3952
+ lastHovered = -1;
3953
+ }
3954
+ });
3955
+ return {
3956
+ reset() {
3957
+ lastClickIndex = -1;
3958
+ lastClickAt = 0;
3959
+ },
3960
+ get lastHovered() {
3961
+ return lastHovered;
3962
+ }
3963
+ };
3964
+ }
3965
+ function attachHoverHighlight(renderer, select, options = {}) {
3966
+ const idleBackground = select.backgroundColor;
3967
+ const hoverBackground = "#ffffff0f";
3968
+ let hovered = -1;
3969
+ select.onMouseOver = () => {
3970
+ renderer.setMousePointer("pointer");
3971
+ select.backgroundColor = hoverBackground;
3972
+ renderer.requestRender();
3973
+ };
3974
+ select.onMouseOut = () => {
3975
+ renderer.setMousePointer("default");
3976
+ select.backgroundColor = idleBackground;
3977
+ if (hovered !== -1) {
3978
+ hovered = -1;
3979
+ options.onLeave?.();
3980
+ }
3981
+ renderer.requestRender();
3982
+ };
3983
+ select.onMouseMove = (event) => {
3984
+ const index = hitIndexForY(selectRowGeometry(select), event.y);
3985
+ if (index === hovered) return;
3986
+ hovered = index;
3987
+ options.onHover?.(index);
3988
+ };
3989
+ }
3990
+ function attachClickOutside(target, onDismiss) {
3991
+ target.onMouseDown = (event) => {
3992
+ event.stopPropagation();
3993
+ onDismiss();
3994
+ };
3995
+ }
3996
+ function attachClickableBox(renderer, box, handlers) {
3997
+ box.onMouseDown = (event) => {
3998
+ event.stopPropagation();
3999
+ handlers.onActivate();
4000
+ };
4001
+ box.onMouseOver = () => {
4002
+ renderer.setMousePointer("pointer");
4003
+ box.borderColor = handlers.activeBorder;
4004
+ handlers.onStateChange?.();
4005
+ renderer.requestRender();
4006
+ };
4007
+ box.onMouseOut = () => {
4008
+ renderer.setMousePointer("default");
4009
+ box.borderColor = handlers.idleBorder;
4010
+ handlers.onStateChange?.();
4011
+ renderer.requestRender();
4012
+ };
4013
+ }
4014
+ var NON_INTERACTIVE_TEXT = { selectable: false };
4015
+
3877
4016
  // src/help.ts
3878
4017
  function buildHelpSections() {
3879
4018
  return [
@@ -3895,7 +4034,7 @@ function buildHelpSections() {
3895
4034
  { keys: "\u2191 \u2193 / j k", action: "Move selection" },
3896
4035
  { keys: "Enter", action: "Open repo in browser" },
3897
4036
  { keys: "/", action: "Focus search input" },
3898
- { keys: "Space", action: "Open command menu" },
4037
+ { keys: "Esc", action: "Open command menu" },
3899
4038
  { keys: "? / Ctrl+H", action: "Toggle this help screen" },
3900
4039
  { keys: "q", action: "Quit" }
3901
4040
  ]
@@ -3929,7 +4068,8 @@ function buildHelpSections() {
3929
4068
  rows: [
3930
4069
  { keys: "1-5", action: "Switch tab: Today \u2192 Week \u2192 Month \u2192 \u2026" },
3931
4070
  { keys: "\u2190 \u2192 / h l", action: "Navigate tabs" }
3932
- ]
4071
+ ],
4072
+ note: "Tabs are also clickable."
3933
4073
  },
3934
4074
  {
3935
4075
  title: " Packages Mode ",
@@ -3938,12 +4078,12 @@ function buildHelpSections() {
3938
4078
  { keys: "/", action: "Search npm packages" },
3939
4079
  { keys: "\u2191 \u2193 / j k", action: "Move selection" },
3940
4080
  { keys: "Enter", action: "View package details" },
3941
- { keys: "Space", action: "Open command menu" }
4081
+ { keys: "Esc", action: "Open command menu" }
3942
4082
  ],
3943
4083
  note: "Search by name, description, or tags. Sort by downloads or score."
3944
4084
  },
3945
4085
  {
3946
- title: " Command Menu (Space)",
4086
+ title: " Command Menu (Esc)",
3947
4087
  titleColor: "purple",
3948
4088
  rows: [
3949
4089
  { keys: "Sort", action: "Cycle sort: best-match \u2192 stars \u2192 \u2026" },
@@ -3968,7 +4108,7 @@ function buildHelpSections() {
3968
4108
  { keys: "History", action: "View search history" },
3969
4109
  { keys: "Help", action: "Show this help screen" }
3970
4110
  ],
3971
- note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc"
4111
+ note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc. Clicking a row runs it directly. Esc (or the \u2630 menu button) opens this menu; Space types a space in the query."
3972
4112
  },
3973
4113
  {
3974
4114
  title: " Bookmarks Page ",
@@ -3981,6 +4121,22 @@ function buildHelpSections() {
3981
4121
  ],
3982
4122
  note: "A full page, not an overlay \u2014 Esc returns to the mode menu."
3983
4123
  },
4124
+ {
4125
+ title: " Mouse ",
4126
+ titleColor: "green",
4127
+ rows: [
4128
+ { keys: "Click", action: "Select a row / card / tab" },
4129
+ { keys: "Double-click", action: "Open the repo or run the action" },
4130
+ { keys: "Scroll wheel", action: "Move the selection / scroll a pane" },
4131
+ { keys: "Click outside", action: "Dismiss an open overlay" },
4132
+ { keys: "Click query box", action: "Focus the search input" },
4133
+ {
4134
+ keys: "\u2630 menu button",
4135
+ action: "Open the command menu (bottom-right)"
4136
+ }
4137
+ ],
4138
+ note: "Every list is fully clickable; keyboard and mouse are interchangeable."
4139
+ },
3984
4140
  {
3985
4141
  title: " Overlay Keys ",
3986
4142
  titleColor: "teal",
@@ -4724,10 +4880,12 @@ var StatusManager = class {
4724
4880
  type = "idle";
4725
4881
  message = "";
4726
4882
  onUpdate;
4883
+ disposed = false;
4727
4884
  constructor(onUpdate) {
4728
4885
  this.onUpdate = onUpdate;
4729
4886
  }
4730
4887
  set(type, message) {
4888
+ if (this.disposed) return;
4731
4889
  this.stopSpinner();
4732
4890
  this.type = type;
4733
4891
  this.message = message;
@@ -4740,6 +4898,7 @@ var StatusManager = class {
4740
4898
  }
4741
4899
  }
4742
4900
  startSpinner(frames) {
4901
+ if (this.disposed) return;
4743
4902
  this.spinnerIndex = 0;
4744
4903
  this.emit(frames[0]);
4745
4904
  this.spinnerInterval = setInterval(() => {
@@ -4754,14 +4913,23 @@ var StatusManager = class {
4754
4913
  }
4755
4914
  }
4756
4915
  emit(spinner) {
4916
+ if (this.disposed) return;
4757
4917
  const icon = this.type === "searching" ? "\u{1F50D}" : this.type === "loading" ? "\u{1F4E6}" : this.type === "success" ? "\u2713" : this.type === "error" ? "\u26A0" : " ";
4758
4918
  const prefix = spinner ?? "";
4759
4919
  this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
4760
4920
  }
4761
4921
  dispose() {
4922
+ this.disposed = true;
4762
4923
  this.stopSpinner();
4763
4924
  }
4764
4925
  };
4926
+ function createStatusManager(bar, onFrame) {
4927
+ return new StatusManager((text) => {
4928
+ if (bar.isDestroyed) return;
4929
+ bar.content = ` ${text}`;
4930
+ onFrame();
4931
+ });
4932
+ }
4765
4933
 
4766
4934
  // src/share.ts
4767
4935
  import { spawn as spawn3 } from "child_process";
@@ -5124,13 +5292,15 @@ function createTuiLogger() {
5124
5292
  };
5125
5293
  }
5126
5294
  var logger = createTuiLogger();
5295
+ var activeStatusMgr = null;
5296
+ var activeRenderer = null;
5127
5297
  var SORT_MODES = [
5128
5298
  { key: "best-match", label: "best-match" },
5129
5299
  { key: "stars", label: "stars" },
5130
5300
  { key: "updated", label: "updated" },
5131
5301
  { key: "forks", label: "forks" }
5132
5302
  ];
5133
- async function launchBrowser(theme_override) {
5303
+ async function launchBrowser(theme_override, startMode) {
5134
5304
  const terminalBg = await detectTerminalBackground();
5135
5305
  let renderer;
5136
5306
  try {
@@ -5166,6 +5336,7 @@ async function launchBrowser(theme_override) {
5166
5336
  return;
5167
5337
  }
5168
5338
  const root = renderer.root;
5339
+ activeRenderer = renderer;
5169
5340
  root.flexDirection = "column";
5170
5341
  const config = loadConfig();
5171
5342
  let githubToken = config.githubToken || process.env.GITHUB_TOKEN;
@@ -5197,7 +5368,7 @@ async function launchBrowser(theme_override) {
5197
5368
  const compareList = [];
5198
5369
  const _quitArmed = false;
5199
5370
  const header = new TextRenderable5(renderer, {
5200
- content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
5371
+ content: " ghfind \u2014 GitHub repo browser [/]search [Esc]menu [\u2192]open [?]help [q]uit \xB7 click select \xB7 dbl-click open",
5201
5372
  bg: colors.bg,
5202
5373
  fg: colors.muted,
5203
5374
  height: 1
@@ -5218,15 +5389,25 @@ async function launchBrowser(theme_override) {
5218
5389
  const isActive = name === trendingTab;
5219
5390
  const label = ` ${name} `;
5220
5391
  const tt = new TextRenderable5(renderer, {
5392
+ ...NON_INTERACTIVE_TEXT,
5221
5393
  content: label,
5222
5394
  fg: isActive ? colors.bg : colors.muted,
5223
5395
  bg: isActive ? colors.blue : colors.bg,
5224
5396
  height: 1
5225
5397
  });
5398
+ tt.onMouseDown = (event) => {
5399
+ event.stopPropagation();
5400
+ if (name === trendingTab) return;
5401
+ trendingTab = name;
5402
+ loadTrending();
5403
+ };
5404
+ tt.onMouseOver = () => renderer.setMousePointer("pointer");
5405
+ tt.onMouseOut = () => renderer.setMousePointer("default");
5226
5406
  trendingTabTexts.push(tt);
5227
5407
  trendingTabBox.add(tt);
5228
5408
  if (i < TAB_NAMES.length - 1) {
5229
5409
  const sp = new TextRenderable5(renderer, {
5410
+ ...NON_INTERACTIVE_TEXT,
5230
5411
  content: " ",
5231
5412
  fg: colors.muted,
5232
5413
  bg: colors.bg,
@@ -5314,7 +5495,10 @@ async function launchBrowser(theme_override) {
5314
5495
  const detailText = new TextRenderable5(renderer, {
5315
5496
  content: "",
5316
5497
  fg: colors.text,
5317
- wrapMode: "none"
5498
+ wrapMode: "none",
5499
+ // Non-interactive: a left-press here would otherwise be swallowed as a
5500
+ // text-selection drag instead of reaching the click handlers.
5501
+ selectable: false
5318
5502
  });
5319
5503
  detailBox.add(detailText);
5320
5504
  body.add(resultsBox);
@@ -5329,6 +5513,7 @@ async function launchBrowser(theme_override) {
5329
5513
  });
5330
5514
  root.add(landingBox);
5331
5515
  const landingBanner = new TextRenderable5(renderer, {
5516
+ ...NON_INTERACTIVE_TEXT,
5332
5517
  content: [
5333
5518
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
5334
5519
  "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
@@ -5406,7 +5591,8 @@ async function launchBrowser(theme_override) {
5406
5591
  card.add(spacerBot);
5407
5592
  }
5408
5593
  const landingHint = new TextRenderable5(renderer, {
5409
- content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
5594
+ ...NON_INTERACTIVE_TEXT,
5595
+ content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit \xB7 click to pick a mode",
5410
5596
  fg: colors.muted,
5411
5597
  bg: colors.bg,
5412
5598
  height: 1,
@@ -5434,6 +5620,28 @@ async function launchBrowser(theme_override) {
5434
5620
  }
5435
5621
  }
5436
5622
  }
5623
+ landingCards.forEach((card, i) => {
5624
+ let lastClickAt = 0;
5625
+ card.onMouseDown = (event) => {
5626
+ event.stopPropagation();
5627
+ const now = Date.now();
5628
+ const isDouble = landingSelected === i && now - lastClickAt < DOUBLE_CLICK_MS;
5629
+ lastClickAt = now;
5630
+ if (isDouble) {
5631
+ landingOptions[i].action();
5632
+ return;
5633
+ }
5634
+ landingSelected = i;
5635
+ updateLandingCards();
5636
+ renderer.requestRender();
5637
+ };
5638
+ card.onMouseOver = () => {
5639
+ renderer.setMousePointer("pointer");
5640
+ };
5641
+ card.onMouseOut = () => {
5642
+ renderer.setMousePointer("default");
5643
+ };
5644
+ });
5437
5645
  renderer.keyInput.on("keypress", (key) => {
5438
5646
  if (!landingKeysActive(currentMode, currentOverlay)) return;
5439
5647
  if (key.name === "up" || key.name === "k") {
@@ -5494,10 +5702,12 @@ async function launchBrowser(theme_override) {
5494
5702
  leaderBox.visible = false;
5495
5703
  leaderDim.visible = false;
5496
5704
  readmeBox.visible = false;
5705
+ orgBox.visible = false;
5497
5706
  updateBox.visible = false;
5498
5707
  updateDim.visible = false;
5499
5708
  if (type === "none") {
5500
5709
  currentOverlay = "none";
5710
+ menuButton.visible = true;
5501
5711
  if (currentMode === "landing") {
5502
5712
  landingBox.visible = true;
5503
5713
  body.visible = false;
@@ -5511,6 +5721,7 @@ async function launchBrowser(theme_override) {
5511
5721
  return;
5512
5722
  }
5513
5723
  currentOverlay = type;
5724
+ menuButton.visible = false;
5514
5725
  landingBox.visible = false;
5515
5726
  if (type !== "leader") {
5516
5727
  hideMainContent();
@@ -5772,7 +5983,8 @@ async function launchBrowser(theme_override) {
5772
5983
  const compareText = new TextRenderable5(renderer, {
5773
5984
  content: "",
5774
5985
  fg: colors.text,
5775
- bg: colors.bg
5986
+ bg: colors.bg,
5987
+ selectable: false
5776
5988
  });
5777
5989
  compareBox.add(compareText);
5778
5990
  root.add(compareBox);
@@ -5901,7 +6113,8 @@ async function launchBrowser(theme_override) {
5901
6113
  itemSpacing: 0
5902
6114
  });
5903
6115
  const leaderFooter = new TextRenderable5(renderer, {
5904
- content: " \u2191\u2193 select Enter run Esc/q close ",
6116
+ ...NON_INTERACTIVE_TEXT,
6117
+ content: " \u2191\u2193 select Enter run Esc/q close \xB7 click to run click outside to close ",
5905
6118
  fg: colors.muted,
5906
6119
  bg: colors.surfaceDim,
5907
6120
  height: 1,
@@ -6033,10 +6246,59 @@ async function launchBrowser(theme_override) {
6033
6246
  neverText.fg = updateSelectedOption === 2 ? colors.yellow : colors.text;
6034
6247
  renderer.requestRender();
6035
6248
  }
6249
+ function runUpdateOption(index) {
6250
+ updateSelectedOption = index;
6251
+ renderUpdateOptions();
6252
+ const action = ["now", "later", "never"][index];
6253
+ if (action === "now") {
6254
+ showOverlay("none");
6255
+ recordPreUpdateState(cachedVersion ?? "");
6256
+ performUpdate().then((ok) => {
6257
+ if (ok) {
6258
+ setStatus("\u2713 Update complete \u2014 restart ghfind");
6259
+ } else {
6260
+ setStatus("\u2717 Update failed \u2014 check terminal output");
6261
+ }
6262
+ }).catch(() => {
6263
+ setStatus("\u2717 Update failed \u2014 check terminal output");
6264
+ });
6265
+ } else if (action === "never") {
6266
+ suppressUpdateNotices();
6267
+ showOverlay("none");
6268
+ setStatus("Update notices suppressed");
6269
+ } else {
6270
+ snoozeUpdateNotices(3);
6271
+ showOverlay("none");
6272
+ setStatus("Update reminder snoozed for 3 days");
6273
+ }
6274
+ renderer.requestRender();
6275
+ }
6276
+ const updateCards = [
6277
+ [updateNowBox, 0],
6278
+ [laterBox, 1],
6279
+ [neverBox, 2]
6280
+ ];
6281
+ for (const [box, index] of updateCards) {
6282
+ attachClickableBox(renderer, box, {
6283
+ onActivate: () => runUpdateOption(index),
6284
+ idleBorder: updateSelectedOption === index ? colors.yellow : colors.border,
6285
+ activeBorder: colors.yellow,
6286
+ // Hovering must not fight the selection highlight, so it only moves the
6287
+ // pointer cursor; the border stays owned by renderUpdateOptions().
6288
+ onStateChange: () => {
6289
+ box.borderColor = updateSelectedOption === index ? colors.yellow : colors.border;
6290
+ }
6291
+ });
6292
+ }
6293
+ attachClickOutside(updateDim, () => {
6294
+ showOverlay("none");
6295
+ renderer.requestRender();
6296
+ });
6036
6297
  updateOptionsRow.add(updateNowBox);
6037
6298
  updateOptionsRow.add(laterBox);
6038
6299
  const updateFooter = new TextRenderable5(renderer, {
6039
- content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close",
6300
+ ...NON_INTERACTIVE_TEXT,
6301
+ content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close \xB7 click a button click outside to close",
6040
6302
  fg: colors.muted,
6041
6303
  bg: colors.surfaceDim,
6042
6304
  height: 1,
@@ -6525,6 +6787,7 @@ async function launchBrowser(theme_override) {
6525
6787
  }
6526
6788
  });
6527
6789
  const orgText = new TextRenderable5(renderer, {
6790
+ ...NON_INTERACTIVE_TEXT,
6528
6791
  content: "",
6529
6792
  fg: colors.text,
6530
6793
  bg: colors.bg
@@ -6532,6 +6795,7 @@ async function launchBrowser(theme_override) {
6532
6795
  orgScroll.add(orgText);
6533
6796
  orgBox.add(orgScroll);
6534
6797
  const orgFooter = new TextRenderable5(renderer, {
6798
+ ...NON_INTERACTIVE_TEXT,
6535
6799
  content: " \u2191\u2193/jk scroll Esc/q close",
6536
6800
  fg: colors.muted,
6537
6801
  bg: colors.bg,
@@ -6671,10 +6935,46 @@ async function launchBrowser(theme_override) {
6671
6935
  paddingX: 1
6672
6936
  });
6673
6937
  root.add(statusBar);
6674
- const statusMgr = new StatusManager((text) => {
6675
- statusBar.content = ` ${text}`;
6676
- renderer.requestRender();
6938
+ const menuButton = new BoxRenderable5(renderer, {
6939
+ position: "absolute",
6940
+ bottom: 1,
6941
+ right: 1,
6942
+ height: 1,
6943
+ paddingX: 1,
6944
+ backgroundColor: colors.surface,
6945
+ focusable: false
6946
+ });
6947
+ const menuButtonLabel = new TextRenderable5(renderer, {
6948
+ ...NON_INTERACTIVE_TEXT,
6949
+ content: "\u2630 menu",
6950
+ fg: colors.muted,
6951
+ bg: colors.surface,
6952
+ height: 1
6677
6953
  });
6954
+ menuButton.add(menuButtonLabel);
6955
+ root.add(menuButton);
6956
+ menuButton.onMouseDown = (event) => {
6957
+ event.stopPropagation();
6958
+ if (currentOverlay === "leader") showOverlay("none");
6959
+ else showLeaderMenu();
6960
+ renderer.requestRender();
6961
+ };
6962
+ menuButton.onMouseOver = () => {
6963
+ renderer.setMousePointer("pointer");
6964
+ menuButtonLabel.fg = colors.accent;
6965
+ renderer.requestRender();
6966
+ };
6967
+ menuButton.onMouseOut = () => {
6968
+ renderer.setMousePointer("default");
6969
+ menuButtonLabel.fg = colors.muted;
6970
+ renderer.requestRender();
6971
+ };
6972
+ activeStatusMgr = createStatusManager(
6973
+ statusBar,
6974
+ () => renderer.requestRender()
6975
+ );
6976
+ const statusMgr = activeStatusMgr;
6977
+ renderer.on("destroy", () => statusMgr.dispose());
6678
6978
  setTimeout(() => {
6679
6979
  if (currentOverlay === "none") {
6680
6980
  setStatus(nextTip());
@@ -7264,6 +7564,158 @@ ${pack.description ?? ""}`;
7264
7564
  if (repo) openUrl(repo.url);
7265
7565
  }
7266
7566
  });
7567
+ attachClickToSelect(renderer, resultsSelect, {
7568
+ keepFocus: true,
7569
+ onOpen: () => resultsSelect.selectCurrent()
7570
+ });
7571
+ function activateOverlaySelection() {
7572
+ switch (currentOverlay) {
7573
+ case "history": {
7574
+ const sel = historySelect.getSelectedOption();
7575
+ if (sel?.value) {
7576
+ const entry = sel.value.entry;
7577
+ showOverlay("none");
7578
+ if (!entry) return;
7579
+ if (entry.mode === "trending") {
7580
+ if (entry.tab)
7581
+ trendingTab = entry.tab;
7582
+ loadTrending();
7583
+ } else {
7584
+ searchInput.value = entry.query;
7585
+ showSearchMode();
7586
+ doSearch(entry.query);
7587
+ }
7588
+ }
7589
+ return;
7590
+ }
7591
+ case "bookmarks": {
7592
+ const sel = bookmarksSelect.getSelectedOption();
7593
+ if (sel?.value)
7594
+ openUrl(sel.value.repo?.url ?? "");
7595
+ return;
7596
+ }
7597
+ case "saved": {
7598
+ const sel = savedSelect.getSelectedOption();
7599
+ if (sel?.value) {
7600
+ const s = sel.value;
7601
+ showOverlay("none");
7602
+ touchSavedSearch(s.name);
7603
+ if (s.mode === "trending") {
7604
+ if (s.tab) trendingTab = s.tab;
7605
+ loadTrending();
7606
+ } else {
7607
+ searchInput.value = s.query;
7608
+ showSearchMode();
7609
+ currentSort = s.sort;
7610
+ currentLimit = s.limit;
7611
+ setToolbar();
7612
+ doSearch(s.query);
7613
+ }
7614
+ }
7615
+ return;
7616
+ }
7617
+ case "topics": {
7618
+ const sel = topicsSelect.getSelectedOption();
7619
+ if (sel?.value) {
7620
+ const topic = sel.value;
7621
+ showOverlay("none");
7622
+ searchInput.value = `topic:${topic.name ?? ""}`;
7623
+ showSearchMode();
7624
+ doSearch(`topic:${topic.name}`);
7625
+ }
7626
+ return;
7627
+ }
7628
+ case "export": {
7629
+ const sel = exportSelect.getSelectedOption();
7630
+ if (sel?.value && currentRepos.length > 0) {
7631
+ const format2 = sel.value;
7632
+ const path = exportToFile(currentRepos, format2);
7633
+ showOverlay("none");
7634
+ setStatus(`\u2713 Exported to ${path}`);
7635
+ } else {
7636
+ setStatus("No results to export");
7637
+ showOverlay("none");
7638
+ }
7639
+ renderer.requestRender();
7640
+ return;
7641
+ }
7642
+ case "share": {
7643
+ const sel = shareSelect.getSelectedOption();
7644
+ if (sel?.value) {
7645
+ const opt = resultsSelect.getSelectedOption();
7646
+ const repo = opt?.value;
7647
+ if (repo) {
7648
+ const text = formatShare(repo, sel.value);
7649
+ copyToClipboard(text).then((ok) => {
7650
+ showOverlay("none");
7651
+ setStatus(ok ? "\u2713 Copied!" : "Clipboard not available");
7652
+ });
7653
+ }
7654
+ }
7655
+ renderer.requestRender();
7656
+ return;
7657
+ }
7658
+ case "help": {
7659
+ showOverlay("none");
7660
+ renderer.requestRender();
7661
+ return;
7662
+ }
7663
+ default:
7664
+ return;
7665
+ }
7666
+ }
7667
+ function attachOverlayMouse(box, select) {
7668
+ attachClickToSelect(renderer, select, {
7669
+ onOpen: () => activateOverlaySelection(),
7670
+ onClickMiss: () => {
7671
+ showOverlay("none");
7672
+ renderer.requestRender();
7673
+ }
7674
+ });
7675
+ attachClickOutside(box, () => {
7676
+ showOverlay("none");
7677
+ renderer.requestRender();
7678
+ });
7679
+ }
7680
+ attachOverlayMouse(historyBox, historySelect);
7681
+ attachOverlayMouse(bookmarksBox, bookmarksSelect);
7682
+ attachOverlayMouse(savedBox, savedSelect);
7683
+ attachOverlayMouse(topicsBox, topicsSelect);
7684
+ attachOverlayMouse(exportBox, exportSelect);
7685
+ attachOverlayMouse(shareBox, shareSelect);
7686
+ for (const panel of [helpBox, orgBox, readmeBox, compareBox, notifsBox]) {
7687
+ attachClickOutside(panel, () => {
7688
+ showOverlay("none");
7689
+ renderer.requestRender();
7690
+ });
7691
+ }
7692
+ for (const scroller of [helpScroll, orgScroll, readmeScroll, updateScroll]) {
7693
+ scroller.onMouseScroll = (event) => {
7694
+ event.stopPropagation();
7695
+ };
7696
+ }
7697
+ attachClickToSelect(renderer, leaderSelect, {
7698
+ onOpen: () => {
7699
+ const entry = leaderSelect.getSelectedOption()?.value;
7700
+ if (!entry) return;
7701
+ if (entry.type === "category") {
7702
+ pushMenuLevel(` Menu > ${entry.name}`, entry.name, entry.children, 0);
7703
+ } else {
7704
+ resetMenu();
7705
+ showOverlay("none");
7706
+ entry.action();
7707
+ }
7708
+ renderer.requestRender();
7709
+ },
7710
+ onClickMiss: () => {
7711
+ showOverlay("none");
7712
+ renderer.requestRender();
7713
+ }
7714
+ });
7715
+ attachClickOutside(leaderDim, () => {
7716
+ showOverlay("none");
7717
+ renderer.requestRender();
7718
+ });
7267
7719
  renderer.keyInput.on("keypress", (key) => {
7268
7720
  if (tokenPromptBox.visible) {
7269
7721
  if (key.name === "escape") {
@@ -7357,21 +7809,7 @@ ${pack.description ?? ""}`;
7357
7809
  return;
7358
7810
  }
7359
7811
  if (key.name === "enter" || key.name === "return") {
7360
- const sel = historySelect.getSelectedOption();
7361
- if (sel?.value) {
7362
- const entry = sel.value.entry;
7363
- showOverlay("none");
7364
- if (!entry) return;
7365
- if (entry.mode === "trending") {
7366
- if (entry.tab)
7367
- trendingTab = entry.tab;
7368
- loadTrending();
7369
- } else {
7370
- searchInput.value = entry.query;
7371
- showSearchMode();
7372
- doSearch(entry.query);
7373
- }
7374
- }
7812
+ activateOverlaySelection();
7375
7813
  return;
7376
7814
  }
7377
7815
  return;
@@ -7390,9 +7828,7 @@ ${pack.description ?? ""}`;
7390
7828
  return;
7391
7829
  }
7392
7830
  if (key.name === "enter" || key.name === "return") {
7393
- const sel = bookmarksSelect.getSelectedOption();
7394
- if (sel?.value)
7395
- openUrl(sel.value.repo?.url ?? "");
7831
+ activateOverlaySelection();
7396
7832
  return;
7397
7833
  }
7398
7834
  return;
@@ -7409,54 +7845,21 @@ ${pack.description ?? ""}`;
7409
7845
  return;
7410
7846
  }
7411
7847
  if (key.name === "enter" || key.name === "return") {
7412
- const sel = savedSelect.getSelectedOption();
7413
- if (sel?.value) {
7414
- const s = sel.value;
7415
- showOverlay("none");
7416
- touchSavedSearch(s.name);
7417
- if (s.mode === "trending") {
7418
- if (s.tab) trendingTab = s.tab;
7419
- loadTrending();
7420
- } else {
7421
- searchInput.value = s.query;
7422
- showSearchMode();
7423
- currentSort = s.sort;
7424
- currentLimit = s.limit;
7425
- setToolbar();
7426
- doSearch(s.query);
7427
- }
7428
- }
7848
+ activateOverlaySelection();
7429
7849
  return;
7430
7850
  }
7431
7851
  return;
7432
7852
  }
7433
7853
  if (currentOverlay === "topics") {
7434
7854
  if (key.name === "enter" || key.name === "return") {
7435
- const sel = topicsSelect.getSelectedOption();
7436
- if (sel?.value) {
7437
- const topic = sel.value;
7438
- showOverlay("none");
7439
- searchInput.value = `topic:${topic.name ?? ""}`;
7440
- showSearchMode();
7441
- doSearch(`topic:${topic.name}`);
7442
- }
7855
+ activateOverlaySelection();
7443
7856
  return;
7444
7857
  }
7445
7858
  return;
7446
7859
  }
7447
7860
  if (currentOverlay === "export") {
7448
7861
  if (key.name === "enter" || key.name === "return") {
7449
- const sel = exportSelect.getSelectedOption();
7450
- if (sel?.value && currentRepos.length > 0) {
7451
- const format2 = sel.value;
7452
- const path = exportToFile(currentRepos, format2);
7453
- showOverlay("none");
7454
- setStatus(`\u2713 Exported to ${path}`);
7455
- } else {
7456
- setStatus("No results to export");
7457
- showOverlay("none");
7458
- }
7459
- renderer.requestRender();
7862
+ activateOverlaySelection();
7460
7863
  return;
7461
7864
  }
7462
7865
  return;
@@ -7485,26 +7888,13 @@ ${pack.description ?? ""}`;
7485
7888
  }
7486
7889
  if (currentOverlay === "share") {
7487
7890
  if (key.name === "enter" || key.name === "return") {
7488
- const sel = shareSelect.getSelectedOption();
7489
- if (sel?.value) {
7490
- const opt = resultsSelect.getSelectedOption();
7491
- const repo = opt?.value;
7492
- if (repo) {
7493
- const text = formatShare(repo, sel.value);
7494
- copyToClipboard(text).then((ok) => {
7495
- showOverlay("none");
7496
- setStatus(ok ? "\u2713 Copied!" : "Clipboard not available");
7497
- });
7498
- }
7499
- }
7500
- renderer.requestRender();
7891
+ activateOverlaySelection();
7501
7892
  return;
7502
7893
  }
7503
7894
  return;
7504
7895
  }
7505
7896
  if (currentOverlay === "help") {
7506
- showOverlay("none");
7507
- renderer.requestRender();
7897
+ activateOverlaySelection();
7508
7898
  return;
7509
7899
  }
7510
7900
  if (currentOverlay === "update") {
@@ -7529,29 +7919,7 @@ ${pack.description ?? ""}`;
7529
7919
  return;
7530
7920
  }
7531
7921
  if (key.name === "enter" || key.name === "return") {
7532
- const action = ["now", "later", "never"][updateSelectedOption];
7533
- if (action === "now") {
7534
- showOverlay("none");
7535
- recordPreUpdateState(cachedVersion ?? "");
7536
- performUpdate().then((ok) => {
7537
- if (ok) {
7538
- setStatus("\u2713 Update complete \u2014 restart ghfind");
7539
- } else {
7540
- setStatus("\u2717 Update failed \u2014 check terminal output");
7541
- }
7542
- }).catch(() => {
7543
- setStatus("\u2717 Update failed \u2014 check terminal output");
7544
- });
7545
- } else if (action === "never") {
7546
- suppressUpdateNotices();
7547
- showOverlay("none");
7548
- setStatus("Update notices suppressed");
7549
- } else {
7550
- snoozeUpdateNotices(3);
7551
- showOverlay("none");
7552
- setStatus("Update reminder snoozed for 3 days");
7553
- }
7554
- renderer.requestRender();
7922
+ runUpdateOption(updateSelectedOption);
7555
7923
  return;
7556
7924
  }
7557
7925
  return;
@@ -7574,8 +7942,14 @@ ${pack.description ?? ""}`;
7574
7942
  toggleGraph();
7575
7943
  return;
7576
7944
  }
7577
- if (key.name === "space" && !searchInput.focused) {
7578
- showLeaderMenu();
7945
+ if (key.name === "escape") {
7946
+ if (currentOverlay !== "none") {
7947
+ showOverlay("none");
7948
+ setToolbar();
7949
+ } else {
7950
+ showLeaderMenu();
7951
+ }
7952
+ renderer.requestRender();
7579
7953
  return;
7580
7954
  }
7581
7955
  if (!searchInput.focused && errorHint) {
@@ -7693,7 +8067,12 @@ ${pack.description ?? ""}`;
7693
8067
  }
7694
8068
  });
7695
8069
  const _currentVersion = getVersion();
7696
- showLanding(true);
8070
+ if (startMode === "trending") {
8071
+ showLanding(false);
8072
+ loadTrending();
8073
+ } else {
8074
+ showLanding(true);
8075
+ }
7697
8076
  checkForUpdateAndShow();
7698
8077
  checkPostUpgradePanel();
7699
8078
  renderer.start();
@@ -7710,10 +8089,17 @@ function formatToolbar(sort, limit, totalCount) {
7710
8089
  return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
7711
8090
  }
7712
8091
  function cleanup() {
8092
+ activeStatusMgr?.dispose();
8093
+ activeStatusMgr = null;
7713
8094
  try {
7714
8095
  rotateHistory();
7715
8096
  } catch {
7716
8097
  }
8098
+ try {
8099
+ activeRenderer?.destroy();
8100
+ } catch {
8101
+ }
8102
+ activeRenderer = null;
7717
8103
  process.exit(0);
7718
8104
  }
7719
8105
  if (import.meta.main && false) {
@@ -8385,7 +8771,7 @@ var TOOLS = [
8385
8771
  token: resolveToken(state, args.token),
8386
8772
  limit
8387
8773
  });
8388
- return { text: formatUserJson(profile), data: { ...profile } };
8774
+ return { text: formatUserText(profile), data: { ...profile } };
8389
8775
  }
8390
8776
  },
8391
8777
  {
@@ -8732,6 +9118,7 @@ var SORT_VALUES = [
8732
9118
  "downloads",
8733
9119
  "name"
8734
9120
  ];
9121
+ var SINCE_VALUES = ["daily", "weekly", "monthly"];
8735
9122
  function parseArgs(args) {
8736
9123
  const flags = {
8737
9124
  query: "",
@@ -8744,6 +9131,7 @@ function parseArgs(args) {
8744
9131
  token: process.env.GITHUB_TOKEN,
8745
9132
  trending: false,
8746
9133
  since: "daily",
9134
+ sinceExplicit: false,
8747
9135
  watch: false,
8748
9136
  releases: false,
8749
9137
  interval: 300,
@@ -8844,15 +9232,51 @@ function parseArgs(args) {
8844
9232
  case "--token":
8845
9233
  flags.token = args[++i];
8846
9234
  break;
8847
- case "--since":
8848
- flags.since = args[++i];
9235
+ case "--interval": {
9236
+ const parsed = parseInt(args[++i], 10);
9237
+ if (Number.isNaN(parsed) || parsed < 1) {
9238
+ console.error(
9239
+ `Invalid interval: ${args[i] ?? "(missing)"} (must be a whole number of seconds, e.g. 300).`
9240
+ );
9241
+ process.exit(1);
9242
+ }
9243
+ flags.interval = parsed;
9244
+ break;
9245
+ }
9246
+ case "--since": {
9247
+ const since = args[++i];
9248
+ if (!since || !SINCE_VALUES.includes(since)) {
9249
+ console.error(
9250
+ `Invalid since: ${since ?? "(missing)"} (use daily|weekly|monthly).`
9251
+ );
9252
+ process.exit(1);
9253
+ }
9254
+ flags.since = since;
9255
+ flags.sinceExplicit = true;
8849
9256
  break;
8850
- case "--pipe":
8851
- flags.pipe = args[++i];
9257
+ }
9258
+ case "--pipe": {
9259
+ const target = args[++i];
9260
+ if (!target || !PIPE_TARGETS.includes(target)) {
9261
+ console.error(
9262
+ `Invalid pipe target: ${target ?? "(missing)"} (use open|clone).`
9263
+ );
9264
+ process.exit(1);
9265
+ }
9266
+ flags.pipe = target;
8852
9267
  break;
8853
- case "--format":
8854
- flags.format = args[++i];
9268
+ }
9269
+ case "--format": {
9270
+ const fmt = args[++i];
9271
+ if (!fmt || !FORMAT_VALUES.includes(fmt)) {
9272
+ console.error(
9273
+ `Invalid format: ${fmt ?? "(missing)"} (use ${FORMAT_VALUES.join("|")}).`
9274
+ );
9275
+ process.exit(1);
9276
+ }
9277
+ flags.format = fmt;
8855
9278
  break;
9279
+ }
8856
9280
  case "--registry":
8857
9281
  flags.registry = args[++i] ?? "npm";
8858
9282
  break;
@@ -9141,7 +9565,13 @@ Options:
9141
9565
  await runNonInteractive(flags, outputFormat);
9142
9566
  return;
9143
9567
  }
9144
- await launchBrowser(flags.theme);
9568
+ await launchBrowser(
9569
+ flags.theme,
9570
+ // `ghfind --trending` with no query/format promises the trending view
9571
+ // (TRACKER.md), but the bare flag used to fall through to the landing
9572
+ // menu. An explicit flag is explicit intent — skip the menu.
9573
+ flags.trending ? "trending" : void 0
9574
+ );
9145
9575
  }
9146
9576
  function buildSearchContext(flags) {
9147
9577
  const parsed = applyFlagFilters(parseQuery(flags.query), {});
@@ -9606,7 +10036,8 @@ async function runNonInteractive(flags, outputFormat) {
9606
10036
  limit: flags.limit,
9607
10037
  token: flags.token,
9608
10038
  intervalMs: flags.interval * 1e3,
9609
- trending: flags.trending
10039
+ trending: flags.trending,
10040
+ since: flags.sinceExplicit ? flags.since : void 0
9610
10041
  },
9611
10042
  (repos, delta) => {
9612
10043
  tick++;
@@ -9694,6 +10125,16 @@ Not found: ${missing.join(", ")}` : "";
9694
10125
  }
9695
10126
  };
9696
10127
  const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
10128
+ const needsQuery = key === "search" || key === "format" || key === "pipe" || key === "watch" && !flags.trending;
10129
+ if (needsQuery && flags.query.trim() === "") {
10130
+ console.error("\u26A0 No search query given.");
10131
+ console.error(
10132
+ ' Usage: ghfind "<query>" [--count|--format <fmt>|--pipe <tgt>|--watch]'
10133
+ );
10134
+ console.error(' Example: ghfind "language:Rust stars:>500" --count');
10135
+ console.error(" Run `ghfind --help` for all options.");
10136
+ process.exit(1);
10137
+ }
9697
10138
  const ctx = buildSearchContext(flags);
9698
10139
  await handlers[key](ctx);
9699
10140
  }