issue.go (2946B)
1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "flag" 8 "fmt" 9 "io" 10 "io/fs" 11 "log" 12 "os" 13 "path/filepath" 14 "strings" 15 16 "9fans.net/go/acme" 17 "olowe.co/issues/issue/hub" 18 ) 19 20 var ( 21 projectFlag = flag.String("p", "", "GitHub owner/repo name") 22 ) 23 24 var fsys fs.FS 25 26 var project string = "golang/go" 27 28 func usage() { 29 fmt.Fprintf(os.Stderr, "usage: issue [-p owner/repo]") 30 flag.PrintDefaults() 31 os.Exit(2) 32 } 33 34 func init() { 35 flag.Usage = usage 36 flag.Parse() 37 log.SetFlags(0) 38 log.SetPrefix("issue: ") 39 } 40 41 func main() { 42 confDir, err := os.UserConfigDir() 43 if err != nil { 44 log.Fatalln("find config dir:", err) 45 } 46 confDir = filepath.Join(confDir, "github") 47 if *projectFlag != "" { 48 project = *projectFlag 49 } else { 50 projPath := confDir + "/project" 51 if _, err := os.Stat(projPath); err == nil { 52 b, err := os.ReadFile(projPath) 53 if err != nil { 54 log.Println("read default project file:", err) 55 } else { 56 project = string(bytes.TrimSpace(b)) 57 } 58 } 59 } 60 token, err := os.ReadFile(filepath.Join(confDir, "token")) 61 if errors.Is(err, fs.ErrNotExist) { 62 // continue unauthenticated 63 } else if err != nil { 64 log.Fatalf("load github auth token: %v", err) 65 } 66 67 owner, repo, ok := strings.Cut(project, "/") 68 if !ok { 69 log.Fatal("invalid project form: must be owner/repo, like golang/go") 70 } 71 72 fsys = &hub.FS{ 73 Client: &hub.Client{ 74 Token: string(token), 75 }, 76 Owner: owner, 77 Repo: repo, 78 } 79 80 var root awin 81 root.Win, err = acme.New() 82 if err != nil { 83 log.Fatalf("create root acme window: %v", err) 84 } 85 root.Name("/issue/") 86 root.load() 87 root.EventLoop(&root) 88 } 89 90 func wrap(t string, prefix string) string { 91 out := "" 92 t = strings.Replace(t, "\r\n", "\n", -1) 93 max := 70 94 lines := strings.Split(t, "\n") 95 for i, line := range lines { 96 if i > 0 { 97 out += "\n" + prefix 98 } 99 s := line 100 for len(s) > max { 101 i := strings.LastIndex(s[:max], " ") 102 if i < 0 { 103 i = max - 1 104 } 105 i++ 106 out += s[:i] + "\n" + prefix 107 s = s[i:] 108 } 109 out += s 110 } 111 return out 112 } 113 114 func mustConfigPath() string { 115 dir, err := os.UserConfigDir() 116 if err != nil { 117 log.Fatal("find user configuration directory: ", err) 118 } 119 return dir 120 } 121 122 // readNewIssue reads from rd an issue in mail message format. 123 // Only one entry in the header is permitted: "Title:". 124 // The first line after the title is ignored. 125 // Every subsequent line is considered part of the body. 126 func readNewIssue(rd io.Reader) (title, body string, err error) { 127 buf := bufio.NewReader(rd) 128 title, err = buf.ReadString('\n') 129 if err != nil { 130 return title, body, fmt.Errorf("read title: %w", err) 131 } 132 title = strings.TrimPrefix(title, "Title: ") 133 134 if _, err := buf.ReadString('\n'); err != nil { 135 return title, body, fmt.Errorf("read line after title: %w", err) 136 } 137 138 builder := &strings.Builder{} 139 if _, err := io.Copy(builder, buf); err != nil { 140 return title, body, fmt.Errorf("read body: %w", err) 141 } 142 return strings.TrimSpace(title), strings.TrimSpace(builder.String()), nil 143 }