Skip to content

NeXuS Compass — Go PNG Char-Gen / Editor (updated 2026-07-04)

The Go character generator: browse (sixel), edit PNG cards, chat (real AI + avatars + scene images), generate images, share Library files with characters, import. Read this to resume.

STATUS (2026-07-04): shipped & PUBLIC. All four planned tasks complete (editor sixel · chat images · Library del/share/export · share files into chat). Repo PUBLISHED at github.com/hackenstacks/adv-go-char-gen (AGPLv3, builds from a clean clone). Working tree clean; dev = main = 108062d. Nothing outstanding except optional polish (see bottom).


What This Is & Where

ACTIVE PROJECT: ~/Projects/adv-go-char-gen/ — the fork we build in now (forked 2026-07-03 from char-gen-cli-go to add the raw sixel chat). Module renamed adv-go-char-gen. Original ~/Projects/char-gen-cli-go/ kept intact as the pre-fork reference.

A single Go binary (Bubbletea TUI) that browses PNG character cards in true-color sixel, edits them, chats with characters via real AI (with sixel avatars + scene images), generates images, and imports cards into an encrypted library. Uses the published Go package nexus-charcard for card I/O + rendering.

  • Build: cd ~/Projects/adv-go-char-gen && go build -o adv-go-char-gen .
  • Run (best in foot, OUTSIDE tmux, for sixel): env CARDS_DIR=~/01/ai-characters ./adv-go-char-gen
  • Cards default: ./cards (override with CARDS_DIR). 75 cards live in ~/01/ai-characters.
  • Shared data dir (same as original): ~/.local/share/char-gen-cli/ — restored characters carry over.
  • Author across NeXuS repos: hackenstacks — hackenstacks@gmail.com (in README + footer).

THE BIG CONSTRAINT: Bubbletea can't do sixel → raw screens

Bubbletea (curses) CANNOT render sixel — it line-diffs and can't measure a pixel blob, so sixel gets clipped/misplaced. Solution: the screens that need true-color images are raw terminal programs run via tea.Exec (which releases the terminal), using golang.org/x/term MakeRaw + absolute cursor positioning (\033[row;colH) + chafa sixel.

  • Raw screens: rawbrowser.go (card browser + fullscreen image preview) and rawchat.go (chat).
  • Each raw screen implements tea.ExecCommand (SetStdin/Stdout/Stderr + Run). On exit it dispatches a bubbletea msg (ShowCardEditorMsg / ChatWithCardMsg / ShowCardBrowserMsg / BackToMainAppMsg).
  • RULE: true-color images ONLY work raw / one-image-on-its-own-rows (multiple sixels = each emitted at an absolute cursor position, but reliability needs a full clean redraw — ansiClear + re-emit, the proven browser pattern). A terminal can't show a multi-image grid in true color inline.
  • Symbols (chafa --format symbols --colors full --color-space rgb) = truecolor but blocky; the only thing bubbletea layouts can hold. That's why the editor's inline portrait is symbols.

★ THE CARD EDITOR — card_editor.go (focus of this compass)

Bubbletea screen. Left = portrait box (symbols) + ctrl+p hint + short path; Right = scrollable field pane (viewport). Reached from the raw browser (e/Enter on a card → ShowCardEditorMsg).

Model (cardEditorModel): user, card *cc.Card, portrait string, fields []cardField, viewport, focus int, status/statusOk, loading, err, cardPath, width, height.

Load flow: Init() → cc.LoadFromPNG(cardPath), renders portrait with cc.RenderOptions{Width:28,Height:20,Format:cc.Symbols} → cardEditorLoadedMsg. On that msg it builds fields and greets with the crisp true-color sixel portrait via tea.Batch(textinput.Blink, runImagePreviewCmd(m.cardPath, name)) — the inline portrait is symbols, so this fullscreen sixel is how you see true color; any key drops into the editor. ctrl+p re-opens it anytime. imagePreviewClosedMsg → re-renders field content.

