mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
37
modules/bamboohr/calendar.go
Normal file
37
modules/bamboohr/calendar.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package bamboohr
|
||||
|
||||
type Calendar struct {
|
||||
Items []Item `xml:"item"`
|
||||
}
|
||||
|
||||
/* -------------------- Public Functions -------------------- */
|
||||
|
||||
func (calendar *Calendar) Holidays() []Item {
|
||||
return calendar.filteredItems("holiday")
|
||||
}
|
||||
|
||||
func (calendar *Calendar) ItemsByType(itemType string) []Item {
|
||||
if itemType == "timeOff" {
|
||||
return calendar.TimeOffs()
|
||||
}
|
||||
|
||||
return calendar.Holidays()
|
||||
}
|
||||
|
||||
func (calendar *Calendar) TimeOffs() []Item {
|
||||
return calendar.filteredItems("timeOff")
|
||||
}
|
||||
|
||||
/* -------------------- Private Functions -------------------- */
|
||||
|
||||
func (calendar *Calendar) filteredItems(itemType string) []Item {
|
||||
items := []Item{}
|
||||
|
||||
for _, item := range calendar.Items {
|
||||
if item.Type == itemType {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
61
modules/bamboohr/client.go
Normal file
61
modules/bamboohr/client.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package bamboohr
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A Client represents the data required to connect to the BambooHR API
|
||||
type Client struct {
|
||||
apiBase string
|
||||
apiKey string
|
||||
subdomain string
|
||||
}
|
||||
|
||||
// NewClient creates and returns a new BambooHR client
|
||||
func NewClient(url string, apiKey string, subdomain string) *Client {
|
||||
client := Client{
|
||||
apiBase: url,
|
||||
apiKey: apiKey,
|
||||
subdomain: subdomain,
|
||||
}
|
||||
|
||||
return &client
|
||||
}
|
||||
|
||||
/* -------------------- Public Functions -------------------- */
|
||||
|
||||
// Away returns a string representation of the people who are out of the office during the defined period
|
||||
func (client *Client) Away(itemType, startDate, endDate string) []Item {
|
||||
calendar, err := client.away(startDate, endDate)
|
||||
if err != nil {
|
||||
return []Item{}
|
||||
}
|
||||
|
||||
items := calendar.ItemsByType(itemType)
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/* -------------------- Private Functions -------------------- */
|
||||
|
||||
// away is the private interface for retrieving structural data about who will be out of the office
|
||||
// This method does the actual communication with BambooHR and returns the raw Go
|
||||
// data structures used by the public interface
|
||||
func (client *Client) away(startDate, endDate string) (cal Calendar, err error) {
|
||||
apiURL := fmt.Sprintf(
|
||||
"%s/%s/v1/time_off/whos_out?start=%s&end=%s",
|
||||
client.apiBase,
|
||||
client.subdomain,
|
||||
startDate,
|
||||
endDate,
|
||||
)
|
||||
|
||||
data, err := Request(client.apiKey, apiURL)
|
||||
if err != nil {
|
||||
return cal, err
|
||||
}
|
||||
err = xml.Unmarshal(data, &cal)
|
||||
|
||||
return
|
||||
}
|
||||
10
modules/bamboohr/employee.go
Normal file
10
modules/bamboohr/employee.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package bamboohr
|
||||
|
||||
/*
|
||||
* Note: this currently implements the minimum number of fields to fulfill the Away functionality.
|
||||
* Undoubtedly there are more fields than this to an employee
|
||||
*/
|
||||
type Employee struct {
|
||||
ID int `xml:"id,attr"`
|
||||
Name string `xml:",chardata"`
|
||||
}
|
||||
42
modules/bamboohr/item.go
Normal file
42
modules/bamboohr/item.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package bamboohr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Employee Employee `xml:"employee"`
|
||||
End string `xml:"end"`
|
||||
Holiday string `xml:"holiday"`
|
||||
Start string `xml:"start"`
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
|
||||
func (item *Item) String() string {
|
||||
return fmt.Sprintf("Item: %s, %s, %s, %s", item.Type, item.Employee.Name, item.Start, item.End)
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (item *Item) IsOneDay() bool {
|
||||
return item.Start == item.End
|
||||
}
|
||||
|
||||
func (item *Item) Name() string {
|
||||
if (item.Employee != Employee{}) {
|
||||
return item.Employee.Name
|
||||
}
|
||||
|
||||
return item.Holiday
|
||||
}
|
||||
|
||||
func (item *Item) PrettyStart() string {
|
||||
return wtf.PrettyDate(item.Start)
|
||||
}
|
||||
|
||||
func (item *Item) PrettyEnd() string {
|
||||
return wtf.PrettyDate(item.End)
|
||||
}
|
||||
39
modules/bamboohr/request.go
Normal file
39
modules/bamboohr/request.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package bamboohr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Request(apiKey string, apiURL string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(apiKey, "x")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ParseBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func ParseBody(resp *http.Response) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
_, err := buffer.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
73
modules/bamboohr/widget.go
Normal file
73
modules/bamboohr/widget.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package bamboohr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "BambooHR", "bamboohr", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
apiKey := wtf.Config.UString(
|
||||
"wtf.mods.bamboohr.apiKey",
|
||||
os.Getenv("WTF_BAMBOO_HR_TOKEN"),
|
||||
)
|
||||
|
||||
subdomain := wtf.Config.UString(
|
||||
"wtf.mods.bamboohr.subdomain",
|
||||
os.Getenv("WTF_BAMBOO_HR_SUBDOMAIN"),
|
||||
)
|
||||
|
||||
client := NewClient("https://api.bamboohr.com/api/gateway.php", apiKey, subdomain)
|
||||
todayItems := client.Away(
|
||||
"timeOff",
|
||||
wtf.Now().Format(wtf.DateFormat),
|
||||
wtf.Now().Format(wtf.DateFormat),
|
||||
)
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(widget.Name))
|
||||
|
||||
widget.View.SetText(widget.contentFrom(todayItems))
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(items []Item) string {
|
||||
if len(items) == 0 {
|
||||
return fmt.Sprintf("\n\n\n\n\n\n\n\n%s", wtf.CenterText("[grey]no one[white]", 50))
|
||||
}
|
||||
|
||||
str := ""
|
||||
for _, item := range items {
|
||||
str = str + widget.format(item)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) format(item Item) string {
|
||||
var str string
|
||||
|
||||
if item.IsOneDay() {
|
||||
str = fmt.Sprintf(" [green]%s[white]\n %s\n\n", item.Name(), item.PrettyEnd())
|
||||
} else {
|
||||
str = fmt.Sprintf(" [green]%s[white]\n %s - %s\n\n", item.Name(), item.PrettyStart(), item.PrettyEnd())
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
10
modules/circleci/build.go
Normal file
10
modules/circleci/build.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package circleci
|
||||
|
||||
type Build struct {
|
||||
AuthorEmail string `json:"author_email"`
|
||||
AuthorName string `json:"author_name"`
|
||||
Branch string `json:"branch"`
|
||||
BuildNum int `json:"build_num"`
|
||||
Reponame string `json:"reponame"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
85
modules/circleci/client.go
Normal file
85
modules/circleci/client.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package circleci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const APIEnvKey = "WTF_CIRCLE_API_KEY"
|
||||
|
||||
func BuildsFor() ([]*Build, error) {
|
||||
builds := []*Build{}
|
||||
|
||||
resp, err := circleRequest("recent-builds")
|
||||
if err != nil {
|
||||
return builds, err
|
||||
}
|
||||
|
||||
parseJson(&builds, resp.Body)
|
||||
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
circleAPIURL = &url.URL{Scheme: "https", Host: "circleci.com", Path: "/api/v1/"}
|
||||
)
|
||||
|
||||
func circleRequest(path string) (*http.Response, error) {
|
||||
params := url.Values{}
|
||||
params.Add("circle-token", apiKey())
|
||||
|
||||
url := circleAPIURL.ResolveReference(&url.URL{Path: path, RawQuery: params.Encode()})
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.circleci.apiKey",
|
||||
os.Getenv(APIEnvKey),
|
||||
)
|
||||
}
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
modules/circleci/widget.go
Normal file
80
modules/circleci/widget.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package circleci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "CircleCI", "circleci", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
builds, err := BuildsFor()
|
||||
|
||||
widget.View.SetTitle(fmt.Sprintf("%s - Builds", widget.Name))
|
||||
|
||||
var content string
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
content = err.Error()
|
||||
} else {
|
||||
widget.View.SetWrap(false)
|
||||
content = widget.contentFrom(builds)
|
||||
}
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(builds []*Build) string {
|
||||
var str string
|
||||
for idx, build := range builds {
|
||||
if idx > 10 {
|
||||
return str
|
||||
}
|
||||
|
||||
str = str + fmt.Sprintf(
|
||||
"[%s] %s-%d (%s) [white]%s\n",
|
||||
buildColor(build),
|
||||
build.Reponame,
|
||||
build.BuildNum,
|
||||
build.Branch,
|
||||
build.AuthorName,
|
||||
)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func buildColor(build *Build) string {
|
||||
switch build.Status {
|
||||
case "failed":
|
||||
return "red"
|
||||
case "running":
|
||||
return "yellow"
|
||||
case "success":
|
||||
return "green"
|
||||
case "fixed":
|
||||
return "green"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
35
modules/clocks/clock.go
Normal file
35
modules/clocks/clock.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package clocks
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Clock struct {
|
||||
Label string
|
||||
Location *time.Location
|
||||
}
|
||||
|
||||
func NewClock(label string, timeLoc *time.Location) Clock {
|
||||
clock := Clock{
|
||||
Label: label,
|
||||
Location: timeLoc,
|
||||
}
|
||||
|
||||
return clock
|
||||
}
|
||||
|
||||
func (clock *Clock) Date(dateFormat string) string {
|
||||
return clock.LocalTime().Format(dateFormat)
|
||||
}
|
||||
|
||||
func (clock *Clock) LocalTime() time.Time {
|
||||
return clock.ToLocal(time.Now())
|
||||
}
|
||||
|
||||
func (clock *Clock) ToLocal(t time.Time) time.Time {
|
||||
return t.In(clock.Location)
|
||||
}
|
||||
|
||||
func (clock *Clock) Time(timeFormat string) string {
|
||||
return clock.LocalTime().Format(timeFormat)
|
||||
}
|
||||
41
modules/clocks/clock_collection.go
Normal file
41
modules/clocks/clock_collection.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package clocks
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type ClockCollection struct {
|
||||
Clocks []Clock
|
||||
}
|
||||
|
||||
func (clocks *ClockCollection) Sorted() []Clock {
|
||||
if "chronological" == wtf.Config.UString("wtf.mods.clocks.sort", "alphabetical") {
|
||||
clocks.SortedChronologically()
|
||||
} else {
|
||||
clocks.SortedAlphabetically()
|
||||
}
|
||||
|
||||
return clocks.Clocks
|
||||
}
|
||||
|
||||
func (clocks *ClockCollection) SortedAlphabetically() {
|
||||
sort.Slice(clocks.Clocks, func(i, j int) bool {
|
||||
clock := clocks.Clocks[i]
|
||||
other := clocks.Clocks[j]
|
||||
|
||||
return clock.Label < other.Label
|
||||
})
|
||||
}
|
||||
|
||||
func (clocks *ClockCollection) SortedChronologically() {
|
||||
now := time.Now()
|
||||
sort.Slice(clocks.Clocks, func(i, j int) bool {
|
||||
clock := clocks.Clocks[i]
|
||||
other := clocks.Clocks[j]
|
||||
|
||||
return clock.ToLocal(now).String() < other.ToLocal(now).String()
|
||||
})
|
||||
}
|
||||
27
modules/clocks/display.go
Normal file
27
modules/clocks/display.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package clocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display(clocks []Clock, dateFormat string, timeFormat string) {
|
||||
if len(clocks) == 0 {
|
||||
widget.View.SetText(fmt.Sprintf("\n%s", " no timezone data available"))
|
||||
return
|
||||
}
|
||||
|
||||
str := ""
|
||||
for idx, clock := range clocks {
|
||||
str = str + fmt.Sprintf(
|
||||
" [%s]%-12s %-10s %7s[white]\n",
|
||||
wtf.RowColor("clocks", idx),
|
||||
clock.Label,
|
||||
clock.Time(timeFormat),
|
||||
clock.Date(dateFormat),
|
||||
)
|
||||
}
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
57
modules/clocks/widget.go
Normal file
57
modules/clocks/widget.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package clocks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
clockColl ClockCollection
|
||||
dateFormat string
|
||||
timeFormat string
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "World Clocks", "clocks", false),
|
||||
}
|
||||
|
||||
widget.clockColl = widget.buildClockCollection(wtf.Config.UMap("wtf.mods.clocks.locations"))
|
||||
|
||||
widget.dateFormat = wtf.Config.UString("wtf.mods.clocks.dateFormat", wtf.SimpleDateFormat)
|
||||
widget.timeFormat = wtf.Config.UString("wtf.mods.clocks.timeFormat", wtf.SimpleTimeFormat)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.display(widget.clockColl.Sorted(), widget.dateFormat, widget.timeFormat)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) buildClockCollection(locData map[string]interface{}) ClockCollection {
|
||||
clockColl := ClockCollection{}
|
||||
|
||||
for label, locStr := range locData {
|
||||
timeLoc, err := time.LoadLocation(widget.sanitizeLocation(locStr.(string)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
clockColl.Clocks = append(clockColl.Clocks, NewClock(label, timeLoc))
|
||||
}
|
||||
|
||||
return clockColl
|
||||
}
|
||||
|
||||
func (widget *Widget) sanitizeLocation(locStr string) string {
|
||||
return strings.Replace(locStr, " ", "_", -1)
|
||||
}
|
||||
55
modules/cmdrunner/widget.go
Normal file
55
modules/cmdrunner/widget.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cmdrunner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
args []string
|
||||
cmd string
|
||||
result string
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "CmdRunner", "cmdrunner", false),
|
||||
|
||||
args: wtf.ToStrs(wtf.Config.UList("wtf.mods.cmdrunner.args")),
|
||||
cmd: wtf.Config.UString("wtf.mods.cmdrunner.cmd"),
|
||||
}
|
||||
|
||||
widget.View.SetWrap(true)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.execute()
|
||||
|
||||
title := tview.TranslateANSI(wtf.Config.UString("wtf.mods.cmdrunner.title", widget.String()))
|
||||
widget.View.SetTitle(title)
|
||||
|
||||
widget.View.SetText(widget.result)
|
||||
}
|
||||
|
||||
func (widget *Widget) String() string {
|
||||
args := strings.Join(widget.args, " ")
|
||||
|
||||
if args != "" {
|
||||
return fmt.Sprintf(" %s %s ", widget.cmd, args)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %s ", widget.cmd)
|
||||
}
|
||||
|
||||
func (widget *Widget) execute() {
|
||||
cmd := exec.Command(widget.cmd, widget.args...)
|
||||
widget.result = tview.TranslateANSI(wtf.ExecuteCommand(cmd))
|
||||
}
|
||||
49
modules/cryptoexchanges/bittrex/bittrex.go
Normal file
49
modules/cryptoexchanges/bittrex/bittrex.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package bittrex
|
||||
|
||||
type summaryList struct {
|
||||
items []*bCurrency
|
||||
}
|
||||
|
||||
// Base Currency
|
||||
type bCurrency struct {
|
||||
name string
|
||||
displayName string
|
||||
markets []*mCurrency
|
||||
}
|
||||
|
||||
// Market Currency
|
||||
type mCurrency struct {
|
||||
name string
|
||||
summaryInfo
|
||||
}
|
||||
|
||||
type summaryInfo struct {
|
||||
Low string
|
||||
High string
|
||||
Volume string
|
||||
Last string
|
||||
OpenSellOrders string
|
||||
OpenBuyOrders string
|
||||
}
|
||||
|
||||
type summaryResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
MarketName string `json:"MarketName"`
|
||||
High float64 `json:"High"`
|
||||
Low float64 `json:"Low"`
|
||||
Last float64 `json:"Last"`
|
||||
Volume float64 `json:"Volume"`
|
||||
OpenSellOrders int `json:"OpenSellOrders"`
|
||||
OpenBuyOrders int `json:"OpenBuyOrders"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func (list *summaryList) addSummaryItem(name, displayName string, marketList []*mCurrency) {
|
||||
list.items = append(list.items, &bCurrency{
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
markets: marketList,
|
||||
})
|
||||
}
|
||||
64
modules/cryptoexchanges/bittrex/display.go
Normal file
64
modules/cryptoexchanges/bittrex/display.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if ok == false {
|
||||
widget.View.SetText(errorText)
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetText(summaryText(&widget.summaryList, &widget.TextColors))
|
||||
}
|
||||
|
||||
func summaryText(list *summaryList, colors *TextColors) string {
|
||||
str := ""
|
||||
|
||||
for _, baseCurrency := range list.items {
|
||||
str += fmt.Sprintf(" [%s]%s[%s] (%s)\n\n", colors.base.displayName, baseCurrency.displayName, colors.base.name, baseCurrency.name)
|
||||
|
||||
resultTemplate := template.New("bittrex")
|
||||
|
||||
for _, marketCurrency := range baseCurrency.markets {
|
||||
writer := new(bytes.Buffer)
|
||||
|
||||
strTemplate, _ := resultTemplate.Parse(
|
||||
" [{{.nameColor}}]{{.mName}}\n" +
|
||||
formatableText("High", "High") +
|
||||
formatableText("Low", "Low") +
|
||||
formatableText("Last", "Last") +
|
||||
formatableText("Volume", "Volume") +
|
||||
"\n" +
|
||||
formatableText("Open Buy", "OpenBuyOrders") +
|
||||
formatableText("Open Sell", "OpenSellOrders"),
|
||||
)
|
||||
|
||||
strTemplate.Execute(writer, map[string]string{
|
||||
"nameColor": colors.market.name,
|
||||
"fieldColor": colors.market.field,
|
||||
"valueColor": colors.market.value,
|
||||
"mName": marketCurrency.name,
|
||||
"High": marketCurrency.High,
|
||||
"Low": marketCurrency.Low,
|
||||
"Last": marketCurrency.Last,
|
||||
"Volume": marketCurrency.Volume,
|
||||
"OpenBuyOrders": marketCurrency.OpenBuyOrders,
|
||||
"OpenSellOrders": marketCurrency.OpenSellOrders,
|
||||
})
|
||||
|
||||
str += writer.String() + "\n"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return str
|
||||
|
||||
}
|
||||
|
||||
func formatableText(key, value string) string {
|
||||
return fmt.Sprintf("[{{.fieldColor}}]%12s: [{{.valueColor}}]{{.%s}}\n", key, value)
|
||||
}
|
||||
169
modules/cryptoexchanges/bittrex/widget.go
Normal file
169
modules/cryptoexchanges/bittrex/widget.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type TextColors struct {
|
||||
base struct {
|
||||
name string
|
||||
displayName string
|
||||
}
|
||||
market struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
}
|
||||
}
|
||||
|
||||
var ok = true
|
||||
var errorText = ""
|
||||
|
||||
var baseURL = "https://bittrex.com/api/v1.1/public/getmarketsummary"
|
||||
|
||||
// Widget define wtf widget to register widget later
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
summaryList
|
||||
TextColors
|
||||
}
|
||||
|
||||
// NewWidget Make new instance of widget
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Bittrex", "bittrex", false),
|
||||
summaryList: summaryList{},
|
||||
}
|
||||
|
||||
ok = true
|
||||
errorText = ""
|
||||
|
||||
widget.config()
|
||||
widget.setSummaryList()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) config() {
|
||||
widget.TextColors.base.name = wtf.Config.UString("wtf.mods.bittrex.colors.base.name", "red")
|
||||
widget.TextColors.base.displayName = wtf.Config.UString("wtf.mods.bittrex.colors.base.displayName", "grey")
|
||||
widget.TextColors.market.name = wtf.Config.UString("wtf.mods.bittrex.colors.market.name", "red")
|
||||
widget.TextColors.market.field = wtf.Config.UString("wtf.mods.bittrex.colors.market.field", "coral")
|
||||
widget.TextColors.market.value = wtf.Config.UString("wtf.mods.bittrex.colors.market.value", "white")
|
||||
}
|
||||
|
||||
func (widget *Widget) setSummaryList() {
|
||||
sCurrencies, _ := wtf.Config.Map("wtf.mods.bittrex.summary")
|
||||
for baseCurrencyName := range sCurrencies {
|
||||
displayName, _ := wtf.Config.String("wtf.mods.bittrex.summary." + baseCurrencyName + ".displayName")
|
||||
mCurrencyList := makeSummaryMarketList(baseCurrencyName)
|
||||
widget.summaryList.addSummaryItem(baseCurrencyName, displayName, mCurrencyList)
|
||||
}
|
||||
}
|
||||
|
||||
func makeSummaryMarketList(currencyName string) []*mCurrency {
|
||||
mCurrencyList := []*mCurrency{}
|
||||
|
||||
configMarketList, _ := wtf.Config.List("wtf.mods.bittrex.summary." + currencyName + ".market")
|
||||
for _, mCurrencyName := range configMarketList {
|
||||
mCurrencyList = append(mCurrencyList, makeMarketCurrency(mCurrencyName.(string)))
|
||||
}
|
||||
|
||||
return mCurrencyList
|
||||
}
|
||||
|
||||
func makeMarketCurrency(name string) *mCurrency {
|
||||
return &mCurrency{
|
||||
name: name,
|
||||
summaryInfo: summaryInfo{
|
||||
High: "",
|
||||
Low: "",
|
||||
Volume: "",
|
||||
Last: "",
|
||||
OpenBuyOrders: "",
|
||||
OpenSellOrders: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh & update after interval time
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.updateSummary()
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) updateSummary() {
|
||||
// In case if anything bad happened!
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(5 * time.Second),
|
||||
}
|
||||
|
||||
for _, baseCurrency := range widget.summaryList.items {
|
||||
for _, mCurrency := range baseCurrency.markets {
|
||||
request := makeRequest(baseCurrency.name, mCurrency.name)
|
||||
response, err := client.Do(request)
|
||||
|
||||
if err != nil {
|
||||
ok = false
|
||||
errorText = "Please Check Your Internet Connection!"
|
||||
break
|
||||
} else {
|
||||
ok = true
|
||||
errorText = ""
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
errorText = response.Status
|
||||
ok = false
|
||||
break
|
||||
} else {
|
||||
ok = true
|
||||
errorText = ""
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
jsonResponse := summaryResponse{}
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
decoder.Decode(&jsonResponse)
|
||||
|
||||
if !jsonResponse.Success {
|
||||
ok = false
|
||||
errorText = fmt.Sprintf("%s-%s: %s", baseCurrency.name, mCurrency.name, jsonResponse.Message)
|
||||
break
|
||||
}
|
||||
ok = true
|
||||
errorText = ""
|
||||
|
||||
mCurrency.Last = fmt.Sprintf("%f", jsonResponse.Result[0].Last)
|
||||
mCurrency.High = fmt.Sprintf("%f", jsonResponse.Result[0].High)
|
||||
mCurrency.Low = fmt.Sprintf("%f", jsonResponse.Result[0].Low)
|
||||
mCurrency.Volume = fmt.Sprintf("%f", jsonResponse.Result[0].Volume)
|
||||
mCurrency.OpenBuyOrders = fmt.Sprintf("%d", jsonResponse.Result[0].OpenBuyOrders)
|
||||
mCurrency.OpenSellOrders = fmt.Sprintf("%d", jsonResponse.Result[0].OpenSellOrders)
|
||||
}
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func makeRequest(baseName, marketName string) *http.Request {
|
||||
url := fmt.Sprintf("%s?market=%s-%s", baseURL, baseName, marketName)
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
|
||||
return request
|
||||
}
|
||||
119
modules/cryptoexchanges/blockfolio/widget.go
Normal file
119
modules/cryptoexchanges/blockfolio/widget.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package blockfolio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
device_token string
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Blockfolio", "blockfolio", false),
|
||||
device_token: wtf.Config.UString("wtf.mods.blockfolio.device_token"),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.View.SetTitle(" Blockfolio ")
|
||||
|
||||
positions, err := Fetch(widget.device_token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetText(contentFrom(positions))
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
func contentFrom(positions *AllPositionsResponse) string {
|
||||
res := ""
|
||||
colorName := wtf.Config.UString("wtf.mods.blockfolio.colors.name")
|
||||
colorGrows := wtf.Config.UString("wtf.mods.blockfolio.colors.grows")
|
||||
colorDrop := wtf.Config.UString("wtf.mods.blockfolio.colors.drop")
|
||||
displayHoldings := wtf.Config.UBool("wtf.mods.blockfolio.displayHoldings")
|
||||
var totalFiat float32
|
||||
totalFiat = 0.0
|
||||
for i := 0; i < len(positions.PositionList); i++ {
|
||||
colorForChange := colorGrows
|
||||
if positions.PositionList[i].TwentyFourHourPercentChangeFiat <= 0 {
|
||||
colorForChange = colorDrop
|
||||
}
|
||||
totalFiat += positions.PositionList[i].HoldingValueFiat
|
||||
if displayHoldings {
|
||||
res = res + fmt.Sprintf("[%s]%-6s - %5.2f ([%s]%.3fk [%s]%.2f%s)\n", colorName, positions.PositionList[i].Coin, positions.PositionList[i].Quantity, colorForChange, positions.PositionList[i].HoldingValueFiat/1000, colorForChange, positions.PositionList[i].TwentyFourHourPercentChangeFiat, "%")
|
||||
} else {
|
||||
res = res + fmt.Sprintf("[%s]%-6s - %5.2f ([%s]%.2f%s)\n", colorName, positions.PositionList[i].Coin, positions.PositionList[i].Quantity, colorForChange, positions.PositionList[i].TwentyFourHourPercentChangeFiat, "%")
|
||||
}
|
||||
}
|
||||
if displayHoldings {
|
||||
res = res + fmt.Sprintf("\n[%s]Total value: $%.3fk", "green", totalFiat/1000)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
//always the same
|
||||
const magic = "edtopjhgn2345piuty89whqejfiobh89-2q453"
|
||||
|
||||
type Position struct {
|
||||
Coin string `json:"coin"`
|
||||
LastPriceFiat float32 `json:"lastPriceFiat"`
|
||||
TwentyFourHourPercentChangeFiat float32 `json:"twentyFourHourPercentChangeFiat"`
|
||||
Quantity float32 `json:"quantity"`
|
||||
HoldingValueFiat float32 `json:"holdingValueFiat"`
|
||||
}
|
||||
|
||||
type AllPositionsResponse struct {
|
||||
PositionList []Position `json:"positionList"`
|
||||
}
|
||||
|
||||
func MakeApiRequest(token string, method string) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
url := "https://api-v0.blockfolio.com/rest/" + method + "/" + token + "?use_alias=true&fiat_currency=USD"
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("magic", magic)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
func GetAllPositions(token string) (*AllPositionsResponse, error) {
|
||||
jsn, _ := MakeApiRequest(token, "get_all_positions")
|
||||
var parsed AllPositionsResponse
|
||||
|
||||
err := json.Unmarshal(jsn, &parsed)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse json %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, err
|
||||
}
|
||||
|
||||
func Fetch(token string) (*AllPositionsResponse, error) {
|
||||
return GetAllPositions(token)
|
||||
}
|
||||
28
modules/cryptoexchanges/cryptolive/price/price.go
Normal file
28
modules/cryptoexchanges/cryptolive/price/price.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package price
|
||||
|
||||
type list struct {
|
||||
items []*fromCurrency
|
||||
}
|
||||
|
||||
type fromCurrency struct {
|
||||
name string
|
||||
displayName string
|
||||
to []*toCurrency
|
||||
}
|
||||
|
||||
type toCurrency struct {
|
||||
name string
|
||||
price float32
|
||||
}
|
||||
|
||||
type cResponse map[string]float32
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (l *list) addItem(name string, displayName string, to []*toCurrency) {
|
||||
l.items = append(l.items, &fromCurrency{
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
to: to,
|
||||
})
|
||||
}
|
||||
154
modules/cryptoexchanges/cryptolive/price/widget.go
Normal file
154
modules/cryptoexchanges/cryptolive/price/widget.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package price
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
var baseURL = "https://min-api.cryptocompare.com/data/price"
|
||||
var ok = true
|
||||
|
||||
// Widget define wtf widget to register widget later
|
||||
type Widget struct {
|
||||
*list
|
||||
|
||||
Result string
|
||||
|
||||
RefreshInterval int
|
||||
}
|
||||
|
||||
// NewWidget Make new instance of widget
|
||||
func NewWidget() *Widget {
|
||||
widget := Widget{}
|
||||
|
||||
widget.setList()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) setList() {
|
||||
currenciesMap, _ := wtf.Config.Map("wtf.mods.cryptolive.currencies")
|
||||
|
||||
widget.list = &list{}
|
||||
|
||||
for currency := range currenciesMap {
|
||||
displayName, _ := wtf.Config.String("wtf.mods.cryptolive.currencies." + currency + ".displayName")
|
||||
toList := getToList(currency)
|
||||
widget.list.addItem(currency, displayName, toList)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh & update after interval time
|
||||
func (widget *Widget) Refresh(wg *sync.WaitGroup) {
|
||||
if len(widget.list.items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
widget.updateCurrencies()
|
||||
|
||||
if !ok {
|
||||
widget.Result = fmt.Sprint("Please check your internet connection!")
|
||||
return
|
||||
}
|
||||
widget.display()
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
str := ""
|
||||
var (
|
||||
fromNameColor = wtf.Config.UString("wtf.mods.cryptolive.colors.from.name", "coral")
|
||||
fromDisplayNameColor = wtf.Config.UString("wtf.mods.cryptolive.colors.from.displayName", "grey")
|
||||
toNameColor = wtf.Config.UString("wtf.mods.cryptolive.colors.to.name", "white")
|
||||
toPriceColor = wtf.Config.UString("wtf.mods.cryptolive.colors.to.price", "green")
|
||||
)
|
||||
for _, item := range widget.list.items {
|
||||
str += fmt.Sprintf(" [%s]%s[%s] (%s)\n", fromNameColor, item.displayName, fromDisplayNameColor, item.name)
|
||||
for _, toItem := range item.to {
|
||||
str += fmt.Sprintf("\t[%s]%s: [%s]%f\n", toNameColor, toItem.name, toPriceColor, toItem.price)
|
||||
}
|
||||
str += "\n"
|
||||
}
|
||||
|
||||
widget.Result = fmt.Sprintf("\n%s", str)
|
||||
}
|
||||
|
||||
func getToList(fromName string) []*toCurrency {
|
||||
toNames, _ := wtf.Config.List("wtf.mods.cryptolive.currencies." + fromName + ".to")
|
||||
|
||||
var toList []*toCurrency
|
||||
|
||||
for _, to := range toNames {
|
||||
toList = append(toList, &toCurrency{
|
||||
name: to.(string),
|
||||
price: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return toList
|
||||
}
|
||||
|
||||
func (widget *Widget) updateCurrencies() {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
for _, fromCurrency := range widget.list.items {
|
||||
|
||||
var (
|
||||
client http.Client
|
||||
jsonResponse cResponse
|
||||
)
|
||||
|
||||
client = http.Client{
|
||||
Timeout: time.Duration(5 * time.Second),
|
||||
}
|
||||
|
||||
request := makeRequest(fromCurrency)
|
||||
response, err := client.Do(request)
|
||||
|
||||
if err != nil {
|
||||
ok = false
|
||||
} else {
|
||||
ok = true
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
_ = json.NewDecoder(response.Body).Decode(&jsonResponse)
|
||||
|
||||
setPrices(&jsonResponse, fromCurrency)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func makeRequest(currency *fromCurrency) *http.Request {
|
||||
fsym := currency.name
|
||||
tsyms := ""
|
||||
for _, to := range currency.to {
|
||||
tsyms += fmt.Sprintf("%s,", to.name)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s?fsym=%s&tsyms=%s", baseURL, fsym, tsyms)
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
func setPrices(response *cResponse, currencry *fromCurrency) {
|
||||
for idx, toCurrency := range currencry.to {
|
||||
currencry.to[idx].price = (*response)[toCurrency.name]
|
||||
}
|
||||
}
|
||||
55
modules/cryptoexchanges/cryptolive/toplist/display.go
Normal file
55
modules/cryptoexchanges/cryptolive/toplist/display.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package toplist
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (widget *Widget) display() {
|
||||
str := ""
|
||||
for _, fromCurrency := range widget.list.items {
|
||||
str += fmt.Sprintf(
|
||||
"[%s]%s [%s](%s)\n",
|
||||
widget.colors.from.displayName,
|
||||
fromCurrency.displayName,
|
||||
widget.colors.from.name,
|
||||
fromCurrency.name,
|
||||
)
|
||||
str += makeToListText(fromCurrency.to, widget.colors)
|
||||
}
|
||||
|
||||
widget.Result = str
|
||||
}
|
||||
|
||||
func makeToListText(toList []*tCurrency, colors textColors) string {
|
||||
str := ""
|
||||
for _, toCurrency := range toList {
|
||||
str += makeToText(toCurrency, colors)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func makeToText(toCurrency *tCurrency, colors textColors) string {
|
||||
str := ""
|
||||
str += fmt.Sprintf(" [%s]%s\n", colors.to.name, toCurrency.name)
|
||||
for _, info := range toCurrency.info {
|
||||
str += makeInfoText(info, colors)
|
||||
str += "\n\n"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func makeInfoText(info tInfo, colors textColors) string {
|
||||
return fmt.Sprintf(
|
||||
" [%s]Exchange: [%s]%s\n",
|
||||
colors.to.field,
|
||||
colors.to.value,
|
||||
info.exchange,
|
||||
) +
|
||||
fmt.Sprintf(
|
||||
" [%s]Volume(24h): [%s]%f-[%s]%f",
|
||||
colors.to.field,
|
||||
colors.to.value,
|
||||
info.volume24h,
|
||||
colors.to.value,
|
||||
info.volume24hTo,
|
||||
)
|
||||
}
|
||||
41
modules/cryptoexchanges/cryptolive/toplist/toplist.go
Normal file
41
modules/cryptoexchanges/cryptolive/toplist/toplist.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package toplist
|
||||
|
||||
type cList struct {
|
||||
items []*fCurrency
|
||||
}
|
||||
|
||||
type fCurrency struct {
|
||||
name, displayName string
|
||||
limit int
|
||||
to []*tCurrency
|
||||
}
|
||||
|
||||
type tCurrency struct {
|
||||
name string
|
||||
info []tInfo
|
||||
}
|
||||
|
||||
type tInfo struct {
|
||||
exchange string
|
||||
volume24h, volume24hTo float32
|
||||
}
|
||||
|
||||
type responseInterface struct {
|
||||
Response string `json:"Response"`
|
||||
Data []struct {
|
||||
Exchange string `json:"exchange"`
|
||||
FromSymbol string `json:"fromSymbol"`
|
||||
ToSymbol string `json:"toSymbol"`
|
||||
Volume24h float32 `json:"volume24h"`
|
||||
Volume24hTo float32 `json:"volume24hTo"`
|
||||
} `json:"Data"`
|
||||
}
|
||||
|
||||
func (list *cList) addItem(name, displayName string, limit int, to []*tCurrency) {
|
||||
list.items = append(list.items, &fCurrency{
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
limit: limit,
|
||||
to: to,
|
||||
})
|
||||
}
|
||||
137
modules/cryptoexchanges/cryptolive/toplist/widget.go
Normal file
137
modules/cryptoexchanges/cryptolive/toplist/widget.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package toplist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
var baseURL = "https://min-api.cryptocompare.com/data/top/exchanges"
|
||||
|
||||
type textColors struct {
|
||||
from struct {
|
||||
name string
|
||||
displayName string
|
||||
}
|
||||
to struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
}
|
||||
}
|
||||
|
||||
// Widget Toplist Widget
|
||||
type Widget struct {
|
||||
Result string
|
||||
|
||||
RefreshInterval int
|
||||
|
||||
list *cList
|
||||
|
||||
colors textColors
|
||||
}
|
||||
|
||||
// NewWidget Make new toplist widget
|
||||
func NewWidget() *Widget {
|
||||
widget := Widget{}
|
||||
|
||||
widget.list = &cList{}
|
||||
widget.setList()
|
||||
widget.config()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) setList() {
|
||||
currenciesMap, _ := wtf.Config.Map("wtf.mods.cryptolive.top")
|
||||
|
||||
for fromCurrency := range currenciesMap {
|
||||
displayName := wtf.Config.UString("wtf.mods.cryptolive.top."+fromCurrency+".displayName", "")
|
||||
limit := wtf.Config.UInt("wtf.mods.cryptolive.top."+fromCurrency+".limit", 1)
|
||||
widget.list.addItem(fromCurrency, displayName, limit, makeToList(fromCurrency, limit))
|
||||
}
|
||||
}
|
||||
|
||||
func makeToList(fCurrencyName string, limit int) (list []*tCurrency) {
|
||||
toList, _ := wtf.Config.List("wtf.mods.cryptolive.top." + fCurrencyName + ".to")
|
||||
|
||||
for _, toCurrency := range toList {
|
||||
list = append(list, &tCurrency{
|
||||
name: toCurrency.(string),
|
||||
info: make([]tInfo, limit),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (widget *Widget) config() {
|
||||
// set colors
|
||||
widget.colors.from.name = wtf.Config.UString("wtf.mods.cryptolive.colors.top.from.name", "coral")
|
||||
widget.colors.from.displayName = wtf.Config.UString("wtf.mods.cryptolive.colors.top.from.displayName", "grey")
|
||||
widget.colors.to.name = wtf.Config.UString("wtf.mods.cryptolive.colors.top.to.name", "red")
|
||||
widget.colors.to.field = wtf.Config.UString("wtf.mods.cryptolive.colors.top.to.field", "white")
|
||||
widget.colors.to.value = wtf.Config.UString("wtf.mods.cryptolive.colors.top.to.value", "value")
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh & update after interval time
|
||||
func (widget *Widget) Refresh(wg *sync.WaitGroup) {
|
||||
if len(widget.list.items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
widget.updateData()
|
||||
|
||||
widget.display()
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) updateData() {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(5 * time.Second),
|
||||
}
|
||||
|
||||
for _, fromCurrency := range widget.list.items {
|
||||
for _, toCurrency := range fromCurrency.to {
|
||||
|
||||
request := makeRequest(fromCurrency.name, toCurrency.name, fromCurrency.limit)
|
||||
response, _ := client.Do(request)
|
||||
|
||||
var jsonResponse responseInterface
|
||||
|
||||
err := json.NewDecoder(response.Body).Decode(&jsonResponse)
|
||||
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for idx, info := range jsonResponse.Data {
|
||||
toCurrency.info[idx] = tInfo{
|
||||
exchange: info.Exchange,
|
||||
volume24h: info.Volume24h,
|
||||
volume24hTo: info.Volume24hTo,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeRequest(fsym, tsym string, limit int) *http.Request {
|
||||
url := fmt.Sprintf("%s?fsym=%s&tsym=%s&limit=%d", baseURL, fsym, tsym, limit)
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
return request
|
||||
}
|
||||
55
modules/cryptoexchanges/cryptolive/widget.go
Normal file
55
modules/cryptoexchanges/cryptolive/widget.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cryptolive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/modules/cryptoexchanges/cryptolive/price"
|
||||
"github.com/wtfutil/wtf/modules/cryptoexchanges/cryptolive/toplist"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
// Widget define wtf widget to register widget later
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
priceWidget *price.Widget
|
||||
toplistWidget *toplist.Widget
|
||||
}
|
||||
|
||||
// NewWidget Make new instance of widget
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "CryptoLive", "cryptolive", false),
|
||||
priceWidget: price.NewWidget(),
|
||||
toplistWidget: toplist.NewWidget(),
|
||||
}
|
||||
|
||||
widget.priceWidget.RefreshInterval = widget.RefreshInterval()
|
||||
widget.toplistWidget.RefreshInterval = widget.RefreshInterval()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh & update after interval time
|
||||
func (widget *Widget) Refresh() {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
widget.priceWidget.Refresh(&wg)
|
||||
widget.toplistWidget.Refresh(&wg)
|
||||
wg.Wait()
|
||||
|
||||
display(widget)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func display(widget *Widget) {
|
||||
str := ""
|
||||
str += widget.priceWidget.Result
|
||||
str += widget.toplistWidget.Result
|
||||
widget.View.SetText(fmt.Sprintf("\n%s", str))
|
||||
}
|
||||
34
modules/datadog/client.go
Normal file
34
modules/datadog/client.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
datadog "github.com/zorkian/go-datadog-api"
|
||||
)
|
||||
|
||||
// Monitors returns a list of newrelic monitors
|
||||
func Monitors() ([]datadog.Monitor, error) {
|
||||
client := datadog.NewClient(apiKey(), applicationKey())
|
||||
|
||||
monitors, err := client.GetMonitorsByTags(wtf.ToStrs(wtf.Config.UList("wtf.mods.datadog.monitors.tags")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return monitors, nil
|
||||
}
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.datadog.apiKey",
|
||||
os.Getenv("WTF_DATADOG_API_KEY"),
|
||||
)
|
||||
}
|
||||
|
||||
func applicationKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.datadog.applicationKey",
|
||||
os.Getenv("WTF_DATADOG_APPLICATION_KEY"),
|
||||
)
|
||||
}
|
||||
73
modules/datadog/widget.go
Normal file
73
modules/datadog/widget.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
datadog "github.com/zorkian/go-datadog-api"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Datadog", "datadog", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
monitors, monitorErr := Monitors()
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s", widget.Name)))
|
||||
widget.View.Clear()
|
||||
|
||||
var content string
|
||||
if monitorErr != nil {
|
||||
widget.View.SetWrap(true)
|
||||
content = monitorErr.Error()
|
||||
} else {
|
||||
widget.View.SetWrap(false)
|
||||
content = widget.contentFrom(monitors)
|
||||
}
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(monitors []datadog.Monitor) string {
|
||||
var str string
|
||||
|
||||
triggeredMonitors := []datadog.Monitor{}
|
||||
|
||||
for _, monitor := range monitors {
|
||||
state := *monitor.OverallState
|
||||
switch state {
|
||||
case "Alert":
|
||||
triggeredMonitors = append(triggeredMonitors, monitor)
|
||||
}
|
||||
}
|
||||
if len(triggeredMonitors) > 0 {
|
||||
str = str + fmt.Sprintf(
|
||||
" %s\n",
|
||||
"[red]Triggered Monitors[white]",
|
||||
)
|
||||
for _, triggeredMonitor := range triggeredMonitors {
|
||||
str = str + fmt.Sprintf("[red] %s\n", *triggeredMonitor.Name)
|
||||
}
|
||||
} else {
|
||||
str = str + fmt.Sprintf(
|
||||
" %s\n",
|
||||
"[green]No Triggered Monitors[white]",
|
||||
)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
108
modules/gcal/cal_event.go
Normal file
108
modules/gcal/cal_event.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package gcal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"google.golang.org/api/calendar/v3"
|
||||
)
|
||||
|
||||
type CalEvent struct {
|
||||
event *calendar.Event
|
||||
}
|
||||
|
||||
func NewCalEvent(event *calendar.Event) *CalEvent {
|
||||
calEvent := CalEvent{
|
||||
event: event,
|
||||
}
|
||||
|
||||
return &calEvent
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (calEvent *CalEvent) AllDay() bool {
|
||||
return len(calEvent.event.Start.Date) > 0
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) ConflictsWith(otherEvents []*CalEvent) bool {
|
||||
hasConflict := false
|
||||
|
||||
for _, otherEvent := range otherEvents {
|
||||
if calEvent.event == otherEvent.event {
|
||||
continue
|
||||
}
|
||||
|
||||
if calEvent.Start().Before(otherEvent.End()) && calEvent.End().After(otherEvent.Start()) {
|
||||
hasConflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hasConflict
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) Now() bool {
|
||||
return time.Now().After(calEvent.Start()) && time.Now().Before(calEvent.End())
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) Past() bool {
|
||||
if calEvent.AllDay() {
|
||||
// FIXME: This should calculate properly
|
||||
return false
|
||||
}
|
||||
|
||||
return (calEvent.Now() == false) && calEvent.Start().Before(time.Now())
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) ResponseFor(email string) string {
|
||||
for _, attendee := range calEvent.event.Attendees {
|
||||
if attendee.Email == email {
|
||||
return attendee.ResponseStatus
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
/* -------------------- DateTimes -------------------- */
|
||||
|
||||
func (calEvent *CalEvent) End() time.Time {
|
||||
var calcTime string
|
||||
var end time.Time
|
||||
|
||||
if calEvent.AllDay() {
|
||||
calcTime = calEvent.event.End.Date
|
||||
end, _ = time.ParseInLocation("2006-01-02", calcTime, time.Local)
|
||||
} else {
|
||||
calcTime = calEvent.event.End.DateTime
|
||||
end, _ = time.Parse(time.RFC3339, calcTime)
|
||||
}
|
||||
|
||||
return end
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) Start() time.Time {
|
||||
var calcTime string
|
||||
var start time.Time
|
||||
|
||||
if calEvent.AllDay() {
|
||||
calcTime = calEvent.event.Start.Date
|
||||
start, _ = time.ParseInLocation("2006-01-02", calcTime, time.Local)
|
||||
} else {
|
||||
calcTime = calEvent.event.Start.DateTime
|
||||
start, _ = time.Parse(time.RFC3339, calcTime)
|
||||
}
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
func (calEvent *CalEvent) Timestamp() string {
|
||||
if calEvent.AllDay() {
|
||||
startTime, _ := time.ParseInLocation("2006-01-02", calEvent.event.Start.Date, time.Local)
|
||||
return startTime.Format(wtf.FriendlyDateFormat)
|
||||
}
|
||||
|
||||
startTime, _ := time.Parse(time.RFC3339, calEvent.event.Start.DateTime)
|
||||
return startTime.Format(wtf.MinimumTimeFormat)
|
||||
}
|
||||
219
modules/gcal/client.go
Normal file
219
modules/gcal/client.go
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* This butt-ugly code is direct from Google itself
|
||||
* https://developers.google.com/calendar/quickstart/go
|
||||
*
|
||||
* With some changes by me to improve things a bit.
|
||||
*/
|
||||
|
||||
package gcal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/cfg"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/calendar/v3"
|
||||
)
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func Fetch() ([]*CalEvent, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
secretPath, _ := wtf.ExpandHomeDir(wtf.Config.UString("wtf.mods.gcal.secretFile"))
|
||||
|
||||
b, err := ioutil.ReadFile(secretPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := getClient(ctx, config)
|
||||
|
||||
srv, err := calendar.New(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
calendarIds, err := getCalendarIdList(srv)
|
||||
|
||||
// Get calendar events
|
||||
var events calendar.Events
|
||||
|
||||
startTime := fromMidnight().Format(time.RFC3339)
|
||||
eventLimit := int64(wtf.Config.UInt("wtf.mods.gcal.eventCount", 10))
|
||||
|
||||
for _, calendarId := range calendarIds {
|
||||
calendarEvents, err := srv.Events.List(calendarId).ShowDeleted(false).TimeMin(startTime).MaxResults(eventLimit).SingleEvents(true).OrderBy("startTime").Do()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
events.Items = append(events.Items, calendarEvents.Items...)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort events
|
||||
timeDateChooser := func(event *calendar.Event) (time.Time, error) {
|
||||
if len(event.Start.Date) > 0 {
|
||||
return time.Parse("2006-01-02", event.Start.Date)
|
||||
}
|
||||
|
||||
return time.Parse(time.RFC3339, event.Start.DateTime)
|
||||
}
|
||||
|
||||
sort.Slice(events.Items, func(i, j int) bool {
|
||||
dateA, _ := timeDateChooser(events.Items[i])
|
||||
dateB, _ := timeDateChooser(events.Items[j])
|
||||
return dateA.Before(dateB)
|
||||
})
|
||||
|
||||
// Wrap the calendar events in our custom CalEvent
|
||||
calEvents := []*CalEvent{}
|
||||
for _, event := range events.Items {
|
||||
calEvents = append(calEvents, NewCalEvent(event))
|
||||
}
|
||||
|
||||
return calEvents, err
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func fromMidnight() time.Time {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
}
|
||||
|
||||
// getClient uses a Context and Config to retrieve a Token
|
||||
// then generate a Client. It returns the generated Client.
|
||||
func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
|
||||
cacheFile, err := tokenCacheFile()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get path to cached credential file. %v", err)
|
||||
}
|
||||
tok, err := tokenFromFile(cacheFile)
|
||||
if err != nil {
|
||||
tok = getTokenFromWeb(config)
|
||||
saveToken(cacheFile, tok)
|
||||
}
|
||||
return config.Client(ctx, tok)
|
||||
}
|
||||
|
||||
func isAuthenticated() bool {
|
||||
cacheFile, err := tokenCacheFile()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get path to cached credential file. %v", err)
|
||||
}
|
||||
_, err = tokenFromFile(cacheFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func authenticate() {
|
||||
filename := wtf.Config.UString("wtf.mods.gcal.secretFile")
|
||||
secretPath, _ := wtf.ExpandHomeDir(filename)
|
||||
|
||||
b, err := ioutil.ReadFile(secretPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read secret file. %v", filename)
|
||||
}
|
||||
|
||||
config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
|
||||
tok := getTokenFromWeb(config)
|
||||
cacheFile, err := tokenCacheFile()
|
||||
saveToken(cacheFile, tok)
|
||||
}
|
||||
|
||||
// getTokenFromWeb uses Config to request a Token.
|
||||
// It returns the retrieved Token.
|
||||
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
|
||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
fmt.Printf("Go to the following link in your browser then type the "+
|
||||
"authorization code: \n%v (press 'return' before inserting the code)", authURL)
|
||||
|
||||
var code string
|
||||
if _, err := fmt.Scan(&code); err != nil {
|
||||
log.Fatalf("Unable to read authorization code %v", err)
|
||||
}
|
||||
|
||||
tok, err := config.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve token from web %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
// tokenCacheFile generates credential file path/filename.
|
||||
// It returns the generated credential path/filename.
|
||||
func tokenCacheFile() (string, error) {
|
||||
return cfg.CreateFile("gcal-auth.json")
|
||||
}
|
||||
|
||||
// tokenFromFile retrieves a Token from a given file path.
|
||||
// It returns the retrieved Token and any read error encountered.
|
||||
func tokenFromFile(file string) (*oauth2.Token, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{}
|
||||
err = json.NewDecoder(f).Decode(t)
|
||||
defer f.Close()
|
||||
return t, err
|
||||
}
|
||||
|
||||
// saveToken uses a file path to create a file and store the
|
||||
// token in it.
|
||||
func saveToken(file string, token *oauth2.Token) {
|
||||
fmt.Printf("Saving credential file to: %s\n", file)
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to cache oauth token: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
json.NewEncoder(f).Encode(token)
|
||||
}
|
||||
|
||||
func getCalendarIdList(srv *calendar.Service) ([]string, error) {
|
||||
// Return single calendar if settings specify we should
|
||||
if !wtf.Config.UBool("wtf.mods.gcal.multiCalendar", false) {
|
||||
id, err := srv.CalendarList.Get("primary").Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{id.Id}, nil
|
||||
}
|
||||
|
||||
// Get all user calendars with at the least writing access
|
||||
var calendarIds []string
|
||||
var pageToken string
|
||||
for {
|
||||
calendarList, err := srv.CalendarList.List().ShowHidden(false).MinAccessRole("writer").PageToken(pageToken).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, calendarListItem := range calendarList.Items {
|
||||
calendarIds = append(calendarIds, calendarListItem.Id)
|
||||
}
|
||||
|
||||
pageToken = calendarList.NextPageToken
|
||||
if pageToken == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return calendarIds, nil
|
||||
}
|
||||
240
modules/gcal/display.go
Normal file
240
modules/gcal/display.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package gcal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) sortedEvents() ([]*CalEvent, []*CalEvent) {
|
||||
allDayEvents := []*CalEvent{}
|
||||
timedEvents := []*CalEvent{}
|
||||
|
||||
for _, calEvent := range widget.calEvents {
|
||||
if calEvent.AllDay() {
|
||||
allDayEvents = append(allDayEvents, calEvent)
|
||||
} else {
|
||||
timedEvents = append(timedEvents, calEvent)
|
||||
}
|
||||
}
|
||||
|
||||
return allDayEvents, timedEvents
|
||||
}
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.calEvents == nil || len(widget.calEvents) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
widget.mutex.Lock()
|
||||
defer widget.mutex.Unlock()
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(widget.Name))
|
||||
widget.View.SetText(widget.contentFrom(widget.calEvents))
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(calEvents []*CalEvent) string {
|
||||
if (calEvents == nil) || (len(calEvents) == 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var str string
|
||||
var prevEvent *CalEvent
|
||||
|
||||
if !wtf.Config.UBool("wtf.mods.gcal.showDeclined", false) {
|
||||
calEvents = removeDeclined(calEvents)
|
||||
}
|
||||
|
||||
for _, calEvent := range calEvents {
|
||||
timestamp := fmt.Sprintf("[%s]%s", widget.descriptionColor(calEvent), calEvent.Timestamp())
|
||||
if calEvent.AllDay() {
|
||||
timestamp = ""
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[%s]%s",
|
||||
widget.titleColor(calEvent),
|
||||
widget.eventSummary(calEvent, calEvent.ConflictsWith(calEvents)),
|
||||
)
|
||||
|
||||
lineOne := fmt.Sprintf(
|
||||
"%s %s %s %s[white]\n",
|
||||
widget.dayDivider(calEvent, prevEvent),
|
||||
widget.responseIcon(calEvent),
|
||||
timestamp,
|
||||
title,
|
||||
)
|
||||
|
||||
str = str + fmt.Sprintf("%s %s%s\n",
|
||||
lineOne,
|
||||
widget.location(calEvent),
|
||||
widget.timeUntil(calEvent),
|
||||
)
|
||||
|
||||
if (widget.location(calEvent) != "") || (widget.timeUntil(calEvent) != "") {
|
||||
str = str + "\n"
|
||||
}
|
||||
|
||||
prevEvent = calEvent
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) dayDivider(event, prevEvent *CalEvent) string {
|
||||
var prevStartTime time.Time
|
||||
|
||||
if prevEvent != nil {
|
||||
prevStartTime = prevEvent.Start()
|
||||
}
|
||||
|
||||
// round times to midnight for comparison
|
||||
toMidnight := func(t time.Time) time.Time {
|
||||
t = t.Local()
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
prevStartDay := toMidnight(prevStartTime)
|
||||
eventStartDay := toMidnight(event.Start())
|
||||
|
||||
if !eventStartDay.Equal(prevStartDay) {
|
||||
|
||||
return fmt.Sprintf("[%s::b]",
|
||||
wtf.Config.UString("wtf.mods.gcal.colors.day", "forestgreen")) +
|
||||
event.Start().Format(wtf.FullDateFormat) +
|
||||
"\n"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (widget *Widget) descriptionColor(calEvent *CalEvent) string {
|
||||
if calEvent.Past() {
|
||||
return wtf.Config.UString("wtf.mods.gcal.colors.past", "gray")
|
||||
}
|
||||
|
||||
return wtf.Config.UString("wtf.mods.gcal.colors.description", "white")
|
||||
}
|
||||
|
||||
func (widget *Widget) eventSummary(calEvent *CalEvent, conflict bool) string {
|
||||
summary := calEvent.event.Summary
|
||||
|
||||
if calEvent.Now() {
|
||||
summary = fmt.Sprintf(
|
||||
"%s %s",
|
||||
wtf.Config.UString("wtf.mods.gcal.currentIcon", "🔸"),
|
||||
summary,
|
||||
)
|
||||
}
|
||||
|
||||
if conflict {
|
||||
return fmt.Sprintf("%s %s", wtf.Config.UString("wtf.mods.gcal.conflictIcon", "🚨"), summary)
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// timeUntil returns the number of hours or days until the event
|
||||
// If the event is in the past, returns nil
|
||||
func (widget *Widget) timeUntil(calEvent *CalEvent) string {
|
||||
duration := time.Until(calEvent.Start()).Round(time.Minute)
|
||||
|
||||
if duration < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
days := duration / (24 * time.Hour)
|
||||
duration -= days * (24 * time.Hour)
|
||||
|
||||
hours := duration / time.Hour
|
||||
duration -= hours * time.Hour
|
||||
|
||||
mins := duration / time.Minute
|
||||
|
||||
untilStr := ""
|
||||
|
||||
color := "[lightblue]"
|
||||
if days > 0 {
|
||||
untilStr = fmt.Sprintf("%dd", days)
|
||||
} else if hours > 0 {
|
||||
untilStr = fmt.Sprintf("%dh", hours)
|
||||
} else {
|
||||
untilStr = fmt.Sprintf("%dm", mins)
|
||||
if mins < 30 {
|
||||
color = "[red]"
|
||||
}
|
||||
}
|
||||
|
||||
return color + untilStr + "[white]"
|
||||
}
|
||||
|
||||
func (widget *Widget) titleColor(calEvent *CalEvent) string {
|
||||
color := wtf.Config.UString("wtf.mods.gcal.colors.title", "white")
|
||||
|
||||
for _, untypedArr := range wtf.Config.UList("wtf.mods.gcal.colors.highlights") {
|
||||
highlightElements := wtf.ToStrs(untypedArr.([]interface{}))
|
||||
|
||||
match, _ := regexp.MatchString(
|
||||
strings.ToLower(highlightElements[0]),
|
||||
strings.ToLower(calEvent.event.Summary),
|
||||
)
|
||||
|
||||
if match == true {
|
||||
color = highlightElements[1]
|
||||
}
|
||||
}
|
||||
|
||||
if calEvent.Past() {
|
||||
color = wtf.Config.UString("wtf.mods.gcal.colors.past", "gray")
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
func (widget *Widget) location(calEvent *CalEvent) string {
|
||||
if wtf.Config.UBool("wtf.mods.gcal.displayLocation", true) == false {
|
||||
return ""
|
||||
}
|
||||
|
||||
if calEvent.event.Location == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"[%s]%s ",
|
||||
widget.descriptionColor(calEvent),
|
||||
calEvent.event.Location,
|
||||
)
|
||||
}
|
||||
|
||||
func (widget *Widget) responseIcon(calEvent *CalEvent) string {
|
||||
if false == wtf.Config.UBool("wtf.mods.gcal.displayResponseStatus", true) {
|
||||
return ""
|
||||
}
|
||||
|
||||
icon := "[gray]"
|
||||
|
||||
switch calEvent.ResponseFor(wtf.Config.UString("wtf.mods.gcal.email")) {
|
||||
case "accepted":
|
||||
return icon + "✔︎"
|
||||
case "declined":
|
||||
return icon + "✘"
|
||||
case "needsAction":
|
||||
return icon + "?"
|
||||
case "tentative":
|
||||
return icon + "~"
|
||||
default:
|
||||
return icon + " "
|
||||
}
|
||||
}
|
||||
|
||||
func removeDeclined(events []*CalEvent) []*CalEvent {
|
||||
var ret []*CalEvent
|
||||
for _, e := range events {
|
||||
if e.ResponseFor(wtf.Config.UString("wtf.mods.gcal.email")) != "declined" {
|
||||
ret = append(ret, e)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
77
modules/gcal/widget.go
Normal file
77
modules/gcal/widget.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package gcal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
calEvents []*CalEvent
|
||||
ch chan struct{}
|
||||
mutex sync.Mutex
|
||||
app *tview.Application
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Calendar", "gcal", true),
|
||||
ch: make(chan struct{}),
|
||||
app: app,
|
||||
}
|
||||
|
||||
go updateLoop(&widget)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Disable() {
|
||||
close(widget.ch)
|
||||
widget.TextWidget.Disable()
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if isAuthenticated() {
|
||||
widget.fetchAndDisplayEvents()
|
||||
return
|
||||
}
|
||||
widget.app.Suspend(authenticate)
|
||||
widget.Refresh()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) fetchAndDisplayEvents() {
|
||||
calEvents, err := Fetch()
|
||||
if err != nil {
|
||||
widget.calEvents = []*CalEvent{}
|
||||
} else {
|
||||
widget.calEvents = calEvents
|
||||
}
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func updateLoop(widget *Widget) {
|
||||
interval := wtf.Config.UInt("wtf.mods.gcal.textInterval", 30)
|
||||
if interval == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tick := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
defer tick.Stop()
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
widget.display()
|
||||
case <-widget.ch:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
76
modules/gerrit/display.go
Normal file
76
modules/gerrit/display.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package gerrit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
|
||||
project := widget.currentGerritProject()
|
||||
if project == nil {
|
||||
widget.View.SetText(fmt.Sprintf("%s", " Gerrit project data is unavailable (1)"))
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s- %s", widget.Name, widget.title(project))))
|
||||
|
||||
str := wtf.SigilStr(len(widget.GerritProjects), widget.Idx, widget.View) + "\n"
|
||||
str = str + " [red]Stats[white]\n"
|
||||
str = str + widget.displayStats(project)
|
||||
str = str + "\n"
|
||||
str = str + " [red]Open Incoming Reviews[white]\n"
|
||||
str = str + widget.displayMyIncomingReviews(project, wtf.Config.UString("wtf.mods.gerrit.username"))
|
||||
str = str + "\n"
|
||||
str = str + " [red]My Outgoing Reviews[white]\n"
|
||||
str = str + widget.displayMyOutgoingReviews(project, wtf.Config.UString("wtf.mods.gerrit.username"))
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyIncomingReviews(project *GerritProject, username string) string {
|
||||
if len(project.IncomingReviews) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for idx, r := range project.IncomingReviews {
|
||||
str = str + fmt.Sprintf(" [%s] [green]%d[white] [%s] %s\n", widget.rowColor(idx), r.Number, widget.rowColor(idx), r.Subject)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyOutgoingReviews(project *GerritProject, username string) string {
|
||||
if len(project.OutgoingReviews) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for idx, r := range project.OutgoingReviews {
|
||||
str = str + fmt.Sprintf(" [%s] [green]%d[white] [%s] %s\n", widget.rowColor(idx+len(project.IncomingReviews)), r.Number, widget.rowColor(idx+len(project.IncomingReviews)), r.Subject)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayStats(project *GerritProject) string {
|
||||
str := fmt.Sprintf(
|
||||
" Reviews: %d\n",
|
||||
project.ReviewCount,
|
||||
)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(index int) string {
|
||||
if widget.View.HasFocus() && (index == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
return wtf.RowColor("gerrit", index)
|
||||
}
|
||||
|
||||
func (widget *Widget) title(project *GerritProject) string {
|
||||
return fmt.Sprintf("[green]%s [white]", project.Path)
|
||||
}
|
||||
102
modules/gerrit/gerrit_repo.go
Normal file
102
modules/gerrit/gerrit_repo.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package gerrit
|
||||
|
||||
import (
|
||||
glb "github.com/andygrunwald/go-gerrit"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type GerritProject struct {
|
||||
gerrit *glb.Client
|
||||
Path string
|
||||
|
||||
Changes *[]glb.ChangeInfo
|
||||
ReviewCount int
|
||||
IncomingReviews []glb.ChangeInfo
|
||||
OutgoingReviews []glb.ChangeInfo
|
||||
}
|
||||
|
||||
func NewGerritProject(path string, gerrit *glb.Client) *GerritProject {
|
||||
project := GerritProject{
|
||||
gerrit: gerrit,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
return &project
|
||||
}
|
||||
|
||||
// Refresh reloads the gerrit data via the Gerrit API
|
||||
func (project *GerritProject) Refresh() {
|
||||
username := wtf.Config.UString("wtf.mods.gerrit.username")
|
||||
project.Changes, _ = project.loadChanges()
|
||||
|
||||
project.ReviewCount = project.countReviews(project.Changes)
|
||||
project.IncomingReviews = project.myIncomingReviews(project.Changes, username)
|
||||
project.OutgoingReviews = project.myOutgoingReviews(project.Changes, username)
|
||||
|
||||
}
|
||||
|
||||
/* -------------------- Counts -------------------- */
|
||||
|
||||
func (project *GerritProject) countReviews(changes *[]glb.ChangeInfo) int {
|
||||
if changes == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(*changes)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
// myOutgoingReviews returns a list of my outgoing reviews created by username on this project
|
||||
func (project *GerritProject) myOutgoingReviews(changes *[]glb.ChangeInfo, username string) []glb.ChangeInfo {
|
||||
var ors []glb.ChangeInfo
|
||||
|
||||
if changes == nil {
|
||||
return ors
|
||||
}
|
||||
|
||||
for _, change := range *changes {
|
||||
user := change.Owner
|
||||
|
||||
if user.Username == username {
|
||||
ors = append(ors, change)
|
||||
}
|
||||
}
|
||||
|
||||
return ors
|
||||
}
|
||||
|
||||
// myIncomingReviews returns a list of merge requests for which username has been requested to ChangeInfo
|
||||
func (project *GerritProject) myIncomingReviews(changes *[]glb.ChangeInfo, username string) []glb.ChangeInfo {
|
||||
var irs []glb.ChangeInfo
|
||||
|
||||
if changes == nil {
|
||||
return irs
|
||||
}
|
||||
|
||||
for _, change := range *changes {
|
||||
reviewers := change.Reviewers
|
||||
|
||||
for _, reviewer := range reviewers["REVIEWER"] {
|
||||
if reviewer.Username == username {
|
||||
irs = append(irs, change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return irs
|
||||
}
|
||||
|
||||
func (project *GerritProject) loadChanges() (*[]glb.ChangeInfo, error) {
|
||||
opt := &glb.QueryChangeOptions{}
|
||||
opt.Query = []string{"(projects:" + project.Path + "+ is:open + owner:self) " + " OR " +
|
||||
"(projects:" + project.Path + " + is:open + ((reviewer:self + -owner:self + -star:ignore) + OR + assignee:self))"}
|
||||
opt.AdditionalFields = []string{"DETAILED_LABELS", "DETAILED_ACCOUNTS"}
|
||||
changes, _, err := project.gerrit.Changes.QueryChanges(opt)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changes, err
|
||||
}
|
||||
238
modules/gerrit/widget.go
Normal file
238
modules/gerrit/widget.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package gerrit
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
glb "github.com/andygrunwald/go-gerrit"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Gerrit:
|
||||
|
||||
/: Show/hide this help window
|
||||
h: Show the previous project
|
||||
l: Show the next project
|
||||
j: Select the next review in the list
|
||||
k: Select the previous review in the list
|
||||
r: Refresh the data
|
||||
|
||||
arrow left: Show the previous project
|
||||
arrow right: Show the next project
|
||||
arrow down: Select the next review in the list
|
||||
arrow up: Select the previous review in the list
|
||||
|
||||
return: Open the selected review in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
gerrit *glb.Client
|
||||
|
||||
GerritProjects []*GerritProject
|
||||
Idx int
|
||||
selected int
|
||||
}
|
||||
|
||||
var (
|
||||
GerritURLPattern = regexp.MustCompile(`^(http|https)://(.*)$`)
|
||||
)
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Gerrit", "gerrit", true),
|
||||
|
||||
Idx: 0,
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
widget.unselect()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
baseURL := wtf.Config.UString("wtf.mods.gerrit.domain")
|
||||
username := wtf.Config.UString("wtf.mods.gerrit.username")
|
||||
|
||||
password := wtf.Config.UString(
|
||||
"wtf.mods.gerrit.password",
|
||||
os.Getenv("WTF_GERRIT_PASSWORD"),
|
||||
)
|
||||
|
||||
verifyServerCertificate := wtf.Config.UBool("wtf.mods.gerrit.verifyServerCertificate", true)
|
||||
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !verifyServerCertificate,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
gerritUrl := baseURL
|
||||
submatches := GerritURLPattern.FindAllStringSubmatch(baseURL, -1)
|
||||
|
||||
if len(submatches) > 0 && len(submatches[0]) > 2 {
|
||||
submatch := submatches[0]
|
||||
gerritUrl = fmt.Sprintf(
|
||||
"%s://%s:%s@%s", submatch[1], username, password, submatch[2])
|
||||
}
|
||||
gerrit, err := glb.NewClient(gerritUrl, httpClient)
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
return
|
||||
}
|
||||
widget.gerrit = gerrit
|
||||
widget.GerritProjects = widget.buildProjectCollection(wtf.Config.UList("wtf.mods.gerrit.projects"))
|
||||
|
||||
for _, project := range widget.GerritProjects {
|
||||
project.Refresh()
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) nextProject() {
|
||||
widget.Idx = widget.Idx + 1
|
||||
widget.unselect()
|
||||
if widget.Idx == len(widget.GerritProjects) {
|
||||
widget.Idx = 0
|
||||
}
|
||||
|
||||
widget.unselect()
|
||||
}
|
||||
|
||||
func (widget *Widget) prevProject() {
|
||||
widget.Idx = widget.Idx - 1
|
||||
if widget.Idx < 0 {
|
||||
widget.Idx = len(widget.GerritProjects) - 1
|
||||
}
|
||||
|
||||
widget.unselect()
|
||||
}
|
||||
|
||||
func (widget *Widget) nextReview() {
|
||||
widget.selected++
|
||||
project := widget.GerritProjects[widget.Idx]
|
||||
if widget.selected >= project.ReviewCount {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prevReview() {
|
||||
widget.selected--
|
||||
project := widget.GerritProjects[widget.Idx]
|
||||
if widget.selected < 0 {
|
||||
widget.selected = project.ReviewCount - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openReview() {
|
||||
sel := widget.selected
|
||||
project := widget.GerritProjects[widget.Idx]
|
||||
if sel >= 0 && sel < project.ReviewCount {
|
||||
change := glb.ChangeInfo{}
|
||||
if sel < len(project.IncomingReviews) {
|
||||
change = project.IncomingReviews[sel]
|
||||
} else {
|
||||
change = project.OutgoingReviews[sel-len(project.IncomingReviews)]
|
||||
}
|
||||
wtf.OpenFile(fmt.Sprintf("%s/%s/%d", wtf.Config.UString("wtf.mods.gerrit.domain"), "#/c", change.Number))
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) buildProjectCollection(projectData []interface{}) []*GerritProject {
|
||||
gerritProjects := []*GerritProject{}
|
||||
|
||||
for _, name := range projectData {
|
||||
project := NewGerritProject(name.(string), widget.gerrit)
|
||||
gerritProjects = append(gerritProjects, project)
|
||||
}
|
||||
|
||||
return gerritProjects
|
||||
}
|
||||
|
||||
func (widget *Widget) currentGerritProject() *GerritProject {
|
||||
if len(widget.GerritProjects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if widget.Idx < 0 || widget.Idx >= len(widget.GerritProjects) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.GerritProjects[widget.Idx]
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.prevProject()
|
||||
return nil
|
||||
case "l":
|
||||
widget.nextProject()
|
||||
return nil
|
||||
case "j":
|
||||
widget.nextReview()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prevReview()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyLeft:
|
||||
widget.prevProject()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.nextProject()
|
||||
return nil
|
||||
case tcell.KeyDown:
|
||||
widget.nextReview()
|
||||
return nil
|
||||
case tcell.KeyUp:
|
||||
widget.prevReview()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openReview()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
83
modules/git/display.go
Normal file
83
modules/git/display.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
repoData := widget.currentData()
|
||||
if repoData == nil {
|
||||
widget.View.SetText(" Git repo data is unavailable ")
|
||||
return
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%s - [green]%s[white]", widget.Name, repoData.Repository)
|
||||
widget.View.SetTitle(widget.ContextualTitle(title))
|
||||
|
||||
str := wtf.SigilStr(len(widget.GitRepos), widget.Idx, widget.View) + "\n"
|
||||
str = str + " [red]Branch[white]\n"
|
||||
str = str + fmt.Sprintf(" %s", repoData.Branch)
|
||||
str = str + "\n"
|
||||
str = str + widget.formatChanges(repoData.ChangedFiles)
|
||||
str = str + "\n"
|
||||
str = str + widget.formatCommits(repoData.Commits)
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
|
||||
func (widget *Widget) formatChanges(data []string) string {
|
||||
str := ""
|
||||
str = str + " [red]Changed Files[white]\n"
|
||||
|
||||
if len(data) == 1 {
|
||||
str = str + " [grey]none[white]\n"
|
||||
} else {
|
||||
for _, line := range data {
|
||||
str = str + widget.formatChange(line)
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) formatChange(line string) string {
|
||||
if len(line) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
firstChar, _ := utf8.DecodeRuneInString(line)
|
||||
|
||||
// Revisit this and kill the ugly duplication
|
||||
switch firstChar {
|
||||
case 'A':
|
||||
line = strings.Replace(line, "A", "[green]A[white]", 1)
|
||||
case 'D':
|
||||
line = strings.Replace(line, "D", "[red]D[white]", 1)
|
||||
case 'M':
|
||||
line = strings.Replace(line, "M", "[yellow]M[white]", 1)
|
||||
case 'R':
|
||||
line = strings.Replace(line, "R", "[purple]R[white]", 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %s\n", strings.Replace(line, "\"", "", -1))
|
||||
}
|
||||
|
||||
func (widget *Widget) formatCommits(data []string) string {
|
||||
str := ""
|
||||
str = str + " [red]Recent Commits[white]\n"
|
||||
|
||||
for _, line := range data {
|
||||
str = str + widget.formatCommit(line)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) formatCommit(line string) string {
|
||||
return fmt.Sprintf(" %s\n", strings.Replace(line, "\"", "", -1))
|
||||
}
|
||||
98
modules/git/git_repo.go
Normal file
98
modules/git/git_repo.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type GitRepo struct {
|
||||
Branch string
|
||||
ChangedFiles []string
|
||||
Commits []string
|
||||
Repository string
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewGitRepo(repoPath string) *GitRepo {
|
||||
repo := GitRepo{Path: repoPath}
|
||||
|
||||
repo.Branch = repo.branch()
|
||||
repo.ChangedFiles = repo.changedFiles()
|
||||
repo.Commits = repo.commits()
|
||||
repo.Repository = strings.TrimSpace(repo.repository())
|
||||
|
||||
return &repo
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (repo *GitRepo) branch() string {
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "rev-parse", "--abbrev-ref", "HEAD"}
|
||||
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *GitRepo) changedFiles() []string {
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "status", "--porcelain"}
|
||||
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
data := strings.Split(str, "\n")
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (repo *GitRepo) commits() []string {
|
||||
numStr := fmt.Sprintf("-n %d", wtf.Config.UInt("wtf.mods.git.commitCount", 10))
|
||||
|
||||
dateFormat := wtf.Config.UString("wtf.mods.git.dateFormat", "%b %d, %Y")
|
||||
dateStr := fmt.Sprintf("--date=format:\"%s\"", dateFormat)
|
||||
|
||||
commitFormat := wtf.Config.UString("wtf.mods.git.commitFormat", "[forestgreen]%h [white]%s [grey]%an on %cd[white]")
|
||||
commitStr := fmt.Sprintf("--pretty=format:\"%s\"", commitFormat)
|
||||
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "log", dateStr, numStr, commitStr}
|
||||
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
data := strings.Split(str, "\n")
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (repo *GitRepo) repository() string {
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "rev-parse", "--show-toplevel"}
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return str
|
||||
}
|
||||
func (repo *GitRepo) pull() string {
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "pull"}
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *GitRepo) checkout(branch string) string {
|
||||
arg := []string{repo.gitDir(), repo.workTree(), "checkout", branch}
|
||||
cmd := exec.Command("git", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *GitRepo) gitDir() string {
|
||||
return fmt.Sprintf("--git-dir=%s/.git", repo.Path)
|
||||
}
|
||||
|
||||
func (repo *GitRepo) workTree() string {
|
||||
return fmt.Sprintf("--work-tree=%s", repo.Path)
|
||||
}
|
||||
258
modules/git/widget.go
Normal file
258
modules/git/widget.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Git:
|
||||
|
||||
/: Show/hide this help window
|
||||
c: Checkout to branch
|
||||
h: Previous git repository
|
||||
l: Next git repository
|
||||
p: Pull current git repository
|
||||
|
||||
arrow left: Previous git repository
|
||||
arrow right: Next git repository
|
||||
`
|
||||
|
||||
const offscreen = -1000
|
||||
const modalWidth = 80
|
||||
const modalHeight = 7
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.MultiSourceWidget
|
||||
wtf.TextWidget
|
||||
|
||||
app *tview.Application
|
||||
GitRepos []*GitRepo
|
||||
pages *tview.Pages
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
MultiSourceWidget: wtf.NewMultiSourceWidget("git", "repository", "repositories"),
|
||||
TextWidget: wtf.NewTextWidget(app, "Git", "git", true),
|
||||
|
||||
app: app,
|
||||
pages: pages,
|
||||
}
|
||||
|
||||
widget.LoadSources()
|
||||
widget.SetDisplayFunction(widget.display)
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Checkout() {
|
||||
form := widget.modalForm("Branch to checkout:", "")
|
||||
|
||||
checkoutFctn := func() {
|
||||
text := form.GetFormItem(0).(*tview.InputField).GetText()
|
||||
repoToCheckout := widget.GitRepos[widget.Idx]
|
||||
repoToCheckout.checkout(text)
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
widget.Refresh()
|
||||
}
|
||||
|
||||
widget.addButtons(form, checkoutFctn)
|
||||
widget.modalFocus(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) Pull() {
|
||||
repoToPull := widget.GitRepos[widget.Idx]
|
||||
repoToPull.pull()
|
||||
widget.Refresh()
|
||||
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
repoPaths := wtf.ToStrs(wtf.Config.UList("wtf.mods.git.repositories"))
|
||||
|
||||
widget.GitRepos = widget.gitRepos(repoPaths)
|
||||
sort.Slice(widget.GitRepos, func(i, j int) bool {
|
||||
return widget.GitRepos[i].Path < widget.GitRepos[j].Path
|
||||
})
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) addCheckoutButton(form *tview.Form, fctn func()) {
|
||||
form.AddButton("Checkout", fctn)
|
||||
}
|
||||
|
||||
func (widget *Widget) addButtons(form *tview.Form, checkoutFctn func()) {
|
||||
widget.addCheckoutButton(form, checkoutFctn)
|
||||
widget.addCancelButton(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) addCancelButton(form *tview.Form) {
|
||||
cancelFn := func() {
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
form.AddButton("Cancel", cancelFn)
|
||||
form.SetCancelFunc(cancelFn)
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFocus(form *tview.Form) {
|
||||
frame := widget.modalFrame(form)
|
||||
widget.pages.AddPage("modal", frame, false, true)
|
||||
widget.app.SetFocus(frame)
|
||||
}
|
||||
|
||||
func (widget *Widget) modalForm(lbl, text string) *tview.Form {
|
||||
form := tview.NewForm().
|
||||
SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonTextColor(tview.Styles.PrimaryTextColor)
|
||||
|
||||
form.AddInputField(lbl, text, 60, nil, nil)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {
|
||||
frame := tview.NewFrame(form).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
frame.SetRect(offscreen, offscreen, modalWidth, modalHeight)
|
||||
frame.SetBorder(true)
|
||||
frame.SetBorders(1, 1, 0, 0, 1, 1)
|
||||
|
||||
drawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
|
||||
w, h := screen.Size()
|
||||
frame.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
frame.SetDrawFunc(drawFunc)
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func (widget *Widget) currentData() *GitRepo {
|
||||
if len(widget.GitRepos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if widget.Idx < 0 || widget.Idx >= len(widget.GitRepos) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.GitRepos[widget.Idx]
|
||||
}
|
||||
|
||||
func (widget *Widget) gitRepos(repoPaths []string) []*GitRepo {
|
||||
repos := []*GitRepo{}
|
||||
|
||||
for _, repoPath := range repoPaths {
|
||||
if strings.HasSuffix(repoPath, "/") {
|
||||
repos = append(repos, findGitRepositories(make([]*GitRepo, 0), repoPath)...)
|
||||
|
||||
} else {
|
||||
repo := NewGitRepo(repoPath)
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
}
|
||||
|
||||
return repos
|
||||
}
|
||||
|
||||
func findGitRepositories(repositories []*GitRepo, directory string) []*GitRepo {
|
||||
directory = strings.TrimSuffix(directory, "/")
|
||||
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var path string
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
path = directory + "/" + file.Name()
|
||||
if file.Name() == ".git" {
|
||||
path = strings.TrimSuffix(path, "/.git")
|
||||
repo := NewGitRepo(path)
|
||||
repositories = append(repositories, repo)
|
||||
continue
|
||||
}
|
||||
if file.Name() == "vendor" || file.Name() == "node_modules" {
|
||||
continue
|
||||
}
|
||||
repositories = findGitRepositories(repositories, path)
|
||||
}
|
||||
}
|
||||
|
||||
return repositories
|
||||
}
|
||||
|
||||
func (widget *Widget) Next() {
|
||||
widget.Idx = widget.Idx + 1
|
||||
if widget.Idx == len(widget.GitRepos) {
|
||||
widget.Idx = 0
|
||||
}
|
||||
|
||||
if widget.DisplayFunction != nil {
|
||||
widget.DisplayFunction()
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) Prev() {
|
||||
widget.Idx = widget.Idx - 1
|
||||
if widget.Idx < 0 {
|
||||
widget.Idx = len(widget.GitRepos) - 1
|
||||
}
|
||||
|
||||
if widget.DisplayFunction != nil {
|
||||
widget.DisplayFunction()
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.Prev()
|
||||
return nil
|
||||
case "l":
|
||||
widget.Next()
|
||||
return nil
|
||||
case "p":
|
||||
widget.Pull()
|
||||
return nil
|
||||
case "c":
|
||||
widget.Checkout()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyLeft:
|
||||
widget.Prev()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.Next()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
96
modules/github/display.go
Normal file
96
modules/github/display.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
repo := widget.currentGithubRepo()
|
||||
if repo == nil {
|
||||
widget.View.SetText(" GitHub repo data is unavailable ")
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - %s", widget.Name, widget.title(repo))))
|
||||
|
||||
str := wtf.SigilStr(len(widget.GithubRepos), widget.Idx, widget.View) + "\n"
|
||||
str = str + " [red]Stats[white]\n"
|
||||
str = str + widget.displayStats(repo)
|
||||
str = str + "\n"
|
||||
str = str + " [red]Open Review Requests[white]\n"
|
||||
str = str + widget.displayMyReviewRequests(repo, wtf.Config.UString("wtf.mods.github.username"))
|
||||
str = str + "\n"
|
||||
str = str + " [red]My Pull Requests[white]\n"
|
||||
str = str + widget.displayMyPullRequests(repo, wtf.Config.UString("wtf.mods.github.username"))
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyPullRequests(repo *GithubRepo, username string) string {
|
||||
prs := repo.myPullRequests(username)
|
||||
|
||||
if len(prs) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for _, pr := range prs {
|
||||
str = str + fmt.Sprintf(" %s[green]%4d[white] %s\n", mergeString(pr), *pr.Number, *pr.Title)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyReviewRequests(repo *GithubRepo, username string) string {
|
||||
prs := repo.myReviewRequests(username)
|
||||
|
||||
if len(prs) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for _, pr := range prs {
|
||||
str = str + fmt.Sprintf(" [green]%4d[white] %s\n", *pr.Number, *pr.Title)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayStats(repo *GithubRepo) string {
|
||||
str := fmt.Sprintf(
|
||||
" PRs: %d Issues: %d Stars: %d\n",
|
||||
repo.PullRequestCount(),
|
||||
repo.IssueCount(),
|
||||
repo.StarCount(),
|
||||
)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) title(repo *GithubRepo) string {
|
||||
return fmt.Sprintf("[green]%s - %s[white]", repo.Owner, repo.Name)
|
||||
}
|
||||
|
||||
func showStatus() bool {
|
||||
return wtf.Config.UBool("wtf.mods.github.enableStatus", false)
|
||||
}
|
||||
|
||||
var mergeIcons = map[string]string{
|
||||
"dirty": "[red]![white] ",
|
||||
"clean": "[green]✔[white] ",
|
||||
"unstable": "[red]✖[white] ",
|
||||
"blocked": "[red]✖[white] ",
|
||||
}
|
||||
|
||||
func mergeString(pr *github.PullRequest) string {
|
||||
if !showStatus() {
|
||||
return ""
|
||||
}
|
||||
if str, ok := mergeIcons[pr.GetMergeableState()]; ok {
|
||||
return str
|
||||
}
|
||||
return "? "
|
||||
}
|
||||
203
modules/github/github_repo.go
Normal file
203
modules/github/github_repo.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
ghb "github.com/google/go-github/github"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type GithubRepo struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
uploadURL string
|
||||
|
||||
Name string
|
||||
Owner string
|
||||
PullRequests []*ghb.PullRequest
|
||||
RemoteRepo *ghb.Repository
|
||||
}
|
||||
|
||||
func NewGithubRepo(name, owner string) *GithubRepo {
|
||||
repo := GithubRepo{
|
||||
Name: name,
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
repo.loadAPICredentials()
|
||||
|
||||
return &repo
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) Open() {
|
||||
wtf.OpenFile(*repo.RemoteRepo.HTMLURL)
|
||||
}
|
||||
|
||||
// Refresh reloads the github data via the Github API
|
||||
func (repo *GithubRepo) Refresh() {
|
||||
repo.PullRequests, _ = repo.loadPullRequests()
|
||||
repo.RemoteRepo, _ = repo.loadRemoteRepository()
|
||||
}
|
||||
|
||||
/* -------------------- Counts -------------------- */
|
||||
|
||||
func (repo *GithubRepo) IssueCount() int {
|
||||
if repo.RemoteRepo == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return *repo.RemoteRepo.OpenIssuesCount
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) PullRequestCount() int {
|
||||
return len(repo.PullRequests)
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) StarCount() int {
|
||||
if repo.RemoteRepo == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return *repo.RemoteRepo.StargazersCount
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (repo *GithubRepo) isGitHubEnterprise() bool {
|
||||
if len(repo.baseURL) > 0 {
|
||||
if len(repo.uploadURL) == 0 {
|
||||
repo.uploadURL = repo.baseURL
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) oauthClient() *http.Client {
|
||||
tokenService := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: repo.apiKey},
|
||||
)
|
||||
|
||||
return oauth2.NewClient(context.Background(), tokenService)
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) githubClient() (*ghb.Client, error) {
|
||||
oauthClient := repo.oauthClient()
|
||||
|
||||
if repo.isGitHubEnterprise() {
|
||||
return ghb.NewEnterpriseClient(repo.baseURL, repo.uploadURL, oauthClient)
|
||||
}
|
||||
|
||||
return ghb.NewClient(oauthClient), nil
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) loadAPICredentials() {
|
||||
repo.apiKey = wtf.Config.UString(
|
||||
"wtf.mods.github.apiKey",
|
||||
os.Getenv("WTF_GITHUB_TOKEN"),
|
||||
)
|
||||
|
||||
repo.baseURL = wtf.Config.UString(
|
||||
"wtf.mods.github.baseURL",
|
||||
os.Getenv("WTF_GITHUB_BASE_URL"),
|
||||
)
|
||||
|
||||
repo.uploadURL = wtf.Config.UString(
|
||||
"wtf.mods.github.uploadURL",
|
||||
os.Getenv("WTF_GITHUB_UPLOAD_URL"),
|
||||
)
|
||||
}
|
||||
|
||||
// myPullRequests returns a list of pull requests created by username on this repo
|
||||
func (repo *GithubRepo) myPullRequests(username string) []*ghb.PullRequest {
|
||||
prs := []*ghb.PullRequest{}
|
||||
|
||||
for _, pr := range repo.PullRequests {
|
||||
user := *pr.User
|
||||
|
||||
if *user.Login == username {
|
||||
prs = append(prs, pr)
|
||||
}
|
||||
}
|
||||
|
||||
if showStatus() {
|
||||
prs = repo.individualPRs(prs)
|
||||
}
|
||||
|
||||
return prs
|
||||
}
|
||||
|
||||
// individualPRs takes a list of pull requests (presumably returned from
|
||||
// github.PullRequests.List) and fetches them individually to get more detailed
|
||||
// status info on each. see: https://developer.github.com/v3/git/#checking-mergeability-of-pull-requests
|
||||
func (repo *GithubRepo) individualPRs(prs []*ghb.PullRequest) []*ghb.PullRequest {
|
||||
github, err := repo.githubClient()
|
||||
if err != nil {
|
||||
return prs
|
||||
}
|
||||
|
||||
var ret []*ghb.PullRequest
|
||||
for i := range prs {
|
||||
pr, _, err := github.PullRequests.Get(context.Background(), repo.Owner, repo.Name, prs[i].GetNumber())
|
||||
if err != nil {
|
||||
// worst case, just keep the original one
|
||||
ret = append(ret, prs[i])
|
||||
} else {
|
||||
ret = append(ret, pr)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// myReviewRequests returns a list of pull requests for which username has been
|
||||
// requested to do a code review
|
||||
func (repo *GithubRepo) myReviewRequests(username string) []*ghb.PullRequest {
|
||||
prs := []*ghb.PullRequest{}
|
||||
|
||||
for _, pr := range repo.PullRequests {
|
||||
for _, reviewer := range pr.RequestedReviewers {
|
||||
if *reviewer.Login == username {
|
||||
prs = append(prs, pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prs
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) loadPullRequests() ([]*ghb.PullRequest, error) {
|
||||
github, err := repo.githubClient()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := &ghb.PullRequestListOptions{}
|
||||
|
||||
prs, _, err := github.PullRequests.List(context.Background(), repo.Owner, repo.Name, opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
func (repo *GithubRepo) loadRemoteRepository() (*ghb.Repository, error) {
|
||||
github, err := repo.githubClient()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repository, _, err := github.Repositories.Get(context.Background(), repo.Owner, repo.Name)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repository, nil
|
||||
}
|
||||
137
modules/github/widget.go
Normal file
137
modules/github/widget.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for GitHub:
|
||||
|
||||
/: Show/hide this help window
|
||||
h: Previous git repository
|
||||
l: Next git repository
|
||||
r: Refresh the data
|
||||
|
||||
arrow left: Previous git repository
|
||||
arrow right: Next git repository
|
||||
|
||||
return: Open the selected repository in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
GithubRepos []*GithubRepo
|
||||
Idx int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "GitHub", "github", true),
|
||||
|
||||
Idx: 0,
|
||||
}
|
||||
|
||||
widget.GithubRepos = widget.buildRepoCollection(wtf.Config.UMap("wtf.mods.github.repositories"))
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
for _, repo := range widget.GithubRepos {
|
||||
repo.Refresh()
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) Next() {
|
||||
widget.Idx = widget.Idx + 1
|
||||
if widget.Idx == len(widget.GithubRepos) {
|
||||
widget.Idx = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) Prev() {
|
||||
widget.Idx = widget.Idx - 1
|
||||
if widget.Idx < 0 {
|
||||
widget.Idx = len(widget.GithubRepos) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) buildRepoCollection(repoData map[string]interface{}) []*GithubRepo {
|
||||
githubRepos := []*GithubRepo{}
|
||||
|
||||
for name, owner := range repoData {
|
||||
repo := NewGithubRepo(name, owner.(string))
|
||||
githubRepos = append(githubRepos, repo)
|
||||
}
|
||||
|
||||
return githubRepos
|
||||
}
|
||||
|
||||
func (widget *Widget) currentGithubRepo() *GithubRepo {
|
||||
if len(widget.GithubRepos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if widget.Idx < 0 || widget.Idx >= len(widget.GithubRepos) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.GithubRepos[widget.Idx]
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.Prev()
|
||||
return nil
|
||||
case "l":
|
||||
widget.Next()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyEnter:
|
||||
widget.openRepo()
|
||||
return nil
|
||||
case tcell.KeyLeft:
|
||||
widget.Prev()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.Next()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) openRepo() {
|
||||
repo := widget.currentGithubRepo()
|
||||
|
||||
if repo != nil {
|
||||
repo.Open()
|
||||
}
|
||||
}
|
||||
75
modules/gitlab/display.go
Normal file
75
modules/gitlab/display.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
|
||||
project := widget.currentGitlabProject()
|
||||
if project == nil {
|
||||
widget.View.SetText(" Gitlab project data is unavailable ")
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetTitle(fmt.Sprintf("%s- %s", widget.Name, widget.title(project)))
|
||||
|
||||
str := wtf.SigilStr(len(widget.GitlabProjects), widget.Idx, widget.View) + "\n"
|
||||
str = str + " [red]Stats[white]\n"
|
||||
str = str + widget.displayStats(project)
|
||||
str = str + "\n"
|
||||
str = str + " [red]Open Approval Requests[white]\n"
|
||||
str = str + widget.displayMyApprovalRequests(project, wtf.Config.UString("wtf.mods.gitlab.username"))
|
||||
str = str + "\n"
|
||||
str = str + " [red]My Merge Requests[white]\n"
|
||||
str = str + widget.displayMyMergeRequests(project, wtf.Config.UString("wtf.mods.gitlab.username"))
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyMergeRequests(project *GitlabProject, username string) string {
|
||||
mrs := project.myMergeRequests(username)
|
||||
|
||||
if len(mrs) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for _, mr := range mrs {
|
||||
str = str + fmt.Sprintf(" [green]%4d[white] %s\n", mr.IID, mr.Title)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayMyApprovalRequests(project *GitlabProject, username string) string {
|
||||
mrs := project.myApprovalRequests(username)
|
||||
|
||||
if len(mrs) == 0 {
|
||||
return " [grey]none[white]\n"
|
||||
}
|
||||
|
||||
str := ""
|
||||
for _, mr := range mrs {
|
||||
str = str + fmt.Sprintf(" [green]%4d[white] %s\n", mr.IID, mr.Title)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) displayStats(project *GitlabProject) string {
|
||||
str := fmt.Sprintf(
|
||||
" MRs: %d Issues: %d Stars: %d\n",
|
||||
project.MergeRequestCount(),
|
||||
project.IssueCount(),
|
||||
project.StarCount(),
|
||||
)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) title(project *GitlabProject) string {
|
||||
return fmt.Sprintf("[green]%s [white]", project.Path)
|
||||
}
|
||||
113
modules/gitlab/gitlab_repo.go
Normal file
113
modules/gitlab/gitlab_repo.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
glb "github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
type GitlabProject struct {
|
||||
gitlab *glb.Client
|
||||
Path string
|
||||
|
||||
MergeRequests []*glb.MergeRequest
|
||||
RemoteProject *glb.Project
|
||||
}
|
||||
|
||||
func NewGitlabProject(name string, namespace string, gitlab *glb.Client) *GitlabProject {
|
||||
path := namespace + "/" + name
|
||||
project := GitlabProject{
|
||||
gitlab: gitlab,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
return &project
|
||||
}
|
||||
|
||||
// Refresh reloads the gitlab data via the Gitlab API
|
||||
func (project *GitlabProject) Refresh() {
|
||||
project.MergeRequests, _ = project.loadMergeRequests()
|
||||
project.RemoteProject, _ = project.loadRemoteProject()
|
||||
}
|
||||
|
||||
/* -------------------- Counts -------------------- */
|
||||
|
||||
func (project *GitlabProject) IssueCount() int {
|
||||
if project.RemoteProject == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return project.RemoteProject.OpenIssuesCount
|
||||
}
|
||||
|
||||
func (project *GitlabProject) MergeRequestCount() int {
|
||||
return len(project.MergeRequests)
|
||||
}
|
||||
|
||||
func (project *GitlabProject) StarCount() int {
|
||||
if project.RemoteProject == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return project.RemoteProject.StarCount
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
// myMergeRequests returns a list of merge requests created by username on this project
|
||||
func (project *GitlabProject) myMergeRequests(username string) []*glb.MergeRequest {
|
||||
mrs := []*glb.MergeRequest{}
|
||||
|
||||
for _, mr := range project.MergeRequests {
|
||||
user := mr.Author
|
||||
|
||||
if user.Username == username {
|
||||
mrs = append(mrs, mr)
|
||||
}
|
||||
}
|
||||
|
||||
return mrs
|
||||
}
|
||||
|
||||
// myApprovalRequests returns a list of merge requests for which username has been
|
||||
// requested to approve
|
||||
func (project *GitlabProject) myApprovalRequests(username string) []*glb.MergeRequest {
|
||||
mrs := []*glb.MergeRequest{}
|
||||
|
||||
for _, mr := range project.MergeRequests {
|
||||
approvers, _, err := project.gitlab.MergeRequests.GetMergeRequestApprovals(project.Path, mr.IID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, approver := range approvers.Approvers {
|
||||
if approver.User.Username == username {
|
||||
mrs = append(mrs, mr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mrs
|
||||
}
|
||||
|
||||
func (project *GitlabProject) loadMergeRequests() ([]*glb.MergeRequest, error) {
|
||||
state := "opened"
|
||||
opts := glb.ListProjectMergeRequestsOptions{
|
||||
State: &state,
|
||||
}
|
||||
|
||||
mrs, _, err := project.gitlab.MergeRequests.ListProjectMergeRequests(project.Path, &opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mrs, nil
|
||||
}
|
||||
|
||||
func (project *GitlabProject) loadRemoteProject() (*glb.Project, error) {
|
||||
projectsitory, _, err := project.gitlab.Projects.GetProject(project.Path)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return projectsitory, nil
|
||||
}
|
||||
145
modules/gitlab/widget.go
Normal file
145
modules/gitlab/widget.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
glb "github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Gitlab:
|
||||
|
||||
/: Show/hide this help window
|
||||
h: Previous project
|
||||
l: Next project
|
||||
r: Refresh the data
|
||||
|
||||
arrow left: Previous project
|
||||
arrow right: Next project
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
gitlab *glb.Client
|
||||
|
||||
GitlabProjects []*GitlabProject
|
||||
Idx int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
baseURL := wtf.Config.UString("wtf.mods.gitlab.domain")
|
||||
gitlab := glb.NewClient(nil, apiKey())
|
||||
|
||||
if baseURL != "" {
|
||||
gitlab.SetBaseURL(baseURL)
|
||||
}
|
||||
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Gitlab", "gitlab", true),
|
||||
|
||||
gitlab: gitlab,
|
||||
|
||||
Idx: 0,
|
||||
}
|
||||
|
||||
widget.GitlabProjects = widget.buildProjectCollection(wtf.Config.UMap("wtf.mods.gitlab.projects"))
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
for _, project := range widget.GitlabProjects {
|
||||
project.Refresh()
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) Next() {
|
||||
widget.Idx = widget.Idx + 1
|
||||
if widget.Idx == len(widget.GitlabProjects) {
|
||||
widget.Idx = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) Prev() {
|
||||
widget.Idx = widget.Idx - 1
|
||||
if widget.Idx < 0 {
|
||||
widget.Idx = len(widget.GitlabProjects) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.gitlab.apiKey",
|
||||
os.Getenv("WTF_GITLAB_TOKEN"),
|
||||
)
|
||||
}
|
||||
|
||||
func (widget *Widget) buildProjectCollection(projectData map[string]interface{}) []*GitlabProject {
|
||||
gitlabProjects := []*GitlabProject{}
|
||||
|
||||
for name, namespace := range projectData {
|
||||
project := NewGitlabProject(name, namespace.(string), widget.gitlab)
|
||||
gitlabProjects = append(gitlabProjects, project)
|
||||
}
|
||||
|
||||
return gitlabProjects
|
||||
}
|
||||
|
||||
func (widget *Widget) currentGitlabProject() *GitlabProject {
|
||||
if len(widget.GitlabProjects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if widget.Idx < 0 || widget.Idx >= len(widget.GitlabProjects) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.GitlabProjects[widget.Idx]
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.Prev()
|
||||
return nil
|
||||
case "l":
|
||||
widget.Next()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyLeft:
|
||||
widget.Prev()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.Next()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
95
modules/gitter/client.go
Normal file
95
modules/gitter/client.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package gitter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wtfutil/wtf/logger"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetMessages(roomId string, numberOfMessages int) ([]Message, error) {
|
||||
var messages []Message
|
||||
|
||||
resp, err := apiRequest("rooms/" + roomId + "/chatMessages?limit=" + strconv.Itoa(numberOfMessages))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseJson(&messages, resp.Body)
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func GetRoom(roomUri string) (*Room, error) {
|
||||
var rooms Rooms
|
||||
|
||||
resp, err := apiRequest("rooms?q=" + roomUri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseJson(&rooms, resp.Body)
|
||||
|
||||
for _, room := range rooms.Results {
|
||||
logger.Log(fmt.Sprintf("room: %s", room))
|
||||
if room.URI == roomUri {
|
||||
return &room, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
apiBaseURL = "https://api.gitter.im/v1/"
|
||||
)
|
||||
|
||||
func apiRequest(path string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", apiBaseURL+path, nil)
|
||||
bearer := fmt.Sprintf("Bearer %s", apiToken())
|
||||
req.Header.Add("Authorization", bearer)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiToken() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.gitter.apiToken",
|
||||
os.Getenv("WTF_GITTER_API_TOKEN"),
|
||||
)
|
||||
}
|
||||
28
modules/gitter/gitter.go
Normal file
28
modules/gitter/gitter.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package gitter
|
||||
|
||||
import "time"
|
||||
|
||||
type Rooms struct {
|
||||
Results []Room `json:"results"`
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
Sent time.Time `json:"sent"`
|
||||
From User `json:"fromUser"`
|
||||
Unread bool `json:"unread"`
|
||||
}
|
||||
181
modules/gitter/widget.go
Normal file
181
modules/gitter/widget.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package gitter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Gitter:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next message in the list
|
||||
k: Select the previous message in the list
|
||||
r: Refresh the data
|
||||
|
||||
arrow down: Select the next message in the list
|
||||
arrow up: Select the previous message in the list
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
messages []Message
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Gitter", "gitter", true),
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetScrollable(true)
|
||||
widget.View.SetRegions(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
room, err := GetRoom(wtf.Config.UString("wtf.mods.gitter.roomUri", "wtfutil/Lobby"))
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if room == nil {
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := GetMessages(room.ID, wtf.Config.UInt("wtf.mods.gitter.numberOfMessages", 10))
|
||||
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
} else {
|
||||
widget.messages = messages
|
||||
}
|
||||
|
||||
widget.display()
|
||||
widget.View.ScrollToEnd()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.messages == nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.Clear()
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - %s", widget.Name, wtf.Config.UString("wtf.mods.gitter.roomUri", "wtfutil/Lobby"))))
|
||||
widget.View.SetText(widget.contentFrom(widget.messages))
|
||||
widget.View.Highlight(strconv.Itoa(widget.selected)).ScrollToHighlight()
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(messages []Message) string {
|
||||
var str string
|
||||
for idx, message := range messages {
|
||||
str = str + fmt.Sprintf(
|
||||
`["%d"][""][%s] [blue]%s [lightslategray]%s: [%s]%s [aqua]%s`,
|
||||
idx,
|
||||
widget.rowColor(idx),
|
||||
message.From.DisplayName,
|
||||
message.From.Username,
|
||||
widget.rowColor(idx),
|
||||
message.Text,
|
||||
message.Sent.Format("Jan 02, 15:04 MST"),
|
||||
)
|
||||
|
||||
str = str + "\n"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
|
||||
return wtf.RowColor("gitter", idx)
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.messages != nil && widget.selected >= len(widget.messages) {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.messages != nil {
|
||||
widget.selected = len(widget.messages) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openMessage() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.messages != nil && sel < len(widget.messages) {
|
||||
message := &widget.messages[widget.selected]
|
||||
wtf.OpenFile(message.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
widget.next()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prev()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
widget.next()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
widget.prev()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
145
modules/gspreadsheets/client.go
Normal file
145
modules/gspreadsheets/client.go
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* This butt-ugly code is direct from Google itself
|
||||
* https://developers.google.com/sheets/api/quickstart/go
|
||||
*/
|
||||
|
||||
package gspreadsheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
sheets "google.golang.org/api/sheets/v4"
|
||||
)
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func Fetch() ([]*sheets.ValueRange, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
secretPath, _ := wtf.ExpandHomeDir(wtf.Config.UString("wtf.mods.gspreadsheets.secretFile"))
|
||||
|
||||
b, err := ioutil.ReadFile(secretPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read secretPath. %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets.readonly")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get config from JSON. %v", err)
|
||||
return nil, err
|
||||
}
|
||||
client := getClient(ctx, config)
|
||||
|
||||
srv, err := sheets.New(client)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get create server. %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cells := wtf.ToStrs(wtf.Config.UList("wtf.mods.gspreadsheets.cells.addresses"))
|
||||
documentId := wtf.Config.UString("wtf.mods.gspreadsheets.sheetId")
|
||||
addresses := strings.Join(cells[:], ";")
|
||||
|
||||
responses := make([]*sheets.ValueRange, len(cells))
|
||||
|
||||
for i := 0; i < len(cells); i++ {
|
||||
resp, err := srv.Spreadsheets.Values.Get(documentId, cells[i]).Do()
|
||||
if err != nil {
|
||||
log.Fatalf("Error fetching cells %s", addresses)
|
||||
return nil, err
|
||||
}
|
||||
responses[i] = resp
|
||||
}
|
||||
|
||||
return responses, err
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
// getClient uses a Context and Config to retrieve a Token
|
||||
// then generate a Client. It returns the generated Client.
|
||||
func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
|
||||
cacheFile, err := tokenCacheFile()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get path to cached credential file. %v", err)
|
||||
}
|
||||
tok, err := tokenFromFile(cacheFile)
|
||||
if err != nil {
|
||||
tok = getTokenFromWeb(config)
|
||||
saveToken(cacheFile, tok)
|
||||
}
|
||||
return config.Client(ctx, tok)
|
||||
}
|
||||
|
||||
// getTokenFromWeb uses Config to request a Token.
|
||||
// It returns the retrieved Token.
|
||||
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
|
||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
fmt.Printf("Go to the following link in your browser then type the "+
|
||||
"authorization code: \n%v\n", authURL)
|
||||
|
||||
var code string
|
||||
if _, err := fmt.Scan(&code); err != nil {
|
||||
log.Fatalf("Unable to read authorization code %v", err)
|
||||
}
|
||||
|
||||
tok, err := config.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve token from web %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
// tokenCacheFile generates credential file path/filename.
|
||||
// It returns the generated credential path/filename.
|
||||
func tokenCacheFile() (string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
|
||||
os.MkdirAll(tokenCacheDir, 0700)
|
||||
return filepath.Join(tokenCacheDir,
|
||||
url.QueryEscape("spreadsheets-go-quickstart.json")), err
|
||||
}
|
||||
|
||||
// tokenFromFile retrieves a Token from a given file path.
|
||||
// It returns the retrieved Token and any read error encountered.
|
||||
func tokenFromFile(file string) (*oauth2.Token, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{}
|
||||
err = json.NewDecoder(f).Decode(t)
|
||||
defer f.Close()
|
||||
return t, err
|
||||
}
|
||||
|
||||
// saveToken uses a file path to create a file and store the
|
||||
// token in it.
|
||||
func saveToken(file string, token *oauth2.Token) {
|
||||
fmt.Printf("Saving credential file to: %s\n", file)
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to cache oauth token: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
json.NewEncoder(f).Encode(token)
|
||||
}
|
||||
47
modules/gspreadsheets/widget.go
Normal file
47
modules/gspreadsheets/widget.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package gspreadsheets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
sheets "google.golang.org/api/sheets/v4"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Google Spreadsheets", "gspreadsheets", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
cells, _ := Fetch()
|
||||
|
||||
widget.View.SetText(widget.contentFrom(cells))
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(valueRanges []*sheets.ValueRange) string {
|
||||
if valueRanges == nil {
|
||||
return "error 1"
|
||||
}
|
||||
|
||||
valuesColor := wtf.Config.UString("wtf.mods.gspreadsheets.colors.values", "green")
|
||||
res := ""
|
||||
|
||||
cells := wtf.ToStrs(wtf.Config.UList("wtf.mods.gspreadsheets.cells.names"))
|
||||
for i := 0; i < len(valueRanges); i++ {
|
||||
res = res + fmt.Sprintf("%s\t[%s]%s\n", cells[i], valuesColor, valueRanges[i].Values[0][0])
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
80
modules/hackernews/client.go
Normal file
80
modules/hackernews/client.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package hackernews
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetStories(storyType string) ([]int, error) {
|
||||
var storyIds []int
|
||||
|
||||
switch strings.ToLower(storyType) {
|
||||
case "new", "top", "job", "ask":
|
||||
resp, err := apiRequest(storyType + "stories")
|
||||
if err != nil {
|
||||
return storyIds, err
|
||||
}
|
||||
|
||||
parseJson(&storyIds, resp.Body)
|
||||
}
|
||||
|
||||
return storyIds, nil
|
||||
}
|
||||
|
||||
func GetStory(id int) (Story, error) {
|
||||
var story Story
|
||||
|
||||
resp, err := apiRequest("item/" + strconv.Itoa(id))
|
||||
if err != nil {
|
||||
return story, err
|
||||
}
|
||||
|
||||
parseJson(&story, resp.Body)
|
||||
|
||||
return story, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
apiEndpoint = "https://hacker-news.firebaseio.com/v0/"
|
||||
)
|
||||
|
||||
func apiRequest(path string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", apiEndpoint+path+".json", nil)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
modules/hackernews/story.go
Normal file
13
modules/hackernews/story.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package hackernews
|
||||
|
||||
type Story struct {
|
||||
By string `json:"by"`
|
||||
Descendants int `json:"descendants"`
|
||||
ID int `json:"id"`
|
||||
Kids []int `json:"kids"`
|
||||
Score int `json:"score"`
|
||||
Time int `json:"time"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
203
modules/hackernews/widget.go
Normal file
203
modules/hackernews/widget.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package hackernews
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Hacker News:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next story in the list
|
||||
k: Select the previous story in the list
|
||||
r: Refresh the data
|
||||
|
||||
arrow down: Select the next story in the list
|
||||
arrow up: Select the previous story in the list
|
||||
|
||||
return: Open the selected story in a browser
|
||||
c: Open the comments of the article
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
stories []Story
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Hacker News", "hackernews", true),
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetScrollable(true)
|
||||
widget.View.SetRegions(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
storyIds, err := GetStories(wtf.Config.UString("wtf.mods.hackernews.storyType", "top"))
|
||||
if storyIds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
} else {
|
||||
var stories []Story
|
||||
numberOfStoriesToDisplay := wtf.Config.UInt("wtf.mods.hackernews.numberOfStories", 10)
|
||||
for idx := 0; idx < numberOfStoriesToDisplay; idx++ {
|
||||
story, e := GetStory(storyIds[idx])
|
||||
if e != nil {
|
||||
panic(e)
|
||||
} else {
|
||||
stories = append(stories, story)
|
||||
}
|
||||
}
|
||||
|
||||
widget.stories = stories
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.stories == nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
widget.View.Clear()
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - %sstories", widget.Name, wtf.Config.UString("wtf.mods.hackernews.storyType", "top"))))
|
||||
widget.View.SetText(widget.contentFrom(widget.stories))
|
||||
widget.View.Highlight(strconv.Itoa(widget.selected)).ScrollToHighlight()
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(stories []Story) string {
|
||||
var str string
|
||||
for idx, story := range stories {
|
||||
u, _ := url.Parse(story.URL)
|
||||
str = str + fmt.Sprintf(
|
||||
`["%d"][""][%s] [yellow]%d. [%s]%s [blue](%s)`,
|
||||
idx,
|
||||
widget.rowColor(idx),
|
||||
idx+1,
|
||||
widget.rowColor(idx),
|
||||
story.Title,
|
||||
strings.TrimPrefix(u.Host, "www."),
|
||||
)
|
||||
|
||||
str = str + "\n"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
|
||||
return wtf.RowColor("hackernews", idx)
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.stories != nil && widget.selected >= len(widget.stories) {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.stories != nil {
|
||||
widget.selected = len(widget.stories) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openStory() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.stories != nil && sel < len(widget.stories) {
|
||||
story := &widget.stories[widget.selected]
|
||||
wtf.OpenFile(story.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) openComments() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.stories != nil && sel < len(widget.stories) {
|
||||
story := &widget.stories[widget.selected]
|
||||
wtf.OpenFile(fmt.Sprintf("https://news.ycombinator.com/item?id=%d", story.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
widget.next()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prev()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
case "c":
|
||||
widget.openComments()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
widget.next()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openStory()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
widget.prev()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
126
modules/ipaddresses/ipapi/widget.go
Normal file
126
modules/ipaddresses/ipapi/widget.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package ipapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
// Widget widget struct
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
result string
|
||||
colors struct {
|
||||
name, value string
|
||||
}
|
||||
}
|
||||
|
||||
type ipinfo struct {
|
||||
Query string `json:"query"`
|
||||
ISP string `json:"isp"`
|
||||
AS string `json:"as"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Latitude float64 `json:"lat"`
|
||||
Longitude float64 `json:"lon"`
|
||||
PostalCode string `json:"zip"`
|
||||
Organization string `json:"org"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
// NewWidget constructor
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "IPInfo", "ipapi", false),
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
widget.config()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
// Refresh refresh the module
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.ipinfo()
|
||||
widget.View.SetText(widget.result)
|
||||
}
|
||||
|
||||
//this method reads the config and calls ipinfo for ip information
|
||||
func (widget *Widget) ipinfo() {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://ip-api.com/json", nil)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "curl")
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var info ipinfo
|
||||
err = json.NewDecoder(response.Body).Decode(&info)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
widget.setResult(&info)
|
||||
}
|
||||
|
||||
// read module configs
|
||||
func (widget *Widget) config() {
|
||||
nameColor, valueColor := wtf.Config.UString("wtf.mods.ipinfo.colors.name", "red"), wtf.Config.UString("wtf.mods.ipinfo.colors.value", "white")
|
||||
widget.colors.name = nameColor
|
||||
widget.colors.value = valueColor
|
||||
}
|
||||
|
||||
func (widget *Widget) setResult(info *ipinfo) {
|
||||
resultTemplate, _ := template.New("ipinfo_result").Parse(
|
||||
formatableText("IP Address", "Ip") +
|
||||
formatableText("ISP", "ISP") +
|
||||
formatableText("AS", "AS") +
|
||||
formatableText("City", "City") +
|
||||
formatableText("Region", "Region") +
|
||||
formatableText("Country", "Country") +
|
||||
formatableText("Coordinates", "Coordinates") +
|
||||
formatableText("Postal Code", "PostalCode") +
|
||||
formatableText("Organization", "Organization") +
|
||||
formatableText("Timezone", "Timezone"),
|
||||
)
|
||||
|
||||
resultBuffer := new(bytes.Buffer)
|
||||
|
||||
resultTemplate.Execute(resultBuffer, map[string]string{
|
||||
"nameColor": widget.colors.name,
|
||||
"valueColor": widget.colors.value,
|
||||
"Ip": info.Query,
|
||||
"ISP": info.ISP,
|
||||
"AS": info.AS,
|
||||
"City": info.City,
|
||||
"Region": info.Region,
|
||||
"Country": info.Country,
|
||||
"Coordinates": strconv.FormatFloat(info.Latitude, 'f', 6, 64) + "," + strconv.FormatFloat(info.Longitude, 'f', 6, 64),
|
||||
"PostalCode": info.PostalCode,
|
||||
"Organization": info.Organization,
|
||||
"Timezone": info.Timezone,
|
||||
})
|
||||
|
||||
widget.result = resultBuffer.String()
|
||||
}
|
||||
|
||||
func formatableText(key, value string) string {
|
||||
return fmt.Sprintf(" [{{.nameColor}}]%s: [{{.valueColor}}]{{.%s}}\n", key, value)
|
||||
}
|
||||
115
modules/ipaddresses/ipinfo/widget.go
Normal file
115
modules/ipaddresses/ipinfo/widget.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package ipinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
result string
|
||||
colors struct {
|
||||
name, value string
|
||||
}
|
||||
}
|
||||
|
||||
type ipinfo struct {
|
||||
Ip string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Coordinates string `json:"loc"`
|
||||
PostalCode string `json:"postal"`
|
||||
Organization string `json:"org"`
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "IPInfo", "ipinfo", false),
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
widget.config()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.ipinfo()
|
||||
widget.View.Clear()
|
||||
|
||||
widget.View.SetText(widget.result)
|
||||
}
|
||||
|
||||
//this method reads the config and calls ipinfo for ip information
|
||||
func (widget *Widget) ipinfo() {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://ipinfo.io/", nil)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "curl")
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var info ipinfo
|
||||
err = json.NewDecoder(response.Body).Decode(&info)
|
||||
if err != nil {
|
||||
widget.result = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
widget.setResult(&info)
|
||||
}
|
||||
|
||||
// read module configs
|
||||
func (widget *Widget) config() {
|
||||
widget.colors.name = wtf.Config.UString("wtf.mods.ipinfo.colors.name", "white")
|
||||
widget.colors.value = wtf.Config.UString("wtf.mods.ipinfo.colors.value", "white")
|
||||
}
|
||||
|
||||
func (widget *Widget) setResult(info *ipinfo) {
|
||||
resultTemplate, _ := template.New("ipinfo_result").Parse(
|
||||
formatableText("IP", "Ip") +
|
||||
formatableText("Hostname", "Hostname") +
|
||||
formatableText("City", "City") +
|
||||
formatableText("Region", "Region") +
|
||||
formatableText("Country", "Country") +
|
||||
formatableText("Coords", "Coordinates") +
|
||||
formatableText("Org", "Organization"),
|
||||
)
|
||||
|
||||
resultBuffer := new(bytes.Buffer)
|
||||
|
||||
resultTemplate.Execute(resultBuffer, map[string]string{
|
||||
"nameColor": widget.colors.name,
|
||||
"valueColor": widget.colors.value,
|
||||
"Ip": info.Ip,
|
||||
"Hostname": info.Hostname,
|
||||
"City": info.City,
|
||||
"Region": info.Region,
|
||||
"Country": info.Country,
|
||||
"Coordinates": info.Coordinates,
|
||||
"PostalCode": info.PostalCode,
|
||||
"Organization": info.Organization,
|
||||
})
|
||||
|
||||
widget.result = resultBuffer.String()
|
||||
}
|
||||
|
||||
func formatableText(key, value string) string {
|
||||
return fmt.Sprintf(" [{{.nameColor}}]%8s: [{{.valueColor}}]{{.%s}}\n", key, value)
|
||||
}
|
||||
73
modules/jenkins/client.go
Normal file
73
modules/jenkins/client.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package jenkins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func Create(jenkinsURL string, username string, apiKey string) (*View, error) {
|
||||
const apiSuffix = "api/json?pretty=true"
|
||||
parsedSuffix, err := url.Parse(apiSuffix)
|
||||
if err != nil {
|
||||
return &View{}, err
|
||||
}
|
||||
|
||||
parsedJenkinsURL, err := url.Parse(ensureLastSlash(jenkinsURL))
|
||||
if err != nil {
|
||||
return &View{}, err
|
||||
}
|
||||
jenkinsAPIURL := parsedJenkinsURL.ResolveReference(parsedSuffix)
|
||||
|
||||
req, _ := http.NewRequest("GET", jenkinsAPIURL.String(), nil)
|
||||
req.SetBasicAuth(username, apiKey)
|
||||
|
||||
verifyServerCertificate := wtf.Config.UBool("wtf.mods.jenkins.verifyServerCertificate", true)
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !verifyServerCertificate,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return &View{}, err
|
||||
}
|
||||
|
||||
view := &View{}
|
||||
parseJson(view, resp.Body)
|
||||
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func ensureLastSlash(URL string) string {
|
||||
return strings.TrimRight(URL, "/") + "/"
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
modules/jenkins/job.go
Normal file
8
modules/jenkins/job.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package jenkins
|
||||
|
||||
type Job struct {
|
||||
Class string `json:"_class"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
10
modules/jenkins/view.go
Normal file
10
modules/jenkins/view.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package jenkins
|
||||
|
||||
type View struct {
|
||||
Class string `json:"_class"`
|
||||
Description string `json:"description"`
|
||||
Jobs []Job `json:"jobs"`
|
||||
Name string `json:"name"`
|
||||
Property []string `json:"property"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
193
modules/jenkins/widget.go
Normal file
193
modules/jenkins/widget.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package jenkins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Jenkins:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next job in the list
|
||||
k: Select the previous job in the list
|
||||
r: Refresh the data
|
||||
|
||||
arrow down: Select the next job in the list
|
||||
arrow up: Select the previous job in the list
|
||||
|
||||
return: Open the selected job in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
view *View
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Jenkins", "jenkins", true),
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetScrollable(true)
|
||||
widget.View.SetRegions(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
view, err := Create(
|
||||
wtf.Config.UString("wtf.mods.jenkins.url"),
|
||||
wtf.Config.UString("wtf.mods.jenkins.user"),
|
||||
widget.apiKey(),
|
||||
)
|
||||
widget.view = view
|
||||
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.ContextualTitle(widget.Name))
|
||||
widget.View.SetText(err.Error())
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.view == nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
widget.View.Clear()
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s: [red]%s", widget.Name, widget.view.Name)))
|
||||
widget.View.SetText(widget.contentFrom(widget.view))
|
||||
widget.View.Highlight(strconv.Itoa(widget.selected)).ScrollToHighlight()
|
||||
}
|
||||
|
||||
func (widget *Widget) apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.jenkins.apiKey",
|
||||
os.Getenv("WTF_JENKINS_API_KEY"),
|
||||
)
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(view *View) string {
|
||||
var str string
|
||||
for idx, job := range view.Jobs {
|
||||
str = str + fmt.Sprintf(
|
||||
`["%d"][""][%s] [%s]%-6s[white]`,
|
||||
idx,
|
||||
widget.rowColor(idx),
|
||||
widget.jobColor(&job),
|
||||
job.Name,
|
||||
)
|
||||
|
||||
str = str + "\n"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
|
||||
return wtf.DefaultRowColor()
|
||||
}
|
||||
|
||||
func (widget *Widget) jobColor(job *Job) string {
|
||||
switch job.Color {
|
||||
case "blue":
|
||||
return "blue"
|
||||
case "red":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.view != nil && widget.selected >= len(widget.view.Jobs) {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.view != nil {
|
||||
widget.selected = len(widget.view.Jobs) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openJob() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.view != nil && sel < len(widget.view.Jobs) {
|
||||
job := &widget.view.Jobs[widget.selected]
|
||||
wtf.OpenFile(job.Url)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
widget.next()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prev()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
widget.next()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openJob()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
widget.prev()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
123
modules/jira/client.go
Normal file
123
modules/jira/client.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func IssuesFor(username string, projects []string, jql string) (*SearchResult, error) {
|
||||
query := []string{}
|
||||
|
||||
var projQuery = getProjectQuery(projects)
|
||||
if projQuery != "" {
|
||||
query = append(query, projQuery)
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
query = append(query, buildJql("assignee", username))
|
||||
}
|
||||
|
||||
if jql != "" {
|
||||
query = append(query, jql)
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
|
||||
v.Set("jql", strings.Join(query, " AND "))
|
||||
|
||||
url := fmt.Sprintf("/rest/api/2/search?%s", v.Encode())
|
||||
|
||||
resp, err := jiraRequest(url)
|
||||
if err != nil {
|
||||
return &SearchResult{}, err
|
||||
}
|
||||
|
||||
searchResult := &SearchResult{}
|
||||
parseJson(searchResult, resp.Body)
|
||||
|
||||
return searchResult, nil
|
||||
}
|
||||
|
||||
func buildJql(key string, value string) string {
|
||||
return fmt.Sprintf("%s = \"%s\"", key, value)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.jira.apiKey",
|
||||
os.Getenv("WTF_JIRA_API_KEY"),
|
||||
)
|
||||
}
|
||||
|
||||
func jiraRequest(path string) (*http.Response, error) {
|
||||
url := fmt.Sprintf("%s%s", wtf.Config.UString("wtf.mods.jira.domain"), path)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(wtf.Config.UString("wtf.mods.jira.email"), apiKey())
|
||||
|
||||
verifyServerCertificate := wtf.Config.UBool("wtf.mods.jira.verifyServerCertificate", true)
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !verifyServerCertificate,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getProjectQuery(projects []string) string {
|
||||
singleEmptyProject := len(projects) == 1 && len(projects[0]) == 0
|
||||
if len(projects) == 0 || singleEmptyProject {
|
||||
return ""
|
||||
} else if len(projects) == 1 {
|
||||
return buildJql("project", projects[0])
|
||||
}
|
||||
|
||||
quoted := make([]string, len(projects))
|
||||
for i := range projects {
|
||||
quoted[i] = fmt.Sprintf("\"%s\"", projects[i])
|
||||
}
|
||||
return fmt.Sprintf("project in (%s)", strings.Join(quoted, ", "))
|
||||
}
|
||||
32
modules/jira/issues.go
Normal file
32
modules/jira/issues.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package jira
|
||||
|
||||
type Issue struct {
|
||||
Expand string `json:"expand"`
|
||||
ID string `json:"id"`
|
||||
Self string `json:"self"`
|
||||
Key string `json:"key"`
|
||||
|
||||
IssueFields *IssueFields `json:"fields"`
|
||||
}
|
||||
|
||||
type IssueFields struct {
|
||||
Summary string `json:"summary"`
|
||||
|
||||
IssueType *IssueType `json:"issuetype"`
|
||||
IssueStatus *IssueStatus `json:"status"`
|
||||
}
|
||||
|
||||
type IssueType struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
Name string `json:"name"`
|
||||
Subtask bool `json:"subtask"`
|
||||
}
|
||||
|
||||
type IssueStatus struct {
|
||||
ISelf string `json:"self"`
|
||||
IDescription string `json:"description"`
|
||||
IName string `json:"name"`
|
||||
}
|
||||
8
modules/jira/search_result.go
Normal file
8
modules/jira/search_result.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package jira
|
||||
|
||||
type SearchResult struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Issues []Issue `json:"issues"`
|
||||
}
|
||||
213
modules/jira/widget.go
Normal file
213
modules/jira/widget.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Jira:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next item in the list
|
||||
k: Select the previous item in the list
|
||||
|
||||
arrow down: Select the next item in the list
|
||||
arrow up: Select the previous item in the list
|
||||
|
||||
return: Open the selected issue in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
result *SearchResult
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Jira", "jira", true),
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetScrollable(true)
|
||||
widget.View.SetRegions(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
searchResult, err := IssuesFor(
|
||||
wtf.Config.UString("wtf.mods.jira.username"),
|
||||
getProjects(),
|
||||
wtf.Config.UString("wtf.mods.jira.jql", ""),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
widget.result = nil
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
} else {
|
||||
widget.result = searchResult
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.result == nil {
|
||||
return
|
||||
}
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
str := fmt.Sprintf("%s- [green]%s[white]", widget.Name, wtf.Config.UString("wtf.mods.jira.project"))
|
||||
|
||||
widget.View.Clear()
|
||||
widget.View.SetTitle(widget.ContextualTitle(str))
|
||||
widget.View.SetText(fmt.Sprintf("%s", widget.contentFrom(widget.result)))
|
||||
widget.View.Highlight(strconv.Itoa(widget.selected)).ScrollToHighlight()
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.result != nil && widget.selected >= len(widget.result.Issues) {
|
||||
widget.selected = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.result != nil {
|
||||
widget.selected = len(widget.result.Issues) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) openItem() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.result != nil && sel < len(widget.result.Issues) {
|
||||
issue := &widget.result.Issues[widget.selected]
|
||||
wtf.OpenFile(wtf.Config.UString("wtf.mods.jira.domain") + "/browse/" + issue.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(searchResult *SearchResult) string {
|
||||
str := " [red]Assigned Issues[white]\n"
|
||||
|
||||
for idx, issue := range searchResult.Issues {
|
||||
fmtStr := fmt.Sprintf(
|
||||
`["%d"][""][%s] [%s]%-6s[white] [green]%-10s[white] [yellow][%s][white] [%s]%s`,
|
||||
idx,
|
||||
widget.rowColor(idx),
|
||||
widget.issueTypeColor(&issue),
|
||||
issue.IssueFields.IssueType.Name,
|
||||
issue.Key,
|
||||
issue.IssueFields.IssueStatus.IName,
|
||||
widget.rowColor(idx),
|
||||
issue.IssueFields.Summary,
|
||||
)
|
||||
|
||||
_, _, w, _ := widget.View.GetInnerRect()
|
||||
fmtStr = fmtStr + wtf.PadRow(len(issue.IssueFields.Summary), w+1)
|
||||
|
||||
str = str + fmtStr + "\n"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
return wtf.RowColor("jira", idx)
|
||||
}
|
||||
|
||||
func (widget *Widget) issueTypeColor(issue *Issue) string {
|
||||
switch issue.IssueFields.IssueType.Name {
|
||||
case "Bug":
|
||||
return "red"
|
||||
case "Story":
|
||||
return "blue"
|
||||
case "Task":
|
||||
return "orange"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
func getProjects() []string {
|
||||
// see if project is set to a single string
|
||||
configPath := "wtf.mods.jira.project"
|
||||
singleProject, err := wtf.Config.String(configPath)
|
||||
if err == nil {
|
||||
return []string{singleProject}
|
||||
}
|
||||
// else, assume list
|
||||
projList := wtf.Config.UList(configPath)
|
||||
var ret []string
|
||||
for _, proj := range projList {
|
||||
if str, ok := proj.(string); ok {
|
||||
ret = append(ret, str)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
// Select the next item down
|
||||
widget.next()
|
||||
widget.display()
|
||||
return nil
|
||||
case "k":
|
||||
// Select the next item up
|
||||
widget.prev()
|
||||
widget.display()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
// Select the next item down
|
||||
widget.next()
|
||||
widget.display()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openItem()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
// Unselect the current row
|
||||
widget.unselect()
|
||||
widget.display()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
// Select the next item up
|
||||
widget.prev()
|
||||
widget.display()
|
||||
return nil
|
||||
default:
|
||||
// Pass it along
|
||||
return event
|
||||
}
|
||||
}
|
||||
83
modules/mercurial/display.go
Normal file
83
modules/mercurial/display.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package mercurial
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func (widget *Widget) display() {
|
||||
repoData := widget.currentData()
|
||||
if repoData == nil {
|
||||
widget.View.SetText(" Mercurial repo data is unavailable ")
|
||||
return
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%s - [green]%s[white]", widget.Name, repoData.Repository)
|
||||
widget.View.SetTitle(widget.ContextualTitle(title))
|
||||
|
||||
str := wtf.SigilStr(len(widget.Data), widget.Idx, widget.View) + "\n"
|
||||
str = str + " [red]Branch:Bookmark[white]\n"
|
||||
str = str + fmt.Sprintf(" %s:%s\n", repoData.Branch, repoData.Bookmark)
|
||||
str = str + "\n"
|
||||
str = str + widget.formatChanges(repoData.ChangedFiles)
|
||||
str = str + "\n"
|
||||
str = str + widget.formatCommits(repoData.Commits)
|
||||
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
|
||||
func (widget *Widget) formatChanges(data []string) string {
|
||||
str := ""
|
||||
str = str + " [red]Changed Files[white]\n"
|
||||
|
||||
if len(data) == 1 {
|
||||
str = str + " [grey]none[white]\n"
|
||||
} else {
|
||||
for _, line := range data {
|
||||
str = str + widget.formatChange(line)
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) formatChange(line string) string {
|
||||
if len(line) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
firstChar, _ := utf8.DecodeRuneInString(line)
|
||||
|
||||
// Revisit this and kill the ugly duplication
|
||||
switch firstChar {
|
||||
case 'A':
|
||||
line = strings.Replace(line, "A", "[green]A[white]", 1)
|
||||
case 'D':
|
||||
line = strings.Replace(line, "D", "[red]D[white]", 1)
|
||||
case 'M':
|
||||
line = strings.Replace(line, "M", "[yellow]M[white]", 1)
|
||||
case 'R':
|
||||
line = strings.Replace(line, "R", "[purple]R[white]", 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %s\n", strings.Replace(line, "\"", "", -1))
|
||||
}
|
||||
|
||||
func (widget *Widget) formatCommits(data []string) string {
|
||||
str := ""
|
||||
str = str + " [red]Recent Commits[white]\n"
|
||||
|
||||
for _, line := range data {
|
||||
str = str + widget.formatCommit(line)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) formatCommit(line string) string {
|
||||
return fmt.Sprintf(" %s\n", strings.Replace(line, "\"", "", -1))
|
||||
}
|
||||
96
modules/mercurial/hg_repo.go
Normal file
96
modules/mercurial/hg_repo.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package mercurial
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type MercurialRepo struct {
|
||||
Branch string
|
||||
Bookmark string
|
||||
ChangedFiles []string
|
||||
Commits []string
|
||||
Repository string
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewMercurialRepo(repoPath string) *MercurialRepo {
|
||||
repo := MercurialRepo{Path: repoPath}
|
||||
|
||||
repo.Branch = strings.TrimSpace(repo.branch())
|
||||
repo.Bookmark = strings.TrimSpace(repo.bookmark())
|
||||
repo.ChangedFiles = repo.changedFiles()
|
||||
repo.Commits = repo.commits()
|
||||
repo.Repository = strings.TrimSpace(repo.Path)
|
||||
|
||||
return &repo
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (repo *MercurialRepo) branch() string {
|
||||
arg := []string{"branch", repo.repoPath()}
|
||||
|
||||
cmd := exec.Command("hg", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) bookmark() string {
|
||||
bookmark, err := ioutil.ReadFile(path.Join(repo.Path, ".hg", "bookmarks.current"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(bookmark)
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) changedFiles() []string {
|
||||
arg := []string{"status", repo.repoPath()}
|
||||
|
||||
cmd := exec.Command("hg", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
data := strings.Split(str, "\n")
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) commits() []string {
|
||||
numStr := fmt.Sprintf("-l %d", wtf.Config.UInt("wtf.mods.mercurial.commitCount", 10))
|
||||
|
||||
commitFormat := wtf.Config.UString("wtf.mods.mercurial.commitFormat", "[forestgreen]{rev}:{phase} [white]{desc|firstline|strip} [grey]{author|person} {date|age}[white]")
|
||||
commitStr := fmt.Sprintf("--template=\"%s\n\"", commitFormat)
|
||||
|
||||
arg := []string{"log", repo.repoPath(), numStr, commitStr}
|
||||
|
||||
cmd := exec.Command("hg", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
data := strings.Split(str, "\n")
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) pull() string {
|
||||
arg := []string{"pull", repo.repoPath()}
|
||||
cmd := exec.Command("hg", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) checkout(branch string) string {
|
||||
arg := []string{"checkout", repo.repoPath(), branch}
|
||||
cmd := exec.Command("hg", arg...)
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
return str
|
||||
}
|
||||
|
||||
func (repo *MercurialRepo) repoPath() string {
|
||||
return fmt.Sprintf("--repository=%s", repo.Path)
|
||||
}
|
||||
195
modules/mercurial/widget.go
Normal file
195
modules/mercurial/widget.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package mercurial
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Mercurial:
|
||||
|
||||
/: Show/hide this help window
|
||||
c: Checkout to branch
|
||||
h: Previous mercurial repository
|
||||
l: Next mercurial repository
|
||||
p: Pull current mercurial repository
|
||||
|
||||
arrow left: Previous mercurial repository
|
||||
arrow right: Next mercurial repository
|
||||
`
|
||||
|
||||
const offscreen = -1000
|
||||
const modalWidth = 80
|
||||
const modalHeight = 7
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.MultiSourceWidget
|
||||
wtf.TextWidget
|
||||
|
||||
app *tview.Application
|
||||
Data []*MercurialRepo
|
||||
pages *tview.Pages
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
MultiSourceWidget: wtf.NewMultiSourceWidget("mercurial", "repository", "repositories"),
|
||||
TextWidget: wtf.NewTextWidget(app, "Mercurial", "mercurial", true),
|
||||
|
||||
app: app,
|
||||
pages: pages,
|
||||
}
|
||||
|
||||
widget.LoadSources()
|
||||
widget.SetDisplayFunction(widget.display)
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Checkout() {
|
||||
form := widget.modalForm("Branch to checkout:", "")
|
||||
|
||||
checkoutFctn := func() {
|
||||
text := form.GetFormItem(0).(*tview.InputField).GetText()
|
||||
repoToCheckout := widget.Data[widget.Idx]
|
||||
repoToCheckout.checkout(text)
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
widget.Refresh()
|
||||
}
|
||||
|
||||
widget.addButtons(form, checkoutFctn)
|
||||
widget.modalFocus(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) Pull() {
|
||||
repoToPull := widget.Data[widget.Idx]
|
||||
repoToPull.pull()
|
||||
widget.Refresh()
|
||||
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
repoPaths := wtf.ToStrs(wtf.Config.UList("wtf.mods.mercurial.repositories"))
|
||||
|
||||
widget.Data = widget.mercurialRepos(repoPaths)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) addCheckoutButton(form *tview.Form, fctn func()) {
|
||||
form.AddButton("Checkout", fctn)
|
||||
}
|
||||
|
||||
func (widget *Widget) addButtons(form *tview.Form, checkoutFctn func()) {
|
||||
widget.addCheckoutButton(form, checkoutFctn)
|
||||
widget.addCancelButton(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) addCancelButton(form *tview.Form) {
|
||||
cancelFn := func() {
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
form.AddButton("Cancel", cancelFn)
|
||||
form.SetCancelFunc(cancelFn)
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFocus(form *tview.Form) {
|
||||
frame := widget.modalFrame(form)
|
||||
widget.pages.AddPage("modal", frame, false, true)
|
||||
widget.app.SetFocus(frame)
|
||||
}
|
||||
|
||||
func (widget *Widget) modalForm(lbl, text string) *tview.Form {
|
||||
form := tview.NewForm().
|
||||
SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonTextColor(tview.Styles.PrimaryTextColor)
|
||||
|
||||
form.AddInputField(lbl, text, 60, nil, nil)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {
|
||||
frame := tview.NewFrame(form).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
frame.SetRect(offscreen, offscreen, modalWidth, modalHeight)
|
||||
frame.SetBorder(true)
|
||||
frame.SetBorders(1, 1, 0, 0, 1, 1)
|
||||
|
||||
drawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
|
||||
w, h := screen.Size()
|
||||
frame.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
frame.SetDrawFunc(drawFunc)
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func (widget *Widget) currentData() *MercurialRepo {
|
||||
if len(widget.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if widget.Idx < 0 || widget.Idx >= len(widget.Data) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.Data[widget.Idx]
|
||||
}
|
||||
|
||||
func (widget *Widget) mercurialRepos(repoPaths []string) []*MercurialRepo {
|
||||
repos := []*MercurialRepo{}
|
||||
|
||||
for _, repoPath := range repoPaths {
|
||||
repo := NewMercurialRepo(repoPath)
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
return repos
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.Prev()
|
||||
return nil
|
||||
case "l":
|
||||
widget.Next()
|
||||
return nil
|
||||
case "p":
|
||||
widget.Pull()
|
||||
return nil
|
||||
case "c":
|
||||
widget.Checkout()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyLeft:
|
||||
widget.Prev()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.Next()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
38
modules/newrelic/client.go
Normal file
38
modules/newrelic/client.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package newrelic
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
nr "github.com/yfronto/newrelic"
|
||||
)
|
||||
|
||||
func Application() (*nr.Application, error) {
|
||||
client := nr.NewClient(apiKey())
|
||||
|
||||
application, err := client.GetApplication(wtf.Config.UInt("wtf.mods.newrelic.applicationId"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return application, nil
|
||||
}
|
||||
|
||||
func Deployments() ([]nr.ApplicationDeployment, error) {
|
||||
client := nr.NewClient(apiKey())
|
||||
|
||||
opts := &nr.ApplicationDeploymentOptions{Page: 1}
|
||||
deployments, err := client.GetApplicationDeployments(wtf.Config.UInt("wtf.mods.newrelic.applicationId"), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deployments, nil
|
||||
}
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.newrelic.apiKey",
|
||||
os.Getenv("WTF_NEW_RELIC_API_KEY"),
|
||||
)
|
||||
}
|
||||
88
modules/newrelic/widget.go
Normal file
88
modules/newrelic/widget.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package newrelic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
nr "github.com/yfronto/newrelic"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "New Relic", "newrelic", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
app, appErr := Application()
|
||||
deploys, depErr := Deployments()
|
||||
|
||||
appName := "error"
|
||||
if appErr == nil {
|
||||
appName = app.Name
|
||||
}
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - [green]%s[white]", widget.Name, appName)))
|
||||
widget.View.Clear()
|
||||
|
||||
var content string
|
||||
if depErr != nil {
|
||||
widget.View.SetWrap(true)
|
||||
content = depErr.Error()
|
||||
} else {
|
||||
widget.View.SetWrap(false)
|
||||
content = widget.contentFrom(deploys)
|
||||
}
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(deploys []nr.ApplicationDeployment) string {
|
||||
str := fmt.Sprintf(
|
||||
" %s\n",
|
||||
"[red]Latest Deploys[white]",
|
||||
)
|
||||
|
||||
revisions := []string{}
|
||||
|
||||
for _, deploy := range deploys {
|
||||
if (deploy.Revision != "") && wtf.Exclude(revisions, deploy.Revision) {
|
||||
lineColor := "white"
|
||||
if wtf.IsToday(deploy.Timestamp) {
|
||||
lineColor = "lightblue"
|
||||
}
|
||||
|
||||
revLen := 8
|
||||
if revLen > len(deploy.Revision) {
|
||||
revLen = len(deploy.Revision)
|
||||
}
|
||||
|
||||
str = str + fmt.Sprintf(
|
||||
" [green]%s[%s] %s %-.16s[white]\n",
|
||||
deploy.Revision[0:revLen],
|
||||
lineColor,
|
||||
deploy.Timestamp.Format("Jan 02 15:04 MST"),
|
||||
wtf.NameFromEmail(deploy.User),
|
||||
)
|
||||
|
||||
revisions = append(revisions, deploy.Revision)
|
||||
|
||||
if len(revisions) == wtf.Config.UInt("wtf.mods.newrelic.deployCount", 5) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
76
modules/opsgenie/client.go
Normal file
76
modules/opsgenie/client.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package opsgenie
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type OnCallResponse struct {
|
||||
OnCallData OnCallData `json:"data"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"requestId"`
|
||||
Took float32 `json:"took"`
|
||||
}
|
||||
|
||||
type OnCallData struct {
|
||||
Recipients []string `json:"onCallRecipients"`
|
||||
Parent Parent `json:"_parent"`
|
||||
}
|
||||
|
||||
type Parent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func Fetch(scheduleIdentifierType string, schedules []string) ([]*OnCallResponse, error) {
|
||||
agregatedResponses := []*OnCallResponse{}
|
||||
for _, sched := range schedules {
|
||||
scheduleUrl := fmt.Sprintf("https://api.opsgenie.com/v2/schedules/%s/on-calls?scheduleIdentifierType=%s&flat=true", sched, scheduleIdentifierType)
|
||||
response, err := opsGenieRequest(scheduleUrl, apiKey())
|
||||
agregatedResponses = append(agregatedResponses, response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return agregatedResponses, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.opsgenie.apiKey",
|
||||
os.Getenv("WTF_OPS_GENIE_API_KEY"),
|
||||
)
|
||||
}
|
||||
|
||||
func opsGenieRequest(url string, apiKey string) (*OnCallResponse, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
response := &OnCallResponse{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
91
modules/opsgenie/widget.go
Normal file
91
modules/opsgenie/widget.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package opsgenie
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "OpsGenie", "opsgenie", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
data, err := Fetch(
|
||||
wtf.Config.UString("wtf.mods.opsgenie.scheduleIdentifierType"),
|
||||
getSchedules(),
|
||||
)
|
||||
widget.View.SetTitle(widget.ContextualTitle(widget.Name))
|
||||
|
||||
var content string
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
content = err.Error()
|
||||
} else {
|
||||
widget.View.SetWrap(false)
|
||||
content = widget.contentFrom(data)
|
||||
}
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func getSchedules() []string {
|
||||
// see if schedule is set to a single string
|
||||
configPath := "wtf.mods.opsgenie.schedule"
|
||||
singleSchedule, err := wtf.Config.String(configPath)
|
||||
if err == nil {
|
||||
return []string{singleSchedule}
|
||||
}
|
||||
// else, assume list
|
||||
scheduleList := wtf.Config.UList(configPath)
|
||||
var ret []string
|
||||
for _, schedule := range scheduleList {
|
||||
if str, ok := schedule.(string); ok {
|
||||
ret = append(ret, str)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(onCallResponses []*OnCallResponse) string {
|
||||
str := ""
|
||||
|
||||
displayEmpty := wtf.Config.UBool("wtf.mods.opsgenie.displayEmpty", true)
|
||||
|
||||
for _, data := range onCallResponses {
|
||||
if (len(data.OnCallData.Recipients) == 0) && (displayEmpty == false) {
|
||||
continue
|
||||
}
|
||||
|
||||
var msg string
|
||||
if len(data.OnCallData.Recipients) == 0 {
|
||||
msg = " [gray]no one[white]\n\n"
|
||||
} else {
|
||||
msg = fmt.Sprintf(" %s\n\n", strings.Join(wtf.NamesFromEmails(data.OnCallData.Recipients), ", "))
|
||||
}
|
||||
|
||||
str = str + widget.cleanScheduleName(data.OnCallData.Parent.Name)
|
||||
str = str + msg
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) cleanScheduleName(schedule string) string {
|
||||
cleanedName := strings.Replace(schedule, "_", " ", -1)
|
||||
return fmt.Sprintf(" [green]%s[white]\n", cleanedName)
|
||||
}
|
||||
73
modules/pagerduty/client.go
Normal file
73
modules/pagerduty/client.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package pagerduty
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/PagerDuty/go-pagerduty"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
// GetOnCalls returns a list of people currently on call
|
||||
func GetOnCalls() ([]pagerduty.OnCall, error) {
|
||||
client := pagerduty.NewClient(apiKey())
|
||||
|
||||
var results []pagerduty.OnCall
|
||||
|
||||
var queryOpts pagerduty.ListOnCallOptions
|
||||
queryOpts.Since = time.Now().Format("2006-01-02T15:04:05Z07:00")
|
||||
queryOpts.Until = time.Now().Format("2006-01-02T15:04:05Z07:00")
|
||||
|
||||
oncalls, err := client.ListOnCalls(queryOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results = append(results, oncalls.OnCalls...)
|
||||
|
||||
for oncalls.APIListObject.More == true {
|
||||
queryOpts.APIListObject.Offset = oncalls.APIListObject.Offset
|
||||
oncalls, err = client.ListOnCalls(queryOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, oncalls.OnCalls...)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetIncidents returns a list of people currently on call
|
||||
func GetIncidents() ([]pagerduty.Incident, error) {
|
||||
client := pagerduty.NewClient(apiKey())
|
||||
|
||||
var results []pagerduty.Incident
|
||||
|
||||
var queryOpts pagerduty.ListIncidentsOptions
|
||||
queryOpts.DateRange = "all"
|
||||
queryOpts.Statuses = []string{"triggered", "acknowledged"}
|
||||
|
||||
items, err := client.ListIncidents(queryOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, items.Incidents...)
|
||||
|
||||
for items.APIListObject.More == true {
|
||||
queryOpts.APIListObject.Offset = items.APIListObject.Offset
|
||||
items, err = client.ListIncidents(queryOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, items.Incidents...)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func apiKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.pagerduty.apiKey",
|
||||
os.Getenv("WTF_PAGERDUTY_API_KEY"),
|
||||
)
|
||||
}
|
||||
12
modules/pagerduty/sort.go
Normal file
12
modules/pagerduty/sort.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package pagerduty
|
||||
|
||||
import "github.com/PagerDuty/go-pagerduty"
|
||||
|
||||
type ByEscalationLevel []pagerduty.OnCall
|
||||
|
||||
func (s ByEscalationLevel) Len() int { return len(s) }
|
||||
func (s ByEscalationLevel) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
func (s ByEscalationLevel) Less(i, j int) bool {
|
||||
return s[i].EscalationLevel < s[j].EscalationLevel
|
||||
}
|
||||
113
modules/pagerduty/widget.go
Normal file
113
modules/pagerduty/widget.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package pagerduty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/PagerDuty/go-pagerduty"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "PagerDuty", "pagerduty", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
var onCalls []pagerduty.OnCall
|
||||
var incidents []pagerduty.Incident
|
||||
|
||||
var err1 error
|
||||
var err2 error
|
||||
|
||||
if wtf.Config.UBool("wtf.mods.pagerduty.showSchedules", true) {
|
||||
onCalls, err1 = GetOnCalls()
|
||||
}
|
||||
|
||||
if wtf.Config.UBool("wtf.mods.pagerduty.showIncidents") {
|
||||
incidents, err2 = GetIncidents()
|
||||
}
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s", widget.Name)))
|
||||
widget.View.Clear()
|
||||
|
||||
var content string
|
||||
if err1 != nil || err2 != nil {
|
||||
widget.View.SetWrap(true)
|
||||
if err1 != nil {
|
||||
content = content + err1.Error()
|
||||
}
|
||||
if err2 != nil {
|
||||
content = content + err2.Error()
|
||||
}
|
||||
} else {
|
||||
widget.View.SetWrap(false)
|
||||
content = widget.contentFrom(onCalls, incidents)
|
||||
}
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(onCalls []pagerduty.OnCall, incidents []pagerduty.Incident) string {
|
||||
var str string
|
||||
|
||||
if len(incidents) > 0 {
|
||||
str = str + "[yellow]Incidents[white]\n"
|
||||
for _, incident := range incidents {
|
||||
str = str + fmt.Sprintf("[red]%s[white]\n", incident.Summary)
|
||||
str = str + fmt.Sprintf("Status: %s\n", incident.Status)
|
||||
str = str + fmt.Sprintf("Service: %s\n", incident.Service.Summary)
|
||||
str = str + fmt.Sprintf("Escalation: %s\n", incident.EscalationPolicy.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
tree := make(map[string][]pagerduty.OnCall)
|
||||
|
||||
filtering := wtf.Config.UList("wtf.mods.pagerduty.escalationFilter")
|
||||
filter := make(map[string]bool)
|
||||
for _, item := range filtering {
|
||||
filter[item.(string)] = true
|
||||
}
|
||||
|
||||
for _, onCall := range onCalls {
|
||||
key := onCall.EscalationPolicy.Summary
|
||||
if len(filtering) == 0 || filter[key] {
|
||||
tree[key] = append(tree[key], onCall)
|
||||
}
|
||||
}
|
||||
|
||||
// We want to sort our escalation policies for predictability/ease of finding
|
||||
keys := make([]string, 0, len(tree))
|
||||
for k := range tree {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) > 0 {
|
||||
str = str + "[yellow]Schedules[white]\n"
|
||||
// Print out policies, and escalation order of users
|
||||
for _, key := range keys {
|
||||
str = str + fmt.Sprintf("[red]%s\n", key)
|
||||
values := tree[key]
|
||||
sort.Sort(ByEscalationLevel(values))
|
||||
for _, item := range values {
|
||||
str = str + fmt.Sprintf("[white]%d - %s\n", item.EscalationLevel, item.User.Summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
118
modules/power/battery.go
Normal file
118
modules/power/battery.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// +build !linux
|
||||
|
||||
package power
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const TimeRegExp = "^(?:\\d|[01]\\d|2[0-3]):[0-5]\\d"
|
||||
|
||||
type Battery struct {
|
||||
args []string
|
||||
cmd string
|
||||
result string
|
||||
|
||||
Charge string
|
||||
Remaining string
|
||||
}
|
||||
|
||||
func NewBattery() *Battery {
|
||||
battery := Battery{
|
||||
args: []string{"-g", "batt"},
|
||||
cmd: "pmset",
|
||||
}
|
||||
|
||||
return &battery
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (battery *Battery) Refresh() {
|
||||
data := battery.execute()
|
||||
battery.result = battery.parse(data)
|
||||
}
|
||||
|
||||
func (battery *Battery) String() string {
|
||||
return battery.result
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (battery *Battery) execute() string {
|
||||
cmd := exec.Command(battery.cmd, battery.args...)
|
||||
return wtf.ExecuteCommand(cmd)
|
||||
}
|
||||
|
||||
func (battery *Battery) parse(data string) string {
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) < 2 {
|
||||
return "unknown (1)"
|
||||
}
|
||||
|
||||
stats := strings.Split(lines[1], "\t")
|
||||
if len(stats) < 2 {
|
||||
return "unknown (2)"
|
||||
}
|
||||
|
||||
details := strings.Split(stats[1], "; ")
|
||||
if len(details) < 3 {
|
||||
return "unknown (3)"
|
||||
}
|
||||
|
||||
str := ""
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "Charge", battery.formatCharge(details[0]))
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "Remaining", battery.formatRemaining(details[2]))
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "State", battery.formatState(details[1]))
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (battery *Battery) formatCharge(data string) string {
|
||||
percent, _ := strconv.ParseFloat(strings.Replace(data, "%", "", -1), 32)
|
||||
|
||||
color := ""
|
||||
|
||||
switch {
|
||||
case percent >= 70:
|
||||
color = "[green]"
|
||||
case percent >= 35:
|
||||
color = "[yellow]"
|
||||
default:
|
||||
color = "[red]"
|
||||
}
|
||||
|
||||
return color + data + "[white]"
|
||||
}
|
||||
|
||||
func (battery *Battery) formatRemaining(data string) string {
|
||||
r, _ := regexp.Compile(TimeRegExp)
|
||||
|
||||
result := r.FindString(data)
|
||||
if result == "" {
|
||||
result = "∞"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (battery *Battery) formatState(data string) string {
|
||||
color := ""
|
||||
|
||||
switch data {
|
||||
case "charging":
|
||||
color = "[green]"
|
||||
case "discharging":
|
||||
color = "[yellow]"
|
||||
default:
|
||||
color = "[white]"
|
||||
}
|
||||
|
||||
return color + data + "[white]"
|
||||
}
|
||||
112
modules/power/battery_linux.go
Normal file
112
modules/power/battery_linux.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// +build linux
|
||||
|
||||
package power
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
var batteryState string
|
||||
|
||||
type Battery struct {
|
||||
args []string
|
||||
cmd string
|
||||
result string
|
||||
|
||||
Charge string
|
||||
Remaining string
|
||||
}
|
||||
|
||||
func NewBattery() *Battery {
|
||||
return &Battery{}
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (battery *Battery) Refresh() {
|
||||
data := battery.execute()
|
||||
battery.result = battery.parse(data)
|
||||
}
|
||||
|
||||
func (battery *Battery) String() string {
|
||||
return battery.result
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (battery *Battery) execute() string {
|
||||
cmd := exec.Command("upower", "-e")
|
||||
lines := strings.Split(wtf.ExecuteCommand(cmd), "\n")
|
||||
var target string
|
||||
for _, l := range lines {
|
||||
if strings.Contains(l, "/battery") {
|
||||
target = l
|
||||
break
|
||||
}
|
||||
}
|
||||
cmd = exec.Command("upower", "-i", target)
|
||||
return wtf.ExecuteCommand(cmd)
|
||||
}
|
||||
|
||||
func (battery *Battery) parse(data string) string {
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) < 2 {
|
||||
return "unknown"
|
||||
}
|
||||
table := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
table[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
if s := table["time to empty"]; s == "" {
|
||||
table["time to empty"] = "∞"
|
||||
}
|
||||
str := ""
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "Charge", battery.formatCharge(table["percentage"]))
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "Remaining", table["time to empty"])
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "State", battery.formatState(table["state"]))
|
||||
if s := table["time to full"]; s != "" {
|
||||
str = str + fmt.Sprintf(" %10s: %s\n", "TimeToFull", table["time to full"])
|
||||
}
|
||||
batteryState = table["state"]
|
||||
return str
|
||||
}
|
||||
|
||||
func (battery *Battery) formatCharge(data string) string {
|
||||
percent, _ := strconv.ParseFloat(strings.Replace(data, "%", "", -1), 32)
|
||||
color := ""
|
||||
|
||||
switch {
|
||||
case percent >= 70:
|
||||
color = "[green]"
|
||||
case percent >= 35:
|
||||
color = "[yellow]"
|
||||
default:
|
||||
color = "[red]"
|
||||
}
|
||||
|
||||
return color + data + "[white]"
|
||||
}
|
||||
|
||||
func (battery *Battery) formatState(data string) string {
|
||||
color := ""
|
||||
|
||||
switch data {
|
||||
case "charging":
|
||||
color = "[green]"
|
||||
case "discharging":
|
||||
color = "[yellow]"
|
||||
default:
|
||||
color = "[white]"
|
||||
}
|
||||
|
||||
return color + data + "[white]"
|
||||
}
|
||||
27
modules/power/source.go
Normal file
27
modules/power/source.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// +build !linux
|
||||
|
||||
package power
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const SingleQuotesRegExp = "'(.*)'"
|
||||
|
||||
// powerSource returns the name of the current power source, probably one of
|
||||
// "AC Power" or "Battery Power"
|
||||
func powerSource() string {
|
||||
cmd := exec.Command("pmset", []string{"-g", "ps"}...)
|
||||
result := wtf.ExecuteCommand(cmd)
|
||||
|
||||
r, _ := regexp.Compile(SingleQuotesRegExp)
|
||||
|
||||
source := r.FindString(result)
|
||||
source = strings.Replace(source, "'", "", -1)
|
||||
|
||||
return source
|
||||
}
|
||||
15
modules/power/source_linux.go
Normal file
15
modules/power/source_linux.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// +build linux
|
||||
|
||||
package power
|
||||
|
||||
// powerSource returns the name of the current power source, probably one of
|
||||
// "AC Power" or "Battery Power"
|
||||
func powerSource() string {
|
||||
switch batteryState {
|
||||
case "charging", "fully-charged":
|
||||
return "AC Power"
|
||||
case "discharging":
|
||||
return "Battery Power"
|
||||
}
|
||||
return batteryState
|
||||
}
|
||||
36
modules/power/widget.go
Normal file
36
modules/power/widget.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package power
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
Battery *Battery
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Power", "power", false),
|
||||
Battery: NewBattery(),
|
||||
}
|
||||
|
||||
widget.View.SetWrap(true)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.Battery.Refresh()
|
||||
|
||||
content := ""
|
||||
content = content + fmt.Sprintf(" %10s: %s\n", "Source", powerSource())
|
||||
content = content + "\n"
|
||||
content = content + widget.Battery.String()
|
||||
|
||||
widget.View.SetText(content)
|
||||
}
|
||||
121
modules/resourceusage/widget.go
Normal file
121
modules/resourceusage/widget.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package resourceusage
|
||||
|
||||
import (
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"fmt"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
var started = false
|
||||
var ok = true
|
||||
|
||||
// Widget define wtf widget to register widget later
|
||||
type Widget struct {
|
||||
wtf.BarGraph
|
||||
}
|
||||
|
||||
// NewWidget Make new instance of widget
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
BarGraph: wtf.NewBarGraph(app, "Resource Usage", "resourceusage", false),
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
widget.View.SetWordWrap(false)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// MakeGraph - Load the dead drop stats
|
||||
func MakeGraph(widget *Widget) {
|
||||
|
||||
cpuStats, err := cpu.Percent(time.Duration(0), true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var stats = make([]wtf.Bar, len(cpuStats)+2)
|
||||
|
||||
for i, stat := range cpuStats {
|
||||
// Stats sometimes jump outside the 0-100 range, possibly due to timing
|
||||
stat = math.Min(100, stat)
|
||||
stat = math.Max(0, stat)
|
||||
|
||||
bar := wtf.Bar{
|
||||
Label: fmt.Sprint(i),
|
||||
Percent: int(stat),
|
||||
ValueLabel: fmt.Sprintf("%d%%", int(stat)),
|
||||
}
|
||||
|
||||
stats[i] = bar
|
||||
}
|
||||
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
memIndex := len(cpuStats)
|
||||
|
||||
usedMemLabel := bytefmt.ByteSize(memInfo.Used)
|
||||
totalMemLabel := bytefmt.ByteSize(memInfo.Total)
|
||||
|
||||
if usedMemLabel[len(usedMemLabel)-1] == totalMemLabel[len(totalMemLabel)-1] {
|
||||
usedMemLabel = usedMemLabel[:len(usedMemLabel)-1]
|
||||
}
|
||||
|
||||
stats[memIndex] = wtf.Bar{
|
||||
Label: "Mem",
|
||||
Percent: int(memInfo.UsedPercent),
|
||||
ValueLabel: fmt.Sprintf("%s/%s", usedMemLabel, totalMemLabel),
|
||||
}
|
||||
|
||||
swapIndex := len(cpuStats) + 1
|
||||
swapUsed := memInfo.SwapTotal - memInfo.SwapFree
|
||||
var swapPercent float64
|
||||
if memInfo.SwapTotal > 0 {
|
||||
swapPercent = float64(swapUsed) / float64(memInfo.SwapTotal)
|
||||
}
|
||||
|
||||
usedSwapLabel := bytefmt.ByteSize(swapUsed)
|
||||
totalSwapLabel := bytefmt.ByteSize(memInfo.SwapTotal)
|
||||
|
||||
if usedSwapLabel[len(usedSwapLabel)-1] == totalMemLabel[len(totalSwapLabel)-1] {
|
||||
usedSwapLabel = usedSwapLabel[:len(usedSwapLabel)-1]
|
||||
}
|
||||
|
||||
stats[swapIndex] = wtf.Bar{
|
||||
Label: "Swp",
|
||||
Percent: int(swapPercent * 100),
|
||||
ValueLabel: fmt.Sprintf("%s/%s", usedSwapLabel, totalSwapLabel),
|
||||
}
|
||||
|
||||
widget.BarGraph.BuildBars(stats[:])
|
||||
|
||||
}
|
||||
|
||||
// Refresh & update after interval time
|
||||
func (widget *Widget) Refresh() {
|
||||
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.Clear()
|
||||
|
||||
display(widget)
|
||||
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func display(widget *Widget) {
|
||||
MakeGraph(widget)
|
||||
}
|
||||
80
modules/rollbar/client.go
Normal file
80
modules/rollbar/client.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package rollbar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
func CurrentActiveItems() (*ActiveItems, error) {
|
||||
items := &ActiveItems{}
|
||||
|
||||
accessToken := wtf.Config.UString("wtf.mods.rollbar.accessToken", "")
|
||||
rollbarAPIURL.Host = "api.rollbar.com"
|
||||
rollbarAPIURL.Path = "/api/1/items"
|
||||
resp, err := rollbarItemRequest(accessToken)
|
||||
if err != nil {
|
||||
return items, err
|
||||
}
|
||||
|
||||
parseJSON(&items, resp.Body)
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
rollbarAPIURL = &url.URL{Scheme: "https"}
|
||||
)
|
||||
|
||||
func rollbarItemRequest(accessToken string) (*http.Response, error) {
|
||||
params := url.Values{}
|
||||
params.Add("access_token", accessToken)
|
||||
userName := wtf.Config.UString("wtf.mods.rollbar.assignedToName", "")
|
||||
params.Add("assigned_user", userName)
|
||||
active := wtf.Config.UBool("wtf.mods.rollbar.activeOnly", false)
|
||||
if active {
|
||||
params.Add("status", "active")
|
||||
}
|
||||
|
||||
requestURL := rollbarAPIURL.ResolveReference(&url.URL{RawQuery: params.Encode()})
|
||||
req, err := http.NewRequest("GET", requestURL.String(), nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseJSON(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
modules/rollbar/rollbar.go
Normal file
17
modules/rollbar/rollbar.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package rollbar
|
||||
|
||||
type ActiveItems struct {
|
||||
Results Result `json:"result"`
|
||||
}
|
||||
type Item struct {
|
||||
Environment string `json:"environment"`
|
||||
Title string `json:"title"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
TotalOccurrences int `json:"total_occurrences"`
|
||||
Level string `json:"level"`
|
||||
ID int `json:"counter"`
|
||||
}
|
||||
type Result struct {
|
||||
Items []Item `json:"items"`
|
||||
}
|
||||
203
modules/rollbar/widget.go
Normal file
203
modules/rollbar/widget.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package rollbar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Rollbar:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next item in the list
|
||||
k: Select the previous item in the list
|
||||
r: Refresh the data
|
||||
u: unselect the current item(removes item being perma highlighted)
|
||||
|
||||
arrow down: Select the next item in the list
|
||||
arrow up: Select the previous item in the list
|
||||
|
||||
return: Open the selected item in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
items *Result
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Rollbar", "rollbar", true),
|
||||
}
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
items, err := CurrentActiveItems()
|
||||
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
} else {
|
||||
widget.items = &items.Results
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.items == nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
projectName := wtf.Config.UString("wtf.mods.rollbar.projectName", "Items")
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - %s", widget.Name, projectName)))
|
||||
widget.View.SetText(widget.contentFrom(widget.items))
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(result *Result) string {
|
||||
var str string
|
||||
count := wtf.Config.UInt("wtf.mods.rollbar.count", 10)
|
||||
if len(result.Items) > count {
|
||||
result.Items = result.Items[:count]
|
||||
}
|
||||
for idx, item := range result.Items {
|
||||
|
||||
str = str + fmt.Sprintf(
|
||||
"[%s] [%s] %s [%s] %s [%s]count: %d [%s]%s\n",
|
||||
widget.rowColor(idx),
|
||||
levelColor(&item),
|
||||
item.Level,
|
||||
statusColor(&item),
|
||||
item.Title,
|
||||
widget.rowColor(idx),
|
||||
item.TotalOccurrences,
|
||||
"blue",
|
||||
item.Environment,
|
||||
)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
return "white"
|
||||
}
|
||||
|
||||
func statusColor(item *Item) string {
|
||||
switch item.Status {
|
||||
case "active":
|
||||
return "red"
|
||||
case "resolved":
|
||||
return "green"
|
||||
default:
|
||||
return "red"
|
||||
}
|
||||
}
|
||||
func levelColor(item *Item) string {
|
||||
switch item.Level {
|
||||
case "error":
|
||||
return "red"
|
||||
case "critical":
|
||||
return "green"
|
||||
case "warning":
|
||||
return "yellow"
|
||||
default:
|
||||
return "grey"
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.items != nil && widget.selected >= len(widget.items.Items) {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.items.Items != nil {
|
||||
widget.selected = len(widget.items.Items) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openBuild() {
|
||||
sel := widget.selected
|
||||
projectOwner := wtf.Config.UString("wtf.mods.rollbar.projectOwner", "")
|
||||
projectName := wtf.Config.UString("wtf.mods.rollbar.projectName", "")
|
||||
if sel >= 0 && widget.items != nil && sel < len(widget.items.Items) {
|
||||
item := &widget.items.Items[widget.selected]
|
||||
wtf.OpenFile(fmt.Sprintf("https://rollbar.com/%s/%s/%s/%d", projectOwner, projectName, "items", item.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
widget.next()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prev()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
case "u":
|
||||
widget.unselect()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
widget.next()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openBuild()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
widget.prev()
|
||||
widget.display()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
65
modules/security/dns.go
Normal file
65
modules/security/dns.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func DnsServers() []string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return dnsLinux()
|
||||
case "darwin":
|
||||
return dnsMacOS()
|
||||
case "windows":
|
||||
return dnsWindows()
|
||||
default:
|
||||
return []string{runtime.GOOS}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func dnsLinux() []string {
|
||||
// This may be very Ubuntu specific
|
||||
cmd := exec.Command("nmcli", "device", "show")
|
||||
out := wtf.ExecuteCommand(cmd)
|
||||
|
||||
lines := strings.Split(out, "\n")
|
||||
|
||||
dns := []string{}
|
||||
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "IP4.DNS") {
|
||||
parts := strings.Split(l, ":")
|
||||
dns = append(dns, strings.TrimSpace(parts[1]))
|
||||
}
|
||||
}
|
||||
return dns
|
||||
}
|
||||
|
||||
func dnsMacOS() []string {
|
||||
cmdString := `scutil --dns | head -n 7 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}'`
|
||||
cmd := exec.Command("sh", "-c", cmdString)
|
||||
out := wtf.ExecuteCommand(cmd)
|
||||
|
||||
lines := strings.Split(out, "\n")
|
||||
|
||||
if len(lines) > 0 {
|
||||
return lines
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func dnsWindows() []string {
|
||||
|
||||
cmd := exec.Command("powershell.exe", "-NoProfile", "Get-DnsClientServerAddress | Select-Object –ExpandProperty ServerAddresses")
|
||||
|
||||
return []string{wtf.ExecuteCommand(cmd)}
|
||||
}
|
||||
128
modules/security/firewall.go
Normal file
128
modules/security/firewall.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const osxFirewallCmd = "/usr/libexec/ApplicationFirewall/socketfilterfw"
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func FirewallState() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return firewallStateLinux()
|
||||
case "darwin":
|
||||
return firewallStateMacOS()
|
||||
case "windows":
|
||||
return firewallStateWindows()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func FirewallStealthState() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return firewallStealthStateLinux()
|
||||
case "darwin":
|
||||
return firewallStealthStateMacOS()
|
||||
case "windows":
|
||||
return firewallStealthStateWindows()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func firewallStateLinux() string { // might be very Ubuntu specific
|
||||
user, _ := user.Current()
|
||||
|
||||
if strings.Contains(user.Username, "root") {
|
||||
cmd := exec.Command("ufw", "status")
|
||||
|
||||
var o bytes.Buffer
|
||||
cmd.Stdout = &o
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "[red]NA[white]"
|
||||
}
|
||||
|
||||
if strings.Contains(o.String(), "inactive") {
|
||||
return "[red]Disabled[white]"
|
||||
} else {
|
||||
return "[green]Enabled[white]"
|
||||
}
|
||||
} else {
|
||||
return "[red]N/A[white]"
|
||||
}
|
||||
}
|
||||
|
||||
func firewallStateMacOS() string {
|
||||
cmd := exec.Command(osxFirewallCmd, "--getglobalstate")
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return statusLabel(str)
|
||||
}
|
||||
|
||||
func firewallStateWindows() string {
|
||||
// The raw way to do this in PS, not using netsh, nor registry, is the following:
|
||||
// if (((Get-NetFirewallProfile | select name,enabled)
|
||||
// | where { $_.Enabled -eq $True } | measure ).Count -eq 3)
|
||||
// { Write-Host "OK" -ForegroundColor Green} else { Write-Host "OFF" -ForegroundColor Red }
|
||||
|
||||
cmd := exec.Command("powershell.exe", "-NoProfile",
|
||||
"-Command", "& { ((Get-NetFirewallProfile | select name,enabled) | where { $_.Enabled -eq $True } | measure ).Count }")
|
||||
|
||||
fwStat := wtf.ExecuteCommand(cmd)
|
||||
fwStat = strings.TrimSpace(fwStat) // Always sanitize PowerShell output: "3\r\n"
|
||||
//fmt.Printf("%d %q\n", len(fwStat), fwStat)
|
||||
|
||||
switch fwStat {
|
||||
case "3":
|
||||
return "[green]Good[white] (3/3)"
|
||||
case "2":
|
||||
return "[orange]Poor[white] (2/3)"
|
||||
case "1":
|
||||
return "[yellow]Bad[white] (1/3)"
|
||||
case "0":
|
||||
return "[red]Disabled[white]"
|
||||
default:
|
||||
return "[white]N/A[white]"
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Getting Stealth State ------------------- */
|
||||
// "Stealth": Not responding to pings from unauthorized devices
|
||||
|
||||
func firewallStealthStateLinux() string {
|
||||
return "[white]N/A[white]"
|
||||
}
|
||||
|
||||
func firewallStealthStateMacOS() string {
|
||||
cmd := exec.Command(osxFirewallCmd, "--getstealthmode")
|
||||
str := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return statusLabel(str)
|
||||
}
|
||||
|
||||
func firewallStealthStateWindows() string {
|
||||
return "[white]N/A[white]"
|
||||
}
|
||||
|
||||
|
||||
func statusLabel(str string) string {
|
||||
label := "off"
|
||||
|
||||
if strings.Contains(str, "enabled") {
|
||||
label = "on"
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
30
modules/security/security_data.go
Normal file
30
modules/security/security_data.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package security
|
||||
|
||||
type SecurityData struct {
|
||||
Dns []string
|
||||
FirewallEnabled string
|
||||
FirewallStealth string
|
||||
LoggedInUsers []string
|
||||
WifiEncryption string
|
||||
WifiName string
|
||||
}
|
||||
|
||||
func NewSecurityData() *SecurityData {
|
||||
return &SecurityData{}
|
||||
}
|
||||
|
||||
func (data SecurityData) DnsAt(idx int) string {
|
||||
if len(data.Dns) > idx {
|
||||
return data.Dns[idx]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (data *SecurityData) Fetch() {
|
||||
data.Dns = DnsServers()
|
||||
data.FirewallEnabled = FirewallState()
|
||||
data.FirewallStealth = FirewallStealthState()
|
||||
data.LoggedInUsers = LoggedInUsers()
|
||||
data.WifiName = WifiName()
|
||||
data.WifiEncryption = WifiEncryption()
|
||||
}
|
||||
100
modules/security/users.go
Normal file
100
modules/security/users.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package security
|
||||
|
||||
// http://applehelpwriter.com/2017/05/21/how-to-reveal-hidden-users/
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func LoggedInUsers() []string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return loggedInUsersLinux()
|
||||
case "darwin":
|
||||
return loggedInUsersMacOs()
|
||||
case "windows":
|
||||
return loggedInUsersWindows()
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func cleanUsers(users []string) []string {
|
||||
rejects := []string{"_", "root", "nobody", "daemon", "Guest"}
|
||||
cleaned := []string{}
|
||||
|
||||
for _, user := range users {
|
||||
clean := true
|
||||
|
||||
for _, reject := range rejects {
|
||||
if strings.HasPrefix(user, reject) {
|
||||
clean = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if clean && user != "" {
|
||||
cleaned = append(cleaned, user)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func loggedInUsersLinux() []string {
|
||||
cmd := exec.Command("who", "-us")
|
||||
users := wtf.ExecuteCommand(cmd)
|
||||
|
||||
cleaned := []string{}
|
||||
|
||||
for _, user := range strings.Split(users, "\n") {
|
||||
clean := true
|
||||
col := strings.Split(user, " ")
|
||||
|
||||
if len(col) > 0 {
|
||||
for _, cleanedU := range cleaned {
|
||||
u := strings.TrimSpace(col[0])
|
||||
if len(u) == 0 || strings.Compare(cleanedU, col[0]) == 0 {
|
||||
clean = false
|
||||
}
|
||||
}
|
||||
if clean {
|
||||
cleaned = append(cleaned, col[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func loggedInUsersMacOs() []string {
|
||||
cmd := exec.Command("dscl", []string{".", "-list", "/Users"}...)
|
||||
users := wtf.ExecuteCommand(cmd)
|
||||
|
||||
return cleanUsers(strings.Split(users, "\n"))
|
||||
}
|
||||
|
||||
func loggedInUsersWindows() []string {
|
||||
// We can use either one:
|
||||
// (Get-WMIObject -class Win32_ComputerSystem | select username).username
|
||||
// [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
// The original was:
|
||||
// cmd := exec.Command("powershell.exe", "(query user) -replace '\\s{2,}', ','")
|
||||
// But that didn't work!
|
||||
// The real powershell command reads:
|
||||
// powershell.exe -NoProfile -Command "& { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name }"
|
||||
// But we here have to write it as:
|
||||
cmd := exec.Command("powershell.exe", "-NoProfile", "-Command", "& { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name }")
|
||||
// ToDo: Make list for multi-user systems
|
||||
|
||||
users := wtf.ExecuteCommand(cmd)
|
||||
return cleanUsers(strings.Split(users, "\n"))
|
||||
}
|
||||
73
modules/security/widget.go
Normal file
73
modules/security/widget.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Security", "security", false),
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
data := NewSecurityData()
|
||||
data.Fetch()
|
||||
|
||||
widget.View.SetText(widget.contentFrom(data))
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) contentFrom(data *SecurityData) string {
|
||||
str := " [red]WiFi[white]\n"
|
||||
str = str + fmt.Sprintf(" %8s: %s\n", "Network", data.WifiName)
|
||||
str = str + fmt.Sprintf(" %8s: %s\n", "Crypto", data.WifiEncryption)
|
||||
str = str + "\n"
|
||||
|
||||
str = str + " [red]Firewall[white]\n"
|
||||
str = str + fmt.Sprintf(" %8s: %4s\n", "Status", data.FirewallEnabled)
|
||||
str = str + fmt.Sprintf(" %8s: %4s\n", "Stealth", data.FirewallStealth)
|
||||
str = str + "\n"
|
||||
|
||||
str = str + " [red]Users[white]\n"
|
||||
str = str + fmt.Sprintf(" %s", strings.Join(data.LoggedInUsers, "\n "))
|
||||
str = str + "\n"
|
||||
|
||||
str = str + " [red]DNS[white]\n"
|
||||
//str = str + fmt.Sprintf(" %8s: [%s]%-3s[white] %-16s\n", "Enabled", widget.labelColor(data.FirewallEnabled), data.FirewallEnabled, data.DnsAt(0))
|
||||
//str = str + fmt.Sprintf(" %8s: [%s]%-3s[white] %-16s\n", "Stealth", widget.labelColor(data.FirewallStealth), data.FirewallStealth, data.DnsAt(1))
|
||||
str = str + fmt.Sprintf(" %12s\n", data.DnsAt(0))
|
||||
str = str + fmt.Sprintf(" %12s\n", data.DnsAt(1))
|
||||
str = str + "\n"
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) labelColor(label string) string {
|
||||
switch label {
|
||||
case "on":
|
||||
return "green"
|
||||
case "off":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
121
modules/security/wifi.go
Normal file
121
modules/security/wifi.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
// https://github.com/yelinaung/wifi-name/blob/master/wifi-name.go
|
||||
const osxWifiCmd = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"
|
||||
const osxWifiArg = "-I"
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func WifiEncryption() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return wifiEncryptionLinux()
|
||||
case "darwin":
|
||||
return wifiEncryptionMacOS()
|
||||
case "windows":
|
||||
return wifiEncryptionWindows()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func WifiName() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return wifiNameLinux()
|
||||
case "darwin":
|
||||
return wifiNameMacOS()
|
||||
case "windows":
|
||||
return wifiNameWindows()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func wifiEncryptionLinux() string {
|
||||
cmd := exec.Command("nmcli", "-t", "-f", "in-use,security", "dev", "wifi")
|
||||
out := wtf.ExecuteCommand(cmd)
|
||||
|
||||
name := wtf.FindMatch(`\*:(.+)`, out)
|
||||
|
||||
if len(name) > 0 {
|
||||
return name[0][1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func wifiEncryptionMacOS() string {
|
||||
name := wtf.FindMatch(`s*auth: (.+)s*`, wifiInfo())
|
||||
return matchStr(name)
|
||||
}
|
||||
|
||||
func wifiInfo() string {
|
||||
cmd := exec.Command(osxWifiCmd, osxWifiArg)
|
||||
return wtf.ExecuteCommand(cmd)
|
||||
}
|
||||
|
||||
func wifiNameLinux() string {
|
||||
cmd := exec.Command("nmcli", "-t", "-f", "in-use,ssid", "dev", "wifi")
|
||||
out := wtf.ExecuteCommand(cmd)
|
||||
name := wtf.FindMatch(`\*:(.+)`, out)
|
||||
if len(name) > 0 {
|
||||
return name[0][1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func wifiNameMacOS() string {
|
||||
name := wtf.FindMatch(`s*SSID: (.+)s*`, wifiInfo())
|
||||
return matchStr(name)
|
||||
}
|
||||
|
||||
func matchStr(data [][]string) string {
|
||||
if len(data) <= 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return data[1][1]
|
||||
}
|
||||
|
||||
//Windows
|
||||
func wifiEncryptionWindows() string {
|
||||
return parseWlanNetsh("Authentication")
|
||||
}
|
||||
|
||||
func wifiNameWindows() string {
|
||||
return parseWlanNetsh("SSID")
|
||||
}
|
||||
|
||||
func parseWlanNetsh(target string) string {
|
||||
cmd := exec.Command("netsh.exe", "wlan", "show", "interfaces")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
splits := strings.Split(string(out), "\n")
|
||||
var words []string
|
||||
for _, line := range splits {
|
||||
token := strings.Split(string(line), ":")
|
||||
for _, word := range token {
|
||||
words = append(words, strings.TrimSpace(word))
|
||||
}
|
||||
}
|
||||
for i, token := range words {
|
||||
switch token {
|
||||
case target:
|
||||
return words[i+1]
|
||||
}
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
91
modules/spotify/widget.go
Normal file
91
modules/spotify/widget.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/sticreations/spotigopher/spotigopher"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
To control Spotify use:
|
||||
[Spacebar] for Play & Pause
|
||||
[h] for Previous Song
|
||||
[l] for Next Song
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
spotigopher.SpotifyClient
|
||||
spotigopher.Info
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
spotifyClient := spotigopher.NewClient()
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Spotify", "spotify", true),
|
||||
SpotifyClient: spotifyClient,
|
||||
Info: spotigopher.Info{},
|
||||
}
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.TextWidget.RefreshInt = 5
|
||||
widget.View.SetInputCapture(widget.captureInput)
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetWordWrap(true)
|
||||
widget.View.SetTitle(fmt.Sprint("[green]Spotify[white]"))
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (w *Widget) refreshSpotifyInfos() error {
|
||||
info, err := w.SpotifyClient.GetInfo()
|
||||
w.Info = info
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *Widget) Refresh() {
|
||||
w.render()
|
||||
}
|
||||
|
||||
func (w *Widget) render() {
|
||||
err := w.refreshSpotifyInfos()
|
||||
w.View.Clear()
|
||||
if err != nil {
|
||||
w.TextWidget.View.SetText(err.Error())
|
||||
} else {
|
||||
w.TextWidget.View.SetText(w.createOutput())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Widget) captureInput(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch (string)(event.Rune()) {
|
||||
case "h":
|
||||
w.SpotifyClient.Previous()
|
||||
time.Sleep(time.Second * 1)
|
||||
w.Refresh()
|
||||
return nil
|
||||
case "l":
|
||||
w.SpotifyClient.Next()
|
||||
time.Sleep(time.Second * 1)
|
||||
w.Refresh()
|
||||
return nil
|
||||
case " ":
|
||||
w.SpotifyClient.PlayPause()
|
||||
time.Sleep(time.Second * 1)
|
||||
w.Refresh()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Widget) createOutput() string {
|
||||
output := wtf.CenterText(fmt.Sprintf("[green]Now %v [white]\n", w.Info.Status), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Title:[white] %v\n ", w.Info.Title), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Artist:[white] %v\n", w.Info.Artist), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]%v:[white] %v\n", w.Info.TrackNumber, w.Info.Album), w.Width())
|
||||
return output
|
||||
}
|
||||
245
modules/spotifyweb/widget.go
Normal file
245
modules/spotifyweb/widget.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package spotifyweb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/logger"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"github.com/zmb3/spotify"
|
||||
)
|
||||
|
||||
// HelpText contains the help text for the Spotify Web API widget.
|
||||
const HelpText = `
|
||||
Keyboard commands for Spotify Web:
|
||||
|
||||
Before any of these commands are used, you should authenticate using the
|
||||
URL provided by the widget.
|
||||
|
||||
The widget should automatically open a browser window for you, otherwise
|
||||
you should check out the logs for the URL.
|
||||
|
||||
/: Show/hide this help window
|
||||
h: Switch to previous song in Spotify queue
|
||||
l: Switch to next song in Spotify queue
|
||||
s: Toggle shuffle
|
||||
|
||||
[space]: Pause/play current song
|
||||
|
||||
esc: Unselect the Spotify Web module
|
||||
`
|
||||
|
||||
// Info is the struct that contains all the information the Spotify player displays to the user
|
||||
type Info struct {
|
||||
Artists string
|
||||
Title string
|
||||
Album string
|
||||
TrackNumber int
|
||||
Status string
|
||||
}
|
||||
|
||||
// Widget is the struct used by all WTF widgets to transfer to the main widget controller
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
Info
|
||||
clientChan chan *spotify.Client
|
||||
client *spotify.Client
|
||||
playerState *spotify.PlayerState
|
||||
}
|
||||
|
||||
var (
|
||||
auth spotify.Authenticator
|
||||
tempClientChan = make(chan *spotify.Client)
|
||||
state = "wtfSpotifyWebStateString"
|
||||
authURL string
|
||||
callbackPort string
|
||||
redirectURI string
|
||||
)
|
||||
|
||||
func authHandler(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Log("[SpotifyWeb] Got an authentication hit!")
|
||||
tok, err := auth.Token(state, r)
|
||||
if err != nil {
|
||||
http.Error(w, "Couldn't get token", http.StatusForbidden)
|
||||
logger.Log(err.Error())
|
||||
}
|
||||
if st := r.FormValue("state"); st != state {
|
||||
http.NotFound(w, r)
|
||||
logger.Log(fmt.Sprintf("State mismatch: %s != %s\n", st, state))
|
||||
}
|
||||
// use the token to get an authenticated client
|
||||
client := auth.NewClient(tok)
|
||||
fmt.Fprintf(w, "Login Completed!")
|
||||
tempClientChan <- &client
|
||||
}
|
||||
|
||||
func clientID() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.spotifyweb.clientID",
|
||||
os.Getenv("SPOTIFY_ID"),
|
||||
)
|
||||
}
|
||||
|
||||
func secretKey() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.spotifyweb.secretKey",
|
||||
os.Getenv("SPOTIFY_SECRET"),
|
||||
)
|
||||
}
|
||||
|
||||
// NewWidget creates a new widget for WTF
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
callbackPort = wtf.Config.UString("wtf.mods.spotifyweb.callbackPort", "8080")
|
||||
redirectURI = "http://localhost:" + callbackPort + "/callback"
|
||||
|
||||
auth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadCurrentlyPlaying, spotify.ScopeUserReadPlaybackState, spotify.ScopeUserModifyPlaybackState)
|
||||
auth.SetAuthInfo(clientID(), secretKey())
|
||||
authURL = auth.AuthURL(state)
|
||||
|
||||
var client *spotify.Client
|
||||
var playerState *spotify.PlayerState
|
||||
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "SpotifyWeb", "spotifyweb", true),
|
||||
Info: Info{},
|
||||
clientChan: tempClientChan,
|
||||
client: client,
|
||||
playerState: playerState,
|
||||
}
|
||||
|
||||
http.HandleFunc("/callback", authHandler)
|
||||
go http.ListenAndServe(":"+callbackPort, nil)
|
||||
|
||||
go func() {
|
||||
// wait for auth to complete
|
||||
logger.Log("[SpotifyWeb] Waiting for authentication... URL: " + authURL)
|
||||
client = <-tempClientChan
|
||||
|
||||
// use the client to make calls that require authorization
|
||||
_, err := client.CurrentUser()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
playerState, err = client.PlayerState()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger.Log("[SpotifyWeb] Authentication complete.")
|
||||
widget.client = client
|
||||
widget.playerState = playerState
|
||||
widget.Refresh()
|
||||
}()
|
||||
|
||||
// While I wish I could find the reason this doesn't work, I can't.
|
||||
//
|
||||
// Normally, this should open the URL to the browser, however it opens the Explorer window in Windows.
|
||||
// This mostly likely has to do with the fact that the URL includes some very special characters that no terminal likes.
|
||||
// The only solution would be to include quotes in the command, which is why I do here, but it doesn't work.
|
||||
//
|
||||
// If inconvenient, I'll remove this option and save the URL in a file or some other method.
|
||||
wtf.OpenFile(`"` + authURL + `"`)
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.TextWidget.RefreshInt = 5
|
||||
widget.View.SetInputCapture(widget.captureInput)
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetWordWrap(true)
|
||||
widget.View.SetTitle("[green]Spotify Web[white]")
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (w *Widget) refreshSpotifyInfos() error {
|
||||
if w.client == nil || w.playerState == nil {
|
||||
return errors.New("Authentication failed! Please log in to Spotify by visiting the following page in your browser: " + authURL)
|
||||
}
|
||||
var err error
|
||||
w.playerState, err = w.client.PlayerState()
|
||||
if err != nil {
|
||||
return errors.New("Extracting player state failed! Please refresh or restart WTF")
|
||||
}
|
||||
w.Info.Album = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Album.Name)
|
||||
artists := ""
|
||||
for _, artist := range w.playerState.CurrentlyPlaying.Item.Artists {
|
||||
artists += artist.Name + ", "
|
||||
}
|
||||
artists = artists[:len(artists)-2]
|
||||
w.Info.Artists = artists
|
||||
w.Info.Title = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Name)
|
||||
w.Info.TrackNumber = w.playerState.CurrentlyPlaying.Item.TrackNumber
|
||||
if w.playerState.CurrentlyPlaying.Playing {
|
||||
w.Info.Status = "Playing"
|
||||
} else {
|
||||
w.Info.Status = "Paused"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh refreshes the current view of the widget
|
||||
func (w *Widget) Refresh() {
|
||||
w.render()
|
||||
}
|
||||
|
||||
func (w *Widget) render() {
|
||||
err := w.refreshSpotifyInfos()
|
||||
w.View.Clear()
|
||||
if err != nil {
|
||||
w.TextWidget.View.SetText(err.Error())
|
||||
} else {
|
||||
w.TextWidget.View.SetText(w.createOutput())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Widget) captureInput(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch (string)(event.Rune()) {
|
||||
case "/":
|
||||
w.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
w.client.Previous()
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
w.Refresh()
|
||||
return nil
|
||||
case "l":
|
||||
w.client.Next()
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
w.Refresh()
|
||||
return nil
|
||||
case " ":
|
||||
if w.playerState.CurrentlyPlaying.Playing {
|
||||
w.client.Pause()
|
||||
} else {
|
||||
w.client.Play()
|
||||
}
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
w.Refresh()
|
||||
return nil
|
||||
case "s":
|
||||
w.playerState.ShuffleState = !w.playerState.ShuffleState
|
||||
w.client.Shuffle(w.playerState.ShuffleState)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
w.Refresh()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Widget) createOutput() string {
|
||||
output := wtf.CenterText(fmt.Sprintf("[green]Now %v [white]\n", w.Info.Status), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Title:[white] %v\n", w.Info.Title), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Artist:[white] %v\n", w.Info.Artists), w.Width())
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Album:[white] %v\n", w.Info.Album), w.Width())
|
||||
if w.playerState.ShuffleState {
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Shuffle:[white] on\n"), w.Width())
|
||||
} else {
|
||||
output += wtf.CenterText(fmt.Sprintf("[green]Shuffle:[white] off\n"), w.Width())
|
||||
}
|
||||
return output
|
||||
}
|
||||
41
modules/status/widget.go
Normal file
41
modules/status/widget.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
CurrentIcon int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "Status", "status", false),
|
||||
CurrentIcon: 0,
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.View.SetText(widget.animation())
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) animation() string {
|
||||
icons := []string{"|", "/", "-", "\\", "|"}
|
||||
next := icons[widget.CurrentIcon]
|
||||
|
||||
widget.CurrentIcon = widget.CurrentIcon + 1
|
||||
if widget.CurrentIcon == len(icons) {
|
||||
widget.CurrentIcon = 0
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
63
modules/system/system_info.go
Normal file
63
modules/system/system_info.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// +build !windows
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
ProductName string
|
||||
ProductVersion string
|
||||
BuildVersion string
|
||||
}
|
||||
|
||||
func NewSystemInfo() *SystemInfo {
|
||||
m := make(map[string]string)
|
||||
|
||||
arg := []string{}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
arg = append(arg, "-a")
|
||||
cmd = exec.Command("lsb_release", arg...)
|
||||
case "darwin":
|
||||
cmd = exec.Command("sw_vers", arg...)
|
||||
default:
|
||||
cmd = exec.Command("sw_vers", arg...)
|
||||
}
|
||||
|
||||
raw := wtf.ExecuteCommand(cmd)
|
||||
|
||||
for _, row := range strings.Split(raw, "\n") {
|
||||
parts := strings.Split(row, ":")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
var sysInfo *SystemInfo
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
sysInfo = &SystemInfo{
|
||||
ProductName: m["Distributor ID"],
|
||||
ProductVersion: m["Description"],
|
||||
BuildVersion: m["Release"],
|
||||
}
|
||||
default:
|
||||
sysInfo = &SystemInfo{
|
||||
ProductName: m["ProductName"],
|
||||
ProductVersion: m["ProductVersion"],
|
||||
BuildVersion: m["BuildVersion"],
|
||||
}
|
||||
|
||||
}
|
||||
return sysInfo
|
||||
}
|
||||
36
modules/system/system_info_windows.go
Normal file
36
modules/system/system_info_windows.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// +build windows
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
ProductName string
|
||||
ProductVersion string
|
||||
BuildVersion string
|
||||
}
|
||||
|
||||
func NewSystemInfo() *SystemInfo {
|
||||
m := make(map[string]string)
|
||||
|
||||
cmd := exec.Command("powershell.exe", "(Get-CimInstance Win32_OperatingSystem).version")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s := strings.Split(string(out), ".")
|
||||
m["ProductName"] = "Windows"
|
||||
m["ProductVersion"] = "Windows " + s[0] + "." + s[1]
|
||||
m["BuildVersion"] = s[2]
|
||||
|
||||
sysInfo := SystemInfo{
|
||||
ProductName: m["ProductName"],
|
||||
ProductVersion: m["ProductVersion"],
|
||||
BuildVersion: m["BuildVersion"],
|
||||
}
|
||||
|
||||
return &sysInfo
|
||||
}
|
||||
56
modules/system/widget.go
Normal file
56
modules/system/widget.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
type Widget struct {
|
||||
wtf.TextWidget
|
||||
|
||||
systemInfo *SystemInfo
|
||||
Date string
|
||||
Version string
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, date, version string) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: wtf.NewTextWidget(app, "System", "system", false),
|
||||
|
||||
Date: date,
|
||||
Version: version,
|
||||
}
|
||||
|
||||
widget.systemInfo = NewSystemInfo()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.View.SetText(
|
||||
fmt.Sprintf(
|
||||
"%8s: %s\n%8s: %s\n\n%8s: %s\n%8s: %s",
|
||||
"Built",
|
||||
widget.prettyDate(),
|
||||
"Vers",
|
||||
widget.Version,
|
||||
"OS",
|
||||
widget.systemInfo.ProductVersion,
|
||||
"Build",
|
||||
widget.systemInfo.BuildVersion,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (widget *Widget) prettyDate() string {
|
||||
str, err := time.Parse(wtf.TimestampFormat, widget.Date)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return str.Format("Jan _2, 15:04")
|
||||
}
|
||||
199
modules/textfile/widget.go
Normal file
199
modules/textfile/widget.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package textfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/chroma/formatters"
|
||||
"github.com/alecthomas/chroma/lexers"
|
||||
"github.com/alecthomas/chroma/styles"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/radovskyb/watcher"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Textfile:
|
||||
|
||||
/: Show/hide this help window
|
||||
h: Previous text file
|
||||
l: Next text file
|
||||
o: Open the text file in the operating system
|
||||
|
||||
arrow left: Previous text file
|
||||
arrow right: Next text file
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.MultiSourceWidget
|
||||
wtf.TextWidget
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
MultiSourceWidget: wtf.NewMultiSourceWidget("textfile", "filePath", "filePaths"),
|
||||
TextWidget: wtf.NewTextWidget(app, "TextFile", "textfile", true),
|
||||
}
|
||||
|
||||
// Don't use a timer for this widget, watch for filesystem changes instead
|
||||
widget.RefreshInt = 0
|
||||
|
||||
widget.LoadSources()
|
||||
widget.SetDisplayFunction(widget.display)
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetWordWrap(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
go widget.watchForFileChanges()
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh is only called once on start-up. Its job is to display the
|
||||
// text files that first time. After that, the watcher takes over
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
title := fmt.Sprintf("[green]%s[white]", widget.CurrentSource())
|
||||
title = widget.ContextualTitle(title)
|
||||
|
||||
text := wtf.SigilStr(len(widget.Sources), widget.Idx, widget.View) + "\n"
|
||||
|
||||
if wtf.Config.UBool("wtf.mods.textfile.format", false) {
|
||||
text = text + widget.formattedText()
|
||||
} else {
|
||||
text = text + widget.plainText()
|
||||
}
|
||||
|
||||
//widget.View.Lock()
|
||||
widget.View.SetTitle(title) // <- Writes to TextView's title
|
||||
widget.View.SetText(text) // <- Writes to TextView's text
|
||||
//widget.View.Unlock()
|
||||
}
|
||||
|
||||
func (widget *Widget) fileName() string {
|
||||
return filepath.Base(widget.CurrentSource())
|
||||
}
|
||||
|
||||
func (widget *Widget) formattedText() string {
|
||||
filePath, _ := wtf.ExpandHomeDir(widget.CurrentSource())
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
lexer := lexers.Match(filePath)
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
style := styles.Get(wtf.Config.UString("wtf.mods.textfile.formatStyle", "vim"))
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
formatter := formatters.Get("terminal256")
|
||||
if formatter == nil {
|
||||
formatter = formatters.Fallback
|
||||
}
|
||||
|
||||
contents, _ := ioutil.ReadAll(file)
|
||||
iterator, _ := lexer.Tokenise(nil, string(contents))
|
||||
|
||||
var buf bytes.Buffer
|
||||
formatter.Format(&buf, style, iterator)
|
||||
|
||||
return tview.TranslateANSI(buf.String())
|
||||
}
|
||||
|
||||
func (widget *Widget) plainText() string {
|
||||
filePath, _ := wtf.ExpandHomeDir(widget.CurrentSource())
|
||||
|
||||
fmt.Println(filePath)
|
||||
|
||||
text, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(text)
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "h":
|
||||
widget.Prev()
|
||||
return nil
|
||||
case "l":
|
||||
widget.Next()
|
||||
return nil
|
||||
case "o":
|
||||
wtf.OpenFile(widget.CurrentSource())
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyLeft:
|
||||
widget.Prev()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
widget.Next()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (widget *Widget) watchForFileChanges() {
|
||||
watch := watcher.New()
|
||||
watch.FilterOps(watcher.Write)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-watch.Event:
|
||||
widget.display()
|
||||
case err := <-watch.Error:
|
||||
log.Fatalln(err)
|
||||
case <-watch.Closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Watch each textfile for changes
|
||||
for _, source := range widget.Sources {
|
||||
fullPath, err := wtf.ExpandHomeDir(source)
|
||||
if err == nil {
|
||||
if err := watch.Add(fullPath); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the watching process - it'll check for changes every 100ms.
|
||||
if err := watch.Start(time.Millisecond * 100); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
66
modules/todo/display.go
Normal file
66
modules/todo/display.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package todo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/checklist"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const checkWidth = 4
|
||||
|
||||
func (widget *Widget) display() {
|
||||
str := ""
|
||||
newList := checklist.NewChecklist()
|
||||
|
||||
offset := 0
|
||||
|
||||
for idx, item := range widget.list.UncheckedItems() {
|
||||
str = str + widget.formattedItemLine(idx, item, widget.list.SelectedItem(), widget.list.LongestLine())
|
||||
newList.Items = append(newList.Items, item)
|
||||
offset++
|
||||
}
|
||||
|
||||
for idx, item := range widget.list.CheckedItems() {
|
||||
str = str + widget.formattedItemLine(idx+offset, item, widget.list.SelectedItem(), widget.list.LongestLine())
|
||||
newList.Items = append(newList.Items, item)
|
||||
}
|
||||
|
||||
newList.SetSelectedByItem(widget.list.SelectedItem())
|
||||
widget.SetList(newList)
|
||||
|
||||
widget.View.Clear()
|
||||
widget.View.SetText(str)
|
||||
widget.View.Highlight(strconv.Itoa(widget.list.Selected)).ScrollToHighlight()
|
||||
}
|
||||
|
||||
func (widget *Widget) formattedItemLine(idx int, item *checklist.ChecklistItem, selectedItem *checklist.ChecklistItem, maxLen int) string {
|
||||
foreColor, backColor := "white", wtf.Config.UString("wtf.colors.background", "black")
|
||||
|
||||
if item.Checked {
|
||||
foreColor = wtf.Config.UString("wtf.colors.checked", "white")
|
||||
}
|
||||
|
||||
if widget.View.HasFocus() && (item == selectedItem) {
|
||||
foreColor = wtf.Config.UString("wtf.colors.highlight.fore", "black")
|
||||
backColor = wtf.Config.UString("wtf.colors.highlight.back", "orange")
|
||||
}
|
||||
|
||||
str := fmt.Sprintf(
|
||||
`["%d"][""][%s:%s]|%s| %s[white]`,
|
||||
idx,
|
||||
foreColor,
|
||||
backColor,
|
||||
item.CheckMark(),
|
||||
tview.Escape(item.Text),
|
||||
)
|
||||
|
||||
_, _, w, _ := widget.View.GetInnerRect()
|
||||
if w > maxLen {
|
||||
maxLen = w
|
||||
}
|
||||
|
||||
return str + wtf.PadRow((checkWidth+len(item.Text)), (checkWidth+maxLen+1)) + "\n"
|
||||
}
|
||||
284
modules/todo/widget.go
Normal file
284
modules/todo/widget.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package todo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/cfg"
|
||||
"github.com/wtfutil/wtf/checklist"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Todo:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next item in the list
|
||||
k: Select the previous item in the list
|
||||
n: Create a new list item
|
||||
o: Open the todo file in the operating system
|
||||
|
||||
arrow down: Select the next item in the list
|
||||
arrow up: Select the previous item in the list
|
||||
|
||||
ctrl-d: Delete the selected item
|
||||
|
||||
esc: Unselect the todo list
|
||||
return: Edit selected item
|
||||
space: Check the selected item on or off
|
||||
`
|
||||
|
||||
const offscreen = -1000
|
||||
const modalWidth = 80
|
||||
const modalHeight = 7
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
app *tview.Application
|
||||
filePath string
|
||||
list checklist.Checklist
|
||||
pages *tview.Pages
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Todo", "todo", true),
|
||||
|
||||
app: app,
|
||||
filePath: wtf.Config.UString("wtf.mods.todo.filename"),
|
||||
list: checklist.NewChecklist(),
|
||||
pages: pages,
|
||||
}
|
||||
|
||||
widget.init()
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
|
||||
widget.View.SetScrollable(true)
|
||||
widget.View.SetRegions(true)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
widget.load()
|
||||
widget.display()
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(widget.Name))
|
||||
}
|
||||
|
||||
func (widget *Widget) SetList(newList checklist.Checklist) {
|
||||
widget.list = newList
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
// edit opens a modal dialog that permits editing the text of the currently-selected item
|
||||
func (widget *Widget) editItem() {
|
||||
if widget.list.SelectedItem() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := widget.modalForm("Edit:", widget.list.SelectedItem().Text)
|
||||
|
||||
saveFctn := func() {
|
||||
text := form.GetFormItem(0).(*tview.InputField).GetText()
|
||||
|
||||
widget.list.Update(text)
|
||||
widget.persist()
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
widget.addButtons(form, saveFctn)
|
||||
widget.modalFocus(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) init() {
|
||||
_, err := cfg.CreateFile(widget.filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case " ":
|
||||
// Check/uncheck selected item
|
||||
widget.list.Toggle()
|
||||
widget.persist()
|
||||
widget.display()
|
||||
return nil
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
return nil
|
||||
case "j":
|
||||
// Select the next item down
|
||||
widget.list.Next()
|
||||
widget.display()
|
||||
return nil
|
||||
case "k":
|
||||
// Select the next item up
|
||||
widget.list.Prev()
|
||||
widget.display()
|
||||
return nil
|
||||
case "n":
|
||||
// Add a new item
|
||||
widget.newItem()
|
||||
return nil
|
||||
case "o":
|
||||
// Open the file
|
||||
confDir, _ := cfg.ConfigDir()
|
||||
wtf.OpenFile(fmt.Sprintf("%s/%s", confDir, widget.filePath))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyCtrlD:
|
||||
// Delete the selected item
|
||||
widget.list.Delete()
|
||||
widget.persist()
|
||||
widget.display()
|
||||
return nil
|
||||
case tcell.KeyCtrlJ:
|
||||
// Move selected item down in the list
|
||||
widget.list.Demote()
|
||||
widget.persist()
|
||||
widget.display()
|
||||
return nil
|
||||
case tcell.KeyCtrlK:
|
||||
// Move selected item up in the list
|
||||
widget.list.Promote()
|
||||
widget.persist()
|
||||
widget.display()
|
||||
return nil
|
||||
case tcell.KeyDown:
|
||||
// Select the next item down
|
||||
widget.list.Next()
|
||||
widget.display()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.editItem()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
// Unselect the current row
|
||||
widget.list.Unselect()
|
||||
widget.display()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
// Select the next item up
|
||||
widget.list.Prev()
|
||||
widget.display()
|
||||
return nil
|
||||
default:
|
||||
// Pass it along
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
// Loads the todo list from Yaml file
|
||||
func (widget *Widget) load() {
|
||||
confDir, _ := cfg.ConfigDir()
|
||||
filePath := fmt.Sprintf("%s/%s", confDir, widget.filePath)
|
||||
|
||||
fileData, _ := wtf.ReadFileBytes(filePath)
|
||||
yaml.Unmarshal(fileData, &widget.list)
|
||||
}
|
||||
|
||||
func (widget *Widget) newItem() {
|
||||
form := widget.modalForm("New Todo:", "")
|
||||
|
||||
saveFctn := func() {
|
||||
text := form.GetFormItem(0).(*tview.InputField).GetText()
|
||||
|
||||
widget.list.Add(false, text)
|
||||
widget.persist()
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
widget.addButtons(form, saveFctn)
|
||||
widget.modalFocus(form)
|
||||
}
|
||||
|
||||
// persist writes the todo list to Yaml file
|
||||
func (widget *Widget) persist() {
|
||||
confDir, _ := cfg.ConfigDir()
|
||||
filePath := fmt.Sprintf("%s/%s", confDir, widget.filePath)
|
||||
|
||||
fileData, _ := yaml.Marshal(&widget.list)
|
||||
|
||||
err := ioutil.WriteFile(filePath, fileData, 0644)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Modal Form -------------------- */
|
||||
|
||||
func (widget *Widget) addButtons(form *tview.Form, saveFctn func()) {
|
||||
widget.addSaveButton(form, saveFctn)
|
||||
widget.addCancelButton(form)
|
||||
}
|
||||
|
||||
func (widget *Widget) addCancelButton(form *tview.Form) {
|
||||
cancelFn := func() {
|
||||
widget.pages.RemovePage("modal")
|
||||
widget.app.SetFocus(widget.View)
|
||||
widget.display()
|
||||
}
|
||||
|
||||
form.AddButton("Cancel", cancelFn)
|
||||
form.SetCancelFunc(cancelFn)
|
||||
}
|
||||
|
||||
func (widget *Widget) addSaveButton(form *tview.Form, fctn func()) {
|
||||
form.AddButton("Save", fctn)
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFocus(form *tview.Form) {
|
||||
frame := widget.modalFrame(form)
|
||||
widget.pages.AddPage("modal", frame, false, true)
|
||||
widget.app.SetFocus(frame)
|
||||
widget.app.Draw()
|
||||
}
|
||||
|
||||
func (widget *Widget) modalForm(lbl, text string) *tview.Form {
|
||||
form := tview.NewForm().
|
||||
SetFieldBackgroundColor(wtf.ColorFor(wtf.Config.UString("wtf.colors.background", "black")))
|
||||
|
||||
form.SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonTextColor(wtf.ColorFor(wtf.Config.UString("wtf.colors.text", "white")))
|
||||
|
||||
form.AddInputField(lbl, text, 60, nil, nil)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
func (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {
|
||||
frame := tview.NewFrame(form).SetBorders(0, 0, 0, 0, 0, 0)
|
||||
frame.SetRect(offscreen, offscreen, modalWidth, modalHeight)
|
||||
frame.SetBorder(true)
|
||||
frame.SetBorders(1, 1, 0, 0, 1, 1)
|
||||
|
||||
drawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
|
||||
w, h := screen.Size()
|
||||
frame.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
frame.SetDrawFunc(drawFunc)
|
||||
|
||||
return frame
|
||||
}
|
||||
51
modules/todoist/display.go
Normal file
51
modules/todoist/display.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package todoist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const checkWidth = 4
|
||||
|
||||
func (widget *Widget) display() {
|
||||
proj := widget.CurrentProject()
|
||||
|
||||
if proj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[green]%s[white]", proj.Project.Name)
|
||||
widget.View.SetTitle(widget.ContextualTitle(title))
|
||||
|
||||
str := wtf.SigilStr(len(widget.projects), widget.idx, widget.View) + "\n"
|
||||
|
||||
maxLen := proj.LongestLine()
|
||||
|
||||
for index, item := range proj.tasks {
|
||||
foreColor, backColor := "white", wtf.Config.UString("wtf.colors.background", "black")
|
||||
|
||||
if index == proj.index {
|
||||
foreColor = wtf.Config.UString("wtf.colors.highlight.fore", "black")
|
||||
backColor = wtf.Config.UString("wtf.colors.highlight.back", "orange")
|
||||
}
|
||||
|
||||
row := fmt.Sprintf(
|
||||
"[%s:%s]| | %s[white]",
|
||||
foreColor,
|
||||
backColor,
|
||||
tview.Escape(item.Content),
|
||||
)
|
||||
|
||||
_, _, w, _ := widget.View.GetInnerRect()
|
||||
if w > maxLen {
|
||||
maxLen = w
|
||||
}
|
||||
|
||||
str = str + row + wtf.PadRow((checkWidth+len(item.Content)), (checkWidth+maxLen+1)) + "\n"
|
||||
}
|
||||
|
||||
//widget.View.Clear()
|
||||
widget.View.SetText(str)
|
||||
}
|
||||
112
modules/todoist/project.go
Normal file
112
modules/todoist/project.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package todoist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/darkSasori/todoist"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
todoist.Project
|
||||
|
||||
index int
|
||||
tasks []todoist.Task
|
||||
}
|
||||
|
||||
func NewProject(id int) *Project {
|
||||
// Todoist seems to experience a lot of network issues on their side
|
||||
// If we can't connect, handle it with an empty project until we can
|
||||
project, err := todoist.GetProject(id)
|
||||
if err != nil {
|
||||
return &Project{}
|
||||
}
|
||||
|
||||
proj := &Project{
|
||||
Project: project,
|
||||
|
||||
index: -1,
|
||||
}
|
||||
|
||||
proj.loadTasks()
|
||||
|
||||
return proj
|
||||
}
|
||||
|
||||
func (proj *Project) isFirst() bool {
|
||||
return proj.index == 0
|
||||
}
|
||||
|
||||
func (proj *Project) isLast() bool {
|
||||
return proj.index >= len(proj.tasks)-1
|
||||
}
|
||||
|
||||
func (proj *Project) up() {
|
||||
proj.index = proj.index - 1
|
||||
|
||||
if proj.index < 0 {
|
||||
proj.index = len(proj.tasks) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (proj *Project) down() {
|
||||
if proj.index == -1 {
|
||||
proj.index = 0
|
||||
return
|
||||
}
|
||||
|
||||
proj.index = proj.index + 1
|
||||
if proj.index >= len(proj.tasks) {
|
||||
proj.index = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (proj *Project) loadTasks() {
|
||||
tasks, err := todoist.ListTask(todoist.QueryParam{"project_id": fmt.Sprintf("%d", proj.ID)})
|
||||
if err == nil {
|
||||
proj.tasks = tasks
|
||||
}
|
||||
}
|
||||
|
||||
func (proj *Project) LongestLine() int {
|
||||
maxLen := 0
|
||||
|
||||
for _, task := range proj.tasks {
|
||||
if len(task.Content) > maxLen {
|
||||
maxLen = len(task.Content)
|
||||
}
|
||||
}
|
||||
|
||||
return maxLen
|
||||
}
|
||||
|
||||
func (proj *Project) currentTask() *todoist.Task {
|
||||
if proj.index < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &proj.tasks[proj.index]
|
||||
}
|
||||
|
||||
func (proj *Project) closeSelectedTask() {
|
||||
currTask := proj.currentTask()
|
||||
|
||||
if currTask != nil {
|
||||
if err := currTask.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
proj.loadTasks()
|
||||
}
|
||||
}
|
||||
|
||||
func (proj *Project) deleteSelectedTask() {
|
||||
currTask := proj.currentTask()
|
||||
|
||||
if currTask != nil {
|
||||
if err := currTask.Delete(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
proj.loadTasks()
|
||||
}
|
||||
}
|
||||
201
modules/todoist/widget.go
Normal file
201
modules/todoist/widget.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package todoist
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/darkSasori/todoist"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Todoist:
|
||||
|
||||
/: Show/hide this help window
|
||||
c: Close the selected item
|
||||
d: Delete the selected item
|
||||
h: Previous Todoist list
|
||||
j: Select the next item in the list
|
||||
k: Select the previous item in the list
|
||||
l: Next Todoist list
|
||||
r: Refresh the todo list data
|
||||
|
||||
arrow down: Select the next item in the list
|
||||
arrow left: Previous Todoist list
|
||||
arrow right: Next Todoist list
|
||||
arrow up: Select the previous item in the list
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
projects []*Project
|
||||
idx int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "Todoist", "todoist", true),
|
||||
}
|
||||
|
||||
widget.loadAPICredentials()
|
||||
widget.projects = loadProjects()
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) CurrentProject() *Project {
|
||||
return widget.ProjectAt(widget.idx)
|
||||
}
|
||||
|
||||
func (widget *Widget) ProjectAt(idx int) *Project {
|
||||
if len(widget.projects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return widget.projects[idx]
|
||||
}
|
||||
|
||||
func (w *Widget) NextProject() {
|
||||
w.idx = w.idx + 1
|
||||
if w.idx == len(w.projects) {
|
||||
w.idx = 0
|
||||
}
|
||||
|
||||
w.display()
|
||||
}
|
||||
|
||||
func (w *Widget) PreviousProject() {
|
||||
w.idx = w.idx - 1
|
||||
if w.idx < 0 {
|
||||
w.idx = len(w.projects) - 1
|
||||
}
|
||||
|
||||
w.display()
|
||||
}
|
||||
|
||||
func (w *Widget) Refresh() {
|
||||
if w.Disabled() || w.CurrentProject() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.display()
|
||||
}
|
||||
|
||||
/* -------------------- Keyboard Movement -------------------- */
|
||||
|
||||
// Down selects the next item in the list
|
||||
func (w *Widget) Down() {
|
||||
w.CurrentProject().down()
|
||||
w.display()
|
||||
}
|
||||
|
||||
// Up selects the previous item in the list
|
||||
func (w *Widget) Up() {
|
||||
w.CurrentProject().up()
|
||||
w.display()
|
||||
}
|
||||
|
||||
// Close closes the currently-selected task in the currently-selected project
|
||||
func (w *Widget) Close() {
|
||||
w.CurrentProject().closeSelectedTask()
|
||||
|
||||
if w.CurrentProject().isLast() {
|
||||
w.Up()
|
||||
return
|
||||
}
|
||||
|
||||
w.Down()
|
||||
}
|
||||
|
||||
// Delete deletes the currently-selected task in the currently-selected project
|
||||
func (w *Widget) Delete() {
|
||||
w.CurrentProject().deleteSelectedTask()
|
||||
|
||||
if w.CurrentProject().isLast() {
|
||||
w.Up()
|
||||
return
|
||||
}
|
||||
|
||||
w.Down()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (w *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
if len(w.projects) == 0 {
|
||||
return event
|
||||
}
|
||||
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
w.ShowHelp()
|
||||
return nil
|
||||
case "r":
|
||||
w.Refresh()
|
||||
return nil
|
||||
case "d":
|
||||
w.Delete()
|
||||
return nil
|
||||
case "c":
|
||||
w.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch w.vimBindings(event) {
|
||||
case tcell.KeyLeft:
|
||||
w.PreviousProject()
|
||||
return nil
|
||||
case tcell.KeyRight:
|
||||
w.NextProject()
|
||||
return nil
|
||||
case tcell.KeyUp:
|
||||
w.Up()
|
||||
return nil
|
||||
case tcell.KeyDown:
|
||||
w.Down()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (widget *Widget) loadAPICredentials() {
|
||||
todoist.Token = wtf.Config.UString(
|
||||
"wtf.mods.todoist.apiKey",
|
||||
os.Getenv("WTF_TODOIST_TOKEN"),
|
||||
)
|
||||
}
|
||||
|
||||
func loadProjects() []*Project {
|
||||
projects := []*Project{}
|
||||
|
||||
for _, id := range wtf.Config.UList("wtf.mods.todoist.projects") {
|
||||
proj := NewProject(id.(int))
|
||||
projects = append(projects, proj)
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
func (w *Widget) vimBindings(event *tcell.EventKey) tcell.Key {
|
||||
switch string(event.Rune()) {
|
||||
case "h":
|
||||
return tcell.KeyLeft
|
||||
case "l":
|
||||
return tcell.KeyRight
|
||||
case "k":
|
||||
return tcell.KeyUp
|
||||
case "j":
|
||||
return tcell.KeyDown
|
||||
}
|
||||
return event.Key()
|
||||
}
|
||||
95
modules/travisci/client.go
Normal file
95
modules/travisci/client.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package travisci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
)
|
||||
|
||||
var TRAVIS_HOSTS = map[bool]string{
|
||||
false: "travis-ci.org",
|
||||
true: "travis-ci.com",
|
||||
}
|
||||
|
||||
func BuildsFor() (*Builds, error) {
|
||||
builds := &Builds{}
|
||||
|
||||
pro := wtf.Config.UBool("wtf.mods.travisci.pro", false)
|
||||
travisAPIURL.Host = "api." + TRAVIS_HOSTS[pro]
|
||||
|
||||
resp, err := travisRequest("builds")
|
||||
if err != nil {
|
||||
return builds, err
|
||||
}
|
||||
|
||||
parseJson(&builds, resp.Body)
|
||||
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
travisAPIURL = &url.URL{Scheme: "https", Path: "/"}
|
||||
)
|
||||
|
||||
func travisRequest(path string) (*http.Response, error) {
|
||||
params := url.Values{}
|
||||
params.Add("limit", "10")
|
||||
|
||||
requestUrl := travisAPIURL.ResolveReference(&url.URL{Path: path, RawQuery: params.Encode()})
|
||||
|
||||
req, err := http.NewRequest("GET", requestUrl.String(), nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Travis-API-Version", "3")
|
||||
|
||||
bearer := fmt.Sprintf("token %s", apiToken())
|
||||
req.Header.Add("Authorization", bearer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func apiToken() string {
|
||||
return wtf.Config.UString(
|
||||
"wtf.mods.travisci.apiKey",
|
||||
os.Getenv("WTF_TRAVIS_API_TOKEN"),
|
||||
)
|
||||
}
|
||||
|
||||
func parseJson(obj interface{}, text io.Reader) {
|
||||
jsonStream, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonStream))
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(obj); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
modules/travisci/travis.go
Normal file
32
modules/travisci/travis.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package travisci
|
||||
|
||||
type Builds struct {
|
||||
Builds []Build `json:"builds"`
|
||||
}
|
||||
|
||||
type Build struct {
|
||||
ID int `json:"id"`
|
||||
CreatedBy Owner `json:"created_by"`
|
||||
Branch Branch `json:"branch"`
|
||||
Number string `json:"number"`
|
||||
Repository Repository `json:"repository"`
|
||||
Commit Commit `json:"commit"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type Owner struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type Branch struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
192
modules/travisci/widget.go
Normal file
192
modules/travisci/widget.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package travisci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/wtf"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const HelpText = `
|
||||
Keyboard commands for Travis CI:
|
||||
|
||||
/: Show/hide this help window
|
||||
j: Select the next build in the list
|
||||
k: Select the previous build in the list
|
||||
r: Refresh the data
|
||||
|
||||
arrow down: Select the next build in the list
|
||||
arrow up: Select the previous build in the list
|
||||
|
||||
return: Open the selected build in a browser
|
||||
`
|
||||
|
||||
type Widget struct {
|
||||
wtf.HelpfulWidget
|
||||
wtf.TextWidget
|
||||
|
||||
builds *Builds
|
||||
selected int
|
||||
}
|
||||
|
||||
func NewWidget(app *tview.Application, pages *tview.Pages) *Widget {
|
||||
widget := Widget{
|
||||
HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText),
|
||||
TextWidget: wtf.NewTextWidget(app, "TravisCI", "travisci", true),
|
||||
}
|
||||
|
||||
widget.HelpfulWidget.SetView(widget.View)
|
||||
widget.unselect()
|
||||
|
||||
widget.View.SetInputCapture(widget.keyboardIntercept)
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
builds, err := BuildsFor()
|
||||
|
||||
if err != nil {
|
||||
widget.View.SetWrap(true)
|
||||
widget.View.SetTitle(widget.Name)
|
||||
widget.View.SetText(err.Error())
|
||||
} else {
|
||||
widget.builds = builds
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) display() {
|
||||
if widget.builds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
widget.View.SetWrap(false)
|
||||
|
||||
widget.View.SetTitle(widget.ContextualTitle(fmt.Sprintf("%s - Builds", widget.Name)))
|
||||
widget.View.SetText(widget.contentFrom(widget.builds))
|
||||
}
|
||||
|
||||
func (widget *Widget) contentFrom(builds *Builds) string {
|
||||
var str string
|
||||
for idx, build := range builds.Builds {
|
||||
|
||||
str = str + fmt.Sprintf(
|
||||
"[%s] [%s] %s-%s (%s) [%s]%s - [blue]%s\n",
|
||||
widget.rowColor(idx),
|
||||
buildColor(&build),
|
||||
build.Repository.Name,
|
||||
build.Number,
|
||||
build.Branch.Name,
|
||||
widget.rowColor(idx),
|
||||
strings.Split(build.Commit.Message, "\n")[0],
|
||||
build.CreatedBy.Login,
|
||||
)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (widget *Widget) rowColor(idx int) string {
|
||||
if widget.View.HasFocus() && (idx == widget.selected) {
|
||||
return wtf.DefaultFocussedRowColor()
|
||||
}
|
||||
return "White"
|
||||
}
|
||||
|
||||
func buildColor(build *Build) string {
|
||||
switch build.State {
|
||||
case "broken":
|
||||
return "red"
|
||||
case "failed":
|
||||
return "red"
|
||||
case "failing":
|
||||
return "red"
|
||||
case "pending":
|
||||
return "yellow"
|
||||
case "started":
|
||||
return "yellow"
|
||||
case "fixed":
|
||||
return "green"
|
||||
case "passed":
|
||||
return "green"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) next() {
|
||||
widget.selected++
|
||||
if widget.builds != nil && widget.selected >= len(widget.builds.Builds) {
|
||||
widget.selected = 0
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) prev() {
|
||||
widget.selected--
|
||||
if widget.selected < 0 && widget.builds != nil {
|
||||
widget.selected = len(widget.builds.Builds) - 1
|
||||
}
|
||||
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) openBuild() {
|
||||
sel := widget.selected
|
||||
if sel >= 0 && widget.builds != nil && sel < len(widget.builds.Builds) {
|
||||
build := &widget.builds.Builds[widget.selected]
|
||||
travisHost := TRAVIS_HOSTS[wtf.Config.UBool("wtf.mods.travisci.pro", false)]
|
||||
wtf.OpenFile(fmt.Sprintf("https://%s/%s/%s/%d", travisHost, build.Repository.Slug, "builds", build.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *Widget) unselect() {
|
||||
widget.selected = -1
|
||||
widget.display()
|
||||
}
|
||||
|
||||
func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch string(event.Rune()) {
|
||||
case "/":
|
||||
widget.ShowHelp()
|
||||
case "j":
|
||||
widget.next()
|
||||
return nil
|
||||
case "k":
|
||||
widget.prev()
|
||||
return nil
|
||||
case "r":
|
||||
widget.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown:
|
||||
widget.next()
|
||||
return nil
|
||||
case tcell.KeyEnter:
|
||||
widget.openBuild()
|
||||
return nil
|
||||
case tcell.KeyEsc:
|
||||
widget.unselect()
|
||||
return event
|
||||
case tcell.KeyUp:
|
||||
widget.prev()
|
||||
widget.display()
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user