nip: display markdown directly, default to list.

This commit is contained in:
fiatjaf
2025-11-27 12:14:02 -03:00
parent 3ff4dbe196
commit 16916d7d95
3 changed files with 171 additions and 89 deletions

193
nip.go
View File

@@ -9,30 +9,25 @@ import (
"runtime"
"strings"
"github.com/charmbracelet/glamour"
"github.com/urfave/cli/v3"
)
type nipInfo struct {
nip, desc, link string
}
var nip = &cli.Command{
Name: "nip",
Usage: "get the description of a NIP from its number",
Description: `fetches the NIPs README from GitHub and parses it to find the description of the given NIP number.
example:
nak nip 1
nak nip list
nak nip open 1`,
ArgsUsage: "<NIP number>",
Usage: "list NIPs or get the description of a NIP from its number",
Description: `lists NIPs, fetches and displays NIP text, or opens a NIP page in the browser.
examples:
nak nip # list all NIPs
nak nip 29 # shows nip29 details
nak nip open 29 # opens nip29 in browser`,
ArgsUsage: "[NIP number]",
Commands: []*cli.Command{
{
Name: "list",
Usage: "list all NIPs",
Action: func(ctx context.Context, c *cli.Command) error {
return iterateNips(func(nip, desc, link string) bool {
stdout(nip + ": " + desc)
return true
})
},
},
{
Name: "open",
Usage: "open the NIP page in the browser",
@@ -55,17 +50,12 @@ example:
reqNum = normalize(reqNum)
foundLink := ""
err := iterateNips(func(nip, desc, link string) bool {
nipNum := normalize(nip)
for info := range listnips() {
nipNum := normalize(info.nip)
if nipNum == reqNum {
foundLink = link
return false
foundLink = info.link
break
}
return true
})
if err != nil {
return err
}
if foundLink == "" {
@@ -92,7 +82,11 @@ example:
Action: func(ctx context.Context, c *cli.Command) error {
reqNum := c.Args().First()
if reqNum == "" {
return fmt.Errorf("missing NIP number")
// list all NIPs
for info := range listnips() {
stdout(info.nip + ": " + info.desc)
}
return nil
}
normalize := func(s string) string {
@@ -107,80 +101,101 @@ example:
reqNum = normalize(reqNum)
found := false
err := iterateNips(func(nip, desc, link string) bool {
nipNum := normalize(nip)
var foundLink string
for info := range listnips() {
nipNum := normalize(info.nip)
if nipNum == reqNum {
stdout(strings.TrimSpace(desc))
found = true
return false
foundLink = info.link
break
}
return true
})
if err != nil {
return err
}
if !found {
if foundLink == "" {
return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum))
}
// fetch the NIP markdown
url := "https://raw.githubusercontent.com/nostr-protocol/nips/master/" + foundLink
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch NIP: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read NIP: %w", err)
}
// render markdown
rendered, err := glamour.Render(string(body), "auto")
if err != nil {
return fmt.Errorf("failed to render markdown: %w", err)
}
fmt.Print(rendered)
return nil
},
}
func iterateNips(yield func(nip, desc, link string) bool) error {
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
if err != nil {
return fmt.Errorf("failed to fetch NIPs README: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read NIPs README: %w", err)
}
bodyStr := string(body)
epoch := strings.Index(bodyStr, "## List")
lines := strings.SplitSeq(bodyStr[epoch+8:], "\n")
for line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "##") {
break
func listnips() <-chan nipInfo {
ch := make(chan nipInfo)
go func() {
defer close(ch)
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
if err != nil {
// TODO: handle error? but since chan, maybe send error somehow, but for now, just close
return
}
if !strings.HasPrefix(line, "- [NIP-") {
continue
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return
}
bodyStr := string(body)
epoch := strings.Index(bodyStr, "## List")
if epoch == -1 {
return
}
start := strings.Index(line, "[")
end := strings.Index(line, "]")
if start == -1 || end == -1 || end < start {
continue
lines := strings.SplitSeq(bodyStr[epoch+8:], "\n")
for line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "##") {
break
}
if !strings.HasPrefix(line, "- [NIP-") {
continue
}
start := strings.Index(line, "[")
end := strings.Index(line, "]")
if start == -1 || end == -1 || end < start {
continue
}
content := line[start+1 : end]
parts := strings.SplitN(content, ":", 2)
if len(parts) != 2 {
continue
}
nipPart := parts[0]
descPart := parts[1]
rest := line[end+1:]
linkStart := strings.Index(rest, "(")
linkEnd := strings.Index(rest, ")")
link := ""
if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart {
link = rest[linkStart+1 : linkEnd]
}
ch <- nipInfo{nipPart, strings.TrimSpace(descPart), link}
}
content := line[start+1 : end]
parts := strings.SplitN(content, ":", 2)
if len(parts) != 2 {
continue
}
nipPart := parts[0]
descPart := parts[1]
rest := line[end+1:]
linkStart := strings.Index(rest, "(")
linkEnd := strings.Index(rest, ")")
link := ""
if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart {
link = rest[linkStart+1 : linkEnd]
}
if !yield(nipPart, strings.TrimSpace(descPart), link) {
break
}
}
return nil
}()
return ch
}