Skip to content

Commit 0d064e4

Browse files
committed
feat: add command to set window title
1 parent 5a207ef commit 0d064e4

File tree

15 files changed

+188
-24
lines changed

15 files changed

+188
-24
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,20 @@ Set WindowBar Colorful
469469
<img width="600" alt="Example of setting the margin" src="https://vhs.charm.sh/vhs-4VgviCu38DbaGtbRzhtOUI.gif">
470470
</picture>
471471

472+
#### Set Window Title
473+
474+
Set a title on the window bar of the terminal window with the `Set WindowTitle` command.
475+
476+
```elixir
477+
Set WindowTitle "Live in the Terminal"
478+
```
479+
480+
<picture>
481+
<source media="(prefers-color-scheme: dark)" srcset="https://vhs.charm.sh/vhs-6s07y8kWtPAC5TBLzxhRfS.gif">
482+
<source media="(prefers-color-scheme: light)" srcset="https://vhs.charm.sh/vhs-6s07y8kWtPAC5TBLzxhRfS.gif">
483+
<img width="600" alt="Example of setting the window title" src="https://vhs.charm.sh/vhs-6s07y8kWtPAC5TBLzxhRfS.gif">
484+
</picture>
485+
472486
#### Set Border Radius
473487

474488
Set the border radius (in pixels) of the terminal window with the `Set BorderRadius` command.

