diff --git a/go.mod b/go.mod index aff7879..3b93d1d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ replace github.com/InVisionApp/go-health/v2 => github.com/f1bonacc1/go-health/v2 require ( github.com/InVisionApp/go-logger v1.0.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/f1bonacc1/glippy v0.0.0-20221207220753-a53cdbf9bae7 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -31,6 +32,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/goccy/go-json v0.10.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jezek/xgb v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect diff --git a/go.sum b/go.sum index 0218513..b821de3 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/f1bonacc1/glippy v0.0.0-20221207220753-a53cdbf9bae7 h1:4e3jb2TWZp24ME220mKFMLMPVb6/T6b5bdtXfQPnz68= +github.com/f1bonacc1/glippy v0.0.0-20221207220753-a53cdbf9bae7/go.mod h1:4FvlEkhBa/BJMEuMGVlocGYDJAvO7FwhJhHH9MY6vaM= github.com/f1bonacc1/go-health/v2 v2.1.3 h1:UbiB5hSNpnqAAs7tsCZ8aA/ScdIpuGbDo2r7lhfIGkQ= github.com/f1bonacc1/go-health/v2 v2.1.3/go.mod h1:Iz2FZRfK3sJecRvGCIgyBsKOjILdKTdLGiGFaO+JDYc= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= @@ -82,6 +84,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= +github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/src/tui/actions.go b/src/tui/actions.go index 785ae83..51c4884 100644 --- a/src/tui/actions.go +++ b/src/tui/actions.go @@ -16,6 +16,7 @@ const ( ActionLogScreen = ActionName("log_screen") ActionFollowLog = ActionName("log_follow") ActionWrapLog = ActionName("log_wrap") + ActionLogSelection = ActionName("log_select") ActionProcessStart = ActionName("process_start") ActionProcessInfo = ActionName("process_info") ActionProcessStop = ActionName("process_stop") @@ -28,6 +29,7 @@ var defaultShortcuts = map[ActionName]tcell.Key{ ActionLogScreen: tcell.KeyF4, ActionFollowLog: tcell.KeyF5, ActionWrapLog: tcell.KeyF6, + ActionLogSelection: tcell.KeyCtrlS, ActionProcessInfo: tcell.KeyF3, ActionProcessStart: tcell.KeyF7, ActionProcessStop: tcell.KeyF9, @@ -162,6 +164,12 @@ func getDefaultActions() ShortCuts { false: "Wrap Off", }, }, + ActionLogSelection: { + ToggleDescription: map[bool]string{ + true: "Select On", + false: "Select Off", + }, + }, ActionProcessInfo: { Description: "Info", }, diff --git a/src/tui/dialog.go b/src/tui/dialog.go index 720cc7e..912e5f4 100644 --- a/src/tui/dialog.go +++ b/src/tui/dialog.go @@ -1,64 +1,17 @@ package tui import ( - "github.com/f1bonacc1/process-compose/src/app" - "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "strings" ) -func (pv *pcView) showProcInfo(info *app.ProcessConfig) { - f := tview.NewForm() - f.SetCancelFunc(func() { - pv.pages.RemovePage(PageDialog) - }) - f.SetItemPadding(1) - f.SetBorder(true) - f.SetFieldBackgroundColor(tcell.ColorLightSkyBlue) - f.SetFieldTextColor(tcell.ColorBlack) - f.SetButtonsAlign(tview.AlignCenter) - f.SetTitle("Process " + info.Name + " Info") - addStringIfNotEmpty("Command:", info.Command, f) - addStringIfNotEmpty("Working Directory:", info.WorkingDir, f) - addStringIfNotEmpty("Log Location:", info.LogLocation, f) - addSliceIfNotEmpty("Environment:", info.Environment, f) - addSliceIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f) - f.AddCheckbox("Is Disabled:", info.Disabled, nil) - f.AddCheckbox("Is Daemon:", info.IsDaemon, nil) - f.AddButton("Close", func() { - pv.pages.RemovePage(PageDialog) - }) - f.SetFocus(f.GetFormItemCount()) - pv.pages.AddPage(PageDialog, createPage(f, 0, 0), true, true) +func (pv *pcView) showDialog(primitive tview.Primitive) { + pv.pages.AddPage(PageDialog, createDialogPage(primitive, 0, 0), true, true) + pv.appView.SetFocus(primitive) } -func createPage(p tview.Primitive, width, height int) tview.Primitive { +func createDialogPage(p tview.Primitive, width, height int) tview.Primitive { return tview.NewGrid(). SetColumns(0, width, 0). SetRows(0, height, 0). AddItem(p, 1, 1, 1, 1, 0, 0, true) } - -func addStringIfNotEmpty(label, value string, f *tview.Form) { - if len(strings.TrimSpace(value)) > 0 { - f.AddInputField(label, value, 0, nil, nil) - } -} - -func addSliceIfNotEmpty(label string, value []string, f *tview.Form) { - if len(value) > 0 { - f.AddDropDown(label, value, 0, nil) - } -} - -// mapKeysToSlice extract keys of map as slice, -func mapKeysToSlice[K comparable, V any](m map[K]V) []K { - keys := make([]K, len(m)) - - i := 0 - for k := range m { - keys[i] = k - i++ - } - return keys -} diff --git a/src/tui/log_viewer.go b/src/tui/log_viewer.go index 3d73723..455a258 100644 --- a/src/tui/log_viewer.go +++ b/src/tui/log_viewer.go @@ -22,9 +22,12 @@ func NewLogView(maxLines int) *LogView { l := &LogView{ isWrapOn: true, - TextView: *tview.NewTextView().SetDynamicColors(true).SetScrollable(true).SetMaxLines(maxLines), - buffer: &strings.Builder{}, - useAnsi: false, + TextView: *tview.NewTextView(). + SetDynamicColors(true). + SetScrollable(true). + SetMaxLines(maxLines), + buffer: &strings.Builder{}, + useAnsi: false, } l.ansiWriter = tview.ANSIWriter(l) l.SetBorder(true) diff --git a/src/tui/proc-info-form.go b/src/tui/proc-info-form.go new file mode 100644 index 0000000..9748c9c --- /dev/null +++ b/src/tui/proc-info-form.go @@ -0,0 +1,58 @@ +package tui + +import ( + "github.com/f1bonacc1/process-compose/src/app" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "strings" +) + +func (pv *pcView) createProcInfoForm(info *app.ProcessConfig) *tview.Form { + f := tview.NewForm() + f.SetCancelFunc(func() { + pv.pages.RemovePage(PageDialog) + }) + f.SetItemPadding(1) + f.SetBorder(true) + f.SetFieldBackgroundColor(tcell.ColorLightSkyBlue) + f.SetFieldTextColor(tcell.ColorBlack) + f.SetButtonsAlign(tview.AlignCenter) + f.SetTitle("Process " + info.Name + " Info") + addStringIfNotEmpty("Command:", info.Command, f) + addStringIfNotEmpty("Working Directory:", info.WorkingDir, f) + addStringIfNotEmpty("Log Location:", info.LogLocation, f) + addSliceIfNotEmpty("Environment:", info.Environment, f) + addSliceIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f) + f.AddCheckbox("Is Disabled:", info.Disabled, nil) + f.AddCheckbox("Is Daemon:", info.IsDaemon, nil) + f.AddButton("Close", func() { + pv.pages.RemovePage(PageDialog) + }) + + f.SetFocus(f.GetFormItemCount()) + return f +} + +func addStringIfNotEmpty(label, value string, f *tview.Form) { + if len(strings.TrimSpace(value)) > 0 { + f.AddInputField(label, value, 0, nil, nil) + } +} + +func addSliceIfNotEmpty(label string, value []string, f *tview.Form) { + if len(value) > 0 { + f.AddDropDown(label, value, 0, nil) + } +} + +// mapKeysToSlice extract keys of map as slice, +func mapKeysToSlice[K comparable, V any](m map[K]V) []K { + keys := make([]K, len(m)) + + i := 0 + for k := range m { + keys[i] = k + i++ + } + return keys +} diff --git a/src/tui/view.go b/src/tui/view.go index 5ee7113..47851ab 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -2,6 +2,7 @@ package tui import ( "fmt" + "github.com/f1bonacc1/glippy" "github.com/f1bonacc1/process-compose/src/config" "github.com/f1bonacc1/process-compose/src/updater" "os" @@ -43,10 +44,13 @@ type pcView struct { pages *tview.Pages procNames []string logFollow bool + logSelect bool fullScrState FullScrState loggedProc string shortcuts ShortCuts procCountCell *tview.TableCell + mainGrid *tview.Grid + logsTextArea *tview.TextArea } func newPcView(logLength int) *pcView { @@ -62,13 +66,29 @@ func newPcView(logLength int) *pcView { loggedProc: "", shortcuts: getDefaultActions(), procCountCell: tview.NewTableCell(""), + mainGrid: tview.NewGrid(), + logsTextArea: tview.NewTextArea(), + logSelect: false, } pv.loadShortcuts() pv.procTable = pv.createProcTable() pv.statTable = pv.createStatTable() pv.updateHelpTextView() + pv.createGrid() + pv.logsTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyCR: + text, start, _ := pv.logsTextArea.GetSelection() + glippy.Set(text) + pv.logsTextArea.Select(start, start) + case tcell.KeyEsc: + pv.toggleLogSelection() + pv.updateHelpTextView() + } + return nil + }) pv.pages = tview.NewPages(). - AddPage(PageMain, pv.createGrid(pv.fullScrState), true, true) + AddPage(PageMain, pv.mainGrid, true, true) pv.appView.SetRoot(pv.pages, true).EnableMouse(true).SetInputCapture(pv.onAppKey) if len(pv.procNames) > 0 { @@ -97,20 +117,25 @@ func (pv *pcView) onAppKey(event *tcell.EventKey) *tcell.EventKey { } else { pv.fullScrState = LogFull } - pv.appView.SetRoot(pv.createGrid(pv.fullScrState), true) + pv.redrawGrid() pv.updateHelpTextView() case pv.shortcuts.ShortCutKeys[ActionFollowLog].key: pv.toggleLogFollow() case pv.shortcuts.ShortCutKeys[ActionWrapLog].key: pv.logsText.ToggleWrap() pv.updateHelpTextView() + case pv.shortcuts.ShortCutKeys[ActionLogSelection].key: + pv.stopFollowLog() + pv.toggleLogSelection() + pv.appView.SetFocus(pv.logsTextArea) + pv.updateHelpTextView() case pv.shortcuts.ShortCutKeys[ActionProcessScreen].key: if pv.fullScrState == ProcFull { pv.fullScrState = LogProcHalf } else { pv.fullScrState = ProcFull } - pv.appView.SetRoot(pv.createGrid(pv.fullScrState), true) + pv.redrawGrid() pv.onProcRowSpanChange() pv.updateHelpTextView() case tcell.KeyCtrlC: @@ -136,7 +161,7 @@ func (pv *pcView) terminateAppView() { pv.pages.RemovePage(PageDialog) }) // Display and focus the dialog - pv.pages.AddPage(PageDialog, createPage(m, 50, 50), true, true) + pv.pages.AddPage(PageDialog, createDialogPage(m, 50, 50), true, true) } func (pv *pcView) showError(errMessage string) { @@ -148,7 +173,7 @@ func (pv *pcView) showError(errMessage string) { pv.pages.RemovePage(PageDialog) }) - pv.pages.AddPage(PageDialog, createPage(m, 50, 50), true, true) + pv.pages.AddPage(PageDialog, createDialogPage(m, 50, 50), true, true) } func (pv *pcView) showInfo() { @@ -158,7 +183,22 @@ func (pv *pcView) showInfo() { pv.showError(err.Error()) return } - pv.showProcInfo(info) + form := pv.createProcInfoForm(info) + pv.showDialog(form) +} + +func (pv *pcView) toggleLogSelection() { + name := pv.getSelectedProcName() + pv.logSelect = !pv.logSelect + if pv.logSelect { + pv.logsTextArea.SetText(pv.logsText.GetText(true), true). + SetBorder(true). + SetTitle(name + " [Select & Press Enter to Copy]") + } else { + pv.logsTextArea.SetText("", false) + } + + pv.redrawGrid() } func (pv *pcView) handleShutDown() { @@ -347,6 +387,7 @@ func (pv *pcView) updateHelpTextView() { pv.shortcuts.ShortCutKeys[ActionLogScreen].writeToggleButton(pv.helpText, logScrBool) pv.shortcuts.ShortCutKeys[ActionFollowLog].writeToggleButton(pv.helpText, !pv.logFollow) pv.shortcuts.ShortCutKeys[ActionWrapLog].writeToggleButton(pv.helpText, !pv.logsText.IsWrapOn()) + pv.shortcuts.ShortCutKeys[ActionLogSelection].writeToggleButton(pv.helpText, !pv.logSelect) fmt.Fprintf(pv.helpText, "%s ", "[lightskyblue::b]PROCESS:[-:-:-]") pv.shortcuts.ShortCutKeys[ActionProcessInfo].writeButton(pv.helpText) pv.shortcuts.ShortCutKeys[ActionProcessStart].writeButton(pv.helpText) @@ -356,27 +397,38 @@ func (pv *pcView) updateHelpTextView() { pv.shortcuts.ShortCutKeys[ActionQuit].writeButton(pv.helpText) } -func (pv *pcView) createGrid(fullScrState FullScrState) *tview.Grid { - - grid := tview.NewGrid(). +func (pv *pcView) createGrid() { + pv.mainGrid.Clear(). SetRows(3, 0, 0, 1). //SetColumns(30, 0, 30). SetBorders(true). AddItem(pv.statTable, 0, 0, 1, 1, 0, 0, false). AddItem(pv.helpText, 3, 0, 1, 1, 0, 0, false) - switch fullScrState { + var log tview.Primitive + if !pv.logSelect { + log = pv.logsText + } else { + log = pv.logsTextArea + } + switch pv.fullScrState { case LogFull: - grid.AddItem(pv.logsText, 1, 0, 2, 1, 0, 0, true) + pv.mainGrid.AddItem(log, 1, 0, 2, 1, 0, 0, true) case ProcFull: - grid.AddItem(pv.procTable, 1, 0, 2, 1, 0, 0, true) + pv.mainGrid.AddItem(pv.procTable, 1, 0, 2, 1, 0, 0, true) case LogProcHalf: - grid.AddItem(pv.procTable, 1, 0, 1, 1, 0, 0, true) - grid.AddItem(pv.logsText, 2, 0, 1, 1, 0, 0, false) + pv.mainGrid.AddItem(pv.procTable, 1, 0, 1, 1, 0, 0, true) + pv.mainGrid.AddItem(log, 2, 0, 1, 1, 0, 0, false) } - grid.SetTitle("Process Compose") - return grid + pv.mainGrid.SetTitle("Process Compose") + //pv.mainGrid = grid +} + +func (pv *pcView) redrawGrid() { + go pv.appView.QueueUpdateDraw(func() { + pv.createGrid() + }) } func (pv *pcView) updateTable() {