chore: import upstream snapshot with attribution
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
- "*"
|
||||
@@ -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 }}"
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
*.svg
|
||||
*.png
|
||||
!test/golden/**/*.svg
|
||||
!test/golden/**/*.png
|
||||
!examples/*.svg
|
||||
/result
|
||||
dist/
|
||||
@@ -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$
|
||||
@@ -0,0 +1,12 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
version: 2
|
||||
includes:
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-simple.yaml
|
||||
variables:
|
||||
main: "."
|
||||
description: "Generate images of code and terminal output."
|
||||
github_url: "https://github.com/charmbracelet/freeze"
|
||||
maintainer: "Maas Lalani <maas@charm.sh>"
|
||||
brew_commit_author_name: "Maas Lalani"
|
||||
brew_commit_author_email: "maas@charm.sh"
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
.PHONY: test
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
golden:
|
||||
cp -r test/output/* test/golden
|
||||
@@ -0,0 +1,383 @@
|
||||
# Freeze
|
||||
|
||||
<p>
|
||||
<a href="https://stuff.charm.sh/freeze/freeze-4k.png"><img src="https://github.com/charmbracelet/freeze/assets/25087/de76b799-fa67-4b5b-8da2-d990ca5b4e06" width="500" /></a><br>
|
||||
<a href="https://github.com/charmbracelet/freeze/releases"><img src="https://img.shields.io/github/release/charmbracelet/freeze.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/charmbracelet/freeze/actions"><img src="https://github.com/charmbracelet/freeze/workflows/build/badge.svg" alt="Build Status"></a>
|
||||
</p>
|
||||
|
||||
Generate images of code and terminal output.
|
||||
|
||||
<p align="left">
|
||||
<a><img width="600" src="https://vhs.charm.sh/vhs-1C6z5SUKlTdqdj4KL1ADlH.gif" alt="Freeze code screenshot"></a>
|
||||
</p>
|
||||
|
||||
## Examples
|
||||
|
||||
Freeze generates PNGs, SVGs, and WebPs of code and terminal output alike.
|
||||
|
||||
### Generate an image of code
|
||||
|
||||
```sh
|
||||
freeze artichoke.hs -o artichoke.png
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/f15efdda-8e9b-4cb1-9e87-3d32b692eb7c">
|
||||
<img alt="output of freeze command, Haskell code block" src="./test/golden/svg/shadow.svg" width="800" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
### Generate an image of terminal output
|
||||
|
||||
You can use `freeze` to capture ANSI output of a terminal command with the
|
||||
`--execute` flag.
|
||||
|
||||
```bash
|
||||
freeze --execute "eza -lah"
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/aa5447ed-999a-4809-909d-67093d758f5a">
|
||||
<img alt="output of freeze command, ANSI" src="./test/golden/svg/eza.svg" width="800" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Freeze is also [super customizable](#customization) and ships with an [interactive TUI](#interactive-mode).
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
# macOS or Linux
|
||||
brew install charmbracelet/tap/freeze
|
||||
|
||||
# Arch Linux (btw)
|
||||
yay -S freeze
|
||||
|
||||
# Nix
|
||||
nix-env -iA nixpkgs.charm-freeze
|
||||
```
|
||||
|
||||
Or, download it:
|
||||
|
||||
- [Packages][releases] are available in Debian and RPM formats
|
||||
- [Binaries][releases] are available for Linux, macOS, and Windows
|
||||
|
||||
Or, just install it with `go`:
|
||||
|
||||
```sh
|
||||
go install github.com/charmbracelet/freeze@latest
|
||||
```
|
||||
|
||||
[releases]: https://github.com/charmbracelet/freeze/releases
|
||||
|
||||
## Customization
|
||||
|
||||
### Interactive mode
|
||||
|
||||
Freeze features a fully interactive mode for easy customization.
|
||||
|
||||
```bash
|
||||
freeze --interactive
|
||||
```
|
||||
|
||||
<img alt="freeze interactive mode" src="https://vhs.charm.sh/vhs-1AGhIlc2Mtn9Ltc8vPtaAP.gif" width="400" />
|
||||
|
||||
Settings are written to `$XDG_CONFIG/freeze/user.json` and can be accessed with
|
||||
`freeze --config user`.
|
||||
|
||||
### Flags
|
||||
|
||||
Screenshots can be customized with `--flags` or [Configuration](#configuration) files.
|
||||
|
||||
> [!NOTE]
|
||||
> You can view all freeze customization with `freeze --help`.
|
||||
|
||||
- [`-b`](#background), [`--background`](#background): Apply a background fill.
|
||||
- [`-c`](#configuration), [`--config`](#configuration): Base configuration file or template.
|
||||
- [`-l`](#language), [`--language`](#language): Language to apply to code
|
||||
- [`-m`](#margin), [`--margin`](#margin): Apply margin to the window.
|
||||
- [`-o`](#output), [`--output`](#output): Output location for .svg, .png, .jpg.
|
||||
- [`-p`](#padding), [`--padding`](#padding): Apply padding to the code.
|
||||
- [`-r`](#border-radius), [`--border.radius`](#border-radius): Corner radius of window.
|
||||
- [`-t`](#theme), [`--theme`](#theme): Theme to use for syntax highlighting.
|
||||
- [`-w`](#window), [`--window`](#window): Display window controls.
|
||||
- [`-H`](#height), [`--height`](#height): Height of terminal window.
|
||||
- [`--border.width`](#border-width): Border width thickness.
|
||||
- [`--border.color`](#border-width): Border color.
|
||||
- [`--shadow.blur`](#shadow): Shadow Gaussian Blur.
|
||||
- [`--shadow.x`](#shadow): Shadow offset x coordinate.
|
||||
- [`--shadow.y`](#shadow): Shadow offset y coordinate.
|
||||
- [`--font.family`](#font): Font family to use for code.
|
||||
- [`--font.ligatures`](#font): Use ligatures in the font.
|
||||
- [`--font.size`](#font): Font size to use for code.
|
||||
- [`--font.file`](#font): File path to the font to use (embedded in the SVG).
|
||||
- [`--line-height`](#font): Line height relative to font size.
|
||||
- [`--show-line-numbers`](#line-numbers): Show line numbers.
|
||||
- [`--lines`](#line-numbers): Lines to capture (start,end).
|
||||
|
||||
### Language
|
||||
|
||||
If possible, `freeze` auto-detects the language from the file name or analyzing
|
||||
the file contents. Override this inference with the `--language` flag.
|
||||
|
||||
```bash
|
||||
cat artichoke.hs | freeze --language haskell
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/d746f028-3d51-4bfd-ba81-94bbc47b3587">
|
||||
<img alt="output of freeze command, Haskell code block" src="./test/golden/svg/haskell.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Theme
|
||||
|
||||
Change the color theme.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --theme dracula
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/c693dc80-89b8-43c6-a34e-2d49a332d0c7">
|
||||
<img alt="output of freeze command, Haskell code block with dracula theme" src="./test/golden/svg/dracula.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Output
|
||||
|
||||
Change the output file location, defaults to `out.svg` or stdout if piped. This
|
||||
value supports `.svg`, `.png`, `.webp`.
|
||||
|
||||
```bash
|
||||
freeze main.go --output out.svg
|
||||
freeze main.go --output out.png
|
||||
freeze main.go --output out.webp
|
||||
|
||||
# or all of the above
|
||||
freeze main.go --output out.{svg,png,webp}
|
||||
```
|
||||
|
||||
### Font
|
||||
|
||||
Specify the font family, font size, and font line height of the output image.
|
||||
Defaults to `JetBrains Mono`, `14`(px), `1.2`(em).
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs \
|
||||
--font.family "SF Mono" \
|
||||
--font.size 16 \
|
||||
--line-height 1.4
|
||||
```
|
||||
|
||||
You can also embed a font file (in TTF, WOFF, or WOFF2 format) using the
|
||||
`--font.file` flag.
|
||||
|
||||
To use ligatures in the font, you can apply the `--font.ligatures` flag.
|
||||
|
||||
### Line Numbers
|
||||
|
||||
Show line numbers in the terminal window with the `--show-line-numbers` flag.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --show-line-numbers
|
||||
```
|
||||
|
||||
To capture only a specific range of line numbers you can use the `--lines` flag.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --show-line-numbers --lines 2,3
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
|
||||
Add rounded corners to the terminal.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --border.radius 8
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/50932b10-56fd-4312-adbd-f64b36a15441">
|
||||
<img alt="code screenshot with corner radius of 8px" src="./test/golden/svg/border-radius.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Window
|
||||
|
||||
Add window controls to the terminal, macOS-style.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --window
|
||||
```
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/ba46bc4a-fb36-4718-88d4-f63b93343615">
|
||||
<img alt="output of freeze command, Haskell code block with window controls applied" src="./test/golden/svg/window.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Background
|
||||
|
||||
Set the background color of the terminal window.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --background "#08163f"
|
||||
```
|
||||
|
||||
### Height
|
||||
|
||||
Set the height of the terminal window.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --height 400
|
||||
```
|
||||
|
||||
### Border Width
|
||||
|
||||
Add a border outline to the terminal window.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --border.width 1 --border.color "#515151" --border.radius 8
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/b9207976-50fe-479b-974b-e1bf2ad1684c">
|
||||
<img alt="output of freeze command, Haskell code block with border applied" src="./test/golden/svg/border-width.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Padding
|
||||
|
||||
Add padding to the terminal window. You can provide 1, 2, or 4 values.
|
||||
|
||||
```bash
|
||||
freeze main.go --padding 20 # all sides
|
||||
freeze main.go --padding 20,40 # vertical, horizontal
|
||||
freeze main.go --padding 20,60,20,40 # top, right, bottom, left
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/41da80bb-a6d8-402b-9f14-c73050720c0f">
|
||||
<img alt="output of freeze command, Haskell code block with padding applied" src="./test/golden/svg/padding.svg" width="600" />
|
||||
</a>
|
||||
|
||||
### Margin
|
||||
|
||||
Add margin to the terminal window. You can provide 1, 2, or 4 values.
|
||||
|
||||
```bash
|
||||
freeze main.go --margin 20 # all sides
|
||||
freeze main.go --margin 20,40 # vertical, horizontal
|
||||
freeze main.go --margin 20,60,20,40 # top, right, bottom, left
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/eca78a8e-2dbc-4cfa-81da-6c2194925238">
|
||||
<img alt="output of freeze command, Haskell code block with margin applied" src="./test/golden/svg/margin.svg" width="720" />
|
||||
</a>
|
||||
|
||||
### Shadow
|
||||
|
||||
Add a shadow under the terminal window.
|
||||
|
||||
```bash
|
||||
freeze artichoke.hs --shadow.blur 20 --shadow.x 0 --shadow.y 10
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
<a href="https://github.com/charmbracelet/freeze/assets/42545625/b9a8cc17-5ec6-4004-941b-5ae1b65d2b18">
|
||||
<img alt="output of freeze command, Haskell code block with a shadow" src="./test/golden/svg/shadow.svg" width="720" />
|
||||
</a>
|
||||
|
||||
## Screenshot TUIs
|
||||
|
||||
Use `tmux capture-pane` to generate screenshots of TUIs.
|
||||
|
||||
Run your TUI in `tmux` and get it to the state you want to capture.
|
||||
Next, use `capture-pane` to capture the pane and pipe that to freeze.
|
||||
|
||||
```bash
|
||||
hx # in a separate pane
|
||||
tmux capture-pane -pet 1 | freeze -c full
|
||||
```
|
||||
|
||||
<img width="650px" src="./test/golden/svg/helix.svg" alt="helix captured with freeze">
|
||||
|
||||
## Configuration
|
||||
|
||||
Freeze also supports configuration via a JSON file which can be passed with the
|
||||
`--config` / `-c` flag. In general, all `--flag` options map directly to keys
|
||||
and values in the config file
|
||||
|
||||
There are also some default configurations built into `freeze` which can be passed by name.
|
||||
|
||||
- `base`: Simple screenshot of code.
|
||||
- `full`: macOS-like screenshot.
|
||||
- `user`: Uses `~/.config/freeze/user.json`.
|
||||
|
||||
If you use `--interactive` mode, a configuration file will be created for you at
|
||||
`~/.config/freeze/user.json`. This will be the default configuration file used
|
||||
in your screenshots.
|
||||
|
||||
```bash
|
||||
freeze -c base main.go
|
||||
freeze -c full main.go
|
||||
freeze -c user main.go # alias for ~/.config/freeze/user.json
|
||||
freeze -c ./custom.json main.go
|
||||
```
|
||||
|
||||
Here's what an example configuration looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"window": false,
|
||||
"border": {
|
||||
"radius": 0,
|
||||
"width": 0,
|
||||
"color": "#515151"
|
||||
},
|
||||
"shadow": false,
|
||||
"padding": [20, 40, 20, 20],
|
||||
"margin": "0",
|
||||
"font": {
|
||||
"family": "JetBrains Mono",
|
||||
"size": 14
|
||||
},
|
||||
"line_height": 1.2
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
See [contributing][contribute].
|
||||
|
||||
[contribute]: https://github.com/charmbracelet/freeze/contribute
|
||||
|
||||
## Feedback
|
||||
|
||||
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).
|
||||
|
||||
<a href="https://charm.sh/">
|
||||
<img
|
||||
alt="The Charm logo"
|
||||
width="400"
|
||||
src="https://stuff.charm.sh/charm-badge.jpg"
|
||||
/>
|
||||
</a>
|
||||
|
||||
Charm热爱开源 • Charm loves open source
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`charmbracelet/freeze`
|
||||
- 原始仓库:https://github.com/charmbracelet/freeze
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please email vt100@charm.sh
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{pkgs}:
|
||||
pkgs.buildGoModule {
|
||||
name = "freeze";
|
||||
src = ./.;
|
||||
vendorHash = "sha256-AUFzxmQOb/h0UgcprY09IVI7Auitn3JTDU/ptKicIAU=";
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.assets.fonts.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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",
|
||||
}))
|
||||
@@ -0,0 +1,121 @@
|
||||
package svg //nolint:revive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// AddShadow adds a definition of a shadow to the <defs> with the given id.
|
||||
func AddShadow(element *etree.Element, id string, x, y, blur float64) {
|
||||
f := etree.NewElement("filter")
|
||||
f.CreateAttr("id", id)
|
||||
f.CreateAttr("filterUnits", "userSpaceOnUse")
|
||||
|
||||
b := etree.NewElement("feGaussianBlur")
|
||||
b.CreateAttr("in", "SourceAlpha")
|
||||
b.CreateAttr("stdDeviation", fmt.Sprintf("%.2f", blur))
|
||||
|
||||
o := etree.NewElement("feOffset")
|
||||
o.CreateAttr("result", "offsetblur")
|
||||
o.CreateAttr("dx", fmt.Sprintf("%.2f", x))
|
||||
o.CreateAttr("dy", fmt.Sprintf("%.2f", y))
|
||||
|
||||
m := etree.NewElement("feMerge")
|
||||
mn1 := etree.NewElement("feMergeNode")
|
||||
mn2 := etree.NewElement("feMergeNode")
|
||||
mn2.CreateAttr("in", "SourceGraphic")
|
||||
m.AddChild(mn1)
|
||||
m.AddChild(mn2)
|
||||
|
||||
f.AddChild(b)
|
||||
f.AddChild(o)
|
||||
f.AddChild(m)
|
||||
|
||||
defs := etree.NewElement("defs")
|
||||
defs.AddChild(f)
|
||||
element.AddChild(defs)
|
||||
}
|
||||
|
||||
// AddClipPath adds a definition of a clip path to the <defs> with the given id.
|
||||
func AddClipPath(element *etree.Element, id string, x, y, w, h float64) {
|
||||
p := etree.NewElement("clipPath")
|
||||
p.CreateAttr("id", id)
|
||||
|
||||
rect := etree.NewElement("rect")
|
||||
rect.CreateAttr("x", fmt.Sprintf("%.2f", x))
|
||||
rect.CreateAttr("y", fmt.Sprintf("%.2f", y))
|
||||
rect.CreateAttr("width", fmt.Sprintf("%.2f", w))
|
||||
rect.CreateAttr("height", fmt.Sprintf("%.2f", h))
|
||||
|
||||
p.AddChild(rect)
|
||||
|
||||
defs := etree.NewElement("defs")
|
||||
defs.AddChild(p)
|
||||
element.AddChild(defs)
|
||||
}
|
||||
|
||||
// AddCornerRadius adds corner radius to an element.
|
||||
func AddCornerRadius(e *etree.Element, radius float64) {
|
||||
e.CreateAttr("rx", fmt.Sprintf("%.2f", radius))
|
||||
e.CreateAttr("ry", fmt.Sprintf("%.2f", radius))
|
||||
}
|
||||
|
||||
// Move moves the given element to the (x, y) position.
|
||||
func Move(e *etree.Element, x, y float64) {
|
||||
e.CreateAttr("x", fmt.Sprintf("%.2fpx", x))
|
||||
e.CreateAttr("y", fmt.Sprintf("%.2fpx", y))
|
||||
}
|
||||
|
||||
// AddOutline adds an outline to the given element.
|
||||
func AddOutline(e *etree.Element, width float64, color string) {
|
||||
e.CreateAttr("stroke", color)
|
||||
e.CreateAttr("stroke-width", fmt.Sprintf("%.2f", width))
|
||||
}
|
||||
|
||||
const (
|
||||
red string = "#FF5A54"
|
||||
yellow string = "#E6BF29"
|
||||
green string = "#52C12B"
|
||||
)
|
||||
|
||||
// NewWindowControls returns a colorful window bar element.
|
||||
func NewWindowControls(r float64, x, y float64) *etree.Element {
|
||||
bar := etree.NewElement("svg")
|
||||
for i, color := range []string{red, yellow, green} {
|
||||
circle := etree.NewElement("circle")
|
||||
circle.CreateAttr("cx", fmt.Sprintf("%.2f", float64(i+1)*float64(x)-float64(r)))
|
||||
circle.CreateAttr("cy", fmt.Sprintf("%.2f", y))
|
||||
circle.CreateAttr("r", fmt.Sprintf("%.2f", r))
|
||||
circle.CreateAttr("fill", color)
|
||||
bar.AddChild(circle)
|
||||
}
|
||||
return bar
|
||||
}
|
||||
|
||||
// SetDimensions sets the width and height of the given element.
|
||||
func SetDimensions(element *etree.Element, width, height float64) {
|
||||
widthAttr := element.SelectAttr("width")
|
||||
heightAttr := element.SelectAttr("height")
|
||||
heightAttr.Value = fmt.Sprintf("%.2f", height)
|
||||
widthAttr.Value = fmt.Sprintf("%.2f", width)
|
||||
}
|
||||
|
||||
// GetDimensions returns the width and height of the element.
|
||||
func GetDimensions(element *etree.Element) (int, int) {
|
||||
widthValue := element.SelectAttrValue("width", "0px")
|
||||
heightValue := element.SelectAttrValue("height", "0px")
|
||||
width := dimensionToInt(widthValue)
|
||||
height := dimensionToInt(heightValue)
|
||||
return width, height
|
||||
}
|
||||
|
||||
// dimensionToInt takes a string and returns the integer value.
|
||||
// e.g. "500px" -> 500.
|
||||
func dimensionToInt(px string) int {
|
||||
d := strings.TrimSuffix(px, "px")
|
||||
v, _ := strconv.ParseInt(d, 10, 64)
|
||||
return int(v)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f52bce59d0380e71d1a672221ede9640f42475a573a760a30ec49fa8543777fd
|
||||
size 207818
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
|
||||
size 147861
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc8f829d28156540257cf13857c34c68b98cd55a322b248f41d569f8c1e43b37
|
||||
size 211518
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d876c87dc4434c2cd43618096b7e156eef8d7b91e9cec8bdaf758fe970894629
|
||||
size 149366
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d448145affbe77c87aaa75df6c7749b0116f8f9b0de0dba185a8bc2ecaad8841
|
||||
size 153649
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf3b282c57a1014d0af882d275833803d55cfafb19cf4b6f1c2678ee24cb210e
|
||||
size 92440
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1c0fb0d3979aab6458e649afad987b2aa565b6d488ec31145773e389687a895
|
||||
size 26812
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f72a36395b12d483cf6181aa4c71fd84a14a2782f0448505c928dda2b34113bd
|
||||
size 19441
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e740f62d59af375f2001989fdcdf1baa84bc92ede18d1674476d3ae07f672e0
|
||||
size 17848
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:545a98f24051d5e2ee418a01ba14536222fba1f4abe32a2741bc88850d9e365c
|
||||
size 18086
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba1d14aa0e7ba09bc5011b82b3dc59379dd5df3fb76da587ec38b876ad031efa
|
||||
size 39809
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:20f9e3afe71cab4408cdf36b6bfb8276d8fb6bdc2846249c09b4a08477904836
|
||||
size 155158
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f0159e6ad36e814a46bc3ed7a5f6f52ea4fdfc5ff543e78f1b5805ea2f43526
|
||||
size 8545
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8b2a28a393837a46e16862205d6de3dba9ebd47a8af27e0c5b48fd7e906e1450
|
||||
size 192707
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
|
||||
size 147861
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d9f4173e0080bd8019ef18e77b318f0e43325cf5d63c7004384458207357505
|
||||
size 296701
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f06b531e59cdc66acc74a549a2033c5a523a555536ec6f11a9fa0decf9c8d28b
|
||||
size 276597
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7bf9953af89ae68326f637789e870b7010ddfae08b8da1c17e296b61eadba2d
|
||||
size 147861
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d61b33e0201f891d3da4c262edefde0c59196360b8d729601ddf47bd98439676
|
||||
size 73456
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ed561fcdc7f04e8f0013e7e249082262a930e8cf9275107f41b8cad3c66376a
|
||||
size 717940
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d5c4a9970c91ee8133a9bc07ff53f14a18a1a2b63ce6ad58755b2e90788eca2a
|
||||
size 157530
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fef8f6ec4e6cfed47ec49285596a9479cbde27e93d301356dc864b9a3c665134
|
||||
size 112089
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e7d453086e221a1dc2cc182e382abb157e39987fedf93a4faf6fd019bc06b507
|
||||
size 168045
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a88047ee5a0168829361090bc856dccd91f8685f2bd958e959c89efda6f48d2
|
||||
size 432565
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2a72b7dbe7d21c11a04be68c4ae6e38b60a79cc4c0dfe80cc5f820b2f017a3b
|
||||
size 123387
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90328caeded476f2d9b4f50af2f9c19c99c217dd6bbefd62b9ac543ef0bcde9c
|
||||
size 155758
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c242915353fe2e6a18de496109fc9b8104360063ee57e910d45279214c641780
|
||||
size 220401
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:142c5bb67207d6c4516347c270b156f5063aa7c750e47b4a468b734c52916813
|
||||
size 152003
|
||||
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 365 KiB |