name: Vim Theme Preview on: pull_request: paths: - 'files/home/.vimrc' permissions: contents: write pull-requests: write jobs: preview: name: Generate theme preview runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install Playwright run: | npm install playwright npx playwright install chromium --with-deps - name: Generate HTML preview and take screenshots uses: actions/github-script@v7 with: script: | const fs = require('fs'); // === GENERATE HTML PREVIEW === const vimrc = fs.readFileSync('files/home/.vimrc', 'utf8'); // Parse highlight definitions const highlights = {}; const highlightRegex = /^highlight\s+(\w+)\s+(.+)$/gm; let match; while ((match = highlightRegex.exec(vimrc)) !== null) { const [, group, attrs] = match; const parsed = {}; const guifg = attrs.match(/guifg=(#?\w+)/); const guibg = attrs.match(/guibg=(#?\w+)/); const gui = attrs.match(/gui=(\S+)/); if (guifg) parsed.fg = guifg[1]; if (guibg) parsed.bg = guibg[1]; if (gui) parsed.style = gui[1]; highlights[group] = parsed; } function toStyle(group) { const h = highlights[group] || {}; let style = ''; if (h.fg && h.fg !== 'NONE') style += `color:${h.fg};`; if (h.bg && h.bg !== 'NONE') style += `background:${h.bg};`; if (h.style) { if (h.style.includes('bold')) style += 'font-weight:bold;'; if (h.style.includes('italic')) style += 'font-style:italic;'; if (h.style.includes('underline')) style += 'text-decoration:underline;'; } return style; } const html = ` Vim Color Scheme Preview

Vim Color Scheme Preview

Python

Python
            import os
            from typing import Optional

            @dataclass
            class User:
                # User model
                name: str
                age: int = 0
                active: bool = True

            def process(data: Optional[str])  float:
                if data is None:
                    raise ValueError("No data")
                return 3.14

TypeScript

TypeScript
            import { useState } from 'react';

            interface Props {
                name: string;
                count?: number;
            }

            const Button = ({ name, count = 0 }: Props) => {
                const [active, setActive] = useState(false);
                return (
                    <button onClick={() => setActive(true)}>
                        {name}: {count}
                    </button>
                );
            };

Go

Go
            package main

            import (
                "fmt"
                "errors"
            )

            type User struct {
                Name   string
                Active bool
            }

            func process(id int) (*User, error) {
                if id < 0 {
                    return nil, errors.New("invalid")
                }
                return &User{Active: true}, nil
            }

Ruby

Ruby
            require 'json'

            class User
              attr_accessor :name, :active

              def initialize(name)
                @name = name
                @active = true
              end

              def greet
                "Hello, #{@name}!"
              end
            end

            users = [1, 2, 3].map { |id| User.new("user_#{id}") }

Markdown

Markdown
            # Main Heading
            ## Section Title
            ### Subsection

            Regular paragraph text with **bold** and *italic* text.

            - List item one
            - List item two

            Here's a [link to docs](https://example.com) in the text.

            Inline \`code\` and a code block:

            \`\`\`javascript
            const x = 42;
            \`\`\`

Git Diff

Diff
            diff --git a/file.txt b/file.txt
            index abc123..def456 100644
            @@ -1,5 +1,5 @@
             unchanged line
            -removed line
            +added line
             another unchanged

UI Elements

Messages
            Warning: File has been modified outside of Vim
            E488: Trailing characters: xyz
            Press ENTER or type command to continue
            Save changes? [Y]es, [N]o, [C]ancel:
            -- INSERT --
            TODO: Fix this later
`; fs.writeFileSync('/tmp/preview.html', html); console.log('Generated /tmp/preview.html'); - name: Take screenshots run: | node << 'EOF' const { chromium } = require('playwright'); const fs = require('fs'); (async () => { fs.mkdirSync('/tmp/screenshots', { recursive: true }); const browser = await chromium.launch(); const page = await browser.newPage(); // Use tall viewport to ensure all elements render with correct bounding boxes await page.setViewportSize({ width: 1200, height: 4000 }); await page.goto('file:///tmp/preview.html'); await page.waitForLoadState('networkidle'); const sections = ['python', 'typescript', 'go', 'ruby', 'markdown', 'diff', 'ui']; for (const id of sections) { const heading = await page.$(`#${id}`); const pre = await page.$(`#${id} + pre`); if (!heading || !pre) { console.log(`Skipped: ${id} (not found)`); continue; } // Create a wrapper div for combined screenshot const headingBox = await heading.boundingBox(); const preBox = await pre.boundingBox(); if (!headingBox || !preBox) continue; const padding = 16; const clip = { x: Math.max(0, Math.min(headingBox.x, preBox.x) - padding), y: Math.max(0, headingBox.y - padding), width: Math.max(headingBox.width, preBox.width) + padding * 2, height: (preBox.y + preBox.height) - headingBox.y + padding * 2 }; await page.screenshot({ path: `/tmp/screenshots/${id}.png`, clip }); console.log(`Captured: ${id}.png`); } await browser.close(); })(); EOF - name: Upload images and create PR comment uses: actions/github-script@v7 with: script: | const fs = require('fs'); const { owner, repo } = context.repo; const prNumber = context.issue.number; const sha = context.payload.pull_request.head.sha.slice(0, 7); const files = fs.readdirSync('/tmp/screenshots').filter(f => f.endsWith('.png')); const labels = { python: 'Python', typescript: 'TypeScript', go: 'Go', ruby: 'Ruby', markdown: 'Markdown', diff: 'Git Diff', ui: 'UI Elements' }; // Create release for this commit const tag = `vim-preview-${sha}`; const { data: release } = await github.rest.repos.createRelease({ owner, repo, tag_name: tag, name: `Preview ${sha}`, body: `PR #${prNumber}`, prerelease: true }); // Upload images const urls = {}; for (const file of files) { const { data: asset } = await github.rest.repos.uploadReleaseAsset({ owner, repo, release_id: release.id, name: file, data: fs.readFileSync(`/tmp/screenshots/${file}`) }); urls[file.replace('.png', '')] = asset.browser_download_url; } // Hide previous preview comments const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber }); for (const c of comments) { if (c.user.type === 'Bot' && c.body.includes('')) { await github.graphql(`mutation { minimizeComment(input: {subjectId: "${c.node_id}", classifier: OUTDATED}) { clientMutationId } }`); } } // Create new comment const fullSha = context.payload.pull_request.head.sha; const commitUrl = `https://github.com/${owner}/${repo}/commit/${fullSha}`; const workflowUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; let body = `\n## Vim Theme Preview\n\nCommit: ${commitUrl}\n\n`; for (const [name, url] of Object.entries(urls)) { body += `
${labels[name] || name}\n\n![${name}](${url})\n\n
\n\n`; } body += `---\n*Generated by [workflow](${workflowUrl})*`; await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });