diff --git a/go.mod b/go.mod index 9ae21c1f..7fb0caf2 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,10 @@ require ( github.com/google/pprof v0.0.0-20190515194954-54271f7e092f // indirect github.com/gorilla/mux v1.7.1 // indirect github.com/grpc-ecosystem/grpc-gateway v1.9.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/hekmon/cunits v2.0.1+incompatible // indirect + github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd github.com/jessevdk/go-flags v1.4.0 github.com/kisielk/errcheck v1.2.0 // indirect github.com/kisielk/gotool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 35f0a27a..6d2cd83e 100644 --- a/go.sum +++ b/go.sum @@ -122,9 +122,15 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hekmon/cunits v2.0.1+incompatible h1:yy/Wq5YvNtrweqfeRjvrvMdBMH6axBrlL8t7arLlm5A= +github.com/hekmon/cunits v2.0.1+incompatible/go.mod h1:0QdfIGGkucx1VgStMNiHOYn84t/Ru65b+D3z1QszVPc= +github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd h1:bZZYKxyrUV8EsKB5AjsPsJiWE7n0FDURijlSzYZVqAM= +github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd/go.mod h1:b1R6hlzo+gEUNWGh53mw9mPWyYyODdZu/qlVqT+W+PU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/maker/widget_maker.go b/maker/widget_maker.go index d02a970a..4d3a2791 100644 --- a/maker/widget_maker.go +++ b/maker/widget_maker.go @@ -41,6 +41,7 @@ import ( "github.com/wtfutil/wtf/modules/textfile" "github.com/wtfutil/wtf/modules/todo" "github.com/wtfutil/wtf/modules/todoist" + "github.com/wtfutil/wtf/modules/transmission" "github.com/wtfutil/wtf/modules/travisci" "github.com/wtfutil/wtf/modules/trello" "github.com/wtfutil/wtf/modules/twitter" @@ -181,6 +182,9 @@ func MakeWidget( case "todoist": settings := todoist.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig) widget = todoist.NewWidget(app, pages, settings) + case "transmission": + settings := transmission.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig) + widget = transmission.NewWidget(app, pages, settings) case "travisci": settings := travisci.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig) widget = travisci.NewWidget(app, pages, settings) diff --git a/modules/hibp/settings.go b/modules/hibp/settings.go index a387443e..b9b7c269 100644 --- a/modules/hibp/settings.go +++ b/modules/hibp/settings.go @@ -29,7 +29,6 @@ type Settings struct { // 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, ymlConfig, globalConfig), diff --git a/modules/hibp/widget.go b/modules/hibp/widget.go index c623ace0..c33a390c 100644 --- a/modules/hibp/widget.go +++ b/modules/hibp/widget.go @@ -17,18 +17,18 @@ type Widget struct { // NewWidget creates a new instance of a widget func NewWidget(app *tview.Application, settings *Settings) *Widget { - widget := Widget{ + widget := &Widget{ TextWidget: wtf.NewTextWidget(app, settings.common, false), settings: settings, } - return &widget + return widget } /* -------------------- Exported Functions -------------------- */ -// Fetch rettrieves HIBP data from the HIBP API +// Fetch retrieves HIBP data from the HIBP API func (widget *Widget) Fetch(accounts []string) ([]*Status, error) { data := []*Status{} diff --git a/modules/todoist/display.go b/modules/todoist/display.go index e88fcaab..1c24df64 100644 --- a/modules/todoist/display.go +++ b/modules/todoist/display.go @@ -17,15 +17,15 @@ func (widget *Widget) display() { title := fmt.Sprintf("[green]%s[white]", proj.Project.Name) str := "" - for index, item := range proj.tasks { + for idx, item := range proj.tasks { row := fmt.Sprintf( `[%s]| | %s[%s]`, - widget.RowColor(index), + widget.RowColor(idx), tview.Escape(item.Content), - widget.RowColor(index), + widget.RowColor(idx), ) - str += wtf.HighlightableHelper(widget.View, row, index, len(item.Content)) + str += wtf.HighlightableHelper(widget.View, row, idx, len(item.Content)) } widget.ScrollableWidget.Redraw(title, str, false) diff --git a/modules/transmission/display.go b/modules/transmission/display.go new file mode 100644 index 00000000..32fb263a --- /dev/null +++ b/modules/transmission/display.go @@ -0,0 +1,77 @@ +package transmission + +import ( + "fmt" + "strings" + + "github.com/hekmon/transmissionrpc" + "github.com/rivo/tview" + "github.com/wtfutil/wtf/wtf" +) + +func (widget *Widget) contentFrom(data []*transmissionrpc.Torrent) string { + str := "" + + for idx, torrent := range data { + torrName := *torrent.Name + + row := fmt.Sprintf( + "[%s] %s %s%s[white]", + widget.RowColor(idx), + widget.torrentPercentDone(torrent), + widget.torrentState(torrent), + tview.Escape(widget.prettyTorrentName(torrName)), + ) + + str += wtf.HighlightableHelper(widget.View, row, idx, len(torrName)) + } + + return str +} + +func (widget *Widget) display() { + if len(widget.torrents) == 0 { + widget.ScrollableWidget.Redraw(widget.CommonSettings.Title, "no torrents", false) + return + } + + content := widget.contentFrom(widget.torrents) + widget.ScrollableWidget.Redraw(widget.CommonSettings.Title, content, false) +} + +func (widget *Widget) prettyTorrentName(name string) string { + str := strings.Replace(name, "[", "(", -1) + str = strings.Replace(str, "]", ")", -1) + + return str +} + +func (widget *Widget) torrentPercentDone(torrent *transmissionrpc.Torrent) string { + pctDone := *torrent.PercentDone + str := fmt.Sprintf("%3d", int(pctDone*100)) + + if pctDone == 0.0 { + str = "[gray::b]" + str + } else if pctDone == 1.0 { + str = "[green::b]" + str + } else { + str = "[lightblue::b]" + str + } + + return str + "[white]" +} + +func (widget *Widget) torrentState(torrent *transmissionrpc.Torrent) string { + str := "" + + switch *torrent.Status { + case transmissionrpc.TorrentStatusStopped: + str += "[gray]" + case transmissionrpc.TorrentStatusDownload: + str += "[lightblue]" + case transmissionrpc.TorrentStatusSeed: + str += "[green]" + } + + return str +} diff --git a/modules/transmission/keyboard.go b/modules/transmission/keyboard.go new file mode 100644 index 00000000..a52ccd04 --- /dev/null +++ b/modules/transmission/keyboard.go @@ -0,0 +1,16 @@ +package transmission + +import "github.com/gdamore/tcell" + +func (widget *Widget) initializeKeyboardControls() { + widget.SetKeyboardChar("/", widget.ShowHelp, "Show/hide this help prompt") + widget.SetKeyboardChar("j", widget.Prev, "Select previous item") + widget.SetKeyboardChar("k", widget.Next, "Select next item") + widget.SetKeyboardChar("u", widget.Unselect, "Clear selection") + + widget.SetKeyboardKey(tcell.KeyCtrlD, widget.deleteSelectedTorrent, "Delete selected torrent") + widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item") + widget.SetKeyboardKey(tcell.KeyEnter, widget.pauseUnpauseTorrent, "Pause/unpause torrent") + widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection") + widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item") +} diff --git a/modules/transmission/settings.go b/modules/transmission/settings.go new file mode 100644 index 00000000..3c8072a5 --- /dev/null +++ b/modules/transmission/settings.go @@ -0,0 +1,38 @@ +package transmission + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +// Settings defines the configuration properties for this module +type Settings struct { + common *cfg.Common + + host string + https bool + password string + port int + url string + username string +} + +const ( + defaultTitle = "Transmission" +) + +// 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, ymlConfig, globalConfig), + + host: ymlConfig.UString("host"), + https: ymlConfig.UBool("https", false), + password: ymlConfig.UString("password"), + port: ymlConfig.UInt("port", 9091), + url: ymlConfig.UString("url", "/transmission/"), + username: ymlConfig.UString("username", ""), + } + + return &settings +} diff --git a/modules/transmission/widget.go b/modules/transmission/widget.go new file mode 100644 index 00000000..d53297e1 --- /dev/null +++ b/modules/transmission/widget.go @@ -0,0 +1,150 @@ +package transmission + +import ( + "errors" + + "github.com/hekmon/transmissionrpc" + "github.com/rivo/tview" + "github.com/wtfutil/wtf/wtf" +) + +// Widget is the container for transmission data +type Widget struct { + wtf.KeyboardWidget + wtf.ScrollableWidget + + client *transmissionrpc.Client + settings *Settings + torrents []*transmissionrpc.Torrent +} + +// NewWidget creates a new instance of a widget +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := Widget{ + KeyboardWidget: wtf.NewKeyboardWidget(app, pages, settings.common), + ScrollableWidget: wtf.NewScrollableWidget(app, settings.common, true), + + settings: settings, + } + + widget.SetRenderFunction(widget.display) + widget.initializeKeyboardControls() + widget.View.SetInputCapture(widget.InputCapture) + + widget.KeyboardWidget.SetView(widget.View) + + // Create a persisten transmission client for use in the calls below + client, err := transmissionrpc.New(widget.settings.host, widget.settings.username, widget.settings.password, nil) + if err != nil { + client = nil + } + widget.client = client + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +// Fetch retrieves torrent data from the Transmission daemon +func (widget *Widget) Fetch() ([]*transmissionrpc.Torrent, error) { + if widget.client == nil { + return nil, errors.New("client could not be initialized") + } + + torrents, err := widget.client.TorrentGetAll() + if err != nil { + return nil, err + } + + return torrents, nil +} + +// Refresh updates the data for this widget and displays it onscreen +func (widget *Widget) Refresh() { + torrents, err := widget.Fetch() + if err != nil { + widget.SetItemCount(0) + widget.ScrollableWidget.Redraw(widget.CommonSettings.Title, err.Error(), false) + return + } + + widget.torrents = torrents + widget.SetItemCount(len(torrents)) + + widget.display() +} + +// HelpText returns the help text for this widget +func (widget *Widget) HelpText() string { + return widget.KeyboardWidget.HelpText() +} + +// Next selects the next item in the list +func (widget *Widget) Next() { + widget.ScrollableWidget.Next() +} + +// Prev selects the previous item in the list +func (widget *Widget) Prev() { + widget.ScrollableWidget.Prev() +} + +// Unselect clears the selection of list items +func (widget *Widget) Unselect() { + widget.ScrollableWidget.Unselect() + widget.RenderFunction() +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) currentTorrent() *transmissionrpc.Torrent { + if len(widget.torrents) == 0 { + return nil + } + + return widget.torrents[widget.Selected] +} + +// deleteSelected removes the selected torrent from transmission +// This action is non-destructive, it does not delete the files on the host +func (widget *Widget) deleteSelectedTorrent() { + if widget.client == nil { + return + } + + currTorrent := widget.currentTorrent() + if currTorrent == nil { + return + } + + ids := []int64{*currTorrent.ID} + + removePayload := &transmissionrpc.TorrentRemovePayload{ + IDs: ids, + DeleteLocalData: false, + } + + widget.client.TorrentRemove(removePayload) +} + +// pauseUnpauseTorrent either pauses or unpauses the downloading and seeding of the selected torrent +func (widget *Widget) pauseUnpauseTorrent() { + if widget.client == nil { + return + } + + currTorrent := widget.currentTorrent() + if currTorrent == nil { + return + } + + ids := []int64{*currTorrent.ID} + + if *currTorrent.Status == transmissionrpc.TorrentStatusStopped { + widget.client.TorrentStartIDs(ids) + } else { + widget.client.TorrentStopIDs(ids) + } + + widget.display() +} diff --git a/wtf/scrollable.go b/wtf/scrollable.go index 496151ed..78385e2a 100644 --- a/wtf/scrollable.go +++ b/wtf/scrollable.go @@ -24,6 +24,7 @@ func NewScrollableWidget(app *tview.Application, commonSettings *cfg.Common, foc widget.Unselect() widget.View.SetScrollable(true) widget.View.SetRegions(true) + return widget } @@ -71,7 +72,6 @@ func (widget *ScrollableWidget) Unselect() { } func (widget *ScrollableWidget) Redraw(title, content string, wrap bool) { - widget.TextWidget.Redraw(title, content, wrap) widget.app.QueueUpdateDraw(func() { widget.View.Highlight(strconv.Itoa(widget.Selected)).ScrollToHighlight()