Your own logo on the dash above the prompt, between your other bands.

Your own logo on the dash above the prompt. It sits beside your other bands, such as Gas Gauge and Speedometer, so the dash carries your brand.
/logo set C:/path/to/logo.png puts a PNG on the dash. A square logo with bold shapes reads best./logo shows which file is in use./logo hide and /logo show toggle it.The logo is drawn in coloured block characters, two pixels to a character, so it shows in any terminal that can show colour. Pictures are not needed.
For the sharpest logo, draw a small PNG for the dash yourself, 40 pixels a side or smaller. A 16 by 16 PNG is a good size. A small PNG is drawn exactly as you drew it, one pixel to half a character, with nothing blurred.
A bigger PNG works too. Its see-through border is trimmed, and it is shrunk to 16 by 16 pixels. Every pixel comes out either solid colour or clear, never a blur of the two, but thin lines can still break up at that size.
The PNG is read once when you set it, on your own machine with Windows PowerShell, and the result is kept across sessions in Claude Code's plugin store. Nothing is sent anywhere. See-through parts of the PNG take your terminal's own colour.
Windows only for now, because reading the PNG uses PowerShell.
Tested with Claude Code 2.1.288.
See every Rostech mod, live, at rostech.app.
hooks/register.tsx 122 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3import type { LogoPicture } from '../types'
4
5// The logo on the dash, already shrunk to coloured cells. $.store keeps it across
6// sessions, so the PNG is read once, when it is set, and never again.
7const picture = atom({ plugin: 'logo', key: 'picture' } as const, null as LogoPicture | null)
8const isHidden = atom({ plugin: 'logo', key: 'isHidden' } as const, false)
9
10// A terminal cell is about twice as tall as it is wide. Each cell is an upper
11// half block: its foreground is the top pixel and its background the bottom one,
12// so two square pixels fit in a cell.
13//
14// 0.2.0 shrank every PNG to 12 by 12 pixels with smoothing, and his mark came
15// out pixellated: its thinnest bars fell between pixels and smeared. Now a PNG
16// of MAX pixels a side or smaller is drawn pixel for pixel, so a mark drawn by
17// hand for the dash arrives exactly as drawn. A bigger PNG is trimmed to the
18// mark and shrunk to 16 by 16, and every pixel is either the mark's colour or
19// clear, never a blend of the two.
20const MAX = 40
21const COLUMNS = 16
22const PIXEL_ROWS = 16
23const HALF_BLOCK = 0x2580
24const TERMINAL_COLOUR = 0x01000000
25
26// One pixel as the Raster wants its colour. A pixel under half see-through takes
27// the terminal's own colour, and one over it takes its own colour in full.
28function colour(rgba: string): number {
29 return parseInt(rgba.slice(6, 8), 16) < 128 ? TERMINAL_COLOUR : parseInt(rgba.slice(0, 6), 16)
30}
31
32const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
33
34// Standard padded base64, by hand, so it does not lean on a runtime helper.
35function base64(bytes: readonly number[]): string {
36 let text = ''
37 for (let i = 0; i < bytes.length; i += 3) {
38 const a = bytes[i]
39 const b = bytes[i + 1]
40 const c = bytes[i + 2]
41 const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0)
42 text += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63]
43 text += b === undefined ? '=' : ALPHABET[(n >> 6) & 63]
44 text += c === undefined ? '=' : ALPHABET[n & 63]
45 }
46 return text
47}
48
49// Pixels, two rows per cell, packed as little-endian u32 triplets [glyph, fg, bg].
50function cells(pixels: readonly string[], columns: number, rows: number): string {
51 const bytes: number[] = []
52 const word = (n: number) => bytes.push(n & 255, (n >>> 8) & 255, (n >>> 16) & 255, (n >>> 24) & 255)
53 for (let row = 0; row < rows; row++) {
54 for (let x = 0; x < columns; x++) {
55 word(HALF_BLOCK)
56 word(colour(pixels[row * 2 * columns + x]))
57 word(colour(pixels[(row * 2 + 1) * columns + x]))
58 }
59 }
60 return base64(bytes)
61}
62
63async function shrink($: EngineInterface, file: string): Promise<LogoPicture | null> {
64 const ran = await $.process
65 .run(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', `${$.plugin.root}/hooks/shrink.ps1`], {
66 env: { LOGO_FILE: file, LOGO_MAX: String(MAX), LOGO_W: String(COLUMNS), LOGO_H: String(PIXEL_ROWS) },
67 timeoutMs: 20000,
68 })
69 .catch(() => null)
70 const words = ran && ran.exitCode === 0 ? ran.stdout.trim().split(/\s+/) : []
71 const [columns, height] = [Number(words[0]), Number(words[1])]
72 const pixels = words.slice(2)
73 if (!(columns > 0 && height > 0 && height % 2 === 0) || pixels.length !== columns * height) return null
74 if (!pixels.every(p => /^[0-9A-F]{8}$/i.test(p))) return null
75 return { file, columns, rows: height / 2, cells: cells(pixels, columns, height / 2) }
76}
77
78export const register: Register = on => {
79 on('session.start', async ($, e, next) => {
80 await $.command.register({ name: 'logo', description: 'Put your logo on the dash. /logo set <path to a PNG>, /logo hide, /logo show.' })
81 const kept = (await $.store.get('picture')) as LogoPicture | undefined
82 if (kept && typeof kept.cells === 'string') await update($, picture, () => kept)
83 return next(e)
84 })
85
86 on('command.run', { command: 'logo' }, async ($, e) => {
87 const [verb, ...rest] = e.args.trim().split(/\s+/)
88 if (verb === 'hide' || verb === 'show') {
89 await update($, isHidden, () => verb === 'hide')
90 return { text: verb === 'hide' ? 'Logo hidden.' : 'Logo showing.' }
91 }
92 if (verb === 'set') {
93 const path = rest.join(' ').replace(/^"|"$/g, '')
94 if (!/\.png$/i.test(path)) return { text: 'The logo has to be a PNG file. /logo set C:/path/to/logo.png' }
95 const made = await shrink($, path)
96 if (!made) return { text: `Could not read ${path}. Check the path, and that it is a PNG.` }
97 await $.store.set('picture', made)
98 await update($, picture, () => made)
99 return { text: `Logo set: ${path}, ${made.columns} columns by ${made.rows} rows.` }
100 }
101 const now = await read($, picture)
102 return { text: now ? `Logo: ${now.file}` : 'No logo yet. /logo set <path to a PNG> puts one on the dash.' }
103 })
104
105 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
106 const now = await read($, picture)
107 if (e.props.hasSurvey || !now || e.surface !== 'terminal' || (await read($, isHidden))) return next(e)
108
109 const { Box, Raster } = $.ui.resolve(e)
110 // Ours, then every other band above the prompt draws beside it.
111 const below = await next(e)
112 return (
113 <Box flexDirection="row">
114 <Box marginRight={2} flexShrink={0}>
115 <Raster key="logo" columns={now.columns} rows={now.rows} cells={now.cells} />
116 </Box>
117 {below}
118 </Box>
119 )
120 })
121}
122types/index.d.ts 9 lines1// The logo already shrunk to terminal cells: base64 of [glyph, fg, bg] u32 triplets.
2export type LogoPicture = { file: string; columns: number; rows: number; cells: string }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'logo': { picture: LogoPicture | null; isHidden: boolean }
7 }
8}
9