1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Migrate all modules to their own subfolder

Handles #375
This commit is contained in:
Sean Smith
2019-02-18 11:16:34 -05:00
parent c28c31aedb
commit 8030380f89
123 changed files with 51 additions and 51 deletions

View 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
}

View 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
}

View 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
View 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)
}

View 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
}

View 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
View 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"`
}

View 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)
}
}
}

View 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
View 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)
}

View 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
View 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
View 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)
}

View 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))
}

View 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,
})
}

View 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)
}

View 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
}

View 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)
}

View 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,
})
}

View 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]
}
}

View 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,
)
}

View 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,
})
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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 "? "
}

View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
}
}

View 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)
}

View 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
}

View 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)
}
}
}

View 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"`
}

View 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
}
}

View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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"`
}

View 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
View 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
}
}

View 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))
}

View 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
View 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
}
}

View 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"),
)
}

View 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
}

View 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
}

View 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)
}

View 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
View 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
View 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
View 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]"
}

View 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
View 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
}

View 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
View 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)
}

View 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
View 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)
}
}
}

View 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
View 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
View 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)}
}

View 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
}

View 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
View 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"))
}

View 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
View 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
View 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
}

View 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
View 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
}

View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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()
}

View 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)
}
}
}

View 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
View 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