[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\"@babel/preset-env\"]\n}\n"
  },
  {
    "path": ".browserslistrc",
    "content": "last 1 electron version\n"
  },
  {
    "path": ".dockerignore",
    "content": "build\ndist\noutput\ntools\ndocs\nassets\ncoverage\nnode_modules\n.git\n.gitignore\n.electron-symbols\n.DS_Store\n.env\n.nyc_output"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Run tests\non: push\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    env:\n      PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1\n    steps:\n    - name: Install ubuntu requirements\n      if: ${{ matrix.os == 'ubuntu-latest' }}\n      run: |\n        sudo apt-get -qq update\n        sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev\n    - uses: actions/checkout@v5\n    - name: Use Node.js\n      uses: actions/setup-node@v6\n      with:\n        node-version-file: '.nvmrc'\n        cache: 'npm'\n    - uses: actions/setup-python@v5\n      with:\n        python-version: \"3.11\"\n    - name: Specify MSVS version\n      if: ${{ matrix.os == 'windows-latest' }}\n      shell: powershell\n      run: |\n        echo \"GYP_MSVS_VERSION=2022\" >> $env:GITHUB_ENV\n    - run: npm ci\n    - run: npm rebuild\n    - run: npm run test-lib\n    - name: Run integration tests\n      if: ${{ matrix.os != 'ubuntu-latest' }}\n      run: npm run test-ui\n    - uses: actions/upload-artifact@v4\n      if: always()\n      with:\n        name: logs\n        #/Users/runner/Library/Logs/mocha/\n        # ~/.config/Before Dawn/logs/\n        path: |\n          C:\\Users\\runneradmin\\AppData\\Roaming\\mocha\\logs\\\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n  schedule:\n    - cron: '42 16 * * 5'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'javascript' ]\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v5\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/eslint.yml",
    "content": "name: eslint\n\n# Run this workflow every time a new commit pushed to your repository\non: push\n\njobs:\n  # Set the job key. The key is displayed as the job name\n  # when a job name is not provided\n  eslint:\n    # Name the Job\n    name: Run eslint\n    # Set the type of machine to run on\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install ubuntu requirements\n        run: |\n          sudo apt-get -qq update\n          sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev\n\n      # Checks out a copy of your repository on the ubuntu-latest machine\n      - name: Checkout code\n        uses: actions/checkout@v5\n      - name: Use Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: '.nvmrc'\n          cache: 'npm'\n      - uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n      - name: Setup code\n        run: npm ci\n      - name: Run eslint\n        run: npm run eslint-all\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Build release\non:\n  push:\n    branches:\n      - main\njobs:\n\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    env:\n      # https://stackoverflow.com/questions/77251296/distutils-not-found-when-running-npm-install\n      PYTHON: 3.11\n    steps:\n    - name: Install ubuntu requirements\n      if: ${{ matrix.os == 'ubuntu-latest' }}\n      run: |\n        sudo apt-get -qq update\n        sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev\n    - name: Checkout code\n      uses: actions/checkout@v5\n    - name: Use Node.js\n      uses: actions/setup-node@v6\n      with:\n        node-version-file: '.nvmrc'\n        cache: 'npm'\n    - uses: actions/setup-python@v5\n      with:\n        python-version: \"3.11\"\n    - name: Specify MSVS version\n      if: ${{ matrix.os == 'windows-latest' }}\n      shell: powershell\n      run: |\n        echo \"GYP_MSVS_VERSION=2022\" >> $env:GITHUB_ENV    \n    - run: npm ci\n    - run: npm rebuild\n    - name: Release\n      env:\n        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n      run: npm run release\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\nnode_modules\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\nbower_components\n\n*.map\n\n\n.sass-cache\n.nyc_output\n\noutput/*\ndist/*\n\n.electron-symbols\n.env\n\ndata/savers\ndata/*.*\n\n.vscode/*\nTODO\nnotes.txt\nbin/sentry.properties\n\n# ignore some assets i don't want in git\nassets/noun*\n\n# ignore VS code file\n*.code-workspace\n# Sentry Config File\n.env.sentry-build-plugin\n"
  },
  {
    "path": ".nvmrc",
    "content": "22.21.1\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n## [v0.9.25](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-04-05)\n- Fix some issues with the preview tool, which wasn't properly passing\n  params to the screensaver preview\n\n## [v0.9.24](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-03-15)\n- Save settings when 'create screensaver' button is clicked\n- Fix issue where screensaver data wasn't loading properly\n- Remove lodash from codebase\n- Add environment variable toggle for running in development mode without hot reload\n- Fix issue with updating timestamp of package update checks\n- Remove command-line argument parsing\n- Switch a lot of callback-based code to use promises\n- Attempt to deal with failed download issues\n\n## [v0.9.23](https://github.com/muffinista/before-dawn/tree/v0.9.23) (2018-02-23)\n\n- Fix logging issue\n\n## [v0.9.22](https://github.com/muffinista/before-dawn/tree/v0.9.22) (2018-02-22)\n\n- Fix issue where screensaver package would be downloaded twice at the\n  same time, which is definitely a bad thing.\n\n## [v0.9.21](https://github.com/muffinista/before-dawn/tree/v0.9.21) (2018-02-19)\n\n- Add preferences option to run on a single display -- this helps keep\n  CPU load down\n- Add a 'yes/no' boolean option for screensavers -- this can be used\n  to add boolean fields for screensavers.\n- Minor updates to styles -- I'm working on cleaning up the app styles\n  and CSS\n- Keep app running if user quits the main window\n- Activate asar for builds -- this should make downloads smaller and\n  maybe help a bit with performance.\n- Cleanup logging output and the code used to setup auto launching\n- Update electron version\n\n\n## [v0.9.20](https://github.com/muffinista/before-dawn/tree/v0.9.20) (2018-01-07)\n- Fix some issues with proper release checking -- it was broken before\n\n## [v0.9.19](https://github.com/muffinista/before-dawn/tree/v0.9.19) (2018-01-06)\n- Fix issue with missing version data\n\n## [v0.9.18](https://github.com/muffinista/before-dawn/tree/v0.9.18) (2018-01-04)\n- Make sure delay/sleep values are integers\n\n## [v0.9.17](https://github.com/muffinista/before-dawn/tree/v0.9.17) (2018-01-03)\n- Update method of displaying screensaver windows since things seem to\n  have broken in High Sierra\n- Tweak fullscreen.js to handle OSX issues\n- Add some logging code to savers library\n- Handle screensaver load/parse errors rather than entirely failing\n- Update Electron version\n\n## [v0.9.16](https://github.com/muffinista/before-dawn/tree/v0.9.16) (2017-12-14)\n- Notify main process when user preferences have been updated\n- Disable 'Save' button when creating new screensaver, but local\n  directory isn't setup.\n- Remove crash reporter, since it's basically unused\n\n## [v0.9.15](https://github.com/muffinista/before-dawn/tree/v0.9.15) (2017-12-14)\n- Update raven/sentry setup, fix some issues with paths in the app\n\n## [v0.9.14](https://github.com/muffinista/before-dawn/tree/v0.9.14) (2017-12-11)\n- Fix some issues with setting local directory properly\n\n## [v0.9.13](https://github.com/muffinista/before-dawn/tree/v0.9.13) (2017-12-08)\n- Replaced React with Vue.js, and did a lot of refactoring and cleanup\n  of javascript in general. Most of the UI code is in Vue components\n  now. Previously, it was a mix of React, vanilla JS, and some jQuery.\n- Moved code/asset compilation into webpack. This makes development a\n  little easier to manage.\n- Updated how data and objects get passed between the UI and the main\n  process, which should help make the app more performant.\n- Added some tweaks to hopefully take care of some annoying OSX\n  security issues.\n\n\n## [v0.9.12](https://github.com/muffinista/before-dawn/tree/v0.9.12) (2017-11-17)\n- Tweak some temp directory usage to try and fix some OSX issues\n\n\n## [v0.9.11](https://github.com/muffinista/before-dawn/tree/v0.9.11) (2017-11-16)\n- Add some safety checks to config reading -- if it's corrupted\n  somehow, just restart with a clean config\n- Pass the savers module to windows when opening them -- I think this\n  is faster than passing data back and forth\n\n## [v0.9.10](https://github.com/muffinista/before-dawn/tree/v0.9.10) (2017-11-13)\n- Add a random screensaver picker, as well as basic 'system\n  screensaver' support -- ie, screensavers that are integral to the\n  application and not installed as a separate package.\n- If 'Run Now' chosen in menu, don't check power state\n- Improve dock display -- show icon for more windows and hide only\n  when all windows are closed\n- Tweak layout of prefs window and the preview tool\n- Update main process to listen for events from windows and pass data\n  around. The main process has responsibilty for opening windows,\n  saving new screensavers, etc. \n- Reorganize code for app, switch to a single package.json\n- Make a bunch of calls asychronous\n- Use async/await in a few places\n- Add some data caching to help performance\n- When launching screensavers, don't take screengrab unless\n  requested - this greatly speeds up launch time\n- Switch to yarn, cleanup build process\n  - I'd prefer to not have yarn as a dependency, but it does a better\n    job of handling installations across multiple platforms -- ie,\n    windows and OSX\n- Add webpack and use it to build UI assets\n  - I also might get rid of this at some point, and also React for\n    that matter\n- Update bootstrap version and assorted styling\n- Update electron version\n- Update React version and a bunch of assorted components\n- Add mocha tests\n- Add appveyor and Travis builds\n- Update some stale packages, and remove some dead ones\n\n## [v0.9.9](https://github.com/muffinista/before-dawn/tree/v0.9.9) (2017-11-13)\n- This version was yanked before it had a chance to truly shine. RIP\n\n## [v0.9.8](https://github.com/muffinista/before-dawn/tree/v0.9.8) (2017-08-03)\n- Minor bug fixes\n\n## [v0.9.7](https://github.com/muffinista/before-dawn/tree/v0.9.7) (2017-07-28)\n- Scroll to the currently selected screensaver when rendering prefs\n  panel\n- Handle missing screensaver object in watcher window\n\n## [v0.9.6](https://github.com/muffinista/before-dawn/tree/v0.9.6) (2017-07-11)\n- Fix some issues with loading screensavers from folders with spaces in their name\n- Add some handlers for power on/off events\n- Close running screensavers when the display count changes (the user\n  has plugged/unplugged a monitor)\n\n## [v0.9.5](https://github.com/muffinista/before-dawn/tree/v0.9.5) (2017-06-27)\n- Fix some bugs that can occur when setting a custom screensaver\n  source directory that either doesn't exist or is empty.\n\n## [v0.9.4](https://github.com/muffinista/before-dawn/tree/v0.9.4) (2017-06-09)\n- Disable ASAR packages. I think there's a few things that are broken\n  when they are being used, and I want to have the whole app running\n  more smoothly before switching back to them.\n- Add link to issues URL so users can report bugs.\n- Fix bug where we tried to render preview when a screensaver hadn't\n  been selected yet.\n\n\n## [v0.9.3](https://github.com/muffinista/before-dawn/tree/v0.9.3) (2017-06-01)\n- Tweak state machine to rely on idle time checks and not much else\n- Fix bug with (I think) newer versions of Electron where opening a\n  BrowserWindow would trigger a reset of idle time on Windows.\n- Hide mouse by using robotjs to move mouse off screen when showing screensaver\n- Build ASAR packages\n- Implement crash reporting, and some sentry.io error reporting\n- Add a background color to boot process to look a little nicer\n- Optimize screen grabber code, fix some CPU spikes\n- Assorted library/code updates\n\n## [v0.9.2](https://github.com/muffinista/before-dawn/tree/v0.9.2) (2017-03-01)\n- Fix bug where \"don't run on battery\" would always be true\n- Tweak fullscreen detection code a bit, move into its own module\n- Move some platform-specific deps into 'optionalDependencies'\n\n## [v0.9.0](https://github.com/muffinista/before-dawn/tree/v0.9.0) (2017-03-01)\n- Check for fullscreen apps and don't activate the screensaver if one is running\n- Sort screensavers alphabetically regardless of capitalization\n- Add right-click action to system tray\n- If disabled, display a 'paused' icon in system tray\n- Tweaked tray icon to be a little bolder\n- Updated preferences display\n- Fixed a case where I think the app would stop checking idle time, so it wouldn't load a screensaver\n\n\n## [v0.8.3](https://github.com/muffinista/before-dawn/tree/v0.8.3) (2017-01-27)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.1...v0.8.3)\n\n## [v0.8.1](https://github.com/muffinista/before-dawn/tree/v0.8.1) (2017-01-17)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.0...v0.8.1)\n\n## [v0.8.0](https://github.com/muffinista/before-dawn/tree/v0.8.0) (2017-01-14)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.7.0...v0.8.0)\n\n## [v0.7.0](https://github.com/muffinista/before-dawn/tree/v0.7.0) (2016-07-15)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.3...v0.7.0)\n\n## [v0.6.3](https://github.com/muffinista/before-dawn/tree/v0.6.3) (2016-03-09)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.1...v0.6.3)\n\n## [v0.6.1](https://github.com/muffinista/before-dawn/tree/v0.6.1) (2016-03-05)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.0...v0.6.1)\n\n## [v0.6.0](https://github.com/muffinista/before-dawn/tree/v0.6.0) (2016-03-04)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.5.0...v0.6.0)\n\n## [v0.5.0](https://github.com/muffinista/before-dawn/tree/v0.5.0) (2016-02-23)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.4.0...v0.5.0)\n\n## [v0.4.0](https://github.com/muffinista/before-dawn/tree/v0.4.0) (2016-02-21)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.3...v0.4.0)\n\n## [v0.3](https://github.com/muffinista/before-dawn/tree/v0.3) (2016-02-20)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.2...v0.3)\n\n## [v0.2](https://github.com/muffinista/before-dawn/tree/v0.2) (2016-02-10)\n[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.1...v0.2)\n\n## [v0.1](https://github.com/muffinista/before-dawn/tree/v0.1) (2016-02-04)\n\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License\n\nCopyright (c) 2016 Colin Mitchell http://muffinlabs.com/\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Before Dawn\n\nBefore Dawn is a an open-source, cross-platform screensaver application using\nweb-based technologies. You can generate screensavers with it using HTML/CSS,\njavascript, canvas, and any tools that rely on those technologies. In theory,\ngenerating a Before Dawn screensaver is as simple as writing an HTML page.\n\nThe project developed out of a personal project to explore the history of early\nscreensavers. I decided that I wanted to write a framework that I could use to\nactually run screensavers on my computer. I wanted it to be cross-platform and\neasily accessible to artists and developers.\n\nBefore Dawn is definitely a bit of a experiment -- to actually use it, you need\nto run it as a separate application on your computer and disable whatever\nscreensaver you have running in your OS, but it is fun and definitely works.\n\nThe core of the app is built on [Electron](https://www.electronjs.org/), a system\nthat allows you to build desktop applications that run on\n[node.js](https://nodejs.org/) and are rendered via Chrome.\n\nThere's a bunch of screensavers to choose from. Here's the Flying Emoji screensaver:\n\n![flying\nemoji](https://github.com/muffinista/before-dawn/raw/main/assets/emoji-on-monitor-opt.gif\n\"Flying Emoji!\")\n\nYou can get a quick preview of the other screensavers via this [preview\npage](http://muffinista.github.io/before-dawn-screensavers/).\n\n\nThe first time you open the application, the preferences window will open. You\ncan preview and pick screensavers there:\n\n![preferences window](assets/prefs.png \"Preferences Window\")\n\nThere's an 'Advanced Settings' section where you can specify certain options for\nhow the application should run.\n\n![settings window](assets/settings.png \"Preferences Settings\")\n\n\n## Downloads\n\nInstallers are available from the\n[releases](https://github.com/muffinista/before-dawn/releases) page.\n\n## Status\n\nRight now the application itself is pretty stable. This repo includes the main\ncode for running the actual screensaver, a simple app for picking your\nscreensaver and setting some options, and a bunch of modules to pull it all\ntogether.\n\nThe actual code for the screensavers is in [a separate\nrepo](https://github.com/muffinista/before-dawn-screensavers). If you want to\nwrite a screensaver, please add it to the project via a PR!\n\n## Running It\n\nThe easiest way to use the tool is to install it to your computer. You can grab\nan installer from the\n[releases](https://github.com/muffinista/before-dawn/releases) page. Binaries\nare available for OSX and Windows, and there's an experimental release for\nUbuntu/Debian.\n\nOnce it's running, there will be a sunrise icon in your system tray, with a few\ndifferent options. If you click 'Preferences,' you can preview the different\nscreensavers, set how much idle time is required before the screensaver starts\nto run, specify custom paths, etc.\n\nOnce you've set all of that up, Before Dawn will happily run in the background,\nand when it detects that you have been idle, it will engage your screensaver.\nThat's all there is to it!\n\n### A note for Linux/Wayland users\n\nWhen using Wayland, seemingly every time it's started the app will request \npermission to capture the screen. I get around this by running with \n`XDG_SESSION_TYPE=x11` set.\n\n\n## Building It\n\nSteps for generating your own build Before Dawn are listed in [the\nwiki](https://github.com/muffinista/before-dawn/wiki/Building-Before-Dawn)\n\n## Hacking It\n\nIf you would like to hack on Before Dawn, there's some instructions on the\n[Development page](https://github.com/muffinista/before-dawn/wiki/Development)\nin the wiki. It's pretty straightforward once you have a basic setup in place.\n\n\n## How to Write a Screensaver\n\nA Before Dawn screensaver is basically just a web page running in fullscreen\nmode. That said, there's a few twists to make it run as smoothly as possible.\nThere's a bunch of specific implementation details in [the\nwiki](https://github.com/muffinista/before-dawn/wiki/Writing-A-Screensaver).\n\nThere's also a very basic editor mode built into Before Dawn, which will\ngenerate some basic code for you to work from, and will make it easier to add\nsome configurable options to your work.\n\nThe editor has a simple preview, a form to describe the screensaver, and a\nsection where you can add custom options for your screensaver:\n\n![editor window](assets/editor.png \"Editor Window\")\n\n\n## Contributing\n\nContributions and suggestions are eagerly accepted. Please check out the [code\nof\nconduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)\nbefore contributing.\n\nIf you find a bug or have a suggestion, you can open an issue or a pull request\nhere.\n\nIf you would like to add a screensaver to the program, you can submit a PR to\nthe\n[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)\nrepo.\n\nI will accept pretty much any pull request to the repository given that the\ncontent you are posting is legal and appropriate. If you need help or have a\nsuggestion, please feel free to open an issue here.\n\n\n## Copyright/License\n\nUnless otherwise stated, Copyright (c) 2024 [Colin\nMitchell](http://muffinlabs.com).\n\nBefore Dawn is is distributed under the MIT licence -- Please see LICENSE.txt\nfor further details.\n\n\n"
  },
  {
    "path": "bin/build-icon.js",
    "content": "#!/usr/bin/env node\n\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport * as tmp from \"tmp\";\nimport * as fs from \"fs\";\nimport pngToIco from \"png-to-ico\";\nimport { Jimp } from \"jimp\";\n\nconst sizes = [256, 128, 48, 32, 16];\n\nasync function main() {\n  let outputs = [];\n  let pauseOutputs = [];\n\n  const image = await Jimp.read(\"assets/icon.png\");\n  const pauseImage = await Jimp.read(\"assets/icon-paused.png\");\n\n  const tmpDir = tmp.dirSync().name;\n\n  for ( let index in sizes ) {\n    const size = sizes[index];\n    console.log(size);\n\n    const name = path.join(tmpDir, `icon-${size}.png`);\n    await image.resize({w: size, h: size});\n    await image.write(name);\n\n    outputs.push(name);\n\n    const pausedName = path.join(tmpDir, `icon-paused-${size}.png`);\n    await pauseImage.resize({w: size, h: size});\n    await pauseImage.write(pausedName);\n\n    pauseOutputs.push(pausedName);\n  }\n\n  console.log(outputs);\n  const buf = await pngToIco(outputs);\n  fs.writeFileSync(path.join(\"src\", \"main\", \"assets\", \"icon.ico\"), buf);\n\n  console.log(pauseOutputs);\n  const buf2 = await pngToIco(pauseOutputs);\n  fs.writeFileSync(path.join(\"src\", \"main\", \"assets\", \"icon-paused.ico\"), buf2);\n}\n\nmain().catch(e => console.error(e));\n"
  },
  {
    "path": "bin/build-on-ci.js",
    "content": "#!/usr/bin/env node\n\n/* eslint-disable no-console */\n\nrequire(\"dotenv\").config();\n\nconst apiUrl = \"https://ci.appveyor.com/api/account/muffinista/builds\";\nconst travisApiUrl = \"https://api.travis-ci.org/repo/muffinista%2Fbefore-dawn/requests\";\n\n\nconst body = {\n  \"accountName\": \"muffinista\",\n  \"projectSlug\": \"before-dawn\",\n  \"branch\": \"main\"\n};\n\nconst appveyorOpts = {\n  method: \"post\",\n  body:    JSON.stringify(body),\n  headers: { \n    \"Content-type\": \"application/json\",\n    \"Authorization\": `Bearer ${process.env.APPVEYOR_TOKEN}`\n  },\n};\n\nconst travisBody = {\n  \"request\": {\n    \"branch\": \"main\"\n  }\n};\nconst travisOpts = {\n  method: \"post\",\n  body: JSON.stringify(travisBody),\n  headers: {\n    \"Content-type\": \"application/json\",\n    \"Accept\": \"application/json\",\n    \"Authorization\": `token ${process.env.TRAVIS_TOKEN}`,\n    \"Travis-API-Version\": \"3\"\n  }\n};\n  \n\nfetch(apiUrl, appveyorOpts)\n    .then(res => res.json())\n    .then(json => console.log(json))\n    .then(() => {\n      fetch(travisApiUrl, travisOpts)\n      .then(res => res.json())\n      .then(json => console.log(json));\n    });"
  },
  {
    "path": "bin/capture-screens.js",
    "content": "#!/usr/bin/env node\n\n\"use strict\";\n\nconst path = require(\"path\");\n\nconst { _electron: electron } = require(\"playwright\");\nconst appPath = require(\"electron\");\n\nconst helpers = require(\"../test/helpers.js\");\n\nlet shim;\n\nconst SCREENSAVER = \"Screen Glitcher\";\n\nconst callIpc = async(method, opts={}) => {\n  await shim.fill(\"#ipc\", method);\n  await shim.fill(\"#ipcopts\", JSON.stringify(opts));\n  await shim.click(\"text=go\");\n};\n\nasync function main() {\n  let env = {\n    CONFIG_DIR:  \"/Users/colin/Library/Application Support/Before Dawn\",\n    BEFORE_DAWN_DIR:  \"/Users/colin/Library/Application Support/Before Dawn\",\n    TEST_MODE: true,\n    QUIET_MODE: true\n  };\n  \n  let app = await electron.launch({\n    path: appPath,\n    args: [path.join(__dirname, \"..\", \"output/main.js\")],\n    env: env\n  });\n  \n  // wait for the first window (our test shim) to open, and\n  // hang onto it for later use\n  shim = await app.firstWindow();\n\n  await callIpc(\"open-window prefs\");\n  let window = await helpers.waitFor(app, \"prefs\");\n  await window.click(`text=${SCREENSAVER}`);\n  await helpers.sleep(1000);\n  await window.screenshot({ path: path.join(__dirname, \"..\", \"assets\", \"prefs.png\") });\n\n  await window.click(\"button.settings\");\n  let settings = await helpers.waitFor(app, \"settings\");\n  await helpers.sleep(1000);\n  await settings.screenshot({ path: path.join(__dirname, \"..\", \"assets\", \"settings.png\") });\n\n  await callIpc(\"close-window settings\");\n\n  await window.click(\"button.create\");\n  let create = await helpers.waitFor(app, \"new\");\n  await helpers.sleep(1000);\n  await create.screenshot({ path: path.join(__dirname, \"..\", \"assets\", \"create-screensaver.png\") });\n  await create.click(\"button.cancel\");\n\n  var saversDir = helpers.getTempDir();\n  const saverJSON = helpers.addSaver(saversDir, \"saver-one\", \"saver.json\");\n\n  await callIpc(\"open-window editor\", {\n    screenshot: \"file://\" + path.join(__dirname, \"../fixtures/screenshot.png\"),\n    src: saverJSON\n  });\n\n  let editor = await helpers.waitFor(app, \"editor\");\n  await helpers.sleep(1000);\n  await editor.screenshot({ path: path.join(__dirname, \"..\", \"assets\", \"editor.png\"), fullPage: true });\n\n  app.close();\n}\n\nmain().catch(e => console.error(e));"
  },
  {
    "path": "bin/dev-runner.js",
    "content": "\"use strict\";\n\nimport electron from \"electron\";\nimport * as path from \"path\";\nimport { spawn } from \"child_process\";\nimport webpack from \"webpack\";\nimport WebpackDevServer from \"webpack-dev-server\";\n\nimport { readFile } from 'fs/promises';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nlet devPort;\n\nimport mainConfig from \"../webpack.main.config.js\";\nimport rendererConfig from \"../webpack.renderer.config.js\";\n\ntry {\n  const packageJSON = JSON.parse(\n    await readFile(\n      new URL('../package.json', import.meta.url)\n    )\n  );\n  \n  devPort = packageJSON.devport;\n}\ncatch(e) {\n  console.log(`Can't read package.json, defaulting dev port to 9080: ${e}`);\n  devPort = 9080;\n}\n\n\nlet electronProcess = null;\nlet manualRestart = false;\nlet skipMainRestart = true;\n\n/**\n * Setup webpack compiler and server for the renderer process\n * \n * @returns Promise\n */\nfunction startRenderer () {\n  return new Promise((resolve) => {\n    const compiler = webpack(rendererConfig);\n\n    const serverOptions = {\n      host: \"localhost\",\n      port: devPort,\n      hot: true,\n      onListening: function (devServer) {\n        const port = devServer.server.address().port;\n        console.log(\"Listening on port:\", port);\n      },\n    };\n\n    const devServer = new WebpackDevServer(\n      serverOptions, compiler\n    );\n\n    devServer.startCallback(() => {\n      console.log(\"startRenderer finished\");\n      resolve();\n    })\n  });\n}\n\n/**\n * Setup webpack compiler and watcher for the main process\n * \n * @returns Promise\n */\n function startMain () {\n  return new Promise((resolve) => {\n    const mainCompiler = webpack(mainConfig);\n    mainCompiler.run(() => {\n      mainCompiler.watch({}, (err) => {\n        if (err) {\n          console.log(err);\n          return;\n        }\n\n        // skip the first watch event\n        if ( skipMainRestart ) {\n          skipMainRestart = false;\n          return;\n        }\n\n        // kill and restart the main process\n        if (electronProcess && electronProcess.kill) {\n          manualRestart = true;\n          process.kill(electronProcess.pid);\n          electronProcess = null;\n          startElectron();\n\n          setTimeout(() => {\n            manualRestart = false;\n          }, 5000);\n        }\n      });\n\n      resolve();\n    });\n  });\n}\n\nfunction startElectron () {\n  // @todo set environment here\n  electronProcess = spawn(electron, [\"--no-sandbox\", \"--inspect=5858\", path.join(__dirname, \"../src/main/index.js\")]);\n\n  electronProcess.stdout.on(\"data\", data => {\n    process.stdout.write(data.toString());\n  });\n  electronProcess.stderr.on(\"data\", data => {\n    process.stdout.write(data.toString());\n  });\n  electronProcess.once(\"close\", () => {\n    if (!manualRestart) {process.exit();}\n  });\n}\n\nfunction init () {\n  Promise.all([startRenderer(), startMain()])\n    .then(startElectron)\n    .catch(console.error);\n}\n\ninit();\n"
  },
  {
    "path": "bin/download-screensavers.js",
    "content": "#!/usr/bin/env node\n\n/* eslint-disable no-console */\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\nimport { rimraf } from 'rimraf'\nimport * as mkdirp from \"mkdirp\";\nimport { Octokit } from \"octokit\";\n\nimport Package from \"../src/lib/package.js\";\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst srcRoot = path.join(__dirname, \"..\");\nconst workingDir = path.join(srcRoot, \"data\");\n\nlet opts = {};\n\nconst octokit = new Octokit(opts);\n\nlet owner = \"muffinista\";\nlet repo = \"before-dawn-screensavers\";\n\n\nconsole.log(\"cleaning up working dir\", workingDir);\n\n// ensure directory exists, clean it out, then recreate just to be sure\nmkdirp.sync(workingDir);\nrimraf.sync(workingDir);\nmkdirp.sync(workingDir);\n\nasync function main() {\n  let result = await octokit.rest.repos.getLatestRelease({owner, repo});\n\n  const tag_name = result.data.tag_name;\n  const jsonFile = `${repo}-${tag_name}.json`;\n\n  const jsonDest = path.join(srcRoot, \"data\", jsonFile);\n  fs.writeFileSync(jsonDest, JSON.stringify(result.data));\n\n  const url = result.data.zipball_url;\n  const dest = path.join(srcRoot, \"data\", \"savers\");\n\n  const p = new Package({\n    repo: `${owner}/${repo}`,\n    dest: dest\n  });\n\n  await p.downloadRelease(url, dest);\n}\n\nmain().catch(e => console.error(e));\n\n/* eslint-enable no-console */\n"
  },
  {
    "path": "bin/generate-release.js",
    "content": "#!/usr/bin/env node\n\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport { Octokit } from \"octokit\";\nimport { readFile } from 'fs/promises';\nimport { fileURLToPath } from 'url';\n\nimport SentryCli from '@sentry/cli';\nconst pjson = JSON.parse(\n  await readFile(\n    new URL('../package.json', import.meta.url)\n  )\n);\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n\nlet opts = {\n  auth: `token ${process.env.GITHUB_AUTH_TOKEN}`\n};\n\nconst octokit = new Octokit(opts);\n\nlet owner = \"muffinista\";\nlet repo = \"before-dawn\";\nlet tag_name = `v${pjson.version}`;\nlet draft = true;\nlet releaseName = `${pjson.productName} ${pjson.version}`;\n\nconst sentryCli = new SentryCli(path.join(__dirname, \"sentry.properties\"));\n\nasync function main() {\n  let release = {\n    owner: owner, \n    repo: repo, \n    tag_name: tag_name, \n    target_commitish: \"main\",\n    name: tag_name,\n    body: \"description\",\n    draft: draft\n  };\n  \n  console.log(`checking ${owner}/${repo} for ${tag_name}`);\n\n  let result = await octokit.rest.repos.getLatestRelease({owner, repo});\n  if ( result.data.tag_name === tag_name ) {\n    console.log(\"release already created!\");\n  }\n  else {\n    console.log(release);\n\n    // Create a release\n    result = await octokit.rest.repos.createRelease(release);\n    console.log(result);\n  }\n\n\n\n  console.log(\"Create new release on sentry\");\n  await sentryCli.execute([\"releases\", \"new\", releaseName], true);\n\n  console.log(\"Add commits to release\");\n  await sentryCli.execute([\"releases\", \"set-commits\", \"--auto\", releaseName], true);\n\n  console.log(\"Upload sourcemaps\");\n  await sentryCli.execute([\"releases\", \"files\", releaseName, \"upload-sourcemaps\", \"output\"], true);\n\n  console.log(\"Finalize release\");\n  await sentryCli.execute([\"releases\", \"finalize\", releaseName], true);\n\n  console.log(\"Set new deploy\");\n  await sentryCli.execute([\"releases\", \"deploys\", releaseName, \"new\", \"--env\", \"production\"], true);\n}\n\nmain().catch(e => console.error(e));\n\n/* eslint-enable no-console */\n"
  },
  {
    "path": "bin/get-release-name",
    "content": "#!/usr/bin/env node\n\nconst path = require(\"path\");\n\nvar pjson = require(path.join(__dirname, \"..\", \"package.json\"));\nconsole.log(`${pjson.productName} ${pjson.version}`);\n"
  },
  {
    "path": "code_of_conduct.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at colin@muffinlabs.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "_site\n.sass-cache\n"
  },
  {
    "path": "docs/.ruby-version",
    "content": "3.3\n"
  },
  {
    "path": "docs/Gemfile",
    "content": "source 'https://rubygems.org'\ngem 'github-pages', group: :jekyll_plugins\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "# Site settings\ntitle: Before Dawn -- Screensavers for the Modern Era\nemail: colin at muffinlabs.com\ndescription: > # this means to ignore newlines until \"baseurl:\"\n  This is the help website for Before Dawn, an open source screensaver\n  app. Enjoy!\nbaseurl: \"\" # the subpath of your site, e.g. /blog/\nurl: \"https://muffinista.github.io/before-dawn/\" # the base hostname & protocol for your site\ntwitter_username: muffinista\ngithub_username:  muffinista\n\n# Build settings\nmarkdown: kramdown\n"
  },
  {
    "path": "docs/_includes/footer.html",
    "content": "<footer class=\"site-footer\">\n\n  <div class=\"wrapper\">\n    <div class=\"footer-col-wrapper\">\n      <div class=\"footer-col  footer-col-1\">\n        <p class=\"text\">{{ site.title }}</p>\n      </div>\n\n      <div class=\"footer-col  footer-col-2\">\n        <ul class=\"social-media-list\">\n          {% if site.github_username %}\n          <li>\n            <a href=\"https://github.com/{{ site.github_username }}\">\n              <span class=\"icon  icon--github\">\n                <svg viewBox=\"0 0 16 16\">\n                  <path fill=\"#828282\" d=\"M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z\"/>\n                </svg>\n              </span>\n\n              <span class=\"username\">{{ site.github_username }}</span>\n            </a>\n          </li>\n          {% endif %}\n\n          {% if site.twitter_username %}\n          <li>\n            <a href=\"https://twitter.com/{{ site.twitter_username }}\">\n              <span class=\"icon  icon--twitter\">\n                <svg viewBox=\"0 0 16 16\">\n                  <path fill=\"#828282\" d=\"M15.969,3.058c-0.586,0.26-1.217,0.436-1.878,0.515c0.675-0.405,1.194-1.045,1.438-1.809\n                  c-0.632,0.375-1.332,0.647-2.076,0.793c-0.596-0.636-1.446-1.033-2.387-1.033c-1.806,0-3.27,1.464-3.27,3.27 c0,0.256,0.029,0.506,0.085,0.745C5.163,5.404,2.753,4.102,1.14,2.124C0.859,2.607,0.698,3.168,0.698,3.767 c0,1.134,0.577,2.135,1.455,2.722C1.616,6.472,1.112,6.325,0.671,6.08c0,0.014,0,0.027,0,0.041c0,1.584,1.127,2.906,2.623,3.206 C3.02,9.402,2.731,9.442,2.433,9.442c-0.211,0-0.416-0.021-0.615-0.059c0.416,1.299,1.624,2.245,3.055,2.271 c-1.119,0.877-2.529,1.4-4.061,1.4c-0.264,0-0.524-0.015-0.78-0.046c1.447,0.928,3.166,1.469,5.013,1.469 c6.015,0,9.304-4.983,9.304-9.304c0-0.142-0.003-0.283-0.009-0.423C14.976,4.29,15.531,3.714,15.969,3.058z\"/>\n                </svg>\n              </span>\n\n              <span class=\"username\">{{ site.twitter_username }}</span>\n            </a>\n          </li>\n          {% endif %}\n        </ul>\n      </div>\n\n      <div class=\"footer-col  footer-col-3\">\n        <ul class=\"contact-list\">\n          <li><a href=\"mailto:{{ site.email }}\">{{ site.email }}</a></li>\n        </ul>\n      </div>\n    </div>\n\n  </div>\n\n</footer>\n"
  },
  {
    "path": "docs/_includes/head.html",
    "content": "<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width initial-scale=1\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\n    <title>{% if page.title %}{{ page.title }} :: {% endif %}{{ site.title }}</title>\n    <meta name=\"description\" content=\"{{ site.description }}\">\n    <link rel=\"stylesheet\" media=\"screen\" href=\"https://fontlibrary.org/face/chicagoflf\" type=\"text/css\"/>\n\n    <!--     <link rel=\"stylesheet\" href=\"{{ \"/css/main.css\" | prepend: site.baseurl }}\">-->\n    <link rel=\"stylesheet\" href=\"./css/main.css\">\n    <link rel=\"canonical\" href=\"{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}\">\n</head>\n"
  },
  {
    "path": "docs/_includes/header.html",
    "content": "<header class=\"site-header\">\n\n  <div class=\"wrapper\">\n\n    <a class=\"site-title\" href=\"./\">{{ site.title }}</a>\n\n    <nav class=\"site-nav\">\n      <a href=\"#\" class=\"menu-icon\">\n        <svg viewBox=\"0 0 18 15\">\n          <path fill=\"#424242\" d=\"M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z\"/>\n          <path fill=\"#424242\" d=\"M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z\"/>\n          <path fill=\"#424242\" d=\"M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z\"/>\n        </svg>\n      </a>\n\n      <div class=\"trigger\">\n        {% for page in site.pages %}\n          {% if page.menu_title %}\n          <a class=\"page-link\" href=\"{{ page.url | prepend: site.baseurl }}\">{{ page.menu_title }}</a>\n          {% endif %}\n        {% endfor %}\n      </div>\n    </nav>\n\n  </div>\n\n</header>\n"
  },
  {
    "path": "docs/_layouts/default.html",
    "content": "<!DOCTYPE html>\n<html>\n  {% include head.html %}\n  <body>\n\n    {% include header.html %}\n\n    <div class=\"page-content\">\n      <div class=\"wrapper\">\n        {{ content }}\n      </div>\n    </div>\n\n    {% include footer.html %}\n\n  </body>\n</html>\n"
  },
  {
    "path": "docs/_layouts/page.html",
    "content": "---\nlayout: default\n---\n<div class=\"post\">\n\n  <header class=\"post-header\">\n    <h1 class=\"post-title\">{{ page.title }}</h1>\n  </header>\n\n  <article class=\"post-content\">\n    {{ content }}\n  </article>\n\n</div>\n"
  },
  {
    "path": "docs/_layouts/post.html",
    "content": "---\nlayout: default\n---\n<div class=\"post\">\n\n  <header class=\"post-header\">\n    <h1 class=\"post-title\">{{ page.title }}</h1>\n    <p class=\"post-meta\">{{ page.date | date: \"%b %-d, %Y\" }}{% if page.author %} • {{ page.author }}{% endif %}{% if page.meta %} • {{ page.meta }}{% endif %}</p>\n  </header>\n\n  <article class=\"post-content\">\n    {{ content }}\n  </article>\n\n</div>\n"
  },
  {
    "path": "docs/_posts/2016-12-02-welcome-to-jekyll.markdown",
    "content": "---\nlayout: post\ntitle:  \"Welcome to Jekyll!\"\ndate:   2016-12-02 13:11:20\ncategories: jekyll update\n---\nYou’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve --watch`, which launches a web server and auto-regenerates your site when a file is updated.\n\nTo add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works.\n\nJekyll also offers powerful support for code snippets:\n\n{% highlight ruby %}\ndef print_hi(name)\n  puts \"Hi, #{name}\"\nend\nprint_hi('Tom')\n#=> prints 'Hi, Tom' to STDOUT.\n{% endhighlight %}\n\nCheck out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help].\n\n[jekyll]:      http://jekyllrb.com\n[jekyll-gh]:   https://github.com/jekyll/jekyll\n[jekyll-help]: https://github.com/jekyll/jekyll-help\n"
  },
  {
    "path": "docs/_sass/_base.scss",
    "content": "/**\n * Reset some basic elements\n */\nbody, h1, h2, h3, h4, h5, h6,\np, blockquote, pre, hr,\ndl, dd, ol, ul, figure {\n    margin: 0;\n    padding: 0;\n}\n\n\n\n/**\n * Basic styling\n */\nbody {\n    font-family: $base-font-family;\n    font-size: $base-font-size;\n    line-height: $base-line-height;\n    font-weight: 300;\n    color: $text-color;\n    background-color: $background-color;\n    -webkit-text-size-adjust: 100%;\n}\n\n\n\n/**\n * Set `margin-bottom` to maintain vertical rhythm\n */\nh1, h2, h3, h4, h5, h6,\np, blockquote, pre,\nul, ol, dl, figure,\n%vertical-rhythm {\n    margin-bottom: $spacing-unit / 2;\n}\n\n\n\n/**\n * Images\n */\nimg {\n    max-width: 100%;\n    vertical-align: middle;\n}\n\n\n\n/**\n * Figures\n */\nfigure > img {\n    display: block;\n}\n\nfigcaption {\n    font-size: $small-font-size;\n}\n\n\n\n/**\n * Lists\n */\nul, ol {\n    margin-left: $spacing-unit;\n}\n\nli {\n    > ul,\n    > ol {\n         margin-bottom: 0;\n    }\n}\n\n\n\n/**\n * Headings\n */\nh1, h2, h3, h4, h5, h6 {\n    font-weight: 300;\n}\n\n\n\n/**\n * Links\n */\na {\n    color: $brand-color;\n    text-decoration: none;\n\n    &:visited {\n        color: darken($brand-color, 15%);\n    }\n\n    &:hover {\n        color: $text-color;\n        text-decoration: underline;\n    }\n}\n\n\n\n/**\n * Blockquotes\n */\nblockquote {\n    color: $grey-color;\n    border-left: 4px solid $grey-color-light;\n    padding-left: $spacing-unit / 2;\n    font-size: 18px;\n    letter-spacing: -1px;\n    font-style: italic;\n\n    > :last-child {\n        margin-bottom: 0;\n    }\n}\n\n\n\n/**\n * Code formatting\n */\npre,\ncode {\n    font-size: 15px;\n    border: 1px solid $grey-color-light;\n    border-radius: 3px;\n    background-color: #eef;\n}\n\ncode {\n    padding: 1px 5px;\n}\n\npre {\n    padding: 8px 12px;\n    overflow-x: scroll;\n\n    > code {\n        border: 0;\n        padding-right: 0;\n        padding-left: 0;\n    }\n}\n\n\n\n/**\n * Wrapper\n */\n.wrapper {\n    max-width: -webkit-calc(800px - (#{$spacing-unit} * 2));\n    max-width:         calc(800px - (#{$spacing-unit} * 2));\n    margin-right: auto;\n    margin-left: auto;\n    padding-right: $spacing-unit;\n    padding-left: $spacing-unit;\n    @extend %clearfix;\n\n    @include media-query($on-laptop) {\n        max-width: -webkit-calc(800px - (#{$spacing-unit}));\n        max-width:         calc(800px - (#{$spacing-unit}));\n        padding-right: $spacing-unit / 2;\n        padding-left: $spacing-unit / 2;\n    }\n}\n\n\n\n/**\n * Clearfix\n */\n%clearfix {\n\n    &:after {\n        content: \"\";\n        display: table;\n        clear: both;\n    }\n}\n\n\n\n/**\n * Icons\n */\n.icon {\n\n    > svg {\n        display: inline-block;\n        width: 16px;\n        height: 16px;\n        vertical-align: middle;\n\n        path {\n            fill: $grey-color;\n        }\n    }\n}\n"
  },
  {
    "path": "docs/_sass/_layout.scss",
    "content": "/**\n * Site header\n */\n.site-header {\n    border-top: 5px solid $grey-color-dark;\n    border-bottom: 1px solid $grey-color-light;\n    min-height: 56px;\n\n    // Positioning context for the mobile navigation icon\n    position: relative;\n}\n\n.site-title {\n    font-size: 26px;\n    line-height: 56px;\n    letter-spacing: -1px;\n    margin-bottom: 0;\n    float: left;\n\n    &,\n    &:visited {\n        color: $grey-color-dark;\n    }\n}\n\n.site-nav {\n    float: right;\n    line-height: 56px;\n\n    .menu-icon {\n        display: none;\n    }\n\n    .page-link {\n        color: $text-color;\n        line-height: $base-line-height;\n\n        // Gaps between nav items, but not on the first one\n        &:not(:first-child) {\n            margin-left: 20px;\n        }\n    }\n\n    @include media-query($on-palm) {\n        position: absolute;\n        top: 9px;\n        right: 30px;\n        background-color: $background-color;\n        border: 1px solid $grey-color-light;\n        border-radius: 5px;\n        text-align: right;\n\n        .menu-icon {\n            display: block;\n            float: right;\n            width: 36px;\n            height: 26px;\n            line-height: 0;\n            padding-top: 10px;\n            text-align: center;\n\n            > svg {\n                width: 18px;\n                height: 15px;\n\n                path {\n                    fill: $grey-color-dark;\n                }\n            }\n        }\n\n        .trigger {\n            clear: both;\n            display: none;\n        }\n\n        &:hover .trigger {\n            display: block;\n            padding-bottom: 5px;\n        }\n\n        .page-link {\n            display: block;\n            padding: 5px 10px;\n        }\n    }\n}\n\n\n\n/**\n * Site footer\n */\n.site-footer {\n    border-top: 1px solid $grey-color-light;\n    padding: $spacing-unit 0;\n}\n\n.footer-heading {\n    font-size: 18px;\n    margin-bottom: $spacing-unit / 2;\n}\n\n.contact-list,\n.social-media-list {\n    list-style: none;\n    margin-left: 0;\n}\n\n.footer-col-wrapper {\n    font-size: 15px;\n    color: $grey-color;\n    margin-left: -$spacing-unit / 2;\n    @extend %clearfix;\n}\n\n.footer-col {\n    float: left;\n    margin-bottom: $spacing-unit / 2;\n    padding-left: $spacing-unit / 2;\n}\n\n.footer-col-1 {\n    width: -webkit-calc(35% - (#{$spacing-unit} / 2));\n    width:         calc(35% - (#{$spacing-unit} / 2));\n}\n\n.footer-col-2 {\n    width: -webkit-calc(20% - (#{$spacing-unit} / 2));\n    width:         calc(20% - (#{$spacing-unit} / 2));\n}\n\n.footer-col-3 {\n    width: -webkit-calc(45% - (#{$spacing-unit} / 2));\n    width:         calc(45% - (#{$spacing-unit} / 2));\n}\n\n@include media-query($on-laptop) {\n    .footer-col-1,\n    .footer-col-2 {\n        width: -webkit-calc(50% - (#{$spacing-unit} / 2));\n        width:         calc(50% - (#{$spacing-unit} / 2));\n    }\n\n    .footer-col-3 {\n        width: -webkit-calc(100% - (#{$spacing-unit} / 2));\n        width:         calc(100% - (#{$spacing-unit} / 2));\n    }\n}\n\n@include media-query($on-palm) {\n    .footer-col {\n        float: none;\n        width: -webkit-calc(100% - (#{$spacing-unit} / 2));\n        width:         calc(100% - (#{$spacing-unit} / 2));\n    }\n}\n\n\n\n/**\n * Page content\n */\n.page-content {\n    padding: $spacing-unit 0;\n}\n\n.page-heading {\n    font-size: 20px;\n}\n\n.post-list {\n    margin-left: 0;\n    list-style: none;\n\n    > li {\n        margin-bottom: $spacing-unit;\n    }\n}\n\n.post-meta {\n    font-size: $small-font-size;\n    color: $grey-color;\n}\n\n.post-link {\n    display: block;\n    font-size: 24px;\n}\n\n\n\n/**\n * Posts\n */\n.post-header {\n    margin-bottom: $spacing-unit;\n}\n\n.post-title {\n    font-size: 42px;\n    letter-spacing: -1px;\n    line-height: 1;\n\n    @include media-query($on-laptop) {\n        font-size: 36px;\n    }\n}\n\n.post-content {\n    margin-bottom: $spacing-unit;\n\n    h2 {\n        font-size: 32px;\n\n        @include media-query($on-laptop) {\n            font-size: 28px;\n        }\n    }\n\n    h3 {\n        font-size: 26px;\n\n        @include media-query($on-laptop) {\n            font-size: 22px;\n        }\n    }\n\n    h4 {\n        font-size: 20px;\n\n        @include media-query($on-laptop) {\n            font-size: 18px;\n        }\n    }\n}\n"
  },
  {
    "path": "docs/_sass/_syntax-highlighting.scss",
    "content": "/**\n * Syntax highlighting styles\n */\n.highlight {\n    background: #fff;\n    @extend %vertical-rhythm;\n\n    .c     { color: #998; font-style: italic } // Comment\n    .err   { color: #a61717; background-color: #e3d2d2 } // Error\n    .k     { font-weight: bold } // Keyword\n    .o     { font-weight: bold } // Operator\n    .cm    { color: #998; font-style: italic } // Comment.Multiline\n    .cp    { color: #999; font-weight: bold } // Comment.Preproc\n    .c1    { color: #998; font-style: italic } // Comment.Single\n    .cs    { color: #999; font-weight: bold; font-style: italic } // Comment.Special\n    .gd    { color: #000; background-color: #fdd } // Generic.Deleted\n    .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific\n    .ge    { font-style: italic } // Generic.Emph\n    .gr    { color: #a00 } // Generic.Error\n    .gh    { color: #999 } // Generic.Heading\n    .gi    { color: #000; background-color: #dfd } // Generic.Inserted\n    .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific\n    .go    { color: #888 } // Generic.Output\n    .gp    { color: #555 } // Generic.Prompt\n    .gs    { font-weight: bold } // Generic.Strong\n    .gu    { color: #aaa } // Generic.Subheading\n    .gt    { color: #a00 } // Generic.Traceback\n    .kc    { font-weight: bold } // Keyword.Constant\n    .kd    { font-weight: bold } // Keyword.Declaration\n    .kp    { font-weight: bold } // Keyword.Pseudo\n    .kr    { font-weight: bold } // Keyword.Reserved\n    .kt    { color: #458; font-weight: bold } // Keyword.Type\n    .m     { color: #099 } // Literal.Number\n    .s     { color: #d14 } // Literal.String\n    .na    { color: #008080 } // Name.Attribute\n    .nb    { color: #0086B3 } // Name.Builtin\n    .nc    { color: #458; font-weight: bold } // Name.Class\n    .no    { color: #008080 } // Name.Constant\n    .ni    { color: #800080 } // Name.Entity\n    .ne    { color: #900; font-weight: bold } // Name.Exception\n    .nf    { color: #900; font-weight: bold } // Name.Function\n    .nn    { color: #555 } // Name.Namespace\n    .nt    { color: #000080 } // Name.Tag\n    .nv    { color: #008080 } // Name.Variable\n    .ow    { font-weight: bold } // Operator.Word\n    .w     { color: #bbb } // Text.Whitespace\n    .mf    { color: #099 } // Literal.Number.Float\n    .mh    { color: #099 } // Literal.Number.Hex\n    .mi    { color: #099 } // Literal.Number.Integer\n    .mo    { color: #099 } // Literal.Number.Oct\n    .sb    { color: #d14 } // Literal.String.Backtick\n    .sc    { color: #d14 } // Literal.String.Char\n    .sd    { color: #d14 } // Literal.String.Doc\n    .s2    { color: #d14 } // Literal.String.Double\n    .se    { color: #d14 } // Literal.String.Escape\n    .sh    { color: #d14 } // Literal.String.Heredoc\n    .si    { color: #d14 } // Literal.String.Interpol\n    .sx    { color: #d14 } // Literal.String.Other\n    .sr    { color: #009926 } // Literal.String.Regex\n    .s1    { color: #d14 } // Literal.String.Single\n    .ss    { color: #990073 } // Literal.String.Symbol\n    .bp    { color: #999 } // Name.Builtin.Pseudo\n    .vc    { color: #008080 } // Name.Variable.Class\n    .vg    { color: #008080 } // Name.Variable.Global\n    .vi    { color: #008080 } // Name.Variable.Instance\n    .il    { color: #099 } // Literal.Number.Integer.Long\n}\n"
  },
  {
    "path": "docs/about.md",
    "content": "---\nlayout: page\ntitle: About\npermalink: /about.html\n---\n\n  <p>Before Dawn is a an open-source, cross-platform screensaver\n    application using web-based technologies. You can generate\n    screensavers with it using HTML/CSS, javascript, canvas, and any tools\n    that rely on those technologies. In theory, generating a Before Dawn\n    screensaver is as simple as writing an HTML page.</p>\n\n  <p>The project developed out of a personal project to explore the history\n    of early screensavers. I decided that I wanted to write a framework\n    that I could use to actually run screensavers on my computer. I wanted\n    it to be cross-platform and easily accessible to artists and\n    developers.</p>\n\n  <p>Before Dawn is definitely a bit of a experiment -- to actually use it,\n    you need to run it as a separate application on your computer and\n    disable whatever screensaver you have running in your OS, but it is\n    fun and definitely works.</p>\n\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "---\nlayout: page\ntitle: Contributing\npermalink: /contributing.html\n---\n\nContributions and suggestions are eagerly accepted. Please read the\n[code of conduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)\nbefore contributing.\n\n\nIf you find a bug or have a suggestion, you can\n[open an issue](https://github.com/muffinista/before-dawn/issues) or a\npull request on the main repository.\n\nThe screensavers for Before Dawn are in their own repository. If you\nwould like to add a screensaver to the program, you can submit a PR to\nthe\n[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)\nrepo.\n\nI will accept pretty much any pull request to the repository given\nthat the content you are posting is legal and appropriate. If you need\nhelp or have a suggestion, please feel free to open an issue.\n"
  },
  {
    "path": "docs/creating.md",
    "content": "---\nlayout: page\ntitle: Adding Your Own Screensaver\npermalink: /creating.html\n---\n\nA Before Dawn screensaver is an HTML page that runs in full screen\nmode. You can extend them with CSS and Javascript, and they can get\nvery complicated, but the basics are really very simple.\n\nBefore you add your own screensaver, you'll need to specify a local\ndirectory for your work in the Prefrences window. Once you've done\nthat, click the 'Create Screensaver' button and a little form will\nopen up where you can specify some basic information about your\nscreensaver:\n\n- *Name:* The name of your screensaver\n- *Description:* A brief description of your screensaver.\n- *About URL:* An optional URL with more details about your work.\n- *Author:* The author of this screensaver.\n\nAfter you enter in your values and click save, Before Dawn creates a\nfolder for you which contains an HTML file which serves as the basis\nof your new screensaver, and a `saver.json` file which contains the\ninformation about your screensaver. Before Dawn should display the\nfolder for you so you can get to work building your awesome\nscreensaver!\n\n## Testing, Adding Options ##\n\nAny screensaver that is in your local sources directory can be edited.\nIn the preview list, there will be an 'edit' link. Clicking that link\nopens a window which will allow you to update the basic information\nfor the screensaver, add configurable options to your screensaver,\nview a working preview of the screensaver, debug it, etc.\n\nThere are two tabs and a couple of buttons in the edit window. The\nPreview tab will run your screensaver. The window should auto-reload\nwhenever you save changes to your screensaver. The Settings tab is\nwhere you can update information about your screensaver, or add\nconfigurable options (see below). Then there are four buttons:\n\n- the folder button will open the working folder for your screensaver\n- the save button will save any changes you've made in the settings\n  form\n- the reload button will reload the preview\n- the bug button will open the developer tools console in case you\n  need to debug something.\n\n## Configurable Options ##\n\nBefore Dawn has a very simple interface for adding configurable\noptions to your screensaver. There are two kinds of inputs right now:\ntext and sliders. You could use a text input to allow users to specify\na URL or some text that will be used in your screensaver. A slider can\nbe used to allow the user to input a number, etc.\n\nScreensavers are loaded as URLs, and any options will be passed as\nvalues to the URL. The template contains some code to parse incoming\nvalues.\n\n"
  },
  {
    "path": "docs/css/main.scss",
    "content": "---\n# Only the main Sass file needs front matter (the dashes are enough)\n---\n@charset \"utf-8\";\n\n// Our variables\n$base-font-family: 'ChicagoFLFRegular', Helvetica, Arial, sans-serif;\n$base-font-size:   16px;\n$small-font-size:  $base-font-size * 0.875;\n$base-line-height: 1.5;\n\n$spacing-unit:     30px;\n\n$text-color:       #111;\n$background-color: #fdfdfd;\n$brand-color:      #2a7ae2;\n\n$grey-color:       #828282;\n$grey-color-light: lighten($grey-color, 40%);\n$grey-color-dark:  darken($grey-color, 25%);\n\n$on-palm:          600px;\n$on-laptop:        800px;\n\n\n// Using media queries with like this:\n// @include media-query($palm) {\n//     .wrapper {\n//         padding-right: $spacing-unit / 2;\n//         padding-left: $spacing-unit / 2;\n//     }\n// }\n@mixin media-query($device) {\n    @media screen and (max-width: $device) {\n        @content;\n    }\n}\n\n// Import partials from `sass_dir` (defaults to `_sass`)\n@import \"base\", \"layout\";\n\n\nhtml {\n    height: 100%;\n}\nbody {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    min-height: 100%; /* this helps with the sticky footer */\n}\n\n.page-content {\n    flex-grow: 1;\n    flex-shrink: 0;\n    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkIAIwEqFGipAiKQYGhmeEFIEtwqUIbALMKdgUoSjAZxKKf5BNwjAB3TqcCkAKAW9cBSRvYfskAAAAAElFTkSuQmCC) repeat;\n}\n\nfooter {\n    flex-shrink: 0;\n}\n\n.home {\n    display: flex;\n    p {\n        padding-right: 10px;\n    }\n    .main, .preview {\n        flex: 0 0 50%;\n    }\n\n    .preview {\n        height: 500px;\n        background-image: url(../background3.svg);\n        background-size: contain;\n        background-repeat: no-repeat;\n    }\n\n    img.saver {\n        margin-top: 50px;\n        margin-left: 32px;\n        width:310px;\n        height: 245px;\n    }\n}\n\n\n@media screen and (max-width: 800px) {\n    .home {\n        display: block;\n        .preview {\n            background-image: none;\n            img.saver {\n                width: 100%;\n                height: auto;\n                margin: 0px;\n            }\n        }\n    }\n}\n\n@include media-query($on-palm) {\n    .site-title {\n        max-width: $on-palm - 180px;\n    }\n}"
  },
  {
    "path": "docs/index.html",
    "content": "---\nlayout: page\ntitle: Welcome!\n---\n\n<div class=\"home\">\n  <div class=\"main\" >\n    <p>Welcome to the help website for Before Dawn, an open-source\n      screensaver application. Before Dawn runs on Windows, OSX and Linux,\n      and is powered by javascript.</p>\n\n    <p>Screensavers can be written in HTML\n      and Javascript. If it works in a browser, it can be a screensaver!</p>\n    \n    <p>You can\n      <a href=\"about.html\">learn more</a> about\n      Before Dawn, find out how to install it, and learn about\n      writing your own screensaver.\n    </p>\n  \n    <ul>\n      <li><a href=\"{{ 'installing.html' | prepend: site.baseurl }}\">How to Install</a></li>\n      <li><a href=\"{{ 'preferences.html' | prepend: site.baseurl }}\">Preferences and Options</a></li>\n      <li><a href=\"{{ 'creating.html' | prepend: site.baseurl }}\">Creating Your Own Screensaver</a></li>\n      <li><a href=\"{{ 'contributing.html' | prepend: site.baseurl }}\">Contributing</a></li>\n      <li><a href=\"https://github.com/muffinista/before-dawn\">Github Repository</a></li>\n    </ul>\n  </div>\n\n  <div class=\"preview\"\">\n    <img src=\"https://github.com/muffinista/before-dawn/raw/main/assets/emoji-screensaver.gif\" class=\"saver\" />\n  </div>\n</div>\n"
  },
  {
    "path": "docs/installing.md",
    "content": "---\nlayout: page\ntitle: Installing Before Dawn\npermalink: /installing.html\n---\n\nYou can find installers for Before Dawn on github in the\n[Releases section](https://github.com/muffinista/before-dawn/releases)\nof the project. Installing on OSX or Windows should be as simple as\ndownloading the appropriate file, and running it locally. Installing\non Linux is a little more involved, and I will add documentation for\nthat as soon as I can.\n\nThe first time you install it, you will need to run Before Dawn\nmanually. The preferences window will open, and you can pick a\nscreensaver and set whether you want Before Dawn to automatically load\nwhen your system boots, and you can tweak some other settings as well.\n"
  },
  {
    "path": "docs/preferences.md",
    "content": "---\nlayout: page\ntitle: Preferences\npermalink: /preferences.html\n---\n\nBefore Dawn has a Preferences window where you can see the list of\nscreensavers, and tweak your settings.\n\n## Picking a Screensaver ##\n\nThe main tab of the preferences window lists all of the screensavers\navailable to you. As you click on them, you'll see a preview in the\nright side of the window.\n\n### Screensaver Options ###\n\nSome screensavers have configurable options to control how they run --\nfor example, the Emoji Starfield screensaver has a control to set how\nmany emoji are on your screen at once. You can fiddle with these\nsettings and get an idea of how it looks in the preview area.\n\n## Options ##\n\nThe Options tab of the Preferences window has a number of settings to\ncontrol how Before Dawn runs:\n\n- *Activate after* -- you can set how long your computer is idle\nbefore your screensaver starts\n- *Sleep after* - you can set a time that Before Dawn will\nstop running and blank the screens. It's possible that your OS will do\na better job of this, but since Before Dawn is a bit of an experiment,\nhaving this setting might save your CPU from running when your\ncomputer is idle for a long time.\n- *Lock screen after running?* -- If you want your computer to lock\nonce the screensaver starts running, you can use this option.\n- *Disable when on battery?* -- You can toggle this so that Before Dawn\nwon't run if your computer is running on battery power.\n- *Auto start on login?* -- should Before Dawn start automatically\n  when your computer starts?\n\n### Advanced Options ###\n- *Local Source* -- If you want to develop your own screensavers, or\n  run a set of screensavers that aren't in the main package, you can put a path\n  to a directory here. Before Dawn will check this directory for any\n  local screensavers and add them to the list.\n\n\n### Create Screensaver ###\n\nCheck out the [creating](../creating) section to learn about adding your\nown screensaver.\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import mocha from \"eslint-plugin-mocha\";\nimport globals from \"globals\";\nimport js from \"@eslint/js\";\nimport svelte from 'eslint-plugin-svelte';\n\n\n/** @type {import('eslint').Linter.Config[]} */\nexport default [\n  mocha.configs.recommended,\n  js.configs.recommended,\n ...svelte.configs.recommended,\n  {\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.node, // Add this if you are using SvelteKit in non-SPA mode\n      },\n      ecmaVersion: \"latest\",\n      sourceType: \"module\",\n    }\n  },\n  {\n    files: ['**/*.svelte', '**/*.svelte.js'],\n    languageOptions: {\n      parserOptions: {\n        // We recommend importing and specifying svelte.config.js.\n        // By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.\n        // While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it,\n        // explicitly specifying it ensures better compatibility and functionality.\n        //          svelteConfig\n      }\n    },\n    plugins: {\n      \"plugin:svelte/recommended\": svelte\n    }\n  },\n  {\n    files: ['test/**/*.js'],\n    plugins: {\n      \"plugin:mocha/recommended\": mocha\n    },\n    languageOptions: {\n      globals: {\n        ...globals.mocha\n      },\n    },\n  },\n  {\n    ignores: [\n      \"output/*\",\n      \"data/*\"\n    ],\n    \n    rules: {\n      // Override or add rule settings here, such as:\n      // 'svelte/rule-name': 'error'\n    }\n  }\n];\n"
  },
  {
    "path": "lefthook.yml",
    "content": "# EXAMPLE USAGE\n# Refer for explanation to following link:\n# https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md\n#\n# pre-push:\n#   commands:\n#     packages-audit:\n#       tags: frontend security\n#       run: yarn audit\n#     gems-audit:\n#       tags: backend security\n#       run: bundle audit\n#\npre-commit:\n  parallel: true\n  commands:\n    eslint:\n      glob: \"*.{js,ts}\"\n      run: npm run eslint {staged_files}\n"
  },
  {
    "path": "mise.toml",
    "content": "[tools]\nnode = \"22.21.1\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"before-dawn\",\n  \"productName\": \"Before Dawn\",\n  \"version\": \"0.38.0\",\n  \"description\": \"A desktop screensaver app using web technologies\",\n  \"author\": \"Colin Mitchell <colin@muffinlabs.com> (http://muffinlabs.com)\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/muffinista/before-dawn/\",\n  \"release_server\": \"https://before-dawn-updates.muffinlabs.com\",\n  \"main\": \"output/main.js\",\n  \"type\": \"module\",\n  \"engines\": {\n    \"node\": \">= 22.14.0\"\n  },\n  \"devport\": 9081,\n  \"scripts\": {\n    \"dev\": \"node bin/dev-runner.js\",\n    \"compile\": \"cross-env NODE_ENV=production webpack --mode production --config webpack.config.js\",\n    \"eslint-all\": \"eslint -c eslint.config.mjs src/**/*.js src/**/*.svelte test/**/*.js webpack*.js\",\n    \"eslint\": \"eslint -c eslint.config.mjs\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"pack\": \"npm run compile && electron-builder --dir\",\n    \"dist\": \"npm run compile && electron-builder --x64\",\n    \"test\": \"npm run compile && mocha -b test/**/*.js\",\n    \"test-ui\": \"cross-env DISABLE_SENTRY=true npm run compile && xvfb-maybe mocha test/ui/**/*.js\",\n    \"test-lib\": \"mocha test test/lib/**/*.js test/main/**/*.js\",\n    \"run-local\": \"node bin/build-icon.js && npm run compile && cross-env ELECTRON_IS_DEV=0 electron output/main.js\",\n    \"grab-screens\": \"bin/capture-screens.js\",\n    \"release\": \"node bin/build-icon.js && node bin/download-screensavers.js && npm run dist\",\n    \"publish-release\": \"node bin/generate-release.js && git push origin main\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/muffinista/before-dawn.git\"\n  },\n  \"dependencies\": {\n    \"@muffinista/goto-sleep\": \"github:muffinista/goto-sleep\",\n    \"auto-launch\": \"^5.0.6\",\n    \"conf\": \"^15.0.2\",\n    \"detect-fullscreen\": \"github:muffinista/detect-fullscreen\",\n    \"electron-is-dev\": \"^3.0.1\",\n    \"electron-log\": \"^5.4.3\",\n    \"forcefocus\": \"github:muffinista/forcefocus\",\n    \"fs-extra\": \"^11.3.3\",\n    \"glob\": \"^13.0.0\",\n    \"hide-cursor\": \"github:muffinista/hide-cursor\",\n    \"mkdirp\": \"^3.0.1\",\n    \"proper-lockfile\": \"^4.1.2\",\n    \"rimraf\": \"^6.1.2\",\n    \"semver\": \"^7.7.3\",\n    \"temp\": \"^0.9.4\",\n    \"yauzl\": \"^3.2.1\"\n  },\n  \"overrides\": {\n    \"@electron/rebuild\": {\n      \"node-abi\": \"4.17.0\"\n    }\n  },\n  \"devDependencies\": {\n    \"@arkweid/lefthook\": \"^0.7.7\",\n    \"@babel/core\": \"^7.28.5\",\n    \"@babel/eslint-parser\": \"^7.28.5\",\n    \"@babel/plugin-transform-runtime\": \"^7.28.5\",\n    \"@babel/preset-env\": \"^7.28.5\",\n    \"@electron/rebuild\": \"^4.0.2\",\n    \"@eslint/eslintrc\": \"^3.3.3\",\n    \"@eslint/js\": \"^9.39.2\",\n    \"@sentry/cli\": \"^3.0.1\",\n    \"@sentry/electron\": \"^7.5.0\",\n    \"@sentry/webpack-plugin\": \"^4.6.1\",\n    \"babel-loader\": \"^10.0.0\",\n    \"clean-webpack-plugin\": \"^4.0.0\",\n    \"copy-webpack-plugin\": \"^13.0.1\",\n    \"cross-env\": \"^10.1.0\",\n    \"css-loader\": \"^7.1.2\",\n    \"dotenv\": \"^17.2.3\",\n    \"electron\": \"^39.8.5\",\n    \"electron-builder\": \"^26.3.6\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-friendly-formatter\": \"^4.0.1\",\n    \"eslint-plugin-html\": \"^8.1.3\",\n    \"eslint-plugin-mocha\": \"^11.2.0\",\n    \"eslint-plugin-n\": \"^17.23.1\",\n    \"eslint-plugin-promise\": \"^7.2.1\",\n    \"eslint-plugin-svelte\": \"^3.13.1\",\n    \"eslint-webpack-plugin\": \"^5.0.2\",\n    \"globals\": \"^17.0.0\",\n    \"html-webpack-plugin\": \"^5.6.5\",\n    \"jimp\": \"^1.6.0\",\n    \"mini-css-extract-plugin\": \"^2.9.4\",\n    \"mocha\": \"^11.7.5\",\n    \"nock\": \"^14.0.10\",\n    \"node-gyp\": \"^12.1.0\",\n    \"node-loader\": \"^2.1.0\",\n    \"normalize.css\": \"^8.0.1\",\n    \"octokit\": \"^5.0.5\",\n    \"playwright\": \"^1.57.0\",\n    \"png-to-ico\": \"^3.0.1\",\n    \"sass\": \"^1.97.1\",\n    \"sass-loader\": \"^16.0.6\",\n    \"sinon\": \"^21.0.1\",\n    \"style-loader\": \"^4.0.0\",\n    \"svelte\": \"^5.53.6\",\n    \"svelte-eslint-parser\": \"^1.4.1\",\n    \"svelte-loader\": \"^3.2.4\",\n    \"tmp\": \"^0.2.5\",\n    \"url-loader\": \"^4.1.1\",\n    \"webpack\": \"^5.104.1\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-dev-server\": \"^5.2.2\",\n    \"webpack-hot-middleware\": \"^2.26.1\",\n    \"xvfb-maybe\": \"^0.2.1\"\n  },\n  \"build\": {\n    \"files\": [\n      \"output/**/*\",\n      \"node_modules/**/*\",\n      \"package.json\"\n    ],\n    \"extraResources\": [\n      {\n        \"from\": \"data/savers\",\n        \"to\": \"savers\",\n        \"filter\": [\n          \"**/*\"\n        ]\n      }\n    ],\n    \"appId\": \"Before Dawn\",\n    \"mac\": {\n      \"category\": \"public.app-category.entertainment\",\n      \"extendInfo\": {\n        \"LSUIElement\": 1,\n        \"NSMicrophoneUsageDescription\": \"Some screensavers detect sound to provide interactivity. You can decline this permission if you do not want that.\",\n        \"NSCameraUsageDescription\": \"Some screensavers can use your webcam to provide interactivity. You can decline this permission if you do not want that.\"\n      }\n    },\n    \"nsis\": {\n      \"installerIcon\": \"build/icon.ico\",\n      \"perMachine\": false\n    },\n    \"win\": {\n      \"target\": \"nsis\",\n      \"icon\": \"build/icon.ico\"\n    },\n    \"asar\": true,\n    \"dmg\": {\n      \"contents\": [\n        {\n          \"x\": 338,\n          \"y\": 14,\n          \"type\": \"link\",\n          \"path\": \"/Applications\"\n        },\n        {\n          \"x\": 192,\n          \"y\": 14,\n          \"type\": \"file\"\n        }\n      ]\n    },\n    \"linux\": {\n      \"category\": \"Amusement\",\n      \"target\": \"deb\",\n      \"executableName\": \"before-dawn\",\n      \"maintainer\": \"Colin Mitchell <colin@muffinlabs.com>\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/css/styles.scss",
    "content": ":root {\n  --preview-wrapper-width: 500px;\n  --preview-wrapper-height: 320px;\n  --preview-width: 500px;\n  --preview-height: 320px;\n  --preview-scale: 1.0;\n  --footer-height: 30px;\n  --space-at-top: 1.0rem;\n  --footer-padding: 25px;\n  --small-padding: 10px;\n  --font-size: 14px;\n}\n\n@import \"normalize.css\";\n\nhtml {\n  height: 100%;\n  font-family: system-ui;\n  font-size: var(--font-size);\n}\nbody {\n  min-height: 100%;\n}\n\ninput, button {\n  font: small-caption;\n}\nselect {\n  font: menu;\n}\n\n\n\nbutton {\n  cursor: pointer;\n  height: 21px;\n  padding: 0px 16px;\n  font-size: var(--font-size);\n  border: 1px solid;\n  border-radius: 4px;\n  border-top-color: rgb(198, 198, 198);\n  border-bottom-color: rgb(170, 170, 170);\n  border-left-color: rgb(192, 192, 192);\n  border-right-color: rgb(192, 192, 192);\n}\n\n\nh1 {\n  font-size: 1.1rem;\n}\n#about h1 {\n  font-size: 2.0rem;\n}\nsmall {\n  font-size: 95%;\n}\n.text-muted {\n  color: grey;\n}\n\n.form-check {\n  display: flex;\n  flex-direction: column;\n  margin-bottom: 1.0rem;\n}\n \n.btn:focus {\n  outline: none !important;\n  box-shadow: none !important;\n}\n.input-group-btn.spaced {\n  margin-left: 3px;\n}\n\n.input-group {\n  display: flex;\n  width: 100%;\n}\n\n\n#prefs, #settings, #editor, #about {\n  padding: 0.5rem;\n}\n\n//\n// prefs page\n//\n#prefs {\n  display: grid;\n  grid-template-columns: var(--preview-wrapper-width) 1fr;\n  grid-template-rows: var(--preview-wrapper-height) 1fr var(--footer-height);\n  grid-template-areas: \"saver-preview saver-list\"\n    \"saver-info basic-prefs\"\n    \"footer footer\";\n}\n\n#prefs > header {\n  grid-area: header;\n}\n\n.platform-darwin .hide-on-darwin {\n  display: none;\n}\n\n.saver-list-wrapper {\n  grid-area: saver-list;\n  height: var(--preview-wrapper-height);\n  padding-left: 0.25rem;\n}\n.saver-list {\n  max-height: calc(var(--preview-wrapper-height) - 34px);\n  overflow-y: scroll;\n  margin-bottom: 0px;\n  font: small-caption;\n\n  li.list-group-item {\n    padding-left: 0.25rem;\n    padding-top: 0.5rem;\n    padding-bottom: 0.5rem;\n    padding: 0.75rem 1.25rem;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n    border-left: 1px solid rgba(0, 0, 0, 0.125);\n  }\n\n  li.list-group-item.active {\n    color: #fff;\n    background-color: #007bff;\n    border-color: #007bff;\n  }\n\n  label {\n    font-size: 0.9rem;\n  }\n}\n\n#prefs input[name=screensaver] {\n  display: none;\n}\n\ndiv.saver-detail {\n  grid-area: saver-preview;\n  height: var(--preview-wrapper-height);\n  overflow: hidden;\n}\n\n.saver-preview {\n  width: var(--preview-width);\n  height: var(--preview-height);\n  transform: scale(var(--preview-scale));\n  transform-origin: 0 0;\n  overflow: hidden;\n  border: 0;\n}\n.saver-preview::-webkit-scrollbar {\n  width: 0px;\n  height: 0px;\n}\n\ndiv.basic-prefs {\n  grid-area: basic-prefs;\n  margin-top: 1rem;\n  margin-right: 2rem;\n}\n\ndiv.saver-info {\n  grid-area: saver-info; \n  height: calc(99vh - var(--preview-wrapper-height) - var(--footer-height) - var(--space-at-top) - var(--small-padding));\n  overflow-y: auto;\n}\ndiv.saver-info #wrapper {\n  max-width: 98%;\n}\n\n#settings {\n  padding-top: 0.5rem;\n}\nfooter.footer {\n  height: var(--footer-height);\n  grid-area: footer;\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  align-items: center;\n  background-color: #f5f5f5;\n  padding-left: 5px;\n  padding-right: 5px;\n  display: flex;\n  justify-content: space-between;\n}\n\n#prefs h1 {\n  font-size: 1.4rem;\n}\n#advanced-prefs-form > h1 {\n  margin-top: 30px;\n}\n\nbody > #editor > .content {\n  overflow-y: scroll;\n}\n\nbody > #editor #settings > div {\n  padding-top: 35px;\n}\n\nul {\n  padding-left: 0px;\n  list-style-type: none;\n}\n\n.hide {\n  display: none;\n}\n\n\nbutton.add-option {\n  margin-top: 12px;\n}\n\n.space-at-top {\n  margin-top: var(--space-at-top);\n}\n\n.space-at-bottom {\n  margin-bottom: 20px;\n}\n\n.saver-detail {\n  padding-left: 0px;\n  padding-right: 5px;\n}\n\n.saver-description .actions {\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n}\n\n.hint {\n  color: #888888;\n  font-size: 95%;\n}\n\nh1 .hint {\n  font-size: 60%;\n}\n\n.window > footer {\n  padding: 5px;\n}\n\n.padded-top {\n  margin-top: 20px;\n}\n.padded-bottom {\n  padding-bottom: 60px;\n}\n\n//\n// about page\n//\n#about {\n  overflow-x: hidden;\n}\n#about {\n  margin-left: 5px;\n  text-align: center;\n}\n#about h1, #about h2, #about h3, #about p {\n  overflow: visible;\n  margin-top: 5px;\n  margin-bottom: 5px;  \n}\n#about h2 {\n  font-size: 1.3rem;\n}\n#about h3 {\n  font-size: 1.1rem;\n}\n#about p {\n  font-size: 120%;\n}\n#about svg {\n  width: 50%;\n}\n\n#options {\n  margin-bottom: 10px;\n}\n#options div.field {\n  display: flex;\n}\n#options div.input {\n  width: 75%;\n  align-self: center;\n  padding-right: 10px;\n}\n#options legend {\n  font-size: 95%;\n  width: 25%;\n  align-self: center;\n  margin: 0px;\n}\n\n\nform.submit-attempt {\n  input:invalid {\n    border: 2px dashed red;\n  }\n}\n\nform.entry {\n  margin-top: 5px;\n  margin-bottom: 5px;\n  padding-left: 1.5rem;\n  padding-bottom: 0.5rem;\n  border-bottom: 1px dashed #888888;\n}\n\nform.input {\n  margin-bottom: 10px;\n  display: flex;\n  flex-direction: column;\n}\nform.input input[type=text], input[type=range] {\n  width: 100%;\n}\n\nlabel.for-option {\n  font-weight: bold;\n}\ninput[type=\"range\"] {\n  max-width: 250px;\n}\n\ndiv.form-group {\n  margin-bottom: 1rem;\n  max-width: 95%;\n\n  label {\n    display: inline-block;\n    font-weight: bold;\n    margin-bottom: 0.5rem;\n  }\n  input, button, select {\n    display: block;\n    height: auto;\n    font-size: 1rem;\n    padding: .375rem .75rem;\n    line-height: 1.5;\n  }\n  input {\n    width: 100%;\n  }\n  input[type=checkbox] {\n    width: initial;\n    display: inline-block;\n  }\n  .hint {\n    display: block;\n  }\n}\ndiv.form-group.full-width {\n  max-width: 100%;\n}\n\n// new screensaver\n#new {\n  padding: 0.5rem;\n}\n.need-setup-message {\n  height: 80vh;\n}\n\n.block {\n  display: block;\n}\n\n\n\n#editor {\n  h1, h2, h3, h4 {\n    margin-bottom: 0px;\n  }\n\n  h1, h2, h3, h4 {\n    & + small {\n      display: block;\n      margin-bottom: 1rem;\n    }\n\n    & + input {\n      margin-top: 0.5rem;\n      margin-left: 0.5rem;\n    }\n  }\n}\n\ninput[type=\"checkbox\"] + label {\n  margin-top: 0.5rem;\n  margin-left: 0.5rem;\n}\n\n\ndiv.notarize-wrapper {\n  position: fixed;\n  top: 10px;\n  right: 10px;\n  background: white;\n  min-height: 40px;\n  min-width: 100px;\n  max-width: 200px;\n  border-radius: 5px;\n  border-color: black;\n  border: 2px solid;\n  padding: 5px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 100000;\n}\n\n@keyframes fadeIn {\n  0% {opacity:0;}\n  100% {opacity:1;}\n}\n@keyframes fadeOut {\n  0% {opacity:1;}\n  100% {opacity:0;}\n}\n\n.notarize-in {\n  animation: fadeIn 1.0s; \n}\n\n.notarize-out {\n  animation: fadeOut 1.0s; \n}\n"
  },
  {
    "path": "src/index.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title><%= htmlWebpackPlugin.options.title %></title>\n  </head>\n  <body data-id=\"<%= htmlWebpackPlugin.options.id %>\">\n    <!-- webpack builds are automatically injected -->\n\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/lib/package.js",
    "content": "\"use strict\";\n\nimport fs from 'fs-extra';\nimport path from \"path\";\nimport temp from \"temp\";\nimport os from \"os\";\nimport { mkdirp } from \"mkdirp\";\nimport { rimrafSync } from \"rimraf\";\nimport * as yauzl from \"yauzl\"\nimport * as lockfile from \"proper-lockfile\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport semver from \"semver\";\n\n/**\n * need source repo url\n * call https://developer.github.com/v3/repos/releases/#get-the-latest-release\n * check published_at\n * if it's after stored value, download it!\n */\n\nexport default class Package {\n  constructor(_attrs) {\n    this.repo = _attrs.repo;\n    this.dest = _attrs.dest;\n    this.version = _attrs.version;\n    this.downloaded = false;\n    this.url = `https://api.github.com/repos/${this.repo}/releases/latest`;\n\n    if ( typeof(this.version) === \"undefined\" ) {\n      const saverPackageJson = path.join(this.dest, \"package.json\");\n      if ( fs.existsSync(saverPackageJson) ) {\n        this.version = JSON.parse(fs.readFileSync(saverPackageJson)).version;\n      }\n    }\n  \n    if ( typeof(_attrs.log) === \"undefined\" ) {\n      _attrs.log = function() {};\n    }\n  \n    this.logger = _attrs.log;\n    \n    this.defaultHeaders = {\n      \"User-Agent\": \"Before Dawn\"\n    };  \n  }\n  \n  attrs() {\n    return {\n      dest: this.dest,\n      version: this.version,\n      downloaded: this.downloaded\n    };\n  }\n\n  async getReleaseInfo() {\n    this.logger(`get release info from ${this.url}`);\n    if ( this.data ) {\n      return this.data;\n    }\n\n    this.data = await fetch(this.url, this.defaultHeaders)\n      .then(res => res.json())\n      .then((json) => {\n        const remoteVersion = json.tag_name.replace(/^v/, \"\");\n        json.is_update = this.version === undefined || semver.gt(remoteVersion, this.version);\n        return json;\n      })\n      .catch((err) => {\n        this.logger(err);\n        if ( typeof(this.data) !== \"undefined\" ) {\n          return this.data;\n        }\n        else {\n          return {};\n        }\n      });\n\n    return this.data;\n  }\n\n  async hasUpdate() {\n    const data = await this.getReleaseInfo();\n    return data.is_update;\n  }\n\n  async checkLatestRelease(force) {\n    const data = await this.getReleaseInfo();\n    if ( force === true || data.is_update ) {\n      return this.downloadRelease();\n    }\n    else {\n      this.logger(\"no package update available\");\n      return this.attrs();\n    }\n  }\n\n  async downloadRelease() {\n    this.logger(\"download package updates!\");\n    const data = await this.getReleaseInfo();\n\n    let dest;\n\n    if ( this.useLocalFile ) {\n      dest = this.localZip;\n    }\n    else {\n      dest = await this.downloadFile(data.zipball_url);\n    }\n  \n    await this.zipToSavers(dest);\n\n    this.downloaded = true;\n    this.updated_at = data.published_at;\n\n    return this.attrs();  \n  }\n\n  async downloadFile(url, dest) {\n    if ( dest === undefined ) {\n      dest = temp.path({dir: os.tmpdir(), suffix: \".zip\"});\n    }\n\n    const res = await fetch(url, this.defaultHeaders);\n\n    // https://stackoverflow.com/questions/37614649/how-can-i-download-and-save-a-file-using-the-fetch-api-node-js\n    const fileStream = fs.createWriteStream(dest);\n    await finished(Readable.fromWeb(res.body).pipe(fileStream));\n\n    return dest;\n  }\n\n  zipToSavers(tempName, dest) {\n    let self = this;\n    if ( dest === undefined ) {\n      dest = self.dest;\n    }\n\n    return new Promise(function (resolve, reject) {\n      lockfile.lock(dest, { realpath: false, stale: 30000 }).then((release) => {\n        yauzl.open(tempName, {lazyEntries: true, validateEntrySizes: false}, (err, zipfile) => {\n          if (err) {\n            release().then(() => {\n              reject(err);\n            });\n          }\n          else {\n            //\n            // clean out existing files\n            //\n            try {\n              rimrafSync(self.dest);\n            }\n            catch (err) {\n              self.logger(err);\n            }\n\n            zipfile.readEntry();\n            zipfile.on(\"entry\", function(entry) {\n              var fullPath = entry.fileName;\n              // the incoming zip filename will have on extra directory on it\n              // projectName/dir/etc/file\n              //\n              // example: muffinista-before-dawn-screensavers-d388377/starfield/index.html\n              //\n              // let's get rid of the projectName\n              //\n              var parts = fullPath.split(/\\//);\n              parts.shift();\n              \n              fullPath = path.join(dest, path.join(...parts));\n              if (/\\/$/.test(entry.fileName)) {\n                // directory file names end with '/' \n                mkdirp(fullPath).then(() => {\n                  zipfile.readEntry();\n                }).catch((err) => {\n                  release().then(() => {\n                    return reject(err);\n                  });\n                });\n              }\n              else {\n                // file entry \n                zipfile.openReadStream(entry, function(err, readStream) {\n                  if (err) {\n                    release().then(() => {\n                      return reject(err);\n                    });\n                  }\n                  \n                  // ensure parent directory exists \n                  mkdirp(path.dirname(fullPath)).then(() => {\n                    self.logger(`${entry.fileName} -> ${fullPath}`);\n                    readStream.pipe(fs.createWriteStream(fullPath));\n                    readStream.on(\"end\", function() {\n                      zipfile.readEntry();\n                    });\n                  }).catch((err) => {\n                    release().then(() => {\n                      return reject(err);\n                    });\n                  });\n                });\n              }\n            });\n            \n            zipfile.on(\"end\", function() {\n              release().then(() => {\n                resolve(self.attrs());\n              });\n            });  \n          }\n          \n        });  \n      });\n    });\n  }\n}\n"
  },
  {
    "path": "src/lib/prefs-schema.json",
    "content": "{\n  \"saver\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"sourceRepo\": {\n    \"type\": \"string\",\n    \"default\": \"muffinista/before-dawn-screensavers\"\n  },\n  \"delay\": {\n    \"type\": \"number\",\n    \"default\": 5\n  },\n  \"sleep\": {\n    \"type\": \"number\",\n    \"default\": 10\n  },\n  \"lock\": {\n    \"type\": \"boolean\",\n    \"default\": false\n  },\n  \"disableOnBattery\": {\n    \"type\": \"boolean\",\n    \"default\": true\n  },\n  \"auto_start\": {\n    \"type\": \"boolean\",\n    \"default\": false\n  },\n  \"runOnSingleDisplay\": {\n    \"type\": \"boolean\",\n    \"default\": true\n  },\n  \"localSource\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"options\": {\n    \"default\": {}\n  },\n  \"sourceUpdatedAt\": {\n    \"default\": \"1970-01-01T00:00:00.000Z\"\n  },\n  \"updateCheckTimestamp\": {\n    \"default\": \"1970-01-01T00:00:00.000Z\"\n  },\n  \"launchShortcut\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"firstLoad\": {\n    \"type\": \"boolean\"\n  }\n}"
  },
  {
    "path": "src/lib/prefs.js",
    "content": "\"use strict\";\n\nimport * as path from \"path\";\nimport Conf from \"conf\";\n\nconst DEFAULTS = {\n  \"saver\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"sourceRepo\": {\n    \"type\": \"string\",\n    \"default\": \"muffinista/before-dawn-screensavers\"\n  },\n  \"delay\": {\n    \"type\": \"number\",\n    \"default\": 5\n  },\n  \"sleep\": {\n    \"type\": \"number\",\n    \"default\": 10\n  },\n  \"lock\": {\n    \"type\": \"boolean\",\n    \"default\": false\n  },\n  \"disableOnBattery\": {\n    \"type\": \"boolean\",\n    \"default\": true\n  },\n  \"auto_start\": {\n    \"type\": \"boolean\",\n    \"default\": false\n  },\n  \"runOnSingleDisplay\": {\n    \"type\": \"boolean\",\n    \"default\": true\n  },\n  \"localSource\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"options\": {\n    \"default\": {}\n  },\n  \"sourceUpdatedAt\": {\n    \"default\": \"1970-01-01T00:00:00.000Z\"\n  },\n  \"updateCheckTimestamp\": {\n    \"default\": \"1970-01-01T00:00:00.000Z\"\n  },\n  \"launchShortcut\": {\n    \"type\": \"string\",\n    \"default\": \"\"\n  },\n  \"firstLoad\": {\n    \"type\": \"boolean\"\n  }\n};\n\nclass SaverPrefs {\n  constructor(baseConfigDir, rootDir=undefined, saversDir=undefined) {\n    this.baseDir = baseConfigDir;\n\n    if ( rootDir === undefined ) {\n      this.rootDir = this.baseDir;\n    }\n    else {\n      this.rootDir = rootDir;\n    }\n\n    if ( saversDir === undefined ) {\n      this.saversDir = path.join(this.rootDir, \"savers\");\n    }\n    else {\n      this.saversDir = saversDir;\n    }\n\n    this.systemSource = path.join(this.rootDir, \"system-savers\");\n    this.confOpts = {\n      schema: DEFAULTS,\n      clearInvalidConfig: true,\n      cwd: this.baseDir\n    };\n    if ( process.env.CONFIG_DIR ) {\n      this.confOpts.cwd = process.env.CONFIG_DIR;\n    }\n\n    this.reload();\n  }\n\n  get configFile() {\n    return this.store.path;\n  }\n\n  get data() {\n    let result = {};\n    let self = this;\n    Object.keys(DEFAULTS).forEach(function(name) {\n      result[name] = self.store.get(name);\n    });\n    return result;\n  }\n\n  get defaults() {\n    let result = {};\n    Object.keys(DEFAULTS).forEach(function(name) {\n      result[name] = DEFAULTS[name].default;\n    });\n    return result;\n  }\n\n  reload() {\n    this.store = new Conf(this.confOpts);\n    this.firstLoad = this.store.get(\"firstLoad\", true);\n    if ( this.firstLoad === true ) {\n      this.store.set(\"firstLoad\", false);\n    }\n\n    // if (this.saver) {\n    //   this.saver = this.saver.split(path.sep).join(path.posix.sep);\n    // }\n  }\n\n  reset() {\n    this.store.clear();\n    this.store._write({});\n  }\n\n  get needSetup() {\n    return this.firstLoad === true || \n      this.saver === undefined ||\n      this.saver === \"\";\n  }\n\n  get defaultSaversDir() {\n    return this.saversDir;\n  }\n\n  /**\n   * get a list of folders we should check for screensavers\n   */\n  get sources() {\n    var local = this.localSource;\n    var system = this.systemSource;\n    \n    var folders = [this.defaultSaversDir];\n\n    // if there's a local source, use that\n    if ( local !== \"\" ) {\n      folders = folders.concat( local );\n    }\n    \n    folders = folders.concat( system );\n    return folders;\n  }\n\n  get systemSource() {\n    return this._systemSource;\n  }\n  \n  set systemSource(val) {\n    this._systemSource = val;\n  }\n\n\n  //\n  // get options for the specified screensaver\n  //\n  getOptions(name) {\n    if ( typeof(name) === \"undefined\" ) {\n      name = this.saver;\n    }\n\n    const opts = this.store.get(\"options\", {});\n    const result = opts[name];\n\n    if ( result === undefined ) {\n      return {};\n    }\n\n    return result;\n  }\n}\n\n\nObject.keys(DEFAULTS).forEach(function(name) {\n  Object.defineProperty(SaverPrefs.prototype, name, {\n    get() {\n      const result = this.store.get(name);\n      \n      if ( name === \"sourceUpdatedAt\" || name === \"updateCheckTimestamp\" ) {\n        return new Date(result);\n      }\n      \n      return result;\n    },\n    set(newval) {\n      if ( newval === undefined ) {\n        this.store.delete(name);\n      }\n      else {\n        if ( typeof(newval) === \"object\" && ( name === \"sourceUpdatedAt\" || name === \"updateCheckTimestamp\" )) {\n          newval = newval.toISOString();\n        }\n        this.store.set(name, newval);\n      }\n    }\n  });\n});\n\nexport default SaverPrefs;\n"
  },
  {
    "path": "src/lib/saver-factory.js",
    "content": "\"use strict\";\n\nimport * as path from \"path\";\nimport fs from 'fs-extra'\n\nexport default class SaverFactory {\n  constructor(prefs, logger) {\n    this.prefs = prefs;\n    if ( logger !== undefined ) {\n      this.logger = logger;\n    }\n    else {\n      this.logger = function() {};\n    }\n  }\n\n  /**\n   * generate a screensaver template\n   */\n  create(src, destDir, opts) {\n    if ( destDir === \"\" || destDir === undefined ) {\n      throw new Error(\"No local directory specified!\");\n    }\n\n    this.logger(`SRC: ${src}`);\n    let contents = fs.readdirSync(src);\n    const defaults = {\n      \"source\": \"index.html\",\n      \"options\": []\n    };\n\n    opts = Object.assign({}, defaults, opts);\n    opts.key = opts.name.toLowerCase().\n                    replace(/[^a-z0-9]+/gi, \"-\").\n                    replace(/-$/, \"\").\n                    replace(/^-/, \"\");\n\n    var dest = path.join(destDir, opts.key);\n    this.logger(`mkdir ${dest}`);\n    fs.mkdirpSync(dest);\n\n    contents.forEach(function(content) {\n      fs.copySync(path.join(src, content), path.join(dest, content));\n    });\n\n    //\n    // generate JSON file\n    //\n    var configDest = path.join(dest, \"saver.json\");\n    var content = fs.readFileSync( configDest );\n    contents = Object.assign({}, JSON.parse(content), opts);\n\n    fs.writeFileSync(configDest, JSON.stringify(contents, null, 2));\n\n    // add dest in case someone needs it\n    // but don't persist that data because that would be icky\n    opts.dest = path.join(dest, \"saver.json\");\n    \n    return opts;\n  }\n}"
  },
  {
    "path": "src/lib/saver-list.js",
    "content": "\"use strict\";\n\nimport fs from 'fs-extra';\nimport path from \"path\";\nimport { mkdirp } from \"mkdirp\";\nimport { rimraf } from \"rimraf\";\nimport { glob } from \"glob\";\n\nconst CONFIG_FILE_NAME = \"config.json\";\n\n\n/**\n * skip any folder which contains a '.before-dawn-skip' file\n * this way we can have templates and documentation and things like\n * that which won't get loaded into the app by mistake.\n */\nvar skipFolder = function(p) {\n  return fs.existsSync(path.join(p, \".before-dawn-skip\"));\n};\n\n\nexport default class SaverListManager {\n  constructor(opts, logger) {\n    this.prefs = opts.prefs;\n    this.loadedScreensavers = [];\n\n    if ( logger !== undefined ) {\n      this.logger = logger;\n    }\n    else {\n      this.logger = function() {};\n    }\n  \n    this.baseDir = this.prefs.baseDir;\n\n    if ( opts.rootDir ) {\n      this.rootDir = opts.rootDir;\n    }\n    else {\n      this.rootDir = this.baseDir;\n    }\n  }\n  \n  get defaultSaversDir() {\n    return this.prefs.saversDir;\n  }\n  \n  async setup() {\n    let _self = this;\n    var configPath = path.join(_self.baseDir, CONFIG_FILE_NAME);\n    var saversDir = _self.defaultSaversDir;\n    var results = {\n      first: false,\n      setup: false\n    };\n\n    _self.logger(\"saversDir: \" + saversDir, fs.existsSync(saversDir));\n    _self.logger(\"configPath: \" + configPath);\n    \n    // check for/create our main directory\n    // and our savers directory (which is a subdir\n    // of the main dir)\n    const made = await mkdirp(saversDir);\n\n    // check if we just created the folder,\n    // if there's no config yet,\n    // or if the savers folder was empty\n    if ( made === true || ! fs.existsSync(configPath) || fs.readdirSync(saversDir).length === 0 ) {\n      results.first = true;\n    }\n\n    results.setup = true;\n\n    return results;\n  }\n  \n  /**\n   * reload all our data/config/etc\n   */\n  reload(load_savers) {\n    this.logger(\"savers.reload\");\n    return this.setup(load_savers).then(this.handlePackageChecks);  \n  }\n\n  reset() {\n    this.loadedScreensavers = [];\n  }\n\n  normalizePath(p) {\n    return p.split(path.sep).join(path.posix.sep);\n  }\n\n  /**\n   * search for all screensavers we can find on the filesystem. if cb is specified,\n   * call it with data when done. if reload == true, don't use cached data.\n   */\n  async list(force) {\n    let _self = this;\n    var folders = this.prefs.sources;\n    var pattern, savers;\n    \n    var promises = [];\n    \n    // exclude system screensavers from the cache check\n    // @todo get rid of this\n    var systemScreensaverCount = 1;\n\n    // use cached data if available\n    if ( this.loadedScreensavers.length > systemScreensaverCount &&\n        ( typeof(force) === \"undefined\" || force === false ) ) {\n      return this.loadedScreensavers;\n    }\n\n    // note: using /**/ here instead of /*/ would\n    // also match all subdirectories, which might be desirable\n    // or even required, but is a lot slower, so not doing it\n    // for now\n    folders = folders.filter((el) => { \n      return el !== undefined && el !== \"\" && fs.existsSync(el);\n    });\n\n    folders.forEach((sourceFolder) => {\n      // glob doesn't work with windows style file paths, so convert\n      // to posix\n      sourceFolder = this.normalizePath(sourceFolder);\n\n      pattern = `${sourceFolder}/*/saver.json`;\n      savers = glob.sync(pattern);\n\n      for ( var i = 0; i < savers.length; i++ ) {\n        var f = this.normalizePath(savers[i]);\n        var folder = path.dirname(f);\n\n        // exclude skippable folders\n        var doLoad = ! folder.split(/[/|\\\\]/).reverse()[0].match(/^__/) &&\n                    ! skipFolder(folder);\n        \n        if ( doLoad ) {\n          promises.push(this.loadFromFile(f));\n        }\n      }  \n    });\n\n    // filter out failed promises here\n    // @see https://davidwalsh.name/promises-results\n    promises = promises.map(p => p.catch(() => undefined));\n\n    const data = await Promise.all(promises);\n\n    // remove any undefined screensavers\n    _self.loadedScreensavers = data.\n      filter(s => s !== undefined).\n      sort((a, b) => { \n        return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); \n      });\n\n    return _self.loadedScreensavers;\n  }\n\n\n  /**\n   * pick a random screensaver\n   */\n  random() {\n    var tmp = this.loadedScreensavers.filter((s) => {\n      return ( typeof(s.preload) === \"undefined\" );\n    });\n    var idx = Math.floor(Math.random() * tmp.length);\n\n    return tmp[idx];\n  }\n\n  async confirmExists(key) {\n    await this.list();\n    return this.getByKey(key) !== undefined;\n  }\n\n  /**\n   * look up a screensaver by key, and return it\n   */\n  getByKey(key) {\n    key = this.normalizePath(key);\n    var result = this.loadedScreensavers.find((obj) => {\n      return obj.key === key;\n    });\n    return result;\n  }\n\n\n  /**\n   * load screensaver data from filesystem\n   */\n  loadFromFile(src, settings) {\n    let _self = this;\n    src = this.normalizePath(src);\n\n    return new Promise(function (resolve, reject) {\n      fs.readFile(src, {encoding: \"utf8\"}, (err, content) => {\n        if ( err ) {\n          _self.logger(\"loadFromFile err\", src, err);\n          reject(err);\n        }\n        else {\n          try {\n            var contents = JSON.parse(content);           \n            var stub = path.dirname(src);\n            var s = _self.loadFromData(contents, stub, settings);\n\n            // add the source path as an attribute to make it easier\n            // to load/save/update this saver later if needed\n            s.src = src;\n\n            if ( s.valid ) {\n              resolve(s);\n            }\n            else {\n              _self.logger(\"loadFromFile not valid! \" + src);\n              reject();\n            }\n          }\n          catch(e) {\n            _self.logger(\"loadFromFile exception\", e);\n            reject(e);\n          }\n        }\n      });\n    });\n  }\n\n  loadFromData(contents, stub, settings) {\n    var src = this.normalizePath(this.prefs.localSource);\n\n    if ( typeof(stub) !== \"undefined\" ) {\n      contents.path = stub;\n      contents.key = this.normalizePath(path.join(stub, \"saver.json\"));\n    }\n\n    contents.editable = false;\n    if ( typeof(src) !== \"undefined\" && src !== \"\" ) {\n      contents.editable = (contents.key.indexOf(src) === 0);\n    }\n\n    if ( typeof(contents.settings) === \"undefined\" ) {\n      if ( settings === undefined ) {\n        if ( ! this.prefs.options ) {\n          this.prefs.options = {};\n        }\n\n        // ensure that all screensavers have options set\n        if ( this.prefs.options[contents.key] === undefined ) {\n          this.prefs.options[contents.key] = {};\n        }\n\n        settings = this.prefs.options[contents.key];\n      }\n      contents.settings = settings;\n    }\n\n    // set a URL\n    if ( typeof(contents.url) === \"undefined\" && \n      contents.path !== undefined  && \n      contents.source !== undefined) {\n      contents.url = `file://${[contents.path, contents.source].join(\"/\")}`;\n    }\n\n    if ( typeof(contents.published) === \"undefined\" ) {\n      contents.published = true;\n    }\n\n    if ( typeof(contents.requirements) === \"undefined\" ) {\n      contents.requirements = [\"screen\"];\n    }\n\n    contents.valid = typeof(contents.name) !== \"undefined\" &&\n      typeof(contents.description) !== \"undefined\" &&\n      contents.published === true;\n\n    return contents;\n  }\n\n  /**\n   * delete a screensaver -- this removes the directory that contains all files\n   * for the screensaver.\n   */\n  async delete(s) {\n    var k = s.key;\n    var p = path.dirname(k);\n\n    if ( typeof(s) !== \"undefined\" && s.editable === true ) {\n      await rimraf(p);\n      return true;\n    }\n    else {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "src/lib/saver.js",
    "content": "/**\n * simple class for a screen saver\n */\n\n\n// we will generate a list of requirements that screensavers need\n// to work. for now, it's just a screengrab. to maintain\n// compatability, we'll generate a default list if one isn't\n// specified    \nconst DEFAULT_REQUIREMENTS = [\"screen\"];\n\nimport fs from 'fs-extra';\nimport * as nodePath from \"path\";\n\nexport default class Saver {\n  constructor(_attrs) {\n    this.UNWRITABLE_KEYS = [\"key\", \"path\", \"url\", \"settings\", \"editable\"];\n\n    this.attrs = _attrs;\n    this.path = _attrs.path;\n    this.name = _attrs.name;\n    this.key = _attrs.key;\n    this.description = _attrs.description;\n    this.aboutUrl = _attrs.aboutUrl;\n    this.author = _attrs.author;\n    this.license = _attrs.license;\n    this.preload = _attrs.preload;\n    this.requirements = _attrs.requirements || DEFAULT_REQUIREMENTS;\n\n    // allow for a specified URL -- this way you could create a screensaver\n    // that pointed to a remote URL\n    this.url = _attrs.url;\n    if ( typeof(this.url) === \"undefined\" && \n      _attrs.path !== undefined  && \n      _attrs.source !== undefined) {\n      this.url = `file://${[_attrs.path, _attrs.source].join(\"/\")}`;\n    }\n\n    // keep track of our main saver.json file\n    this.src = _attrs.src;\n    if ( typeof(this.src) === \"undefined\" && typeof(this.key) !== \"undefined\" ) {\n      const baseDir = this.key.replace(/\\\\/g,\"/\").replace(/\\/[^/]*$/, \"\");\n      this.src = [baseDir, \"saver.json\"].join(\"/\");\n    }\n    \n    this.published = _attrs.published;\n    if ( typeof(this.published) === \"undefined\" ) {\n      this.published = true;\n    }\n\n    // provide a default editable value (this will\n    // be set when loading to determine if the user\n    // can edit this screensaver or not)\n    this.editable = _attrs.editable;\n    if ( typeof(this.editable) === \"undefined\" ) {\n      this.editable = false;\n    }\n\n    this.valid = typeof(this.name) !== \"undefined\" &&\n                typeof(this.description) !== \"undefined\" &&\n                this.published === true;\n\n    if ( typeof(_attrs.options) === \"undefined\" ) {\n      _attrs.options = [];\n    }\n    this.options = _attrs.options;\n\n    if ( this.valid === true ) {\n      // figure out the settings from any defaults for this screensaver,\n      // and combine with incoming user-specified settings\n      this.settings = _attrs.options.map(function(o) {\n        return [o.name, o.default];\n      }).reduce(function(o, v) {\n        o[v[0]] = v[1];\n        return o; \n      }, {});\n      this.settings = Object.assign({}, this.settings, _attrs.settings);\n      \n      // allow for custom preview URL -- if not specified, just use the default\n      // if it is specified, do some checks to see if it's a full URL or a filename\n      // in which case we will turn it into a full path\n      if ( typeof(this.attrs.previewUrl) === \"undefined\" ) {\n        this.previewUrl = this.url;\n      }\n      else if ( this.attrs.previewUrl.match(/:\\/\\//) ) {\n        this.previewUrl = this.attrs.previewUrl;\n      }\n      else {\n        this.previewUrl = this.path + \"/\" + this.attrs.previewUrl;\n      }\n    } // if valid\n  }\n\n  urlWithParams(opts={}) {\n    if ( !this.url.match(/^file:/) ) {\n      return this.url;\n    }\n\n    const urlParams = new URLSearchParams(opts);\n\n    if ( this.settings ) {\n      const keys = Object.keys(this.settings);\n      keys.forEach((k) => {\n        urlParams.append(k, this.settings[k]);\n      }); \n    }\n\n    return `${this.url}?${urlParams.toString()}`; \n  }\n\n\n\n  toHash() {\n    return this.attrs;   \n  }\n\n  toJSON(attrs) {\n    for ( var i = 0 ; i < this.UNWRITABLE_KEYS.length; i++ ) {\n      delete(attrs[this.UNWRITABLE_KEYS[i]]);\n    }\n\n    if ( attrs.requirements === undefined ) {\n      attrs.requirements = [];\n    }\n    else {\n      attrs.requirements = attrs.requirements.filter(r => r !== \"none\");\n    }\n\n    if ( attrs.requirements.length === 0 ) {\n      attrs.requirements = [\"none\"];\n    }\n   \n\n    return JSON.stringify(attrs, null, 2);\n  }\n\n  write(attrs, configDest) {\n    if ( typeof(attrs) === \"undefined\" ) {\n      attrs = this.attrs;\n    }\n    if ( typeof(configDest) === \"undefined\" ) {\n      configDest = nodePath.join(this.path, \"saver.json\");\n    }\n    fs.writeFileSync(configDest, this.toJSON(attrs));\n  }\n}\n\n"
  },
  {
    "path": "src/main/assets/global.css",
    "content": "body {margin:0; padding:0; overflow: hidden}\n*, *:hover { cursor: none !important; }\ncanvas {display:block;}\ncanvas:focus {outline:0;}\n"
  },
  {
    "path": "src/main/assets/grabber.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>screen grabber</title>\n</head>\n\n<body>\n  you should never see me\n</body>\n<script>\n  window.grabber.init();\n</script>\n</html>\n"
  },
  {
    "path": "src/main/assets/grabber.mjs",
    "content": "/**\n * This is the preload script for the screen grabber\n * \n */\n\n\nconst { contextBridge, ipcRenderer } = require(\"electron\");\n\n/** \n * look for an element in the DOM and create it if it doesn't exist \n */\nvar findOrCreate = function(type, screen_id) {\n  const id = `${type}${screen_id}`;\n\n  let el = document.getElementById(id);\n  if ( el === null ) {\n    el = document.createElement(type);\n    el.id = id;\n    \n    document.body.appendChild(el);\n  }\n  \n  return el;    \n};\n\n/**\n * Apply the video stream to the canvas. Return a context with a screenshot\n * \n * @param {*} video \n * @param {*} canvas \n */\nvar applyVideoToCanvas = function(video, canvas) {\n  const width = video.videoWidth;\n  const height = video.videoHeight;\n\n  canvas.setAttribute(\"width\", width);\n  canvas.setAttribute(\"height\", height);\n  \n  canvas.width = width;\n  canvas.height = height;\n\n  const context = canvas.getContext(\"2d\");\n  context.drawImage(video, 0, 0, width, height);\n\n  return context;\n};\n\nlet captureIndex = 0;\n\nvar screenToBuffer = async function(video) {\n  const tempName = `capture-index-${captureIndex}`;\n  const canvas = findOrCreate(\"canvas\", tempName);\n  const context = applyVideoToCanvas(video, canvas);\n  \n  const data = canvas.toDataURL(\"image/png\", 1.0);\n  \n  context.clearRect(0, 0, canvas.width, canvas.height);\n    \n  const buffer = Buffer.from(data.split(\",\")[1], \"base64\");\n\n  canvas.remove();\n\n  return buffer;\n};\n\n\n/**\n * cleanup video/media stream\n * @param {*} video \n * @param {*} s \n */\nvar cleanup = function(video, s) {\n  //\n  // stop video capture\n  // this seems to handle a problem where CPU load spikes\n  // after capture\n  //\n  if ( s !== undefined ) {\n    s.getVideoTracks().forEach((track) => {\n      track.stop();\n    });\n  }\n  \n  // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video\n  video.pause();\n  video.src = \"\";\n  video.load();\n  video.remove();\n};\n\n/**\n * capture the screen\n * @param {*} id ID of the screen to capture\n * @param {*} width \n * @param {*} height \n */\nvar captureScreen = async function(id, width, height) {\n  const screen_opts = {\n    audio: false,\n    video: {\n      mandatory: {\n        // fun fact -- you need to use max here\n        // @see https://groups.google.com/a/chromium.org/forum/#!topic/chromium-apps/TP_rsnYVQWg\n        maxWidth: width,\n        maxHeight: height,\n        chromeMediaSource: \"desktop\"\n      }\n    }\n  };\n  \n  screen_opts.video.mandatory.chromeMediaSourceId = id;\n  \n  const video = findOrCreate(\"video\", id);\n\n  // adding muted helps with some security errors\n  // @see https://stackoverflow.com/questions/49930680/\n  // how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use\n  video.muted = \"muted\";\n  \n  const mediaStream = await navigator.mediaDevices.getUserMedia(screen_opts);\n  video.srcObject = mediaStream;\n\n  await video.play();\n  \n  const result = await screenToBuffer(video, mediaStream);\n\n  cleanup(video, mediaStream);\n\n  return result;\n}; // captureScreen\n\n\ncontextBridge.exposeInMainWorld(\n  \"grabber\",\n  {\n    init: () => {\n      ipcRenderer.on(\"request-screenshot\", async (_event, opts) => {\n        const result = await captureScreen(opts.id, opts.width, opts.height);\n        ipcRenderer.send(\"screenshot-\" + opts.id, {buffer: result});\n      });\n    }\n  }\n);\n"
  },
  {
    "path": "src/main/assets/preload.mjs",
    "content": "const { contextBridge, ipcRenderer } = require(\"electron\");\n\nconst api = {\n  platform: () => process.platform,\n  getDisplayBounds: async() => ipcRenderer.invoke(\"get-primary-display-bounds\"),\n  getScreenshot: async() => ipcRenderer.invoke(\"get-primary-screenshot\"),\n  getGlobals: async() => ipcRenderer.invoke(\"get-globals\"),\n  addListener: (key, fn) => ipcRenderer.on(key, fn),\n  removeListener: (key, fn) => ipcRenderer.removeListener(key, fn),\n  getPrefs: async() => ipcRenderer.invoke(\"get-prefs\"),\n  getDefaults: async() => ipcRenderer.invoke(\"get-defaults\"),\n  openWindow: (name, opts) => ipcRenderer.send(\"open-window\", name, opts),\n  closeWindow: (name) => ipcRenderer.send(\"close-window\", name),\n  listSavers: async() => ipcRenderer.invoke(\"list-savers\"),\n  loadSaver: async(src) => ipcRenderer.invoke(\"load-saver\", src),\n  deleteSaverDialog: async(key) => ipcRenderer.invoke(\"delete-screensaver-dialog\", key),\n  deleteSaver: async(key) => ipcRenderer.invoke(\"delete-saver\", key),\n  saveScreensaver: async(saver, src) => ipcRenderer.invoke(\"save-screensaver\", saver, src),\n  updatePrefs: async(prefs) => ipcRenderer.invoke(\"update-prefs\", prefs),\n  setAutostart: (value) => ipcRenderer.send(\"set-autostart\", value),\n  setGlobalLaunchShortcut: (value) => ipcRenderer.send(\"set-global-launch-shortcut\", value),\n  displayUpdateDialog: () => ipcRenderer.send(\"display-update-dialog\"),\n  resetToDefaultsDialog: async() => ipcRenderer.invoke(\"reset-to-defaults-dialog\"),\n  openUrl: (url) => ipcRenderer.send(\"launch-url\", url),\n  updateLocalSource: (ls) => ipcRenderer.invoke(\"update-local-source\", ls),\n  createScreensaver: (opts) => ipcRenderer.invoke(\"create-screensaver\", opts),\n  saversUpdated: (key) => ipcRenderer.send(\"savers-updated\", key),\n  getScreensaverPackage: () => ipcRenderer.invoke(\"check-screensaver-package\"),\n  downloadScreensaverPackage: () => ipcRenderer.invoke(\"download-screensaver-package\"),\n  showOpenDialog: () => ipcRenderer.invoke(\"show-open-dialog\"),\n  openFolder: (path) => ipcRenderer.send(\"open-folder\", path),\n  watchFolder: (src) => ipcRenderer.send(\"watch-folder\", src),\n  unwatchFolder: (src) => ipcRenderer.send(\"unwatch-folder\", src),\n  onFolderUpdate: (cb) => ipcRenderer.on(\"folder-update\", cb),\n  toggleDevTools: () => ipcRenderer.send(\"toggle-dev-tools\"),\n  log: (payload) => ipcRenderer.send(\"console-log\", payload)\n};\n\ncontextBridge.exposeInMainWorld(\"api\", api);\n"
  },
  {
    "path": "src/main/assets/shim.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>test shim</title>\n  </head>\n\n  <body>\n    test shim!\n    <ul id=\"tray\">\n\n    </ul>\n\n\n    <input id=\"ipc\" value=\"\" />\n    <input id=\"ipcopts\" value=\"\" />\n    <button id=\"ipcSend\">go</button>\n\n    <div id=\"currentState\"></div>\n    <script>\n      var list = document.querySelector(\"ul\");\n\n      function htmlToElement(html) {\n        var template = document.createElement('template');\n        html = html.trim(); // Never return a text node of whitespace as the result\n        template.innerHTML = html;\n        return template.content.firstChild;\n      }\n\n      function addTestItem(label) {\n        var className = label.replace(/ /g, '');\n        var el = htmlToElement(`<li><button class=\"${className}\">${label}</button></li>`);\n        list.appendChild(el);\n        el.addEventListener(\"click\", function(e) {\n          e.preventDefault();\n          window.shimApi.clickTray(label);\n        });\n      }\n\n      window.shimApi.getTrayItems().then((items) => {\n        items.forEach((item) => {\n          addTestItem(item);\n        });\n      });\n\n      document.querySelector('#ipcSend').addEventListener(\"click\", async () => {\n        const vals = document.querySelector('#ipc').value.split(' ');\n        let opts;\n\n        if (document.querySelector('#ipcopts').value) {\n          opts = JSON.parse(document.querySelector('#ipcopts').value);\n        }\n        else {\n          opts = {};\n        }\n        await window.shimApi.send(vals[0], vals[1], opts);\n      });\n\n     setInterval(function() {\n       window.shimApi.getCurrentState().then((state) => {\n         document.querySelector(\"#currentState\").innerHTML = state;\n       });\n     }, 100);\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/main/assets/shim.js",
    "content": "const { contextBridge, ipcRenderer } = require(\"electron\");\n\nconst shimApi = {\n  send: (cmd, opts, args={}) => ipcRenderer.send(cmd, opts, args),\n  getCurrentState: async () => ipcRenderer.invoke(\"get-current-state\"),\n  getTrayItems: async () => ipcRenderer.invoke(\"get-tray-items\"),\n  clickTray: (label) => {\n    ipcRenderer.invoke(\"click-tray-item\", label);\n  }\n};\n\ncontextBridge.exposeInMainWorld(\"shimApi\", shimApi);\n"
  },
  {
    "path": "src/main/autostarter.js",
    "content": "\"use strict\";\n\nimport * as main from \"./index.js\";\nimport AutoLaunch from \"auto-launch\";\n\nexport function toggle(appName, value) {\n  var appLauncher = new AutoLaunch({\n    name: appName\n  });\n\n  if ( value === true ) {\n    appLauncher.isEnabled().then((isEnabled) => {\n      if ( isEnabled ) {\n        return;\n      }\n      \n      appLauncher.\n        enable().\n        then((err) =>{\n          main.log.info(\"appLauncher enable\", err);\n        }).catch((err) => {\n          main.log.info(\"appLauncher enable failed\", err);\n        });\n    });\n  }\n  else {\n    main.log.info(\"set auto start == false\");\n    appLauncher.isEnabled().then((isEnabled) => {\n      if ( !isEnabled ) {\n        return;\n      }\n      appLauncher.\n        disable().\n        then(function() { \n          main.log.info(\"appLauncher disabled\");\n        }).\n        catch((err) => {\n          main.log.info(\"appLauncher disable failed\", err);\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/main/bootstrap.js",
    "content": "import { readFile } from 'fs/promises';\n\nexport default async function bootstrapApp() {\n  const packageJSON = JSON.parse(\n    await readFile(\n      new URL('../../package.json', import.meta.url)\n    )\n  );\n  \n\n  var version = undefined;\n\n  try {\n    version = packageJSON.version;\n\n    if ( ! process.env.BEFORE_DAWN_RELEASE_NAME ) {\n      process.env.BEFORE_DAWN_RELEASE_NAME = `${packageJSON.productName} ${packageJSON.version}`;\n    }\n  }\n  catch {\n    version = \"0.0.0\";\n  }\n\n  global.APP_NAME = \"Before Dawn\";\n  global.APP_DIR = \"Before Dawn\";\n  global.SAVER_REPO = \"muffinista/before-dawn-screensavers\";\n  global.APP_REPO = \"muffinista/before-dawn\";\n  global.APP_VERSION_BASE = version;\n  global.APP_VERSION = `v${version}`;\n  global.NEW_RELEASE_AVAILABLE = false;\n  global.HELP_URL = \"https://muffinista.github.io/before-dawn/\";\n  global.ISSUES_URL = \"https://github.com/muffinista/before-dawn/issues\";\n  global.APP_CREDITS = \"by Colin Mitchell // muffinlabs.com\";\n\n  if ( packageJSON.release_server ) {\n    global.RELEASE_SERVER = packageJSON.release_server;\n    global.RELEASE_CHECK_URL = `${global.RELEASE_SERVER}/update/${process.platform}/${global.APP_VERSION_BASE}`;\n    global.PACKAGE_DOWNLOAD_URL = `https://github.com/${global.APP_REPO}/releases/latest`;\n  }\n}"
  },
  {
    "path": "src/main/dock.js",
    "content": "\"use strict\";\n\nimport {app, BrowserWindow} from \"electron\";\n\n/**\n * if we're using the dock, and all our windows are closed, hide the\n * dock icon\n */\nexport const hideDockIfInactive = function() {\n  let openWindowCount = BrowserWindow.getAllWindows().\n                                      filter(win => (win !== undefined && win.noTray !== true) ).length;\n\n  if ( typeof(app.dock) !== \"undefined\" && openWindowCount === 0 ) {\n    app.dock.hide();\n  }\n};\n\n/**\n * show the dock if it's available\n */\nexport const showDock = function() {\n  if ( typeof(app.dock) !== \"undefined\" ) {\n    app.dock.show();\n  }\n};\n"
  },
  {
    "path": "src/main/index.dev.js",
    "content": "/**\n * This file is used specifically and only for development. There shouldn't be\n * any need to modify this file, but it can be used to extend your development\n * environment.\n */\n\n// Set environment for development\nprocess.env.NODE_ENV = 'development';\n\n// Require `main` process to boot app\nrequire('./index');\n"
  },
  {
    "path": "src/main/index.js",
    "content": "\"use strict\";\n\n// process.traceDeprecation = true;\n// process.traceProcessWarnings = true;\n\n\n/***\n\n   Welcome to....\n\n   ____        __                  ____                       \n   | __ )  ___ / _| ___  _ __ ___  |  _ \\  __ ___      ___ __  \n   |  _ \\ / _ \\ |_ / _ \\| '__/ _ \\ | | | |/ _` \\ \\ /\\ / / '_ \\ \n   | |_) |  __/  _| (_) | | |  __/ | |_| | (_| |\\ V  V /| | | |\n   |____/ \\___|_|  \\___/|_|  \\___| |____/ \\__,_| \\_/\\_/ |_| |_|\n\n   a screensaver package built on the tools of the web. Enjoy!\n   \n */\n\n\nimport { init } from '@sentry/electron';\nif ( process.env.TEST_MODE === undefined && process.env.SENTRY_DSN !== undefined ) {\n  console.log(`setting up sentry with ${process.env.SENTRY_DSN}`);\n  try {\n    init({\n      dsn: process.env.SENTRY_DSN,\n      onFatalError: console.log\n    });  \n  }\n  catch(e) {\n    console.log(e);\n  }\n}\n   \nimport {app,\n  BrowserWindow,\n  desktopCapturer,\n  dialog,\n  globalShortcut,\n  ipcMain,\n  Menu,\n  net,\n  session,\n  shell,\n  systemPreferences,\n  Tray,\n  powerMonitor} from \"electron\";\n\nimport isDev from 'electron-is-dev';\nimport log from 'electron-log';\n\nimport { screen as electronScreen } from \"electron\";\n\nimport * as fs from \"fs\";\nimport { readFile } from 'fs/promises';\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport * as temp from \"temp\";\nimport * as url from \"url\";\nimport { execFile as exec } from \"child_process\";\n\nimport * as screenLock  from \"./screen.js\";\n\nimport StateManager from \"./state_manager.js\";\nimport SaverPrefs from \"../lib/prefs.js\";\nimport SaverFactory from \"../lib/saver-factory.js\";\nimport Saver from \"../lib/saver.js\";\nimport SaverListManager from \"../lib/saver-list.js\";\nimport Package from \"../lib/package.js\";\nimport Power from \"../main/power.js\";\n\nimport * as menusAndTrays from \"./menus.js\";\nimport * as dock from \"./dock.js\";\nimport * as windows from \"./windows.js\";\n\nimport forceFocus from \"forcefocus\";\nimport ReleaseCheck from \"./release_check.js\";\nimport * as autostarter from \"./autostarter.js\";\n\n/**\n * try and guess if we are in fullscreen mode or not\n */\nimport FullScreen from \"detect-fullscreen\";\nconst { isFullscreen } = FullScreen;\n\nconst packageJSON = JSON.parse(\n  await readFile(\n    new URL('../../package.json', import.meta.url)\n  )\n);\n\nvar releaseChecker;\n\n// NOTE -- this needs to be global, otherwise the app icon gets\n// garbage collected and won't show up in the system tray\nlet appIcon = null;\n\nlet debugMode = ( process.env.DEBUG_MODE !== undefined );\nlet testMode = ( process.env.TEST_MODE !== undefined );\n\nlet cursor;\n\nif (testMode || debugMode) {\n  log.transports.console.format = \"{h}:{i}:{s} {text}\";\n  log.catchErrors();\n}\n\n//\n// don't hide cursor in tests or in windows, since\n// that causes the tray to stop working???\n//\nif ( testMode || process.platform === \"win32\" ) {\n  cursor = {\n    hide: () => {},\n    show: () => {}\n  };\n}\nelse {\n  cursor = await import(\"hide-cursor\");\n  cursor = cursor.default;\n}\n\nlet exitOnQuit = false;\n\n/**\n * track some information about windows and preview bounds for the prefs window\n * and editor window\n */\nlet handles = {\n  prefs: {\n    window: null,\n    bounds: {\n      width: 320,\n      height: 0\n    },\n    max: {\n      width: 320,\n      height: 320\n    }\n  },\n  settings: {\n    window: null\n  },\n  addNew: {\n    window: null\n  },\n  about: {\n    window: null\n  },\n  editor: {\n    window: null,\n    bounds: {\n      width: 320,\n      height: 0\n    },\n    max: {\n      width: 320,\n      height: 320\n    }\n  },\n  // shim: {\n  //   window: null\n  // }\n};\n\nlet trayMenu;\n\nlet prefs = undefined;\nlet savers = undefined;\nlet stateManager = undefined;\n\n\n// usually we want to check power state before running, but\n// we'll skip that check depending on the value of this toggle\n// so that manually running screensaver works just fine\nlet checkPowerState = true;\n\nconst RELEASE_CHECK_INTERVAL = 1000 * 60 * 60 * 12;\n\n// load a few global variables\nimport bootstrapApp from \"./bootstrap.js\";\nawait bootstrapApp();\n\nconst defaultWebPreferences = {\n  enableRemoteModule: false,\n  contextIsolation: true,\n  nodeIntegration: false,\n  nativeWindowOpen: true,\n  webSecurity: !isDev\n};\n\nconst singleLock = app.requestSingleInstanceLock();\nif (! singleLock ) {\n  console.log(\"looks like another copy of app is running, exiting!\");\n  app.quit();\n  process.exit();\n}\n\nconst power = new Power({\n  platform: process.platform,\n  method: powerMonitor.isOnBatteryPower\n});\n\n\nlet screenData = [];\nvar listScreens = async function() {\n  try {\n    const sources = await desktopCapturer.getSources({\n      types: [\"screen\"],\n    });\n    \n    screenData = sources;\n\n    return sources;\n  } catch(e) {\n    log.info(e);\n  }\n\n  return [];\n};\n\n\n/**\n * Open the screengrab window\n * \n * @returns {Promise} Promise that resolves once window is loaded\n */\nvar openGrabberWindow = function() {\n  return new Promise((resolve) => {\n    log.info(\"openGrabberWindow\");\n    const grabberUrl = `file://${getAssetsDir()}/grabber.html`;\n\n    var grabberWindow = new BrowserWindow({\n      show: false,\n      skipTaskbar: true,\n      width: 100,\n      height: 100,\n      x: 6000,\n      y: 2000,\n      webPreferences: {\n        ...defaultWebPreferences,\n        preload: path.join(getAssetsDir(), \"grabber.mjs\")\n      }\n    });\n    // grabberWindow.noTray = true;\n    \n    grabberWindow.once(\"ready-to-show\", () => {\n      resolve(grabberWindow);\n    });\n\n    grabberWindow.loadURL(grabberUrl); \n\n  });\n};\n\n/**\n * open our screen grabber tool and issue a screengrab request\n * @param {Screen} s the screen to grab\n * @returns {Promise} Promise that resolves with object containing URL of screenshot\n */\nvar grabScreen = function(s) {\n  log.info(`grab screen ${s.id}`);\n\n  let screen = screenData.find((s) => { return s.display_id.toString() === s.id.toString(); });\n  if ( ! screen ) {\n    screen = screenData[0];\n  }\n\n  return new Promise((resolve) => {\n    //\n    // bypass screen capture in test mode\n    // or if the user has blocked screen access\n    //\n    if (\n      (process.platform === \"darwin\" && systemPreferences.getMediaAccessStatus(\"screen\") !== \"granted\" ) ||\n      testMode === true ) {\n      resolve({\n        url: path.join(getAssetsDir(), \"color-bars.png\")\n      });\n    }\n    else {\n      let windowRef;\n      ipcMain.once(`screenshot-${screen.id}`, function(_e, message) {\n        const tempName = temp.path({dir: os.tmpdir(), suffix:\".png\"});\n\n        fs.writeFileSync(tempName, message.buffer);\n        resolve({\n          url: tempName\n        });\n\n        // close the screen grabber window\n        try {\n          windowRef.close();\n        }\n        catch(ex) {\n          if ( typeof(Sentry) !== \"undefined\" ) {\n            // eslint-disable-next-line no-undef\n            Sentry.captureException(ex);\n          }\n        }\n\n        // rewrite file paths to always have unix slashes instead\n        // of windows slashes. sometimes windows slashes are fine, but\n        // there's a few situations where they won't render properly.\n        // message.url = message.url.split(path.sep).join(path.posix.sep);\n\n        resolve(message);\n      });\n\n      openGrabberWindow().then((w) => {\n        windowRef = w;\n        windowRef.webContents.send(\"request-screenshot\", { \n          id: screen.id, \n          width: s.bounds.width, \n          height: s.bounds.height});  \n      });\n    }\n  });\n};\n\n\n/**\n * open a simple window that our mocha/spectron tests can use.\n *\n * this exists mostly because it's basically impossible to test\n * an app that doesn't open a window.\n */\nvar openTestShim = function() {\n  var testWindow = new BrowserWindow({\n    width: 800,\n    height: 600,\n    webPreferences: {\n      ...defaultWebPreferences,\n      preload: path.join(getAssetsDir(), \"shim.js\")\n    }\n  });\n\n  const shimUrl = `file://${getAssetsDir()}/shim.html`;\n  testWindow.loadURL(shimUrl);\n\n  // testWindow.webContents.openDevTools();\n};\n\n\nlet screenshots = {};\n\n\n/**\n * Open the preferences window\n * @returns {Promise} Promise that resolves when prefs window is shown\n */\nvar openPrefsWindow = function() {\n  if ( handles.prefs.window !== null && handles.prefs.window !== undefined ) {\n    return new Promise((resolve) => {\n      handles.prefs.window.show();\n      resolve();\n    });\n  }\n\n  return new Promise((resolve) => {\n    const primary = electronScreen.getPrimaryDisplay();\n\n    // take a screenshot of the main screen for use in previews\n    grabScreen(primary).then((grab) => {\n      screenshots[primary.id] = grab.url;\n\n      const prefsUrl = getUrl(\"prefs.html\");\n      handles.prefs.window = new BrowserWindow({\n        show: false,\n        width: 910,\n        height: 700,\n        minWidth: 800,\n        maxWidth: 910,\n        minHeight: 600,\n        resizable: true,\n        webPreferences: {\n          ...defaultWebPreferences,\n          preload: path.join(getAssetsDir(), \"preload.mjs\")\n        },\n        icon: path.join(getAssetsDir(), \"iconTemplate.png\")\n      });\n\n      if ( !isDev && handles.prefs.window.removeMenu !== undefined ) {\n        handles.prefs.window.removeMenu();\n      }\n      \n      handles.prefs.window.on(\"closed\", () => {\n        handles.prefs.window = null;\n        dock.hideDockIfInactive(app);\n      });\n\n      handles.prefs.window.once(\"ready-to-show\", () => {\n        handles.prefs.window.show();\n        dock.showDock(app);\n      });\n      \n      handles.prefs.window.once(\"show\", resolve);\n\n      log.info(\"loading \" + prefsUrl);\n      handles.prefs.window.loadURL(prefsUrl);\n    });\n  });\n};\n\nvar openSettingsWindow = function() {\n  if ( handles.settings.window !== null && handles.settings.window !== undefined ) {\n    return new Promise((resolve) => {\n      handles.settings.window.show();\n      resolve();\n    });\n  }\n\n  var settingsUrl = getUrl(\"settings.html\");\n  handles.settings.window = new BrowserWindow({\n    show: false,\n    width:600,\n    height:650,\n    maxWidth: 600,\n    minWidth: 600,\n    resizable: true,\n    parent: handles.prefs.window,\n    modal: true,\n    icon: path.join(getAssetsDir(), \"iconTemplate.png\"),\n    webPreferences: {\n      ...defaultWebPreferences,\n      preload: path.join(getAssetsDir(), \"preload.mjs\"),\n    }\n  });\n\n  // hide the file menu\n  if ( !isDev && handles.settings.window.removeMenu !== undefined ) {\n    handles.settings.window.removeMenu();\n  }\n\n  handles.settings.window.on(\"closed\", () => {\n    handles.settings.window = null;\n    dock.hideDockIfInactive(app);\n  });\n\n  handles.settings.window.once(\"ready-to-show\", () => {\n    handles.settings.window.show();\n    dock.showDock(app);\n  });\n\n  log.info(`open ${settingsUrl}`);\n  handles.settings.window.loadURL(settingsUrl);\n};\n\n/**\n * handle new screensaver event. open the window to create a screensaver\n */\nvar addNewSaver = async function(opts) {\n  var newUrl = getUrl(\"new.html\");\n  var primary = electronScreen.getPrimaryDisplay();\n\n  // take a screenshot of the main screen for use in previews\n  if ( !opts.screenshot) {\n    const grab = await grabScreen(primary);\n    opts.screenshot = grab.url;\n  }\n\n  handles.addNew.window = new BrowserWindow({\n    show: false,\n    width: 450,\n    height: 700,\n    resizable:true,\n    webPreferences: {\n      ...defaultWebPreferences,\n      preload: path.join(getAssetsDir(), \"preload.mjs\"),\n    },\n    icon: path.join(getAssetsDir(), \"iconTemplate.png\")\n  });\n\n  handles.addNew.window.on(\"closed\", () => {\n    handles.addNew.window = null;\n    dock.hideDockIfInactive(app);\n  });\n\n  handles.addNew.window.once(\"ready-to-show\", () => {\n    handles.addNew.window.show();\n    dock.showDock(app);\n  });\n\n  handles.addNew.window.loadURL(newUrl);\n};\n\n/**\n * Open the About window for the app\n */\nvar openAboutWindow = function() {\n  var aboutUrl = getUrl(\"about.html\");\n  handles.about.window = new BrowserWindow({\n    show: false,\n    width:500,\n    height:600,\n    resizable:false,\n    icon: path.join(getAssetsDir(), \"iconTemplate.png\"),\n    webPreferences: {\n      ...defaultWebPreferences,\n      preload: path.join(getAssetsDir(), \"preload.mjs\"),\n    }\n  });\n\n  if ( !isDev && handles.about.window.removeMenu !== undefined ) {\n    handles.about.window.removeMenu();\n  }\n\n  handles.about.window.on(\"closed\", () => {\n    handles.about.window = null;\n    dock.hideDockIfInactive(app);\n  });\n\n  handles.about.window.once(\"ready-to-show\", () => {\n    handles.about.window.show();\n    dock.showDock(app);\n  });\n\n  log.info(`open ${aboutUrl}`);\n  handles.about.window.loadURL(aboutUrl);\n};\n\n\n/**\n * open the editor tool for a screensaver\n * @param {Object} args object with arguments\n * @param {string} args.src path to the JSON file for the screensaver\n * @param {string} args.screenshot path to the screenshot to use when editing\n */\nvar openEditor = (args) => {\n  var key = args.src;\n  var screenshot = args.screenshot;\n  \n  var editorUrl = getUrl(\"editor.html\");\n  \n  var target = editorUrl + \"?\" +\n               \"src=\" + encodeURIComponent(key) +\n               \"&screenshot=\" + encodeURIComponent(screenshot);\n\n  if ( handles.editor.window == null ) {\n    handles.editor.window = new BrowserWindow({\n      show: false,\n      webPreferences: {\n        ...defaultWebPreferences,\n        preload: path.join(getAssetsDir(), \"preload.mjs\"),\n      },\n    });  \n  }\n\n  handles.editor.window.screenshot = screenshot;\n\n  handles.editor.window.once(\"ready-to-show\", () => {\n    handles.editor.window.send(\"args\", args);\n    handles.editor.window.show();\n\n    if (process.env.NODE_ENV === \"test\") {\n      handles.editor.window.webContents.closeDevTools();\n    }\n\n    dock.showDock(app);\n  });\n\n  handles.editor.window.on(\"closed\", () => {\n    handles.editor.window = null;\n    if ( handles.editor.preview ) {\n      handles.editor.preview.destroy();\n    }\n    handles.editor.preview = null;\n\n    dock.hideDockIfInactive(app);\n  });\n\n  handles.editor.window.loadURL(target);  \n};\n\n\n\n/**\n * get the BrowserWindow options we'll use to launch\n * on the given screen.\n */\nvar getWindowOpts = function(s) {\n  var opts = {\n    backgroundColor: \"#000000\",\n    alwaysOnTop: true,\n    x: s.bounds.x,\n    y: s.bounds.y,\n    show: false,\n    roundedCorners: false,\n    titleBarStyle: \"customButtonsOnHover\",\n    webPreferences: {\n      ...defaultWebPreferences\n    }\n  };\n\n  // osx will display window immediately if fullscreen is true\n  // so we default it to false there\n  if (process.platform !== \"darwin\" ) {\n    opts.fullscreen = true;\n  }\n\n  if ( testMode === true ) {\n    opts.fullscreen = false;\n    // opts.x = 100;\n    // opts.y = 100;\n    opts.show = true;\n    opts.width = 400;\n    opts.height = 400;\n  }\n\n  return opts;\n};\n\nvar applyScreensaverWindowEvents = function(w) {\n  // Emitted when the window is closed.\n  w.once(\"closed\", function() {\n    if (process.platform !== \"win32\" ) {\n      cursor.show();\n    }\n    windows.forceWindowClose(w);\n  });\n  \n  // inject our custom CSS into the screensaver window\n  w.webContents.on(\"did-finish-load\", function() {\n    log.info(\"did-finish-load\");\n    if (!w.isDestroyed()) {\n      // load some global CSS we'll inject into running screensavers\n      const globalCSSCode = fs.readFileSync( path.join(getAssetsDir(), \"global.css\"), \"ascii\");  \n\n      w.webContents.insertCSS(globalCSSCode);\n    }\n  });\n  \n  // we could do something nice with either of these events\n  w.webContents.on(\"render-process-gone\", log.info);\n  w.webContents.on(\"unresponsive\", log.info);\n};\n\n/**\n * \n * @param {String} screenshot URL of screenshot\n * @param {Saver} saver the screensaver to run\n * @param {Screen} s the screen to run it on\n * @param {Object} url_opts any options to pass on the url\n * @param {number} tickCount hrtime value of when we started\n */\nvar runSaver = function(screenshot, saver, s, url_opts, tickCount) {\n  const windowOpts = getWindowOpts(s);\n  var w = new BrowserWindow(windowOpts);       \n  w.isSaver = true;\n\n  if ( w.removeMenu !== undefined ) {\n    w.removeMenu();\n  }\n\n  let diff = process.hrtime(tickCount);\n  log.info(`run screensaver ${saver.name} on screen ${s.id} ${saver.url} ts: ${diff[0] * 1e9 + diff[1]}`);\n\n\n  return new Promise((resolve, reject) => {\n    try {\n      applyScreensaverWindowEvents(w);\n      \n      w.webContents.once(\"did-fail-load\", (_event, _code, description) => {\n        log.info(`did-fail-load: ${description}`);\n        windows.forceWindowClose(w);\n        reject(s.id, description);  \n      });\n\n      w.once(\"ready-to-show\", () => {\n        log.info(\"ready-to-show\", s.id);\n        if ( testMode !== true ) {\n          windows.setFullScreen(w);\n        }\n\n        if (process.platform === \"win32\" ) {\n          log.info(\"force focus\");\n          forceFocus.focusWindow(w);\n        }\n        \n        diff = process.hrtime(tickCount);\n        log.info(`rendered in ${diff[0] * 1e9 + diff[1]} nanoseconds`);\n        resolve(s.id);\n      });\n      \n      if ( typeof(screenshot) !== \"undefined\" ) {\n        log.info(`pass screenshot ${screenshot}`);\n        url_opts.screenshot = encodeURIComponent(\"file://\" + screenshot);\n      }\n      // w.webContents.openDevTools();\n\n\n      // generate screensaver object, then get url to load\n      const saverObj = new Saver(saver);\n      const url = saverObj.urlWithParams(url_opts);\n      \n      log.info(\"Loading \" + url, s.id);\n      \n      // and load the index.html of the app.\n      w.loadURL(url);\n    }\n    catch (e) {\n      log.info(e);\n      windows.forceWindowClose(w);\n      reject(s.id, e);\n    }\n  });\n};\n\n/**\n * run the specified screensaver on the specified screen\n */\nvar runScreenSaverOnDisplay = function(saver, s) {\n  var size = s.bounds;\n  var url_opts = { \n    width: size.width,\n    height: size.height,\n    platform: process.platform\n  };\n  \n  log.info(\"runScreenSaverOnDisplay\", s.id);\n\n  // don't do anything if we don't actually have a screensaver\n  if ( typeof(saver) === \"undefined\" || saver === null ) {\n    log.info(\"no saver, exiting\");\n    return Promise.resolve();\n  }\n\n  let tickCount = process.hrtime();\n\n  //\n  // if this screensaver uses a screengrab, get it. \n  // otherwise just boot it\n  //\n  const reqs = saver.requirements;\n  if ( reqs !== undefined && reqs.findIndex && reqs.findIndex((x) => { return x === \"screen\"; }) > -1 ) {\n    return grabScreen(s).then((message) => {\n      runSaver(message.url, saver, s, url_opts, tickCount);\n    });\n  }\n  else {\n    return runSaver(undefined, saver, s, url_opts, tickCount);\n  }\n};\n\n/**\n * blank out the given screen\n */\nvar blankScreen = async function(s) {\n  if ( process.env.TEST_MODE ) {\n    log.info(\"refusing to blank screen in test mode\");\n    return s.id;\n  }\n\n  const systemPath = getSystemDir();\n  const blankUrl = `file://${path.join(systemPath, \"system-savers\", \"blank\", \"index.html\")}`;\n  const saver = {\n    name: \"Blank\",\n    url: blankUrl\n  };\n  return runScreenSaverOnDisplay(saver, s);\n};\n\n\n/**\n * get a list of displays connected to the computer.\n */\nvar getDisplays = function() {\n  var displays = [];\n  if ( debugMode === true || prefs.runOnSingleDisplay === true ) {\n    displays = [\n      electronScreen.getPrimaryDisplay()\n    ];\n  }\n  else {\n    displays = electronScreen.getAllDisplays();\n  }\n\n  return displays;\n};\n\n\n/**\n * get a list of the non primary displays connected to the computer\n */\nvar getNonPrimaryDisplays = function() {\n  var primary = electronScreen.getPrimaryDisplay();\n  return electronScreen.getAllDisplays().filter((d) => {\n    return d.id !== primary.id;\n  });\n};\n\n/**\n * manually trigger screensaver by setting state to run\n */\nvar setStateToRunning = function() {\n  log.info(\"setStateToRunning\");\n  // disable power state check\n  checkPowerState = false;\n  stateManager.run();\n};\n\nvar setStateToPaused = function() {\n  log.info(\"setStateToPaused\");\n  stateManager.pause();\n  stateManager.stopTicking();\n};\nvar resetState = function() {\n  stateManager.reset();\n};\n\n\n/**\n * return a promise the resolves to the path to the screensaver and its options\n */\nvar findScreensaver = function() {\n  const workingPath = getSystemDir();\n  // check if the user is running the random screensaver. if so, pick one!\n  const randomPath = path.join(workingPath, \"system-savers\", \"random\", \"saver.json\");\n  // log.info(\"random: \" + randomPath);\n  if ( prefs.saver === randomPath ) {\n    return new Promise((resolve) => {\n      savers.list(() => {\n        // @todo s can be undefined\n        // https://sentry.io/organizations/colin-mitchell/issues/955633850/?project=172824&query=is%3Aunresolved&statsPeriod=14d&utc=false\n        let s = savers.random();\n        resolve(s.key, prefs.getOptions(s.key));\n      });\n    });\n  }\n\n  return Promise.resolve(prefs.saver, prefs.getOptions(prefs.saver));\n};\n\n/**\n * run the user's chosen screensaver on any available screens\n */\nvar runScreenSaver = function() {\n  log.info(\"runScreenSaver\");\n  const setupPromise = findScreensaver();\n\n  setupPromise.\n    then((saverKey, settings) => savers.loadFromFile(saverKey, settings)).\n          catch((err) => {\n            log.info(\"================ loading saver failed?\");\n            log.info(err.message);\n            return undefined;\n          }).\n          then((saver) => {\n            let displays = [];\n            let blanks = [];\n\n            // make sure we have something to display\n            if ( typeof(saver) === \"undefined\" ) {\n              log.info(\"No screensaver defined! Just blank everything\");\n              blanks = getDisplays().concat(getNonPrimaryDisplays());\n            }\n            else if ( testMode === true ) {\n              blanks = [];\n            } else {\n              displays = getDisplays();\n              if ( debugMode !== true && testMode !== true && prefs.runOnSingleDisplay === true ) {\n                blanks = getNonPrimaryDisplays();\n              }\n            }\n\n            // turn off idle checks for a couple seconds while loading savers\n            stateManager.ignoreReset(true);\n\n            cursor.hide();\n\n            //\n            // generate an array of promises for rendering screensavers on any screens\n            //\n            const promises = displays\n              .map((d) => runScreenSaverOnDisplay(saver, d))\n              .concat(\n                blanks.map((d) => blankScreen(d))\n              );\n\n            Promise.allSettled(promises).then((values) => {\n              log.info(\"final result\", values);\n              setRunningInABit();\n            }).catch((e) => {\n              log.info(\"running screensaver failed\");\n              log.info(e);\n\n              stateManager.reset();\n              cursor.show(); \n            });\n          });\n};\n\n/**\n * After a short delay, set state manager to running. This should\n * help with mouse wiggle/etc\n */\nvar setRunningInABit = function() {\n  setTimeout(function() {\n    log.info(\"our work is done, set state to running\");\n    stateManager.running();\n  }, 1500);\n};\n\n/**\n * should we lock the user's screen when returning from running the saver?\n */\nvar shouldLockScreen = function() {\n  // we can't lock the screen on OSX because it would involve using\n  // private APIs and is a super pain in the butt\n  return ( prefs.lock === true );\n};\n\n/**\n * stop the running screensaver\n */\nvar stopScreenSaver = function(fromBlank) {\n  log.info(\"received stopScreenSaver call\");\n\n  if ( fromBlank !== true ) {\n    stateManager.reset();\n  }\n\n  // trigger lock screen before actually closing anything\n  else if ( shouldLockScreen() && screenLock.doLockScreen ) {\n    log.info(\"lock the screen\");\n    screenLock.doLockScreen();\n  }\n\n  windows.closeRunningScreensavers();\n  cursor.show();\n};\n\n\n/**\n * determine what our system directory is. this should basically be\n * where the app exists, and where the system-savers directory and\n * other critical files exist.\n */\nvar getSystemDir = function() {\n  if ( process.env.BEFORE_DAWN_SYSTEM_DIR !== undefined ) {\n    return process.env.BEFORE_DAWN_SYSTEM_DIR;\n  }\n\n  if ( process.env.TEST_MODE ) {\n    return app.getAppPath();\n  }\n  if ( app.isPackaged ) {\n    return path.join(app.getAppPath(), \"output\");\n  }\n\n  return path.join(app.getAppPath(), \"..\", \"..\", \"output\");\n};\n\n\n/**\n * determine what our assets directory is. This is where global CSS,\n * icons, etc, can be found.\n */\nlet getAssetsDir = function() {\n  if ( process.env.BEFORE_DAWN_ASSETS_DIR !== undefined ) {\n    return process.env.BEFORE_DAWN_ASSETS_DIR;\n  }\n\n  if ( app.isPackaged ) {\n    return path.join(app.getAppPath(), \"output\", \"assets\");\n  }\n  if ( process.env.TEST_MODE ) {\n    return path.join(app.getAppPath(), \"assets\");\n  }\n\n  return path.join(app.getAppPath(), \"assets\");\n};\n\n\n/**\n * return the URL prefix we should use when loading app windows. if\n * running in development mode with hot reload enabled, we'll use an\n * HTTP request, otherwise we'll use a file:// url.\n */\nvar getUrl = function(dest) {\n  let baseUrl;\n  if ( !testMode && isDev ) {\n    let devPort;\n\n    try {\n      devPort = packageJSON.devport;\n    }\n    catch {\n      devPort = 9080;\n    }\n    \n    baseUrl = `http://localhost:${devPort}`;\n\n    return new URL(dest, new URL(baseUrl)).toString();\n  }\n\n  log.info(`hey!!! ${app.getAppPath()}`);\n  if ( testMode ) {\n    return `file://${app.getAppPath()}/${dest}`;\n  }\n\n  return `file://${app.getAppPath()}/output/${dest}`;\n};\n\nvar setupForTesting = function() {\n  if ( testMode === true ) {\n    log.info(\"opening shim for test mode\");\n    openTestShim();\n  } \n};\n\n/**\n * build and apply an application menu and tray menu\n */\nvar setupMenuAndTray = function() {\n  var menu = Menu.buildFromTemplate(menusAndTrays.buildMenuTemplate(app));\n\n  Menu.setApplicationMenu(menu);\n\n  //\n  // build the tray menu\n  //\n  trayMenu = Menu.buildFromTemplate(menusAndTrays.trayMenuTemplate());\n\n  trayMenu.items[3].visible = global.NEW_RELEASE_AVAILABLE;\n\n  const iconImage = menusAndTrays.trayIconImage();\n\n  appIcon = new Tray(iconImage);\n  appIcon.setToolTip(global.APP_NAME);\n  appIcon.setContextMenu(trayMenu); \n  \n  // show tray menu on right click\n  // @todo should this be osx only?\n  appIcon.on(\"right-click\", () => {\n    appIcon.popUpContextMenu();\n  });\n  appIcon.on(\"click\", () => {\n    appIcon.popUpContextMenu();\n  });\n};\n\n/**\n * setup any requirements for the app\n * \n * @returns {Promise} Promise that resolves with true if setup for first time, false if app was ready\n */\nvar setupIfNeeded = async function() {\n  log.info(\"setupIfNeeded\");\n\n  if ( process.env.QUIET_MODE === \"true\" || process.env.NODE_ENV === \"test\" ) {\n    log.info(\"Quiet/test mode, skip setup checks!\");\n    return false;\n  }\n\n  // check if we should download savers, set something up, etc\n  if ( process.env.FORCE_SETUP || prefs.needSetup ) {\n    // stop processing here, we know we need to setup\n    log.info(\"needSetup!\");\n    return true;\n  }\n\n  // log.info(`checking if ${prefs.saver} is valid`);\n  const exists = await savers.confirmExists(prefs.saver);\n  if ( ! exists ) {\n    log.info(\"need to pick a new screensaver\");\n  }\n  else {\n    log.info(\"looks like we are good to go\");\n  }\n\n  return !exists;\n};\n\n/**\n * open the preferences window if needed\n * \n * @param {Boolean} status true if we need to open the prefs window\n */\nvar openPrefsWindowIfNeeded = function(status) {\n  log.info(\"openPrefsWindowIfNeeded\");\n  if ( status === true ) {\n    log.info(\"we do need to open prefs window\");\n    return openPrefsWindow();\n  }\n\n  return Promise.resolve();\n};\n\n/**\n * setup our periodic release check\n */\nvar setupReleaseCheck = function() {\n  if ( ! global.RELEASE_CHECK_URL ) {\n    log.info(\"no release server set, so no release checks\");\n    return;\n  }\n\n  releaseChecker = new ReleaseCheck();\n\n  releaseChecker.setFeed(global.RELEASE_CHECK_URL);\n  releaseChecker.setLogger(log.info);\n  releaseChecker.onUpdate(() => {\n    global.NEW_RELEASE_AVAILABLE = true;\n    log.info(\"update available, show it\");\n\n    getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;\n  });\n  releaseChecker.onNoUpdate(() => {\n    global.NEW_RELEASE_AVAILABLE = false;\n\n    log.info(\"no update available, hide it\");\n    getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;\n  });\n\n  log.info(\"Run initial release check\");\n  checkForNewRelease();\n\n  // check for a new release every 12 hours\n  log.info(\"Setup release check\");\n  setInterval(checkForNewRelease, RELEASE_CHECK_INTERVAL);\n};\n\n/**\n * Check if we should move the app to the actual application folder.\n * This is important because the app is pretty fragile on OSX otherwise.\n */\nvar askAboutApplicationsFolder = function() {\n  if ( testMode === true || isDev === true || app.isInApplicationsFolder === undefined ) {\n    return;\n  }\n\n  if ( !app.isInApplicationsFolder() ) {\n    const chosen = dialog.showMessageBoxSync({\n      type: \"question\",\n      buttons: [\"Move to Applications\", \"Do Not Move\"],\n      message: \"Move to Applications folder?\",\n      detail: \"Hello! I work better in your Applications folder, should I move myself there?\"\n    });\n\n    if ( chosen === 0 ) {\n      app.moveToApplicationsFolder();\n    }\n  }\n};\n\n/**\n * check for permissions to access certain systems on OSX\n */\nvar askAboutMediaAccess = async function() {\n  if (process.platform !== \"darwin\" || testMode === true ) {\n    return;\n  }\n\n  [\"microphone\", \"camera\", \"screen\"].forEach(async (type) => {\n    log.info(type);\n    // note: this might be handy\n    //     \"mac-screen-capture-permissions\": \"^1.1.0\",\n    // if ( type === \"screen\" ) {\n    //   const {\n    //     hasScreenCapturePermission,\n    //     hasPromptedForPermission \n    //   } = require('mac-screen-capture-permissions');\n    //   const result = hasPromptedForPermission();\n    //   const result2 = hasScreenCapturePermission();\n    // }\n    // https://www.electronjs.org/docs/api/system-preferences#systempreferencesaskformediaaccessmediatype-macos\n    log.info(`access to ${type}: ${systemPreferences.getMediaAccessStatus(type)}`);\n\n    // re: screen -- This permission can only be granted manually in the System\n    // Preferences. Therefore systemPreferences.askForMediaAccess() cannot be\n    // extended in the same way.\n\n    if ( systemPreferences.getMediaAccessStatus(type) !== \"granted\" && type !== \"screen\" ) {\n      await systemPreferences.askForMediaAccess(type);\n    }\n  });\n};\n\nconst getPackage = function() {\n  const attrs = {\n    repo: prefs.sourceRepo,\n    dest: prefs.defaultSaversDir,\n    log: log.info,\n    fetch: net.fetch\n  };\n\n  console.log(attrs);\n\n  return new Package(attrs);\n};\n\n\n/**\n * setup assorted IPC listeners\n */\nlet setupIPC = function() {\n  /**\n   * open the window specified by 'key', passing args along\n   */\n  ipcMain.on(\"open-window\", (_event, key, args) => {\n    windowMethods[key](args);\n  });\n\n  /**\n   * set screensaver state to paused\n   */\n  ipcMain.on(\"pause\", () => {\n    setStateToPaused();\n  });\n\n  /**\n   * set screensaver state to enabled\n   */\n  ipcMain.on(\"enable\", () => {\n    resetState();\n  });\n  \n  /**\n   * close the window specified by 'key'\n   */\n  ipcMain.on(\"close-window\", (event, key) => {\n    if ( handles[key].window ) {\n      handles[key].window.close();\n    }\n  });\n\n  ipcMain.on(\"close-all-windows\", () => {\n    console.log(\"close-all-windows\");\n    Object.keys(handles).forEach(function(key) {\n      if ( handles[key].window ) {\n        console.log(\"close $key\");\n        handles[key].window.close();\n      }\n    });\n  });\n\n  /**\n   * return prefs data to requester\n   */\n  ipcMain.handle(\"get-prefs\", () => {\n    log.info(\"get-prefs\");\n    return prefs.data;\n  });\n\n  /**\n   * return a couple of global variables\n   */\n  ipcMain.handle(\"get-globals\", () => {\n    return {\n      APP_VERSION: global.APP_VERSION,\n      APP_REPO: global.APP_REPO,\n      NEW_RELEASE_AVAILABLE: global.NEW_RELEASE_AVAILABLE\n    };\n  });\n\n  /**\n   * return a list of screensavers\n   */\n  ipcMain.handle(\"list-savers\", async () => {\n    const entries = await savers.list();\n    return entries;\n  });\n\n  /**\n   * load and return the specified screensaver\n   */\n  ipcMain.handle(\"load-saver\", async (_event, key) => {\n    return await savers.loadFromFile(key);\n  });\n\n  /**\n   * delete the specified screensaver\n   */\n  ipcMain.handle(\"delete-saver\", async(_event, attrs) => {\n    log.info(\"delete-saver\", attrs);\n    await savers.delete(attrs);\n    savers.reset();\n    prefs.reload();\n  });\n\n  /**\n   * update prefs with the incoming attrs\n   */\n  ipcMain.handle(\"update-prefs\", async(_event, attrs) => {\n    log.info(\"update-prefs\", attrs);\n\n    // ensure a value for this\n    attrs.firstLoad = false;\n    if ( attrs.launchShortcut === undefined ) {\n      attrs.launchShortcut = \"\";\n    }\n\n    prefs.store.set(attrs);\n\n    savers.reset();\n    updateStateManager();\n  });\n\n  ipcMain.handle(\"check-screensaver-package\", async() => {\n    log.info(\"check-screensaver-package\");\n    return getPackage().getReleaseInfo();\n  });\n\n  ipcMain.handle(\"download-screensaver-package\", async() => {\n    log.info(\"download-screensaver-package\");\n    const result = await getPackage().downloadRelease();\n    \n    log.info(result);\n    toggleSaversUpdated();\n    return result;\n  });\n\n\n  /**\n   * return the default settings for the app\n   */\n  ipcMain.handle(\"get-defaults\", async() => {\n    log.info(\"get-defaults\");\n    log.info(prefs.defaults);\n    return prefs.defaults;\n  });\n\n  /**\n   * update the local source settings\n   */\n  ipcMain.handle(\"update-local-source\", async(_event, ls) => {\n    log.info(\"update-local-source\", ls);\n    prefs.store.set(\"localSource\", ls);\n\n    savers.reset();\n  });\n\n  /**\n   * create a new screensaver from our template\n   */\n  ipcMain.handle(\"create-screensaver\", async(_event, attrs) => {\n    const factory = new SaverFactory();\n    \n    const src = path.join(getSystemDir(), \"system-savers\", \"__template\");\n    log.info(`create-screensaver from ${src}`);\n    const dest = prefs.localSource;\n    const data = factory.create(src, dest, attrs);\n    \n    savers.reset();\n\n    return data;\n  });\n\n  /**\n   * save/update a screensaver object\n   */\n  ipcMain.handle(\"save-screensaver\", async(_event, attrs, dest) => {\n    const s = new Saver(attrs);\n    s.write(attrs, dest);\n  });\n\n  /**\n   * return the bounds of the primary screen to the requester\n   */\n  ipcMain.handle(\"get-primary-display-bounds\", () => {\n    return electronScreen.getPrimaryDisplay().bounds;\n  });\n\n  /**\n   * return a screengrab of the primary screen to the requester\n   */\n  ipcMain.handle(\"get-primary-screenshot\", () => {\n    return screenshots[electronScreen.getPrimaryDisplay().id];\n  });\n  \n  /**\n   * load the requested URL in a browser\n   */\n  ipcMain.on(\"launch-url\", (_event, url) => {\n    shell.openExternal(url);\n  });\n  \n  /**\n   * handle savers-updated event. this is sent when a screensaver is created/updated\n   */\n  ipcMain.on(\"savers-updated\", () => {\n    log.info(\"savers-updated\");\n    toggleSaversUpdated();\n  });\n\n  /**\n   * set autostart value\n   */\n  ipcMain.on(\"set-autostart\", (_event, value) => {\n    log.info(\"set-autostart\");\n    if ( process.env.TEST_MODE !== undefined ) {\n      log.info(\"we're in test mode, skipping autostart\");\n      return;\n    }\n  \n    autostarter.toggle(global.APP_NAME, value);\n  });\n\n  /**\n   * handle event to set global launch shortcut\n   */\n  ipcMain.on(\"set-global-launch-shortcut\", () => {\n    log.info(\"set-global-launch-shortcut\");\n    setupLaunchShortcut();\n  });\n\n  /**\n   * run the users specified screensaver\n   */\n  ipcMain.on(\"run-screensaver\", () => {\n    log.info(\"run-screensaver\");\n    setStateToRunning();\n  });\n\n  ipcMain.on(\"toggle-dev-tools\", () => {\n    log.info(\"toggle-dev-tools\");\n    if ( handles.editor.window !== null ) {\n      handles.editor.window.webContents.openDevTools();\n    }\n  });\n\n  ipcMain.on(\"console-log\", (_event, payload) => {\n    log.info(payload);\n  });\n\n  /**\n   * open a folder\n   */\n  ipcMain.on(\"open-folder\", (_event, src) => {\n    var cmd;\n    var args = [];\n\n    // figure out the path to the screensaver folder. use\n    // decodeURIComponent to convert %20 to spaces\n    const filePath = path.dirname(decodeURIComponent(url.parse(src).path)); //.split(path.posix.sep).join(path.sep);\n\n    switch(process.platform) {\n    case \"darwin\":\n      cmd = \"open\";\n      args = [ filePath ];\n      break;\n    case \"win32\":\n      if (process.env.SystemRoot) {\n        cmd = path.join(process.env.SystemRoot, \"explorer.exe\");\n      }\n      else {\n        cmd = \"explorer.exe\";\n      }\n      args = [`${filePath}`];\n      break;\n    default:\n      // # Strip the filename from the path to make sure we pass a directory\n      // # path. If we pass xdg-open a file path, it will open that file in the\n      // # most suitable application instead, which is not what we want.\n      cmd = \"xdg-open\";\n      args = [ filePath ];\n    }\n    \n    exec(cmd, args, function() {});\n  });\n\n  ipcMain.on(\"watch-folder\", (event, src) => {\n    const webContents = event.sender;\n    const win = BrowserWindow.fromWebContents(webContents);\n\n    const folderPath = path.dirname(src);\n    // make sure folder actually exists\n    if ( fs.existsSync(folderPath) ) {\n      win.fsWatcher = fs.watch(folderPath, (eventType, filename) => {\n        if (filename && win?.webContents) {\n          win.webContents.send(\"folder-update\", filename);\n        }\n      });\n    }\n  });\n\n  ipcMain.on(\"unwatch-folder\", (event) => {\n    const webContents = event.sender;\n    const win = BrowserWindow.fromWebContents(webContents);\n\n    win.fsWatcher.close();\n  });\n\n  /**\n   * display a dialog about a package update\n   */\n  if ( process.env.TEST_MODE === undefined ) {\n    ipcMain.on(\"display-update-dialog\", async () => {\n      const result = await dialog.showMessageBox({\n        type: \"info\",\n        title: \"Update Available!\",\n        message: \"There's a new update available! Would you like to download it?\",\n        buttons: [\"No\", \"Yes\"],\n        defaultId: 0\n      });\n      \n      if ( result.response === 1 ) {\n        const appRepo = global.APP_REPO;\n        shell.openExternal(`https://github.com/${appRepo}/releases/latest`);\n      }\n    });  \n  }\n\n  /**\n   * display a dialog when the user wants to reset to default settings\n   */\n  ipcMain.handle(\"reset-to-defaults-dialog\", async () => {\n    const result = await dialog.showMessageBox({\n      type: \"info\",\n      title: \"Are you sure?\",\n      message: \"Are you sure you want to reset to the default settings?\",\n      buttons: [\"No\", \"Yes\"],\n      defaultId: 0\n    });\n    return result.response;\n  });\n\n  /**\n   * display a confirmation dialog for deleting a screensaver\n   */\n  ipcMain.handle(\"delete-screensaver-dialog\", async (_event, saver) => {\n    const result = await dialog.showMessageBox(\n      {\n        type: \"info\",\n        title: \"Are you sure?\",\n        message: \"Are you sure you want to delete this screensaver?\",\n        detail: `Deleting screensaver ${saver.name}`,\n        buttons: [\"No\", \"Yes\"],\n        defaultId: 0\n      }); \n    \n    return result.response;\n  });\n\n  /**\n   * display a folder chooser for setting local source\n   */\n  ipcMain.handle(\"show-open-dialog\", async () => {\n    const result = await dialog.showOpenDialog(\n      {\n        title: \"Pick a screensaver directory\",\n        message: \"Pick a folder to store your custom screensavers\",\n        properties: [ \"openDirectory\", \"createDirectory\" ]\n      });\n    return result;\n  });\n\n  //\n  // setup a couple of IPC methods we only use in tests\n  //\n  if ( testMode === true ) {\n    /**\n     * handle requests to get the current state of the app. this\n     * is currently only called by our test shim\n     */\n    ipcMain.handle(\"get-current-state\", async () => {\n      return stateManager.currentStateString;\n    });\n\n    /**\n     * get a list of tray item labels\n     */\n    ipcMain.handle(\"get-tray-items\", async () => {\n      return menusAndTrays.trayMenuTemplate().map(item => item.label);\n    });\n\n    /**\n     * fake a click on a tray item\n     */\n    ipcMain.handle(\"click-tray-item\", (_event, label) => {\n      log.info(`click-tray-item ${label}`);\n      const items = menusAndTrays.trayMenuTemplate();\n      const item = items.find(item => item.label === label);\n      item.click();\n    });\n  }\n  \n  /**\n   * handle quit app events\n   */\n  ipcMain.once(\"quit-app\", () => {\n    log.info(\"quit-app\");\n    quitApp();\n  });\n};\n\n\n/**\n * handle initial startup of app\n */\nvar bootApp = async function() {\n  log.info(\"bootApp\");\n\n  session.defaultSession.webRequest.onHeadersReceived((details, callback) => {\n    callback({responseHeaders: Object.fromEntries(Object.entries(details.responseHeaders).filter(header => !/x-frame-options/i.test(header[0])))});\n  });\n\n  askAboutApplicationsFolder();\n  await askAboutMediaAccess();\n\n  global.NEW_RELEASE_AVAILABLE = false;\n\n  // ensure proper data in about panel when available\n  if ( app.setAboutPanelOptions ) {\n    app.setAboutPanelOptions({\n      applicationName: global.APP_NAME,\n      applicationVersion: global.APP_VERSION,\n      version: global.APP_VERSION_BASE,\n      credits: global.APP_CREDITS\n    });\n  }\n\n  let saversDir;\n  if ( process.env.SAVERS_DIR ) {\n    saversDir = process.env.SAVERS_DIR;\n  }\n  else if ( isDev ) {\n    saversDir = path.join(app.getAppPath(), \"..\", \"..\", \"data\", \"savers\");\n    log.info(\"hello from dev mode, 'node bin/download-screensavers' to grab screensavers\");\n  }\n  else {\n    saversDir = path.join(process.resourcesPath, \"savers\");\n  }\n\n  const systemDir = getSystemDir();\n\n  let basePath;\n  // store our root path as a global variable so we can access it from screens\n  if ( process.env.BEFORE_DAWN_DIR !== undefined ) {\n    basePath = process.env.BEFORE_DAWN_DIR;\n  }\n  else {\n    basePath = app.getPath(\"userData\");\n  }\n  log.info(\"use base path\", basePath);\n\n\n  log.info(\"Loading prefs\");\n  log.info(`baseDir: ${basePath}`);\n  log.info(`saversDir: ${saversDir}`);\n  log.info(`system savers: ${systemDir}/system-savers`);\n\n  prefs = new SaverPrefs(basePath, systemDir, saversDir);\n  savers = new SaverListManager({\n    prefs: prefs\n  });\n\n  await listScreens();\n\n  //\n  // setup some event handlers for when screen count changes, mostly\n  // to ensure that we wake up if the user plugs in or removes a\n  // monitor\n  //\n  [\"display-added\", \"display-removed\"].forEach((type) => {\n    electronScreen.on(type, async () => {\n      log.info(type);\n\n      await listScreens();\n      windows.handleDisplayChange();\n    });\n  });\n\n  [\"suspend\", \"lock-screen\"].forEach((type) => {\n    powerMonitor.on(type, (ev) => {\n      if ( stateManager.isTicking() ) {\n        log.info(`system ${type} event, stop screensavers`);\n        ev.preventDefault();\n        stateManager.stopTicking();\n        windows.closeRunningScreensavers();\n      }\n    }); \n  });\n\n  if ( testMode !== true ) {\n    setInterval(() => {\n      if ( stateManager.isTicking() ) {\n        return;\n      }\n\n      const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;\n      const idleState = powerMonitor.getSystemIdleState(delayTime);\n\n      // don't restart state manager if we're paused\n      if ( ! stateManager.isTicking() && !stateManager.paused() && idleState === \"active\" ) {\n        log.info(\"looks like we are awake again lets go\");\n        stateManager.reset();\n        stateManager.startTicking();\n      }\n    }, 10000);\n  }\n  \n  powerMonitor.on(\"on-ac\", () => {\n    log.info(\"system on-ac event, reset state manager\");\n    stateManager.reset();\n  }); \n\n  setupIPC();\n\n  stateManager = new StateManager();\n  stateManager.idleFn = powerMonitor.getSystemIdleTime;\n\n  updateStateManager();\n\n  let result = await setupIfNeeded();\n  await openPrefsWindowIfNeeded(result);\n\n  setupForTesting();\n\n  setupMenuAndTray();\n  setupReleaseCheck();\n  setupLaunchShortcut();\n\n  // don't show app in dock\n  dock.hideDockIfInactive(app);\n\n  // start the idle check\n  stateManager.startTicking();\n};\n\n/**\n * toggle our 'ok to quit' variable and quit\n */\nvar quitApp = () => {\n  exitOnQuit = true;\n  app.quit(); \n};\n\n\n/**\n * run the screensaver, but only if there isn't an app in fullscreen mode right now\n */\nvar runScreenSaverIfNotFullscreen = function() {\n  log.info(\"runScreenSaverIfNotFullscreen\");\n  if ( ! isFullscreen() ) {\n    log.info(\"I don't think we're in fullscreen mode\");\n    runScreenSaver();\n  }\n  else {\n    log.info(\"looks like we are in fullscreen mode\");\n  }\n};\n\n/**\n * activate the screensaver, but only if we're plugged in, or if the user\n * is fine with running on battery\n */\nvar runScreenSaverIfPowered = async function() {\n  log.info(\"runScreenSaverIfPowered\");\n\n  if ( windows.screenSaverIsRunning() ) {\n    log.info(\"looks like we're already running\");\n    return;\n  }\n  \n  // check if we are on battery, and if we should be running in that case\n  if ( checkPowerState && prefs.disableOnBattery ) {\n    const isPowered = await power.charging();\n\n    if ( isPowered ) {       \n      runScreenSaverIfNotFullscreen();\n    }\n    else {\n      log.info(\"I would run, but we're on battery :(\");\n      stateManager.unrunnable();\n    }\n  }\n  else {\n    checkPowerState = true;\n    runScreenSaverIfNotFullscreen();\n  }\n};\n\n/**\n * if the screensaver is running, blank the screen. otherwise,\n * reset state machine\n */\nvar blankScreenIfNeeded = function() {\n  log.info(\"blankScreenIfNeeded\");\n  if ( windows.screenSaverIsRunning() ) {\n    log.info(\"running, close windows\");\n    stopScreenSaver(true);\n    screenLock.doSleep();\n  }\n};\n\n/**\n * update the state manager with our\n * timeout values, etc\n */\nvar updateStateManager = function() {\n  const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;\n  const blankOffset = process.platform === \"win32\" ? 0 : prefs.delay;\n  const blankTime = prefs.sleep > 0 ? (blankOffset + prefs.sleep) * 60 : Number.POSITIVE_INFINITY;\n  log.info(`updateStateManager idleTime: ${delayTime} blankTime: ${blankTime}`);\n\n  stateManager.setup({\n    idleTime: delayTime,\n    blankTime: blankTime,\n    onIdleTime: runScreenSaverIfPowered, \n    onBlankTime: blankScreenIfNeeded,\n    onReset: windows.closeRunningScreensavers,\n    logger: log.info\n  });\n};\n\n\n/**\n * check for a new release of the app\n */\nvar checkForNewRelease = function() {\n  log.info(\"checkForNewRelease\");\n  releaseChecker.checkLatestRelease();\n};\n\n/**\n * setup a global shortcut to run a screensaver\n */\nvar setupLaunchShortcut = function() {\n  globalShortcut.unregisterAll();\n  if ( prefs.launchShortcut !== undefined && prefs.launchShortcut !== \"\" ) {\n    log.info(`register launch shortcut: ${prefs.launchShortcut}`);\n    try {\n      const ret = globalShortcut.register(prefs.launchShortcut, () => {\n        log.info(\"shortcut triggered!\");\n        if ( handles.prefs.window && handles.prefs.window.isFocused() ) {\n          log.info(\"no shortcut when prefs active!\");\n          return;\n        }\n\n        try {\n          // turn off idle checks for a couple seconds while loading savers\n          stateManager.ignoreReset(true);\n          setStateToRunning();\n        }\n        catch (e) {\n          log.info(e);\n          stateManager.ignoreReset(false);\n        }\n        finally {\n          setTimeout(function() {\n            stateManager.ignoreReset(false);\n          }, 2500);\n        }\n      });\n      \n      if ( ! ret ) {\n        log.info(\"shortcut registration failed\");\n      }\n\n      log.info(`registered? ${globalShortcut.isRegistered(prefs.launchShortcut)}`);\n    }\n    catch(e) {\n      log.info(\"shortcut registration threw an error?\");\n      log.info(e);\n    }\n  }\n};\n\n\n/**\n * return our state manager\n * @returns {StateManager}\n */\nlet getStateManager = function() {\n  return stateManager;\n};\n\n/**\n * return the app icon\n * @returns {Tray}\n */\nlet getAppIcon = function() {\n  return appIcon;\n};\n\n/**\n * return the tray menu\n * @returns {Menu}\n */\nlet getTrayMenu = function() {\n  return trayMenu;\n};\n\nlet updateTrayMenu = function() {\n  appIcon.setContextMenu(trayMenu); \n}\n\n\n/**\n * if the user has updated one of their screensavers, we can let\n * the prefs window know that it needs to reload\n */\nlet toggleSaversUpdated = (arg) => {\n  prefs.reload();  \n  savers.reset();\n\n  if ( handles.prefs.window !== null ) {\n    handles.prefs.window.send(\"savers-updated\", arg);\n  }\n};\n\nconst windowMethods = {\n  editor: openEditor,\n  settings: openSettingsWindow,\n  prefs: openPrefsWindow,\n  about: openAboutWindow,\n  \"add-new\": addNewSaver\n};\n\n\nlog.transports.file.level = \"debug\";\nlog.transports.file.maxSize = 1 * 1024 * 1024;\n\nif (process.env.LOG_FILE) {\n  log.transports.file.resolvePathFn = () => process.env.LOG_FILE;\n}\n\n\nlog.info(`Hello from version: ${global.APP_VERSION_BASE} running in ${isDev ? \"development\" : \"production\"}`);\n\nif ( isDev ) {\n  app.name = global.APP_NAME;\n  log.info(`set app name to ${app.name}`);\n\n  if ( testMode !== true ) {\n    let userDataPath = path.join(app.getPath(\"appData\"), app.name);\n    log.info(`set userData path to ${userDataPath}`);\n    app.setPath(\"userData\", userDataPath);\n  }\n}\n\n/**\n * make sure we're only running a single instance\n */\nif ( testMode !== true ) {\n  app.on(\"second-instance\", () => {\n    try {\n      if ( handles.prefs.window === null && handles.prefs.window !== undefined ) {\n        openPrefsWindow();\n      }\n      else {\n        if ( handles.prefs.window.isMinimized() ) {\n          handles.prefs.window.restore();\n        }\n        handles.prefs.window.focus();\n      }\n    }\n    catch(e) {\n      console.log(e);\n    }\n  });\n}\n\n// seems like we need to catch this event to keep OSX from exiting app after screensaver runs?\napp.on(\"window-all-closed\", function() {\n  log.info(\"window-all-closed\");\n});\napp.on(\"before-quit\", function() {\n  log.info(\"before-quit\");\n});\napp.on(\"will-quit\", function(e) {\n  log.info(\"will-quit\");\n  if ( testMode !== true && isDev !== true && exitOnQuit !== true ) {\n    log.info(`don't quit! testMode: ${testMode} IS_DEV ${isDev} exitOnQuit ${exitOnQuit}`);\n    e.preventDefault();\n  }\n  else {\n    globalShortcut.unregisterAll();\n  }\n});\napp.once(\"quit\", function() {\n  log.info(\"quit\");\n});\n\n\nprocess.on(\"uncaughtException\", function (ex) {\n  log.info(ex);\n  log.info(ex.stack);\n});\n\nlog.info(\"readyto wait for bootApp\");\n\n// This method will be called when Electron has finished\n// initialization and is ready to create browser windows.\napp.whenReady().then(bootApp);\n\nif ( testMode === true ) {\n  exports.getTrayMenuItems = function() {\n    return menusAndTrays.trayMenuTemplate();\n  };  \n}\n\nexport {\n  log,\n  setStateToRunning,\n  setStateToPaused,\n  resetState,\n  getAssetsDir,\n  getStateManager,\n  getAppIcon,\n  getTrayMenu,\n  updateTrayMenu,\n  openPrefsWindow,\n  openAboutWindow,\n  addNewSaver,\n  openEditor,\n  toggleSaversUpdated,\n  quitApp,\n};"
  },
  {
    "path": "src/main/menus.js",
    "content": "\"use strict\";\n\nimport * as main from \"./index.js\";\n\nimport * as path from \"path\";\nimport { \n  nativeImage, \n  nativeTheme,\n  shell \n} from \"electron\";\n\n\n\nvar openUrl = (url) => {\n  try {\n    shell.openExternal(url);\n  }\n  catch(e) {\n    main.log.info(e);\n  }\n};\n\n/**\n * open the help section in a browser\n */\nvar openHelpUrl = () => { openUrl(global.HELP_URL); };\n\n\n/**\n * open the github issues url in a browser\n */\nvar openIssuesUrl = () => { openUrl(global.ISSUES_URL); };\n\n/**\n * open the website for the app\n */\nvar openHomepage = () => { openUrl(\"https://github.com/muffinista/before-dawn\"); };\n\n/**\n * Build the menubar for the app\n * \n * @param {Application} a the main app instance\n */\nexport const buildMenuTemplate = function(a) {\n  var app = a;\n  var base = [\n    {\n      label: \"File\",\n      submenu: [\n        {\n          label: \"Add New Screensaver\",\n          accelerator: \"CmdOrCtrl+N\",\n          click: function() {\n            main.addNewSaver();\n          }\n        },\n      ]\n    },\n\n    {\n      label: \"Edit\",\n      submenu: [\n        {\n          label: \"Undo\",\n          accelerator: \"CmdOrCtrl+Z\",\n          role: \"undo\"\n        },\n        {\n          label: \"Redo\",\n          accelerator: \"Shift+CmdOrCtrl+Z\",\n          role: \"redo\"\n        },\n        {\n          type: \"separator\"\n        },\n        {\n          label: \"Cut\",\n          accelerator: \"CmdOrCtrl+X\",\n          role: \"cut\"\n        },\n        {\n          label: \"Copy\",\n          accelerator: \"CmdOrCtrl+C\",\n          role: \"copy\"\n        },\n        {\n          label: \"Paste\",\n          accelerator: \"CmdOrCtrl+V\",\n          role: \"paste\"\n        },\n        {\n          label: \"Select All\",\n          accelerator: \"CmdOrCtrl+A\",\n          role: \"selectall\"\n        }\n      ]\n    },\n    {\n      label: \"View\",\n      submenu: [\n        {\n          label: \"Reload\",\n          accelerator: \"CmdOrCtrl+R\",\n          click: function(item, focusedWindow) {\n            if (focusedWindow) {\n              focusedWindow.reload();\n            }\n          }\n        },\n        {\n          label: \"Toggle Developer Tools\",\n          accelerator: (function() {\n            if (process.platform == \"darwin\") {\n              return \"Alt+Command+I\";\n            }\n            else {\n              return \"Ctrl+Shift+I\";\n            }\n          })(),\n          click: function(item, focusedWindow) {\n            if (focusedWindow) {\n              focusedWindow.toggleDevTools();\n            }\n          }\n        }\n      ]\n    },\n    {\n      label: \"Window\",\n      role: \"window\",\n      submenu: [\n        {\n          label: \"Minimize\",\n          accelerator: \"CmdOrCtrl+M\",\n          role: \"minimize\"\n        },\n        {\n          label: \"Close\",\n          accelerator: \"CmdOrCtrl+W\",\n          role: \"close\"\n        }\n      ]\n    },\n    {\n      label: \"Help\",\n      role: \"help\",\n      submenu: [\n        {\n          label: \"Learn More\",\n          click: openHomepage\n        },\n        {\n          label: \"Help\",\n          click: openHelpUrl\n        }\n      ]\n    }\n  ];\n\n\n  if (process.platform == \"darwin\") {\n    var name = app.name;\n    base.unshift({\n      label: name,\n      submenu: [\n        {\n          label: \"About \" + name,\n          role: \"about\"\n        },\n        {\n          type: \"separator\"\n        },\n        {\n          label: \"Services\",\n          role: \"services\",\n          submenu: []\n        },\n        {\n          type: \"separator\"\n        },\n        {\n          label: \"Hide \" + name,\n          accelerator: \"Command+H\",\n          role: \"hide\"\n        },\n        {\n          label: \"Hide Others\",\n          accelerator: \"Command+Alt+H\",\n          role: \"hideothers\"\n        },\n        {\n          label: \"Show All\",\n          role: \"unhide\"\n        },\n        {\n          type: \"separator\"\n        },\n        {\n          label: \"Quit\",\n          accelerator: \"Command+Q\",\n          click: main.quitApp\n        }\n      ]\n    });\n  }\n\n\n  return base;\n};\n\n/**\n * build the tray menu template for the app\n */\nexport const trayMenuTemplate = function() {\n  return [\n    {\n      label: \"Run Now\",\n      click: function() {\n        setTimeout(main.setStateToRunning, 1000);\n      }\n    },\n    {\n      label: \"Disable\",\n      click: function() {\n        main.setStateToPaused();\n        updateTrayIcon();\n        main.getTrayMenu().items[1].visible = false;\n        main.getTrayMenu().items[2].visible = true;\n        main.updateTrayMenu();\n      }\n    },\n    {\n      label: \"Enable\",\n      click: function() { \n        main.resetState();\n        updateTrayIcon();\n        main.getTrayMenu().items[1].visible = true;\n        main.getTrayMenu().items[2].visible = false;\n        main.updateTrayMenu();\n      },\n      visible: false\n    },\n    {\n      label: \"Update Available!\",\n      click: function() { \n        shell.openExternal(global.PACKAGE_DOWNLOAD_URL);\n      },\n      visible: (global.NEW_RELEASE_AVAILABLE === true)\n    },\n    {\n      label: \"Preferences\",\n      click: () => {\n        main.openPrefsWindow();\n      }\n    },\n    {\n      label: \"About \" + global.APP_NAME,\n      click: () => {\n        main.openAboutWindow();\n      }\n    },\n    {\n      label: \"Help\",\n      click: () => {\n        openHelpUrl();\n      }\n    },\n    {\n      label: \"Report a Bug\",\n      click: () => {\n        openIssuesUrl();\n      }\n    },\n    {\n      label: \"Quit\",\n      click: () => {\n        main.quitApp();\n      }\n    }\n  ];\n};\n\n\n/**\n * get icons for the current platform\n */\nexport const getIcons = function() {\n  const useDarkIcon = !nativeTheme.shouldUseDarkColorsForSystemIntegratedUI;\n  const modifier = useDarkIcon ? '-dark' : ''; \n  const icons = {\n    \"win32\" : {\n      active: path.join(main.getAssetsDir() , \"icon.ico\"),\n      paused: path.join(main.getAssetsDir(), \"icon-paused.ico\")\n    },\n    \"default\": {\n      active: path.join(main.getAssetsDir() , `iconTemplate${modifier}.png`),\n      paused: path.join(main.getAssetsDir() , `icon-pausedTemplate${modifier}.png`)\n    }\n  };\n  \n  if ( icons[process.platform] ) {\n    return icons[process.platform];\n  }\n\n  return icons.default;\n};\n\nexport const trayIconImage = function() {\n  var icons = getIcons();\n  let stateManager = main.getStateManager();\n\n  let iconPath;\n  if ( stateManager.currentState === stateManager.STATES.STATE_PAUSED ) {\n    iconPath = icons.paused;\n\n  }\n  else {\n    iconPath = icons.active;\n  }\n\n  main.log.info(`use icon ${iconPath}`);\n  return nativeImage.createFromPath(iconPath);\n};\n\n/**\n * update tray icon to match our current state\n */\nvar updateTrayIcon = function() {\n  let appIcon = main.getAppIcon();\n\n  const iconImage = trayIconImage();\n  if ( !iconImage.isEmpty() ) {\n    appIcon.setImage(iconImage);\n  }\n};\n"
  },
  {
    "path": "src/main/power.js",
    "content": "\n\"use strict\";\n\nimport { execFile } from \"child_process\";\n\nexport default class Power {\n  constructor(opts = {}) {\n    this.method = opts.method;\n    this.platform = opts.platform;\n    if ( this.platform === undefined ) {\n      this.platform = process.platform;\n    }\n\n    // https://stackoverflow.com/questions/651563/getting-the-last-element-of-a-split-string-array\n    this.commands = {\n      linux: {\n        cmd: \"dbus-send\",\n        opts: [\n          \"--print-reply\",\n          \"--system\",\n          \"--dest=org.freedesktop.UPower\",\n          \"/org/freedesktop/UPower\",\n          \"org.freedesktop.DBus.Properties.Get\",\n          \"string:org.freedesktop.UPower\",\n          \"string:OnBattery\" \n        ]\n      },\n    };\n    this.default = true;\n  }\n\n  async rawData() {\n    if ( this.commands[this.platform] ) {\n      const cmd = this.commands[this.platform].cmd;\n      const opts = this.commands[this.platform].opts;\n  \n      try {\n        return await this.query(cmd, opts);\n      }\n      catch {\n        return undefined;\n      }\n    }\n\n    return undefined;\n  }\n\n  async charging(raw = null) {\n    if ( this.method !== undefined) {\n      return !this.method();\n    }\n\n    if ( raw === null ) {\n      raw = await this.rawData();\n    }\n\n    if ( raw === undefined ) {\n      return this.default;\n    }\n\n    try {\n      // method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n      // variant       boolean false      \n      const result = raw.split(\"\\n\").find((line) => line.indexOf(\"variant\") !== -1);\n\n      // OnBattery == false means we're plugged in\n      return result.indexOf(\"false\") !== -1;\n    }\n    catch(e) {\n      console.log(e);\n      return this.default; \n    }\n  }\n \n  query(cmd, args) {\n    return new Promise((resolve) => {\n      execFile(cmd, args, (error, stdout, stderr) => {\n        if (error) {\n          console.warn(error);\n        }\n        resolve(stdout? stdout : stderr);\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "src/main/release_check.js",
    "content": "\"use strict\";\nexport default class ReleaseCheck {\n  constructor() {\n    this.onUpdateCallback = () => {};\n    this.onNoUpdateCallback = () => {};\n    this.logger = () => {};\n  }\n\n  setFeed(u) {\n    this.url = u;\n  }\n  setLogger(l) {\n    this.logger = l;\n  }\n\n  onUpdate(f) {\n    this.onUpdateCallback = f;\n  }\n  onNoUpdate(f) {\n    this.onNoUpdateCallback = f;\n  }\n\n  checkLatestRelease() {\n    this.logger(`check ${this.url} for new release`);\n    let _self = this;\n    fetch(this.url, {\n      timeout: 5000,\n      headers: {\n        \"User-Agent\": \"Before Dawn\"\n      }\n    }).then(function(response) {\n      if ( response.ok ) {\n        return response.json();\n      }\n      return undefined;\n    }).then(function(body) {\n      _self.logger(body);\n      \n      if ( body !== undefined ) {\n        _self.onUpdateCallback(body);\n      }\n      else {\n        _self.onNoUpdateCallback();\n      }\n    }).catch(() => {\n      this.onNoUpdateCallback();\n    });\n  }\n}"
  },
  {
    "path": "src/main/screen.js",
    "content": "\"use strict\";\n\nimport gotoSleep from \"@muffinista/goto-sleep\";\n\nexport const doLockScreen = gotoSleep.lockScreen;\nexport const doSleep = gotoSleep.gotoSleep;\n"
  },
  {
    "path": "src/main/state_manager.js",
    "content": "\"use strict\";\n\n/**\n * These are the possible states that the app can be in.\n */\nconst STATES = {\n  STATE_NONE: Symbol(\"none\"), // initial state\n  STATE_IDLE: Symbol(\"idle\"), // not running, waiting\n  STATE_LOADING: Symbol(\"loading\"), // the liminal state when a screesaver is loaded but not 100%\n  STATE_RUNNING: Symbol(\"running\"), // running a screensaver\n  STATE_BLANKED: Symbol(\"blanked\"), // long idle, screen is blanked\n  STATE_PAUSED: Symbol(\"paused\"), // screensaver is paused,\n  STATE_UNRUNNABLE: Symbol(\"unrunnable\")\n};\n\n// check for updates every 5 seconds when idle\nconst IDLE_CHECK_RATE = 5000;\n\n// check for updates every .25 second when active\nconst ACTIVE_CHECK_RATE = 250;\n\nconst IDLE_PADDING_CHECK = 1;\n\nclass StateManager {\n  constructor(fn) {\n    this.STATES = STATES;\n\n    this.currentState = STATES.STATE_NONE;\n\n    this._idleTime = () => {};\n    this._blankTime = () => {};\n    this._onIdleTime = () => {};\n    this._onBlankTime = () => {};\n    this._onReset = () => {};\n    this.logger = function() {};\n\n    this.lastTime = -1;\n    this.enteredStateTimestamp = -1;\n\n    this._ignoreReset = false;\n    this.keepTicking = true;  \n\n    this._idleFn = fn;\n\n\n    this.rates = {\n      idle: IDLE_CHECK_RATE,\n      active: ACTIVE_CHECK_RATE\n    };\n }\n\n  get currentTimeStamp() {\n    return process.hrtime()[0];\n  }\n\n  set idleFn(x) {\n    this._idleFn = x;\n  }\n\n  /**\n   * setup timing/callbacks\n   */\n  setup(opts) {\n    if ( opts.logger !== undefined ) {\n      this.logger = opts.logger;\n    }\n    else {\n      this.logger = function() {};\n    }\n\n    if ( opts.idleTime && opts.onIdleTime ) {\n      this._idleTime = opts.idleTime;\n      this._onIdleTime = opts.onIdleTime;\n    }\n\n    if ( opts.blankTime && opts.onBlankTime ) {\n      this._blankTime = opts.blankTime;\n      this._onBlankTime = opts.onBlankTime;\n    }\n\n    if ( opts.onReset ) {\n      this._onReset = opts.onReset;\n    }\n\n    if ( opts.state ) {\n      this.switchState(opts.state);\n    }\n    else {\n      this.switchState(STATES.STATE_IDLE);\n    }\n  }\n\n\n  /**\n   * reset to idle and clear any timers\n   */\n  reset() {\n    this.switchState(STATES.STATE_IDLE);\n    this.ignoreReset(false);\n  }\n\n  unrunnable() {\n    this.switchState(STATES.STATE_UNRUNNABLE);\n  }\n\n  /**\n   * pause the state machine\n   */\n  pause() {\n    this.switchState(STATES.STATE_PAUSED);\n  }\n\n  paused() {\n    return this.currentState === STATES.STATE_PAUSED;\n  }\n\n  /**\n   * start running the state machine\n   */\n  run() {\n    this.switchState(STATES.STATE_LOADING);\n  }\n\n  running() {\n    this.ignoreReset(false);\n    this.switchState(STATES.STATE_RUNNING);\n  }\n\n  /**\n   * handle calling the onIdleTime callback specified in setup\n   */\n  onIdleTime() {\n    this._onIdleTime();\n  }\n\n  /**\n   * handle calling the onBlankTime callback specified in setup\n   */\n  onBlankTime() {\n    this._onBlankTime();\n  }\n\n  onReset() {\n    this._onReset();\n  }\n\n  /**\n   * switch to a new state. if we're not already in that state, or if\n   * force == true, call onEnterState\n   */\n  switchState(s, force) {\n    // we run onEnterState if the state has changed or if we need to\n    // force a reload. we also run it if the new state is idle, this\n    // should help with some weird issues where timers aren't being\n    // reset properly\n    const callEnterState = ( this.currentState !== s || s === STATES.STATE_IDLE || force === true);\n\n    this.currentState = s;\n    this.enteredStateTimestamp = this.currentTimeStamp;\n\n    if ( callEnterState ) {\n      this.onEnterState(s);\n    }\n  }\n\n\n  /**\n   * enter a new state. set any timers/etc needed\n   */\n  onEnterState(s) {\n    switch (s) {\n      case STATES.STATE_IDLE:\n        this.onReset();\n        break;\n      case STATES.STATE_LOADING:\n        this.onIdleTime();\n        break;\n      case STATES.STATE_BLANKED:\n        this.onBlankTime();\n        break;\n      case STATES.STATE_PAUSED:\n        break;\n    }\n  }\n\n  getCurrentState() {\n    return this.currentState;\n  }\n\n  get currentStateString() {\n    return this.currentState.toString();\n  }\n\n  /**\n   * based on our current state, figure out the timestamp\n   * that we will enter the next state\n   */\n  getNextTime() {\n    if ( this.currentState === STATES.STATE_RUNNING ) {\n      return this._blankTime;\n    }\n    return this._idleTime;\n  }\n\n  ignoreReset(val) {\n    this.logger(`set ignoreReset to ${val}`);\n    this._ignoreReset = val;\n    if ( this._ignoreReset === false ) {\n      this.lastTime = -1;\n    } \n  }\n\n  /**\n   * check idle time and determine if we should switch states\n   */\n  tick(runAgain) {\n    if ( this.currentState !== STATES.STATE_NONE && this.currentState !== STATES.STATE_PAUSED ) {\n      const i = this._idleFn();\n      const nextTime = this.getNextTime();\n      const hadActivity = (i < this.lastTime ||\n        (this.currentState === STATES.STATE_RUNNING && i <= 10  && this.currentTimeStamp - i - IDLE_PADDING_CHECK > this.enteredStateTimestamp));\n\n      // this.logger(`${i} ${this.lastTime} -- ${this.currentStateString}`);\n\n      if ( this.currentState === STATES.STATE_PAUSED ) {\n        // do nothing\n      } else if ( hadActivity && this.currentState !== STATES.STATE_IDLE ) {\n        // we won't actually reset the state while a screensaver is\n        // loading, because sometimes we get zombie electron windows\n        // when we do that\n        if ( ! this._ignoreReset ) {\n          this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- ${this.currentStateString} -- reset`);\n          this.reset();\n        }\n        else {\n          this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- but ignoreReset is true`);\n        }\n      }\n      else if ( i >= nextTime && this.currentState !== STATES.STATE_BLANKED ) {\n        if ( this.currentState === STATES.STATE_IDLE) {\n          this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to loading`);\n          this.switchState(STATES.STATE_LOADING);\n        }\n        else if ( this.currentState === STATES.STATE_RUNNING) {\n          this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to blanked`);\n          this.switchState(STATES.STATE_BLANKED);\n        }\n        else {\n          // this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to ????`);\n        }\n      }\n\n      if ( this.currentState !== STATES.STATE_LOADING ) {\n        this.lastTime = i;\n      }\n    }\n\n    if ( runAgain !== false ) {\n      this.scheduleTick();\n    }\n  }\n\n  scheduleTick() {\n    if ( this.keepTicking ) {\n      let rate = this.rates.idle;\n      if ( this.currentState === STATES.STATE_RUNNING ) {\n        rate = this.rates.active;\n      }\n\n      setTimeout(() => {\n        this.tick(true);\n      }, rate);\n    }\n  }\n\n  setupLogging() {\n    this.logger(\"setupLogging\");\n    // clearInterval(this.loggingInterval);\n\n    // every minute or so, output the current state\n    this.loggingInterval = setInterval(() => {\n      this.logger(`Current idle: ${this._idleFn()} Last idle: ${this.lastTime} -- ${this.currentStateString}`);\n    }, 60000);\n  }\n\n  isTicking() {\n    return this.keepTicking === true;\n  }\n\n  startTicking() {\n    this.logger(\"startTicking\");\n    this.keepTicking = true;\n    this.setupLogging();\n    this.scheduleTick();\n  }\n  stopTicking() {\n    this.logger(\"stopTicking\");\n    this.keepTicking = false;\n    clearInterval(this.loggingInterval);\n  }\n}\n\nexport default StateManager;\n"
  },
  {
    "path": "src/main/system-savers/__template/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>My Awesome Screensaver</title>\n    <style>\n     body {\n       width: 100%;\n       background-color: black;\n       color: white;\n     }\n    </style>\n    <script>\n     // load any incoming URL parameters. you could just use the\n     // URLSearchParams object directly to manage these variables, but\n     // having them in a hash is a little easier sometimes.\n     var tmpParams = new URLSearchParams(document.location.search);\n     window.urlParams = {};\n\n     for(let k of tmpParams.keys() ) {\n       window.urlParams[k] = tmpParams.get(k);\n     }\n    </script>\n  </head>\n  <body>\n    <h1>Hello! I am a screensaver template!</h1>\n    <p>I am located in <code><script>document.write(decodeURIComponent(document.location.pathname));</script></code>.</p>\n    <p>Put the content of your screensaver here!</p>\n    <h2>Incoming Values</h2>\n    <p>(These parameters will be sent to your screensaver when it loads)</p> \n    <ul>\n      <script>\n       for(let k of Object.keys(window.urlParams) ) {\n         document.write(\"<li><code>window.urlParams[\" + k + \"]</code>: \" + window.urlParams[k] + \"</li>\");\n       }\n      </script>\n    </ul>\n\n\n    <h2>Helpful Snippets</h2>\n    Here's some code to load the incoming screenshot, so you can apply effects/etc to it:\n    <pre><code>\n      &lt;body&gt;\n        &lt;img id=\"screen\" /&gt;\n      &lt;/body&gt;\n      &lt;script&gt;\n       var img = document.getElementById(\"screen\");\n       var url = unescape(decodeURIComponent(window.urlParams.screenshot));\n       img.src = url;\n     \n       if ( typeof(window.urlParams.width) !== \"undefined\" ) {\n         img.width = window.urlParams.width;\n         img.height = window.urlParams.height;\n       }\n      &lt;/script&gt;\n    </code></pre>\n\n    It might make sense to hide any margins on elements in your screensaver with some CSS like this:\n    <pre><code>\n      &lt;style&gt;\n        * {\n        padding: 0;\n        margin: 0;\n        }\n      &lt;/style&gt;\n\n    </code></pre>\n\n    \n  </body>\n</html>\n"
  },
  {
    "path": "src/main/system-savers/__template/saver.json",
    "content": "{\n  \"name\": \"My Awesome Screensaver\",\n  \"description\": \"a description of my terrific screensaver\",\n  \"aboutUrl\": \"http://mywebsite.com/about\",\n  \"author\": \"my name/etc\",\n  \"source\": \"index.html\"\n}\n"
  },
  {
    "path": "src/main/system-savers/blank/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Blank</title>\n    <style>\n      * {\n      padding: 0;\n      margin: 0;\n      background-color: black;\n      }\n    </style>\n  </head>\n  <body>\n  </body>\n</html>\n"
  },
  {
    "path": "src/main/system-savers/blank/saver.json",
    "content": "{\n  \"name\": \"Blank screen\",\n  \"description\": \"Blank the screen\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"requirements\": []\n}\n"
  },
  {
    "path": "src/main/system-savers/dimmer/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Dimmer</title>\n    <script>\n     // this little javascript snippet will parse any incoming URL\n     // parameters and place them in the window.urlParams object\n     window.urlParams = window.location.search.split(/[?&]/).slice(1).map(function(paramPair) {\n       return paramPair.split(/=(.+)?/).slice(0, 2);\n     }).reduce(function (obj, pairArray) {            \n       obj[pairArray[0]] = pairArray[1];\n       return obj;\n     }, {});\n    </script>\n    <style>\n      * {\n      padding: 0;\n      margin: 0;\n      }\n     img {\n      filter: brightness(30%);\n      transition: 10s filter linear;\n     }\n    </style>\n  </head>\n  <body>\n    <img id=\"screen\" />\n  </body>\n  <script>\n   var img = document.getElementById(\"screen\");\n   var url = unescape(decodeURIComponent(window.urlParams.screenshot));\n   img.src = url;\n   \n   if ( typeof(window.urlParams.width) !== \"undefined\" ) {\n     img.width = window.urlParams.width;\n     img.height = window.urlParams.height;\n   }\n  </script>\n</html>\n"
  },
  {
    "path": "src/main/system-savers/dimmer/saver.json",
    "content": "{\n  \"name\": \"Dimmer\",\n  \"description\": \"Dim the screen a bit\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"requirements\": [\n    \"screen\"\n  ]\n}\n"
  },
  {
    "path": "src/main/system-savers/random/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Random Screensaver</title>\n    <style>\n     body {\n       width: 100%;\n       font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n     }\n    </style>\n    <script>\n     // load any incoming URL parameters. you could just use the\n     // URLSearchParams object directly to manage these variables, but\n     // having them in a hash is a little easier sometimes.\n     var tmpParams = new URLSearchParams(document.location.search);\n     window.urlParams = {};\n\n     for(let k of tmpParams.keys() ) {\n       window.urlParams[k] = tmpParams.get(k);\n     }\n    </script>\n  </head>\n  <body>\n    <h1>Hello!</h1>\n    <p>This screensaver will pick a random screensaver and run it for you. Enjoy!</p>   \n  </body>\n</html>\n"
  },
  {
    "path": "src/main/system-savers/random/saver.json",
    "content": "{\n  \"name\": \"Random\",\n  \"description\": \"Pick a random screensaver each time\",\n  \"author\": \"Colin Mitchell\",\n  \"preload\": \"random\",\n  \"source\": \"index.html\"\n}\n"
  },
  {
    "path": "src/main/windows.js",
    "content": "\"use strict\";\n\nimport * as main from \"./index.js\";\nimport { BrowserWindow } from \"electron\";\n\nvar getSaverWindows = function() {\n  return BrowserWindow.getAllWindows().filter((w) => {\n    return w.isSaver === true;\n  });\n};\n\n\n/**\n * check if the screensaver is still running\n */\nexport const screenSaverIsRunning = function() {\n  return ( getSaverWindows().length > 0 );\n};\n\n\n/**\n * check if the specified window exists and isn't destroyed\n */\nvar activeWindowHandle = function(w) {\n  return (typeof(w) !== \"undefined\" && ! w.isDestroyed());\n};\n\n/**\n * when the display count changes, close any running windows\n */\nexport const handleDisplayChange = function() {\n  // main.log.info(\"display change, let's close running screensavers\");\n  closeRunningScreensavers();\n};\n\n/**\n * close any running screensavers\n */\nexport const closeRunningScreensavers = function() {\n  main.log.info(\"closeRunningScreensavers\");\n  attemptToStopScreensavers();\n\n  // be really aggressive about closing lagging windows\n  setTimeout(forcefullyCloseScreensavers, 2500);\n  setTimeout(forcefullyCloseScreensavers, 5000);\n};\n\n/**\n * iterate through our list of running screensaver windows and attempt\n * to close them nicely\n */\nvar attemptToStopScreensavers = function() {\n  getSaverWindows().forEach((w) => {\n    if ( activeWindowHandle(w) ) {\n      w.close();\n    }    \n  });\n};\n\n/**\n * iterate through our list of running screensaver windows and close\n * them forcefully if needed\n */\nvar forcefullyCloseScreensavers = function() {\n  getSaverWindows().forEach((w) => {\n    if ( activeWindowHandle(w) ) {\n      w.destroy();\n    }\n  });\n};\n\n/**\n * forcefully close a screensaver window\n */\nexport const forceWindowClose = function(w) {\n  // 100% close/kill this window\n  if ( typeof(w) !== \"undefined\" ) {\n    try {\n      w.destroy();\n    }\n    catch (e) {\n      main.log.info(e);\n    }\n  }\n};\n\n/**\n * Set full screen mode for the given window. Use OSX's \n * fast/simple fullscreen mode if available.\n * @param {BrowserWindow} w the window to apply\n */\nexport const setFullScreen = function(w) {\n  if ( process.platform !== \"darwin\" ) {\n    w.setFullScreen(true);\n  }\n  else {\n    w.setSimpleFullScreen(true);\n  }\n  w.show();\n//  w.moveTop();\n};\n\n"
  },
  {
    "path": "src/renderer/AboutScreen.svelte",
    "content": "<div id=\"about\">\n  <div>\n    <h1>Before Dawn</h1>\n    <h2>// screensaver fun //</h2>\n    {#await loadData() then}\n    <h3>{globals.APP_VERSION}</h3>\n    {/await}\n    <p>\n      An open-source screensaver project.<br>\n      <a\n        href=\"http://muffinista.github.io/before-dawn/\"\n        onclick={open}\n      >\n        learn more\n      </a>\n    </p>\n  \n    <p>\n      Having trouble?\n      <a\n        href=\"http://github.com/muffinista/before-dawn/issues\"\n        onclick={open}>\n        please let us know!\n      </a>\n    </p>\n\n    <svg\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\" \n      xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n      x=\"0px\"\n      y=\"0px\"\n      viewBox=\"0 0 100 100\"\n      style=\"enable-background:new 0 0 100 100;\"\n      xml:space=\"preserve\"\n    >\n      <g>\n        <path\n          d=\"M92.5,66.2H77.8C78,65,78,63.8,78,62.7c0-15.3-12.5-27.8-27.8-27.8S22.4,47.5,22.4,62.7c0,1.2,0.1,2.3,0.2,3.5H7.5\n      c-0.6,0-1.2,0.5-1.2,1.2c0,0.6,0.5,1.2,1.2,1.2h85c0.6,0,1.2-0.5,1.2-1.2S93.2,66.2,92.5,66.2z M25,66.2c-0.2-1.2-0.2-2.4-0.2-3.5\n      c0-14.1,11.4-25.5,25.5-25.5c14,0,25.5,11.4,25.5,25.5c0,1.2-0.1,2.3-0.2,3.5H25z\"\n        />\n        <path d=\"M84.5,73H14.8c-0.6,0-1.2,0.5-1.2,1.2c0,0.6,0.5,1.2,1.2,1.2h69.7c0.6,0,1.2-0.5,1.2-1.2C85.6,73.5,85.1,73,84.5,73z\" />\n        <path d=\"M72.3,80H27.8c-0.6,0-1.2,0.5-1.2,1.2c0,0.6,0.5,1.2,1.2,1.2h44.5c0.6,0,1.2-0.5,1.2-1.2C73.4,80.5,72.9,80,72.3,80z\" />\n        <path d=\"M50,31.8c0.6,0,1.2-0.5,1.2-1.2V18.7c0-0.6-0.5-1.2-1.2-1.2s-1.2,0.5-1.2,1.2v11.9C48.9,31.3,49.4,31.8,50,31.8z\" />\n        <path d=\"M94.2,58.2H83.9c-0.6,0-1.2,0.5-1.2,1.2s0.5,1.2,1.2,1.2h10.3c0.6,0,1.2-0.5,1.2-1.2S94.8,58.2,94.2,58.2z\" />\n        <path d=\"M17.3,59.4c0-0.6-0.5-1.2-1.2-1.2H5.8c-0.6,0-1.2,0.5-1.2,1.2s0.5,1.2,1.2,1.2h10.3C16.8,60.5,17.3,60,17.3,59.4z\" />\n        <path\n          d=\"M73.5,41c0.3,0,0.6-0.1,0.8-0.3l8.4-8.4c0.5-0.5,0.5-1.2,0-1.6c-0.5-0.5-1.2-0.5-1.6,0l-8.4,8.4c-0.5,0.5-0.5,1.2,0,1.6\n      C72.9,40.9,73.2,41,73.5,41z\"\n        />\n        <path\n          d=\"M26.1,40.7c0.2,0.2,0.5,0.3,0.8,0.3s0.6-0.1,0.8-0.3c0.5-0.5,0.5-1.2,0-1.6l-8.4-8.4c-0.5-0.5-1.2-0.5-1.6,0\n      c-0.5,0.5-0.5,1.2,0,1.6L26.1,40.7z\"\n        />\n        <path\n          d=\"M36.8,33.3c0.2,0.4,0.6,0.6,1,0.6c0.2,0,0.4,0,0.5-0.1c0.6-0.3,0.8-1,0.5-1.6L35.1,25c-0.3-0.6-1-0.8-1.6-0.5\n      s-0.8,1-0.5,1.6L36.8,33.3z\"\n        />\n        <path\n          d=\"M62,33.8c0.2,0.1,0.3,0.1,0.5,0.1c0.4,0,0.8-0.2,1-0.6l3.7-7.2c0.3-0.6,0.1-1.3-0.5-1.6s-1.3-0.1-1.6,0.5l-3.7,7.2\n      C61.2,32.8,61.4,33.5,62,33.8z\"\n        />\n        <path\n          d=\"M79.7,49.8c0.2,0.5,0.6,0.8,1.1,0.8c0.1,0,0.3,0,0.4-0.1l7.7-2.8c0.6-0.2,0.9-0.9,0.7-1.5c-0.2-0.6-0.9-0.9-1.5-0.7\n      l-7.7,2.8C79.8,48.6,79.4,49.2,79.7,49.8z\"\n        />\n        <path\n          d=\"M11.6,47.8l7.7,2.8c0.1,0,0.3,0.1,0.4,0.1c0.5,0,0.9-0.3,1.1-0.8c0.2-0.6-0.1-1.3-0.7-1.5l-7.7-2.8\n      c-0.6-0.2-1.3,0.1-1.5,0.7C10.7,46.9,11,47.6,11.6,47.8z\"\n        />\n      </g>\n    </svg>\n\n    <hr>\n\n    <p>\n      <small>\n        <a\n          href=\"https://thenounproject.com/search/?q=sunrise&i=172009\"\n          onclick={open}>\n          App\n          icon\n        </a> Sun by Ale Estrada from the Noun Project\n      </small>\n    </p>\n  \n    <p>\n      <small>\n        Powered by <a\n          href=\"https://www.electronjs.org/\"\n          onclick={open}\n        >\n          Electron\n        </a>\n      </small>\n    </p>\n  </div>\n</div>\n\n<script>\nconsole.log = window.api.log;\nwindow.addEventListener(\"error\", console.log);\nwindow.addEventListener(\"unhandledrejection\", console.log);\n\nlet globals = $state({});\n\nasync function loadData() {\n  globals = await window.api.getGlobals();\n}\n\nfunction open(evt) {\n  evt.preventDefault();\n  window.api.openUrl(evt.target.href);\n}\n</script>\n"
  },
  {
    "path": "src/renderer/EditorScreen.svelte",
    "content": "<!-- #editor -->\n<script>\n  import { onMount, onDestroy } from \"svelte\";\n\n  import Notarize from \"@/components/Notarize\";\n  import SaverForm from \"@/components/SaverForm.svelte\";\n  import SaverOptions from \"@/components/SaverOptions.svelte\";\n  import SaverOptionInput from \"@/components/SaverOptionInput.svelte\";\n\n  import ReloadIcon from \"@/components/icons/ReloadIcon.svelte\";\n  import FolderIcon from \"@/components/icons/FolderIcon.svelte\";\n  import SaveIcon from \"@/components/icons/SaveIcon.svelte\";\n  import BugIcon from \"@/components/icons/BugIcon.svelte\";\n\n  console.log = window.api.log;\n  window.addEventListener(\"error\", console.log);\n  window.addEventListener(\"unhandledrejection\", console.log);\n\n\n  let size = undefined;\n  let saver = $state({options: [], requirements: []});\n  let optionValues = $state({});\n  let disabled = $state(false);\n  let lastIndex = 0;\n  let previewUrl = $state(undefined);\n\n  let validOptions = $derived(saver?.options?.filter((o) => o.name !== \"\"));\n  let params = $derived(new URLSearchParams(document.location.search));\n  let src = $derived(params.get(\"src\"));\n  let screenshot = $derived(params.get(\"screenshot\"));\n\n  onMount(async () => {\n    size = await window.api.getDisplayBounds();\n    saver = await window.api.loadSaver(src);\n\n    if (saver.settings === undefined) {\n      saver.settings = {};\n    }\n    if (saver.options === undefined) {\n      saver.options = [];\n    }\n\n    lastIndex = saver.options.length;\n\n    window.api.onFolderUpdate(() => {\n      updatePreview();\n    });\n    window.api.watchFolder(src);\n    addEventListener(\"resize\", updatePreview);\n\n    // wait a tick so that all our required elements actually exist\n    setTimeout(() => {\n      updatePreview();\n    }, 0);\n  });\n\n  onDestroy(() => {\n    window.api.unwatchFolder(src);\n  });\n\n  function addSaverOption() {\n    saver.options.push({\n      index: lastIndex + 1,\n      name: `New Option ${lastIndex + 1}`,\n      type: \"slider\",\n      description: \"\",\n      min: \"1\",\n      max: \"100\",\n      default: \"75\",\n    });\n\n    lastIndex += 1;\n  }\n\n  function optionDefaults() {\n    var result = {};\n    for (var i = 0; i < saver.options.length; i++) {\n      var opt = saver.options[i];\n      result[opt.name] = opt.default;\n    }\n\n    return result;\n  }\n\n  function urlOpts(s) {\n    var base = {\n      width: size.width,\n      height: size.height,\n      preview: 1,\n      platform: window.api.platform(),\n      screenshot: screenshot,\n    };\n\n    if (typeof s === \"undefined\") {\n      s = saver;\n    }\n\n    const mergedOpts = Object.assign(\n      base,\n      optionValues,\n      optionDefaults(),\n      saver.settings\n    );\n\n    return mergedOpts;\n  }\n\n  function resizePreview() {\n    const docStyle = getComputedStyle(document.documentElement);\n    const sidebarWidth = Number(docStyle.getPropertyValue(\"--sidebar-width\").replace(\"px\", \"\"));\n\n    const maxWidth = window.innerWidth - sidebarWidth - 50;\n    // const maxHeight = wrapper.clientHeight;\n\n    document.documentElement.style.setProperty(\n      \"--preview-width\",\n      `${size.width}px`\n    );\n    document.documentElement.style.setProperty(\n      \"--preview-height\",\n      `${size.height}px`\n    );\n    const scale = maxWidth / size.width;\n\n    document.documentElement.style.setProperty(\n      \"--preview-wrapper-width\",\n      `${size.width * scale}px`\n    );\n    document.documentElement.style.setProperty(\n      \"--preview-wrapper-height\",\n      `${size.height * scale}px`\n    );\n\n    document.documentElement.style.setProperty(\"--preview-scale\", `${scale}`);\n  }\n\n  function updatePreview() {\n    resizePreview();\n\n    const urlParams = new URLSearchParams(urlOpts(saver));\n    previewUrl = `${saver.url}?${urlParams.toString()}`;\n\n    const el = document.getElementById(\"saver-preview\");\n    if (el) {\n      // force a reload (just binding it doesnt work)\n      el.src = previewUrl;\n    }\n  }\n\n  function openFolder() {\n    window.api.openFolder(src);\n  }\n\n  function openConsole() {\n    window.api.toggleDevTools();\n  }\n\n  function onOptionsChange() {\n    updatePreview();\n  }\n\n  function onDeleteOption(deletedOption) {\n    saver.options = saver.options.filter(o => o.index !== deletedOption.index);\n  }\n\n  async function saveData() {\n    if (document.querySelectorAll(\":invalid\").length > 0) {\n      document.querySelectorAll(\"form:invalid\").forEach((el) => el.classList.add(\"submit-attempt\"));\n      return;\n    }\n\n    disabled = true;\n    const clone = JSON.parse(JSON.stringify(saver));\n    await window.api.saveScreensaver(clone, src);\n    window.api.saversUpdated(src);\n\n    new Notarize({ timeout: 1000 }).show(\"Changes saved!\");\n\n    document.querySelectorAll(\"form:invalid\").forEach((el) => el.classList.remove(\"submit-attempt\"));\n\n    disabled = false;\n  }\n</script>\n\n<style>\n  :root {\n    --sidebar-width: 500px;\n    --navbar-height: 25px;\n  }\n\n  #editor {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    padding-left: 7px;\n    padding-top: 0px;\n    padding-bottom: 0px;\n    padding-right: 0px;\n  }\n  nav {\n    position: fixed;\n    top: 0px;\n    height: var(--navbar-height);\n    z-index: 1000;\n    background-color: white;\n    width: 100%;\n  }\n  main {\n    position: absolute;\n    top: var(--navbar-height);\n    left: 7;\n    width: 99%;\n    display: grid;\n    grid-template-columns: 1fr var(--sidebar-width);\n    grid-area: main;\n    height: calc(100vh - var(--navbar-height));\n  }\n\n  .saver-detail {\n    width: var(--preview-wrapper-width);\n    height: var(--preview-wrapper-height);\n  }\n\n  #preview {\n    max-width: calc((100vw - var(--sidebar-width)) - 10);\n    overflow-y: scroll;\n  }\n  #sidebar {\n    width: var(--sidebar-width);\n    overflow-y: scroll;\n  }\n  #sidebar > * {\n    padding-left: 7px;\n    width: 95%;\n  }\n\n  section {\n    font-size: 90%;\n  }\n  section > h2 {\n    margin-top: 0px;\n    font-size: 140%;\n  }\n\n  .saver-option-input {\n    margin-bottom: 25px;\n  }\n</style>\n\n<div id=\"editor\">\n  <nav>\n    <button variant=\"default\" title=\"Open screensaver folder\" onclick={openFolder}>\n      <FolderIcon />\n    </button>\n    <button variant=\"default\" title=\"Save changes\" disabled=\"{disabled}\" class=\"save\" onclick={saveData}>\n      <SaveIcon />\n    </button>\n    <button variant=\"default\" title=\"Reload preview\" onclick={updatePreview}>\n      <ReloadIcon />\n    </button>\n    <button variant=\"default\" title=\"View Developer Console\" onclick={openConsole}>\n      <BugIcon />\n    </button>\n  </nav>\n  <main>\n    <div id=\"preview\">\n      <div class=\"saver-detail\">\n        <iframe\n          id=\"saver-preview\"\n          title=\"preview\"\n          src={previewUrl}\n          scrolling=\"no\"\n          class=\"saver-preview\"\n        ></iframe>\n      </div>\n\n      {#if validOptions && validOptions.length > 0}\n        <h3>Preview settings</h3>\n        <small>\n          Tweak the values here and they will be sent along to your preview.\n        </small>\n        <SaverOptions bind:saver on:optionsChanged={onOptionsChange} />\n      {/if}\n    </div>\n    <div id=\"sidebar\">\n      <section id=\"description\">\n        <h2>Details</h2>\n        <small> You can enter the basics about this screensaver here.</small>\n        <SaverForm bind:saver></SaverForm>\n      </section>\n      <hr />\n      <section id=\"options\">\n        <h2>Custom Options</h2>\n        <small>\n          You can offer users configurable options to control your screensaver.\n          Add and remove those here.\n        </small>\n\n        <!-- eslint-disable no-unused-vars -->\n        {#each saver.options as option, index (option.name)}\n          <div class=\"saver-option-input\" data-index=\"{index}\">\n            <SaverOptionInput\n              bind:option={saver.options[index]}\n              on:optionsChanged={updatePreview}\n              index={index}\n            />\n            <div class=\"form-actions\">          \n              <button\n              type=\"button\"\n              class=\"btn btn-danger remove-option\"\n              onclick={() => { onDeleteOption(option) }}\n            >\n              Remove this Option\n              </button>\n            </div>\n\n          </div>\n        {/each}\n\n        <div class=\"padded-top padded-bottom\">\n          <button\n            type=\"button\"\n            class=\"btn add-option\"\n            onclick={addSaverOption}\n          >\n            Add Option\n          </button>\n        </div>\n      </section>\n    </div>\n  </main>\n</div>\n"
  },
  {
    "path": "src/renderer/NewScreensaverScreen.svelte",
    "content": "<div id=\"new\">\n  <div class=\"content\">\n    <div>\n      <h1>New Screensaver</h1>\n      {#if canAdd}\n        <p>\n          Screensavers in Before Dawn are web pages, so if you can use HTML, \n          CSS, and/or Javascript, you can make your own screensaver.\n        </p>\n\n        <p>\n          Use this form to create a new screensaver. A template will be\n          added to the system that you can fill in with your code.\n        </p>\n        <SaverForm bind:saver></SaverForm>\n      {:else}\n      <div class=\"need-setup-message\">\n        <p>\n          Screensavers in Before Dawn are web pages, so if you can use HTML, \n          CSS, and/or Javascript, you can make your own screensaver. But before \n          you can do that, you'll need to set a local directory!\n        </p>\n\n        <form>\n          <div class=\"form-group full-width\">\n            <label for=\"localSource\">Local Source:</label>\n            <FolderChooser bind:source=\"{prefs.localSource}\" on:picked=\"{updatePrefs}\" />\n            <small class=\"form-text text-muted\">\n              We will load screensavers from any directories listed here. Use this to add your own screensavers!\n            </small>\n          </div>\n        </form>\n      </div>\n      {/if}\n    </div>\n  </div>\n  <footer class=\"footer\">\n    <div>\n      <button\n        class=\"btn cancel\"\n        onclick={closeWindow}\n      >\n        Cancel\n      </button>\n      <button\n        class=\"btn save\"\n        disabled=\"{disabled || !canAdd}\"\n        onclick={saveData}\n      >\n        Save\n      </button>\n    </div>\n  </footer>\n</div> <!-- #new -->\n\n<script>\nimport SaverForm from \"./components/SaverForm.svelte\";\nimport FolderChooser from \"@/components/FolderChooser.svelte\";\nimport { onMount } from \"svelte\";\n\nconsole.log = window.api.log;\nwindow.addEventListener(\"error\", console.log);\nwindow.addEventListener(\"unhandledrejection\", console.log);\n\n\nlet prefs = $state({});\nlet screenshot = undefined;\nlet saver = $state({\n  requirements: [\"screen\"]\n});\nlet disabled = $state(false);\n\nlet canAdd = $derived(prefs !== undefined && prefs.localSource !== undefined && prefs.localSource !== \"\");\n\nonMount(async () => {\n  prefs = await window.api.getPrefs();\n  screenshot = await window.api.getScreenshot();\n});\n\nfunction closeWindow() {\n  window.api.closeWindow(\"addNew\");\n}\n\nasync function updatePrefs() {\n  const clone = JSON.parse(JSON.stringify(prefs));   \n  return await window.api.updatePrefs(clone);\n}\n\nasync function saveData() {\n  if ( document.querySelectorAll(\":invalid\").length > 0 ) {\n    var form = document.querySelector(\"form\");\n    form.classList.add(\"submit-attempt\");\n\n    return;\n  }\n\n  disabled = true;\n  // https://forum.vuejs.org/t/how-to-clone-property-value-as-simple-object/40032/2\n  const clone = JSON.parse(JSON.stringify(saver));\n  const data = await window.api.createScreensaver(clone);\n\n  window.api.saversUpdated();\n\n  window.api.openWindow(\"editor\", {\n    src: data.dest,\n    screenshot\n  });\n  window.api.closeWindow(\"addNew\");\n}\n</script>\n"
  },
  {
    "path": "src/renderer/PrefsScreen.svelte",
    "content": "<div id=\"prefs\">\n  <div class=\"saver-detail\">\n    <iframe\n      title=\"preview\"\n      src=\"{previewUrl}\"\n      scrolling=\"no\"\n      class=\"saver-preview\"\n    ></iframe>\n  </div>\n  <div class=\"saver-info space-at-top\">\n      <SaverSummary saver={saverObj} on:editScreensaver={editSaver} on:deleteScreensaver=\"{deleteSaver}\" />\n      {#if saverObj !== undefined && saverObj.options}\n        <SaverOptions bind:saver=\"{saverObj}\" on:optionsChanged=\"{updatePreview}\" />\n      {/if}\n  </div>\n\n  <SaverList bind:savers={savers} bind:current={saver} saverPicked={saverPicked} />\n\n  <div class=\"basic-prefs space-at-top\">\n    <h1>Settings</h1>\n    <form class=\"grid\">\n      <div>\n        <div class=\"form-group\">\n          <label for=\"delay\">Activate after:</label>\n          <select\n            bind:value=\"{prefs.delay}\"\n            onchange=\"{({ target }) => { target.value = Number(target.value); }}\"\n            name=\"delay\"\n          >\n            <option value=\"{Number(0)}\">\n              never\n            </option>\n            <option value=\"{Number(1)}\">\n              1 minute\n            </option>\n            <option value=\"{Number(5)}\">\n              5 minutes\n            </option>\n            <option value=\"{Number(10)}\">\n              10 minutes\n            </option>\n            <option value=\"{Number(15)}\">\n              15 minutes\n            </option>\n            <option value=\"{Number(30)}\">\n              30 minutes\n            </option>\n            <option value=\"{Number(60)}\">\n              1 hour\n            </option>\n          </select>\n          <small class=\"form-text text-muted block\">\n            The screensaver will activate once your computer has been idle for this amount of time.\n          </small>\n        </div>\n        \n        <div class=\"form-group\">\n          <label for=\"sleep\">Sleep after:</label>\n          <select\n           bind:value=\"{prefs.sleep}\"\n           onchange=\"{({ target }) => { target.value = Number(target.value); }}\"\n           name=\"sleep\"\n          >\n            <option value=\"{Number(0)}\">\n              never\n            </option>\n            <option value=\"{Number(1)}\">\n              1 minute\n            </option>\n            <option value=\"{Number(5)}\">\n              5 minutes\n            </option>\n            <option value=\"{Number(10)}\">\n              10 minutes\n            </option>\n            <option value=\"{Number(15)}\">\n              15 minutes\n            </option>\n            <option value=\"{Number(30)}\">\n              30 minutes\n            </option>\n            <option value=\"{Number(60)}\">\n              1 hour\n            </option>\n          </select>\n          <small class=\"form-text text-muted block\">\n            The screensaver will stop, and the displays will\n            be turned off to save energy after this amount\n            of time.\n          </small>\n        </div>\n      </div>\n    </form>\n  </div>\n\n  <footer class=\"footer\">\n    <div>\n      <button\n        class=\"align-middle btn create\"\n        onclick=\"{createNewScreensaver}\"\n      >\n        Create Screensaver\n      </button>\n    </div>\n\n    <div>\n      <button\n        class=\"btn settings\"\n        onclick=\"{openSettings}\"\n      >\n        Advanced Settings\n      </button>\n      <button\n        class=\"btn save\"\n        disabled=\"{disabled}\"\n        onclick=\"{saveDataClick}\"\n      >\n        Save\n      </button>\n    </div>\n  </footer>\n</div> <!-- #prefs -->\n\n\n<script>\n  import Notarize from \"@/components/Notarize\";\n  import SaverList from \"@/components/SaverList.svelte\";\n  import SaverOptions from \"@/components/SaverOptions.svelte\";\n  import SaverSummary from \"@/components/SaverSummary.svelte\";\n\timport { onMount, onDestroy, tick } from \"svelte\";\n\n\n\tlet savers = $state([]);\n  let prefs = $state({});\n  let saver = $state(undefined);\n  let size = undefined;\n  let screenshot = undefined;\n  let previewUrl = $state(undefined);\n  let disabled = $state(false);\n\n  let saverIndex = $derived.by(() => {\n    return findIndexOf(saver);\n  });\n  let saverObj = $state(undefined);\n\n\n\tonMount(async () => {\n    console.log = window.api.log;\n    window.addEventListener(\"error\", console.log);\n    window.addEventListener(\"unhandledrejection\", console.log);\n\n    size = await window.api.getDisplayBounds();\n    screenshot = await window.api.getScreenshot();\n\n    await getData();\n\n    resizePreview();\n    setPreviewUrl();\n\n    window.api.addListener(\"savers-updated\", onSaversUpdated);\n\n    const globals = await window.api.getGlobals();\n    if ( globals.NEW_RELEASE_AVAILABLE ) {\n      await tick();\n      // window.api.displayUpdateDialog();\n    }\n  });\n\n  onDestroy(() => {\n   window.api.removeListener(\"savers-updated\", onSaversUpdated);\n  });\n\n  async function getData() {\n    savers = await window.api.listSavers();\n    prefs = await window.api.getPrefs();\n    saver = prefs.saver;\n\n    if ( savers.length <= 0 ) {\n      return;\n    }\n\n    // ensure default settings in the config for all savers\n    savers = savers.map((s) => {\n      if ( s.settings === undefined ) {\n        s.settings = {};\n      }\n      return s;\n    });\n\n    savers = savers;\n\n    // saverIndex won't resolve yet so find via function instead\n    const tmpIndex = findIndexOf(saver);\n    saverObj = savers[tmpIndex];\n\n    if ( tmpIndex == -1 ) {\n      saverObj = savers[0];\n    }\n  }\n\n  async function onSaversUpdated() {\n    getData();\n  }\n\n  function findIndexOf(saver) {\n    if ( saver === undefined ) {\n      return 0;\n    }\n    let lookup = saver;\n    if ( saver.key !== undefined ) {\n      lookup = saver.key; \n    }\n\n    return savers.findIndex((s) => s.key === lookup);\n  }\n\n  function urlOpts(s, opts) {\n    var base = {\n      width: size.width,\n      height: size.height,\n      preview: 1,\n      platform: window.api.platform(),\n      screenshot: screenshot\n    };\n\n    if ( typeof(s) === \"undefined\" ) {\n      s = saver;\n    }\n    if (!opts) {\n      opts = s.settings;\n    }\n\n    var mergedOpts = Object.assign(base, opts);\n\n\n    return mergedOpts;\n  }\n\n  function setPreviewUrl(opts) {\n    if (saverObj === undefined) {\n      return;\n    }\n\n    const urlParams = new URLSearchParams(urlOpts(saverObj, opts));\n    previewUrl = `${saverObj.url}?${urlParams.toString()}`;\n  }\n\n\n  function updatePreview(event) {\n    setPreviewUrl(event.detail.options);\n  }\n\n  function saverPicked() {\n    saverObj = savers[saverIndex];\n    setPreviewUrl();\n  }\n\n  function createNewScreensaver() {\n    window.api.openWindow(\"add-new\", {\n      screenshot\n    });\n  }\n\n  async function saveData() {\n    // copy in some data (not sure we really need this)\n    if (saverIndex !== -1 ) {\n      prefs.saver = saver;\n      prefs.options[saver] = savers[saverIndex].settings;\n    }\n\n    const clone = JSON.parse(JSON.stringify(prefs));   \n    return await window.api.updatePrefs(clone);\n  }\n\n  async function saveDataClick() {\n    let output;\n\n    disabled = true;\n    try {\n      await saveData();\n      output = \"Changes saved!\";\n    }\n    catch(e) {\n      output = \"Something went wrong!\";\n      console.log(e);\n    }\n\n    disabled = false;\n    new Notarize({timeout: 1000}).show(output);\n  }\n\n  function resizePreview() {\n    document.documentElement.style\n      .setProperty(\"--preview-width\", `${size.width}px`);\n    document.documentElement.style\n      .setProperty(\"--preview-height\", `${size.height}px`);\n    const scale = 500 / (size.width + 40);\n\n    document.documentElement.style\n      .setProperty(\"--preview-scale\", `${scale}`);\n  }\n\n  function openSettings() {\n    window.api.openWindow(\"settings\");\n  }\n\n  function editSaver() {\n    var opts = {\n      src: saverObj.src,\n      screenshot: screenshot\n    };\n    window.api.openWindow(\"editor\", opts);\n  }\n\n  async function deleteSaver() {\n    const index = savers.indexOf(saverObj);\n    const newIndex = Math.max(index-1, 0);\n    const saverToDelete = savers[index];\n\n    saver = savers[newIndex].key;\n\n    savers = savers.filter(s => s !== saverToDelete);\n\n    await window.api.deleteSaver(saverToDelete);\n    await getData();\n\n    saver = prefs.saver;\n    setPreviewUrl();\n  }\n</script>\n\n<style>\n</style>\n"
  },
  {
    "path": "src/renderer/SettingsScreen.svelte",
    "content": "<div id=\"settings\">\n  <div id=\"prefs-form\">\n    <h1>Settings</h1>\n    <form class=\"grid\">\n      <div class=\"options\">\n        <div class=\"form-check\">\n          <label>\n            <input\n              id=\"lock\"\n              bind:checked=\"{prefs.lock}\"\n              type=\"checkbox\"\n              class=\"form-check-input\"\n            >\n            Lock screen after running?\n          </label>\n          <small class=\"form-text text-muted\">\n            When the screen saver turns off, the user will need to enter their password.\n          </small>\n        </div>\n\n        <div class=\"form-check\">\n          <label>\n            <input\n              id=\"disableOnBattery\"\n              bind:checked=\"{prefs.disableOnBattery}\"\n              type=\"checkbox\"\n              class=\"form-check-input\"\n            >\n            Disable when on battery?\n          </label>\n          <small class=\"form-text text-muted\">\n            If checked, Before Dawn won't\n            activate when you're not plugged in -- your\n            computer's power settings can blank the screen\n            instead.\n          </small>\n        </div>\n        <!-- ' -->\n        <div class=\"form-check\">\n          <label>\n            <input\n              bind:checked=\"{prefs.auto_start}\"\n              type=\"checkbox\"\n              class=\"form-check-input\"\n            >\n            Auto start on login?\n          </label>\n          <small class=\"form-text text-muted\">\n            If checked, Before Dawn will start when your computer starts.\n          </small>\n        </div>\n\n        <div class=\"form-check\">\n          <label>\n            <input\n              id=\"primary-display\"\n              bind:checked=\"{prefs.runOnSingleDisplay}\"\n              type=\"checkbox\"\n              class=\"form-check-input\"\n            >\n            Only run on the primary display?\n          </label>\n          <small class=\"form-text text-muted\">\n            If you have multiple displays, only run on the primary one.\n          </small>\n        </div>\n      </div>\n    </form>\n  </div> \n  <div>\n    {#if hasScreensaverUpdate === true}\n      <button\n        class=\"btn reset-to-defaults\"\n        onclick={downloadScreensaverUpdates}\n      >\n        Download screensaver updates\n        {#if downloadingUpdates}\n          <Spinner />\n        {/if}\n      </button>\n    {/if}\n  </div>\n  <div id=\"advanced-prefs-form\">\n    <h1>Advanced Options</h1>\n    <p class=\"form-text text-muted\">\n      Be careful with these!\n    </p>\n    <form>\n      <div class=\"form-group full-width\">\n        <label for=\"localSource\">Local Source:</label>\n        <FolderChooser bind:source=\"{prefs.localSource}\" />\n        <small class=\"form-text text-muted\">\n          We will load screensavers from any directories listed here. Use this to add your own screensavers!\n        </small>\n      </div>\n\n      <div class=\"form-group\">\n        <label for=\"hotkey\">Global hotkey:</label>\n        <input\n          bind:value=\"{prefs.launchShortcut}\"\n          type=\"text\"\n          name=\"hotkey\"\n          readonly=\"readonly\"\n          class=\"form-control form-control-sm\"\n          onkeydown={updateHotkey}\n        >\n        <small class=\"form-text text-muted\">\n          Enter a key combination that will be used to run a screensaver.\n        </small>\n      </div>\n    </form>\n  </div>\n\n  <footer class=\"footer\">\n    <div>\n      <button\n        class=\"btn reset-to-defaults\"\n        onclick={resetToDefaults}\n      >\n      Reset to Defaults\n      </button>\n    </div>\n    <div>\n      <button\n        class=\"btn close-window\"\n        disabled=\"{disabled}\"\n        onclick={closeWindow}\n      >\n      Cancel\n      </button>\n      <button\n        class=\"btn save\"\n        disabled=\"{disabled}\"\n        onclick={saveDataClick}\n      >\n      Save\n      </button>\n    </div>\n  </footer>\n</div> <!-- #settings -->\n\n<script>\nimport { onMount } from \"svelte\";\nimport Spinner from \"@/components/Spinner.svelte\";\nimport Notarize from \"@/components/Notarize\";\nimport FolderChooser from \"@/components/FolderChooser.svelte\";\n\nconsole.log = window.api.log;\nwindow.addEventListener(\"error\", console.log);\nwindow.addEventListener(\"unhandledrejection\", console.log);\n\nlet prefs = $state({});\nlet release = undefined;\nlet disabled = $state(false);\nlet hasScreensaverUpdate = $state(false);\nlet downloadingUpdates = $state(false);\n\nonMount(async () => {\n  prefs = await window.api.getPrefs();\n  release = await window.api.getScreensaverPackage();\n\n  hasScreensaverUpdate = typeof(release) !== \"undefined\" && release.is_update === true;\n});\n\nasync function downloadScreensaverUpdates() {\n  if ( downloadingUpdates === true ) {\n    return;\n  }\n\n  try {\n    downloadingUpdates = true;\n    await window.api.downloadScreensaverPackage();\n    new Notarize({timeout: 1000}).show(\"Screensavers updated!\");\n  }\n  finally {\n    downloadingUpdates = false;\n  }\n}\n\nasync function resetToDefaults() {\n  const result = await window.api.resetToDefaultsDialog();\n  if ( result === 1 ) {\n    prefs = await window.api.getDefaults();        \n    await handleSave(\"Settings reset\");\n  }\n}\n\nfunction updateHotkey(event) {\n  if ( event.key == \"Control\" || event.key == \"Shift\" || event.key == \"Alt\" || event.key == \"Meta\" ) {\n    return;\n  }\n\n  let output = [];\n  if ( event.ctrlKey ) {\n    output.push(\"Control\");\n  }\n  if ( event.altKey ) {\n    output.push(\"Alt\");\n  }\n  if ( event.metaKey) {\n    output.push(\"Command\");\n  }\n  if ( event.shiftKey ) {\n    output.push(\"Shift\");\n  }\n\n  if ( output.length === 0 ) {\n    if ( event.key == \"Backspace\" ) {\n      event.target.value = \"\";\n    }\n\n    return;\n  }\n\n  output.push(`${event.key}`.toUpperCase());\n  output = output.join(\"+\");\n\n  event.target.value = output;\n}\n\nfunction closeWindow() {\n  window.api.closeWindow(\"settings\");\n}\n\nasync function saveDataClick() {\n  handleSave(\"Changes saved!\");\n  closeWindow();\n}\n\nasync function handleSave(output) {\n  disabled = true;\n\n  try {\n    // https://forum.vuejs.org/t/how-to-clone-property-value-as-simple-object/40032/2\n    const clone = JSON.parse(JSON.stringify(prefs));\n    await window.api.updatePrefs(clone);\n    await window.api.saversUpdated();\n\n    window.api.setAutostart(prefs.auto_start);\n    window.api.setGlobalLaunchShortcut(prefs.launchShortcut);\n  }\n  catch {\n    output = \"Something went wrong!\";\n  }\n\n  disabled = false;\n  new Notarize({timeout: 1000}).show(output);\n}\n</script>\n\n<style>\n</style>\n"
  },
  {
    "path": "src/renderer/components/FolderChooser.svelte",
    "content": "<div class=\"input-group\">\n  <input\n    type=\"text\"\n    readonly=\"readonly\"\n    bind:value=\"{source}\"\n  >\n  <button\n    type=\"button\"\n    class=\"pick\"\n    onclick={showPathChooser}\n  >\n    ...\n  </button>\n  <button\n    type=\"button\"\n    class=\"clear\"\n    onclick={clearLocalSource}\n  >\n    X\n  </button>\n</div>\n\n<script>\nimport { createEventDispatcher } from \"svelte\";\n\nconst dispatch = createEventDispatcher();\n  let { source = $bindable() } = $props();\n\nasync function showPathChooser() {\n  const result = await window.api.showOpenDialog();\n  handlePathChoice(result);\n}\n\nfunction handlePathChoice(result) {\n  if ( result === undefined || result.canceled ) {\n    return;\n  }\n\n  const choice = result.filePaths[0];\n  source = choice;\n\n  dispatch(\"picked\", {\n    folder: source\n  });\n}\n\nfunction clearLocalSource() {\n  source = \"\";\n}\n</script>\n"
  },
  {
    "path": "src/renderer/components/Notarize.js",
    "content": "\nconst NOTARIZE_DEFAULTS = {\n  wrapperClass: \"notarize-wrapper\",\n  interiorClass: \"notarize\",\n  timeout: 150000,\n  transitionIn: \"notarize-in\",\n  transitionOut: \"notarize-out\",\n  template: (args) => { return `<div class='${args.wrapperClass}'><div class='${args.interiorClass}'>${args.contents}</div></div>`; }\n};\n\nexport default class Notarize {\n  constructor(options={}) {\n    this.options = Object.assign({}, NOTARIZE_DEFAULTS, options);\n    return this;\n  }\n\n  show(contents) {\n    const body = document.querySelector(\"body\");\n    const guts = this.options.template({\n      wrapperClass: [this.options.wrapperClass, this.options.transitionIn].join(\" \"),\n      interiorClass: this.options.interiorClass,\n      contents: contents\n    });\n    const el = this.toDom(guts);\n    body.insertBefore(el, body.firstChild);\n\n    el.addEventListener(\"animationend\", this.handleTransitionIn.bind(this));       \n  }\n\n  toDom(html) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html.trim(); // Never return a text node of whitespace as the result\n\n    if (template.content) {\n      return template.content.firstChild;\n    }\n\n    return template.firstChild;\n  }\n\n  handleTransitionIn(ev) {\n    const el = ev.target;\n    el.removeEventListener(\"animationend\", this.handleTransitionIn);\n\n    setTimeout(() => {\n      el.addEventListener(\"animationend\", this.handleTransitionOut.bind(this));\n      el.classList.add(this.options.transitionOut);\n    }, this.options.timeout);\n  }\n\n  handleTransitionOut(ev) {\n    ev.target.removeEventListener(\"animationend\", this.handleTransitionOut);\n    ev.target.parentNode.removeChild(ev.target);\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/SaverForm.svelte",
    "content": "<script>\n  let { saver = $bindable() } = $props();\n</script>\n\n<div id=\"saver-form\">\n  <form>\n    <div class=\"form-group\">\n      <label for=\"name\">Name:</label>\n      <input bind:value={saver.name} type=\"text\" name=\"name\" required />\n      <div class=\"hint\">The name of your screensaver.</div>\n    </div>\n\n    <div class=\"form-group\">\n      <label for=\"name\">Description:</label>\n      <input\n        bind:value={saver.description}\n        type=\"text\"\n        name=\"description\"\n        required\n      />\n      <div class=\"hint\">A brief description of your screensaver.</div>\n    </div>\n    <div class=\"form-group\">\n      <label for=\"aboutUrl\">About URL:</label>\n      <input bind:value={saver.aboutUrl} type=\"text\" name=\"aboutUrl\" />\n      <div class=\"hint\">\n        If you have a URL with more details about your work, put it\n        here!\n      </div>\n    </div>\n    <div class=\"form-group\">\n      <label for=\"author\">Author:</label>\n      <input bind:value={saver.author} type=\"text\" name=\"author\" />\n      <div class=\"hint\">The author of this screensaver.</div>\n    </div>\n\n    <div class=\"form-group\">\n      <h3>Requirements:</h3>\n      <input\n        type=\"checkbox\"\n        name=\"requirements\"\n        bind:group={saver.requirements}\n        value=\"screen\"\n      />\n      <label for=\"screen\">Screen capture</label>\n      <div class=\"hint\">\n        This screensaver will be sent an image of the desktop\n      </div>\n    </div>\n  </form>\n</div>\n\n"
  },
  {
    "path": "src/renderer/components/SaverList.svelte",
    "content": "<div class=\"saver-list-wrapper\">\n  <h1>Screensavers</h1>\n  <ul class=\"saver-list list-group-flush\">\n    {#each savers as saver (saver.key)}\n      <li class=\"{saver.key == current ? CHECKED_CLASS : UNCHECKED_CLASS}\">\n        <div>\n          <label>\n            <div class=\"body\">\n              <input\n                type=\"radio\" \n                name=\"screensaver\" \n                bind:group=\"{current}\"\n                data-name=\"{saver.name}\" \n                value=\"{saver.key}\" \n                checked=\"{saver.key == current}\"\n                onchange={onSelect}\n                >\n\n              <b>{saver.name}</b>\n              <template v-if=\"saver.editable === true\">\n                &nbsp;(<small>custom</small>)\n              </template>\n      \n              <div class=\"description\">\n                <small>{saver.description}</small>\n              </div>\n            </div>\n          </label>\n        </div>\n      </li>\n    {/each}\n  </ul>\n</div>\n\n<script>\n  let { savers, current = $bindable(), saverPicked } = $props();\n\n  const UNCHECKED_CLASS = \"list-group-item flex-column entry\";\n  const CHECKED_CLASS = `${UNCHECKED_CLASS} active`;\n\n  function onSelect(e) {\n    saverPicked({\n      saver: e.target.value\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/renderer/components/SaverOptionInput.svelte",
    "content": "<form>\n  <div class=\"form-group\">\n    <label for=\"name\">Name</label>\n    <div>\n      <input\n        bind:value=\"{option.name}\"\n        type=\"text\"\n        name=\"name\"\n        placeholder=\"Pick a name for this option\"\n        required\n      >\n    </div>\n  </div>\n  <div class=\"form-group\">\n    <label for=\"description\">Description</label>\n    <div>\n      <input\n        bind:value=\"{option.description}\"\n        type=\"text\"\n        name=\"description\"\n        placeholder=\"Describe what this option does\"\n      >\n    </div>\n  </div>       \n  <div class=\"form-group\">\n    <label for=\"inputType\">Type</label>\n    <div>\n      <select\n        name=\"type\"\n        bind:value=\"{option.type}\"\n      >\n      <option value=\"slider\">\n          slider\n        </option>\n        <option value=\"text\">\n          text\n        </option>\n        <option value=\"boolean\">\n          yes/no\n        </option>\n      </select>\n    </div>\n  </div>\n\n  {#if option.type === \"slider\"}\n    <div class=\"space-evenly\">\n      <div class=\"form-group\">\n        <label for=\"min\">Min</label>\n        <input\n          name=\"min\"\n          bind:value=\"{option.min}\"\n          type=\"number\"\n        >\n      </div>\n        \n      <div class=\"form-group\">\n        <label for=\"max\">Max</label>\n        <input\n          name=\"max\"\n          bind:value=\"{option.max}\"\n          type=\"number\"\n        >\n      </div>\n      \n      <div class=\"form-group\">\n        <label for=\"default\">Default</label>\n        <input\n          name=\"default\"\n          bind:value=\"{option.default}\"\n          type=\"text\"\n          placeholder=\"Default value of this option\"\n        >\n      </div>\n    </div>\n  {:else if option.type === \"text\"}\n    <div class=\"form-group\">\n      <label for=\"default\">Default</label>\n      <div>\n        <input\n          name=\"default\"\n          bind:value=\"{option.default}\"\n          type=\"text\"\n          placeholder=\"Default value of this option\"\n        >\n      </div>\n    </div>\n    {:else if option.type === \"boolean\"}\n    <div class=\"form-group\">\n      <label for=\"default\">Default</label>\n      <div>\n        <select\n          name=\"default\"\n          bind:value=\"{option.default}\"\n        >\n        <option\n            disabled\n            value=\"\"\n          >\n            Please select one\n          </option>\n          <option value=\"true\">\n            Yes\n          </option>\n          <option value=\"false\">\n            No\n          </option>\n        </select>\n      </div>\n    </div>\n  {/if}\n</form>\n\n<style>\ndiv.space-evenly {\n  display: flex;\n  justify-content: space-between;\n  max-width: 95%;\n}\n\ndiv.space-evenly > .form-group {\n  max-width: 25%;\n}\n\n</style>\n\n<script>\nlet { option = $bindable() } = $props();\n</script>\n"
  },
  {
    "path": "src/renderer/components/SaverOptions.svelte",
    "content": "<div id=\"wrapper\">\n  <ul>\n    {#each saver.options as option, index (option.name)}\n    <li key={index}>\n      <div class=\"wrapper\">\n        {#if option.type === \"boolean\"}\n          <form class=\"input\">\n            <label class=\"for-option\" for=\"{option.name}\">{option.name}: {option.description}</label>\n            <div class=\"boolean-options\">\n              <label>Yes\n                <input\n                  type=\"radio\"\n                  name=\"{option.name}\"\n                  value=\"true\"\n                  checked=\"{saver.settings[option.name] == true || saver.settings[option.name] === \"true\"}\"\n                  onchange={notifyPreviewChange}\n                ></label>\n              <label>No\n                <input\n                  type=\"radio\"\n                  name=\"{option.name}\"\n                  value=\"false\"\n                  checked=\"{saver.settings[option.name] == false || saver.settings[option.name] === \"false\"}\"\n                  onchange={notifyPreviewChange}\n                ></label>\n              </div>\n          </form>\n          {:else if option.type === \"slider\"}\n          <form class=\"input\">\n            <label class=\"for-option\" for=\"{option.name}\">{option.name}:</label>\n            <input\n              type=\"range\"\n              name=\"{option.name}\"\n              min=\"{option.min}\"\n              max=\"{option.max}\"\n              bind:value=\"{saver.settings[option.name]}\"\n              class=\"inputClass\"\n              onchange={notifyPreviewChange}\n            >\n            <small class=\"form-text text-muted\">{option.description}</small>\n          </form>\n\n          {:else}\n          <form class=\"input\">\n            <label class=\"for-option\" for=\"{option.name}\">{option.name}:</label>\n            <input\n              type=\"text\"\n              name=\"{option.name}\"\n              bind:value=\"{saver.settings[option.name]}\"\n              class=\"inputClass\"\n              onchange={notifyPreviewChange}\n            >\n            <small class=\"form-text text-muted\">{option.description}</small>\n          </form>\n        {/if}\n      </div>\n    </li>\n    {/each}\n  </ul>\n</div>\n\n<script>\n\timport { createEventDispatcher } from \"svelte\";\n  const dispatch = createEventDispatcher();\n\n  let { saver = $bindable() } = $props();\n\n  function notifyPreviewChange() {\n\t\tdispatch(\"optionsChanged\", {\n\t\t\toptions: saver.settings\n\t\t});\n\t}\n</script>\n"
  },
  {
    "path": "src/renderer/components/SaverSummary.svelte",
    "content": "<div class=\"saver-description\">\n  {#if saver}\n    <h1>\n      {saver.name} \n      {#if saver.aboutUrl && saver.aboutUrl !== \"\"}<small><a href=\"{saver.aboutUrl}\" onclick={open}>learn more</a></small>{/if}\n    </h1>\n    {#if saver.editable}\n      <div class=\"actions\">\n        <button\n          class=\"btn edit\" \n          href=\"#\"\n          onclick={onEditClick}\n        >\n        edit\n        </button>\n        <button\n          class=\"btn\" \n          href=\"#\"\n          onclick={onDeleteClick}\n        >\n          delete\n        </button>\n      </div>\n    {/if}\n\n    <p>{saver.description}</p>\n    {#if saver.author && saver.author !== \"\"}\n      <span>\n        by: {saver.author}\n      </span>\n    {/if}\n  {/if}\n</div>\n\n<script>\n\timport { createEventDispatcher } from \"svelte\";\n\n  const dispatch = createEventDispatcher();\n\n  let { saver } = $props();\n\n  function open(event) {\n    event.preventDefault();\n    window.api.openUrl(event.target.href);\n  }\n\n  async function onEditClick() {\n    dispatch(\"editScreensaver\", {\n      saver\n    });\n  }\n\n  async function onDeleteClick() {\n    const result = await window.api.deleteSaverDialog(saver);\n    if ( result === 1 ) {\n      dispatch(\"deleteScreensaver\", {\n        saver\n      });\n    }\n  }\n</script>\n"
  },
  {
    "path": "src/renderer/components/Spinner.svelte",
    "content": "<div class=\"wrapper\">\n  <div class=\"lds-dual-ring\"></div>\n</div>\n\n<style>\n.lds-dual-ring {\n  display: inline-block;\n  width: 80px;\n  height: 80px;\n}\n.lds-dual-ring:after {\n  content: \" \";\n  display: block;\n  width: 64px;\n  height: 64px;\n  margin: 8px;\n  border-radius: 50%;\n  border: 6px solid #fff;\n  border-color: #fff transparent #fff transparent;\n  animation: lds-dual-ring 1.2s linear infinite;\n}\n@keyframes lds-dual-ring {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/icons/BugIcon.svelte",
    "content": "<svg\nid=\"Bug\"\nversion=\"1.1\"\nxmlns=\"http://www.w3.org/2000/svg\"\nxmlns:xlink=\"http://www.w3.org/1999/xlink\"\nx=\"0px\"\ny=\"0px\"\nwidth=\"14\"\nheight=\"14\"\nviewBox=\"0 0 20 20\"\nenable-background=\"new 0 0 20 20\"\nxml:space=\"preserve\"\n>\n<path\n  d=\"M10,1C7.7907715,1,6,2.7908325,6,5h8C14,2.7908325,12.2092285,1,10,1z M19,10h-3V7.5031738\nc0-0.02771-0.0065918-0.0535278-0.0080566-0.0808716l2.2150879-2.21521c0.390625-0.3905029,0.390625-1.0236816,0-1.4141846\nc-0.3903809-0.390564-1.0236816-0.390564-1.4140625,0l-2.215332,2.21521C14.550293,6.0066528,14.5246582,6,14.4970703,6H5.5029297\nC5.4753418,6,5.449707,6.0066528,5.4223633,6.0081177l-2.215332-2.21521c-0.3903809-0.390564-1.0236816-0.390564-1.4140625,0\nc-0.390625,0.3905029-0.390625,1.0236816,0,1.4141846l2.2150879,2.21521C4.0065918,7.449646,4,7.4754639,4,7.5031738V10H1\nc-0.5522461,0-1,0.4476929-1,1c0,0.5522461,0.4477539,1,1,1h3c0,0.7799683,0.15625,1.520813,0.4272461,2.2037354\nc-0.0441895,0.0316162-0.0947266,0.0494995-0.1342773,0.0891724l-2.8286133,2.8283691\nc-0.3903809,0.390564-0.3903809,1.0237427,0,1.4142456c0.390625,0.3905029,1.0239258,0.3905029,1.4143066,0L5.4802246,15.93396\nC6.3725586,16.9555054,7.6027832,17.6751099,9,17.9100342V8h2v9.9100342\nc1.3972168-0.2349243,2.6274414-0.9545288,3.5197754-1.9760132l2.6015625,2.6015015\nc0.3903809,0.3905029,1.0236816,0.3905029,1.4143066,0c0.3903809-0.3905029,0.3903809-1.0236816,0-1.4142456l-2.8286133-2.8283691\nc-0.0395508-0.0396729-0.0900879-0.0575562-0.1342773-0.0891724C15.84375,13.520813,16,12.7799683,16,12h3\nc0.5522461,0,1-0.4477539,1-1C20,10.4476929,19.5522461,10,19,10z\"\n/>\n</svg>\n"
  },
  {
    "path": "src/renderer/components/icons/FolderIcon.svelte",
    "content": "<svg\nx=\"0px\"\ny=\"0px\"\nwidth=\"14\"\nheight=\"14\"\nviewBox=\"0 0 14 14\"\nenable-background=\"new 0 0 14 14\"\nxml:space=\"preserve\"\n>\n<path\n  d=\"M18.405,4.799C18.294,4.359,17.75,4,17.195,4h-6.814C9.827,4,9.051,3.682,8.659,3.293L8.063,2.705\nC7.671,2.316,6.896,2,6.342,2H3.087C2.532,2,2.028,2.447,1.967,2.994L1.675,6h16.931L18.405,4.799z M19.412,7H0.588\nc-0.342,0-0.61,0.294-0.577,0.635l0.923,9.669C0.971,17.698,1.303,18,1.7,18H18.3c0.397,0,0.728-0.302,0.766-0.696l0.923-9.669\nC20.022,7.294,19.754,7,19.412,7z\"\n/>\n</svg>\n"
  },
  {
    "path": "src/renderer/components/icons/ReloadIcon.svelte",
    "content": "<svg\n  id=\"Cycle\"\n  version=\"1.1\"\n  xmlns=\"http://www.w3.org/2000/svg\"\n  xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n  x=\"0px\"\n  y=\"0px\"\n  width=\"14\"\n  height=\"14\"\n  viewBox=\"0 0 20 20\"\n  enable-background=\"new 0 0 20 20\"\n  xml:space=\"preserve\"\n>\n  <path\n    d=\"M5.516,14.224c-2.262-2.432-2.222-6.244,0.128-8.611c0.962-0.969,2.164-1.547,3.414-1.736L8.989,1.8\nC7.234,2.013,5.537,2.796,4.192,4.151c-3.149,3.17-3.187,8.289-0.123,11.531l-1.741,1.752l5.51,0.301l-0.015-5.834L5.516,14.224z\nM12.163,2.265l0.015,5.834l2.307-2.322c2.262,2.434,2.222,6.246-0.128,8.611c-0.961,0.969-2.164,1.547-3.414,1.736l0.069,2.076\nc1.755-0.213,3.452-0.996,4.798-2.35c3.148-3.172,3.186-8.291,0.122-11.531l1.741-1.754L12.163,2.265z\"\n  />\n</svg>\n\n\n"
  },
  {
    "path": "src/renderer/components/icons/SaveIcon.svelte",
    "content": "<svg\nid=\"Save\"\nversion=\"1.1\"\nxmlns=\"http://www.w3.org/2000/svg\"\nxmlns:xlink=\"http://www.w3.org/1999/xlink\"\nx=\"0px\"\ny=\"0px\"\nwidth=\"14\"\nheight=\"14\"\nviewBox=\"0 0 20 20\"\nenable-background=\"new 0 0 20 20\"\nxml:space=\"preserve\"\n>\n<path\n  d=\"M15.173,2H4C2.899,2,2,2.9,2,4v12c0,1.1,0.899,2,2,2h12c1.101,0,2-0.9,2-2V5.127L15.173,2z M14,8c0,0.549-0.45,1-1,1H7\nC6.45,9,6,8.549,6,8V3h8V8z M13,4h-2v4h2V4z\"\n/>\n</svg>\n"
  },
  {
    "path": "src/renderer/main.js",
    "content": "import \"~/css/styles.scss\";\n\nimport { mount } from 'svelte';\n\nimport PrefsScreen from \"./PrefsScreen.svelte\";\nimport SettingsScreen from \"./SettingsScreen.svelte\";\nimport AboutScreen from \"./AboutScreen.svelte\";\nimport NewScreensaverScreen from \"./NewScreensaverScreen.svelte\";\nimport EditorScreen from \"./EditorScreen.svelte\";\n// import * as Sentry from \"@sentry/electron/renderer\";\n\n// if ( process.env.SENTRY_DSN !== undefined ) {\n//   Sentry.init({\n//     dsn: process.env.SENTRY_DSN,\n//     enableNative: false,\n//     onFatalError: console.log\n//   });  \n// }\n\nconst actions = {\n  \"prefs\": PrefsScreen,\n  \"settings\": SettingsScreen,\n  \"about\": AboutScreen,\n  \"new\": NewScreensaverScreen,\n  \"editor\": EditorScreen\n};\n\nconst id = document.querySelector(\"body\").dataset.id;\nconst klass = actions[id];\n\nconst app = mount(klass, {\n  target: document.getElementById(\"root\"), // entry point in ../public/index.html\n});\n\nexport default app;\n"
  },
  {
    "path": "test/fixtures/bad-config.json",
    "content": "{\n  \"source\": {\n    \"repo\": \"\"\n  },\n  \"saver\": \"before-dawn-screensavers/emoji/index.html\",\n  \"options\": {\n"
  },
  {
    "path": "test/fixtures/config-2.json",
    "content": "{\n  \"source\": {\n    \"repo\": \"\"\n  },\n  \"saver\": \"before-dawn-screensavers/blur/saver.json\",\n  \"options\": {\n    \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\": {}\n  },\n  \"delay\": 10,\n  \"lock\": false,\n  \"localSource\": \"/home/tester/my screensavers\",\n  \"disableOnBattery\": true,\n  \"sleep\": 10\n}\n"
  },
  {
    "path": "test/fixtures/config-with-options.json",
    "content": "{\n  \"source\": {\n    \"repo\": \"\"\n  },\n  \"saver\": \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\",\n  \"options\": {\n    \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\": {\n      \"foo\": \"bar\",\n      \"level\": 100\n    },\n    \"/Users/colin/Projects/before-dawn-screensavers/key/saver.json\": {\n      \"baz\": \"boo\",\n      \"level\": 10\n    }\n  },\n  \"delay\": 10,\n  \"lock\": false,\n  \"localSource\": \"/home/tester/my screensavers\",\n  \"disableOnBattery\": true,\n  \"sleep\": 10\n}\n"
  },
  {
    "path": "test/fixtures/config.json",
    "content": "{\n  \"source\": {\n    \"repo\": \"\"\n  },\n  \"saver\": \"before-dawn-screensavers/emoji/saver.json\",\n  \"options\": {\n    \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\": {}\n  },\n  \"delay\": 10,\n  \"lock\": false,\n  \"localSource\": \"/home/tester/my screensavers\",\n  \"disableOnBattery\": false,\n  \"runOnSingleDisplay\": true,\n  \"auto_start\": true,\n  \"sleep\": 10\n}\n"
  },
  {
    "path": "test/fixtures/default-repo.json",
    "content": "{\n  \"sourceRepo\": \"mocha/screensavers\",\n  \"sourceUpdatedAt\": \"2018-01-07T15:59:04.499Z\",\n  \"saver\": \"before-dawn-screensavers/emoji/saver.json\",\n  \"options\": {\n    \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\": {}\n  },\n  \"delay\": 10,\n  \"lock\": false,\n  \"localSource\": \"/home/tester/my screensavers\",\n  \"disableOnBattery\": true,\n  \"sleep\": 10\n}\n"
  },
  {
    "path": "test/fixtures/index.html",
    "content": "<html>\n  <head>\n    <title>screensaver</title>\n  </head>\n  <body>I AM A SCREENSAVER!</body>\n</html>\n"
  },
  {
    "path": "test/fixtures/invalid.json",
    "content": "{\n  \"description\": \"A Screensaver\",\n  \"aboutUrl\": \"\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"requirements\": [],\n  \"options\": [\n    {\n      \"index\": 0,\n      \"name\": \"load_url\",\n      \"type\": \"text\",\n      \"description\": \"Load the specified URL\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"\"\n    },\n    {\n      \"index\": 1,\n      \"name\": \"sound\",\n      \"type\": \"boolean\",\n      \"description\": \"Play sound?\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"true\"\n    }\n  ],\n  \"editable\": true\n}\n"
  },
  {
    "path": "test/fixtures/no-requirements.json",
    "content": "{\n  \"name\": \"Screensaver One\",\n  \"description\": \"A Screensaver\",\n  \"aboutUrl\": \"\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"options\": [\n    {\n      \"index\": 0,\n      \"name\": \"load_url\",\n      \"type\": \"text\",\n      \"description\": \"Load the specified URL\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"\"\n    },\n    {\n      \"index\": 1,\n      \"name\": \"sound\",\n      \"type\": \"boolean\",\n      \"description\": \"Play sound?\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"true\"\n    }\n  ],\n  \"editable\": true\n}\n"
  },
  {
    "path": "test/fixtures/old-config.json",
    "content": "{\n  \"source\": {\n    \"repo\": \"muffinista/before-dawn-screensavers\"\n  },\n  \"sourceCheckTimestamp\": 2512407042366,\n  \"saver\": \"before-dawn-screensavers/emoji/index.html\",\n  \"options\": {\n    \"/Users/colin/Projects/before-dawn-screensavers/emoji/index.html\": {}\n  },\n  \"delay\": 10,\n  \"lock\": false,\n  \"localSource\": \"/home/tester/my screensavers\",\n  \"disableOnBattery\": true,\n  \"sleep\": 10\n}\n"
  },
  {
    "path": "test/fixtures/power/linux-charged.txt",
    "content": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n   variant       boolean false"
  },
  {
    "path": "test/fixtures/power/linux-charging.txt",
    "content": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n   variant       boolean false"
  },
  {
    "path": "test/fixtures/power/linux-discharging.txt",
    "content": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n   variant       boolean true"
  },
  {
    "path": "test/fixtures/release-no-update.json",
    "content": "{\n  \"url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664\",\n  \"assets_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets\",\n  \"upload_url\": \"https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets{?name,label}\",\n  \"html_url\": \"https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.2\",\n  \"id\": 6625664,\n  \"tag_name\": \"v0.9.2\",\n  \"target_commitish\": \"main\",\n  \"name\": \"\",\n  \"draft\": false,\n  \"author\": {\n      \"login\": \"muffinista\",\n      \"id\": 49172,\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/49172?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/muffinista\",\n      \"html_url\": \"https://github.com/muffinista\",\n      \"followers_url\": \"https://api.github.com/users/muffinista/followers\",\n      \"following_url\": \"https://api.github.com/users/muffinista/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/muffinista/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/muffinista/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/muffinista/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/muffinista/orgs\",\n      \"repos_url\": \"https://api.github.com/users/muffinista/repos\",\n      \"events_url\": \"https://api.github.com/users/muffinista/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/muffinista/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n  },\n  \"prerelease\": false,\n  \"created_at\": \"2017-06-06T23:54:52Z\",\n  \"published_at\": \"2017-06-06T23:55:44Z\",\n  \"assets\": [],\n  \"tarball_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.2\",\n  \"zipball_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.2\",\n  \"body\": \"\",\n  \"is_update\": false\n}"
  },
  {
    "path": "test/fixtures/release.json",
    "content": "{\n    \"url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664\",\n    \"assets_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets\",\n    \"upload_url\": \"https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets{?name,label}\",\n    \"html_url\": \"https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.2\",\n    \"id\": 6625664,\n    \"tag_name\": \"v0.9.2\",\n    \"target_commitish\": \"main\",\n    \"name\": \"\",\n    \"draft\": false,\n    \"author\": {\n        \"login\": \"muffinista\",\n        \"id\": 49172,\n        \"avatar_url\": \"https://avatars1.githubusercontent.com/u/49172?v=4\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/muffinista\",\n        \"html_url\": \"https://github.com/muffinista\",\n        \"followers_url\": \"https://api.github.com/users/muffinista/followers\",\n        \"following_url\": \"https://api.github.com/users/muffinista/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/muffinista/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/muffinista/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/muffinista/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/muffinista/orgs\",\n        \"repos_url\": \"https://api.github.com/users/muffinista/repos\",\n        \"events_url\": \"https://api.github.com/users/muffinista/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/muffinista/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n    },\n    \"prerelease\": false,\n    \"created_at\": \"2017-06-06T23:54:52Z\",\n    \"published_at\": \"2017-06-06T23:55:44Z\",\n    \"assets\": [],\n    \"tarball_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.2\",\n    \"zipball_url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.2\",\n    \"body\": \"\",\n    \"is_update\": true\n}"
  },
  {
    "path": "test/fixtures/releases/updates.json",
    "content": "{\"name\":\"v0.9.26\",\"notes\":\"This release updates some packages, including Electron, fixes some icon rendering issues, and has some other minor cleanup.\",\"pub_date\":\"2018-10-06T17:55:43Z\",\"url\":\"https://github.com/muffinista/before-dawn/releases/download/v0.9.26/before-dawn-setup-0.9.26.exe\"}"
  },
  {
    "path": "test/fixtures/saver.json",
    "content": "{\n  \"name\": \"Screensaver One\",\n  \"description\": \"A Screensaver\",\n  \"aboutUrl\": \"\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"requirements\": [],\n  \"options\": [\n    {\n      \"index\": 0,\n      \"name\": \"load_url\",\n      \"type\": \"text\",\n      \"description\": \"Load the specified URL\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"\"\n    },\n    {\n      \"index\": 1,\n      \"name\": \"sound\",\n      \"type\": \"boolean\",\n      \"description\": \"Play sound?\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"true\"\n    }\n  ],\n  \"editable\": true\n}\n"
  },
  {
    "path": "test/fixtures/saver2.json",
    "content": "{\n  \"name\": \"Saver Two\",\n  \"description\": \"Another Screensaver\",\n  \"aboutUrl\": \"\",\n  \"author\": \"Colin Mitchell\",\n  \"source\": \"index.html\",\n  \"requirements\": [],\n  \"options\": [\n    {\n      \"index\": 0,\n      \"name\": \"New Option I Guess\",\n      \"type\": \"slider\",\n      \"description\": \"Description\",\n      \"min\": \"1\",\n      \"max\": \"100\",\n      \"default\": \"75\"\n    }\n  ],\n  \"editable\": true\n}\n"
  },
  {
    "path": "test/fixtures/test-savers.json",
    "content": "{\"url\":\"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721\",\"assets_url\":\"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721/assets\",\"upload_url\":\"https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721/assets{?name,label}\",\"html_url\":\"https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.35\",\"id\":19343721,\"node_id\":\"MDc6UmVsZWFzZTE5MzQzNzIx\",\"tag_name\":\"v0.9.35\",\"target_commitish\":\"main\",\"name\":\"version 0.9.35\",\"draft\":false,\"author\":{\"login\":\"muffinista\",\"id\":49172,\"node_id\":\"MDQ6VXNlcjQ5MTcy\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/49172?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/muffinista\",\"html_url\":\"https://github.com/muffinista\",\"followers_url\":\"https://api.github.com/users/muffinista/followers\",\"following_url\":\"https://api.github.com/users/muffinista/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/muffinista/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/muffinista/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/muffinista/subscriptions\",\"organizations_url\":\"https://api.github.com/users/muffinista/orgs\",\"repos_url\":\"https://api.github.com/users/muffinista/repos\",\"events_url\":\"https://api.github.com/users/muffinista/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/muffinista/received_events\",\"type\":\"User\",\"site_admin\":false},\"prerelease\":false,\"created_at\":\"2019-08-16T16:47:00Z\",\"published_at\":\"2019-08-16T16:47:05Z\",\"assets\":[],\"tarball_url\":\"https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.35\",\"zipball_url\":\"https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.35\",\"body\":null}"
  },
  {
    "path": "test/helpers.js",
    "content": "/* eslint-disable mocha/no-exports */\n\nimport * as path from \"path\";\nimport fs from 'fs-extra';\nimport * as tmp from \"tmp\";\nimport temp from \"temp\";\n\nimport Conf from \"conf\";\n\nimport { _electron as playwright } from \"playwright\";\nimport electron from \"electron\";\n\nimport assert from \"assert\";\n\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nlet windowCheckDelay = 5000;\nlet testTimeout = 50000;\nlet testRetryCount = 0;\nlet logPath;\n\nlet app;\n\nif (process.env.CI) {\n  windowCheckDelay = 10000;\n  testTimeout = 60000;\n  testRetryCount = 3;\n}\n\nconst delayStep = 10;\n\n/**\n * keep a list of window titles here so we can have\n * a really clean system to open/load/wait for windows\n */\n const windowTitles = {\n  new: \"Before Dawn: Create Screensaver!\",\n  about: \"Before Dawn: About!\",\n  editor: \"Before Dawn: Editor\",\n  prefs: \"Before Dawn: Preferences\",\n  settings: \"Before Dawn: Settings\"\n};\n\nexport function specifyConfig(dest, name) {\n  fs.copySync(\n    path.join(__dirname, \"fixtures\", name + \".json\"),\n    dest\n  );\n}\n\nexport function setupConfig(workingDir, name=\"config\", attrs={}) {\n  const dest = path.join(workingDir, \"config.json\");\n  fs.copySync(\n    path.join(__dirname, \"fixtures\", name + \".json\"),\n    dest\n  );\n\n  if ( Object.keys(attrs) > 0 ) {\n    let store = new Conf({cwd: workingDir});\n    store.set(attrs);\n  }\n}\n\nexport function setConfigValue(workingDir, name, value) {\n  let f = path.join(workingDir, \"config.json\");\n  let currentVals = JSON.parse(fs.readFileSync(f));\n  currentVals[name] = value;\n\n  fs.writeFileSync(f, JSON.stringify(currentVals));\n}\n\nexport function addSaver(dest, name, source) {\n  // make a subdir in the savers directory and drop screensaver\n  // config there\n  if ( source === undefined ) {\n    source = \"saver.json\";\n  }\n  var src = path.join(__dirname, \"fixtures\", source);\n  var htmlSrc = path.join(__dirname, \"fixtures\", \"index.html\");\n  var testSaverDir = path.join(dest, name);\n\n  var saverJSONFile = path.join(testSaverDir, \"saver.json\");\n  var saverHTMLFile = path.join(testSaverDir, \"index.html\");\n\n  if ( ! fs.existsSync(dest) ) {\n    fs.mkdirSync(dest);\n  }\n\n  fs.mkdirSync(testSaverDir);\n\n  fs.copySync(src, saverJSONFile);\n  fs.copySync(htmlSrc, saverHTMLFile);    \n\n  return saverJSONFile;\n}\n\nexport function prefsToJSON(tmpdir) {\n  let testFile = path.join(tmpdir, \"config.json\");\n  let data = {};\n\n  try {\n    data = JSON.parse(fs.readFileSync(testFile));\n  }\n  catch {\n    data = {};\n  }\n\n  return data;\n}\n\nexport function getTempDir() {\n  const base = tmp.dirSync().name;\n  if ( process.platform === \"win32\" && base.lastIndexOf(\"~\") !== -1) {\n    return base.replace(\"RUNNER~1\", \"runneradmin\");\n  }\n  return base;\n}\n\nexport function savedConfig(p) {\n  var data = path.join(p, \"config.json\");\n  var json = fs.readFileSync(data);\n  return JSON.parse(json);\n}\n\n\nexport function setupFullConfig(workingDir) {\n  let saversDir = getTempDir();\n  let saverJSONFile = addSaver(saversDir, \"saver\");\n\n  setupConfig(workingDir, \"config\", {\n    \"saver\": saverJSONFile \n  });\n}\n\nexport function addLocalSource(workingDir, saversDir) {\n  var src = path.join(workingDir, \"config.json\");\n  var data = savedConfig(workingDir);\n  data.localSource = saversDir;\n  fs.writeFileSync(src, JSON.stringify(data));\n}\n\nexport function removeLocalSource(workingDir) {\n  var src = path.join(workingDir, \"config.json\");\n  var data = savedConfig(workingDir);\n  data.localSource = \"\";\n  fs.writeFileSync(src, JSON.stringify(data));\n}\n\n\n/**\n * Launch the application via playwright\n * \n * @param {string} workingDir \n * @param {boolean} quietMode \n * @returns application\n */\nexport async function application(workingDir, quietMode=false, logFile=undefined) {\n  let env = {\n    ...process.env,\n    BEFORE_DAWN_DIR: workingDir,\n    CONFIG_DIR: workingDir,\n    SAVERS_DIR: workingDir,\n    TEST_MODE: true,\n    QUIET_MODE: quietMode,\n    ELECTRON_ENABLE_LOGGING: true,\n    LOG_FILE: logFile,\n    XDG_SESSION_TYPE: 'x11'\n  };\n\n\n  let a = await playwright.launch({\n    path: electron,\n    args: [\n      path.join(__dirname, \"..\", \"output\", \"main.js\")\n    ],\n    env: env\n  });\n\n  \n  a.logData = [];\n  \n  a.once(\"window\", (w) => {\n    w.on(\"console\", (payload) => {\n      a.logData.push(payload);\n    });\n  });\n\n  // wait for the first window (our test shim) to open\n  await a.firstWindow();\n\n  app = a;\n\n  return a;\n}\n\nexport async function dumpOutput(app) {\n  if (app) {\n    console.log(app.logData);\n    app.logData = [];\n  }\n\n  if (fs.existsSync(logPath)) {\n    console.log(fs.readFileSync(logPath));\n  }\n}\n\n/**\n * \n * @param {app} app electron application\n * @param {string} windowName the name of the window to wait for\n * @returns Page\n */\n export async function waitFor(app, windowName) {\n  const title = windowTitles[windowName];\n  await waitForWindow(app, title);\n  return getWindowByTitle(app, title);\n}\n\n\n/**\n * Kill the app\n * \n * @param {application} app \n */\nexport async function stopApp(app) {\n  try {\n    if (app ) {\n      await app.close();\n    }\n  }\n  catch(e) {\n    console.log(e);\n  }\n}\n\n\n/**\n * Generate a lookup table of currently open windows\n * \n * @param {*} app \n * @returns hash of window objects keyed by title\n */\nexport async function getWindowLookup(app) {\n  const windows = await app.windows();\n  const promises = windows.map(async (window) => {\n    try {\n      const title = await window.title();\n      return [title, window];\n    } catch {\n      // sometimes a window will be closing and trying to get the title\n      // will throw an error, but it's probably fine\n      return [\"Missing window\", window];\n    }\n  });\n\n  const results = await Promise.all(promises);\n  return results.reduce((map, obj) => {\n    map[obj[0]] = obj[1];\n    return map;\n  }, {});\n}\n\n/**\n * Get window with the given title\n * \n * @param {*} app \n * @param {*} title \n * @returns \n */\nexport async function getWindowByTitle(app, title) {\n  // make sure the app is open\n  await app.firstWindow();\n\n  const lookup = await getWindowLookup(app);\n  return lookup[title];\n}\n\n/**\n * wait for text on the given window\n * @param {Page} window \n * @param {string} lookup lookup to pull a specific DOM section\n * @param {string} text text to look for\n * @param {boolean} doAssert \n */\nexport async function waitForText(window, lookup, text, doAssert) {\n  const content = await window.textContent(lookup);\n  if ( doAssert === true ) {\n    assert(content.lastIndexOf(text) !== -1);\n  }\n}\n\n/**\n * wait for ms milliseconds\n * @param {*} ms \n * @returns \n */\nexport function sleep (ms) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n\n/**\n * wait for window with the specified name to be available\n * @param {*} app \n * @param {*} title \n * @param {*} skipAssert \n * @returns \n */\nexport async function waitForWindow(app, title, skipAssert) {\n  let result = -1;\n  for ( var totalTime = 0; totalTime < windowCheckDelay; totalTime += delayStep ) {\n    result = await getWindowByTitle(app, title);\n    if ( result ) {\n      return true;\n    }\n    else {\n      await sleep(delayStep);\n    }\n  }\n\n  if ( skipAssert !== true ) {\n    assert.notStrictEqual(-1, result, `window ${title} not opened`);\n  }\n\n  return result;\n}\n\n\n/**\n * Use the shim window to send an IPC command to the app\n * @param {*} app \n * @param {*} method \n * @param {*} opts \n */\nexport async function callIpc(app, method, opts={}) {\n  await waitForWindow(app, 'test shim');\n  const window = await getWindowByTitle(app, 'test shim');\n\n\n  await window.fill(\"#ipc\", method);\n  await window.fill(\"#ipcopts\", JSON.stringify(opts));\n  await window.click(\"text=go\");\n}\n\nexport function setupTest(test) {\n  test.timeout(testTimeout);\n  test.retries(testRetryCount);\n\n\t// eslint-disable-next-line mocha/no-top-level-hooks\n  beforeEach(function () {\n    logPath = temp.path();\n  });\n\n\t// eslint-disable-next-line mocha/no-top-level-hooks\n\tafterEach(async function () {\n    if (this.currentTest.state !== \"passed\") {\n      await dumpOutput(app);\n    }\n\n    await stopApp(app);\n\t});\n}\n"
  },
  {
    "path": "test/lib/package.js",
    "content": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs\";\nimport * as tmp from \"tmp\";\nimport { rimrafSync } from 'rimraf'\nimport sinon from \"sinon\";\nimport nock from \"nock\";\n\nimport Package from \"../../src/lib/package.js\";\n\nimport * as helpers from \"../helpers.js\";\n\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nvar attrs;\n\nvar workingDir;\nvar dataPath;\nvar zipPath;\n\nvar sandbox;\n\ndescribe(\"Package\", function() {\n  beforeEach(function() {\n    sandbox = sinon.createSandbox();\n\n    workingDir = helpers.getTempDir();\n    dataPath = path.join(__dirname, \"..\", \"fixtures\", \"release.json\");\n    const saverZipSource = path.join(__dirname, \"..\", \"fixtures\", \"test-savers.zip\");\n\n    zipPath =  path.join(tmp.dirSync().name, \"test-savers.zip\");\n    fs.copyFileSync(saverZipSource, zipPath);\n\n\n    attrs = {\n      repo: \"muffinista/before-dawn-screensavers\",\n      dest: workingDir\n    };\n  });\n\n  afterEach(function () {\n    sandbox.restore();\n  });\n\n\tdescribe(\"initialization\", function() {\n    it(\"loads data\", function() {\n      var p = new Package(attrs);\n\n\t\t\tassert.equal(false, p.downloaded);\n\t\t\tassert.equal(false, p.attrs().downloaded);      \n\n      assert.equal(workingDir, p.dest);\n      assert.equal(workingDir, p.attrs().dest);      \n\t\t});\n  });\n\n  describe(\"getReleaseInfo\", function() {\n    describe(\"withValidResponse\", function() {\n      it(\"does stuff\", async function() {\n        nock(\"https://api.github.com\").\n          get(\"/repos/muffinista/before-dawn-screensavers/releases/latest\").\n          replyWithFile(200, dataPath, {\n            \"Content-Type\": \"application/json\",\n        });\n  \n        var p = new Package(attrs);\n        let results = await p.getReleaseInfo();\n        console.log(results);\n        assert.equal(\"muffinista\", results.author.login);\n      });\n    });\n\n    describe(\"withReject\", function() {\n      it(\"survives\", async function() {\n        nock(\"https://api.github.com\").\n          get(\"/repos/muffinista/before-dawn-screensavers/releases/latest\").\n          replyWithError({\n            message: \"something awful happened\",\n            code: \"AWFUL_ERROR\",\n          });\n        var p = new Package(attrs);\n        let results = await p.getReleaseInfo();\n        assert.deepEqual({}, results);\n      });\n    });\n  });\n\n  describe(\"checkLatestRelease\", function() {\n    var p;\n\n    describe(\"remote package\", function() {\n      beforeEach(function() {\n        p = new Package(attrs);\n      });\n      \n      it(\"calls downloadFile\", async function() {\n        const data = JSON.parse(fs.readFileSync(\"./test/fixtures/release.json\"));\n        sandbox.stub(p, \"getReleaseInfo\").\n          returns(data);\n\n        var df = sandbox.stub(p, \"downloadFile\").resolves(zipPath);\n        sandbox.stub(p, \"zipToSavers\").resolves({});\n\n        await p.checkLatestRelease();\n        assert(df.calledOnce);\n      });\n\n      it(\"doesnt call if not needed\", async function() {\n        const data = JSON.parse(fs.readFileSync(\"./test/fixtures/release-no-update.json\"));\n        sandbox.stub(p, \"getReleaseInfo\").\n          returns(data);\n\n        var cb = sinon.spy();\n        var df = sandbox.stub(p, \"downloadFile\");\n        \n        await p.checkLatestRelease(cb);\n        assert(!df.calledOnce);\n      });\n    });\n  });\n\n  describe(\"downloadFile\", function() {\n    var testUrl = \"https://test.file/savers.zip\";\n\n    beforeEach(function() {\n      nock(\"https://test.file\").\n                        get(\"/savers.zip\").\n       reply(200, () => {\n         return fs.createReadStream(zipPath);\n       });\n      rimrafSync(workingDir);\n      fs.mkdirSync(workingDir);\n    });\n\n    it(\"works\", async function() {\n      let p = new Package(attrs);\n      const dest = await p.downloadFile(testUrl);\n      assert(fs.existsSync(dest));\n    });\n  });\n\n  describe(\"zipToSavers\", function() {\n    var p;\n\n    beforeEach(function() {\n      p = new Package(attrs);\n      rimrafSync(workingDir);\n      fs.mkdirSync(workingDir);\n    });\n\n    it(\"unzips files\", async function() {\n      if (process.platform == \"darwin\") {\n        this.skip();\n      }\n      \n      await p.zipToSavers(zipPath);\n\n      var testDest = path.resolve(workingDir, \"sparks\", \"index.html\");\n      assert(fs.existsSync(testDest));\n    });\n\n    it(\"recovers from errors\", function(done) {\n      if (process.platform == \"darwin\") {\n        this.skip();\n      }\n\n      p.zipToSavers(dataPath).\n        then(() => {}).\n        catch( () => {\n          done();\n        });\n    });\n\n    it(\"keeps files on failure\", function(done) {\n      if (process.platform == \"darwin\") {\n        this.skip();\n      }\n\n      helpers.addSaver(workingDir, \"saver-one\", \"saver.json\");\n      \n      var testDest = path.resolve(workingDir, \"saver-one\", \"saver.json\");\n      assert(fs.existsSync(testDest));\n      \n      p.zipToSavers(dataPath).catch( () => {\n        assert(fs.existsSync(testDest));\n        done();\n      });\n    });\n\n\n    it(\"removes files that arent needed\", function(done) {\n      if (process.platform == \"darwin\") {\n        this.skip();\n      }\n\n      helpers.addSaver(workingDir, \"saver-one\", \"saver.json\");\n      \n      var testDest = path.resolve(workingDir, \"saver-one\", \"saver.json\");\n      assert(fs.existsSync(testDest));\n\n\n      p.zipToSavers(zipPath).then(() => {\n        assert(!fs.existsSync(testDest));\n        done();\n      });     \n    });\n  });\n  \n});\n"
  },
  {
    "path": "test/lib/prefs.js",
    "content": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport * as tmp from \"tmp\";\nimport fs from \"fs\";\n\nimport * as helpers from \"../helpers.js\";\n\nimport SaverPrefs from \"../../src/lib/prefs.js\";\n\n\ndescribe(\"SaverPrefs\", function() {\n  var tmpdir, prefs;\n\n  beforeEach(function() {\n    tmpdir = tmp.dirSync().name;\n  });\n\n  describe(\"without config\", function() {\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n    });\n\n    it(\"should load\", function() {\n      assert.equal(true, prefs.needSetup);\n    });\n  });\n\n  // reload\n  describe(\"reload\", function() {\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n    });\n  \n    it(\"works with existing config\", function() {\n      prefs.saver = \"foo/bar/baz.json\";\n      assert.equal(\"foo/bar/baz.json\", prefs.saver);\n\n      prefs.reload();\n      assert.equal(\"foo/bar/baz.json\", prefs.saver);\n    });\n\n    it(\"persists\", function() {\n      prefs.saver = \"foo/bar/baz.json\";\n\n      prefs = new SaverPrefs(tmpdir);\n      prefs.reload();\n      assert.equal(\"foo/bar/baz.json\", prefs.saver);\n\n    });\n  });\n\n  describe(\"needSetup\", function() {\n    it(\"is false with config\", function() {\n      prefs = new SaverPrefs(tmpdir);\n      assert.equal(true, prefs.needSetup);\n\n      prefs.localSource = \"local/dir\";\n      prefs.saver = \"foo/bar/baz\";\n\n      prefs = new SaverPrefs(tmpdir);\n      \n      assert.equal(false, prefs.needSetup);  \n    });\n\n    it(\"is true if saver is undefined\", function() {\n      prefs = new SaverPrefs(tmpdir);\n      assert.equal(true, prefs.needSetup);\n\n      prefs.localSource = \"local/dir\";\n      prefs.saver = \"foo/bar/baz\";\n\n      prefs = new SaverPrefs(tmpdir);\n      assert.equal(true, !prefs.needSetup);\n\n      prefs.saver = undefined;\n      assert.equal(true, prefs.needSetup);\n    });\n\n    it(\"is true if saver is blank\", function() {\n      prefs = new SaverPrefs(tmpdir);\n      assert.equal(true, prefs.needSetup);\n\n      prefs.localSource = \"local/dir\";\n      prefs.saver = \"foo/bar/baz\";\n\n      prefs = new SaverPrefs(tmpdir);\n      assert.equal(true, !prefs.needSetup);\n\n      prefs.saver = \"\";\n      assert.equal(true, prefs.needSetup);\n    });\n  });\n\n  // no source\n  describe(\"noSource\", function() {\n    describe(\"with config\", function() {\n      beforeEach(function() {\n        prefs = new SaverPrefs(tmpdir);\n        helpers.specifyConfig(prefs.configFile, \"config\");\n      });\n\n      it(\"is false if source repo\", function() {\n        prefs.sourceRepo = \"foo\";\n        prefs.localSource = \"\";\n\n        assert.equal(true, !prefs.noSource);\n      });\n\n      it(\"is false if local source\", function() {\n        prefs.store.delete(\"sourceRepo\");\n        prefs.localSource = \"foo\";\n\n        assert.equal(true, !prefs.noSource);\n      });\n    });\n  });\n\n\n  // defaultSaversDir\n  describe(\"defaultSaversDir\", function() {\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n    });\n\n    it(\"is the working directory\", function() {\n      let dest = path.join(tmpdir, \"savers\");\n      assert.equal(dest, prefs.defaultSaversDir);\n    });\n  });\n\n  // sources\n  describe(\"sources\", function() {\n    let systemDir;\n\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n      helpers.specifyConfig(prefs.configFile, \"config\");\n      systemDir = path.join(tmpdir, \"system-savers\");\n    });\n\n    it(\"includes localSource\", function() {\n      let saversDir = path.join(tmpdir, \"savers\");\n      let localSourceDir = helpers.getTempDir();\n      prefs.localSource = localSourceDir;\n\n      let result = prefs.sources;\n      assert.deepStrictEqual(\n        [ saversDir, localSourceDir, systemDir ], result);\n    });\n\n    it(\"includes repo\", function() {\n      prefs.sourceRepo = \"foo\";\n      let result = prefs.sources;\n      let dest = path.join(tmpdir, \"savers\");\n\n      assert.equal(true, result.lastIndexOf(dest) !== -1);\n      assert.equal(true, result.lastIndexOf(systemDir) !== -1);\n    });\n\n    it(\"includes both repo and localsource\", function() {\n      let saversDir = path.join(tmpdir, \"savers\");\n      let localSourceDir = helpers.getTempDir();\n\n      prefs.localSource = localSourceDir;\n      prefs.sourceRepo = \"foo\";\n\n\n      let result = prefs.sources;\n      assert.deepEqual(\n        [ saversDir, localSourceDir, systemDir ], result);\n    });\n\n    it(\"includes system\", function() {\n      fs.mkdirSync(systemDir);\n      let result = prefs.sources;\n      assert.equal(true, result.lastIndexOf(systemDir) !== -1);\n    });\n  });\n\n  // systemSource\n  describe(\"systemSource\", function() {\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n    });\n\n    it(\"works\", function() {\n      let expected = path.join(tmpdir, \"system-savers\");\n      assert.equal(expected, prefs.systemSource);\n    });\n  });\n\n  // getOptions\n  describe(\"getOptions\", function() {\n    beforeEach(function() {\n      prefs = new SaverPrefs(tmpdir);\n      helpers.specifyConfig(prefs.configFile, \"config-with-options\");\n    });\n\n    it(\"works without key\", function() {\n      let opts = prefs.getOptions();\n      assert.deepEqual({ foo: \"bar\", level: 100 }, opts);\n    });\n\n    it(\"works with key\", function() {\n      let opts = prefs.getOptions(\"/Users/colin/Projects/before-dawn-screensavers/key/saver.json\");\n      assert.deepEqual({ baz: \"boo\", level: 10 }, opts);\n    });\n\n    it(\"returns empty hash when key is undefined\", function() {\n      prefs.store.delete(\"saver\");\n      let opts = prefs.getOptions();\n      assert.deepEqual({}, opts);\n    });\n  });\n});\n"
  },
  {
    "path": "test/lib/saver-factory.js",
    "content": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport { rimrafSync } from 'rimraf'\nimport * as mkdirp from \"mkdirp\";\n\nimport * as helpers from \"../helpers.js\";\n\nimport SaverPrefs from \"../../src/lib/prefs.js\";\nimport SaverFactory from \"../../src/lib/saver-factory.js\";\nimport SaverListManager from \"../../src/lib/saver-list.js\";\n\n\ndescribe(\"SaverFactory\", function() { \n  var savers;\n  var prefs;\n  var factory;\n  \n  var workingDir;\n  var saversDir;\n  var systemDir;\n  \n  beforeEach(function() {\n    // this will be the working directory of the app\n    workingDir = helpers.getTempDir();\n\n    // this will be the separate directory to hold screensavers\n    saversDir = helpers.getTempDir();\n\n    mkdirp.sync(workingDir);\n    mkdirp.sync(saversDir);\n\n    systemDir = path.join(workingDir, \"system-savers\");\n    fs.mkdirSync(systemDir);\n\n    helpers.addSaver(systemDir, \"random-saver\");\n    helpers.addSaver(systemDir, \"__template\");    \n\n    prefs = new SaverPrefs(workingDir);\n    prefs.localSource = saversDir;\n  });\n\n  afterEach(function() {\n    if ( fs.existsSync(workingDir) ) {\n      rimrafSync(workingDir);\n    }\n  });\n\n  \n  describe(\"create\", function() {\n    it(\"works\", async function() {\n      var templateSrc;\n      const attrs = {\n        name: \"New Screensaver\"\n      };\n  \n      savers = new SaverListManager({\n        prefs: prefs\n      });\n      factory = new SaverFactory();\n      templateSrc = path.join(systemDir, \"__template\");\n  \n      let data = await savers.list();\n      let oldCount = data.length;\n\n      const result = factory.create(templateSrc, saversDir, attrs);\n\n      data = await savers.list();\n      assert.equal(oldCount + 1, data.length);\n\n      assert.equal(\"new-screensaver\", result.key);\n      assert.equal(\"New Screensaver\", result.name);\n\n      const expectedDest = path.join(saversDir, \"new-screensaver\", \"saver.json\");\n      assert.equal(expectedDest, result.dest);\n    });\n\n    it(\"throws exception\", function(done) {\n      assert.throws(\n        () => {\n          savers.create({\n            name:\"New Screensaver\"\n          });\n        },\n        Error);\n      done();\n    });\n  });\n});"
  },
  {
    "path": "test/lib/saver-list.js",
    "content": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport { rimrafSync } from 'rimraf'\nimport sinon from \"sinon\";\nimport * as helpers from \"../helpers.js\";\n\nimport SaverPrefs from \"../../src/lib/prefs.js\";\nimport SaverListManager from \"../../src/lib/saver-list.js\";\n\nvar sandbox;\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ndescribe(\"SaverListManager\", function() { \n  var savers;\n  var prefs;\n  \n  var workingDir;\n  var saversDir;\n  var systemDir;\n  var saverJSONFile;\n  \n  beforeEach(function() {\n    sandbox = sinon.createSandbox();\n\n    // this will be the working directory of the app\n    workingDir = helpers.getTempDir();\n\n    // this will be the separate directory to hold screensavers\n    saversDir = helpers.getTempDir();\n\n    saverJSONFile = helpers.addSaver(saversDir, \"saver\");\n    helpers.addSaver(saversDir, \"saver2\");    \n\n    systemDir = path.join(workingDir, \"system-savers\");\n    fs.mkdirSync(systemDir);\n\n    helpers.addSaver(systemDir, \"random-saver\");\n    helpers.addSaver(systemDir, \"__template\");    \n\n    prefs = new SaverPrefs(workingDir);\n    prefs.localSource = saversDir;\n    savers = new SaverListManager({\n      prefs: prefs\n    });\n  });\n\n  afterEach(function() {\n    if ( fs.existsSync(workingDir) ) {\n      rimrafSync(workingDir);\n    }\n    sandbox.restore();\n  });\n\n  describe(\"setup\", function() {\n    it(\"works\", function(done) {\n      savers.setup().then((results) => {\n        assert(results.first);\n        assert(results.setup);\n\n        done();\n      });\n    });\n  });\n\n  describe(\"reload\", function() {\n    it(\"works\", function(done) {\n      savers.reload(true).then(() => {\n        done();\n      });\n    });\n  });\n  \n  describe(\"loadFromFile\", function() {\n    it(\"loads data\", function(done) {\n      savers.loadFromFile(saverJSONFile).then((s) => {\n        assert.equal(\"Screensaver One\", s.name);\n        done();\n      });\n    });\n    \n    it(\"applies options\", function(done) {\n      savers.loadFromFile(saverJSONFile, { \"New Option I Guess\": \"25\" }).then((s) => {\n        assert.equal(s.settings[\"New Option I Guess\"], \"25\");\n        done();\n      });\n    });\n\n    it(\"rejects bad json\", function(done) {\n      var f = path.join(__dirname, \"../fixtures/index.html\");\n      savers.loadFromFile(f, false, { \"New Option I Guess\": \"25\" }).\n             then(() => {\n               done(new Error(\"Expected method to reject.\"));               \n             }).\n             catch((err) => {\n               assert(typeof(err) !== \"undefined\");\n               done();\n             }).\n             catch(done);\n    });\n\n    it(\"rejects invalid savers\", function(done) {\n      var f = path.join(__dirname, \"../fixtures/invalid.json\");\n      savers.loadFromFile(f, false, {}).\n        then(() => {\n          done(new Error(\"Expected method to reject.\"));               \n        }).\n        catch(() => {\n          done();\n        });\n    });\n\n    it(\"adds requirements if missing\", function(done) {\n      var f = path.join(__dirname, \"../fixtures/no-requirements.json\");\n      savers.loadFromFile(f, false, {}).then((s) => {\n        assert.deepEqual([\"screen\"], s.requirements);\n        done();\n      });\n    });\n  });\n  \n  describe(\"list\", function() {\n    it(\"loads data\", async function() {\n      const data = await savers.list();\n      assert.equal(3, data.length);\n    });\n\n    it(\"handles bad data\", async function() {\n      helpers.addSaver(saversDir, \"invalid\", \"invalid.json\");\n      const data = await savers.list();\n      assert.equal(3, data.length);\n    });\n\n    it(\"uses cache\", async function() {\n      let cache = [0, 1, 2, 3, 4, 5];\n      savers.loadedScreensavers = cache;\n      const data = await savers.list();\n      assert.deepEqual(cache, data);\n    });\n\n    it(\"forces reset\", async function() {\n      let cache = [0, 1, 2, 3, 4, 5];\n      savers.loadedScreensavers = cache;\n      const data = await savers.list(true);\n\n      assert.notDeepEqual(cache, data);\n      assert.equal(3, data.length);\n    });\n  });\n\n  describe(\"reset\", function() {\n    it(\"resets cache\", async function() {\n      await savers.list();\n      assert.equal(3, savers.loadedScreensavers.length);\n      savers.reset();\n      assert.equal(0, savers.loadedScreensavers.length);\n    });\n  });\n\n  describe(\"random\", function() {\n    it(\"returns something\", async function() {\n      const data = await savers.list();\n\n      assert.equal(3, data.length);\n      let foo = savers.random();\n      assert(foo.key !== undefined);\n    });\n  });\n\n  describe(\"confirmExists\", function() {\n    it(\"returns true if present\", async function() {\n      let key = path.join(saversDir, \"saver2\", \"saver.json\");    \n      const result = await savers.confirmExists(key);\n      assert(result);\n    });\n\n    it(\"returns false if not present\", async function() {\n      let key = \"junk\";\n      const result = await savers.confirmExists(key);\n      assert(!result);\n    });\n  });\n\n  describe(\"getByKey\", function() {\n    it(\"returns saver\", async function() {\n      const data = await savers.list();\n      var key = data[2].key;\n      var s = savers.getByKey(key);\n      assert.equal(\"Screensaver One\", s.name);\n    });\n  });\n\n  describe(\"delete\", function() {\n    it(\"can delete if editable\", async function() {\n      const data = await savers.list();\n\n      let s = data.find(s => s.editable);\n      const result = await savers.delete(s);\n      assert(result);\n    });\n\n    it(\"doesn't delete if not editable\", async function() {\n      const data = await savers.list();\n\n      let s = data.find(s => !s.editable);\n      try {\n        await savers.delete(s);\n      }\n      catch {\n        assert(true);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "test/lib/saver.js",
    "content": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport * as tmp from \"tmp\";\n\nimport Saver from \"../../src/lib/saver.js\";\n\ndescribe(\"Saver\", function() {\n  const testName = \"Test Screensaver\";\n  const testDescription = \"It's a screensaver, but for testing\";\n\n  const attrs = {\n    \"name\": testName,\n    \"description\": testDescription,\n    \"aboutUrl\": \"\",\n    \"author\": \"Colin Mitchell\",\n    \"path\": \"/tmp\",\n    \"source\": \"index.html\",\n    \"requirements\": [],\n    \"options\": [\n      {\n        \"index\": 0,\n        \"name\": \"New Option I Guess\",\n        \"type\": \"text\",\n        \"description\": \"Description\",\n        \"min\": \"1\",\n        \"max\": \"100\",\n        \"default\": \"50\"\n      },\n      {\n        \"index\": 1,\n        \"name\": \"New Option\",\n        \"type\": \"slider\",\n        \"description\": \"Description\",\n        \"min\": \"1\",\n        \"max\": \"100\",\n        \"default\": \"75\"\n      }\n    ],\n    \"editable\": true\n  };\n\n  var loadSaver = function(opts) {\n    if ( typeof(opts) === \"undefined\" ) {\n      opts = {};\n    }\n\n    var vals = Object.assign({}, attrs, opts);\n    return new Saver(vals);\n  };\n  \n\tdescribe(\"initialization\", function() {\n    it(\"loads data\", function() {\n      var s = loadSaver();\n\t\t\tassert.equal(testName, s.name);\n\t\t\tassert.equal(testDescription, s.description);      \n\t\t});\n    \n    it(\"loads options\", function() {\n      var s = loadSaver();\n\t\t\tassert.equal(testName, s.name);\n      assert.equal(2, s.options.length);\n    });\n    \n    it(\"is published by default\", function() {\n      var s = loadSaver();\n\t\t\tassert.equal(testName, s.name);\n      assert(s.published);\n    });\n    \n    it(\"is not valid if not published\", function() {\n      var s = loadSaver({published: false});\n      assert(!s.valid);\n    });\n\n    it(\"has default settings\", function() {\n      var s = loadSaver();\n      assert.equal(\"75\", s.settings[\"New Option\"]);\n      assert.equal(\"50\", s.settings[\"New Option I Guess\"]);      \n    });\n\n    it(\"merges user settings\", function() {\n      var s = loadSaver({settings: []});\n      assert.equal(\"75\", s.settings[\"New Option\"]);\n      assert.equal(\"50\", s.settings[\"New Option I Guess\"]);      \n    });\n\n    it(\"loads local previewUrl\", function() {\n      var s = loadSaver({path: \"path\", previewUrl:\"preview.html\"});\n      assert.equal(\"path/preview.html\", s.previewUrl);\n    });\n  });\n\n  describe(\"requirements\", function() {\n    it(\"defaults to empty\", function() {\n      var s = loadSaver();\n      assert.deepEqual([], s.requirements);\n    });\n\n    it(\"reads from incoming params\", function() {\n      var s = loadSaver({requirements:[\"stuff\"]});\n      assert.deepEqual([\"stuff\"], s.requirements);\n    });\n  });\n  \n  describe(\"toHash\", function() {\n    it(\"should return attributes\", function() {\n      var s = loadSaver();\n      assert.deepEqual(attrs, s.toHash());\n    });\n  });\n\n  describe(\"urlWithParams\", function() {\n    it(\"returns url if it is remote\", function() {\n      var s = loadSaver({url: \"http://muffinlabs.com\"});\n      assert.deepEqual(\"http://muffinlabs.com\", s.urlWithParams({foo: \"bar\"}));\n    });\n\n    it(\"returns url if not remote but no params\", function() {\n      var s = loadSaver({path: \"path\", settings: {}});\n      assert.deepEqual(\"file://path/index.html?New+Option+I+Guess=50&New+Option=75\", s.urlWithParams());\n    });\n\n    it(\"includes params\", function() {\n      var s = loadSaver({path: \"path\", settings: {}});\n      assert.deepEqual(\"file://path/index.html?foo=bar&New+Option+I+Guess=50&New+Option=75\", s.urlWithParams({foo: \"bar\"}));\n    });\n  });\n\n  describe(\"write\", function() {\n    it(\"should write some output\", function() {\n      var dest = tmp.fileSync().name;\n      var s = loadSaver();\n      s.attrs.name = \"New Name To Write\";\n      \n      s.write(s.toHash(), dest);\n\n      var data = JSON.parse(fs.readFileSync(dest));\n      s = new Saver(data);\n      assert.equal(\"New Name To Write\", s.name);\n    });\n\n    it(\"should work without a dest\", function() {\n      var p = tmp.dirSync().name;\n      var dest = path.join(p, \"saver.json\");\n\n      var s = loadSaver({path: p});\n      s.attrs.name = \"New Name To Write\";\n      \n      s.write(s.toHash());\n\n      var data = JSON.parse(fs.readFileSync(dest));\n      s = new Saver(data);\n      assert.equal(\"New Name To Write\", s.name);\n    });\n\n    it(\"sets default requirements\", function() {\n      var p = tmp.dirSync().name;\n      var dest = path.join(p, \"saver.json\");\n\n      var s = loadSaver({path: p});\n      delete s.attrs.requirements;\n      s.write(s.toHash());\n      \n      var data = JSON.parse(fs.readFileSync(dest));\n      s = new Saver(data);\n      assert.equal(\"none\", s.requirements[0]);\n    });\n\n    it(\"allows requirements\", function() {\n      var p = tmp.dirSync().name;\n      var dest = path.join(p, \"saver.json\");\n\n      var s = loadSaver({path: p});\n      s.attrs.requirements = [\"magic\"];\n      s.write(s.toHash());\n      \n      var data = JSON.parse(fs.readFileSync(dest));\n      s = new Saver(data);\n      assert.equal(1, s.requirements.length);\n    });\n  });\n\n  describe(\"published\", function() {\n    it(\"defaults to true\", function() {\n      var s = new Saver({\n        path:\"\"\n      });\n      assert.equal(true, s.published);\n    });\n\n    it(\"accepts incoming value\", function() {\n      var s = new Saver({\n        path:\"\",\n        published: false\n      });\n      assert.equal(false, s.published);\n    });\n  });\n\n  describe(\"valid\", function() {\n    it(\"false without data\", function() {\n      var s = new Saver({\n        path:\"\"\n      });\n      assert.equal(false, s.valid);\n    });\n\n    it(\"false without name\", function() {\n      var s = new Saver({\n        description:\"description\",\n        path:\"\"\n      });\n      assert.equal(false, s.valid);\n    });\n\n    it(\"false without description\", function() {\n      var s = new Saver({\n        name:\"name\",\n        path:\"\"\n      });\n      assert.equal(false, s.valid);\n    });\n\n    it(\"false if not published\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        published:false,\n        path:\"\"\n      });\n      assert.equal(false, s.valid);\n    });\n\n    it(\"true if published with name and description\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        path:\"\"\n      });\n      assert.equal(true, s.valid);\n    });\n\n  });\n\n  describe(\"settings\", function() {\n    it(\"defaults to empty\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        path:\"\"\n      });\n      assert.deepEqual({}, s.settings);\n    });\n\n    it(\"accepts incoming options\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        path:\"\",\n        options: [\n          {\n            \"name\": \"density\",\n            \"type\": \"slider\",\n            \"description\": \"how dense?\",\n            \"min\": \"1\",\n            \"max\": \"100\",\n            \"default\": \"75\"\n          }\n        ]\n      });\n      assert.deepEqual({density:\"75\"}, s.settings);\n    });\n\n    it(\"accepts incoming options and user settings\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        path:\"\",\n        options: [\n          {\n            \"name\": \"density\",\n            \"type\": \"slider\",\n            \"description\": \"how dense?\",\n            \"min\": \"1\",\n            \"max\": \"100\",\n            \"default\": \"75\"\n          }\n        ],\n        settings: {\n          density:\"100\",\n          other:\"hello\"\n        }\n      });\n      assert.deepEqual({density:\"100\", other:\"hello\"}, s.settings);\n    });\n  });\n\n  describe(\"url\", function() {\n    it(\"uses url\", function() {\n      var s = new Saver({\n        name:\"name\",\n        description:\"description\",\n        url:\"http://yahoo.com/\",\n        path:\"\"\n      });\n\n      assert.equal(\"http://yahoo.com/\", s.url);\n    });\n  });\n});\n"
  },
  {
    "path": "test/main/power.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\n\nimport Power from \"../../src/main/power.js\";\n\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ndescribe(\"Power\", function() {\n  describe(\"charging\", function() {\n    const loadFixture = (platform, type) => {\n      const f = path.join(__dirname, `../fixtures/power/${platform}-${type}.txt`);\n      return fs.readFileSync(f).toString();\n    };\n\n    let power;\n\n    describe(\"unhandled platform\", function() {\n      it(\"works\", async function() {\n        const power = new Power({platform: \"beos\"});\n        assert(await power.charging());\n      });\n    });\n\n    describe(\"linux\", function() {\n      let platform;\n\n      beforeEach(function() {\n        platform = \"linux\";\n        power = new Power(platform);\n      });\n\n      it(\"is correct when charged\", async function() {\n        assert(await power.charging(loadFixture(platform, \"charged\")));\n      });\n\n      it(\"is correct when charging\", async function() {\n        assert(await power.charging(loadFixture(platform, \"charging\")));\n      });\n\n      it(\"is correct when discharging\", async function() {\n        assert.strictEqual(false, await power.charging(loadFixture(platform, \"discharging\")));\n      });\n    });\n\n    [\"darwin\", \"win32\"].forEach((platform) => {\n      \n      describe(platform, function() {\n        beforeEach(function() {\n          const method = () => {\n            return false;\n          };\n\n          power = new Power({platform, method});\n        });\n\n        it(\"returns the reverse of the method\", async function() {\n          assert.strictEqual(true, await power.charging());\n        });\n \n      });\n    });\n  });\n});\n\n"
  },
  {
    "path": "test/main/release_check.js",
    "content": "\"use strict\";\n\n\nimport assert from 'assert';\nimport path from \"path\";\nimport nock from \"nock\";\n\nimport ReleaseCheck from \"../../src/main/release_check.js\";\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n  \n\ndescribe(\"ReleaseCheck\", function() {\n  let releaseChecker;\n  let version = \"0.1.1\";\n  let server = \"https://sillynotreal.domain\";\n  let uriPath = `/update/win32/${version}`;\n  let url = `${server}${uriPath}`;\n  let fixturePath;\n\n  beforeEach(function() {\n    fixturePath = path.join(__dirname, \"../fixtures/releases/updates.json\");\n    releaseChecker = new ReleaseCheck();  \n  });\n\n  it(\"handles updates\", function(done) {\n    nock(server).\n      get(uriPath).\n      replyWithFile(200, fixturePath, {\n        \"Content-Type\": \"application/json\",\n      });\n\n    releaseChecker.setFeed(url);\n    releaseChecker.onUpdate((result) => {\n      assert.equal(\"v0.9.26\", result.name);\n      done();\n    });\n\n    releaseChecker.checkLatestRelease();\n  });\n\n  it(\"handles no updates\", function(done) {\n    nock(server).\n      get(uriPath).\n      reply(204, () => {\n        return \"\";\n      });\n\n    releaseChecker.setFeed(url);\n    releaseChecker.onNoUpdate(() => {\n      done();\n    });\n    releaseChecker.checkLatestRelease();\n  });\n});"
  },
  {
    "path": "test/main/state_manager.js",
    "content": "\"use strict\";\n\n\nimport assert from 'assert';\nimport sinon from \"sinon\";\n\nimport StateManager from \"../../src/main/state_manager.js\";\n\nconst fakeIdler = {\n  getIdleTime: () => { return 0; }\n};\n\ndescribe(\"StateManager\", function() {\n  let hitIdle, hitBlank, hitReset;\n  let sandbox;\n  let stateManager;\n\n  beforeEach(function() {\n    stateManager = new StateManager();\n\n    hitIdle = false;\n    hitBlank = false;\n    hitReset = false;\n  \n    sandbox = sinon.createSandbox();\n\n    stateManager.reset();\n    stateManager.setup({\n      idleTime: 100,\n      blankTime: 200,\n      onIdleTime: () => {\n        hitIdle = true;\n      },\n      onBlankTime: () => {\n        hitBlank = true;\n      },\n      onReset: () => {\n        hitReset = true;\n      }\n    });\n  });\n\n  afterEach(function() {\n    stateManager.stopTicking();\n    sandbox.restore();\n  });\n\n  it(\"does nothing\", function(done) {\n    sandbox.stub(fakeIdler, \"getIdleTime\").returns(0.01);\n    stateManager.idleFn = fakeIdler.getIdleTime;\n\n    stateManager.tick(false);\n    setTimeout(() => {\n      assert(!hitIdle);\n      assert(!hitBlank);\n      //assert(!hitReset);\n\n      done();\n    }, 50);\n  });\n\n  it(\"idles\", function(done) {\n    sandbox.stub(fakeIdler, \"getIdleTime\").returns(200);\n    stateManager.idleFn = fakeIdler.getIdleTime;\n\n    stateManager.tick(false);\n    setTimeout(() => {\n      assert(hitIdle);\n      assert(!hitBlank);\n\n      done();\n    }, 50);\n  });\n\n  it(\"blanks\", function(done) {\n    sandbox.stub(fakeIdler, \"getIdleTime\").returns(1000);\n    stateManager.idleFn = fakeIdler.getIdleTime;\n    stateManager.switchState(stateManager.STATES.STATE_RUNNING);\n\n    stateManager.tick(false);\n    setTimeout(() => {\n      assert(hitBlank);\n      done();\n    }, 50);\n  });\n\n  it(\"resets\", function(done) {\n    var idleCount = sandbox.stub(fakeIdler, \"getIdleTime\");\n    idleCount.onCall(0).returns(3);\n\n    stateManager.idleFn = fakeIdler.getIdleTime;\n    stateManager.switchState(stateManager.STATES.STATE_RUNNING);\n\n    setTimeout(() => {\n      stateManager.tick(false);\n      assert(hitReset);\n      done();\n    }, 50);\n\n  });\n});"
  },
  {
    "path": "test/ui/about.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \"../helpers.js\";\nimport fs from \"fs\";\n\nconst packageJSON = JSON.parse(fs.readFileSync(\"./package.json\"));\n\nvar workingDir;\nlet app;\n\ndescribe(\"About\", function() {\n  helpers.setupTest(this);\n\n  beforeEach(async function() {\n    workingDir = helpers.getTempDir();\n    helpers.setupFullConfig(workingDir);\n\n    app = await helpers.application(workingDir, true);\n    await helpers.callIpc(app, \"open-window about\");\n  });\n\n  it(\"has some text and current version number\", async function() {\n    const window = await helpers.waitFor(app, \"about\");\n\n    const elem = await window.$(\"body\");\n    const text = await elem.innerText();\n    assert(text.lastIndexOf(\"// screensaver fun //\") !== -1);\n    assert(text.lastIndexOf(packageJSON.version) !== -1);\n  });\n});\n"
  },
  {
    "path": "test/ui/bootstrap.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport * as tmp from \"tmp\";\nimport * as helpers from \"../helpers.js\";\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\ndescribe(\"bootstrap\", function() {\n  const saverZipSource = path.join(__dirname, \"..\", \"fixtures\", \"test-savers.zip\");\n  const saverData = path.join(__dirname, \"..\", \"fixtures\", \"test-savers.json\");\n\n  let configDest;\n  var workingDir;\n  let app;\n  let saverZip;\n\n  helpers.setupTest(this);\n\n  beforeEach(function() {\n    saverZip =  path.join(tmp.dirSync().name, \"test-savers.zip\");\n    fs.copyFileSync(saverZipSource, saverZip);\n  \n    workingDir = helpers.getTempDir();\n    configDest = path.join(workingDir, \"config.json\");  \n  });\n\n\n  describe(\"without config\", function() {\n    beforeEach(async function() {\n      assert(!fs.existsSync(configDest));\n      app = await helpers.application(workingDir, false, saverZip, saverData);\n    });\n\n    it(\"creates config file and shows prefs\", async function() {\n      await helpers.waitFor(app, \"prefs\");\n      assert(fs.existsSync(configDest));\n\n      // the test was crashing without waiting here a bit\n      await helpers.sleep(1000);\n    });\n  });\n\n  describe(\"with invalid config\", function() {\n    beforeEach(async function() {\n      const dest = path.join(workingDir, \"config.json\");\n      fs.copySync(\n        path.join(__dirname, \"..\", \"fixtures\", \"bad-config.json\"),\n        dest\n      );\n\n      app = await helpers.application(workingDir, true);\n    });\n\n    it(\"creates config file and shows prefs\", async function() {\n      await helpers.waitFor(app, \"prefs\");\n      assert(fs.existsSync(configDest));\n\n      const data = JSON.parse(fs.readFileSync(configDest));\n      assert.deepStrictEqual(5, data.delay);\n    });\n  });\n});\n"
  },
  {
    "path": "test/ui/editor.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport * as helpers from \"../helpers.js\";\n\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nvar workingDir;\nlet app;\nlet window;\n\ndescribe(\"Editor\", function() {\n  var saverJSON;\n  helpers.setupTest(this);\n\n  beforeEach(async function() {\n    workingDir = helpers.getTempDir();\n    \n    var saversDir = helpers.getTempDir();\n    saverJSON = helpers.addSaver(saversDir, \"saver-one\", \"saver.json\");\n\n    app = await helpers.application(workingDir, true);\n    await helpers.callIpc(app, \"open-window editor\", {\n      screenshot: \"file://\" + path.join(__dirname, \"../fixtures/screenshot.png\"),\n      src: saverJSON\n    });\n\n    window = await helpers.waitFor(app, \"editor\");\n  });\n    \n  it(\"edits basic settings\", async function() {\n    const val = await window.inputValue(\"#saver-form [name='name']\");\n    assert.strictEqual(\"Screensaver One\", val);\n\n    await window.fill(\"#saver-form [name='name']\", \"A New Name!!!\");\n    await window.fill(\"#saver-form [name='description']\", \"A Thing I Made?\");\n    await window.click(\"button.save\");\n\n    await helpers.sleep(100);\n\n    var x = JSON.parse(fs.readFileSync(saverJSON)).name;\n    assert.strictEqual(x, \"A New Name!!!\");\n  });\n\n  it(\"adds and removes options\", async function() {\n    await window.fill(\".saver-option-input[data-index='0'] [name='name']\", \"My Option\");\n    await window.fill(\".saver-option-input[data-index='0'] [name='description']\", \"An Option I Guess?\");\n    await window.click(\"button.add-option\");\n\n    await window.fill(\".saver-option-input[data-index='1'] [name='name']\", \"My Second Option\");\n    await window.fill(\".saver-option-input[data-index='1'] [name='description']\", \"Another Option I Guess?\");\n\n    await window.selectOption(\".saver-option-input[data-index='1'] select\", {label: \"yes/no\"});\n\n    await window.click(\"button.add-option\");\n    await window.fill(\".saver-option-input[data-index='2'] [name='name']\", \"My Third Option\");\n    await window.fill(\".saver-option-input[data-index='2'] [name='description']\", \"Here We Go Again\");\n\n    await window.selectOption(\".saver-option-input[data-index='2'] select\", {label: \"slider\"});\n\n    await window.click(\"button.save\");\n\n    await helpers.sleep(100);\n\n    var data = JSON.parse(fs.readFileSync(saverJSON));\n\n    var opt = data.options[0];\n    assert.strictEqual(\"My Option\", opt.name);\n    assert.strictEqual(\"An Option I Guess?\", opt.description);\n    assert.strictEqual(\"text\", opt.type);\n\n    opt = data.options[1];\n    assert.strictEqual(\"My Second Option\", opt.name);\n    assert.strictEqual(\"Another Option I Guess?\", opt.description);\n    assert.strictEqual(\"boolean\", opt.type);\n\n    opt = data.options[2];\n    assert.strictEqual(\"My Third Option\", opt.name);\n    assert.strictEqual(\"Here We Go Again\", opt.description);\n    assert.strictEqual(\"slider\", opt.type);\n\n    await window.click(\".saver-option-input[data-index='1'] button.remove-option\");\n    await window.click(\"button.save\");\n\n    await helpers.sleep(100);\n\n    data = JSON.parse(fs.readFileSync(saverJSON));\n\n    opt = data.options[0];\n    assert.strictEqual(\"My Option\", opt.name);\n    assert.strictEqual(\"An Option I Guess?\", opt.description);\n    assert.strictEqual(\"text\", opt.type);\n    \n    opt = data.options[1];\n    assert.strictEqual(\"My Third Option\", opt.name);\n    assert.strictEqual(\"Here We Go Again\", opt.description);\n    assert.strictEqual(\"slider\", opt.type);\n  });\n});\n"
  },
  {
    "path": "test/ui/new.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport * as helpers from \"../helpers.js\";\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nvar saversDir;\nvar workingDir;\nlet app;\n\ndescribe(\"Add New\", function() {\n  let screensaverUrl;\n\n  helpers.setupTest(this);\n\n  beforeEach(async function() {\n    screensaverUrl = \"file://\" + path.join(__dirname, \"../fixtures/screenshot.png\");\n    saversDir = helpers.getTempDir();\n    workingDir = helpers.getTempDir();\n\n    app = await helpers.application(workingDir, true);\n  });\n\n  // describe(\"when not setup\", function() {\n  //   beforeEach(async function() {\n  //     await helpers.callIpc(app, `open-window add-new ${screensaverUrl}`);\n  //   });\n\n  //   it(\"shows alert if not setup\", async function() {\n  //     const window = await helpers.waitFor(app, \"new\");\n  //     const elem = await window.$(\"body\");\n  //     const text = await elem.innerText();\n  //     assert(text.lastIndexOf(\"set a local directory\") !== -1);\n  //   });\n\n  //   it.skip(\"can set local source\", async function() {\n  //     await helpers.waitForWindow(app, windowTitle);\n  //     await helpers.waitForText(app, \"body\", \"set a local directory\", true);\n\n  //     await helpers.click(app, \"button.pick\");\n  //     await helpers.click(app, \"button.save\");\n\n  //     await helpers.sleep(100);\n\n  //     assert.equal(\"/not/a/real/path\", currentPrefs().localSource);\n  //     const res = await helpers.getElementText(app, \"body\");\n  //     assert(res.lastIndexOf(\"Use this form\") !== -1);\n  //   });\n  // });\n\n  describe(\"when setup\", function() {\n    let window;\n\n    beforeEach(async function() {\n      helpers.addLocalSource(workingDir, saversDir);\n      await helpers.callIpc(app, `open-window add-new ${screensaverUrl}`);\n      window = await helpers.waitFor(app, \"new\");\n    });\n\n    it(\"creates screensaver and shows editor\", async function() {\n      const src = path.join(saversDir, \"a-new-name\", \"saver.json\");\n\n      await window.fill(\"[name='name']\", \"A New Name\");\n      await window.fill(\"[name='description']\", \"A Thing I Made?\");\n      await window.click(\"button.save\");\n      await helpers.waitFor(app, \"editor\");\n\n      assert(fs.existsSync(src));\n    });\n  });\n});\n"
  },
  {
    "path": "test/ui/prefs.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \"../helpers.js\";\nimport SaverPrefs from \"../../src/lib/prefs.js\";\n\nlet app;\nvar workingDir;\nvar saversDir;\n\ndescribe(\"Prefs\", function() { \n  helpers.setupTest(this);\n\n  let currentPrefs = function() {\n    return new SaverPrefs(workingDir);\n  };\n\n  let window;\n\n  beforeEach(async function() {\n    workingDir = helpers.getTempDir();\n    saversDir = helpers.getTempDir();\n    \n    helpers.setupConfig(workingDir);\n    helpers.addLocalSource(workingDir, saversDir);\n    helpers.addSaver(saversDir, \"saver-one\", \"saver.json\");\n\n    app = await helpers.application(workingDir, true);\n    await helpers.callIpc(app, \"open-window prefs\");\n    window = await helpers.waitFor(app, \"prefs\");\n\t});\n\n  it(\"lists screensavers\", async function() {\n    await helpers.waitForText(window, \"body\", \"Screensaver One\", true);\n  });\n\n  it(\"lists included default screensavers\", async function() {\n    await helpers.waitForText(window, \"body\", \"Random\", true);\n  });\n\n  it(\"allows picking a screensaver\", async function() {\n    await helpers.waitForText(window, \"body\", \"Screensaver One\", true);\n    await window.click(\"text=Screensaver One\");\n    await helpers.waitForText(window, \".saver-description\", \"A Screensaver\", true);\n    await window.click(\"button.save\");\n\n    await helpers.waitForText(window, \"body\", \"Changes saved!\", true);\n    console.log(currentPrefs().saver);\n    assert(currentPrefs().saver.lastIndexOf(\"saver-one\") !== -1);\n  });\n\n  it(\"sets options for screensaver\", async function() {\n    await helpers.waitForText(window, \"body\", \"Screensaver One\", true);\n    await window.click(\"text=Screensaver One\");\n\n    await helpers.waitForText(window, \"body\", \"Load the specified URL\", true);\n    await window.click(\"[name='sound'][value='false']\");\n\n    await window.fill(\"[name='load_url']\", \"barfoo\");\n    await window.click(\"button.save\");\n\n    await helpers.waitForText(window, \"body\", \"Changes saved!\", true);\n\n    var options = currentPrefs().options;\n    var k = Object.keys(options).find((i) => {\n      return i.indexOf(\"saver-one\") !== -1;\n    });\n\n    assert.strictEqual(\"barfoo\", options[k].load_url);\n    assert(!options[k].sound);\n  });\n\n  it(\"sets timing options\", async function() {\n    await helpers.waitForText(window, \"body\", \"Activate after\", true);\n\n    await window.selectOption(\"[name=delay]\", {label: \"30 minutes\"});\n    await window.selectOption(\"[name=sleep]\", {label: \"15 minutes\"});\n\n    await window.click(\"button.save\");\n    await helpers.waitForText(window, \"body\", \"Changes saved!\", true);\n\n    assert.strictEqual(30, currentPrefs().delay);\n    assert.strictEqual(15, currentPrefs().sleep);\n  });\n});\n"
  },
  {
    "path": "test/ui/settings.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \"../helpers.js\";\nimport SaverPrefs from \"../../src/lib/prefs.js\";\n\nlet app;\nvar workingDir;\nvar saversDir;\n\ndescribe(\"Settings\", function() { \n  const closeWindowDelay = 750;\n  let window;\n\n  helpers.setupTest(this);\n\n  let currentPrefs = function() {\n    return new SaverPrefs(workingDir).data;\n  };\n\n  beforeEach(async function() {\n    workingDir = helpers.getTempDir();\n    saversDir = helpers.getTempDir();\n    \n    helpers.setupConfig(workingDir);\n    helpers.addLocalSource(workingDir, saversDir);\n    helpers.addSaver(saversDir, \"saver-one\", \"saver.json\");\n\n    app = await helpers.application(workingDir, true);\n    await helpers.callIpc(app, \"open-window prefs\");\n    await helpers.waitForWindow(app, \"Before Dawn: Preferences\");\n\n    window = await helpers.waitFor(app, \"prefs\");\n    await window.click(\"button.settings\");\n\n    window = await helpers.waitFor(app, \"settings\");\n  });\n\n  it(\"toggles checkboxes\", async function() {\n    let oldConfig = currentPrefs();\n\n    await window.check(\"text=Lock screen after running\");\n    await window.check(\"text=Disable when on battery?\");\n    await window.uncheck(\"text=Auto start on login?\");\n    await window.uncheck(\"text=Only run on the primary display?\");\n    await window.click(\"button.save\");\n\n    await helpers.sleep(closeWindowDelay);\n\n    let updatedPrefs = currentPrefs();\n    assert.strictEqual(!oldConfig.lock, updatedPrefs.lock);\n    assert.strictEqual(!oldConfig.disableOnBattery, updatedPrefs.disableOnBattery);\n    assert.strictEqual(!oldConfig.auto_start, updatedPrefs.auto_start);\n    assert.strictEqual(!oldConfig.runOnSingleDisplay, updatedPrefs.runOnSingleDisplay);\n\n    await helpers.waitFor(app, \"prefs\");\n  });\n\n  it(\"leaves checkboxes\", async function() {\n    let oldConfig = currentPrefs();\n\n    await window.click(\"button.save\");\n    await helpers.sleep(closeWindowDelay);\n\n    let updatedPrefs = currentPrefs();\n    assert.strictEqual(oldConfig.lock, updatedPrefs.lock);\n    assert.strictEqual(oldConfig.disableOnBattery, updatedPrefs.disableOnBattery);\n    assert.strictEqual(oldConfig.auto_start, updatedPrefs.auto_start);\n    assert.strictEqual(oldConfig.runOnSingleDisplay, updatedPrefs.runOnSingleDisplay);\n\n    await helpers.waitFor(app, \"prefs\");\n  });\n  \n  // it.skip(\"allows setting path via dialog\", async function() {\n  //   const [fileChooser] = await Promise.all([\n  //     window.waitForEvent(\"filechooser\"),\n  //     window.click(\"button.pick\")\n  //   ]);\n  //   await fileChooser.setFiles(\"/not/a/real/path\");\n\n  //   await window.click(\"button.save\");\n  //   await helpers.sleep(closeWindowDelay);\n\n  //   assert.strictEqual(\"/not/a/real/path\", currentPrefs().localSource);\n\n  //   await helpers.waitFor(app, \"prefs\");\n  // });\n\n  it(\"clears localSource\", async function() {\n    let ls = currentPrefs().localSource;\n    assert( ls != \"\" && ls !== undefined);\n\n    await window.click(\"button.clear\");\n    await helpers.sleep(50);\n    await window.click(\"button.save\");\n\n    await helpers.sleep(closeWindowDelay);\n\n    assert.strictEqual(\"\", currentPrefs().localSource);\n\n    await helpers.waitFor(app, \"prefs\");\n  });\n\n\n  // // dialogs don't work yet\n  // // @see https://github.com/microsoft/playwright/issues/8278\n  // it.skip(\"resets defaults\", async function() {\n  //   window = await helpers.waitFor(app, \"settings\");\n\n  //   window.on(\"dialog\", async dialog => {\n  //     console.log(dialog.message());\n  //     await dialog.accept();\n  //   });\n\n  //   await window.click(\"button.reset-to-defaults\");\n  //   await helpers.waitForText(window, \"body\", \"Settings reset\", true);\n  //   await helpers.sleep(closeWindowDelay);\n\n  //   assert.strictEqual(\"\", currentPrefs().localSource);\n\n  //   await helpers.waitFor(app, \"prefs\");\n  // });\n});\n"
  },
  {
    "path": "test/ui/tray.js",
    "content": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \"../helpers.js\";\n\ndescribe(\"tray\", function() {\n  var workingDir;\n  let saversDir;\n  let app;\n  let window;\n\n  helpers.setupTest(this);\n\n  beforeEach(async function() {\n    workingDir = helpers.getTempDir();\n    saversDir = helpers.getTempDir();\n    let saverJSONFile = helpers.addSaver(saversDir, \"saver\");\n\n    helpers.setupConfig(workingDir, \"config\", {\n      \"firstLoad\": false,\n      \"sourceRepo\": \"\",\n      \"localSource\": saversDir,\n      \"saver\": saverJSONFile \n    });\n\n    app = await helpers.application(workingDir, true);\n    await helpers.waitForWindow(app, 'test shim');\n\n    window = await helpers.getWindowByTitle(app, 'test shim'); \n  });\n\n  describe(\"run now\", function() {\n   it(\"opens screensaver\", async function() {\n    await window.click(\"button.RunNow\");\n    await helpers.waitForText(window, \"#currentState\", \"running\");\n   });\n  });\n\n  describe(\"preferences\", function() {\n    it(\"opens prefs window\", async function() {\n      await window.click(\"button.Preferences\");\n      assert(await helpers.waitForWindow(app, \"Before Dawn: Preferences\"));\n    });\n  });\n\n  describe(\"about\", function() {\n    it(\"opens about window\", async function() {\n      await window.click(\"button.AboutBeforeDawn\");\n      await helpers.waitForWindow(app, \"Before Dawn: About!\");\n      assert(await helpers.getWindowByTitle(app, \"Before Dawn: About!\"));\n    });\n  });\n\n  describe(\"enable/disable\", function() {\n    it(\"toggles app status\", async function() {\n      await helpers.waitForText(window, \"body\", \"idle\");\n\n\n      await window.click(\"button.Disable\");\n      await helpers.waitForText(window, \"body\", \"paused\");\n\n      await window.click(\"button.Enable\");\n      await helpers.waitForText(window, \"body\", \"idle\");\n    });\n  });\n});\n"
  },
  {
    "path": "tools/build-packages.sh",
    "content": "#!/bin/bash\n\nDEST=\"/tmp/before-dawn-packages\"\nTARGET=\"$1\"\nWORKING_DIR=\"/tmp/before-dawn-build\"\nREPO=\"https://github.com/muffinista/before-dawn.git\"\n\nSTART_DIR=`pwd`\n\necho \"== Cleaning up $WORKING_DIR\"\n\nrm -rf $WORKING_DIR\nmkdir -p $WORKING_DIR\n\nif [ \"$LOCAL_BUILD\" == \"1\" ]; then\n    echo \"== Building from local copy ==\"\n    cp -r . $WORKING_DIR/\nelse\n    echo \"== Checking Out Code ==\"\n    git clone $REPO $WORKING_DIR\nfi\n\ncd $WORKING_DIR   \n\necho \"== Building assets ==\"\ncd app\nnpm install --save-dev\ngrunt\n\nrm -rf node_modules\n\ncd ..\n\necho \"== Cleaning out node packages ==\"\nnpm prune\n\necho \"== Installing node packages ==\"\nnpm install\n\necho \"== BUILDING APP ==\"\nnpm run dist\n\necho \"== Copying to $START_DIR/dist\"\n\nmkdir -p \"$START_DIR/dist\"\ncp -r $WORKING_DIR/dist/* \"$START_DIR/dist\"\n"
  },
  {
    "path": "tools/update-build-version.js",
    "content": "'use strict';\nconst fs = require('fs');\nconst path = require('path');\n\nvar version = JSON.parse(fs.readFileSync(\"package.json\")).version;\n\nconsole.log(\"Specifying v\" + version);\n\nvar build = JSON.parse(fs.readFileSync(\"build.json\"));\n\nbuild.win.version = version;\nbuild.osx.version = version;\nbuild.linux.version = version;\n\nconsole.log(build);\n\nfs.writeFileSync(\"build.json\", JSON.stringify(build, null, 4));\n"
  },
  {
    "path": "webpack.config.js",
    "content": "import mainConfig from \"./webpack.main.config.js\";\nimport rendererConfig from \"./webpack.renderer.config.js\";\nexport default [mainConfig, rendererConfig];\n"
  },
  {
    "path": "webpack.main.config.js",
    "content": "\"use strict\";\n\nimport * as path from \"path\";\nimport webpack from \"webpack\";\nimport \"dotenv/config\";\n\nimport CopyWebpackPlugin from \"copy-webpack-plugin\";\nimport { CleanWebpackPlugin } from \"clean-webpack-plugin\";\nimport { sentryWebpackPlugin } from \"@sentry/webpack-plugin\";\nimport ESLintPlugin from \"eslint-webpack-plugin\";\nimport { readFile } from 'fs/promises';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst packageJSON = JSON.parse(\n  await readFile(\n    new URL('./package.json', import.meta.url)\n  )\n);\n\n\nconst dependencies = packageJSON.dependencies;\nconst optionalDependencies = packageJSON.optionalDependencies || {};\n\nconst outputDir = path.join(__dirname, \"output\");\n\nconst COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;\n\n//\n// get a list of node dependencies, and then\n// convert it to an array of package names\n// this prevents some warnings like:\n//\n//   Critical dependency: the request of a dependency is an expression\n//\n// and\n//\n//   ERROR in ./src/main/fullscreen.js\n//   Module not found: Error: Can't resolve 'winctl'\n//\n// Basically, webpack falls down when you're including node modules\nconst deps = [].concat(\n  Object.keys(dependencies),\n  Object.keys(optionalDependencies)\n);\n\n\nlet mainConfig = {\n  devtool: \"source-map\",\n  mode: (process.env.NODE_ENV === \"production\" ? \"production\" : \"development\"),\n  entry: {\n    main: path.join(__dirname, \"src\", \"main\", \"index.js\")\n  },\n  experiments: {\n    outputModule: true,\n  },\n  externals: deps,\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\",\n          options: {\n            presets: [\"@babel/preset-env\"]\n          }\n        }\n      },\n      {\n        test: /\\.node$/,\n        use: \"node-loader\"\n      }\n    ]\n  },\n  node: {\n    __dirname: false,\n    __filename: false\n  },\n  optimization: {\n    emitOnErrors: false,\n    nodeEnv: (process.env.NODE_ENV === \"production\" ? \"production\" : \"development\")\n  },\n  output: {\n    filename: \"[name].js\",\n    path: outputDir,\n    sourceMapFilename: \"[name].js.map\",\n    chunkFormat: \"module\",\n    module: true\n  },\n  plugins: [\n    new ESLintPlugin({\n      fix: false,\n      configType: 'flat'\n    }),\n    new CleanWebpackPlugin({\n      cleanOnceBeforeBuildPatterns: []\n    }),\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: path.join(__dirname, \"package.json\"),\n          to: path.join(outputDir)\n        },\n        {\n          from: path.join(__dirname, \"src\", \"main\", \"assets\"),\n          to: path.join(outputDir, \"assets\"),\n        },\n        {\n          from: path.join(__dirname, \"src\", \"main\", \"system-savers\"),\n          to: path.join(outputDir, \"system-savers\"),\n        }\n      ]\n    })\n  ],\n  resolve: {\n    extensions: [\".js\", \".json\"],\n    fallback: {\n      \"child_process\": false,\n      \"url\": false,\n      \"fs\": false,\n      \"path\": false,\n      \"os\": false,\n      \"stream\": false,\n      \"stream/promises\": false,\n    }\n  },\n  target: \"electron-main\"\n};\n\n/**\n * Adjust mainConfig for development/production settings\n */\nif (process.env.NODE_ENV === \"production\") {\n  mainConfig.devtool = \"source-map\";\n  \n  if ( process.env.SENTRY_DSN ) {\n    mainConfig.plugins.push(\n      new webpack.EnvironmentPlugin([\"SENTRY_DSN\"])\n    );\n  }\n\n  mainConfig.plugins.push(\n    new webpack.DefinePlugin({\n      \"process.env.NODE_ENV\": JSON.stringify(\"production\"),\n      \"process.env.BEFORE_DAWN_RELEASE_NAME\": JSON.stringify(COMMIT_SHA),\n    })\n  );\n\n  if ( process.env.SENTRY_AUTH_TOKEN && !process.env.DISABLE_SENTRY ) {\n    mainConfig.plugins.push(\n      sentryWebpackPlugin({\n        include: \"src\",\n        ignoreFile: \".sentrycliignore\",\n        ignore: [\"node_modules\", \"webpack.config.js\", \"webpack.main.config.js\", \"webpack.renderer.config.js\"],\n        org: \"colin-mitchell\",\n        project: \"before-dawn\",\n        authToken: process.env.SENTRY_AUTH_TOKEN,\n        release: COMMIT_SHA,\n      })\n    );\n  }\n}\n\nexport default mainConfig;\n"
  },
  {
    "path": "webpack.renderer.config.js",
    "content": "\"use strict\";\n\nimport * as path from \"path\";\nimport webpack from \"webpack\";\nimport \"dotenv/config\";\n\nimport HtmlWebpackPlugin from \"html-webpack-plugin\";\nimport MiniCssExtractPlugin from \"mini-css-extract-plugin\";\nimport { sentryWebpackPlugin } from \"@sentry/webpack-plugin\";\nimport ESLintPlugin from \"eslint-webpack-plugin\";\nimport { readFile } from 'fs/promises';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst packageJSON = JSON.parse(\n  await readFile(\n    new URL('./package.json', import.meta.url)\n  )\n);\n\nconst productName = packageJSON.productName;\nconst outputDir = path.join(__dirname, \"output\");\n\nconst COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;\n\nvar htmlPageOptions = function(id, title) {\n  return {\n    filename: `${id}.html`,\n    template: path.resolve(__dirname, \"src/index.ejs\"),\n    id: id,\n    title: `${productName}: ${title}`,\n    minify: {\n      collapseWhitespace: false,\n      removeAttributeQuotes: false,\n      removeComments: false\n    }\n  };\n};\n\n/**\n * List of node_modules to include in webpack bundle\n *\n * Required for specific packages like Vue UI libraries\n * that provide pure *.vue files that need compiling\n * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals\n */\n\nlet rendererConfig = {\n  devtool: \"source-map\",\n  entry: {\n    renderer: path.join(__dirname, \"src\", \"renderer\", \"main.js\")\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(sa|sc|c)ss$/,\n        use: [\n          MiniCssExtractPlugin.loader,\n          \"css-loader\",\n          \"sass-loader\",\n        ]\n      },\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: \"babel-loader\",\n          options: {\n            presets: [\"@babel/preset-env\"]\n          }\n        }\n      },\n      {\n        test: /\\.svelte$/,\n        use: {\n            loader: \"svelte-loader\",\n            options: {\n              emitCss: true,\n            }\n        },\n      },\n      {\n        // required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4\n        test: /node_modules\\/svelte\\/.*\\.mjs$/,\n        resolve: {\n          fullySpecified: false\n        }\n      },\n    ]\n  },\n  node: {\n    __dirname: false,\n    __filename: false\n  },\n  plugins: [\n    new ESLintPlugin({\n      fix: false,\n      configType: 'flat',\n      extensions: [\"js\"]\n    }),\n    new HtmlWebpackPlugin(htmlPageOptions(\"prefs\", \"Preferences\")),\n    new HtmlWebpackPlugin(htmlPageOptions(\"settings\", \"Settings\")),\n    new HtmlWebpackPlugin(htmlPageOptions(\"editor\", \"Editor\")),    \n    new HtmlWebpackPlugin(htmlPageOptions(\"new\", \"Create Screensaver!\")),\n    new HtmlWebpackPlugin(htmlPageOptions(\"about\", \"About!\")),\n    new MiniCssExtractPlugin({\n      filename: \"[name].css\",\n      chunkFilename: \"[id].css\"\n    }),\n  ],\n  optimization: {\n    emitOnErrors: false,\n    nodeEnv: (process.env.NODE_ENV === \"production\" ? \"production\" : \"development\")\n  },\n  output: {\n    filename: \"[name].js\",\n    library: \"[name]\",\n    libraryTarget: \"var\",\n    path: outputDir,\n    publicPath: \"\"\n  },\n  mode: (process.env.NODE_ENV === \"production\" ? \"production\" : \"development\"),\n  resolve: {\n    alias: {\n      // handy alias for the root path of render files\n      \"@\": path.join(__dirname, \"src\", \"renderer\"),\n      \"~\": path.join(__dirname, \"src\")\n    },\n    extensions: [\".js\", \".json\", \".css\", \".svelte\"],\n    conditionNames: [\"svelte\", \"browser\", \"import\"]\n  },\n  target: \"web\"\n};\n\n\n/**\n * Adjust rendererConfig for production settings\n */\nif (process.env.NODE_ENV === \"production\") {\n  rendererConfig.devtool = \"source-map\";\n\n  if ( process.env.SENTRY_DSN ) {\n    rendererConfig.plugins.push(\n      new webpack.EnvironmentPlugin([\"SENTRY_DSN\"])\n    );\n  }\n\n  rendererConfig.plugins.push(\n    new webpack.DefinePlugin({\n      \"process.env.NODE_ENV\": JSON.stringify(\"production\"),\n      \"process.env.BEFORE_DAWN_RELEASE_NAME\": JSON.stringify(COMMIT_SHA),\n    }),\n    new webpack.LoaderOptionsPlugin({\n      minimize: true\n    })\n  );\n\n  if ( process.env.SENTRY_AUTH_TOKEN && !process.env.DISABLE_SENTRY ) {\n    rendererConfig.plugins.push(\n      sentryWebpackPlugin({\n        include: \"src\",\n        ignoreFile: \".sentrycliignore\",\n        ignore: [\"node_modules\", \"webpack.config.js\", \"webpack.main.config.js\", \"webpack.renderer.config.js\"],\n        org: \"colin-mitchell\",\n        project: \"before-dawn\",\n        authToken: process.env.SENTRY_AUTH_TOKEN,\n        release: COMMIT_SHA,\n      })\n    );\n  }\n}\n\nexport default rendererConfig;\n"
  }
]