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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,34 @@ Set Height 1000
<img width="300" alt="Example of changing the height of the terminal" src="https://stuff.charm.sh/vhs/examples/height.gif">
</picture>

#### Set Columns

Set the width of the terminal in columns (character cells) with the `Set
Columns` command. VHS derives the final pixel width from the current font
settings (`FontSize`, `FontFamily`, `LetterSpacing`) plus `Padding`/`Margin`.

```elixir
Set Columns 100
```

`Set Columns` cannot be combined with `Set Width` — use one or the other to
control the terminal's width. It can be freely combined with `Set Height`
or `Set Rows` to control height.

#### Set Rows

Set the height of the terminal in rows (character cells) with the `Set Rows`
command. VHS derives the final pixel height from the current font settings
(`FontSize`, `FontFamily`, `LineHeight`) plus `Padding`/`Margin`/`WindowBar`.

```elixir
Set Rows 40
```

`Set Rows` cannot be combined with `Set Height` — use one or the other to
control the terminal's height. It can be freely combined with `Set Width`
or `Set Columns` to control width.

#### Set Letter Spacing

Set the spacing between letters (tracking) with the `Set LetterSpacing`
Expand Down
46 changes: 46 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@
"Theme": ExecuteSetTheme,
"TypingSpeed": ExecuteSetTypingSpeed,
"Width": ExecuteSetWidth,
"Rows": ExecuteSetRows,
"Columns": ExecuteSetColumns,
"Shell": ExecuteSetShell,

Check failure on line 474 in command.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

string `Shell` has 3 occurrences, make it a constant (goconst)
"LoopOffset": ExecuteLoopOffset,
"MarginFill": ExecuteSetMarginFill,
"Margin": ExecuteSetMargin,
Expand Down Expand Up @@ -528,7 +530,11 @@
if err != nil {
return fmt.Errorf("failed to parse height: %w", err)
}
if v.Options.Video.Style.Rows > 0 {
return fmt.Errorf("cannot set both Height and Rows")
}
v.Options.Video.Style.Height = height
v.heightExplicit = true

return nil
}
Expand All @@ -539,7 +545,47 @@
if err != nil {
return fmt.Errorf("failed to parse width: %w", err)
}
if v.Options.Video.Style.Columns > 0 {
return fmt.Errorf("cannot set both Width and Columns")
}
v.Options.Video.Style.Width = width
v.widthExplicit = true

return nil
}

// ExecuteSetRows applies the number of terminal rows on the vhs, overriding
// Height once the terminal is set up.
func ExecuteSetRows(c parser.Command, v *VHS) error {
rows, err := strconv.Atoi(c.Args)
if err != nil {
return fmt.Errorf("failed to parse rows: %w", err)
}
if rows <= 0 {
return fmt.Errorf("rows must be a positive integer")
}
if v.heightExplicit {
return fmt.Errorf("cannot set both Height and Rows")
}
v.Options.Video.Style.Rows = rows

return nil
}

// ExecuteSetColumns applies the number of terminal columns on the vhs,
// overriding Width once the terminal is set up.
func ExecuteSetColumns(c parser.Command, v *VHS) error {
columns, err := strconv.Atoi(c.Args)
if err != nil {
return fmt.Errorf("failed to parse columns: %w", err)
}
if columns <= 0 {
return fmt.Errorf("columns must be a positive integer")
}
if v.widthExplicit {
return fmt.Errorf("cannot set both Width and Columns")
}
v.Options.Video.Style.Columns = columns

return nil
}
Expand Down
87 changes: 87 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/charmbracelet/vhs/parser"
"github.com/charmbracelet/vhs/token"
)

func TestCommand(t *testing.T) {
Expand Down Expand Up @@ -55,6 +56,92 @@ func TestExecuteSetTheme(t *testing.T) {
})
}

