feat: add -f (follow) and -s (sleep) options to req command

Add tail -f style auto-refresh behavior to the req command. When enabled,
new events are automatically displayed instead of being buffered behind
a "X new events" button.

- Add -f/--follow flag to enable auto-refresh mode
- Add -s/--sleep flag to set refresh interval (default: 1 second)
- Reuse existing freeze/unfreeze mechanism with periodic auto-unfreeze
- Add comprehensive tests for new parser options
- Update man page with documentation and examples

Usage:
  req -k 1 -f                  # Follow mode with 1s refresh
  req -k 1 -f -s 0.5           # Follow mode with 0.5s refresh
  req -k 1 -a $contacts -f     # Follow contacts' notes in real-time

https://claude.ai/code/session_01Tv9Th39LyQwctwEWqqDVNo
This commit is contained in:
Claude
2026-01-27 10:15:46 +00:00
parent a28ffc1ec3
commit 50535e67e6
4 changed files with 137 additions and 0 deletions

View File

@@ -117,6 +117,8 @@ interface ReqViewerProps {
relays?: string[];
closeOnEose?: boolean;
view?: ViewMode;
follow?: boolean; // Auto-refresh mode (like tail -f)
followInterval?: number; // Refresh interval in seconds (default: 1)
nip05Authors?: string[];
nip05PTags?: string[];
domainAuthors?: string[];
@@ -713,6 +715,8 @@ export default function ReqViewer({
relays,
closeOnEose = false,
view = "list",
follow = false,
followInterval = 1,
nip05Authors,
nip05PTags,
domainAuthors,
@@ -889,6 +893,17 @@ export default function ReqViewer({
});
}, []);
// Auto-refresh in follow mode (like tail -f)
useEffect(() => {
if (!follow || !isFrozen || newEventCount === 0) return;
const timer = setInterval(() => {
handleUnfreeze();
}, followInterval * 1000);
return () => clearInterval(timer);
}, [follow, followInterval, isFrozen, newEventCount, handleUnfreeze]);
/**
* Export events to JSONL format with chunked processing for large datasets
* Uses Share API on mobile for reliable file sharing, falls back to download on desktop

View File

@@ -1402,6 +1402,89 @@ describe("parseReqCommand", () => {
});
});
describe("follow flag (-f, --follow)", () => {
it("should parse -f flag", () => {
const result = parseReqCommand(["-f"]);
expect(result.follow).toBe(true);
});
it("should parse --follow flag", () => {
const result = parseReqCommand(["--follow"]);
expect(result.follow).toBe(true);
});
it("should default to false when not provided", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.follow).toBe(false);
});
it("should work with other flags", () => {
const result = parseReqCommand(["-k", "1", "-f", "-l", "50"]);
expect(result.filter.kinds).toEqual([1]);
expect(result.follow).toBe(true);
expect(result.filter.limit).toBe(50);
});
});
describe("sleep flag (-s, --sleep)", () => {
it("should parse -s flag with integer value", () => {
const result = parseReqCommand(["-s", "2"]);
expect(result.followInterval).toBe(2);
});
it("should parse -s flag with decimal value", () => {
const result = parseReqCommand(["-s", "0.5"]);
expect(result.followInterval).toBe(0.5);
});
it("should parse --sleep flag", () => {
const result = parseReqCommand(["--sleep", "3"]);
expect(result.followInterval).toBe(3);
});
it("should be undefined when not provided", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.followInterval).toBeUndefined();
});
it("should ignore invalid values", () => {
const result = parseReqCommand(["-s", "invalid"]);
expect(result.followInterval).toBeUndefined();
});
it("should ignore zero value", () => {
const result = parseReqCommand(["-s", "0"]);
expect(result.followInterval).toBeUndefined();
});
it("should ignore negative value", () => {
const result = parseReqCommand(["-s", "-1"]);
expect(result.followInterval).toBeUndefined();
});
it("should work with -f flag", () => {
const result = parseReqCommand(["-f", "-s", "2"]);
expect(result.follow).toBe(true);
expect(result.followInterval).toBe(2);
});
it("should work with other flags", () => {
const result = parseReqCommand([
"-k",
"1",
"-f",
"-s",
"0.5",
"-l",
"50",
]);
expect(result.filter.kinds).toEqual([1]);
expect(result.follow).toBe(true);
expect(result.followInterval).toBe(0.5);
expect(result.filter.limit).toBe(50);
});
});
describe("complex scenarios", () => {
it("should handle multiple flags together", () => {
const hex = "a".repeat(64);

View File

@@ -15,6 +15,8 @@ export interface ParsedReqCommand {
relays?: string[];
closeOnEose?: boolean;
view?: ViewMode; // Display mode for results
follow?: boolean; // Auto-refresh mode (like tail -f)
followInterval?: number; // Refresh interval in seconds (default: 1)
nip05Authors?: string[]; // NIP-05 identifiers that need async resolution
nip05PTags?: string[]; // NIP-05 identifiers for #p tags that need async resolution
nip05PTagsUppercase?: string[]; // NIP-05 identifiers for #P tags that need async resolution
@@ -83,6 +85,8 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
let closeOnEose = false;
let view: ViewMode | undefined;
let follow = false;
let followInterval: number | undefined;
let i = 0;
@@ -414,6 +418,25 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
break;
}
case "-f":
case "--follow": {
follow = true;
i++;
break;
}
case "-s":
case "--sleep": {
const interval = parseFloat(nextArg);
if (!isNaN(interval) && interval > 0) {
followInterval = interval;
i += 2;
} else {
i++;
}
break;
}
case "-T":
case "--tag": {
// Generic tag filter: --tag <letter> <value>
@@ -490,6 +513,8 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
relays: relays.length > 0 ? relays : undefined,
closeOnEose,
view,
follow,
followInterval,
nip05Authors: nip05Authors.size > 0 ? Array.from(nip05Authors) : undefined,
nip05PTags: nip05PTags.size > 0 ? Array.from(nip05PTags) : undefined,
nip05PTagsUppercase:

View File

@@ -234,6 +234,16 @@ export const manPages: Record<string, ManPageEntry> = {
description:
"Display mode for results. 'list' shows full event cards, 'compact' shows condensed single-line rows. Defaults to 'list'.",
},
{
flag: "-f, --follow",
description:
"Auto-refresh mode (like tail -f). Automatically displays new events instead of buffering them behind a 'X new events' button.",
},
{
flag: "-s, --sleep <seconds>",
description:
"Refresh interval in follow mode (default: 1 second). Only meaningful with -f. Lower values update more frequently but use more resources.",
},
{
flag: "[relay...]",
description:
@@ -272,6 +282,10 @@ export const manPages: Record<string, ManPageEntry> = {
"req --search bitcoin -k 1 Search notes for 'bitcoin'",
"req -k 1 theforest.nostr1.com relay.damus.io Query specific relays (overrides auto-selection)",
"req -k 1 -l 100 --view compact Get notes in compact view mode",
"req -k 1 -f Follow mode: auto-display new notes (1s refresh)",
"req -k 1 -a $contacts -f Follow your contacts' notes in real-time",
"req -k 1 -f -s 2 Follow mode with 2-second refresh interval",
"req -k 1 -f -s 0.5 Follow mode with faster 0.5-second refresh",
],
seeAlso: ["kind", "nip"],
appId: "req",