Fields (12, order matters — makeFields): Name, Description, Personality, Scenario, System Prompt, Post History Instructions, First Message, Example Messages, Creator, Creator Notes, Version, Tags (comma-separated). Multiline ones (desc/personality/scenario/system_prompt/post_history/first_mes/ mes_example) use textarea (h=5, no line numbers, CharLimit 0); the rest use textinput. Field widths recompute on WindowSizeMsg (fieldW = width-32-6, min 40).

Keys: tab/shift+tab move focus (+scrollToFocus), ctrl+s save PNG, ctrl+r export aichat role (→ Paths.RolesDir/<slug>.md), ctrl+j export JSON (→ card dir <slug>.json), ctrl+t chat with this card (ChatWithCardMsg), ctrl+p true-color sixel preview, esc back to browser. Non-key input delegates to the focused textarea/textinput.

Save (doSave → collectFields → card.SaveToPNG): collectFields maps each field.key back onto card.Data (tags split on comma, trimmed, empties dropped). Writes embedded-JSON PNG in place — status "💾 Saved to PNG". ctrl+c is app-wide QUIT (main.go) so the editor never sees it — that's why chat is ctrl+t not ctrl+c.

View layout: header (name + Save/.md/JSON/Chat button chips) · body = JoinHorizontal(metaBlock, fieldPane) where metaBlock = portrait + ctrl+p true-color view hint + short path, fieldPane = bordered viewport · status line · help line. Loading/err states short-circuit View.

Editor gotcha: you cannot get inline TRUE sixel in the editor without raw-ifying the whole editor (big rebuild) — deliberately NOT done. The portrait-on-open + ctrl+p is the agreed true-color path.


Files (all in ~/Projects/adv-go-char-gen/)

File Role
card_editor.go PNG card editor (above) — the star of this compass
rawbrowser.go Raw sixel card browser + fullscreen rawImagePreview. renderSixel = chafa --format sixel --colors full --color-space rgb --size WxH --dither ordered (+--passthrough tmux if $TMUX)
rawchat.go Raw sixel chat — true-color avatar + live conversation + top-row image filmstrip + scene-image engine (see below)
card_browser.go Bubbletea card model (mostly superseded by raw); holds initialChatModelFromCard, importCardToLibrary, resolveDefaultModel
chat.go Old bubbletea chat model (superseded for card chat by rawchat.go via runRawChatCmd)
library.go / library_files.go Encrypted library + file import
pollinations.go Pollinations text + image services
openai_compat.go OpenAICompatService + .env loader (LoadEnvOverlay, ApplyEnvToConfig); openAIMessage.Content interface{} + contentPart/imageURLRef for vision
services.go LLMFactory / ImageFactory (Mock, Mistral, OpenAI, Pollinations, Custom, AIHorde); VisionLLMService, ImageAttachment, modelSupportsVision, dataURI
data_store.go SaveEncryptedData / load — AES store under storage/<sha256(username)>/data/
paths.go Env-configurable paths + GraphicsMode (high=sixel, low=symbols)
main.go AppModel; forwards WindowSizeMsg; ShowCardBrowserMsg→runRawBrowserCmd, ChatWithCardMsg→runRawChatCmd
messages.go AppState enum + message types

Package: ~/git/nexus-charcard/ (published github.com/hackenstacks/nexus-charcard) — LoadFromPNG, SaveToPNG, ListCards, SystemPrompt, ToAichatRole, ToJSON, RenderPortraitCard/RenderOptions{Width,Height,Format: Sixel|Symbols}.


RAW SIXEL CHAT (rawchat.go) — key facts