func TestExecuteSetRowsColumns(t *testing.T) {
t.Run("rows", func(t *testing.T) {
v := New()
err := ExecuteSetRows(parser.Command{Type: token.SET, Options: "Rows", Args: "40"}, &v)
requireNoErr(t, err)
if v.Options.Video.Style.Rows != 40 {
t.Errorf("expected Rows to be 40, got %d", v.Options.Video.Style.Rows)
}
})

t.Run("columns", func(t *testing.T) {
v := New()
err := ExecuteSetColumns(parser.Command{Type: token.SET, Options: "Columns", Args: "100"}, &v)
requireNoErr(t, err)
if v.Options.Video.Style.Columns != 100 {
t.Errorf("expected Columns to be 100, got %d", v.Options.Video.Style.Columns)
}
})

t.Run("rows must be positive", func(t *testing.T) {
v := New()
err := ExecuteSetRows(parser.Command{Type: token.SET, Options: "Rows", Args: "0"}, &v)
requireErr(t, err)
})

t.Run("columns must be positive", func(t *testing.T) {
v := New()
err := ExecuteSetColumns(parser.Command{Type: token.SET, Options: "Columns", Args: "0"}, &v)
requireErr(t, err)
})

t.Run("height then rows conflict", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetHeight(parser.Command{Type: token.SET, Options: "Height", Args: "600"}, &v))
err := ExecuteSetRows(parser.Command{Type: token.SET, Options: "Rows", Args: "40"}, &v)
requireErr(t, err)
if v.Options.Video.Style.Rows != 0 {
t.Errorf("expected Rows to remain unset, got %d", v.Options.Video.Style.Rows)
}
})

t.Run("rows then height conflict", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetRows(parser.Command{Type: token.SET, Options: "Rows", Args: "40"}, &v))
defaultHeight := v.Options.Video.Style.Height
err := ExecuteSetHeight(parser.Command{Type: token.SET, Options: "Height", Args: "700"}, &v)
requireErr(t, err)
if v.Options.Video.Style.Height != defaultHeight {
t.Errorf("expected Height to remain unchanged, got %d", v.Options.Video.Style.Height)
}
})

t.Run("width then columns conflict", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetWidth(parser.Command{Type: token.SET, Options: "Width", Args: "1200"}, &v))
err := ExecuteSetColumns(parser.Command{Type: token.SET, Options: "Columns", Args: "100"}, &v)
requireErr(t, err)
if v.Options.Video.Style.Columns != 0 {
t.Errorf("expected Columns to remain unset, got %d", v.Options.Video.Style.Columns)
}
})

t.Run("columns then width conflict", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetColumns(parser.Command{Type: token.SET, Options: "Columns", Args: "100"}, &v))
defaultWidth := v.Options.Video.Style.Width
err := ExecuteSetWidth(parser.Command{Type: token.SET, Options: "Width", Args: "1300"}, &v)
requireErr(t, err)
if v.Options.Video.Style.Width != defaultWidth {
t.Errorf("expected Width to remain unchanged, got %d", v.Options.Video.Style.Width)
}
})

t.Run("width and rows can coexist", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetWidth(parser.Command{Type: token.SET, Options: "Width", Args: "1200"}, &v))
requireNoErr(t, ExecuteSetRows(parser.Command{Type: token.SET, Options: "Rows", Args: "40"}, &v))
})

t.Run("height and columns can coexist", func(t *testing.T) {
v := New()
requireNoErr(t, ExecuteSetHeight(parser.Command{Type: token.SET, Options: "Height", Args: "600"}, &v))
requireNoErr(t, ExecuteSetColumns(parser.Command{Type: token.SET, Options: "Columns", Args: "100"}, &v))
})
}

