Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

<p align="center">
Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions freeze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
})
}
}
52 changes: 48 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,21 +314,57 @@

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

Check failure on line 352 in main.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to x (ineffassign)
// Command
// TODO eventually remove these hard-coded styles and use shell as

Check failure on line 354 in main.go

View workflow job for this annotation

GitHub Actions / lint-soft

main.go:354: Line contains TODO/BUG/FIXME: "TODO eventually remove these hard-coded ..." (godox)
// 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)

Expand All @@ -343,7 +379,7 @@
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
Expand Down Expand Up @@ -392,6 +428,10 @@

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.
Expand Down Expand Up @@ -452,3 +492,7 @@
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))
}
6 changes: 3 additions & 3 deletions test/golden/svg/execute.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading