diff --git a/README.md b/README.md index ad0ba77..f6d1e7a 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 853cbb8..d574782 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 ) require ( @@ -47,6 +48,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/sys v0.28.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/go.sum b/go.sum index 8853999..8f3043e 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,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 3a0919d..7a0f54f 100644 --- a/main.go +++ b/main.go @@ -398,6 +398,9 @@ func main() { istty := isatty.IsTerminal(os.Stdout.Fd()) + if config.Output == "clipboard" { // convert to png because we can't copy svg to clipboard + config.Output = "clipboard.png" + } switch { case strings.HasSuffix(config.Output, ".png"): // use libsvg conversion. @@ -417,6 +420,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..6461120 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.HasPrefix(output, "clipboard") { + png, err := os.ReadFile(output) + defer os.Remove(output) // nolint: errcheck + 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..40807bd --- /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 } + + + diff --git a/test/golden/svg/bubbletea.svg b/test/golden/svg/bubbletea.svg index 304e551..40807bd 100644 --- a/test/golden/svg/bubbletea.svg +++ b/test/golden/svg/bubbletea.svg @@ -42,8 +42,8 @@ 30 } 31 32 func (m model) View() string { - 33     return // ... - 34 } + 33     return // ... + 34 } diff --git a/test/golden/svg/overflow-line-numbers.svg b/test/golden/svg/overflow-line-numbers.svg index b5461c9..1a263f7 100644 --- a/test/golden/svg/overflow-line-numbers.svg +++ b/test/golden/svg/overflow-line-numbers.svg @@ -77,7 +77,7 @@ 65       {{- if eq .Arch "amd64" }}x86_64 66       {{- else if eq .Arch "386" }}i386 67       {{- else }}{{ .Arch }}{{ end }} - 68       {{- with .Arm}}v{{ . }}{{ end }}       + 68       {{- with .Arm}}v{{ . }}{{ end }} 69     wrap_in_directory: true 70     files: 71       - README* @@ -93,7 +93,7 @@ 81     file_name_template: >- 82       {{- trimsuffix .ConventionalFileName .ConventionalExtension -}} 83       {{- if and (eq .Arm "6") (eq .ConventionalExtension ".deb") }}6{{ end -}} - 84       {{- .ConventionalExtension -}}       + 84       {{- .ConventionalExtension -}} 85     license: MIT 86     formats: 87       - apk diff --git a/test/golden/svg/tab.svg b/test/golden/svg/tab.svg index c44874f..f67a879 100644 --- a/test/golden/svg/tab.svg +++ b/test/golden/svg/tab.svg @@ -12,10 +12,10 @@ package main -// freeze/issues/50 - -type Config struct { //nolint: revive -    Telegram struct { +// freeze/issues/50 + +type Config struct { //nolint: revive +    Telegram struct {         Token   string `env:"TG_TOKEN"`         ChatID  string `env:"TG_CHAT"`         OwnerID string `env:"TG_ADMIN"` @@ -32,8 +32,8 @@     Debug bool } -func Load() (*Config, error) { //nolint: revive -    var c Config +func Load() (*Config, error) { //nolint: revive +    var c Config     var err error     return &c, err } diff --git a/test/golden/svg/wrap.svg b/test/golden/svg/wrap.svg index cca22cd..badc169 100644 --- a/test/golden/svg/wrap.svg +++ b/test/golden/svg/wrap.svg @@ -14,8 +14,8 @@ import "fmt" -// freeze/issues/14 - +// freeze/issues/14 + func main() {     fmt.Println("This is a really long line that is going to go over the 80 character limit. This is a really long line that is going to go over the 80