diff --git a/README.md b/README.md index ad0ba773..074de144 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,14 @@ freeze artichoke.hs -o artichoke.png ### Generate an image of terminal output -You can use `freeze` to capture ANSI output of a terminal command with the -`--execute` flag. +You can use `freeze` to capture a terminal command and its ANSI output with the +`--execute` flag. If you want to omit the command used, use the +`--execute.command=false` flag. ```bash freeze --execute "eza -lah" +# or +freeze --execute "eza -lah" --execute.command=false ```

diff --git a/config.go b/config.go index 2aebc457..5fcf401f 100644 --- a/config.go +++ b/config.go @@ -35,6 +35,7 @@ type Config struct { Output string `json:"output,omitempty" help:"Output location for {{.svg}}, {{.png}}, or {{.webp}}." short:"o" group:"Settings" default:"" placeholder:"freeze.svg"` Execute string `json:"-" help:"Capture output of command execution." short:"x" group:"Settings" default:""` + Command bool `json:"-" help:"Capture command executed and its output." group:"Settings" default:"true" prefix:"execute."` ExecuteTimeout time.Duration `json:"-" help:"Execution timeout." group:"Settings" default:"10s" prefix:"execute." name:"timeout" hidden:""` // Decoration diff --git a/freeze_test.go b/freeze_test.go index 53bd6c0d..8a6f9aba 100644 --- a/freeze_test.go +++ b/freeze_test.go @@ -329,3 +329,79 @@ func TestFreezeConfigurations(t *testing.T) { }) } } + +// Test README examples. +func TestREADME(t *testing.T) { + tests := []struct { + input string + flags []string + output string + }{ + { + flags: []string{"--execute", "echo 'Hello World'"}, + output: "execute-command", + }, + { + flags: []string{"--execute", "echo 'Hello World'", "--execute.command=false"}, + output: "execute-command-false", + }, + { + flags: []string{"--execute", "echo '\x1b[1;31mHello'"}, + output: "execute-command-ansi", + }, + } + + for _, tc := range tests { + t.Run(tc.output, func(t *testing.T) { + // output SVG + out := bytes.Buffer{} + args := []string{tc.input} + args = append(args, tc.flags...) + args = append(args, "--output", "test/readme/output/"+tc.output+".svg") + cmd := exec.Command(binary, args...) + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + t.Log(err) + t.Log(out.String()) + t.Fatal("unexpected error") + } + gotfile := "test/readme/output/" + tc.output + ".svg" + got, err := os.ReadFile(gotfile) + if err != nil { + t.Fatal("no output file for:", gotfile) + } + goldenfile := "test/readme/golden/" + tc.output + ".svg" + if *update { + if err := os.WriteFile(goldenfile, got, 0o644); err != nil { + t.Log(err) + t.Fatal("unexpected error") + } + } + want, err := os.ReadFile(goldenfile) + if err != nil { + t.Fatal("no golden file for:", goldenfile) + } + if string(want) != string(got) { + t.Log(udiff.Unified("want", "got", string(want), string(got))) + t.Fatalf("%s != %s", goldenfile, gotfile) + } + + // output PNG + if png != nil && *png { + out = bytes.Buffer{} + args = []string{tc.input} + args = append(args, tc.flags...) + args = append(args, "--output", "test/readme/output/"+tc.output+".png") + cmd = exec.Command(binary, args...) + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + t.Log(err) + t.Log(out.String()) + t.Fatal("unexpected error") + } + } + }) + } +} diff --git a/main.go b/main.go index 58e9a6f9..78f353bf 100644 --- a/main.go +++ b/main.go @@ -314,21 +314,57 @@ func main() { config.LineHeight *= float64(scale) + const ( + cursor string = "#5D53C7" + ) + + var longestLine int for i, line := range text { if isAnsi { line.SetText("") } + + x := float64(config.Padding[left] + config.Margin[left]) + y := (float64(i+1))*(config.Font.Size*config.LineHeight) + float64(config.Padding[top]) + float64(config.Margin[top]) + // Offset the text by padding... // (x, y) -> (x+p, y+p) - if config.ShowLineNumbers { + // Don't show line numbers for command execution. + if config.ShowLineNumbers && config.Execute == "" { ln := etree.NewElement("tspan") ln.CreateAttr("xml:space", "preserve") ln.CreateAttr("fill", s.Get(chroma.LineNumbers).Colour.String()) ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine)) line.InsertChildAt(0, ln) } - x := float64(config.Padding[left] + config.Margin[left]) - y := (float64(i+1))*(config.Font.Size*config.LineHeight) + float64(config.Padding[top]) + float64(config.Margin[top]) + + // Add a cursor to the first line, if running a command. + if i == 0 && config.Command { + newline := etree.NewElement("text") + // Prompt + ln := etree.NewElement("tspan") + ln.CreateAttr("xml:space", "preserve") + ln.CreateAttr("fill", cursor) + ln.SetText("> ") + newline.InsertChildAt(0, ln) + textGroup.InsertChildAt(0, newline) + svg.Move(newline, x, y) + x += float64(config.Font.Size) * 3 + // Command + // TODO eventually remove these hard-coded styles and use shell as + // the language for commands. + cmdText := etree.NewElement("tspan") + cmdText.CreateAttr("xml:space", "preserve") + cmdText.CreateAttr("fill", "#FAFAFA") + cmdText.SetText(config.Execute) + newline.InsertChildAt(1, cmdText) + // Reset position for next line. + y += float64(config.Font.Size * config.LineHeight) + x = float64(config.Padding[left] + config.Margin[left]) + // We are showing raw ANSI sequences in the commands, so we need to + // account for that when determining the longest line. + longestLine = max(longestLine, len(config.Execute)) + } svg.Move(line, x, y) @@ -343,7 +379,7 @@ func main() { if isAnsi { tabWidth = 6 } - longestLine := lipgloss.Width(strings.ReplaceAll(strippedInput, "\t", strings.Repeat(" ", tabWidth))) + longestLine = max(longestLine, lipgloss.Width(strings.ReplaceAll(strippedInput, "\t", strings.Repeat(" ", tabWidth)))) terminalWidth = float64(longestLine+1) * (config.Font.Size / fontHeightToWidthRatio) terminalWidth *= scale terminalWidth += hPadding @@ -392,6 +428,10 @@ func main() { istty := isatty.IsTerminal(os.Stdout.Fd()) + if config.Execute != "" && config.ShowLineNumbers { + printWarning("You cannot show line numbers for command execution.\nThis directive will have no effect.") + } + switch { case strings.HasSuffix(config.Output, ".png"): // use libsvg conversion. @@ -452,3 +492,7 @@ var outputHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Bac func printFilenameOutput(filename string) { fmt.Println(lipgloss.JoinHorizontal(lipgloss.Center, outputHeader.String(), filename)) } + +func printWarning(text string) { + fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("#D74E6F")).Render(text)) +} diff --git a/test/golden/svg/execute.svg b/test/golden/svg/execute.svg index 2811ee77..ddfa6194 100644 --- a/test/golden/svg/execute.svg +++ b/test/golden/svg/execute.svg @@ -1,6 +1,6 @@ - + + -Hello, world! +> echo "Hello, world!"Hello, world!