Skip to content

Commit

Permalink
add support for executing terminal commands and highlighting ansi esc…
Browse files Browse the repository at this point in the history
…apes
  • Loading branch information
watzon committed Nov 23, 2024
1 parent f4a04b5 commit 4e34a9f
Show file tree
Hide file tree
Showing 9 changed files with 654 additions and 61 deletions.
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/cmd/goshot",
"args": ["--execute", "/home/watzon/.asdf/shims/goshot help"]
}
]
}
116 changes: 96 additions & 20 deletions cmd/goshot/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"bytes"
"fmt"
"image"
Expand Down Expand Up @@ -34,9 +35,10 @@ var (
title lipgloss.Style
subtitle lipgloss.Style
error lipgloss.Style
success lipgloss.Style
info lipgloss.Style
groupTitle lipgloss.Style
successBox lipgloss.Style
infoBox lipgloss.Style
}{
title: lipgloss.NewStyle().
Bold(true).
Expand All @@ -48,10 +50,14 @@ var (
error: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#d70000", Dark: "#FF5555"}),
success: lipgloss.NewStyle().
successBox: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#FFFFFF"}).
Background(lipgloss.AdaptiveColor{Light: "#2E7D32", Dark: "#388E3C"}),
infoBox: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#FFFFFF"}).
Background(lipgloss.AdaptiveColor{Light: "#0087af", Dark: "#8BE9FD"}),
info: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#0087af", Dark: "#8BE9FD"}),
groupTitle: lipgloss.NewStyle().
Expand All @@ -71,7 +77,7 @@ type Config struct {
FromClipboard bool
ToStdout bool
ExecuteCommand string
ShowCommand bool
ShowPrompt bool
AutoTitle bool

// Appearance
Expand All @@ -84,7 +90,7 @@ type Config struct {
BackgroundColor string
BackgroundImage string
BackgroundImageFit string
ShowLineNumbers bool
NoLineNumbers bool
CornerRadius float64
NoWindowControls bool
WindowTitle string
Expand Down Expand Up @@ -121,6 +127,15 @@ type Config struct {
HighlightLines string
}

// logMessage prints a styled message with consistent alignment
func logMessage(box lipgloss.Style, tag string, message string) {
// Set a consistent width for the tag box and center the text
const boxWidth = 11 // 9 characters + 2 padding spaces
paddedTag := fmt.Sprintf("%*s", -boxWidth, tag)
centeredBox := box.Width(boxWidth).Align(lipgloss.Center)
fmt.Println(centeredBox.Render(paddedTag) + " " + styles.info.Render(message))
}

func main() {
var config Config

Expand Down Expand Up @@ -166,7 +181,7 @@ func main() {
outputFlagSet.BoolVar(&config.FromClipboard, "from-clipboard", false, "Read input from clipboard")
outputFlagSet.BoolVarP(&config.ToStdout, "to-stdout", "s", false, "Write output to stdout")
outputFlagSet.StringVar(&config.ExecuteCommand, "execute", "", "Execute command and use output as input")
outputFlagSet.BoolVar(&config.ShowCommand, "show-command", false, "Show the command used to generate the screenshot")
outputFlagSet.BoolVar(&config.ShowPrompt, "show-prompt", false, "Show the prompt used to generate the screenshot")
outputFlagSet.BoolVar(&config.AutoTitle, "auto-title", false, "Automatically set the window title to the filename or command")

rootCmd.Flags().AddFlagSet(outputFlagSet)
Expand All @@ -185,7 +200,7 @@ func main() {
appearanceFlagSet.StringVarP(&config.BackgroundColor, "background", "b", "#aaaaff", "Background color")
appearanceFlagSet.StringVar(&config.BackgroundImage, "background-image", "", "Background image path")
appearanceFlagSet.StringVar(&config.BackgroundImageFit, "background-image-fit", "cover", "Background image fit (contain, cover, fill, stretch, tile)")
appearanceFlagSet.BoolVar(&config.ShowLineNumbers, "no-line-number", false, "Hide line numbers")
appearanceFlagSet.BoolVar(&config.NoLineNumbers, "no-line-number", false, "Hide line numbers")
appearanceFlagSet.Float64Var(&config.CornerRadius, "corner-radius", 10.0, "Corner radius of the image")
appearanceFlagSet.BoolVar(&config.NoWindowControls, "no-window-controls", false, "Hide window controls")
appearanceFlagSet.StringVar(&config.WindowTitle, "window-title", "", "Window title")
Expand Down Expand Up @@ -394,19 +409,77 @@ func renderImage(config *Config, echo bool, args []string) error {

switch {
case config.ExecuteCommand != "":
// Execute command and capture output
cmd := exec.Command("sh", "-c", config.ExecuteCommand)
var stdout bytes.Buffer
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute command: %v", err)
env := append(os.Environ(),
"TERM=xterm-256color", // Support 256 colors
"COLORTERM=truecolor", // Support 24-bit true color
"FORCE_COLOR=1", // Generic force color
"CLICOLOR_FORCE=1", // BSD apps
"CLICOLOR=1", // BSD apps
"NO_COLOR=", // Clear NO_COLOR
"COLUMNS=120", // Set terminal width
"LINES=40", // Set terminal height
)

logMessage(styles.infoBox, "EXECUTING", config.ExecuteCommand)

// Try script first as it's most reliable for TTY emulation
cmd := exec.Command("script", "-qfec", config.ExecuteCommand, "/dev/null")
cmd.Env = env
cmd.Dir, _ = os.Getwd() // Set working directory to current directory

// Create pipes for stdout
r, w := io.Pipe()
bufR := bufio.NewReaderSize(r, 32*1024) // 32KB buffer
cmd.Stdout = w
cmd.Stderr = w

// Start the command
if err := cmd.Start(); err != nil {
// If script fails, try stdbuf
cmd = exec.Command("stdbuf", "-o0", "-e0", "sh", "-c", config.ExecuteCommand)
cmd.Stdout = w
cmd.Stderr = w
cmd.Env = env

if err := cmd.Start(); err != nil {
// If stdbuf fails, try unbuffer
cmd = exec.Command("unbuffer", "-p", config.ExecuteCommand)
cmd.Stdout = w
cmd.Stderr = w
cmd.Env = env

if err := cmd.Start(); err != nil {
// Last resort: direct execution
cmd = exec.Command("sh", "-c", config.ExecuteCommand)
cmd.Stdout = w
cmd.Stderr = w
cmd.Env = env

if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start command: %v", err)
}
}
}
}
config.Language = "shell"
if config.ShowCommand {
code = fmt.Sprintf("$ %s\n%s", config.ExecuteCommand, stdout.String())
} else {
code = stdout.String()

// Copy output in a goroutine
doneChan := make(chan struct{})
go func() {
defer w.Close()
defer close(doneChan)
cmd.Wait()
}()

// Read the output
if _, err := io.Copy(&stdout, bufR); err != nil {
return fmt.Errorf("failed to read command output: %v", err)
}

<-doneChan

code = stdout.String()
config.NoLineNumbers = true
case config.FromClipboard:
// Read from clipboard
code, err = clipboard.ReadAll()
Expand Down Expand Up @@ -607,6 +680,9 @@ func renderImage(config *Config, echo bool, args []string) error {

// Configure code style
canvas.SetCodeStyle(&render.CodeStyle{
UseANSI: config.ExecuteCommand != "",
ShowPrompt: config.ShowPrompt,
PromptCommand: config.ExecuteCommand,
Language: config.Language,
Theme: strings.ToLower(config.Theme),
FontFamily: requestedFont,
Expand All @@ -616,7 +692,7 @@ func renderImage(config *Config, echo bool, args []string) error {
PaddingRight: config.CodePadRight,
PaddingTop: config.CodePadTop,
PaddingBottom: config.CodePadBottom,
ShowLineNumbers: !config.ShowLineNumbers,
ShowLineNumbers: !config.NoLineNumbers,
LineNumberRange: render.LineRange{
Start: config.StartLine,
End: config.EndLine,
Expand All @@ -643,7 +719,7 @@ func renderImage(config *Config, echo bool, args []string) error {
}

if echo {
fmt.Println(styles.success.Render(" COPIED ") + " to clipboard")
logMessage(styles.successBox, "COPIED", "to clipboard")
}
}

Expand All @@ -659,7 +735,7 @@ func renderImage(config *Config, echo bool, args []string) error {
err = saveImage(img, config)
if err == nil {
if echo {
fmt.Println(styles.success.Render(" WROTE ") + " " + config.OutputFile)
logMessage(styles.successBox, "WROTE", config.OutputFile)
}
} else {
return fmt.Errorf("failed to save image: %v", err)
Expand All @@ -671,7 +747,7 @@ func renderImage(config *Config, echo bool, args []string) error {
func saveImage(img image.Image, config *Config) error {
// If no output file is specified, use png as default
if config.OutputFile == "" {
config.OutputFile = "screenshot.png"
config.OutputFile = "output.png"
}

// Get the extension from the filename
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand All @@ -28,6 +29,7 @@ require (
)

require (
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/fogleman/gg v1.3.0
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
Expand All @@ -13,10 +15,15 @@ github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOY
github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM=
github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
Expand All @@ -35,6 +42,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
Expand All @@ -44,6 +52,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
Expand All @@ -57,6 +67,7 @@ golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4=
Expand Down
10 changes: 5 additions & 5 deletions pkg/chrome/gnome.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const (
gnomeDefaultTitleBarHeight = 32
gnomeDefaultControlSize = 16
gnomeDefaultControlSpacing = 8
gnomeDefaultTitleFontSize = 18
gnomeDefaultTitleFontSize = 14
gnomeDefaultControlPadding = 8
gnomeDefaultCornerRadius = 8.0
adwaitaTitleBarHeight = 45
Expand Down Expand Up @@ -57,7 +57,7 @@ func registerAdwaitaTheme() {
Name: "adwaita",
Properties: ThemeProperties{
TitleFont: "Cantarell",
TitleFontSize: 18,
TitleFontSize: adwaitaTitleFontSize,
TitleBackground: color.RGBA{R: 242, G: 242, B: 242, A: 255},
TitleText: color.RGBA{R: 40, G: 40, B: 40, A: 255},
ControlsColor: color.RGBA{R: 40, G: 40, B: 40, A: 255},
Expand All @@ -83,7 +83,7 @@ func registerAdwaitaTheme() {
Name: "adwaita",
Properties: ThemeProperties{
TitleFont: "Cantarell",
TitleFontSize: 18,
TitleFontSize: adwaitaTitleFontSize,
TitleBackground: color.RGBA{R: 36, G: 36, B: 36, A: 255},
TitleText: color.RGBA{R: 255, G: 255, B: 255, A: 255},
ControlsColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Expand Down Expand Up @@ -114,7 +114,7 @@ func registerBreezeTheme() {
Name: "breeze",
Properties: ThemeProperties{
TitleFont: "Cantarell",
TitleFontSize: 14,
TitleFontSize: gnomeDefaultTitleFontSize,
TitleBackground: color.RGBA{R: 205, G: 209, B: 214, A: 255},
TitleText: color.RGBA{R: 109, G: 113, B: 120, A: 255},
ControlsColor: color.RGBA{R: 40, G: 40, B: 40, A: 255},
Expand All @@ -140,7 +140,7 @@ func registerBreezeTheme() {
Name: "breeze",
Properties: ThemeProperties{
TitleFont: "Cantarell",
TitleFontSize: 14,
TitleFontSize: gnomeDefaultTitleFontSize,
TitleBackground: color.RGBA{R: 68, G: 82, B: 91, A: 255},
TitleText: color.RGBA{R: 255, G: 255, B: 255, A: 255},
ControlsColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Expand Down
8 changes: 8 additions & 0 deletions pkg/render/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ type CodeStyle struct {
ShowLineNumbers bool
LineNumberRange LineRange
LineHighlightRanges []LineRange
ShowPrompt bool
PromptCommand string

// Rendering options
UseANSI bool
FontSize float64
FontFamily *fonts.Font
LineHeight float64
Expand Down Expand Up @@ -89,6 +92,8 @@ func NewCanvas() *Canvas {
MinWidth: 0,
MaxWidth: 0,
LineNumberPadding: 16,
ShowPrompt: false,
PromptCommand: "",
},
}
}
Expand Down Expand Up @@ -147,6 +152,9 @@ func (c *Canvas) SetLineHeight(height float64) *Canvas {
func (c *Canvas) RenderToImage(code string) (image.Image, error) {
// Get highlighted code
highlightOpts := &syntax.HighlightOptions{
UseANSI: c.codeStyle.UseANSI,
ShowPrompt: c.codeStyle.ShowPrompt,
PromptCommand: c.codeStyle.PromptCommand,
Style: c.codeStyle.Theme,
Language: c.codeStyle.Language,
TabWidth: c.codeStyle.TabWidth,
Expand Down
Loading

0 comments on commit 4e34a9f

Please sign in to comment.