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

Merge branch 'master' into dependabot/go_modules/github.com/shirou/gopsutil-2.20.9incompatible

This commit is contained in:
Chris Cummer 2020-10-08 11:46:23 -07:00
commit b96d319b26
37 changed files with 797 additions and 536 deletions

View File

@ -959,6 +959,41 @@
"name": "Tim Hwang",
"avatar_url": "https://avatars3.githubusercontent.com/u/5831434?v=4",
"profile": "http://timhwang21.gitbook.io",
"contributions": []
},
{
"login": "cyingfan",
"name": "Ying Fan Chong",
"avatar_url": "https://avatars1.githubusercontent.com/u/10404961?v=4",
"profile": "http://about.me/yingfan",
"contributions": []
},
{
"login": "MartinJohns",
"name": "Martin Johns",
"avatar_url": "https://avatars1.githubusercontent.com/u/5269069?v=4",
"profile": "https://github.com/MartinJohns",
"contributions": []
},
{
"login": "jamietanna",
"name": "Jamie Tanna",
"avatar_url": "https://avatars0.githubusercontent.com/u/3315059?v=4",
"profile": "https://www.jvt.me",
"contributions": []
},
{
"login": "trimble",
"name": "Todd Trimble",
"avatar_url": "https://avatars3.githubusercontent.com/u/371317?v=4",
"profile": "https://github.com/trimble",
"contributions": []
},
{
"login": "mhanberg",
"name": "Mitchell Hanberg",
"avatar_url": "https://avatars2.githubusercontent.com/u/5523984?v=4",
"profile": "https://www.mitchellhanberg.com",
"contributions": [
]
}

View File

@ -1,7 +1,7 @@
language: go
go:
- "1.14.x"
- "1.15.x"
before_install:
# Make sure travis builds work for forks

View File

