1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00
This commit is contained in:
Logger 2025-11-06 21:09:15 +00:00 committed by GitHub
commit 2f3efbdef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 599 additions and 0 deletions

View file

@ -0,0 +1,11 @@
{
time = "2025-10-01T12:44:15+00:00";
condition = true;
message = ''
A new module is available: 'programs.webApps'.
This module enables declarative configuration of web applications as
desktop entries, supporting Chromium-based browsers, Firefox, and
automatic browser detection with proper app mode integration.
'';
}

View file

@ -0,0 +1,212 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.programs.webApps;
# Type for a single web app
webAppOpts = types.submodule ({
options = {
url = mkOption {
type = types.str;
description = "URL of the web application to launch.";
example = "https://github.com";
};
name = mkOption {
type = types.nullOr types.str;
default = null;
description = "Name of the web application. If not provided, will be derived from the attribute name.";
example = "GitHub";
};
icon = mkOption {
type = types.nullOr (types.either types.str types.path);
default = null;
description = ''
Icon for the web application.
Can be a path to an icon file or a name of an icon from the current theme.
For best results, use declarative icon packages like:
- `"$${pkgs.papirus-icon-theme}/share/icons/Papirus/64x64/apps/Gmail-mail.google.com.svg"`
- Theme icon names like `"mail-client"` (requires icon theme in `home.packages`)
Popular icon themes: papirus-icon-theme, adwaita-icon-theme, arc-icon-theme
'';
example = literalExpression ''
"$${pkgs.papirus-icon-theme}/share/icons/Papirus/64x64/apps/Gmail-mail.google.com.svg"
'';
};
categories = mkOption {
type = with types; nullOr (listOf str);
default = [
"Network"
"WebBrowser"
];
description = "Categories in which the entry should be shown in application menus.";
example = ''[ "Development" "Network" ]'';
};
mimeTypes = mkOption {
type = with types; nullOr (listOf str);
default = null;
description = "The MIME types supported by this application.";
example = ''[ "x-scheme-handler/mailto" ]'';
};
startupWmClass = mkOption {
type = types.nullOr types.str;
default = null;
description = "The StartupWMClass to use in the .desktop file.";
example = "github.com";
};
extraOptions = mkOption {
type = types.attrs;
default = { };
description = "Extra options to pass to the browser when launching the webapp.";
example = ''{ profile-directory = "Profile 3"; }'';
};
};
});
# Get browser command based on package
getBrowserCommand =
browserPkg: url: extraOptions:
let
# Desktop entries don't need shell escaping, just basic space escaping
escapeDesktopArg = arg: builtins.replaceStrings [ " " ] [ "\\ " ] (toString arg);
optionString = concatStringsSep " " (
mapAttrsToList (name: value: "--${name}=${escapeDesktopArg value}") extraOptions
);
# Detect browser type from package name
browserName = browserPkg.pname or (builtins.parseDrvName browserPkg.name).name;
isChromiumBased = elem browserName [
"chromium"
"brave"
"google-chrome"
"google-chrome-stable"
"vivaldi"
];
binary = "${toString browserPkg}/bin/${browserName}";
in
if isChromiumBased then
"${binary} --app=${escapeDesktopArg url} ${optionString}"
else if browserName == "firefox" then
"${binary} ${escapeDesktopArg url}" # Firefox doesn't support --app mode
else
# Fallback: assume chromium-based behavior
"${binary} --app=${escapeDesktopArg url} ${optionString}";
# Auto-detect browser if not explicitly set
detectedBrowser =
if cfg.browser != null then
cfg.browser
else if config.programs.chromium.enable && config.programs.chromium.package != null then
config.programs.chromium.package
else if config.programs.brave.enable && config.programs.brave.package != null then
config.programs.brave.package
else if config.programs.firefox.enable && config.programs.firefox.package != null then
config.programs.firefox.package
else
pkgs.chromium; # Default fallback
# Create a desktop entry for a webapp
makeWebAppDesktopEntry =
name: appCfg:
let
# Derive app name if not explicitly set
appName = if appCfg.name != null then appCfg.name else name;
# Get the browser package
browserPkg = detectedBrowser;
# Create the launch command
launchCommand = getBrowserCommand browserPkg appCfg.url appCfg.extraOptions;
# Get browser name for StartupWMClass
browserName = browserPkg.pname or (builtins.parseDrvName browserPkg.name).name;
# Prepare StartupWMClass
startupWmClass =
if appCfg.startupWmClass != null then appCfg.startupWmClass else "${browserName}-webapp-${name}";
in
nameValuePair "webapp-${name}" {
name = appName;
genericName = "${appName} Web App";
exec = launchCommand;
icon = appCfg.icon;
terminal = false;
type = "Application";
categories = appCfg.categories;
mimeType = appCfg.mimeTypes;
settings = {
StartupWMClass = startupWmClass;
};
};
in
{
meta.maintainers = with lib.maintainers; [ realsnick ];
options.programs.webApps = {
enable = mkEnableOption "web applications";
browser = mkOption {
type = types.nullOr types.package;
default = null;
example = literalExpression "pkgs.chromium";
description = ''
Browser package to use for launching web applications.
If null, will try to auto-detect from enabled browser programs.
Chromium-based browsers (chromium, brave, google-chrome) work best with --app mode.
'';
};
apps = mkOption {
type = types.attrsOf webAppOpts;
default = { };
description = "Set of web applications to install.";
example = literalExpression ''
{
github = {
url = "https://github.com";
icon = "github";
categories = [ "Development" "Network" ];
};
gmail = {
url = "https://mail.google.com";
name = "Gmail";
icon = ./icons/gmail.png;
mimeTypes = [ "x-scheme-handler/mailto" ];
};
}
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.browser == null || lib.isDerivation cfg.browser;
message = ''
programs.webApps: browser must be a package derivation or null for auto-detection.
'';
}
];
# Create desktop entries for each web app
xdg.desktopEntries = mapAttrs' makeWebAppDesktopEntry cfg.apps;
};
}

