diff --git a/.github/workflows/vim-preview.yml b/.github/workflows/vim-preview.yml new file mode 100644 index 0000000..7539ddd --- /dev/null +++ b/.github/workflows/vim-preview.yml @@ -0,0 +1,298 @@ +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 = ` +
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 + 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 + 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 + 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 + # 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; + \`\`\`+ +
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+ +
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 += `