@ -2,9 +2,17 @@
## Unreleased
### ⚡️ Added
* FeedReader module supports [display mode switching](https://github.com/wtfutil/wtf/pull/976) via the `t` key, by [@cyingfan](https://github.com/cyingfan)
* Clocks module now supports `natural` ordering option, [#896](https://github.com/wtfutil/wtf/issues/896) by [@seanstoppable](https://github.com/Seanstoppable)
### 🐞 Fixed
* Exchange Rates module now displays rates in alphabetical order, sorted by the base rate, by [@senorprogrammer](https://github.com/senorprogrammer)
* Feed Reader module no longer crashes on feeds that don't have a published date, [#958](https://github.com/wtfutil/wtf/issues/958) by [@cyingfan](https://github.com/cyingfan)
* Stray blank lines no longer appear at the end of highlightable lists, [#977](https://github.com/wtfutil/wtf/pull/977) by [@cyingfan](https://github.com/cyingfan)
* `config.yml` now properly set to `0600` instead of `0666`, by [@cyingfan](https://github.com/cyingfan)
### 👍 Updated
@ -16,6 +24,8 @@
* Updated `github.com/adlio/trello` from 1.7.0 to 1.8.0
* Updated `github.com/alecthomas/chroma` from 0.8.0 to 0.8.1
* Now requires Go 1.15 for compilation in Dockerfiles and go.mod, by [@seanstoppable](https://github.com/Seanstoppable)
## v0.32.0
### ⚡️ Added

View File

@ -1,4 +1,4 @@
FROM golang:1.13-alpine as build
FROM golang:1.15-alpine as build
ARG version=master

View File

@ -1,4 +1,4 @@
FROM golang:1.13 as build
FROM golang:1.15 as build
ARG version=master

View File

@ -130,6 +130,10 @@ docker rm wtf_build
**Note:** WTF is _only_ compatible with Go versions **1.12.0** or later (due to the use of Go modules and newer standard library functions). If you would like to use `gccgo` to compile, you _must_ use `gccgo-9` or later which introduces support for Go modules.
### Installing via Arch User Repository
Arch Linux users can utilise the [wtfutil](https://aur.archlinux.org/packages/wtfutil) package to build it from source, or [wtfutil-bin](https://aur.archlinux.org/packages/wtfutil-bin/) to install pre-built binaries.
## Running via Docker
You can run `wtf` inside a docker container:
@ -396,6 +400,13 @@ Dependency management in WTF is handled by [Go modules](https://github.com/golan
<td align="center"><a href="https://github.com/fmotrifork"><img src="https://avatars3.githubusercontent.com/u/18327738?v=4" width="48px;" alt=""/><br /><sub><b>Frederik Mogensen</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/aeter"><img src="https://avatars0.githubusercontent.com/u/238607?v=4" width="48px;" alt=""/><br /><sub><b>aeter</b></sub></a><br /></td>
<td align="center"><a href="http://timhwang21.gitbook.io"><img src="https://avatars3.githubusercontent.com/u/5831434?v=4" width="48px;" alt=""/><br /><sub><b>Tim Hwang</b></sub></a><br /></td>
<td align="center"><a href="http://about.me/yingfan"><img src="https://avatars1.githubusercontent.com/u/10404961?v=4" width="48px;" alt=""/><br /><sub><b>Ying Fan Chong</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/MartinJohns"><img src="https://avatars1.githubusercontent.com/u/5269069?v=4" width="48px;" alt=""/><br /><sub><b>Martin Johns</b></sub></a><br /></td>
<td align="center"><a href="https://www.jvt.me"><img src="https://avatars0.githubusercontent.com/u/3315059?v=4" width="48px;" alt=""/><br /><sub><b>Jamie Tanna</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/trimble"><img src="https://avatars3.githubusercontent.com/u/371317?v=4" width="48px;" alt=""/><br /><sub><b>Todd Trimble</b></sub></a><br /></td>
</tr>
<tr>
<td align="center"><a href="https://www.mitchellhanberg.com"><img src="https://avatars2.githubusercontent.com/u/5523984?v=4" width="48px;" alt=""/><br /><sub><b>Mitchell Hanberg</b></sub></a><br /></td>
</tr>
</table>

View File

@ -59,10 +59,9 @@ import (
"github.com/wtfutil/wtf/modules/subreddit"
"github.com/wtfutil/wtf/modules/textfile"
"github.com/wtfutil/wtf/modules/todo"
"github.com/wtfutil/wtf/modules/todoist"
"github.com/wtfutil/wtf/modules/todo_plus"
"github.com/wtfutil/wtf/modules/transmission"
"github.com/wtfutil/wtf/modules/travisci"
"github.com/wtfutil/wtf/modules/trello"
"github.com/wtfutil/wtf/modules/twitch"
"github.com/wtfutil/wtf/modules/twitter"
"github.com/wtfutil/wtf/modules/twitterstats"
@ -269,9 +268,12 @@ func MakeWidget(
case "todo":
settings := todo.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = todo.NewWidget(app, pages, settings)
case "todo_plus":
settings := todo_plus.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = todo_plus.NewWidget(app, pages, settings)
case "todoist":
settings := todoist.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = todoist.NewWidget(app, pages, settings)
settings := todo_plus.FromTodoist(moduleName, moduleConfig, config)
widget = todo_plus.NewWidget(app, pages, settings)
case "transmission":
settings := transmission.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = transmission.NewWidget(app, pages, settings)
@ -279,8 +281,8 @@ func MakeWidget(
settings := travisci.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = travisci.NewWidget(app, pages, settings)
case "trello":
settings := trello.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = trello.NewWidget(app, settings)
settings := todo_plus.FromTrello(moduleName, moduleConfig, config)
widget = todo_plus.NewWidget(app, pages, settings)
case "twitch":
settings := twitch.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = twitch.NewWidget(app, pages, settings)

View File

@ -37,7 +37,7 @@ func CreateFile(fileName string) (string, error) {
return "", err
}
filePath := fmt.Sprintf("%s/%s", configDir, fileName)
filePath := filepath.Join(configDir, fileName)
// Check if the file already exists; if it does not, create it
_, err = os.Stat(filePath)
@ -106,7 +106,7 @@ func LoadWtfConfigFile(filePath string) *config.Config {
// chmodConfigFile sets the mode of the config file to r+w for the owner only
func chmodConfigFile() {
configDir, _ := WtfConfigDir()
relPath := fmt.Sprintf("%s%s", configDir, WtfConfigFile)
relPath := filepath.Join(configDir, WtfConfigFile)
absPath, _ := expandHomeDir(relPath)
_, err := os.Stat(absPath)

8
go.mod
View File

@ -1,6 +1,6 @@
module github.com/wtfutil/wtf
go 1.14
go 1.15
require (
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
@ -15,7 +15,7 @@ require (
github.com/briandowns/openweathermap v0.0.0-20180804155945-5f41b7c9d92d
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/digitalocean/godo v1.44.0
github.com/digitalocean/godo v1.46.0
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/docker-credential-helpers v0.6.3
@ -40,7 +40,7 @@ require (
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b
github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191014190507-26902c1d4325
github.com/mmcdole/gofeed v1.1.0
github.com/nicklaw5/helix v0.5.9
github.com/nicklaw5/helix v0.7.0
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4
github.com/olekukonko/tablewriter v0.0.4
@ -56,6 +56,7 @@ require (
github.com/spf13/cobra v0.0.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1
github.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a
github.com/wtfutil/todoist v0.0.2-0.20191216004217-0ec29ceda61a
@ -69,6 +70,7 @@ require (
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181110093347-3be5f16b70eb // indirect
gopkg.in/yaml.v2 v2.3.0
gotest.tools v2.2.0+incompatible
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7
k8s.io/apimachinery v0.0.0-20190223094358-dcb391cde5ca
k8s.io/client-go v10.0.0+incompatible
)

12
go.sum
View File

@ -172,8 +172,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/digitalocean/godo v1.44.0 h1:IMElzMUpO1dVR8qjSg53+5vDkOLzMbhJt4yTAq7NGCQ=
github.com/digitalocean/godo v1.44.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
github.com/digitalocean/godo v1.46.0 h1:WRbwjATilgz2NE4NGMeSDpeicy9h4xSKNGuRJ/Nq/fA=
github.com/digitalocean/godo v1.46.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31 h1:Dzuw9GtbmllUqEcoHfScT9YpKFUssSiZ5PgZkIGf/YQ=
@ -551,8 +551,8 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a h1:SAjW6pL/9NssyKM1Qvyy5/V4kR3z76qlTbaqJLixhP4=
github.com/ncw/swift v0.0.0-20171019114456-c95c6e5c2d1a/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/nicklaw5/helix v0.5.9 h1:THNJ3o+OM+Dh1gPD9qh2jQlVRTPhjB0rYVBus+9GoB4=
github.com/nicklaw5/helix v0.5.9/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/nicklaw5/helix v0.7.0 h1:+E8YUqNewLFX0jSxlzKvDpyOs/v299ZZtp1aEqlonWk=
github.com/nicklaw5/helix v0.7.0/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
@ -709,6 +709,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8 h1:l6epF6yBwuejBfhGkM5m8VSNM/QAm7ApGyH35ehA7eQ=
github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -1130,6 +1132,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
k8s.io/api v0.0.0-20181204000039-89a74a8d264d h1:HQoGWsWUe/FmRcX9BU440AAMnzBFEf+DBo4nbkQlNzs=
k8s.io/api v0.0.0-20181204000039-89a74a8d264d/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20190223094358-dcb391cde5ca h1:rOczZwIfERvL7YLSQ96uI3NUYamzdK/MmSDIfChQRBk=

View File

@ -1,6 +1,7 @@
package clocks
import (
"strings"
"time"
)
@ -18,6 +19,14 @@ func NewClock(label string, timeLoc *time.Location) Clock {
return clock
}
func BuildClock(label string, location string) (clock Clock, err error) {
timeLoc, err := time.LoadLocation(sanitizeLocation(location))
if err != nil {
return Clock{}, err
}
return NewClock(label, timeLoc), nil
}
func (clock *Clock) Date(dateFormat string) string {
return clock.LocalTime().Format(dateFormat)
}
@ -33,3 +42,7 @@ func (clock *Clock) ToLocal(t time.Time) time.Time {
func (clock *Clock) Time(timeFormat string) string {
return clock.LocalTime().Format(timeFormat)
}
func sanitizeLocation(locStr string) string {
return strings.Replace(locStr, " ", "_", -1)
}

View File

@ -10,7 +10,9 @@ type ClockCollection struct {
}
func (clocks *ClockCollection) Sorted(sortOrder string) []Clock {
if sortOrder == "chronological" {
if sortOrder == "natural" {
//no-op
} else if sortOrder == "chronological" {
clocks.SortedChronologically()
} else {
clocks.SortedAlphabetically()

View File

@ -1,15 +1,13 @@
package clocks
import (
"fmt"
)
import "fmt"
func (widget *Widget) display(clocks []Clock, dateFormat string, timeFormat string) {
str := ""
if len(clocks) == 0 {
str = fmt.Sprintf("\n%s", " no timezone data available")
} else {
for idx, clock := range clocks {
str += fmt.Sprintf(
" [%s]%-12s %-10s %7s[white]\n",

View File

@ -15,10 +15,10 @@ const (
type Settings struct {
common *cfg.Common
dateFormat string `help:"The format of the date string for all clocks." values:"Any valid Go date layout which is handled by Time.Format. Defaults to Jan 2."`
timeFormat string `help:"The format of the time string for all clocks." values:"Any valid Go time layout which is handled by Time.Format. Defaults to 15:04 MST."`
locations map[string]interface{} `help:"Defines the timezones for the world clocks that you want to display. key is a unique label that will be displayed in the UI. value is a timezone name." values:"Any TZ database timezone."`
sort string `help:"Defines the display order of the clocks in the widget." values:"'alphabetical' or 'chronological'. 'alphabetical' will sort in acending order by key, 'chronological' will sort in ascending order by date/time."`
dateFormat string `help:"The format of the date string for all clocks." values:"Any valid Go date layout which is handled by Time.Format. Defaults to Jan 2."`
timeFormat string `help:"The format of the time string for all clocks." values:"Any valid Go time layout which is handled by Time.Format. Defaults to 15:04 MST."`
locations []Clock `help:"Defines the timezones for the world clocks that you want to display. key is a unique label that will be displayed in the UI. value is a timezone name." values:"Any TZ database timezone."`
sort string `help:"Defines the display order of the clocks in the widget." values:"'alphabetical', 'chronological', or 'natural. 'alphabetical' will sort in ascending order by key, 'chronological' will sort in ascending order by date/time, 'natural' will keep ordering as per the config."`
}
// NewSettingsFromYAML creates a new settings instance from a YAML config block
@ -28,9 +28,40 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co
dateFormat: ymlConfig.UString("dateFormat", utils.SimpleDateFormat),
timeFormat: ymlConfig.UString("timeFormat", utils.SimpleTimeFormat),
locations: ymlConfig.UMap("locations"),
locations: buildLocations(ymlConfig),
sort: ymlConfig.UString("sort"),
}
return &settings
}
func buildLocations(ymlConfig *config.Config) []Clock {
clocks := []Clock{}
locations, err := ymlConfig.Map("locations")
if err == nil {
for k, v := range locations {
name := k
zone := v.(string)
clock, err := BuildClock(name, zone)
if err == nil {
clocks = append(clocks, clock)
}
}
return clocks
}
listLocations := ymlConfig.UList("locations")
for _, location := range listLocations {
if location, ok := location.(map[string]interface{}); ok {
for k, v := range location {
name := k
zone := v.(string)
clock, err := BuildClock(name, zone)
if err == nil {
clocks = append(clocks, clock)
}
}
}
}
return clocks
}

View File

@ -1,9 +1,6 @@
package clocks
import (
"strings"
"time"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/view"
)
@ -28,7 +25,7 @@ func NewWidget(app *tview.Application, settings *Settings) *Widget {
timeFormat: settings.timeFormat,
}
widget.clockColl = widget.buildClockCollection(settings.locations)
widget.clockColl = widget.buildClockCollection()
return &widget
}
@ -45,21 +42,10 @@ func (widget *Widget) Refresh() {
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) buildClockCollection(locData map[string]interface{}) ClockCollection {
func (widget *Widget) buildClockCollection() 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))
}
clockColl.Clocks = widget.settings.locations
return clockColl
}
func (widget *Widget) sanitizeLocation(locStr string) string {
return strings.Replace(locStr, " ", "_", -1)
}

View File

@ -8,6 +8,7 @@ func (widget *Widget) initializeKeyboardControls() {
widget.SetKeyboardChar("j", widget.Next, "Select next item")
widget.SetKeyboardChar("k", widget.Prev, "Select previous item")
widget.SetKeyboardChar("o", widget.openStory, "Open story in browser")
widget.SetKeyboardChar("t", widget.toggleDisplayText, "Toggle display between title, link and title+content")
widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item")
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item")

View File

@ -3,11 +3,21 @@ package feedreader
import (
"fmt"
"sort"
"strings"
"github.com/mmcdole/gofeed"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/utils"
"github.com/wtfutil/wtf/view"
"jaytaylor.com/html2text"
)
type ShowType int
const (
SHOW_TITLE ShowType = iota
SHOW_LINK
SHOW_CONTENT
)
// FeedItem represents an item returned from an RSS or Atom feed
@ -25,6 +35,32 @@ type Widget struct {
parser *gofeed.Parser
settings *Settings
err error
showType ShowType
}
func rotateShowType(showtype ShowType) ShowType {
returnValue := SHOW_TITLE
switch showtype {
case SHOW_TITLE:
returnValue = SHOW_LINK
case SHOW_LINK:
returnValue = SHOW_CONTENT
case SHOW_CONTENT:
returnValue = SHOW_TITLE
}
return returnValue
}
func getShowText(feedItem *FeedItem, showType ShowType) string {
returnValue := feedItem.item.Title
switch showType {
case SHOW_LINK:
returnValue = feedItem.item.Link
case SHOW_CONTENT:
text, _ := html2text.FromString(feedItem.item.Content, html2text.Options{PrettyTables: true})
returnValue = strings.TrimSpace(feedItem.item.Title + "\n" + strings.TrimSpace(text))
}
return returnValue
}
// NewWidget creates a new instance of a widget
@ -35,6 +71,7 @@ func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *
parser: gofeed.NewParser(),
settings: settings,
showType: SHOW_TITLE,
}
widget.SetRenderFunction(widget.Render)
@ -137,11 +174,13 @@ func (widget *Widget) content() (string, string, bool) {
}
}
displayText := getShowText(feedItem, widget.showType)
row := fmt.Sprintf(
"[%s]%2d. %s[white]",
rowColor,
idx+1,
feedItem.item.Title,
displayText,
)
str += utils.HighlightableHelper(widget.View, row, idx, len(feedItem.item.Title))
@ -153,7 +192,9 @@ func (widget *Widget) content() (string, string, bool) {
// feedItems are sorted by published date
func (widget *Widget) sort(feedItems []*FeedItem) []*FeedItem {
sort.Slice(feedItems, func(i, j int) bool {
return feedItems[i].item.PublishedParsed.After(*feedItems[j].item.PublishedParsed)
return feedItems[i].item.PublishedParsed != nil &&
feedItems[j].item.PublishedParsed != nil &&
feedItems[i].item.PublishedParsed.After(*feedItems[j].item.PublishedParsed)
})
return feedItems
@ -169,3 +210,8 @@ func (widget *Widget) openStory() {
utils.OpenFile(story.item.Link)
}
}
func (widget *Widget) toggleDisplayText() {
widget.showType = rotateShowType(widget.showType)
widget.Render()
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"github.com/google/go-github/v26/github"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func (widget *Widget) display() {
@ -54,7 +56,7 @@ func (widget *Widget) content() (string, string, bool) {
return title, str, false
}
func (widget *Widget) displayMyPullRequests(repo *GithubRepo, username string) string {
func (widget *Widget) displayMyPullRequests(repo *Repo, username string) string {
prs := repo.myPullRequests(username, widget.settings.enableStatus)
prLength := len(prs)
@ -77,7 +79,7 @@ func (widget *Widget) displayMyPullRequests(repo *GithubRepo, username string) s
return str
}
func (widget *Widget) displayCustomQuery(repo *GithubRepo, filter string, perPage int) string {
func (widget *Widget) displayCustomQuery(repo *Repo, filter string, perPage int) string {
res := repo.customIssueQuery(filter, perPage)
if res == nil {
@ -104,7 +106,7 @@ func (widget *Widget) displayCustomQuery(repo *GithubRepo, filter string, perPag
return str
}
func (widget *Widget) displayMyReviewRequests(repo *GithubRepo, username string) string {
func (widget *Widget) displayMyReviewRequests(repo *Repo, username string) string {
prs := repo.myReviewRequests(username)
if len(prs) == 0 {
@ -121,18 +123,20 @@ func (widget *Widget) displayMyReviewRequests(repo *GithubRepo, username string)
return str
}
func (widget *Widget) displayStats(repo *GithubRepo) string {
func (widget *Widget) displayStats(repo *Repo) string {
prntr := message.NewPrinter(language.English)
str := fmt.Sprintf(
" PRs: %d Issues: %d Stars: %d\n",
repo.PullRequestCount(),
repo.IssueCount(),
repo.StarCount(),
" PRs: %s Issues: %s Stars: %s\n",
prntr.Sprintf("%d", repo.PullRequestCount()),
prntr.Sprintf("%d", repo.IssueCount()),
prntr.Sprintf("%d", repo.StarCount()),
)
return str
}
func (widget *Widget) title(repo *GithubRepo) string {
func (widget *Widget) title(repo *Repo) string {
return fmt.Sprintf(
"[%s]%s - %s[white]",
widget.settings.common.Colors.TextTheme.Title,

View File

@ -15,8 +15,8 @@ const (
issuesPath = "/issues"
)
// GithubRepo defines a new GithubRepo structure
type GithubRepo struct {
// Repo defines a new GitHub Repo structure
type Repo struct {
apiKey string
baseURL string
uploadURL string
@ -29,8 +29,8 @@ type GithubRepo struct {
}
// NewGithubRepo returns a new Github Repo with a name, owner, apiKey, baseURL and uploadURL
func NewGithubRepo(name, owner, apiKey, baseURL, uploadURL string) *GithubRepo {
repo := GithubRepo{
func NewGithubRepo(name, owner, apiKey, baseURL, uploadURL string) *Repo {
repo := Repo{
Name: name,
Owner: owner,
@ -43,22 +43,22 @@ func NewGithubRepo(name, owner, apiKey, baseURL, uploadURL string) *GithubRepo {
}
// Open will open the GitHub Repo URL using the utils helper
func (repo *GithubRepo) Open() {
func (repo *Repo) Open() {
utils.OpenFile(*repo.RemoteRepo.HTMLURL)
}
// Open will open the GitHub Pull Requests URL using the utils helper
func (repo *GithubRepo) OpenPulls() {
// OpenPulls will open the GitHub Pull Requests URL using the utils helper
func (repo *Repo) OpenPulls() {
utils.OpenFile(*repo.RemoteRepo.HTMLURL + pullRequestsPath)
}
// Open will open the GitHub Issues URL using the utils helper
func (repo *GithubRepo) OpenIssues() {
// OpenIssues will open the GitHub Issues URL using the utils helper
func (repo *Repo) OpenIssues() {
utils.OpenFile(*repo.RemoteRepo.HTMLURL + issuesPath)
}
// Refresh reloads the github data via the Github API
func (repo *GithubRepo) Refresh() {
func (repo *Repo) Refresh() {
prs, err := repo.loadPullRequests()
repo.Err = err
repo.PullRequests = prs
@ -73,7 +73,7 @@ func (repo *GithubRepo) Refresh() {
/* -------------------- Counts -------------------- */
// IssueCount return the total amount of issues as an int
func (repo *GithubRepo) IssueCount() int {
func (repo *Repo) IssueCount() int {
if repo.RemoteRepo == nil {
return 0
}
@ -84,12 +84,12 @@ func (repo *GithubRepo) IssueCount() int {
}
// PullRequestCount returns the total amount of pull requests as an int
func (repo *GithubRepo) PullRequestCount() int {
func (repo *Repo) PullRequestCount() int {
return len(repo.PullRequests)
}
// StarCount returns the total amount of stars this repo has gained as an int
func (repo *GithubRepo) StarCount() int {
func (repo *Repo) StarCount() int {
if repo.RemoteRepo == nil {
return 0
}
@ -99,7 +99,7 @@ func (repo *GithubRepo) StarCount() int {
/* -------------------- Unexported Functions -------------------- */
func (repo *GithubRepo) isGitHubEnterprise() bool {
func (repo *Repo) isGitHubEnterprise() bool {
if len(repo.baseURL) > 0 {
if len(repo.uploadURL) == 0 {
repo.uploadURL = repo.baseURL
@ -109,7 +109,7 @@ func (repo *GithubRepo) isGitHubEnterprise() bool {
return false
}
func (repo *GithubRepo) oauthClient() *http.Client {
func (repo *Repo) oauthClient() *http.Client {
tokenService := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: repo.apiKey},
)
@ -117,7 +117,7 @@ func (repo *GithubRepo) oauthClient() *http.Client {
return oauth2.NewClient(context.Background(), tokenService)
}
func (repo *GithubRepo) githubClient() (*ghb.Client, error) {
func (repo *Repo) githubClient() (*ghb.Client, error) {
oauthClient := repo.oauthClient()
if repo.isGitHubEnterprise() {
@ -128,7 +128,7 @@ func (repo *GithubRepo) githubClient() (*ghb.Client, error) {
}
// myPullRequests returns a list of pull requests created by username on this repo
func (repo *GithubRepo) myPullRequests(username string, showStatus bool) []*ghb.PullRequest {
func (repo *Repo) myPullRequests(username string, showStatus bool) []*ghb.PullRequest {
prs := []*ghb.PullRequest{}
for _, pr := range repo.PullRequests {
@ -149,7 +149,7 @@ func (repo *GithubRepo) myPullRequests(username string, showStatus bool) []*ghb.
// 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 {
func (repo *Repo) individualPRs(prs []*ghb.PullRequest) []*ghb.PullRequest {
github, err := repo.githubClient()
if err != nil {
return prs
@ -170,7 +170,7 @@ func (repo *GithubRepo) individualPRs(prs []*ghb.PullRequest) []*ghb.PullRequest
// 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 {
func (repo *Repo) myReviewRequests(username string) []*ghb.PullRequest {
prs := []*ghb.PullRequest{}
for _, pr := range repo.PullRequests {
@ -184,7 +184,7 @@ func (repo *GithubRepo) myReviewRequests(username string) []*ghb.PullRequest {
return prs
}
func (repo *GithubRepo) customIssueQuery(filter string, perPage int) *ghb.IssuesSearchResult {
func (repo *Repo) customIssueQuery(filter string, perPage int) *ghb.IssuesSearchResult {
github, err := repo.githubClient()
if err != nil {
return nil
@ -199,7 +199,7 @@ func (repo *GithubRepo) customIssueQuery(filter string, perPage int) *ghb.Issues
return prs
}
func (repo *GithubRepo) loadPullRequests() ([]*ghb.PullRequest, error) {
func (repo *Repo) loadPullRequests() ([]*ghb.PullRequest, error) {
github, err := repo.githubClient()
if err != nil {
return nil, err
@ -217,7 +217,7 @@ func (repo *GithubRepo) loadPullRequests() ([]*ghb.PullRequest, error) {
return prs, nil
}
func (repo *GithubRepo) loadRemoteRepository() (*ghb.Repository, error) {
func (repo *Repo) loadRemoteRepository() (*ghb.Repository, error) {
github, err := repo.githubClient()
if err != nil {

View File

@ -15,7 +15,7 @@ type Widget struct {
view.KeyboardWidget
view.TextWidget
GithubRepos []*GithubRepo
GithubRepos []*Repo
settings *Settings
Selected int
@ -110,8 +110,8 @@ func (widget *Widget) HelpText() string {
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) buildRepoCollection(repoData []string) []*GithubRepo {
githubRepos := []*GithubRepo{}
func (widget *Widget) buildRepoCollection(repoData []string) []*Repo {
githubRepos := []*Repo{}
for _, repo := range repoData {
split := strings.Split(repo, "/")
@ -130,7 +130,7 @@ func (widget *Widget) buildRepoCollection(repoData []string) []*GithubRepo {
return githubRepos
}
func (widget *Widget) currentGithubRepo() *GithubRepo {
func (widget *Widget) currentGithubRepo() *Repo {
if len(widget.GithubRepos) == 0 {
return nil
}

View File

@ -0,0 +1,16 @@
package backend
import (
"github.com/olebedev/config"
)
type Backend interface {
Title() string
Setup(*config.Config)
BuildProjects() []*Project
GetProject(string) *Project
LoadTasks(string) ([]Task, error)
CloseTask(*Task) error
DeleteTask(*Task) error
Sources() []string
}

View File

@ -0,0 +1,66 @@
package backend
type Task struct {
ID string
Completed bool
Name string
}
type Project struct {
ID string
Name string
Index int
Tasks []Task
Err error
backend Backend
}
func (proj *Project) IsLast() bool {
return proj.Index >= len(proj.Tasks)-1
}
func (proj *Project) loadTasks() {
Tasks, err := proj.backend.LoadTasks(proj.ID)
proj.Err = err
proj.Tasks = Tasks
}
func (proj *Project) LongestLine() int {
maxLen := 0
for _, task := range proj.Tasks {
if len(task.Name) > maxLen {
maxLen = len(task.Name)
}
}
return maxLen
}
func (proj *Project) currentTask() *Task {
if proj.Index < 0 {
return nil
}
return &proj.Tasks[proj.Index]
}
func (proj *Project) CloseSelectedTask() {
currTask := proj.currentTask()
if currTask != nil {
_ = proj.backend.CloseTask(currTask)
proj.loadTasks()
}
}
func (proj *Project) DeleteSelectedTask() {
currTask := proj.currentTask()
if currTask != nil {
_ = proj.backend.DeleteTask(currTask)
proj.loadTasks()
}
}

View File

@ -0,0 +1,108 @@
package backend
import (
"strconv"
"github.com/olebedev/config"
"github.com/wtfutil/todoist"
)
type Todoist struct {
projects []interface{}
}
func (todo *Todoist) Title() string {
return "Todoist"
}
func (todo *Todoist) Setup(config *config.Config) {
todoist.Token = config.UString("apiKey")
todo.projects = config.UList("projects")
}
func (todo *Todoist) BuildProjects() []*Project {
projects := []*Project{}
for _, id := range todo.projects {
i := strconv.Itoa(id.(int))
proj := todo.GetProject(i)
projects = append(projects, proj)
}
return projects
}
func (todo *Todoist) GetProject(id string) *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
proj := &Project{
Index: -1,
backend: todo,
}
i64, _ := strconv.ParseUint(id, 10, 32)
i := uint(i64)
project, err := todoist.GetProject(i)
if err != nil {
proj.Err = err
return proj
}
proj.ID = strconv.FormatUint(uint64(project.ID), 10)
proj.Name = project.Name
tasks, err := todo.LoadTasks(proj.ID)
proj.Err = err
proj.Tasks = tasks
return proj
}
func toTask(task todoist.Task) Task {
id := strconv.FormatUint(uint64(task.ID), 10)
return Task{
ID: id,
Completed: task.Completed,
Name: task.Content,
}
}
func (todo *Todoist) LoadTasks(id string) ([]Task, error) {
tasks, err := todoist.ListTask(todoist.QueryParam{"project_id": id})
if err != nil {
return nil, err
}
var finalTasks []Task
for _, item := range tasks {
finalTasks = append(finalTasks, toTask(item))
}
return finalTasks, nil
}
func (todo *Todoist) CloseTask(task *Task) error {
if task != nil {
i64, _ := strconv.ParseUint(task.ID, 10, 32)
i := uint(i64)
internal := todoist.Task{ID: i}
return internal.Close()
}
return nil
}
func (todo *Todoist) DeleteTask(task *Task) error {
if task != nil {
i64, _ := strconv.ParseUint(task.ID, 10, 32)
i := uint(i64)
internal := todoist.Task{ID: i}
return internal.Delete()
}
return nil
}
func (todo *Todoist) Sources() []string {
var result []string
for _, id := range todo.projects {
i := strconv.Itoa(id.(int))
result = append(result, i)
}
return result
}

View File

@ -0,0 +1,170 @@
package backend
import (
"fmt"
"log"
"github.com/adlio/trello"
"github.com/olebedev/config"
)
type Trello struct {
username string
boardName string
client *trello.Client
board string
projects []interface{}
}
func (todo *Trello) Title() string {
return "Trello"
}
func (todo *Trello) Setup(config *config.Config) {
todo.username = config.UString("username")
todo.boardName = config.UString("board")
todo.client = trello.NewClient(
config.UString("apiKey"),
config.UString("accessToken"),
)
board, err := getBoardID(todo.client, todo.username, todo.boardName)
if err != nil {
log.Fatal(err)
}
todo.board = board
todo.projects = config.UList("lists")
}
func getBoardID(client *trello.Client, username, boardName string) (string, error) {
member, err := client.GetMember(username, trello.Defaults())
if err != nil {
return "", err
}
boards, err := member.GetBoards(trello.Defaults())
if err != nil {
return "", err
}
for _, board := range boards {
if board.Name == boardName {
return board.ID, nil
}
}
return "", fmt.Errorf("could not find board with name %s", boardName)
}
func getListId(client *trello.Client, boardID string, listName string) (string, error) {
board, err := client.GetBoard(boardID, trello.Defaults())
if err != nil {
return "", err
}
boardLists, err := board.GetLists(trello.Defaults())
if err != nil {
return "", err
}
for _, list := range boardLists {
if list.Name == listName {
return list.ID, nil
}
}
return "", nil
}
func getCardsOnList(client *trello.Client, listID string) ([]*trello.Card, error) {
list, err := client.GetList(listID, trello.Defaults())
if err != nil {
return nil, err
}
cards, err := list.GetCards(trello.Defaults())
if err != nil {
return nil, err
}
return cards, nil
}
func (todo *Trello) BuildProjects() []*Project {
projects := []*Project{}
for _, id := range todo.projects {
proj := todo.GetProject(id.(string))
projects = append(projects, proj)
}
return projects
}
func (todo *Trello) GetProject(id string) *Project {
proj := &Project{
Index: -1,
backend: todo,
}
listId, err := getListId(todo.client, todo.board, id)
if err != nil {
proj.Err = err
return proj
}
proj.ID = listId
proj.Name = id
tasks, err := todo.LoadTasks(listId)
proj.Err = err
proj.Tasks = tasks
return proj
}
func fromTrello(task *trello.Card) Task {
return Task{
ID: task.ID,
Completed: task.Closed,
Name: task.Name,
}
}
func (todo *Trello) LoadTasks(id string) ([]Task, error) {
tasks, err := getCardsOnList(todo.client, id)
if err != nil {
return nil, err
}
var finalTasks []Task
for _, item := range tasks {
finalTasks = append(finalTasks, fromTrello(item))
}
return finalTasks, nil
}
func (todo *Trello) CloseTask(task *Task) error {
args := trello.Arguments{
"closed": "true",
}
if task != nil {
// Card has an internal client rep which we can't access
// Just force a lookup
internal, err := todo.client.GetCard(task.ID, trello.Arguments{})
if err != nil {
return err
}
return internal.Update(args)
}
return nil
}
func (todo *Trello) DeleteTask(task *Task) error {
return nil
}
func (todo *Trello) Sources() []string {
var result []string
for _, id := range todo.projects {
result = append(result, id.(string))
}
return result
}

View File

@ -1,4 +1,4 @@
package todoist
package todo_plus
import (
"fmt"
@ -14,27 +14,26 @@ func (widget *Widget) content() (string, string, bool) {
return widget.CommonSettings().Title, "", false
}
if proj.err != nil {
return widget.CommonSettings().Title, proj.err.Error(), true
if proj.Err != nil {
return widget.CommonSettings().Title, proj.Err.Error(), true
}
title := fmt.Sprintf(
"[%s]%s[white]",
widget.settings.common.Colors.TextTheme.Title,
proj.Project.Name,
)
proj.Name)
str := ""
for idx, item := range proj.tasks {
for idx, item := range proj.Tasks {
row := fmt.Sprintf(
`[%s]| | %s[%s]`,
widget.RowColor(idx),
tview.Escape(item.Content),
tview.Escape(item.Name),
widget.RowColor(idx),
)
str += utils.HighlightableHelper(widget.View, row, idx, len(item.Content))
str += utils.HighlightableHelper(widget.View, row, idx, len(item.Name))
}
return title, str, false
}

View File

@ -1,21 +1,21 @@
package todoist
package todo_plus
import "github.com/gdamore/tcell"
func (widget *Widget) initializeKeyboardControls() {
widget.InitializeCommonControls(widget.Refresh)
widget.SetKeyboardChar("c", widget.Close, "Close item")
widget.SetKeyboardChar("d", widget.Delete, "Delete item")
widget.SetKeyboardChar("h", widget.PrevSource, "Select previous project")
widget.SetKeyboardChar("j", widget.Prev, "Select previous item")
widget.SetKeyboardChar("k", widget.Next, "Select next item")
widget.SetKeyboardChar("h", widget.PrevSource, "Select previous project")
widget.SetKeyboardChar("c", widget.Close, "Close item")
widget.SetKeyboardChar("l", widget.NextSource, "Select next project")
widget.SetKeyboardChar("u", widget.Unselect, "Clear selection")
widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item")
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item")
widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection")
widget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, "Select previous project")
widget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, "Select next project")
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item")
}

View File

@ -0,0 +1,81 @@
package todo_plus
import (
"os"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
)
const (
defaultTitle = "Todo"
defaultFocusable = true
)
type Settings struct {
common *cfg.Common
backendType string
backendSettings *config.Config
}
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
backend, _ := ymlConfig.Get("backendSettings")
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
backendType: ymlConfig.UString("backendType"),
backendSettings: backend,
}
return &settings
}
func FromTodoist(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
apiKey := ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_TODOIST_TOKEN")))
cfg.ModuleSecret(name, globalConfig, &apiKey).Load()
projects := ymlConfig.UList("projects")
backend, _ := config.ParseYaml("apiKey: " + apiKey)
_ = backend.Set(".projects", projects)
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
backendType: "todoist",
backendSettings: backend,
}
return &settings
}
func FromTrello(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
accessToken := ymlConfig.UString("accessToken", ymlConfig.UString("apikey", os.Getenv("WTF_TRELLO_ACCESS_TOKEN")))
apiKey := ymlConfig.UString("apiKey", os.Getenv("WTF_TRELLO_API_KEY"))
cfg.ModuleSecret(name, globalConfig, &apiKey).Load()
board := ymlConfig.UString("board")
username := ymlConfig.UString("username")
var lists []interface{}
list, err := ymlConfig.String("list")
if err == nil {
lists = append(lists, list)
} else {
lists = ymlConfig.UList("list")
}
backend, _ := config.ParseYaml("apiKey: " + apiKey)
_ = backend.Set(".accessToken", accessToken)
_ = backend.Set(".board", board)
_ = backend.Set(".username", username)
_ = backend.Set(".lists", lists)
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
backendType: "trello",
backendSettings: backend,
}
return &settings
}

View File

@ -1,8 +1,10 @@
package todoist
package todo_plus
import (
"log"
"github.com/rivo/tview"
"github.com/wtfutil/todoist"
"github.com/wtfutil/wtf/modules/todo_plus/backend"
"github.com/wtfutil/wtf/view"
)
@ -12,8 +14,9 @@ type Widget struct {
view.MultiSourceWidget
view.ScrollableWidget
projects []*Project
projects []*backend.Project
settings *Settings
backend backend.Backend
}
// NewWidget creates a new instance of a widget
@ -26,8 +29,9 @@ func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *
settings: settings,
}
widget.loadAPICredentials()
widget.loadProjects()
widget.backend = getBackend(settings.backendType)
widget.backend.Setup(settings.backendSettings)
widget.CommonSettings().Title = widget.backend.Title()
widget.SetRenderFunction(widget.display)
widget.initializeKeyboardControls()
@ -39,13 +43,28 @@ func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *
return &widget
}
func getBackend(backendType string) backend.Backend {
switch backendType {
case "trello":
backend := &backend.Trello{}
return backend
case "todoist":
backend := &backend.Todoist{}
return backend
default:
log.Fatal(backendType + " is not a supported backend")
return nil
}
}
/* -------------------- Exported Functions -------------------- */
func (widget *Widget) CurrentProject() *Project {
func (widget *Widget) CurrentProject() *backend.Project {
return widget.ProjectAt(widget.Idx)
}
func (widget *Widget) ProjectAt(idx int) *Project {
func (widget *Widget) ProjectAt(idx int) *backend.Project {
if len(widget.projects) == 0 {
return nil
}
@ -54,14 +73,13 @@ func (widget *Widget) ProjectAt(idx int) *Project {
}
func (widget *Widget) Refresh() {
if widget.Disabled() || widget.CurrentProject() == nil {
widget.SetItemCount(0)
if widget.Disabled() {
return
}
widget.loadProjects()
widget.SetItemCount(len(widget.CurrentProject().tasks))
widget.projects = widget.backend.BuildProjects()
widget.Sources = widget.backend.Sources()
widget.SetItemCount(len(widget.CurrentProject().Tasks))
widget.display()
}
@ -71,74 +89,57 @@ func (widget *Widget) HelpText() string {
func (widget *Widget) NextSource() {
widget.MultiSourceWidget.NextSource()
widget.Selected = widget.CurrentProject().index
widget.SetItemCount(len(widget.CurrentProject().tasks))
widget.Selected = widget.CurrentProject().Index
widget.SetItemCount(len(widget.CurrentProject().Tasks))
widget.RenderFunction()
}
func (widget *Widget) PrevSource() {
widget.MultiSourceWidget.PrevSource()
widget.Selected = widget.CurrentProject().index
widget.SetItemCount(len(widget.CurrentProject().tasks))
widget.Selected = widget.CurrentProject().Index
widget.SetItemCount(len(widget.CurrentProject().Tasks))
widget.RenderFunction()
}
func (widget *Widget) Prev() {
widget.ScrollableWidget.Prev()
widget.CurrentProject().index = widget.Selected
widget.CurrentProject().Index = widget.Selected
}
func (widget *Widget) Next() {
widget.ScrollableWidget.Next()
widget.CurrentProject().index = widget.Selected
widget.CurrentProject().Index = widget.Selected
}
func (widget *Widget) Unselect() {
widget.ScrollableWidget.Unselect()
widget.CurrentProject().index = -1
widget.CurrentProject().Index = -1
widget.RenderFunction()
}
/* -------------------- Keyboard Movement -------------------- */
// Close closes the currently-selected task in the currently-selected project
func (widget *Widget) Close() {
widget.CurrentProject().closeSelectedTask()
widget.SetItemCount(len(widget.CurrentProject().tasks))
func (w *Widget) Close() {
w.CurrentProject().CloseSelectedTask()
w.SetItemCount(len(w.CurrentProject().Tasks))
if widget.CurrentProject().isLast() {
widget.Prev()
if w.CurrentProject().IsLast() {
w.Prev()
return
}
widget.CurrentProject().index = widget.Selected
widget.RenderFunction()
w.CurrentProject().Index = w.Selected
w.RenderFunction()
}
// Delete deletes the currently-selected task in the currently-selected project
func (widget *Widget) Delete() {
widget.CurrentProject().deleteSelectedTask()
widget.SetItemCount(len(widget.CurrentProject().tasks))
func (w *Widget) Delete() {
w.CurrentProject().DeleteSelectedTask()
w.SetItemCount(len(w.CurrentProject().Tasks))
if widget.CurrentProject().isLast() {
widget.Prev()
if w.CurrentProject().IsLast() {
w.Prev()
}
widget.CurrentProject().index = widget.Selected
widget.RenderFunction()
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) loadAPICredentials() {
todoist.Token = widget.settings.apiKey
}
func (widget *Widget) loadProjects() {
projects := []*Project{}
for _, id := range widget.settings.projects {
proj := NewProject(id)
projects = append(projects, proj)
}
widget.projects = projects
w.CurrentProject().Index = w.Selected
w.RenderFunction()
}

View File

@ -1,93 +0,0 @@
package todoist
import (
"fmt"
"github.com/wtfutil/todoist"
)
type Project struct {
todoist.Project
index int
tasks []todoist.Task
err error
}
func NewProject(id uint) *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)
proj := &Project{
index: -1,
}
if err != nil {
proj.err = err
return proj
}
proj.Project = project
proj.loadTasks()
return proj
}
func (proj *Project) isLast() bool {
return proj.index >= len(proj.tasks)-1
}
func (proj *Project) loadTasks() {
tasks, err := todoist.ListTask(todoist.QueryParam{"project_id": fmt.Sprintf("%d", proj.ID)})
if err != nil {
proj.err = err
proj.tasks = nil
} else {
proj.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()
}
}

View File

@ -1,36 +0,0 @@
package todoist
import (
"os"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
)
const (
defaultFocusable = true
defaultTitle = "Todoist"
)
// Settings defines the configuration properties for this module
type Settings struct {
common *cfg.Common
apiKey string `help:"Your Todoist API key"`
projects []uint
}
// NewSettingsFromYAML creates a new settings instance from a YAML config block
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_TODOIST_TOKEN"))),
projects: utils.IntsToUints(utils.ToInts(ymlConfig.UList("projects"))),
}
cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()
return &settings
}

View File

@ -1,13 +0,0 @@
package trello
type TrelloCard struct {
ID string
Name string
List string
Description string
}
type TrelloList struct {
ID string
Name string
}

View File

@ -1,106 +0,0 @@
package trello
import (
"fmt"
"github.com/adlio/trello"
)
func GetCards(client *trello.Client, username string, boardName string, listNames []string) (*SearchResult, error) {
boardID, err := getBoardID(client, username, boardName)
if err != nil {
return nil, err
}
lists, err := getLists(client, boardID, listNames)
if err != nil {
return nil, err
}
searchResult := &SearchResult{Total: 0}
searchResult.TrelloCards = make(map[string][]TrelloCard)
for _, list := range lists {
cards, err := getCardsOnList(client, list.ID)
if err != nil {
return nil, err
}
searchResult.Total += len(cards)
cardArray := make([]TrelloCard, 0)
for _, card := range cards {
trelloCard := TrelloCard{
ID: card.ID,
List: list.Name,
Name: card.Name,
Description: card.Desc,
}
cardArray = append(cardArray, trelloCard)
}
searchResult.TrelloCards[list.Name] = cardArray
}
return searchResult, nil
}
func getBoardID(client *trello.Client, username, boardName string) (string, error) {
member, err := client.GetMember(username, trello.Defaults())
if err != nil {
return "", err
}
boards, err := member.GetBoards(trello.Defaults())
if err != nil {
return "", err
}
for _, board := range boards {
if board.Name == boardName {
return board.ID, nil
}
}
return "", fmt.Errorf("could not find board with name %s", boardName)
}
func getLists(client *trello.Client, boardID string, listNames []string) ([]TrelloList, error) {
comparison := make(map[string]string, len(listNames))
results := []TrelloList{}
//convert to a map for comparison
for _, item := range listNames {
comparison[item] = ""
}
board, err := client.GetBoard(boardID, trello.Defaults())
if err != nil {
return nil, err
}
boardLists, err := board.GetLists(trello.Defaults())
if err != nil {
return nil, err
}
for _, list := range boardLists {
if _, ok := comparison[list.Name]; ok {
results = append(results, TrelloList{ID: list.ID, Name: list.Name})
}
}
return results, nil
}
func getCardsOnList(client *trello.Client, listID string) ([]*trello.Card, error) {
list, err := client.GetList(listID, trello.Defaults())
if err != nil {
return nil, err
}
cards, err := list.GetCards(trello.Defaults())
if err != nil {
return nil, err
}
return cards, nil
}

View File

@ -1,6 +0,0 @@
package trello
type SearchResult struct {
Total int
TrelloCards map[string][]TrelloCard
}

View File

@ -1,62 +0,0 @@
package trello
import (
"os"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
)
const (
defaultFocusable = false
defaultTitle = "Trello"
)
type Settings struct {
common *cfg.Common
accessToken string
apiKey string
board string
list []string
username string
}
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
accessToken: ymlConfig.UString("accessToken", ymlConfig.UString("apikey", os.Getenv("WTF_TRELLO_ACCESS_TOKEN"))),
apiKey: ymlConfig.UString("apiKey", os.Getenv("WTF_TRELLO_API_KEY")),
board: ymlConfig.UString("board"),
username: ymlConfig.UString("username"),
}
cfg.ModuleSecret(name+"-api", globalConfig, &settings.apiKey).Load()
cfg.ModuleSecret(name+"-access", globalConfig, &settings.accessToken).Load()
settings.list = buildLists(ymlConfig, globalConfig)
return &settings
}
func buildLists(ymlConfig *config.Config, globalConfig *config.Config) []string {
lists := []string{}
// Single list
list, err := ymlConfig.String("list")
if err == nil {
lists = append(lists, list)
return lists
}
// Array of lists
listList := ymlConfig.UList("list")
for _, listName := range listList {
if list, ok := listName.(string); ok {
lists = append(lists, list)
}
}
return lists
}

View File

@ -1,75 +0,0 @@
package trello
import (
"fmt"
"github.com/adlio/trello"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/view"
)
type Widget struct {
view.TextWidget
settings *Settings
}
func NewWidget(app *tview.Application, settings *Settings) *Widget {
widget := Widget{
TextWidget: view.NewTextWidget(app, settings.common),
settings: settings,
}
return &widget
}
/* -------------------- Exported Functions -------------------- */
func (widget *Widget) Refresh() {
widget.Redraw(widget.content)
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) content() (string, string, bool) {
client := trello.NewClient(
widget.settings.apiKey,
widget.settings.accessToken,
)
// Get the cards
searchResult, err := GetCards(
client,
widget.settings.username,
widget.settings.board,
widget.settings.list,
)
var title string
content := ""
wrap := false
if err != nil {
wrap = true
title = widget.CommonSettings().Title
content = err.Error()
} else {
title = fmt.Sprintf(
"[white]%s: [green]%s ",
widget.CommonSettings().Title,
widget.settings.board,
)
for list, cardArray := range searchResult.TrelloCards {
content += fmt.Sprintf(" [%s]%s[white]\n", widget.settings.common.Colors.Subheading, list)
for _, card := range cardArray {
content += fmt.Sprintf(" %s[white]\n", card.Name)
}
content = fmt.Sprintf("%s\n", content)
}
}
return title, content, wrap
}

62
view/info_table_test.go Normal file
View File

@ -0,0 +1,62 @@
package view
import (
"testing"
"github.com/stretchr/testify/assert"
)
func makeMap() map[string]string {
m := make(map[string]string)
m["foo"] = "val1"
m["bar"] = "val2"
m["baz"] = "val3"
return m
}
func newTestTable(height, colWidth0, colWidth1 int) *InfoTable {
var headers [2]string
headers[0] = "hdr0"
headers[1] = "hdr1"
table := NewInfoTable(
headers[:],
makeMap(),
colWidth0,
colWidth1,
height,
)
return table
}
func Test_RenderSimpleInfoTable(t *testing.T) {
table := newTestTable(4, 1, 1).Render()
assert.Equal(t, " ----- ------ \n HDR0 HDR1 \n ----- ------ \n bar val2 \n baz val3 \n foo val1 \n ----- ------ \n", table)
}
func Test_RenderPaddedInfoTable(t *testing.T) {
table := newTestTable(6, 1, 1).Render()
assert.Equal(t, " ----- ------ \n HDR0 HDR1 \n ----- ------ \n bar val2 \n baz val3 \n foo val1 \n \n \n ----- ------ \n", table)
}
func Test_RenderWithSpecifiedWidthLeftColumn(t *testing.T) {
table := newTestTable(4, 10, 1).Render()
assert.Equal(t, " ------------ ------ \n HDR0 HDR1 \n ------------ ------ \n bar val2 \n baz val3 \n foo val1 \n ------------ ------ \n", table)
}
func Test_RenderWithSpecifiedWidthRightColumn(t *testing.T) {
table := newTestTable(4, 1, 10).Render()
assert.Equal(t, " ----- ------------ \n HDR0 HDR1 \n ----- ------------ \n bar val2 \n baz val3 \n foo val1 \n ----- ------------ \n", table)
}
func Test_RenderWithSpecifiedWidthBothColumns(t *testing.T) {
table := newTestTable(4, 15, 10).Render()
assert.Equal(t, " ----------------- ------------ \n HDR0 HDR1 \n ----------------- ------------ \n bar val2 \n baz val3 \n foo val1 \n ----------------- ------------ \n", table)
}

View File

@ -1,6 +1,8 @@
package view
import (
"strings"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/wtf"
@ -36,7 +38,9 @@ func (widget *TextWidget) Redraw(data func() (string, string, bool)) {
widget.View.Clear()
widget.View.SetWrap(wrap)
widget.View.SetTitle(widget.ContextualTitle(title))
widget.View.SetText(content)
// widget.View.SetText(strings.TrimSpace(content))
widget.View.SetText(strings.TrimRight(content, "\n"))
// widget.View.SetText(content)
})
}