diff --git a/app/widget_maker.go b/app/widget_maker.go index c12d4dc88..3d4705a2f 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -70,6 +70,7 @@ import ( "github.com/wtfutil/wtf/modules/stocks/yfinance" "github.com/wtfutil/wtf/modules/subreddit" "github.com/wtfutil/wtf/modules/textfile" + "github.com/wtfutil/wtf/modules/tmuxinator" "github.com/wtfutil/wtf/modules/todo" "github.com/wtfutil/wtf/modules/todo_plus" "github.com/wtfutil/wtf/modules/transmission" @@ -314,6 +315,9 @@ func MakeWidget( case "textfile": settings := textfile.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = textfile.NewWidget(tviewApp, redrawChan, pages, settings) + case "tmuxinator": + settings := tmuxinator.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = tmuxinator.NewWidget(tviewApp, redrawChan, pages, settings) case "todo": settings := todo.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = todo.NewWidget(tviewApp, redrawChan, pages, settings) diff --git a/modules/tmuxinator/client/client.go b/modules/tmuxinator/client/client.go new file mode 100644 index 000000000..486967eb5 --- /dev/null +++ b/modules/tmuxinator/client/client.go @@ -0,0 +1,60 @@ +package client + +import ( + "fmt" + "github.com/wtfutil/wtf/utils" + "os/exec" + "strings" +) + +func ProjectList() []string { + cmdString := `tmuxinator list | grep -v "tmuxinator projects:" | tr -s ' ' | tr '\n' ' '` + + cmd := exec.Command("sh", "-c", cmdString) + + result := strings.Split(utils.ExecuteCommand(cmd), " ") + + var projects []string + + for _, str := range result { + if str != "" { + projects = append(projects, str) + } + } + + return projects +} + +func StartProject(projectName string) { + _, err := exec.Command("tmuxinator", "start", projectName).Output() + + if err != nil { + fmt.Println(err.Error()) + } +} + +func EditProject(projectName string) { + subcommand := fmt.Sprintf("tmuxinator edit %s", projectName) + _, err := exec.Command("tmux", "new-window", subcommand).Output() + + if err != nil { + fmt.Println(err.Error()) + } +} + +func DeleteProject(projectName string) { + _, err := exec.Command("tmuxinator", "delete", projectName).Output() + + if err != nil { + fmt.Println(err.Error()) + } +} + +func CopyProject(leftProj, rightProj string) { + subcommand := fmt.Sprintf("tmuxinator copy %s %s", leftProj, rightProj) + _, err := exec.Command("tmux", "new-window", subcommand).Output() + + if err != nil { + fmt.Println(err.Error()) + } +} diff --git a/modules/tmuxinator/form.go b/modules/tmuxinator/form.go new file mode 100644 index 000000000..407179adf --- /dev/null +++ b/modules/tmuxinator/form.go @@ -0,0 +1,91 @@ +package tmuxinator + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "github.com/wtfutil/wtf/wtf" +) + +const ( + modalHeight = 7 + modalWidth = 80 + offscreen = -1000 +) + +func (widget *Widget) processFormInput(prompt string, initValue string, onSave func(string)) { + form := widget.modalForm(prompt, initValue) + + saveFctn := func() { + onSave(form.GetFormItem(0).(*tview.InputField).GetText()) + + widget.pages.RemovePage("modal") + widget.tviewApp.SetFocus(widget.View) + widget.display() + } + + widget.addButtons(form, saveFctn) + widget.modalFocus(form) + + // Tell the app to force redraw the screen + widget.Base.RedrawChan <- true +} + +/* -------------------- Modal Form -------------------- */ + +func (widget *Widget) addButtons(form *tview.Form, saveFctn func()) { + widget.addSaveButton(form, saveFctn) + widget.addCancelButton(form) +} + +func (widget *Widget) addCancelButton(form *tview.Form) { + cancelFn := func() { + widget.pages.RemovePage("modal") + widget.tviewApp.SetFocus(widget.View) + widget.display() + } + + form.AddButton("Cancel", cancelFn) + form.SetCancelFunc(cancelFn) +} + +func (*Widget) addSaveButton(form *tview.Form, fctn func()) { + form.AddButton("Save", fctn) +} + +func (widget *Widget) modalFocus(form *tview.Form) { + frame := widget.modalFrame(form) + widget.pages.AddPage("modal", frame, false, true) + widget.tviewApp.SetFocus(frame) + + // Tell the app to force redraw the screen + widget.Base.RedrawChan <- true +} + +func (widget *Widget) modalForm(lbl, text string) *tview.Form { + form := tview.NewForm() + form.SetFieldBackgroundColor(wtf.ColorFor(widget.settings.common.Colors.Background)) + form.SetButtonsAlign(tview.AlignCenter) + form.SetButtonTextColor(wtf.ColorFor(widget.settings.common.Colors.Text)) + + form.AddInputField(lbl, text, 60, nil, nil) + + return form +} + +func (*Widget) modalFrame(form *tview.Form) *tview.Frame { + frame := tview.NewFrame(form) + frame.SetBorders(0, 0, 0, 0, 0, 0) + frame.SetRect(offscreen, offscreen, modalWidth, modalHeight) + frame.SetBorder(true) + frame.SetBorders(1, 1, 0, 0, 1, 1) + + drawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) { + w, h := screen.Size() + frame.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height) + return x, y, width, height + } + + frame.SetDrawFunc(drawFunc) + + return frame +} diff --git a/modules/tmuxinator/keyboard.go b/modules/tmuxinator/keyboard.go new file mode 100644 index 000000000..b55b31608 --- /dev/null +++ b/modules/tmuxinator/keyboard.go @@ -0,0 +1,86 @@ +package tmuxinator + +import ( + "github.com/gdamore/tcell/v2" + tc "github.com/wtfutil/wtf/modules/tmuxinator/client" + "strings" +) + +func (widget *Widget) initializeKeyboardControls() { + widget.InitializeHelpTextKeyboardControl(widget.ShowHelp) + widget.InitializeRefreshKeyboardControl(widget.Refresh) + + widget.SetKeyboardChar("j", widget.Next, "Select next project") + widget.SetKeyboardChar("k", widget.Prev, "Select previous project") + widget.SetKeyboardChar("e", widget.editProject, "Edit project") + widget.SetKeyboardChar("n", widget.newProject, "Create new project") + widget.SetKeyboardChar("c", widget.cloneProject, "Clone existing project") + + widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next project") + widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous project") + widget.SetKeyboardKey(tcell.KeyEnter, widget.startProject, "Start or go to project") + widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection") +} + +func (widget *Widget) Next() { + widget.Selected++ + + if widget.Selected >= widget.MaxItems() { + widget.Selected = 0 + } + + widget.display() +} + +func (widget *Widget) Prev() { + widget.Selected-- + + if widget.Selected < 0 { + widget.Selected = widget.MaxItems() - 1 + } + + widget.display() +} + +func (widget *Widget) Unselect() { + widget.Selected = -1 + widget.display() +} + +func (widget *Widget) startProject() { + if widget.GetSelected() >= 0 && len(widget.Items) > 0 { + projectName := widget.Items[widget.GetSelected()] + + tc.StartProject(projectName) + } +} + +func (widget *Widget) editProject() { + if widget.GetSelected() >= 0 && len(widget.Items) > 0 { + projectName := widget.Items[widget.GetSelected()] + + tc.EditProject(projectName) + } +} + +func (widget *Widget) newProject() { + widget.processFormInput("New Project:", "", func(t string) { + projectName := strings.ReplaceAll(t, " ", "_") + + tc.EditProject(projectName) + widget.Base.RedrawChan <- true + }) +} + +func (widget *Widget) cloneProject() { + if widget.GetSelected() >= 0 && len(widget.Items) > 0 { + currentProjectName := widget.Items[widget.GetSelected()] + + widget.processFormInput("Copy Project:", currentProjectName, func(t string) { + newProjectName := strings.ReplaceAll(t, " ", "_") + + tc.CopyProject(currentProjectName, newProjectName) + widget.Base.RedrawChan <- true + }) + } +} diff --git a/modules/tmuxinator/settings.go b/modules/tmuxinator/settings.go new file mode 100644 index 000000000..ab9023504 --- /dev/null +++ b/modules/tmuxinator/settings.go @@ -0,0 +1,29 @@ +package tmuxinator + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "Tmuxinator Projects" +) + +type Settings struct { + common *cfg.Common +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + common := cfg.NewCommonSettingsFromModule( + name, + defaultTitle, + defaultFocusable, + ymlConfig, + globalConfig, + ) + + settings := Settings{common: common} + + return &settings +} diff --git a/modules/tmuxinator/widget.go b/modules/tmuxinator/widget.go new file mode 100644 index 000000000..4d8943db1 --- /dev/null +++ b/modules/tmuxinator/widget.go @@ -0,0 +1,107 @@ +package tmuxinator + +import ( + "fmt" + "github.com/rivo/tview" + tc "github.com/wtfutil/wtf/modules/tmuxinator/client" + "github.com/wtfutil/wtf/view" +) + +type Widget struct { + pages *tview.Pages + + settings *Settings + Selected int + Items []string + + tviewApp *tview.Application + view.ScrollableWidget +} + +func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget { + widget := Widget{ + ScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.common), + + tviewApp: tviewApp, + settings: settings, + pages: pages, + } + + widget.initializeKeyboardControls() + + widget.Items = tc.ProjectList() + + widget.Unselect() + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) GetSelected() int { + if widget.Selected < 0 { + return 0 + } + + return widget.Selected +} + +func (widget *Widget) MaxItems() int { + return len(widget.Items) +} + +func (widget *Widget) Refresh() { + widget.Items = tc.ProjectList() + widget.Unselect() + widget.display() +} + +func (widget *Widget) RowColor(idx int) string { + if widget.View.HasFocus() && (idx == widget.GetSelected()) { + foreground := widget.CommonSettings().Colors.RowTheme.HighlightedForeground + + return fmt.Sprintf( + "%s:%s", + foreground, + widget.CommonSettings().Colors.RowTheme.HighlightedBackground, + ) + } + + if idx%2 == 0 { + return fmt.Sprintf( + "%s:%s", + widget.settings.common.Colors.RowTheme.EvenForeground, + widget.settings.common.Colors.RowTheme.EvenBackground, + ) + } + + return fmt.Sprintf( + "%s:%s", + widget.settings.common.Colors.RowTheme.OddForeground, + widget.settings.common.Colors.RowTheme.OddBackground, + ) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) display() { + widget.Redraw(func() (string, string, bool) { + return widget.CommonSettings().Title, widget.content(), false + }) +} + +func (widget *Widget) content() string { + cnt := "" + + if len(widget.Items) <= 0 { + cnt += " [grey]No projects found[white]\n" + } + + for idx, projectName := range widget.Items { + cnt += fmt.Sprintf("[%s] %s \n", + widget.RowColor(idx), + projectName) + } + + return cnt +}