True-color sixel avatar drawn top-left on its own rows; conversation + input regions repaint per keystroke (avatar rows untouched = flicker-free). Async LLM/image via goroutines + events chan chatEvent (kinds: reply/error/image/sys) + background key-reader goroutine + spinner ticker; UI never blocks. layout() sizes avatar (rows/3, 8..14) & regions; drawFilmstrip() paints RETAINED images across the top row right of the avatar; images render via fullDraw() (clean full redraw) — the fix for "images not appearing." Scroll arrows/PgUp/PgDn, auto-snap to bottom.

Scene-image engine: every 3 rounds (if autoImage on & not Mock) the character illustrates the current scene; /image (blank) = LLM summarizes last 3 msgs → image prompt → GenerateImage; /image <prompt> = direct. /autoimage on|off toggles. GenerateResponse/GenerateImage are synchronous → called directly in goroutines. Quick-launch reply bar: ^G /image · ^F /files · ^O /memory · ^K /compact · ^E /export · ^S /save · ^X exit.

Full chat commands: /image /autoimage /files /share <n> /shared /unshare [all|n] /memory /commit /compact /prompt /save /export /models /help /exit.

Share Library files INTO chat (Task #4): /files (or ^F) lists the Library newest-first into libPick; /share <n> loads entry n as a sharedAttachment{name,ftype,text,image} appended to shared. buildPrompt injects a "--- Shared materials ---" block: text-like docs (text/json/chat_log, or looksTextual sniff) inject their content (4000-char cap/doc); images add a caption-reference line. sendToLLM: if visionCapable (llmSvc implements VisionLLMService AND modelSupportsVision(model)) and there are image attachments → GenerateResponseWithImages (real pixels); else plain GenerateResponse (images referenced by caption). /shared lists, /unshare [all|n] removes.


Library — LOCAL AES store, NOT IPFS (library.go / library_files.go)

The Library is a local AES-256-GCM encrypted file store — no IPFS, no CIDs, no network (the broader NeXuS stack has decentralized pieces; this app does not). Entries = library_content/<id>.bin + library_metadata/<id>.bin; LibraryFile{ID,Name,Type,OriginalPath, StoredPath,Timestamp}; Type ∈ image|chat_log|character_card|json|text. Store fns: AddDataToLibrary(user,name,type,bytes,origPath) (core), AddFileToLibrary (reads file→AddData), UpdateLibraryFileContent (in-place, same ID, bumps timestamp), ListLibraryFiles, LoadFileFromLibrary, ExportLibraryFile(user,id,destDir) (decrypt→plain file, ext by type), DeleteFileFromLibrary, EncryptAndMarshalSharedFile (separate sharing password → self-contained JSON).

Chat now auto-files into the Library (2026-07-04): every generated/scene image is added on creation (type image); conversations snapshot on /save AND on exit (≥1 exchange) into ONE entry per session that updates in place via libChatID (no duplicates, type chat_log). Library UI keys: e export → DataDir/exports/, d delete, enter→details→s share; list sorted newest-first. Round-trip test: go test -run TestLibraryRoundTrip . (whole suite still hits the pre-existing TestEndToEndFlow login panic — filter with -run).

Saved files (shared data dir ~/.local/share/char-gen-cli/)

Encrypted (open in-app after login) under storage/<sha256(username)>/data/: characters/, chat_sessions/, library_metadata/ + library_content/, configs/. Plain: images/*.jpg (from /image, img-<UnixNano>.jpg), exports/*.json (/export), backups/*.tar.gz. Old data was orphaned in char-gen-cli-go/users/ (relative path from earlier runs) — restored into storage/ on 2026-07-03 (cp -rn, originals left intact; dated backups first).


Providers & .env (.env is GITIGNORED — never commit)

.env → LoadEnvOverlay → ApplyEnvToConfig, overlays stored config. Pollinations (PRIMARY): unified gen.pollinations.ai; generation REQUIRES an sk_ key. Text = POST /v1/chat/completions (Bearer). Image = GET /image/{prompt}?model=flux&width=&height=&key=sk_ (query param; returns JPEG directly). POLLINATIONS_API_KEY auto-selects Pollinations for text+image. - Key sk_6ZnGyeQNEHmIPqlQbfVsyyU0zy1mA3vm came through chat → rotate at enter.pollinations.ai. - Text models: openai, gpt-5.4, mistral, gemini, claude, deepseek, grok, llama, qwen-coder… - Image models: flux, kontext, nanobanana, seedream, gptimage, qwen-image… Env vars: POLLINATIONS_API_KEY/_TEXT_MODEL/_IMAGE_MODEL, MISTRAL_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, CUSTOM_API_URL/_MODEL, CHAR_GEN_GRAPHICS=high|low.


Git & Release — PUBLISHED (public, AGPLv3)

Repo: github.com/hackenstacks/adv-go-char-gen (PUBLIC). main = default/stable, dev = working; both at 108062d. Protocol followed: .gitignore → init → dev branch, first commit on dev never main. Commit trail: c1f9b03 fork · 8515e81 raw chat · ea5cb5c image beside avatar · 29f88ca scene engine+attribution · 3732fae filmstrip/clean redraw · b93067e editor sixel-on-open · 216a2b5 Library del/share/export · 4abd975 Library test · c49020b README · d26ebaf self-contained build · d52b566 AGPLv3 LICENSE · 108062d share files into chat (vision). First two commits are under the default git author; the rest are hackenstacks. - .env gitignored, verified NO secrets in history. Key sk_6ZnGyeQ… still rotate-worthy. - License: GNU AGPL-3.0 (LICENSE = verbatim FSF text; GitHub detects AGPL-3.0). - Self-contained build (d26ebaf): dropped the local replace; tagged nexus-charcard v0.1.0 (at commit 23f2f3d, pushed) and pinned it. Verified by fresh clone + go clean -modcache + go build -mod=readonly → 13M binary. Anyone can git clone && go build . (needs chafa + sixel term).

Tests

  • go test -run TestLibraryRoundTrip . — Library add/list/load/update/export/delete.
  • go test -run 'TestOpenAIMessageContentPolymorphic|TestModelSupportsVision|TestLooksTextualAndMedia' . — vision serialization + share classifiers.
  • LIVE=1 go test -run TestPollinationsVisionLive . — real Pollinations vision round-trip (network; synthesizes a 32×32 red JPEG → model replied "red"). Curl POSTs to the API fail in-sandbox (HTTP 000 / curl err 43); the Go live test with sandbox disabled is the reliable check.
  • Whole suite (go test ./...) still hits the PRE-EXISTING TestEndToEndFlow login/account panic — unrelated to this work; confirmed identical on a clean tree. Use -run filters for clean passes.

Done this arc (all 4 tasks ✅)

  1. Editor sixel — card opens greeting with true-color sixel portrait; ctrl+p re-opens (b93067e).
  2. Chat images — generated/scene images render beside avatar as a top-row filmstrip (ea5cb5c/3732fae).
  3. Library del/share/export — conversations + images auto-file into the encrypted Library; UI e/d/s (216a2b5, test 4abd975).
  4. Share files into chat — /files /share <n> /shared /unshare; docs inject text, images via vision or caption (108062d). Live-verified. Plus: published PUBLIC, AGPLv3, self-contained buildable clone.

Optional polish / next (nothing blocking)

  • Not yet USER-tested interactively: the /files→/share picker + prompt injection in a live foot session (vision transport is live-proven; the in-chat UX is only build-verified).
  • Library views not split into Characters/Images/Chats tabs — one newest-first list (Type per row).
  • Optional: per-file AGPL headers (not required — LICENSE suffices), README hero screenshot/GIF, PDF text extraction (no parser dep yet; PDFs currently share by name unless they sniff textual).
  • Old bubbletea card_browser.go/chat.go View code now dead for the card flow — harmless, clean later.

Author across NeXuS repos: hackenstacks — hackenstacks@gmail.com