27
tests/flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -0,0 +1,36 @@
{ pkgs, ... }:
{
config = {
# Enable brave browser program to test auto-detection
programs.brave = {
enable = true;
package = pkgs.brave;
};
programs.webApps = {
enable = true;
# browser = null; (let it auto-detect from brave)
apps = {
discord = {
url = "https://discord.com/channels/@me";
name = "Discord";
};
};
};
nmt.script = ''
# Check that the desktop entry was created
assertFileExists home-path/share/applications/webapp-discord.desktop
# Check that it detected brave and used --app mode
assertFileRegex home-path/share/applications/webapp-discord.desktop \
'Exec=.*brave.*--app=https://discord.com/channels/@me'
# Check StartupWMClass uses brave
assertFileRegex home-path/share/applications/webapp-discord.desktop \
'StartupWMClass=brave-webapp-discord'
'';
};
}

View file

@ -0,0 +1,43 @@
{ pkgs, ... }:
{
config = {
programs.webApps = {
enable = true;
browser = pkgs.chromium;
apps = {
github = {
url = "https://github.com";
};
};
};
nmt.script = ''
# Check that the desktop entry was created
assertFileExists home-path/share/applications/webapp-github.desktop
# Check basic desktop entry content
assertFileRegex home-path/share/applications/webapp-github.desktop \
'Name=github'
assertFileRegex home-path/share/applications/webapp-github.desktop \
'GenericName=github Web App'
assertFileRegex home-path/share/applications/webapp-github.desktop \
'Type=Application'
assertFileRegex home-path/share/applications/webapp-github.desktop \
'Terminal=false'
# Check the exec command contains chromium with --app
assertFileRegex home-path/share/applications/webapp-github.desktop \
'Exec=.*chromium.*--app=https://github.com'
# Check categories
assertFileRegex home-path/share/applications/webapp-github.desktop \
'Categories=Network;WebBrowser'
# Check StartupWMClass
assertFileRegex home-path/share/applications/webapp-github.desktop \
'StartupWMClass=chromium-webapp-github'
'';
};
}

View file

@ -0,0 +1,67 @@
{ pkgs, ... }:
{
config = {
programs.webApps = {
enable = true;
browser = pkgs.chromium;
apps = {
gmail = {
url = "https://mail.google.com";
name = "Gmail";
categories = [
"Office"
"Network"
"Email"
];
mimeTypes = [ "x-scheme-handler/mailto" ];
startupWmClass = "gmail-webapp";
extraOptions = {
"profile-directory" = "Profile 2";
"user-data-dir" = "/tmp/gmail-profile";
};
};
simple = {
url = "https://example.com";
# Test minimal configuration
};
};
};
nmt.script = ''
# Test Gmail with custom options
assertFileExists home-path/share/applications/webapp-gmail.desktop
# Check custom name and categories
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'Name=Gmail'
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'Categories=Office;Network;Email'
# Check MIME type support
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'MimeType=x-scheme-handler/mailto'
# Check custom StartupWMClass
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'StartupWMClass=gmail-webapp'
# Check extra browser options are included
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'Exec=.*--profile-directory=Profile\\ 2.*'
assertFileRegex home-path/share/applications/webapp-gmail.desktop \
'Exec=.*--user-data-dir=/tmp/gmail-profile.*'
# Test simple app with defaults
assertFileExists home-path/share/applications/webapp-simple.desktop
assertFileRegex home-path/share/applications/webapp-simple.desktop \
'Name=simple'
assertFileRegex home-path/share/applications/webapp-simple.desktop \
'Categories=Network;WebBrowser'
assertFileRegex home-path/share/applications/webapp-simple.desktop \
'StartupWMClass=chromium-webapp-simple'
'';
};
}

View file