command.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ var Settings = map[string]CommandFunc{
369369
"MarginFill": ExecuteSetMarginFill,
370370
"Margin": ExecuteSetMargin,
371371
"WindowBar": ExecuteSetWindowBar,
372+
"WindowTitle": ExecuteSetWindowTitle,
372373
"WindowBarSize": ExecuteSetWindowBarSize,
373374
"BorderRadius": ExecuteSetBorderRadius,
374375
"CursorBlink": ExecuteSetCursorBlink,
@@ -506,6 +507,7 @@ func ExecuteSetTheme(c parser.Command, v *VHS) error {
506507

507508
v.Options.Video.Style.BackgroundColor = v.Options.Theme.Background
508509
v.Options.Video.Style.WindowBarColor = v.Options.Theme.Background
510+
v.Options.Video.Style.WindowTitleColor = v.Options.Theme.Foreground
509511

510512
return nil
511513
}
@@ -588,6 +590,11 @@ func ExecuteSetWindowBar(c parser.Command, v *VHS) error {
588590
return nil
589591
}
590592

593+
func ExecuteSetWindowTitle(c parser.Command, v *VHS) error {
594+
v.Options.Video.Style.WindowTitle = c.Args
595+
return nil
596+
}
597+
591598
// ExecuteSetWindowBar sets window bar size
592599
func ExecuteSetWindowBarSize(c parser.Command, v *VHS) error {
593600
windowBarSize, err := strconv.Atoi(c.Args)

draw.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import (
88
"image/png"
99
"math"
1010
"os"
11+
12+
"github.com/golang/freetype"
13+
"github.com/golang/freetype/truetype"
14+
"golang.org/x/image/font"
15+
"golang.org/x/image/font/gofont/goregular"
1116
)
1217

1318
type circle struct {
@@ -227,6 +232,72 @@ func MakeWindowBar(termWidth, termHeight int, opts StyleOptions, file string) {
227232
const barToDotRatio = 6
228233
const barToDotBorderRatio = 5
229234

235+
func drawWindowTitle(
236+
dotsSpace,
237+
borderSpace int,
238+
dotsRight bool,
239+
opts StyleOptions,
240+
windowBar *image.RGBA,
241+
) error {
242+
ttf, err := freetype.ParseFont(goregular.TTF)
243+
if err != nil {
244+
return err
245+
}
246+
247+
title := opts.WindowTitle
248+
249+
titleMinX := dotsSpace
250+
titleMaxX := borderSpace
251+
if dotsRight {
252+
titleMinX, titleMaxX = titleMaxX, titleMinX
253+
}
254+
255+
img := image.NewRGBA(
256+
image.Rectangle{
257+
image.Point{
258+
windowBar.Rect.Min.X + titleMinX,
259+
windowBar.Rect.Min.Y,
260+
},
261+
image.Point{
262+
windowBar.Rect.Max.X - titleMaxX,
263+
windowBar.Rect.Max.Y,
264+
},
265+
},
266+
)
267+
268+
fg, _ := parseHexColor(opts.WindowTitleColor)
269+
270+
c := freetype.NewContext()
271+
c.SetDPI(72)
272+
c.SetFont(ttf)
273+
c.SetFontSize(float64(half(opts.WindowBarSize)))
274+
c.SetClip(img.Bounds())
275+
c.SetDst(windowBar)
276+
c.SetSrc(&image.Uniform{fg})
277+
c.SetHinting(font.HintingNone)
278+
279+
fontFace := truetype.NewFace(ttf, &truetype.Options{})
280+
titleTextWidth := font.MeasureString(fontFace, title).Ceil()
281+
titleBarWidth := img.Rect.Max.X - img.Rect.Min.X
282+
283+
// Center-align title text if it will fit, else left-align (truncated by bounds)
284+
var textStartX int
285+
if titleBarWidth >= titleTextWidth {
286+
textStartX = (img.Rect.Max.X - titleTextWidth - titleMaxX) / 2
287+
} else {
288+
textStartX = titleMinX
289+
}
290+
291+
// Font size is half window bar size, thus Y start pos is 0.75 of window bar size
292+
textStartY := int(c.PointToFixed(float64(opts.WindowBarSize)*0.75) >> 6)
293+
294+
pt := freetype.Pt(textStartX, textStartY)
295+
if _, err := c.DrawString(title, pt); err != nil {
296+
return err
297+
}
298+
return nil
299+
}
300+
230301
func makeColorfulBar(termWidth int, termHeight int, isRight bool, opts StyleOptions, targetpng string) error {
231302
// Radius of dots
232303
dotRad := opts.WindowBarSize / barToDotRatio
@@ -299,6 +370,21 @@ func makeColorfulBar(termWidth int, termHeight int, isRight bool, opts StyleOpti
299370
draw.Over,
300371
)
301372

373+
if opts.WindowTitle != "" {
374+
titleDotsSpace := dotGap + dotRad*3 + dotSpace*3
375+
titleBorderSpace := dotGap + dotSpace
376+
377+
if err := drawWindowTitle(
378+
titleDotsSpace,
379+
titleBorderSpace,
380+
isRight,
381+
opts,
382+
img,
383+
); err != nil {
384+
fmt.Println(ErrorStyle.Render(fmt.Sprintf("Couldn't draw window title: %s.", err)))
385+
}
386+
}
387+
302388
f, err := os.Create(targetpng)
303389
if err != nil {
304390
fmt.Println(ErrorStyle.Render("Couldn't draw colorful bar: unable to save file."))
@@ -373,6 +459,21 @@ func makeRingBar(termWidth int, termHeight int, isRight bool, opts StyleOptions,
373459
)
374460
}
375461

462+
if opts.WindowTitle != "" {
463+
titleDotsSpace := ringGap + outerRad*3 + ringSpace*3
464+
titleBorderSpace := ringGap + ringSpace
465+
466+
if err := drawWindowTitle(
467+
titleDotsSpace,
468+
titleBorderSpace,
469+
isRight,
470+
opts,
471+
img,
472+
); err != nil {
473+
fmt.Println(ErrorStyle.Render(fmt.Sprintf("Couldn't draw window title: %s.", err)))
474+
}
475+
}
476+
376477
f, err := os.Create(targetpng)
377478
if err != nil {
378479
fmt.Println(ErrorStyle.Render("Couldn't draw ring bar: unable to save file."))

examples/decorations/decorations.tape

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ Set WindowBarSize 40
1212
Set BorderRadius 8
1313

1414
Type "I can't believe it's not butter."
15-
Sleep 2s
15+
Sleep 2s

examples/demo.tape

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# Set BorderRadius <number> Set terminal border radius, in pixels.
2626
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
2727
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
28+
# Set WindowTitle <string> Set window title. Text color is taken from theme.
2829
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
2930
#
3031
# Sleep:

examples/fixtures/all.tape

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Set TypingSpeed .1
2222
Set LoopOffset 60.4
2323
Set LoopOffset 20.99%
2424
Set CursorBlink false
25+
Set WindowTitle "Hello, world!"
2526

2627
# Sleep:
2728
Sleep 1
22.9 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Output examples/settings/set-window-title.gif
2+
3+
Set FontSize 25
4+
Set Width 800
5+
Set Height 400
6+
Set Padding 20
7+
8+
Set WindowBar Colorful
9+
Set WindowTitle "Live in the Terminal"
10+
Set WindowBarSize 40
11+
12+
Type "This terminal window has a title."
13+
Sleep 2s

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
3939
github.com/dlclark/regexp2 v1.8.1 // indirect
4040
github.com/go-logfmt/logfmt v0.6.0 // indirect
41+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
4142
github.com/gorilla/css v1.0.0 // indirect
4243
github.com/inconshreveable/mousetrap v1.1.0 // indirect
4344
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@@ -60,8 +61,9 @@ require (
6061
github.com/yuin/goldmark v1.5.4 // indirect
6162
github.com/yuin/goldmark-emoji v1.0.2 // indirect
6263
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
64+
golang.org/x/image v0.18.0 // indirect
6365
golang.org/x/net v0.22.0 // indirect
64-
golang.org/x/sync v0.6.0 // indirect
66+
golang.org/x/sync v0.7.0 // indirect
6567
golang.org/x/sys v0.20.0 // indirect
66-
golang.org/x/text v0.15.0 // indirect
68+
golang.org/x/text v0.16.0 // indirect
6769
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi
5050
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
5151
github.com/go-rod/rod v0.116.0 h1:ypRryjTys3EnqHskJ/TdgodFMvXV0EHvmy4bSkKZgHM=
5252
github.com/go-rod/rod v0.116.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
53+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
54+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
5355
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
5456
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
5557
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
@@ -125,10 +127,14 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
125127
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
126128
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
127129
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
130+
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
131+
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
128132
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
129133
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
130134
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
131135
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
136+
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
137+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
132138
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
133139
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
134140
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
@@ -137,6 +143,8 @@ golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
137143
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
138144
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
139145
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
146+
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
147+
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
140148
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
141149
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
142150
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

lexer/lexer_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
func TestNextToken(t *testing.T) {
1111
input := `
1212
Output examples/out.gif
13+
Set WindowTitle "Hello, world!"
1314
Set FontSize 42
1415
Set Padding 5
1516
Set CursorBlink false
@@ -33,6 +34,9 @@ Sleep 2`
3334
{token.OUTPUT, "Output"},
3435
{token.STRING, "examples/out.gif"},
3536
{token.SET, "Set"},
37+
{token.WINDOW_TITLE, "WindowTitle"},
38+
{token.STRING, "Hello, world!"},
39+
{token.SET, "Set"},
3640
{token.FONT_SIZE, "FontSize"},
3741
{token.NUMBER, "42"},
3842
{token.SET, "Set"},
@@ -156,6 +160,9 @@ func TestLexTapeFile(t *testing.T) {
156160
{token.SET, "Set"},
157161
{token.CURSOR_BLINK, "CursorBlink"},
158162
{token.BOOLEAN, "false"},
163+
{token.SET, "Set"},
164+
{token.WINDOW_TITLE, "WindowTitle"},
165+
{token.STRING, "Hello, world!"},
159166
{token.COMMENT, " Sleep:"},
160167
{token.SLEEP, "Sleep"},
161168
{token.NUMBER, "1"},

parser/parser.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ func (p *Parser) parseSet() Command {
425425
NewError(p.cur, windowBar+" is not a valid bar style."),
426426
)
427427
}
428+
case token.WINDOW_TITLE:
429+
cmd.Args = p.peek.Literal
430+
p.nextToken()
428431
case token.MARGIN_FILL:
429432
cmd.Args = p.peek.Literal
430433
p.nextToken()

parser/parser_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func TestParseTapeFile(t *testing.T) {
129129
{Type: token.SET, Options: "LoopOffset", Args: "60.4%"},
130130
{Type: token.SET, Options: "LoopOffset", Args: "20.99%"},
131131
{Type: token.SET, Options: "CursorBlink", Args: "false"},
132+
{Type: token.SET, Options: "WindowTitle", Args: "Hello, world!"},
132133
{Type: token.SLEEP, Options: "", Args: "1s"},
133134
{Type: token.SLEEP, Options: "", Args: "500ms"},
134135
{Type: token.SLEEP, Options: "", Args: ".5s"},

style.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,34 @@ var (
6060

6161
// StyleOptions represents the ui options for video and screenshots.
6262
type StyleOptions struct {
63-
Width int
64-
Height int
65-
Padding int
66-
BackgroundColor string
67-
MarginFill string
68-
Margin int
69-
WindowBar string
70-
WindowBarSize int
71-
WindowBarColor string
72-
BorderRadius int
63+
Width int
64+
Height int
65+
Padding int
66+
BackgroundColor string
67+
MarginFill string
68+
Margin int
69+
WindowBar string
70+
WindowTitle string
71+
WindowTitleColor string
72+
WindowBarSize int
73+
WindowBarColor string
74+
BorderRadius int
7375
}
7476

7577
// DefaultStyleOptions returns default Style config.
7678
func DefaultStyleOptions() *StyleOptions {
7779
return &StyleOptions{
78-
Width: defaultWidth,
79-
Height: defaultHeight,
80-
Padding: defaultPadding,
81-
MarginFill: DefaultTheme.Background,
82-
Margin: 0,
83-
WindowBar: "",
84-
WindowBarSize: defaultWindowBarSize,
85-
WindowBarColor: DefaultTheme.Background,
86-
BorderRadius: 0,
87-
BackgroundColor: DefaultTheme.Background,
80+
Width: defaultWidth,
81+
Height: defaultHeight,
82+
Padding: defaultPadding,
83+
MarginFill: DefaultTheme.Background,
84+
Margin: 0,
85+
WindowBar: "",
86+
WindowTitle: "",
87+
WindowTitleColor: DefaultTheme.Foreground,
88+
WindowBarSize: defaultWindowBarSize,
89+
WindowBarColor: DefaultTheme.Background,
90+
BorderRadius: 0,
91+
BackgroundColor: DefaultTheme.Background,
8892
}
8993
}

token/token.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const (
8888
MARGIN_FILL = "MARGIN_FILL" //nolint:revive
8989
MARGIN = "MARGIN" //nolint:revive
9090
WINDOW_BAR = "WINDOW_BAR" //nolint:revive
91+
WINDOW_TITLE = "WINDOW_TITLE" //nolint:revive
9192
WINDOW_BAR_SIZE = "WINDOW_BAR_SIZE" //nolint:revive
9293
BORDER_RADIUS = "CORNER_RADIUS" //nolint:revive
9394
CURSOR_BLINK = "CURSOR_BLINK" //nolint:revive
@@ -129,6 +130,7 @@ var Keywords = map[string]Type{
129130
"MarginFill": MARGIN_FILL,
130131
"Margin": MARGIN,
131132
"WindowBar": WINDOW_BAR,
133+
"WindowTitle": WINDOW_TITLE,
132134
"WindowBarSize": WINDOW_BAR_SIZE,
133135
"BorderRadius": BORDER_RADIUS,
134136
"FontSize": FONT_SIZE,
@@ -157,7 +159,7 @@ func IsSetting(t Type) bool {
157159
switch t {
158160
case SHELL, FONT_FAMILY, FONT_SIZE, LETTER_SPACING, LINE_HEIGHT,
159161
FRAMERATE, TYPING_SPEED, THEME, PLAYBACK_SPEED, HEIGHT, WIDTH,
160-
PADDING, LOOP_OFFSET, MARGIN_FILL, MARGIN, WINDOW_BAR,
162+
PADDING, LOOP_OFFSET, MARGIN_FILL, MARGIN, WINDOW_BAR, WINDOW_TITLE,
161163
WINDOW_BAR_SIZE, BORDER_RADIUS, CURSOR_BLINK:
162164
return true
163165
default:

0 commit comments

Comments
 (0)