Rules-based browser launcher for TUI + GNOME. switchyard.aly.codes
tui gome bowser go
0

Configure Feed

Select the types of activity you want to include in your feed.

add nix flake

Aly Raffauf (Jan 16, 2026, 7:02 PM EST) c4326bc5 fd49e7fd

+408 -103
+10 -21
.gitignore
··· 1 - # Build outputs 2 - switchyard 3 - build/ 1 + *.flatpak 4 2 *.o 5 - 6 - # Flatpak build artifacts 3 + *.swp 4 + *~ 5 + .DS_Store 7 6 .flatpak-builder/ 7 + .idea/ 8 + .pre-commit-config.yaml 9 + .vscode/ 8 10 build-dir/ 9 - *.flatpak 11 + build/ 12 + coverage.out 10 13 repo/ 11 - 12 - # Go 13 - # vendor/ - committed for Flatpak reproducible builds 14 - 15 - # Test artifacts 16 - coverage.out 17 - 18 - # IDE 19 - .idea/ 20 - .vscode/ 21 - *.swp 22 - *~ 23 - 24 - # macOS 25 - .DS_Store 14 + switchyard
+60 -61
LICENSE.md
··· 1 - GNU General Public License 2 - ========================== 1 + # GNU General Public License 3 2 4 3 _Version 3, 29 June 2007_ 5 4 _Copyright © 2007 Free Software Foundation, Inc. &lt;<http://fsf.org/>&gt;_ ··· 193 192 the Program, in the form of source code under the terms of section 4, provided that 194 193 you also meet all of these conditions: 195 194 196 - * **a)** The work must carry prominent notices stating that you modified it, and giving a 197 - relevant date. 198 - * **b)** The work must carry prominent notices stating that it is released under this 199 - License and any conditions added under section 7. This requirement modifies the 200 - requirement in section 4 to “keep intact all notices”. 201 - * **c)** You must license the entire work, as a whole, under this License to anyone who 202 - comes into possession of a copy. This License will therefore apply, along with any 203 - applicable section 7 additional terms, to the whole of the work, and all its parts, 204 - regardless of how they are packaged. This License gives no permission to license the 205 - work in any other way, but it does not invalidate such permission if you have 206 - separately received it. 207 - * **d)** If the work has interactive user interfaces, each must display Appropriate Legal 208 - Notices; however, if the Program has interactive interfaces that do not display 209 - Appropriate Legal Notices, your work need not make them do so. 195 + - **a)** The work must carry prominent notices stating that you modified it, and giving a 196 + relevant date. 197 + - **b)** The work must carry prominent notices stating that it is released under this 198 + License and any conditions added under section 7. This requirement modifies the 199 + requirement in section 4 to “keep intact all notices”. 200 + - **c)** You must license the entire work, as a whole, under this License to anyone who 201 + comes into possession of a copy. This License will therefore apply, along with any 202 + applicable section 7 additional terms, to the whole of the work, and all its parts, 203 + regardless of how they are packaged. This License gives no permission to license the 204 + work in any other way, but it does not invalidate such permission if you have 205 + separately received it. 206 + - **d)** If the work has interactive user interfaces, each must display Appropriate Legal 207 + Notices; however, if the Program has interactive interfaces that do not display 208 + Appropriate Legal Notices, your work need not make them do so. 210 209 211 210 A compilation of a covered work with other separate and independent works, which are 212 211 not by their nature extensions of the covered work, and which are not combined with ··· 222 221 5, provided that you also convey the machine-readable Corresponding Source under the 223 222 terms of this License, in one of these ways: 224 223 225 - * **a)** Convey the object code in, or embodied in, a physical product (including a 226 - physical distribution medium), accompanied by the Corresponding Source fixed on a 227 - durable physical medium customarily used for software interchange. 228 - * **b)** Convey the object code in, or embodied in, a physical product (including a 229 - physical distribution medium), accompanied by a written offer, valid for at least 230 - three years and valid for as long as you offer spare parts or customer support for 231 - that product model, to give anyone who possesses the object code either **(1)** a copy of 232 - the Corresponding Source for all the software in the product that is covered by this 233 - License, on a durable physical medium customarily used for software interchange, for 234 - a price no more than your reasonable cost of physically performing this conveying of 235 - source, or **(2)** access to copy the Corresponding Source from a network server at no 236 - charge. 237 - * **c)** Convey individual copies of the object code with a copy of the written offer to 238 - provide the Corresponding Source. This alternative is allowed only occasionally and 239 - noncommercially, and only if you received the object code with such an offer, in 240 - accord with subsection 6b. 241 - * **d)** Convey the object code by offering access from a designated place (gratis or for 242 - a charge), and offer equivalent access to the Corresponding Source in the same way 243 - through the same place at no further charge. You need not require recipients to copy 244 - the Corresponding Source along with the object code. If the place to copy the object 245 - code is a network server, the Corresponding Source may be on a different server 246 - (operated by you or a third party) that supports equivalent copying facilities, 247 - provided you maintain clear directions next to the object code saying where to find 248 - the Corresponding Source. Regardless of what server hosts the Corresponding Source, 249 - you remain obligated to ensure that it is available for as long as needed to satisfy 250 - these requirements. 251 - * **e)** Convey the object code using peer-to-peer transmission, provided you inform 252 - other peers where the object code and Corresponding Source of the work are being 253 - offered to the general public at no charge under subsection 6d. 224 + - **a)** Convey the object code in, or embodied in, a physical product (including a 225 + physical distribution medium), accompanied by the Corresponding Source fixed on a 226 + durable physical medium customarily used for software interchange. 227 + - **b)** Convey the object code in, or embodied in, a physical product (including a 228 + physical distribution medium), accompanied by a written offer, valid for at least 229 + three years and valid for as long as you offer spare parts or customer support for 230 + that product model, to give anyone who possesses the object code either **(1)** a copy of 231 + the Corresponding Source for all the software in the product that is covered by this 232 + License, on a durable physical medium customarily used for software interchange, for 233 + a price no more than your reasonable cost of physically performing this conveying of 234 + source, or **(2)** access to copy the Corresponding Source from a network server at no 235 + charge. 236 + - **c)** Convey individual copies of the object code with a copy of the written offer to 237 + provide the Corresponding Source. This alternative is allowed only occasionally and 238 + noncommercially, and only if you received the object code with such an offer, in 239 + accord with subsection 6b. 240 + - **d)** Convey the object code by offering access from a designated place (gratis or for 241 + a charge), and offer equivalent access to the Corresponding Source in the same way 242 + through the same place at no further charge. You need not require recipients to copy 243 + the Corresponding Source along with the object code. If the place to copy the object 244 + code is a network server, the Corresponding Source may be on a different server 245 + (operated by you or a third party) that supports equivalent copying facilities, 246 + provided you maintain clear directions next to the object code saying where to find 247 + the Corresponding Source. Regardless of what server hosts the Corresponding Source, 248 + you remain obligated to ensure that it is available for as long as needed to satisfy 249 + these requirements. 250 + - **e)** Convey the object code using peer-to-peer transmission, provided you inform 251 + other peers where the object code and Corresponding Source of the work are being 252 + offered to the general public at no charge under subsection 6d. 254 253 255 254 A separable portion of the object code, whose source code is excluded from the 256 255 Corresponding Source as a System Library, need not be included in conveying the ··· 316 315 covered work, you may (if authorized by the copyright holders of that material) 317 316 supplement the terms of this License with terms: 318 317 319 - * **a)** Disclaiming warranty or limiting liability differently from the terms of 320 - sections 15 and 16 of this License; or 321 - * **b)** Requiring preservation of specified reasonable legal notices or author 322 - attributions in that material or in the Appropriate Legal Notices displayed by works 323 - containing it; or 324 - * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 325 - modified versions of such material be marked in reasonable ways as different from the 326 - original version; or 327 - * **d)** Limiting the use for publicity purposes of names of licensors or authors of the 328 - material; or 329 - * **e)** Declining to grant rights under trademark law for use of some trade names, 330 - trademarks, or service marks; or 331 - * **f)** Requiring indemnification of licensors and authors of that material by anyone 332 - who conveys the material (or modified versions of it) with contractual assumptions of 333 - liability to the recipient, for any liability that these contractual assumptions 334 - directly impose on those licensors and authors. 318 + - **a)** Disclaiming warranty or limiting liability differently from the terms of 319 + sections 15 and 16 of this License; or 320 + - **b)** Requiring preservation of specified reasonable legal notices or author 321 + attributions in that material or in the Appropriate Legal Notices displayed by works 322 + containing it; or 323 + - **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 324 + modified versions of such material be marked in reasonable ways as different from the 325 + original version; or 326 + - **d)** Limiting the use for publicity purposes of names of licensors or authors of the 327 + material; or 328 + - **e)** Declining to grant rights under trademark law for use of some trade names, 329 + trademarks, or service marks; or 330 + - **f)** Requiring indemnification of licensors and authors of that material by anyone 331 + who conveys the material (or modified versions of it) with contractual assumptions of 332 + liability to the recipient, for any liability that these contractual assumptions 333 + directly impose on those licensors and authors. 335 334 336 335 All other non-permissive additional terms are considered “further 337 336 restrictions” within the meaning of section 10. If the Program as you received
+27 -21
README.md
··· 86 86 ### Keyboard Shortcuts 87 87 88 88 **In the picker:** 89 + 89 90 - `Ctrl+1-9` - Select browser by number 90 91 - `Escape` - Close picker 91 92 92 93 **In settings:** 94 + 93 95 - `Ctrl+Q` - Quit 94 96 95 97 ## Configuration ··· 154 156 155 157 ### Rule Options 156 158 157 - | Field | Description | 158 - |-------|-------------| 159 - | `name` | Optional friendly name displayed in the UI | 160 - | `conditions` | Array of conditions to match (see below) | 161 - | `logic` | How to combine conditions: `all` (AND) or `any` (OR). Default: `all` | 162 - | `browser` | Desktop file ID of the target browser | 159 + | Field | Description | 160 + | ------------ | --------------------------------------------------------------------- | 161 + | `name` | Optional friendly name displayed in the UI | 162 + | `conditions` | Array of conditions to match (see below) | 163 + | `logic` | How to combine conditions: `all` (AND) or `any` (OR). Default: `all` | 164 + | `browser` | Desktop file ID of the target browser | 163 165 | `always_ask` | If true, show browser picker instead of auto-opening (default: false) | 164 166 165 167 ### Condition Options 166 168 167 - | Field | Description | 168 - |-------|-------------| 169 - | `type` | Match type: `domain`, `keyword`, `glob`, or `regex` | 170 - | `pattern` | The pattern to match against | 169 + | Field | Description | 170 + | --------- | --------------------------------------------------- | 171 + | `type` | Match type: `domain`, `keyword`, `glob`, or `regex` | 172 + | `pattern` | The pattern to match against | 171 173 172 174 ### Condition Types 173 175 174 - | Type | Description | Example | 175 - |------|-------------|---------| 176 - | `domain` | Exact Domain - matches specific hostname | `github.com` | 177 - | `keyword` | URL Contains - matches if URL contains text | `youtube.com/watch` | 178 - | `glob` | Wildcard - pattern with * wildcards | `*.github.com` | 179 - | `regex` | Regex - regular expression matching | `^https://.*\.example\.(com\|org)` | 176 + | Type | Description | Example | 177 + | --------- | ------------------------------------------- | ---------------------------------- | 178 + | `domain` | Exact Domain - matches specific hostname | `github.com` | 179 + | `keyword` | URL Contains - matches if URL contains text | `youtube.com/watch` | 180 + | `glob` | Wildcard - pattern with \* wildcards | `*.github.com` | 181 + | `regex` | Regex - regular expression matching | `^https://.*\.example\.(com\|org)` | 180 182 181 183 ### Logic Modes 182 184 ··· 187 189 188 190 ### Settings 189 191 190 - | Setting | Description | 191 - |---------|-------------| 192 - | `prompt_on_click` | Show picker when no rule matches (default: true) | 193 - | `favorite_browser` | Favorite browser that always appears first in picker and is used as fallback when picker is disabled | 194 - | `check_default_browser` | Prompt to set Switchyard as system default browser on startup (default: true) | 192 + | Setting | Description | 193 + | ----------------------- | ---------------------------------------------------------------------------------------------------- | 194 + | `prompt_on_click` | Show picker when no rule matches (default: true) | 195 + | `favorite_browser` | Favorite browser that always appears first in picker and is used as fallback when picker is disabled | 196 + | `check_default_browser` | Prompt to set Switchyard as system default browser on startup (default: true) | 195 197 196 198 ## Development 197 199 ··· 217 219 Switchyard draws inspiration from other excellent URL routers and browser pickers: 218 220 219 221 **Linux:** 222 + 220 223 - **[Junction](https://github.com/sonnyp/Junction)** - Elegant browser picker with a modern interface. 221 224 - **[Braus](https://braus.properlypurple.com/)** - GTK/Python browser picker for selecting browsers on each link click. 222 225 223 226 **macOS:** 227 + 224 228 - **[Choosy](https://choosy.app/)** - The gold standard URL router with beautiful UI and powerful rule-based routing. 225 229 226 230 **Windows:** 231 + 227 232 - **[BrowseRouter](https://github.com/nref/BrowseRouter)** - JSON-configured browser router for Windows 10/11. 228 233 - **[BrowserPicker](https://browserpicker.z13.web.core.windows.net/)** - Microsoft Store app for picking browsers and routing by URL patterns. 229 234 230 235 **Cross-platform:** 236 + 231 237 - **[Linklever](https://linklever.net/)** - Fast browser router with URL filtering available on Windows, macOS, and Linux. 232 238 233 239 Switchyard combines the best ideas from these tools: powerful rule-based routing with a fast, native Linux experience built on GTK4 and libadwaita.
+140
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-compat": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1767039857, 7 + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", 8 + "owner": "NixOS", 9 + "repo": "flake-compat", 10 + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", 11 + "type": "github" 12 + }, 13 + "original": { 14 + "owner": "NixOS", 15 + "repo": "flake-compat", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-parts": { 20 + "inputs": { 21 + "nixpkgs-lib": "nixpkgs-lib" 22 + }, 23 + "locked": { 24 + "lastModified": 1768135262, 25 + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", 26 + "owner": "hercules-ci", 27 + "repo": "flake-parts", 28 + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", 29 + "type": "github" 30 + }, 31 + "original": { 32 + "owner": "hercules-ci", 33 + "repo": "flake-parts", 34 + "type": "github" 35 + } 36 + }, 37 + "git-hooks-nix": { 38 + "inputs": { 39 + "flake-compat": "flake-compat", 40 + "gitignore": "gitignore", 41 + "nixpkgs": [ 42 + "nixpkgs" 43 + ] 44 + }, 45 + "locked": { 46 + "lastModified": 1767281941, 47 + "narHash": "sha256-6MkqajPICgugsuZ92OMoQcgSHnD6sJHwk8AxvMcIgTE=", 48 + "owner": "cachix", 49 + "repo": "git-hooks.nix", 50 + "rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa", 51 + "type": "github" 52 + }, 53 + "original": { 54 + "owner": "cachix", 55 + "repo": "git-hooks.nix", 56 + "type": "github" 57 + } 58 + }, 59 + "gitignore": { 60 + "inputs": { 61 + "nixpkgs": [ 62 + "git-hooks-nix", 63 + "nixpkgs" 64 + ] 65 + }, 66 + "locked": { 67 + "lastModified": 1709087332, 68 + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 69 + "owner": "hercules-ci", 70 + "repo": "gitignore.nix", 71 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 72 + "type": "github" 73 + }, 74 + "original": { 75 + "owner": "hercules-ci", 76 + "repo": "gitignore.nix", 77 + "type": "github" 78 + } 79 + }, 80 + "nixpkgs": { 81 + "locked": { 82 + "lastModified": 1768323494, 83 + "narHash": "sha256-yBXJLE6WCtrGo7LKiB6NOt6nisBEEkguC/lq/rP3zRQ=", 84 + "rev": "2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a", 85 + "revCount": 905755, 86 + "type": "tarball", 87 + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.905755%2Brev-2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a/019bbd9a-e3d9-7ccf-8dc2-3b9a70d0cdf8/source.tar.gz" 88 + }, 89 + "original": { 90 + "type": "tarball", 91 + "url": "https://flakehub.com/f/NixOS/nixpkgs/0" 92 + } 93 + }, 94 + "nixpkgs-lib": { 95 + "locked": { 96 + "lastModified": 1765674936, 97 + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", 98 + "owner": "nix-community", 99 + "repo": "nixpkgs.lib", 100 + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", 101 + "type": "github" 102 + }, 103 + "original": { 104 + "owner": "nix-community", 105 + "repo": "nixpkgs.lib", 106 + "type": "github" 107 + } 108 + }, 109 + "root": { 110 + "inputs": { 111 + "flake-parts": "flake-parts", 112 + "git-hooks-nix": "git-hooks-nix", 113 + "nixpkgs": "nixpkgs", 114 + "treefmt-nix": "treefmt-nix" 115 + } 116 + }, 117 + "treefmt-nix": { 118 + "inputs": { 119 + "nixpkgs": [ 120 + "nixpkgs" 121 + ] 122 + }, 123 + "locked": { 124 + "lastModified": 1768158989, 125 + "narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=", 126 + "owner": "numtide", 127 + "repo": "treefmt-nix", 128 + "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca", 129 + "type": "github" 130 + }, 131 + "original": { 132 + "owner": "numtide", 133 + "repo": "treefmt-nix", 134 + "type": "github" 135 + } 136 + } 137 + }, 138 + "root": "root", 139 + "version": 7 140 + }
+32
flake.nix
··· 1 + { 2 + description = "Aly's flake template with flake-parts."; 3 + 4 + inputs = { 5 + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0"; 6 + flake-parts.url = "github:hercules-ci/flake-parts"; 7 + 8 + git-hooks-nix = { 9 + url = "github:cachix/git-hooks.nix"; 10 + inputs.nixpkgs.follows = "nixpkgs"; 11 + }; 12 + 13 + treefmt-nix = { 14 + url = "github:numtide/treefmt-nix"; 15 + inputs.nixpkgs.follows = "nixpkgs"; 16 + }; 17 + }; 18 + 19 + outputs = inputs @ {flake-parts, ...}: 20 + flake-parts.lib.mkFlake {inherit inputs;} { 21 + systems = [ 22 + "aarch64-linux" 23 + "x86_64-linux" 24 + ]; 25 + 26 + imports = [ 27 + ./flake 28 + inputs.git-hooks-nix.flakeModule 29 + inputs.treefmt-nix.flakeModule 30 + ]; 31 + }; 32 + }
+8
flake/default.nix
··· 1 + { 2 + imports = [ 3 + ./devShells.nix 4 + ./git-hooks-nix.nix 5 + ./packages.nix 6 + ./treefmt.nix 7 + ]; 8 + }
+28
flake/devShells.nix
··· 1 + _: { 2 + perSystem = { 3 + config, 4 + lib, 5 + pkgs, 6 + ... 7 + }: { 8 + devShells.default = pkgs.mkShell { 9 + packages = 10 + (with pkgs; [ 11 + glib 12 + gobject-introspection 13 + gtk4 14 + just 15 + libadwaita 16 + pkg-config 17 + ]) 18 + ++ lib.attrValues config.treefmt.build.programs; 19 + 20 + shellHook = '' 21 + echo "Installing pre-commit hooks..." 22 + ${config.pre-commit.installationScript} 23 + export FLAKE="." NH_FLAKE="." 24 + echo "👋 Welcome to the switchyard devShell!" 25 + ''; 26 + }; 27 + }; 28 + }
+22
flake/git-hooks-nix.nix
··· 1 + _: { 2 + perSystem = _: { 3 + pre-commit.settings = { 4 + excludes = ["^vendor/"]; 5 + 6 + hooks = { 7 + alejandra.enable = true; 8 + deadnix.enable = true; 9 + gofmt.enable = true; 10 + prettier.enable = true; 11 + shellcheck.enable = true; 12 + 13 + shfmt = { 14 + enable = true; 15 + args = ["-i" "2"]; 16 + }; 17 + 18 + statix.enable = true; 19 + }; 20 + }; 21 + }; 22 + }
+64
flake/packages.nix
··· 1 + {inputs, ...}: { 2 + perSystem = { 3 + pkgs, 4 + self', 5 + ... 6 + }: { 7 + packages = { 8 + default = self'.packages.switchyard; 9 + 10 + switchyard = pkgs.buildGoModule { 11 + pname = "switchyard"; 12 + version = "0.1.0"; 13 + 14 + src = inputs.self; 15 + 16 + vendorHash = null; 17 + 18 + subPackages = ["src"]; 19 + 20 + postBuild = '' 21 + mv $GOPATH/bin/src $GOPATH/bin/switchyard || true 22 + ''; 23 + 24 + nativeBuildInputs = with pkgs; [ 25 + pkg-config 26 + wrapGAppsHook4 27 + ]; 28 + 29 + buildInputs = with pkgs; [ 30 + glib 31 + gobject-introspection 32 + gtk4 33 + libadwaita 34 + pkg-config 35 + ]; 36 + 37 + postInstall = '' 38 + install -Dm644 data/io.github.alyraffauf.Switchyard.desktop \ 39 + $out/share/applications/io.github.alyraffauf.Switchyard.desktop 40 + 41 + install -Dm644 data/io.github.alyraffauf.Switchyard.metainfo.xml \ 42 + $out/share/metainfo/io.github.alyraffauf.Switchyard.metainfo.xml 43 + 44 + install -Dm644 data/icons/hicolor/scalable/apps/io.github.alyraffauf.Switchyard.svg \ 45 + $out/share/icons/hicolor/scalable/apps/io.github.alyraffauf.Switchyard.svg 46 + install -Dm644 data/icons/hicolor/symbolic/apps/io.github.alyraffauf.Switchyard-symbolic.svg \ 47 + $out/share/icons/hicolor/symbolic/apps/io.github.alyraffauf.Switchyard-symbolic.svg 48 + install -Dm644 data/icons/hicolor/128x128/apps/io.github.alyraffauf.Switchyard.png \ 49 + $out/share/icons/hicolor/128x128/apps/io.github.alyraffauf.Switchyard.png 50 + install -Dm644 data/icons/hicolor/64x64/apps/io.github.alyraffauf.Switchyard.png \ 51 + $out/share/icons/hicolor/64x64/apps/io.github.alyraffauf.Switchyard.png 52 + ''; 53 + 54 + meta = with pkgs.lib; { 55 + description = "A configurable default browser for Linux"; 56 + homepage = "https://github.com/alyraffauf/switchyard"; 57 + license = licenses.gpl3Plus; 58 + platforms = platforms.linux; 59 + mainProgram = "switchyard"; 60 + }; 61 + }; 62 + }; 63 + }; 64 + }
+17
flake/treefmt.nix
··· 1 + _: { 2 + perSystem = _: { 3 + treefmt.config = { 4 + settings.global.excludes = ["vendor/*"]; 5 + 6 + programs = { 7 + alejandra.enable = true; 8 + deadnix.enable = true; 9 + gofmt.enable = true; 10 + prettier.enable = true; 11 + shellcheck.enable = true; 12 + shfmt.enable = true; 13 + statix.enable = true; 14 + }; 15 + }; 16 + }; 17 + }