Showing preview only (336K chars total). Download the full file or copy to clipboard to get everything.
Repository: muffinista/before-dawn
Branch: main
Commit: f873649102d4
Files: 139
Total size: 305.3 KB
Directory structure:
gitextract_ufwarflo/
├── .babelrc
├── .browserslistrc
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ ├── eslint.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── assets/
│ ├── icon-paused.xcf
│ ├── icon.xcf
│ └── monitor-overlay.psd
├── bin/
│ ├── build-icon.js
│ ├── build-on-ci.js
│ ├── capture-screens.js
│ ├── dev-runner.js
│ ├── download-screensavers.js
│ ├── generate-release.js
│ └── get-release-name
├── build/
│ └── icon.icns
├── code_of_conduct.md
├── docs/
│ ├── .gitignore
│ ├── .ruby-version
│ ├── Gemfile
│ ├── _config.yml
│ ├── _includes/
│ │ ├── footer.html
│ │ ├── head.html
│ │ └── header.html
│ ├── _layouts/
│ │ ├── default.html
│ │ ├── page.html
│ │ └── post.html
│ ├── _posts/
│ │ └── 2016-12-02-welcome-to-jekyll.markdown
│ ├── _sass/
│ │ ├── _base.scss
│ │ ├── _layout.scss
│ │ └── _syntax-highlighting.scss
│ ├── about.md
│ ├── contributing.md
│ ├── creating.md
│ ├── css/
│ │ └── main.scss
│ ├── index.html
│ ├── installing.md
│ └── preferences.md
├── eslint.config.mjs
├── lefthook.yml
├── mise.toml
├── package.json
├── src/
│ ├── css/
│ │ └── styles.scss
│ ├── index.ejs
│ ├── lib/
│ │ ├── package.js
│ │ ├── prefs-schema.json
│ │ ├── prefs.js
│ │ ├── saver-factory.js
│ │ ├── saver-list.js
│ │ └── saver.js
│ ├── main/
│ │ ├── assets/
│ │ │ ├── global.css
│ │ │ ├── grabber.html
│ │ │ ├── grabber.mjs
│ │ │ ├── preload.mjs
│ │ │ ├── shim.html
│ │ │ └── shim.js
│ │ ├── autostarter.js
│ │ ├── bootstrap.js
│ │ ├── dock.js
│ │ ├── index.dev.js
│ │ ├── index.js
│ │ ├── menus.js
│ │ ├── power.js
│ │ ├── release_check.js
│ │ ├── screen.js
│ │ ├── state_manager.js
│ │ ├── system-savers/
│ │ │ ├── __template/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ ├── blank/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ ├── dimmer/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ └── random/
│ │ │ ├── index.html
│ │ │ └── saver.json
│ │ └── windows.js
│ └── renderer/
│ ├── AboutScreen.svelte
│ ├── EditorScreen.svelte
│ ├── NewScreensaverScreen.svelte
│ ├── PrefsScreen.svelte
│ ├── SettingsScreen.svelte
│ ├── components/
│ │ ├── FolderChooser.svelte
│ │ ├── Notarize.js
│ │ ├── SaverForm.svelte
│ │ ├── SaverList.svelte
│ │ ├── SaverOptionInput.svelte
│ │ ├── SaverOptions.svelte
│ │ ├── SaverSummary.svelte
│ │ ├── Spinner.svelte
│ │ └── icons/
│ │ ├── BugIcon.svelte
│ │ ├── FolderIcon.svelte
│ │ ├── ReloadIcon.svelte
│ │ └── SaveIcon.svelte
│ └── main.js
├── test/
│ ├── fixtures/
│ │ ├── bad-config.json
│ │ ├── config-2.json
│ │ ├── config-with-options.json
│ │ ├── config.json
│ │ ├── default-repo.json
│ │ ├── index.html
│ │ ├── invalid.json
│ │ ├── no-requirements.json
│ │ ├── old-config.json
│ │ ├── power/
│ │ │ ├── linux-charged.txt
│ │ │ ├── linux-charging.txt
│ │ │ └── linux-discharging.txt
│ │ ├── release-no-update.json
│ │ ├── release.json
│ │ ├── releases/
│ │ │ └── updates.json
│ │ ├── saver.json
│ │ ├── saver2.json
│ │ └── test-savers.json
│ ├── helpers.js
│ ├── lib/
│ │ ├── package.js
│ │ ├── prefs.js
│ │ ├── saver-factory.js
│ │ ├── saver-list.js
│ │ └── saver.js
│ ├── main/
│ │ ├── power.js
│ │ ├── release_check.js
│ │ └── state_manager.js
│ └── ui/
│ ├── about.js
│ ├── bootstrap.js
│ ├── editor.js
│ ├── new.js
│ ├── prefs.js
│ ├── settings.js
│ └── tray.js
├── tools/
│ ├── build-packages.sh
│ └── update-build-version.js
├── webpack.config.js
├── webpack.main.config.js
└── webpack.renderer.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["@babel/preset-env"]
}
================================================
FILE: .browserslistrc
================================================
last 1 electron version
================================================
FILE: .dockerignore
================================================
build
dist
output
tools
docs
assets
coverage
node_modules
.git
.gitignore
.electron-symbols
.DS_Store
.env
.nyc_output
================================================
FILE: .github/workflows/ci.yml
================================================
name: Run tests
on: push
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
steps:
- name: Install ubuntu requirements
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
- uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Specify MSVS version
if: ${{ matrix.os == 'windows-latest' }}
shell: powershell
run: |
echo "GYP_MSVS_VERSION=2022" >> $env:GITHUB_ENV
- run: npm ci
- run: npm rebuild
- run: npm run test-lib
- name: Run integration tests
if: ${{ matrix.os != 'ubuntu-latest' }}
run: npm run test-ui
- uses: actions/upload-artifact@v4
if: always()
with:
name: logs
#/Users/runner/Library/Logs/mocha/
# ~/.config/Before Dawn/logs/
path: |
C:\Users\runneradmin\AppData\Roaming\mocha\logs\
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '42 16 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout code
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
================================================
FILE: .github/workflows/eslint.yml
================================================
name: eslint
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
# Set the job key. The key is displayed as the job name
# when a job name is not provided
eslint:
# Name the Job
name: Run eslint
# Set the type of machine to run on
runs-on: ubuntu-latest
steps:
- name: Install ubuntu requirements
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
# Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup code
run: npm ci
- name: Run eslint
run: npm run eslint-all
================================================
FILE: .github/workflows/release.yml
================================================
name: Build release
on:
push:
branches:
- main
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
# https://stackoverflow.com/questions/77251296/distutils-not-found-when-running-npm-install
PYTHON: 3.11
steps:
- name: Install ubuntu requirements
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
- name: Checkout code
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Specify MSVS version
if: ${{ matrix.os == 'windows-latest' }}
shell: powershell
run: |
echo "GYP_MSVS_VERSION=2022" >> $env:GITHUB_ENV
- run: npm ci
- run: npm rebuild
- name: Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: npm run release
================================================
FILE: .gitignore
================================================
.DS_Store
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
bower_components
*.map
.sass-cache
.nyc_output
output/*
dist/*
.electron-symbols
.env
data/savers
data/*.*
.vscode/*
TODO
notes.txt
bin/sentry.properties
# ignore some assets i don't want in git
assets/noun*
# ignore VS code file
*.code-workspace
# Sentry Config File
.env.sentry-build-plugin
================================================
FILE: .nvmrc
================================================
22.21.1
================================================
FILE: CHANGELOG.md
================================================
# Change Log
## [v0.9.25](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-04-05)
- Fix some issues with the preview tool, which wasn't properly passing
params to the screensaver preview
## [v0.9.24](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-03-15)
- Save settings when 'create screensaver' button is clicked
- Fix issue where screensaver data wasn't loading properly
- Remove lodash from codebase
- Add environment variable toggle for running in development mode without hot reload
- Fix issue with updating timestamp of package update checks
- Remove command-line argument parsing
- Switch a lot of callback-based code to use promises
- Attempt to deal with failed download issues
## [v0.9.23](https://github.com/muffinista/before-dawn/tree/v0.9.23) (2018-02-23)
- Fix logging issue
## [v0.9.22](https://github.com/muffinista/before-dawn/tree/v0.9.22) (2018-02-22)
- Fix issue where screensaver package would be downloaded twice at the
same time, which is definitely a bad thing.
## [v0.9.21](https://github.com/muffinista/before-dawn/tree/v0.9.21) (2018-02-19)
- Add preferences option to run on a single display -- this helps keep
CPU load down
- Add a 'yes/no' boolean option for screensavers -- this can be used
to add boolean fields for screensavers.
- Minor updates to styles -- I'm working on cleaning up the app styles
and CSS
- Keep app running if user quits the main window
- Activate asar for builds -- this should make downloads smaller and
maybe help a bit with performance.
- Cleanup logging output and the code used to setup auto launching
- Update electron version
## [v0.9.20](https://github.com/muffinista/before-dawn/tree/v0.9.20) (2018-01-07)
- Fix some issues with proper release checking -- it was broken before
## [v0.9.19](https://github.com/muffinista/before-dawn/tree/v0.9.19) (2018-01-06)
- Fix issue with missing version data
## [v0.9.18](https://github.com/muffinista/before-dawn/tree/v0.9.18) (2018-01-04)
- Make sure delay/sleep values are integers
## [v0.9.17](https://github.com/muffinista/before-dawn/tree/v0.9.17) (2018-01-03)
- Update method of displaying screensaver windows since things seem to
have broken in High Sierra
- Tweak fullscreen.js to handle OSX issues
- Add some logging code to savers library
- Handle screensaver load/parse errors rather than entirely failing
- Update Electron version
## [v0.9.16](https://github.com/muffinista/before-dawn/tree/v0.9.16) (2017-12-14)
- Notify main process when user preferences have been updated
- Disable 'Save' button when creating new screensaver, but local
directory isn't setup.
- Remove crash reporter, since it's basically unused
## [v0.9.15](https://github.com/muffinista/before-dawn/tree/v0.9.15) (2017-12-14)
- Update raven/sentry setup, fix some issues with paths in the app
## [v0.9.14](https://github.com/muffinista/before-dawn/tree/v0.9.14) (2017-12-11)
- Fix some issues with setting local directory properly
## [v0.9.13](https://github.com/muffinista/before-dawn/tree/v0.9.13) (2017-12-08)
- Replaced React with Vue.js, and did a lot of refactoring and cleanup
of javascript in general. Most of the UI code is in Vue components
now. Previously, it was a mix of React, vanilla JS, and some jQuery.
- Moved code/asset compilation into webpack. This makes development a
little easier to manage.
- Updated how data and objects get passed between the UI and the main
process, which should help make the app more performant.
- Added some tweaks to hopefully take care of some annoying OSX
security issues.
## [v0.9.12](https://github.com/muffinista/before-dawn/tree/v0.9.12) (2017-11-17)
- Tweak some temp directory usage to try and fix some OSX issues
## [v0.9.11](https://github.com/muffinista/before-dawn/tree/v0.9.11) (2017-11-16)
- Add some safety checks to config reading -- if it's corrupted
somehow, just restart with a clean config
- Pass the savers module to windows when opening them -- I think this
is faster than passing data back and forth
## [v0.9.10](https://github.com/muffinista/before-dawn/tree/v0.9.10) (2017-11-13)
- Add a random screensaver picker, as well as basic 'system
screensaver' support -- ie, screensavers that are integral to the
application and not installed as a separate package.
- If 'Run Now' chosen in menu, don't check power state
- Improve dock display -- show icon for more windows and hide only
when all windows are closed
- Tweak layout of prefs window and the preview tool
- Update main process to listen for events from windows and pass data
around. The main process has responsibilty for opening windows,
saving new screensavers, etc.
- Reorganize code for app, switch to a single package.json
- Make a bunch of calls asychronous
- Use async/await in a few places
- Add some data caching to help performance
- When launching screensavers, don't take screengrab unless
requested - this greatly speeds up launch time
- Switch to yarn, cleanup build process
- I'd prefer to not have yarn as a dependency, but it does a better
job of handling installations across multiple platforms -- ie,
windows and OSX
- Add webpack and use it to build UI assets
- I also might get rid of this at some point, and also React for
that matter
- Update bootstrap version and assorted styling
- Update electron version
- Update React version and a bunch of assorted components
- Add mocha tests
- Add appveyor and Travis builds
- Update some stale packages, and remove some dead ones
## [v0.9.9](https://github.com/muffinista/before-dawn/tree/v0.9.9) (2017-11-13)
- This version was yanked before it had a chance to truly shine. RIP
## [v0.9.8](https://github.com/muffinista/before-dawn/tree/v0.9.8) (2017-08-03)
- Minor bug fixes
## [v0.9.7](https://github.com/muffinista/before-dawn/tree/v0.9.7) (2017-07-28)
- Scroll to the currently selected screensaver when rendering prefs
panel
- Handle missing screensaver object in watcher window
## [v0.9.6](https://github.com/muffinista/before-dawn/tree/v0.9.6) (2017-07-11)
- Fix some issues with loading screensavers from folders with spaces in their name
- Add some handlers for power on/off events
- Close running screensavers when the display count changes (the user
has plugged/unplugged a monitor)
## [v0.9.5](https://github.com/muffinista/before-dawn/tree/v0.9.5) (2017-06-27)
- Fix some bugs that can occur when setting a custom screensaver
source directory that either doesn't exist or is empty.
## [v0.9.4](https://github.com/muffinista/before-dawn/tree/v0.9.4) (2017-06-09)
- Disable ASAR packages. I think there's a few things that are broken
when they are being used, and I want to have the whole app running
more smoothly before switching back to them.
- Add link to issues URL so users can report bugs.
- Fix bug where we tried to render preview when a screensaver hadn't
been selected yet.
## [v0.9.3](https://github.com/muffinista/before-dawn/tree/v0.9.3) (2017-06-01)
- Tweak state machine to rely on idle time checks and not much else
- Fix bug with (I think) newer versions of Electron where opening a
BrowserWindow would trigger a reset of idle time on Windows.
- Hide mouse by using robotjs to move mouse off screen when showing screensaver
- Build ASAR packages
- Implement crash reporting, and some sentry.io error reporting
- Add a background color to boot process to look a little nicer
- Optimize screen grabber code, fix some CPU spikes
- Assorted library/code updates
## [v0.9.2](https://github.com/muffinista/before-dawn/tree/v0.9.2) (2017-03-01)
- Fix bug where "don't run on battery" would always be true
- Tweak fullscreen detection code a bit, move into its own module
- Move some platform-specific deps into 'optionalDependencies'
## [v0.9.0](https://github.com/muffinista/before-dawn/tree/v0.9.0) (2017-03-01)
- Check for fullscreen apps and don't activate the screensaver if one is running
- Sort screensavers alphabetically regardless of capitalization
- Add right-click action to system tray
- If disabled, display a 'paused' icon in system tray
- Tweaked tray icon to be a little bolder
- Updated preferences display
- Fixed a case where I think the app would stop checking idle time, so it wouldn't load a screensaver
## [v0.8.3](https://github.com/muffinista/before-dawn/tree/v0.8.3) (2017-01-27)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.1...v0.8.3)
## [v0.8.1](https://github.com/muffinista/before-dawn/tree/v0.8.1) (2017-01-17)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.0...v0.8.1)
## [v0.8.0](https://github.com/muffinista/before-dawn/tree/v0.8.0) (2017-01-14)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.7.0...v0.8.0)
## [v0.7.0](https://github.com/muffinista/before-dawn/tree/v0.7.0) (2016-07-15)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.3...v0.7.0)
## [v0.6.3](https://github.com/muffinista/before-dawn/tree/v0.6.3) (2016-03-09)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.1...v0.6.3)
## [v0.6.1](https://github.com/muffinista/before-dawn/tree/v0.6.1) (2016-03-05)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.0...v0.6.1)
## [v0.6.0](https://github.com/muffinista/before-dawn/tree/v0.6.0) (2016-03-04)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.5.0...v0.6.0)
## [v0.5.0](https://github.com/muffinista/before-dawn/tree/v0.5.0) (2016-02-23)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.4.0...v0.5.0)
## [v0.4.0](https://github.com/muffinista/before-dawn/tree/v0.4.0) (2016-02-21)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.3...v0.4.0)
## [v0.3](https://github.com/muffinista/before-dawn/tree/v0.3) (2016-02-20)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.2...v0.3)
## [v0.2](https://github.com/muffinista/before-dawn/tree/v0.2) (2016-02-10)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.1...v0.2)
## [v0.1](https://github.com/muffinista/before-dawn/tree/v0.1) (2016-02-04)
================================================
FILE: LICENSE.txt
================================================
The MIT License
Copyright (c) 2016 Colin Mitchell http://muffinlabs.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Before Dawn
Before Dawn is a an open-source, cross-platform screensaver application using
web-based technologies. You can generate screensavers with it using HTML/CSS,
javascript, canvas, and any tools that rely on those technologies. In theory,
generating a Before Dawn screensaver is as simple as writing an HTML page.
The project developed out of a personal project to explore the history of early
screensavers. I decided that I wanted to write a framework that I could use to
actually run screensavers on my computer. I wanted it to be cross-platform and
easily accessible to artists and developers.
Before Dawn is definitely a bit of a experiment -- to actually use it, you need
to run it as a separate application on your computer and disable whatever
screensaver you have running in your OS, but it is fun and definitely works.
The core of the app is built on [Electron](https://www.electronjs.org/), a system
that allows you to build desktop applications that run on
[node.js](https://nodejs.org/) and are rendered via Chrome.
There's a bunch of screensavers to choose from. Here's the Flying Emoji screensaver:

You can get a quick preview of the other screensavers via this [preview
page](http://muffinista.github.io/before-dawn-screensavers/).
The first time you open the application, the preferences window will open. You
can preview and pick screensavers there:

There's an 'Advanced Settings' section where you can specify certain options for
how the application should run.

## Downloads
Installers are available from the
[releases](https://github.com/muffinista/before-dawn/releases) page.
## Status
Right now the application itself is pretty stable. This repo includes the main
code for running the actual screensaver, a simple app for picking your
screensaver and setting some options, and a bunch of modules to pull it all
together.
The actual code for the screensavers is in [a separate
repo](https://github.com/muffinista/before-dawn-screensavers). If you want to
write a screensaver, please add it to the project via a PR!
## Running It
The easiest way to use the tool is to install it to your computer. You can grab
an installer from the
[releases](https://github.com/muffinista/before-dawn/releases) page. Binaries
are available for OSX and Windows, and there's an experimental release for
Ubuntu/Debian.
Once it's running, there will be a sunrise icon in your system tray, with a few
different options. If you click 'Preferences,' you can preview the different
screensavers, set how much idle time is required before the screensaver starts
to run, specify custom paths, etc.
Once you've set all of that up, Before Dawn will happily run in the background,
and when it detects that you have been idle, it will engage your screensaver.
That's all there is to it!
### A note for Linux/Wayland users
When using Wayland, seemingly every time it's started the app will request
permission to capture the screen. I get around this by running with
`XDG_SESSION_TYPE=x11` set.
## Building It
Steps for generating your own build Before Dawn are listed in [the
wiki](https://github.com/muffinista/before-dawn/wiki/Building-Before-Dawn)
## Hacking It
If you would like to hack on Before Dawn, there's some instructions on the
[Development page](https://github.com/muffinista/before-dawn/wiki/Development)
in the wiki. It's pretty straightforward once you have a basic setup in place.
## How to Write a Screensaver
A Before Dawn screensaver is basically just a web page running in fullscreen
mode. That said, there's a few twists to make it run as smoothly as possible.
There's a bunch of specific implementation details in [the
wiki](https://github.com/muffinista/before-dawn/wiki/Writing-A-Screensaver).
There's also a very basic editor mode built into Before Dawn, which will
generate some basic code for you to work from, and will make it easier to add
some configurable options to your work.
The editor has a simple preview, a form to describe the screensaver, and a
section where you can add custom options for your screensaver:

## Contributing
Contributions and suggestions are eagerly accepted. Please check out the [code
of
conduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)
before contributing.
If you find a bug or have a suggestion, you can open an issue or a pull request
here.
If you would like to add a screensaver to the program, you can submit a PR to
the
[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)
repo.
I will accept pretty much any pull request to the repository given that the
content you are posting is legal and appropriate. If you need help or have a
suggestion, please feel free to open an issue here.
## Copyright/License
Unless otherwise stated, Copyright (c) 2024 [Colin
Mitchell](http://muffinlabs.com).
Before Dawn is is distributed under the MIT licence -- Please see LICENSE.txt
for further details.
================================================
FILE: bin/build-icon.js
================================================
#!/usr/bin/env node
import "dotenv/config";
import * as path from "path";
import * as tmp from "tmp";
import * as fs from "fs";
import pngToIco from "png-to-ico";
import { Jimp } from "jimp";
const sizes = [256, 128, 48, 32, 16];
async function main() {
let outputs = [];
let pauseOutputs = [];
const image = await Jimp.read("assets/icon.png");
const pauseImage = await Jimp.read("assets/icon-paused.png");
const tmpDir = tmp.dirSync().name;
for ( let index in sizes ) {
const size = sizes[index];
console.log(size);
const name = path.join(tmpDir, `icon-${size}.png`);
await image.resize({w: size, h: size});
await image.write(name);
outputs.push(name);
const pausedName = path.join(tmpDir, `icon-paused-${size}.png`);
await pauseImage.resize({w: size, h: size});
await pauseImage.write(pausedName);
pauseOutputs.push(pausedName);
}
console.log(outputs);
const buf = await pngToIco(outputs);
fs.writeFileSync(path.join("src", "main", "assets", "icon.ico"), buf);
console.log(pauseOutputs);
const buf2 = await pngToIco(pauseOutputs);
fs.writeFileSync(path.join("src", "main", "assets", "icon-paused.ico"), buf2);
}
main().catch(e => console.error(e));
================================================
FILE: bin/build-on-ci.js
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
require("dotenv").config();
const apiUrl = "https://ci.appveyor.com/api/account/muffinista/builds";
const travisApiUrl = "https://api.travis-ci.org/repo/muffinista%2Fbefore-dawn/requests";
const body = {
"accountName": "muffinista",
"projectSlug": "before-dawn",
"branch": "main"
};
const appveyorOpts = {
method: "post",
body: JSON.stringify(body),
headers: {
"Content-type": "application/json",
"Authorization": `Bearer ${process.env.APPVEYOR_TOKEN}`
},
};
const travisBody = {
"request": {
"branch": "main"
}
};
const travisOpts = {
method: "post",
body: JSON.stringify(travisBody),
headers: {
"Content-type": "application/json",
"Accept": "application/json",
"Authorization": `token ${process.env.TRAVIS_TOKEN}`,
"Travis-API-Version": "3"
}
};
fetch(apiUrl, appveyorOpts)
.then(res => res.json())
.then(json => console.log(json))
.then(() => {
fetch(travisApiUrl, travisOpts)
.then(res => res.json())
.then(json => console.log(json));
});
================================================
FILE: bin/capture-screens.js
================================================
#!/usr/bin/env node
"use strict";
const path = require("path");
const { _electron: electron } = require("playwright");
const appPath = require("electron");
const helpers = require("../test/helpers.js");
let shim;
const SCREENSAVER = "Screen Glitcher";
const callIpc = async(method, opts={}) => {
await shim.fill("#ipc", method);
await shim.fill("#ipcopts", JSON.stringify(opts));
await shim.click("text=go");
};
async function main() {
let env = {
CONFIG_DIR: "/Users/colin/Library/Application Support/Before Dawn",
BEFORE_DAWN_DIR: "/Users/colin/Library/Application Support/Before Dawn",
TEST_MODE: true,
QUIET_MODE: true
};
let app = await electron.launch({
path: appPath,
args: [path.join(__dirname, "..", "output/main.js")],
env: env
});
// wait for the first window (our test shim) to open, and
// hang onto it for later use
shim = await app.firstWindow();
await callIpc("open-window prefs");
let window = await helpers.waitFor(app, "prefs");
await window.click(`text=${SCREENSAVER}`);
await helpers.sleep(1000);
await window.screenshot({ path: path.join(__dirname, "..", "assets", "prefs.png") });
await window.click("button.settings");
let settings = await helpers.waitFor(app, "settings");
await helpers.sleep(1000);
await settings.screenshot({ path: path.join(__dirname, "..", "assets", "settings.png") });
await callIpc("close-window settings");
await window.click("button.create");
let create = await helpers.waitFor(app, "new");
await helpers.sleep(1000);
await create.screenshot({ path: path.join(__dirname, "..", "assets", "create-screensaver.png") });
await create.click("button.cancel");
var saversDir = helpers.getTempDir();
const saverJSON = helpers.addSaver(saversDir, "saver-one", "saver.json");
await callIpc("open-window editor", {
screenshot: "file://" + path.join(__dirname, "../fixtures/screenshot.png"),
src: saverJSON
});
let editor = await helpers.waitFor(app, "editor");
await helpers.sleep(1000);
await editor.screenshot({ path: path.join(__dirname, "..", "assets", "editor.png"), fullPage: true });
app.close();
}
main().catch(e => console.error(e));
================================================
FILE: bin/dev-runner.js
================================================
"use strict";
import electron from "electron";
import * as path from "path";
import { spawn } from "child_process";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let devPort;
import mainConfig from "../webpack.main.config.js";
import rendererConfig from "../webpack.renderer.config.js";
try {
const packageJSON = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
);
devPort = packageJSON.devport;
}
catch(e) {
console.log(`Can't read package.json, defaulting dev port to 9080: ${e}`);
devPort = 9080;
}
let electronProcess = null;
let manualRestart = false;
let skipMainRestart = true;
/**
* Setup webpack compiler and server for the renderer process
*
* @returns Promise
*/
function startRenderer () {
return new Promise((resolve) => {
const compiler = webpack(rendererConfig);
const serverOptions = {
host: "localhost",
port: devPort,
hot: true,
onListening: function (devServer) {
const port = devServer.server.address().port;
console.log("Listening on port:", port);
},
};
const devServer = new WebpackDevServer(
serverOptions, compiler
);
devServer.startCallback(() => {
console.log("startRenderer finished");
resolve();
})
});
}
/**
* Setup webpack compiler and watcher for the main process
*
* @returns Promise
*/
function startMain () {
return new Promise((resolve) => {
const mainCompiler = webpack(mainConfig);
mainCompiler.run(() => {
mainCompiler.watch({}, (err) => {
if (err) {
console.log(err);
return;
}
// skip the first watch event
if ( skipMainRestart ) {
skipMainRestart = false;
return;
}
// kill and restart the main process
if (electronProcess && electronProcess.kill) {
manualRestart = true;
process.kill(electronProcess.pid);
electronProcess = null;
startElectron();
setTimeout(() => {
manualRestart = false;
}, 5000);
}
});
resolve();
});
});
}
function startElectron () {
// @todo set environment here
electronProcess = spawn(electron, ["--no-sandbox", "--inspect=5858", path.join(__dirname, "../src/main/index.js")]);
electronProcess.stdout.on("data", data => {
process.stdout.write(data.toString());
});
electronProcess.stderr.on("data", data => {
process.stdout.write(data.toString());
});
electronProcess.once("close", () => {
if (!manualRestart) {process.exit();}
});
}
function init () {
Promise.all([startRenderer(), startMain()])
.then(startElectron)
.catch(console.error);
}
init();
================================================
FILE: bin/download-screensavers.js
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
import "dotenv/config";
import * as path from "path";
import * as fs from "fs";
import { rimraf } from 'rimraf'
import * as mkdirp from "mkdirp";
import { Octokit } from "octokit";
import Package from "../src/lib/package.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srcRoot = path.join(__dirname, "..");
const workingDir = path.join(srcRoot, "data");
let opts = {};
const octokit = new Octokit(opts);
let owner = "muffinista";
let repo = "before-dawn-screensavers";
console.log("cleaning up working dir", workingDir);
// ensure directory exists, clean it out, then recreate just to be sure
mkdirp.sync(workingDir);
rimraf.sync(workingDir);
mkdirp.sync(workingDir);
async function main() {
let result = await octokit.rest.repos.getLatestRelease({owner, repo});
const tag_name = result.data.tag_name;
const jsonFile = `${repo}-${tag_name}.json`;
const jsonDest = path.join(srcRoot, "data", jsonFile);
fs.writeFileSync(jsonDest, JSON.stringify(result.data));
const url = result.data.zipball_url;
const dest = path.join(srcRoot, "data", "savers");
const p = new Package({
repo: `${owner}/${repo}`,
dest: dest
});
await p.downloadRelease(url, dest);
}
main().catch(e => console.error(e));
/* eslint-enable no-console */
================================================
FILE: bin/generate-release.js
================================================
#!/usr/bin/env node
import "dotenv/config";
import * as path from "path";
import { Octokit } from "octokit";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import SentryCli from '@sentry/cli';
const pjson = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let opts = {
auth: `token ${process.env.GITHUB_AUTH_TOKEN}`
};
const octokit = new Octokit(opts);
let owner = "muffinista";
let repo = "before-dawn";
let tag_name = `v${pjson.version}`;
let draft = true;
let releaseName = `${pjson.productName} ${pjson.version}`;
const sentryCli = new SentryCli(path.join(__dirname, "sentry.properties"));
async function main() {
let release = {
owner: owner,
repo: repo,
tag_name: tag_name,
target_commitish: "main",
name: tag_name,
body: "description",
draft: draft
};
console.log(`checking ${owner}/${repo} for ${tag_name}`);
let result = await octokit.rest.repos.getLatestRelease({owner, repo});
if ( result.data.tag_name === tag_name ) {
console.log("release already created!");
}
else {
console.log(release);
// Create a release
result = await octokit.rest.repos.createRelease(release);
console.log(result);
}
console.log("Create new release on sentry");
await sentryCli.execute(["releases", "new", releaseName], true);
console.log("Add commits to release");
await sentryCli.execute(["releases", "set-commits", "--auto", releaseName], true);
console.log("Upload sourcemaps");
await sentryCli.execute(["releases", "files", releaseName, "upload-sourcemaps", "output"], true);
console.log("Finalize release");
await sentryCli.execute(["releases", "finalize", releaseName], true);
console.log("Set new deploy");
await sentryCli.execute(["releases", "deploys", releaseName, "new", "--env", "production"], true);
}
main().catch(e => console.error(e));
/* eslint-enable no-console */
================================================
FILE: bin/get-release-name
================================================
#!/usr/bin/env node
const path = require("path");
var pjson = require(path.join(__dirname, "..", "package.json"));
console.log(`${pjson.productName} ${pjson.version}`);
================================================
FILE: code_of_conduct.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at colin@muffinlabs.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: docs/.gitignore
================================================
_site
.sass-cache
================================================
FILE: docs/.ruby-version
================================================
3.3
================================================
FILE: docs/Gemfile
================================================
source 'https://rubygems.org'
gem 'github-pages', group: :jekyll_plugins
================================================
FILE: docs/_config.yml
================================================
# Site settings
title: Before Dawn -- Screensavers for the Modern Era
email: colin at muffinlabs.com
description: > # this means to ignore newlines until "baseurl:"
This is the help website for Before Dawn, an open source screensaver
app. Enjoy!
baseurl: "" # the subpath of your site, e.g. /blog/
url: "https://muffinista.github.io/before-dawn/" # the base hostname & protocol for your site
twitter_username: muffinista
github_username: muffinista
# Build settings
markdown: kramdown
================================================
FILE: docs/_includes/footer.html
================================================
<footer class="site-footer">
<div class="wrapper">
<div class="footer-col-wrapper">
<div class="footer-col footer-col-1">
<p class="text">{{ site.title }}</p>
</div>
<div class="footer-col footer-col-2">
<ul class="social-media-list">
{% if site.github_username %}
<li>
<a href="https://github.com/{{ site.github_username }}">
<span class="icon icon--github">
<svg viewBox="0 0 16 16">
<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"/>
</svg>
</span>
<span class="username">{{ site.github_username }}</span>
</a>
</li>
{% endif %}
{% if site.twitter_username %}
<li>
<a href="https://twitter.com/{{ site.twitter_username }}">
<span class="icon icon--twitter">
<svg viewBox="0 0 16 16">
<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
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"/>
</svg>
</span>
<span class="username">{{ site.twitter_username }}</span>
</a>
</li>
{% endif %}
</ul>
</div>
<div class="footer-col footer-col-3">
<ul class="contact-list">
<li><a href="mailto:{{ site.email }}">{{ site.email }}</a></li>
</ul>
</div>
</div>
</div>
</footer>
================================================
FILE: docs/_includes/head.html
================================================
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% if page.title %}{{ page.title }} :: {% endif %}{{ site.title }}</title>
<meta name="description" content="{{ site.description }}">
<link rel="stylesheet" media="screen" href="https://fontlibrary.org/face/chicagoflf" type="text/css"/>
<!-- <link rel="stylesheet" href="{{ "/css/main.css" | prepend: site.baseurl }}">-->
<link rel="stylesheet" href="./css/main.css">
<link rel="canonical" href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}">
</head>
================================================
FILE: docs/_includes/header.html
================================================
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="./">{{ site.title }}</a>
<nav class="site-nav">
<a href="#" class="menu-icon">
<svg viewBox="0 0 18 15">
<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"/>
<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"/>
<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"/>
</svg>
</a>
<div class="trigger">
{% for page in site.pages %}
{% if page.menu_title %}
<a class="page-link" href="{{ page.url | prepend: site.baseurl }}">{{ page.menu_title }}</a>
{% endif %}
{% endfor %}
</div>
</nav>
</div>
</header>
================================================
FILE: docs/_layouts/default.html
================================================
<!DOCTYPE html>
<html>
{% include head.html %}
<body>
{% include header.html %}
<div class="page-content">
<div class="wrapper">
{{ content }}
</div>
</div>
{% include footer.html %}
</body>
</html>
================================================
FILE: docs/_layouts/page.html
================================================
---
layout: default
---
<div class="post">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
</header>
<article class="post-content">
{{ content }}
</article>
</div>
================================================
FILE: docs/_layouts/post.html
================================================
---
layout: default
---
<div class="post">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
<p class="post-meta">{{ page.date | date: "%b %-d, %Y" }}{% if page.author %} • {{ page.author }}{% endif %}{% if page.meta %} • {{ page.meta }}{% endif %}</p>
</header>
<article class="post-content">
{{ content }}
</article>
</div>
================================================
FILE: docs/_posts/2016-12-02-welcome-to-jekyll.markdown
================================================
---
layout: post
title: "Welcome to Jekyll!"
date: 2016-12-02 13:11:20
categories: jekyll update
---
You’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.
To 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.
Jekyll also offers powerful support for code snippets:
{% highlight ruby %}
def print_hi(name)
puts "Hi, #{name}"
end
print_hi('Tom')
#=> prints 'Hi, Tom' to STDOUT.
{% endhighlight %}
Check 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].
[jekyll]: http://jekyllrb.com
[jekyll-gh]: https://github.com/jekyll/jekyll
[jekyll-help]: https://github.com/jekyll/jekyll-help
================================================
FILE: docs/_sass/_base.scss
================================================
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font-family: $base-font-family;
font-size: $base-font-size;
line-height: $base-line-height;
font-weight: 300;
color: $text-color;
background-color: $background-color;
-webkit-text-size-adjust: 100%;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
%vertical-rhythm {
margin-bottom: $spacing-unit / 2;
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: $small-font-size;
}
/**
* Lists
*/
ul, ol {
margin-left: $spacing-unit;
}
li {
> ul,
> ol {
margin-bottom: 0;
}
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: 300;
}
/**
* Links
*/
a {
color: $brand-color;
text-decoration: none;
&:visited {
color: darken($brand-color, 15%);
}
&:hover {
color: $text-color;
text-decoration: underline;
}
}
/**
* Blockquotes
*/
blockquote {
color: $grey-color;
border-left: 4px solid $grey-color-light;
padding-left: $spacing-unit / 2;
font-size: 18px;
letter-spacing: -1px;
font-style: italic;
> :last-child {
margin-bottom: 0;
}
}
/**
* Code formatting
*/
pre,
code {
font-size: 15px;
border: 1px solid $grey-color-light;
border-radius: 3px;
background-color: #eef;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: scroll;
> code {
border: 0;
padding-right: 0;
padding-left: 0;
}
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(800px - (#{$spacing-unit} * 2));
max-width: calc(800px - (#{$spacing-unit} * 2));
margin-right: auto;
margin-left: auto;
padding-right: $spacing-unit;
padding-left: $spacing-unit;
@extend %clearfix;
@include media-query($on-laptop) {
max-width: -webkit-calc(800px - (#{$spacing-unit}));
max-width: calc(800px - (#{$spacing-unit}));
padding-right: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
}
/**
* Clearfix
*/
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
/**
* Icons
*/
.icon {
> svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
path {
fill: $grey-color;
}
}
}
================================================
FILE: docs/_sass/_layout.scss
================================================
/**
* Site header
*/
.site-header {
border-top: 5px solid $grey-color-dark;
border-bottom: 1px solid $grey-color-light;
min-height: 56px;
// Positioning context for the mobile navigation icon
position: relative;
}
.site-title {
font-size: 26px;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
&,
&:visited {
color: $grey-color-dark;
}
}
.site-nav {
float: right;
line-height: 56px;
.menu-icon {
display: none;
}
.page-link {
color: $text-color;
line-height: $base-line-height;
// Gaps between nav items, but not on the first one
&:not(:first-child) {
margin-left: 20px;
}
}
@include media-query($on-palm) {
position: absolute;
top: 9px;
right: 30px;
background-color: $background-color;
border: 1px solid $grey-color-light;
border-radius: 5px;
text-align: right;
.menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
> svg {
width: 18px;
height: 15px;
path {
fill: $grey-color-dark;
}
}
}
.trigger {
clear: both;
display: none;
}
&:hover .trigger {
display: block;
padding-bottom: 5px;
}
.page-link {
display: block;
padding: 5px 10px;
}
}
}
/**
* Site footer
*/
.site-footer {
border-top: 1px solid $grey-color-light;
padding: $spacing-unit 0;
}
.footer-heading {
font-size: 18px;
margin-bottom: $spacing-unit / 2;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
}
.footer-col-wrapper {
font-size: 15px;
color: $grey-color;
margin-left: -$spacing-unit / 2;
@extend %clearfix;
}
.footer-col {
float: left;
margin-bottom: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
.footer-col-1 {
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
width: calc(35% - (#{$spacing-unit} / 2));
}
.footer-col-2 {
width: -webkit-calc(20% - (#{$spacing-unit} / 2));
width: calc(20% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(45% - (#{$spacing-unit} / 2));
width: calc(45% - (#{$spacing-unit} / 2));
}
@include media-query($on-laptop) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
width: calc(50% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
@include media-query($on-palm) {
.footer-col {
float: none;
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
/**
* Page content
*/
.page-content {
padding: $spacing-unit 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
> li {
margin-bottom: $spacing-unit;
}
}
.post-meta {
font-size: $small-font-size;
color: $grey-color;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: $spacing-unit;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
@include media-query($on-laptop) {
font-size: 36px;
}
}
.post-content {
margin-bottom: $spacing-unit;
h2 {
font-size: 32px;
@include media-query($on-laptop) {
font-size: 28px;
}
}
h3 {
font-size: 26px;
@include media-query($on-laptop) {
font-size: 22px;
}
}
h4 {
font-size: 20px;
@include media-query($on-laptop) {
font-size: 18px;
}
}
}
================================================
FILE: docs/_sass/_syntax-highlighting.scss
================================================
/**
* Syntax highlighting styles
*/
.highlight {
background: #fff;
@extend %vertical-rhythm;
.c { color: #998; font-style: italic } // Comment
.err { color: #a61717; background-color: #e3d2d2 } // Error
.k { font-weight: bold } // Keyword
.o { font-weight: bold } // Operator
.cm { color: #998; font-style: italic } // Comment.Multiline
.cp { color: #999; font-weight: bold } // Comment.Preproc
.c1 { color: #998; font-style: italic } // Comment.Single
.cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
.gd { color: #000; background-color: #fdd } // Generic.Deleted
.gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
.ge { font-style: italic } // Generic.Emph
.gr { color: #a00 } // Generic.Error
.gh { color: #999 } // Generic.Heading
.gi { color: #000; background-color: #dfd } // Generic.Inserted
.gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
.go { color: #888 } // Generic.Output
.gp { color: #555 } // Generic.Prompt
.gs { font-weight: bold } // Generic.Strong
.gu { color: #aaa } // Generic.Subheading
.gt { color: #a00 } // Generic.Traceback
.kc { font-weight: bold } // Keyword.Constant
.kd { font-weight: bold } // Keyword.Declaration
.kp { font-weight: bold } // Keyword.Pseudo
.kr { font-weight: bold } // Keyword.Reserved
.kt { color: #458; font-weight: bold } // Keyword.Type
.m { color: #099 } // Literal.Number
.s { color: #d14 } // Literal.String
.na { color: #008080 } // Name.Attribute
.nb { color: #0086B3 } // Name.Builtin
.nc { color: #458; font-weight: bold } // Name.Class
.no { color: #008080 } // Name.Constant
.ni { color: #800080 } // Name.Entity
.ne { color: #900; font-weight: bold } // Name.Exception
.nf { color: #900; font-weight: bold } // Name.Function
.nn { color: #555 } // Name.Namespace
.nt { color: #000080 } // Name.Tag
.nv { color: #008080 } // Name.Variable
.ow { font-weight: bold } // Operator.Word
.w { color: #bbb } // Text.Whitespace
.mf { color: #099 } // Literal.Number.Float
.mh { color: #099 } // Literal.Number.Hex
.mi { color: #099 } // Literal.Number.Integer
.mo { color: #099 } // Literal.Number.Oct
.sb { color: #d14 } // Literal.String.Backtick
.sc { color: #d14 } // Literal.String.Char
.sd { color: #d14 } // Literal.String.Doc
.s2 { color: #d14 } // Literal.String.Double
.se { color: #d14 } // Literal.String.Escape
.sh { color: #d14 } // Literal.String.Heredoc
.si { color: #d14 } // Literal.String.Interpol
.sx { color: #d14 } // Literal.String.Other
.sr { color: #009926 } // Literal.String.Regex
.s1 { color: #d14 } // Literal.String.Single
.ss { color: #990073 } // Literal.String.Symbol
.bp { color: #999 } // Name.Builtin.Pseudo
.vc { color: #008080 } // Name.Variable.Class
.vg { color: #008080 } // Name.Variable.Global
.vi { color: #008080 } // Name.Variable.Instance
.il { color: #099 } // Literal.Number.Integer.Long
}
================================================
FILE: docs/about.md
================================================
---
layout: page
title: About
permalink: /about.html
---
<p>Before Dawn is a an open-source, cross-platform screensaver
application using web-based technologies. You can generate
screensavers with it using HTML/CSS, javascript, canvas, and any tools
that rely on those technologies. In theory, generating a Before Dawn
screensaver is as simple as writing an HTML page.</p>
<p>The project developed out of a personal project to explore the history
of early screensavers. I decided that I wanted to write a framework
that I could use to actually run screensavers on my computer. I wanted
it to be cross-platform and easily accessible to artists and
developers.</p>
<p>Before Dawn is definitely a bit of a experiment -- to actually use it,
you need to run it as a separate application on your computer and
disable whatever screensaver you have running in your OS, but it is
fun and definitely works.</p>
================================================
FILE: docs/contributing.md
================================================
---
layout: page
title: Contributing
permalink: /contributing.html
---
Contributions and suggestions are eagerly accepted. Please read the
[code of conduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)
before contributing.
If you find a bug or have a suggestion, you can
[open an issue](https://github.com/muffinista/before-dawn/issues) or a
pull request on the main repository.
The screensavers for Before Dawn are in their own repository. If you
would like to add a screensaver to the program, you can submit a PR to
the
[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)
repo.
I will accept pretty much any pull request to the repository given
that the content you are posting is legal and appropriate. If you need
help or have a suggestion, please feel free to open an issue.
================================================
FILE: docs/creating.md
================================================
---
layout: page
title: Adding Your Own Screensaver
permalink: /creating.html
---
A Before Dawn screensaver is an HTML page that runs in full screen
mode. You can extend them with CSS and Javascript, and they can get
very complicated, but the basics are really very simple.
Before you add your own screensaver, you'll need to specify a local
directory for your work in the Prefrences window. Once you've done
that, click the 'Create Screensaver' button and a little form will
open up where you can specify some basic information about your
screensaver:
- *Name:* The name of your screensaver
- *Description:* A brief description of your screensaver.
- *About URL:* An optional URL with more details about your work.
- *Author:* The author of this screensaver.
After you enter in your values and click save, Before Dawn creates a
folder for you which contains an HTML file which serves as the basis
of your new screensaver, and a `saver.json` file which contains the
information about your screensaver. Before Dawn should display the
folder for you so you can get to work building your awesome
screensaver!
## Testing, Adding Options ##
Any screensaver that is in your local sources directory can be edited.
In the preview list, there will be an 'edit' link. Clicking that link
opens a window which will allow you to update the basic information
for the screensaver, add configurable options to your screensaver,
view a working preview of the screensaver, debug it, etc.
There are two tabs and a couple of buttons in the edit window. The
Preview tab will run your screensaver. The window should auto-reload
whenever you save changes to your screensaver. The Settings tab is
where you can update information about your screensaver, or add
configurable options (see below). Then there are four buttons:
- the folder button will open the working folder for your screensaver
- the save button will save any changes you've made in the settings
form
- the reload button will reload the preview
- the bug button will open the developer tools console in case you
need to debug something.
## Configurable Options ##
Before Dawn has a very simple interface for adding configurable
options to your screensaver. There are two kinds of inputs right now:
text and sliders. You could use a text input to allow users to specify
a URL or some text that will be used in your screensaver. A slider can
be used to allow the user to input a number, etc.
Screensavers are loaded as URLs, and any options will be passed as
values to the URL. The template contains some code to parse incoming
values.
================================================
FILE: docs/css/main.scss
================================================
---
# Only the main Sass file needs front matter (the dashes are enough)
---
@charset "utf-8";
// Our variables
$base-font-family: 'ChicagoFLFRegular', Helvetica, Arial, sans-serif;
$base-font-size: 16px;
$small-font-size: $base-font-size * 0.875;
$base-line-height: 1.5;
$spacing-unit: 30px;
$text-color: #111;
$background-color: #fdfdfd;
$brand-color: #2a7ae2;
$grey-color: #828282;
$grey-color-light: lighten($grey-color, 40%);
$grey-color-dark: darken($grey-color, 25%);
$on-palm: 600px;
$on-laptop: 800px;
// Using media queries with like this:
// @include media-query($palm) {
// .wrapper {
// padding-right: $spacing-unit / 2;
// padding-left: $spacing-unit / 2;
// }
// }
@mixin media-query($device) {
@media screen and (max-width: $device) {
@content;
}
}
// Import partials from `sass_dir` (defaults to `_sass`)
@import "base", "layout";
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
width: 100%;
min-height: 100%; /* this helps with the sticky footer */
}
.page-content {
flex-grow: 1;
flex-shrink: 0;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkIAIwEqFGipAiKQYGhmeEFIEtwqUIbALMKdgUoSjAZxKKf5BNwjAB3TqcCkAKAW9cBSRvYfskAAAAAElFTkSuQmCC) repeat;
}
footer {
flex-shrink: 0;
}
.home {
display: flex;
p {
padding-right: 10px;
}
.main, .preview {
flex: 0 0 50%;
}
.preview {
height: 500px;
background-image: url(../background3.svg);
background-size: contain;
background-repeat: no-repeat;
}
img.saver {
margin-top: 50px;
margin-left: 32px;
width:310px;
height: 245px;
}
}
@media screen and (max-width: 800px) {
.home {
display: block;
.preview {
background-image: none;
img.saver {
width: 100%;
height: auto;
margin: 0px;
}
}
}
}
@include media-query($on-palm) {
.site-title {
max-width: $on-palm - 180px;
}
}
================================================
FILE: docs/index.html
================================================
---
layout: page
title: Welcome!
---
<div class="home">
<div class="main" >
<p>Welcome to the help website for Before Dawn, an open-source
screensaver application. Before Dawn runs on Windows, OSX and Linux,
and is powered by javascript.</p>
<p>Screensavers can be written in HTML
and Javascript. If it works in a browser, it can be a screensaver!</p>
<p>You can
<a href="about.html">learn more</a> about
Before Dawn, find out how to install it, and learn about
writing your own screensaver.
</p>
<ul>
<li><a href="{{ 'installing.html' | prepend: site.baseurl }}">How to Install</a></li>
<li><a href="{{ 'preferences.html' | prepend: site.baseurl }}">Preferences and Options</a></li>
<li><a href="{{ 'creating.html' | prepend: site.baseurl }}">Creating Your Own Screensaver</a></li>
<li><a href="{{ 'contributing.html' | prepend: site.baseurl }}">Contributing</a></li>
<li><a href="https://github.com/muffinista/before-dawn">Github Repository</a></li>
</ul>
</div>
<div class="preview"">
<img src="https://github.com/muffinista/before-dawn/raw/main/assets/emoji-screensaver.gif" class="saver" />
</div>
</div>
================================================
FILE: docs/installing.md
================================================
---
layout: page
title: Installing Before Dawn
permalink: /installing.html
---
You can find installers for Before Dawn on github in the
[Releases section](https://github.com/muffinista/before-dawn/releases)
of the project. Installing on OSX or Windows should be as simple as
downloading the appropriate file, and running it locally. Installing
on Linux is a little more involved, and I will add documentation for
that as soon as I can.
The first time you install it, you will need to run Before Dawn
manually. The preferences window will open, and you can pick a
screensaver and set whether you want Before Dawn to automatically load
when your system boots, and you can tweak some other settings as well.
================================================
FILE: docs/preferences.md
================================================
---
layout: page
title: Preferences
permalink: /preferences.html
---
Before Dawn has a Preferences window where you can see the list of
screensavers, and tweak your settings.
## Picking a Screensaver ##
The main tab of the preferences window lists all of the screensavers
available to you. As you click on them, you'll see a preview in the
right side of the window.
### Screensaver Options ###
Some screensavers have configurable options to control how they run --
for example, the Emoji Starfield screensaver has a control to set how
many emoji are on your screen at once. You can fiddle with these
settings and get an idea of how it looks in the preview area.
## Options ##
The Options tab of the Preferences window has a number of settings to
control how Before Dawn runs:
- *Activate after* -- you can set how long your computer is idle
before your screensaver starts
- *Sleep after* - you can set a time that Before Dawn will
stop running and blank the screens. It's possible that your OS will do
a better job of this, but since Before Dawn is a bit of an experiment,
having this setting might save your CPU from running when your
computer is idle for a long time.
- *Lock screen after running?* -- If you want your computer to lock
once the screensaver starts running, you can use this option.
- *Disable when on battery?* -- You can toggle this so that Before Dawn
won't run if your computer is running on battery power.
- *Auto start on login?* -- should Before Dawn start automatically
when your computer starts?
### Advanced Options ###
- *Local Source* -- If you want to develop your own screensavers, or
run a set of screensavers that aren't in the main package, you can put a path
to a directory here. Before Dawn will check this directory for any
local screensavers and add them to the list.
### Create Screensaver ###
Check out the [creating](../creating) section to learn about adding your
own screensaver.
================================================
FILE: eslint.config.mjs
================================================
import mocha from "eslint-plugin-mocha";
import globals from "globals";
import js from "@eslint/js";
import svelte from 'eslint-plugin-svelte';
/** @type {import('eslint').Linter.Config[]} */
export default [
mocha.configs.recommended,
js.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node, // Add this if you are using SvelteKit in non-SPA mode
},
ecmaVersion: "latest",
sourceType: "module",
}
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
// We recommend importing and specifying svelte.config.js.
// By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
// While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it,
// explicitly specifying it ensures better compatibility and functionality.
// svelteConfig
}
},
plugins: {
"plugin:svelte/recommended": svelte
}
},
{
files: ['test/**/*.js'],
plugins: {
"plugin:mocha/recommended": mocha
},
languageOptions: {
globals: {
...globals.mocha
},
},
},
{
ignores: [
"output/*",
"data/*"
],
rules: {
// Override or add rule settings here, such as:
// 'svelte/rule-name': 'error'
}
}
];
================================================
FILE: lefthook.yml
================================================
# EXAMPLE USAGE
# Refer for explanation to following link:
# https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md
#
# pre-push:
# commands:
# packages-audit:
# tags: frontend security
# run: yarn audit
# gems-audit:
# tags: backend security
# run: bundle audit
#
pre-commit:
parallel: true
commands:
eslint:
glob: "*.{js,ts}"
run: npm run eslint {staged_files}
================================================
FILE: mise.toml
================================================
[tools]
node = "22.21.1"
================================================
FILE: package.json
================================================
{
"name": "before-dawn",
"productName": "Before Dawn",
"version": "0.38.0",
"description": "A desktop screensaver app using web technologies",
"author": "Colin Mitchell <colin@muffinlabs.com> (http://muffinlabs.com)",
"license": "MIT",
"homepage": "https://github.com/muffinista/before-dawn/",
"release_server": "https://before-dawn-updates.muffinlabs.com",
"main": "output/main.js",
"type": "module",
"engines": {
"node": ">= 22.14.0"
},
"devport": 9081,
"scripts": {
"dev": "node bin/dev-runner.js",
"compile": "cross-env NODE_ENV=production webpack --mode production --config webpack.config.js",
"eslint-all": "eslint -c eslint.config.mjs src/**/*.js src/**/*.svelte test/**/*.js webpack*.js",
"eslint": "eslint -c eslint.config.mjs",
"postinstall": "electron-builder install-app-deps",
"pack": "npm run compile && electron-builder --dir",
"dist": "npm run compile && electron-builder --x64",
"test": "npm run compile && mocha -b test/**/*.js",
"test-ui": "cross-env DISABLE_SENTRY=true npm run compile && xvfb-maybe mocha test/ui/**/*.js",
"test-lib": "mocha test test/lib/**/*.js test/main/**/*.js",
"run-local": "node bin/build-icon.js && npm run compile && cross-env ELECTRON_IS_DEV=0 electron output/main.js",
"grab-screens": "bin/capture-screens.js",
"release": "node bin/build-icon.js && node bin/download-screensavers.js && npm run dist",
"publish-release": "node bin/generate-release.js && git push origin main"
},
"repository": {
"type": "git",
"url": "git://github.com/muffinista/before-dawn.git"
},
"dependencies": {
"@muffinista/goto-sleep": "github:muffinista/goto-sleep",
"auto-launch": "^5.0.6",
"conf": "^15.0.2",
"detect-fullscreen": "github:muffinista/detect-fullscreen",
"electron-is-dev": "^3.0.1",
"electron-log": "^5.4.3",
"forcefocus": "github:muffinista/forcefocus",
"fs-extra": "^11.3.3",
"glob": "^13.0.0",
"hide-cursor": "github:muffinista/hide-cursor",
"mkdirp": "^3.0.1",
"proper-lockfile": "^4.1.2",
"rimraf": "^6.1.2",
"semver": "^7.7.3",
"temp": "^0.9.4",
"yauzl": "^3.2.1"
},
"overrides": {
"@electron/rebuild": {
"node-abi": "4.17.0"
}
},
"devDependencies": {
"@arkweid/lefthook": "^0.7.7",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@electron/rebuild": "^4.0.2",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@sentry/cli": "^3.0.1",
"@sentry/electron": "^7.5.0",
"@sentry/webpack-plugin": "^4.6.1",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"dotenv": "^17.2.3",
"electron": "^39.8.5",
"electron-builder": "^26.3.6",
"eslint": "^9.39.2",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-html": "^8.1.3",
"eslint-plugin-mocha": "^11.2.0",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-svelte": "^3.13.1",
"eslint-webpack-plugin": "^5.0.2",
"globals": "^17.0.0",
"html-webpack-plugin": "^5.6.5",
"jimp": "^1.6.0",
"mini-css-extract-plugin": "^2.9.4",
"mocha": "^11.7.5",
"nock": "^14.0.10",
"node-gyp": "^12.1.0",
"node-loader": "^2.1.0",
"normalize.css": "^8.0.1",
"octokit": "^5.0.5",
"playwright": "^1.57.0",
"png-to-ico": "^3.0.1",
"sass": "^1.97.1",
"sass-loader": "^16.0.6",
"sinon": "^21.0.1",
"style-loader": "^4.0.0",
"svelte": "^5.53.6",
"svelte-eslint-parser": "^1.4.1",
"svelte-loader": "^3.2.4",
"tmp": "^0.2.5",
"url-loader": "^4.1.1",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"webpack-hot-middleware": "^2.26.1",
"xvfb-maybe": "^0.2.1"
},
"build": {
"files": [
"output/**/*",
"node_modules/**/*",
"package.json"
],
"extraResources": [
{
"from": "data/savers",
"to": "savers",
"filter": [
"**/*"
]
}
],
"appId": "Before Dawn",
"mac": {
"category": "public.app-category.entertainment",
"extendInfo": {
"LSUIElement": 1,
"NSMicrophoneUsageDescription": "Some screensavers detect sound to provide interactivity. You can decline this permission if you do not want that.",
"NSCameraUsageDescription": "Some screensavers can use your webcam to provide interactivity. You can decline this permission if you do not want that."
}
},
"nsis": {
"installerIcon": "build/icon.ico",
"perMachine": false
},
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"asar": true,
"dmg": {
"contents": [
{
"x": 338,
"y": 14,
"type": "link",
"path": "/Applications"
},
{
"x": 192,
"y": 14,
"type": "file"
}
]
},
"linux": {
"category": "Amusement",
"target": "deb",
"executableName": "before-dawn",
"maintainer": "Colin Mitchell <colin@muffinlabs.com>"
}
}
}
================================================
FILE: src/css/styles.scss
================================================
:root {
--preview-wrapper-width: 500px;
--preview-wrapper-height: 320px;
--preview-width: 500px;
--preview-height: 320px;
--preview-scale: 1.0;
--footer-height: 30px;
--space-at-top: 1.0rem;
--footer-padding: 25px;
--small-padding: 10px;
--font-size: 14px;
}
@import "normalize.css";
html {
height: 100%;
font-family: system-ui;
font-size: var(--font-size);
}
body {
min-height: 100%;
}
input, button {
font: small-caption;
}
select {
font: menu;
}
button {
cursor: pointer;
height: 21px;
padding: 0px 16px;
font-size: var(--font-size);
border: 1px solid;
border-radius: 4px;
border-top-color: rgb(198, 198, 198);
border-bottom-color: rgb(170, 170, 170);
border-left-color: rgb(192, 192, 192);
border-right-color: rgb(192, 192, 192);
}
h1 {
font-size: 1.1rem;
}
#about h1 {
font-size: 2.0rem;
}
small {
font-size: 95%;
}
.text-muted {
color: grey;
}
.form-check {
display: flex;
flex-direction: column;
margin-bottom: 1.0rem;
}
.btn:focus {
outline: none !important;
box-shadow: none !important;
}
.input-group-btn.spaced {
margin-left: 3px;
}
.input-group {
display: flex;
width: 100%;
}
#prefs, #settings, #editor, #about {
padding: 0.5rem;
}
//
// prefs page
//
#prefs {
display: grid;
grid-template-columns: var(--preview-wrapper-width) 1fr;
grid-template-rows: var(--preview-wrapper-height) 1fr var(--footer-height);
grid-template-areas: "saver-preview saver-list"
"saver-info basic-prefs"
"footer footer";
}
#prefs > header {
grid-area: header;
}
.platform-darwin .hide-on-darwin {
display: none;
}
.saver-list-wrapper {
grid-area: saver-list;
height: var(--preview-wrapper-height);
padding-left: 0.25rem;
}
.saver-list {
max-height: calc(var(--preview-wrapper-height) - 34px);
overflow-y: scroll;
margin-bottom: 0px;
font: small-caption;
li.list-group-item {
padding-left: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
border-left: 1px solid rgba(0, 0, 0, 0.125);
}
li.list-group-item.active {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
label {
font-size: 0.9rem;
}
}
#prefs input[name=screensaver] {
display: none;
}
div.saver-detail {
grid-area: saver-preview;
height: var(--preview-wrapper-height);
overflow: hidden;
}
.saver-preview {
width: var(--preview-width);
height: var(--preview-height);
transform: scale(var(--preview-scale));
transform-origin: 0 0;
overflow: hidden;
border: 0;
}
.saver-preview::-webkit-scrollbar {
width: 0px;
height: 0px;
}
div.basic-prefs {
grid-area: basic-prefs;
margin-top: 1rem;
margin-right: 2rem;
}
div.saver-info {
grid-area: saver-info;
height: calc(99vh - var(--preview-wrapper-height) - var(--footer-height) - var(--space-at-top) - var(--small-padding));
overflow-y: auto;
}
div.saver-info #wrapper {
max-width: 98%;
}
#settings {
padding-top: 0.5rem;
}
footer.footer {
height: var(--footer-height);
grid-area: footer;
position: fixed;
bottom: 0;
left: 0;
right: 0;
align-items: center;
background-color: #f5f5f5;
padding-left: 5px;
padding-right: 5px;
display: flex;
justify-content: space-between;
}
#prefs h1 {
font-size: 1.4rem;
}
#advanced-prefs-form > h1 {
margin-top: 30px;
}
body > #editor > .content {
overflow-y: scroll;
}
body > #editor #settings > div {
padding-top: 35px;
}
ul {
padding-left: 0px;
list-style-type: none;
}
.hide {
display: none;
}
button.add-option {
margin-top: 12px;
}
.space-at-top {
margin-top: var(--space-at-top);
}
.space-at-bottom {
margin-bottom: 20px;
}
.saver-detail {
padding-left: 0px;
padding-right: 5px;
}
.saver-description .actions {
margin-top: 1rem;
margin-bottom: 1rem;
}
.hint {
color: #888888;
font-size: 95%;
}
h1 .hint {
font-size: 60%;
}
.window > footer {
padding: 5px;
}
.padded-top {
margin-top: 20px;
}
.padded-bottom {
padding-bottom: 60px;
}
//
// about page
//
#about {
overflow-x: hidden;
}
#about {
margin-left: 5px;
text-align: center;
}
#about h1, #about h2, #about h3, #about p {
overflow: visible;
margin-top: 5px;
margin-bottom: 5px;
}
#about h2 {
font-size: 1.3rem;
}
#about h3 {
font-size: 1.1rem;
}
#about p {
font-size: 120%;
}
#about svg {
width: 50%;
}
#options {
margin-bottom: 10px;
}
#options div.field {
display: flex;
}
#options div.input {
width: 75%;
align-self: center;
padding-right: 10px;
}
#options legend {
font-size: 95%;
width: 25%;
align-self: center;
margin: 0px;
}
form.submit-attempt {
input:invalid {
border: 2px dashed red;
}
}
form.entry {
margin-top: 5px;
margin-bottom: 5px;
padding-left: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed #888888;
}
form.input {
margin-bottom: 10px;
display: flex;
flex-direction: column;
}
form.input input[type=text], input[type=range] {
width: 100%;
}
label.for-option {
font-weight: bold;
}
input[type="range"] {
max-width: 250px;
}
div.form-group {
margin-bottom: 1rem;
max-width: 95%;
label {
display: inline-block;
font-weight: bold;
margin-bottom: 0.5rem;
}
input, button, select {
display: block;
height: auto;
font-size: 1rem;
padding: .375rem .75rem;
line-height: 1.5;
}
input {
width: 100%;
}
input[type=checkbox] {
width: initial;
display: inline-block;
}
.hint {
display: block;
}
}
div.form-group.full-width {
max-width: 100%;
}
// new screensaver
#new {
padding: 0.5rem;
}
.need-setup-message {
height: 80vh;
}
.block {
display: block;
}
#editor {
h1, h2, h3, h4 {
margin-bottom: 0px;
}
h1, h2, h3, h4 {
& + small {
display: block;
margin-bottom: 1rem;
}
& + input {
margin-top: 0.5rem;
margin-left: 0.5rem;
}
}
}
input[type="checkbox"] + label {
margin-top: 0.5rem;
margin-left: 0.5rem;
}
div.notarize-wrapper {
position: fixed;
top: 10px;
right: 10px;
background: white;
min-height: 40px;
min-width: 100px;
max-width: 200px;
border-radius: 5px;
border-color: black;
border: 2px solid;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
}
@keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@keyframes fadeOut {
0% {opacity:1;}
100% {opacity:0;}
}
.notarize-in {
animation: fadeIn 1.0s;
}
.notarize-out {
animation: fadeOut 1.0s;
}
================================================
FILE: src/index.ejs
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body data-id="<%= htmlWebpackPlugin.options.id %>">
<!-- webpack builds are automatically injected -->
<div id="root"></div>
</body>
</html>
================================================
FILE: src/lib/package.js
================================================
"use strict";
import fs from 'fs-extra';
import path from "path";
import temp from "temp";
import os from "os";
import { mkdirp } from "mkdirp";
import { rimrafSync } from "rimraf";
import * as yauzl from "yauzl"
import * as lockfile from "proper-lockfile";
import { Readable } from "stream";
import { finished } from "stream/promises";
import semver from "semver";
/**
* need source repo url
* call https://developer.github.com/v3/repos/releases/#get-the-latest-release
* check published_at
* if it's after stored value, download it!
*/
export default class Package {
constructor(_attrs) {
this.repo = _attrs.repo;
this.dest = _attrs.dest;
this.version = _attrs.version;
this.downloaded = false;
this.url = `https://api.github.com/repos/${this.repo}/releases/latest`;
if ( typeof(this.version) === "undefined" ) {
const saverPackageJson = path.join(this.dest, "package.json");
if ( fs.existsSync(saverPackageJson) ) {
this.version = JSON.parse(fs.readFileSync(saverPackageJson)).version;
}
}
if ( typeof(_attrs.log) === "undefined" ) {
_attrs.log = function() {};
}
this.logger = _attrs.log;
this.defaultHeaders = {
"User-Agent": "Before Dawn"
};
}
attrs() {
return {
dest: this.dest,
version: this.version,
downloaded: this.downloaded
};
}
async getReleaseInfo() {
this.logger(`get release info from ${this.url}`);
if ( this.data ) {
return this.data;
}
this.data = await fetch(this.url, this.defaultHeaders)
.then(res => res.json())
.then((json) => {
const remoteVersion = json.tag_name.replace(/^v/, "");
json.is_update = this.version === undefined || semver.gt(remoteVersion, this.version);
return json;
})
.catch((err) => {
this.logger(err);
if ( typeof(this.data) !== "undefined" ) {
return this.data;
}
else {
return {};
}
});
return this.data;
}
async hasUpdate() {
const data = await this.getReleaseInfo();
return data.is_update;
}
async checkLatestRelease(force) {
const data = await this.getReleaseInfo();
if ( force === true || data.is_update ) {
return this.downloadRelease();
}
else {
this.logger("no package update available");
return this.attrs();
}
}
async downloadRelease() {
this.logger("download package updates!");
const data = await this.getReleaseInfo();
let dest;
if ( this.useLocalFile ) {
dest = this.localZip;
}
else {
dest = await this.downloadFile(data.zipball_url);
}
await this.zipToSavers(dest);
this.downloaded = true;
this.updated_at = data.published_at;
return this.attrs();
}
async downloadFile(url, dest) {
if ( dest === undefined ) {
dest = temp.path({dir: os.tmpdir(), suffix: ".zip"});
}
const res = await fetch(url, this.defaultHeaders);
// https://stackoverflow.com/questions/37614649/how-can-i-download-and-save-a-file-using-the-fetch-api-node-js
const fileStream = fs.createWriteStream(dest);
await finished(Readable.fromWeb(res.body).pipe(fileStream));
return dest;
}
zipToSavers(tempName, dest) {
let self = this;
if ( dest === undefined ) {
dest = self.dest;
}
return new Promise(function (resolve, reject) {
lockfile.lock(dest, { realpath: false, stale: 30000 }).then((release) => {
yauzl.open(tempName, {lazyEntries: true, validateEntrySizes: false}, (err, zipfile) => {
if (err) {
release().then(() => {
reject(err);
});
}
else {
//
// clean out existing files
//
try {
rimrafSync(self.dest);
}
catch (err) {
self.logger(err);
}
zipfile.readEntry();
zipfile.on("entry", function(entry) {
var fullPath = entry.fileName;
// the incoming zip filename will have on extra directory on it
// projectName/dir/etc/file
//
// example: muffinista-before-dawn-screensavers-d388377/starfield/index.html
//
// let's get rid of the projectName
//
var parts = fullPath.split(/\//);
parts.shift();
fullPath = path.join(dest, path.join(...parts));
if (/\/$/.test(entry.fileName)) {
// directory file names end with '/'
mkdirp(fullPath).then(() => {
zipfile.readEntry();
}).catch((err) => {
release().then(() => {
return reject(err);
});
});
}
else {
// file entry
zipfile.openReadStream(entry, function(err, readStream) {
if (err) {
release().then(() => {
return reject(err);
});
}
// ensure parent directory exists
mkdirp(path.dirname(fullPath)).then(() => {
self.logger(`${entry.fileName} -> ${fullPath}`);
readStream.pipe(fs.createWriteStream(fullPath));
readStream.on("end", function() {
zipfile.readEntry();
});
}).catch((err) => {
release().then(() => {
return reject(err);
});
});
});
}
});
zipfile.on("end", function() {
release().then(() => {
resolve(self.attrs());
});
});
}
});
});
});
}
}
================================================
FILE: src/lib/prefs-schema.json
================================================
{
"saver": {
"type": "string",
"default": ""
},
"sourceRepo": {
"type": "string",
"default": "muffinista/before-dawn-screensavers"
},
"delay": {
"type": "number",
"default": 5
},
"sleep": {
"type": "number",
"default": 10
},
"lock": {
"type": "boolean",
"default": false
},
"disableOnBattery": {
"type": "boolean",
"default": true
},
"auto_start": {
"type": "boolean",
"default": false
},
"runOnSingleDisplay": {
"type": "boolean",
"default": true
},
"localSource": {
"type": "string",
"default": ""
},
"options": {
"default": {}
},
"sourceUpdatedAt": {
"default": "1970-01-01T00:00:00.000Z"
},
"updateCheckTimestamp": {
"default": "1970-01-01T00:00:00.000Z"
},
"launchShortcut": {
"type": "string",
"default": ""
},
"firstLoad": {
"type": "boolean"
}
}
================================================
FILE: src/lib/prefs.js
================================================
"use strict";
import * as path from "path";
import Conf from "conf";
const DEFAULTS = {
"saver": {
"type": "string",
"default": ""
},
"sourceRepo": {
"type": "string",
"default": "muffinista/before-dawn-screensavers"
},
"delay": {
"type": "number",
"default": 5
},
"sleep": {
"type": "number",
"default": 10
},
"lock": {
"type": "boolean",
"default": false
},
"disableOnBattery": {
"type": "boolean",
"default": true
},
"auto_start": {
"type": "boolean",
"default": false
},
"runOnSingleDisplay": {
"type": "boolean",
"default": true
},
"localSource": {
"type": "string",
"default": ""
},
"options": {
"default": {}
},
"sourceUpdatedAt": {
"default": "1970-01-01T00:00:00.000Z"
},
"updateCheckTimestamp": {
"default": "1970-01-01T00:00:00.000Z"
},
"launchShortcut": {
"type": "string",
"default": ""
},
"firstLoad": {
"type": "boolean"
}
};
class SaverPrefs {
constructor(baseConfigDir, rootDir=undefined, saversDir=undefined) {
this.baseDir = baseConfigDir;
if ( rootDir === undefined ) {
this.rootDir = this.baseDir;
}
else {
this.rootDir = rootDir;
}
if ( saversDir === undefined ) {
this.saversDir = path.join(this.rootDir, "savers");
}
else {
this.saversDir = saversDir;
}
this.systemSource = path.join(this.rootDir, "system-savers");
this.confOpts = {
schema: DEFAULTS,
clearInvalidConfig: true,
cwd: this.baseDir
};
if ( process.env.CONFIG_DIR ) {
this.confOpts.cwd = process.env.CONFIG_DIR;
}
this.reload();
}
get configFile() {
return this.store.path;
}
get data() {
let result = {};
let self = this;
Object.keys(DEFAULTS).forEach(function(name) {
result[name] = self.store.get(name);
});
return result;
}
get defaults() {
let result = {};
Object.keys(DEFAULTS).forEach(function(name) {
result[name] = DEFAULTS[name].default;
});
return result;
}
reload() {
this.store = new Conf(this.confOpts);
this.firstLoad = this.store.get("firstLoad", true);
if ( this.firstLoad === true ) {
this.store.set("firstLoad", false);
}
// if (this.saver) {
// this.saver = this.saver.split(path.sep).join(path.posix.sep);
// }
}
reset() {
this.store.clear();
this.store._write({});
}
get needSetup() {
return this.firstLoad === true ||
this.saver === undefined ||
this.saver === "";
}
get defaultSaversDir() {
return this.saversDir;
}
/**
* get a list of folders we should check for screensavers
*/
get sources() {
var local = this.localSource;
var system = this.systemSource;
var folders = [this.defaultSaversDir];
// if there's a local source, use that
if ( local !== "" ) {
folders = folders.concat( local );
}
folders = folders.concat( system );
return folders;
}
get systemSource() {
return this._systemSource;
}
set systemSource(val) {
this._systemSource = val;
}
//
// get options for the specified screensaver
//
getOptions(name) {
if ( typeof(name) === "undefined" ) {
name = this.saver;
}
const opts = this.store.get("options", {});
const result = opts[name];
if ( result === undefined ) {
return {};
}
return result;
}
}
Object.keys(DEFAULTS).forEach(function(name) {
Object.defineProperty(SaverPrefs.prototype, name, {
get() {
const result = this.store.get(name);
if ( name === "sourceUpdatedAt" || name === "updateCheckTimestamp" ) {
return new Date(result);
}
return result;
},
set(newval) {
if ( newval === undefined ) {
this.store.delete(name);
}
else {
if ( typeof(newval) === "object" && ( name === "sourceUpdatedAt" || name === "updateCheckTimestamp" )) {
newval = newval.toISOString();
}
this.store.set(name, newval);
}
}
});
});
export default SaverPrefs;
================================================
FILE: src/lib/saver-factory.js
================================================
"use strict";
import * as path from "path";
import fs from 'fs-extra'
export default class SaverFactory {
constructor(prefs, logger) {
this.prefs = prefs;
if ( logger !== undefined ) {
this.logger = logger;
}
else {
this.logger = function() {};
}
}
/**
* generate a screensaver template
*/
create(src, destDir, opts) {
if ( destDir === "" || destDir === undefined ) {
throw new Error("No local directory specified!");
}
this.logger(`SRC: ${src}`);
let contents = fs.readdirSync(src);
const defaults = {
"source": "index.html",
"options": []
};
opts = Object.assign({}, defaults, opts);
opts.key = opts.name.toLowerCase().
replace(/[^a-z0-9]+/gi, "-").
replace(/-$/, "").
replace(/^-/, "");
var dest = path.join(destDir, opts.key);
this.logger(`mkdir ${dest}`);
fs.mkdirpSync(dest);
contents.forEach(function(content) {
fs.copySync(path.join(src, content), path.join(dest, content));
});
//
// generate JSON file
//
var configDest = path.join(dest, "saver.json");
var content = fs.readFileSync( configDest );
contents = Object.assign({}, JSON.parse(content), opts);
fs.writeFileSync(configDest, JSON.stringify(contents, null, 2));
// add dest in case someone needs it
// but don't persist that data because that would be icky
opts.dest = path.join(dest, "saver.json");
return opts;
}
}
================================================
FILE: src/lib/saver-list.js
================================================
"use strict";
import fs from 'fs-extra';
import path from "path";
import { mkdirp } from "mkdirp";
import { rimraf } from "rimraf";
import { glob } from "glob";
const CONFIG_FILE_NAME = "config.json";
/**
* skip any folder which contains a '.before-dawn-skip' file
* this way we can have templates and documentation and things like
* that which won't get loaded into the app by mistake.
*/
var skipFolder = function(p) {
return fs.existsSync(path.join(p, ".before-dawn-skip"));
};
export default class SaverListManager {
constructor(opts, logger) {
this.prefs = opts.prefs;
this.loadedScreensavers = [];
if ( logger !== undefined ) {
this.logger = logger;
}
else {
this.logger = function() {};
}
this.baseDir = this.prefs.baseDir;
if ( opts.rootDir ) {
this.rootDir = opts.rootDir;
}
else {
this.rootDir = this.baseDir;
}
}
get defaultSaversDir() {
return this.prefs.saversDir;
}
async setup() {
let _self = this;
var configPath = path.join(_self.baseDir, CONFIG_FILE_NAME);
var saversDir = _self.defaultSaversDir;
var results = {
first: false,
setup: false
};
_self.logger("saversDir: " + saversDir, fs.existsSync(saversDir));
_self.logger("configPath: " + configPath);
// check for/create our main directory
// and our savers directory (which is a subdir
// of the main dir)
const made = await mkdirp(saversDir);
// check if we just created the folder,
// if there's no config yet,
// or if the savers folder was empty
if ( made === true || ! fs.existsSync(configPath) || fs.readdirSync(saversDir).length === 0 ) {
results.first = true;
}
results.setup = true;
return results;
}
/**
* reload all our data/config/etc
*/
reload(load_savers) {
this.logger("savers.reload");
return this.setup(load_savers).then(this.handlePackageChecks);
}
reset() {
this.loadedScreensavers = [];
}
normalizePath(p) {
return p.split(path.sep).join(path.posix.sep);
}
/**
* search for all screensavers we can find on the filesystem. if cb is specified,
* call it with data when done. if reload == true, don't use cached data.
*/
async list(force) {
let _self = this;
var folders = this.prefs.sources;
var pattern, savers;
var promises = [];
// exclude system screensavers from the cache check
// @todo get rid of this
var systemScreensaverCount = 1;
// use cached data if available
if ( this.loadedScreensavers.length > systemScreensaverCount &&
( typeof(force) === "undefined" || force === false ) ) {
return this.loadedScreensavers;
}
// note: using /**/ here instead of /*/ would
// also match all subdirectories, which might be desirable
// or even required, but is a lot slower, so not doing it
// for now
folders = folders.filter((el) => {
return el !== undefined && el !== "" && fs.existsSync(el);
});
folders.forEach((sourceFolder) => {
// glob doesn't work with windows style file paths, so convert
// to posix
sourceFolder = this.normalizePath(sourceFolder);
pattern = `${sourceFolder}/*/saver.json`;
savers = glob.sync(pattern);
for ( var i = 0; i < savers.length; i++ ) {
var f = this.normalizePath(savers[i]);
var folder = path.dirname(f);
// exclude skippable folders
var doLoad = ! folder.split(/[/|\\]/).reverse()[0].match(/^__/) &&
! skipFolder(folder);
if ( doLoad ) {
promises.push(this.loadFromFile(f));
}
}
});
// filter out failed promises here
// @see https://davidwalsh.name/promises-results
promises = promises.map(p => p.catch(() => undefined));
const data = await Promise.all(promises);
// remove any undefined screensavers
_self.loadedScreensavers = data.
filter(s => s !== undefined).
sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
return _self.loadedScreensavers;
}
/**
* pick a random screensaver
*/
random() {
var tmp = this.loadedScreensavers.filter((s) => {
return ( typeof(s.preload) === "undefined" );
});
var idx = Math.floor(Math.random() * tmp.length);
return tmp[idx];
}
async confirmExists(key) {
await this.list();
return this.getByKey(key) !== undefined;
}
/**
* look up a screensaver by key, and return it
*/
getByKey(key) {
key = this.normalizePath(key);
var result = this.loadedScreensavers.find((obj) => {
return obj.key === key;
});
return result;
}
/**
* load screensaver data from filesystem
*/
loadFromFile(src, settings) {
let _self = this;
src = this.normalizePath(src);
return new Promise(function (resolve, reject) {
fs.readFile(src, {encoding: "utf8"}, (err, content) => {
if ( err ) {
_self.logger("loadFromFile err", src, err);
reject(err);
}
else {
try {
var contents = JSON.parse(content);
var stub = path.dirname(src);
var s = _self.loadFromData(contents, stub, settings);
// add the source path as an attribute to make it easier
// to load/save/update this saver later if needed
s.src = src;
if ( s.valid ) {
resolve(s);
}
else {
_self.logger("loadFromFile not valid! " + src);
reject();
}
}
catch(e) {
_self.logger("loadFromFile exception", e);
reject(e);
}
}
});
});
}
loadFromData(contents, stub, settings) {
var src = this.normalizePath(this.prefs.localSource);
if ( typeof(stub) !== "undefined" ) {
contents.path = stub;
contents.key = this.normalizePath(path.join(stub, "saver.json"));
}
contents.editable = false;
if ( typeof(src) !== "undefined" && src !== "" ) {
contents.editable = (contents.key.indexOf(src) === 0);
}
if ( typeof(contents.settings) === "undefined" ) {
if ( settings === undefined ) {
if ( ! this.prefs.options ) {
this.prefs.options = {};
}
// ensure that all screensavers have options set
if ( this.prefs.options[contents.key] === undefined ) {
this.prefs.options[contents.key] = {};
}
settings = this.prefs.options[contents.key];
}
contents.settings = settings;
}
// set a URL
if ( typeof(contents.url) === "undefined" &&
contents.path !== undefined &&
contents.source !== undefined) {
contents.url = `file://${[contents.path, contents.source].join("/")}`;
}
if ( typeof(contents.published) === "undefined" ) {
contents.published = true;
}
if ( typeof(contents.requirements) === "undefined" ) {
contents.requirements = ["screen"];
}
contents.valid = typeof(contents.name) !== "undefined" &&
typeof(contents.description) !== "undefined" &&
contents.published === true;
return contents;
}
/**
* delete a screensaver -- this removes the directory that contains all files
* for the screensaver.
*/
async delete(s) {
var k = s.key;
var p = path.dirname(k);
if ( typeof(s) !== "undefined" && s.editable === true ) {
await rimraf(p);
return true;
}
else {
return false;
}
}
}
================================================
FILE: src/lib/saver.js
================================================
/**
* simple class for a screen saver
*/
// we will generate a list of requirements that screensavers need
// to work. for now, it's just a screengrab. to maintain
// compatability, we'll generate a default list if one isn't
// specified
const DEFAULT_REQUIREMENTS = ["screen"];
import fs from 'fs-extra';
import * as nodePath from "path";
export default class Saver {
constructor(_attrs) {
this.UNWRITABLE_KEYS = ["key", "path", "url", "settings", "editable"];
this.attrs = _attrs;
this.path = _attrs.path;
this.name = _attrs.name;
this.key = _attrs.key;
this.description = _attrs.description;
this.aboutUrl = _attrs.aboutUrl;
this.author = _attrs.author;
this.license = _attrs.license;
this.preload = _attrs.preload;
this.requirements = _attrs.requirements || DEFAULT_REQUIREMENTS;
// allow for a specified URL -- this way you could create a screensaver
// that pointed to a remote URL
this.url = _attrs.url;
if ( typeof(this.url) === "undefined" &&
_attrs.path !== undefined &&
_attrs.source !== undefined) {
this.url = `file://${[_attrs.path, _attrs.source].join("/")}`;
}
// keep track of our main saver.json file
this.src = _attrs.src;
if ( typeof(this.src) === "undefined" && typeof(this.key) !== "undefined" ) {
const baseDir = this.key.replace(/\\/g,"/").replace(/\/[^/]*$/, "");
this.src = [baseDir, "saver.json"].join("/");
}
this.published = _attrs.published;
if ( typeof(this.published) === "undefined" ) {
this.published = true;
}
// provide a default editable value (this will
// be set when loading to determine if the user
// can edit this screensaver or not)
this.editable = _attrs.editable;
if ( typeof(this.editable) === "undefined" ) {
this.editable = false;
}
this.valid = typeof(this.name) !== "undefined" &&
typeof(this.description) !== "undefined" &&
this.published === true;
if ( typeof(_attrs.options) === "undefined" ) {
_attrs.options = [];
}
this.options = _attrs.options;
if ( this.valid === true ) {
// figure out the settings from any defaults for this screensaver,
// and combine with incoming user-specified settings
this.settings = _attrs.options.map(function(o) {
return [o.name, o.default];
}).reduce(function(o, v) {
o[v[0]] = v[1];
return o;
}, {});
this.settings = Object.assign({}, this.settings, _attrs.settings);
// allow for custom preview URL -- if not specified, just use the default
// if it is specified, do some checks to see if it's a full URL or a filename
// in which case we will turn it into a full path
if ( typeof(this.attrs.previewUrl) === "undefined" ) {
this.previewUrl = this.url;
}
else if ( this.attrs.previewUrl.match(/:\/\//) ) {
this.previewUrl = this.attrs.previewUrl;
}
else {
this.previewUrl = this.path + "/" + this.attrs.previewUrl;
}
} // if valid
}
urlWithParams(opts={}) {
if ( !this.url.match(/^file:/) ) {
return this.url;
}
const urlParams = new URLSearchParams(opts);
if ( this.settings ) {
const keys = Object.keys(this.settings);
keys.forEach((k) => {
urlParams.append(k, this.settings[k]);
});
}
return `${this.url}?${urlParams.toString()}`;
}
toHash() {
return this.attrs;
}
toJSON(attrs) {
for ( var i = 0 ; i < this.UNWRITABLE_KEYS.length; i++ ) {
delete(attrs[this.UNWRITABLE_KEYS[i]]);
}
if ( attrs.requirements === undefined ) {
attrs.requirements = [];
}
else {
attrs.requirements = attrs.requirements.filter(r => r !== "none");
}
if ( attrs.requirements.length === 0 ) {
attrs.requirements = ["none"];
}
return JSON.stringify(attrs, null, 2);
}
write(attrs, configDest) {
if ( typeof(attrs) === "undefined" ) {
attrs = this.attrs;
}
if ( typeof(configDest) === "undefined" ) {
configDest = nodePath.join(this.path, "saver.json");
}
fs.writeFileSync(configDest, this.toJSON(attrs));
}
}
================================================
FILE: src/main/assets/global.css
================================================
body {margin:0; padding:0; overflow: hidden}
*, *:hover { cursor: none !important; }
canvas {display:block;}
canvas:focus {outline:0;}
================================================
FILE: src/main/assets/grabber.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>screen grabber</title>
</head>
<body>
you should never see me
</body>
<script>
window.grabber.init();
</script>
</html>
================================================
FILE: src/main/assets/grabber.mjs
================================================
/**
* This is the preload script for the screen grabber
*
*/
const { contextBridge, ipcRenderer } = require("electron");
/**
* look for an element in the DOM and create it if it doesn't exist
*/
var findOrCreate = function(type, screen_id) {
const id = `${type}${screen_id}`;
let el = document.getElementById(id);
if ( el === null ) {
el = document.createElement(type);
el.id = id;
document.body.appendChild(el);
}
return el;
};
/**
* Apply the video stream to the canvas. Return a context with a screenshot
*
* @param {*} video
* @param {*} canvas
*/
var applyVideoToCanvas = function(video, canvas) {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.drawImage(video, 0, 0, width, height);
return context;
};
let captureIndex = 0;
var screenToBuffer = async function(video) {
const tempName = `capture-index-${captureIndex}`;
const canvas = findOrCreate("canvas", tempName);
const context = applyVideoToCanvas(video, canvas);
const data = canvas.toDataURL("image/png", 1.0);
context.clearRect(0, 0, canvas.width, canvas.height);
const buffer = Buffer.from(data.split(",")[1], "base64");
canvas.remove();
return buffer;
};
/**
* cleanup video/media stream
* @param {*} video
* @param {*} s
*/
var cleanup = function(video, s) {
//
// stop video capture
// this seems to handle a problem where CPU load spikes
// after capture
//
if ( s !== undefined ) {
s.getVideoTracks().forEach((track) => {
track.stop();
});
}
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video
video.pause();
video.src = "";
video.load();
video.remove();
};
/**
* capture the screen
* @param {*} id ID of the screen to capture
* @param {*} width
* @param {*} height
*/
var captureScreen = async function(id, width, height) {
const screen_opts = {
audio: false,
video: {
mandatory: {
// fun fact -- you need to use max here
// @see https://groups.google.com/a/chromium.org/forum/#!topic/chromium-apps/TP_rsnYVQWg
maxWidth: width,
maxHeight: height,
chromeMediaSource: "desktop"
}
}
};
screen_opts.video.mandatory.chromeMediaSourceId = id;
const video = findOrCreate("video", id);
// adding muted helps with some security errors
// @see https://stackoverflow.com/questions/49930680/
// how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use
video.muted = "muted";
const mediaStream = await navigator.mediaDevices.getUserMedia(screen_opts);
video.srcObject = mediaStream;
await video.play();
const result = await screenToBuffer(video, mediaStream);
cleanup(video, mediaStream);
return result;
}; // captureScreen
contextBridge.exposeInMainWorld(
"grabber",
{
init: () => {
ipcRenderer.on("request-screenshot", async (_event, opts) => {
const result = await captureScreen(opts.id, opts.width, opts.height);
ipcRenderer.send("screenshot-" + opts.id, {buffer: result});
});
}
}
);
================================================
FILE: src/main/assets/preload.mjs
================================================
const { contextBridge, ipcRenderer } = require("electron");
const api = {
platform: () => process.platform,
getDisplayBounds: async() => ipcRenderer.invoke("get-primary-display-bounds"),
getScreenshot: async() => ipcRenderer.invoke("get-primary-screenshot"),
getGlobals: async() => ipcRenderer.invoke("get-globals"),
addListener: (key, fn) => ipcRenderer.on(key, fn),
removeListener: (key, fn) => ipcRenderer.removeListener(key, fn),
getPrefs: async() => ipcRenderer.invoke("get-prefs"),
getDefaults: async() => ipcRenderer.invoke("get-defaults"),
openWindow: (name, opts) => ipcRenderer.send("open-window", name, opts),
closeWindow: (name) => ipcRenderer.send("close-window", name),
listSavers: async() => ipcRenderer.invoke("list-savers"),
loadSaver: async(src) => ipcRenderer.invoke("load-saver", src),
deleteSaverDialog: async(key) => ipcRenderer.invoke("delete-screensaver-dialog", key),
deleteSaver: async(key) => ipcRenderer.invoke("delete-saver", key),
saveScreensaver: async(saver, src) => ipcRenderer.invoke("save-screensaver", saver, src),
updatePrefs: async(prefs) => ipcRenderer.invoke("update-prefs", prefs),
setAutostart: (value) => ipcRenderer.send("set-autostart", value),
setGlobalLaunchShortcut: (value) => ipcRenderer.send("set-global-launch-shortcut", value),
displayUpdateDialog: () => ipcRenderer.send("display-update-dialog"),
resetToDefaultsDialog: async() => ipcRenderer.invoke("reset-to-defaults-dialog"),
openUrl: (url) => ipcRenderer.send("launch-url", url),
updateLocalSource: (ls) => ipcRenderer.invoke("update-local-source", ls),
createScreensaver: (opts) => ipcRenderer.invoke("create-screensaver", opts),
saversUpdated: (key) => ipcRenderer.send("savers-updated", key),
getScreensaverPackage: () => ipcRenderer.invoke("check-screensaver-package"),
downloadScreensaverPackage: () => ipcRenderer.invoke("download-screensaver-package"),
showOpenDialog: () => ipcRenderer.invoke("show-open-dialog"),
openFolder: (path) => ipcRenderer.send("open-folder", path),
watchFolder: (src) => ipcRenderer.send("watch-folder", src),
unwatchFolder: (src) => ipcRenderer.send("unwatch-folder", src),
onFolderUpdate: (cb) => ipcRenderer.on("folder-update", cb),
toggleDevTools: () => ipcRenderer.send("toggle-dev-tools"),
log: (payload) => ipcRenderer.send("console-log", payload)
};
contextBridge.exposeInMainWorld("api", api);
================================================
FILE: src/main/assets/shim.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>test shim</title>
</head>
<body>
test shim!
<ul id="tray">
</ul>
<input id="ipc" value="" />
<input id="ipcopts" value="" />
<button id="ipcSend">go</button>
<div id="currentState"></div>
<script>
var list = document.querySelector("ul");
function htmlToElement(html) {
var template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
function addTestItem(label) {
var className = label.replace(/ /g, '');
var el = htmlToElement(`<li><button class="${className}">${label}</button></li>`);
list.appendChild(el);
el.addEventListener("click", function(e) {
e.preventDefault();
window.shimApi.clickTray(label);
});
}
window.shimApi.getTrayItems().then((items) => {
items.forEach((item) => {
addTestItem(item);
});
});
document.querySelector('#ipcSend').addEventListener("click", async () => {
const vals = document.querySelector('#ipc').value.split(' ');
let opts;
if (document.querySelector('#ipcopts').value) {
opts = JSON.parse(document.querySelector('#ipcopts').value);
}
else {
opts = {};
}
await window.shimApi.send(vals[0], vals[1], opts);
});
setInterval(function() {
window.shimApi.getCurrentState().then((state) => {
document.querySelector("#currentState").innerHTML = state;
});
}, 100);
</script>
</body>
</html>
================================================
FILE: src/main/assets/shim.js
================================================
const { contextBridge, ipcRenderer } = require("electron");
const shimApi = {
send: (cmd, opts, args={}) => ipcRenderer.send(cmd, opts, args),
getCurrentState: async () => ipcRenderer.invoke("get-current-state"),
getTrayItems: async () => ipcRenderer.invoke("get-tray-items"),
clickTray: (label) => {
ipcRenderer.invoke("click-tray-item", label);
}
};
contextBridge.exposeInMainWorld("shimApi", shimApi);
================================================
FILE: src/main/autostarter.js
================================================
"use strict";
import * as main from "./index.js";
import AutoLaunch from "auto-launch";
export function toggle(appName, value) {
var appLauncher = new AutoLaunch({
name: appName
});
if ( value === true ) {
appLauncher.isEnabled().then((isEnabled) => {
if ( isEnabled ) {
return;
}
appLauncher.
enable().
then((err) =>{
main.log.info("appLauncher enable", err);
}).catch((err) => {
main.log.info("appLauncher enable failed", err);
});
});
}
else {
main.log.info("set auto start == false");
appLauncher.isEnabled().then((isEnabled) => {
if ( !isEnabled ) {
return;
}
appLauncher.
disable().
then(function() {
main.log.info("appLauncher disabled");
}).
catch((err) => {
main.log.info("appLauncher disable failed", err);
});
});
}
}
================================================
FILE: src/main/bootstrap.js
================================================
import { readFile } from 'fs/promises';
export default async function bootstrapApp() {
const packageJSON = JSON.parse(
await readFile(
new URL('../../package.json', import.meta.url)
)
);
var version = undefined;
try {
version = packageJSON.version;
if ( ! process.env.BEFORE_DAWN_RELEASE_NAME ) {
process.env.BEFORE_DAWN_RELEASE_NAME = `${packageJSON.productName} ${packageJSON.version}`;
}
}
catch {
version = "0.0.0";
}
global.APP_NAME = "Before Dawn";
global.APP_DIR = "Before Dawn";
global.SAVER_REPO = "muffinista/before-dawn-screensavers";
global.APP_REPO = "muffinista/before-dawn";
global.APP_VERSION_BASE = version;
global.APP_VERSION = `v${version}`;
global.NEW_RELEASE_AVAILABLE = false;
global.HELP_URL = "https://muffinista.github.io/before-dawn/";
global.ISSUES_URL = "https://github.com/muffinista/before-dawn/issues";
global.APP_CREDITS = "by Colin Mitchell // muffinlabs.com";
if ( packageJSON.release_server ) {
global.RELEASE_SERVER = packageJSON.release_server;
global.RELEASE_CHECK_URL = `${global.RELEASE_SERVER}/update/${process.platform}/${global.APP_VERSION_BASE}`;
global.PACKAGE_DOWNLOAD_URL = `https://github.com/${global.APP_REPO}/releases/latest`;
}
}
================================================
FILE: src/main/dock.js
================================================
"use strict";
import {app, BrowserWindow} from "electron";
/**
* if we're using the dock, and all our windows are closed, hide the
* dock icon
*/
export const hideDockIfInactive = function() {
let openWindowCount = BrowserWindow.getAllWindows().
filter(win => (win !== undefined && win.noTray !== true) ).length;
if ( typeof(app.dock) !== "undefined" && openWindowCount === 0 ) {
app.dock.hide();
}
};
/**
* show the dock if it's available
*/
export const showDock = function() {
if ( typeof(app.dock) !== "undefined" ) {
app.dock.show();
}
};
================================================
FILE: src/main/index.dev.js
================================================
/**
* This file is used specifically and only for development. There shouldn't be
* any need to modify this file, but it can be used to extend your development
* environment.
*/
// Set environment for development
process.env.NODE_ENV = 'development';
// Require `main` process to boot app
require('./index');
================================================
FILE: src/main/index.js
================================================
"use strict";
// process.traceDeprecation = true;
// process.traceProcessWarnings = true;
/***
Welcome to....
____ __ ____
| __ ) ___ / _| ___ _ __ ___ | _ \ __ ___ ___ __
| _ \ / _ \ |_ / _ \| '__/ _ \ | | | |/ _` \ \ /\ / / '_ \
| |_) | __/ _| (_) | | | __/ | |_| | (_| |\ V V /| | | |
|____/ \___|_| \___/|_| \___| |____/ \__,_| \_/\_/ |_| |_|
a screensaver package built on the tools of the web. Enjoy!
*/
import { init } from '@sentry/electron';
if ( process.env.TEST_MODE === undefined && process.env.SENTRY_DSN !== undefined ) {
console.log(`setting up sentry with ${process.env.SENTRY_DSN}`);
try {
init({
dsn: process.env.SENTRY_DSN,
onFatalError: console.log
});
}
catch(e) {
console.log(e);
}
}
import {app,
BrowserWindow,
desktopCapturer,
dialog,
globalShortcut,
ipcMain,
Menu,
net,
session,
shell,
systemPreferences,
Tray,
powerMonitor} from "electron";
import isDev from 'electron-is-dev';
import log from 'electron-log';
import { screen as electronScreen } from "electron";
import * as fs from "fs";
import { readFile } from 'fs/promises';
import * as os from "os";
import * as path from "path";
import * as temp from "temp";
import * as url from "url";
import { execFile as exec } from "child_process";
import * as screenLock from "./screen.js";
import StateManager from "./state_manager.js";
import SaverPrefs from "../lib/prefs.js";
import SaverFactory from "../lib/saver-factory.js";
import Saver from "../lib/saver.js";
import SaverListManager from "../lib/saver-list.js";
import Package from "../lib/package.js";
import Power from "../main/power.js";
import * as menusAndTrays from "./menus.js";
import * as dock from "./dock.js";
import * as windows from "./windows.js";
import forceFocus from "forcefocus";
import ReleaseCheck from "./release_check.js";
import * as autostarter from "./autostarter.js";
/**
* try and guess if we are in fullscreen mode or not
*/
import FullScreen from "detect-fullscreen";
const { isFullscreen } = FullScreen;
const packageJSON = JSON.parse(
await readFile(
new URL('../../package.json', import.meta.url)
)
);
var releaseChecker;
// NOTE -- this needs to be global, otherwise the app icon gets
// garbage collected and won't show up in the system tray
let appIcon = null;
let debugMode = ( process.env.DEBUG_MODE !== undefined );
let testMode = ( process.env.TEST_MODE !== undefined );
let cursor;
if (testMode || debugMode) {
log.transports.console.format = "{h}:{i}:{s} {text}";
log.catchErrors();
}
//
// don't hide cursor in tests or in windows, since
// that causes the tray to stop working???
//
if ( testMode || process.platform === "win32" ) {
cursor = {
hide: () => {},
show: () => {}
};
}
else {
cursor = await import("hide-cursor");
cursor = cursor.default;
}
let exitOnQuit = false;
/**
* track some information about windows and preview bounds for the prefs window
* and editor window
*/
let handles = {
prefs: {
window: null,
bounds: {
width: 320,
height: 0
},
max: {
width: 320,
height: 320
}
},
settings: {
window: null
},
addNew: {
window: null
},
about: {
window: null
},
editor: {
window: null,
bounds: {
width: 320,
height: 0
},
max: {
width: 320,
height: 320
}
},
// shim: {
// window: null
// }
};
let trayMenu;
let prefs = undefined;
let savers = undefined;
let stateManager = undefined;
// usually we want to check power state before running, but
// we'll skip that check depending on the value of this toggle
// so that manually running screensaver works just fine
let checkPowerState = true;
const RELEASE_CHECK_INTERVAL = 1000 * 60 * 60 * 12;
// load a few global variables
import bootstrapApp from "./bootstrap.js";
await bootstrapApp();
const defaultWebPreferences = {
enableRemoteModule: false,
contextIsolation: true,
nodeIntegration: false,
nativeWindowOpen: true,
webSecurity: !isDev
};
const singleLock = app.requestSingleInstanceLock();
if (! singleLock ) {
console.log("looks like another copy of app is running, exiting!");
app.quit();
process.exit();
}
const power = new Power({
platform: process.platform,
method: powerMonitor.isOnBatteryPower
});
let screenData = [];
var listScreens = async function() {
try {
const sources = await desktopCapturer.getSources({
types: ["screen"],
});
screenData = sources;
return sources;
} catch(e) {
log.info(e);
}
return [];
};
/**
* Open the screengrab window
*
* @returns {Promise} Promise that resolves once window is loaded
*/
var openGrabberWindow = function() {
return new Promise((resolve) => {
log.info("openGrabberWindow");
const grabberUrl = `file://${getAssetsDir()}/grabber.html`;
var grabberWindow = new BrowserWindow({
show: false,
skipTaskbar: true,
width: 100,
height: 100,
x: 6000,
y: 2000,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "grabber.mjs")
}
});
// grabberWindow.noTray = true;
grabberWindow.once("ready-to-show", () => {
resolve(grabberWindow);
});
grabberWindow.loadURL(grabberUrl);
});
};
/**
* open our screen grabber tool and issue a screengrab request
* @param {Screen} s the screen to grab
* @returns {Promise} Promise that resolves with object containing URL of screenshot
*/
var grabScreen = function(s) {
log.info(`grab screen ${s.id}`);
let screen = screenData.find((s) => { return s.display_id.toString() === s.id.toString(); });
if ( ! screen ) {
screen = screenData[0];
}
return new Promise((resolve) => {
//
// bypass screen capture in test mode
// or if the user has blocked screen access
//
if (
(process.platform === "darwin" && systemPreferences.getMediaAccessStatus("screen") !== "granted" ) ||
testMode === true ) {
resolve({
url: path.join(getAssetsDir(), "color-bars.png")
});
}
else {
let windowRef;
ipcMain.once(`screenshot-${screen.id}`, function(_e, message) {
const tempName = temp.path({dir: os.tmpdir(), suffix:".png"});
fs.writeFileSync(tempName, message.buffer);
resolve({
url: tempName
});
// close the screen grabber window
try {
windowRef.close();
}
catch(ex) {
if ( typeof(Sentry) !== "undefined" ) {
// eslint-disable-next-line no-undef
Sentry.captureException(ex);
}
}
// rewrite file paths to always have unix slashes instead
// of windows slashes. sometimes windows slashes are fine, but
// there's a few situations where they won't render properly.
// message.url = message.url.split(path.sep).join(path.posix.sep);
resolve(message);
});
openGrabberWindow().then((w) => {
windowRef = w;
windowRef.webContents.send("request-screenshot", {
id: screen.id,
width: s.bounds.width,
height: s.bounds.height});
});
}
});
};
/**
* open a simple window that our mocha/spectron tests can use.
*
* this exists mostly because it's basically impossible to test
* an app that doesn't open a window.
*/
var openTestShim = function() {
var testWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "shim.js")
}
});
const shimUrl = `file://${getAssetsDir()}/shim.html`;
testWindow.loadURL(shimUrl);
// testWindow.webContents.openDevTools();
};
let screenshots = {};
/**
* Open the preferences window
* @returns {Promise} Promise that resolves when prefs window is shown
*/
var openPrefsWindow = function() {
if ( handles.prefs.window !== null && handles.prefs.window !== undefined ) {
return new Promise((resolve) => {
handles.prefs.window.show();
resolve();
});
}
return new Promise((resolve) => {
const primary = electronScreen.getPrimaryDisplay();
// take a screenshot of the main screen for use in previews
grabScreen(primary).then((grab) => {
screenshots[primary.id] = grab.url;
const prefsUrl = getUrl("prefs.html");
handles.prefs.window = new BrowserWindow({
show: false,
width: 910,
height: 700,
minWidth: 800,
maxWidth: 910,
minHeight: 600,
resizable: true,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs")
},
icon: path.join(getAssetsDir(), "iconTemplate.png")
});
if ( !isDev && handles.prefs.window.removeMenu !== undefined ) {
handles.prefs.window.removeMenu();
}
handles.prefs.window.on("closed", () => {
handles.prefs.window = null;
dock.hideDockIfInactive(app);
});
handles.prefs.window.once("ready-to-show", () => {
handles.prefs.window.show();
dock.showDock(app);
});
handles.prefs.window.once("show", resolve);
log.info("loading " + prefsUrl);
handles.prefs.window.loadURL(prefsUrl);
});
});
};
var openSettingsWindow = function() {
if ( handles.settings.window !== null && handles.settings.window !== undefined ) {
return new Promise((resolve) => {
handles.settings.window.show();
resolve();
});
}
var settingsUrl = getUrl("settings.html");
handles.settings.window = new BrowserWindow({
show: false,
width:600,
height:650,
maxWidth: 600,
minWidth: 600,
resizable: true,
parent: handles.prefs.window,
modal: true,
icon: path.join(getAssetsDir(), "iconTemplate.png"),
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
}
});
// hide the file menu
if ( !isDev && handles.settings.window.removeMenu !== undefined ) {
handles.settings.window.removeMenu();
}
handles.settings.window.on("closed", () => {
handles.settings.window = null;
dock.hideDockIfInactive(app);
});
handles.settings.window.once("ready-to-show", () => {
handles.settings.window.show();
dock.showDock(app);
});
log.info(`open ${settingsUrl}`);
handles.settings.window.loadURL(settingsUrl);
};
/**
* handle new screensaver event. open the window to create a screensaver
*/
var addNewSaver = async function(opts) {
var newUrl = getUrl("new.html");
var primary = electronScreen.getPrimaryDisplay();
// take a screenshot of the main screen for use in previews
if ( !opts.screenshot) {
const grab = await grabScreen(primary);
opts.screenshot = grab.url;
}
handles.addNew.window = new BrowserWindow({
show: false,
width: 450,
height: 700,
resizable:true,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
},
icon: path.join(getAssetsDir(), "iconTemplate.png")
});
handles.addNew.window.on("closed", () => {
handles.addNew.window = null;
dock.hideDockIfInactive(app);
});
handles.addNew.window.once("ready-to-show", () => {
handles.addNew.window.show();
dock.showDock(app);
});
handles.addNew.window.loadURL(newUrl);
};
/**
* Open the About window for the app
*/
var openAboutWindow = function() {
var aboutUrl = getUrl("about.html");
handles.about.window = new BrowserWindow({
show: false,
width:500,
height:600,
resizable:false,
icon: path.join(getAssetsDir(), "iconTemplate.png"),
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
}
});
if ( !isDev && handles.about.window.removeMenu !== undefined ) {
handles.about.window.removeMenu();
}
handles.about.window.on("closed", () => {
handles.about.window = null;
dock.hideDockIfInactive(app);
});
handles.about.window.once("ready-to-show", () => {
handles.about.window.show();
dock.showDock(app);
});
log.info(`open ${aboutUrl}`);
handles.about.window.loadURL(aboutUrl);
};
/**
* open the editor tool for a screensaver
* @param {Object} args object with arguments
* @param {string} args.src path to the JSON file for the screensaver
* @param {string} args.screenshot path to the screenshot to use when editing
*/
var openEditor = (args) => {
var key = args.src;
var screenshot = args.screenshot;
var editorUrl = getUrl("editor.html");
var target = editorUrl + "?" +
"src=" + encodeURIComponent(key) +
"&screenshot=" + encodeURIComponent(screenshot);
if ( handles.editor.window == null ) {
handles.editor.window = new BrowserWindow({
show: false,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
},
});
}
handles.editor.window.screenshot = screenshot;
handles.editor.window.once("ready-to-show", () => {
handles.editor.window.send("args", args);
handles.editor.window.show();
if (process.env.NODE_ENV === "test") {
handles.editor.window.webContents.closeDevTools();
}
dock.showDock(app);
});
handles.editor.window.on("closed", () => {
handles.editor.window = null;
if ( handles.editor.preview ) {
handles.editor.preview.destroy();
}
handles.editor.preview = null;
dock.hideDockIfInactive(app);
});
handles.editor.window.loadURL(target);
};
/**
* get the BrowserWindow options we'll use to launch
* on the given screen.
*/
var getWindowOpts = function(s) {
var opts = {
backgroundColor: "#000000",
alwaysOnTop: true,
x: s.bounds.x,
y: s.bounds.y,
show: false,
roundedCorners: false,
titleBarStyle: "customButtonsOnHover",
webPreferences: {
...defaultWebPreferences
}
};
// osx will display window immediately if fullscreen is true
// so we default it to false there
if (process.platform !== "darwin" ) {
opts.fullscreen = true;
}
if ( testMode === true ) {
opts.fullscreen = false;
// opts.x = 100;
// opts.y = 100;
opts.show = true;
opts.width = 400;
opts.height = 400;
}
return opts;
};
var applyScreensaverWindowEvents = function(w) {
// Emitted when the window is closed.
w.once("closed", function() {
if (process.platform !== "win32" ) {
cursor.show();
}
windows.forceWindowClose(w);
});
// inject our custom CSS into the screensaver window
w.webContents.on("did-finish-load", function() {
log.info("did-finish-load");
if (!w.isDestroyed()) {
// load some global CSS we'll inject into running screensavers
const globalCSSCode = fs.readFileSync( path.join(getAssetsDir(), "global.css"), "ascii");
w.webContents.insertCSS(globalCSSCode);
}
});
// we could do something nice with either of these events
w.webContents.on("render-process-gone", log.info);
w.webContents.on("unresponsive", log.info);
};
/**
*
* @param {String} screenshot URL of screenshot
* @param {Saver} saver the screensaver to run
* @param {Screen} s the screen to run it on
* @param {Object} url_opts any options to pass on the url
* @param {number} tickCount hrtime value of when we started
*/
var runSaver = function(screenshot, saver, s, url_opts, tickCount) {
const windowOpts = getWindowOpts(s);
var w = new BrowserWindow(windowOpts);
w.isSaver = true;
if ( w.removeMenu !== undefined ) {
w.removeMenu();
}
let diff = process.hrtime(tickCount);
log.info(`run screensaver ${saver.name} on screen ${s.id} ${saver.url} ts: ${diff[0] * 1e9 + diff[1]}`);
return new Promise((resolve, reject) => {
try {
applyScreensaverWindowEvents(w);
w.webContents.once("did-fail-load", (_event, _code, description) => {
log.info(`did-fail-load: ${description}`);
windows.forceWindowClose(w);
reject(s.id, description);
});
w.once("ready-to-show", () => {
log.info("ready-to-show", s.id);
if ( testMode !== true ) {
windows.setFullScreen(w);
}
if (process.platform === "win32" ) {
log.info("force focus");
forceFocus.focusWindow(w);
}
diff = process.hrtime(tickCount);
log.info(`rendered in ${diff[0] * 1e9 + diff[1]} nanoseconds`);
resolve(s.id);
});
if ( typeof(screenshot) !== "undefined" ) {
log.info(`pass screenshot ${screenshot}`);
url_opts.screenshot = encodeURIComponent("file://" + screenshot);
}
// w.webContents.openDevTools();
// generate screensaver object, then get url to load
const saverObj = new Saver(saver);
const url = saverObj.urlWithParams(url_opts);
log.info("Loading " + url, s.id);
// and load the index.html of the app.
w.loadURL(url);
}
catch (e) {
log.info(e);
windows.forceWindowClose(w);
reject(s.id, e);
}
});
};
/**
* run the specified screensaver on the specified screen
*/
var runScreenSaverOnDisplay = function(saver, s) {
var size = s.bounds;
var url_opts = {
width: size.width,
height: size.height,
platform: process.platform
};
log.info("runScreenSaverOnDisplay", s.id);
// don't do anything if we don't actually have a screensaver
if ( typeof(saver) === "undefined" || saver === null ) {
log.info("no saver, exiting");
return Promise.resolve();
}
let tickCount = process.hrtime();
//
// if this screensaver uses a screengrab, get it.
// otherwise just boot it
//
const reqs = saver.requirements;
if ( reqs !== undefined && reqs.findIndex && reqs.findIndex((x) => { return x === "screen"; }) > -1 ) {
return grabScreen(s).then((message) => {
runSaver(message.url, saver, s, url_opts, tickCount);
});
}
else {
return runSaver(undefined, saver, s, url_opts, tickCount);
}
};
/**
* blank out the given screen
*/
var blankScreen = async function(s) {
if ( process.env.TEST_MODE ) {
log.info("refusing to blank screen in test mode");
return s.id;
}
const systemPath = getSystemDir();
const blankUrl = `file://${path.join(systemPath, "system-savers", "blank", "index.html")}`;
const saver = {
name: "Blank",
url: blankUrl
};
return runScreenSaverOnDisplay(saver, s);
};
/**
* get a list of displays connected to the computer.
*/
var getDisplays = function() {
var displays = [];
if ( debugMode === true || prefs.runOnSingleDisplay === true ) {
displays = [
electronScreen.getPrimaryDisplay()
];
}
else {
displays = electronScreen.getAllDisplays();
}
return displays;
};
/**
* get a list of the non primary displays connected to the computer
*/
var getNonPrimaryDisplays = function() {
var primary = electronScreen.getPrimaryDisplay();
return electronScreen.getAllDisplays().filter((d) => {
return d.id !== primary.id;
});
};
/**
* manually trigger screensaver by setting state to run
*/
var setStateToRunning = function() {
log.info("setStateToRunning");
// disable power state check
checkPowerState = false;
stateManager.run();
};
var setStateToPaused = function() {
log.info("setStateToPaused");
stateManager.pause();
stateManager.stopTicking();
};
var resetState = function() {
stateManager.reset();
};
/**
* return a promise the resolves to the path to the screensaver and its options
*/
var findScreensaver = function() {
const workingPath = getSystemDir();
// check if the user is running the random screensaver. if so, pick one!
const randomPath = path.join(workingPath, "system-savers", "random", "saver.json");
// log.info("random: " + randomPath);
if ( prefs.saver === randomPath ) {
return new Promise((resolve) => {
savers.list(() => {
// @todo s can be undefined
// https://sentry.io/organizations/colin-mitchell/issues/955633850/?project=172824&query=is%3Aunresolved&statsPeriod=14d&utc=false
let s = savers.random();
resolve(s.key, prefs.getOptions(s.key));
});
});
}
return Promise.resolve(prefs.saver, prefs.getOptions(prefs.saver));
};
/**
* run the user's chosen screensaver on any available screens
*/
var runScreenSaver = function() {
log.info("runScreenSaver");
const setupPromise = findScreensaver();
setupPromise.
then((saverKey, settings) => savers.loadFromFile(saverKey, settings)).
catch((err) => {
log.info("================ loading saver failed?");
log.info(err.message);
return undefined;
}).
then((saver) => {
let displays = [];
let blanks = [];
// make sure we have something to display
if ( typeof(saver) === "undefined" ) {
log.info("No screensaver defined! Just blank everything");
blanks = getDisplays().concat(getNonPrimaryDisplays());
}
else if ( testMode === true ) {
blanks = [];
} else {
displays = getDisplays();
if ( debugMode !== true && testMode !== true && prefs.runOnSingleDisplay === true ) {
blanks = getNonPrimaryDisplays();
}
}
// turn off idle checks for a couple seconds while loading savers
stateManager.ignoreReset(true);
cursor.hide();
//
// generate an array of promises for rendering screensavers on any screens
//
const promises = displays
.map((d) => runScreenSaverOnDisplay(saver, d))
.concat(
blanks.map((d) => blankScreen(d))
);
Promise.allSettled(promises).then((values) => {
log.info("final result", values);
setRunningInABit();
}).catch((e) => {
log.info("running screensaver failed");
log.info(e);
stateManager.reset();
cursor.show();
});
});
};
/**
* After a short delay, set state manager to running. This should
* help with mouse wiggle/etc
*/
var setRunningInABit = function() {
setTimeout(function() {
log.info("our work is done, set state to running");
stateManager.running();
}, 1500);
};
/**
* should we lock the user's screen when returning from running the saver?
*/
var shouldLockScreen = function() {
// we can't lock the screen on OSX because it would involve using
// private APIs and is a super pain in the butt
return ( prefs.lock === true );
};
/**
* stop the running screensaver
*/
var stopScreenSaver = function(fromBlank) {
log.info("received stopScreenSaver call");
if ( fromBlank !== true ) {
stateManager.reset();
}
// trigger lock screen before actually closing anything
else if ( shouldLockScreen() && screenLock.doLockScreen ) {
log.info("lock the screen");
screenLock.doLockScreen();
}
windows.closeRunningScreensavers();
cursor.show();
};
/**
* determine what our system directory is. this should basically be
* where the app exists, and where the system-savers directory and
* other critical files exist.
*/
var getSystemDir = function() {
if ( process.env.BEFORE_DAWN_SYSTEM_DIR !== undefined ) {
return process.env.BEFORE_DAWN_SYSTEM_DIR;
}
if ( process.env.TEST_MODE ) {
return app.getAppPath();
}
if ( app.isPackaged ) {
return path.join(app.getAppPath(), "output");
}
return path.join(app.getAppPath(), "..", "..", "output");
};
/**
* determine what our assets directory is. This is where global CSS,
* icons, etc, can be found.
*/
let getAssetsDir = function() {
if ( process.env.BEFORE_DAWN_ASSETS_DIR !== undefined ) {
return process.env.BEFORE_DAWN_ASSETS_DIR;
}
if ( app.isPackaged ) {
return path.join(app.getAppPath(), "output", "assets");
}
if ( process.env.TEST_MODE ) {
return path.join(app.getAppPath(), "assets");
}
return path.join(app.getAppPath(), "assets");
};
/**
* return the URL prefix we should use when loading app windows. if
* running in development mode with hot reload enabled, we'll use an
* HTTP request, otherwise we'll use a file:// url.
*/
var getUrl = function(dest) {
let baseUrl;
if ( !testMode && isDev ) {
let devPort;
try {
devPort = packageJSON.devport;
}
catch {
devPort = 9080;
}
baseUrl = `http://localhost:${devPort}`;
return new URL(dest, new URL(baseUrl)).toString();
}
log.info(`hey!!! ${app.getAppPath()}`);
if ( testMode ) {
return `file://${app.getAppPath()}/${dest}`;
}
return `file://${app.getAppPath()}/output/${dest}`;
};
var setupForTesting = function() {
if ( testMode === true ) {
log.info("opening shim for test mode");
openTestShim();
}
};
/**
* build and apply an application menu and tray menu
*/
var setupMenuAndTray = function() {
var menu = Menu.buildFromTemplate(menusAndTrays.buildMenuTemplate(app));
Menu.setApplicationMenu(menu);
//
// build the tray menu
//
trayMenu = Menu.buildFromTemplate(menusAndTrays.trayMenuTemplate());
trayMenu.items[3].visible = global.NEW_RELEASE_AVAILABLE;
const iconImage = menusAndTrays.trayIconImage();
appIcon = new Tray(iconImage);
appIcon.setToolTip(global.APP_NAME);
appIcon.setContextMenu(trayMenu);
// show tray menu on right click
// @todo should this be osx only?
appIcon.on("right-click", () => {
appIcon.popUpContextMenu();
});
appIcon.on("click", () => {
appIcon.popUpContextMenu();
});
};
/**
* setup any requirements for the app
*
* @returns {Promise} Promise that resolves with true if setup for first time, false if app was ready
*/
var setupIfNeeded = async function() {
log.info("setupIfNeeded");
if ( process.env.QUIET_MODE === "true" || process.env.NODE_ENV === "test" ) {
log.info("Quiet/test mode, skip setup checks!");
return false;
}
// check if we should download savers, set something up, etc
if ( process.env.FORCE_SETUP || prefs.needSetup ) {
// stop processing here, we know we need to setup
log.info("needSetup!");
return true;
}
// log.info(`checking if ${prefs.saver} is valid`);
const exists = await savers.confirmExists(prefs.saver);
if ( ! exists ) {
log.info("need to pick a new screensaver");
}
else {
log.info("looks like we are good to go");
}
return !exists;
};
/**
* open the preferences window if needed
*
* @param {Boolean} status true if we need to open the prefs window
*/
var openPrefsWindowIfNeeded = function(status) {
log.info("openPrefsWindowIfNeeded");
if ( status === true ) {
log.info("we do need to open prefs window");
return openPrefsWindow();
}
return Promise.resolve();
};
/**
* setup our periodic release check
*/
var setupReleaseCheck = function() {
if ( ! global.RELEASE_CHECK_URL ) {
log.info("no release server set, so no release checks");
return;
}
releaseChecker = new ReleaseCheck();
releaseChecker.setFeed(global.RELEASE_CHECK_URL);
releaseChecker.setLogger(log.info);
releaseChecker.onUpdate(() => {
global.NEW_RELEASE_AVAILABLE = true;
log.info("update available, show it");
getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;
});
releaseChecker.onNoUpdate(() => {
global.NEW_RELEASE_AVAILABLE = false;
log.info("no update available, hide it");
getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;
});
log.info("Run initial release check");
checkForNewRelease();
// check for a new release every 12 hours
log.info("Setup release check");
setInterval(checkForNewRelease, RELEASE_CHECK_INTERVAL);
};
/**
* Check if we should move the app to the actual application folder.
* This is important because the app is pretty fragile on OSX otherwise.
*/
var askAboutApplicationsFolder = function() {
if ( testMode === true || isDev === true || app.isInApplicationsFolder === undefined ) {
return;
}
if ( !app.isInApplicationsFolder() ) {
const chosen = dialog.showMessageBoxSync({
type: "question",
buttons: ["Move to Applications", "Do Not Move"],
message: "Move to Applications folder?",
detail: "Hello! I work better in your Applications folder, should I move myself there?"
});
if ( chosen === 0 ) {
app.moveToApplicationsFolder();
}
}
};
/**
* check for permissions to access certain systems on OSX
*/
var askAboutMediaAccess = async function() {
if (process.platform !== "darwin" || testMode === true ) {
return;
}
["microphone", "camera", "screen"].forEach(async (type) => {
log.info(type);
// note: this might be handy
// "mac-screen-capture-permissions": "^1.1.0",
// if ( type === "screen" ) {
// const {
// hasScreenCapturePermission,
// hasPromptedForPermission
// } = require('mac-screen-capture-permissions');
// const result = hasPromptedForPermission();
// const result2 = hasScreenCapturePermission();
// }
// https://www.electronjs.org/docs/api/system-preferences#systempreferencesaskformediaaccessmediatype-macos
log.info(`access to ${type}: ${systemPreferences.getMediaAccessStatus(type)}`);
// re: screen -- This permission can only be granted manually in the System
// Preferences. Therefore systemPreferences.askForMediaAccess() cannot be
// extended in the same way.
if ( systemPreferences.getMediaAccessStatus(type) !== "granted" && type !== "screen" ) {
await systemPreferences.askForMediaAccess(type);
}
});
};
const getPackage = function() {
const attrs = {
repo: prefs.sourceRepo,
dest: prefs.defaultSaversDir,
log: log.info,
fetch: net.fetch
};
console.log(attrs);
return new Package(attrs);
};
/**
* setup assorted IPC listeners
*/
let setupIPC = function() {
/**
* open the window specified by 'key', passing args along
*/
ipcMain.on("open-window", (_event, key, args) => {
windowMethods[key](args);
});
/**
* set screensaver state to paused
*/
ipcMain.on("pause", () => {
setStateToPaused();
});
/**
* set screensaver state to enabled
*/
ipcMain.on("enable", () => {
resetState();
});
/**
* close the window specified by 'key'
*/
ipcMain.on("close-window", (event, key) => {
if ( handles[key].window ) {
handles[key].window.close();
}
});
ipcMain.on("close-all-windows", () => {
console.log("close-all-windows");
Object.keys(handles).forEach(function(key) {
if ( handles[key].window ) {
console.log("close $key");
handles[key].window.close();
}
});
});
/**
* return prefs data to requester
*/
ipcMain.handle("get-prefs", () => {
log.info("get-prefs");
return prefs.data;
});
/**
* return a couple of global variables
*/
ipcMain.handle("get-globals", () => {
return {
APP_VERSION: global.APP_VERSION,
APP_REPO: global.APP_REPO,
NEW_RELEASE_AVAILABLE: global.NEW_RELEASE_AVAILABLE
};
});
/**
* return a list of screensavers
*/
ipcMain.handle("list-savers", async () => {
const entries = await savers.list();
return entries;
});
/**
* load and return the specified screensaver
*/
ipcMain.handle("load-saver", async (_event, key) => {
return await savers.loadFromFile(key);
});
/**
* delete the specified screensaver
*/
ipcMain.handle("delete-saver", async(_event, attrs) => {
log.info("delete-saver", attrs);
await savers.delete(attrs);
savers.reset();
prefs.reload();
});
/**
* update prefs with the incoming attrs
*/
ipcMain.handle("update-prefs", async(_event, attrs) => {
log.info("update-prefs", attrs);
// ensure a value for this
attrs.firstLoad = false;
if ( attrs.launchShortcut === undefined ) {
attrs.launchShortcut = "";
}
prefs.store.set(attrs);
savers.reset();
updateStateManager();
});
ipcMain.handle("check-screensaver-package", async() => {
log.info("check-screensaver-package");
return getPackage().getReleaseInfo();
});
ipcMain.handle("download-screensaver-package", async() => {
log.info("download-screensaver-package");
const result = await getPackage().downloadRelease();
log.info(result);
toggleSaversUpdated();
return result;
});
/**
* return the default settings for the app
*/
ipcMain.handle("get-defaults", async() => {
log.info("get-defaults");
log.info(prefs.defaults);
return prefs.defaults;
});
/**
* update the local source settings
*/
ipcMain.handle("update-local-source", async(_event, ls) => {
log.info("update-local-source", ls);
prefs.store.set("localSource", ls);
savers.reset();
});
/**
* create a new screensaver from our template
*/
ipcMain.handle("create-screensaver", async(_event, attrs) => {
const factory = new SaverFactory();
const src = path.join(getSystemDir(), "system-savers", "__template");
log.info(`create-screensaver from ${src}`);
const dest = prefs.localSource;
const data = factory.create(src, dest, attrs);
savers.reset();
return data;
});
/**
* save/update a screensaver object
*/
ipcMain.handle("save-screensaver", async(_event, attrs, dest) => {
const s = new Saver(attrs);
s.write(attrs, dest);
});
/**
* return the bounds of the primary screen to the requester
*/
ipcMain.handle("get-primary-display-bounds", () => {
return electronScreen.getPrimaryDisplay().bounds;
});
/**
* return a screengrab of the primary screen to the requester
*/
ipcMain.handle("get-primary-screenshot", () => {
return screenshots[electronScreen.getPrimaryDisplay().id];
});
/**
* load the requested URL in a browser
*/
ipcMain.on("launch-url", (_event, url) => {
shell.openExternal(url);
});
/**
* handle savers-updated event. this is sent when a screensaver is created/updated
*/
ipcMain.on("savers-updated", () => {
log.info("savers-updated");
toggleSaversUpdated();
});
/**
* set autostart value
*/
ipcMain.on("set-autostart", (_event, value) => {
log.info("set-autostart");
if ( process.env.TEST_MODE !== undefined ) {
log.info("we're in test mode, skipping autostart");
return;
}
autostarter.toggle(global.APP_NAME, value);
});
/**
* handle event to set global launch shortcut
*/
ipcMain.on("set-global-launch-shortcut", () => {
log.info("set-global-launch-shortcut");
setupLaunchShortcut();
});
/**
* run the users specified screensaver
*/
ipcMain.on("run-screensaver", () => {
log.info("run-screensaver");
setStateToRunning();
});
ipcMain.on("toggle-dev-tools", () => {
log.info("toggle-dev-tools");
if ( handles.editor.window !== null ) {
handles.editor.window.webContents.openDevTools();
}
});
ipcMain.on("console-log", (_event, payload) => {
log.info(payload);
});
/**
* open a folder
*/
ipcMain.on("open-folder", (_event, src) => {
var cmd;
var args = [];
// figure out the path to the screensaver folder. use
// decodeURIComponent to convert %20 to spaces
const filePath = path.dirname(decodeURIComponent(url.parse(src).path)); //.split(path.posix.sep).join(path.sep);
switch(process.platform) {
case "darwin":
cmd = "open";
args = [ filePath ];
break;
case "win32":
if (process.env.SystemRoot) {
cmd = path.join(process.env.SystemRoot, "explorer.exe");
}
else {
cmd = "explorer.exe";
}
args = [`${filePath}`];
break;
default:
// # Strip the filename from the path to make sure we pass a directory
// # path. If we pass xdg-open a file path, it will open that file in the
// # most suitable application instead, which is not what we want.
cmd = "xdg-open";
args = [ filePath ];
}
exec(cmd, args, function() {});
});
ipcMain.on("watch-folder", (event, src) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
const folderPath = path.dirname(src);
// make sure folder actually exists
if ( fs.existsSync(folderPath) ) {
win.fsWatcher = fs.watch(folderPath, (eventType, filename) => {
if (filename && win?.webContents) {
win.webContents.send("folder-update", filename);
}
});
}
});
ipcMain.on("unwatch-folder", (event) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
win.fsWatcher.close();
});
/**
* display a dialog about a package update
*/
if ( process.env.TEST_MODE === undefined ) {
ipcMain.on("display-update-dialog", async () => {
const result = await dialog.showMessageBox({
type: "info",
title: "Update Available!",
message: "There's a new update available! Would you like to download it?",
buttons: ["No", "Yes"],
defaultId: 0
});
if ( result.response === 1 ) {
const appRepo = global.APP_REPO;
shell.openExternal(`https://github.com/${appRepo}/releases/latest`);
}
});
}
/**
* display a dialog when the user wants to reset to default settings
*/
ipcMain.handle("reset-to-defaults-dialog", async () => {
const result = await dialog.showMessageBox({
type: "info",
title: "Are you sure?",
message: "Are you sure you want to reset to the default settings?",
buttons: ["No", "Yes"],
defaultId: 0
});
return result.response;
});
/**
* display a confirmation dialog for deleting a screensaver
*/
ipcMain.handle("delete-screensaver-dialog", async (_event, saver) => {
const result = await dialog.showMessageBox(
{
type: "info",
title: "Are you sure?",
message: "Are you sure you want to delete this screensaver?",
detail: `Deleting screensaver ${saver.name}`,
buttons: ["No", "Yes"],
defaultId: 0
});
return result.response;
});
/**
* display a folder chooser for setting local source
*/
ipcMain.handle("show-open-dialog", async () => {
const result = await dialog.showOpenDialog(
{
title: "Pick a screensaver directory",
message: "Pick a folder to store your custom screensavers",
properties: [ "openDirectory", "createDirectory" ]
});
return result;
});
//
// setup a couple of IPC methods we only use in tests
//
if ( testMode === true ) {
/**
* handle requests to get the current state of the app. this
* is currently only called by our test shim
*/
ipcMain.handle("get-current-state", async () => {
return stateManager.currentStateString;
});
/**
* get a list of tray item labels
*/
ipcMain.handle("get-tray-items", async () => {
return menusAndTrays.trayMenuTemplate().map(item => item.label);
});
/**
* fake a click on a tray item
*/
ipcMain.handle("click-tray-item", (_event, label) => {
log.info(`click-tray-item ${label}`);
const items = menusAndTrays.trayMenuTemplate();
const item = items.find(item => item.label === label);
item.click();
});
}
/**
* handle quit app events
*/
ipcMain.once("quit-app", () => {
log.info("quit-app");
quitApp();
});
};
/**
* handle initial startup of app
*/
var bootApp = async function() {
log.info("bootApp");
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({responseHeaders: Object.fromEntries(Object.entries(details.responseHeaders).filter(header => !/x-frame-options/i.test(header[0])))});
});
askAboutApplicationsFolder();
await askAboutMediaAccess();
global.NEW_RELEASE_AVAILABLE = false;
// ensure proper data in about panel when available
if ( app.setAboutPanelOptions ) {
app.setAboutPanelOptions({
applicationName: global.APP_NAME,
applicationVersion: global.APP_VERSION,
version: global.APP_VERSION_BASE,
credits: global.APP_CREDITS
});
}
let saversDir;
if ( process.env.SAVERS_DIR ) {
saversDir = process.env.SAVERS_DIR;
}
else if ( isDev ) {
saversDir = path.join(app.getAppPath(), "..", "..", "data", "savers");
log.info("hello from dev mode, 'node bin/download-screensavers' to grab screensavers");
}
else {
saversDir = path.join(process.resourcesPath, "savers");
}
const systemDir = getSystemDir();
let basePath;
// store our root path as a global variable so we can access it from screens
if ( process.env.BEFORE_DAWN_DIR !== undefined ) {
basePath = process.env.BEFORE_DAWN_DIR;
}
else {
basePath = app.getPath("userData");
}
log.info("use base path", basePath);
log.info("Loading prefs");
log.info(`baseDir: ${basePath}`);
log.info(`saversDir: ${saversDir}`);
log.info(`system savers: ${systemDir}/system-savers`);
prefs = new SaverPrefs(basePath, systemDir, saversDir);
savers = new SaverListManager({
prefs: prefs
});
await listScreens();
//
// setup some event handlers for when screen count changes, mostly
// to ensure that we wake up if the user plugs in or removes a
// monitor
//
["display-added", "display-removed"].forEach((type) => {
electronScreen.on(type, async () => {
log.info(type);
await listScreens();
windows.handleDisplayChange();
});
});
["suspend", "lock-screen"].forEach((type) => {
powerMonitor.on(type, (ev) => {
if ( stateManager.isTicking() ) {
log.info(`system ${type} event, stop screensavers`);
ev.preventDefault();
stateManager.stopTicking();
windows.closeRunningScreensavers();
}
});
});
if ( testMode !== true ) {
setInterval(() => {
if ( stateManager.isTicking() ) {
return;
}
const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;
const idleState = powerMonitor.getSystemIdleState(delayTime);
// don't restart state manager if we're paused
if ( ! stateManager.isTicking() && !stateManager.paused() && idleState === "active" ) {
log.info("looks like we are awake again lets go");
stateManager.reset();
stateManager.startTicking();
}
}, 10000);
}
powerMonitor.on("on-ac", () => {
log.info("system on-ac event, reset state manager");
stateManager.reset();
});
setupIPC();
stateManager = new StateManager();
stateManager.idleFn = powerMonitor.getSystemIdleTime;
updateStateManager();
let result = await setupIfNeeded();
await openPrefsWindowIfNeeded(result);
setupForTesting();
setupMenuAndTray();
setupReleaseCheck();
setupLaunchShortcut();
// don't show app in dock
dock.hideDockIfInactive(app);
// start the idle check
stateManager.startTicking();
};
/**
* toggle our 'ok to quit' variable and quit
*/
var quitApp = () => {
exitOnQuit = true;
app.quit();
};
/**
* run the screensaver, but only if there isn't an app in fullscreen mode right now
*/
var runScreenSaverIfNotFullscreen = function() {
log.info("runScreenSaverIfNotFullscreen");
if ( ! isFullscreen() ) {
log.info("I don't think we're in fullscreen mode");
runScreenSaver();
}
else {
log.info("looks like we are in fullscreen mode");
}
};
/**
* activate the screensaver, but only if we're plugged in, or if the user
* is fine with running on battery
*/
var runScreenSaverIfPowered = async function() {
log.info("runScreenSaverIfPowered");
if ( windows.screenSaverIsRunning() ) {
log.info("looks like we're already running");
return;
}
// check if we are on battery, and if we should be running in that case
if ( checkPowerState && prefs.disableOnBattery ) {
const isPowered = await power.charging();
if ( isPowered ) {
runScreenSaverIfNotFullscreen();
}
else {
log.info("I would run, but we're on battery :(");
stateManager.unrunnable();
}
}
else {
checkPowerState = true;
runScreenSaverIfNotFullscreen();
}
};
/**
* if the screensaver is running, blank the screen. otherwise,
* reset state machine
*/
var blankScreenIfNeeded = function() {
log.info("blankScreenIfNeeded");
if ( windows.screenSaverIsRunning() ) {
log.info("running, close windows");
stopScreenSaver(true);
screenLock.doSleep();
}
};
/**
* update the state manager with our
* timeout values, etc
*/
var updateStateManager = function() {
const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;
const blankOffset = process.platform === "win32" ? 0 : prefs.delay;
const blankTime = prefs.sleep > 0 ? (blankOffset + prefs.sleep) * 60 : Number.POSITIVE_INFINITY;
log.info(`updateStateManager idleTime: ${delayTime} blankTime: ${blankTime}`);
stateManager.setup({
idleTime: delayTime,
blankTime: blankTime,
onIdleTime: runScreenSaverIfPowered,
onBlankTime: blankScreenIfNeeded,
onReset: windows.closeRunningScreensavers,
logger: log.info
});
};
/**
* check for a new release of the app
*/
var checkForNewRelease = function() {
log.info("checkForNewRelease");
releaseChecker.checkLatestRelease();
};
/**
* setup a global shortcut to run a screensaver
*/
var setupLaunchShortcut = function() {
globalShortcut.unregisterAll();
if ( prefs.launchShortcut !== undefined && prefs.launchShortcut !== "" ) {
log.info(`register launch shortcut: ${prefs.launchShortcut}`);
try {
const ret = globalShortcut.register(prefs.launchShortcut, () => {
log.info("shortcut triggered!");
if ( handles.prefs.window && handles.prefs.window.isFocused() ) {
log.info("no shortcut when prefs active!");
return;
}
try {
// turn off idle checks for a couple seconds while loading savers
stateManager.ignoreReset(true);
setStateToRunning();
}
catch (e) {
log.info(e);
stateManager.ignoreReset(false);
}
finally {
setTimeout(function() {
stateManager.ignoreReset(false);
}, 2500);
}
});
if ( ! ret ) {
log.info("shortcut registration failed");
}
log.info(`registered? ${globalShortcut.isRegistered(prefs.launchShortcut)}`);
}
catch(e) {
log.info("shortcut registration threw an error?");
log.info(e);
}
}
};
/**
* return our state manager
* @returns {StateManager}
*/
let getStateManager = function() {
return stateManager;
};
/**
* return the app icon
* @returns {Tray}
*/
let getAppIcon = function() {
return appIcon;
};
/**
* return the tray menu
* @returns {Menu}
*/
let getTrayMenu = function() {
return trayMenu;
};
let updateTrayMenu = function() {
appIcon.setContextMenu(trayMenu);
}
/**
* if the user has updated one of their screensavers, we can let
* the prefs window know that it needs to reload
*/
let toggleSaversUpdated = (arg) => {
prefs.reload();
savers.reset();
if ( handles.prefs.window !== null ) {
handles.prefs.window.send("savers-updated", arg);
}
};
const windowMethods = {
editor: openEditor,
settings: openSettingsWindow,
prefs: openPrefsWindow,
about: openAboutWindow,
"add-new": addNewSaver
};
log.transports.file.level = "debug";
log.transports.file.maxSize = 1 * 1024 * 1024;
if (process.env.LOG_FILE) {
log.transports.file.resolvePathFn = () => process.env.LOG_FILE;
}
log.info(`Hello from version: ${global.APP_VERSION_BASE} running in ${isDev ? "development" : "production"}`);
if ( isDev ) {
app.name = global.APP_NAME;
log.info(`set app name to ${app.name}`);
if ( testMode !== true ) {
let userDataPath = path.join(app.getPath("appData"), app.name);
log.info(`set userData path to ${userDataPath}`);
app.setPath("userData", userDataPath);
}
}
/**
* make sure we're only running a single instance
*/
if ( testMode !== true ) {
app.on("second-instance", () => {
try {
if ( handles.prefs.window === null && handles.prefs.window !== undefined ) {
openPrefsWindow();
}
else {
if ( handles.prefs.window.isMinimized() ) {
handles.prefs.window.restore();
}
handles.prefs.window.focus();
}
}
catch(e) {
console.log(e);
}
});
}
// seems like we need to catch this event to keep OSX from exiting app after screensaver runs?
app.on("window-all-closed", function() {
log.info("window-all-closed");
});
app.on("before-quit", function() {
log.info("before-quit");
});
app.on("will-quit", function(e) {
log.info("will-quit");
if ( testMode !== true && isDev !== true && exitOnQuit !== true ) {
log.info(`don't quit! testMode: ${testMode} IS_DEV ${isDev} exitOnQuit ${exitOnQuit}`);
e.preventDefault();
}
else {
globalShortcut.unregisterAll();
}
});
app.once("quit", function() {
log.info("quit");
});
process.on("uncaughtException", function (ex) {
log.info(ex);
log.info(ex.stack);
});
log.info("readyto wait for bootApp");
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.whenReady().then(bootApp);
if ( testMode === true ) {
exports.getTrayMenuItems = function() {
return menusAndTrays.trayMenuTemplate();
};
}
export {
log,
setStateToRunning,
setStateToPaused,
resetState,
getAssetsDir,
getStateManager,
getAppIcon,
getTrayMenu,
updateTrayMenu,
openPrefsWindow,
openAboutWindow,
addNewSaver,
openEditor,
toggleSaversUpdated,
quitApp,
};
================================================
FILE: src/main/menus.js
================================================
"use strict";
import * as main from "./index.js";
import * as path from "path";
import {
nativeImage,
nativeTheme,
shell
} from "electron";
var openUrl = (url) => {
try {
shell.openExternal(url);
}
catch(e) {
main.log.info(e);
}
};
/**
* open the help section in a browser
*/
var openHelpUrl = () => { openUrl(global.HELP_URL); };
/**
* open the github issues url in a browser
*/
var openIssuesUrl = () => { openUrl(global.ISSUES_URL); };
/**
* open the website for the app
*/
var openHomepage = () => { openUrl("https://github.com/muffinista/before-dawn"); };
/**
* Build the menubar for the app
*
* @param {Application} a the main app instance
*/
export const buildMenuTemplate = function(a) {
var app = a;
var base = [
{
label: "File",
submenu: [
{
label: "Add New Screensaver",
accelerator: "CmdOrCtrl+N",
click: function() {
main.addNewSaver();
}
},
]
},
{
label: "Edit",
submenu: [
{
label: "Undo",
accelerator: "CmdOrCtrl+Z",
role: "undo"
},
{
label: "Redo",
accelerator: "Shift+CmdOrCtrl+Z",
role: "redo"
},
{
type: "separator"
},
{
label: "Cut",
accelerator: "CmdOrCtrl+X",
role: "cut"
},
{
label: "Copy",
accelerator: "CmdOrCtrl+C",
role: "copy"
},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
role: "paste"
},
{
label: "Select All",
accelerator: "CmdOrCtrl+A",
role: "selectall"
}
]
},
{
label: "View",
submenu: [
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
click: function(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload();
}
}
},
{
label: "Toggle Developer Tools",
accelerator: (function() {
if (process.platform == "darwin") {
return "Alt+Command+I";
}
else {
return "Ctrl+Shift+I";
}
})(),
click: function(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools();
}
}
}
]
},
{
label: "Window",
role: "window",
submenu: [
{
label: "Minimize",
accelerator: "CmdOrCtrl+M",
role: "minimize"
},
{
label: "Close",
accelerator: "CmdOrCtrl+W",
role: "close"
}
]
},
{
label: "Help",
role: "help",
submenu: [
{
label: "Learn More",
click: openHomepage
},
{
label: "Help",
click: openHelpUrl
}
]
}
];
if (process.platform == "darwin") {
var name = app.name;
base.unshift({
label: name,
submenu: [
{
label: "About " + name,
role: "about"
},
{
type: "separator"
},
{
label: "Services",
role: "services",
submenu: []
},
{
type: "separator"
},
{
label: "Hide " + name,
accelerator: "Command+H",
role: "hide"
},
{
label: "Hide Others",
accelerator: "Command+Alt+H",
role: "hideothers"
},
{
label: "Show All",
role: "unhide"
},
{
type: "separator"
},
{
label: "Quit",
accelerator: "Command+Q",
click: main.quitApp
}
]
});
}
return base;
};
/**
* build the tray menu template for the app
*/
export const trayMenuTemplate = function() {
return [
{
label: "Run Now",
click: function() {
setTimeout(main.setStateToRunning, 1000);
}
},
{
label: "Disable",
click: function() {
main.setStateToPaused();
updateTrayIcon();
main.getTrayMenu().items[1].visible = false;
main.getTrayMenu().items[2].visible = true;
main.updateTrayMenu();
}
},
{
label: "Enable",
click: function() {
main.resetState();
updateTrayIcon();
main.getTrayMenu().items[1].visible = true;
main.getTrayMenu().items[2].visible = false;
main.updateTrayMenu();
},
visible: false
},
{
label: "Update Available!",
click: function() {
shell.openExternal(global.PACKAGE_DOWNLOAD_URL);
},
visible: (global.NEW_RELEASE_AVAILABLE === true)
},
{
label: "Preferences",
click: () => {
main.openPrefsWindow();
}
},
{
label: "About " + global.APP_NAME,
click: () => {
main.openAboutWindow();
}
},
{
label: "Help",
click: () => {
openHelpUrl();
}
},
{
label: "Report a Bug",
click: () => {
openIssuesUrl();
}
},
{
label: "Quit",
click: () => {
main.quitApp();
}
}
];
};
/**
* get icons for the current platform
*/
export const getIcons = function() {
const useDarkIcon = !nativeTheme.shouldUseDarkColorsForSystemIntegratedUI;
const modifier = useDarkIcon ? '-dark' : '';
const icons = {
"win32" : {
active: path.join(main.getAssetsDir() , "icon.ico"),
paused: path.join(main.getAssetsDir(), "icon-paused.ico")
},
"default": {
active: path.join(main.getAssetsDir() , `iconTemplate${modifier}.png`),
paused: path.join(main.getAssetsDir() , `icon-pausedTemplate${modifier}.png`)
}
};
if ( icons[process.platform] ) {
return icons[process.platform];
}
return icons.default;
};
export const trayIconImage = function() {
var icons = getIcons();
let stateManager = main.getStateManager();
let iconPath;
if ( stateManager.currentState === stateManager.STATES.STATE_PAUSED ) {
iconPath = icons.paused;
}
else {
iconPath = icons.active;
}
main.log.info(`use icon ${iconPath}`);
return nativeImage.createFromPath(iconPath);
};
/**
* update tray icon to match our current state
*/
var updateTrayIcon = function() {
let appIcon = main.getAppIcon();
const iconImage = trayIconImage();
if ( !iconImage.isEmpty() ) {
appIcon.setImage(iconImage);
}
};
================================================
FILE: src/main/power.js
================================================
"use strict";
import { execFile } from "child_process";
export default class Power {
constructor(opts = {}) {
this.method = opts.method;
this.platform = opts.platform;
if ( this.platform === undefined ) {
this.platform = process.platform;
}
// https://stackoverflow.com/questions/651563/getting-the-last-element-of-a-split-string-array
this.commands = {
linux: {
cmd: "dbus-send",
opts: [
"--print-reply",
"--system",
"--dest=org.freedesktop.UPower",
"/org/freedesktop/UPower",
"org.freedesktop.DBus.Properties.Get",
"string:org.freedesktop.UPower",
"string:OnBattery"
]
},
};
this.default = true;
}
async rawData() {
if ( this.commands[this.platform] ) {
const cmd = this.commands[this.platform].cmd;
const opts = this.commands[this.platform].opts;
try {
return await this.query(cmd, opts);
}
catch {
return undefined;
}
}
return undefined;
}
async charging(raw = null) {
if ( this.method !== undefined) {
return !this.method();
}
if ( raw === null ) {
raw = await this.rawData();
}
if ( raw === undefined ) {
return this.default;
}
try {
// method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2
// variant boolean false
const result = raw.split("\n").find((line) => line.indexOf("variant") !== -1);
// OnBattery == false means we're plugged in
return result.indexOf("false") !== -1;
}
catch(e) {
console.log(e);
return this.default;
}
}
query(cmd, args) {
return new Promise((resolve) => {
execFile(cmd, args, (error, stdout, stderr) => {
if (error) {
console.warn(error);
}
resolve(stdout? stdout : stderr);
});
});
}
}
================================================
FILE: src/main/release_check.js
================================================
"use strict";
export default class ReleaseCheck {
constructor() {
this.onUpdateCallback = () => {};
this.onNoUpdateCallback = () => {};
this.logger = () => {};
}
setFeed(u) {
this.url = u;
}
setLogger(l) {
this.logger = l;
}
onUpdate(f) {
this.onUpdateCallback = f;
}
onNoUpdate(f) {
this.onNoUpdateCallback = f;
}
checkLatestRelease() {
this.logger(`check ${this.url} for new release`);
let _self = this;
fetch(this.url, {
timeout: 5000,
headers: {
"User-Agent": "Before Dawn"
}
}).then(function(response) {
if ( response.ok ) {
return response.json();
}
return undefined;
}).then(function(body) {
_self.logger(body);
if ( body !== undefined ) {
_self.onUpdateCallback(body);
}
else {
_self.onNoUpdateCallback();
}
}).catch(() => {
this.onNoUpdateCallback();
});
}
}
================================================
FILE: src/main/screen.js
================================================
"use strict";
import gotoSleep from "@muffinista/goto-sleep";
export const doLockScreen = gotoSleep.lockScreen;
export const doSleep = gotoSleep.gotoSleep;
================================================
FILE: src/main/state_manager.js
================================================
"use strict";
/**
* These are the possible states that the app can be in.
*/
const STATES = {
STATE_NONE: Symbol("none"), // initial state
STATE_IDLE: Symbol("idle"), // not running, waiting
STATE_LOADING: Symbol("loading"), // the liminal state when a screesaver is loaded but not 100%
STATE_RUNNING: Symbol("running"), // running a screensaver
STATE_BLANKED: Symbol("blanked"), // long idle, screen is blanked
STATE_PAUSED: Symbol("paused"), // screensaver is paused,
STATE_UNRUNNABLE: Symbol("unrunnable")
};
// check for updates every 5 seconds when idle
const IDLE_CHECK_RATE = 5000;
// check for updates every .25 second when active
const ACTIVE_CHECK_RATE = 250;
const IDLE_PADDING_CHECK = 1;
class StateManager {
constructor(fn) {
this.STATES = STATES;
this.currentState = STATES.STATE_NONE;
this._idleTime = () => {};
this._blankTime = () => {};
this._onIdleTime = () => {};
this._onBlankTime = () => {};
this._onReset = () => {};
this.logger = function() {};
this.lastTime = -1;
this.enteredStateTimestamp = -1;
this._ignoreReset = false;
this.keepTicking = true;
this._idleFn = fn;
this.rates = {
idle: IDLE_CHECK_RATE,
active: ACTIVE_CHECK_RATE
};
}
get currentTimeStamp() {
return process.hrtime()[0];
}
set idleFn(x) {
this._idleFn = x;
}
/**
* setup timing/callbacks
*/
setup(opts) {
if ( opts.logger !== undefined ) {
this.logger = opts.logger;
}
else {
this.logger = function() {};
}
if ( opts.idleTime && opts.onIdleTime ) {
this._idleTime = opts.idleTime;
this._onIdleTime = opts.onIdleTime;
}
if ( opts.blankTime && opts.onBlankTime ) {
this._blankTime = opts.blankTime;
this._onBlankTime = opts.onBlankTime;
}
if ( opts.onReset ) {
this._onReset = opts.onReset;
}
if ( opts.state ) {
this.switchState(opts.state);
}
else {
this.switchState(STATES.STATE_IDLE);
}
}
/**
* reset to idle and clear any timers
*/
reset() {
this.switchState(STATES.STATE_IDLE);
this.ignoreReset(false);
}
unrunnable() {
this.switchState(STATES.STATE_UNRUNNABLE);
}
/**
* pause the state machine
*/
pause() {
this.switchState(STATES.STATE_PAUSED);
}
paused() {
return this.currentState === STATES.STATE_PAUSED;
}
/**
* start running the state machine
*/
run() {
this.switchState(STATES.STATE_LOADING);
}
running() {
this.ignoreReset(false);
this.switchState(STATES.STATE_RUNNING);
}
/**
* handle calling the onIdleTime callback specified in setup
*/
onIdleTime() {
this._onIdleTime();
}
/**
* handle calling the onBlankTime callback specified in setup
*/
onBlankTime() {
this._onBlankTime();
}
onReset() {
this._onReset();
}
/**
* switch to a new state. if we're not already in that state, or if
* force == true, call onEnterState
*/
switchState(s, force) {
// we run onEnterState if the state has changed or if we need to
// force a reload. we also run it if the new state is idle, this
// should help with some weird issues where timers aren't being
// reset properly
const callEnterState = ( this.currentState !== s || s === STATES.STATE_IDLE || force === true);
this.currentState = s;
this.enteredStateTimestamp = this.currentTimeStamp;
if ( callEnterState ) {
this.onEnterState(s);
}
}
/**
* enter a new state. set any timers/etc needed
*/
onEnterState(s) {
switch (s) {
case STATES.STATE_IDLE:
this.onReset();
break;
case STATES.STATE_LOADING:
this.onIdleTime();
break;
case STATES.STATE_BLANKED:
this.onBlankTime();
break;
case STATES.STATE_PAUSED:
break;
}
}
getCurrentState() {
return this.currentState;
}
get currentStateString() {
return this.currentState.toString();
}
/**
* based on our current state, figure out the timestamp
* that we will enter the next state
*/
getNextTime() {
if ( this.currentState === STATES.STATE_RUNNING ) {
return this._blankTime;
}
return this._idleTime;
}
ignoreReset(val) {
this.logger(`set ignoreReset to ${val}`);
this._ignoreReset = val;
if ( this._ignoreReset === false ) {
this.lastTime = -1;
}
}
/**
* check idle time and determine if we should switch states
*/
tick(runAgain) {
if ( this.currentState !== STATES.STATE_NONE && this.currentState !== STATES.STATE_PAUSED ) {
const i = this._idleFn();
const nextTime = this.getNextTime();
const hadActivity = (i < this.lastTime ||
(this.currentState === STATES.STATE_RUNNING && i <= 10 && this.currentTimeStamp - i - IDLE_PADDING_CHECK > this.enteredStateTimestamp));
// this.logger(`${i} ${this.lastTime} -- ${this.currentStateString}`);
if ( this.currentState === STATES.STATE_PAUSED ) {
// do nothing
} else if ( hadActivity && this.currentState !== STATES.STATE_IDLE ) {
// we won't actually reset the state while a screensaver is
// loading, because sometimes we get zombie electron windows
// when we do that
if ( ! this._ignoreReset ) {
this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- ${this.currentStateString} -- reset`);
this.reset();
}
else {
this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- but ignoreReset is true`);
}
}
else if ( i >= nextTime && this.currentState !== STATES.STATE_BLANKED ) {
if ( this.currentState === STATES.STATE_IDLE) {
this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to loading`);
this.switchState(STATES.STATE_LOADING);
}
else if ( this.currentState === STATES.STATE_RUNNING) {
this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to blanked`);
this.switchState(STATES.STATE_BLANKED);
}
else {
// this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to ????`);
}
}
if ( this.currentState !== STATES.STATE_LOADING ) {
this.lastTime = i;
}
}
if ( runAgain !== false ) {
this.scheduleTick();
}
}
scheduleTick() {
if ( this.keepTicking ) {
let rate = this.rates.idle;
if ( this.currentState === STATES.STATE_RUNNING ) {
rate = this.rates.active;
}
setTimeout(() => {
this.tick(true);
}, rate);
}
}
setupLogging() {
this.logger("setupLogging");
// clearInterval(this.loggingInterval);
// every minute or so, output the current state
this.loggingInterval = setInterval(() => {
this.logger(`Current idle: ${this._idleFn()} Last idle: ${this.lastTime} -- ${this.currentStateString}`);
}, 60000);
}
isTicking() {
return this.keepTicking === true;
}
startTicking() {
this.logger("startTicking");
this.keepTicking = true;
this.setupLogging();
this.scheduleTick();
}
stopTicking() {
this.logger("stopTicking");
this.keepTicking = false;
clearInterval(this.loggingInterval);
}
}
export default StateManager;
================================================
FILE: src/main/system-savers/__template/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>My Awesome Screensaver</title>
<style>
body {
width: 100%;
background-color: black;
color: white;
}
</style>
<script>
// load any incoming URL parameters. you could just use the
// URLSearchParams object directly to manage these variables, but
// having them in a hash is a little easier sometimes.
var tmpParams = new URLSearchParams(document.location.search);
window.urlParams = {};
for(let k of tmpParams.keys() ) {
window.urlParams[k] = tmpParams.get(k);
}
</script>
</head>
<body>
<h1>Hello! I am a screensaver template!</h1>
<p>I am located in <code><script>document.write(decodeURIComponent(document.location.pathname));</script></code>.</p>
<p>Put the content of your screensaver here!</p>
<h2>Incoming Values</h2>
<p>(These parameters will be sent to your screensaver when it loads)</p>
<ul>
<script>
for(let k of Object.keys(window.urlParams) ) {
document.write("<li><code>window.urlParams[" + k + "]</code>: " + window.urlParams[k] + "</li>");
}
</script>
</ul>
<h2>Helpful Snippets</h2>
Here's some code to load the incoming screenshot, so you can apply effects/etc to it:
<pre><code>
<body>
<img id="screen" />
</body>
<script>
var img = document.getElementById("screen");
var url = unescape(decodeURIComponent(window.urlParams.screenshot));
img.src = url;
if ( typeof(window.urlParams.width) !== "undefined" ) {
img.width = window.urlParams.width;
img.height = window.urlParams.height;
}
</script>
</code></pre>
It might make sense to hide any margins on elements in your screensaver with some CSS like this:
<pre><code>
<style>
* {
padding: 0;
margin: 0;
}
</style>
</code></pre>
</body>
</html>
================================================
FILE: src/main/system-savers/__template/saver.json
================================================
{
"name": "My Awesome Screensaver",
"description": "a description of my terrific screensaver",
"aboutUrl": "http://mywebsite.com/about",
"author": "my name/etc",
"source": "index.html"
}
================================================
FILE: src/main/system-savers/blank/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Blank</title>
<style>
* {
padding: 0;
margin: 0;
background-color: black;
}
</style>
</head>
<body>
</body>
</html>
================================================
FILE: src/main/system-savers/blank/saver.json
================================================
{
"name": "Blank screen",
"description": "Blank the screen",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": []
}
================================================
FILE: src/main/system-savers/dimmer/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Dimmer</title>
<script>
// this little javascript snippet will parse any incoming URL
// parameters and place them in the window.urlParams object
window.urlParams = window.location.search.split(/[?&]/).slice(1).map(function(paramPair) {
return paramPair.split(/=(.+)?/).slice(0, 2);
}).reduce(function (obj, pairArray) {
obj[pairArray[0]] = pairArray[1];
return obj;
}, {});
</script>
<style>
* {
padding: 0;
margin: 0;
}
img {
filter: brightness(30%);
transition: 10s filter linear;
}
</style>
</head>
<body>
<img id="screen" />
</body>
<script>
var img = document.getElementById("screen");
var url = unescape(decodeURIComponent(window.urlParams.screenshot));
img.src = url;
if ( typeof(window.urlParams.width) !== "undefined" ) {
img.width = window.urlParams.width;
img.height = window.urlParams.height;
}
</script>
</html>
================================================
FILE: src/main/system-savers/dimmer/saver.json
================================================
{
"name": "Dimmer",
"description": "Dim the screen a bit",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": [
"screen"
]
}
================================================
FILE: src/main/system-savers/random/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Random Screensaver</title>
<style>
body {
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
</style>
<script>
// load any incoming URL parameters. you could just use the
// URLSearchParams object directly to manage these variables, but
// having them in a hash is a little easier sometimes.
var tmpParams = new URLSearchParams(document.location.search);
window.urlParams = {};
for(let k of tmpParams.keys() ) {
window.urlParams[k] = tmpParams.get(k);
}
</script>
</head>
<body>
<h1>Hello!</h1>
<p>This screensaver will pick a random screensaver and run it for you. Enjoy!</p>
</body>
</html>
================================================
FILE: src/main/system-savers/random/saver.json
================================================
{
"name": "Random",
"description": "Pick a random screensaver each time",
"author": "Colin Mitchell",
"preload": "random",
"source": "index.html"
}
================================================
FILE: src/main/windows.js
================================================
"use strict";
import * as main from "./index.js";
import { BrowserWindow } from "electron";
var getSaverWindows = function() {
return BrowserWindow.getAllWindows().filter((w) => {
return w.isSaver === true;
});
};
/**
* check if the screensaver is still running
*/
export const screenSaverIsRunning = function() {
return ( getSaverWindows().length > 0 );
};
/**
* check if the specified window exists and isn't destroyed
*/
var activeWindowHandle = function(w) {
return (typeof(w) !== "undefined" && ! w.isDestroyed());
};
/**
* when the display count changes, close any running windows
*/
export const handleDisplayChange = function() {
// main.log.info("display change, let's close running screensavers");
closeRunningScreensavers();
};
/**
* close any running screensavers
*/
export const closeRunningScreensavers = function() {
main.log.info("closeRunningScreensavers");
attemptToStopScreensavers();
// be really aggressive about closing lagging windows
setTimeout(forcefullyCloseScreensavers, 2500);
setTimeout(forcefullyCloseScreensavers, 5000);
};
/**
* iterate through our list of running screensaver windows and attempt
* to close them nicely
*/
var attemptToStopScreensavers = function() {
getSaverWindows().forEach((w) => {
if ( activeWindowHandle(w) ) {
w.close();
}
});
};
/**
* iterate through our list of running screensaver windows and close
* them forcefully if needed
*/
var forcefullyCloseScreensavers = function() {
getSaverWindows().forEach((w) => {
if ( activeWindowHandle(w) ) {
w.destroy();
}
});
};
/**
* forcefully close a screensaver window
*/
export const forceWindowClose = function(w) {
// 100% close/kill this window
if ( typeof(w) !== "undefined" ) {
try {
w.destroy();
}
catch (e) {
main.log.info(e);
}
}
};
/**
* Set full screen mode for the given window. Use OSX's
* fast/simple fullscreen mode if available.
* @param {BrowserWindow} w the window to apply
*/
export const setFullScreen = function(w) {
if ( process.platform !== "darwin" ) {
w.setFullScreen(true);
}
else {
w.setSimpleFullScreen(true);
}
w.show();
// w.moveTop();
};
================================================
FILE: src/renderer/AboutScreen.svelte
================================================
<div id="about">
<div>
<h1>Before Dawn</h1>
<h2>// screensaver fun //</h2>
{#await loadData() then}
<h3>{globals.APP_VERSION}</h3>
{/await}
<p>
An open-source screensaver project.<br>
<a
href="http://muffinista.github.io/before-dawn/"
onclick={open}
>
learn more
</a>
</p>
<p>
Having trouble?
<a
href="http://github.com/muffinista/before-dawn/issues"
onclick={open}>
please let us know!
</a>
</p>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x
gitextract_ufwarflo/ ├── .babelrc ├── .browserslistrc ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── eslint.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── assets/ │ ├── icon-paused.xcf │ ├── icon.xcf │ └── monitor-overlay.psd ├── bin/ │ ├── build-icon.js │ ├── build-on-ci.js │ ├── capture-screens.js │ ├── dev-runner.js │ ├── download-screensavers.js │ ├── generate-release.js │ └── get-release-name ├── build/ │ └── icon.icns ├── code_of_conduct.md ├── docs/ │ ├── .gitignore │ ├── .ruby-version │ ├── Gemfile │ ├── _config.yml │ ├── _includes/ │ │ ├── footer.html │ │ ├── head.html │ │ └── header.html │ ├── _layouts/ │ │ ├── default.html │ │ ├── page.html │ │ └── post.html │ ├── _posts/ │ │ └── 2016-12-02-welcome-to-jekyll.markdown │ ├── _sass/ │ │ ├── _base.scss │ │ ├── _layout.scss │ │ └── _syntax-highlighting.scss │ ├── about.md │ ├── contributing.md │ ├── creating.md │ ├── css/ │ │ └── main.scss │ ├── index.html │ ├── installing.md │ └── preferences.md ├── eslint.config.mjs ├── lefthook.yml ├── mise.toml ├── package.json ├── src/ │ ├── css/ │ │ └── styles.scss │ ├── index.ejs │ ├── lib/ │ │ ├── package.js │ │ ├── prefs-schema.json │ │ ├── prefs.js │ │ ├── saver-factory.js │ │ ├── saver-list.js │ │ └── saver.js │ ├── main/ │ │ ├── assets/ │ │ │ ├── global.css │ │ │ ├── grabber.html │ │ │ ├── grabber.mjs │ │ │ ├── preload.mjs │ │ │ ├── shim.html │ │ │ └── shim.js │ │ ├── autostarter.js │ │ ├── bootstrap.js │ │ ├── dock.js │ │ ├── index.dev.js │ │ ├── index.js │ │ ├── menus.js │ │ ├── power.js │ │ ├── release_check.js │ │ ├── screen.js │ │ ├── state_manager.js │ │ ├── system-savers/ │ │ │ ├── __template/ │ │ │ │ ├── index.html │ │ │ │ └── saver.json │ │ │ ├── blank/ │ │ │ │ ├── index.html │ │ │ │ └── saver.json │ │ │ ├── dimmer/ │ │ │ │ ├── index.html │ │ │ │ └── saver.json │ │ │ └── random/ │ │ │ ├── index.html │ │ │ └── saver.json │ │ └── windows.js │ └── renderer/ │ ├── AboutScreen.svelte │ ├── EditorScreen.svelte │ ├── NewScreensaverScreen.svelte │ ├── PrefsScreen.svelte │ ├── SettingsScreen.svelte │ ├── components/ │ │ ├── FolderChooser.svelte │ │ ├── Notarize.js │ │ ├── SaverForm.svelte │ │ ├── SaverList.svelte │ │ ├── SaverOptionInput.svelte │ │ ├── SaverOptions.svelte │ │ ├── SaverSummary.svelte │ │ ├── Spinner.svelte │ │ └── icons/ │ │ ├── BugIcon.svelte │ │ ├── FolderIcon.svelte │ │ ├── ReloadIcon.svelte │ │ └── SaveIcon.svelte │ └── main.js ├── test/ │ ├── fixtures/ │ │ ├── bad-config.json │ │ ├── config-2.json │ │ ├── config-with-options.json │ │ ├── config.json │ │ ├── default-repo.json │ │ ├── index.html │ │ ├── invalid.json │ │ ├── no-requirements.json │ │ ├── old-config.json │ │ ├── power/ │ │ │ ├── linux-charged.txt │ │ │ ├── linux-charging.txt │ │ │ └── linux-discharging.txt │ │ ├── release-no-update.json │ │ ├── release.json │ │ ├── releases/ │ │ │ └── updates.json │ │ ├── saver.json │ │ ├── saver2.json │ │ └── test-savers.json │ ├── helpers.js │ ├── lib/ │ │ ├── package.js │ │ ├── prefs.js │ │ ├── saver-factory.js │ │ ├── saver-list.js │ │ └── saver.js │ ├── main/ │ │ ├── power.js │ │ ├── release_check.js │ │ └── state_manager.js │ └── ui/ │ ├── about.js │ ├── bootstrap.js │ ├── editor.js │ ├── new.js │ ├── prefs.js │ ├── settings.js │ └── tray.js ├── tools/ │ ├── build-packages.sh │ └── update-build-version.js ├── webpack.config.js ├── webpack.main.config.js └── webpack.renderer.config.js
SYMBOL INDEX (134 symbols across 20 files)
FILE: bin/build-icon.js
function main (line 12) | async function main() {
FILE: bin/capture-screens.js
constant SCREENSAVER (line 14) | const SCREENSAVER = "Screen Glitcher";
function main (line 22) | async function main() {
FILE: bin/dev-runner.js
function startRenderer (line 43) | function startRenderer () {
function startMain (line 73) | function startMain () {
function startElectron (line 107) | function startElectron () {
function init (line 122) | function init () {
FILE: bin/download-screensavers.js
function main (line 34) | async function main() {
FILE: bin/generate-release.js
function main (line 33) | async function main() {
FILE: src/lib/package.js
class Package (line 22) | class Package {
method constructor (line 23) | constructor(_attrs) {
method attrs (line 48) | attrs() {
method getReleaseInfo (line 56) | async getReleaseInfo() {
method hasUpdate (line 82) | async hasUpdate() {
method checkLatestRelease (line 87) | async checkLatestRelease(force) {
method downloadRelease (line 98) | async downloadRelease() {
method downloadFile (line 119) | async downloadFile(url, dest) {
method zipToSavers (line 133) | zipToSavers(tempName, dest) {
FILE: src/lib/prefs.js
constant DEFAULTS (line 6) | const DEFAULTS = {
class SaverPrefs (line 61) | class SaverPrefs {
method constructor (line 62) | constructor(baseConfigDir, rootDir=undefined, saversDir=undefined) {
method configFile (line 92) | get configFile() {
method data (line 96) | get data() {
method defaults (line 105) | get defaults() {
method reload (line 113) | reload() {
method reset (line 125) | reset() {
method needSetup (line 130) | get needSetup() {
method defaultSaversDir (line 136) | get defaultSaversDir() {
method sources (line 143) | get sources() {
method systemSource (line 158) | get systemSource() {
method systemSource (line 162) | set systemSource(val) {
method getOptions (line 170) | getOptions(name) {
method get (line 189) | get() {
method set (line 198) | set(newval) {
FILE: src/lib/saver-factory.js
class SaverFactory (line 6) | class SaverFactory {
method constructor (line 7) | constructor(prefs, logger) {
method create (line 20) | create(src, destDir, opts) {
FILE: src/lib/saver-list.js
constant CONFIG_FILE_NAME (line 9) | const CONFIG_FILE_NAME = "config.json";
class SaverListManager (line 22) | class SaverListManager {
method constructor (line 23) | constructor(opts, logger) {
method defaultSaversDir (line 44) | get defaultSaversDir() {
method setup (line 48) | async setup() {
method reload (line 80) | reload(load_savers) {
method reset (line 85) | reset() {
method normalizePath (line 89) | normalizePath(p) {
method list (line 97) | async list(force) {
method random (line 164) | random() {
method confirmExists (line 173) | async confirmExists(key) {
method getByKey (line 181) | getByKey(key) {
method loadFromFile (line 193) | loadFromFile(src, settings) {
method loadFromData (line 230) | loadFromData(contents, stub, settings) {
method delete (line 285) | async delete(s) {
FILE: src/lib/saver.js
constant DEFAULT_REQUIREMENTS (line 10) | const DEFAULT_REQUIREMENTS = ["screen"];
class Saver (line 15) | class Saver {
method constructor (line 16) | constructor(_attrs) {
method urlWithParams (line 94) | urlWithParams(opts={}) {
method toHash (line 113) | toHash() {
method toJSON (line 117) | toJSON(attrs) {
method write (line 137) | write(attrs, configDest) {
FILE: src/main/autostarter.js
function toggle (line 6) | function toggle(appName, value) {
FILE: src/main/bootstrap.js
function bootstrapApp (line 3) | async function bootstrapApp() {
FILE: src/main/index.js
constant RELEASE_CHECK_INTERVAL (line 179) | const RELEASE_CHECK_INTERVAL = 1000 * 60 * 60 * 12;
FILE: src/main/power.js
class Power (line 6) | class Power {
method constructor (line 7) | constructor(opts = {}) {
method rawData (line 32) | async rawData() {
method charging (line 48) | async charging(raw = null) {
method query (line 75) | query(cmd, args) {
FILE: src/main/release_check.js
class ReleaseCheck (line 2) | class ReleaseCheck {
method constructor (line 3) | constructor() {
method setFeed (line 9) | setFeed(u) {
method setLogger (line 12) | setLogger(l) {
method onUpdate (line 16) | onUpdate(f) {
method onNoUpdate (line 19) | onNoUpdate(f) {
method checkLatestRelease (line 23) | checkLatestRelease() {
FILE: src/main/state_manager.js
constant STATES (line 6) | const STATES = {
constant IDLE_CHECK_RATE (line 17) | const IDLE_CHECK_RATE = 5000;
constant ACTIVE_CHECK_RATE (line 20) | const ACTIVE_CHECK_RATE = 250;
constant IDLE_PADDING_CHECK (line 22) | const IDLE_PADDING_CHECK = 1;
class StateManager (line 24) | class StateManager {
method constructor (line 25) | constructor(fn) {
method currentTimeStamp (line 52) | get currentTimeStamp() {
method idleFn (line 56) | set idleFn(x) {
method setup (line 63) | setup(opts) {
method reset (line 97) | reset() {
method unrunnable (line 102) | unrunnable() {
method pause (line 109) | pause() {
method paused (line 113) | paused() {
method run (line 120) | run() {
method running (line 124) | running() {
method onIdleTime (line 132) | onIdleTime() {
method onBlankTime (line 139) | onBlankTime() {
method onReset (line 143) | onReset() {
method switchState (line 151) | switchState(s, force) {
method onEnterState (line 170) | onEnterState(s) {
method getCurrentState (line 186) | getCurrentState() {
method currentStateString (line 190) | get currentStateString() {
method getNextTime (line 198) | getNextTime() {
method ignoreReset (line 205) | ignoreReset(val) {
method tick (line 216) | tick(runAgain) {
method scheduleTick (line 263) | scheduleTick() {
method setupLogging (line 276) | setupLogging() {
method isTicking (line 286) | isTicking() {
method startTicking (line 290) | startTicking() {
method stopTicking (line 296) | stopTicking() {
FILE: src/renderer/components/Notarize.js
constant NOTARIZE_DEFAULTS (line 2) | const NOTARIZE_DEFAULTS = {
class Notarize (line 11) | class Notarize {
method constructor (line 12) | constructor(options={}) {
method show (line 17) | show(contents) {
method toDom (line 30) | toDom(html) {
method handleTransitionIn (line 41) | handleTransitionIn(ev) {
method handleTransitionOut (line 51) | handleTransitionOut(ev) {
FILE: test/helpers.js
function specifyConfig (line 47) | function specifyConfig(dest, name) {
function setupConfig (line 54) | function setupConfig(workingDir, name="config", attrs={}) {
function setConfigValue (line 67) | function setConfigValue(workingDir, name, value) {
function addSaver (line 75) | function addSaver(dest, name, source) {
function prefsToJSON (line 100) | function prefsToJSON(tmpdir) {
function getTempDir (line 114) | function getTempDir() {
function savedConfig (line 122) | function savedConfig(p) {
function setupFullConfig (line 129) | function setupFullConfig(workingDir) {
function addLocalSource (line 138) | function addLocalSource(workingDir, saversDir) {
function removeLocalSource (line 145) | function removeLocalSource(workingDir) {
function application (line 160) | async function application(workingDir, quietMode=false, logFile=undefine...
function dumpOutput (line 199) | async function dumpOutput(app) {
function waitFor (line 216) | async function waitFor(app, windowName) {
function stopApp (line 228) | async function stopApp(app) {
function getWindowLookup (line 246) | async function getWindowLookup(app) {
function getWindowByTitle (line 273) | async function getWindowByTitle(app, title) {
function waitForText (line 288) | async function waitForText(window, lookup, text, doAssert) {
function sleep (line 300) | function sleep (ms) {
function waitForWindow (line 312) | async function waitForWindow(app, title, skipAssert) {
function callIpc (line 338) | async function callIpc(app, method, opts={}) {
function setupTest (line 348) | function setupTest(test) {
FILE: webpack.main.config.js
constant COMMIT_SHA (line 27) | const COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;
FILE: webpack.renderer.config.js
constant COMMIT_SHA (line 25) | const COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;
Condensed preview — 139 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (338K chars).
[
{
"path": ".babelrc",
"chars": 39,
"preview": "{\n \"presets\": [\"@babel/preset-env\"]\n}\n"
},
{
"path": ".browserslistrc",
"chars": 24,
"preview": "last 1 electron version\n"
},
{
"path": ".dockerignore",
"chars": 118,
"preview": "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",
"chars": 1272,
"preview": "name: Run tests\non: push\njobs:\n build:\n runs-on: ${{ matrix.os }}\n strategy:\n matrix:\n os: [ubuntu-la"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 1277,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/eslint.yml",
"chars": 967,
"preview": "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."
},
{
"path": ".github/workflows/release.yml",
"chars": 1251,
"preview": "name: Build release\non:\n push:\n branches:\n - main\njobs:\n\n build:\n runs-on: ${{ matrix.os }}\n strategy:\n "
},
{
"path": ".gitignore",
"chars": 843,
"preview": ".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 generat"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "22.21.1\n"
},
{
"path": "CHANGELOG.md",
"chars": 10141,
"preview": "# 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 t"
},
{
"path": "LICENSE.txt",
"chars": 1099,
"preview": "The MIT License\n\nCopyright (c) 2016 Colin Mitchell http://muffinlabs.com/\n\nPermission is hereby granted, free of charge,"
},
{
"path": "README.md",
"chars": 5252,
"preview": "# Before Dawn\n\nBefore Dawn is a an open-source, cross-platform screensaver application using\nweb-based technologies. You"
},
{
"path": "bin/build-icon.js",
"chars": 1230,
"preview": "#!/usr/bin/env node\n\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport * as tmp from \"tmp\";\nimport * as fs fr"
},
{
"path": "bin/build-on-ci.js",
"chars": 1097,
"preview": "#!/usr/bin/env node\n\n/* eslint-disable no-console */\n\nrequire(\"dotenv\").config();\n\nconst apiUrl = \"https://ci.appveyor.c"
},
{
"path": "bin/capture-screens.js",
"chars": 2208,
"preview": "#!/usr/bin/env node\n\n\"use strict\";\n\nconst path = require(\"path\");\n\nconst { _electron: electron } = require(\"playwright\")"
},
{
"path": "bin/dev-runner.js",
"chars": 2922,
"preview": "\"use strict\";\n\nimport electron from \"electron\";\nimport * as path from \"path\";\nimport { spawn } from \"child_process\";\nimp"
},
{
"path": "bin/download-screensavers.js",
"chars": 1380,
"preview": "#!/usr/bin/env node\n\n/* eslint-disable no-console */\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport * as f"
},
{
"path": "bin/generate-release.js",
"chars": 2014,
"preview": "#!/usr/bin/env node\n\nimport \"dotenv/config\";\nimport * as path from \"path\";\nimport { Octokit } from \"octokit\";\nimport { r"
},
{
"path": "bin/get-release-name",
"chars": 171,
"preview": "#!/usr/bin/env node\n\nconst path = require(\"path\");\n\nvar pjson = require(path.join(__dirname, \"..\", \"package.json\"));\ncon"
},
{
"path": "code_of_conduct.md",
"chars": 3228,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "docs/.gitignore",
"chars": 18,
"preview": "_site\n.sass-cache\n"
},
{
"path": "docs/.ruby-version",
"chars": 4,
"preview": "3.3\n"
},
{
"path": "docs/Gemfile",
"chars": 73,
"preview": "source 'https://rubygems.org'\ngem 'github-pages', group: :jekyll_plugins\n"
},
{
"path": "docs/_config.yml",
"chars": 491,
"preview": "# Site settings\ntitle: Before Dawn -- Screensavers for the Modern Era\nemail: colin at muffinlabs.com\ndescription: > # th"
},
{
"path": "docs/_includes/footer.html",
"chars": 3003,
"preview": "<footer class=\"site-footer\">\n\n <div class=\"wrapper\">\n <div class=\"footer-col-wrapper\">\n <div class=\"footer-col "
},
{
"path": "docs/_includes/head.html",
"chars": 694,
"preview": "<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width initial-scale=1\" />\n <meta ht"
},
{
"path": "docs/_includes/header.html",
"chars": 1101,
"preview": "<header class=\"site-header\">\n\n <div class=\"wrapper\">\n\n <a class=\"site-title\" href=\"./\">{{ site.title }}</a>\n\n <na"
},
{
"path": "docs/_layouts/default.html",
"chars": 245,
"preview": "<!DOCTYPE html>\n<html>\n {% include head.html %}\n <body>\n\n {% include header.html %}\n\n <div class=\"page-content\">"
},
{
"path": "docs/_layouts/page.html",
"chars": 209,
"preview": "---\nlayout: default\n---\n<div class=\"post\">\n\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{ page.title }}</"
},
{
"path": "docs/_layouts/post.html",
"chars": 373,
"preview": "---\nlayout: default\n---\n<div class=\"post\">\n\n <header class=\"post-header\">\n <h1 class=\"post-title\">{{ page.title }}</"
},
{
"path": "docs/_posts/2016-12-02-welcome-to-jekyll.markdown",
"chars": 1223,
"preview": "---\nlayout: post\ntitle: \"Welcome to Jekyll!\"\ndate: 2016-12-02 13:11:20\ncategories: jekyll update\n---\nYou’ll find this"
},
{
"path": "docs/_sass/_base.scss",
"chars": 2698,
"preview": "/**\n * Reset some basic elements\n */\nbody, h1, h2, h3, h4, h5, h6,\np, blockquote, pre, hr,\ndl, dd, ol, ul, figure {\n "
},
{
"path": "docs/_sass/_layout.scss",
"chars": 4153,
"preview": "/**\n * Site header\n */\n.site-header {\n border-top: 5px solid $grey-color-dark;\n border-bottom: 1px solid $grey-col"
},
{
"path": "docs/_sass/_syntax-highlighting.scss",
"chars": 3297,
"preview": "/**\n * Syntax highlighting styles\n */\n.highlight {\n background: #fff;\n @extend %vertical-rhythm;\n\n .c { col"
},
{
"path": "docs/about.md",
"chars": 955,
"preview": "---\nlayout: page\ntitle: About\npermalink: /about.html\n---\n\n <p>Before Dawn is a an open-source, cross-platform screensav"
},
{
"path": "docs/contributing.md",
"chars": 844,
"preview": "---\nlayout: page\ntitle: Contributing\npermalink: /contributing.html\n---\n\nContributions and suggestions are eagerly accept"
},
{
"path": "docs/creating.md",
"chars": 2592,
"preview": "---\nlayout: page\ntitle: Adding Your Own Screensaver\npermalink: /creating.html\n---\n\nA Before Dawn screensaver is an HTML "
},
{
"path": "docs/css/main.scss",
"chars": 2174,
"preview": "---\n# Only the main Sass file needs front matter (the dashes are enough)\n---\n@charset \"utf-8\";\n\n// Our variables\n$base-f"
},
{
"path": "docs/index.html",
"chars": 1223,
"preview": "---\nlayout: page\ntitle: Welcome!\n---\n\n<div class=\"home\">\n <div class=\"main\" >\n <p>Welcome to the help website for Be"
},
{
"path": "docs/installing.md",
"chars": 707,
"preview": "---\nlayout: page\ntitle: Installing Before Dawn\npermalink: /installing.html\n---\n\nYou can find installers for Before Dawn "
},
{
"path": "docs/preferences.md",
"chars": 1943,
"preview": "---\nlayout: page\ntitle: Preferences\npermalink: /preferences.html\n---\n\nBefore Dawn has a Preferences window where you can"
},
{
"path": "eslint.config.mjs",
"chars": 1510,
"preview": "import mocha from \"eslint-plugin-mocha\";\nimport globals from \"globals\";\nimport js from \"@eslint/js\";\nimport svelte from "
},
{
"path": "lefthook.yml",
"chars": 429,
"preview": "# EXAMPLE USAGE\n# Refer for explanation to following link:\n# https://github.com/Arkweid/lefthook/blob/master/docs/full_g"
},
{
"path": "mise.toml",
"chars": 25,
"preview": "[tools]\nnode = \"22.21.1\"\n"
},
{
"path": "package.json",
"chars": 5322,
"preview": "{\n \"name\": \"before-dawn\",\n \"productName\": \"Before Dawn\",\n \"version\": \"0.38.0\",\n \"description\": \"A desktop screensave"
},
{
"path": "src/css/styles.scss",
"chars": 6570,
"preview": ":root {\n --preview-wrapper-width: 500px;\n --preview-wrapper-height: 320px;\n --preview-width: 500px;\n --preview-heigh"
},
{
"path": "src/index.ejs",
"chars": 292,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title><%= htmlWebpackPlugin.options.title %></"
},
{
"path": "src/lib/package.js",
"chars": 6045,
"preview": "\"use strict\";\n\nimport fs from 'fs-extra';\nimport path from \"path\";\nimport temp from \"temp\";\nimport os from \"os\";\nimport "
},
{
"path": "src/lib/prefs-schema.json",
"chars": 905,
"preview": "{\n \"saver\": {\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"sourceRepo\": {\n \"type\": \"string\",\n \"default\": \"muff"
},
{
"path": "src/lib/prefs.js",
"chars": 4165,
"preview": "\"use strict\";\n\nimport * as path from \"path\";\nimport Conf from \"conf\";\n\nconst DEFAULTS = {\n \"saver\": {\n \"type\": \"stri"
},
{
"path": "src/lib/saver-factory.js",
"chars": 1527,
"preview": "\"use strict\";\n\nimport * as path from \"path\";\nimport fs from 'fs-extra'\n\nexport default class SaverFactory {\n constructo"
},
{
"path": "src/lib/saver-list.js",
"chars": 7655,
"preview": "\"use strict\";\n\nimport fs from 'fs-extra';\nimport path from \"path\";\nimport { mkdirp } from \"mkdirp\";\nimport { rimraf } fr"
},
{
"path": "src/lib/saver.js",
"chars": 4260,
"preview": "/**\n * simple class for a screen saver\n */\n\n\n// we will generate a list of requirements that screensavers need\n// to wor"
},
{
"path": "src/main/assets/global.css",
"chars": 135,
"preview": "body {margin:0; padding:0; overflow: hidden}\n*, *:hover { cursor: none !important; }\ncanvas {display:block;}\ncanvas:focu"
},
{
"path": "src/main/assets/grabber.html",
"chars": 164,
"preview": "<!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>"
},
{
"path": "src/main/assets/grabber.mjs",
"chars": 3309,
"preview": "/**\n * This is the preload script for the screen grabber\n * \n */\n\n\nconst { contextBridge, ipcRenderer } = require(\"elect"
},
{
"path": "src/main/assets/preload.mjs",
"chars": 2413,
"preview": "const { contextBridge, ipcRenderer } = require(\"electron\");\n\nconst api = {\n platform: () => process.platform,\n getDisp"
},
{
"path": "src/main/assets/shim.html",
"chars": 1724,
"preview": "<!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 "
},
{
"path": "src/main/assets/shim.js",
"chars": 421,
"preview": "const { contextBridge, ipcRenderer } = require(\"electron\");\n\nconst shimApi = {\n send: (cmd, opts, args={}) => ipcRender"
},
{
"path": "src/main/autostarter.js",
"chars": 940,
"preview": "\"use strict\";\n\nimport * as main from \"./index.js\";\nimport AutoLaunch from \"auto-launch\";\n\nexport function toggle(appName"
},
{
"path": "src/main/bootstrap.js",
"chars": 1276,
"preview": "import { readFile } from 'fs/promises';\n\nexport default async function bootstrapApp() {\n const packageJSON = JSON.parse"
},
{
"path": "src/main/dock.js",
"chars": 609,
"preview": "\"use strict\";\n\nimport {app, BrowserWindow} from \"electron\";\n\n/**\n * if we're using the dock, and all our windows are clo"
},
{
"path": "src/main/index.dev.js",
"chars": 315,
"preview": "/**\n * This file is used specifically and only for development. There shouldn't be\n * any need to modify this file, but "
},
{
"path": "src/main/index.js",
"chars": 50424,
"preview": "\"use strict\";\n\n// process.traceDeprecation = true;\n// process.traceProcessWarnings = true;\n\n\n/***\n\n Welcome to....\n\n "
},
{
"path": "src/main/menus.js",
"chars": 6698,
"preview": "\"use strict\";\n\nimport * as main from \"./index.js\";\n\nimport * as path from \"path\";\nimport { \n nativeImage, \n nativeThem"
},
{
"path": "src/main/power.js",
"chars": 1975,
"preview": "\n\"use strict\";\n\nimport { execFile } from \"child_process\";\n\nexport default class Power {\n constructor(opts = {}) {\n t"
},
{
"path": "src/main/release_check.js",
"chars": 961,
"preview": "\"use strict\";\nexport default class ReleaseCheck {\n constructor() {\n this.onUpdateCallback = () => {};\n this.onNoU"
},
{
"path": "src/main/screen.js",
"chars": 158,
"preview": "\"use strict\";\n\nimport gotoSleep from \"@muffinista/goto-sleep\";\n\nexport const doLockScreen = gotoSleep.lockScreen;\nexport"
},
{
"path": "src/main/state_manager.js",
"chars": 7401,
"preview": "\"use strict\";\n\n/**\n * These are the possible states that the app can be in.\n */\nconst STATES = {\n STATE_NONE: Symbol(\"n"
},
{
"path": "src/main/system-savers/__template/index.html",
"chars": 2029,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>My Awesome Screensaver</title>\n <style>\n body {\n width: 100%;\n "
},
{
"path": "src/main/system-savers/__template/saver.json",
"chars": 197,
"preview": "{\n \"name\": \"My Awesome Screensaver\",\n \"description\": \"a description of my terrific screensaver\",\n \"aboutUrl\": \"http:/"
},
{
"path": "src/main/system-savers/blank/index.html",
"chars": 203,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Blank</title>\n <style>\n * {\n padding: 0;\n margin: 0;\n "
},
{
"path": "src/main/system-savers/blank/saver.json",
"chars": 144,
"preview": "{\n \"name\": \"Blank screen\",\n \"description\": \"Blank the screen\",\n \"author\": \"Colin Mitchell\",\n \"source\": \"index.html\","
},
{
"path": "src/main/system-savers/dimmer/index.html",
"chars": 1032,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Dimmer</title>\n <script>\n // this little javascript snippet will parse"
},
{
"path": "src/main/system-savers/dimmer/saver.json",
"chars": 158,
"preview": "{\n \"name\": \"Dimmer\",\n \"description\": \"Dim the screen a bit\",\n \"author\": \"Colin Mitchell\",\n \"source\": \"index.html\",\n "
},
{
"path": "src/main/system-savers/random/index.html",
"chars": 805,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Random Screensaver</title>\n <style>\n body {\n width: 100%;\n "
},
{
"path": "src/main/system-savers/random/saver.json",
"chars": 158,
"preview": "{\n \"name\": \"Random\",\n \"description\": \"Pick a random screensaver each time\",\n \"author\": \"Colin Mitchell\",\n \"preload\":"
},
{
"path": "src/main/windows.js",
"chars": 2222,
"preview": "\"use strict\";\n\nimport * as main from \"./index.js\";\nimport { BrowserWindow } from \"electron\";\n\nvar getSaverWindows = func"
},
{
"path": "src/renderer/AboutScreen.svelte",
"chars": 3763,
"preview": "<div id=\"about\">\n <div>\n <h1>Before Dawn</h1>\n <h2>// screensaver fun //</h2>\n {#await loadData() then}\n <h"
},
{
"path": "src/renderer/EditorScreen.svelte",
"chars": 8355,
"preview": "<!-- #editor -->\n<script>\n import { onMount, onDestroy } from \"svelte\";\n\n import Notarize from \"@/components/Notarize\""
},
{
"path": "src/renderer/NewScreensaverScreen.svelte",
"chars": 3009,
"preview": "<div id=\"new\">\n <div class=\"content\">\n <div>\n <h1>New Screensaver</h1>\n {#if canAdd}\n <p>\n "
},
{
"path": "src/renderer/PrefsScreen.svelte",
"chars": 8091,
"preview": "<div id=\"prefs\">\n <div class=\"saver-detail\">\n <iframe\n title=\"preview\"\n src=\"{previewUrl}\"\n scrolling"
},
{
"path": "src/renderer/SettingsScreen.svelte",
"chars": 6399,
"preview": "<div id=\"settings\">\n <div id=\"prefs-form\">\n <h1>Settings</h1>\n <form class=\"grid\">\n <div class=\"options\">\n "
},
{
"path": "src/renderer/components/FolderChooser.svelte",
"chars": 843,
"preview": "<div class=\"input-group\">\n <input\n type=\"text\"\n readonly=\"readonly\"\n bind:value=\"{source}\"\n >\n <button\n t"
},
{
"path": "src/renderer/components/Notarize.js",
"chars": 1662,
"preview": "\nconst NOTARIZE_DEFAULTS = {\n wrapperClass: \"notarize-wrapper\",\n interiorClass: \"notarize\",\n timeout: 150000,\n trans"
},
{
"path": "src/renderer/components/SaverForm.svelte",
"chars": 1451,
"preview": "<script>\n let { saver = $bindable() } = $props();\n</script>\n\n<div id=\"saver-form\">\n <form>\n <div class=\"form-group\""
},
{
"path": "src/renderer/components/SaverList.svelte",
"chars": 1247,
"preview": "<div class=\"saver-list-wrapper\">\n <h1>Screensavers</h1>\n <ul class=\"saver-list list-group-flush\">\n {#each savers as"
},
{
"path": "src/renderer/components/SaverOptionInput.svelte",
"chars": 2707,
"preview": "<form>\n <div class=\"form-group\">\n <label for=\"name\">Name</label>\n <div>\n <input\n bind:value=\"{option."
},
{
"path": "src/renderer/components/SaverOptions.svelte",
"chars": 2379,
"preview": "<div id=\"wrapper\">\n <ul>\n {#each saver.options as option, index (option.name)}\n <li key={index}>\n <div class"
},
{
"path": "src/renderer/components/SaverSummary.svelte",
"chars": 1251,
"preview": "<div class=\"saver-description\">\n {#if saver}\n <h1>\n {saver.name} \n {#if saver.aboutUrl && saver.aboutUrl !"
},
{
"path": "src/renderer/components/Spinner.svelte",
"chars": 524,
"preview": "<div class=\"wrapper\">\n <div class=\"lds-dual-ring\"></div>\n</div>\n\n<style>\n.lds-dual-ring {\n display: inline-block;\n wi"
},
{
"path": "src/renderer/components/icons/BugIcon.svelte",
"chars": 1575,
"preview": "<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"
},
{
"path": "src/renderer/components/icons/FolderIcon.svelte",
"chars": 506,
"preview": "<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>"
},
{
"path": "src/renderer/components/icons/ReloadIcon.svelte",
"chars": 720,
"preview": "<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="
},
{
"path": "src/renderer/components/icons/SaveIcon.svelte",
"chars": 403,
"preview": "<svg\nid=\"Save\"\nversion=\"1.1\"\nxmlns=\"http://www.w3.org/2000/svg\"\nxmlns:xlink=\"http://www.w3.org/1999/xlink\"\nx=\"0px\"\ny=\"0p"
},
{
"path": "src/renderer/main.js",
"chars": 939,
"preview": "import \"~/css/styles.scss\";\n\nimport { mount } from 'svelte';\n\nimport PrefsScreen from \"./PrefsScreen.svelte\";\nimport Set"
},
{
"path": "test/fixtures/bad-config.json",
"chars": 107,
"preview": "{\n \"source\": {\n \"repo\": \"\"\n },\n \"saver\": \"before-dawn-screensavers/emoji/index.html\",\n \"options\": {\n"
},
{
"path": "test/fixtures/config-2.json",
"chars": 310,
"preview": "{\n \"source\": {\n \"repo\": \"\"\n },\n \"saver\": \"before-dawn-screensavers/blur/saver.json\",\n \"options\": {\n \"/Users/co"
},
{
"path": "test/fixtures/config-with-options.json",
"chars": 493,
"preview": "{\n \"source\": {\n \"repo\": \"\"\n },\n \"saver\": \"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json\",\n \"opt"
},
{
"path": "test/fixtures/config.json",
"chars": 364,
"preview": "{\n \"source\": {\n \"repo\": \"\"\n },\n \"saver\": \"before-dawn-screensavers/emoji/saver.json\",\n \"options\": {\n \"/Users/c"
},
{
"path": "test/fixtures/default-repo.json",
"chars": 364,
"preview": "{\n \"sourceRepo\": \"mocha/screensavers\",\n \"sourceUpdatedAt\": \"2018-01-07T15:59:04.499Z\",\n \"saver\": \"before-dawn-screens"
},
{
"path": "test/fixtures/index.html",
"chars": 100,
"preview": "<html>\n <head>\n <title>screensaver</title>\n </head>\n <body>I AM A SCREENSAVER!</body>\n</html>\n"
},
{
"path": "test/fixtures/invalid.json",
"chars": 533,
"preview": "{\n \"description\": \"A Screensaver\",\n \"aboutUrl\": \"\",\n \"author\": \"Colin Mitchell\",\n \"source\": \"index.html\",\n \"require"
},
{
"path": "test/fixtures/no-requirements.json",
"chars": 540,
"preview": "{\n \"name\": \"Screensaver One\",\n \"description\": \"A Screensaver\",\n \"aboutUrl\": \"\",\n \"author\": \"Colin Mitchell\",\n \"sour"
},
{
"path": "test/fixtures/old-config.json",
"chars": 387,
"preview": "{\n \"source\": {\n \"repo\": \"muffinista/before-dawn-screensavers\"\n },\n \"sourceCheckTimestamp\": 2512407042366,\n \"saver"
},
{
"path": "test/fixtures/power/linux-charged.txt",
"chars": 128,
"preview": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n variant boole"
},
{
"path": "test/fixtures/power/linux-charging.txt",
"chars": 128,
"preview": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n variant boole"
},
{
"path": "test/fixtures/power/linux-discharging.txt",
"chars": 127,
"preview": "method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2\n variant boole"
},
{
"path": "test/fixtures/release-no-update.json",
"chars": 1927,
"preview": "{\n \"url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664\",\n \"assets_url\": \"https:/"
},
{
"path": "test/fixtures/release.json",
"chars": 1998,
"preview": "{\n \"url\": \"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664\",\n \"assets_url\": \"htt"
},
{
"path": "test/fixtures/releases/updates.json",
"chars": 291,
"preview": "{\"name\":\"v0.9.26\",\"notes\":\"This release updates some packages, including Electron, fixes some icon rendering issues, and"
},
{
"path": "test/fixtures/saver.json",
"chars": 562,
"preview": "{\n \"name\": \"Screensaver One\",\n \"description\": \"A Screensaver\",\n \"aboutUrl\": \"\",\n \"author\": \"Colin Mitchell\",\n \"sour"
},
{
"path": "test/fixtures/saver2.json",
"chars": 388,
"preview": "{\n \"name\": \"Saver Two\",\n \"description\": \"Another Screensaver\",\n \"aboutUrl\": \"\",\n \"author\": \"Colin Mitchell\",\n \"sour"
},
{
"path": "test/fixtures/test-savers.json",
"chars": 1787,
"preview": "{\"url\":\"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721\",\"assets_url\":\"https://api.gi"
},
{
"path": "test/helpers.js",
"chars": 8027,
"preview": "/* eslint-disable mocha/no-exports */\n\nimport * as path from \"path\";\nimport fs from 'fs-extra';\nimport * as tmp from \"tm"
},
{
"path": "test/lib/package.js",
"chars": 5443,
"preview": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs\";\nimport * as tmp from \"tmp\";\nim"
},
{
"path": "test/lib/prefs.js",
"chars": 5507,
"preview": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport * as tmp from \"tmp\";\nimport fs from \"fs\";\n\ni"
},
{
"path": "test/lib/saver-factory.js",
"chars": 2180,
"preview": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport { rimrafSync } fr"
},
{
"path": "test/lib/saver-list.js",
"chars": 5742,
"preview": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport { rimrafSync } fr"
},
{
"path": "test/lib/saver.js",
"chars": 7905,
"preview": "\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimport fs from \"fs-extra\";\nimport * as tmp from \"tm"
},
{
"path": "test/main/power.js",
"chars": 1800,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\n\nimport assert from 'assert';\nimport path from \"path\";\nim"
},
{
"path": "test/main/release_check.js",
"chars": 1290,
"preview": "\"use strict\";\n\n\nimport assert from 'assert';\nimport path from \"path\";\nimport nock from \"nock\";\n\nimport ReleaseCheck from"
},
{
"path": "test/main/state_manager.js",
"chars": 2102,
"preview": "\"use strict\";\n\n\nimport assert from 'assert';\nimport sinon from \"sinon\";\n\nimport StateManager from \"../../src/main/state_"
},
{
"path": "test/ui/about.js",
"chars": 874,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \".."
},
{
"path": "test/ui/bootstrap.js",
"chars": 1904,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimp"
},
{
"path": "test/ui/editor.js",
"chars": 3588,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimp"
},
{
"path": "test/ui/new.js",
"chars": 2349,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport path from \"path\";\nimp"
},
{
"path": "test/ui/prefs.js",
"chars": 2743,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \".."
},
{
"path": "test/ui/settings.js",
"chars": 3861,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \".."
},
{
"path": "test/ui/tray.js",
"chars": 1858,
"preview": "/* eslint-disable mocha/no-setup-in-describe */\n\"use strict\";\n\nimport assert from 'assert';\nimport * as helpers from \".."
},
{
"path": "tools/build-packages.sh",
"chars": 790,
"preview": "#!/bin/bash\n\nDEST=\"/tmp/before-dawn-packages\"\nTARGET=\"$1\"\nWORKING_DIR=\"/tmp/before-dawn-build\"\nREPO=\"https://github.com/"
},
{
"path": "tools/update-build-version.js",
"chars": 410,
"preview": "'use strict';\nconst fs = require('fs');\nconst path = require('path');\n\nvar version = JSON.parse(fs.readFileSync(\"package"
},
{
"path": "webpack.config.js",
"chars": 155,
"preview": "import mainConfig from \"./webpack.main.config.js\";\nimport rendererConfig from \"./webpack.renderer.config.js\";\nexport def"
},
{
"path": "webpack.main.config.js",
"chars": 4060,
"preview": "\"use strict\";\n\nimport * as path from \"path\";\nimport webpack from \"webpack\";\nimport \"dotenv/config\";\n\nimport CopyWebpackP"
},
{
"path": "webpack.renderer.config.js",
"chars": 4593,
"preview": "\"use strict\";\n\nimport * as path from \"path\";\nimport webpack from \"webpack\";\nimport \"dotenv/config\";\n\nimport HtmlWebpackP"
}
]
// ... and 4 more files (download for full content)
About this extraction
This page contains the full source code of the muffinista/before-dawn GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 139 files (305.3 KB), approximately 85.9k tokens, and a symbol index with 134 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.