diff --git a/app/widget_maker.go b/app/widget_maker.go index b9a3fcdc..3c366ae4 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -26,6 +26,7 @@ import ( "github.com/wtfutil/wtf/modules/git" "github.com/wtfutil/wtf/modules/github" "github.com/wtfutil/wtf/modules/gitlab" + "github.com/wtfutil/wtf/modules/gitlabtodo" "github.com/wtfutil/wtf/modules/gitter" "github.com/wtfutil/wtf/modules/googleanalytics" "github.com/wtfutil/wtf/modules/gspreadsheets" @@ -162,6 +163,9 @@ func MakeWidget( case "gitlab": settings := gitlab.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = gitlab.NewWidget(app, pages, settings) + case "gitlabtodo": + settings := gitlabtodo.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = gitlabtodo.NewWidget(app, pages, settings) case "gitter": settings := gitter.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = gitter.NewWidget(app, pages, settings) diff --git a/modules/gitlabtodo/keyboard.go b/modules/gitlabtodo/keyboard.go new file mode 100644 index 00000000..e6507d92 --- /dev/null +++ b/modules/gitlabtodo/keyboard.go @@ -0,0 +1,17 @@ +package gitlabtodo + +import "github.com/gdamore/tcell" + +func (widget *Widget) initializeKeyboardControls() { + widget.SetKeyboardChar("/", widget.ShowHelp, "Show/hide this help widget") + widget.SetKeyboardChar("r", widget.Refresh, "Refresh widget") + widget.SetKeyboardChar("j", widget.Next, "Select next item") + widget.SetKeyboardChar("k", widget.Prev, "Select previous item") + widget.SetKeyboardChar("o", widget.openTodo, "Open todo in browser") + widget.SetKeyboardChar("x", widget.markAsDone, "Mark todo as done") + + widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item") + widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item") + widget.SetKeyboardKey(tcell.KeyEnter, widget.openTodo, "Open todo in browser") + widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection") +} diff --git a/modules/gitlabtodo/settings.go b/modules/gitlabtodo/settings.go new file mode 100644 index 00000000..b1f3d72d --- /dev/null +++ b/modules/gitlabtodo/settings.go @@ -0,0 +1,36 @@ +package gitlabtodo + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "GitLab Todos" +) + +type Settings struct { + common *cfg.Common + + numberOfTodos int `help:"Defines number of stories to be displayed. Default is 10" optional:"true"` + apiKey string `help:"A GitLab personal access token. Requires at least api access."` + domain string `help:"Your GitLab corporate domain."` + showProject bool `help:"Determines whether or not to show the project a given todo is for."` +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + numberOfTodos: ymlConfig.UInt("numberOfTodos", 10), + apiKey: ymlConfig.UString("apiKey", os.Getenv("WTF_GITLAB_TOKEN")), + domain: ymlConfig.UString("domain"), + showProject: ymlConfig.UBool("showProject", true), + } + + return &settings +} diff --git a/modules/gitlabtodo/widget.go b/modules/gitlabtodo/widget.go new file mode 100644 index 00000000..5e412185 --- /dev/null +++ b/modules/gitlabtodo/widget.go @@ -0,0 +1,139 @@ +package gitlabtodo + +import ( + "fmt" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/utils" + "github.com/wtfutil/wtf/view" + gitlab "github.com/xanzy/go-gitlab" +) + +type Widget struct { + view.KeyboardWidget + view.ScrollableWidget + + todos []*gitlab.Todo + gitlabClient *gitlab.Client + settings *Settings + err error +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := &Widget{ + KeyboardWidget: view.NewKeyboardWidget(app, pages, settings.common), + ScrollableWidget: view.NewScrollableWidget(app, settings.common), + + settings: settings, + } + + widget.gitlabClient = gitlab.NewClient(nil, settings.apiKey) + + widget.SetRenderFunction(widget.Render) + widget.initializeKeyboardControls() + widget.View.SetInputCapture(widget.InputCapture) + + widget.KeyboardWidget.SetView(widget.View) + + return widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + todos, _ := widget.getTodos(widget.settings.apiKey) + widget.todos = todos + widget.SetItemCount(len(todos)) + + widget.Render() +} + +// Render sets up the widget data for redrawing to the screen +func (widget *Widget) Render() { + widget.Redraw(widget.content) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + title := fmt.Sprintf("GitLab ToDos (%d)", len(widget.todos)) + + if widget.err != nil { + return title, widget.err.Error(), true + } + + if widget.todos == nil { + return title, "No ToDos to display", false + } + + str := widget.contentFrom(widget.todos) + + return title, str, false +} + +func (widget *Widget) getTodos(apiKey string) ([]*gitlab.Todo, error) { + opts := gitlab.ListTodosOptions{} + + todos, _, err := widget.gitlabClient.Todos.ListTodos(&opts) + if err != nil { + return nil, err + } + + return todos, nil +} + +// trim the todo body so it fits on a single line +func (widget *Widget) trimTodoBody(body string) string { + r := []rune(body) + + // Cut at first occurance of a newline + for i, a := range r { + if a == '\n' { + return string(r[:i]) + } + } + + return body +} + +func (widget *Widget) contentFrom(todos []*gitlab.Todo) string { + var str string + + for idx, todo := range todos { + row := fmt.Sprintf(`[%s]%2d. `, widget.RowColor(idx), idx+1) + if widget.settings.showProject { + row = fmt.Sprintf(`%s%s `, row, todo.Project.Path) + } + row = fmt.Sprintf(`%s[mediumpurple](%s)[%s] %s`, + row, + todo.Author.Username, + widget.RowColor(idx), + widget.trimTodoBody(todo.Body), + ) + + str += utils.HighlightableHelper(widget.View, row, idx, len(todo.Body)) + } + + return str +} + +func (widget *Widget) markAsDone() { + sel := widget.GetSelected() + if sel >= 0 && widget.todos != nil && sel < len(widget.todos) { + todo := widget.todos[sel] + widget.gitlabClient.Todos.MarkTodoAsDone(todo.ID) + widget.Refresh() + } +} + +func (widget *Widget) openTodo() { + sel := widget.GetSelected() + if sel >= 0 && widget.todos != nil && sel < len(widget.todos) { + todo := widget.todos[sel] + utils.OpenFile(todo.TargetURL) + } +}