├── .gitignore ├── .gitattributes ├── repo-imgs ├── s-desc.png ├── s-hover1.png ├── s-hover2.png ├── s-interfaces.png ├── s-percentage.png ├── s-scroll.png ├── s-filtering-excl.png └── s-filtering-incl.png ├── .gitmodules ├── package.json ├── minify.js ├── web ├── base.css ├── index.html ├── page.css └── page.js ├── .github └── workflows │ └── deploy-pages.yml ├── prepare.sh ├── README.md ├── prepare-data.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist 3 | .vscode 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.jpg filter=lfs diff=lfs merge=lfs -text 2 | *.png filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /repo-imgs/s-desc.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2f5dff760b26fadaca83afd0389d1b6d2d4968c96ad9ff6e2638253c319b8e63 3 | size 347975 4 | -------------------------------------------------------------------------------- /repo-imgs/s-hover1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4dec2597e8e43d27a863a3f0eb82f41134aa68ca22ea52f1a83290db28f49f8b 3 | size 331554 4 | -------------------------------------------------------------------------------- /repo-imgs/s-hover2.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:93f5d4c5cd42150c44c587e1ddc46a0b18c28d828d87577af6dad41d217b6c45 3 | size 331193 4 | -------------------------------------------------------------------------------- /repo-imgs/s-interfaces.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3545a409df3d7d480530efe28c32ef2fd48b95c1826c23261b5f3c379b156f72 3 | size 73731 4 | -------------------------------------------------------------------------------- /repo-imgs/s-percentage.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:896671891d570107c6a2cdc3cd9531bb4509f120678ca3c308e7122cb99304a1 3 | size 47772 4 | -------------------------------------------------------------------------------- /repo-imgs/s-scroll.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9748193a62c306f7ea96fe86168607a3802025efccfebf7d54f82a9aaebb4e60 3 | size 414225 4 | -------------------------------------------------------------------------------- /repo-imgs/s-filtering-excl.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8c6d287ded51b2df40432f3bcf38d4da23d4cb6ee61e523a270f8d33c0e327fa 3 | size 280650 4 | -------------------------------------------------------------------------------- /repo-imgs/s-filtering-incl.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a3c2af716e72501b5bb5e2552d3cab07de03fd89f2e87d3becde155d3039bbeb 3 | size 233771 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wayland-explorer"] 2 | path = wayland-explorer 3 | url = https://github.com/vially/wayland-explorer.git 4 | branch = main 5 | ignore = dirty 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wayland-explorer-listings", 3 | "devDependencies": { 4 | "@types/node": "^16.7.10", 5 | "clean-css": "^5.3.3", 6 | "terser": "^5.39.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /minify.js: -------------------------------------------------------------------------------- 1 | const process = require("process") 2 | const fs = require("fs") 3 | 4 | const { minify: jsMinify } = require("terser"); 5 | const CleanCSS = require('clean-css'); 6 | 7 | const cssMinifier = new CleanCSS({}) 8 | const jsMinifyCfg = { 9 | compress: { 10 | booleans_as_integers: true, 11 | passes: 3, 12 | }, 13 | mangle: { 14 | module: true, 15 | properties: false, 16 | }, 17 | ecma: 2025, 18 | enclose: true, 19 | toplevel: true, 20 | } 21 | 22 | async function processFile(logPrefix, file, action) { 23 | console.log(`+ ${logPrefix}: ${file}`) 24 | const input = fs.readFileSync(file, { encoding: "utf-8" }) 25 | fs.writeFileSync(file, await action(input)) 26 | } 27 | 28 | async function main() { 29 | for (const file of process.argv.slice(2)) { 30 | const stat = fs.statSync(file) 31 | if (stat.isDirectory()) 32 | console.log("- skipping directory:", file) 33 | else if (file.match(/\.css$/i)) 34 | processFile("css", file, (data) => cssMinifier.minify(data).styles) 35 | else if (file.match(/\.[mc]?js$/i)) 36 | processFile("js", file, async (data) => (await jsMinify(data, jsMinifyCfg)).code) 37 | else 38 | console.log("- skipping unknown:", file) 39 | } 40 | 41 | console.log("minify finished") 42 | } 43 | 44 | if (require.main === module) 45 | main() 46 | -------------------------------------------------------------------------------- /web/base.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | --bg: rgb(250 250 250); 4 | --font-sans: "Noto Sans", sans-serif; 5 | --font-mono: "Source Code Pro", monospace; 6 | 7 | padding: 0; 8 | margin: 0; 9 | font-family: var(--font-sans); 10 | background-color: var(--bg); 11 | } 12 | 13 | .page-wrapper { 14 | width: 100%; 15 | min-width: max-content; 16 | display: flex; 17 | flex-flow: column nowrap; 18 | align-items: center; 19 | } 20 | 21 | .page-container { 22 | min-width: max-content; 23 | } 24 | 25 | .loading-placeholder { 26 | margin: 50px 0; 27 | display: flex; 28 | flex-flow: column nowrap; 29 | align-items: center; 30 | row-gap: 10px; 31 | min-width: 50vw; 32 | } 33 | 34 | .loading-placeholder .i-m { 35 | font-size: 1.5em; 36 | font-weight: 600; 37 | } 38 | 39 | .loading-placeholder .i-s { 40 | font-size: 0.75em; 41 | font-weight: 400; 42 | } 43 | 44 | .footer { 45 | display: flex; 46 | flex-flow: column nowrap; 47 | row-gap: 20px; 48 | margin: 40px 10px; 49 | width: 100%; 50 | } 51 | 52 | .footer .i-heading { 53 | font-size: 1.2em; 54 | font-weight: 600; 55 | color: #353535; 56 | } 57 | 58 | .footer .i-info { 59 | display: flex; 60 | flex-flow: column nowrap; 61 | color: #202020; 62 | font-size: 0.8em; 63 | } 64 | 65 | .footer .i-info a { 66 | color: inherit; 67 | text-decoration: underline; 68 | } 69 | 70 | .footer .a-mono { 71 | font-family: var(--font-mono); 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy to github pages 2 | 3 | permissions: 4 | contents: read 5 | pages: write 6 | id-token: write 7 | 8 | on: 9 | push: 10 | branches: 11 | - master 12 | workflow_dispatch: 13 | inputs: 14 | pullWE: 15 | description: "Pull wayland-explorer submodule" 16 | type: boolean 17 | default: false 18 | required: true 19 | schedule: 20 | - cron: '0 2 * * *' 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-24.04 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: 22 31 | cache: 'npm' 32 | - name: NPM install 33 | run: npm install 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Check trigger event 37 | if: ${{ github.event_name == 'schedule' || inputs.pullWE }} 38 | run: echo "WE_PULL=1" >>"$GITHUB_ENV" 39 | - name: Generate dist 40 | run: | 41 | ./prepare.sh 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: ./dist 46 | 47 | deploy-pages: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Wayland protocols support table 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
Loading
17 |
This page needs JS to work
18 |
19 |
20 | 32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | log() { 5 | echo "|" "$@" # 1>&2 6 | } 7 | 8 | update_submodules() { 9 | local upd_default=(--init --recursive) 10 | local upd 11 | 12 | run_update() { 13 | [[ $upd == 'default' ]] && upd=("${upd_default[@]}") 14 | log "- Updating $path with ${upd[*]}" 15 | git submodule update "${upd[@]}" "${path}" 16 | } 17 | 18 | while read -r path; do 19 | case "$path" in 20 | 'wayland-explorer') 21 | upd=(--init --depth=1) 22 | [[ $WE_PULL == 1 ]] && upd+=(--remote) 23 | run_update 24 | ;; 25 | *) 26 | # shellcheck disable=SC2178 27 | upd="default" 28 | run_update 29 | ;; 30 | esac 31 | done < <(git config --file .gitmodules --get-regexp path | cut -d' ' -f2 ) 32 | } 33 | 34 | template_replace() { 35 | local content file=$1 36 | content=$(<"$file") 37 | sed " 38 | s/{{COMMIT}}/$(git rev-parse --short HEAD)/g; 39 | s/{{COMMIT_WE}}/$(git -C wayland-explorer rev-parse --short HEAD)/g; 40 | " >"$file" <<<"$content" 41 | } 42 | 43 | if [[ $SKIP_SUBMODULES != 1 ]]; then 44 | log Updating submodules 45 | update_submodules 46 | log - Submodules updated 47 | fi 48 | 49 | [[ -n $WE_DATA_PATH ]] \ 50 | || export WE_DATA_PATH=./wayland-explorer/src/data 51 | log Using data path: "$WE_DATA_PATH" 52 | [[ -L $WE_DATA_PATH ]] \ 53 | && log Data path is a symlink to: "$(readlink "$WE_DATA_PATH")" 54 | 55 | if [[ $SKIP_TSC != 1 ]]; then 56 | log Compiling Typescript registry modules 57 | pushd "$WE_DATA_PATH" 58 | tsc compositor-registry.ts protocol-registry.ts 59 | popd 60 | else 61 | log Using existing compiled js files as data source '[SKIP_TSC]' 62 | fi 63 | 64 | log Preparing 'dist' directory 65 | rm -r dist || true 66 | mkdir -v dist 67 | cp -rv web/* dist 68 | 69 | log Replacing build data in index.html 70 | template_replace dist/index.html 71 | 72 | log Copying logo svgs 73 | mkdir -v dist/logos || true 74 | cp -v wayland-explorer/public/logos/* dist/logos/ 75 | 76 | if [[ $SKIP_PREP != 1 ]]; then 77 | log Running data.json prepare script 78 | node prepare-data.js | tee generated/data_last.json dist/data.json >/dev/null 79 | else 80 | log Copying data.json from repo '[SKIP_PREP]' 81 | cp generated/data_last.json dist/data.json 82 | fi 83 | 84 | if [[ $PRETTY != 1 ]]; then 85 | log Minifying dist files 86 | node minify.js dist/* 87 | fi 88 | 89 | log Finished 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Wayland protocol support table

2 | 3 |
4 | 5 |

6 |
7 | A convenient way to explore current Wayland protocols and their support status.
8 | Try it out on GitHub Pages 9 |

10 | 11 | ## Features 12 | 13 | ### Protocol descriptions 14 | 15 | 16 | Click the document icon to view the protocol description and author information directly in the table. 17 | Or click its name to read the full description and API specification on Wayland explorer 18 |
19 | 20 | ### Interfaces listings 21 | 22 | 23 | Click the bullet-list icon to view the list of interfaces for each protocol, as well as the full compositor support matrix for each interface. 24 | If a compositor has only partial support, interface list will expand automatically. 25 | To view the complete API specification, click on a protocol name to go to Wayland explorer 26 |
27 | 28 | ### Open/close all toggles 29 | 30 | Use the corresponding icons in the header to toggle all descriptions or interface listings at once 31 | 32 | ### Supported protocols percentage 33 | 34 | 35 | See what percentage of listed protocols each compositor supports. 36 | You can exclude non-standard protocols from these calculations by toggling the option under the Settings button in the header 37 |
38 | 39 | ### Filter protocols by compositor 40 | 41 | 42 | 43 |

44 | 45 | To filter the table, click on a compositor’s name in the header: 46 | 47 | - First click shows only supported protocols 48 | - Second click shows only unsupported protocols 49 | - Third click clears the filter 50 | 51 | Support percentages in the header are also updated according to active the filter 52 | 53 | ### Hover helpers 54 | 55 | 56 | 57 |

58 | Hover highlights help with visual navigation within the table. Colored dots beneath compositor names indicate the support status of the currently highlighted protocol row 59 | 60 | ## Building 61 | 62 | Run `prepare.sh` and it will: 63 | 64 | - Initialize and checkout `wayland-explorer` submodule 65 | - Compile the necessary TypeScript files containing protocol and compositor data 66 | - Generate the static website in the `dist` directory 67 | 68 | Also note that: 69 | 70 | - Node.js and TypeScript are required to generate `data.json` file 71 | - You can use one included in repo, though it may be outdated since it's not updated during CI. Use `SKIP_TSC=1` and/or `SKIP_PREP=1` environment variables to skip ts compile stage, or preparation js script altogether, respectively 72 | - `prepare.sh` needs bash to run and is most likely linux-only 73 | 74 | ## Why not React, Webpack, etc. ? 75 | 76 | Initial goal for this project was to make it with as little dependencies as possible. Any contributions, including refactors that require new dependencies, are welcome 77 | 78 | ## Acknowledgments 79 | 80 | Many thanks to people behind Wayland Explorer (aka wayland.app) project: https://github.com/vially/wayland-explorer 81 | This project uses precompiled .ts registry files from wayland-explorer as data source, and also links to wayland.app for full protocol descriptions 82 | -------------------------------------------------------------------------------- /prepare-data.js: -------------------------------------------------------------------------------- 1 | const { stdout, env } = require("process") 2 | 3 | const baseDataPath = env["WE_DATA_PATH"] 4 | const compData = require(`${baseDataPath}/compositor-registry`) 5 | const protoData = require(`${baseDataPath}/protocol-registry`) 6 | 7 | function objIncr(obj, key, incrBy, init) { 8 | incrBy ??= 1 9 | if (obj[key] == null) 10 | obj[key] = (init ?? 0) + incrBy 11 | else 12 | obj[key] += incrBy 13 | } 14 | 15 | const SUPPORT_FULL = "full" 16 | const SUPPORT_PARTIAL = "partial" 17 | const SUPPORT_NONE = "none" 18 | 19 | const DEPRECATED_FULL = "deprecated" 20 | 21 | function createDescriptions() { 22 | const descriptions = [] 23 | 24 | function getTitle(titleProp) { 25 | if (titleProp == null) 26 | return null 27 | if (typeof titleProp === "string") 28 | return { text: titleProp } 29 | return titleProp 30 | } 31 | 32 | return { 33 | descriptions, 34 | add(text, opts) { 35 | if (typeof text !== "string") 36 | text = text?.text 37 | if (text == null) 38 | return 39 | 40 | const descriptionObj = { 41 | title: getTitle(opts.title), 42 | subTitle: getTitle(opts.subTitle), 43 | text: text.replace(/(\w)\r?\n(\w)/g, "$1 $2"), 44 | textOpts: { secondary: opts.textSecondary }, 45 | } 46 | 47 | descriptions.push(descriptionObj) 48 | return descriptionObj 49 | } 50 | } 51 | } 52 | 53 | const protocols = [] 54 | const protocolInterfaceMap = {} 55 | protoData.waylandProtocolRegistry.protocols.forEach((p) => { 56 | const xml = p.protocol 57 | const deprecations = p.deprecated 58 | ? Object.fromEntries(p.deprecated.map((d) => [d.name, d.reason])) 59 | : null 60 | 61 | const descriptions = createDescriptions() 62 | descriptions.add(xml.description, { title: p.name, subTitle: { text: p.id, mono: true } }) 63 | for (const interface of xml.interfaces) 64 | descriptions.add(interface.description, { title: { text: interface.name, mono: true } }) 65 | descriptions.add(xml.copyright, { title: { text: "Protocol copyright", level: 2 }, textSecondary: true }) 66 | 67 | const protocolPrepared = { 68 | id: p.id, 69 | name: p.name, 70 | desc: xml.description?.summary, 71 | descFull: descriptions.descriptions, 72 | tags: { 73 | source: p.source.replace(/-protocols$/, ""), 74 | stability: p.stability, 75 | }, 76 | source: p.source, 77 | supportIf: {}, 78 | supportSum: {}, 79 | countSupportSumAny: 0, 80 | countSupportSumFull: 0, 81 | defaultExpand: false, 82 | deprecations, 83 | } 84 | 85 | xml?.interfaces.forEach((iface) => { 86 | protocolInterfaceMap[iface.name] = protocolPrepared 87 | }) 88 | protocols.push(protocolPrepared) 89 | }) 90 | 91 | const compositors = [] 92 | const compositorsById = {} 93 | compData.compositorRegistry.forEach((c) => { 94 | const shortData = { 95 | id: c.id, 96 | name: c.name, 97 | icon: c.icon, 98 | } 99 | compositors.push(shortData) 100 | compositorsById[c.id] = shortData 101 | for (let compProto of c.info.globals) { 102 | const ifName = compProto.interface 103 | const protoCompSupport = protocolInterfaceMap[ifName]?.supportIf 104 | if (protoCompSupport == null) 105 | continue 106 | (protoCompSupport[ifName] ??= {})[c.id] = 1 107 | } 108 | }) 109 | 110 | function deprecationStatus(p) { 111 | const d = p.deprecations 112 | if (!d || d.length == 0) 113 | return null 114 | if (Object.keys(p.supportIf).length == 0) 115 | return DEPRECATED_FULL 116 | 117 | const supportedDeprecated = new Set() 118 | const supportedInterfaces = Object.keys(p.supportIf) 119 | for (const interface of supportedInterfaces) { 120 | if (d[interface] != null) 121 | supportedDeprecated.add(interface) 122 | } 123 | if (supportedDeprecated.size == supportedInterfaces.length) 124 | return DEPRECATED_FULL 125 | 126 | return null 127 | } 128 | 129 | protocols.forEach((p) => { 130 | let ifTotal = 0 131 | let hasNonFull = false 132 | const compCount = {} 133 | for (const compSet of Object.values(p.supportIf)) { 134 | ifTotal += 1 135 | for (const compName of Object.keys(compSet)) 136 | objIncr(compCount, compName) 137 | } 138 | for (const [compId, cnt] of Object.entries(compCount)) { 139 | const supportGrade = 140 | cnt >= ifTotal 141 | ? SUPPORT_FULL 142 | : cnt > 0 143 | ? SUPPORT_PARTIAL 144 | : SUPPORT_NONE 145 | p.supportSum[compId] = supportGrade 146 | if (supportGrade != SUPPORT_FULL) 147 | hasNonFull = true 148 | if (supportGrade !== SUPPORT_NONE) 149 | p.countSupportSumAny += 1 150 | if (supportGrade === SUPPORT_FULL) 151 | p.countSupportSumFull += 1 152 | } 153 | p.defaultExpand = hasNonFull 154 | 155 | const deprecation = deprecationStatus(p) 156 | if (deprecation === DEPRECATED_FULL) { 157 | p.tags.deprecated = deprecation 158 | p.deprecatedFull = true 159 | } 160 | }) 161 | 162 | const dataOut = { compositors, protocols } 163 | stdout.write(JSON.stringify(dataOut, null, env["PRETTY"] === "1" ? 4 : null)) 164 | -------------------------------------------------------------------------------- /web/page.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .striped-bg { 4 | --stripe-a: #000; 5 | --stripe-b: #fff; 6 | --stripe-size: 10px; 7 | --stripe-dir: 315deg; 8 | background: 9 | repeating-linear-gradient(var(--stripe-dir), 10 | var(--stripe-a), 11 | var(--stripe-a) var(--stripe-size), 12 | var(--stripe-b) var(--stripe-size), 13 | var(--stripe-b) calc(var(--stripe-size) * 2)); 14 | } 15 | 16 | .comp-table-root { 17 | position: relative; 18 | margin-top: 20px; 19 | } 20 | 21 | .comp-table-dummy { 22 | display: flex; 23 | flex-flow: column; 24 | align-items: end; 25 | justify-content: end; 26 | padding-bottom: 5px; 27 | margin-right: var(--desc-gap); 28 | } 29 | 30 | .comp-header-fix { 31 | --dummy-w: 0px; 32 | --head-w: 10px; 33 | 34 | width: fit-content; 35 | position: fixed; 36 | transition: opacity .1s ease-out; 37 | background-color: #eee; 38 | /* background-color: #875050; */ 39 | 40 | z-index: 10; 41 | opacity: 0; 42 | visibility: hidden; 43 | top: 0; 44 | } 45 | 46 | .comp-header-fix .comp-table-dummy { 47 | width: var(--dummy-w); 48 | } 49 | 50 | .comp-header-fix .comp-table-name { 51 | width: var(--head-w); 52 | } 53 | 54 | .comp-table { 55 | --row-gap: 14px; 56 | --desc-gap: 20px; 57 | --col-width: 57px; 58 | --cell-size: 35px; 59 | 60 | --row-border: 1px; 61 | --row-border-style: none; 62 | --row-border-color: #0000001f; 63 | --row-border-color-hover: #00000029; 64 | 65 | --cell-bg: #ececec; 66 | --cell-bg-alt: #e1e1e1; 67 | --cell-hover-bg: #a9cee4; 68 | --cell-hover-bg-alt: #9bc2d9; 69 | --cell-hover-row-bg: #c8c8c8; 70 | --cell-hover-row-bg-alt: #c8c8c8; 71 | --row-gap-real: calc(var(--row-gap) / 2); 72 | 73 | --cell-support-partial-bg: #fdba64; 74 | --cell-support-partial-fg: #533f26; 75 | --cell-support-full-bg: #b5ef6e; 76 | --cell-support-full-fg: #638429; 77 | --cell-support-none-bg: #fd8064; 78 | --cell-support-none-fg: #7b3723; 79 | 80 | position: relative; 81 | width: fit-content; 82 | display: grid; 83 | grid-template-columns: fit-content(350px) repeat(calc(var(--cols) - 1), var(--col-width)); 84 | } 85 | 86 | .comp-header-fix-inner { 87 | padding-top: 10px; 88 | padding-bottom: 10px; 89 | } 90 | 91 | .comp-table .comp-table-dummy { 92 | grid-row: 1 / span 2; 93 | } 94 | 95 | .comp-table-name { 96 | padding-bottom: var(--row-gap); 97 | padding-top: var(--row-gap); 98 | border-radius: 4px; 99 | 100 | display: flex; 101 | row-gap: .8em; 102 | flex-flow: column; 103 | justify-content: end; 104 | align-items: center; 105 | cursor: pointer; 106 | } 107 | 108 | .comp-table-name-selected { 109 | background-color: #b9d992; 110 | } 111 | 112 | .comp-table-name-selected-inv { 113 | background-color: #d6794b; 114 | } 115 | 116 | .comp-table-name-text { 117 | writing-mode: vertical-rl; 118 | transform: rotate(180deg); 119 | font-weight: 600; 120 | } 121 | 122 | .comp-table-name .i-support-indicator { 123 | --size: 10px; 124 | --color: #0000; 125 | width: var(--size); 126 | height: var(--size); 127 | background-color: var(--color); 128 | border-radius: 100%; 129 | } 130 | 131 | .comp-table-desc { 132 | --padding-left: 10px; 133 | 134 | padding-bottom: var(--row-gap-real); 135 | padding-top: var(--row-gap-real); 136 | padding-right: var(--desc-gap); 137 | padding-left: var(--padding-left); 138 | border-top: var(--row-border) var(--row-border-style) var(--row-border-color); 139 | min-height: var(--cell-size); 140 | 141 | display: flex; 142 | row-gap: 3px; 143 | flex-flow: column; 144 | justify-content: center; 145 | 146 | background-color: var(--cell-bg); 147 | } 148 | 149 | .comp-table-desc.comp-table-interface { 150 | padding-left: calc(var(--padding-left) + 30px); 151 | } 152 | 153 | .comp-table-desc-name { 154 | word-break: break-word; 155 | color: #404040; 156 | font-weight: 800; 157 | } 158 | 159 | .comp-table-desc-name a { 160 | text-decoration: none; 161 | color: inherit; 162 | } 163 | 164 | .comp-table-desc-name a:hover { 165 | color: #2b5b80; 166 | } 167 | 168 | .comp-table-desc-name-interface { 169 | font-family: var(--font-mono); 170 | font-size: 14px; 171 | font-weight: 500; 172 | } 173 | 174 | .comp-table-desc-id { 175 | font-family: var(--font-mono); 176 | font-size: 13px; 177 | color: #444; 178 | } 179 | 180 | .comp-table-interface-deprecation { 181 | color: #b74646; 182 | } 183 | 184 | .comp-table-interface-deprecation b { 185 | font-weight: 600; 186 | } 187 | 188 | .comp-table-tag-box { 189 | --tag-pad-v: 2px; 190 | 191 | position: relative; 192 | display: flex; 193 | flex-flow: row wrap; 194 | align-items: center; 195 | gap: 10px; 196 | font-size: 13px; 197 | } 198 | 199 | .comp-table-tag-box.m-outer { 200 | margin: 5px 0; 201 | } 202 | 203 | .comp-table-tag { 204 | --tag-bg: #eee; 205 | --tag-fg: #000; 206 | 207 | cursor: default; 208 | height: fit-content; 209 | padding: var(--tag-pad-v) 10px; 210 | border-radius: 9999px; 211 | font-weight: 600; 212 | color: var(--tag-fg); 213 | background-color: var(--tag-bg); 214 | outline: 1px solid var(--tag-fg); 215 | display: flex; 216 | justify-content: center; 217 | align-items: center; 218 | } 219 | 220 | .comp-table-tag-deprecated { 221 | --stripe-a: #0000; 222 | --stripe-b: rgb(247, 211, 200); 223 | --stripe-size: 5px; 224 | } 225 | 226 | .comp-table-db { 227 | --size: 24px; 228 | --icon-size: 16px; 229 | --border-color-hover: #444; 230 | --border-color-active: #8f8f8f; 231 | --border-size-active: 2px; 232 | 233 | display: flex; 234 | align-items: center; 235 | justify-content: center; 236 | 237 | --gap-size: calc((var(--size) - var(--icon-size)) / 2); 238 | padding: 0 var(--gap-size); 239 | height: var(--size); 240 | column-gap: var(--gap-size); 241 | 242 | background-color: #0000; 243 | border-radius: 5px; 244 | outline: 1px solid #0000002e; 245 | color: #606060; 246 | font-weight: 500; 247 | 248 | cursor: pointer; 249 | text-align: center; 250 | user-select: none; 251 | } 252 | 253 | .comp-table-db .material-symbols-outlined { 254 | font-size: var(--icon-size); 255 | font-variation-settings: 'opsz' 20, 'wght' 400; 256 | } 257 | 258 | .comp-table-db:hover { 259 | outline: 1px solid var(--border-color-hover); 260 | } 261 | 262 | .comp-table-db-active { 263 | outline: var(--border-size-active) solid var(--border-color-active); 264 | } 265 | 266 | .comp-table-db-active:hover { 267 | outline: var(--border-size-active) solid var(--border-color-hover); 268 | } 269 | 270 | .comp-table-cell { 271 | padding-top: var(--row-gap-real); 272 | border-top: var(--row-border) var(--row-border-style) var(--row-border-color); 273 | display: flex; 274 | width: 100%; 275 | justify-content: center; 276 | align-items: center; 277 | z-index: 0; 278 | 279 | background-color: var(--cell-bg); 280 | } 281 | 282 | .comp-table-cell-no-border { 283 | border: none; 284 | } 285 | 286 | .comp-table-cell-prc { 287 | background: none; 288 | } 289 | 290 | .comp-table-cell-prc-content { 291 | font-weight: 500; 292 | margin-bottom: 5px; 293 | padding: 3px; 294 | position: relative; 295 | } 296 | 297 | .comp-table-cell-prc-bg { 298 | background-color: #5cc81f; 299 | opacity: var(--prc); 300 | position: absolute; 301 | top: 0; 302 | left: 0; 303 | width: 100%; 304 | height: 100%; 305 | z-index: -1; 306 | } 307 | 308 | .comp-table-cell-content { 309 | position: relative; 310 | width: var(--cell-size); 311 | height: var(--cell-size); 312 | display: flex; 313 | justify-content: center; 314 | align-items: center; 315 | font-size: 25px; 316 | cursor: default; 317 | border-radius: 4px; 318 | 319 | margin-bottom: var(--row-gap-real); 320 | } 321 | 322 | .comp-table-cell-interface-content { 323 | border-radius: 9999px; 324 | --cell-size: 30px; 325 | } 326 | 327 | .comp-table-cell-support-partial { 328 | background-color: var(--cell-support-partial-bg); 329 | color: var(--cell-support-partial-fg); 330 | } 331 | 332 | .comp-table-cell-support-full { 333 | background-color: var(--cell-support-full-bg); 334 | color: var(--cell-support-full-fg) 335 | } 336 | 337 | .comp-table-cell-support-none { 338 | background-color: var(--cell-support-none-bg); 339 | color: var(--cell-support-none-fg); 340 | } 341 | 342 | .comp-table-row-hover { 343 | background-color: var(--cell-hover-row-bg); 344 | } 345 | 346 | .comp-table-cell-hover { 347 | background-color: var(--cell-hover-bg); 348 | --row-border-color: var(--row-border-color-hover); 349 | } 350 | 351 | .comp-table-even-odd { 352 | --cell-bg: var(--cell-bg-alt); 353 | } 354 | 355 | .comp-table-row-hover.comp-table-even-odd { 356 | --cell-hover-row-bg: var(--cell-hover-row-bg-alt); 357 | } 358 | 359 | .comp-table-cell-hover.comp-table-even-odd { 360 | --cell-hover-bg: var(--cell-hover-bg-alt); 361 | } 362 | 363 | .comp-table-interface { 364 | --row-border-style: solid; 365 | } 366 | 367 | .comp-table-fulldesc { 368 | grid-column: 1 / span var(--cols); 369 | justify-content: stretch; 370 | } 371 | 372 | .comp-table-fulldesc .i-wrapper { 373 | padding: 20px; 374 | column-gap: 25px; 375 | max-height: 400px; 376 | column-width: 500px; 377 | column-fill: auto; 378 | 379 | width: 100%; 380 | overflow-x: scroll; 381 | } 382 | 383 | .comp-table-fulldesc .i-text { 384 | white-space: pre-line; 385 | margin-top: 10px; 386 | } 387 | 388 | .comp-table-fulldesc .i-text.m-sec { 389 | font-size: .7em; 390 | color: #474747; 391 | } 392 | 393 | .comp-table-fulldesc .i-heading { 394 | margin: 0; 395 | margin-top: 20px; 396 | 397 | break-after: avoid-column; 398 | /* not supported in FF ?? */ 399 | } 400 | 401 | .comp-table-fulldesc h1 { 402 | font-size: 20px; 403 | font-weight: 600; 404 | } 405 | 406 | .comp-table-fulldesc h1.m-sub { 407 | font-size: 15px; 408 | font-weight: 400; 409 | margin-top: 5px; 410 | color: #313131; 411 | } 412 | 413 | .comp-table-fulldesc h2 { 414 | font-size: 17px; 415 | font-weight: 500; 416 | } 417 | 418 | .comp-table-fulldesc .m-mono { 419 | font-family: var(--font-mono); 420 | } 421 | 422 | .comp-table-fulldesc :is(.i-text, h1, h2):first-child { 423 | margin-top: 0; 424 | } 425 | 426 | .comp-icon { 427 | --size: 20px; 428 | 429 | width: var(--size); 430 | height: var(--size); 431 | } 432 | 433 | .comp-table-filter.m-backdrop { 434 | position: absolute; 435 | inset: 0; 436 | z-index: 1; 437 | 438 | /* backdrop-filter: blur(2px); */ 439 | /* background-color: #00000038; */ 440 | } 441 | 442 | .comp-table-filter.m-wnd { 443 | position: absolute; 444 | background-color: #fff; 445 | z-index: 2; 446 | 447 | top: 100%; 448 | margin-top: 10px; 449 | border-radius: 5px; 450 | padding: 10px; 451 | border: 1px solid #00000073; 452 | box-shadow: 2px 2px 2px 0 #0000004d; 453 | 454 | width: max-content; 455 | } 456 | 457 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap'); 458 | -------------------------------------------------------------------------------- /web/page.js: -------------------------------------------------------------------------------- 1 | 2 | // === Utils === 3 | 4 | function childrenAppend(target, elements_) { 5 | for (const c of (elements_ || [])) { 6 | if (c == null || c === false) 7 | continue 8 | if (Array.isArray(c)) 9 | childrenAppend(target, c) 10 | else if (typeof c === "string") 11 | target.appendChild(document.createTextNode(c)) 12 | else 13 | target.appendChild(c) 14 | } 15 | } 16 | 17 | function attributesSet(target, attrs) { 18 | const el = target 19 | const sa = (name, value) => el.setAttribute(name, value) 20 | 21 | for (let [name, value] of Object.entries(attrs || {})) { 22 | if (value == null || value === false) { } 23 | else if (value === true) { 24 | sa(name, "") 25 | } 26 | else if (name == 'on') { 27 | for (const [event, cb] of Object.entries(value)) { 28 | el.addEventListener(event.toLocaleLowerCase(), cb) 29 | } 30 | } 31 | else if (name.startsWith('on') && name.toLowerCase() in window) { 32 | el.addEventListener(name.toLowerCase().substring(2), value) 33 | } 34 | else if (name === "class" && Array.isArray(value)) { 35 | if (value.length > 0) 36 | sa(name, value.filter((c) => c != null).join(" ")) 37 | } 38 | else if (name === "data") { 39 | for (const [dataName, dataValue] of Object.entries(value)) { 40 | el.dataset[dataName] = dataValue 41 | } 42 | } 43 | else if (name === "style") { 44 | const t = typeof (value) 45 | if (t === "string") { } 46 | else if (Array.isArray(value)) 47 | value = value.map(i => i.replace(/;*$/, ";")).join("") 48 | else if (t === "object") 49 | value = Object.entries(value).map(([k, v]) => `${k}: ${v};`).join("") 50 | else 51 | value = value.toString() 52 | sa(name, value) 53 | } 54 | else 55 | sa(name, value.toString()) 56 | } 57 | } 58 | 59 | function e(elementName, opts, children) { 60 | const el = document.createElement(elementName) 61 | attributesSet(el, opts) 62 | childrenAppend(el, children) 63 | return el 64 | } 65 | 66 | const createMaterialIcons = () => ({ 67 | baseUrl: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200", 68 | usedIcons: new Set(), 69 | getIconLazy(name) { 70 | this.usedIcons.add(name) 71 | return () => e("span", { class: "material-symbols-outlined" }, [name]) 72 | }, 73 | initIcons() { 74 | if (this.usedIcons.size == 0) 75 | return 76 | const icons = this.usedIcons.values() 77 | .toArray().sort().join(",") 78 | const url = new URL(this.baseUrl) 79 | url.searchParams.set("icon_names", icons) 80 | document.querySelector("head").appendChild( 81 | e("link", { rel: "stylesheet", href: url.toString() }) 82 | ) 83 | }, 84 | }) 85 | 86 | function findParent(child, selector, opts) { 87 | let skipParents = opts?.skip ?? 0 88 | while (child != null && (skipParents > 0 || !child.matches(selector))) { 89 | if (skipParents > 0) 90 | skipParents-- 91 | child = child.parentElement 92 | } 93 | return child ?? null 94 | } 95 | 96 | function setDisplay(el, visible) { 97 | if (visible) 98 | el.style.removeProperty("display") 99 | else 100 | el.style["display"] = "none" 101 | } 102 | 103 | function stateKey(...keys) { 104 | return keys.join(":") 105 | } 106 | 107 | function stateToggleBool(map, key, force, default_) { 108 | const newState = force != null ? force : !(map.get(key) ?? default_) 109 | map.set(key, newState) 110 | return newState 111 | } 112 | 113 | function stateAdd(map, key, value, default_) { 114 | default_ ??= 0 115 | const oldValue = map.get(key) 116 | const newValue = (oldValue ?? default_) + value 117 | map.set(key, newValue) 118 | return newValue 119 | } 120 | 121 | // === Defs === 122 | 123 | const SUPPORT_FULL = "full" 124 | const SUPPORT_PARTIAL = "partial" 125 | const SUPPORT_NONE = "none" 126 | 127 | const KEY_EXPAND_INTERFACES = "interfaces" 128 | const KEY_EXPAND_FULLDESC = "fulldesc" 129 | const KEY_EXPAND_FILTERS = "filters" 130 | 131 | const SYNC_DUE_TO_MOUSEMOVE = 1 132 | 133 | const tagColors = { 134 | core: ["rgb(220 252 231)", "rgb(22 101 52)"], 135 | wayland: ["rgb(219 234 254)", "rgb(30 64 175)"], 136 | wlr: ["rgb(254 226 226)", "rgb(153 27 27)"], 137 | kde: ["rgb(243 232 255)", "rgb(107 33 168)"], 138 | hyprland: ["rgb(224 242 254)", "rgb(7 89 133)"], 139 | cosmic: ["rgb(254 226 226)", "rgb(146 64 14)"], 140 | weston: ["rgb(254 249 195)", "rgb(133 77 14)"], 141 | treeland: ["rgb(207 250 254)", "rgb(21 94 117)"], 142 | external: ["rgb(244 244 245)", "rgb(39 39 42)"], 143 | 144 | stable: ["rgb(220 252 231)", "rgb(22 101 52)"], 145 | staging: ["rgb(254 226 226)", "rgb(153 27 27)"], 146 | unstable: ["rgb(252 231 243)", "rgb(157 23 77)"], 147 | deprecated: ["rgb(252, 236, 231)", "rgb(157, 61, 23)", ["striped-bg", "comp-table-tag-deprecated"]], 148 | 149 | __default: ["rgb(244 244 245)", "rgb(39 39 42)"] 150 | } 151 | 152 | 153 | function isProtocolPrivate(proto) { 154 | return proto.tags.source != "wayland" && proto.countSupportSumAny <= 1 155 | } 156 | 157 | function renderPageCompositorTable(targetContainer, data) { 158 | const compCount = data.compositors.length 159 | 160 | // === Page state === 161 | 162 | const expandDefaultState = { 163 | [KEY_EXPAND_INTERFACES]: false, 164 | [KEY_EXPAND_FULLDESC]: false, 165 | [KEY_EXPAND_FILTERS]: false, 166 | } 167 | 168 | const materialIcons = createMaterialIcons() 169 | 170 | let initHeaderWidthSet = false 171 | 172 | let highlightColumn = null 173 | let highlightRow = null 174 | let highlightSourceCellMetadata = null 175 | 176 | let compFilter = null 177 | let compFilterInvert = false 178 | 179 | const expandState = new Map() 180 | const expandAllIsChanged = new Map() 181 | 182 | const filterOpts = {} 183 | 184 | function expandRowStateToggle(type, protoId) { 185 | return stateToggleBool(expandState, stateKey(type, protoId), null, expandDefaultState[type]) 186 | } 187 | 188 | function isCompFiltered(compId) { 189 | return compFilter != null && compFilter === compId 190 | } 191 | 192 | function compFilterStateSetToggle(compId) { 193 | if (isCompFiltered(compId)) { 194 | if (!compFilterInvert) 195 | compFilterInvert = true 196 | else 197 | compFilter = null 198 | } 199 | else { 200 | compFilter = compId 201 | compFilterInvert = false 202 | } 203 | } 204 | 205 | function getCompositorSupport(compId, proto, interface) { 206 | return interface == null 207 | ? proto.supportSum[compId] ?? SUPPORT_NONE 208 | : proto.supportIf[interface]?.[compId] 209 | ? SUPPORT_FULL : SUPPORT_NONE 210 | } 211 | 212 | function getRowKey(m) { 213 | if (m?.proto == null) 214 | return null 215 | return `${m.proto.id};${m.interface ?? ""}` 216 | } 217 | 218 | function updateHeadSupportIndicator(inidicator, support) { 219 | const style = inidicator.style 220 | const target = "--color" 221 | if (support != null) 222 | style.setProperty(target, `var(--cell-support-${support}-bg)`) 223 | else 224 | style.removeProperty(target) 225 | } 226 | 227 | // === Adjust header widths === 228 | 229 | function setFixWidthVar(name, sel) { 230 | if (typeof sel === "string") 231 | sel = table.querySelector(sel) 232 | const width = sel.clientWidth 233 | tableFix.style.setProperty(name, width + "px") 234 | return width 235 | } 236 | 237 | function updateFixedHeaderPosition() { 238 | tableFixOuter.style['left'] = table.getBoundingClientRect().x + "px" 239 | } 240 | 241 | function updateHeaderWidth() { 242 | const dummyWidth = setFixWidthVar("--dummy-w", ".comp-table-dummy") 243 | setFixWidthVar("--head-w", ".comp-table-name") 244 | if (!initHeaderWidthSet) { 245 | table.querySelector(".comp-table-dummy").style["width"] = dummyWidth + "px" 246 | table.style['width'] = "min-content" 247 | initHeaderWidthSet = true 248 | } 249 | updateFixedHeaderPosition() 250 | } 251 | 252 | 253 | // === State sync === 254 | 255 | const dynState = new WeakMap() 256 | const dynElements = new Set() 257 | 258 | function dynRegister(element, metadata) { 259 | dynState.set(element, metadata) 260 | dynElements.add(new WeakRef(element)) 261 | return element 262 | } 263 | 264 | function dynRegisterAll(elements, metadata) { 265 | const iterElements = 266 | elements[Symbol.iterator] == null 267 | ? Object.values(elements) : elements 268 | for (const el of iterElements) 269 | dynRegister(el, { ...metadata }) 270 | return elements 271 | } 272 | 273 | const syncState = (() => { 274 | 275 | function createSupportPercentStore() { 276 | const init = () => ({ 277 | supportPercentIndicators: new Map(), 278 | supportCount: new Map(), 279 | supportTotal: 0, 280 | }) 281 | 282 | return { 283 | state: init(), 284 | countAdd(compId, value) { 285 | stateAdd(this.state.supportCount, compId, value) 286 | }, 287 | countGet(compId) { 288 | return this.state.supportCount.get(compId) 289 | }, 290 | totalInc() { 291 | this.state.supportTotal += 1 292 | }, 293 | indicatorAssociate(compId, element) { 294 | this.state.supportPercentIndicators.set(compId, element) 295 | }, 296 | exportAndReset() { 297 | const state = this.state 298 | this.state = init() 299 | const total = state.supportTotal 300 | if (total === 0) 301 | return [] 302 | return state.supportPercentIndicators.entries().map(([compId, indicator]) => { 303 | const count = state.supportCount.get(compId) 304 | return { compId, indicator, count, value: count / total } 305 | }) 306 | } 307 | } 308 | } 309 | 310 | const hoverColumnClass = "comp-table-cell-hover" 311 | const hoverRowClass = "comp-table-row-hover" 312 | const headSelectedClass = "comp-table-name-selected" 313 | const headSelectedInvClass = "comp-table-name-selected-inv" 314 | const descButtonActiveClass = "comp-table-db-active" 315 | 316 | let lastHighlight = {} 317 | 318 | const supportPercentStore = createSupportPercentStore() 319 | 320 | function changeVisibility(el, m, visible) { 321 | if (m.visible === visible) 322 | return 323 | visible = visible ?? false 324 | setDisplay(el, visible) 325 | m.visible = visible 326 | } 327 | 328 | function expandGetState(expandType, ...keys) { 329 | const key = stateKey(expandType, ...keys) 330 | if (expandAllIsChanged.get(expandType)) { 331 | expandState.delete(key) 332 | return expandDefaultState[expandType] 333 | } 334 | return expandState.get(key) ?? expandDefaultState[expandType] 335 | } 336 | 337 | function expandRowGetState(expandType, m) { 338 | return expandGetState(expandType, m.proto.id) 339 | } 340 | 341 | function isExcludedBySupportFilter(m) { 342 | let supportExclude = false 343 | if (compFilter != null) { 344 | const supportComp = 345 | getCompositorSupport(compFilter, m.proto, m.interface) 346 | if (compFilterInvert) 347 | supportExclude = supportComp === SUPPORT_FULL 348 | else 349 | supportExclude = supportComp === SUPPORT_NONE 350 | } 351 | return supportExclude 352 | } 353 | 354 | function protoHide(shouldHide, m) { 355 | if (m.interface != null) { 356 | shouldHide = shouldHide 357 | || !expandRowGetState(KEY_EXPAND_INTERFACES, m) 358 | } 359 | 360 | const supportExclude = isExcludedBySupportFilter(m) 361 | shouldHide = shouldHide || supportExclude 362 | 363 | if ( 364 | !supportExclude && m.interface != null 365 | && (!filterOpts.excludePrivate || !isProtocolPrivate(m.proto)) 366 | ) { 367 | if (m.type == "data") { 368 | const compId = m.comp.id 369 | const supportCell = getCompositorSupport(compId, m.proto, m.interface) 370 | supportPercentStore.countAdd(compId, 371 | supportCell === SUPPORT_FULL 372 | ? 1 373 | : supportCell === SUPPORT_PARTIAL ? 0.5 : 0 374 | ) 375 | } 376 | else if (m.type === "row") { 377 | supportPercentStore.totalInc() 378 | } 379 | } 380 | 381 | return shouldHide 382 | } 383 | 384 | function mouseMoveCell(el, m) { 385 | if (lastHighlight.col != highlightColumn) 386 | el.classList.toggle(hoverColumnClass, m.comp == null ? false : highlightColumn == m.comp.id) 387 | if (lastHighlight.row != highlightRow) 388 | el.classList.toggle(hoverRowClass, highlightRow == getRowKey(m)) 389 | } 390 | 391 | function mouseMoveHeaderSupportInd(el, m) { 392 | const hlM = highlightSourceCellMetadata 393 | const support = highlightRow != null && hlM.proto 394 | ? getCompositorSupport(m.comp.id, hlM.proto, hlM.interface) 395 | : null 396 | updateHeadSupportIndicator(el, support) 397 | } 398 | 399 | return (dueTo) => { 400 | let rowVisibilityChanged = false 401 | 402 | if (!dueTo) 403 | highlightColumn = null 404 | 405 | for (const elWeak of dynElements.values()) { 406 | const el = elWeak.deref() 407 | const m = dynState.get(el) 408 | if (el === undefined || m === undefined) { 409 | dynElements.delete(elWeak) 410 | continue 411 | } 412 | 413 | if (dueTo == SYNC_DUE_TO_MOUSEMOVE) { 414 | if (m.type === "headHoverSupport") 415 | mouseMoveHeaderSupportInd(el, m) 416 | if (m.visible && (m.type === "data" || m.type === "row")) 417 | mouseMoveCell(el, m) 418 | continue 419 | } 420 | 421 | if (m.type === "data") { 422 | let shouldHide = protoHide(false, m) 423 | changeVisibility(el, m, !shouldHide) 424 | mouseMoveCell(el, m) 425 | } 426 | else if (m.type === "row") { 427 | let shouldHide = protoHide(false, m) 428 | changeVisibility(el, m, !shouldHide) 429 | rowVisibilityChanged = true // actually detect if changed? 430 | mouseMoveCell(el, m) 431 | } 432 | else if (m.type === "head") { 433 | const cl = el.classList 434 | if (isCompFiltered(m.comp.id)) { 435 | cl.add(headSelectedClass) 436 | cl.toggle(headSelectedInvClass, compFilterInvert) 437 | } 438 | else { 439 | cl.remove(headSelectedClass, headSelectedInvClass) 440 | } 441 | 442 | } 443 | else if (m.type === "descButton") { 444 | const active = expandRowGetState(m.buttonType, m) 445 | if (active !== m.active) { 446 | m.active = active 447 | el.classList.toggle(descButtonActiveClass, active) 448 | } 449 | } 450 | else if (m.type === "supportPercent") { 451 | supportPercentStore.indicatorAssociate(m.comp.id, el) 452 | } 453 | else if (m.type === "descFull") { 454 | let shouldHide = !expandRowGetState(KEY_EXPAND_FULLDESC, m) 455 | shouldHide = shouldHide || isExcludedBySupportFilter(m) 456 | changeVisibility(el, m, !shouldHide) 457 | } 458 | else if (m.type === "headHoverSupport") { 459 | mouseMoveHeaderSupportInd(el, m) 460 | } 461 | else if (m.type === "filterWindow") { 462 | changeVisibility(el, m, expandGetState(KEY_EXPAND_FILTERS)) 463 | } 464 | } 465 | 466 | lastHighlight.col = highlightColumn 467 | lastHighlight.row = highlightRow 468 | expandAllIsChanged.clear() 469 | 470 | for (const { indicator, value } of supportPercentStore.exportAndReset()) { 471 | indicator.querySelector(".i-value").innerText = Math.round(value * 100) + "%" 472 | indicator.querySelector(".i-bg").style.setProperty("--prc", value) 473 | } 474 | 475 | if (!dueTo) 476 | updateHeaderWidth() 477 | 478 | if (rowVisibilityChanged) { 479 | setTimeout(async () => { 480 | const cells = 481 | document.querySelectorAll(":is(.comp-table-desc, .comp-table-cell)") 482 | let i = 0 483 | for (const cell of cells) { 484 | const m = dynState.get(cell) 485 | if (!m || !m.visible) 486 | continue 487 | if (m.type == "row" && m.interface == null) 488 | i++ 489 | cell.classList.toggle("comp-table-even-odd", i % 2 == 0) 490 | } 491 | }, 0) 492 | } 493 | } 494 | })(); 495 | 496 | // === Table handlers === 497 | 498 | function filterByCompClickHandler(ev) { 499 | const targetHeadCell = ev.currentTarget 500 | const targetComp = targetHeadCell.dataset.comp 501 | compFilterStateSetToggle(targetComp) 502 | syncState() 503 | } 504 | 505 | function descButtonGetProtoId(ev) { 506 | const button = ev.currentTarget 507 | const descCell = findParent(button, ".comp-table-desc") 508 | return descCell.dataset.proto 509 | } 510 | 511 | function interfacesExpandClickHandler(ev) { 512 | const protoId = descButtonGetProtoId(ev) 513 | expandRowStateToggle(KEY_EXPAND_INTERFACES, protoId) 514 | syncState() 515 | } 516 | 517 | function fullDescExpandClickHandler(ev) { 518 | const protoId = descButtonGetProtoId(ev) 519 | expandRowStateToggle(KEY_EXPAND_FULLDESC, protoId) 520 | syncState() 521 | } 522 | 523 | function expandAllClickHandler(ev, key) { 524 | const shouldExpand = !expandDefaultState[key] 525 | expandDefaultState[key] = shouldExpand 526 | expandAllIsChanged.set(key, true) 527 | syncState() 528 | } 529 | 530 | function toggleFilterWindow(force) { 531 | stateToggleBool(expandState, KEY_EXPAND_FILTERS, force) 532 | syncState() 533 | } 534 | 535 | // === Table populate === 536 | 537 | const icSupportCell = { 538 | [SUPPORT_FULL]: "+", 539 | [SUPPORT_PARTIAL]: "~", 540 | [SUPPORT_NONE]: "X", 541 | 542 | __default: "?" 543 | } 544 | 545 | const icFilters = materialIcons.getIconLazy("tune") 546 | const icInterfaces = materialIcons.getIconLazy("format_list_bulleted") 547 | const icDescription = materialIcons.getIconLazy("description") 548 | 549 | const filterWindow = dynRegisterAll({ 550 | backdrop: e("div", { 551 | class: ["comp-table-filter", "m-backdrop"], 552 | onClick: () => toggleFilterWindow(false), 553 | }, []), 554 | wnd: e("div", { class: ["comp-table-filter", "m-wnd"] }, [ 555 | e("label", {}, [ 556 | e("input", { 557 | type: "checkbox", 558 | onChange: (ev) => { 559 | filterOpts.excludePrivate = ev.currentTarget.checked 560 | syncState() 561 | } 562 | }), 563 | "Exclude compositor‑specific protocols from the percentage calculation" 564 | ]), 565 | ]), 566 | }, { type: "filterWindow" }) 567 | 568 | function getTableFirstCell(opts) { 569 | return e("div", { class: "comp-table-dummy" }, [ 570 | e("div", { class: ["comp-table-tag-box"] }, [ 571 | !opts?.fixedHeader ? [ 572 | filterWindow.wnd, 573 | e("div", { 574 | class: ["comp-table-db"], 575 | onClick: toggleFilterWindow, 576 | }, [icFilters(), "Settings"]), 577 | ] : null, 578 | e("div", { 579 | class: ["comp-table-db"], 580 | onClick: (ev) => expandAllClickHandler(ev, KEY_EXPAND_INTERFACES) 581 | }, [(icInterfaces())]), 582 | e("div", { 583 | class: ["comp-table-db"], 584 | onClick: (ev) => expandAllClickHandler(ev, KEY_EXPAND_FULLDESC) 585 | }, [icDescription()]), 586 | ]) 587 | ]) 588 | } 589 | 590 | const tableFix = e("div", 591 | { class: ["comp-table", "comp-header-fix-inner"] }, 592 | [getTableFirstCell({ fixedHeader: true })] 593 | ) 594 | 595 | const table = e("div", 596 | { class: ["comp-table", "comp-table-main"] }, 597 | [filterWindow.backdrop, getTableFirstCell()] 598 | ) 599 | 600 | for (const c of data.compositors) { 601 | /* Setup headings (normal & fixed/floating) */ 602 | const headerCell = () => { 603 | const headCell = e("div", 604 | { 605 | class: "comp-table-name", 606 | data: { comp: c.id }, 607 | onClick: filterByCompClickHandler, 608 | }, 609 | [ 610 | e("div", { class: "comp-table-name-text" }, [c.name]), 611 | dynRegister( 612 | e("div", { class: "i-support-indicator" }), 613 | { type: "headHoverSupport", comp: c } 614 | ), 615 | c.icon == null 616 | ? e("div", { class: ["comp-icon", "comp-icon-dummy"] }) 617 | : e("img", { class: ["comp-icon", "comp-icon-img"], src: `./logos/${c.icon}.svg` }), 618 | ] 619 | ) 620 | dynRegister(headCell, { type: "head", comp: c }) 621 | return headCell 622 | } 623 | table.appendChild(headerCell()) 624 | tableFix.appendChild(headerCell()) 625 | } 626 | 627 | for (const c of data.compositors) { 628 | /* Setup support percentages */ 629 | table.appendChild( 630 | dynRegister( 631 | e("div", { 632 | class: ["comp-table-cell", "comp-table-cell-prc", "comp-table-cell-no-border"], 633 | }, [ 634 | e("div", { 635 | class: ["comp-table-cell-prc-content"], 636 | }, [ 637 | e("div", { class: ["i-value"] }, ["?"]), 638 | e("div", { class: ["i-bg", "comp-table-cell-prc-bg"] }), 639 | ]), 640 | ]), 641 | { type: "supportPercent", comp: c } 642 | ) 643 | ) 644 | } 645 | 646 | for (const p of data.protocols) { 647 | /* Setup protocol row titles */ 648 | 649 | const tags = ['source', 'deprecated', 'stability'] 650 | .map((t) => p.tags[t]) 651 | .filter((t) => t != null) 652 | .map((t) => { 653 | const [bg, fg, classes] = tagColors[t] ?? tagColors.__default 654 | return e("div", { 655 | class: ["comp-table-tag", ...(classes ?? [])], 656 | style: { "--tag-bg": bg, "--tag-fg": fg } 657 | }, [t]) 658 | }) 659 | 660 | const descCell = e("div", { class: "comp-table-desc", data: { proto: p.id } }, [ 661 | e("div", { class: "comp-table-desc-name", data: { proto: p.id } }, [ 662 | e("a", { href: `https://wayland.app/protocols/${p.id}`, target: "_blank" }, [p.name]), 663 | ]), 664 | e("div", { class: "comp-table-desc-id" }, [p.id]), 665 | e("div", { class: ["comp-table-tag-box", "m-outer"] }, [ 666 | ...tags, 667 | e("div", { class: ["comp-table-tag-box"], style: "margin-left: auto;" }, [ 668 | dynRegister( 669 | e("div", { 670 | class: ["comp-table-db", "comp-db-interfaces"], 671 | onClick: interfacesExpandClickHandler 672 | }, [icInterfaces()]), 673 | { type: "descButton", buttonType: KEY_EXPAND_INTERFACES, proto: p } 674 | ), 675 | dynRegister( 676 | e("div", { 677 | class: ["comp-table-db", "comp-db-description"], 678 | onClick: fullDescExpandClickHandler 679 | }, [icDescription()]), 680 | { type: "descButton", buttonType: KEY_EXPAND_FULLDESC, proto: p } 681 | ), 682 | ]), 683 | ]), 684 | ]) 685 | table.appendChild(descCell) 686 | dynRegister(descCell, { type: "row", proto: p }) 687 | 688 | function createDataCell(c, opts) { 689 | const interface = opts?.interface 690 | 691 | const support = 692 | getCompositorSupport(c.id, p, interface) 693 | 694 | const cellContentClass = `comp-table-cell-support-${support}` 695 | const cellText = icSupportCell[support] ?? icSupportCell.__default 696 | 697 | const cellClasses = ["comp-table-cell", "comp-table-cell-data"] 698 | const cellContentClasses = ["comp-table-cell-content", cellContentClass] 699 | if (interface != null) { 700 | cellClasses.push("comp-table-interface") 701 | cellContentClasses.push("comp-table-cell-interface-content") 702 | } 703 | 704 | const cell = e("div", { class: cellClasses, data: { comp: c.id } }, [ 705 | e("div", { class: cellContentClasses, title: c.name }, [cellText]) 706 | ]) 707 | 708 | table.appendChild(cell) 709 | dynRegister(cell, { type: "data", comp: c, proto: p, interface }) 710 | } 711 | 712 | for (const c of data.compositors) { 713 | /* Setup data cells (summary) */ 714 | createDataCell(c) 715 | } 716 | 717 | function getDescriptionTextElements(d) { 718 | const elements = [] 719 | 720 | function addTitle(title, class_) { 721 | if (title == null) 722 | return 723 | elements.push(e(`h${title.level ?? 1}`, { 724 | class: ["i-heading", title.mono ? "m-mono" : null, class_] 725 | }, [title.text])) 726 | } 727 | 728 | addTitle(d.title) 729 | addTitle(d.subTitle, "m-sub") 730 | 731 | elements.push(e("div", { 732 | class: ["i-text", d.textOpts?.secondary ? "m-sec" : null] 733 | }, [d.text]),) 734 | 735 | return elements 736 | } 737 | 738 | if (p.descFull != null) { 739 | const fullDescCell = dynRegister( 740 | e("div", { class: ["comp-table-cell", "comp-table-fulldesc"] }, [ 741 | e("div", { class: ["i-wrapper"] }, [ 742 | p.descFull.flatMap((d) => getDescriptionTextElements(d)) 743 | ]) 744 | ]), 745 | { type: "descFull", proto: p } 746 | ) 747 | table.appendChild(fullDescCell) 748 | } 749 | 750 | const interfacesToShow = [] 751 | const hasAnyDeprecations = p.deprecations != null 752 | let hasVisibleDeprecations = false 753 | 754 | for (const interfaceId of Object.keys(p.supportIf)) { 755 | const data = { id: interfaceId } 756 | const interfaceDeprecation = p.deprecations?.[interfaceId] 757 | if (interfaceDeprecation) { 758 | hasVisibleDeprecations = true 759 | data.deprecated = interfaceDeprecation 760 | } 761 | interfacesToShow.push(data) 762 | } 763 | 764 | if (interfacesToShow.length == 0 && hasAnyDeprecations) { 765 | for (const [interfaceId, reason] of Object.entries(p.deprecations)) { 766 | const data = { id: interfaceId, deprecated: reason } 767 | interfacesToShow.push(data) 768 | } 769 | } 770 | 771 | for (const interface of interfacesToShow) { 772 | /* Setup interface row titles */ 773 | const interfaceId = interface.id 774 | 775 | const intetfaceCell = e("div", { class: ["comp-table-desc", "comp-table-interface"] }, [ 776 | e("div", { class: ["comp-table-desc-name", "comp-table-desc-name-interface"] }, [ 777 | interfaceId.replace(/_/g, "\u200b_"), 778 | ]), 779 | interface.deprecated 780 | ? e("div", { class: ["comp-table-interface-deprecation"] }, [ 781 | e("b", {}, ["Deprecated: "]), 782 | e("span", {}, [interface.deprecated]) 783 | ]) 784 | : null, 785 | ]) 786 | table.appendChild(intetfaceCell) 787 | dynRegister(intetfaceCell, { type: "row", proto: p, interface: interfaceId }) 788 | 789 | for (const c of data.compositors) { 790 | /* Setup data cells (interfaces) */ 791 | createDataCell(c, { interface: interfaceId }) 792 | } 793 | } 794 | 795 | if ((hasVisibleDeprecations && !p.deprecatedFull) || p.defaultExpand) 796 | expandState.set(stateKey(KEY_EXPAND_INTERFACES, p.id), true) 797 | } 798 | 799 | // === Root elements === 800 | 801 | const tableFixOuter = e("div", 802 | { class: ["comp-header-fix"] }, 803 | [tableFix] 804 | ) 805 | 806 | const root = e("div", 807 | { class: "comp-table-root", style: { "--cols": compCount + 1 } }, 808 | [tableFixOuter, table] 809 | ) 810 | 811 | // === Setup hover handling === 812 | 813 | function mouseMoveHandlerSet(...elements) { 814 | let lastHoverElement = null 815 | let lastCursorPos = null 816 | 817 | function highlightHandler(cursorX, cursorY) { 818 | const hoverElement = document.elementFromPoint(cursorX, cursorY) 819 | const targetElement = findParent(hoverElement, ":is(.comp-table-cell, .comp-table-desc)") 820 | 821 | if (targetElement == null) { 822 | const hadLast = lastHoverElement != null 823 | lastHoverElement = null 824 | highlightColumn = null 825 | highlightRow = null 826 | if (hadLast) 827 | syncState(SYNC_DUE_TO_MOUSEMOVE) 828 | } 829 | else if (lastHoverElement == null || lastHoverElement.deref() != targetElement) { 830 | const m = dynState.get(targetElement) 831 | highlightColumn = m?.comp?.id ?? null 832 | highlightRow = getRowKey(m) 833 | highlightSourceCellMetadata = m 834 | if (targetElement != null) 835 | lastHoverElement = new WeakRef(targetElement) 836 | syncState(SYNC_DUE_TO_MOUSEMOVE) 837 | } 838 | } 839 | 840 | function mouseMovementHighlightHandler(ev) { 841 | lastCursorPos = [ev.clientX, ev.clientY] 842 | highlightHandler(ev.clientX, ev.clientY) 843 | } 844 | 845 | function scrollHighlightHandler(ev) { 846 | if (lastCursorPos == null) 847 | return 848 | const [x, y] = lastCursorPos 849 | highlightHandler(x, y) 850 | } 851 | 852 | elements.forEach((el) => { 853 | if (el != null) 854 | el.addEventListener("mousemove", mouseMovementHighlightHandler) 855 | }) 856 | 857 | document.addEventListener("scroll", scrollHighlightHandler) 858 | } 859 | 860 | mouseMoveHandlerSet( 861 | table, 862 | root, 863 | targetContainer, 864 | document.querySelector("body") 865 | ) 866 | 867 | // === Setup fixed header scroll handling === 868 | 869 | const fixStyle = tableFixOuter.style 870 | 871 | function fixedHeaderVisibilityCallback() { 872 | const offset = table.getBoundingClientRect().y 873 | const show = offset < -100 874 | if (show) 875 | fixStyle["visibility"] = "visible" 876 | fixStyle["opacity"] = show ? 1 : 0 877 | } 878 | 879 | tableFixOuter.addEventListener("transitionend", () => { 880 | const opacity = tableFixOuter.style["opacity"] ?? 0 881 | tableFixOuter.style["visibility"] = opacity > 0 ? "visible" : "hidden" 882 | }) 883 | 884 | document.addEventListener("scroll", fixedHeaderVisibilityCallback) 885 | fixedHeaderVisibilityCallback() 886 | 887 | window.addEventListener("resize", updateHeaderWidth) 888 | document.addEventListener("scroll", updateFixedHeaderPosition) 889 | 890 | // === Populate page === 891 | 892 | targetContainer.innerHTML = "" 893 | targetContainer.appendChild(root) 894 | 895 | materialIcons.initIcons() 896 | syncState() 897 | } 898 | 899 | (() => { 900 | const boot = () => setTimeout(async () => { 901 | // === Setup page === 902 | 903 | const container = document.getElementById("content") 904 | const statusText = document.getElementById("loading-status") 905 | const setStatus = (text) => { if (statusText) statusText.innerText = text } 906 | 907 | setStatus("Downloading data.json...") 908 | const dataResp = await fetch("./data.json") 909 | const data = await dataResp.json() 910 | 911 | setStatus("Rendering page...") 912 | renderPageCompositorTable(container, data) 913 | }, 0) 914 | 915 | switch (document.readyState) { 916 | case "complete": 917 | case "interactive": 918 | case "loaded": 919 | boot() 920 | break 921 | default: 922 | document.addEventListener("DOMContentLoaded", boot) 923 | } 924 | })(); 925 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: AGPL-3.0-only 2 | 3 | GNU AFFERO GENERAL PUBLIC LICENSE 4 | Version 3, 19 November 2007 5 | 6 | Copyright (C) 2007 Free Software Foundation, Inc. 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The GNU Affero General Public License is a free, copyleft license for 13 | software and other kinds of works, specifically designed to ensure 14 | cooperation with the community in the case of network server software. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | our General Public Licenses are intended to guarantee your freedom to 19 | share and change all versions of a program--to make sure it remains free 20 | software for all its users. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | Developers that use our General Public Licenses protect your rights 30 | with two steps: (1) assert copyright on the software, and (2) offer 31 | you this License which gives you legal permission to copy, distribute 32 | and/or modify the software. 33 | 34 | A secondary benefit of defending all users' freedom is that 35 | improvements made in alternate versions of the program, if they 36 | receive widespread use, become available for other developers to 37 | incorporate. Many developers of free software are heartened and 38 | encouraged by the resulting cooperation. However, in the case of 39 | software used on network servers, this result may fail to come about. 40 | The GNU General Public License permits making a modified version and 41 | letting the public access it on a server without ever releasing its 42 | source code to the public. 43 | 44 | The GNU Affero General Public License is designed specifically to 45 | ensure that, in such cases, the modified source code becomes available 46 | to the community. It requires the operator of a network server to 47 | provide the source code of the modified version running there to the 48 | users of that server. Therefore, public use of a modified version, on 49 | a publicly accessible server, gives the public access to the source 50 | code of the modified version. 51 | 52 | An older license, called the Affero General Public License and 53 | published by Affero, was designed to accomplish similar goals. This is 54 | a different license, not a version of the Affero GPL, but Affero has 55 | released a new version of the Affero GPL which permits relicensing under 56 | this license. 57 | 58 | The precise terms and conditions for copying, distribution and 59 | modification follow. 60 | 61 | TERMS AND CONDITIONS 62 | 63 | 0. Definitions. 64 | 65 | "This License" refers to version 3 of the GNU Affero General Public License. 66 | 67 | "Copyright" also means copyright-like laws that apply to other kinds of 68 | works, such as semiconductor masks. 69 | 70 | "The Program" refers to any copyrightable work licensed under this 71 | License. Each licensee is addressed as "you". "Licensees" and 72 | "recipients" may be individuals or organizations. 73 | 74 | To "modify" a work means to copy from or adapt all or part of the work 75 | in a fashion requiring copyright permission, other than the making of an 76 | exact copy. The resulting work is called a "modified version" of the 77 | earlier work or a work "based on" the earlier work. 78 | 79 | A "covered work" means either the unmodified Program or a work based 80 | on the Program. 81 | 82 | To "propagate" a work means to do anything with it that, without 83 | permission, would make you directly or secondarily liable for 84 | infringement under applicable copyright law, except executing it on a 85 | computer or modifying a private copy. Propagation includes copying, 86 | distribution (with or without modification), making available to the 87 | public, and in some countries other activities as well. 88 | 89 | To "convey" a work means any kind of propagation that enables other 90 | parties to make or receive copies. Mere interaction with a user through 91 | a computer network, with no transfer of a copy, is not conveying. 92 | 93 | An interactive user interface displays "Appropriate Legal Notices" 94 | to the extent that it includes a convenient and prominently visible 95 | feature that (1) displays an appropriate copyright notice, and (2) 96 | tells the user that there is no warranty for the work (except to the 97 | extent that warranties are provided), that licensees may convey the 98 | work under this License, and how to view a copy of this License. If 99 | the interface presents a list of user commands or options, such as a 100 | menu, a prominent item in the list meets this criterion. 101 | 102 | 1. Source Code. 103 | 104 | The "source code" for a work means the preferred form of the work 105 | for making modifications to it. "Object code" means any non-source 106 | form of a work. 107 | 108 | A "Standard Interface" means an interface that either is an official 109 | standard defined by a recognized standards body, or, in the case of 110 | interfaces specified for a particular programming language, one that 111 | is widely used among developers working in that language. 112 | 113 | The "System Libraries" of an executable work include anything, other 114 | than the work as a whole, that (a) is included in the normal form of 115 | packaging a Major Component, but which is not part of that Major 116 | Component, and (b) serves only to enable use of the work with that 117 | Major Component, or to implement a Standard Interface for which an 118 | implementation is available to the public in source code form. A 119 | "Major Component", in this context, means a major essential component 120 | (kernel, window system, and so on) of the specific operating system 121 | (if any) on which the executable work runs, or a compiler used to 122 | produce the work, or an object code interpreter used to run it. 123 | 124 | The "Corresponding Source" for a work in object code form means all 125 | the source code needed to generate, install, and (for an executable 126 | work) run the object code and to modify the work, including scripts to 127 | control those activities. However, it does not include the work's 128 | System Libraries, or general-purpose tools or generally available free 129 | programs which are used unmodified in performing those activities but 130 | which are not part of the work. For example, Corresponding Source 131 | includes interface definition files associated with source files for 132 | the work, and the source code for shared libraries and dynamically 133 | linked subprograms that the work is specifically designed to require, 134 | such as by intimate data communication or control flow between those 135 | subprograms and other parts of the work. 136 | 137 | The Corresponding Source need not include anything that users 138 | can regenerate automatically from other parts of the Corresponding 139 | Source. 140 | 141 | The Corresponding Source for a work in source code form is that 142 | same work. 143 | 144 | 2. Basic Permissions. 145 | 146 | All rights granted under this License are granted for the term of 147 | copyright on the Program, and are irrevocable provided the stated 148 | conditions are met. This License explicitly affirms your unlimited 149 | permission to run the unmodified Program. The output from running a 150 | covered work is covered by this License only if the output, given its 151 | content, constitutes a covered work. This License acknowledges your 152 | rights of fair use or other equivalent, as provided by copyright law. 153 | 154 | You may make, run and propagate covered works that you do not 155 | convey, without conditions so long as your license otherwise remains 156 | in force. You may convey covered works to others for the sole purpose 157 | of having them make modifications exclusively for you, or provide you 158 | with facilities for running those works, provided that you comply with 159 | the terms of this License in conveying all material for which you do 160 | not control copyright. Those thus making or running the covered works 161 | for you must do so exclusively on your behalf, under your direction 162 | and control, on terms that prohibit them from making any copies of 163 | your copyrighted material outside their relationship with you. 164 | 165 | Conveying under any other circumstances is permitted solely under 166 | the conditions stated below. Sublicensing is not allowed; section 10 167 | makes it unnecessary. 168 | 169 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 170 | 171 | No covered work shall be deemed part of an effective technological 172 | measure under any applicable law fulfilling obligations under article 173 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 174 | similar laws prohibiting or restricting circumvention of such 175 | measures. 176 | 177 | When you convey a covered work, you waive any legal power to forbid 178 | circumvention of technological measures to the extent such circumvention 179 | is effected by exercising rights under this License with respect to 180 | the covered work, and you disclaim any intention to limit operation or 181 | modification of the work as a means of enforcing, against the work's 182 | users, your or third parties' legal rights to forbid circumvention of 183 | technological measures. 184 | 185 | 4. Conveying Verbatim Copies. 186 | 187 | You may convey verbatim copies of the Program's source code as you 188 | receive it, in any medium, provided that you conspicuously and 189 | appropriately publish on each copy an appropriate copyright notice; 190 | keep intact all notices stating that this License and any 191 | non-permissive terms added in accord with section 7 apply to the code; 192 | keep intact all notices of the absence of any warranty; and give all 193 | recipients a copy of this License along with the Program. 194 | 195 | You may charge any price or no price for each copy that you convey, 196 | and you may offer support or warranty protection for a fee. 197 | 198 | 5. Conveying Modified Source Versions. 199 | 200 | You may convey a work based on the Program, or the modifications to 201 | produce it from the Program, in the form of source code under the 202 | terms of section 4, provided that you also meet all of these conditions: 203 | 204 | a) The work must carry prominent notices stating that you modified 205 | it, and giving a relevant date. 206 | 207 | b) The work must carry prominent notices stating that it is 208 | released under this License and any conditions added under section 209 | 7. This requirement modifies the requirement in section 4 to 210 | "keep intact all notices". 211 | 212 | c) You must license the entire work, as a whole, under this 213 | License to anyone who comes into possession of a copy. This 214 | License will therefore apply, along with any applicable section 7 215 | additional terms, to the whole of the work, and all its parts, 216 | regardless of how they are packaged. This License gives no 217 | permission to license the work in any other way, but it does not 218 | invalidate such permission if you have separately received it. 219 | 220 | d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms 238 | of sections 4 and 5, provided that you also convey the 239 | machine-readable Corresponding Source under the terms of this License, 240 | in one of these ways: 241 | 242 | a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | 247 | b) Convey the object code in, or embodied in, a physical product 248 | (including a physical distribution medium), accompanied by a 249 | written offer, valid for at least three years and valid for as 250 | long as you offer spare parts or customer support for that product 251 | model, to give anyone who possesses the object code either (1) a 252 | copy of the Corresponding Source for all the software in the 253 | product that is covered by this License, on a durable physical 254 | medium customarily used for software interchange, for a price no 255 | more than your reasonable cost of physically performing this 256 | conveying of source, or (2) access to copy the 257 | Corresponding Source from a network server at no charge. 258 | 259 | c) Convey individual copies of the object code with a copy of the 260 | written offer to provide the Corresponding Source. This 261 | alternative is allowed only occasionally and noncommercially, and 262 | only if you received the object code with such an offer, in accord 263 | with subsection 6b. 264 | 265 | d) Convey the object code by offering access from a designated 266 | place (gratis or for a charge), and offer equivalent access to the 267 | Corresponding Source in the same way through the same place at no 268 | further charge. You need not require recipients to copy the 269 | Corresponding Source along with the object code. If the place to 270 | copy the object code is a network server, the Corresponding Source 271 | may be on a different server (operated by you or a third party) 272 | that supports equivalent copying facilities, provided you maintain 273 | clear directions next to the object code saying where to find the 274 | Corresponding Source. Regardless of what server hosts the 275 | Corresponding Source, you remain obligated to ensure that it is 276 | available for as long as needed to satisfy these requirements. 277 | 278 | e) Convey the object code using peer-to-peer transmission, provided 279 | you inform other peers where the object code and Corresponding 280 | Source of the work are being offered to the general public at no 281 | charge under subsection 6d. 282 | 283 | A separable portion of the object code, whose source code is excluded 284 | from the Corresponding Source as a System Library, need not be 285 | included in conveying the object code work. 286 | 287 | A "User Product" is either (1) a "consumer product", which means any 288 | tangible personal property which is normally used for personal, family, 289 | or household purposes, or (2) anything designed or sold for incorporation 290 | into a dwelling. In determining whether a product is a consumer product, 291 | doubtful cases shall be resolved in favor of coverage. For a particular 292 | product received by a particular user, "normally used" refers to a 293 | typical or common use of that class of product, regardless of the status 294 | of the particular user or of the way in which the particular user 295 | actually uses, or expects or is expected to use, the product. A product 296 | is a consumer product regardless of whether the product has substantial 297 | commercial, industrial or non-consumer uses, unless such uses represent 298 | the only significant mode of use of the product. 299 | 300 | "Installation Information" for a User Product means any methods, 301 | procedures, authorization keys, or other information required to install 302 | and execute modified versions of a covered work in that User Product from 303 | a modified version of its Corresponding Source. The information must 304 | suffice to ensure that the continued functioning of the modified object 305 | code is in no case prevented or interfered with solely because 306 | modification has been made. 307 | 308 | If you convey an object code work under this section in, or with, or 309 | specifically for use in, a User Product, and the conveying occurs as 310 | part of a transaction in which the right of possession and use of the 311 | User Product is transferred to the recipient in perpetuity or for a 312 | fixed term (regardless of how the transaction is characterized), the 313 | Corresponding Source conveyed under this section must be accompanied 314 | by the Installation Information. But this requirement does not apply 315 | if neither you nor any third party retains the ability to install 316 | modified object code on the User Product (for example, the work has 317 | been installed in ROM). 318 | 319 | The requirement to provide Installation Information does not include a 320 | requirement to continue to provide support service, warranty, or updates 321 | for a work that has been modified or installed by the recipient, or for 322 | the User Product in which it has been modified or installed. Access to a 323 | network may be denied when the modification itself materially and 324 | adversely affects the operation of the network or violates the rules and 325 | protocols for communication across the network. 326 | 327 | Corresponding Source conveyed, and Installation Information provided, 328 | in accord with this section must be in a format that is publicly 329 | documented (and with an implementation available to the public in 330 | source code form), and must require no special password or key for 331 | unpacking, reading or copying. 332 | 333 | 7. Additional Terms. 334 | 335 | "Additional permissions" are terms that supplement the terms of this 336 | License by making exceptions from one or more of its conditions. 337 | Additional permissions that are applicable to the entire Program shall 338 | be treated as though they were included in this License, to the extent 339 | that they are valid under applicable law. If additional permissions 340 | apply only to part of the Program, that part may be used separately 341 | under those permissions, but the entire Program remains governed by 342 | this License without regard to the additional permissions. 343 | 344 | When you convey a copy of a covered work, you may at your option 345 | remove any additional permissions from that copy, or from any part of 346 | it. (Additional permissions may be written to require their own 347 | removal in certain cases when you modify the work.) You may place 348 | additional permissions on material, added by you to a covered work, 349 | for which you have or can give appropriate copyright permission. 350 | 351 | Notwithstanding any other provision of this License, for material you 352 | add to a covered work, you may (if authorized by the copyright holders of 353 | that material) supplement the terms of this License with terms: 354 | 355 | a) Disclaiming warranty or limiting liability differently from the 356 | terms of sections 15 and 16 of this License; or 357 | 358 | b) Requiring preservation of specified reasonable legal notices or 359 | author attributions in that material or in the Appropriate Legal 360 | Notices displayed by works containing it; or 361 | 362 | c) Prohibiting misrepresentation of the origin of that material, or 363 | requiring that modified versions of such material be marked in 364 | reasonable ways as different from the original version; or 365 | 366 | d) Limiting the use for publicity purposes of names of licensors or 367 | authors of the material; or 368 | 369 | e) Declining to grant rights under trademark law for use of some 370 | trade names, trademarks, or service marks; or 371 | 372 | f) Requiring indemnification of licensors and authors of that 373 | material by anyone who conveys the material (or modified versions of 374 | it) with contractual assumptions of liability to the recipient, for 375 | any liability that these contractual assumptions directly impose on 376 | those licensors and authors. 377 | 378 | All other non-permissive additional terms are considered "further 379 | restrictions" within the meaning of section 10. If the Program as you 380 | received it, or any part of it, contains a notice stating that it is 381 | governed by this License along with a term that is a further 382 | restriction, you may remove that term. If a license document contains 383 | a further restriction but permits relicensing or conveying under this 384 | License, you may add to a covered work material governed by the terms 385 | of that license document, provided that the further restriction does 386 | not survive such relicensing or conveying. 387 | 388 | If you add terms to a covered work in accord with this section, you 389 | must place, in the relevant source files, a statement of the 390 | additional terms that apply to those files, or a notice indicating 391 | where to find the applicable terms. 392 | 393 | Additional terms, permissive or non-permissive, may be stated in the 394 | form of a separately written license, or stated as exceptions; 395 | the above requirements apply either way. 396 | 397 | 8. Termination. 398 | 399 | You may not propagate or modify a covered work except as expressly 400 | provided under this License. Any attempt otherwise to propagate or 401 | modify it is void, and will automatically terminate your rights under 402 | this License (including any patent licenses granted under the third 403 | paragraph of section 11). 404 | 405 | However, if you cease all violation of this License, then your 406 | license from a particular copyright holder is reinstated (a) 407 | provisionally, unless and until the copyright holder explicitly and 408 | finally terminates your license, and (b) permanently, if the copyright 409 | holder fails to notify you of the violation by some reasonable means 410 | prior to 60 days after the cessation. 411 | 412 | Moreover, your license from a particular copyright holder is 413 | reinstated permanently if the copyright holder notifies you of the 414 | violation by some reasonable means, this is the first time you have 415 | received notice of violation of this License (for any work) from that 416 | copyright holder, and you cure the violation prior to 30 days after 417 | your receipt of the notice. 418 | 419 | Termination of your rights under this section does not terminate the 420 | licenses of parties who have received copies or rights from you under 421 | this License. If your rights have been terminated and not permanently 422 | reinstated, you do not qualify to receive new licenses for the same 423 | material under section 10. 424 | 425 | 9. Acceptance Not Required for Having Copies. 426 | 427 | You are not required to accept this License in order to receive or 428 | run a copy of the Program. Ancillary propagation of a covered work 429 | occurring solely as a consequence of using peer-to-peer transmission 430 | to receive a copy likewise does not require acceptance. However, 431 | nothing other than this License grants you permission to propagate or 432 | modify any covered work. These actions infringe copyright if you do 433 | not accept this License. Therefore, by modifying or propagating a 434 | covered work, you indicate your acceptance of this License to do so. 435 | 436 | 10. Automatic Licensing of Downstream Recipients. 437 | 438 | Each time you convey a covered work, the recipient automatically 439 | receives a license from the original licensors, to run, modify and 440 | propagate that work, subject to this License. You are not responsible 441 | for enforcing compliance by third parties with this License. 442 | 443 | An "entity transaction" is a transaction transferring control of an 444 | organization, or substantially all assets of one, or subdividing an 445 | organization, or merging organizations. If propagation of a covered 446 | work results from an entity transaction, each party to that 447 | transaction who receives a copy of the work also receives whatever 448 | licenses to the work the party's predecessor in interest had or could 449 | give under the previous paragraph, plus a right to possession of the 450 | Corresponding Source of the work from the predecessor in interest, if 451 | the predecessor has it or can get it with reasonable efforts. 452 | 453 | You may not impose any further restrictions on the exercise of the 454 | rights granted or affirmed under this License. For example, you may 455 | not impose a license fee, royalty, or other charge for exercise of 456 | rights granted under this License, and you may not initiate litigation 457 | (including a cross-claim or counterclaim in a lawsuit) alleging that 458 | any patent claim is infringed by making, using, selling, offering for 459 | sale, or importing the Program or any portion of it. 460 | 461 | 11. Patents. 462 | 463 | A "contributor" is a copyright holder who authorizes use under this 464 | License of the Program or a work on which the Program is based. The 465 | work thus licensed is called the contributor's "contributor version". 466 | 467 | A contributor's "essential patent claims" are all patent claims 468 | owned or controlled by the contributor, whether already acquired or 469 | hereafter acquired, that would be infringed by some manner, permitted 470 | by this License, of making, using, or selling its contributor version, 471 | but do not include claims that would be infringed only as a 472 | consequence of further modification of the contributor version. For 473 | purposes of this definition, "control" includes the right to grant 474 | patent sublicenses in a manner consistent with the requirements of 475 | this License. 476 | 477 | Each contributor grants you a non-exclusive, worldwide, royalty-free 478 | patent license under the contributor's essential patent claims, to 479 | make, use, sell, offer for sale, import and otherwise run, modify and 480 | propagate the contents of its contributor version. 481 | 482 | In the following three paragraphs, a "patent license" is any express 483 | agreement or commitment, however denominated, not to enforce a patent 484 | (such as an express permission to practice a patent or covenant not to 485 | sue for patent infringement). To "grant" such a patent license to a 486 | party means to make such an agreement or commitment not to enforce a 487 | patent against the party. 488 | 489 | If you convey a covered work, knowingly relying on a patent license, 490 | and the Corresponding Source of the work is not available for anyone 491 | to copy, free of charge and under the terms of this License, through a 492 | publicly available network server or other readily accessible means, 493 | then you must either (1) cause the Corresponding Source to be so 494 | available, or (2) arrange to deprive yourself of the benefit of the 495 | patent license for this particular work, or (3) arrange, in a manner 496 | consistent with the requirements of this License, to extend the patent 497 | license to downstream recipients. "Knowingly relying" means you have 498 | actual knowledge that, but for the patent license, your conveying the 499 | covered work in a country, or your recipient's use of the covered work 500 | in a country, would infringe one or more identifiable patents in that 501 | country that you have reason to believe are valid. 502 | 503 | If, pursuant to or in connection with a single transaction or 504 | arrangement, you convey, or propagate by procuring conveyance of, a 505 | covered work, and grant a patent license to some of the parties 506 | receiving the covered work authorizing them to use, propagate, modify 507 | or convey a specific copy of the covered work, then the patent license 508 | you grant is automatically extended to all recipients of the covered 509 | work and works based on it. 510 | 511 | A patent license is "discriminatory" if it does not include within 512 | the scope of its coverage, prohibits the exercise of, or is 513 | conditioned on the non-exercise of one or more of the rights that are 514 | specifically granted under this License. You may not convey a covered 515 | work if you are a party to an arrangement with a third party that is 516 | in the business of distributing software, under which you make payment 517 | to the third party based on the extent of your activity of conveying 518 | the work, and under which the third party grants, to any of the 519 | parties who would receive the covered work from you, a discriminatory 520 | patent license (a) in connection with copies of the covered work 521 | conveyed by you (or copies made from those copies), or (b) primarily 522 | for and in connection with specific products or compilations that 523 | contain the covered work, unless you entered into that arrangement, 524 | or that patent license was granted, prior to 28 March 2007. 525 | 526 | Nothing in this License shall be construed as excluding or limiting 527 | any implied license or other defenses to infringement that may 528 | otherwise be available to you under applicable patent law. 529 | 530 | 12. No Surrender of Others' Freedom. 531 | 532 | If conditions are imposed on you (whether by court order, agreement or 533 | otherwise) that contradict the conditions of this License, they do not 534 | excuse you from the conditions of this License. If you cannot convey a 535 | covered work so as to satisfy simultaneously your obligations under this 536 | License and any other pertinent obligations, then as a consequence you may 537 | not convey it at all. For example, if you agree to terms that obligate you 538 | to collect a royalty for further conveying from those to whom you convey 539 | the Program, the only way you could satisfy both those terms and this 540 | License would be to refrain entirely from conveying the Program. 541 | 542 | 13. Remote Network Interaction; Use with the GNU General Public License. 543 | 544 | Notwithstanding any other provision of this License, if you modify the 545 | Program, your modified version must prominently offer all users 546 | interacting with it remotely through a computer network (if your version 547 | supports such interaction) an opportunity to receive the Corresponding 548 | Source of your version by providing access to the Corresponding Source 549 | from a network server at no charge, through some standard or customary 550 | means of facilitating copying of software. This Corresponding Source 551 | shall include the Corresponding Source for any work covered by version 3 552 | of the GNU General Public License that is incorporated pursuant to the 553 | following paragraph. 554 | 555 | Notwithstanding any other provision of this License, you have 556 | permission to link or combine any covered work with a work licensed 557 | under version 3 of the GNU General Public License into a single 558 | combined work, and to convey the resulting work. The terms of this 559 | License will continue to apply to the part which is the covered work, 560 | but the work with which it is combined will remain governed by version 561 | 3 of the GNU General Public License. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU Affero General Public License from time to time. Such new versions 567 | will be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU Affero General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU Affero General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU Affero General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU Affero General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU Affero General Public License for more details. 646 | 647 | You should have received a copy of the GNU Affero General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If your software can interact with users remotely through a computer 653 | network, you should also make sure that it provides a way for users to 654 | get its source. For example, if your program is a web application, its 655 | interface could display a "Source" link that leads users to an archive 656 | of the code. There are many ways you could offer source, and different 657 | solutions will be better for different programs; see section 13 for the 658 | specific requirements. 659 | 660 | You should also get your employer (if you work as a programmer) or school, 661 | if any, to sign a "copyright disclaimer" for the program, if necessary. 662 | For more information on this, and how to apply and follow the GNU AGPL, see 663 | . 664 | --------------------------------------------------------------------------------