From 50535e67e65b561be13f9924cb063086e0f9bb9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Jan 2026 10:15:46 +0000 Subject: [PATCH] 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 --- src/components/ReqViewer.tsx | 15 +++++++ src/lib/req-parser.test.ts | 83 ++++++++++++++++++++++++++++++++++++ src/lib/req-parser.ts | 25 +++++++++++ src/types/man.ts | 14 ++++++ 4 files changed, 137 insertions(+) diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 9f258fa..60ddf4a 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -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 diff --git a/src/lib/req-parser.test.ts b/src/lib/req-parser.test.ts index 3b80ad5..e9e78d3 100644 --- a/src/lib/req-parser.test.ts +++ b/src/lib/req-parser.test.ts @@ -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); diff --git a/src/lib/req-parser.ts b/src/lib/req-parser.ts index d233572..27bfe18 100644 --- a/src/lib/req-parser.ts +++ b/src/lib/req-parser.ts @@ -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 @@ -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: diff --git a/src/types/man.ts b/src/types/man.ts index 7e30314..80c685d 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -234,6 +234,16 @@ export const manPages: Record = { 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 ", + 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 = { "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",