diff --git a/README.md b/README.md
index 533a1de..2111ac9 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,8 @@ Screenshots can be customized with `--flags` or [Configuration](#configuration)
- [`-t`](#theme), [`--theme`](#theme): Theme to use for syntax highlighting.
- [`-w`](#window), [`--window`](#window): Display window controls.
- [`-H`](#height), [`--height`](#height): Height of terminal window.
+- [`--title.text`](#window-title): Display input file or custom text when window controls are displayed.
+- [`--title.position`](#window-title): Position of the title text.
- [`--border.width`](#border-width): Border width thickness.
- [`--border.color`](#border-width): Border color.
- [`--shadow.blur`](#shadow): Shadow Gaussian Blur.
@@ -216,6 +218,26 @@ freeze artichoke.hs --window
+#### Window Title
+
+Display the input file as the title of the window if `--title.text` is not passed in the arguments (`--title.text=auto` as default).
+
+Display a custom title if `--title.text` is passed in the arguments (`--title.text="custom title"`).
+
+Don't display the title if `--title.text` is passed in the arguments as empty string (`--title.text=""`).
+
+To position the title text, use `--title.position` with `left`, `center` or `right` (`--title.position=center` as default).
+
+```bash
+freeze artichoke.hs --window --title.text "My artichoke code"
+```
+
+
+
+> [!WARNING]
+>
+> The **title** can only be supported when using **window** mode (`--window=true`).
+
### Background
Set the background color of the terminal window.
diff --git a/config.go b/config.go
index 68a3554..1aa1d1c 100644
--- a/config.go
+++ b/config.go
@@ -22,6 +22,7 @@ type Config struct {
Margin []float64 `json:"margin" help:"Apply margin to the window." short:"m" placeholder:"0" group:"Window"`
Padding []float64 `json:"padding" help:"Apply padding to the code." short:"p" placeholder:"0" group:"Window"`
Window bool `json:"window" help:"Display window controls." group:"Window"`
+ Title Title `json:"title" embed:"" prefix:"title." group:"Window"`
Width float64 `json:"width" help:"Width of terminal window." short:"W" group:"Window"`
Height float64 `json:"height" help:"Height of terminal window." short:"H" group:"Window"`
@@ -49,6 +50,11 @@ type Config struct {
ShowLineNumbers bool `json:"show_line_numbers" help:"" group:"Line" placeholder:"false"`
}
+type Title struct {
+ Text string `json:"title" help:"Display window title. {{--title.text=auto}} as default for input filename." default:"auto"`
+ Position string `json:"position" help:"Position of window title, one of {{left}}, {{center}}, or {{right}}. {{--title.position=center}} as default." default:"center"`
+}
+
// Shadow is the configuration options for a drop shadow.
type Shadow struct {
Blur float64 `json:"blur" help:"Shadow Gaussian Blur." placeholder:"0"`
diff --git a/configurations/full.json b/configurations/full.json
index 40d5131..9ee527d 100644
--- a/configurations/full.json
+++ b/configurations/full.json
@@ -1,5 +1,9 @@
{
"window": true,
+ "title": {
+ "text": "auto",
+ "position": "center"
+ },
"theme": "charm",
"border": {
"radius": 8,
@@ -30,4 +34,4 @@
"ligatures": true
},
"line_height": 1.2
-}
\ No newline at end of file
+}
diff --git a/freeze_test.go b/freeze_test.go
index 0443255..f4b2f95 100644
--- a/freeze_test.go
+++ b/freeze_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/aymanbagabas/go-udiff"
+ "github.com/beevik/etree"
)
const binary = "./test/freeze-test"
@@ -106,6 +107,62 @@ func TestFreezeErrorFileMissing(t *testing.T) {
}
}
+func TestFreezeWindowTitleFilename(t *testing.T) {
+ output := "artichoke-default-title.svg"
+ t.Cleanup(func() { os.Remove(output) })
+ testTitle := "artichoke.hs"
+ cmd := exec.Command(binary, "test/input/artichoke.hs", "--output", output, "--window")
+ err := cmd.Run()
+
+ if err != nil {
+ t.Fatal("unexpected error", err)
+ }
+
+ doc := etree.NewDocument()
+ err = doc.ReadFromFile(output)
+ if err != nil {
+ t.Fatal("unexpected error", err)
+ }
+ childs := doc.ChildElements()
+ if len(childs) == 0 {
+ t.Fatal("no child elements")
+ }
+ lastChild := childs[len(childs)-1]
+ got := lastChild.FindElement("text").Text()
+
+ if got != testTitle {
+ t.Fatalf("expected %s to be %s", got, testTitle)
+ }
+}
+
+func TestFreezeCustomWindowTitle(t *testing.T) {
+ output := "artichoke-custom-title.svg"
+ t.Cleanup(func() { os.Remove(output) })
+ testTitle := "custom-test title"
+ cmd := exec.Command(binary, "test/input/artichoke.hs", "--output", output, "--title.text", testTitle, "--window")
+ err := cmd.Run()
+
+ if err != nil {
+ t.Fatal("unexpected error", err)
+ }
+
+ doc := etree.NewDocument()
+ err = doc.ReadFromFile(output)
+ if err != nil {
+ t.Fatal("unexpected error", err)
+ }
+ childs := doc.ChildElements()
+ if len(childs) == 0 {
+ t.Fatal("no child elements")
+ }
+ lastChild := childs[len(childs)-1]
+ got := lastChild.FindElement("text").Text()
+
+ if got != testTitle {
+ t.Fatalf("expected %s to be %s", got, testTitle)
+ }
+}
+
func TestFreezeConfigurations(t *testing.T) {
tests := []struct {
input string
@@ -260,6 +317,11 @@ func TestFreezeConfigurations(t *testing.T) {
flags: []string{"--wrap", "80", "--width", "600"},
output: "wrap",
},
+ {
+ input: "test/input/artichoke.hs",
+ flags: []string{"--border.radius", "8", "--window", "--title.text", "My artichoke code"},
+ output: "title",
+ },
}
err := os.RemoveAll("test/output/svg")
diff --git a/interactive.go b/interactive.go
index 094e517..858a316 100644
--- a/interactive.go
+++ b/interactive.go
@@ -103,6 +103,17 @@ func runForm(config *Config) (*Config, error) {
Inline(true).
Value(&config.Window),
+ huh.NewInput().Title("Title text").
+ Placeholder("auto").
+ Inline(true).
+ Prompt("").
+ Value(&config.Title.Text),
+
+ huh.NewSelect[string]().Title("Title position").
+ Inline(true).
+ Options(huh.NewOptions("left", "center", "right")...).
+ Value(&config.Title.Position),
+
huh.NewNote().Title("Font"),
huh.NewInput().Title("Font Family ").
diff --git a/main.go b/main.go
index 5a710d7..3b8667d 100644
--- a/main.go
+++ b/main.go
@@ -287,10 +287,29 @@ func main() {
}
if config.Window {
- windowControls := svg.NewWindowControls(5.5*float64(scale), 19.0*scale, 12.0*scale)
+ x := 19.0 * scale
+ y := 12.0 * scale
+ r := 5.5 * scale
+ windowControls := svg.NewWindowControls(r, x, y)
svg.Move(windowControls, float64(config.Margin[left]), float64(config.Margin[top]))
image.AddChild(windowControls)
config.Padding[top] += (15 * scale)
+ if config.Title.Text != "" {
+ windowChilds := windowControls.ChildElements()
+ if len(windowChilds) == 0 {
+ printErrorFatal("Unable to add title", errors.New("no window controls found"))
+ }
+ controlsWidth := float64(len(windowChilds)) * float64(x)
+ titlePos := getPositions(config, x, y, imageWidth, controlsWidth)
+ title, err := NewWindowTitle(config, titlePos, scale, s, r)
+ if err != nil {
+ printErrorFatal("Unable to add title", err)
+ }
+ image.AddChild(title)
+ }
+ } else if config.Title.Text != "auto" || config.Title.Position != "center" {
+ err := errors.New("Title is not supported when not using a window controls")
+ printErrorFatal("Unable to add title", err)
}
if config.Border.Radius > 0 {
@@ -455,3 +474,79 @@ var outputHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Bac
func printFilenameOutput(filename string) {
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Center, outputHeader.String(), filename))
}
+
+type Positions struct {
+ Left float64
+ Center float64
+ Right float64
+ Top float64
+}
+
+func getPositions(config Config, x, y, imgW, controlsWidth float64) Positions {
+ return Positions{
+ Left: config.Margin[left] + x + controlsWidth,
+ Center: imgW / 2,
+ Right: imgW - (config.Margin[right] + x),
+ Top: config.Margin[top] + y,
+ }
+}
+
+const (
+ posLeft = "left"
+ posCenter = "center"
+ posRight = "right"
+)
+
+func (title Title) Validate() error {
+ if title.Text == "" || title.Text == "-" {
+ return errors.New("Invalid title text provided.")
+ }
+ switch title.Position {
+ case posLeft, posCenter, posRight:
+ return nil
+ default:
+ return errors.New("Invalid title position. Must be one of \"left\", \"center\", or \"right\".")
+ }
+}
+
+// NewWindowTitle returns a title element with the given text.
+func NewWindowTitle(config Config, positions Positions, scale float64, s *chroma.Style, fs float64) (*etree.Element, error) {
+ err := config.Title.Validate()
+ if err != nil {
+ return nil, err
+ }
+ titleText := config.Title.Text
+ if titleText == "auto" {
+ titleText = filepath.Base(config.Input)
+ }
+ x := 0.0
+ y := positions.Top
+ moveY := hasLibsvg()
+ if moveY == nil {
+ y += fs
+ }
+ var anchor string
+ switch config.Title.Position {
+ case posLeft:
+ x = positions.Left
+ anchor = "start"
+ break
+ case posCenter:
+ x = positions.Center
+ anchor = "middle"
+ break
+ case posRight:
+ x = positions.Right
+ anchor = "end"
+ break
+ }
+ input := etree.NewElement("text")
+ input.CreateAttr("font-size", fmt.Sprintf("%.2fpx", fs*float64(scale)-config.Font.Size))
+ input.CreateAttr("fill", s.Get(chroma.Text).Colour.String())
+ input.CreateAttr("font-family", config.Font.Family)
+ input.CreateAttr("text-anchor", anchor)
+ input.CreateAttr("alignment-baseline", "middle")
+ input.SetText(titleText)
+ svg.Move(input, float64(x), float64(y))
+ return input, err
+}
diff --git a/png.go b/png.go
index b7d0184..647b30f 100644
--- a/png.go
+++ b/png.go
@@ -11,12 +11,16 @@ import (
"github.com/kanrichan/resvg-go"
)
-func libsvgConvert(doc *etree.Document, _, _ float64, output string) error {
+func hasLibsvg() error {
_, err := exec.LookPath("rsvg-convert")
+ return err
+}
+
+func libsvgConvert(doc *etree.Document, _, _ float64, output string) error {
+ err := hasLibsvg()
if err != nil {
return err //nolint: wrapcheck
}
-
svg, err := doc.WriteToBytes()
if err != nil {
return err //nolint: wrapcheck
diff --git a/test/configurations/full.json b/test/configurations/full.json
index bded6b0..4d0090c 100644
--- a/test/configurations/full.json
+++ b/test/configurations/full.json
@@ -1,5 +1,9 @@
{
"window": true,
+ "title": {
+ "text": "auto",
+ "position": "center"
+ },
"theme": "charm",
"border": {
"radius": 8,
@@ -24,4 +28,4 @@
"size": 14
},
"line_height": 1.2
-}
\ No newline at end of file
+}
diff --git a/test/golden/svg/artichoke-full.svg b/test/golden/svg/artichoke-full.svg
index c5e9984..7f24cc6 100644
--- a/test/golden/svg/artichoke-full.svg
+++ b/test/golden/svg/artichoke-full.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs
diff --git a/test/golden/svg/border-width.svg b/test/golden/svg/border-width.svg
index 98bbb13..ce36fae 100644
--- a/test/golden/svg/border-width.svg
+++ b/test/golden/svg/border-width.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs
diff --git a/test/golden/svg/dimensions-config.svg b/test/golden/svg/dimensions-config.svg
index 2478105..62e6661 100644
--- a/test/golden/svg/dimensions-config.svg
+++ b/test/golden/svg/dimensions-config.svg
@@ -19,4 +19,4 @@
hello s =
-
+artichoke.hs
diff --git a/test/golden/svg/eza.svg b/test/golden/svg/eza.svg
index 4336c3e..866e6ec 100644
--- a/test/golden/svg/eza.svg
+++ b/test/golden/svg/eza.svg
@@ -12,4 +12,4 @@
ansi.go cut_test.go go.mod main.go style.goconfig.go error.go go.sum Makefile svgconfig_test.go font help.go png.go tapesconfigurations font.go input pty.go testcut.go freeze_test.go interactive.go README.md
-
+eza.ansi
diff --git a/test/golden/svg/lines.svg b/test/golden/svg/lines.svg
index 186a291..864cfe8 100644
--- a/test/golden/svg/lines.svg
+++ b/test/golden/svg/lines.svg
@@ -16,4 +16,4 @@
7 hello s =
8 "Hello, " ++ s ++ "."
-
+artichoke.hs
diff --git a/test/golden/svg/margin.svg b/test/golden/svg/margin.svg
index 956194b..10c3926 100644
--- a/test/golden/svg/margin.svg
+++ b/test/golden/svg/margin.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs
diff --git a/test/golden/svg/overflow-line-numbers.svg b/test/golden/svg/overflow-line-numbers.svg
index b5461c9..5e51ced 100644
--- a/test/golden/svg/overflow-line-numbers.svg
+++ b/test/golden/svg/overflow-line-numbers.svg
@@ -120,4 +120,4 @@
108 secret_name: FURY_TOKEN
-
+goreleaser-full.yml
diff --git a/test/golden/svg/overflow.svg b/test/golden/svg/overflow.svg
index 6f97838..2bccb45 100644
--- a/test/golden/svg/overflow.svg
+++ b/test/golden/svg/overflow.svg
@@ -55,4 +55,4 @@
goarch:
-
+goreleaser-full.yml
diff --git a/test/golden/svg/padding.svg b/test/golden/svg/padding.svg
index 0b20d97..5cfe326 100644
--- a/test/golden/svg/padding.svg
+++ b/test/golden/svg/padding.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs
diff --git a/test/golden/svg/shadow.svg b/test/golden/svg/shadow.svg
index ad66d04..da980bc 100644
--- a/test/golden/svg/shadow.svg
+++ b/test/golden/svg/shadow.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs
diff --git a/test/golden/svg/title.svg b/test/golden/svg/title.svg
new file mode 100644
index 0000000..080690a
--- /dev/null
+++ b/test/golden/svg/title.svg
@@ -0,0 +1,29 @@
+
+
+
diff --git a/test/golden/svg/window.svg b/test/golden/svg/window.svg
index b64e6ae..5547689 100644
--- a/test/golden/svg/window.svg
+++ b/test/golden/svg/window.svg
@@ -26,4 +26,4 @@
-- Alcachofa, if you were wondering, is artichoke in Spanish.
-
+artichoke.hs