|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "image/color" |
| 6 | + "log" |
| 7 | + "math" |
| 8 | + |
| 9 | + "github.com/boombuler/barcode" |
| 10 | + "github.com/boombuler/barcode/code128" |
| 11 | + "github.com/fogleman/gg" |
| 12 | + "golang.org/x/image/font" |
| 13 | + "golang.org/x/image/font/gofont/goregular" |
| 14 | + "golang.org/x/image/font/opentype" |
| 15 | +) |
| 16 | + |
| 17 | +// NOTE: The scanner always appends <CR> (Enter). Therefore: |
| 18 | +// - All Code values below DO NOT include "<CR>" or a newline. |
| 19 | +// - They are mostly ":"-style ex commands where Enter is expected. |
| 20 | + |
| 21 | +// VimOp represents a single barcode entry. |
| 22 | +type VimOp struct { |
| 23 | + Code string // Exact string encoded in the barcode (no <CR>) |
| 24 | + Label string // Short label shown under barcode |
| 25 | + Description string // Human description |
| 26 | +} |
| 27 | + |
| 28 | +// Curated set of multi-keystroke commands where automatic <CR> is useful. |
| 29 | +var vimOps = []VimOp{ |
| 30 | + // --- Files: write / quit / reload / sudo tricks --- |
| 31 | + {":w", ":w", "Write current file"}, |
| 32 | + {":wa", ":wa", "Write all files"}, |
| 33 | + {":q", ":q", "Quit (fails if unsaved)"}, |
| 34 | + {":wq", ":wq", "Write & quit"}, |
| 35 | + {":wqa", ":wqa", "Write & quit all"}, |
| 36 | + {":x", ":x", "Write if changed & quit"}, |
| 37 | + {":q!", ":q!", "Force quit without saving"}, |
| 38 | + {":w!", ":w!", "Force write (read-only files)"}, |
| 39 | + {":e!", ":e!", "Reload file (discard changes)"}, |
| 40 | + {":up", ":up", "Write only if buffer changed"}, |
| 41 | + {":w ++ff=unix", ":w ++ff=unix", "Write with Unix fileformat"}, |
| 42 | + {":w ++ff=dos", ":w ++ff=dos", "Write with DOS fileformat"}, |
| 43 | + {":!sudo tee %", ":!sudo tee %", "Write as root via sudo tee"}, |
| 44 | + |
| 45 | + // --- Buffer / file navigation --- |
| 46 | + {":ls", ":ls", "List buffers"}, |
| 47 | + {":bnext", ":bnext", "Next buffer"}, |
| 48 | + {":bprev", ":bprev", "Previous buffer"}, |
| 49 | + {":bfirst", ":bfirst", "First buffer"}, |
| 50 | + {":blast", ":blast", "Last buffer"}, |
| 51 | + {":b#", ":b#", "Alternate buffer"}, |
| 52 | + {":bd", ":bd", "Delete current buffer"}, |
| 53 | + {":bufdo wqa", ":bufdo wqa", "Write & quit all buffers"}, |
| 54 | + {":edit .", ":edit .", "Open file explorer (netrw)"}, |
| 55 | + {":Explore", ":Explore", "Netrw file explorer"}, |
| 56 | + {":Hexplore", ":Hexplore", "Horizontal explorer split"}, |
| 57 | + {":Vexplore", ":Vexplore", "Vertical explorer split"}, |
| 58 | + |
| 59 | + // --- Windows & splits --- |
| 60 | + {":sp", ":sp", "Horizontal split"}, |
| 61 | + {":vsp", ":vsp", "Vertical split"}, |
| 62 | + {":only", ":only", "Close all other windows"}, |
| 63 | + {":close", ":close", "Close current window"}, |
| 64 | + {":new", ":new", "New empty window"}, |
| 65 | + {":vnew", ":vnew", "New empty vertical split"}, |
| 66 | + {":wincmd = ", ":wincmd =", "Equalize split sizes"}, |
| 67 | + {":wincmd H", ":wincmd H", "Move window to far left"}, |
| 68 | + {":wincmd J", ":wincmd J", "Move window to bottom"}, |
| 69 | + {":wincmd K", ":wincmd K", "Move window to top"}, |
| 70 | + {":wincmd L", ":wincmd L", "Move window to far right"}, |
| 71 | + |
| 72 | + // --- Tabs --- |
| 73 | + {":tabnew", ":tabnew", "New tab"}, |
| 74 | + {":tabclose", ":tabclose", "Close current tab"}, |
| 75 | + {":tabonly", ":tabonly", "Close all other tabs"}, |
| 76 | + {":tabnext", ":tabnext", "Next tab"}, |
| 77 | + {":tabprev", ":tabprev", "Previous tab"}, |
| 78 | + {":tabmove 0", ":tabmove 0", "Move tab to front"}, |
| 79 | + {":tabmove$", ":tabmove$", "Move tab to end"}, |
| 80 | + |
| 81 | + // --- Search & highlight behaviour --- |
| 82 | + {":noh", ":noh", "Clear search highlight"}, |
| 83 | + {":set hlsearch", "hlsearch", "Highlight all search matches"}, |
| 84 | + {":set nohlsearch", "nohlsearch", "Disable search highlight"}, |
| 85 | + {":set incsearch", "incsearch", "Incremental search"}, |
| 86 | + {":set noincsearch", "noincsearch", "Disable incremental search"}, |
| 87 | + {":set ignorecase", "ignorecase", "Case-insensitive search"}, |
| 88 | + {":set noignorecase", "noignorecase", "Case-sensitive search"}, |
| 89 | + {":set smartcase", "smartcase", "Smart case search"}, |
| 90 | + {":set nosmartcase", "nosmartcase", "Disable smart case"}, |
| 91 | + |
| 92 | + // --- Indent / tabs / formatting --- |
| 93 | + {":set autoindent", "autoindent", "Enable auto indent"}, |
| 94 | + {":set noautoindent", "noautoindent", "Disable auto indent"}, |
| 95 | + {":set smartindent", "smartindent", "Enable smart indent"}, |
| 96 | + {":set nosmartindent", "nosmartindent", "Disable smart indent"}, |
| 97 | + {":set expandtab", "expandtab", "Convert tabs to spaces"}, |
| 98 | + {":set noexpandtab", "noexpandtab", "Keep literal tabs"}, |
| 99 | + {":set tabstop=2", "ts=2", "Tab width = 2"}, |
| 100 | + {":set tabstop=4", "ts=4", "Tab width = 4"}, |
| 101 | + {":set shiftwidth=2", "sw=2", "Indent width = 2"}, |
| 102 | + {":set shiftwidth=4", "sw=4", "Indent width = 4"}, |
| 103 | + {":set softtabstop=2", "sts=2", "Soft tabstop = 2"}, |
| 104 | + {":set softtabstop=4", "sts=4", "Soft tabstop = 4"}, |
| 105 | + {":retab", ":retab", "Convert indentation to current settings"}, |
| 106 | + |
| 107 | + // --- Background / colours / UI tweaks --- |
| 108 | + {":set background=dark", "bg=dark", "Dark background"}, |
| 109 | + {":set background=light", "bg=light", "Light background"}, |
| 110 | + {":set number", "number", "Show line numbers"}, |
| 111 | + {":set nonumber", "nonumber", "Hide line numbers"}, |
| 112 | + {":set relativenumber", "relativenumber", "Relative line numbers"}, |
| 113 | + {":set norelativenumber", "norelativenumber", "Disable relative numbers"}, |
| 114 | + {":set cursorline", "cursorline", "Highlight current line"}, |
| 115 | + {":set nocursorline", "nocursorline", "Disable line highlight"}, |
| 116 | + {":set list", "list", "Show invisible chars"}, |
| 117 | + {":set nolist", "nolist", "Hide invisible chars"}, |
| 118 | + {":set wrap", "wrap", "Wrap long lines"}, |
| 119 | + {":set nowrap", "nowrap", "No wrap; horizontal scroll"}, |
| 120 | + {":set colorcolumn=80", "cc=80", "Mark column 80"}, |
| 121 | + {":set colorcolumn=", "cc=", "Clear colorcolumn"}, |
| 122 | + {":set showmatch", "showmatch", "Briefly jump to matching bracket"}, |
| 123 | + {":set noshowmatch", "noshowmatch", "Disable showmatch"}, |
| 124 | + {":set ruler", "ruler", "Show cursor position"}, |
| 125 | + {":set noruler", "noruler", "Hide ruler"}, |
| 126 | + {":set showcmd", "showcmd", "Show partial commands"}, |
| 127 | + {":set noshowcmd", "noshowcmd", "Hide partial commands"}, |
| 128 | + |
| 129 | + // --- Spellchecking --- |
| 130 | + {":set spell", "spell", "Enable spell checking"}, |
| 131 | + {":set nospell", "nospell", "Disable spell checking"}, |
| 132 | + {":set spelllang=en_au", "spelllang=en_au", "Set spell lang to en_au"}, |
| 133 | + {":set spelllang=en_gb", "spelllang=en_gb", "Set spell lang to en_gb"}, |
| 134 | + |
| 135 | + // --- Mouse / paste / misc convenience --- |
| 136 | + {":set mouse=a", "mouse=a", "Enable mouse in all modes"}, |
| 137 | + {":set mouse=", "mouse=", "Disable mouse"}, |
| 138 | + {":set paste", "paste", "Enable paste mode"}, |
| 139 | + {":set nopaste", "nopaste", "Disable paste mode"}, |
| 140 | + {":set clipboard=unnamedplus", "clipboard=unnamedplus", "Use system clipboard"}, |
| 141 | + {":set clipboard=", "clipboard=", "Use default Vim registers"}, |
| 142 | + {":set foldmethod=indent", "fold=indent", "Fold by indent level"}, |
| 143 | + {":set foldmethod=manual", "fold=manual", "Manual folding"}, |
| 144 | + {":set foldenable", "foldenable", "Enable folding"}, |
| 145 | + {":set nofoldenable", "nofoldenable", "Disable folding"}, |
| 146 | + |
| 147 | + // --- Global substitutions & quick refactors --- |
| 148 | + {":%s/old/new/g", ":%s/old/new/g", "Substitute in whole file"}, |
| 149 | + {":%s/old/new/gc", ":%s/old/new/gc", "Substitute with confirm"}, |
| 150 | + {":%s/\\s\\+$//e", ":%s/\\s\\+$//e", "Strip trailing whitespace"}, |
| 151 | + {":g/DEBUG/d", ":g/DEBUG/d", "Delete all lines containing DEBUG"}, |
| 152 | + {":vimgrep /TODO/ **/*", ":vimgrep /TODO/ **/*", "Search TODO in project"}, |
| 153 | + {":copen", ":copen", "Open quickfix window"}, |
| 154 | + {":cclose", ":cclose", "Close quickfix window"}, |
| 155 | +} |
| 156 | + |
| 157 | +// font cache so we only parse goregular once per size |
| 158 | +var fontCache = map[float64]font.Face{} |
| 159 | + |
| 160 | +func main() { |
| 161 | + // A4 @ 300dpi |
| 162 | + const dpi = 300 |
| 163 | + const a4WidthInches = 8.27 |
| 164 | + const a4HeightInches = 11.69 |
| 165 | + |
| 166 | + width := int(a4WidthInches * dpi) |
| 167 | + height := int(a4HeightInches * dpi) |
| 168 | + |
| 169 | + dc := gg.NewContext(width, height) |
| 170 | + |
| 171 | + // Background |
| 172 | + dc.SetRGB(1, 1, 1) |
| 173 | + dc.Clear() |
| 174 | + |
| 175 | + margin := 80.0 |
| 176 | + |
| 177 | + // Title using Go Regular |
| 178 | + dc.SetColor(color.Black) |
| 179 | + dc.SetFontFace(mustGoRegularFace(24)) |
| 180 | + title := "Vim Barcode Cheat Sheet (Scanner adds <CR>)" |
| 181 | + dc.DrawStringAnchored(title, float64(width)/2, margin/2, 0.5, 0.5) |
| 182 | + |
| 183 | + // Layout: many commands => 4 columns is a good balance |
| 184 | + cols := 4 |
| 185 | + rows := int(math.Ceil(float64(len(vimOps)) / float64(cols))) |
| 186 | + |
| 187 | + top := margin |
| 188 | + bottom := float64(height) - margin |
| 189 | + left := margin |
| 190 | + right := float64(width) - margin |
| 191 | + |
| 192 | + cellWidth := (right - left) / float64(cols) |
| 193 | + cellHeight := (bottom - top) / float64(rows) |
| 194 | + |
| 195 | + barcodeWidth := cellWidth * 0.80 |
| 196 | + barcodeHeight := cellHeight * 0.38 |
| 197 | + |
| 198 | + for i, op := range vimOps { |
| 199 | + col := i % cols |
| 200 | + row := i / cols |
| 201 | + |
| 202 | + x := left + float64(col)*cellWidth |
| 203 | + y := top + float64(row)*cellHeight |
| 204 | + |
| 205 | + cx := x + cellWidth/2 |
| 206 | + |
| 207 | + // Light cell boundary |
| 208 | + dc.SetLineWidth(0.4) |
| 209 | + dc.SetColor(color.RGBA{R: 230, G: 230, B: 230, A: 255}) |
| 210 | + dc.DrawRectangle(x, y, cellWidth, cellHeight) |
| 211 | + dc.Stroke() |
| 212 | + |
| 213 | + // --- Barcode generation (Code 128) --- |
| 214 | + raw, err := code128.Encode(op.Code) // BarcodeIntCS |
| 215 | + if err != nil { |
| 216 | + log.Printf("encode error for %q: %v", op.Code, err) |
| 217 | + continue |
| 218 | + } |
| 219 | + |
| 220 | + scaled, err := barcode.Scale(raw, int(barcodeWidth), int(barcodeHeight)) // Barcode |
| 221 | + if err != nil { |
| 222 | + log.Printf("scale error for %q: %v", op.Code, err) |
| 223 | + continue |
| 224 | + } |
| 225 | + |
| 226 | + // Draw barcode in upper half of the cell |
| 227 | + bx := cx - float64(scaled.Bounds().Dx())/2 |
| 228 | + by := y + 6 // top padding inside cell |
| 229 | + dc.DrawImage(scaled, int(bx), int(by)) |
| 230 | + |
| 231 | + // Text under barcode (label + description) |
| 232 | + labelY := by + float64(scaled.Bounds().Dy()) + 8 |
| 233 | + |
| 234 | + dc.SetColor(color.Black) |
| 235 | + dc.SetFontFace(mustGoRegularFace(11)) |
| 236 | + dc.DrawStringAnchored(op.Label, cx, labelY, 0.5, 0) |
| 237 | + |
| 238 | + descY := labelY + 12 |
| 239 | + dc.SetFontFace(mustGoRegularFace(8)) |
| 240 | + dc.DrawStringWrapped(op.Description, x+6, descY, 0, 0, cellWidth-12, 1.3, gg.AlignCenter) |
| 241 | + } |
| 242 | + |
| 243 | + out := "vim-barcodes-a4.png" |
| 244 | + if err := dc.SavePNG(out); err != nil { |
| 245 | + log.Fatalf("failed to save PNG: %v", err) |
| 246 | + } |
| 247 | + |
| 248 | + fmt.Println("Saved:", out) |
| 249 | +} |
| 250 | + |
| 251 | +// mustGoRegularFace returns a Go Regular font.Face at the given size. |
| 252 | +// Always uses the goregular TTF embedded in the Go font set. |
| 253 | +func mustGoRegularFace(size float64) font.Face { |
| 254 | + if face, ok := fontCache[size]; ok { |
| 255 | + return face |
| 256 | + } |
| 257 | + |
| 258 | + fnt, err := opentype.Parse(goregular.TTF) |
| 259 | + if err != nil { |
| 260 | + log.Fatalf("failed to parse goregular TTF: %v", err) |
| 261 | + } |
| 262 | + |
| 263 | + face, err := opentype.NewFace(fnt, &opentype.FaceOptions{ |
| 264 | + Size: size, |
| 265 | + DPI: 72, |
| 266 | + Hinting: font.HintingFull, |
| 267 | + }) |
| 268 | + if err != nil { |
| 269 | + log.Fatalf("failed to create goregular face (size=%.1f): %v", size, err) |
| 270 | + } |
| 271 | + |
| 272 | + fontCache[size] = face |
| 273 | + return face |
| 274 | +} |
0 commit comments