commit 93eb77f7cf306cf2359efcfa8ab6f42f8273556e Author: wehub-resource-sync Date: Mon Jul 13 13:12:56 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..81a416d --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d944991 --- /dev/null +++ b/.github/dependabot.yml @@ -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: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3511f7f --- /dev/null +++ b/.github/workflows/build.yml @@ -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 }}" diff --git a/.github/workflows/dependabot-sync.yml b/.github/workflows/dependabot-sync.yml new file mode 100644 index 0000000..9b08259 --- /dev/null +++ b/.github/workflows/dependabot-sync.yml @@ -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 }} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml new file mode 100644 index 0000000..9c8bd13 --- /dev/null +++ b/.github/workflows/goreleaser.yml @@ -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 }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1ffad5f --- /dev/null +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..025653b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.svg +*.png +!test/golden/**/*.svg +!test/golden/**/*.png +!examples/*.svg +/result +dist/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c5bed7c --- /dev/null +++ b/.golangci.yml @@ -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$ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..8f0529b --- /dev/null +++ b/.goreleaser.yml @@ -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 " + brew_commit_author_name: "Maas Lalani" + brew_commit_author_email: "maas@charm.sh" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d06620 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..664af8a --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: test + +test: + go test ./... + +golden: + cp -r test/output/* test/golden diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad0ba77 --- /dev/null +++ b/README.md @@ -0,0 +1,383 @@ +# Freeze + +

+
+ Latest Release + Build Status +

+ +Generate images of code and terminal output. + +

+ Freeze code screenshot +

+ +## 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 +``` + +

+ + output of freeze command, Haskell code block + +

+ +### 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" +``` + +

+ + output of freeze command, ANSI + +

+ +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 +``` + +freeze interactive mode + +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 +``` + +
+ + + output of freeze command, Haskell code block + + +### Theme + +Change the color theme. + +```bash +freeze artichoke.hs --theme dracula +``` + +
+ + + output of freeze command, Haskell code block with dracula theme + + +### 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 +``` + +
+ + + code screenshot with corner radius of 8px + + +### Window + +Add window controls to the terminal, macOS-style. + +```bash +freeze artichoke.hs --window +``` + + + output of freeze command, Haskell code block with window controls applied + + +### 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 +``` + +
+ + + output of freeze command, Haskell code block with border applied + + +### 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 +``` + +
+ + + output of freeze command, Haskell code block with padding applied + + +### 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 +``` + +
+ + + output of freeze command, Haskell code block with margin applied + + +### Shadow + +Add a shadow under the terminal window. + +```bash +freeze artichoke.hs --shadow.blur 20 --shadow.x 0 --shadow.y 10 +``` + +
+ + + output of freeze command, Haskell code block with a shadow + + +## 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 +``` + +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 + +We’d 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). + + + The Charm logo + + +Charm热爱开源 • Charm loves open source diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..12b6f24 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`charmbracelet/freeze` +- 原始仓库:https://github.com/charmbracelet/freeze +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f3894f0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please email vt100@charm.sh diff --git a/ansi.go b/ansi.go new file mode 100644 index 0000000..b0b2729 --- /dev/null +++ b/ansi.go @@ -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", +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..bbe8799 --- /dev/null +++ b/config.go @@ -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 +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..a23d1eb --- /dev/null +++ b/config_test.go @@ -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) + } + } +} diff --git a/configurations/base.json b/configurations/base.json new file mode 100644 index 0000000..2a73830 --- /dev/null +++ b/configurations/base.json @@ -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 +} \ No newline at end of file diff --git a/configurations/full.json b/configurations/full.json new file mode 100644 index 0000000..40d5131 --- /dev/null +++ b/configurations/full.json @@ -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 +} \ No newline at end of file diff --git a/cut.go b/cut.go new file mode 100644 index 0000000..c83e4fc --- /dev/null +++ b/cut.go @@ -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) +} diff --git a/cut_test.go b/cut_test.go new file mode 100644 index 0000000..497b0c2 --- /dev/null +++ b/cut_test.go @@ -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) + } + } +} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..4d4c70b --- /dev/null +++ b/default.nix @@ -0,0 +1,6 @@ +{pkgs}: +pkgs.buildGoModule { + name = "freeze"; + src = ./.; + vendorHash = "sha256-AUFzxmQOb/h0UgcprY09IVI7Auitn3JTDU/ptKicIAU="; +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..611bd65 --- /dev/null +++ b/error.go @@ -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) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d78e564 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..74f1b69 --- /dev/null +++ b/flake.nix @@ -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;}; + }; + }; +} diff --git a/font.go b/font.go new file mode 100644 index 0000000..68725c0 --- /dev/null +++ b/font.go @@ -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 +} diff --git a/font/JetBrainsMono-Regular.ttf b/font/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..3b75b26 Binary files /dev/null and b/font/JetBrainsMono-Regular.ttf differ diff --git a/font/JetBrainsMonoNL-Regular.ttf b/font/JetBrainsMonoNL-Regular.ttf new file mode 100644 index 0000000..70d2ec9 Binary files /dev/null and b/font/JetBrainsMonoNL-Regular.ttf differ diff --git a/font/font.go b/font/font.go new file mode 100644 index 0000000..6e31e4e --- /dev/null +++ b/font/font.go @@ -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) +) diff --git a/freeze_test.go b/freeze_test.go new file mode 100644 index 0000000..1e38ed7 --- /dev/null +++ b/freeze_test.go @@ -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") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd600bb --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5f54d04 --- /dev/null +++ b/go.sum @@ -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= diff --git a/help.go b/help.go new file mode 100644 index 0000000..ed695ff --- /dev/null +++ b/help.go @@ -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)) +} diff --git a/input/input.go b/input/input.go new file mode 100644 index 0000000..58818a6 --- /dev/null +++ b/input/input.go @@ -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 +} diff --git a/interactive.go b/interactive.go new file mode 100644 index 0000000..094e517 --- /dev/null +++ b/interactive.go @@ -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 diff --git a/main.go b/main.go new file mode 100644 index 0000000..5302773 --- /dev/null +++ b/main.go @@ -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)) +} diff --git a/png.go b/png.go new file mode 100644 index 0000000..b7d0184 --- /dev/null +++ b/png.go @@ -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 +} diff --git a/pty.go b/pty.go new file mode 100644 index 0000000..d7e8271 --- /dev/null +++ b/pty.go @@ -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 +} diff --git a/release/entitlements.plist b/release/entitlements.plist new file mode 100644 index 0000000..1c540c0 --- /dev/null +++ b/release/entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.assets.fonts.read-only + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/style.go b/style.go new file mode 100644 index 0000000..fb37bfc --- /dev/null +++ b/style.go @@ -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", +})) diff --git a/svg/svg.go b/svg/svg.go new file mode 100644 index 0000000..d4fc5aa --- /dev/null +++ b/svg/svg.go @@ -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 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 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) +} diff --git a/tapes/artichoke.hs b/tapes/artichoke.hs new file mode 100644 index 0000000..2cf946f --- /dev/null +++ b/tapes/artichoke.hs @@ -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. diff --git a/tapes/demo.tape b/tapes/demo.tape new file mode 100644 index 0000000..788ddfc --- /dev/null +++ b/tapes/demo.tape @@ -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 diff --git a/tapes/interactive.gif b/tapes/interactive.gif new file mode 100644 index 0000000..06f8a18 --- /dev/null +++ b/tapes/interactive.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f52bce59d0380e71d1a672221ede9640f42475a573a760a30ec49fa8543777fd +size 207818 diff --git a/tapes/interactive.tape b/tapes/interactive.tape new file mode 100644 index 0000000..30e5cc7 --- /dev/null +++ b/tapes/interactive.tape @@ -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 diff --git a/test/configurations/base.json b/test/configurations/base.json new file mode 100644 index 0000000..d12760a --- /dev/null +++ b/test/configurations/base.json @@ -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 +} \ No newline at end of file diff --git a/test/configurations/full.json b/test/configurations/full.json new file mode 100644 index 0000000..bded6b0 --- /dev/null +++ b/test/configurations/full.json @@ -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 +} \ No newline at end of file diff --git a/test/golden/png/artichoke-base.png b/test/golden/png/artichoke-base.png new file mode 100644 index 0000000..30f3564 --- /dev/null +++ b/test/golden/png/artichoke-base.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d +size 147861 diff --git a/test/golden/png/artichoke-full.png b/test/golden/png/artichoke-full.png new file mode 100644 index 0000000..1b9b129 --- /dev/null +++ b/test/golden/png/artichoke-full.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc8f829d28156540257cf13857c34c68b98cd55a322b248f41d569f8c1e43b37 +size 211518 diff --git a/test/golden/png/border-radius.png b/test/golden/png/border-radius.png new file mode 100644 index 0000000..9372a7f --- /dev/null +++ b/test/golden/png/border-radius.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d876c87dc4434c2cd43618096b7e156eef8d7b91e9cec8bdaf758fe970894629 +size 149366 diff --git a/test/golden/png/border-width.png b/test/golden/png/border-width.png new file mode 100644 index 0000000..94cc533 --- /dev/null +++ b/test/golden/png/border-width.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d448145affbe77c87aaa75df6c7749b0116f8f9b0de0dba185a8bc2ecaad8841 +size 153649 diff --git a/test/golden/png/bubbletea.png b/test/golden/png/bubbletea.png new file mode 100644 index 0000000..9e01e5d --- /dev/null +++ b/test/golden/png/bubbletea.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf3b282c57a1014d0af882d275833803d55cfafb19cf4b6f1c2678ee24cb210e +size 92440 diff --git a/test/golden/png/dimensions-config.png b/test/golden/png/dimensions-config.png new file mode 100644 index 0000000..2cfbc2b --- /dev/null +++ b/test/golden/png/dimensions-config.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1c0fb0d3979aab6458e649afad987b2aa565b6d488ec31145773e389687a895 +size 26812 diff --git a/test/golden/png/dimensions-margin-line-numbers.png b/test/golden/png/dimensions-margin-line-numbers.png new file mode 100644 index 0000000..894f1d5 --- /dev/null +++ b/test/golden/png/dimensions-margin-line-numbers.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f72a36395b12d483cf6181aa4c71fd84a14a2782f0448505c928dda2b34113bd +size 19441 diff --git a/test/golden/png/dimensions-margin.png b/test/golden/png/dimensions-margin.png new file mode 100644 index 0000000..061eaf4 --- /dev/null +++ b/test/golden/png/dimensions-margin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e740f62d59af375f2001989fdcdf1baa84bc92ede18d1674476d3ae07f672e0 +size 17848 diff --git a/test/golden/png/dimensions-padding.png b/test/golden/png/dimensions-padding.png new file mode 100644 index 0000000..d0dabb7 --- /dev/null +++ b/test/golden/png/dimensions-padding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:545a98f24051d5e2ee418a01ba14536222fba1f4abe32a2741bc88850d9e365c +size 18086 diff --git a/test/golden/png/dimensions.png b/test/golden/png/dimensions.png new file mode 100644 index 0000000..cb829aa --- /dev/null +++ b/test/golden/png/dimensions.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba1d14aa0e7ba09bc5011b82b3dc59379dd5df3fb76da587ec38b876ad031efa +size 39809 diff --git a/test/golden/png/dracula.png b/test/golden/png/dracula.png new file mode 100644 index 0000000..910b3a6 --- /dev/null +++ b/test/golden/png/dracula.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20f9e3afe71cab4408cdf36b6bfb8276d8fb6bdc2846249c09b4a08477904836 +size 155158 diff --git a/test/golden/png/execute.png b/test/golden/png/execute.png new file mode 100644 index 0000000..b38a88f --- /dev/null +++ b/test/golden/png/execute.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f0159e6ad36e814a46bc3ed7a5f6f52ea4fdfc5ff543e78f1b5805ea2f43526 +size 8545 diff --git a/test/golden/png/eza.png b/test/golden/png/eza.png new file mode 100644 index 0000000..0e68b44 --- /dev/null +++ b/test/golden/png/eza.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b2a28a393837a46e16862205d6de3dba9ebd47a8af27e0c5b48fd7e906e1450 +size 192707 diff --git a/test/golden/png/font-size-14.png b/test/golden/png/font-size-14.png new file mode 100644 index 0000000..30f3564 --- /dev/null +++ b/test/golden/png/font-size-14.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d +size 147861 diff --git a/test/golden/png/font-size-28.png b/test/golden/png/font-size-28.png new file mode 100644 index 0000000..18f7e63 --- /dev/null +++ b/test/golden/png/font-size-28.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d9f4173e0080bd8019ef18e77b318f0e43325cf5d63c7004384458207357505 +size 296701 diff --git a/test/golden/png/glow.png b/test/golden/png/glow.png new file mode 100644 index 0000000..d0aaf5c --- /dev/null +++ b/test/golden/png/glow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f06b531e59cdc66acc74a549a2033c5a523a555536ec6f11a9fa0decf9c8d28b +size 276597 diff --git a/test/golden/png/haskell.png b/test/golden/png/haskell.png new file mode 100644 index 0000000..30f3564 --- /dev/null +++ b/test/golden/png/haskell.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d +size 147861 diff --git a/test/golden/png/helix.png b/test/golden/png/helix.png new file mode 100644 index 0000000..3383c4d --- /dev/null +++ b/test/golden/png/helix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d61b33e0201f891d3da4c262edefde0c59196360b8d729601ddf47bd98439676 +size 73456 diff --git a/test/golden/png/layout.png b/test/golden/png/layout.png new file mode 100644 index 0000000..a1471e6 --- /dev/null +++ b/test/golden/png/layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ed561fcdc7f04e8f0013e7e249082262a930e8cf9275107f41b8cad3c66376a +size 717940 diff --git a/test/golden/png/line-height-2.png b/test/golden/png/line-height-2.png new file mode 100644 index 0000000..88b6872 --- /dev/null +++ b/test/golden/png/line-height-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5c4a9970c91ee8133a9bc07ff53f14a18a1a2b63ce6ad58755b2e90788eca2a +size 157530 diff --git a/test/golden/png/lines.png b/test/golden/png/lines.png new file mode 100644 index 0000000..e55e1ee --- /dev/null +++ b/test/golden/png/lines.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fef8f6ec4e6cfed47ec49285596a9479cbde27e93d301356dc864b9a3c665134 +size 112089 diff --git a/test/golden/png/margin.png b/test/golden/png/margin.png new file mode 100644 index 0000000..73b0d45 --- /dev/null +++ b/test/golden/png/margin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7d453086e221a1dc2cc182e382abb157e39987fedf93a4faf6fd019bc06b507 +size 168045 diff --git a/test/golden/png/overflow-line-numbers.png b/test/golden/png/overflow-line-numbers.png new file mode 100644 index 0000000..2e00c35 --- /dev/null +++ b/test/golden/png/overflow-line-numbers.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a88047ee5a0168829361090bc856dccd91f8685f2bd958e959c89efda6f48d2 +size 432565 diff --git a/test/golden/png/overflow.png b/test/golden/png/overflow.png new file mode 100644 index 0000000..a480418 --- /dev/null +++ b/test/golden/png/overflow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a72b7dbe7d21c11a04be68c4ae6e38b60a79cc4c0dfe80cc5f820b2f017a3b +size 123387 diff --git a/test/golden/png/padding.png b/test/golden/png/padding.png new file mode 100644 index 0000000..fb9199d --- /dev/null +++ b/test/golden/png/padding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90328caeded476f2d9b4f50af2f9c19c99c217dd6bbefd62b9ac543ef0bcde9c +size 155758 diff --git a/test/golden/png/shadow.png b/test/golden/png/shadow.png new file mode 100644 index 0000000..daaa649 --- /dev/null +++ b/test/golden/png/shadow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c242915353fe2e6a18de496109fc9b8104360063ee57e910d45279214c641780 +size 220401 diff --git a/test/golden/png/window.png b/test/golden/png/window.png new file mode 100644 index 0000000..93446fb --- /dev/null +++ b/test/golden/png/window.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:142c5bb67207d6c4516347c270b156f5063aa7c750e47b4a468b734c52916813 +size 152003 diff --git a/test/golden/svg/artichoke-base.svg b/test/golden/svg/artichoke-base.svg new file mode 100644 index 0000000..530cedd --- /dev/null +++ b/test/golden/svg/artichoke-base.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/artichoke-full.svg b/test/golden/svg/artichoke-full.svg new file mode 100644 index 0000000..c5e9984 --- /dev/null +++ b/test/golden/svg/artichoke-full.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/border-radius.svg b/test/golden/svg/border-radius.svg new file mode 100644 index 0000000..7a80712 --- /dev/null +++ b/test/golden/svg/border-radius.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/border-width.svg b/test/golden/svg/border-width.svg new file mode 100644 index 0000000..98bbb13 --- /dev/null +++ b/test/golden/svg/border-width.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/bubbletea.svg b/test/golden/svg/bubbletea.svg new file mode 100644 index 0000000..40807bd --- /dev/null +++ b/test/golden/svg/bubbletea.svg @@ -0,0 +1,49 @@ + + + + + + 1 func (m model) Init() tea.Cmd { + 2     return nil + 3 } + 4 + 5 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + 6     switch msg := msg.(type) { + 7 + 8     case tea.KeyMsg: + 9         switch msg.String() { + 10         case "ctrl+c", "q": + 11             return m, tea.Quit + 12         case "up", "k": + 13             if m.cursor > 0 { + 14                 m.cursor-- + 15             } + 16         case "down", "j": + 17             if m.cursor < len(m.choices)-1 { + 18                 m.cursor++ + 19             } + 20         case "enter", " ": + 21             _, ok := m.selected[m.cursor] + 22             if ok { + 23                 delete(m.selected, m.cursor) + 24             } else { + 25                 m.selected[m.cursor] = struct{}{} + 26             } + 27         } + 28     } + 29     return m, nil + 30 } + 31 + 32 func (m model) View() string { + 33     return // ... + 34 } + + + diff --git a/test/golden/svg/dimensions-config.svg b/test/golden/svg/dimensions-config.svg new file mode 100644 index 0000000..2478105 --- /dev/null +++ b/test/golden/svg/dimensions-config.svg @@ -0,0 +1,22 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = + + + diff --git a/test/golden/svg/dimensions-margin-line-numbers.svg b/test/golden/svg/dimensions-margin-line-numbers.svg new file mode 100644 index 0000000..89c31d2 --- /dev/null +++ b/test/golden/svg/dimensions-margin-line-numbers.svg @@ -0,0 +1,24 @@ + + + + + + 1 module Main where + 2 + 3 import Data.Function ( (&) ) + 4 import Data.List ( intercalate ) + 5 + 6 hello :: String -> String + 7 hello s = + 8   "Hello, " ++ s ++ "." + 9 + + + diff --git a/test/golden/svg/dimensions-margin.svg b/test/golden/svg/dimensions-margin.svg new file mode 100644 index 0000000..aaa33ff --- /dev/null +++ b/test/golden/svg/dimensions-margin.svg @@ -0,0 +1,24 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + + + + diff --git a/test/golden/svg/dimensions-padding.svg b/test/golden/svg/dimensions-padding.svg new file mode 100644 index 0000000..ef73f47 --- /dev/null +++ b/test/golden/svg/dimensions-padding.svg @@ -0,0 +1,26 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = + + + diff --git a/test/golden/svg/dimensions.svg b/test/golden/svg/dimensions.svg new file mode 100644 index 0000000..b24d801 --- /dev/null +++ b/test/golden/svg/dimensions.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/dracula.svg b/test/golden/svg/dracula.svg new file mode 100644 index 0000000..67579f0 --- /dev/null +++ b/test/golden/svg/dracula.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke""alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/execute.svg b/test/golden/svg/execute.svg new file mode 100644 index 0000000..2811ee7 --- /dev/null +++ b/test/golden/svg/execute.svg @@ -0,0 +1,15 @@ + + + + + +Hello, world! + + diff --git a/test/golden/svg/eza.svg b/test/golden/svg/eza.svg new file mode 100644 index 0000000..4336c3e --- /dev/null +++ b/test/golden/svg/eza.svg @@ -0,0 +1,15 @@ + + + + + +ansi.go cut_test.go go.mod main.go style.goconfig.go error.go go.sum Makefile svgconfig_test.go font help.go png.go tapesconfigurations font.go input pty.go testcut.go freeze_test.go interactive.go README.md + + diff --git a/test/golden/svg/font-size-14.svg b/test/golden/svg/font-size-14.svg new file mode 100644 index 0000000..f6eb4c4 --- /dev/null +++ b/test/golden/svg/font-size-14.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/font-size-28.svg b/test/golden/svg/font-size-28.svg new file mode 100644 index 0000000..3b7d023 --- /dev/null +++ b/test/golden/svg/font-size-28.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/glow.svg b/test/golden/svg/glow.svg new file mode 100644 index 0000000..43e5a7d --- /dev/null +++ b/test/golden/svg/glow.svg @@ -0,0 +1,15 @@ + + + + + + Professional Glow 17 documents charm / everyone / docs/README.md 18 Mar 2024 18:51 UTC by christian charm / everyone / README.md 15 Mar 2024 19:05 UTC by carlos charm / everyone / docs-faq.md 15 Mar 2024 19:05 UTC by carlos charm / everyone / README.md 15 Mar 2024 19:04 UTC by carlos charm / everyone / secret notes 15 Mar 2024 16:45 UTC by carlos charm / everyone / ./README.md 15 Mar 2024 16:43 UTC by carlos h/l ←/→ page/ findt team filterr refreshs stash + + diff --git a/test/golden/svg/haskell.svg b/test/golden/svg/haskell.svg new file mode 100644 index 0000000..f6eb4c4 --- /dev/null +++ b/test/golden/svg/haskell.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/helix.svg b/test/golden/svg/helix.svg new file mode 100644 index 0000000..d03899c --- /dev/null +++ b/test/golden/svg/helix.svg @@ -0,0 +1,15 @@ + + + + + + 1 package main 2 3 import "fmt" 4 5 func main() { 6 fmt.Println("Hello, world!") 7 } ~ NORMAL examples/main.go 1 sel 1:1 LF go + + diff --git a/test/golden/svg/layout.svg b/test/golden/svg/layout.svg new file mode 100644 index 0000000..f27cc26 --- /dev/null +++ b/test/golden/svg/layout.svg @@ -0,0 +1,15 @@ + + + + + + ╭───────────╮╭───────╮╭────────────╮╭─────────╮╭────────────╮ Lip Gloss Blush Eye Shadow Mascara Foundation ┘ └┴───────┴┴────────────┴┴─────────┴┴────────────┴───────────────── Lip Gloss Lip Gloss Style Definitions for Nice Terminal Layouts Lip Gloss ────────────────────────────────────────────────────── Lip Gloss From Charm https://github.com/charmbracelet/lipgloss Lip Gloss ╭──────────────────────────────────────────────────╮ Are you sure you want to eat marmalade? Yes Maybe ╰──────────────────────────────────────────────────╯ Citrus Fruits to Try Actual Lip Gloss Vendors ──────────────────── ──────────────────────── Grapefruit Glossier Yuzu Claire‘s Boutique Citron Nyx Kumquat Mac Pomelo Milk The Romans learned from Medieval quince preserves, In 1524, H the Greeks that quinces which went by the French of England slowly cooked with honey name cotignac, produced in “box of ma would “set” when cool. The a clear version and a Mr. Hull o Apicius gives a recipe for fruit pulp version, began was probab preserving whole quinces, to lose their medieval solid quin stems and leaves attached, seasoning of spices in the Portugal, in a bath of honey diluted 16th century. In the 17th sold in so with defrutum: Roman century, La Varenne today. It marmalade. Preserves of provided recipes for both favourite quince and lemon appear thick and clear cotignac. Boleyn and (along with rose, apple, waiting. plum and pear) in the Book of ceremonies of the Byzantine Emperor Constantine VII Porphyrogennetos. STATUS Ravishing UT + + diff --git a/test/golden/svg/line-height-2.svg b/test/golden/svg/line-height-2.svg new file mode 100644 index 0000000..a838877 --- /dev/null +++ b/test/golden/svg/line-height-2.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/lines.svg b/test/golden/svg/lines.svg new file mode 100644 index 0000000..186a291 --- /dev/null +++ b/test/golden/svg/lines.svg @@ -0,0 +1,19 @@ + + + + + + 4 import Data.List ( intercalate ) + 5 + 6 hello :: String -> String + 7 hello s = + 8   "Hello, " ++ s ++ "." + + diff --git a/test/golden/svg/margin.svg b/test/golden/svg/margin.svg new file mode 100644 index 0000000..956194b --- /dev/null +++ b/test/golden/svg/margin.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/overflow-line-numbers.svg b/test/golden/svg/overflow-line-numbers.svg new file mode 100644 index 0000000..1a263f7 --- /dev/null +++ b/test/golden/svg/overflow-line-numbers.svg @@ -0,0 +1,123 @@ + + + + + + 1 variables: + 2   main: "" + 3   binary_name: "" + 4   description: "" + 5   maintainer: "" + 6   homepage: "https://charm.sh/" + 7   brew_commit_author_name: "" + 8   brew_commit_author_email: "" + 9   brew_owner: charmbracelet + 10   docker_io_registry_owner: charmcli + 11   ghcr_io_registry_owner: charmbracelet + 12   aur_project_name: "" + 13   scoop_name: "" + 14 + 15 before: + 16   hooks: + 17     - go mod tidy + 18     - rm -rf completions + 19     - mkdir completions + 20     - rm -rf manpages + 21     - mkdir manpages + 22     - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "bash" >./completions/{{ .ProjectName }}.bash' + 23     - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "zsh" >./completions/{{ .ProjectName }}.zsh' + 24     - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "fish" >./completions/{{ .ProjectName }}.fish' + 25     - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} man | gzip -c >./manpages/{{ .ProjectName }}.1.gz' + 26 + 27 gomod: + 28   proxy: true + 29 + 30 builds: + 31   - binary: "{{ with .Var.binary_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}" + 32     env: + 33       - CGO_ENABLED=0 + 34     main: "{{ with .Var.main }}{{ . }}{{ else }}.{{ end }}" + 35     ldflags: -s -w -X main.Version=v{{ .Version }} -X main.CommitSHA={{ .Commit }} -X main.CommitDate={{ .CommitDate }} + 36     goos: + 37       - linux + 38       - darwin + 39       - windows + 40       - freebsd + 41       - openbsd + 42       - netbsd + 43     goarch: + 44       - amd64 + 45       - arm64 + 46       - "386" + 47       - arm + 48     goarm: + 49       - "6" + 50       - "7" + 51     ignore: + 52       - goos: windows + 53         goarch: arm64 + 54       - goos: windows + 55         goarch: arm + 56 + 57 archives: + 58   - format_overrides: + 59       - goos: windows + 60         format: zip + 61     name_template: >- + 62       {{ .ProjectName }}_ + 63       {{- .Version }}_ + 64       {{- title .Os }}_ + 65       {{- if eq .Arch "amd64" }}x86_64 + 66       {{- else if eq .Arch "386" }}i386 + 67       {{- else }}{{ .Arch }}{{ end }} + 68       {{- with .Arm}}v{{ . }}{{ end }} + 69     wrap_in_directory: true + 70     files: + 71       - README* + 72       - LICENSE* + 73       - manpages/* + 74       - completions/* + 75 + 76 nfpms: + 77   - vendor: charmbracelet + 78     homepage: "{{ .Var.homepage }}" + 79     maintainer: "{{ .Var.maintainer }}" + 80     description: "{{ .Var.description }}" + 81     file_name_template: >- + 82       {{- trimsuffix .ConventionalFileName .ConventionalExtension -}} + 83       {{- if and (eq .Arm "6") (eq .ConventionalExtension ".deb") }}6{{ end -}} + 84       {{- .ConventionalExtension -}} + 85     license: MIT + 86     formats: + 87       - apk + 88       - deb + 89       - rpm + 90     contents: + 91       - src: ./completions/{{ .ProjectName }}.bash + 92         dst: /etc/bash_completion.d/{{ .ProjectName }} + 93       - src: ./completions/{{ .ProjectName }}.fish + 94         dst: /usr/share/fish/vendor_completions.d/{{ .ProjectName }}.fish + 95       - src: ./completions/{{ .ProjectName }}.zsh + 96         dst: /usr/share/zsh/site-functions/_{{ .ProjectName }} + 97       - src: ./manpages/{{ .ProjectName }}.1.gz + 98         dst: /usr/share/man/man1/{{ .ProjectName }}.1.gz + 99     rpm: +100       signature: +101         key_file: '{{ if ne (index .Env "GPG_KEY_PATH") "" }}{{ .Env.GPG_KEY_PATH }}{{ else }}{{ end }}' +102     deb: +103       signature: +104         key_file: '{{ if ne (index .Env "GPG_KEY_PATH") "" }}{{ .Env.GPG_KEY_PATH }}{{ else }}{{ end }}' +105 +106 furies: +107   - account: "{{ with .Env.FURY_TOKEN }}charmcli{{ else }}{{ end }}" +108     secret_name: FURY_TOKEN + + + diff --git a/test/golden/svg/overflow.svg b/test/golden/svg/overflow.svg new file mode 100644 index 0000000..6f97838 --- /dev/null +++ b/test/golden/svg/overflow.svg @@ -0,0 +1,58 @@ + + + + + +variables: +  main: "" +  binary_name: "" +  description: "" +  maintainer: "" +  homepage: "https://charm.sh/" +  brew_commit_author_name: "" +  brew_commit_author_email: "" +  brew_owner: charmbracelet +  docker_io_registry_owner: charmcli +  ghcr_io_registry_owner: charmbracelet +  aur_project_name: "" +  scoop_name: "" + +before: +  hooks: +    - go mod tidy +    - rm -rf completions +    - mkdir completions +    - rm -rf manpages +    - mkdir manpages +    - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "bash" >./completions/{{ .ProjectName }}.bash' +    - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "zsh" >./completions/{{ .ProjectName }}.zsh' +    - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "fish" >./completions/{{ .ProjectName }}.fish' +    - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} man | gzip -c >./manpages/{{ .ProjectName }}.1.gz' + +gomod: +  proxy: true + +builds: +  - binary: "{{ with .Var.binary_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}" +    env: +      - CGO_ENABLED=0 +    main: "{{ with .Var.main }}{{ . }}{{ else }}.{{ end }}" +    ldflags: -s -w -X main.Version=v{{ .Version }} -X main.CommitSHA={{ .Commit }} -X main.CommitDate={{ .CommitDate }} +    goos: +      - linux +      - darwin +      - windows +      - freebsd +      - openbsd +      - netbsd +    goarch: + + + diff --git a/test/golden/svg/padding.svg b/test/golden/svg/padding.svg new file mode 100644 index 0000000..0b20d97 --- /dev/null +++ b/test/golden/svg/padding.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/shadow.svg b/test/golden/svg/shadow.svg new file mode 100644 index 0000000..ad66d04 --- /dev/null +++ b/test/golden/svg/shadow.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/tab.svg b/test/golden/svg/tab.svg new file mode 100644 index 0000000..7bce3f3 --- /dev/null +++ b/test/golden/svg/tab.svg @@ -0,0 +1,42 @@ + + + + + +package main //nolint:revive + +// freeze/issues/50 + +type Config struct { //nolint: revive +    Telegram struct { +        Token   string `env:"TG_TOKEN"` +        ChatID  string `env:"TG_CHAT"` +        OwnerID string `env:"TG_ADMIN"` +    } + +    Database struct { +        DSN string `env:"DB_DSN"` +    } + +    Log struct { +        Level string `env:"LOG_LEVEL"` +    } + +    Debug bool +} + +func Load() (*Config, error) { //nolint: revive +    var c Config +    var err error +    return &c, err +} + + + diff --git a/test/golden/svg/window.svg b/test/golden/svg/window.svg new file mode 100644 index 0000000..b64e6ae --- /dev/null +++ b/test/golden/svg/window.svg @@ -0,0 +1,29 @@ + + + + + +module Main where + +import Data.Function ( (&) ) +import Data.List ( intercalate ) + +hello :: String -> String +hello s = +  "Hello, " ++ s ++ "." + +main :: IO () +main = +  map hello [ "artichoke", "alcachofa" ] & intercalate "\n" & putStrLn + +-- Alcachofa, if you were wondering, is artichoke in Spanish. + + + diff --git a/test/golden/svg/wrap.svg b/test/golden/svg/wrap.svg new file mode 100644 index 0000000..badc169 --- /dev/null +++ b/test/golden/svg/wrap.svg @@ -0,0 +1,28 @@ + + + + + +package main + +import "fmt" + +// freeze/issues/14 + +func main() { +    fmt.Println("This is a really long line that is going to go over the 80 +character limit. This is a really long line that is going to go over the 80 +character limit. This is a really long line that is going to go over the 80 +character limit. This is a really long line that is going to go over the 80 +character limit.") +} + + + diff --git a/test/input/artichoke.hs b/test/input/artichoke.hs new file mode 100644 index 0000000..2cf946f --- /dev/null +++ b/test/input/artichoke.hs @@ -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. diff --git a/test/input/bubbletea.model b/test/input/bubbletea.model new file mode 100644 index 0000000..611f1bc --- /dev/null +++ b/test/input/bubbletea.model @@ -0,0 +1,34 @@ +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + case "down", "j": + if m.cursor < len(m.choices)-1 { + m.cursor++ + } + case "enter", " ": + _, ok := m.selected[m.cursor] + if ok { + delete(m.selected, m.cursor) + } else { + m.selected[m.cursor] = struct{}{} + } + } + } + return m, nil +} + +func (m model) View() string { + return // ... +} diff --git a/test/input/eza.ansi b/test/input/eza.ansi new file mode 100644 index 0000000..ac1e529 --- /dev/null +++ b/test/input/eza.ansi @@ -0,0 +1,5 @@ +ansi.go cut_test.go go.mod main.go style.go +config.go error.go go.sum Makefile svg +config_test.go font help.go png.go tapes +configurations font.go input pty.go test +cut.go freeze_test.go interactive.go README.md diff --git a/test/input/glow.ansi b/test/input/glow.ansi new file mode 100644 index 0000000..42abaf8 --- /dev/null +++ b/test/input/glow.ansi @@ -0,0 +1,29 @@ + +  Professional Glow  + + 17 documents + + │ • charm / everyone / docs/README.md + │ 18 Mar 2024 18:51 UTC by christian + + • charm / everyone / README.md + 15 Mar 2024 19:05 UTC by carlos + + • charm / everyone / docs-faq.md + 15 Mar 2024 19:05 UTC by carlos + + • charm / everyone / README.md + 15 Mar 2024 19:04 UTC by carlos + + • charm / everyone / secret notes + 15 Mar 2024 16:45 UTC by carlos + + • charm / everyone / ./README.md + 15 Mar 2024 16:43 UTC by carlos + + + + ••• + + h/l ←/→ page • / find • t team filter • r refresh • s stash • … + diff --git a/test/input/goreleaser-full.yml b/test/input/goreleaser-full.yml new file mode 100644 index 0000000..6532025 --- /dev/null +++ b/test/input/goreleaser-full.yml @@ -0,0 +1,347 @@ +variables: + main: "" + binary_name: "" + description: "" + maintainer: "" + homepage: "https://charm.sh/" + brew_commit_author_name: "" + brew_commit_author_email: "" + brew_owner: charmbracelet + docker_io_registry_owner: charmcli + ghcr_io_registry_owner: charmbracelet + aur_project_name: "" + scoop_name: "" + +before: + hooks: + - go mod tidy + - rm -rf completions + - mkdir completions + - rm -rf manpages + - mkdir manpages + - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "bash" >./completions/{{ .ProjectName }}.bash' + - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "zsh" >./completions/{{ .ProjectName }}.zsh' + - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} completion "fish" >./completions/{{ .ProjectName }}.fish' + - sh -c 'go run {{ with .Var.main }}{{ . }}{{ else }}.{{ end }} man | gzip -c >./manpages/{{ .ProjectName }}.1.gz' + +gomod: + proxy: true + +builds: + - binary: "{{ with .Var.binary_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}" + env: + - CGO_ENABLED=0 + main: "{{ with .Var.main }}{{ . }}{{ else }}.{{ end }}" + ldflags: -s -w -X main.Version=v{{ .Version }} -X main.CommitSHA={{ .Commit }} -X main.CommitDate={{ .CommitDate }} + goos: + - linux + - darwin + - windows + - freebsd + - openbsd + - netbsd + goarch: + - amd64 + - arm64 + - "386" + - arm + goarm: + - "6" + - "7" + ignore: + - goos: windows + goarch: arm64 + - goos: windows + goarch: arm + +archives: + - format_overrides: + - goos: windows + format: zip + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- with .Arm}}v{{ . }}{{ end }} + wrap_in_directory: true + files: + - README* + - LICENSE* + - manpages/* + - completions/* + +nfpms: + - vendor: charmbracelet + homepage: "{{ .Var.homepage }}" + maintainer: "{{ .Var.maintainer }}" + description: "{{ .Var.description }}" + file_name_template: >- + {{- trimsuffix .ConventionalFileName .ConventionalExtension -}} + {{- if and (eq .Arm "6") (eq .ConventionalExtension ".deb") }}6{{ end -}} + {{- .ConventionalExtension -}} + license: MIT + formats: + - apk + - deb + - rpm + contents: + - src: ./completions/{{ .ProjectName }}.bash + dst: /etc/bash_completion.d/{{ .ProjectName }} + - src: ./completions/{{ .ProjectName }}.fish + dst: /usr/share/fish/vendor_completions.d/{{ .ProjectName }}.fish + - src: ./completions/{{ .ProjectName }}.zsh + dst: /usr/share/zsh/site-functions/_{{ .ProjectName }} + - src: ./manpages/{{ .ProjectName }}.1.gz + dst: /usr/share/man/man1/{{ .ProjectName }}.1.gz + rpm: + signature: + key_file: '{{ if ne (index .Env "GPG_KEY_PATH") "" }}{{ .Env.GPG_KEY_PATH }}{{ else }}{{ end }}' + deb: + signature: + key_file: '{{ if ne (index .Env "GPG_KEY_PATH") "" }}{{ .Env.GPG_KEY_PATH }}{{ else }}{{ end }}' + +furies: + - account: "{{ with .Env.FURY_TOKEN }}charmcli{{ else }}{{ end }}" + secret_name: FURY_TOKEN + +brews: + - repository: + owner: "{{ .Var.brew_owner }}" + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + commit_author: + name: "{{ .Var.brew_commit_author_name }}" + email: "{{ .Var.brew_commit_author_email }}" + homepage: "{{ .Var.homepage }}" + description: "{{ .Var.description }}" + goarm: 7 + install: |- + bin.install "{{ with .Var.binary_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}" + bash_completion.install "completions/{{ .ProjectName }}.bash" => "{{ .ProjectName }}" + zsh_completion.install "completions/{{ .ProjectName }}.zsh" => "_{{ .ProjectName }}" + fish_completion.install "completions/{{ .ProjectName }}.fish" + man1.install "manpages/{{ .ProjectName }}.1.gz" + +scoops: + - repository: + owner: "{{ .Var.brew_owner }}" + name: scoop-bucket + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + commit_author: + name: "{{ .Var.brew_commit_author_name }}" + email: "{{ .Var.brew_commit_author_email }}" + name: "{{ with .Var.scoop_name }}{{ . }}{{ end }}" + homepage: "{{ .Var.homepage }}" + description: "{{ .Var.description }}" + license: MIT + +aurs: + - maintainers: ["{{ .Var.maintainer }}"] + description: "{{ .Var.description }}" + name: "{{ with .Var.aur_project_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}-bin" + homepage: "{{ .Var.homepage }}" + license: MIT + private_key: "{{ .Env.AUR_KEY }}" + git_url: "ssh://aur@aur.archlinux.org/{{ with .Var.aur_project_name }}{{ . }}{{ else }}{{ .ProjectName }}{{ end }}-bin.git" + package: |- + cd "${srcdir}/{{ .ProjectName }}_${pkgver}_Linux_${CARCH}" + # bin + install -Dm755 "./{{ .ProjectName }}" "${pkgdir}/usr/bin/{{ .ProjectName }}" + # license + mkdir -p "${pkgdir}/usr/share/licenses/{{ .ProjectName }}/" + install -Dm644 ./LICENSE* "${pkgdir}/usr/share/licenses/{{ .ProjectName }}/" + # completions + mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" + mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" + mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" + install -Dm644 "./completions/{{ .ProjectName }}.bash" "${pkgdir}/usr/share/bash-completion/completions/{{ .ProjectName }}" + install -Dm644 "./completions/{{ .ProjectName }}.zsh" "${pkgdir}/usr/share/zsh/site-functions/_{{ .ProjectName }}" + install -Dm644 "./completions/{{ .ProjectName }}.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/{{ .ProjectName }}.fish" + # man pages + install -Dm644 "./manpages/{{ .ProjectName }}.1.gz" "${pkgdir}/usr/share/man/man1/{{ .ProjectName }}.1.gz" + +nix: + - repository: + owner: "{{ .Var.brew_owner }}" + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + name: nur + homepage: "{{ .Var.homepage }}" + description: "{{ .Var.description }}" + license: mit + extra_install: |- + installManPage ./manpages/{{.ProjectName}}.1.gz + installShellCompletion ./completions/* + +winget: + - publisher: charmbracelet + license: MIT + copyright: Charmbracelet, Inc + homepage: "{{ .Var.homepage }}" + short_description: "{{ .Var.description }}" + repository: + owner: "{{ .Var.brew_owner }}" + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + name: winget-pkgs + branch: "{{.ProjectName}}-{{.Version}}" + pull_request: + enabled: true + draft: false + check_boxes: true + base: + owner: microsoft + name: winget-pkgs + branch: master + +checksum: + name_template: "checksums.txt" + +source: + enabled: true + +sboms: + - artifacts: archive + - id: source + artifacts: source + +snapshot: + name_template: "{{ incpatch .Version }}-snapshot" + +nightly: + name_template: "{{ incpatch .Version }}-devel" + +changelog: + sort: asc + use: github + filters: + exclude: + - "^test:" + - "^chore" + - "merge conflict" + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + groups: + - title: Dependency updates + regexp: "^.*\\(deps\\)*:+.*$" + order: 300 + - title: "New Features" + regexp: "^.*feat[(\\w)]*:+.*$" + order: 100 + - title: "Bug fixes" + regexp: "^.*fix[(\\w)]*:+.*$" + order: 200 + - title: "Documentation updates" + regexp: "^.*docs[(\\w)]*:+.*$" + order: 400 + - title: Other work + order: 9999 + +signs: + - cmd: cosign + certificate: "${artifact}.pem" + args: + - sign-blob + - "--output-certificate=${certificate}" + - "--output-signature=${signature}" + - "${artifact}" + - "--yes" + artifacts: checksum + output: true + +dockers: + - image_templates: + - "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64{{ end }}" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + goarch: amd64 + build_flag_templates: + - --platform=linux/amd64 + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .Var.description }} + - --label=org.opencontainers.image.source={{ .GitURL }} + - --label=org.opencontainers.image.version=v{{ .Version }} + - --label=org.opencontainers.image.created={{ .Date }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses=MIT + dockerfile: Dockerfile + use: buildx + - image_templates: + - "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64{{ end }}" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + goarch: arm64 + build_flag_templates: + - --platform=linux/arm64 + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .Var.description }} + - --label=org.opencontainers.image.source={{ .GitURL }} + - --label=org.opencontainers.image.version=v{{ .Version }} + - --label=org.opencontainers.image.created={{ .Date }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses=MIT + dockerfile: Dockerfile + use: buildx + - image_templates: + - "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7{{ end }}" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + goarch: arm + goarm: "7" + build_flag_templates: + - --platform=linux/arm/v7 + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .Var.description }} + - --label=org.opencontainers.image.source={{ .GitURL }} + - --label=org.opencontainers.image.version=v{{ .Version }} + - --label=org.opencontainers.image.created={{ .Date }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses=MIT + dockerfile: Dockerfile + use: buildx + +docker_manifests: + - name_template: "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:latest{{ end }}" + image_templates: + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + - name_template: "{{ if not .IsNightly }}ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:latest{{ end }}" + image_templates: + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + - name_template: "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}{{ end }}" + image_templates: + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + - name_template: "{{ if not .IsNightly }}ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}{{ end }}" + image_templates: + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + - name_template: "{{ if not .IsNightly }}docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}{{ end }}" + image_templates: + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "docker.io/{{ .Var.docker_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + - name_template: "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}" + image_templates: + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-arm64" + - "ghcr.io/{{ .Var.ghcr_io_registry_owner }}/{{ .ProjectName }}:v{{ .Version }}-armv7" + +docker_signs: + - cmd: cosign + artifacts: manifests + output: true + args: + - "sign" + - "${artifact}" + - "--yes" + +release: + footer: + from_url: + url: https://raw.githubusercontent.com/charmbracelet/meta/main/footer.md diff --git a/test/input/helix.ansi b/test/input/helix.ansi new file mode 100644 index 0000000..e795b4e --- /dev/null +++ b/test/input/helix.ansi @@ -0,0 +1,14 @@ +  1 package main  +  2   +  3 import "fmt"  +  4   +  5 func main() {  +  6  fmt.Println("Hello, world!")  +  7 }  +  ~   + + + + + NORMAL  examples/main.go 1 sel 1:1 LF go + diff --git a/test/input/layout.ansi b/test/input/layout.ansi new file mode 100644 index 0000000..153bf90 --- /dev/null +++ b/test/input/layout.ansi @@ -0,0 +1,52 @@ + + ╭───────────╮╭───────╮╭────────────╮╭─────────╮╭────────────╮ + │ Lip Gloss ││ Blush ││ Eye Shadow ││ Mascara ││ Foundation │ + ┘ └┴───────┴┴────────────┴┴─────────┴┴────────────┴───────────────── + +  Lip Gloss  +  Lip Gloss  Style Definitions for Nice Terminal Layouts +  Lip Gloss  ────────────────────────────────────────────────────── +  Lip Gloss  From Charm • https://github.com/charmbracelet/lipgloss +  Lip Gloss  + + 猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫 + 猫咪猫咪猫咪猫咪猫咪猫╭──────────────────────────────────────────────────╮猫咪 + 猫咪猫咪猫咪猫咪猫咪猫│ │猫咪 + 猫咪猫咪猫咪猫咪猫咪猫│ Are you sure you want to eat marmalade? │猫咪 + 猫咪猫咪猫咪猫咪猫咪猫│ │猫咪 + 猫咪猫咪猫咪猫咪猫咪猫│  Yes   Maybe  │猫咪 + 猫咪猫咪猫咪猫咪猫咪猫│ │猫咪 + 猫咪猫咪猫咪猫咪猫咪猫╰──────────────────────────────────────────────────╯猫咪 + 猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫咪猫 + + Citrus Fruits to Try │ Actual Lip Gloss Vendors │        + ──────────────────── │ ──────────────────────── │        + ✓ Grapefruit │ Glossier │        + ✓ Yuzu │ Claire‘s Boutique │        + Citron │ ✓ Nyx │        + Kumquat │ Mac │        + Pomelo │ ✓ Milk │        + │ │        + +        +   The Romans learned from   Medieval quince preserves,   In 1524, H +   the Greeks that quinces    which went by the French    of England +   slowly cooked with honey   name cotignac, produced in   “box of ma +  would “set” when cool. The    a clear version and a    Mr. Hull o +  Apicius gives a recipe for   fruit pulp version, began    was probab +   preserving whole quinces,    to lose their medieval    solid quin +  stems and leaves attached,   seasoning of spices in the   Portugal,  +  in a bath of honey diluted   16th century. In the 17th    sold in so +   with defrutum: Roman    century, La Varenne    today. It  +   marmalade. Preserves of   provided recipes for both    favourite  +   quince and lemon appear   thick and clear cotignac.    Boleyn and +   (along with rose, apple,      waiting.  +  plum and pear) in the Book       +   of ceremonies of the       +   Byzantine Emperor       +   Constantine VII       +   Porphyrogennetos.       +        + +  STATUS  Ravishing  UT + diff --git a/test/input/tab.go b/test/input/tab.go new file mode 100644 index 0000000..6d4cfc0 --- /dev/null +++ b/test/input/tab.go @@ -0,0 +1,27 @@ +package main //nolint:revive + +// freeze/issues/50 + +type Config struct { //nolint: revive + Telegram struct { + Token string `env:"TG_TOKEN"` + ChatID string `env:"TG_CHAT"` + OwnerID string `env:"TG_ADMIN"` + } + + Database struct { + DSN string `env:"DB_DSN"` + } + + Log struct { + Level string `env:"LOG_LEVEL"` + } + + Debug bool +} + +func Load() (*Config, error) { //nolint: revive + var c Config + var err error + return &c, err +} diff --git a/test/input/wrap.go b/test/input/wrap.go new file mode 100644 index 0000000..5195fd2 --- /dev/null +++ b/test/input/wrap.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +// freeze/issues/14 + +func main() { + fmt.Println("This is a really long line that is going to go over the 80 character limit. This is a really long line that is going to go over the 80 character limit. This is a really long line that is going to go over the 80 character limit. This is a really long line that is going to go over the 80 character limit.") +}