From 56472fbea18b5399fb9e0eb508a89c017c7a8686 Mon Sep 17 00:00:00 2001 From: Andrejus Kostarevas Date: Thu, 29 Jan 2026 10:48:32 +0000 Subject: [PATCH] vim colour schemes (#70) * feat: vim colour schemes * feat: preview workflow * fix: ci * fix: ci * fix: format --- .github/workflows/vim-preview.yml | 298 ++++++++++++++++++++++++++++++ files/home/.vimrc | 73 +++++++- 2 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/vim-preview.yml 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 = ` + 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 }); diff --git a/files/home/.vimrc b/files/home/.vimrc index d16e2d2..21b1dca 100644 --- a/files/home/.vimrc +++ b/files/home/.vimrc @@ -76,7 +76,7 @@ highlight Constant guifg=#F88C14 highlight String guifg=#2CB494 highlight Number guifg=#F88C14 highlight Identifier guifg=NONE -highlight Function guifg=#0C48CC gui=bold +highlight Function guifg=#7290B8 gui=bold highlight Statement guifg=#4068D4 highlight Keyword guifg=#4068D4 highlight Type guifg=#2CB494 @@ -95,7 +95,7 @@ highlight DiffText guifg=#CCE0D0 guibg=#1A2A3A gui=bold highlight diffAdded guifg=#2CB494 gui=bold highlight diffRemoved guifg=#F40404 gui=bold -highlight diffFile guifg=#0C48CC gui=bold +highlight diffFile guifg=#7290B8 gui=bold highlight diffIndexLine guifg=#88409C highlight diffLine guifg=#00E4FC highlight diffSubname guifg=#F88C14 @@ -114,6 +114,75 @@ highlight FoldColumn guifg=#808080 guibg=NONE highlight QuickFixLine guibg=#1A3050 gui=bold +" UI Groups +highlight Title guifg=#00E4FC gui=bold +highlight Underlined guifg=#CCE0D0 gui=underline +highlight Directory guifg=#7290B8 +highlight MatchParen guifg=#3C3C3C guibg=#2CB494 +highlight NonText guifg=#808080 +highlight SpecialKey guifg=#808080 +highlight EndOfBuffer guifg=#808080 +highlight Conceal guifg=#808080 +highlight CursorLine guibg=#0A2A1A gui=NONE +highlight CursorColumn guibg=#0A2A1A +highlight SignColumn guifg=#808080 guibg=NONE +highlight WarningMsg guifg=#FCFC38 +highlight ErrorMsg guifg=#F40404 gui=bold +highlight ModeMsg guifg=#CCE0D0 gui=bold +highlight MoreMsg guifg=#2CB494 +highlight Question guifg=#2CB494 +highlight WildMenu guifg=#3C3C3C guibg=#FCFC38 +highlight TabLine guifg=#808080 guibg=#000080 gui=NONE +highlight TabLineFill guibg=#000080 +highlight TabLineSel guifg=#CCE0D0 guibg=#000080 gui=bold +highlight SpellLocal guifg=#00E4FC gui=undercurl guisp=#00E4FC + +" Syntax Groups +highlight Boolean guifg=#F88C14 +highlight Float guifg=#F88C14 +highlight Character guifg=#2CB494 +highlight Conditional guifg=#4068D4 +highlight Repeat guifg=#4068D4 +highlight Label guifg=#88409C +highlight Operator guifg=#CCE0D0 +highlight Exception guifg=#F40404 +highlight Include guifg=#F88C14 +highlight Define guifg=#F88C14 +highlight Macro guifg=#F88C14 +highlight PreCondit guifg=#F88C14 +highlight StorageClass guifg=#4068D4 +highlight Structure guifg=#4068D4 +highlight Typedef guifg=#4068D4 +highlight Delimiter guifg=#CCE0D0 +highlight SpecialChar guifg=#88409C +highlight SpecialComment guifg=#FCFC38 +highlight Debug guifg=#F032E6 +highlight Tag guifg=#7290B8 +highlight Ignore guifg=#808080 + +" HTML/Markdown Groups +highlight htmlBold guifg=#CCE0D0 gui=bold +highlight htmlItalic guifg=#CCE0D0 gui=italic +highlight htmlTagName guifg=#4068D4 +highlight htmlLink guifg=#CCE0D0 gui=underline +highlight htmlTag guifg=#808080 +highlight htmlEndTag guifg=#808080 +highlight markdownH1Delimiter guifg=#808080 +highlight markdownH2Delimiter guifg=#808080 +highlight markdownH3Delimiter guifg=#808080 +highlight markdownH4Delimiter guifg=#808080 +highlight markdownH5Delimiter guifg=#808080 +highlight markdownH6Delimiter guifg=#808080 + +" Language-specific Groups +highlight pythonBuiltinObj guifg=#F88C14 +highlight pythonDottedName guifg=#CCE0D0 +highlight rubyInterpolation guifg=#88409C +highlight rubyInterpolationDelimiter guifg=#88409C +highlight rubyBlockParameter guifg=#CCE0D0 +highlight rubyBlockArgument guifg=#CCE0D0 +highlight jsxCloseString guifg=#4068D4 + function! GitRoot() let l:root = systemlist('git rev-parse --show-toplevel')[0] if v:shell_error == 0 && isdirectory(l:root)