diff --git a/README.md b/README.md index 533a1de..24d1ee9 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,14 @@ freeze main.go --output out.webp freeze main.go --output out.{svg,png,webp} ``` +### Copy + +Copy the output image to your clipboard, so you can paste it anywhere. + +```bash +freeze main.go --output clipboard +``` + ### Font Specify the font family, font size, and font line height of the output image. diff --git a/configurations/full.json b/configurations/full.json index 40d5131..36f5fde 100644 --- a/configurations/full.json +++ b/configurations/full.json @@ -29,5 +29,6 @@ "size": 14, "ligatures": true }, - "line_height": 1.2 -} \ No newline at end of file + "line_height": 1.2, + "copy": false +} diff --git a/freeze_test.go b/freeze_test.go index 0443255..716a5b5 100644 --- a/freeze_test.go +++ b/freeze_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/aymanbagabas/go-udiff" + "golang.design/x/clipboard" ) const binary = "./test/freeze-test" @@ -58,6 +59,25 @@ func TestFreezeOutput(t *testing.T) { } } +func TestFreezeCopy(t *testing.T) { + output := "clipboard" + defer os.Remove(output) + + cmd := exec.Command(binary, "test/input/bubbletea.model", "-o", output, "--language", "go", "--height", "800", "--width", "750", "--config", "full", "--window=false", "--show-line-numbers") + err := cmd.Run() + if err != nil { + t.Fatal(err) + } + err = clipboard.Init() + if err != nil { + t.Fatal(err) + } + png := clipboard.Read(clipboard.FmtImage) + if png == nil { + t.Fatal("clipboard is empty") + } +} + func TestFreezeHelp(t *testing.T) { out := bytes.Buffer{} cmd := exec.Command(binary) @@ -136,6 +156,11 @@ func TestFreezeConfigurations(t *testing.T) { flags: []string{"--language", "go", "--height", "800", "--width", "750", "--config", "full", "--window=false", "--show-line-numbers"}, output: "bubbletea", }, + { + input: "test/input/bubbletea.model", + flags: []string{"--language", "go", "--height", "800", "--width", "750", "--config", "full", "--window=false", "--show-line-numbers"}, + output: "bubbletea-copy", + }, // { // flags: []string{"--execute", "layout", "--height", "800", "--config", "full", "--margin", "50,10"}, // output: "composite-2", diff --git a/go.mod b/go.mod index 91f4d88..14b7f32 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-runewidth v0.0.16 github.com/muesli/reflow v0.3.0 + golang.design/x/clipboard v0.7.0 golang.org/x/sys v0.25.0 ) @@ -46,6 +47,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect + golang.org/x/exp/shiny v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/image v0.14.0 // indirect + golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 63e5ccc..8d85d96 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,16 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo= +golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp/shiny v0.0.0-20240506185415-9bf2ced13842 h1:kEvPiBVeT1JJGw/3THfe1W1zvTAvU1V6pCFV0icZvQs= +golang.org/x/exp/shiny v0.0.0-20240506185415-9bf2ced13842/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 5a710d7..c5cd5f3 100644 --- a/main.go +++ b/main.go @@ -395,10 +395,14 @@ func main() { istty := isatty.IsTerminal(os.Stdout.Fd()) + outputName := config.Output + if config.Output == "clipboard" { + outputName = "clipboard.png" + } switch { - case strings.HasSuffix(config.Output, ".png"): + case strings.HasSuffix(outputName, ".png"): // use libsvg conversion. - svgConversionErr := libsvgConvert(doc, imageWidth, imageHeight, config.Output) + svgConversionErr := libsvgConvert(doc, imageWidth, imageHeight, outputName) if svgConversionErr == nil { printFilenameOutput(config.Output) break @@ -414,6 +418,10 @@ func main() { default: // output file specified. if config.Output != "" { + _, err := doc.WriteToBytes() + if err != nil { + printErrorFatal("Unable to write output", err) + } err = doc.WriteToFile(config.Output) if err != nil { printErrorFatal("Unable to write output", err) diff --git a/png.go b/png.go index b7d0184..c14c039 100644 --- a/png.go +++ b/png.go @@ -5,12 +5,24 @@ import ( "context" "os" "os/exec" + "strings" "github.com/beevik/etree" "github.com/charmbracelet/freeze/font" "github.com/kanrichan/resvg-go" + "golang.design/x/clipboard" ) +func copyToClipboard(img []byte) error { + err := clipboard.Init() + if err != nil { + return err + } + clipboard.Write(clipboard.FmtImage, img) + clipboard.Read(clipboard.FmtImage) + return err +} + func libsvgConvert(doc *etree.Document, _, _ float64, output string) error { _, err := exec.LookPath("rsvg-convert") if err != nil { @@ -27,6 +39,17 @@ func libsvgConvert(doc *etree.Document, _, _ float64, output string) error { rsvgConvert := exec.Command("rsvg-convert", "-o", output) rsvgConvert.Stdin = bytes.NewReader(svg) err = rsvgConvert.Run() + if err != nil { + return err + } + if strings.Contains(output, "clipboard") { + png, err := os.ReadFile(output) + defer os.Remove(output) + if err != nil { + return err + } + return copyToClipboard(png) + } return err //nolint: wrapcheck } @@ -90,9 +113,8 @@ func resvgConvert(doc *etree.Document, w, h float64, output string) error { return err //nolint: wrapcheck } - err = os.WriteFile(output, png, 0o600) - if err != nil { - return err //nolint: wrapcheck + if output == "clipboard" { + return copyToClipboard(png) } - return err //nolint: wrapcheck + return os.WriteFile(output, png, 0o600) } diff --git a/test/golden/svg/bubbletea-copy.svg b/test/golden/svg/bubbletea-copy.svg new file mode 100644 index 0000000..304e551 --- /dev/null +++ b/test/golden/svg/bubbletea-copy.svg @@ -0,0 +1,49 @@ + + + + + + 1 func (m model) Init() tea.Cmd { + 2     return nil + 3 } + 4 + 5 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + 6     switch msg := msg.(type) { + 7 + 8     case tea.KeyMsg: + 9         switch msg.String() { + 10         case "ctrl+c", "q": + 11             return m, tea.Quit + 12         case "up", "k": + 13             if m.cursor > 0 { + 14                 m.cursor-- + 15             } + 16         case "down", "j": + 17             if m.cursor < len(m.choices)-1 { + 18                 m.cursor++ + 19             } + 20         case "enter", " ": + 21             _, ok := m.selected[m.cursor] + 22             if ok { + 23                 delete(m.selected, m.cursor) + 24             } else { + 25                 m.selected[m.cursor] = struct{}{} + 26             } + 27         } + 28     } + 29     return m, nil + 30 } + 31 + 32 func (m model) View() string { + 33     return // ... + 34 } + + +