package main import ( "encoding/json" "fmt" termui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" "io/ioutil" "log" // "math" "net/http" //"sort" "os/exec" "runtime" "strconv" "sync" "time" ) type HnStories struct { Stories []HnItem Items []int List []string } type HnItem struct { By string `json:"by,omitempty"` Descendants int `json:"descendants,omitempty"` Id int `json:"id,omitempty"` Kids []int `json:"kids,omitempty"` Score int `json:"score,omitempty"` Added int `json:"time,omitempty"` Title string `json:"title,omitempty"` Type string `json:"type,omitempty"` Url string `json:"url,omitempty"` } type LStories struct { Stories []LobsterItem List []string } type LobsterItem struct { ShortID string `json:"short_id"` ShortIDURL string `json:"short_id_url"` CreatedAt string `json:"created_at"` Title string `json:"title"` Url string `json:"url"` Score int `json:"score"` Upvotes int `json:"upvotes"` Downvotes int `json:"downvotes"` CommentCount int `json:"comment_count"` Description string `json:"description"` CommentsURL string `json:"comments_url"` SubmitterUser struct { Username string `json:"username"` CreatedAt string `json:"created_at"` IsAdmin bool `json:"is_admin"` About string `json:"about"` IsModerator bool `json:"is_moderator"` Karma int `json:"karma"` AvatarURL string `json:"avatar_url"` InvitedByUser string `json:"invited_by_user"` } `json:"submitter_user"` Tags []string `json:"tags"` } const apiUrlHN string = "https://hacker-news.firebaseio.com/v0" const apiUrlL string = "https://lobste.rs/hottest.json" const mpRSS string = "https://blog.acolyer.org/feed/" const hnumstories int = 30 const lnumstories int = 25 const refreshrate time.Duration = 300 func (l *LStories) fetchLStories() { timeout := time.Duration(10 * time.Second) client := &http.Client{ Timeout: timeout, } request, err := http.NewRequest("GET", apiUrlL, nil) if err != nil { log.Println(err) } response, err := client.Do(request) if err != nil { log.Println(err) } storydata, err := ioutil.ReadAll(response.Body) if err != nil { log.Println(err) } json.Unmarshal(storydata, &l.Stories) } func (h *HnStories) fetchHNStories(item string, wg *sync.WaitGroup) { defer wg.Done() var itemUrl string itemUrl = apiUrlHN + "/item/" + item + ".json" timeout := time.Duration(10 * time.Second) client := &http.Client{ Timeout: timeout, } request, err := http.NewRequest("GET", itemUrl, nil) if err != nil { log.Println(err) } response, err := client.Do(request) if err != nil { log.Println(err) } storydata, err := ioutil.ReadAll(response.Body) if err != nil { log.Println(err) } var story HnItem json.Unmarshal(storydata, &story) h.Stories = append(h.Stories, HnItem{ By: story.By, Id: story.Id, Added: story.Added, Title: story.Title, Type: story.Type, Url: story.Url, Kids: story.Kids, }) } func (h *HnStories) httpHNFetchIds(done chan bool) { timeout := time.Duration(5 * time.Second) client := &http.Client{ Timeout: timeout, } request, err := http.NewRequest("GET", apiUrlHN+"/topstories.json", nil) if err != nil { log.Println(err) } response, err := client.Do(request) if err != nil { log.Println(err) } d := json.NewDecoder(response.Body) err = d.Decode(&h.Items) if err != nil { log.Println(err) } done <- true } func (h *HnStories) httpHNDisplayStories(wg *sync.WaitGroup) { wg.Add(1) var counter int = 1 for _, item := range h.Stories[:hnumstories] { itemstring := fmt.Sprintf("%-6s %-6s | %d comments", "["+strconv.Itoa(counter)+".](fg:white,bold)", "["+item.Title+"](fg:green)", len(item.Kids)) h.List = append(h.List, itemstring) counter++ } } func (l *LStories) listLStories() { var counter int = 1 for _, item := range l.Stories[:lnumstories] { itemstring := fmt.Sprintf("%-6s %-6s | %d comments", "["+strconv.Itoa(counter)+".](fg:white,bold)", "["+item.Title+"](fg:green)", item.CommentCount) l.List = append(l.List, itemstring) counter++ } } func (h *HnStories) populateHNStories(gauge *widgets.Gauge, wg *sync.WaitGroup) { done := make(chan bool) go h.httpHNFetchIds(done) <-done var counter int = 1 for _, item := range h.Items[:hnumstories] { wg.Add(1) go h.fetchHNStories(strconv.Itoa(item), wg) fcounter := float64(counter) fnumstories := float64(hnumstories) percentage := (fcounter / fnumstories) * 100 gauge.Percent = int(percentage) termui.Render(gauge) counter++ } wg.Wait() go h.httpHNDisplayStories(wg) wg.Wait() } func (h *HnStories) clearHNStruct() { h = nil } func (l *LStories) clearLStruct() { l = nil } func main() { if err := termui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer termui.Close() x, y := termui.TerminalDimensions() tabpane := widgets.NewTabPane("[Y] Hacker News", "[L] Lobste.rs") tabpane.SetRect(0, 0, x, 1) tabpane.Border = false g0 := widgets.NewGauge() g0.Title = "Fetching Stories.." g0.SetRect(0, 3, x, 6) g0.BarColor = termui.ColorYellow g0.LabelStyle = termui.NewStyle(termui.ColorBlue) g0.BorderStyle.Fg = termui.ColorWhite g0.Percent = 1 termui.Render(g0) var h HnStories var l LStories var wg sync.WaitGroup l.fetchLStories() l.listLStories() h.populateHNStories(g0, &wg) termui.Clear() hlist := widgets.NewList() hlist.Border = false x, y = termui.TerminalDimensions() hlist.SetRect(0, 1, x, y) hlist.TextStyle = termui.NewStyle(termui.ColorYellow) hlist.WrapText = true hlist.Rows = h.List llist := widgets.NewList() llist.Border = false x, y = termui.TerminalDimensions() llist.SetRect(0, 1, x, y) llist.TextStyle = termui.NewStyle(termui.ColorYellow) llist.WrapText = true llist.Rows = l.List renderTab := func() { switch tabpane.ActiveTabIndex { case 0: termui.Render(hlist) case 1: termui.Render(llist) } } termui.Render(tabpane, hlist) ticker := time.NewTicker(refreshrate * time.Second).C previousKey := "" uiEvents := termui.PollEvents() for { select { case e := <-uiEvents: switch e.ID { case "h", "": tabpane.FocusLeft() termui.Clear() termui.Render(tabpane) renderTab() case "l", "": tabpane.FocusRight() termui.Clear() termui.Render(tabpane) renderTab() case "q", "", "": return case "r": termui.Clear() if tabpane.ActiveTabIndex == 0 { h.clearHNStruct() g0.Title = "Refreshing HN Stories.." g0.Percent = 1 var wg2 sync.WaitGroup h.populateHNStories(g0, &wg2) hlist := widgets.NewList() hlist.Border = false x, y = termui.TerminalDimensions() hlist.SetRect(0, 1, x, y) hlist.TextStyle = termui.NewStyle(termui.ColorYellow) hlist.WrapText = true hlist.Rows = h.List } else if tabpane.ActiveTabIndex == 1 { l.clearLStruct() g0.Title = "Refreshing Lobster Stories.." g0.Percent = 100 termui.Render(g0) l.fetchLStories() l.listLStories() llist := widgets.NewList() llist.Border = false x, y = termui.TerminalDimensions() llist.SetRect(0, 1, x, y) llist.TextStyle = termui.NewStyle(termui.ColorYellow) llist.WrapText = true llist.Rows = l.List } case "j", "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollDown() } if tabpane.ActiveTabIndex == 1 { llist.ScrollDown() } case "k", "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollUp() } if tabpane.ActiveTabIndex == 1 { llist.ScrollUp() } case "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollHalfPageDown() } if tabpane.ActiveTabIndex == 1 { llist.ScrollHalfPageDown() } case "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollHalfPageUp() } if tabpane.ActiveTabIndex == 1 { llist.ScrollHalfPageUp() } case "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollPageDown() } if tabpane.ActiveTabIndex == 1 { llist.ScrollPageDown() } case "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollPageUp() } if tabpane.ActiveTabIndex == 1 { llist.ScrollPageUp() } case "g": if tabpane.ActiveTabIndex == 0 { hlist.ScrollDown() } if tabpane.ActiveTabIndex == 1 { llist.ScrollDown() } if previousKey == "g" { if tabpane.ActiveTabIndex == 0 { hlist.ScrollTop() } else if tabpane.ActiveTabIndex == 1 { llist.ScrollTop() } } case "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollTop() } if tabpane.ActiveTabIndex == 1 { llist.ScrollTop() } case "G", "": if tabpane.ActiveTabIndex == 0 { hlist.ScrollBottom() } if tabpane.ActiveTabIndex == 1 { llist.ScrollBottom() } case "": if tabpane.ActiveTabIndex == 0 { url := h.Stories[hlist.SelectedRow].Url openbrowser(url) } else if tabpane.ActiveTabIndex == 1 { url := l.Stories[llist.SelectedRow].Url openbrowser(url) } case "": payload := e.Payload.(termui.Resize) hlist.SetRect(0, 1, payload.Width, payload.Height) termui.Clear() termui.Render(tabpane, hlist, llist) } if previousKey == "g" { previousKey = "" } else { previousKey = e.ID } if tabpane.ActiveTabIndex == 0 { termui.Render(tabpane, hlist) } else if tabpane.ActiveTabIndex == 1 { termui.Render(tabpane, llist) } case <-ticker: termui.Clear() if tabpane.ActiveTabIndex == 0 { h.clearHNStruct() g0.Title = "Refreshing HN Stories.." g0.Percent = 1 var wg2 sync.WaitGroup h.populateHNStories(g0, &wg2) hlist := widgets.NewList() hlist.Border = false x, y = termui.TerminalDimensions() hlist.SetRect(0, 1, x, y) hlist.TextStyle = termui.NewStyle(termui.ColorYellow) hlist.WrapText = true hlist.Rows = h.List termui.Render(tabpane, hlist) } else if tabpane.ActiveTabIndex == 1 { l.clearLStruct() g0.Title = "Refreshing Lobster Stories.." g0.Percent = 100 termui.Render(g0) l.fetchLStories() l.listLStories() llist := widgets.NewList() llist.Border = false x, y = termui.TerminalDimensions() llist.SetRect(0, 1, x, y) llist.TextStyle = termui.NewStyle(termui.ColorYellow) llist.WrapText = true llist.Rows = l.List termui.Render(tabpane, llist) } } } } func openbrowser(url string) { var err error switch runtime.GOOS { case "linux", "freebsd": err = exec.Command("xdg-open", url).Start() case "windows": err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() default: err = fmt.Errorf("unsupported platform") } if err != nil { log.Fatal(err) } }