chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:56 +08:00
commit 93eb77f7cf
114 changed files with 4812 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
examples/*.svg filter=lfs diff=lfs merge=lfs -text
tapes/*.gif filter=lfs diff=lfs merge=lfs -text
test/output/**/*.png filter=lfs diff=lfs merge=lfs -text
test/golden/**/*.png filter=lfs diff=lfs merge=lfs -text
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+57
View File
@@ -0,0 +1,57 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
+11
View File
@@ -0,0 +1,11 @@
name: build
on: [push, pull_request]
jobs:
build:
uses: charmbracelet/meta/.github/workflows/build.yml@main
with:
go-version: ""
go-version-file: ./go.mod
secrets:
gh_pat: "${{ secrets.PERSONAL_ACCESS_TOKEN }}"
+17
View File
@@ -0,0 +1,17 @@
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+32
View File
@@ -0,0 +1,32 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: goreleaser
on:
push:
tags:
- v*.*.*
concurrency:
group: goreleaser
cancel-in-progress: true
jobs:
goreleaser:
uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
with:
macos_sign_entitlements: "./release/entitlements.plist"
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
aur_key: ${{ secrets.AUR_KEY }}
fury_token: ${{ secrets.FURY_TOKEN }}
nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }}
nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }}
macos_sign_p12: ${{ secrets.MACOS_SIGN_P12 }}
macos_sign_password: ${{ secrets.MACOS_SIGN_PASSWORD }}
macos_notary_issuer_id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
macos_notary_key_id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
macos_notary_key: ${{ secrets.MACOS_NOTARY_KEY }}
+11
View File
@@ -0,0 +1,11 @@
name: lint
on:
push:
pull_request:
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
with:
golangci_path: .golangci.yml
timeout: 10m
+7
View File
@@ -0,0 +1,7 @@
*.svg
*.png
!test/golden/**/*.svg
!test/golden/**/*.png
!examples/*.svg
/result
dist/
+36
View File
@@ -0,0 +1,36 @@
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- gosec
- nilerr
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
exclusions:
generated: lax
presets:
- common-false-positives
paths:
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+12
View File
@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-simple.yaml
variables:
main: "."
description: "Generate images of code and terminal output."
github_url: "https://github.com/charmbracelet/freeze"
maintainer: "Maas Lalani <maas@charm.sh>"
brew_commit_author_name: "Maas Lalani"
brew_commit_author_email: "maas@charm.sh"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
.PHONY: test
test:
go test ./...
golden:
cp -r test/output/* test/golden
+383
View File
@@ -0,0 +1,383 @@
# Freeze
<p>
<a href="https://stuff.charm.sh/freeze/freeze-4k.png"><img src="https://github.com/charmbracelet/freeze/assets/25087/de76b799-fa67-4b5b-8da2-d990ca5b4e06" width="500" /></a><br>
<a href="https://github.com/charmbracelet/freeze/releases"><img src="https://img.shields.io/github/release/charmbracelet/freeze.svg" alt="Latest Release"></a>
<a href="https://github.com/charmbracelet/freeze/actions"><img src="https://github.com/charmbracelet/freeze/workflows/build/badge.svg" alt="Build Status"></a>
</p>
Generate images of code and terminal output.
<p align="left">
<a><img width="600" src="https://vhs.charm.sh/vhs-1C6z5SUKlTdqdj4KL1ADlH.gif" alt="Freeze code screenshot"></a>
</p>
## Examples
Freeze generates PNGs, SVGs, and WebPs of code and terminal output alike.
### Generate an image of code
```sh
freeze artichoke.hs -o artichoke.png
```
<p align="center">
<a href="https://github.com/charmbracelet/freeze/assets/42545625/f15efdda-8e9b-4cb1-9e87-3d32b692eb7c">
<img alt="output of freeze command, Haskell code block" src="./test/golden/svg/shadow.svg" width="800" />
</a>
</p>
### Generate an image of terminal output
You can use `freeze` to capture ANSI output of a terminal command with the
`--execute` flag.
```bash
freeze --execute "eza -lah"
```
<p align="center">
<a href="https://github.com/charmbracelet/freeze/assets/42545625/aa5447ed-999a-4809-909d-67093d758f5a">
<img alt="output of freeze command, ANSI" src="./test/golden/svg/eza.svg" width="800" />
</a>
</p>
Freeze is also [super customizable](#customization) and ships with an [interactive TUI](#interactive-mode).
## Installation
```sh
# macOS or Linux
brew install charmbracelet/tap/freeze
# Arch Linux (btw)
yay -S freeze
# Nix
nix-env -iA nixpkgs.charm-freeze
```
Or, download it:
- [Packages][releases] are available in Debian and RPM formats
- [Binaries][releases] are available for Linux, macOS, and Windows
Or, just install it with `go`:
```sh
go install github.com/charmbracelet/freeze@latest
```
[releases]: https://github.com/charmbracelet/freeze/releases
## Customization
### Interactive mode
Freeze features a fully interactive mode for easy customization.
```bash
freeze --interactive
```
<img alt="freeze interactive mode" src="https://vhs.charm.sh/vhs-1AGhIlc2Mtn9Ltc8vPtaAP.gif" width="400" />
Settings are written to `$XDG_CONFIG/freeze/user.json` and can be accessed with
`freeze --config user`.
### Flags
Screenshots can be customized with `--flags` or [Configuration](#configuration) files.
> [!NOTE]
> You can view all freeze customization with `freeze --help`.
- [`-b`](#background), [`--background`](#background): Apply a background fill.
- [`-c`](#configuration), [`--config`](#configuration): Base configuration file or template.
- [`-l`](#language), [`--language`](#language): Language to apply to code
- [`-m`](#margin), [`--margin`](#margin): Apply margin to the window.
- [`-o`](#output), [`--output`](#output): Output location for .svg, .png, .jpg.
- [`-p`](#padding), [`--padding`](#padding): Apply padding to the code.
- [`-r`](#border-radius), [`--border.radius`](#border-radius): Corner radius of window.
- [`-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.
- [`--border.width`](#border-width): Border width thickness.
- [`--border.color`](#border-width): Border color.
- [`--shadow.blur`](#shadow): Shadow Gaussian Blur.
- [`--shadow.x`](#shadow): Shadow offset x coordinate.
- [`--shadow.y`](#shadow): Shadow offset y coordinate.
- [`--font.family`](#font): Font family to use for code.
- [`--font.ligatures`](#font): Use ligatures in the font.
- [`--font.size`](#font): Font size to use for code.
- [`--font.file`](#font): File path to the font to use (embedded in the SVG).
- [`--line-height`](#font): Line height relative to font size.
- [`--show-line-numbers`](#line-numbers): Show line numbers.
- [`--lines`](#line-numbers): Lines to capture (start,end).
### Language
If possible, `freeze` auto-detects the language from the file name or analyzing
the file contents. Override this inference with the `--language` flag.
```bash
cat artichoke.hs | freeze --language haskell
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/d746f028-3d51-4bfd-ba81-94bbc47b3587">
<img alt="output of freeze command, Haskell code block" src="./test/golden/svg/haskell.svg" width="600" />
</a>
### Theme
Change the color theme.
```bash
freeze artichoke.hs --theme dracula
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/c693dc80-89b8-43c6-a34e-2d49a332d0c7">
<img alt="output of freeze command, Haskell code block with dracula theme" src="./test/golden/svg/dracula.svg" width="600" />
</a>
### Output
Change the output file location, defaults to `out.svg` or stdout if piped. This
value supports `.svg`, `.png`, `.webp`.
```bash
freeze main.go --output out.svg
freeze main.go --output out.png
freeze main.go --output out.webp
# or all of the above
freeze main.go --output out.{svg,png,webp}
```
### Font
Specify the font family, font size, and font line height of the output image.
Defaults to `JetBrains Mono`, `14`(px), `1.2`(em).
```bash
freeze artichoke.hs \
--font.family "SF Mono" \
--font.size 16 \
--line-height 1.4
```
You can also embed a font file (in TTF, WOFF, or WOFF2 format) using the
`--font.file` flag.
To use ligatures in the font, you can apply the `--font.ligatures` flag.
### Line Numbers
Show line numbers in the terminal window with the `--show-line-numbers` flag.
```bash
freeze artichoke.hs --show-line-numbers
```
To capture only a specific range of line numbers you can use the `--lines` flag.
```bash
freeze artichoke.hs --show-line-numbers --lines 2,3
```
### Border Radius
Add rounded corners to the terminal.
```bash
freeze artichoke.hs --border.radius 8
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/50932b10-56fd-4312-adbd-f64b36a15441">
<img alt="code screenshot with corner radius of 8px" src="./test/golden/svg/border-radius.svg" width="600" />
</a>
### Window
Add window controls to the terminal, macOS-style.
```bash
freeze artichoke.hs --window
```
<a href="https://github.com/charmbracelet/freeze/assets/42545625/ba46bc4a-fb36-4718-88d4-f63b93343615">
<img alt="output of freeze command, Haskell code block with window controls applied" src="./test/golden/svg/window.svg" width="600" />
</a>
### Background
Set the background color of the terminal window.
```bash
freeze artichoke.hs --background "#08163f"
```
### Height
Set the height of the terminal window.
```bash
freeze artichoke.hs --height 400
```
### Border Width
Add a border outline to the terminal window.
```bash
freeze artichoke.hs --border.width 1 --border.color "#515151" --border.radius 8
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/b9207976-50fe-479b-974b-e1bf2ad1684c">
<img alt="output of freeze command, Haskell code block with border applied" src="./test/golden/svg/border-width.svg" width="600" />
</a>
### Padding
Add padding to the terminal window. You can provide 1, 2, or 4 values.
```bash
freeze main.go --padding 20 # all sides
freeze main.go --padding 20,40 # vertical, horizontal
freeze main.go --padding 20,60,20,40 # top, right, bottom, left
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/41da80bb-a6d8-402b-9f14-c73050720c0f">
<img alt="output of freeze command, Haskell code block with padding applied" src="./test/golden/svg/padding.svg" width="600" />
</a>
### Margin
Add margin to the terminal window. You can provide 1, 2, or 4 values.
```bash
freeze main.go --margin 20 # all sides
freeze main.go --margin 20,40 # vertical, horizontal
freeze main.go --margin 20,60,20,40 # top, right, bottom, left
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/eca78a8e-2dbc-4cfa-81da-6c2194925238">
<img alt="output of freeze command, Haskell code block with margin applied" src="./test/golden/svg/margin.svg" width="720" />
</a>
### Shadow
Add a shadow under the terminal window.
```bash
freeze artichoke.hs --shadow.blur 20 --shadow.x 0 --shadow.y 10
```
<br />
<a href="https://github.com/charmbracelet/freeze/assets/42545625/b9a8cc17-5ec6-4004-941b-5ae1b65d2b18">
<img alt="output of freeze command, Haskell code block with a shadow" src="./test/golden/svg/shadow.svg" width="720" />
</a>
## Screenshot TUIs
Use `tmux capture-pane` to generate screenshots of TUIs.
Run your TUI in `tmux` and get it to the state you want to capture.
Next, use `capture-pane` to capture the pane and pipe that to freeze.
```bash
hx # in a separate pane
tmux capture-pane -pet 1 | freeze -c full
```
<img width="650px" src="./test/golden/svg/helix.svg" alt="helix captured with freeze">
## Configuration
Freeze also supports configuration via a JSON file which can be passed with the
`--config` / `-c` flag. In general, all `--flag` options map directly to keys
and values in the config file
There are also some default configurations built into `freeze` which can be passed by name.
- `base`: Simple screenshot of code.
- `full`: macOS-like screenshot.
- `user`: Uses `~/.config/freeze/user.json`.
If you use `--interactive` mode, a configuration file will be created for you at
`~/.config/freeze/user.json`. This will be the default configuration file used
in your screenshots.
```bash
freeze -c base main.go
freeze -c full main.go
freeze -c user main.go # alias for ~/.config/freeze/user.json
freeze -c ./custom.json main.go
```
Here's what an example configuration looks like:
```json
{
"window": false,
"border": {
"radius": 0,
"width": 0,
"color": "#515151"
},
"shadow": false,
"padding": [20, 40, 20, 20],
"margin": "0",
"font": {
"family": "JetBrains Mono",
"size": 14
},
"line_height": 1.2
}
```
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/freeze/contribute
## Feedback
Wed love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/freeze/raw/main/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/">
<img
alt="The Charm logo"
width="400"
src="https://stuff.charm.sh/charm-badge.jpg"
/>
</a>
Charm热爱开源 • Charm loves open source
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`charmbracelet/freeze`
- 原始仓库:https://github.com/charmbracelet/freeze
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please email vt100@charm.sh
+234
View File
@@ -0,0 +1,234 @@
package main //nolint:revive
import (
"fmt"
"github.com/beevik/etree"
"github.com/charmbracelet/x/ansi"
"github.com/mattn/go-runewidth"
)
type dispatcher struct {
scale float64
svg *etree.Element
bg *etree.Element
config *Config
lines []*etree.Element
row int
col int
bgWidth int
}
func (p *dispatcher) Print(r rune) {
p.row = clamp(p.row, 0, len(p.lines)-1)
// insert the rune in the last tspan
children := p.lines[p.row].ChildElements()
var lastChild *etree.Element
isFirstChild := len(children) == 0
if isFirstChild {
lastChild = etree.NewElement("tspan")
lastChild.CreateAttr("xml:space", "preserve")
p.lines[p.row].AddChild(lastChild)
} else {
lastChild = children[len(children)-1]
}
if runewidth.RuneWidth(r) > 1 {
newChild := lastChild.Copy()
newChild.SetText(string(r))
newChild.CreateAttr("dx", fmt.Sprintf("%.2fpx", (p.config.Font.Size/5)*p.scale))
p.lines[p.row].AddChild(newChild)
} else {
lastChild.SetText(lastChild.Text() + string(r))
}
p.col += runewidth.RuneWidth(r)
if p.bg != nil {
p.bgWidth += runewidth.RuneWidth(r)
}
}
func (p *dispatcher) Execute(code byte) {
if code == '\t' {
for p.col%16 != 0 {
p.Print(' ')
}
}
if code == '\n' {
p.endBackground()
p.row++
p.col = 0
}
}
const fontHeightToWidthRatio = 1.68
func (p *dispatcher) beginBackground(fill string) {
rect := etree.NewElement("rect")
rect.CreateAttr("fill", fill)
topOffset := p.config.Padding[top] + p.config.Margin[top] + (((p.config.Font.Size + p.config.LineHeight) / 5) * p.scale)
rowMultiplier := p.config.Font.Size * p.config.LineHeight
y := fmt.Sprintf("%.2fpx", float64(p.row)*rowMultiplier+topOffset)
x := p.scale * float64(p.col) * (p.config.Font.Size / fontHeightToWidthRatio)
x += float64(p.config.Margin[left] + p.config.Padding[left])
if p.config.ShowLineNumbers {
x += float64(p.config.Font.Size) * 3
}
rect.CreateAttr("x", fmt.Sprintf("%.2fpx", x))
rect.CreateAttr("y", y)
rect.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight+1))
p.bg = rect
}
func (p *dispatcher) endBackground() {
if p.bg == nil {
return
}
width := (float64(p.bgWidth) + 0.5) * p.scale
if p.bgWidth == 0 {
width = 0
}
p.bg.CreateAttr("width", fmt.Sprintf("%.5fpx", width*(p.config.Font.Size/fontHeightToWidthRatio)))
p.svg.InsertChildAt(0, p.bg)
p.bg = nil
p.bgWidth = 0
}
func (p *dispatcher) CsiDispatch(cmd ansi.Cmd, params ansi.Params) {
if cmd != 'm' {
// ignore incomplete or non Style (SGR) sequences
return
}
span := etree.NewElement("tspan")
span.CreateAttr("xml:space", "preserve")
reset := func() {
// reset ANSI, this is done by creating a new empty tspan,
// which would reset all the styles such that when text is appended to the last
// child of this line there is no styling applied.
if p.row < len(p.lines) {
p.lines[p.row].AddChild(span)
}
p.endBackground()
}
if len(params) == 0 {
// zero params means reset
reset()
return
}
var i int
for i < len(params) {
v := params[i].Param(0)
switch v {
case 0:
reset()
case 1:
// span.CreateAttr("font-weight", "bold")
p.lines[p.row].AddChild(span)
case 9:
span.CreateAttr("text-decoration", "line-through")
p.lines[p.row].AddChild(span)
case 3:
span.CreateAttr("font-style", "italic")
p.lines[p.row].AddChild(span)
case 4:
span.CreateAttr("text-decoration", "underline")
p.lines[p.row].AddChild(span)
case 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97:
span.CreateAttr("fill", ansiPalette[v])
p.lines[p.row].AddChild(span)
case 38:
i++
switch params[i] {
case 5:
n := params[i+1]
i++
fill := palette[n]
span.CreateAttr("fill", fill)
p.lines[p.row].AddChild(span)
case 2:
span.CreateAttr("fill", fmt.Sprintf("#%02x%02x%02x", params[i+1], params[i+2], params[i+3]))
p.lines[p.row].AddChild(span)
i += 3
}
case 48:
p.endBackground()
i++
switch params[i] {
case 5:
n := params[i+1]
i++
fill := palette[n]
p.beginBackground(fill)
case 2:
fill := fmt.Sprintf("#%02x%02x%02x", params[i+1], params[i+2], params[i+3])
p.beginBackground(fill)
i += 3
}
case 100, 101, 102, 103, 104, 105, 106, 107:
p.beginBackground(ansiPalette[v])
}
i++
}
}
var ansiPalette = map[int]string{
30: "#282a2e", // black
31: "#D74E6F", // red
32: "#31BB71", // green
33: "#D3E561", // yellow
34: "#8056FF", // blue
35: "#ED61D7", // magenta
36: "#04D7D7", // cyan
37: "#C5C8C6", // white
90: "#4B4B4B", // bright black
91: "#FE5F86", // bright red
92: "#00D787", // bright green
93: "#EBFF71", // bright yellow
94: "#8F69FF", // bright blue
95: "#FF7AEA", // bright magenta
96: "#00FEFE", // bright cyan
97: "#FFFFFF", // bright white
}
var palette = []string{
"#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0",
"#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff",
"#000000", "#00005f", "#000087", "#0000af", "#0000d7", "#0000ff", "#005f00", "#005f5f",
"#005f87", "#005faf", "#005fd7", "#005fff", "#008700", "#00875f", "#008787", "#0087af",
"#0087d7", "#0087ff", "#00af00", "#00af5f", "#00af87", "#00afaf", "#00afd7", "#00afff",
"#00d700", "#00d75f", "#00d787", "#00d7af", "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f",
"#00ff87", "#00ffaf", "#00ffd7", "#00ffff", "#5f0000", "#5f005f", "#5f0087", "#5f00af",
"#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", "#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff",
"#5f8700", "#5f875f", "#5f8787", "#5f87af", "#5f87d7", "#5f87ff", "#5faf00", "#5faf5f",
"#5faf87", "#5fafaf", "#5fafd7", "#5fafff", "#5fd700", "#5fd75f", "#5fd787", "#5fd7af",
"#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", "#5fff87", "#5fffaf", "#5fffd7", "#5fffff",
"#870000", "#87005f", "#870087", "#8700af", "#8700d7", "#8700ff", "#875f00", "#875f5f",
"#875f87", "#875faf", "#875fd7", "#875fff", "#878700", "#87875f", "#878787", "#8787af",
"#8787d7", "#8787ff", "#87af00", "#87af5f", "#87af87", "#87afaf", "#87afd7", "#87afff",
"#87d700", "#87d75f", "#87d787", "#87d7af", "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f",
"#87ff87", "#87ffaf", "#87ffd7", "#87ffff", "#af0000", "#af005f", "#af0087", "#af00af",
"#af00d7", "#af00ff", "#af5f00", "#af5f5f", "#af5f87", "#af5faf", "#af5fd7", "#af5fff",
"#af8700", "#af875f", "#af8787", "#af87af", "#af87d7", "#af87ff", "#afaf00", "#afaf5f",
"#afaf87", "#afafaf", "#afafd7", "#afafff", "#afd700", "#afd75f", "#afd787", "#afd7af",
"#afd7d7", "#afd7ff", "#afff00", "#afff5f", "#afff87", "#afffaf", "#afffd7", "#afffff",
"#d70000", "#d7005f", "#d70087", "#d700af", "#d700d7", "#d700ff", "#d75f00", "#d75f5f",
"#d75f87", "#d75faf", "#d75fd7", "#d75fff", "#d78700", "#d7875f", "#d78787", "#d787af",
"#d787d7", "#d787ff", "#d7af00", "#d7af5f", "#d7af87", "#d7afaf", "#d7afd7", "#d7afff",
"#d7d700", "#d7d75f", "#d7d787", "#d7d7af", "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f",
"#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", "#ff0000", "#ff005f", "#ff0087", "#ff00af",
"#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", "#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff",
"#ff8700", "#ff875f", "#ff8787", "#ff87af", "#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f",
"#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", "#ffd700", "#ffd75f", "#ffd787", "#ffd7af",
"#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", "#ffff87", "#ffffaf", "#ffffd7", "#ffffff",
"#080808", "#121212", "#1c1c1c", "#262626", "#303030", "#3a3a3a", "#444444", "#4e4e4e",
"#585858", "#606060", "#666666", "#767676", "#808080", "#8a8a8a", "#949494", "#9e9e9e",
"#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6", "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee",
}
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"embed"
"encoding/json"
"io/fs"
"os"
"path/filepath"
"time"
"github.com/adrg/xdg"
)
const defaultOutputFilename = "freeze.png"
// Config is the configuration options for a screenshot.
type Config struct {
Input string `json:",omitempty" arg:"" help:"Code to screenshot." optional:""`
// Window
Background string `json:"background" help:"Apply a background fill." short:"b" placeholder:"#171717" group:"Window"`
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"`
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"`
// Settings
Version bool `json:"version" help:"Display Freeze's version." short:"v" group:"Settings"`
Config string `json:"config,omitempty" help:"Base configuration file or template." short:"c" group:"Settings" default:"default" placeholder:"base"`
Interactive bool `hidden:"" json:",omitempty" help:"Use an interactive form for configuration options." short:"i" group:"Settings"`
Language string `json:"language,omitempty" help:"Language of code file." short:"l" group:"Settings" placeholder:"go"`
Theme string `json:"theme" help:"Theme to use for syntax highlighting." short:"t" group:"Settings" placeholder:"charm"`
Wrap int `json:"wrap" help:"Wrap lines at a specific width." short:"w" group:"Settings" default:"0" placeholder:"80"`
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:""`
ExecuteTimeout time.Duration `json:"-" help:"Execution timeout." group:"Settings" default:"10s" prefix:"execute." name:"timeout" hidden:""`
// Decoration
Border Border `json:"border" embed:"" prefix:"border." group:"Border"`
Shadow Shadow `json:"shadow" embed:"" prefix:"shadow." help:"add a shadow to the window" short:"s" group:"Shadow"`
// Font
Font Font `json:"font" embed:"" prefix:"font." group:"Font"`
// Line
LineHeight float64 `json:"line_height" help:"Line height relative to font size." group:"Line" placeholder:"1.2"`
Lines []int `json:"-" help:"Lines to capture (start,end)." group:"Line" placeholder:"0,-1" value:"0,-1"`
ShowLineNumbers bool `json:"show_line_numbers" help:"" group:"Line" placeholder:"false"`
}
// Shadow is the configuration options for a drop shadow.
type Shadow struct {
Blur float64 `json:"blur" help:"Shadow Gaussian Blur." placeholder:"0"`
X float64 `json:"x" help:"Shadow offset {{x}} coordinate." placeholder:"0"`
Y float64 `json:"y" help:"Shadow offset {{y}} coordinate." placeholder:"0"`
}
// Border is the configuration options for a window border.
type Border struct {
Radius float64 `json:"radius" help:"Corner radius of window." short:"r" placeholder:"0"`
Width float64 `json:"width" help:"Border width thickness." placeholder:"1"`
Color string `json:"color" help:"Border color." placeholder:"#000"`
}
// Font is the configuration options for a font.
type Font struct {
Family string `json:"family" help:"Font family to use for code." placeholder:"monospace"`
File string `json:"file" help:"Font file to embed." placeholder:"monospace.ttf"`
Size float64 `json:"size" help:"Font size to use for code." placeholder:"14"`
Ligatures bool `json:"ligatures" help:"Use ligatures in the font." placeholder:"true" value:"true" negatable:""`
}
//go:embed configurations/*
var configs embed.FS
func expandPadding(p []float64, scale float64) []float64 {
switch len(p) {
case 1:
return []float64{p[top] * scale, p[top] * scale, p[top] * scale, p[top] * scale}
case 2:
return []float64{p[top] * scale, p[right] * scale, p[top] * scale, p[right] * scale}
case 4:
return []float64{p[top] * scale, p[right] * scale, p[bottom] * scale, p[left] * scale}
default:
return []float64{0, 0, 0, 0}
}
}
var expandMargin = expandPadding
type side int
const (
top side = 0
right side = 1
bottom side = 2
left side = 3
)
var userConfigPath = filepath.Join(xdg.ConfigHome, "freeze", "user.json")
func loadUserConfig() (fs.File, error) {
return os.Open(userConfigPath) //nolint: wrapcheck
}
func saveUserConfig(config Config) error {
config.Input = ""
config.Output = ""
config.Interactive = false
err := os.MkdirAll(filepath.Dir(userConfigPath), os.ModePerm) //nolint:gosec
if err != nil {
return err //nolint: wrapcheck
}
f, err := os.Create(userConfigPath)
if err != nil {
return err //nolint: wrapcheck
}
b, err := json.Marshal(config)
if err != nil {
return err //nolint: wrapcheck
}
_, err = f.Write(b)
printFilenameOutput(userConfigPath)
return err //nolint: wrapcheck
}
+31
View File
@@ -0,0 +1,31 @@
package main
import (
"testing"
"github.com/alecthomas/kong"
)
func TestConfig(t *testing.T) {
dir := "configurations"
entries, err := configs.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatal(entries)
}
for _, entry := range entries {
f, err := configs.Open(dir + "/" + entry.Name())
if err != nil {
t.Fatal(err)
}
_, err = kong.JSON(f)
if err != nil {
t.Fatal(err)
}
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"window": false,
"theme": "charm",
"border": {
"radius": 0,
"width": 0,
"color": "#515151"
},
"shadow": {
"blur": 0,
"x": 0,
"y": 0
},
"padding": [
20,
40,
20,
20
],
"margin": "0",
"background": "#171717",
"font": {
"family": "JetBrains Mono",
"size": 14,
"ligatures": true
},
"line_height": 1.2
}
+33
View File
@@ -0,0 +1,33 @@
{
"window": true,
"theme": "charm",
"border": {
"radius": 8,
"width": 1,
"color": "#515151"
},
"shadow": {
"blur": 24,
"x": 0,
"y": 12
},
"padding": [
20,
40,
20,
20
],
"margin": [
50,
60,
70,
60
],
"background": "#171717",
"font": {
"family": "JetBrains Mono",
"size": 14,
"ligatures": true
},
"line_height": 1.2
}
+45
View File
@@ -0,0 +1,45 @@
package main
import "strings"
func cut(input string, window []int) string {
if len(window) == 0 {
return input
}
if len(window) == 1 && window[0] == 0 {
return input
}
if len(window) == 2 && window[0] == 0 && window[1] == -1 {
return input
}
lines := strings.Split(input, "\n")
start := 0
end := len(lines)
switch len(window) {
case 1:
if window[0] > 0 {
start = window[0]
} else {
start = len(lines) + window[0] // add negative = subtract
}
case 2:
start = window[0]
end = window[1]
}
start = clamp(start, 0, len(lines))
end = clamp(end+1, start, len(lines))
if start == end && start < len(lines) {
return lines[start]
}
return strings.Join(lines[start:end], "\n")
}
func clamp(n, low, high int) int {
return min(max(n, low), high)
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"testing"
)
const test = `1 package main
2
3 import (
4 "fmt"
5 )
6
7 func main() {
8 fmt.Println("Hello World")
9 }`
func TestCut(t *testing.T) {
tests := []struct {
input string
lines []int
expected string
}{
{"", []int{0, 0}, ""},
{"Hello World", []int{0, 0}, "Hello World"},
{"Hello World", []int{0, 1}, "Hello World"},
{"Hello World", []int{0, 10}, "Hello World"},
{"Hello World", []int{10, 10}, ""},
{test, []int{0, 10}, test},
{test, []int{0, 1}, "1 package main\n2 "},
{test, []int{1, 2}, "2 \n3 import ("},
{test, []int{2, 4}, "3 import (\n4 \"fmt\"\n5 )"},
{test, []int{6}, "7 func main() {\n8 fmt.Println(\"Hello World\")\n9 }"},
{test, []int{-3}, "7 func main() {\n8 fmt.Println(\"Hello World\")\n9 }"},
{test, []int{-2}, "8 fmt.Println(\"Hello World\")\n9 }"},
{test, []int{-1}, "9 }"},
{test, []int{0, 9}, test},
{test, []int{6, 6}, "7 func main() {"},
{test, []int{7, 7}, "8 fmt.Println(\"Hello World\")"},
}
for _, test := range tests {
actual := cut(test.input, test.lines)
if actual != test.expected {
t.Log(actual)
t.Log(test.expected)
t.Errorf("cut(%s, %v)", test.input, test.lines)
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{pkgs}:
pkgs.buildGoModule {
name = "freeze";
src = ./.;
vendorHash = "sha256-AUFzxmQOb/h0UgcprY09IVI7Auitn3JTDU/ptKicIAU=";
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"fmt"
"os"
"github.com/charmbracelet/lipgloss"
)
var (
errorHeader = lipgloss.NewStyle().
Foreground(lipgloss.Color("#F1F1F1")).
Background(lipgloss.Color("#FF5F87")).
Bold(true).
Padding(0, 1).
Margin(1).
MarginLeft(2).
SetString("ERROR")
errorDetails = lipgloss.NewStyle().
Foreground(lipgloss.Color("#757575")).
MarginLeft(2)
)
func printError(title string, err error) {
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Center, errorHeader.String(), title))
fmt.Println(errorDetails.Render(err.Error()))
}
func printErrorFatal(title string, err error) {
printError(title, err)
os.Exit(1)
}
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1711460390,
"narHash": "sha256-akSgjDZL6pVHEfSE6sz1DNSXuYX6hq+P/1Z5IoYWs7E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "44733514b72e732bd49f5511bd0203dea9b9a434",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+24
View File
@@ -0,0 +1,24 @@
{
description = "A tool for generating images of code and terminal output";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
in {
packages.default = import ./default.nix {inherit pkgs;};
})
// {
overlays.default = final: prev: {
freeze = import ./default.nix {pkgs = final;};
};
};
}
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"github.com/alecthomas/chroma/v2/formatters/svg"
"github.com/charmbracelet/freeze/font"
)
func fontOptions(config *Config) ([]svg.Option, error) {
if config.Font.File != "" {
bts, err := os.ReadFile(config.Font.File)
if err != nil {
return nil, fmt.Errorf("invalid font file: %w", err)
}
var format svg.FontFormat
switch ext := filepath.Ext(config.Font.File); ext {
case ".ttf":
format = svg.TRUETYPE
case ".woff2":
format = svg.WOFF2
case ".woff":
format = svg.WOFF
default:
return nil, fmt.Errorf("%s is not a supported font extension", ext)
}
return []svg.Option{
svg.EmbedFont(
config.Font.Family,
base64.StdEncoding.EncodeToString(bts),
format,
),
svg.FontFamily(config.Font.Family),
}, nil
}
if config.Font.Family != "JetBrains Mono" {
return []svg.Option{
svg.FontFamily(config.Font.Family),
}, nil
}
config.Font.Family = "JetBrains Mono"
fontBase64 := font.JetBrainsMono
if !config.Font.Ligatures {
fontBase64 = font.JetBrainsMonoNL
}
return []svg.Option{
svg.EmbedFont(config.Font.Family, fontBase64, svg.WOFF2),
svg.FontFamily(config.Font.Family),
}, nil
}
Binary file not shown.
Binary file not shown.
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
//
// This Font Software is licensed under the SIL Open Font License, Version 1.1.
// This license is copied below, and is also available with a FAQ at:
// https://scripts.sil.org/OFL
//
//
// -----------------------------------------------------------
// SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
// -----------------------------------------------------------
//
// PREAMBLE
// The goals of the Open Font License (OFL) are to stimulate worldwide
// development of collaborative font projects, to support the font creation
// efforts of academic and linguistic communities, and to provide a free and
// open framework in which fonts may be shared and improved in partnership
// with others.
//
// The OFL allows the licensed fonts to be used, studied, modified and
// redistributed freely as long as they are not sold by themselves. The
// fonts, including any derivative works, can be bundled, embedded,
// redistributed and/or sold with any software provided that any reserved
// names are not used by derivative works. The fonts and derivatives,
// however, cannot be released under any other type of license. The
// requirement for fonts to remain under this license does not apply
// to any document created using the fonts or their derivatives.
//
// DEFINITIONS
// "Font Software" refers to the set of files released by the Copyright
// Holder(s) under this license and clearly marked as such. This may
// include source files, build scripts and documentation.
//
// "Reserved Font Name" refers to any names specified as such after the
// copyright statement(s).
//
// "Original Version" refers to the collection of Font Software components as
// distributed by the Copyright Holder(s).
//
// "Modified Version" refers to any derivative made by adding to, deleting,
// or substituting -- in part or in whole -- any of the components of the
// Original Version, by changing formats or by porting the Font Software to a
// new environment.
//
// "Author" refers to any designer, engineer, programmer, technical
// writer or other person who contributed to the Font Software.
//
// PERMISSION & CONDITIONS
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of the Font Software, to use, study, copy, merge, embed, modify,
// redistribute, and sell modified and unmodified copies of the Font
// Software, subject to the following conditions:
//
// 1) Neither the Font Software nor any of its individual components,
// in Original or Modified Versions, may be sold by itself.
//
// 2) Original or Modified Versions of the Font Software may be bundled,
// redistributed and/or sold with any software, provided that each copy
// contains the above copyright notice and this license. These can be
// included either as stand-alone text files, human-readable headers or
// in the appropriate machine-readable metadata fields within text or
// binary files as long as those fields can be easily viewed by the user.
//
// 3) No Modified Version of the Font Software may use the Reserved Font
// Name(s) unless explicit written permission is granted by the corresponding
// Copyright Holder. This restriction only applies to the primary font name as
// presented to the users.
//
// 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
// Software shall not be used to promote, endorse or advertise any
// Modified Version, except to acknowledge the contribution(s) of the
// Copyright Holder(s) and the Author(s) or with their explicit written
// permission.
//
// 5) The Font Software, modified or unmodified, in part or in whole,
// must be distributed entirely under this license, and must not be
// distributed under any other license. The requirement for fonts to
// remain under this license does not apply to any document created
// using the Font Software.
//
// TERMINATION
// This license becomes null and void if any of the above conditions are
// not met.
//
// DISCLAIMER
// THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
// OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
// DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
// OTHER DEALINGS IN THE FONT SOFTWARE.
package font //nolint:revive
import (
_ "embed"
"encoding/base64"
)
// JetBrainsMonoTTF contains the embedded JetBrainsMono-Regular.ttf font.
//
//go:embed JetBrainsMono-Regular.ttf
var JetBrainsMonoTTF []byte
// JetBrainsMonoNLTTF contains the embedded JetBrainsMonoNL-Regular.ttf font.
//
//go:embed JetBrainsMonoNL-Regular.ttf
var JetBrainsMonoNLTTF []byte
var (
// JetBrainsMono font.
JetBrainsMono = base64.StdEncoding.EncodeToString(JetBrainsMonoTTF)
// JetBrainsMonoNL font.
JetBrainsMonoNL = base64.StdEncoding.EncodeToString(JetBrainsMonoNLTTF)
)
+350
View File
@@ -0,0 +1,350 @@
package main
import (
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"testing"
"github.com/aymanbagabas/go-udiff"
)
var binary = "./test/freeze-test"
func init() {
if runtime.GOOS == "windows" {
binary += ".exe"
}
}
var (
update = flag.Bool("update", false, "update golden files")
png = flag.Bool("png", false, "update pngs")
)
func TestMain(m *testing.M) {
flag.Parse()
cmd := exec.Command("go", "build", "-o", binary)
err := cmd.Run()
if err != nil {
fmt.Println(err)
}
exit := m.Run()
err = os.Remove(binary)
if err != nil {
fmt.Println(err)
}
os.Exit(exit)
}
func TestFreeze(t *testing.T) {
cmd := exec.Command(binary)
err := cmd.Run()
if err != nil {
t.Fatal(err)
}
}
func TestFreezeOutput(t *testing.T) {
output := "artichoke-test.svg"
defer os.Remove(output)
cmd := exec.Command(binary, "test/input/artichoke.hs", "-o", output)
err := cmd.Run()
if err != nil {
t.Fatal(err)
}
_, err = os.Stat(output)
if err != nil {
t.Fatal(err)
}
}
func TestFreezeHelp(t *testing.T) {
out := bytes.Buffer{}
cmd := exec.Command(binary)
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
t.Fatal("unexpected error")
}
got := out.String()
contains := []string{
"Generate images of code and terminal output.",
"freeze main.go [-o code.svg] [--flags]",
"--theme", "Theme to use for syntax highlighting",
"--border.color", "Border color.",
"--shadow.blur", "Shadow Gaussian Blur.",
"--font.family", "Font family to use for code.",
}
for _, c := range contains {
if !strings.Contains(got, c) {
t.Fatalf("expected %s to contain \"%s\"", got, c)
}
}
}
func TestFreezeErrorFileMissing(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this fails on windows for some reason")
}
out := bytes.Buffer{}
cmd := exec.Command(binary, "this-file-does-not-exist")
cmd.Stdout = &out
err := cmd.Run()
if err == nil {
t.Fatal("expected error")
}
got := out.String()
contains := []string{"ERROR", "File not found", "open this-file-does-not-exist: no such file or directory"}
for _, c := range contains {
if !strings.Contains(got, c) {
t.Fatalf("expected %s to contain \"%s\"", got, c)
}
}
}
func TestFreezeConfigurations(t *testing.T) {
tests := []struct {
input string
flags []string
output string
}{
{
input: "test/input/artichoke.hs",
flags: []string{"--config", "test/configurations/base.json"},
output: "artichoke-base",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--config", "test/configurations/full.json"},
output: "artichoke-full",
},
{
input: "test/input/eza.ansi",
flags: []string{"--config", "full"},
output: "eza",
},
{
flags: []string{"--execute", `echo "Hello, world!"`},
output: "execute",
},
{
input: "test/input/bubbletea.model",
flags: []string{"--language", "go", "--height", "800", "--width", "750", "--config", "full", "--window=false", "--show-line-numbers"},
output: "bubbletea",
},
{
input: "test/input/layout.ansi",
flags: []string{},
output: "layout",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--language", "haskell"},
output: "haskell",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--theme", "dracula"},
output: "dracula",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--border.radius", "8"},
output: "border-radius",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--border.radius", "8", "--window"},
output: "window",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--border.radius", "8", "--window", "--border.width", "1"},
output: "border-width",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--border.radius", "8", "--window", "--border.width", "1", "--padding", "30,50,30,30"},
output: "padding",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--border.radius", "8", "--window", "--border.width", "1", "--padding", "30,50,30,30", "--margin", "50,60,100,60"},
output: "margin",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--config", "full"},
output: "shadow",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--width", "1920", "--height", "1080"},
output: "dimensions",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--margin", "50", "--width", "600", "--height", "300"},
output: "dimensions-margin",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--margin", "50", "--width", "600", "--height", "300", "--show-line-numbers"},
output: "dimensions-margin-line-numbers",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--padding", "50", "--width", "600", "--height", "300"},
output: "dimensions-padding",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--config", "full", "--width", "600", "--height", "300"},
output: "dimensions-config",
},
{
input: "test/input/goreleaser-full.yml",
flags: []string{"--config", "full", "--width", "600", "--height", "900"},
output: "overflow",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--config", "full", "--lines", "4,8", "--show-line-numbers"},
output: "lines",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--font.size", "28"},
output: "font-size-28",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--font.size", "14"},
output: "font-size-14",
},
{
input: "test/input/artichoke.hs",
flags: []string{"--line-height", "2"},
output: "line-height-2",
},
{
input: "test/input/goreleaser-full.yml",
flags: []string{"--config", "full", "--height", "2000", "--show-line-numbers"},
output: "overflow-line-numbers",
},
{
input: "test/input/helix.ansi",
flags: []string{"--background", "#0d1116"},
output: "helix",
},
{
input: "test/input/glow.ansi",
flags: []string{},
output: "glow",
},
{
input: "test/input/tab.go",
flags: []string{},
output: "tab",
},
{
input: "test/input/wrap.go",
flags: []string{"--wrap", "80", "--width", "600"},
output: "wrap",
},
}
err := os.RemoveAll("test/output/svg")
if err != nil {
t.Fatal("unable to remove output files")
}
err = os.MkdirAll("test/output/svg", 0o755)
if err != nil {
t.Fatal("unable to create output directory")
}
err = os.MkdirAll("test/golden/svg", 0o755)
if err != nil {
t.Fatal("unable to create output directory")
}
err = os.MkdirAll("test/output/png", 0o755)
if err != nil {
t.Fatal("unable to create output directory")
}
for _, tc := range tests {
t.Run(tc.output, func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this fails on windows for some reason")
}
// output SVG
out := bytes.Buffer{}
args := []string{tc.input}
args = append(args, tc.flags...)
args = append(args, "--output", "test/output/svg/"+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/output/svg/" + tc.output + ".svg"
got, err := os.ReadFile(gotfile)
if err != nil {
t.Fatal("no output file for:", gotfile)
}
goldenfile := "test/golden/svg/" + 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 normalizeNewlines(want) != normalizeNewlines(got) {
t.Log(udiff.Unified("want", "got", normalizeNewlines(want), normalizeNewlines(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/output/png/"+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")
}
}
})
}
}
func normalizeNewlines[T string | []byte](s T) string {
return strings.ReplaceAll(string(s), "\r\n", "\n")
}
+55
View File
@@ -0,0 +1,55 @@
module github.com/charmbracelet/freeze
go 1.25.8
require (
github.com/adrg/xdg v0.5.3
github.com/alecthomas/chroma/v2 v2.24.1
github.com/alecthomas/kong v1.15.0
github.com/aymanbagabas/go-udiff v0.4.1
github.com/beevik/etree v1.6.0
github.com/caarlos0/go-shellwords v1.0.12
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/charmbracelet/x/ansi v0.11.7
github.com/charmbracelet/x/cellbuf v0.0.15
github.com/charmbracelet/x/term v0.2.2
github.com/charmbracelet/x/xpty v0.1.3
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-runewidth v0.0.23
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/conpty v0.1.1 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
+113
View File
@@ -0,0 +1,113 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
github.com/caarlos0/go-shellwords v1.0.12 h1:HWrUnu6lGbWfrDcFiHcZiwOLzHWjjrPVehULaTFgPp8=
github.com/caarlos0/go-shellwords v1.0.12/go.mod h1:bYeeX1GrTLPl5cAMYEzdm272qdsQAZiaHgeF0KTk1Gw=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5 h1:rIt3LGU1yOC7U48eZjaAtjdzuSjH6Y0GA1KsRN7wqn8=
github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5 h1:73C9VsX8PMlXxVMKjg7ix67cZWg+zySdyzWRaXS239A=
github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY=
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5/go.mod h1:c9+VS9GaommgIOzNWb5ze4lYwfT8BZ2UDyGiuQTT7yc=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"fmt"
"regexp"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/lipgloss"
)
const space = 18
var highlighter = regexp.MustCompile("{{(.+?)}}")
func helpPrinter(_ kong.HelpOptions, ctx *kong.Context) error {
codeBlockStyle := lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"}).MarginLeft(2).Padding(1, 2)
programStyle := lipgloss.NewStyle().Background(codeBlockStyle.GetBackground()).Foreground(lipgloss.Color("#7E65FF")).PaddingLeft(1)
stringStyle := lipgloss.NewStyle().Background(codeBlockStyle.GetBackground()).Foreground(lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}).PaddingLeft(1)
argumentStyle := lipgloss.NewStyle().Background(codeBlockStyle.GetBackground()).Foreground(lipgloss.Color("248")).PaddingLeft(1)
flagStyle := lipgloss.NewStyle().Background(codeBlockStyle.GetBackground()).Foreground(lipgloss.Color("244")).PaddingLeft(1)
titleStyle := lipgloss.NewStyle().Bold(true).Transform(strings.ToUpper).Margin(1, 0, 0, 2).Foreground(lipgloss.Color("#6C50FF"))
fmt.Println()
fmt.Println(" Generate images of code and terminal output. 📸")
fmt.Println(titleStyle.Render(strings.ToUpper("Usage")))
fmt.Println()
fmt.Println(
codeBlockStyle.Render(
lipgloss.JoinVertical(
lipgloss.Top,
lipgloss.JoinHorizontal(lipgloss.Left, programStyle.Render("freeze"), argumentStyle.Render("main.go"), flagStyle.Render("[-o code.svg] [--flags]")),
lipgloss.JoinHorizontal(lipgloss.Left, programStyle.Render("freeze"), argumentStyle.Render("--execute"), stringStyle.Render("\"ls -la\""), flagStyle.Render("[--flags] ")),
),
),
)
flags := ctx.Flags()
lastGroup := ""
fmt.Println()
for _, f := range flags {
if f.Name == "interactive" {
printFlag(f)
}
}
fmt.Println(titleStyle.Render("Settings"))
for _, f := range flags {
if f.Group != nil && f.Group.Title == "Settings" {
if f.Hidden || f.Name == "help" {
continue
}
printFlag(f)
}
}
fmt.Print(titleStyle.Render("Customization"))
for _, f := range flags {
if f.Hidden || f.Name == "help" || f.Group.Title == "Settings" {
continue
}
if f.Group != nil && lastGroup != f.Group.Title {
lastGroup = f.Group.Title
fmt.Println()
}
printFlag(f)
}
fmt.Println()
return nil
}
const helpForeground = "243"
func printFlag(f *kong.Flag) {
dashStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).MarginLeft(1)
helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(helpForeground))
keywordStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
if f.Short > 0 {
fmt.Print(" ", dashStyle.Render("-"), string(f.Short))
fmt.Print(dashStyle.Render("--"), f.Name)
fmt.Print(strings.Repeat(" ", space-len(f.Name)))
} else {
fmt.Print(" ", dashStyle.Render(" "), " ")
fmt.Print(dashStyle.Render("--"), f.Name)
fmt.Print(strings.Repeat(" ", space-len(f.Name)))
}
help := highlighter.ReplaceAllString(f.Help, keywordStyle.Render("$1")+"\x1b[38;5;"+helpForeground+"m")
fmt.Println(helpStyle.Render(help))
}
+27
View File
@@ -0,0 +1,27 @@
package input //nolint:revive
import (
"io"
"os"
)
// ReadFile returns the files content.
func ReadFile(file string) (string, error) {
b, err := os.ReadFile(file)
return string(b), err
}
// ReadInput reads some input.
func ReadInput(in io.Reader) (string, error) {
b, err := io.ReadAll(in)
return string(b), err
}
// IsPipe returns whether the stdin is a pipe.
func IsPipe(in *os.File) bool {
stat, err := in.Stat()
if err != nil {
return false
}
return (stat.Mode() & os.ModeCharDevice) == 0
}
+268
View File
@@ -0,0 +1,268 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
var green = lipgloss.Color("#03BF87")
func runForm(config *Config) (*Config, error) {
var (
padding = strings.Trim(fmt.Sprintf("%v", config.Padding), "[]")
margin = strings.Trim(fmt.Sprintf("%v", config.Margin), "[]")
fontSize = fmt.Sprintf("%d", int(config.Font.Size))
lineHeight = fmt.Sprintf("%.1f", config.LineHeight)
borderRadius = fmt.Sprintf("%.0f", config.Border.Radius)
borderWidth = fmt.Sprintf("%.0f", config.Border.Width)
shadowBlur = fmt.Sprintf("%.0f", config.Shadow.Blur)
shadowX = fmt.Sprintf("%.0f", config.Shadow.X)
shadowY = fmt.Sprintf("%.0f", config.Shadow.Y)
)
theme := huh.ThemeCharm()
theme.FieldSeparator = lipgloss.NewStyle()
theme.Blurred.TextInput.Text = theme.Blurred.TextInput.Text.Foreground(lipgloss.Color("243"))
theme.Blurred.BlurredButton = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).PaddingRight(1)
theme.Blurred.FocusedButton = lipgloss.NewStyle().Foreground(lipgloss.Color("7")).PaddingRight(1)
theme.Focused.BlurredButton = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).PaddingRight(1)
theme.Focused.FocusedButton = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).PaddingRight(1)
theme.Focused.NoteTitle = theme.Focused.NoteTitle.Margin(1, 0)
theme.Blurred.NoteTitle = theme.Blurred.NoteTitle.Margin(1, 0)
theme.Blurred.Description = theme.Blurred.Description.Foreground(lipgloss.Color("0"))
theme.Focused.Description = theme.Focused.Description.Foreground(lipgloss.Color("7"))
theme.Blurred.Title = theme.Blurred.Title.Width(18).Foreground(lipgloss.Color("7"))
theme.Focused.Title = theme.Focused.Title.Width(18).Foreground(green).Bold(true)
theme.Blurred.SelectedOption = theme.Blurred.SelectedOption.Foreground(lipgloss.Color("243"))
theme.Focused.SelectedOption = lipgloss.NewStyle().Foreground(green)
theme.Focused.Base.BorderForeground(green)
f := huh.NewForm(
huh.NewGroup(
huh.NewNote().Title("\nCapture file"),
huh.NewFilePicker().
Title("").
Picking(true).
Height(10).
Value(&config.Input),
huh.NewNote().Description("Choose a code file to screenshot."),
).WithHide(config.Input != "" || config.Execute != ""),
huh.NewGroup(
huh.NewNote().Title("Settings"),
huh.NewInput().
Title("Output").
Placeholder(defaultOutputFilename).
// Description("Output location for image.").
Inline(true).
Prompt("").
Value(&config.Output),
huh.NewSelect[string]().Title("Theme ").
// Description("Theme for syntax highlighting.").
Inline(true).
Options(huh.NewOptions(styles.Names()...)...).
Value(&config.Theme),
// huh.NewInput().Title("Background ").
// // Description("Apply a background fill.").
// Placeholder("#FFF").
// Value(&config.Background).
// Inline(true).
// Prompt("").
// Validate(validateColor),
huh.NewNote().Title("Window"),
huh.NewInput().Title("Padding ").
// Description("Apply padding to the code.").
Placeholder("20 40").
Inline(true).
Value(&padding).
Prompt("").
Validate(validatePadding),
huh.NewInput().Title("Margin ").
// Description("Apply margin to the window.").
Placeholder("20").
Inline(true).
Value(&margin).
Prompt("").
Validate(validatePadding),
huh.NewConfirm().Title("Controls").
Inline(true).
Value(&config.Window),
huh.NewNote().Title("Font"),
huh.NewInput().Title("Font Family ").
// Description("Font family to use for code").
Placeholder("JetBrains Mono").
Inline(true).
Prompt("").
Value(&config.Font.Family),
huh.NewInput().Title("Font Size ").
// Description("Font size to use for code.").
Placeholder("14").
Inline(true).
Prompt("").
Value(&fontSize).
Validate(validateInteger),
huh.NewInput().Title("Line Height ").
// Description("Line height relative to size.").
Placeholder("1.2").
Inline(true).
Prompt("").
Value(&lineHeight).
Validate(validateFloat),
huh.NewNote().Title("Border"),
huh.NewInput().Title("Border Radius ").
// Description("Corner radius of the window.").
Placeholder("0").
Inline(true).
Prompt("").
Value(&borderRadius).
Validate(validateInteger),
huh.NewInput().Title("Border Width ").
// Description("Border width thickness.").
Placeholder("1").
Inline(true).
Prompt("").
Value(&borderWidth).
Validate(validateInteger),
huh.NewInput().Title("Border Color ").
// Description("Color of outline stroke.").
Validate(validateColor).
Inline(true).
Prompt("").
Value(&config.Border.Color).
Placeholder("#515151"),
huh.NewNote().Title("Shadow"),
huh.NewInput().Title("Blur ").
// Description("Shadow Gaussian Blur.").
Placeholder("0").
Inline(true).
Prompt("").
Value(&shadowBlur).
Validate(validateInteger),
huh.NewInput().Title("X Offset ").
// Description("Shadow offset x coordinate").
Placeholder("0").
Inline(true).
Prompt("").
Value(&shadowX).
Validate(validateInteger),
huh.NewInput().Title("Y Offset ").
// Description("Shadow offset y coordinate").
Placeholder("0").
Inline(true).
Prompt("").
Value(&shadowY).
Validate(validateInteger),
).WithHeight(33),
).WithTheme(theme).WithWidth(40)
err := f.Run()
if config.Output == "" {
config.Output = defaultOutputFilename
}
config.Padding = parsePadding(padding)
config.Margin = parseMargin(margin)
config.Font.Size, _ = strconv.ParseFloat(fontSize, 64)
config.LineHeight, _ = strconv.ParseFloat(lineHeight, 64)
config.Border.Radius, _ = strconv.ParseFloat(borderRadius, 64)
config.Border.Width, _ = strconv.ParseFloat(borderWidth, 64)
config.Shadow.Blur, _ = strconv.ParseFloat(shadowBlur, 64)
config.Shadow.X, _ = strconv.ParseFloat(shadowX, 64)
config.Shadow.Y, _ = strconv.ParseFloat(shadowY, 64)
return config, err //nolint: wrapcheck
}
func validateMargin(s string) error {
tokens := strings.Fields(s)
if len(tokens) > 4 {
return errors.New("maximum four values")
}
for _, t := range tokens {
_, err := strconv.Atoi(t)
if err != nil {
return errors.New("must be valid space-separated integers")
}
}
return nil
}
func validatePadding(s string) error {
return validateMargin(s)
}
func validateInteger(s string) error {
if len(s) <= 0 {
return nil
}
_, err := strconv.Atoi(s)
if err != nil {
return errors.New("must be valid integer")
}
return nil
}
func validateFloat(s string) error {
if len(s) <= 0 {
return nil
}
_, err := strconv.ParseFloat(s, 64)
if err != nil {
return errors.New("must be valid float")
}
return nil
}
var colorRegex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
func validateColor(s string) error {
if len(s) <= 0 {
return nil
}
if !colorRegex.MatchString(s) {
return errors.New("must be valid color")
}
return nil
}
func parsePadding(v string) []float64 {
//nolint: prealloc
var values []float64
for _, p := range strings.Fields(v) {
pi, _ := strconv.ParseFloat(p, 64) // already validated
values = append(values, pi)
}
return expandPadding(values, 1)
}
var parseMargin = parsePadding
+456
View File
@@ -0,0 +1,456 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/alecthomas/chroma/v2"
formatter "github.com/alecthomas/chroma/v2/formatters/svg"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/alecthomas/kong"
"github.com/beevik/etree"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/cellbuf"
"github.com/mattn/go-isatty"
in "github.com/charmbracelet/freeze/input"
"github.com/charmbracelet/freeze/svg"
)
const (
defaultFontSize = 14.0
defaultLineHeight = 1.2
)
var (
// Version contains the application version number. It's set via ldflags
// when building.
Version = ""
// CommitSHA contains the SHA of the commit that this application was built
// against. It's set via ldflags when building.
CommitSHA = ""
)
func main() {
const shaLen = 7
var (
input string
err error
lexer chroma.Lexer
config Config
scale float64
)
k, err := kong.New(&config, kong.Help(helpPrinter))
if err != nil {
printErrorFatal("Something went wrong", err)
}
ctx, err := k.Parse(os.Args[1:])
if err != nil || ctx.Error != nil {
printErrorFatal("Invalid Usage", err)
}
if config.Version {
if Version == "" {
info, ok := debug.ReadBuildInfo()
if ok && info.Main.Sum != "" {
Version = info.Main.Version
} else {
Version = "unknown (built from source)"
}
}
version := fmt.Sprintf("freeze version %s", Version)
if len(CommitSHA) >= shaLen {
version += " (" + CommitSHA[:shaLen] + ")"
}
fmt.Println(version)
os.Exit(0)
}
// Copy the pty output to buffer
if config.Execute != "" {
input, err = executeCommand(config)
if err != nil {
if input != "" {
err = fmt.Errorf("%w\n%s", err, input)
}
printErrorFatal("Something went wrong", err)
}
if input == "" {
printErrorFatal("Something went wrong", errors.New("no command output"))
}
}
isDefaultConfig := config.Config == "default"
configFile, err := configs.Open("configurations/" + config.Config + ".json")
if config.Config == "user" {
configFile, err = loadUserConfig()
}
if err != nil {
configFile, err = os.Open(config.Config)
}
if err != nil {
configFile, _ = configs.Open("configurations/base.json")
}
r, err := kong.JSON(configFile)
if err != nil {
printErrorFatal("Invalid JSON", err)
}
k, err = kong.New(&config, kong.Help(helpPrinter), kong.Resolvers(r))
if err != nil {
printErrorFatal("Something went wrong", err)
}
ctx, err = k.Parse(os.Args[1:])
if err != nil {
printErrorFatal("Invalid Usage", err)
}
if config.Interactive {
cfg, interactiveErr := runForm(&config)
config = *cfg
if interactiveErr != nil {
printErrorFatal("", interactiveErr)
}
if isDefaultConfig {
_ = saveUserConfig(*cfg)
}
}
autoHeight := config.Height == 0
autoWidth := config.Width == 0
if config.Output == "" {
config.Output = defaultOutputFilename
}
scale = 1
if autoHeight && autoWidth && strings.HasSuffix(config.Output, ".png") {
scale = 4
}
config.Margin = expandMargin(config.Margin, scale)
config.Padding = expandPadding(config.Padding, scale)
if config.Input == "" && !in.IsPipe(os.Stdin) && len(ctx.Args) <= 0 {
_ = helpPrinter(kong.HelpOptions{}, ctx)
os.Exit(0)
}
if config.Input == "-" || in.IsPipe(os.Stdin) {
input, err = in.ReadInput(os.Stdin)
lexer = lexers.Analyse(input)
} else if config.Execute != "" {
config.Language = "ansi"
} else {
input, err = in.ReadFile(config.Input)
if err != nil {
printErrorFatal("File not found", err)
}
lexer = lexers.Get(config.Input)
}
if config.Language != "" {
lexer = lexers.Get(config.Language)
}
// adjust for 1-indexing
for i := range config.Lines {
config.Lines[i]--
}
strippedInput := ansi.Strip(input)
isAnsi := strings.ToLower(config.Language) == "ansi" || strippedInput != input
strippedInput = cut(strippedInput, config.Lines)
// wrap to character limit.
if config.Wrap > 0 {
strippedInput = cellbuf.Wrap(strippedInput, config.Wrap, "")
input = cellbuf.Wrap(input, config.Wrap, "")
}
if !isAnsi && lexer == nil {
printErrorFatal("Language Unknown", errors.New("specify a language with the --language flag"))
}
input = cut(input, config.Lines)
if input == "" {
if err != nil {
printErrorFatal("No input", err)
} else {
printErrorFatal("No input", errors.New("check --lines is within bounds"))
}
}
s, ok := styles.Registry[strings.ToLower(config.Theme)]
if s == nil || !ok {
s = charmStyle
}
if !s.Has(chroma.Background) {
s, err = s.Builder().Add(chroma.Background, "bg:"+config.Background).Build()
if err != nil {
printErrorFatal("Could not add background", err)
}
}
// Create a token iterator.
var it chroma.Iterator
if isAnsi {
// For ANSI output, we'll inject our own SVG. For now, let's just strip the ANSI
// codes and print the text to properly size the input.
it = chroma.Literator(chroma.Token{Type: chroma.Text, Value: strippedInput})
} else {
it, err = chroma.Coalesce(lexer).Tokenise(nil, input)
if err != nil {
printErrorFatal("Could not lex file", err)
}
}
// Format the code to an SVG.
options, err := fontOptions(&config)
if err != nil {
printErrorFatal("Invalid font options", err)
}
f := formatter.New(options...)
if err != nil {
printErrorFatal("Malformed text", err)
}
buf := &bytes.Buffer{}
err = f.Format(buf, s, it)
if err != nil {
log.Fatal(err)
}
// Parse SVG (XML document)
doc := etree.NewDocument()
_, err = doc.ReadFrom(buf)
if err != nil {
printErrorFatal("Bad SVG", err)
}
elements := doc.ChildElements()
if len(elements) < 1 {
printErrorFatal("Bad Output", nil)
}
image := elements[0]
hPadding := config.Padding[left] + config.Padding[right]
hMargin := config.Margin[left] + config.Margin[right]
vMargin := config.Margin[top] + config.Margin[bottom]
vPadding := config.Padding[top] + config.Padding[bottom]
terminal := image.SelectElement("rect")
w, h := svg.GetDimensions(image)
imageWidth := float64(w)
imageHeight := float64(h)
imageWidth *= scale
imageHeight *= scale
// chroma automatically calculates the height based on a font size of 14
// and a line height of 1.2
imageHeight *= (config.Font.Size / defaultFontSize)
imageHeight *= (config.LineHeight / defaultLineHeight)
terminalWidth := imageWidth
terminalHeight := imageHeight
if !autoWidth {
imageWidth = config.Width
terminalWidth = config.Width - hMargin
} else {
imageWidth += hMargin + hPadding
terminalWidth += hPadding
}
if !autoHeight {
imageHeight = config.Height
terminalHeight = config.Height - vMargin
} else {
imageHeight += vMargin + vPadding
terminalHeight += vPadding
}
if config.Window {
windowControls := svg.NewWindowControls(5.5*float64(scale), 19.0*scale, 12.0*scale)
svg.Move(windowControls, float64(config.Margin[left]), float64(config.Margin[top]))
image.AddChild(windowControls)
config.Padding[top] += (15 * scale)
}
if config.Border.Radius > 0 {
svg.AddCornerRadius(terminal, config.Border.Radius*scale)
}
if config.Shadow.Blur > 0 || config.Shadow.X > 0 || config.Shadow.Y > 0 {
id := "shadow"
svg.AddShadow(image, id, config.Shadow.X*scale, config.Shadow.Y*scale, config.Shadow.Blur*scale)
terminal.CreateAttr("filter", fmt.Sprintf("url(#%s)", id))
}
textGroup := image.SelectElement("g")
textGroup.CreateAttr("font-size", fmt.Sprintf("%.2fpx", config.Font.Size*float64(scale)))
textGroup.CreateAttr("clip-path", "url(#terminalMask)")
text := textGroup.SelectElements("text")
d := dispatcher{lines: text, svg: textGroup, config: &config, scale: scale}
offsetLine := 0
if len(config.Lines) > 0 {
offsetLine = config.Lines[0]
}
config.LineHeight *= float64(scale)
for i, line := range text {
if isAnsi {
line.SetText("")
}
// Offset the text by padding...
// (x, y) -> (x+p, y+p)
if config.ShowLineNumbers {
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])
svg.Move(line, x, y)
// We are passed visible lines, remove the rest.
if y > float64(imageHeight-config.Margin[bottom]-config.Padding[bottom]) {
textGroup.RemoveChild(line)
}
}
if autoWidth {
tabWidth := 4
if isAnsi {
tabWidth = 6
}
longestLine := lipgloss.Width(strings.ReplaceAll(strippedInput, "\t", strings.Repeat(" ", tabWidth)))
terminalWidth = float64(longestLine+1) * (config.Font.Size / fontHeightToWidthRatio)
terminalWidth *= scale
terminalWidth += hPadding
imageWidth = terminalWidth + hMargin
}
if config.Border.Width > 0 {
svg.AddOutline(terminal, config.Border.Width, config.Border.Color)
// NOTE: necessary so that we don't clip the outline.
terminalHeight -= (config.Border.Width * 2)
terminalWidth -= (config.Border.Width * 2)
}
if config.ShowLineNumbers {
if autoWidth {
terminalWidth += config.Font.Size * 3 * scale
imageWidth += config.Font.Size * 3 * scale
} else {
terminalWidth -= config.Font.Size * 3
}
}
if !autoHeight || !autoWidth {
svg.AddClipPath(image, "terminalMask",
config.Margin[left], config.Margin[top],
terminalWidth, terminalHeight-config.Padding[bottom])
}
svg.Move(terminal, max(float64(config.Margin[left]), float64(config.Border.Width)/2), max(float64(config.Margin[top]), float64(config.Border.Width)/2))
svg.SetDimensions(image, imageWidth, imageHeight)
svg.SetDimensions(terminal, terminalWidth, terminalHeight)
if isAnsi {
parser := ansi.NewParser()
parser.SetHandler(ansi.Handler{
Print: d.Print,
HandleCsi: d.CsiDispatch,
Execute: d.Execute,
})
for _, line := range strings.Split(input, "\n") {
parser.Parse([]byte(line))
d.Execute(ansi.LF) // simulate a newline
}
}
istty := isatty.IsTerminal(os.Stdout.Fd())
switch {
case strings.HasSuffix(config.Output, ".png"):
// use libsvg conversion.
svgConversionErr := libsvgConvert(doc, imageWidth, imageHeight, config.Output)
if svgConversionErr == nil {
printFilenameOutput(config.Output)
break
}
// could not convert with libsvg, try resvg
svgConversionErr = resvgConvert(doc, imageWidth, imageHeight, config.Output)
if svgConversionErr != nil {
printErrorFatal("Unable to convert SVG to PNG", svgConversionErr)
}
printFilenameOutput(config.Output)
default:
// output file specified.
if config.Output != "" {
err = doc.WriteToFile(config.Output)
if err != nil {
printErrorFatal("Unable to write output", err)
}
printFilenameOutput(config.Output)
return
}
// reading from stdin.
if config.Input == "" || config.Input == "-" {
if istty {
err = doc.WriteToFile(defaultOutputFilename)
printFilenameOutput(defaultOutputFilename)
} else {
_, err = doc.WriteTo(os.Stdout)
}
if err != nil {
printErrorFatal("Unable to write output", err)
}
return
}
// reading from file.
if istty {
config.Output = strings.TrimSuffix(filepath.Base(config.Input), filepath.Ext(config.Input)) + ".svg"
err = doc.WriteToFile(config.Output)
printFilenameOutput(config.Output)
} else {
_, err = doc.WriteTo(os.Stdout)
}
if err != nil {
printErrorFatal("Unable to write output", err)
}
}
}
var outputHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Background(lipgloss.Color("#6C50FF")).Bold(true).Padding(0, 1).MarginRight(1).SetString("WROTE")
func printFilenameOutput(filename string) {
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Center, outputHeader.String(), filename))
}
+98
View File
@@ -0,0 +1,98 @@
package main
import (
"bytes"
"context"
"os"
"os/exec"
"github.com/beevik/etree"
"github.com/charmbracelet/freeze/font"
"github.com/kanrichan/resvg-go"
)
func libsvgConvert(doc *etree.Document, _, _ float64, output string) error {
_, err := exec.LookPath("rsvg-convert")
if err != nil {
return err //nolint: wrapcheck
}
svg, err := doc.WriteToBytes()
if err != nil {
return err //nolint: wrapcheck
}
// rsvg-convert is installed use that to convert the SVG to PNG,
// since it is faster.
rsvgConvert := exec.Command("rsvg-convert", "-o", output)
rsvgConvert.Stdin = bytes.NewReader(svg)
err = rsvgConvert.Run()
return err //nolint: wrapcheck
}
func resvgConvert(doc *etree.Document, w, h float64, output string) error {
svg, err := doc.WriteToBytes()
if err != nil {
return err //nolint: wrapcheck
}
worker, err := resvg.NewDefaultWorker(context.Background())
if err != nil {
printErrorFatal("Unable to write output", err)
}
defer worker.Close() //nolint: errcheck
fontdb, err := worker.NewFontDBDefault()
if err != nil {
printErrorFatal("Unable to write output", err)
}
defer fontdb.Close() //nolint: errcheck
err = fontdb.LoadFontData(font.JetBrainsMonoTTF)
if err != nil {
printErrorFatal("Unable to load font", err)
}
err = fontdb.LoadFontData(font.JetBrainsMonoNLTTF)
if err != nil {
printErrorFatal("Unable to load font", err)
}
pixmap, err := worker.NewPixmap(uint32(w), uint32(h))
if err != nil {
printError("Unable to write output", err)
os.Exit(1)
}
defer pixmap.Close() //nolint: errcheck
tree, err := worker.NewTreeFromData(svg, &resvg.Options{
Dpi: 192,
ShapeRenderingMode: resvg.ShapeRenderingModeGeometricPrecision,
TextRenderingMode: resvg.TextRenderingModeOptimizeLegibility,
ImageRenderingMode: resvg.ImageRenderingModeOptimizeQuality,
DefaultSizeWidth: float32(w),
DefaultSizeHeight: float32(h),
})
if err != nil {
printError("Unable to write output", err)
os.Exit(1)
}
defer tree.Close() //nolint: errcheck
err = tree.ConvertText(fontdb)
if err != nil {
return err //nolint: wrapcheck
}
err = tree.Render(resvg.TransformIdentity(), pixmap)
if err != nil {
return err //nolint: wrapcheck
}
png, err := pixmap.EncodePNG()
if err != nil {
return err //nolint: wrapcheck
}
err = os.WriteFile(output, png, 0o600)
if err != nil {
return err //nolint: wrapcheck
}
return err //nolint: wrapcheck
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"github.com/caarlos0/go-shellwords"
"github.com/charmbracelet/x/term"
"github.com/charmbracelet/x/xpty"
)
func executeCommand(config Config) (string, error) {
args, err := shellwords.Parse(config.Execute)
if err != nil {
return "", fmt.Errorf("could not execute: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), config.ExecuteTimeout)
defer cancel()
width, height, err := term.GetSize(os.Stdout.Fd())
if err != nil {
width = 80
height = 24
}
pty, err := xpty.NewPty(width, height)
if err != nil {
return "", fmt.Errorf("could not execute: %w", err)
}
defer func() { _ = pty.Close() }()
cmd := exec.CommandContext(ctx, args[0], args[1:]...) //nolint: gosec
if err := pty.Start(cmd); err != nil {
return "", fmt.Errorf("could not execute: %w", err)
}
var out bytes.Buffer
var errorOut bytes.Buffer
go func() {
_, _ = io.Copy(&out, pty)
errorOut.Write(out.Bytes())
}()
if err := xpty.WaitProcess(ctx, cmd); err != nil {
return errorOut.String(), fmt.Errorf("could not execute: %w", err)
}
return out.String(), nil
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.assets.fonts.read-only</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/styles"
)
var charmStyle = styles.Register(chroma.MustNewStyle("charm", chroma.StyleEntries{
chroma.Text: "#C4C4C4",
chroma.Error: "#F1F1F1 bg:#F05B5B",
chroma.Comment: "#676767",
chroma.CommentPreproc: "#FF875F",
chroma.Keyword: "#00AAFF",
chroma.KeywordReserved: "#FF48DD",
chroma.KeywordNamespace: "#FF5F87",
chroma.KeywordType: "#635ADF",
chroma.Operator: "#FF7F83",
chroma.Punctuation: "#E8E8A8",
chroma.Name: "#C4C4C4",
chroma.NameBuiltin: "#FF7CDB",
chroma.NameTag: "#B083EA",
chroma.NameAttribute: "#7A7AE6",
chroma.NameClass: "#F1F1F1 underline bold",
chroma.NameDecorator: "#FFFF87",
chroma.NameFunction: "#00DC7F",
chroma.LiteralNumber: "#6EEFC0",
chroma.LiteralString: "#E38356",
chroma.LiteralStringEscape: "#AFFFD7",
chroma.GenericDeleted: "#FD5B5B",
chroma.GenericEmph: "italic",
chroma.GenericInserted: "#00D787",
chroma.GenericStrong: "bold",
chroma.GenericSubheading: "#777777",
// chroma.Background: "bg:#171717",
}))
+121
View File
@@ -0,0 +1,121 @@
package svg //nolint:revive
import (
"fmt"
"strconv"
"strings"
"github.com/beevik/etree"
)
// AddShadow adds a definition of a shadow to the <defs> with the given id.
func AddShadow(element *etree.Element, id string, x, y, blur float64) {
f := etree.NewElement("filter")
f.CreateAttr("id", id)
f.CreateAttr("filterUnits", "userSpaceOnUse")
b := etree.NewElement("feGaussianBlur")
b.CreateAttr("in", "SourceAlpha")
b.CreateAttr("stdDeviation", fmt.Sprintf("%.2f", blur))
o := etree.NewElement("feOffset")
o.CreateAttr("result", "offsetblur")
o.CreateAttr("dx", fmt.Sprintf("%.2f", x))
o.CreateAttr("dy", fmt.Sprintf("%.2f", y))
m := etree.NewElement("feMerge")
mn1 := etree.NewElement("feMergeNode")
mn2 := etree.NewElement("feMergeNode")
mn2.CreateAttr("in", "SourceGraphic")
m.AddChild(mn1)
m.AddChild(mn2)
f.AddChild(b)
f.AddChild(o)
f.AddChild(m)
defs := etree.NewElement("defs")
defs.AddChild(f)
element.AddChild(defs)
}
// AddClipPath adds a definition of a clip path to the <defs> with the given id.
func AddClipPath(element *etree.Element, id string, x, y, w, h float64) {
p := etree.NewElement("clipPath")
p.CreateAttr("id", id)
rect := etree.NewElement("rect")
rect.CreateAttr("x", fmt.Sprintf("%.2f", x))
rect.CreateAttr("y", fmt.Sprintf("%.2f", y))
rect.CreateAttr("width", fmt.Sprintf("%.2f", w))
rect.CreateAttr("height", fmt.Sprintf("%.2f", h))
p.AddChild(rect)
defs := etree.NewElement("defs")
defs.AddChild(p)
element.AddChild(defs)
}
// AddCornerRadius adds corner radius to an element.
func AddCornerRadius(e *etree.Element, radius float64) {
e.CreateAttr("rx", fmt.Sprintf("%.2f", radius))
e.CreateAttr("ry", fmt.Sprintf("%.2f", radius))
}
// Move moves the given element to the (x, y) position.
func Move(e *etree.Element, x, y float64) {
e.CreateAttr("x", fmt.Sprintf("%.2fpx", x))
e.CreateAttr("y", fmt.Sprintf("%.2fpx", y))
}
// AddOutline adds an outline to the given element.
func AddOutline(e *etree.Element, width float64, color string) {
e.CreateAttr("stroke", color)
e.CreateAttr("stroke-width", fmt.Sprintf("%.2f", width))
}
const (
red string = "#FF5A54"
yellow string = "#E6BF29"
green string = "#52C12B"
)
// NewWindowControls returns a colorful window bar element.
func NewWindowControls(r float64, x, y float64) *etree.Element {
bar := etree.NewElement("svg")
for i, color := range []string{red, yellow, green} {
circle := etree.NewElement("circle")
circle.CreateAttr("cx", fmt.Sprintf("%.2f", float64(i+1)*float64(x)-float64(r)))
circle.CreateAttr("cy", fmt.Sprintf("%.2f", y))
circle.CreateAttr("r", fmt.Sprintf("%.2f", r))
circle.CreateAttr("fill", color)
bar.AddChild(circle)
}
return bar
}
// SetDimensions sets the width and height of the given element.
func SetDimensions(element *etree.Element, width, height float64) {
widthAttr := element.SelectAttr("width")
heightAttr := element.SelectAttr("height")
heightAttr.Value = fmt.Sprintf("%.2f", height)
widthAttr.Value = fmt.Sprintf("%.2f", width)
}
// GetDimensions returns the width and height of the element.
func GetDimensions(element *etree.Element) (int, int) {
widthValue := element.SelectAttrValue("width", "0px")
heightValue := element.SelectAttrValue("height", "0px")
width := dimensionToInt(widthValue)
height := dimensionToInt(heightValue)
return width, height
}
// dimensionToInt takes a string and returns the integer value.
// e.g. "500px" -> 500.
func dimensionToInt(px string) int {
d := strings.TrimSuffix(px, "px")
v, _ := strconv.ParseInt(d, 10, 64)
return int(v)
}
+14
View File
@@ -0,0 +1,14 @@
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.
+14
View File
@@ -0,0 +1,14 @@
Set Width 700
Set Height 200
Set Padding 40
Sleep 500ms Type "freeze artichoke.hs -o artichoke.png"
Sleep 1s Enter
Sleep 2s
Ctrl+L
Sleep 500ms Type 'freeze --execute "ls -lah"'
Sleep 1s Enter
Sleep 3s
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f52bce59d0380e71d1a672221ede9640f42475a573a760a30ec49fa8543777fd
size 207818
+23
View File
@@ -0,0 +1,23 @@
Set Width 700
Set Height 1200
Sleep 500ms Type "freeze --interactive main.go"
Sleep 1s Enter
Sleep 2s Tab
Sleep 1s Right@500ms 4
Sleep 1s Tab
Sleep 2s Ctrl+W
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Left Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 1s Tab
Sleep 2s Enter Sleep 2s
+27
View File
@@ -0,0 +1,27 @@
{
"window": false,
"theme": "charm",
"border": {
"radius": 0,
"width": 0,
"color": "#515151"
},
"shadow": {
"blur": 0,
"x": 0,
"y": 0
},
"padding": [
20,
40,
20,
20
],
"margin": "0",
"background": "#171717",
"font": {
"family": "JetBrains Mono",
"size": 14
},
"line_height": 1.2
}
+27
View File
@@ -0,0 +1,27 @@
{
"window": true,
"theme": "charm",
"border": {
"radius": 8,
"width": 1,
"color": "#515151"
},
"shadow": {
"blur": 20,
"x": 0,
"y": 10
},
"padding": [
20,
20,
20,
20
],
"margin": "60",
"background": "#171717",
"font": {
"family": "JetBrains Mono",
"size": 14
},
"line_height": 1.2
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
size 147861
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc8f829d28156540257cf13857c34c68b98cd55a322b248f41d569f8c1e43b37
size 211518
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d876c87dc4434c2cd43618096b7e156eef8d7b91e9cec8bdaf758fe970894629
size 149366
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d448145affbe77c87aaa75df6c7749b0116f8f9b0de0dba185a8bc2ecaad8841
size 153649
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf3b282c57a1014d0af882d275833803d55cfafb19cf4b6f1c2678ee24cb210e
size 92440
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1c0fb0d3979aab6458e649afad987b2aa565b6d488ec31145773e389687a895
size 26812
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f72a36395b12d483cf6181aa4c71fd84a14a2782f0448505c928dda2b34113bd
size 19441
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e740f62d59af375f2001989fdcdf1baa84bc92ede18d1674476d3ae07f672e0
size 17848
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:545a98f24051d5e2ee418a01ba14536222fba1f4abe32a2741bc88850d9e365c
size 18086
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba1d14aa0e7ba09bc5011b82b3dc59379dd5df3fb76da587ec38b876ad031efa
size 39809
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20f9e3afe71cab4408cdf36b6bfb8276d8fb6bdc2846249c09b4a08477904836
size 155158
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f0159e6ad36e814a46bc3ed7a5f6f52ea4fdfc5ff543e78f1b5805ea2f43526
size 8545
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8b2a28a393837a46e16862205d6de3dba9ebd47a8af27e0c5b48fd7e906e1450
size 192707
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
size 147861
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d9f4173e0080bd8019ef18e77b318f0e43325cf5d63c7004384458207357505
size 296701
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f06b531e59cdc66acc74a549a2033c5a523a555536ec6f11a9fa0decf9c8d28b
size 276597
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
size 147861
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d61b33e0201f891d3da4c262edefde0c59196360b8d729601ddf47bd98439676
size 73456
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ed561fcdc7f04e8f0013e7e249082262a930e8cf9275107f41b8cad3c66376a
size 717940
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5c4a9970c91ee8133a9bc07ff53f14a18a1a2b63ce6ad58755b2e90788eca2a
size 157530
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fef8f6ec4e6cfed47ec49285596a9479cbde27e93d301356dc864b9a3c665134
size 112089
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7d453086e221a1dc2cc182e382abb157e39987fedf93a4faf6fd019bc06b507
size 168045
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a88047ee5a0168829361090bc856dccd91f8685f2bd958e959c89efda6f48d2
size 432565
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2a72b7dbe7d21c11a04be68c4ae6e38b60a79cc4c0dfe80cc5f820b2f017a3b
size 123387
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90328caeded476f2d9b4f50af2f9c19c99c217dd6bbefd62b9ac543ef0bcde9c
size 155758
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c242915353fe2e6a18de496109fc9b8104360063ee57e910d45279214c641780
size 220401
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:142c5bb67207d6c4516347c270b156f5063aa7c750e47b4a468b734c52916813
size 152003
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 275 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 275 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 367 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 358 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 366 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 363 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 417 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 380 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 365 KiB

Some files were not shown because too many files have changed in this diff Show More