func requireErr(tb testing.TB, err error) {
tb.Helper()
if err == nil {
Expand Down
62 changes: 62 additions & 0 deletions examples/settings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,68 @@ Type "I'm a big teapot"
Sleep 1s
```

### Rows

<img width="600" src="./rows.gif" />

```
Output examples/settings/rows.gif

Set Width 475
Set Rows 8
Set FontSize 32

# Fill the terminal with a line-numbered count. Since it's cleared
# first, the count fills the frame edge-to-edge with nothing else
# on screen: exactly 8 lines for 8 rows, no more, no less. The
# last line skips its trailing newline so the terminal doesn't
# scroll once more to make room for the next prompt.
Type "clear; for i in $(seq 1 7); do echo $i; done; printf 8; sleep 5"
Enter

Sleep 2s
```

### Columns

<img width="600" src="./columns.gif" />

```
Output examples/settings/columns.gif

Set Height 400
Set Columns 30
Set FontSize 32

# Print a 30-character ruler after clearing. It touches the left
# and right edges of the frame exactly, with no wrap: 30 columns.
Type "clear; printf '%030d\n' 0 | tr '0' '#'; sleep 5"
Enter

Sleep 2s
```

### Rows and Columns

<img width="600" src="./rows-columns.gif" />

```
Output examples/settings/rows-columns.gif

Set FontSize 32
Set Rows 10
Set Columns 40

# A 40-char ruler as the first line proves the column count, and
# it plus 9 more numbered lines below it fill the frame exactly
# top to bottom, proving all 10 rows. The last line skips its
# trailing newline so the terminal doesn't scroll for a new prompt.
Type "clear; printf '%040d\n' 0 | tr '0' '#'; for i in $(seq 1 8); do echo $i; done; printf 9; sleep 5"
Enter

Sleep 2s
```

### Font Family

<img width="600" src="./set-font-family.gif" />
Expand Down
3 changes: 3 additions & 0 deletions examples/settings/columns.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/settings/columns.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Output examples/settings/columns.gif

Set Height 400
Set Columns 30
Set FontSize 32

# A count from 1 to 30, one digit per column, touches the left and
# right edges of the frame exactly with no wrap: 30 columns.
Hide
Type "clear; echo 123456789012345678901234567890"
Show

Sleep 1s
Enter

Sleep 2s
3 changes: 3 additions & 0 deletions examples/settings/rows-columns.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions examples/settings/rows-columns.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Output examples/settings/rows-columns.gif

Set FontSize 32
Set Rows 10
Set Columns 40

# A count from 1 to 40 as the first line proves the column count,
# and it plus 9 more numbered lines below it fill the frame exactly
# top to bottom, proving all 10 rows. The last line skips its
# trailing newline so the returning prompt lands on the same line
# instead of scrolling everything up by one.
Hide
Type "clear; echo 1234567890123456789012345678901234567890; echo Line 2; echo Line 3; echo Line 4; echo Line 5; echo Line 6; echo Line 7; echo Line 8; echo Line 9; printf 'Line 10'"
Show

Sleep 1s
Enter

Sleep 2s
3 changes: 3 additions & 0 deletions examples/settings/rows.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions examples/settings/rows.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Output examples/settings/rows.gif

Set Width 475
Set Rows 8
Set FontSize 32

# Eight lines of "Line N", filling the frame edge to edge: exactly
# 8 lines for 8 rows, no more, no less. The last one skips its
# trailing newline so the returning prompt lands on the same line
# instead of scrolling everything up by one.
Hide
Type "clear; echo Line 1; echo Line 2; echo Line 3; echo Line 4; echo Line 5; echo Line 6; echo Line 7; printf 'Line 8'"
Show

Sleep 1s
Enter

Sleep 2s
8 changes: 8 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ func TestNextToken(t *testing.T) {
Output examples/out.gif
Set FontSize 42
Set Padding 5
Set Rows 40
Set Columns 100
Set CursorBlink false
Type "echo 'Hello, world!'"
Enter
Expand Down Expand Up @@ -49,6 +51,12 @@ Wait+Screen@1m /foo\\\/bar/`
{token.PADDING, "Padding"},
{token.NUMBER, "5"},
{token.SET, "Set"},
{token.ROWS, "Rows"},
{token.NUMBER, "40"},
{token.SET, "Set"},
{token.COLUMNS, "Columns"},
{token.NUMBER, "100"},
{token.SET, "Set"},
{token.CURSOR_BLINK, "CursorBlink"},
{token.BOOLEAN, "false"},
{token.TYPE, "Type"},
Expand Down
2 changes: 2 additions & 0 deletions man.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The following is a list of all possible setting commands in VHS:
* Set %FontFamily% <string>
* Set %Height% <number>
* Set %Width% <number>
* Set %Rows% <number>
* Set %Columns% <number>
* Set %LetterSpacing% <float>
* Set %LineHeight% <float>
* Set %TypingSpeed% <time>
Expand Down
Loading
Loading