@ -0,0 +1,10 @@
{ lib, pkgs, ... }:
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
webapps-basic = ./basic.nix;
webapps-explicit-browser = ./explicit-browser.nix;
webapps-auto-detect = ./auto-detect.nix;
webapps-custom-options = ./custom-options.nix;
webapps-gmail-example = ./gmail-example.nix;
webapps-package-icons = ./package-icons.nix;
}

View file

@ -0,0 +1,47 @@
{ pkgs, ... }:
{
config = {
programs.webApps = {
enable = true;
browser = pkgs.firefox;
apps = {
youtube = {
url = "https://youtube.com";
name = "YouTube";
icon = "applications-multimedia";
categories = [
"AudioVideo"
"Network"
];
};
};
};
nmt.script = ''
# Check that the desktop entry was created
assertFileExists home-path/share/applications/webapp-youtube.desktop
# Check custom name
assertFileRegex home-path/share/applications/webapp-youtube.desktop \
'Name=YouTube'
# Check Firefox exec (no --app mode for Firefox)
assertFileRegex home-path/share/applications/webapp-youtube.desktop \
'Exec=.*firefox.*https://youtube.com'
# Make sure it doesn't contain --app (Firefox doesn't support it)
assertFileNotRegex home-path/share/applications/webapp-youtube.desktop \
'Exec=.*--app.*'
# Check custom categories
assertFileRegex home-path/share/applications/webapp-youtube.desktop \
'Categories=AudioVideo;Network'
# Check icon
assertFileRegex home-path/share/applications/webapp-youtube.desktop \
'Icon=applications-multimedia'
'';
};
}

View file

@ -0,0 +1,68 @@
{ pkgs, ... }:
{
programs.webApps = {
enable = true;
browser = pkgs.chromium;
apps = {
# Method 1: Using theme icon names (most compatible)
gmail = {
url = "https://mail.google.com";
name = "Gmail";
icon = "mail-client";
categories = [
"Network"
"Email"
"Office"
];
mimeTypes = [ "x-scheme-handler/mailto" ];
startupWmClass = "gmail-webapp";
};
# Method 2: Using general theme icon names
calendar = {
url = "https://calendar.google.com";
name = "Google Calendar";
icon = "calendar";
categories = [
"Office"
"Calendar"
];
};
# Method 3: Using web browser icon as fallback
slack = {
url = "https://slack.com";
name = "Slack";
icon = "web-browser";
categories = [
"Network"
"Chat"
];
};
# Method 4: Using application icon
discord = {
url = "https://discord.com/app";
name = "Discord";
icon = "application-x-executable";
categories = [
"Network"
"Chat"
];
};
# Method 5: Using folder icon (always available)
github = {
url = "https://github.com";
name = "GitHub";
icon = "folder";
categories = [
"Development"
"Network"
];
};
};
};
}

View file

@ -0,0 +1,78 @@
{ config, pkgs, ... }:
{
xdg.enable = true;
programs.webApps = {
enable = true;
browser = pkgs.chromium;
apps = {
# Test with a package path using stubbed package
package-icon-test = {
url = "https://example.com";
name = "Package Icon Test";
# Use mock icon theme package created by test.stubs
icon = "${pkgs.mockicontheme}/share/icons/test.svg";
categories = [ "Network" ];
};
# Test with string icon name for comparison
string-icon-test = {
url = "https://example2.com";
name = "String Icon Test";
icon = "folder";
categories = [ "Network" ];
};
};
};
# Create a mock icon theme package for testing
test.stubs = {
mockicontheme = {
outPath = null;
buildScript = ''
mkdir -p $out/share/icons
echo "mock test icon" > $out/share/icons/test.svg
'';
};
};
# Test that the desktop entries are created correctly
nmt.script = ''
# Check that desktop entries exist in the correct location
assertFileExists "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop"
assertFileExists "$TESTED/home-path/share/applications/webapp-string-icon-test.desktop"
# Check that package-icon-test has the package-based icon path
# Note: test.stubs creates packages named "dummy" in the Nix store
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'Icon=/nix/store/'
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'dummy'
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'test.svg'
# Check that string-icon-test has the string icon name
assertFileContains "$TESTED/home-path/share/applications/webapp-string-icon-test.desktop" \
'Icon=folder'
# Verify the URLs are correct
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'https://example.com'
assertFileContains "$TESTED/home-path/share/applications/webapp-string-icon-test.desktop" \
'https://example2.com'
# Check the app mode launch commands are correct
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'Exec=@chromium@/bin/chromium --app=https://example.com'
assertFileContains "$TESTED/home-path/share/applications/webapp-string-icon-test.desktop" \
'Exec=@chromium@/bin/chromium --app=https://example2.com'
# Check the proper generic names are set
assertFileContains "$TESTED/home-path/share/applications/webapp-package-icon-test.desktop" \
'GenericName=Package Icon Test Web App'
assertFileContains "$TESTED/home-path/share/applications/webapp-string-icon-test.desktop" \
'GenericName=String Icon Test Web App'
'';
}