cnvrt uses clean layered architecture. The CLI and external tools are adapters. The application core owns conversion decisions, batch reporting, dependency checks, and output naming rules.
cmd/cnvrt
-> internal/bootstrap
-> internal/adapters
-> internal/app
-> internal/ports
-> internal/domain
Domain imports nothing from app, adapters, CLI, terminal libraries, filesystem code, or external command code.
cmd/cnvrt/main.go
internal/domain conversion concepts and the format catalog
internal/ports interfaces owned by the core
internal/app use cases, policies, reports
internal/bootstrap wiring: builds adapters and the service
internal/adapters/cli flag/command parsing, final report rendering
internal/adapters/prompt interactive selectors and confirmations
internal/adapters/fs directory listing and path checks
internal/adapters/exec external command execution
internal/adapters/install install hints for missing tools
internal/adapters/settings user settings and backend preferences
internal/adapters/toolconfig config-defined formats and tools
internal/adapters/progress per-job progress output
internal/adapters/converters built-in backends
internal/scan dependency-free file inspection helpers
internal/shell shared command rendering/quoting helpers
internal/theme color palettes
internal/scan, internal/shell, and internal/theme are leaf helper packages that any layer may import; they depend on nothing inside the project.
The domain layer defines stable conversion concepts:
FormatFileRefConvertJobConvertOptionsToolOptionsConversionCapabilityConversionResultTransformActionArchiveActionMissingDependencyErrorand other domain errors
Formats are open-ended. Built-ins are registered in code, but unknown normalized format names are accepted so config-defined tools can introduce new formats without recompilation.
The app layer owns use cases and policies:
- interactive conversion flow after prompt choices are returned
- direct conversion flow
- tolerant batch conversion
- job building and validation
- output name generation
- same-format action handling
- archive extraction/compression routing
- backend selection and user backend preferences
- command preview confirmation before a job starts
- dependency status, runtime availability, and install hints
- output format availability choices for interactive prompts
- final run report data
Important types:
ServiceConvertRequestInteractiveRequestPreferencesandPairPreferenceRunReportJobReportDependencyStatusFormatChoice
Batch conversion does not abort on the first unsupported pair. Each input becomes a report item with status converted, skipped, or failed.
Ports are interfaces owned by the core. Adapters implement them.
Core ports:
ConverterFileDiscoveryFileSystemPromptCommandRunnerInstallAdvisorProgressReporterAppRunner
Optional interfaces a converter may also implement:
RuntimeDependencyAware— declares sub-dependencies beyondPATHlookups, such as Pandoc PDF enginesInputCapabilityAware— derives capabilities from a specific input fileDependencyStatusAware— reports customdoctorrowsCommandPreviewer— exposes the command that will run, for preview/confirmationCommandOverrideConverter— runs a user-edited command inside the normal pipelineOptionsAware— declares interactive/--optconversion settingsDescribable— supplies the description shown bybackends
The CLI adapter parses flags and commands, calls the application service, and renders final status reports.
Commands:
doctor backend availability, purpose, and install hints
formats known formats grouped by category
backends each backend's description, commands, and pairs
config interactively edit and save user settings
add-format add a config-defined format
add-tool add a config-defined converter tool
help usage
Flags:
-i, --input-format override input format
-o, --output-format output format
--out-dir output directory
--overwrite overwrite existing output files
--quality best-effort quality value for supported backends
--compress compress same-format image output
--resize resize value for supported image and video backends
--action same-format action: convert, compress, or resize
--opt backend option as tool.key=value, repeatable
Supported command shapes:
cnvrt
cnvrt input.svg output.png
cnvrt input.svg
cnvrt -i svg -o png abc.svg cde.svg
cnvrt -o png
cnvrt -o png ../../
cnvrt -o zip ./directory
cnvrt archive.tar.gzThe prompt adapter is built on Bubble Tea v2 models, styled with Lip Gloss v2. It renders inline, not as a full-screen TUI, and keeps a numeric stdin/stdout fallback for non-terminal input.
Terminal controls:
- up/down or j/k moves through options
- pageup/ctrl+up/option+up and pagedown/ctrl+down/option+down move by one visible page
- gg/ctrl+a jumps to the beginning
- G/shift+g/ctrl+e jumps to the end
- space or x selects/toggles where selection is available
- enter/right opens directories in the file picker, left goes up one level, and enter confirms other selections
/filters options; esc clears the active filter- a selects all currently filtered file-picker entries; c clears the selection
- s cycles list sorting (name, reverse, size, format)
- mouse wheel scrolls
- q aborts the interactive prompt
The file picker shows supported files and directories. Directories can be opened with enter and selected with space/x, which supports directory-to-archive workflows. It preserves cursor, scroll offset, and filter text per visited directory, so going deeper and back restores the previous position.
The output format picker receives FormatChoice values from the application layer. Unavailable formats are still displayed and dimmed with the missing dependency reason, but remain selectable. If selected, conversion continues into normal backend selection so the final report can include install hints for missing tools.
Before each terminal job starts, the prompt adapter shows the selected backend and command preview. Option flags are split onto indented colored lines to avoid unreadable wrapping. Supported previews can choose which command is editable; edited commands run through the shell for that job. A batch of multiple files is confirmed once. Non-terminal runs proceed without the confirmation prompt.
If a generated output path already exists and overwrite is not enabled, terminal mode asks for a different output path. Non-terminal mode keeps the strict failure behavior.
Interactive flow for cnvrt:
list files from current directory
-> select one or more inputs
-> select output format
-> select whether outputs go to current directory or source directories
-> optionally select same-format action
-> optionally set backend options
-> confirm command preview
-> convert supported jobs
-> print report
Interactive flow for a single archive:
detect archive
-> offer extract, choose output format, or cancel
Interactive same-format image flow:
input format equals output format
-> offer compress, resize, or convert/copy
The filesystem adapter lists directory entries, checks paths, detects directories, and creates output directories.
The exec adapter wraps os/exec, captures stdout/stderr, normalizes exit failures, and caches PATH lookups.
The progress adapter prints per-job start/success/skip/failure lines while a batch runs. The core uses a no-op reporter when none is supplied.
The install adapter suggests installation commands for missing converter tools.
Preference order:
- Use Homebrew if
brewexists, even on Linux. - Otherwise use the native package manager detected on the host.
- Fall back to configured ecosystem install hints or a manual hint.
Supported config managers:
brew, apt, dnf, yum, pacman, zypper, apk,
npm, pnpm, yarn, pipx, pip, go, cargo, gem, composer
The settings adapter loads user preferences and writes the file used by cnvrt config.
Load paths, later entries overriding earlier ones:
~/.config/cnvrt/settings.json
./cnvrt.settings.json
CNVRT_SETTINGS paths
cnvrt config writes to ~/.config/cnvrt/settings.json, merging into existing keys.
Schema:
{
"theme": "nord",
"ui": { "show_help": true },
"colors": {},
"category_colors": {},
"backends": { "svg->png": "resvg", "pdf": "ghostscript" },
"tools": { "ffmpeg": { "audio_bitrate": "320k" } },
"pairs": [
{ "input": "svg", "output": "png", "tools": ["resvg"] }
]
}backends pins a preferred backend per input->output pair or per input format, which skips the interactive backend question. tools and pairs set default backend options globally or per pair. Themes are nord (default) and classic.
The tool config adapter loads and writes JSON config files for additional formats and tools.
Load paths:
./cnvrt.tools.json
./cnvrt.tools.d/*.json
~/.config/cnvrt/tools.json
~/.config/cnvrt/tools.d/*.json
CNVRT_TOOL_CONFIG paths
cnvrt add-format and cnvrt add-tool write generated files to ~/.config/cnvrt/tools.d/*.json.
In-process, no external command required:
structured: JSON, JSONL, YAML, TOML, CSV, TSV, INI, ENV, XML, PLIST conversions, plus plain-text/Markdown renderingarchive: stdlib zip/tar/tar.gz/tar.bz2 extraction and zip/tar/tar.gz creation
External-tool backends:
resvg: SVG renderinganimated-svg: animated SVG rendering through a headless browser and FFmpegimagemagick: broad image conversion, resize, and quality optionsffmpeg: video, audio, GIF, animated imageslibreoffice: office documentspandoc: text, Markdown, HTML, RTF, TeX, DOCX, EPUBcalibre: EPUB, MOBI, AZW3, FB2, e-book workflowsgdal: geo/vector map conversionsqemu-img: VM and disk images7z: broad archive/package extraction and 7z/zip creationfontforge: outline and bitmap font conversionsinkscape: SVG to pdf/eps/psghostscript: PostScript/PDF conversion and PDF re-compressiondjvulibre: DjVu to pdf/tiff/txtpoppler: PDF text layer extractiontesseract: OCR to txt or searchable PDFgraphviz: DOT diagramsmermaid: Mermaid diagramsjupyter: notebooks to html/markdown/script/pdftts: text to speechwhisper: speech to text
Config-defined converters are appended after built-ins.
Converters are ranked by declared capability priority for the pair, highest first, keeping registration order between equal priorities. User backends preferences reorder that list. The app then selects the first backend that:
- declares a matching
input -> outputcapability - has all required commands available on
PATH - has runtime sub-dependencies available when the backend declares them
When several installed backends can handle the pair, interactive mode asks once per pair per run. Non-terminal runs take the top-ranked backend.
If no backend supports a pair, the job is skipped. If a backend supports the pair but dependencies are missing, the job is failed with install hints.
Input format detection prefers explicit -i/--input-format, then directories, then registered file extensions. If a file has no extension or only an unknown unsupported extension and its content looks like text, it is treated as txt. Custom formats with declared converter capabilities are preserved.
Interactive converter options should default to not changing source parameters when the option is optional. SVG raster/video output size follows this rule: the user is first asked whether to set a size. If skipped, converters preserve source dimensions when possible. If enabled, the input defaults to source dimensions from SVG width/height, SVG viewBox, or readable raster image metadata, falling back to 1024x1024.
The same options are scriptable via --opt tool.key=value and can be defaulted in settings.
For direct conversion, the explicit output path is used:
cnvrt input.svg output.pngFor format-based conversion, the default output is written to the current directory:
cnvrt -o png abc.svg # -> abc.pngInteractive mode asks whether generated outputs should be saved in the current directory or beside each source. Direct non-interactive format-based conversion uses the current directory unless --out-dir is set.
Same-format image actions, archive extraction, and directory archive creation:
photo.jpg -> photo.compressed.jpg
photo.jpg -> photo.resized.jpg
archive.tar.gz -> archive/
./dist (-o zip) -> dist.zip
Existing outputs are rejected unless --overwrite is set; in terminal mode the prompt asks for a different path instead.
Example tool config:
{
"formats": [
{
"name": "plantuml",
"aliases": ["puml"],
"extensions": ["puml"],
"category": "diagram"
}
],
"tools": [
{
"id": "plantuml",
"commands": ["plantuml"],
"capabilities": [
{ "input": "plantuml", "output": "svg", "priority": 80 }
],
"convert": {
"command": "plantuml",
"args": ["-tsvg", "{input}"]
},
"install": [
{ "manager": "brew", "package": "plantuml" },
{ "manager": "apt", "package": "plantuml" },
{ "manager": "npm", "package": "node-plantuml" }
]
}
]
}Command placeholders:
{input}
{output}
{input_format}
{output_format}
{quality}
{resize}
{action}
The built-in format catalog is intentionally broad because this tool targets developers and automation-heavy workflows.
Categories include images and vectors, documents and office files, e-books and scanned documents, audio and video, archives and package archives, fonts including bitmap fonts, geo/map formats, serialization/data formats, API/schema formats, and VM/disk images.
The important rule is that format recognition and conversion capability are separate. A format can be known without having a currently installed backend.
Completed:
- clean architecture skeleton under
github.com/shellcell/cnvrt - interactive CLI with Bubble Tea v2 selectors and Lip Gloss v2 reports
- direct conversion, tolerant batch conversion, final status reports
- archive extraction and directory archive creation
- same-format image action prompts
- backend selection with user preferences and command preview/edit
- dependency install hints and
doctor - config-defined formats and tools, with interactive generators
- user settings, themes, and scriptable backend options
Next:
- dedicated compressor port and compressor adapters
- recursive discovery
- dry-run conversion plans
- integration tests for installed tools
- config validation command
- richer shell-template support for config-defined tools
- browser backend for HTML screenshots/PDF
- OCR for scanned PDFs (rasterize + tesseract, or ocrmypdf)
- mouse click selection in pickers (needs reliable view-relative mouse coordinates in inline mode; wheel scrolling already works)
- piper/whisper.cpp engines for TTS/STT alongside say/espeak-ng and openai-whisper