Using an LLM to help write code, issues, or comments is fine. Submitting its output unread is not.
- You are responsible for every line you submit. You have read it, tested it, and can explain it in review.
- Say in the PR when a meaningful part of it was AI generated.
- Do not file issues or leave comments you have not verified yourself. Reports that do not reproduce get closed.
- PRs that read like unreviewed output, with narrating comments, invented APIs, or style that ignores the file they are in, get closed without review.
Thank you for contributing to the Dank Material Shell Plugins registry!
-
Fork this repository
-
Create a new JSON file in the
plugins/directory following this naming convention:plugins/{github-username}-{plugin-name}.json- Use lowercase letters
- Separate words with hyphens
- Examples:
daniel-42-z-powerusage.json,rochacbruno-worldclock.json
-
Fill in your plugin information using the schema below:
{
"id": "pluginId",
"name": "PluginName",
"capabilities": ["dankbar-widget"],
"category": "monitoring",
"repo": "https://github.com/yourusername/your-plugin-repo",
"path": "optional/path/in/monorepo",
"author": "Your Name",
"description": "Brief description of what your plugin does",
"dependencies": ["dependency1", "dependency2"],
"compositors": ["niri", "hyprland"],
"distro": ["any"],
"screenshot": "https://url/to/screenshot.png"
}- id (required): Unique identifier in camelCase format (e.g.,
worldClock,powerUsageMonitor)- Must start with a lowercase letter
- Can only contain letters and digits (no underscores, hyphens, or spaces)
- Must exactly match the
idfield in your repository'splugin.jsonfile
- name (required): Display name of your plugin (e.g.,
World Clock,Power Usage Monitor)- Must exactly match the
namefield in your repository'splugin.jsonfile
- Must exactly match the
- capabilities (required): Array of capabilities, e.g.,
["dankbar-widget"] - category (required): One of:
monitoring,utilities,appearance,system, etc. - repo (required): Full GitHub URL to your plugin repository
- path (optional): If your plugin is in a monorepo, specify the subdirectory path
- author (required): Your name or GitHub username
- description (required): Clear, concise description of the plugin's purpose
- dependencies (required): Array of dependencies, use
[]if none - compositors (required): Supported Wayland compositors:
["niri", "hyprland"], etc. - distro (required): Supported distributions:
["any"],["fedora"],["arch"], etc. - screenshot (required): Direct URL to a screenshot image showing your plugin — see Previews for easy ways to produce a good one
- i18n (optional):
trueonce your plugin has been approved for central translation via the DMS POEditor project — see Plugin Translations. Do not set it in your initial submission.
Every plugin gets a standardized 960×540 preview card served at https://api.danklinux.com/previews/{id}. The card wraps your screenshot in a dank-themed frame with the plugin's name, category, and description; if the screenshot URL ever becomes unreachable, a metadata-only card is served instead.
Screenshots of any aspect ratio work well — the card letterboxes them over a blurred backdrop rather than cropping. Capture your plugin in a representative state (popout open, real data visible) on the default dank purple theme where possible.
If you prefer to hand-craft the full card image, there is a web generator at https://danklinux.com/thumbnail-generator.html that produces cards matching the standard layout (see docs/PREVIEWS.md for the exact composition spec).
-
Validate your plugin locally before submitting:
Run the following validation commands to ensure your plugin meets all requirements:
# Install dependencies pip install jinja2 requests # Validate JSON schema and required fields python3 .github/generate.py --validate # Validate links, paths, IDs, and names python3 .github/validate_links.py
These validations check:
- Valid JSON syntax (no trailing commas)
- All required fields are present
- Arrays use proper formatting
- URLs are complete and accessible
- Screenshot URLs are reachable
- Repository URLs are valid
- Plugin paths exist (for monorepo plugins)
idfield is in camelCase format and matches your repository'splugin.jsonnamefield matches your repository'splugin.json
-
Submit a Pull Request
- Commit your JSON file
- Push to your fork
- Create a PR to this repository
- Include a brief description of your plugin in the PR
- Keep descriptions concise and informative
- Ensure your repository has proper documentation
- Test that your plugin works with the specified compositors and distros
- Capture your screenshot in a representative state — popout open, real data visible — ideally on the default dank purple theme (see Previews)
- Prefer Built-in DMS Utilities: Do not declare external dependencies for capabilities that DMS already provides natively. See Built-in Alternatives below.
- IMPORTANT: The
idandnamefields in your registry JSON file must exactly match the corresponding fields in your plugin repository'splugin.jsonfile- For regular plugins: Must match
{repo}/plugin.json - For monorepo plugins: Must match
{repo}/{path}/plugin.json
- For regular plugins: Must match
- IMPORTANT: The
idfield must be in camelCase format (starts with lowercase, only letters/digits) - Avoid duplicate plugins: Do not submit plugins that duplicate existing ones unless the original is unmaintained (maintainer unresponsive to issues/PRs for over 30 days) or yours offers major improvements/better design. Contribute upstream first whenever possible; if submitting a successor, link your upstream issue/PR in the submission
To keep plugins lightweight and minimize external package requirements, always prefer DMS built-in CLI commands and QML services over external packages:
| Purpose | External Package | DMS Built-in CLI | Advantage |
|---|---|---|---|
| Clipboard | wl-clipboard (wl-copy, wl-paste), xclip |
dms cl copy <text>, dms cl paste |
Works uniformly across compositors, no wl-clipboard dependency needed |
| Notifications | libnotify (notify-send) |
dms notify "<title>" "<message>" |
Integrates with DMS notification center, supports --file and action buttons |
| Screenshots | grim, slurp, grimblast, flameshot |
dms screenshot [region|full|all] |
Native Wayland capture with region selector |
| HTTP Downloads | curl, wget (basic file/image fetch) |
dms dl <url> -o <path> |
Built-in downloader, avoids requiring curl for basic requests |
| Open File / URL | xdg-open, gio open |
dms open <url|path> |
Uses DMS application picker and browser selection |
| QR Codes | qrencode |
dms qr "<text>" |
Built-in encoder; supports terminal display or PNG output via stdout |
| Trash Management | trash-cli |
dms trash [put|list|restore|empty] |
Conforms to XDG Trash Spec 1.0 safely without shell rm |
| Brightness Control | brightnessctl |
dms brightness <percent> |
Direct hardware brightness control |
| Display Query | wlr-randr, xrandr |
dms randr |
Returns output metadata in structured JSON |
When writing QML, prefer native DMS services over spawning shell processes via Proc.runCommand:
| Capability | Shell Command to Avoid | QML API to Use |
|---|---|---|
| Audio Volume & Mute | pactl, wpctl, amixer |
AudioService (defaultSink.volume, defaultSink.muted, setVolume, toggleMute) |
| Network & VPN | nmcli, ip route |
NetworkService (activeVpn, vpnConnections, toggleVpn) |
| Bluetooth | bluetoothctl |
BluetoothService (devices, toggleDevice, adapterState) |
| Do Not Disturb & Power | loginctl, systemctl suspend, swaylock |
SessionData.setDoNotDisturb(...), SessionService |
| Output / Display Events | Polling niri msg outputs or hyprctl monitors |
NiriService.outputs or CompositorService.screens reactive bindings |
| State Persistence | sh -c 'printf ... > file' |
PluginService.loadPluginState() / savePluginState() or Quickshell.Io.FileView |
| Process Control | killall -SIG..., pkill (psmisc) |
Quickshell.Io.Process.signal(signum) on the process instance |
Any plugin can ship its own translations — a translations/ directory with one JSON file per locale, loaded by DMS automatically. No approval needed, no registry involvement. See the plugin development docs for the file format and the I18n.trFor API.
On top of that, registry plugins can apply to join the central DMS POEditor project — the same one community translators use for DMS itself, currently covering 21 languages. Approved plugins get their strings translated alongside the shell, and finished translations come back to the plugin repo as PRs.
Before applying:
- Your plugin is listed in this registry and actively maintained
- Every user-facing string goes through
I18n.trFor("<your plugin id>", ...), with the id as a literal string exactly matching theidin yourplugin.json— the extraction tooling reads call sites, so a variable there means your strings never get picked up
Applying:
- Open a PR setting
"i18n": truein your plugin's registry JSON. Include a short note: what the plugin does, roughly how many strings - On approval, a maintainer adds your repo to the translation sync. Your English strings get uploaded to POEditor, tagged with your plugin id
- As translators finish languages, you get PRs adding
translations/<locale>.jsonfiles to your repo — merge them and the translations ship with your next plugin update
IMPORTANT: Once your strings are in POEditor, renaming a term is a delete-plus-add — the old term's translations across every language are discarded. Keep English strings stable.
Plugin status tracking, deprecation, and removal policies are managed by registry maintainers according to MODERATION.md.
If you have questions about the contribution process, please open an issue in this repository.
Thank you for contributing a theme to the Dank Material Shell registry!
-
Fork this repository
-
Create a new folder in the
themes/directory:themes/{theme-name}/theme.json- Use lowercase letters and hyphens
- Examples:
tokyonight,gruvbox-dark,catppuccin-mocha
-
Create your theme.json with the following schema:
{
"id": "themeId",
"name": "Theme Name",
"version": "1.0.0",
"author": "Your Name",
"description": "Brief description of your theme",
"dark": {
"primary": "#hex",
"primaryText": "#hex",
"primaryContainer": "#hex",
"secondary": "#hex",
"surface": "#hex",
"surfaceText": "#hex",
"surfaceVariant": "#hex",
"surfaceVariantText": "#hex",
"surfaceTint": "#hex",
"background": "#hex",
"backgroundText": "#hex",
"outline": "#hex",
"surfaceContainerLowest": "#hex",
"surfaceContainerLow": "#hex",
"surfaceContainer": "#hex",
"surfaceContainerHigh": "#hex",
"error": "#hex",
"warning": "#hex",
"info": "#hex"
},
"light": {
"primary": "#hex",
"primaryText": "#hex",
"primaryContainer": "#hex",
"secondary": "#hex",
"surface": "#hex",
"surfaceText": "#hex",
"surfaceVariant": "#hex",
"surfaceVariantText": "#hex",
"surfaceTint": "#hex",
"background": "#hex",
"backgroundText": "#hex",
"outline": "#hex",
"surfaceContainerLowest": "#hex",
"surfaceContainerLow": "#hex",
"surfaceContainer": "#hex",
"surfaceContainerHigh": "#hex",
"error": "#hex",
"warning": "#hex",
"info": "#hex"
}
}Metadata:
- id (required): Unique identifier in camelCase (e.g.,
tokyoNight,gruvboxDark) - name (required): Display name of your theme
- version (required): Semver version (e.g.,
1.0.0) - author (required): Your name or username
- description (required): Brief description of the theme
Color Fields (required for both dark and light):
- primary: Primary accent color
- primaryText: Text color on primary backgrounds
- primaryContainer: Container using primary color. Exported to apps as is, DMS paints a softer derived container (see softPrimaryContainer below)
- secondary: Secondary accent color
- surface: Main surface/card background
- surfaceText: Text on surfaces
- surfaceVariant: Alternative surface color
- surfaceVariantText: Text on variant surfaces
- surfaceTint: Tint overlay color
- background: App background color
- backgroundText: Text on background (matugen-only — see note)
- outline: Border/divider color
- surfaceContainerLowest: Lowest-elevation container background (darkest in dark mode, lightest in light mode) (matugen-only — see note)
- surfaceContainerLow: Low-elevation container background (between surface and surfaceContainer) (matugen-only — see note)
- surfaceContainer: Container background
- surfaceContainerHigh: Elevated container background
- error: Error state color
- warning: Warning state color
- info: Info state color
Note — matugen-only keys:
surfaceContainerLowest,surfaceContainerLow, andbackgroundTextare not rendered by DankMaterialShell's own UI. They complete the Material palette and are exported to matugen templates (VS Code, KDE, Firefox, Zed, etc.), so external apps themed via DMS use them. They are still required for a valid theme — pick sensible values that fit your palette.
Optional color fields (derived when absent, used as is when present):
- surfaceContainerHighest: defaults to
surfaceContainerHigh - surfaceBright, surfaceDim: bright and dim surface variants
- outlineVariant: defaults to
outlineat 60% opacity - secondaryContainer, tertiaryContainer: containers using the secondary / tertiary color
- softPrimaryContainer: the tinted fill DMS paints wherever a primary container appears (clock and weather cards, badges, icon boxes). Otherwise a mix of
surfaceContainerandprimary, both taken at one tone just abovesurfaceContainer - containerTint: number from
0to1, the share ofprimaryin that mix. Defaults to0.5in dark and1in light. Lower reads more pastel - onPrimaryContainer, onSecondaryContainer, onTertiaryContainer: text on those containers, otherwise picked for 4.5:1 contrast. On the derived
softPrimaryContaineranonPrimaryContaineryou set is kept only when it reads at 4.5:1; setsoftPrimaryContainertoo and both are used as is - inverseSurface, inverseOnSurface: inverted surface pair for tooltips
- selectedContainer, onSelectedContainer: the fill and text behind the selected item of every list. Otherwise
secondaryContainerwhen it is set and readable, else a 20%primarytint onsurfaceContainerHighthat keepssurfaceTextat 4.5:1 - accentOnSelectedContainer, accentOnPrimaryContainer: accent glyphs on those containers, otherwise
primarywhen it reads at 3:1 - accents: categorical badge hues, see the theme docs
DMS never adjusts a color you set, even one that fails a contrast check. Derivation only fills gaps. The one exception is onPrimaryContainer without softPrimaryContainer, as noted above.
If your theme has multiple contrast levels or style options (like hard/medium/soft), you can use the variants field instead of defining all colors at the top level.
With variants, you put shared colors in the top-level dark and light objects, then define variant-specific colors in each variant option:
{
"id": "everforest",
"name": "Everforest",
"version": "1.0.0",
"author": "fontaine",
"description": "Everforest is a green based color scheme",
"dark": {
"surfaceText": "#d3c6aa",
"primary": "#a7c080",
"secondary": "#7fbbb3",
"error": "#e57e80"
},
"light": {
"surfaceText": "#5c6a72",
"primary": "#8ca101",
"secondary": "#dea000",
"error": "#f75552"
},
"variants": {
"default": "medium",
"options": [
{
"id": "hard",
"name": "Hard",
"dark": {
"surface": "#1e2326",
"background": "#272e33",
"surfaceContainer": "#2e383c"
},
"light": {
"surface": "#f2efdf",
"background": "#fffbef",
"surfaceContainer": "#f2efdf"
}
},
{
"id": "medium",
"name": "Medium",
"dark": {
"surface": "#232a2e",
"background": "#2d353b",
"surfaceContainer": "#343f44"
},
"light": {
"surface": "#efebd4",
"background": "#fdf6e3",
"surfaceContainer": "#efebd4"
}
}
]
}
}Variant Fields:
- variants.default: The variant ID to use by default
- variants.options: Array of variant definitions
- variants.options[].id: Unique identifier for the variant
- variants.options[].name: Display name for the variant
- variants.options[].dark/light: Colors specific to this variant (merged with top-level colors)
Colors in variant options override the top-level colors. Put colors that stay the same across variants at the top level, and colors that change per variant in the variant options.
For themes with two independent variant axes (like Catppuccin with flavor + accent), use type: "multi":
{
"id": "catppuccin",
"name": "Catppuccin",
"version": "1.0.0",
"author": "Catppuccin",
"description": "Soothing pastel theme",
"dark": {},
"light": {},
"variants": {
"type": "multi",
"defaults": {
"dark": { "flavor": "mocha", "accent": "mauve" },
"light": { "flavor": "latte", "accent": "mauve" }
},
"flavors": [
{
"id": "mocha",
"name": "Mocha",
"dark": { "surface": "#181825", "surfaceText": "#cdd6f4", "..." }
},
{
"id": "latte",
"name": "Latte",
"light": { "surface": "#e6e9ef", "surfaceText": "#4c4f69", "..." }
}
],
"accents": [
{
"id": "mauve",
"name": "Mauve",
"mocha": { "primary": "#cba6f7", "secondary": "#b4befe", "..." },
"latte": { "primary": "#8839ef", "secondary": "#7287fd", "..." }
}
]
}
}Multi-Variant Fields:
- variants.type: Set to
"multi"for two-dimensional variants - variants.defaults.dark: Default flavor + accent for dark mode
- variants.defaults.light: Default flavor + accent for light mode
- variants.flavors[]: Base color sets (each has
darkORlightkey to indicate mode) - variants.accents[]: Accent colors keyed by flavor ID
Resolution: base[mode] + flavor[mode] + accent[flavorId]
UI toggles dark/light mode → picks the corresponding default flavor+accent.
-
Validate your theme locally:
pip install jinja2 python3 .github/validate_themes.py python3 .github/generate.py --validate
-
Submit a Pull Request
- Commit only your
theme.jsonfile (previews are auto-generated) - Push to your fork and create a PR
- A preview will be generated and posted as a comment on your PR
- After merge, the preview SVG will be committed automatically
- Commit only your
- All color values must be 6-digit hex codes (e.g.,
#7aa2f7) - Both
darkandlightvariants are required - The
idmust be camelCase (starts lowercase, alphanumeric only) - Version must follow semver format (
X.Y.Z) - Keep descriptions concise
- Test your colors for readability and contrast