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 output of freeze command, Haskell code block with window controls applied +#### 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" +``` + +output of freeze command, Haskell code block with window title applied + +> [!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 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + +My artichoke code 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