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/digitalocean/godo-1.46.0

This commit is contained in:
Chris Cummer 2020-10-08 11:42:05 -07:00
commit 2990ff4ac0
30 changed files with 693 additions and 525 deletions

View File

@ -973,14 +973,27 @@
"name": "Martin Johns",
"avatar_url": "https://avatars1.githubusercontent.com/u/5269069?v=4",
"profile": "https://github.com/MartinJohns",
"contributions": [
]
"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

@ -5,6 +5,7 @@
### ⚡️ 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

View File

@ -403,6 +403,10 @@ Dependency management in WTF is handled by [Go modules](https://github.com/golan
<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

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

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

@ -38,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(strings.TrimSpace(content))
// widget.View.SetText(strings.TrimSpace(content))
widget.View.SetText(strings.TrimRight(content, "\n"))
// widget.View.SetText(content)
})
}