From d12f9f6de661a909203d2e51abe6177985d25f2e Mon Sep 17 00:00:00 2001 From: cluttrdev Date: Sun, 16 Jun 2024 14:38:55 +0200 Subject: [PATCH] feat: Support copy/paste to/from system clipboard for translate text areas --- internal/ui/translate.go | 73 +++++++++++++++++++++++++++------------- internal/ui/ui.go | 39 +++++++++++++-------- 2 files changed, 74 insertions(+), 38 deletions(-) diff --git a/internal/ui/translate.go b/internal/ui/translate.go index 8d6ae81..ab4ae3e 100644 --- a/internal/ui/translate.go +++ b/internal/ui/translate.go @@ -1,6 +1,7 @@ package ui import ( + "github.com/atotto/clipboard" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -22,7 +23,7 @@ type TranslatePage struct { glossarySelected func(string) inputTextArea *tview.TextArea - outputTextView *tview.TextView + outputTextArea *tview.TextArea } func newTranslatePage(ui *UI) *TranslatePage { @@ -35,11 +36,18 @@ func newTranslatePage(ui *UI) *TranslatePage { page.inputTextArea = tview.NewTextArea(). SetPlaceholder("Type to translate.") - - page.outputTextView = tview.NewTextView(). - SetChangedFunc(func() { - ui.Draw() - }) + page.inputTextArea.SetClipboard(copyToClipboard, pasteFromClipboard) + + page.outputTextArea = tview.NewTextArea() + page.outputTextArea.SetClipboard(copyToClipboard, pasteFromClipboard) + page.outputTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyCtrlQ { // copy to clipboard + return event + } else if event.Modifiers()&(tcell.ModAlt|tcell.ModMeta) > 0 { + return event + } + return nil + }) page.targetLangDropDown = tview.NewDropDown() page.formalityDropDown = tview.NewDropDown() @@ -61,7 +69,7 @@ func newTranslatePage(ui *UI) *TranslatePage { AddItem(page.sourceLangDropDown, 0, 0, 1, 1, 0, 0, false). AddItem(container, 0, 1, 1, 1, 0, 0, false). AddItem(page.inputTextArea, 1, 0, 1, 1, 0, 0, true). - AddItem(page.outputTextView, 1, 1, 1, 1, 0, 0, false) + AddItem(page.outputTextArea, 1, 1, 1, 1, 0, 0, false) page.layout.SetBorderPadding(0, 0, 0, 0) page.glossaryDialog = newGlossariesDialog(). @@ -105,23 +113,26 @@ func (w *TranslatePage) setGlossariesDialogVisibility(visible bool) { func (w *TranslatePage) registerKeyBindings(ui *UI) { w.layout.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Modifiers() == tcell.ModAlt { - switch event.Rune() { - case 's': - ui.SetFocus(w.sourceLangDropDown) - return nil - case 't': - ui.SetFocus(w.targetLangDropDown) - return nil - case 'f': - ui.SetFocus(w.formalityDropDown) - return nil - case 'g': - ui.SetFocus(w.glossaryButton) - return nil - case 'i': - ui.SetFocus(w.inputTextArea) - return nil + switch key := event.Key(); key { + case tcell.KeyRune: + if (event.Modifiers() & tcell.ModAlt) > 0 { + switch event.Rune() { + case 's': + ui.SetFocus(w.sourceLangDropDown) + return nil + case 't': + ui.SetFocus(w.targetLangDropDown) + return nil + case 'f': + ui.SetFocus(w.formalityDropDown) + return nil + case 'g': + ui.SetFocus(w.glossaryButton) + return nil + case 'i': + ui.SetFocus(w.inputTextArea) + return nil + } } } return event @@ -155,3 +166,17 @@ func (w *TranslatePage) adjustToSize() { w.glossaryDialog.SetRect(gbx+gbw-gww, gby, gww, gwh) } + +func copyToClipboard(text string) { + if err := clipboard.WriteAll(text); err != nil { + // what? + } +} + +func pasteFromClipboard() string { + text, err := clipboard.ReadAll() + if err != nil { + // what? + } + return text +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index dbe5404..0a62b12 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "bytes" "io" "strings" @@ -103,28 +104,38 @@ func NewUI() *UI { w, h := screen.Size() return ui.adjustToScreenSize(w, h) }) + ui.SetAfterDrawFunc(func(screen tcell.Screen) { + if ui.translatePage.inputTextArea.HasFocus() { + if ui.translatePage.inputTextArea.HasSelection() { + screen.HideCursor() + } + } else if ui.translatePage.outputTextArea.HasFocus() { + // The output text area is treated as read-only, so it does not make + // sense to show the cursor in it. This also makes selecting test in + // it more straightforward. + screen.HideCursor() + } + }) return ui } func (ui *UI) registerKeybindings() { ui.Application.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { + switch key := event.Key(); key { case tcell.KeyCtrlC: - // don't quit here - return nil + // send key that is usually used for copying to clipboard + return tcell.NewEventKey(tcell.KeyCtrlQ, 'q', tcell.ModCtrl) case tcell.KeyCtrlQ: + // stop app, usually done on Ctrl-C ui.Application.Stop() - } - - if event.Modifiers() == tcell.ModAlt { - if event.Key() == tcell.KeyTab { + case tcell.KeyTAB: + if (event.Modifiers() & tcell.ModAlt) > 0 { ui.cycePage() return nil } - - switch event.Rune() { - case ':': + case tcell.KeyRune: // regular character + if event.Rune() == ':' && (event.Modifiers()&tcell.ModAlt) > 0 { ui.switchToCommandPrompt() return nil } @@ -254,17 +265,17 @@ func (ui *UI) GetInputText() string { } func (ui *UI) WriteOutputText(r io.Reader) error { - w := ui.translatePage.outputTextView.BatchWriter() - defer w.Close() - _, err := io.Copy(w, r) + var w bytes.Buffer + _, err := io.Copy(&w, r) if err != nil { return err } + ui.translatePage.outputTextArea.SetText(w.String(), true) return nil } func (ui *UI) ClearOutputText() { - ui.translatePage.outputTextView.Clear() + ui.translatePage.outputTextArea.SetText("", false) } // Returns a new primitive which puts the provided one at the given position