logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault
commit: a00d4ff679113d1ef9ad84ed3ee5d95d42d8ec8a
Author: Drew DeVault <sir@cmpwn.com>
Date:   Fri, 21 Aug 2020 14:59:29 -0400

Initial commit

Redesigned from scratch

Diffstat:

A.build.yml21+++++++++++++++++++++
A.gitignore2++
ALICENSE229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md4++++
Aassets/main.scss215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.toml14++++++++++++++
Acontent/_index.html3+++
Acontent/blog/A-broad-intro-to-networking.md248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/A-practical-understanding-of-Flux.md248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Abiopause.md90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Absence-of-features-in-IRC.md122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Actually-you-CAN-do-it.md65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Add-a-contrib-directory.md73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Analyzing-HN.md299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Anatomy-of-a-shell.md131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Announcing-aerc-0.1.0.md52++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Announcing-annotations-for-sourcehut.md231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Announcing-first-class-hg-support-on-sourcehut.md43+++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Announcing-wio.md56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Anti-AGPL-propaganda.md91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Arch-Linux-with-full-disk-encryption-in-15-minutes.md72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Archive-it-or-miss-it.md45+++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Avoid-traumatic-changes.md76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/BARE-message-encoding.md215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Backups-and-redundancy-at-sr.ht.md118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Bring-more-tor-into-your-life.md60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Building-a-real-Linux-distro.md110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Calculate-your-doation-fees-for-Patreon.md86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Can-we-talk-about-client-side-certs.md99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/China.md214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Commons-clause-will-destroy-open-source.md123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Complicated.md58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Configuring-aerc-for-git.md70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Conservative-web-development.md98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/DRM-leasing-and-VR-for-Wayland.md478+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Decentralize-decentralize-decentralize.md145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Dependencies-and-maintainers.md74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Designing-a-replacement-part-for-my-truck.md82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Developers-shouldnt-distribute.md74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Dont-sign-a-CLA.md72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Effective-project-governance.md108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Electron-considered-harmful.md101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Email-driven-git.md229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Embedding-files-in-C.md89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Engineers-solve-problems.md38++++++++++++++++++++++++++++++++++++++
Acontent/blog/Enough-to-decide.md177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/FOSDEM-recap.md98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/FOSS-contributor-tracks.md80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Fees-on-donation-platforms.md48++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Firefox-is-on-a-slippery-slope.md86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Fuck-you-nvidia.md64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Fucking-laptops.md62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Future-of-sway.md91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Generics-arent-ready-for-Go.md104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Getting-on-without-Google.md127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Getting-started-with-qemu.md103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Git-email-webcast.md30++++++++++++++++++++++++++++++
Acontent/blog/Git-is-already-distributed.md120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/GitHub-notifications.md87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Go-1.11.md79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Google-embraces-extends-extinguishes.md76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/HDCP-in-Weston.md124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Hack-everything-without-fear.md59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Hacking-on-your-TI-calculator.md195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/History-of-intelligent-observation.md196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Hooks.md26++++++++++++++++++++++++++
Acontent/blog/How-I-learned-to-stop-worrying-and-love-C.md121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-I-maintain-FOSS-projects.md121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-does-virtual-memory-work.md75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-to-abandon-a-FLOSS-project.md71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-to-contribute-to-FOSS.md51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-to-store-data-forever.md214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-to-write-a-better-bloom-filter-in-C.md217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/How-to-write-an-IRC-bot.md88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Im-doing-FOSS-full-time.md69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/In-Memoriam-Mozilla.md64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Input-handling-in-wlroots.md249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Integrating-a-VT220-into-my-life.md206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Interactive-SSH-programs.md223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Introducing-shell-access-for-builds.md88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Introduction-to-POSIX-shell.md102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Introduction-to-Wayland.md246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Its-not-okay-to-pretend-youre-open-source.md70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/KDE-Sprint-retrospective.md69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/KnightOS-was-interesting.md75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Learn-your-package-manager.md41+++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Lessons-to-learn-from-C.md83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Limited-generics-in-C.md110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Line-printer-shell-hack.md190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Local-mail-server.md149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Losing-faith-in-America.md78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/MSG_PEEK-is-more-common-than-you-think-CVE-2016-10229.md49+++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Mail-service-provider-recommendations.md119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/March-2nd-1943.md55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Music-syncing-on-Android.md113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/My-journey-from-MIT-to-GPL.md79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/My-lets-encrypt-setup.md128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/My-weird-branchless-git-workflow.md70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/NewPipe-represents-the-best-of-FOSS.md50++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Open-letter-to-Senator-Casey.md63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Patches-welcome.md61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Phone-maintenance.md64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/PinePhone-review.md138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Please-stop-using-slack.md149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Please-use-text-plain-for-emails.md138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Portability-matters.md81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Porting-Alpine-Linux-to-RISC-V.md118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Porting-an-entire-toolchain-to-the-browser-with-emscripten.md366+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Privacy-as-a-hobby.md135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Process-scheduling-in-KnightOS.md125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Python-datetime-sucks.md102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/RaptorCS-Blackbird-a-horror-story.md172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/RaptorCS-redemption.md101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Re-Slow.md161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Reckless-limitless-scope.md90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Redirecitng-stderr-of-running-process.md53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Rotating-passwords.md81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Rust-is-not-a-good-C-replacement.md129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Self-hosted-livestreaming.md204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Should-you-move-to-sr.ht.md54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Shut-up-and-get-back-to-work-style.md61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Signal.md201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Simple-correct-fast.md48++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Slow.md543+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/State-of-Sway-August-2017.md63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/State-of-sway-April-2017.md82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/State-of-sway.md122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-August-2019.md79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-July-2019.md90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-June-2019.md105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-March-2019.md137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-May-2019.md113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-November-2019.md64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-October-2019.md106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update-September-2019.md97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Status-update.md129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Stress-and-happiness.md144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Sway-0.9-in-retro.md112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Sway-1.0-highlights.md151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Sway-1.0-released.md93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Sway-and-client-side-decorations.md47+++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Sway-wlroots-at-XDC-2018.md77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-case-against-fork.md124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-last-years.md165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-problem-with-Python-3.md151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-profitability-of-online-services.md104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-road-to-sustainable-FOSS.md123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-worst-bugs.md183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/The-wrong-words-but-the-right-ideas.md83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Thoughts-on-performance.md87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Understanding-pointers.md280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Use-the-right-tool.md53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Using-Wl-wrap-for-mocking-in-C.md123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Using-cage-for-a-seamless-RDP-Wayland-desktop.md65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Using-git-with-discipline.md113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Vendor-purpose-OS.md47+++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Wayland-misconceptions-debunked.md237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Wayland-shells.md242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/We-are-complicit-in-our-employers-deeds.md67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Web-browsers-need-to-stop.md43+++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/What-is-a-fork.md128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/What-motivates-the-authors-of-the-software-you-use.md55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/When-not-to-use-a-regex.md79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Why-Go-error-handling-doesnt-sit-right-with-me.md113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Why-I-built-sr.ht-with-Flask.md81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Why-I-use-old-hardware.md58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Why-rewrite-wlc.md93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Writing-a-Wayland-compositor-1.md384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Writing-a-wayland-compositor-part-2.md170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Writing-a-wayland-compositor-part-3.md252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/You-dont-need-jQuery.md147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/Your-VPN-is-a-serious-choice.md98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/cozy-devnotes-machine-specs.md296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/dotfiles.md95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/dotorg.md121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/how-to-fuck-up-releases.md73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/osuweb.md525+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/pkg-go-dev-sucks.md72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/scdoc.md51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/sourcehut-design.md79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/sr.ht-general-availability.md113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blog/wlroots-whitepaper-available.md8++++++++
Acontent/dynlib.html369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/ideas.md59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/japanese.md288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/make-a-blog.md105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/misc.md19+++++++++++++++++++
Acontent/new-server.html214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/new-sysadmin-lecture.md39+++++++++++++++++++++++++++++++++++++++
Acontent/pinebook.html190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/talks/foss-contributors-mindset.md35+++++++++++++++++++++++++++++++++++
Acontent/talks/foss-contributors-mindset.pdf0
Alayouts/_default/single.html11+++++++++++
Alayouts/blog/section.html10++++++++++
Alayouts/blog/single.html27+++++++++++++++++++++++++++
Alayouts/index.html37+++++++++++++++++++++++++++++++++++++
Alayouts/partials/foot.html6++++++
Alayouts/partials/head.html12++++++++++++
Alayouts/partials/webring-in.html23+++++++++++++++++++++++
Alayouts/partials/webring-out.html73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/anki/JLPT-N1-5.apkg0
Astatic/anki/JLPT-N1.apkg0
Astatic/anki/JLPT-N2-5.apkg0
Astatic/anki/JLPT-N2.apkg0
Astatic/anki/JLPT-N3-5.apkg0
Astatic/anki/JLPT-N3.apkg0
Astatic/anki/JLPT-N4-5.apkg0
Astatic/anki/JLPT-N4.apkg0
Astatic/anki/JLPT-N5.apkg0
Astatic/anki/astronomy.apkg0
Astatic/anki/common-vocab.apkg0
Astatic/anki/comp.apkg0
Astatic/anki/counter-vocab-full.apkg0
Astatic/anki/counter-vocab.apkg0
Astatic/anki/linguistic-vocab-full.apkg0
Astatic/anki/linguistic-vocab.apkg0
Astatic/anki/music.apkg0
Astatic/avatar-148.jpg0
Astatic/avatar-148.png0
Astatic/avatar-512.png0
Astatic/avatar.png0
Astatic/video-js.css1234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/video.js22383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
223 files changed, 46700 insertions(+), 0 deletions(-)

diff --git a/.build.yml b/.build.yml @@ -0,0 +1,21 @@ +image: alpine/latest +packages: + - rsync + - go +sources: + - https://git.sr.ht/~sircmpwn/drewdevault.com + - https://git.sr.ht/~sircmpwn/hugo +secrets: + - 160a72cf-34d6-47b7-928b-c13b42b4d4f6 +tasks: +- hugo: | + cd hugo + go build --tags extended + sudo cp hugo /usr/local/bin/ +- build: | + cd drewdevault.com + /usr/local/bin/hugo +- upload: | + cd drewdevault.com + echo "StrictHostKeyChecking=no" >> ~/.ssh/config + rsync -rP public/ deploy@drewdevault.com:/var/www/drewdevault.com/ diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +public/ +resources/ diff --git a/LICENSE b/LICENSE @@ -0,0 +1,229 @@ +The code that powers this blog uses the following license: + + Copyright (c) 2015 Drew DeVault + + 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. + +The articles themselves are CC-BY-SA: + + THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE + COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY + COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS + AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + + BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO + BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS + CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND + CONDITIONS. + + 1. Definitions + + "Collective Work" means a work, such as a periodical issue, anthology + or encyclopedia, in which the Work in its entirety in unmodified form, + along with a number of other contributions, constituting separate and + independent works in themselves, are assembled into a collective whole. + A work that constitutes a Collective Work will not be considered a + Derivative Work (as defined below) for the purposes of this License. + "Derivative Work" means a work based upon the Work or upon the Work and + other pre-existing works, such as a translation, musical arrangement, + dramatization, fictionalization, motion picture version, sound + recording, art reproduction, abridgment, condensation, or any other + form in which the Work may be recast, transformed, or adapted, except + that a work that constitutes a Collective Work will not be considered a + Derivative Work for the purpose of this License. For the avoidance of + doubt, where the Work is a musical composition or sound recording, the + synchronization of the Work in timed-relation with a moving image + ("synching") will be considered a Derivative Work for the purpose of + this License. "Licensor" means the individual or entity that offers + the Work under the terms of this License. "Original Author" means the + individual or entity who created the Work. "Work" means the + copyrightable work of authorship offered under the terms of this + License. "You" means an individual or entity exercising rights under + this License who has not previously violated the terms of this License + with respect to the Work, or who has received express permission from + the Licensor to exercise rights under this License despite a previous + violation. "License Elements" means the following high-level license + attributes as selected by Licensor and indicated in the title of this + License: Attribution, ShareAlike. + + 2. Fair Use Rights. Nothing in this license is intended to reduce, limit, + or restrict any rights arising from fair use, first sale or other + limitations on the exclusive rights of the copyright owner under copyright + law or other applicable laws. + + 3. License Grant. Subject to the terms and conditions of this License, + Licensor hereby grants You a worldwide, royalty-free, non-exclusive, + perpetual (for the duration of the applicable copyright) license to + exercise the rights in the Work as stated below: + + to reproduce the Work, to incorporate the Work into one or more + Collective Works, and to reproduce the Work as incorporated in the + Collective Works; to create and reproduce Derivative Works; to + distribute copies or phonorecords of, display publicly, perform + publicly, and perform publicly by means of a digital audio transmission + the Work including as incorporated in Collective Works; to distribute + copies or phonorecords of, display publicly, perform publicly, and + perform publicly by means of a digital audio transmission Derivative + Works. + + For the avoidance of doubt, where the work is a musical composition: + Performance Royalties Under Blanket Licenses. Licensor waives the + exclusive right to collect, whether individually or via a performance + rights society (e.g. ASCAP, BMI, SESAC), royalties for the public + performance or public digital performance (e.g. webcast) of the Work. + Mechanical Rights and Statutory Royalties. Licensor waives the + exclusive right to collect, whether individually or via a music rights + society or designated agent (e.g. Harry Fox Agency), royalties for any + phonorecord You create from the Work ("cover version") and distribute, + subject to the compulsory license created by 17 USC Section 115 of the + US Copyright Act (or the equivalent in other jurisdictions). + Webcasting Rights and Statutory Royalties. For the avoidance of doubt, + where the Work is a sound recording, Licensor waives the exclusive + right to collect, whether individually or via a performance-rights + society (e.g. SoundExchange), royalties for the public digital + performance (e.g. webcast) of the Work, subject to the compulsory + license created by 17 USC Section 114 of the US Copyright Act (or the + equivalent in other jurisdictions). + + The above rights may be exercised in all media and formats whether now + known or hereafter devised. The above rights include the right to make such + modifications as are technically necessary to exercise the rights in other + media and formats. All rights not expressly granted by Licensor are hereby + reserved. + + 4. Restrictions.The license granted in Section 3 above is expressly made + subject to and limited by the following restrictions: + + You may distribute, publicly display, publicly perform, or publicly + digitally perform the Work only under the terms of this License, and + You must include a copy of, or the Uniform Resource Identifier for, + this License with every copy or phonorecord of the Work You distribute, + publicly display, publicly perform, or publicly digitally perform. You + may not offer or impose any terms on the Work that alter or restrict + the terms of this License or the recipients' exercise of the rights + granted hereunder. You may not sublicense the Work. You must keep + intact all notices that refer to this License and to the disclaimer of + warranties. You may not distribute, publicly display, publicly perform, + or publicly digitally perform the Work with any technological measures + that control access or use of the Work in a manner inconsistent with + the terms of this License Agreement. The above applies to the Work as + incorporated in a Collective Work, but this does not require the + Collective Work apart from the Work itself to be made subject to the + terms of this License. If You create a Collective Work, upon notice + from any Licensor You must, to the extent practicable, remove from the + Collective Work any reference to such Licensor or the Original Author, + as requested. If You create a Derivative Work, upon notice from any + Licensor You must, to the extent practicable, remove from the + Derivative Work any reference to such Licensor or the Original Author, + as requested. You may distribute, publicly display, publicly perform, + or publicly digitally perform a Derivative Work only under the terms of + this License, a later version of this License with the same License + Elements as this License, or a Creative Commons iCommons license that + contains the same License Elements as this License (e.g. + Attribution-ShareAlike 2.0 Japan). You must include a copy of, or the + Uniform Resource Identifier for, this License or other license + specified in the previous sentence with every copy or phonorecord of + each Derivative Work You distribute, publicly display, publicly + perform, or publicly digitally perform. You may not offer or impose any + terms on the Derivative Works that alter or restrict the terms of this + License or the recipients' exercise of the rights granted hereunder, + and You must keep intact all notices that refer to this License and to + the disclaimer of warranties. You may not distribute, publicly display, + publicly perform, or publicly digitally perform the Derivative Work + with any technological measures that control access or use of the Work + in a manner inconsistent with the terms of this License Agreement. The + above applies to the Derivative Work as incorporated in a Collective + Work, but this does not require the Collective Work apart from the + Derivative Work itself to be made subject to the terms of this License. + If you distribute, publicly display, publicly perform, or publicly + digitally perform the Work or any Derivative Works or Collective Works, + You must keep intact all copyright notices for the Work and give the + Original Author credit reasonable to the medium or means You are + utilizing by conveying the name (or pseudonym if applicable) of the + Original Author if supplied; the title of the Work if supplied; to the + extent reasonably practicable, the Uniform Resource Identifier, if any, + that Licensor specifies to be associated with the Work, unless such URI + does not refer to the copyright notice or licensing information for the + Work; and in the case of a Derivative Work, a credit identifying the + use of the Work in the Derivative Work (e.g., "French translation of + the Work by Original Author," or "Screenplay based on original Work by + Original Author"). Such credit may be implemented in any reasonable + manner; provided, however, that in the case of a Derivative Work or + Collective Work, at a minimum such credit will appear where any other + comparable authorship credit appears and in a manner at least as + prominent as such other comparable authorship credit. + + 5. Representations, Warranties and Disclaimer + + UNLESS OTHERWISE AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE + WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND + CONCERNING THE MATERIALS, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, + INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, + FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT + OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER + OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF + IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + + 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE + LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY + SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING + OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. Termination + + This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Derivative Works or + Collective Works from You under this License, however, will not have + their licenses terminated provided such individuals or entities remain + in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 + will survive any termination of this License. Subject to the above + terms and conditions, the license granted here is perpetual (for the + duration of the applicable copyright in the Work). Notwithstanding the + above, Licensor reserves the right to release the Work under different + license terms or to stop distributing the Work at any time; provided, + however that any such election will not serve to withdraw this License + (or any other license that has been, or is required to be, granted + under the terms of this License), and this License will continue in + full force and effect unless terminated as stated above. + + 8. Miscellaneous + + Each time You distribute or publicly digitally perform the Work or a + Collective Work, the Licensor offers to the recipient a license to the + Work on the same terms and conditions as the license granted to You + under this License. Each time You distribute or publicly digitally + perform a Derivative Work, Licensor offers to the recipient a license + to the original Work on the same terms and conditions as the license + granted to You under this License. If any provision of this License is + invalid or unenforceable under applicable law, it shall not affect the + validity or enforceability of the remainder of the terms of this + License, and without further action by the parties to this agreement, + such provision shall be reformed to the minimum extent necessary to + make such provision valid and enforceable. No term or provision of + this License shall be deemed waived and no breach consented to unless + such waiver or consent shall be in writing and signed by the party to + be charged with such waiver or consent. This License constitutes the + entire agreement between the parties with respect to the Work licensed + here. There are no understandings, agreements or representations with + respect to the Work not specified here. Licensor shall not be bound by + any additional provisions that may appear in any communication from + You. This License may not be modified without the mutual written + agreement of the Licensor and You. diff --git a/README.md b/README.md @@ -0,0 +1,4 @@ +# drewdevault.com + +This is my personal blog. You're free to reuse the content under the terms of +CC-BY-SA, and the code under the terms of the MIT license. diff --git a/assets/main.scss b/assets/main.scss @@ -0,0 +1,215 @@ +$black: #080808; + +html { + font-family: sans-serif; + color: $black; +} + +body { + max-width: 920px; + margin: 0 auto; + padding: 1rem; +} + +h1 { + margin-top: 0; + font-size: 1.5rem; + + small { + display: block; + font-size: 1rem; + } +} + +.index { + display: flex; + flex-direction: row; + + .article-list { + flex-grow: 1; + + .article { + margin-bottom: 1rem; + } + + .date { + display: block; + color: #333; + } + } + + aside { + width: 40%; + + img { + display: block; + margin: 0 auto 1rem; + border-radius: 5px; + } + + dt { + font-size: 0.9rem; + } + + dd { + margin-left: 0; + + &:not(:last-child) { + margin-bottom: 0.5rem; + } + } + } +} + +article { + margin: 0 auto; + max-width: 720px; + line-height: 1.3; + + img, video { + display: block; + margin: 0 auto; + max-width: 75%; + + @media(max-width: 640px) { + max-width: calc(100% - 2rem); + } + } + + .comment { + margin: 2rem auto 0; + max-width: 80%; + color: #333; + } +} + +.footnotes { + font-size: 0.85rem; +} + +footer { + margin-top: 2rem; + text-align: center; + font-size: 0.8rem; + color: #333; +} + +.float-img { + float: right; + display: inline; + padding-left: 1rem; + + @media(max-width: 640px) { + display: block; + float: none; + padding-left: inherit; + } +} + +pre { + background-color: #eee; + padding: 0.25rem 1rem; + margin: 0 -1rem; + max-width: 100%; + overflow-x: auto; + + .cp { + color: #800; + } + + .k { + color: #008; + } + + .kt, .kd, .kc { + color: #44F; + } + + .s { + color: #484; + font-style: italic; + } + + .c1 { + color: #333; + font-style: italic; + } + + .gi { + color: green; + } + + .gd { + color: red; + } + + .gu { + color: blue; + } +} + +.webring { + margin-top: 2rem; + + h2 { + font-size: 1.2rem; + } + + .articles { + display: flex; + } + + .title { + margin: 0; + } + + .article { + flex: 1 1 0; + display: flex; + flex-direction: column; + margin: 0 0.5rem; + padding: 0.5rem; + background: #eee; + } + + .article:first-child { + margin-left: 0; + } + + .article:last-child { + margin-right: 0; + } + + .summary { + font-size: 0.8rem; + flex: 1 1 0; + } + + .attribution { + float: right; + font-size: 0.8rem; + color: #555; + line-height: 3; + } + + .date { + color: black; + } +} + +summary { + cursor: pointer; + background-color: #eee; + padding: 0.25rem 1rem; + margin: 0 -1rem; +} + +details[open] { + border-bottom: 1rem solid #eee; + margin: 0 -1rem 1rem; + padding: 0 1rem; +} + +.centered { + text-align: center; +} diff --git a/config.toml b/config.toml @@ -0,0 +1,14 @@ +baseURL = "https://drewdevault.com/" +title = "Drew DeVault's blog" +pygmentsUseClasses = true +uglyurls = true +disablePathToLower = true + +[permalinks] +blog = "/:year/:month/:day/:filename" + +[markup.goldmark.renderer] +unsafe = true + +[markup.tableOfContents] +ordered = true diff --git a/content/_index.html b/content/_index.html @@ -0,0 +1,3 @@ +--- +title: Drew DeVault's blog +--- diff --git a/content/blog/A-broad-intro-to-networking.md b/content/blog/A-broad-intro-to-networking.md @@ -0,0 +1,248 @@ +--- +date: 2016-12-06 +# vim: tw=80 : +layout: post +title: A broad intro to networking +tags: [networking, instructional] +--- + +Disclaimer: I am not a network engineer. That's the point of this blog post, +though - I want to share with non-networking people enough information about +networking to get by. Hopefully by the end of this post you'll know enough about +networking to keep up with a conversation on networking, or know what to search +for when something breaks, or know what tech to research more in-depth when you +are putting together something new. + +## Layers + +The **OSI model** is the standard model we describe networks with. There are 7 +**layers**: + +Layer 1, the physical layer, is the electrical engineering stuff. + +Layer 2, the link layer, is how devices talk to each other. + +Layer 3, the network layer, is what they talk about. + +Layer 4, the transport layer, is where things like TCP and UDP live. + +Layers 5 and 6 aren't very important. + +Layer 7, the application layer, is where Minecraft lives. + +When you hear some security guy talking about a "layer 7 attack", he's +talking about a attack that focuses on flaws in the application layer. In +practice that means i.e. flooding the server with HTTP requests. + +## 1: Physical Layer + +*Generally implemented by matter* + +Layer 1 is the hardware of a network. Commonly you'll find things here like your +computer's **NIC** (network interface controller), aka the network interface or +just the interface, which is the bit of silicon in your PC that you plug network +cables or WiFi signals into. + +On Linux, network interfaces are assigned names like *eth0* or *eno1*. eth0 is +the traditional name for the 0th wired network interface. eno1 is the newer +"consistent network device naming" format popularized by tools like udev (which +manages hardware on many Linux systems) - this is a deterministic name based on +your network hardware, and won't change if you add more interfaces. You can +manage your interfaces with the *ip* command (`man 8 ip`), or the now-deprecated +*ifconfig* command. Some non-Linux Unix systems have not deprecated ifconfig. + +This layer also has ownership over **MAC addresses**, in theory. A MAC address +is an allegedly unique identifier for a network device. In practice, software +at higher layers can use whatever MAC address they want. You can change your MAC +address with the ip command, which is often useful for dealing with annoying +public WiFi resource limits or for frustrating someone else on the network. + +Other things you find at layer 1 include **switches**, which do network +multiplexing (they generally can be thought of as networking's version of a +power strip - they turn one Ethernet port into many). Also common are +**routers**, whose behaviors are better explained in other layers. You also have +hardware like **firewalls**, which filter network traffic, and **load +balancers**, which distribute a load among several nodes. Both firewalls and +load balancers can be done in software, depending on your needs. + +## 2: Data link layer + +*Generally implemented by network hardware* + +At this layer you have protocols that cover how nodes talk to one another. Here +the **ethernet** protocol is almost certainly the most common - the protocol +that goes over your network cables. Said network cables are probably **Cat 5** +cables, or "category 5" cables. + +Other protocols here include tunnels, which allow you to indirectly access a +network. A common example is a **VPN**, or virtual private network, which allows +you to participate in another network remotely. Tunnels can also be useful for +getting around firewalls, or for setting up a secure means to access resources +on another network. + +## 3: Network layer + +*Generally implemented by the kernel* + +As a software guy, this is where the fun really starts. The other layers are how +computers talk to each other - this layer is what they talk about. Computers are +often connected via a **LAN**, or local area network - a *local* network of +computers. Computers are also often connected to a **WAN**, or wide area +network - the internet is one such network. + +The most common protocol at this layer is IP, or Internet Protocol. There are +two versions that matter: IPv4, and IPv6. Both of them use **IP addresses** to +identify nodes on their networks, and they carry **packets** between them. The +major difference between IPv4 and IPv6 is the size of their respective **address +spaces**. IPv4 uses 32 bit addresses, supporting a total of 4.3 billion possible +addresses, which on the public internet are quickly becoming a sparse resource. +IPv6 uses 128-bit addresses, which allows for a zillion unique addresses. + +Ranges of IP addresses can be described with a **subnet mask**. Such a range of +IP addresses constitutes a **subnetwork**, or subnet. Though you're probably +used to seeing an IPv4 address encoded like `10.20.30.40`, remember that it can +also just be represented as one 32-bit number - in this case 169090600, or +0xA141E28, and you can do bitwise math against these numbers. You generally +represent a subnet with CIDR notation, such as `192.168.1.0/24`. In this case, the +first 24 bits are meaningful, and all possible values for the remaining 8 bits +constitute the range of addresses represented by this mask. + +IPv4 has several subnets reserved for this and that. Some important ones are: + +* `0.0.0.0/8` - current network. On many systems, you can treat `0.0.0.0` as all + IP addresses assigned to your device +* `127.0.0.0/8` - loopback network. These addresses refer to yourself. +* `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16` are reserved for private + networks - you can allocate these addresses on a LAN. + +An IPv4 packet includes, among other things: a **time to live**, or TTL, which +limits how long the packet can live for; the **protocol**, such as TCP; the +**source** and **destination** addresses; a header checksum; and the +**payload**, which is specific to the higher level protocol in use. + +Given the limited size of the IPv4 space, most networks are designed with an +isolated LAN that uses **NAT**, or network address translation, to translate IP +addresses from the WAN. Basically, a router or similar component will translate +internal IP addresses (allocated from the private subnets) to its own external +IP address, and vice versa, when passing communications along to the WAN. With +IPv6 there are so many IP addresses that you don't need to use NAT. If you're +wondering whether or not we'll ever run out of IPv6 addresses - leave that to +someone else to solve tens of millions of years from now. + +IPv6 addresses are 128-bits long and are described with strings like +`2001:0db8:0000:0000:0000:ff00:0042:8329`. Luckily the people who designed it +were kind enough to realize people don't want to write that, so it can be +shortened to `2001:db8::ff00:42:8329` by removing leading zeros and removing +sections entirely composed of zeros. Where colons are reserved for another +purpose, you'll typically add brackets around the IPv6 address, such as +`http://[2607:f8b0:400d:c03::64]`. The IPv6 loopback address (localhost) is +`::1`, and IPv6 subnets are written the same way as in IPv4. Given how many +IPv6 addresses there are, it's common to be allocated lots of them in cases when +you might have expected to only receive one IPv4 address. Typically these blocks +will be anywhere from /48 to /56 - which contains more addresses than the entire +IPv4 space. + +IP addresses are often **static**, which means the node connecting to the +network already knows its IP address and starts using it right away. They may +also be **dynamic**, and are allocated by some computer on the network with the +**DHCP** protocol. + +IPsec also lives in layer 3. + +## 4: Transport Layer + +*Generally implemented by the kernel* + +The transport layer is where you have higher level protocols, through which much +of the work gets done. Protocols here include TCP, UDP, ICMP (used for ping), +and others. These protocols are used to power application-layer protocols. + +**TCP**, or the transmission control protocol, is probably the most popular +transport layer protocol out there. It turns the unreliable internet protocol +into a reliable byte stream. TCP (tries to) make four major guarantees: data +will arrive, will arrive exactly once, will arrive in the correct order, and +will be the correct data. + +TCP takes a stream of bytes and breaks it up into **segments**. Each segment is +then stuck into an IP packet and sent on its way. A TCP segment includes the +source and destination **ports**, which are used to distinguish between +different application-layer protocols in use and to distinguish between +different applications using the protocol on the same host; a **sequence +number**, which is used to order the packet; an **ACK number**, which is used to +inform the other end that it has received some packet and it can stop retrying; +a checksum; and the data itself. The protocol also includes a handshake process +and other housekeeping processes that the application needn't be aware of. +Generally speaking, the overhead of TCP is significant for real-time +applications. + +Most TCP servers will **bind** to a certain port to **listen** for incoming +connections, via the operating system's **socket** implementation. Many TCP +**clients** can connect to one server. + +Ports are a 16 bit unsigned integer. Most applications have a default port +they're known to use, such as 80 for HTTP. Originally these numbers were +allocated by the internet police, but this has fallen out of practice. On most +systems, ports less than 1024 require elevated permissions to listen to. + +**UDP**, or the user datagram protocol, is the second most popular transport +layer protocol, and is the lighter of the two. UDP is a paper thin layer on top +of IP. A UDP packet contains a source port, destination port, checksum, and a +payload. This protocol is fast and lightweight, but makes none of the promises +TCP makes - UDP "**datagrams**" may arrive multiple or zero times, in a +different order than they were sent, and possibly with data errors. Many people +who use UDP will implement these guarantees themselves in a some lighter-weight +fashion than TCP. Importantly, UDP source IPs can be spoofed and the destination +has no means of knowing where it really came from - TCP avoids this by doing a +handshake before exchanging any data. + +UDP can also issue broadcasts, which are datagrams that are sent to every node +on the network. Such datagrams should be addressed to `255.255.255.255`. There's +also multicast, which specifies a subset of all nodes to send the datagram to. +Note that both of these have limited support in real-world networks. + +## 5 & 6: Session and presentation + +Think of these as extensions of layer 7, the application layer. Technically +things like SSL, compression, etc are done here, but in practice it doesn't +have any important technical implications. + +## 7: Application layer + +*Generally implemented by end-user software* + +The application layer is the uppermost layer of the network and it's what all +the other layers are there for. At this layer you have all of the hundreds of +thousands of application-specific protocols out there. + +**DNS**, or the domain name system, is a protocol for mapping domain names (i.e. +google.com) to IP addresses (i.e. 209.85.201.100), among other features. DNS +servers keep track of DNS records, which associate names with records of various +types. Common records include A, which maps a name to an IPv4 address, AAAA for +IPv6, CNAME for aliases, and MX for email records. The most popular DNS server +is bind, which you can run on your own network to operate a private name system. + +Some other UDP protocols: NTP, the network time protocol; DHCP, which assigns +dynamic IP addresses on networks; and nearly all real-time video and audio +streaming protocols (like VoIP). Many video games also use UDP for their +multiplayer networking. + +TCP is more popular than UDP and powers many, many, many applications, due +largely to the fact that it simplifies the complex intricacies of networking. +You're probably familiar with HTTP, which is used by web browsers use to fetch +resources. Email applications often communicate over TCP with IMAP to retrieve +the contents of your inbox, and SMTP to send emails to other servers. SSH (the +secure shell), FTP (file transfer protocol), IRC (internet relay chat), and +countless other protocols also use TCP. + +- - - + +Hopefully this article helps you gain a general understanding of how computers +talk to each other. In my own experience, I've used a broad understanding of the +entire stack and a deep understanding of levels 3 and up. I expect most +programmers today need a broad understanding of the entire stack and a deep +understanding of level 7, and I hope that most programmers would seek a deep +understanding of level 4 as well. + +Please leave some feedback if you appreciated this article - I may do more +similar articles in the future, giving a broad introduction to other topics. The +next topics I have in mind are security and encryption (as separate posts). diff --git a/content/blog/A-practical-understanding-of-Flux.md b/content/blog/A-practical-understanding-of-Flux.md @@ -0,0 +1,248 @@ +--- +date: 2015-07-20 +# vim: tw=80 +title: A practical understanding of Flux +layout: post +tags: [javascript, react] +--- + +[React.js](https://facebook.github.io/react/) and the +[Flux](https://facebook.github.io/flux/) are shaping up to be some of the most +important tools for web development in the coming years. The MVC model was +strong on the server when we decided to take the frontend seriously, and it was +shoehorned into the frontend since we didn't know any better. React and Flux +challenge that and I like where it's going very much. That being said, it was +very difficult for me to get into. I put together this blog post to serve as a +more *practical* guide - the upstream documentation tells you a lot of concepts +and expects you to put them together yourself. Hopefully at the end of this +blog post you can confidently start writing things with React+Flux instead of +reading brain-melting docs for a few hours like I did. + +At the core of it, React and Flux are very simple and elegant. Far more simple +than the voodoo sales pitch upstream would have you believe. To be clear, +**React** is a framework-ish that lets you describe your UI through reusable +components, and includes *jsx* for describing HTML elements directly in your +JavaScript code. **Flux** is an *optional* architectural design philosophy that +you can adopt to help structure your applications. I have been using +[Babel](https://babeljs.io/) to compile my React+Flux work, which gives me +ES6/ES7 support - I strongly suggest you do the same. This blog post assumes +you're doing so. For a crash course on ES6, [read this entire +page](http://git.io/es6features). Crash course for ES7 is omitted here for +brevity, but [click this](https://gist.github.com/SirCmpwn/2e8e455c91494b7c3713) +if you're interested. + +## Flux overview + +Flux is based on a unidirectional data flow. The direction is: dispatcher ➜ +stores ➜ views, and the data is actions. At the stores or views level, you can +give actions to the dispatcher, which passes them down the line. + +Let's explain exactly what piece is, and how it fits in to your application. +After this I'll tell you some specific details and I have a starter kit prepared +for you to grab as well. + +### Dispatcher + +The dispatcher is very simple. Anything can register to receive a callback when +an "action" happens. There is one dispatcher and one set of callbacks, and +everything that registers for it will receive every action given to the +dispatcher, and can do with this as it pleases. Generally speaking you will only +have the stores listen to this. The kind of actions you will send along may look +something like this: + +* Add a record +* Delete a record +* Fetch a record with a given ID +* Refresh a store + +Anything that would change data is going to be given to the dispatcher and +passed along to the actions. Since everything receives every action you give to +the dispatcher, you have to encode something into each action that describes +what it's for. I use objects that look something like this: + +```json +{ + "action": "STORE_NAME.ACTION_TYPE.ETC", + ... +} +``` + +Where `...` is whatever extra data you need to include (the ID of the record +to fetch, the contents of the record to be added, the property that needs to +change, etc). Here's an example payload: + +```json +{ + "action": "ACCOUNTS.CREATE.USER", + "username": "SirCmpwn", + "email": "sir@cmpwn.com", + "password": "hunter2" +} +``` + +The Accounts store is listening for actions that start with `ACCOUNTS.` and when +it sees `CREATE.USER`, it knows a new user needs to be created with these +details. + +### Stores + +The stores just have ownership of data and handle any changes that happen to +that data. When the data changes, they raise events that the views can subscribe +to to let them know what's up. There's nothing magic going on here (I initially +thought there was magic). Here's a really simple store: + +```js +import Dispatcher from "whatever"; + +export class UserStore { + constructor() { + this._users = []; + this.action = this.action.bind(this); + Dispatcher.register(this.action); + } + + get Users() { + return this._users; + } + + action(payload) { + switch (payload.action) { + case "ACCOUNTS.CREATE.USER": + this._users.push({ + "username": payload.username, + "email": payload.email, + "password": payload.password + }); + raiseChangeEvent(); // Exercise for the reader + break; + } + } +} + +let store = new UserStore(); +export default new UserStore(); +``` + +Yeah, that's all there is to it. Each store should be a singleton. You use it +like this: + +```js +import UserStore from "whatever/UserStore"; + +console.log(UserStore.Users); + +UserStore.registerChangeEvent(() => { + console.log(UserStore.Users); // This has changed now +}); +``` + +Stores end up having a lot of boilerplate. I haven't quite figured out the best +way to address that yet. + +### Views + +Views are react components. What makes React components interesting is that they +re-render the whole thing when you call `setState`. If you want to change the +way it appears on the page for any reason, a call to `setState` will need to +happen. And here are the two circumstances under which they will change: + +* In response to user input to change non-semantic view state +* In response to a change event from a store + +The first bullet here means that you can call `setState` to change view states, +but not data. The second bullet is for when the data changes. When you change +view states, this refers to things like "click button to reveal form". When you +change data, this refers to things like "a new record was created, show it", or +even "a single property of a record changed, show that change". + +**Wrong way**: you have a text box that updates the "name" of a record. When the +user presses the "Apply" key, the view will re-render itself with the new name. + +**Right way**: When you press "Apply", the view sends an action to the +dispatcher to apply the change. The relevant store picks up the action, applies +the change to its own data store, and raises an event. Your view hears that +event and re-renders itself. + +![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-1300w.png) + +![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png) + +### Why bother? + +* Easy to have stores depend on each other +* All views that depend on the same stores are updated when it changes +* It follows that all cross-store dependencies are updated in a similar fashion +* Single source of truth for data +* Easy as pie to pick up and maintain with little knowledge of the codebase + +## Practical problems + +Here are some problems I ran into, and the fluxy solution to each. + +### Need to load data async + +You have a list of DNS records to show the user, but they're hanging out on the +server instead of in JavaScript objects. Here's how you accomodate for this: + +* When you use a store, call `Store.fetchIfNecessary()` first. +* When you pull data from the store, expect `null` and handle this elegantly. +* When the initial fetch finishes in the store, raise a change event. + +From `fetchIfNecessary` in the store, go do the request unless it's in progress or +done. On the view side, show a loading spinner or something if you get `null`. +When the change event happens, whatever code set the state of your component +initially will be re-run, and this time it won't get `null` - deal with it +appropriately (show the actual UI). + +This works for more than things that are well-defined at dev time. If you need +to, for example, fetch data for an arbitrary ID: + +* View calls `Store.userById(10)` and gets `null`, renders lack of data + appropriately +* Store is like "my bad" and fetches it from the server +* Store raises change event when it arrives and the view re-renders + +### Batteries not included + +Upstream, in terms of actual usable code, flux just gives you a dispatcher. You +also need something to handle your events. This is easy to roll yourself, or you +can grab one of a bazillion things online that will do it for you. There is also +no base Store class for you, so make one of those. You should probably just +include some shared code for raising events and consuming actions. Mine looks +something like this: + +```js +class UserStore extends Store { + constructor() { + super("USER"); + this._users = []; + super.action("CREATE.USER", this.userCreated); + } + + userCreated(payload) { + this._users.push(...); + super.raiseChangeEvent(); + } + + get Users { + return this._users; + } +} +``` + +Do what works best for you. + +## Starter Kit + +If you want something with the batteries in and a base to build from, I've got +you covered. Head over to +[SirCmpwn/react-starter-kit](https://github.com/SirCmpwn/react-starter-kit) on +Github. + +## Conclusion + +React and Flux are going to be big. This feels like the right way to build a +frontend. Hopefully I saved you from all the headache I went through trying to +"get" this stuff, and I hope it serves you well in the future. I'm going to be +pushing pretty hard for this model at my new gig, so I may be writing more blog +posts as I explore it in a large-scale application - stay tuned. diff --git a/content/blog/Abiopause.md b/content/blog/Abiopause.md @@ -0,0 +1,90 @@ +--- +date: 2020-03-03 +title: The Abiopause +layout: post +--- + +The sun has an influence on its surroundings. One of these is in the form of +small particles that are constantly ejected from the sun in all directions, +which exerts an outward pressure, creating an expanding sphere of particles that +moves away from the sun. These particles are the solar wind. As the shell of +particles expands, the density (and pressure) falls. Eventually the solar wind +reaches the *interstellar medium* &mdash; the space between the stars &mdash; +which, despite not being very dense, is not empty. It exerts a pressure that +pushes inwards, towards the sun. + +Where the two pressures balance each other is an interesting place. The sphere +up to this point is called the *heliosphere* &mdash; which can be roughly +defined as the zone in which the influence of the sun is the dominant factor. +The *termination shock* is where the change starts to occur. The plasma from the +sun slows, compresses, and heats, among other changes. The physical interactions +here are interesting, but aren't important to the metaphor. At the +termination shock begins the *heliosheath*. This is a turbulent place where +particles from the sun and from the interstellar medium mix. The interactions in +this area are complicated and interesting, you should read up about it later. + +![Picture of a faucet pouring into a sink](https://legacy.sr.ht/_FIT.svg) + +<div class="text-center"> + <small>Yanpas via Wikimedia Commons, CC-BY-SA</small> +</div> + +Finally, we reach the *heliopause*, beyond which the influence of the +interstellar medium is dominant. Once crossing this threshold, you are said to +have left the solar system. The Voyager 1 space probe, the first man-made object +to leave the solar system, crossed this point on August 25th, 2012. Voyager 2 +completed the same milestone on November 12th, 2018[^1]. + +[^1]: It took longer because Voyager 2 went on to see Uranus and Neptune. Voyager 1 just swung around Saturn and was shot directly up and out of the solar system. Three other man-made objects are currently on trajectories which will leave the solar system. + +In the world of software, the C programming language clearly stands out as the +single most important and influential programming language. Everything +forming the critical, foundational parts of your computer is written in it: +kernels, drivers, compilers, interpreters, runtimes, hypervisors, databases, +libraries, and more are almost all written in C.[^2] For this reason, any +programming language which wants to get anything useful done is certain to +support a C FFI (foreign function interface), which will allow programmers to +communicate with C code from the comfort of a high-level language. No other +language has the clout or ubiquity to demand this level of deference from +everyone else. + +[^2]: Even if you don't like C, it would be ridiculous to dismiss its influence and importance. + +The way that an application passes information back and forth with its +subroutines is called its *ABI*, or application binary interface. There are a +number of ABIs for C, but the most common is the System-V ABI, which is used on +most modern Unix systems. It specifies details like which function parameters to +put in which registers, what goes on the stack, the structure and format of +these values, and how the function returns a value to the caller. In order to +interface with C programs, the FFI layers in other programs have to utilize this +ABI to pass information to and from C functions. + +Other languages often have their own ABIs. C, being a different programming +language from $X, naturally has different semantics. The particular semantics of +C don't necessarily line up to the semantics the language designers want $X to +have, so the typical solution is to define functions with C "linkage", which +means they're called with the C ABI. It's from this that we get keywords like +`extern "C"` (C++, Rust), `export` in Go, `[DllImport]` in C#, and so on. +Naturally, these keywords come with a lot of constraints on how the function +works, limiting the user to the mutually compatible subset of the two ABIs, or +else using some kind of translation layer. + +I like to think of the place where this happens as the "abiopause", and draw +comparisons with the solar system's heliopause. Within the "abiosphere", the +programming language you're using is the dominant influence. The idioms and +features of the language are used to their fullest extent to write idiomatic +code. However, the language's sphere of influence is but a bubble in a sea of C +code, and the interface between these two areas of influence is often quite +turbulent. Directly using functions with C linkage from the abiosphere is not +pleasant, as the design of good C APIs do not match the semantics of good +$X APIs. Often there are layers to this transition, much like our solar +system, where some attempt is made to wrap the C interface in a more idiomatic +abstraction. + +I don't really like this boundary, and I think most programmers who have worked +here would agree. If you like C, you're stuck either writing bad C code or using +poorly-suited tools to interface badly with an otherwise good API. If you like +$X, you're stuck writing very non-idiomatic $X code to interface with a foreign +system. I don't know how to fix this, but it's interesting to me that the +"abiopause" appears to be an interface full of a similar turbulence and +complexity as we find in the heliopause. diff --git a/content/blog/Absence-of-features-in-IRC.md b/content/blog/Absence-of-features-in-IRC.md @@ -0,0 +1,122 @@ +--- +date: 2019-07-01 +layout: post +title: Absence of certain features in IRC considered a feature +tags: ["philosophy", "irc"] +--- + +The other day a friend of mine (an oper on Freenode) wanted to talk about IRC +compared to its peers, such as Matrix, Slack, Discord, etc. The ensuing +discussion deserves summarization here. In short: I'm glad that IRC doesn't have +the features that are "showstoppers" for people choosing other platforms, and +I'm worried that attempts to bring these showstopping "features" to IRC will +worsen the platform for the people who use it now. + +On IRC, features like embedded images, a nice UX for messages longer than a few +lines (e.g. pasted code), threaded messages, etc; are absent. Some sort of +"graceful degradation" to support mixed channels with clients which support +these features and clients which don't may be possible, but it still *degrades* +the experience for many people. By instead making everyone work within the +limitations of IRC, we establish a shared baseline, and expressing yourself +within these limitations is not only possible but makes a better experience for +everyone. + +Remember that [not everyone is like you][old hardware]. I regularly chat with +people on ancient hardware that slows to a crawl when a web browser is +running[^1], or people working from a niche operating system for which porting a +graphical client is a herculean task, or people with accessibility concerns for +whom the "one line of text per statement" fits nicely into their TTS[^2] system +and screenreading Slack is a nightmare. + +[^1]: Often, I *am* this person. +[^2]: Text to speech. +[old hardware]: https://drewdevault.com/2019/01/23/Why-I-use-old-hardware.html + +Let's consider what happens when these features are added but non-uniformly +available. Let's use rich text as an example and examine the fallback +implementation. Which of these is better? + +<span>(A) &lt;user&gt; check out [this website](<a href="https://example.org">https://example.org</a>)</span> + +<span>(B) &lt;user&gt; check out this website: <a href="https://example.org">https://example.org</a></span> + +Example B is what people naturally do when rich text is unavailable, and most +clients will recognize it as a link and make it clickable anyway. But many +clients cannot and will not display example A as a link, which makes it harder +to read. Example A also makes phishing *much* easier. + +Here's another example: how about a nice UI for long messages, such as pasted +code snippets? Let's examine how three different clients would implement this: +(1) a GUI client, (2) a TUI[^3] client, and (3) a client which refuses to +implement it or is unmaintained[^4]. + +The first case is the happy path, we probably get a little scrollbox that the +user can interact with their mouse. Let's say [Weechat](https://weechat.org/) +takes up option 2, but how do they do that? Some terminal emulators have mouse +support, so they could have a similar box, but since Weechat is primarily +keyboard-driven (and some terminal emulators do not support mice!), a +keyboard-based alternative will be necessary. Now we have to have some kind of +command or keybinding for scrolling through the message, and picking which of +the last few long messages we want to scroll through. This will have to be +separate from scrolling through the backlog normally, of course. The third +option is the worst: they just see a hundred lines pasted into their backlog, +which is already highly scorned behavior on most IRC channels. Only the GUI +users come away from this happy, and on IRC they're in the minority. + +Some IRC clients (Matrix) have this feature today, but most Matrix users don't +realize what a nuisance they're being on the chat. Here's what they see: + +![](https://sr.ht/VOeY.png) + +And here's what I see: + +![](https://sr.ht/HZ7Z.png) + +Conservative improvements built on top of existing IRC norms, such as [The +Lounge](https://thelounge.chat/), are much better. Most people post images on +IRC as URLs, which clients can do a quick HEAD request against and embed if the +mimetype is appropriate: + +![](https://sr.ht/9RsR.png) + +[^3]: Text user interface +[^4]: IRC is over 30 years old and has barely changed since - so using unmaintained or barely-maintained clients is not entirely uncommon nor wrong. + +For most of these features, I think that people who have and think they need +them are in fact unhappier for having them. What are some of the most common +complaints from Slack users et al? "It's distracting." "It's hard to keep up +with what people said while I was away." "Threads get too long and hard to +understand." Does any of this sound familiar? Most of these problems are caused +by or exacerbated by features which are missing from IRC. It's distracting +because your colleagues are posting gifs all day. It's hard to keep up with +because the infinite backlog encourages a culture of catching up rather than +setting the expectation that conversations are ephemeral[^5]. Long conversations +shouldn't be organized into threads, but moved into email or another medium more +suitable for that purpose. + +[^5]: Many people have bouncers which allow them to catch up the last few lines, and keep logs which they can reference later if necessary. This is nice to have but adds enough friction to keep the expectation that discussions are ephemeral, which has a positive effect on IRC culture. + +None of this even considers what *is* good about IRC. It's a series of +decentralized networks built on the shoulders of volunteers. It's venerable and +well-supported with hundreds of client and server implementations. You can +connect to IRC manually using telnet and have a pretty good user experience! +Accordingly, [a working IRC bot can be written in about 2 +minutes][irc-slack-bot-comparison]. No one is trying to monetize you on IRC. +It's free, in both meanings, and nothing which has come since has presented a +compelling alternative. I've used IRC all day, every day for over ten years, and +that's not even half of IRC's lifetime. It's outlived everything else by years +and years, and it's not going anywhere soon. + +[irc-slack-bot-comparison]: https://drewdevault.com/2018/03/10/How-to-write-an-IRC-bot.html + +In summary, I like IRC the way it is. It has problems which we ought to address, +but many people focus on the wrong problems. The culture that it fosters is good +and worth preserving, even at the expense of the features users of other +platforms demand - or those users themselves. + +--- + +P.S. A friend pointed out that the migration of non-hackers away from IRC is +like a reverse [Eternal September][eternal-september], which sounds *great* 😉 + +[eternal-september]: https://en.wikipedia.org/wiki/Eternal_September diff --git a/content/blog/Actually-you-CAN-do-it.md b/content/blog/Actually-you-CAN-do-it.md @@ -0,0 +1,65 @@ +--- +date: 2017-01-06 +# vim: tw=80 +title: Actually, you CAN do it +layout: post +tags: [philosophy] +--- + +I maintain a *lot* of open source projects. In order to do so, I have to +effectively manage my time. Most of my projects follow this philosophy: if you +want something changed, send a patch. If you are running into an annoying bug, +fix it and send a patch. If you want a new feature, implement it and send a +patch. It's definitely a good idea to talk about it beforehand on the issue +tracker or IRC, but don't make the mistake of thinking this processes ends with +someone else doing it for you. + +Every developer who contributes to a project I maintain is self-directed. They +work on what they'd like. They scratch their own itches. Sometimes what they'd +like to work on is non-specific, and in that case I'll help them find something +to do based on what users are asking for lately or based on my own goals for the +project. I often maintain a list of "low hanging fruit" issues on Github, and +I am generally willing to offer some suggestions if someone asks for such a +task. However, for more complex, non-"low hanging fruit" tasks, they generally +only get worked on when someone with the know-how wants it done and does it. + +So what does this mean for you, user whose problem no developer is interested +in? Well, it's time for you to step up and work on it yourself. I don't really +care if your problem is "a showstopper" or "the only thing preventing you from +switching to my software", or any of a number of other excuses you may have +lined up for getting someone else to do it for you. None of the other regular +contributors really care about your interpretation of what their priorities +should be, either. We aren't a business. We aren't making a sale. We're just +making cool software that works for us and publishing it in the hopes that +you'll find it useful, too. + +Generally by this point in the conversation with Joe User, they tell me they +*can't* do it. Well, Joe User, I beg to differ. It doesn't matter that you don't +know *[insert programming language]*, or haven't used *[insert relevant +library]* before. You don't learn new things by hanging out in your comfort +zone. Many of the regulars you're bugging to do your work for you were once in +your shoes. + +Everything is setting you up for success. You literally have hundreds of +resources at your disposal. The internet is was made by developers, you know, +and we built tons of resources to support ourselves with it. You have +documentation, Q&A sites, chat rooms, and more waiting to help you when you get +stuck. We're here to answer your questions with the codebase, too. I pride +myself on making the code accessible and easy to get into, and I'll help you +learn to do the same when you integrate your with our project. + +We would much rather give you advice on how to fix the problem yourself than to +fix the problem for you. Even if it takes more of our attention to do so, we get +the added benefit of a new person who is qualified to help out the next guy. A +person who is now fixing their own bugs and improving the software for everyone. +That's a much better outcome than having to waste our own time on a task we +aren't interested in. + +It might be hard, but hey, it'd be hard for us too. You'll learn and be better +for it. Wouldn't it be nice to add *[language you don't know]* or *[library you +don't know]* to your resume, anyway? If you're concerned about the scope of your +problem, how about asking about the low hanging fruit so you have easier tasks +to learn with? + +The cards are stacked in your favor. The only problem is your defeatist +attitude. Just do it! diff --git a/content/blog/Add-a-contrib-directory.md b/content/blog/Add-a-contrib-directory.md @@ -0,0 +1,73 @@ +--- +date: 2020-06-06 +title: Add a "contrib" directory to your projects +layout: post +tags: [software, practices] +--- + +There's a common pattern among free- and open-source software projects to +include a "contrib" directory at the top of their source code tree. I've seen +this in many projects for many years, but I've seen it discussed only rarely +&mdash; so here we are! + +The contrib directory is used as an unorganized (or, at best, lightly organized) +bin of various useful things **contrib**uted by the community around the +software, but which is not necessarily a good candidate for being a proper part +of the software. Things in contrib should not be wired into your build system, +shouldn't be part of your automated testing, shouldn't be included in your +documentation, and should not be installed with your packages. contrib entries +are not supported by the maintainers, and are given only a light code review at +the most. There is no guarantee whatsoever of workitude or maintenance for +anything found in contrib. + +Nevertheless, it is often useful to have such a place to put various little +scripts, config files, and so on, which provide a helpful leg-up for users +hoping to integrate the software with some third-party product, configure it to +fit nicely into an unusual environment, coax it into some unusual behavior, or +whatever else the case may be. The idea is to provide a place to drop a couple +of files which might save a future someone facing similar problems from doing +all of the work themselves. Such people can contribute back small fixes or +improvements, and the maintenance burden of such contributions lies entirely +with the users. + +If the contributor wants to take on a greater maintenance burden, this kind of +stuff is better suited to a standalone project, with its own issue tracking, +releases, and so on. If you just wrote a little script and want somewhere to +drop it so that others may find it useful, then contrib is the place for you. + +For a quick example, let's consult Sway's [contrib +folder](https://github.com/swaywm/sway/tree/master/contrib): + +``` +_incr_version +autoname-workspaces.py +grimshot +grimshot.1 +grimshot.1.scd +inactive-windows-transparency.py +``` + +The `_incr_version` script is something that I use myself to help with preparing +new releases. It is a tool useful only to maintainers, and therefore is not +distributed with the project. + +Looking at `autoname-workspaces.py` next, from which we can see that the quality +criteria is reduced for members of contrib &mdash; none of Sway's upstream code +is written in Python, and the introduction of such a dependency would be +controversial. This script automatically changes your workspace name based on +what applications you're running in it &mdash; an interesting workflow, but +quite different from the <abbr title="out-of-the-box">OOTB</abbr> experience. + +`grimshot` is a shell script which ties together many third-party programs +(grim, slurp, wl-copy, jq, and notify-send) to make a convenient way of taking +screenshots. Adding this upstream would introduce *a lot* of third-party +dependencies for a minor convenience. This tool has had a bit more effort put +into it: notice that a man page is provided as well. Because the contrib +directory does not participate in the upstream build system, the contributor has +also added a pre-compiled man page so that you can skip this step when +installing it on your system. + +Last, we have `inactive-windows-transparency.py`, which is a script for making +all windows other than your focused one semi-transparent. Some people may want +this, but again, it's not really something we'd consider appropriate for the +OOTB experience. Perfect for contrib! diff --git a/content/blog/Analyzing-HN.md b/content/blog/Analyzing-HN.md @@ -0,0 +1,299 @@ +--- +date: 2017-09-13 +layout: post +title: "Analyzing HN moderation & censorship" +tags: [hacker news] +--- + +[Hacker News](https://news.ycombinator.com) is a popular +"[hacker](http://www.catb.org/jargon/html/H/hacker.html)" news board. One thing +I love about HN is that the moderation generally does an excellent job. The site +is free of spam and the conversations are usually respectful and meaningful (if +pessimistic at times). However, there is always room for improvement, and +moderation on Hacker News is no exception. + +**Notice**: on 2017-10-19 this article was updated to incorporate feedback the +Hacker News moderators sent to me to clarify some of the points herein. You may +view a diff of these changes +[here](https://github.com/SirCmpwn/sircmpwn.github.io/commit/553d051c84a4631c3bd3264a437dfbc6c9807d13). + +For some time now, I've been scraping the HN API and website to learn how the +moderators work, and to gather some interesting statistics about posts there +in general. Every 5 minutes, I take a sample of the front page, and every 30 +minutes, I sample the top 500 posts (note that HN may return fewer than this +number). During each sample, I record the ID, author, title, URL, status +(dead/flagged/dupe/alive), score, number of comments, rank, and compute the rank +based on [HN's published algorithm](https://news.ycombinator.com/item?id=231209). +A note is made when the title, URL, or status changes. + +[![](https://sr.ht/IFCA.png)](https://hn.0x2237.club/post/15217697) + +The information gathered is publicly available at +[hn.0x2237.club](https://hn.0x2237.club) (sorry about the stupid domain, I just +picked one at random). You can search for most posts here going back to +2017-04-14, as well as view recent +[title](https://hn.0x2237.club/title-changes) and +[url](https://hn.0x2237.club/url-changes) changes or [deleted +posts](https://hn.0x2237.club/deleted) +([score>10](https://hn.0x2237.club/deleted-10)). Raw data is available as JSON +for any post at `https://hn.0x2237.club/post/:id/json`. Feel free to explore the +site later, or [its shitty code](https://git.sr.ht/~sircmpwn/hnstats). For now, +let's dive into what I've learned from this data. + +### Tools HN mods use + +The main tools I'm aware of that HN moderators can use to perform their duties +are: + +- Editing link titles or URLs +- Influencing story rank via "downweighting" or "burying" +- Deleting or "killing" posts +- Detaching off-topic or rulebreaking comment threads from their parents +- <abbr title="Banning them without making it known to them">Shadowbanning</abbr> + misbehaving users +- Banning misbehaving users (and telling them) + +The moderators emphasize a difference between deleting a post and killing a +post. The former, deleting a post, will remove it from all public view like it +had never existed, and is a tool used infrequently. Killing a post will mark it +as [dead] so it doesn't show up on the post listing. + +Influencing a post's rank can also be done through several means of varying +severity. "Burying" a post will leave a post alive, but plunge it in rank. +"Downweighting" is similar, but does not push its rank as far. + +There are also automated tools for detecting spam and <abbr title="Posts +influenced by a group of early voters hoping to get it on the front page">voting +rings</abbr>, as well as automated de-emphasizing of posts based on certain +<abbr title="'Bitcoin' was known to at some point be one of these">secret +keywords</abbr> and controls to prevent flamewars. Automated tools on Hacker +News are used to downweight or kill posts, but never to bury or delete them. +Dan spoke about these tools and their usage for me: + +>Of these four interventions (deleting, killing, burying, and downweighting), +>the only one that moderators do frequently is downweighting. We downweight +>posts in response to things that go against the site guidelines, such as when a +>submission is unsubstantive, baity or sensational. Typically such posts remain +>on the front page, just at a lower rank. We bury posts when they're dupes, +>but rarely otherwise. We kill posts when they're spam, but rarely +>otherwise. [...] We never delete a post unless the author asks us to. + +Dan also further clarified the difference between dead and deleted for me: + +>The distinction between 'dead' and 'deleted' is important. Dead posts +>are different from deleted ones in that people can still see them if +>they set 'showdead' to 'yes' in their profile. That way, users who +>want a less moderated view can still see everything that has been +>killed by moderators or software or user flags. Deleted posts, on the +>other hand, are erased from the record and never seen again. On HN, +>authors can delete their own posts for a couple hours (unless they are +>comments that have replies). After that, if they want a post deleted +>they can ask us and we usually are happy to oblige. + +Moderators can also artificially influence rank upwards - one way is by inviting +the user to re-submit a post that they want to give another shot at the front +page. This gives the post a healthy upvote to begin with and prevents it from +being flagged. The moderators invited me to re-submit this very article using +this mechanism on 2017-10-19. + +Banning users is another mechanism that they can use. There are two ways bans +are typically applied around the net - telling users they've been banned, and +keeping it quiet. The latter - shadowbanning - is a useful tool against spammers +and serial ban evaders who might otherwise try to circumvent their ban. However, +it's important that this does *not* become the first line of defense against +rulebreaking users, who should instead be informed of the reason for their ban +so they have a chance to reform and appeal it. Here's what Dan has to say about +it: + +>Shadowbanning has proven to still be useful for spammers and trolls +>(i.e. when a new account shows up and is clearly breaking the site +>guidelines off the bat). Most such abuse is by a relatively small +>number of users who create accounts over and over again to do the same +>things. When there's evidence that we've repeatedly banned someone +>before, I don't feel obliged to tell them we're banning them again. +>[...] When we're banning an established account, though, we post a comment +>saying so, and nearly always only after warning that user beforehand. Many such +>users had no idea they were breaking the site guidelines and are +>quite happy to improve their posts, which is a win for everyone. + +Dan also shared a link to search for comments where moderators have explained +to users why they've been banned. Of course, this doesn't include users who were +banned without explanation, or that use slightly different language: + +[dang's bans](https://hn.algolia.com/?query=by:dang%20we%20banned&sort=byDate&dateRange=all&type=comment&storyText=false&prefix&page=0) + +[sctb's bans](https://hn.algolia.com/?query=by:sctb%20we%20banned&sort=byDate&dateRange=all&type=comment&storyText=false&prefix=false&page=0) + +## Data-based insights + +Here's an example of a fairly common moderator action: + +![](https://sr.ht/PhJM.png) + +[This post](https://hn.0x2237.club/post/15217697) had its title changed at +around 09-11-17 12:10 UTC, and had the rank artificially adjusted to push it +further down the front page. We can tell that the drop was artificial just by +correlating it with the known moderator action, but we can also compare it +against the computed base rank: + +![](https://sr.ht/IJQI.png) + +Note however that the base rank is often wildly different from the rank observed +in practice; the factors that go into adjusting it are rather complex. We can +also see that despite the action, the post's score continued to increase, even +at an accelerated pace: + +![](https://sr.ht/FmNU.png) + +This "title change and derank" is a fairly common action - here are some more +examples from the past few days: + +[Betting on the Web - Why I Build PWAs](https://hn.0x2237.club/post/15219154) + +[Silicon Valley is erasing individuality](https://hn.0x2237.club/post/15210767) + +[Chinese government is working on a timetable to end sales of fossil-fuel cars](https://hn.0x2237.club/post/15208565) + +Users can change their own post titles, which I'm unable to distinguish from +moderator changes. However, correlating them with a strange change in rank is +generally a good bet. Submitters also generally will edit their titles earlier +rather than later, so a later change may indicate that it was seen by a +moderator after it rose some distance up the page. + +I also occasionally find what seems to be the opposite - artificially bumping a +post further up the page. Here's two examples: +[15213371](https://hn.0x2237.club/post/15213371) and +[15209377](https://hn.0x2237.club/post/15209377). Rank influencing in either +direction also happens without an associated title or URL change, but +automatically pinning such events down is a bit more subtle than my tools can +currently handle. + +Moderators can also delete a post or indicate it as a dupe. The latter can be +(and is) detected by my tools, but the former is indistinguishable from the user +opting to delete posts themselves. In theory, posts that are deleted *after* the +author is no longer allowed to could be detected, but this happens rarely and my +tools don't track posts once they get old enough. + +### Flagging + +The users have some moderation tools at their disposal, too - downvotes, +flagging, and vouching. When a comment is downvoted, it is moved towards the +bottom of the thread and is gradually colored grayer to become less visible, and +can be reversed with upvotes. When a comment gets enough flags, it is removed +entirely unless you have showdead enabled in your profile. Flagged posts are +downweighted or killed when enough flags accumulate. These posts are moved to +the bottom of the ranked posts even if you have showdead enabled, and can also +be seen in /new. Flagging can be reversed with the vouch feature, but flagged +stories are almost never vouched back into existence. + +**Note**: detection of post flagged status is very buggy with my tools. The API +exposes a boolean for dead posts, so I have to fall back on scraping to +distinguish between different kinds of dead-ness. But this is pretty buggy, so I +encourage you to examine the post yourself when browsing my site if in doubt. + +### Are these tools abused for censorship? + +Well, with all of this data, was I able to find evidence of censorship? There +are two answers: yes and maybe. The "yes" is because users are *definitely* +abusing the flagging feature. The "maybe" is because moderator action leaves +room for interpretation. I'll get to that later, but let's start with flagging +abuse. + +#### Censorship by users + +The threshold for removing a story due to flags is rather low, though I don't +know the exact number. Here are some posts whose flags I consider questionable: + +[Harvey, the Storm That Humans Helped Cause](https://hn.0x2237.club/post/15129859) (23 points) + +[ES6 imports syntax considered harmful](https://hn.0x2237.club/post/15116132) (12 points) + +[White-Owned Restaurants Shamed for Serving Ethnic Food](https://hn.0x2237.club/post/14415411) (33 points) + +[The evidence is piling up – Silicon Valley is being destroyed](https://hn.0x2237.club/post/14152602) (27 points) + +A good place to discover these sorts of events is to browse hnstats for posts +deleted with a score [>10 points](https://hn.0x2237.club/deleted-10). There are +also occasions where the flags seem to be due to a poor title, which is a +fixable problem for which flagging is a harsh solution: + +[Poettering downvoted 5 (at time of this writing) times](https://hn.0x2237.club/post/14679207) + +[Germany passes law restricting free speech on the internet](https://hn.0x2237.club/post/14676296) + +The main issue with flags is that they're often used as an alternative to the +HN's (by design) lack of a downvoting feature. HN also gives users no guidelines +on *why* they should flag posts, which mixes poorly with automated removal of a +post given enough flags. + +#### Censorship by moderators + +Moderator actions are a bit more difficult to judge. Moderation on HN is a black +box - most of the time, moderators don't make the reasoning behind their actions +clear. Many of their actions (such as rank influence) are also subtle and easy +to miss. Thankfully they are often receptive to being asked why some moderation +occurred, but only as often as not. + +Anecdotally, I also find that moderators occasionally moderate selectively, and +keep quiet in the face of users asking them why. Notably this is a problem for +<abbr title="links for which you have to pay money to read the +content">paywalled</abbr> articles, which are [against the +rules](https://news.ycombinator.com/newsfaq.html) but are often allowed to +remain. + +Dan sent me a response to this section: + +>[It's true that we don't explain our actions], but mostly because it would be +>hopeless to try. We could do that all day and still not make everything clear, +>because the quantity is overwhelming and the cost of a high-quality explanation +>is steep. Moreover the experiment would be impossible to run because one +>would die of boredom long before reaching 100%. Our solution to this +>conundrum is not to try to explain everything but to answer specific +>questions as best we can. We don't answer every question, but that's +>mostly because we don't see every question. If people ask us things on +>HN itself, odds are we won't see it (also, the site guidelines ask +>users not to do this, per ([our +>guidelines](https://news.ycombinator.com/newsguidelines.html)). If they +>[email us](mailto:hn@ycombinator.com), the probability of a +>response approaches 1. + +I can attest personally to success reaching out to hn@ycombinator.com for +clarification and even reversal of some moderator decisions, though at a +response ratio further from 1 than this implies. That being said, I don't think +that private discourse between the submitter and the moderators is the only +solution. Other people may be invested in the topic, too - users who upvoted the +story might not notice its disappearance, but would like more attention drawn to +the topic and enjoy more discussion. Commenters are even more invested in the +posts. The submitter is not the only one whoses interests are at stake. This is +even more of a problem for posts which are moderated via user flags - the HN +mods are pretty discretionate but users are much less so. + +Explaining every action is not necessary - I don't think anyone needs you to +explain why someone was banned when they were submitting links to earn money at +home in your spare time. However, I think a public audit log of moderator +actions would go a long way, and could be done by software - avoiding the need +to explain everything. I envision a change to your UI for banning users or +moderating posts with that adds a dropdown of common reasons and a textbox for +further elaboration when appropriate - then makes this information appear on +/moderation. + +### Conclusions + +I should again emphasize that most moderator actions are benign and agreeable. +They do a great job on the whole, but striving to do even better would be +admirable. I suggest a few changes: + +- Make a public audit log of moderation activity, or at least reach out to me to + see what small changes could be done to help improve my statistics gathering. +- Minimize use of more subtle actions like rank influence, and when used, +- More frequently leave comments on posts where moderation has occurred + explaining the rationale and opening an avenue for public discussion and/or + appeal. +- Put flagged posts into a queue for moderator review and don't remove posts + simply because they're flagged. +- Consider appointing one or two moderators from the community, ideally people + with less bias towards SV or startup culture. + +Hacker News is a great place for just that - hacker news. It has been for a long +time and I hope it continues to be. Let's work together on running it +transparently to the benefit of all. diff --git a/content/blog/Anatomy-of-a-shell.md b/content/blog/Anatomy-of-a-shell.md @@ -0,0 +1,131 @@ +--- +date: 2018-12-28 +layout: post +title: Anatomy of a shell +tags: ["shell"] +--- + +I've been contributing where I can to Simon Ser's [mrsh][mrsh] project, a +work-in-progress strictly POSIX shell implementation. I worked on some small +mrsh features during my holiday travels and it's in the forefront of my mind, so +I'd like to share some of its design details with you. + +[mrsh]: https://git.sr.ht/~emersion/mrsh + +There are two main components to a shell: parsing and execution. mrsh uses a +simple [recursive descent parser][rd-parser] to generate an AST (Abstract Syntax +Tree, or an in-memory model of the structure of the parsed source). This design +was chosen to simplify the code and avoid dependencies like flex/bison, and is a +good choice given that performance isn't critical for parsing shell scripts. +Here's an example of the input source and output AST: + +[rd-parser]: https://en.wikipedia.org/wiki/Recursive_descent_parser + +```sh +#!/bin/sh +say_hello() { + echo "hello $1!" +} + +who=$(whoami) +say_hello "$who" +``` + +This script is parsed into this AST (this is the output of `mrsh -n test.sh`): + +``` +program +program +└─command_list ─ pipeline + └─function_definition say_hello ─ brace_group + └─command_list ─ pipeline + └─simple_command + ├─name ─ word_string [3:2 → 3:6] echo + └─argument 1 ─ word_list (quoted) + ├─word_string [3:8 → 3:14] hello + ├─word_parameter + │ └─name 1 + └─word_string [3:16 → 3:17] ! +program +program +└─command_list ─ pipeline + └─simple_command + └─assignment + ├─name who + └─value ─ word_command ─ program + └─command_list ─ pipeline + └─simple_command + └─name ─ word_string [6:7 → 6:13] whoami +program +└─command_list ─ pipeline + └─simple_command + ├─name ─ word_string [7:1 → 7:10] say_hello + └─argument 1 ─ word_list (quoted) + └─word_parameter + └─name who +``` + +Most of these names come directly from the [POSIX shell specification][spec]. +The parser and AST is made available as a standalone public interface of +libmrsh, which can be used for a variety of use-cases like syntax-aware text +editors, syntax highlighting (see [`highlight.c`][hl.c]), linters, etc. The most +important use-case is, of course, task planning and execution. + +[spec]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html +[hl.c]: https://git.sr.ht/~emersion/mrsh/tree/master/highlight.c + +Most of these AST nodes becomes a *task*. A task defines an implementation of +the following interface: + +```c +struct task_interface { + /** + * Request a status update from the task. This starts or continues it. + * `poll` must return without blocking with the current task's status: + * + * - TASK_STATUS_WAIT in case the task is pending + * - TASK_STATUS_ERROR in case a fatal error occured + * - A positive (or null) code in case the task finished + * + * `poll` will be called over and over until the task goes out of the + * TASK_STATUS_WAIT state. Once the task is no longer in progress, the + * returned state is cached and `poll` won't be called anymore. + */ + int (*poll)(struct task *task, struct context *ctx); + void (*destroy)(struct task *task); +}; +``` + +Most of the time the task will just do its thing. Many tasks will have sub-tasks +as well, such as a command list executing a list of commands, or each branch of +an if statement, which it can defer to with `task_poll`. Many tasks will wait on +an external process, in which case it can return TASK_STATUS_WAIT to have the +process `wait`ed on. Feel free to browse the [full list of tasks][tasks] to get +an idea. + +[tasks]: https://git.sr.ht/~emersion/mrsh/tree/master/shell/task + +One concern more specific to POSIX shells is built-in commands. Some commands +have to be built-in because they manipulate the shell's state, such as `.` and +`cd`. Others, like `true` & `false`, are there for performance reasons, since +they're simple and easily implemented internally. POSIX specifies [a list of +special builtins][builtins] which are necessary to implement in the shell +itself. There's [a second list][utilities] that must be present for the shell +environment to be considered POSIX compatible (plus some reserved names like +`local` and `pushd` that invoke undefined behavior - mrsh aborts on these). + +[builtins]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_14 +[utilities]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01 + +Here are some links to more interesting parts of the code so you can explore on +your own: + +- [Redirection](https://git.sr.ht/~emersion/mrsh/tree/master/shell/redir.c) & [pipelines](https://git.sr.ht/~emersion/mrsh/tree/master/shell/task/pipeline.c) +- [Function definition](https://git.sr.ht/~emersion/mrsh/tree/master/shell/task/function_definition.c) & [execution](https://git.sr.ht/~emersion/mrsh/tree/master/shell/task/command_function.c) +- [The . builtin](https://git.sr.ht/~emersion/mrsh/tree/master/builtin/dot.c) +- [main.c and the REPL](https://git.sr.ht/~emersion/mrsh/tree/master/main.c) + +I might write more articles in the future diving into specific concepts, feel +free to shoot me an email if you have suggestions. Shoutout to Simon for +building such a cool project! I'm looking forward to contributing more until we +have a really nice strictly POSIX shell. diff --git a/content/blog/Announcing-aerc-0.1.0.md b/content/blog/Announcing-aerc-0.1.0.md @@ -0,0 +1,52 @@ +--- +date: 2019-06-03 +layout: post +title: "Initial pre-release of aerc: an email client for your terminal" +tags: ["aerc", "announcement"] +--- + +After years of painfully slow development, the [aerc email +client](https://aerc-mail.org) has seen a huge boost in its pace of development +recently. This leads to today's announcement: [aerc 0.1.0 is now +available][tag]! After my transition to [working on free software full +time][full-time-foss] allowed me to spend more time on more projects, I was able +to invest considerably more time into aerc. Your support led us here: thank you +to all of the people who [donate to my work][donate]! + +[full-time-foss]: https://drewdevault.com/2019/01/15/Im-doing-FOSS-full-time.html +[tag]: https://git.sr.ht/~sircmpwn/aerc/refs/0.1.0 +[donate]: https://drewdevault.com/donate + +I've prepared a short webcast demonstrating aerc's basic features - give it a +watch if you're curious about what aerc looks like & what makes it interesting. + +<video controls> + <source src="https://yukari.sr.ht/aerc-intro.webm"></source> + <source src="https://yukari.sr.ht/aerc-intro.mp4"></source> + A video would be shown here, but your web browser does not support it. +</video> + +In summary, aerc is an email client which runs in your terminal emulator. If +you're coming from mutt, you'll appreciate its more efficient & reliable +networking, a keybinding system closer to vims, and embedded terminal emulator +allowing you to compose emails and read new ones at the same time. It builds on +this foundation with a lot of new and exciting features. For example, its +"filter" feature allows us to review patches with syntax highlighting: + +![Screenshot of aerc displaying a patch](https://sr.ht/JoqH.png) + +The embedded terminal emulator also allows us convenient access to nearby git +repositories for running tests against incoming patches, pushing the changes +once accepted, or anything else you might want to do. Want to run +[Weechat](https://weechat.org/) in an aerc tab? Just like that, aerc has a chat +client! Writing emails in vim, manipulating git & hg repositories, playing +nethack to kill some time... all stuff you never realized your email client was +missing. + +I plan on extending aerc in the future with more integrations with version +control systems, calendar & contacts support, and more email configurations like +notmuch and JMAP. Please consider +[contributing](https://git.sr.ht/~sircmpwn/aerc) if you're interested in writing +a little Go, or [donating monthly](https://drewdevault.com/donate) to ensure I +always have time to work on this and other free software projects. Give aerc a +try and let me know what you think! diff --git a/content/blog/Announcing-annotations-for-sourcehut.md b/content/blog/Announcing-annotations-for-sourcehut.md @@ -0,0 +1,231 @@ +--- +date: 2019-07-08 +layout: post +title: Announcing code annotations for SourceHut +tags: ["announcement", "sourcehut"] +--- + +Today I'm happy to announce that code annotations are now available for +[SourceHut](https://sourcehut.org)! <img style="display: inline; height: 1.2rem" +src="/img/party.png" /> These allow you to decorate your code with arbitrary +links and markdown. The end result looks something like this: + +![](https://sr.ht/w767.png) + +<small class="text-muted"> + <a href="https://sourcehut.org">SourceHut</a> is the "hacker's forge", a + 100% open-source platform for hosting Git &amp; Mercurial repos, bug trackers, + mailing lists, continuous integration, and more. No JavaScript required! +</small> + +The annotations shown here are sourced from a JSON file which you can generate +and upload during your CI process. It looks something like this: + +```json +{ + "98bc0394a2f15171fb113acb5a9286a7454f22e7": [ + { + "type": "markdown", + "lineno": 33, + "title": "1 reference", + "content": "- [../main.c:123](https://example.org)" + }, + { + "type": "link", + "lineno": 38, + "colno": 7, + "len": 15, + "to": "#L6" + }, + ... +``` + +You can probably infer from this that annotations are very powerful. Not only +can you annotate your code's semantic elements to your heart's content, but you +can also do exotic things we haven't thought of yet, for every programming +language you can find a parser for. + +I'll be going into some detail on the thought process that went into this +feature's design and implementation in a moment, but if you're just excited and +want to try it out, here are a few interesting annotated repos to browse: + +- [~sircmpwn/scdoc][scdoc]: man page generator (C) +- [~sircmpwn/aerc][aerc]: TUI email client (Go) +- [~mcf/cproc][cproc]: C compiler (C) + +[scdoc]: https://git.sr.ht/~sircmpwn/scdoc/tree/master/src/main.c +[aerc]: https://git.sr.ht/~sircmpwn/aerc/tree/master/widgets/msgviewer.go +[cproc]: https://git.sr.ht/~mcf/cproc/tree/master/scan.c + +And here are the docs for generating your own: [annotations on +git.sr.ht](https://man.sr.ht/git.sr.ht/annotations.md). Currently annotators are +available for C and Go, and I intend to write another for Python. For the rest, +I'll be relying on the community to put together annotators for their favorite +programming languages, and to help me expand on the ones I've built. + +## Design + +A lot of design thought went into this feature, but I knew one thing from the +outset: I wanted to make a generic system that users could use to annotate their +source code in any manner they chose. My friend Andrew Kelley (of +[Zig](https://ziglang.org/) fame) once expressed to me his frustration with +GitHub's refusal to implement syntax highlighting for "small" languages, citing +a shortage of manpower. It's for this reason that it's important to me that +SourceHut's open-source platform allows users large and small to volunteer to +build the perfect integration for their needs - I don't scale alone[^1]. + +[^1]: For the syntax highlighting problem, by the way, this is accomplished by using Pygments. Improvements to Pygments reach not only SourceHut, but a large community of projects, making the software ecosystem better for everyone. + +To get a head start for the most common use-cases - scanning source files and +linking references and definitions together - the best approach was unclear. I +spent a lot of time studying [ctags](http://ctags.sourceforge.net/), for +example, which supports a huge set of programming languages, but unfortunately +only finds definitions. I thought about combining this with another approach for +finding references, but the only generic library with lots of parsers I'm aware +of is [Pygments](http://pygments.org/), and I didn't necessarily want to bring +Python into every user's CI process if they weren't already using it. That +approach would also make it more difficult to customize the annotations for each +language. Other options I considered were +[cscope](http://cscope.sourceforge.net/) and +[gtags](https://www.gnu.org/software/global/), but the former doesn't have many +programming languages supported (making the tradeoff questionable), and the +latter just uses Pygments anyway. + +So I decided: I'm going to write my own annotators for each language. Or at +least the languages I use the most: + +- C, because I like it but also because + [scdoc](https://git.sr.ht/~sircmpwn/scdoc) is the demo repo shown on the + [SourceHut marketing page](https://sourcehut.org). +- Python, because SourceHut is largely written in Python and using it to browse + itself would be cool. +- Go, because parts of SourceHut are written in it but also because I use it a + lot for [my own projects](https://git.sr.ht/~sircmpwn/aerc). I also knew that + Go had at least *some* first-class support for working with its AST - and boy + was I in for a surprise. + +With these initial languages decided, let's turn to the implementations. + +## Annotating C code + +I began with the C annotator, because I knew it would be the most difficult. +There does not exist any widely available standalone C parsing library to +provide C programs with access to an AST. There's LLVM, but I have a deeply held +belief that programming language compiler and introspection tooling should be +implemented in the language itself. So, I set about to write a C parser from +scratch. + +Or, almost from scratch. There exist two standard POSIX tools for writing +compilers with: [lex][lex] and [yacc][yacc], which are respectively a lexer +generator and a compiler compiler. Additionally, there are [pre-fab lex and +yacc files](http://www.quut.com/c/ANSI-C-grammar-y.html) which *mostly* +implement the C11 standard grammar. However, C is [not a context-free +language][context], so additional work was necessary to track typedefs and use +them to change future tokens emitted by the scanner. A little more work was also +necessary for keeping track of line and column numbers in the lexer. Overall, +however, this was relatively easy, and in less than a day's work I had a fully +functional C11 parser. + +[lex]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/lex.html +[yacc]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/yacc.html +[context]: https://eli.thegreenplace.net/2007/11/24/the-context-sensitivity-of-cs-grammar/ + +However, my celebration was short-lived as I started to feed my parser C +programs from the wild. The GNU C Compiler, GCC, implements many C extensions, +and their use, while inadvisable, is extremely common. Not least of the +offenders is glibc, and thus running my parser on any system with glibc headers +installed would likely immediately run into syntax errors. GCC's extensions are +not documented in the form of an addendum to the C specification, but rather as +end-user documentation and a 15 million lines-of-code compiler for you to +reverse engineer. It took me almost a week of frustration to get a parser which +worked passably on a large subset of the C programs found in the wild, and I +imagine I'll be dealing with GNU problems for years to come. Please don't use C +extensions, folks. + +In any case, the result now works fairly well for a lot of programs, and I have +plans on expanding it to integrate more nicely with build systems like meson. +Check out the code here: [annotatec](https://git.sr.ht/~sircmpwn/annotatec). The +features of the C annotator include: + +- Annotating function definitions with a list of files/linenos which call them +- Linking function calls to the definition of that function + +In the future I intend to add support for linking to external symbols as well - +for example, linking to the POSIX spec for functions specified by POSIX, or to +the Linux man pages for Linux calls. It would also be pretty cool to support +linking between related projects, so that wlroots calls in sway can be linked to +their declarations in the wlroots repo. + +## Annotating Go code + +The Go annotator was far easier. I started over my morning cup of coffee today +and I was finished with the basics by lunch. Go has a bunch of support in the +standard library for parsing and analyzing Go programs - I was very impressed: + +- [go/ast](https://golang.org/pkg/go/ast/) +- [go/scanner](https://golang.org/pkg/go/scanner/) +- [go/token](https://golang.org/pkg/go/token/) +- [go/types](https://golang.org/pkg/go/types/) + +To support Go 1.12's go modules, the experimental (but good enough) +[packages](https://godoc.org/golang.org/x/tools/go/packages) module is available +as well. All of this is nicely summarized by a lovely document in the [golang +examples repository](https://github.com/golang/example/tree/master/gotypes). The +type checker is also available as a library, something which is less common even +among languages with parsers-as-libraries, and allows for many features which +would be very difficult without it. Nice work, Go! + +The [resulting annotator](https://git.sr.ht/~sircmpwn/annotatego) clocks in at +just over 250 lines of code - compare that to the C annotator's ~1,300 lines of +C, lex, and yacc source code. The Go annotator is more featureful, too, it can: + +- Link function calls to their definitions, and in reverse +- Link method calls to their definitions, and in reverse +- Link variables to their definitions, even in other files +- Link to godoc for symbols defined in external packages + +I expect a lot more to be possible in the future. It might get noisy if you turn +everything on, so each annotation type is gated behind a command line flag. + +## Displaying annotations + +Displaying these annotations required a bit more effort than I would have liked, +but the end result is fairly clean and reusable. Since SourceHut uses Pygments +for syntax highlighting, I ended up writing a [custom +Formatter](http://pygments.org/docs/formatterdevelopment/) based on the existing +Pygments HtmlFormatter. The result is the [AnnotationFormatter][git.sr.ht +formatter], which splices annotations into the highlighted code. One downside of +this approach is that it works at the token level - a more sophisticated +implementation will be necessary for annotations that span more than a single +token. Annotations are fairly expensive to render, so the rendered HTML is +stowed in Redis. + +[git.sr.ht formatter]: https://git.sr.ht/~sircmpwn/git.sr.ht/tree/master/gitsrht/annotations.py + +## The future? + +I intend to write a Python annotator soon, and I'll be relying on the community +to build more. If you're looking for a fun weekend hack and a chance to learn +more about your favorite programming language, this'd be a great project. The +format for annotations on SourceHut is also pretty generalizable, so I encourage +other code forges to reuse it so that our annotators are useful on every code +hosting platform. + +builds.sr.ht will also soon grow first-class support for making these annotators +available to your build process, as well as for making an OAuth token available +(ideally with a limited set of permissions) to your build environment. Rigging +up an annotator is a bit involved today ([though the docs +help](https://man.sr.ht/git.sr.ht/annotations.md)), and streamlining that +process will be pretty helpful. Additionally, this feature is only available for +git.sr.ht, though it should generalize to hg.sr.ht fairly easily and I hope +we'll see it available there soon. + +I'm also looking forward to seeing more novel use-cases for annotation. Can we +indicate code coverage by coloring a gutter alongside each line of code? Can we +link references to ticket numbers in the comments to your bug tracker? If you +have any cool ideas, I'm all ears. Here's that list of cool annotated repos to +browse again, if you made it this far and want to check them out: + +- [~sircmpwn/scdoc][scdoc]: man page generator (C) +- [~sircmpwn/aerc][aerc]: TUI email client (Go) +- [~mcf/cproc][cproc]: C compiler (C) diff --git a/content/blog/Announcing-first-class-hg-support-on-sourcehut.md b/content/blog/Announcing-first-class-hg-support-on-sourcehut.md @@ -0,0 +1,43 @@ +--- +date: 2019-04-15 +layout: post +title: Announcing first-class Mercurial support on Sourcehut +tags: ["sourcehut", "mercurial"] +--- + +I'm pleased to announce that the final pieces have fallen into place for +[Mercurial](https://www.mercurial-scm.org/) support on +[SourceHut](https://sourcehut.org), which is now on-par with our git offering. +Special thanks are owed to SourceHut contributor Ludovic Chabant, who has been +instrumental in adding Mercurial support to SourceHut. You may have heard about +it while this was still experimental - but I'm happy to tell you that we have +now completely integrated Mercurial support into SourceHut! Want to try it out? +Check out [the tutorial](https://man.sr.ht/tutorials/set-up-account-and-hg.md). + +Mercurial support on SourceHut includes all of the trimmings, including CI +support via [builds.sr.ht](https://builds.sr.ht) and email-driven collaboration +on [lists.sr.ht](https://lists.sr.ht). Of course, it's also 100% +free-as-in-freedom, open source software ([hosted on +itself](https://hg.sr.ht/~sircmpwn/hg.sr.ht)) that you can [deploy on your own +servers](https://man.sr.ht/hg.sr.ht/installation.md). We've tested hg.sr.ht +on some of the largest Mercurial repositories out there, including +mozilla-central and NetBSD src. The NetBSD project in particular has been very +helpful, walking us through their CVS to Hg conversion and stress-testing +hg.sr.ht with the resulting giant repositories. I'm looking forward to working +more with them in the future! + +The Mercurial community is actively innovating their software, and we'll be +right behind them. I'm excited to provide a platform for elevating the Mercurial +community. There weren't a lot of good options for Mercurial fans before +SourceHut. Let's fix that together! SourceHut will be taking a more active role +in the Hg community, just like we have for git, and together we'll build a great +platform for software development. + +I'll see you in Paris in May, at the [inaugural Mercurial +conference](https://www.mercurial-scm.org/pipermail/mercurial/2019-April/051196.html)! + +--- + +Hg support on SourceHut was largely written by members of the Mercurial +community. If there are other version control communities interested in +SourceHut support, please [reach out](mailto:~sircmpwn/sr.ht-dev@lists.sr.ht)! diff --git a/content/blog/Announcing-wio.md b/content/blog/Announcing-wio.md @@ -0,0 +1,56 @@ +--- +date: 2019-05-01 +layout: post +title: "Announcing Wio: A clone of Plan 9's Rio for Wayland" +tags: ["wayland", "wio", "plan 9"] +--- + +For a few hours here and there over the past few months, I've been working on a +side project: [Wio](https://wio-project.org). I'll just let the (3 minute) +screencast do the talking first: + +<video src="https://yukari.sr.ht/wio.webm" controls></video> + +**Note**: this video begins with several seconds of grey video. This is normal. + +In short, Wio is a Wayland compositor based on wlroots which has a similar look +and feel to Plan 9's Rio desktop. It works by running each application in its +own nested Wayland compositor, based on [Cage][cage] - yet another wlroots-based +Wayland compositor. I used Cage in [last week's RDP article][rdp-article], but +here's another cool use-case for it. + +[rdp-article]: https://drewdevault.com/2019/04/23/Using-cage-for-a-seamless-RDP-Wayland-desktop.html +[cage]: https://www.hjdskes.nl/projects/cage/ + +The behavior this allows for (each window taking over its parent's window, +rather than spawning a new window) has been something I wanted to demonstrate on +Wayland for a very long time. This is a good demonstration of how Wayland's +fundamentally different and conservative design allows for some interesting +use-cases which aren't possible at all on X11. + +I've also given Wio some nice features which are easy thanks to wlroots, but +difficult on Plan 9 without kernel hacking. Namely, these are multihead support, +HiDPI support, and support for the wlroots layer shell protocol. Several other +wlroots protocols were invited to the party, useful for taking screenshots, +redshift, and so on. Layer shell support is particularly cool, since programs +like swaybg and waybar work on Wio. + +In terms of Rio compatability, Wio has a ways to go. I would seriously +appreciate help from users who are interested in improving Wio. Some notably +missing features include: + +- Any kind of filesystem resembling Rio's window management filesystem. In + theory this ought to be do-able with FUSE, at least in part (/dev/text might + be tough). +- Running every application in its own namespace, for double the Plan 9 +- Hiding/showing windows (that menu entry is dead) +- Joint improvements with Cage to bring greater support for Wayland features, + like client-side window resize/move, fullscreen windows, etc +- Damage tracking to avoid re-rendering everything on every frame, saving + battery life and GPU time + +If you're interested in helping, please join the IRC channel and say hello: +[#wio on irc.freenode.net][webchat]. For Wio's source code and other +information, visit the website at [wio-project.org](https://wio-project.org). + +[webchat]: http://webchat.freenode.net/?channels=%23wio&uio=MTA9dHJ1ZSYxMT0xNzQmMTM9ZmFsc2U4c diff --git a/content/blog/Anti-AGPL-propaganda.md b/content/blog/Anti-AGPL-propaganda.md @@ -0,0 +1,91 @@ +--- +date: 2020-07-27 +layout: post +title: The falsehoods of anti-AGPL propaganda +tags: ["free software"] +--- + +Google is well-known for [forbidding the use of][google agpl policy] software +using the [GNU Affero General Public License][AGPL], commonly known as "AGPL". +Google is also well-known for being the subject of cargo-culting by fad +startups. Unfortunately, this means that they are susceptible to what is +ultimately anti-AGPL propaganda from Google, with little to no basis in fact. + +*Obligatory: I'm not a lawyer; this is for informational purposes only.* + +[google agpl policy]: https://opensource.google/docs/using/agpl-policy/ +[AGPL]: https://www.gnu.org/licenses/agpl-3.0.en.html + +In truth, the terms of the AGPL are pretty easy to comply with. The basic +obligations of the AGPL which set it apart from other licenses are as follows: + +- Any derivative works of AGPL-licensed software must also use the AGPL. +- Any users of such software are entitled to the source code under the terms of + the AGPL, including users accessing it over the network such as with their web + browser or via an API or internet protocol. + +If you're using AGPL-licensed software like a database engine or [my own +AGPL-licensed works][sourcehut], and you haven't made any changes to the source +code, all you have to do is provide a link to the upstream source code +somewhere, and if users ask for it, direct them there. If you *have* modified +the software, you simply have to publish your modifications. The easiest way to +do this is to send it as a patch upstream, but you could use something as simple +as providing a tarball to your users. + +[sourcehut]: https://sr.ht/~sircmpwn/sourcehut/ + +The nuances are detailed and cover many edge cases to prevent abuse. But in +general, just publish your modifications under the same AGPL terms and you'll +be good to go. The license is usually present in the source code as a `COPYING` +or `LICENSE` file, so if you just tar up your modified source code and drop a +link on your website, that's good enough. If you want to go the extra mile and +express your gratitude to the original software developers, consider submitting +your changes for upstream inclusion. Generally, the feedback you'll receive will +help to make your changes better for your use-case, too; and submitting your +work upstream will prevent your copy from diverging from upstream. + +That's pretty easy, right? I'm positive that your business has to deal with much +more onerous contracts than the AGPL. Then why does Google make a fuss about it? + +[The Google page about the AGPL][google agpl policy] details inaccurate (but +common[^1]) misconceptions about the obligations of the AGPL that don't follow +from the text. Google states that if, for example, Google Maps used PostGIS as +its data store, and PostGIS used the AGPL, Google would be required to release +the Google Maps code. This is not true. They would be required to release *their +PostGIS patches* in this situation. AGPL does not extend the GPL in that it +makes the Internet count as a form of linking which creates a derivative work, +as Google implies, but rather that it makes anyone who uses the software via +the Internet entitled to its source code. It does not update the "what counts +as a 'derivative work'" algorithm, so to speak &mdash; it updates the "what +counts as 'distributing' the software" algorithm. + +The reason they spread these misconceptions is straightforward: they want to +discourage people from using the AGPL, because they cannot productize such +software effectively. Google wants to be able to incorporate FOSS software into +their products and sell it to users without the obligation to release their +derivative works. Google is an Internet company, and they offer Internet +services. The original GPL doesn't threaten their scheme because their software +is accessed over the Internet, not distributed to end-users directly. + +By discouraging the use of AGPL in the broader community, Google hopes to create +a larger set of free- and open-source software that they can take for their own +needs without any obligations to upstream. Ask yourself: why is documentation of +internal-facing decisions like what software licenses to use being published in +a public place? The answer is straightforward: to influence the public. This is +propaganda. + +There's a bizarre idea that software companies which eschew the AGPL in favor of +something like MIT are doing so specifically because they want companies "like +Google[^2]" to pay for their software, and they know that they have no chance if +they use AGPL. In truth, Google was never going to buy your software. If you +don't use the AGPL, they're just going to take your software and give nothing +back. If you do use the AGPL, they're just going to develop a solution in-house. +There's no outcome where Google pays you. + +[^1]: Likely common *because of this page*. +[^2]: By the way, there are no more than 10 companies world-wide which are "like Google" by any measure. + +Don't be afraid to use the AGPL, and don't be afraid to use software which uses +the AGPL. The obligations are not especially onerous or difficult, despite what +Google would have you believe. The license isn't that long &mdash; read it and +see for yourself. diff --git a/content/blog/Arch-Linux-with-full-disk-encryption-in-15-minutes.md b/content/blog/Arch-Linux-with-full-disk-encryption-in-15-minutes.md @@ -0,0 +1,72 @@ +--- +date: 2016-08-18 +# vim: tw=80 +title: '[VIDEO] Arch Linux with full disk encryption in (about) 15 minutes' +layout: post +tags: [video, linux, encryption] +--- + +<link rel="stylesheet" href="/css/video-js.css"> +<script> +window.HELP_IMPROVE_VIDEOJS = false; +</script> +<script src="/js/video.js"></script> + +After my [blog post](/2016/06/29/Privacy-as-a-hobby.html) emphasizing the +importance of taking control of your privacy, I've decided to make a few more +posts going over detailed instructions on how to actually do so. Today we have a +video that goes over the process of installing Arch Linux with full disk +encryption. + +This is my first go at publishing videos on my blog, so please provide some +feedback in the comments of this article. I'd prefer to use my blog instead of +YouTube for publishing technical videos, since it's all open source, ad-free, +and DRM-free. Let me know if you'd like to see more content like this on my +blog and which topics you'd like covered - I intend to at least release another +video going over this process for Ubuntu as well. + +<video class="video-js vjs-16-9" data-setup="{}" controls> + <source src="https://sr.ht/archlinux.webm" type="video/webm"> + <p>Your browser does not support HTML5 video.</p> +</video> + +<a class="pull-right" href="https://sr.ht/archlinux.webm">Download video (WEBM)</a> + +<div class="clearfix"></div> + +The video goes into detail on each of these steps, but here's the high level +overview of how to do this. Always check the latest version of the [Install +Guide](https://wiki.archlinux.org/index.php/Installation_guide) and the +[dm-crypt](https://wiki.archlinux.org/index.php/Dm-crypt) page on the Arch Wiki +for the latest procedure. + +1. Partition your disks with gdisk and be sure to set aside a partition for + /boot +1. Create a filesystem on /boot +1. (optional) Securely erase all of the existing data on your disks with `dd + if=/dev/zero of=/dev/sdXY bs=4096` - *note: this is a correction from the + command mentioned in the video* +1. Set up encryption for your encrypted partitions with `cryptsetup luksFormat + /dev/sdXX` +1. Open the encrypted volumes with `cryptsetup open /dev/sdXX [name]` +1. Create filesystems on /dev/mapper/[names] +1. Mount all of the filesystems on /mnt +1. Perform the base install with `pacstrap /mnt base [extra packages...]` +1. `genfstab -p /mnt >> /mnt/etc/fstab` +1. `arch-chroot /mnt /usr/bin/bash` +1. `ln -s /usr/share/zoneinfo/[region]/[zone] /etc/localtime` +1. `hwclock --systohc --utc` +1. Edit /etc/locale.gen to your liking and run `locale-gen` +1. `locale > /etc/locale.conf` - note this only works for en_US users, adjust if + necessary +1. Edit /etc/hostname to your liking +1. Reconfigure the network +1. Edit /etc/mkinitcpio.conf and ensure that the `keyboard` and `encrypt` hooks + run before the `filesystems` hook +1. `mkinitcpio -p linux` +1. Set the root password with `passwd` +1. Configure /etc/crypttab with any non-root encrypted disks you need. You can + get partition UUIDs with `ls -l /dev/disk/by-partuuid` +1. Configure your kernel command line to include + `cryptdevice=PARTUUID=[...]:[name] root=/dev/mapper/[name] rw` +1. Install your bootloader and reboot! diff --git a/content/blog/Archive-it-or-miss-it.md b/content/blog/Archive-it-or-miss-it.md @@ -0,0 +1,45 @@ +--- +date: 2017-06-19 +layout: post +title: Archive it or you will miss it +tags: [linkrot] +--- + +Let's open with some quotes from the [Wikipedia article on link +rot](https://en.wikipedia.org/wiki/Link_rot): + +>In 2014, bookmarking site Pinboard's owner Maciej Cegłowski reported a “pretty +>steady rate” of 5% link rot per year... approximately 50% of the URLs in +>U.S. Supreme Court opinions no longer link to the original information... +>(analysis of) more than 180,000 links from references in... three major open +>access publishers... found that overall 24.5% of links cited were no longer +>available. + +I hate link rot. It's been common when servers disappeared or domains expired, +in the past and still today. Today, link rot is on the rise under the influence +of more sinister factors. Abuse of DMCA. Region locking. Paywalls. Maybe it +just no longer serves the interests of a walled garden to host the content. +Maybe the walled garden went out of business. Users rely on platforms to host +content and links rot by the millions when the platforms die. Movies disappear +from Netflix. Music vanishes from Spotify. Accounts are banned from SoundCloud. +YouTube channels are banned over false DMCA requests issued by robots. + +At this point, link rot is an axiom of the internet. In the face of this, I +store a personal offline archive of *anything* I want to see twice. When I see a +cool YouTube video I like, I archive the entire channel right away. Rather than +subscribe to it, I update my archive on a cronjob. I scrape content out of RSS +feeds and into offline storage and I have dozens of websites archived with wget. +I mirror most git repositories I'm interested in. I have DRM free offline copies +of all of my music, TV shows, and movies, ill-begotten or not. + +I suggest you do the same. It's sad that it's come to this. Let's all do +ourselves a favor. Don't build unsustainable platforms and ask users to trust +you with their data. Pay for your domain. Give people DRM free downloads. Don't +cripple your software when it can't call home. If you run a website, let +archive.org scrape it. + +And archive anything you want to see again. + +``` +0 0 * * 0 cd ~/archives && wget -m https://drewdevault.com +``` diff --git a/content/blog/Avoid-traumatic-changes.md b/content/blog/Avoid-traumatic-changes.md @@ -0,0 +1,76 @@ +--- +date: 2019-11-26 +layout: post +title: Software developers should avoid traumatic changes +--- + +A lot of software has gone through changes which, in retrospect, I would +describe as "traumatic" to their communities. I recognize these sorts of changes +by their effect: we might have pulled through in the end, but only after a lot +of heartbreak, struggle, and hours of wasted hacking; but the change left a scar +on the community. + +There are two common cases in which a change risks introducing this kind of +trauma: + +1. It requires everyone in the community, or nearly everyone, to overhaul their + code to get it **working** again +2. It requires everyone in the community, or nearly everyone, to overhaul their + code to get it **idiomatic** again + +Let's call these cases, respectively, strong and weak trauma. While these are +both traumatic changes, the kind of trauma they inflict on the community is +different. The first kind is more severe, but the latter is a bad idea, too. We +can examine these through two case-studies in Python: the (in)famous transition +to Python 3, and the less notorious introduction of asyncio. + +In less than one month, Python 2 will reach its end of life, and even as a +staunch advocate of Python 3, I too have some software which is not going to +make it to the finish line in time[^1]. There's no doubt that Python 3 is much, +much better than Python 2. However, the transition was poorly handled, and +upgrading can be no small task for some projects. The result has been hugely +divisive and intimately familiar to anyone who works with Python, creating +massive rifts in the community and wasting millions of hours of engineer time +addressing. This kind of "strong" trauma is fairly easy to spot in advance. + +[^1]: Eh, kind of. I'm theoretically behind the effort to drop Python 2 from Alpine Linux, but the overhaul is tons of work and the time I can put into the effort isn't going to be enough to finish before 2020. + +The weaker kind of traumatic change is more subtle, and less talked about. It's +a slow burn, and it takes a long time for its issues to manifest. Consider the +case of asyncio: clearly it's an improvement for Python, whose previous attempts +at concurrency have fallen completely flat. The introduction of async/await and +coroutines throughout the software ecosystem is something I'm generally very +pleased about. You'll see me reach for threads to solve a problem when hell +freezes over, and no earlier, so I'm quite fond of first-class coroutines. + +Unfortunately, this has a chilling effect on existing Python code. The +introduction of asyncio has made large amounts of code idiomatically obsolete. +Requests, the darling of the Python world, is effectively useless in a +theoretical idiomatic post-asyncio world. The same is true of Flask, SQLAlchemy, +and many, many other projects. Just about anything that does I/O is unidiomatic +now. + +Since nothing has actually *broken* with this change, the effects are more +subtle than with strong traumatic changes. The effect of asyncio has been to +hasten the onset of code rot. Almost all of SourceHut's code pre-dates asyncio, +for example, and I'm starting to feel the limitations of the pre-asyncio model. +The opportunity to solve this problem by rewriting with asyncio in mind, +however, also presents me a chance to rewrite in anything else, and reevaluate +my choice of Python for the project entirely. It's a tough decision to think +about &mdash; the mature and diverse ecosystem of libraries that help to make a +case for Python is dramatically reduced when asyncio support is a consideration. + +It may take years for the trauma to fully manifest, but the rift is still there +and can only grow. Large amounts of code is rotting and will have to be thrown +away for the brave new asyncio world. The introduction of asyncio has made +another clear "before" and "after" in the Python ecosystem. The years in between +will be rough, because all new Python code will either leverage the rotting +pre-asyncio ecosystem or suffer through an immature post-asyncio ecosystem. +It'll likely turn out for the better &mdash; years from now. + +And sometimes these changes *are* for the better, but they should be carefully +thought out, and designed to minimize the potential impact. In practical terms, +it's for this reason that I urge caution with ideas like adding generics to +Go. In a post-generics world, a large amount of the Go ecosystem will suddenly +become unidiomatic, and breaking changes will required to bring it up to spec. +Let's think carefully about it, eh? diff --git a/content/blog/BARE-message-encoding.md b/content/blog/BARE-message-encoding.md @@ -0,0 +1,215 @@ +--- +date: 2020-06-21 +layout: post +title: Introducing the BARE message encoding +--- + +I like stateless tokens. We started with state*ful* tokens: where a generated +string acts as a unique identifier for a resource, and the resource itself is +looked up separately. For example, your sr.ht OAuth token is a stateful token: +we just generate a random number and hand it to you, something like +"a97c4aeeec705f81539aa". To find the information associated with this token, we +query the database &mdash; our local *state* &mdash; to find it. + +<a href="#announcement"> + Click here to skip the context and read the actual announcement -&gt; +</a> + +But, increasingly, we've been using stateless tokens, which are a bloody good +idea. The idea is that, instead of using random numbers, you encode the actual +state you need into the token. For example, your sr.ht login session cookie is a +JSON blob which is encrypted and base64 encoded. Rather than associating your +session with a record in the database, we just decrypt the cookie when your +browser sends it to us, and the session information is right there. This +improves performance and simplicity in a single stroke, which is a huge win in +my book. + +There is one big problem, though: stateless tokens tend to be a lot larger than +their stateful counterparts. For a stateful token, we just need to generate +enough random numbers to be both unique and unpredictable, and then store the +rest of the data elsewhere. Not so for a stateless token, whose length is a +function of the amount of state which has been sequestered into it. Here's an +example: the cursor fields on the new GraphQL APIs are stateless. This is one of +them: + + gAAAAABe7-ysKcvmyavwKIT9k1uVLx_GXI6OunjFIHa3OJmK3eBC9NT6507PBr1WbuGtjlZSTYLYvicH2EvJXI1eAejR4kuNExpwoQsogkE9Ua6JhN10KKYzF9kJKW0hA_-737NurotB + +A whopping 141 characters long! It's hardly as convenient to lug this monster +around. Most of the time it'll be programs doing the carrying, but it's still +annoying when you're messing with the API and debugging your programs. This +isn't an isolated example, either: these stateless tokens tend to be large +throughout sr.ht. + +In general, JSON messages are pretty bulky. They represent everything as text, +which can be 2x as inefficient for certain kinds of data right off the bat. +They're also self-describing: the schema of the message is encoded into the +message itself; that is, the names of fields, hierarchy of objects, and data +types. + +There are many alternatives that attempt to address this problem, and I +considered many of them. Here were a selected few of my conclusions: + +- [protobuf](https://developers.google.com/protocol-buffers/): too + complicated and too fragile, and I've never been fond of the generated code + for protobufs in any language. Writing a third-party protobuf implementation + would be a gargantuan task, and there's no standard. RPC support is also + undesirable for this use-case. +- [Cap'n Proto](https://capnproto.org/): fixed width, alignment, and so on + &mdash; good for performance, bad for message size. Too complex. RPC support + is also undesirable for this use-case. I also passionately hate C++ and I + cannot in good faith consider something which makes it their primary target. +- [BSON](http://bsonspec.org/): MonogoDB implementation details have leaked into + the specification, and it's extensible in the worst way. I appreciate that + JSON is a closed spec and no one is making vendor extensions for it &mdash; + and, similarly, a diverse extension ecosystem is not something I want to see + for this technology. Additionally, encoding schema into the message is wasting + space. +- [MessagePack](https://msgpack.org/): ruled out for similar reasons: too much + extensibility, and the schema is encoded into the message, wasting space. +- [CBOR](https://cbor.io/): ruled out for similar reasons: too much + extensibility, and the schema is encoded into the message. Has the advantage + of a specification, but the disadvantage of that spec being 54 pages long. + +There were others, but hopefully this should give you an idea of what I was +thinking about when evaluating my options. + +There doesn't seem to be anything which meets my criteria just right: + +- Optimized for small messages +- Standardized +- Easy to implement +- Universal &mdash; little to no support for extensions +- Simple &mdash; no extra junk that isn't contributing to the core mission + +The solution is evident. + +[![xkcd comic 927, "Standards"](https://imgs.xkcd.com/comics/standards.png)](https://xkcd.com/927) + +<a id="announcement"></a> + +## BARE: Binary Application Record Encoding + +[BARE](https://baremessages.org) meets all of the criteria: + +- **Optimized for small messages**: messages are binary, not self-describing, + and have no alignment or padding. +- **Standardized & simple**: the specification is just over 1,000 words &mdash; + shorter than this blog post. +- **Easy to implement**: the first implementation (for Go) was done in a single + weekend (this weekend, in fact). +- **Universal**: there is room for user extensibility, but it's done in a manner + which does not require expanding the implementation nor making messages which + are incompatible with other implementations. + +Stateless tokens aren't the only messages that I've wanted a simple binary +encoding for. On many occasions I've evaluated and re-evaluated the same set of +existing solutions, and found none of them quite right. I hope that BARE will +help me solve many of these problems in the future, and I hope you find it +useful, too! + +The cursor token I shared earlier in the article looks like this when encoded +with BARE: + + gAAAAABe7_K9PeskT6xtLDh_a3JGQa_DV5bkXzKm81gCYqNRV4FLJlVvG3puusCGAwQUrKFLO-4LJc39GBFPZomJhkyqrowsUw== + +100 characters (41 fewer than JSON), which happens to be the minimum size of a +padded [Fernet](https://github.com/fernet/spec/) message. If we compare only the +cleartext: + + JSON: eyJjb3VudCI6MjUsIm5leHQiOiIxMjM0NSIsInNlYXJjaCI6bnVsbH0= + BARE: EAUxMjM0NQA= + +Much improved! + +BARE also has an optional schema language for defining your message structure. +Here's a sample: + +``` +type PublicKey data<128> +type Time string # ISO 8601 + +enum Department { + ACCOUNTING + ADMINISTRATION + CUSTOMER_SERVICE + DEVELOPMENT + + # Reserved for the CEO + JSMITH = 99 +} + +type Customer { + name: string + email: string + address: Address + orders: []{ + orderId: i64 + quantity: i32 + } + metadata: map[string]data +} + +type Employee { + name: string + email: string + address: Address + department: Department + hireDate: Time + publicKey: optional + metadata: map[string]data +} + +type Person (Customer | Employee) + +type Address { + address: [4]string + city: string + state: string + country: string +} +``` + +You can feed this into a code generator and get types which can encode & decode +these messages. But, you can also describe your schema just using your +language's existing type system, like this: + +```go +type Coordinates struct { + X uint // uint + Y uint // uint + Z uint // uint + Q *uint // optional<uint> +} + +func main() { + var coords Coordinates + payload := []byte{0x01, 0x02, 0x03, 0x01, 0x04} + err := bare.Unmarshal(payload, &coords) + if err != nil { + panic(err) + } + fmt.Printf("coords: %d, %d, %d (%d)\n", /* coords: 1, 2, 3 (4) */ + coords.X, coords.Y, coords.Z, *coords.Q) +} +``` + +Bonus: you can get the schema language definition for this struct with +`schema.SchemaFor(coords)`. + +## BARE is under development + +There are some possible changes that could come to BARE before finalizing the +specification. Here are some questions I'm thinking about: + +- Should the schema language include support for arbitrary annotations to + inform code generators? I'm inclined to think "no", but if you use BARE and + find yourself wishing for this, tell me about it. +- Should BARE have first-class support for bitfield enums? +- Should maps be ordered? + +[Feedback welcome](mailto:~sircmpwn/public-inbox@lists.sr.ht)! + +**Errata** + +- This article was originally based on an older version of the draft + specification, and was updated accordingly. diff --git a/content/blog/Backups-and-redundancy-at-sr.ht.md b/content/blog/Backups-and-redundancy-at-sr.ht.md @@ -0,0 +1,118 @@ +--- +date: 2019-01-13 +layout: post +title: "Backups & redundancy at sr.ht" +tags: ["sourcehut", "ops"] +--- + +[sr.ht](https://sr.ht)[^1] is [100% open source][sr.ht-code] and I encourage +people to install it on their own infrastructure, especially if they'll be +sending patches upstream. However, I am equally thrilled to host sr.ht for you +on the "official" instance, and most users find this useful because the +maintenance burden is non-trivial. Today I'll give you an idea of what your +subscription fee pays for. In this first post on ops at sr.ht, I'll talk about +backups and redundancy. In future posts, I'll talk about security, high +availability, automation, and more. + +[^1]: sr.ht is a software project hosting website, with git hosting, ticket tracking, continuous integration, mailing lists, and more. [Try it out!](https://sr.ht) +[sr.ht-code]: https://git.sr.ht/~sircmpwn?search=sr.ht + +As sr.ht is still in the alpha phase, high availability has been on the +backburner. However, data integrity has always been of paramount importance to +me. The very earliest versions of sr.ht, from well before it was even trying to +be a software forge, made a point to never lose a single byte of user data. +Outages are okay - so long as when service is restored, everything is still +there. Over time I'm working to make outages a thing of the past, too, but let's +start with backups. + +There are several ways that sr.ht stores data: + +- Important data on the filesystem (e.g. bare git repositories) +- Important persistent data in PostgreSQL +- Unimportant ephemeral data in Redis (& caches) +- Miscellaneous filesystem storage, like the operating system + +Some of this data is important and kept redundant (PostgreSQL, git repos), and +others are unimportant and is not redundant. For example, I store a rendered +Markdown cache for git.sr.ht in Redis. If the Redis cluster goes *poof*, the +source Markdown is still available, so I don't bother backing up Redis. Most +services run in a VM and I generally don't store important data on these - the +hosts usually only have one hard drive with no backups and no redundancy. If the +host dies, I have to reprovision all of those VMs. + +Other data is more important. Consider PostgreSQL, which contains some of the +most important data for sr.ht. I have one master PostgreSQL server, a dedicated +server in the space I colocate in my home town of Philadelphia. I run sr.ht on +this server, but I also use it for a variety of other projects - I maintain many +myself, and I volunteer as a sysadmin for more still. This box (named Remilia) +has four hard drives configured in a ZRAID (ZFS). I buy these hard drives from a +variety of vendors, mostly Western Digital and Seagate, and from different +batches - reducing the likelihood that they'll fail around the same time. ZFS is +well-known for it's excellent design, featureset and for simply keeping your +data intact, and I don't trust any other filesystem with important data. I take +ZFS snapshots every 15 minutes and retain them for 30 days. These snapshots are +important for correcting the "oh shit, I rm'd something important" mistakes - +you can mount them later and see what the filesystem looked like at the time +they were taken. + +On top of this, the PostgreSQL server is set up with two additional important +features: continuous archiving and streaming replication. Continuous archiving +has PostgreSQL writing each transaction to log files on disk, which represents a +re-playable history of the entire database, and allows you to restore the +database to any point in time. This helps with "oh shit, I dropped an important +table" mistakes. Streaming replication ships changes to an off-site standby +server, in this case set up in my second colocation in San Francisco (the main +backup box, which we'll talk about more shortly). This takes a near real-time +backup of the database, and has the advantage of being able to quickly failover +to it as the primary database during maintenance and outages (more on this +during the upcoming high availability article). Soon I'll be setting up a second +failover server as well, on-site. + +So there are multiple layers to this: + +- ZFS & zraid prevents disk failure from causing data loss +- ZFS snapshots allows retrieving filesystem-level data from the past +- Continuous archiving allows retrieving database-level data from the past +- Streaming replication prevents datacenter existence failure from causing data + loss + +Having multiple layers of data redundancy here protects sr.ht from a wide +variety of failure modes, and also protects each redundant system from itself - +if any of these systems fails, there's another place to get this data from. + +The off-site backup in San Francisco (this box is called Konpaku) has a whopping +52T of storage in two ZFS pools, named "small" (4T) and "large" (48T). The +PostgreSQL standby server lives in the small pool, and [borg +backups](https://www.borgbackup.org/) live in the large pool. This has the same +ZFS snapshotting and retention policy as Remilia, and also has drives sourced +from a variety of vendors and batches. Borg is how important filesystem-level +data is backed up, for example git repositories on git.sr.ht. Borg is nice +enough to compress, encrypt, and deduplicate its backups for us, which I take +hourly with a cronjob on the machines which own that data. The retention policy +is hourly backups stored for 48 hours, daily backups for 2 weeks, and weekly +backups stored indefinitely. + +There are two other crucial steps in maintaining a working backup system: +monitoring and testing. The old wisdom is "you don't have backups until you've +tested them". The simplest monitoring comes from cron - when I provision a new +box, I make sure to set `MAILTO`, make sure sendmail works, and set up a +deliberately failing cron entry to ensure I hear about it when it breaks. I also +set up zfs-zed to email me whenever ZFS encounters issues, which also has a test +mode you should use. For testing, I periodically provision private replicas of +sr.ht services from backups and make sure that they work as expected. PostgreSQL +replication is fairly new to my setup, but my intention is to switch the primary +and standby servers on every database upgrade for HA[^2] purposes, which +conveniently also tests that each standby is up-to-date and still replicating. + +[^2]: High availability + +To many veteran sysadmins, a lot of this is basic stuff, but it took me a long +time to learn how all of this worked and establish a set of best practices for +myself. With the rise in popularity of managed ops like AWS and GCP, it seems +like ops & sysadmin roles are becoming less common. Some of us still love the +sound of a datacenter and the greater level of control you have over your +services, and as a bonus my users aren't worrying about $bigcorp having access +to their data. + +The next ops thing on my todo list is high availability, which is still +in-progress on sr.ht. When it's done, expect another blog post! diff --git a/content/blog/Bring-more-tor-into-your-life.md b/content/blog/Bring-more-tor-into-your-life.md @@ -0,0 +1,60 @@ +--- +date: 2015-11-11 +# vim: tw=80 +title: Bring more Tor into your life +layout: post +tags: [privacy, tor] +--- + +[Tor](https://www.torproject.org/) is a project that improves your privacy +online by encrypting and bouncing your connection through several nodes before +leaving for the outside world. It makes it much more difficult for someone +spying on you to know who you're talking to online and what you're saying to +them. Many people use it with the Tor Browser (a fork of Firefox) and only use +it with HTTP. + +What some people do not know is that Tor works at the TCP level, and can be used +for any kind of traffic. There is a glaring issue with using Tor for your daily +browsing - it's significantly slower. That being said, there are several things +you run on your computer where speed is not quite as important. I am personally +using Tor for several things (this list is incomplete): + +* IRC (chat) +* Email client +* DNS lookups (systemwide) +* Downloading system updates + +Anything that supports downloading through a SOCKS proxy can be used through +Tor. You can also use programs like +[torify](https://trac.torproject.org/projects/tor/wiki/doc/TorifyHOWTO) to +transparently wrap syscalls in Tor for any program (this is how I got my email +to use Tor). + +Of course, Tor can't help you if you compromise yourself. You should not use +bittorrent over Tor, and you should check your other applications. You should +also be using SSL/TLS/etc on top of Tor, so that exit nodes can't be evil with +your traffic. + +## Orbot + +I also use Tor on my phone. I run all of my phone's traffic through Tor, since I +don't use the internet on my phone much. I have whitelisted apps that need to +stream video or audio, though, for the sake of speed. You can do this, too - set +up a black or whitelist of apps on your phone whose networking will be done +through Tor. The app for this is +[here](https://guardianproject.info/apps/orbot/). + +## Why bother? + +The easy answer is "secure everything". If you don't have a good reason to +remain insecure, you should default to secure. That argument doesn't work on +everyone, though, so here are some others. + +* Securing trivial traffic makes more noise to hide the things you care about +* You can have more peace of mind about using public WiFi networks if you're + using Tor. +* ISPs can't inject extra ads and tracking into things you're using over Tor. +* The NSA targets people who use Tor. If you "have nothing to hide", then you + can help defend those who do by adding more noise and giving agencies that + engage in illegal spying a bigger haystack. Bonus: Tor helps make sure that + even though you're being looked at, you're secure. diff --git a/content/blog/Building-a-real-Linux-distro.md b/content/blog/Building-a-real-Linux-distro.md @@ -0,0 +1,110 @@ +--- +date: 2017-05-05 +layout: post +title: Building a "real" Linux distro +tags: [linux] +--- + +I recently saw a post on Hacker News: "[Build yourself a +Linux](https://github.com/MichielDerhaeg/build-linux)", a cool project +that guides you through building a simple Linux system. It's similar to Linux +from Scratch in that it helps you build a simple Linux system for personal use. +I'd like to supplement this with some insight into my experience with a more +difficult task: building a full blown Linux distribution. The result is +[agunix](http://agunix.org), the "silver unix" system. + +For many years I've been frustrated with every distribution I've tried. Many of +them have compelling features and design, but there's always a catch. The +popular distros are stable and portable, but cons include bloat, frequent use of +GNU, systemd, and often apt. Some more niche distros generally have good points +but often have some combination of GNU, an init system I don't like, poor docs, +dynamic linking, or an overall amateurish or incomplete design. Many of them are +tolerable, but none have completely aligned with my desires. + +I've also looked at not-Linux - I have plenty of beefs with the Linux kernel. I +like the BSD kernels, but I dislike the userspaces (though NetBSD is pretty good) +I like the microkernel design of Minix, but it's too unstable and has shit +hardware support. plan9/9front has the most elegant kernel and userspace design +ever made, but it's not POSIX and has shit hardware support. Though none of +these userspaces are for me, I intend to attempt a port of the agunix userspace +to all of their kernels at some point (a KFreeBSD port is underway). + +After trying a great number of distros and coming away with a kind of +dissatisfaction unique to each one, I resolved to make a distro that embodied my +own principles about userspace design. It turns out this is a ton of work - +here's how it's done. + +Let's distinguish a Linux "system" from a Linux "distribution". A Linux system +is anything that boots up from the Linux kernel. A Linux *distribution*, on the +other hand, is a Linux system that can be *distributed* to end users. It's this +sort of system that I wanted to build. In my opinion, there are two core +requirements for a Linux system to become a Linux distribution: + +1. It has a package manager (or some other way of staying up to date) +2. It is self-hosting (it can compile itself and all of the infrastructure runs + on it) + +The first order of business in creating a Linux distro is to fulfill these two +requirements. Getting to this stage is called *bootstrapping* your distribution - +everything else can come later. To do this, you'll need to port your package +manager to your current system, and start building the base packages with it. +If your new distro doesn't use the same architecture or libc as your current +system, you also need to build a cross compiler and use it for building your +new packages. + +My initial approach was different - I used my cross compiler to fill up a chroot +with software without using my package manager, hoping to later bootstrap from +it. I used this approach on my first 3 attempts before deciding to make +base packages on the host system instead. With this approach, I started by +building packages that weren't necessarily self hosting - they used the +host-specific cross compiler builds and such - but produced working packages for +the new environment. I built packages for: + +* my package manager +* musl libc +* bash +* busybox +* autotools +* make +* gcc (clang can't compile the Linux kernel) +* vim + +I also had to package all of the dependencies for these. Once I had a system +that was reasonably capable of compiling arbitrary software, I transferred my +PKGBUILDs (scripts used to build packages) to my chroot and started tweaking +them to re-build packages from the new distro itself. This process took months to +get completely right - there are *tons* of edge cases and corner cases. Simply +getting this software to run in a new Linux system is only moderately difficult - +getting a system that can build itself is *much harder*. I was successful on +my 4th attempt, but threw it out and redid it to get a cleaner distribution with +the benefit of hindsight. This became agunix. + +Once you reach this stage you can go ham on making packages for your system. The +next step for me was graduating from a chroot to dedicated hardware. I built out +an init system with runit and [agunix-init](http://git.agunix.org/init/) and +various other packages that are useful on a proper install. I also compiled a +kernel without support for loadable modules (on par with the static linking theme +of agunix). If you make your own Linux distribution you will probably have to +figure out modules yourself, likely implicating something like eudev. +Eventually, I was able to get agunix [running on my +laptop](https://sr.ht/OzCq.jpg), which has now become my primary agunix dev +machine (often via SSH from my dev desktop). + +The next stage for me was getting agunix.org up and running on agunix. I +deliberately chose not to have a website until it could be hosted on agunix +itself. I deployed agunix to a VPS, then ported nginx and put the website up. +The rest of the infrastructure was a bit more difficult: cgit took me about 10 +packages of work, and bugzilla was about 100 packages of work. Haven't started +working on mailman yet. + +Then begins the eternal packaging phase. At this point you've successfully made +a Linux distribution, and now you just need to fill it with packages. This takes +*forever*. I have made 407 packages to date and I still don't have a desktop to +show for it (I'm *almost* there, just have to make a few dozen more packages +before [sway](https://github.com/SirCmpwn/sway) will run). At this point to have +success you need others to buy into your ideas and start contributing - it's +impossible to package everything yourself. Speaking of which, check out +[agunix.org](http://agunix.org) and see if you like it! I haven't been doing +much marketing for this distro yet, but I do have a little bit of help. If +you're interested in contributing in a new distro, we have lots of work for you +to do! diff --git a/content/blog/Calculate-your-doation-fees-for-Patreon.md b/content/blog/Calculate-your-doation-fees-for-Patreon.md @@ -0,0 +1,86 @@ +--- +date: 2019-05-06 +layout: post +title: Calculating your donation's value following Patreon's fee changes +tags: ["money"] +--- + +In January 2018, I wrote a blog post which included a [fee +calculator](https://drewdevault.com/2018/01/16/Fees-on-donation-platforms.html). +Patreon [changes their fee model +tomorrow](https://www.patreon.com/new-creator-plans), and it's time for an +updated calculator. I'm grandfathered into the old fees, so not much has changed +for me, but I want to equip Patreon users - creators and supporters - with more +knowledge of how their money is moving through the platform. + +Patreon makes money by siphoning some money off the top of a donation flow +between supporters and creators. Because of the nature of its business (a +private, VC-backed corporation), the siphon's size and semantics are prone to +change in undesirable ways, since VC's expect infinite growth and a private +business generally puts profit first. For this reason, I diversify my income, so +that when Patreon makes these changes it limits their impact on my financial +well-being. Even so, Patreon is the biggest of my donation platforms, +representing over $500/month at the time of writing ([full breakdown +here](https://drewdevault.com/donate/))[^1]. + +[^1]: This is supplemented with my Sourcehut income as well, which is covered in the recent [Q1 financial report][q1-finances], as well as some [consulting work](/consulting), which I don't publish numbers for. + +[q1-finances]: https://lists.sr.ht/~sircmpwn/sr.ht-discuss/%3C20190426160729.GC1351@homura.localdomain%3E + +So, for any patrons who are curious about where their money goes, here's a handy +calculator to help you navigate the complex fees. Enjoy! + +**Note**: I don't normally ask you to share my posts, but the Patreon community +is too distributed for me to effectively reach them alone. Please share this +with your Patreon creators and communities! + +<noscript>Sorry, the calculator requires JavaScript.</noscript> +<div id="react-root"></div> +<script src="/js/donation-calc.js"></script> + +**Note**: this calculator does not include the withdrawal fee. When the creator +withdraws their funds from the platform, an additional fee is charged, but the +nature of that fee changes depending on the frequency with which they make +withdrawals and the total amount of money they make from all patrons - which is +information that's not easily available to the average patron for using with +this calculator. For details on the withdrawal fees, see [Patreon's support +article on the +subject](https://support.patreon.com/hc/en-us/articles/203913489-What-are-my-options-to-receive-payout-). + +One question that's been left unanswered is how many times Patreon is going to +charge patrons for each creator they support. Previously, they batched payments +and only accordingly charged the payment processing fees once. However, along +with these changes, they're going to charge payment processing fees for each +creator, but they haven't lowered the payment processing fees. When we take a +look at our bank returns in the coming months, if Patreon is still batching +payments internally... hmm, where is the extra money going? We'll have to wait +and see. + +<h2 id="founding-creators">What are founding creators?</h2> + +Creators who used the Patreon platform prior to 2019-05-07 are "founding +creators", and have different rates. They have different rates for each plan, +and lower payment processing fees. Founding creators are also not usually lite +creators, but were grandfathered into the pro plan. + +<h2 id="charge-up-front">What does charge up front mean?</h2> + +Some creators have the option to charge you as soon as you join the platform, +rather than once monthly or per-creation. This results in higher payment +processing fees for founding creators, as Patreon cannot batch the charge +alongside with your other creators. + +<h2 id="which-plan">How do I know what plan my creator uses?</h2> + +We can guess which plan our creator uses by looking at the features they use on +Patreon. Here are some giveaways: + +- If they have different membership tiers, they use the Pro plan or better. +- If they offer merch through Patreon, they use the Premium plan. + +You can also just reach out to your creator and ask! + +<!-- Hack to get footnotes from javascript to work --> +<span style="display: none">[^2]</span> + +[^2]: This is an assumption based on public PayPal and Stripe payment processing rates. In practice, it's likely that Patreon has a volume discount with their payment processors. Patreon does not publish these rates. diff --git a/content/blog/Can-we-talk-about-client-side-certs.md b/content/blog/Can-we-talk-about-client-side-certs.md @@ -0,0 +1,99 @@ +--- +date: 2020-06-12 +layout: post +title: Can we talk about client-side certificates? +categories: [tls, security, oauth] +--- + +I'm working on improving the means by which API users authenticate with the +SourceHut API. Today, I was reading [RFC 6749][6749] (OAuth2) for this purpose, +and it got me thinking about the original OAuth spec. I recalled vaguely that it +had the API clients actually *sign* every request, and... [yep, indeed it +does][5849]. This also got me thinking: what else signs requests? TLS! + +[6749]: https://tools.ietf.org/html/rfc6749 +[5849]: https://tools.ietf.org/html/rfc5849 + +OAuth is very complicated. The RFC is 76 pages long, the separate bearer token +RFC (6750) is another 18, and no one has ever read either of them. Add JSON Web +Tokens (RFC 7519, 30 pages), too. The process is complicated and everyone +implements it themselves &mdash; a sure way to make mistakes in a +security-critical component. Not all of the data is authenticated, no +cryptography is involved at any step, and it's easy for either party to end up +in an unexpected state. The server has to deal with problems of revocation and +generating a secure token itself. Have you ever met anyone who feels positively +about OAuth? + +Now, take a seat. Have a cup of coffee. I want to talk about client-side +certificates. Why didn't they take off? Let's sketch up a hypothetical TLS-based +protocol as an alternative to OAuth. Picture the following... + +1. You, an API client developer, generate a certificate authority and + intermediate, and you upload your CA certificate to the Service Provider as + part of your registration as a user agent. +2. When you want a user to authorize you to access their account, you generate a + certificate for them, and redirect them to the Service Provider's + authorization page with a <abbr title="Certificate Signing Request">CSR</abbr> + in tow. Your certificate includes, among other things, the list of authorized + scopes for which you want to be granted access. It is already signed with + your client CA key, or one of its intermediates. +3. The client reviews the desired access, and consents. They are redirected back + to your API client application, along with the signed certificate. +4. Use this client-side certificate to authenticate your API requests. Hooray! + +Several advantages to this approach occur to me. + +- You get strong encryption and authentication guarantees for free. +- TLS is basically the single most ironclad, battle-tested security mechanism on + the internet, and mature implementations are available for every platform. + Everyone implements OAuth themselves, and often poorly. +- Client-side certificates are stateless. They contain all of the information + necessary to prove that the client is entitled to access. +- If you handle SSL termination with nginx, haproxy, etc, you can reject + unauthorized requests before your application backend ever even sees them. +- The service provider can untrust the client's CA in a single revocation, if + they are malicious or lose their private keys. +- The API client and service provider are both always certain that the process + was deliberately initiated by the API client. No weird state tokens to carry + through the process like OAuth uses! +- Lots of free features: any metadata you like, built-in expirations, API + clients can self-organize into intermediates at their discretion, and so on. +- Security-concious end users can toggle a flag in their account which would, as + part of the consent process, ask them to sign the API client's certificate + themselves, before the signed certificate is returned to the API client. Then + any API request authorized for that user's account has to be signed by the API + client, the service provider, *and* the user to be valid. + +Here's another example: say your organization has several services, each of +which interacts with a subset of Acme Co's API on behalf of their users. Your +organization generates a single root CA, and signs up for Acme Co's API with it. +Then you issue intermediate CAs to each of your services, which are *only* +allowed to issue CSRs for the subset of scopes they require. If any service is +compromised, it can't be used to get more access than it already had, and you +can revoke just that one intermediate without affecting the rest. + +Even some famous downsides, such as <abbr title="Certificate Revocation +Lists">CRLs</abbr> and <abbr title="Online Certificate Status +Protocol">OCSP</abbr>, are mitigated here, because the system is much more +centralized. You control all of the endpoints which will be validating +certificates, you can just distribute revocations directly to them as soon as +they come in. + +The advantages are clearly numerous. Let's wrap it up in a cute, Google-able +name, write some nice tooling and helper libraries for it, and ship it! + +Or, maybe not. I have a nagging feeling that I'm missing something here. It +doesn't seem right that such an obvious solution would have been left on the +table, by everyone, for decades. Maybe it's just that the whole certificate +signing dance has left a bad taste in everyone's mouth &mdash; many of us have +not-so-fond memories of futzing around with the awful OpenSSL CLI to generate a +CSR. But, there's no reason why we couldn't do it better, and more streamlined, +if we had the motivation to. + +There are also more use-cases for client-side certificates that seem rather +compelling, such as an alternative to user passwords. Web browser support for +client-side certificates totally sucks, but that is a solvable problem. + +For the record, I have no intention of using this approach for the SourceHut +API. This thought simply occurred to me, and I want to hear what you think. Why +aren't we using client-side certificates? diff --git a/content/blog/China.md b/content/blog/China.md @@ -0,0 +1,214 @@ +--- +date: 2019-11-20 +layout: distraction-free-page +title: China +tags: [politics] +--- + +This article will be difficult to read and was difficult to write. I hope that +you can stomach the uncomfortable nature of this topic and read my thoughts in +earnest. I usually focus on technology-related content, but at the end of the +day, this is my personal blog and I feel that it would betray my personal +principles to remain silent. I've made an effort to provide citations for all of +my assertions. + +*Note: if you are interested in conducting an independent review of the +factuality of the claims expressed in this article, please [contact +me](mailto:sir@cmpwn.com).* + +The keyboard I'm typing these words into bears "Made in China" on the bottom. +The same is true of the monitor I'm using to edit the article. It's not true of +all of my electronics &mdash; the graphics processing unit which is driving the +monitor was made in Taiwan[^taiwan-definition] and my phone was made in +Vietnam.[^vietnam-concerns] Regardless, there's no doubt that my life would be, +to some degree, worse off if not for trade with China. Despite this, I am +prepared to accept the consequences of severing economic relations with China. + +[^taiwan-definition]: An island in the sea east of China governed by the sovereign Republic of China. +[^vietnam-concerns]: Which, admittedly, raises concerns of its own. + +How bad would being cut-off from China's economy be? We're a net importer from +China, and by over 4 times the volume.[^us-china-trade-volume] Let's assume, in +the worst case, trade ties were completely severed. The United States would be +unable to buy $155B worth of electronics, which we already have domestic +manufacturing capabilities for[^electronics-at-home] and which have a productive +life of several years. We could definitely stand to get used to repairing and +reusing these instead of throwing them out. We'd lose $34B in mattresses and +furniture &mdash; same story. The bulk of our imports from China are luxury +goods that we can already make here at home[^itc-trade-map] &mdash; it's just +cheaper to buy them from China. But cheaper for whom? + +[^us-china-trade-volume]: [US Census Bureau, International Trade Data](https://www.census.gov/foreign-trade/balance/c5700.html) +[^electronics-at-home]: [LG](https://www.lg.com/us/press-release/lg-electronics-to-build-us-factory-for-home-appliances-in-tennessee), [Intel](http://download.intel.com/newsroom/kits/22nm/pdfs/Global-Intel-Manufacturing_FactSheet.pdf) (PDF) +[^itc-trade-map]: [ITC Trade Map](https://www.trademap.org/Bilateral_TS.aspx?nvpm=1%7C842%7C%7C156%7C%7CTOTAL%7C%7C%7C2%7C1%7C1%7C1%7C2%7C1%7C1%7C1%7C1) + +This gets at the heart of the reason why we're tied to China economically. It's +economically productive *for the 1%* to maintain a trade relationship with +China. The financial incentives don't help any Americans, and in fact, most of +us are hurt by this relationship.[^decline-of-manufacture] Trade is what keeps +us shackled to the Chinese Communist Party government, but it's not beneficial +to anyone but those who are already obscenely rich, and certainly not for our +poorest &mdash; who, going into 2020, are as likely to be high school dropouts +as they are to be doctors.[^studentdebt] + +[^decline-of-manufacture]: Source(s): Ebenstein, Avraham, et al. "Understanding the Role of China in the ‘Decline’of US Manufacturing." Manuscript, Hebrew University of Jerusalem (2011); [The China toll deepens](https://www.epi.org/publication/the-china-toll-deepens-growth-in-the-bilateral-trade-deficit-between-2001-and-2017-cost-3-4-million-u-s-jobs-with-losses-in-every-state-and-congressional-district/), Robert E. Scott and Zane Mokhiber, Economic Policy Institute +[^studentdebt]: Source: Ulbrich, Timothy R., and Loren M. Kirk. "It’s time to broaden the conversation about the student debt crisis beyond rising tuition costs." American journal of pharmaceutical education 81.6 (2017): 101. + +So, we can cut off China. Why should we? Let's lay out the facts: China is +conducting human rights violations on the largest scale the world has seen since +Nazi Germany. China executes political prisoners[^political-prisoners] and +harvests their organs for transplant to sick elites on an industrial scale, +targeting and killing civilians based on not only political, but also ethnic and +religious factors. This is commonly known as genocide. China denies using the +organs of prisoners, but there's credible doubt[^transplant-transparency] from +the scientific community. + +[^political-prisoners]: A political prisoner is someone who is imprisoned for political reasons, rather than legal reasons. In the eyes of Chinese law, there may be a legal standing for the imprisonment of some of these people, but because this is often based on dissent from the single political party, I consider these prisoners political as well. A related term is "prisoner of conscience", and for the purposes of this article I do not distinguish between the two; the execution of either kind of prisoner is a crime against humanity regardless. +[^transplant-transparency]: Trey, T., et al. "Transplant medicine in China: need for transparency and international scrutiny remains." American Journal of Transplantation 16.11 (2016): 3115-3120. + +Recent evidence directly connecting executions to organ harvesting is somewhat +unreliable, but I don't think China deserves the benefit of the doubt. +China is a world leader in executions, and is believed to conduct more +executions than the rest of the world combined.[^amnesty-executions] +Wait times for organ transplantation are extraordinarily low in +China,[^organ-wait-time] on the order of weeks &mdash; in most of the developed +world these timeframes are measured in terms of years,[^uk-wait] and China has +been unable to explain the source for tens of thousands of transplants in the +past[^falun-gong]. And, looking past recent evidence, China directly admitted to +using the organs of executed prisoners in 2005.[^2005-admission] + +[^organ-wait-time]: Jensen, Steven J., ed. The ethics of organ transplantation. CUA Press, 2011. +[^uk-wait]: UK has some of the best times in the developed world, and averages about 3 years. Source: [NHS](https://web.archive.org/web/20110903084007/http://www.organdonation.nhs.uk/ukt/statistics/centre-specific_reports/pdf/waiting_time_to_transplant.pdf) +[^falun-gong]: Matas, David, and David Kilgour. "An independent investigation into allegations of organ harvesting of Falun Gong practitioners in China." Electronic document accessed September 5 (2007): 2008. +[^2005-admission]: [China to ‘tidy up’ trade in executed prisoners’ organs](https://web.archive.org/web/20140304045314/http://www.thetimes.co.uk/tto/news/world/asia/article2612313.ece), the UK Times, December 3 2005 +[^amnesty-executions]: [Death Penalty: World’s biggest executioner China must come clean about ‘grotesque’ level of capital punishment](https://www.amnesty.org/en/latest/news/2017/04/china-must-come-clean-about-capital-punishment/), Amnesty International, 11 April 2017 + +These atrocities are being committed against cultural minorities to further +China's power. The UN published a statement in August 2018 stating that they +have credible reports of over a million ethnic Uighurs being held in internment +camps in Xinjiang,[^bbc-uighurs-un] imprisoned with various other ethnic +minorities from the region. Leaks in November 2019 reported by the New York +Times showed that China admits the imprisoned have committed no crimes other +than dissent,[^nyt-leaks] and that the camps were to be run with, quote, +"absolutely no mercy". + +[^bbc-uighurs-un]: [China Uighurs: One million held in political camps, UN told](https://www.bbc.com/news/world-asia-china-45147972), BBC, 10 August 2018 +[^nyt-leaks]: [‘Absolutely No Mercy’: Leaked Files Expose How China Organized Mass Detentions of Muslims](https://www.nytimes.com/interactive/2019/11/16/world/asia/china-xinjiang-documents.html), New York Times, 16 November 2019 + +It's nice to believe that we would have stood up to Nazi Germany if we had been +there in the 1940's. China is our generation's chance to prove ourselves of that +conviction. We talk a big game about fighting against white nationalists in our +own country, and pride ourselves on standing up against "fascists". It's time we +turned attention to the real fascists, on the world stage. + +Instead, the staunch capitalism of America, and the West as a whole, has swooped +in to leverage Chinese fascism for a profit. Marriott Hotels apologized for +listing Hong Kong, Macau, and Taiwan as countries separate from China.[^mariott] +Apple removed the Taiwanese flag from iOS in China and the territories it +claims.[^apple] Activision/Blizzard banned several players for making pro-Hong +Kong statements in tournaments and online.[^blizzard] These behaviors make me +ashamed to be an American. + +[^mariott]: [Marriott to China: We Do Not Support Separatists](https://www.nytimes.com/2018/01/11/business/china-marriott-tibet-taiwan.html), New York Times, 11 January 2018 +[^apple]: [Apple bows to China by censoring Taiwan flag emoji](https://qz.com/1723334/apple-removes-taiwan-flag-emoji-in-hong-kong-macau-in-ios-13-1-1/), Quartz, 7 October 2019 +[^blizzard]: [Blizzard Entertainment Bans Esports Player After Pro-Hong Kong Comments](https://www.npr.org/2019/10/08/768245386/blizzard-entertainment-bans-esports-player-after-pro-hong-kong-comments), NPR, 8 October 2019 + +Fuck that. + +A brief history lesson: Hong Kong was originally controlled by the United +Kingdom at the end of the Opium Wars. It's beyond the scope of this article, but +it'll suffice to say that the United Kingdom was brutal and out of line, and the +end result is that Hong Kong became a British colony. Because of this, it was +protected from direct Chinese influence during China's turbulent years +following, and they were insulated from the effects of the Great Leap Forward +and the Cultural Revolution, which together claimed tens of millions of lives +and secured the Communist Party of China's power into the present. + +On July 1st, 1997, the [Sino-British Joint +Declaration](https://en.wikipedia.org/wiki/Sino-British_Joint_Declaration) [went +into effect](https://www.youtube.com/watch?v=k7YzJzq1Mvk), and Hong Kong was +turned over to China. The agreement stipulated that Hong Kong would remain +effectively autonomous and self-governing for a period of 50 years &mdash; +until 2047. China has been gradually and illegally eroding that autonomy +ever since. Today, Hong Kong citizens have effectively no representation in +their government. The Legislative Council of Hong Kong has been deliberately +engineered by China to be pro-Beijing &mdash; a majority of the council is +selected through processes with an inherent pro-Beijing bias, giving Hong Kong +effectively no autonomous power to pass laws.[^legislative-structure] + +[^legislative-structure]: [Legislative Council of Hong Kong, Wikipedia](https://en.wikipedia.org/wiki/Legislative_Council_of_Hong_Kong#Procedure) + +Hong Kong's executive branch is even worse. The Chief Executive of Hong Kong +(Carrie Lam) is elected by a committee of 1,200 members largely controlled by +pro-Beijing seats, from a pool of pro-Beijing candidates, and the people have +effectively no representation in the election. The office has been held by +pro-Beijing politicians since it was established.[^list-of-executives] + +[^list-of-executives]: [List of Chief Executives of Hong Kong](https://en.wikipedia.org/wiki/Chief_Executive_of_Hong_Kong#List_of_Chief_Executives_of_Hong_Kong), Wikipedia + +The ongoing protests in Hong Kong were sparked by a mainland attempt to rein +in Hong Kong's judicial system in a similar manner, with the introduction of the +"Fugitive Offenders and Mutual Legal Assistance in Criminal Matters Legislation +(Amendment) Bill 2019",[^poison-bill] which would have allowed the authorities +to extradite suspects awaiting trial to mainland China. These protests inspired +the Hong Kong people to stand up against all of the injustices they have faced +from China's illegal encroachments on their politics. The protesters have five +demands:[^demands] + +[^poison-bill]: https://www.hklii.hk/eng/hk/legis/ord/503/index.html +[^demands]: https://focustaiwan.tw/news/acs/201906270014.aspx + +1. Complete withdrawal of the extradition bill +2. No prosecution of the protesters +3. Retraction of the characterization of the protests as "riots" +4. Establish an independent inquiry into police misconduct +5. Resignation of Carrie Lam and the implementation of universal suffrage + +Their first demand has been met, but the others are equally important and the +protests show no signs of slowing. Unfortunately, China shows no signs of +slowing their crackdown either, and have been consistently escalating the +matter. The police are now threatening to use live rounds on the +protesters,[^live-rounds] and people are already being shot in the +streets.[^video] China is going to kill the protesters, [again][tiananmen]. + +[^live-rounds]: [Hong Kong police move on university campus, threaten live rounds, retreat before growing flames](https://www.washingtonpost.com/world/hong-kong-police-pummel-university-with-water-cannon-as-officer-hit-by-arrow/2019/11/17/f004c978-091f-11ea-8054-289aef6e38a3_story.html), The Washington Post, 17 November 2019 +[tiananmen]: https://en.wikipedia.org/wiki/1989_Tiananmen_Square_protests +[^video]: Source: [Video (graphic)](https://streamable.com/0pexa) + +The third demand &mdash; the retraction of the characterization of the +demonstrations as "riots" &mdash; and the government's refusal to meet it, +conveys a lot about China's true intentions. Chinese law defines rioting as a +capital offense,[^chinese-criminal-law] and we've already demonstrated their +willingness to execute political prisoners on a massive scale. These protesters +are going to be killed if their demands aren't met.[^riot-definition] + +[^riot-definition]: As pointed out by Hong Kongers reading this article, Hong Kong has a [separate definition of rioting](https://en.wikipedia.org/wiki/Public_Order_Ordinance), which is not a capital offense. For my part, I am not entirely convinced that China isn't planning to use the "riots" classification as justification for a violent response. +[^chinese-criminal-law]: Criminal Law of the People's Republic of China, [translation provided by US Congressional-Executive Commission of China](https://www.cecc.gov/resources/legal-provisions/criminal-law-of-the-peoples-republic-of-china) + +Hong Kong is the place where humanity makes its stand against oppressors. The +people of Hong Kong have been constant allies to the West, and their liberty is +at stake. If we want others to stand up for us when our liberties are on the +line, then it's our turn to pay it forward now. The founding document of the +United States of America[^doi] describes the rights they're defending as +"unalienable" &mdash; endowed upon all people by their Creator. The people of +Hong Kong are our friends and we're watching them get killed for rights that we +hold dear in our own nation's founding principles. + +[^doi]: [Declaration of Independence, full text](https://www.archives.gov/founding-docs/declaration-transcript) + +We have a legal basis for demanding these rights for Hong Kong's people &mdash; +China is blatantly violating their autonomy, which they agreed to uphold +in 1984. The United Kingdom should feel obligated to step in, but they'll need +the support of the international community, which we need to be prepared to give +them. We need to make an ultimatum: if China uses deadly force in Hong Kong, +the international community will respond in kind. + +China isn't the only perpetrator of genocide today, but they are persecuting our +friends. China has the second highest GDP[^china-gdp] in the world, and somehow +this makes it okay. If we won't stand up to them, then who will? I call for a +worldwide boycott of Chinese products, and of companies who kowtow to their +demands or accept investment from China. I call for international condemnation +of the Communist Party of China's behavior and premise for governance. And I +call for an ultimatum to protect our allies from slaughter. + +[^china-gdp]: [List of countries by GDP (nominal) - Wikipedia](https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)) diff --git a/content/blog/Commons-clause-will-destroy-open-source.md b/content/blog/Commons-clause-will-destroy-open-source.md @@ -0,0 +1,123 @@ +--- +date: 2018-08-22 +layout: post +title: The Commons Clause will destroy open source +tags: [philosophy, free software] +--- + +An alarmist title, I know, but it's true. If the [Commons +clause](https://commonsclause.com/) were to be adopted by all open source +projects, they would cease to be open source[^1], and therefore the Commons +clause is trying to destroy open source. When this first appeared I spoke out +about it in discussion threads around the net, but didn't think anyone would +take it seriously. Well, yesterday, some parts of Redis [became proprietary +software](https://redislabs.com/community/commons-clause/). + +[^1]: Under both the OSI and FSF definitions. The Commons Clause removes freedom 0 of the [four essential freedoms](https://www.gnu.org/philosophy/free-sw.en.html). + +The Commons Clause promoted by Kevin Wang presents one of the greatest +existential threats to open source I've ever seen. It preys on a vulnerability +open source maintainers all suffer from, and one I can strongly relate to. It +*sucks* to not be able to make money from your open source work. It *really* +sucks when companies are using your work to make money for themselves. If a +solution presents itself, it's tempting to jump at it. But the Commons Clause +doesn't present a solution for supporting open source software. It presents a +framework for turning open source software into proprietary software. + +What should we do about open source maintainers not getting the funding +they need? It's a very real problem, and one Kevin has [explicitly asked +us](https://twitter.com/kevinverse/status/1032074268291424257) to talk about +before we criticise his solution to it. I would be happy to share my thoughts. +I've struggled for many years to find a way to finance myself as the maintainer +of many dozens of projects. For a long time it has been a demotivating struggle +with no clear solutions, a struggle which at one point probably left me +vulnerable to the temptations offered by the Common Clause. But today, the +situation is clearly improving. + +Personally, I have a harder go of it because very little of my open source +software is appealing to the businesses that have the budget to sponsor them. +Instead, I rely on the (much smaller and less stable) recurring donations of my +individual users. When I started accepting these, I did not think that it was +going to work out. But today, I'm making far more money from these donations +than I ever thought possible[^2], and I see an upwards trend which will +eventually lead me to being able to work on open source full time. If I were +able to add only a few business-level sponsorships to this equation, I think I +would easily have already reached my goals. + +[^2]: [Figures here](https://drewdevault.com/donate) + +There are other options for securing financing for open source, some of which +Redis has already been exploring. Selling a hosted and supported version of +your service is often a good call. Offering consulting support for your +software has also worked for many groups in the past. Some projects succeed with +(A)GPL for everyone and BSD for a price. These are all better avenues to +explore - making your software proprietary is a tragic alternative that should +not be considered. + +We need to combine these methods with a greater appreciation for open source in +the business community. Businesses need engineers - appeal to your peers so they +can appeal to the money on behalf of the projects they depend on. A $250/mo +recurring donation to would be a drop in the bucket of most businesses, but a +major boon to any open source project, with which the business will almost +certainly see tangible value-add as a result. When I get to work today I'm going +to identify open source projects we use that accept donations and make the +plea[^3], and keep making the plea week over week until money is spent. You +should, too. + +[^3]: I intend to do an audit, but I have always (and I encourage you to always) kept an eye on the stuff we use as I come across it, looking for opportunities to donate. + +Redis also stands out as a cautionary entry in the history of Contributor +License Agreements. Everyone who has contributed to the now-proprietary Redis +modules has had their hard work stolen and sold by RedisLabs under a proprietary +license. I do not sign CLAs and I think they're a harmful practice for this very +reason. Asking a contributor to sign them is a slap in the face to the good will +which led them to make a contribution in the first place. Don't sign these and +don't ask others to. + +I respect antirez[^4] very much, but I am sorely disappointed in him. He should +have known better and, if you're reading this, I urge you to roll back your +misguided decision. But the Commons Clause is much more deeply disturbing. What +Kevin is doing will ruin open source software, maybe for good[^5]. + +[^4]: The maintainer of Redis + +[^5]: As software gets abandoned, making the license more permissive is the last thing on the maintainer's minds. So as the body of Commons Clause software grows, the graveyard will only ever fill. + +I really appreciate some of Kevin's work. [FOSSA](https://fossa.io/) is a really +cool tool that can stand to provide some serious value to the open source +community. [TL;DR Legal](https://tldrlegal.com/) is a fantastic tool which has +already delivered a tremendous amount of value to open source, and I've +personally referenced it dozens of times. Thank you, honestly, for your work on +improving the legal landscape of open source. With Commons Clause, however, +Kevin has taken it too far. [The four +freedoms](https://www.gnu.org/philosophy/free-sw.en.html) are *important*. The +only solution is to bury the Common Clause project. Kill the website and GitHub +repository, and we can try to forget this ever happened. + +I understand that turning back is going to be hard, which scares me. I know that +Kevin has already put a lot of effort into it and convinced himself that it's +the Right Thing To Do. It takes work to write the clause, vet it for legal +issues, design a website (a beautiful one, I'll give you that), and to promote +it among your target audience. I know how hard it is to distance yourself from +something you've staked your personal reputation on. You had only the best +intentions[^6], Kevin, but please step back from the ego and do the right thing - +take this down. You stand to undo all of your hard work for the open source +community in one fell swoop with this initiative. I'm begging you, stop while +it's not too late. + +Man, two angry articles in a row. I have more technical articles coming up, I +promise. + +[^6]: Honestly, this is a real problem that open source suffers from and I really appreciate the attempt to fix it, misguided as it may have been. But this is not okay, and Kevin needs to recognize the gravity of his mistake and move to correct it. + +--- + +**Update 2018-08-23 03:00 UTC:** Richard Stallman of the Free Software +Foundation reached out asking me to clarify the use of "open source" in this +article. I have refered to the FSF's document on essential freedoms as a +definition of "open source". In fact, it is the definition of free software - a +distinct concept. The FSF does not advocate for open source software, but +particularly for free (or "libre") software, of which there is some intersection +with open source software. For more information on the difference, refer to +[Richard's article on the +subject](https://www.gnu.org/philosophy/open-source-misses-the-point.html). diff --git a/content/blog/Complicated.md b/content/blog/Complicated.md @@ -0,0 +1,58 @@ +--- +date: 2017-09-08 +layout: post +title: Killing ants with nuclear weapons +tags: [philosophy] +--- + +Complexity is quickly becoming an epidemic. In this developer's opinion, +complexity is the ultimate enemy - the final boss - of good software design. +Complicated software generally has complicated bugs. Simple software generally +has simple bugs. It's as easy as that. + +It's for this reason that I strongly dislike many of the tools and architectures +that have been proliferating over the past few years, particularly in web +development. When I look at a tool like Gulp, I wonder if its success is largely +attributable to people not bothering to learn how Makefiles work. Tools like +Docker make me wonder if they're an excuse to avoid learning how to do ops or +how to use your distribution's package manager. Chef makes me wonder if its +users forgot that shell scripts can use SSH, too. + +These tools offer a value add. But how does it compare to the cost of the +additional complexity? In my opinion, in *every case*[^1] the value add is far +outweighed by the massive complexity cost. This complexity cost shows itself +when the system breaks (and it will - all systems break) and you have to dive +into these overengineered tools. Don't forget that dependencies are fallible, +and never add a dependency you wouldn't feel comfortable debugging. The time +spent learning these complicated systems to fix the inevitable bugs is surely +much less than the time spent learning the venerable tools that fill the same +niche (or, in many cases, accepting that you don't even need this particular +shiny thing). + +Reinventing the wheel is a favorite pastime of mine. There are many such wheels +that I have reinvented or am currently reinventing. The problem isn't in +reinventing the wheel - it's in doing so before you actually understand the +wheel[^2]. I wonder if many of these complicated tools are written by people who +set out before they fully understood what they were replacing, and I'm *certain* +that they're mostly used by such people. I understand it may seem intimidating +to learn venerable tools like make(1) or chroot(1), but they're just a short man +page away[^3]. + +It's not just tools, though. I couldn't explain the features of C++ in fewer than +several thousand words (same goes for Rust). GNU continues to add proprietary +extensions and unnecessary features to everything they work on. Every update +shipped to your phone is making it slower to ensure you'll buy the new one. +Desktop applications are shipping entire web browsers into your disk and your +RAM; server applications ship entire operating systems in glorified chroots; and +hundreds of megabytes of JavaScript, ads, and spyware are shoved down the pipe +on every web page you visit. + +This is an epidemic. It's time we cut this shit out. Please, design your systems +with simplicity in mind. Moore's law is running out[^4], the free lunch is +coming to an end. We have heaps and heaps of complicated, fragile abstractions +to dismantle. + +[^1]: That I've seen (or heard of) +[^2]: "Those who don't understand UNIX are doomed to reinvent it, poorly." +[^3]: Of course, [*"...full documentation for make is maintained as a GNU info page..."*](https://xkcd.com/912/) +[^4]: Transistors are approaching a scale where quantum problems come into play, and we are limited by the speed of light without getting any smaller. The RAM bottleneck is another serious issue, for which innovation has been stagnant for some time now. diff --git a/content/blog/Configuring-aerc-for-git.md b/content/blog/Configuring-aerc-for-git.md @@ -0,0 +1,70 @@ +--- +date: 2020-04-20 +layout: post +title: Configuring aerc for git via email +tags: ["workflow", "aerc", "git"] +--- + +I use [aerc](https://aerc-mail.org) as my email client (naturally &mdash; I +wrote it, after all), and I use [git send-email](https://git-send-email.io) to +receive patches to many of my projects. I designed aerc specifically to be +productive for this workflow, but there are a few extra things that I use in my +personal aerc configuration that I thought were worth sharing briefly. This blog +post will be boring and clerical, feel free to skip it unless it's something +you're interested in. + +When I want to review a patch, I first tell aerc to `:cd sources/<that +project>`, then I open up the patch and give it a read. If it needs work, I'll +use "rq" ("reply quoted"), a keybinding which is available by default, to open +my editor with the patch pre-quoted to trim down and reply with feedback inline. +If it looks good, I use the first of my custom keybindings: "ga", short for git +am. The entry in `~/.config/aerc/binds.conf` is: + +``` +ga = :pipe -mb git am -3<Enter> +``` + +This pipes the entire message (-m, in case I'm viewing a message part) into `git +am -3` (-3 uses a three-way merge, in case of conflicts), in the background +(-b). Then I'll use C-t (ctrl-T), another keybinding which is included by +default, to open a terminal tab in that directory, where I can compile the code, +run the tests, and so on. When I'm done, I use the "gp" keybinding to push the +changes: + +``` +gp = :term git push<Enter> +``` + +This runs the command in a new terminal, so I can monitor the progress. Finally, +I like to reply to the patch, letting the contributor know their work was merged +and thanking them for the contribution. I have a keybinding for this, too: + +``` +rt = :reply -Tthanks<Enter> +``` + +My "thanks" template is at `~/.config/aerc/templates/thanks` and looks like +this: + +``` +Thanks! + +{% raw %} +{{exec "{ git remote get-url --push origin; + git reflog -2 origin/master --pretty=format:%h; } + | xargs printf 'To %s\n %s..%s master -> master\n'" ""}} +{% endraw %} +``` + +That git command prints a summary of the most recent push to master. The result +is that my editor is pre-filled with something like this: + +``` +Thanks! + +To git@git.sr.ht:~sircmpwn/builds.sr.ht + 7aabe74..191f4a0 master -> master +``` + +I occasionally append a few lines asking questions about follow-up work or +clarifying the deployment schedule for the change. diff --git a/content/blog/Conservative-web-development.md b/content/blog/Conservative-web-development.md @@ -0,0 +1,98 @@ +--- +date: 2018-09-04 +layout: post +title: Conservative web development +tags: ["philosophy", "web"] +--- + +Today I turned off my ad blocker, enabled JavaScript, opened my network monitor, +and clicked the first link on Hacker News - a New York Times article. It started +by downloading a megabyte of data as it rendered the page over the course of +eight full seconds. The page opens with an advertisement 281 pixels tall, placed +before even the title of the article. As I scrolled down, more and more requests +were made, downloading a total of 2.8 MB of data with 748 HTTP requests. An +article was weaved between a grand total of 1419 vertical pixels of ad space, +greater than the vertical resolution of my display. Another 153-pixel ad is +shown at the bottom, after the article. Four of the ads were identical. + +I was reminded to subscribe three times, for $1/week (after one year this would +become $3.75/week). One of these reminders attached itself to the bottom of my +screen and followed along as a scrolled. If I scrolled up, it replaced this with +a larger banner, which showed me three other articles and an ad. I was asked for +my email address once, though I would have had to fill out a captcha to submit +it. I took out my phone and repeated the experiment. It took 15 seconds to load, +and I estimate the ads took up a vertical space equal to 4 times my phone's +vertical resolution, each ad alone taking up half of my screen. + +The text of the article is a total of 9037 bytes, including the title, author, +and date. I downloaded the images relevant to the article, including the +1477x1082[^1] title image. Before I ran them through an optimizer, they weighed +260 KB; after, 236 KB (using only lossless optimizations). 8% of the total +download was dedicated to the content. 5 discrete external companies were +informed of my visit to the page and given the opportunity to run artibrary +JavaScript on it. + +[^1]: Greater than the vertical resolution of my desktop display. + +If these are the symptoms, what is the cure? My basic principles are these: + +- Use no, or very little, JavaScript +- Use raster images sparingly, if at all, and optimize them +- Provide interactivity with forms and clever CSS +- Identify wasted bandwidth and CPU cycles and optimize them + +I've been building [sr.ht](https://meta.sr.ht) with these principles in mind, +and I spent a few hours this optimizing it further. What do the results look +like? The heaviest page, [the marketing page](https://meta.sr.ht), today weighs +<strong class="text-info">110 KB</strong> with a cold cache, and <strong +class="text-danger">4.6 KB</strong> warm. [A similar page](https://github.com/) +on GitHub.com[^2] weighs <strong class="text-info">2900 KB</strong> cold, +<strong class="text-danger">19.4 KB</strong> warm. [A more typical +page][srht-main] on sr.ht weighs <strong class="text-info">56.8 KB</strong> +cold and <strong class="text-danger">31.9 KB</strong> warm, after <strong +class="text-warning">2</strong> HTTP requests; on GitHub [the same +page][github-main] is <strong class="text-info">781 KB</strong> cold and +<strong class="text-danger">57.4 KB</strong> warm, <strong +class="text-warning">118</strong> requests. This file is 29.1 KB. The sr.ht +overhead is <strong class="text-info">27.6 KB</strong> cold and <strong +class="text-danger">2.7 KB</strong> warm. The GitHub overhead is respectively +<strong class="text-info">751.9 KB</strong> and <strong class="text-danger">28.2 +KB</strong>. There's also a 174-pixel-tall ad on GitHub encouraging me to sign +up for an account, shown before any of the content. + +[srht-main]: https://git.sr.ht/~sircmpwn/linux/tree/master/init/main.c +[github-main]: https://github.com/torvalds/linux/blob/master/init/main.c + +[^2]: You may have to log out to see this. + +To be fair, the GitHub page has more features. As far as I can tell, most of +these aren't implemented *in* the page, though, and are rather links to other +pages. Some of the features *in* the page include a dropdown for filtering +branches and tags, popups that show detail when you hover over an avatars, some +basic interactivity in the search, all things that I can't imagine taking up +much space. Does this justify an order of magnitude increase in resource usage? + +Honestly, GitHub does a pretty good job overall. Compared to our New York Times +example, they're downright *great*. But they could be doing better, and so could +we all. You can build beautiful, interactive websites with HTML and CSS alone, +supported by a simple backend. Pushing the complexity of rendering your +single-page app into the frontend might save you miniscule amounts of +server-side performance, but you'd just be offloading the costs onto your +visitor's phone and sucking their battery dry. + +There are easy changes you can make. Enable caching on your web server, with a +generous expiry. Use a hash of your resources in the URL so that you can bust +the cache when you need to. Enable gzip for text resources, and HTTP/2. Run your +images through an optimizer, odds are they can be losslessly compressed. There +are harder changes, too. Design your website to be usable without JavaScript, +and use small amounts of it to enhance the experience - rather than to *be* the +experience. Use CSS cleverly to provide interactivity[^3]. Find ways to offload +work to the server where you can[^4]. Measure your pages to look for places to +improve. Challenge yourself to find the simplest way of building the features +you want. + +[^3]: For example, check out how I implemented the collapsable message details on the [lists.sr.ht archives](https://lists.sr.ht/~sircmpwn/sr.ht-dev/%3C20180830183221.32377-1-hilobakho%40gmail.com%3E) +[^4]: I did this when I upgraded to Font Awesome 5 recently. They want you to include some JavaScript to make their SVG icons work, but instead I wrote a [dozen lines of Python](https://git.sr.ht/~sircmpwn/core.sr.ht/tree/srht/flask.py?id=70e75e96dc664a1b487ef02cb9936cb8f69105c0#n49) on the backend which gave me a macro to dump the desired SVG directly into the page. + +And if anyone at Google is reading, you should try recommending these strategies +for speeding up pages instead of pushing self-serving faux standards like AMP. diff --git a/content/blog/DRM-leasing-and-VR-for-Wayland.md b/content/blog/DRM-leasing-and-VR-for-Wayland.md @@ -0,0 +1,478 @@ +--- +date: 2019-08-09 +title: "DRM leasing: VR for Wayland" +layout: post +tags: [wayland] +--- + +As those who read my [status updates](/2019/07/15/Status-update-July-2019.html) +have been aware, recently I've been working on bringing VR to Wayland (and vice +versa). The deepest and most technical part of this work is *DRM leasing* +(Direct Rendering Manager, *not* Digital Restrictions Management), and I think +it'd be good to write in detail about what's involved in this part of the +effort. This work has been sponsored by [Status.im](https://status.im/), as part +of an effort to build a comprehensive Wayland-driven VR workspace. When we got +started, most of the plumbing was missing for VR headsets to be useful on +Wayland, so this has been my focus for a while. The result of this work is +summed up in this crappy handheld video: + +<video src="https://yukari.sr.ht/steamvr.webm" controls> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> + +Keith Packard, a long time Linux graphics developer, [wrote several blog posts +documenting his work implementing this feature for +X11](https://keithp.com/blogs/DRM-lease/). My journey was somewhat similar, +though thanks to his work I was able to save a lot of time. The rub of this idea +is that the Wayland compositor, the DRM (Direct Rendering Manager) master, can +"lease" some of its resources to a client so they can drive your display +directly. DRM is the kernel subsystem we use for enumerating and setting modes, +allocating pixel buffers, and presenting them in sync with the display's refresh +rate. For a number of reasons, minimizing latency being an important one, VR +applications prefer to do these tasks directly rather than be routed through the +display server like most applications are. The main tasks for implementing this +for Wayland were: + +1. Draft a [protocol extension][wl-ext] for issuing DRM leases +1. Write implementations for [wlroots][wlr-pr] and [sway][sway-pr] +1. Get a [simple test client][kmscube] working +1. Draft a Vulkan extension for leasing via Wayland +1. Write an implementation for [Mesa's Vulkan WSI implementation][wsi] +1. Get a more complex [Vulkan test client][xrgears] working +1. Add support to [Xwayland][xwayland] + +[wl-ext]: https://lists.freedesktop.org/archives/wayland-devel/2019-July/040768.html +[wlr-pr]: https://github.com/swaywm/wlroots/pull/1730 +[sway-pr]: https://github.com/swaywm/sway/pull/4289 +[kmscube]: https://git.sr.ht/~sircmpwn/kmscube +[wsi]: https://gitlab.freedesktop.org/mesa/mesa/merge_requests/1509 +[xrgears]: https://git.sr.ht/~sircmpwn/xrgears +[xwayland]: https://gitlab.freedesktop.org/xorg/xserver/merge_requests/248 + +Let's break down exactly what was necessary for each of these steps. + +## Wayland protocol extension + +Writing a protocol extension was the first order of business. There was an +[earlier attempt][original proposal] which petered off in January. I started +with this, by cleaning it up based on my prior experience writing protocols, +normalizing much of the terminology and style, and cleaning up the state +management. After some initial rounds of review, there were some questions to +answer. The most important ones were: + +- How do we identify the display? Should we send the EDID, which may be + bigger than the maximum size of a Wayland message? +- Are there security concerns? Could malicious clients read from framebuffers + they weren't given a lease for? + +The EDID I ended up sending in a side channel (file descriptor to shared +memory), and the latter was proven to be a non-issue by writing a malicious +client and demonstrating that the kernel rejects its attempts to do evil. + +```xml +<event name="edid"> + <description summary="edid"> + The compositor may send this event once the connector is created to + provide a file descriptor which may be memory-mapped to read the + connector's EDID, to assist in selecting the correct connectors + for lease. The fd must be mapped with MAP_PRIVATE by the recipient. + + Note that not all displays have an EDID, and this event will not be + sent in such cases. + </description> + <arg name="edid" type="fd" summary="EDID file descriptor" /> + <arg name="size" type="uint" summary="EDID size, in bytes"/> +</event> +``` + +A few more changes would happen to this protocol in the following weeks, but +this was good enough to move on to... + +[original proposal]: https://lists.freedesktop.org/archives/wayland-devel/2018-January/036652.html + +## wlroots & sway implementation + +After a chat with Scott Anderson (the maintainer of DRM support in wlroots) and +thanks to his timely refactoring efforts, the stage was well set for introducing +this feature to wlroots. I had a good idea of how it would take shape. [Half of +the work][state machine] - the state machine which maintains the server-side +view of the protocol - is well trodden ground and was fairly easy to put +together. Despite being a well-understood problem in the wlroots codebase, these +state machines are always a bit tedious to implement correctly, and I was still +to flushing out bugs well into the remainder of this workstream. + +[state machine]: https://github.com/swaywm/wlroots/pull/1730/files#diff-77b17feac8a8af251811a20e5b9bbdd1 + +The other half of this work was in [the DRM subsystem][drm subsystem]. We +decided that we'd have leased connectors appear "destroyed" to the compositor, +and thus the compositor would have an opportunity to clean it up and stop using +them, similar to the behavior of when an output is hotplugged. Further changes +were necessary to have the DRM internals elegantly carry around some state for +the leased connector and avoid using the connector itself, as well as dealing +with the termination of the lease (either by the client or by the compositor). +With all of this in place, it's a [simple matter][lease issuance] to enumerate +the DRM object IDs for all of the resources we intend to lease and issue the +lease itself. + +```c +int nobjects = 0; +for (int i = 0; i < nconns; ++i) { + struct wlr_drm_connector *conn = conns[i]; + assert(conn->state != WLR_DRM_CONN_LEASED); + nobjects += 0 + + 1 /* connector */ + + 1 /* crtc */ + + 1 /* primary plane */ + + (conn->crtc->cursor != NULL ? 1 : 0) /* cursor plane */ + + conn->crtc->num_overlays; /* overlay planes */ +} +if (nobjects <= 0) { + wlr_log(WLR_ERROR, "Attempted DRM lease with <= 0 objects"); + return -1; +} +wlr_log(WLR_DEBUG, "Issuing DRM lease with the %d objects:", nobjects); +uint32_t objects[nobjects + 1]; +for (int i = 0, j = 0; i < nconns; ++i) { + struct wlr_drm_connector *conn = conns[i]; + objects[j++] = conn->id; + objects[j++] = conn->crtc->id; + objects[j++] = conn->crtc->primary->id; + wlr_log(WLR_DEBUG, "connector: %d crtc: %d primary plane: %d", + conn->id, conn->crtc->id, conn->crtc->primary->id); + if (conn->crtc->cursor) { + wlr_log(WLR_DEBUG, "cursor plane: %d", conn->crtc->cursor->id); + objects[j++] = conn->crtc->cursor->id; + } + if (conn->crtc->num_overlays > 0) { + wlr_log(WLR_DEBUG, "+%zd overlay planes:", conn->crtc->num_overlays); + } + for (size_t k = 0; k < conn->crtc->num_overlays; ++k) { + objects[j++] = conn->crtc->overlays[k]; + wlr_log(WLR_DEBUG, "\toverlay plane: %d", conn->crtc->overlays[k]); + } +} +int lease_fd = drmModeCreateLease(backend->fd, + objects, nobjects, 0, lessee_id); +if (lease_fd < 0) { + return lease_fd; +} +wlr_log(WLR_DEBUG, "Issued DRM lease %d", *lessee_id); +for (int i = 0; i < nconns; ++i) { + struct wlr_drm_connector *conn = conns[i]; + conn->lessee_id = *lessee_id; + conn->crtc->lessee_id = *lessee_id; + conn->state = WLR_DRM_CONN_LEASED; + conn->lease_terminated_cb = lease_terminated_cb; + conn->lease_terminated_data = lease_terminated_data; + wlr_output_destroy(&conn->output); +} +return lease_fd; +``` + +[drm subsystem]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4b +[lease issuance]: https://github.com/swaywm/wlroots/pull/1730/files#diff-8b05a774317ee8e87d51422170f82d4bR1601 + +The [sway implementation][sway-pr] is very simple. I added a note in wlroots +which exposes whether or not an output is considered "non-desktop" (a property +which is set for most VR headsets), then sway just rigs up the lease manager and +offers all non-desktop outputs for lease. + +## kmscube + +Testing all of this required the use of a simple test client. During his earlier +work, Keith wrote some patches on top of +[kmscube](https://gitlab.freedesktop.org/mesa/kmscube/), a simple Mesa demo +which renders a spinning cube directly via DRM/KMS/GBM. A [few simple +tweaks][kmscube patch] was suitable to get this working through my protocol +extension, and for the first time I saw something rendered on my headset through +sway! + +<video src="https://yukari.sr.ht/vr.webm" controls> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> + +[kmscube patch]: https://git.sr.ht/~sircmpwn/kmscube/commit/60d89ef1d9304427a1289174d9a311ab06e39b44 + +## Vulkan + +Vulkan has a subsystem called WSI - Window System Integration - which handles +the linkage between Vulkan's rendering process and the underlying window system, +such as Wayland, X11, or win32. Keith added an extension to this system called +[VK_EXT_acquire_xlib_display][VK_EXT_acquire_xlib_display], which lives on top +of [VK_EXT_direct_mode_display][VK_EXT_direct_mode_display], a system for +driving displays directly with Vulkan. As the name implies, this system is +especially X11-specific, so I've drafted my own VK extension for Wayland: +VK_EXT_acquire_wl_display. This is the crux of it: + +[VK_EXT_acquire_xlib_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_acquire_xlib_display +[VK_EXT_direct_mode_display]: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_EXT_direct_mode_display + +```xml +<command successcodes="VK_SUCCESS" errorcodes="VK_ERROR_INITIALIZATION_FAILED"> + <proto><type>VkResult</type> <name>vkAcquireWaylandDisplayEXT</name></proto> + <param><type>VkPhysicalDevice</type> <name>physicalDevice</name></param> + <param>struct <type>wl_display</type>* <name>display</name></param> + <param>struct <type>zwp_drm_lease_manager_v1</type>* <name>manager</name></param> + <param><type>int</type> <name>nConnectors</name></param> + <param><type>VkWaylandLeaseConnectorEXT</type>* <name>pConnectors</name></param> +</command> +``` + +I chose to leave it up to the user to enumerate the leasable connectors from the +Wayland protocol, then populate these structs with references to the connectors +they want to lease: + +```xml +<type category="struct" name="VkWaylandLeaseConnectorEXT"> + <member>struct <type>zwp_drm_lease_connector_v1</type>* <name>pConnectorIn</name></member> + <member><type>VkDisplayKHR</type> <name>displayOut</name></member> +</type> +``` + +Again, this was the result of some iteration and design discussions with other +folks knowledgable in these topics. I owe special thanks to Daniel Stone for +sitting down with me (figuratively, on IRC) and going over ideas for how to +design the Vulkan API. Armed with this specification, I now needed a Vulkan +driver which supported it. + +## Implementing the VK extension in Mesa + +[Mesa](https://www.mesa3d.org/) is the premier free software graphics suite +powering graphics on Linux and other operating systems. It includes an +implementation of OpenGL and Vulkan for several GPU vendors, and is the home of +the userspace end of AMDGPU, Intel, nouveau, and other graphics drivers. A +specification is nothing without its implementation, so I set out to +implementing this extension for Mesa. In the end, it turned out to be much +simpler than the corresponding X version. This is the complete code for the WSI +part of this feature: + +```c +static void drm_lease_handle_lease_fd( + void *data, + struct zwp_drm_lease_v1 *zwp_drm_lease_v1, + int32_t leased_fd) +{ + struct wsi_display *wsi = data; + wsi->fd = leased_fd; +} + +static void drm_lease_handle_finished( + void *data, + struct zwp_drm_lease_v1 *zwp_drm_lease_v1) +{ + struct wsi_display *wsi = data; + if (wsi->fd > 0) { + close(wsi->fd); + wsi->fd = -1; + } +} + +static const struct zwp_drm_lease_v1_listener drm_lease_listener = { + drm_lease_handle_lease_fd, + drm_lease_handle_finished, +}; + +/* VK_EXT_acquire_wl_display */ +VkResult +wsi_acquire_wl_display(VkPhysicalDevice physical_device, + struct wsi_device *wsi_device, + struct wl_display *display, + struct zwp_drm_lease_manager_v1 *manager, + int nConnectors, + VkWaylandLeaseConnectorEXT *connectors) +{ + struct wsi_display *wsi = + (struct wsi_display *) wsi_device->wsi[VK_ICD_WSI_PLATFORM_DISPLAY]; + + /* XXX no support for mulitple leases yet */ + if (wsi->fd >= 0) + return VK_ERROR_INITIALIZATION_FAILED; + + /* XXX no support for mulitple connectors yet */ + /* The solution will eventually involve adding a listener to each + * connector, round tripping, and matching EDIDs once the lease is + * granted. */ + if (nConnectors > 1) + return VK_ERROR_INITIALIZATION_FAILED; + + struct zwp_drm_lease_request_v1 *lease_request = + zwp_drm_lease_manager_v1_create_lease_request(manager); + for (int i = 0; i < nConnectors; ++i) { + zwp_drm_lease_request_v1_request_connector(lease_request, + connectors[i].pConnectorIn); + } + + struct zwp_drm_lease_v1 *drm_lease = + zwp_drm_lease_request_v1_submit(lease_request); + zwp_drm_lease_request_v1_destroy(lease_request); + zwp_drm_lease_v1_add_listener(drm_lease, &drm_lease_listener, wsi); + wl_display_roundtrip(display); + + if (wsi->fd < 0) + return VK_ERROR_INITIALIZATION_FAILED; + + int nconn = 0; + drmModeResPtr res = drmModeGetResources(wsi->fd); + drmModeObjectListPtr lease = drmModeGetLease(wsi->fd); + for (uint32_t i = 0; i < res->count_connectors; ++i) { + for (uint32_t j = 0; j < lease->count; ++j) { + if (res->connectors[i] != lease->objects[j]) { + continue; + } + struct wsi_display_connector *connector = + wsi_display_get_connector(wsi_device, res->connectors[i]); + /* TODO: Match EDID with requested connector */ + connectors[nconn].displayOut = + wsi_display_connector_to_handle(connector); + ++nconn; + } + } + drmModeFreeResources(res); + + return VK_SUCCESS; +} +``` + +Rigging it up to each driver's WSI shim is pretty straightforward from this +point. I only did it for radv - AMD's Vulkan driver (cause that's the hardware I +was using at the time) - but the rest should be trivial to add. Equipped with a +driver in hand, it's time to make a Real VR Application work on Wayland. + +## xrgears + +[xrgears](https://gitlab.com/lubosz/xrgears) is another simple demo application +like kmscube - but designed to render a VR scene. It leverages Vulkan and +[OpenHMD](http://www.openhmd.net/) (Open Head Mounted Display) to display this +scene and stick the camera to your head. With the Vulkan extension implemented, +it was a fairly simple matter to [rig up a Wayland backend][xrgears-patch]. The +result: + +[xrgears-patch]: https://git.sr.ht/~sircmpwn/xrgears/commit/41ef1d1dfe3e56766d1f8b72b335567eb7842d04 + +<video src="https://yukari.sr.ht/xrgears.webm" controls> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> + +## Xwayland + +The final step was to integrate this extension with Xwayland, so that X +applications which took advantage of Keith's work would work via Xwayland. This +ended up being more difficult than I expected for one reason in particular: +modes. Keith's Vulkan extension is designed in two steps: + +1. Convert an RandR output into a VkDisplayKHR +2. Acquire a lease for a set of VkDisplayKHRs + +Between these steps, you can query the modes (available resolutions and refresh +rates) of the display. However, the Wayland protocol I designed does not let you +query modes until *after* you get the DRM handle, at which point you should +query them through DRM, thus reducing the number of sources of truth and +simplifying things considerably. This is arguably a design misstep in the +original Vulkan extension, but it's shipped in a lot of software and is beyond +fixing. So how do we deal with it? + +One way (which was suggested at one point) would be to change the protocol to +include the relevant mode information, so that Xwayland could populate the RandR +modes from it. I found this distasteful, because it was making the protocol more +complex for the sake of a legacy system. Another option would be to make a +second protocol which includes this extra information especially for Xwayland, +but this also seemed like a compromise that compositors would rather not make. +Yet another option would be to have Xwayland request a lease with zero objects +and scan connectors itself, but zero-object leases are not possible. + +The option I ended up going with is to have Xwayland open the DRM device itself +and scan connectors there. This is less palatable because (1) we can't be sure +which DRM device is correct, and (2) we can't be sure Xwayland will have +permission to read it. We're still not sure how best to solve this in the long +term. As it stands, this approach is sufficient to get it working in the common +case. The code looks something like this: + +```c +static RRModePtr * +xwl_get_rrmodes_from_connector_id(int32_t connector_id, int *nmode, int *npref) +{ + drmDevicePtr devices[1]; + drmModeConnectorPtr conn; + drmModeModeInfoPtr kmode; + RRModePtr *rrmodes; + int drm; + int pref, i; + + *nmode = *npref = 0; + + /* TODO: replace with zero-object lease once kernel supports them */ + if (drmGetDevices2(DRM_NODE_PRIMARY, devices, 1) < 1 + || !*devices[0]->nodes[0]) { + ErrorF("Failed to enumerate DRM devices"); + return NULL; + } + drm = open(devices[0]->nodes[0], O_RDONLY); + drmFreeDevices(devices, 1); + + conn = drmModeGetConnector(drm, connector_id); + if (!conn) { + close(drm); + ErrorF("drmModeGetConnector failed"); + return NULL; + } + rrmodes = xallocarray(conn->count_modes, sizeof(RRModePtr)); + if (!rrmodes) { + close(drm); + ErrorF("Failed to allocate connector modes"); + return NULL; + } + + /* This spaghetti brought to you courtesey of xf86RandrR12.c + * It adds preferred modes first, then non-preferred modes */ + for (pref = 1; pref >= 0; pref--) { + for (i = 0; i < conn->count_modes; ++i) { + kmode = &conn->modes[i]; + if ((pref != 0) == ((kmode->type & DRM_MODE_TYPE_PREFERRED) != 0)) { + xRRModeInfo modeInfo; + RRModePtr rrmode; + + modeInfo.nameLength = strlen(kmode->name); + + modeInfo.width = kmode->hdisplay; + modeInfo.dotClock = kmode->clock * 1000; + modeInfo.hSyncStart = kmode->hsync_start; + modeInfo.hSyncEnd = kmode->hsync_end; + modeInfo.hTotal = kmode->htotal; + modeInfo.hSkew = kmode->hskew; + + modeInfo.height = kmode->vdisplay; + modeInfo.vSyncStart = kmode->vsync_start; + modeInfo.vSyncEnd = kmode->vsync_end; + modeInfo.vTotal = kmode->vtotal; + modeInfo.modeFlags = kmode->flags; + + rrmode = RRModeGet(&modeInfo, kmode->name); + if (rrmode) { + rrmodes[*nmode] = rrmode; + *nmode = *nmode + 1; + *npref = *npref + pref; + } + } + } + } + + close(drm); + return rrmodes; +} + +``` + +A simple update to the Wayland protocol was necessary to add the `CONNECTOR_ID` +atom to the RandR output, which is used by Mesa's Xlib WSI code for acquiring +the display, and was reused here to line up a connector offered by the Wayland +compositor with a connector found in the kernel. The [rest of the +changes][xwayland] were pretty simple, and the result is that SteamVR works, +capping everything off nicely: + +<video src="https://yukari.sr.ht/steamvr.webm" controls> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> diff --git a/content/blog/Decentralize-decentralize-decentralize.md b/content/blog/Decentralize-decentralize-decentralize.md @@ -0,0 +1,145 @@ +--- +date: 2018-03-24 +layout: post +title: Achtung! Decentralize, decentralize, decentralize! +tags: [philosophy, privacy] +--- + +I can hardly believe it, but the media is finally putting Facebook's feet to the +fire! No longer is it just the weird paranoid kids shouting at everyone to stop +giving all of their information to these companies. We need to take this bull by +the horns and drive it in a productive direction, and for that reason, it's time +to talk about decentralization, federation, and open source. + +*This article has been [translated into +Russian](http://getcolorings.com/ru-decentralize) by [Get +Colorings](http://getcolorings.com).* + +It's important to remember that Facebook is not the only villain on this stage. +Did you know that Google keeps [a map of everywhere you've +been](https://www.google.com/maps/timeline?pb)? That Twitter is analyzing your +tweets just like Facebook does, and sells it to advertisers just like Facebook +does? Virtually all internet companies - Snapchat, Tinder, Uber &amp; Lift, and +even more - are spying on you and selling it to advertisers. It's so lucrative +and easy to do this that it's become an *industry standard practice*. + +The solution to the Facebook problem is not jumping ship to another centralized +commercial platform. They will be exactly the same. The commercial model for +internet services is inherently flawed. Companies like Facebook, publicly +traded, have a legal obligation to maximize profits for their shareholders. +Private companies with investors are similarly obligated. Nowhere in the +equation does it say that they're obligated to do *anything* for you - the only +role you serve is to be a vehicle for exploitation. + +You need to find services whose incentives are aligned with yours. What asks do +you have from your social media platforms? It probably starts with basic things: + +- I want to keep up with my family and friends +- I want my family and friends to be able to keep up with me + +But if you're smart, you might have some deeper asks: + +- I don't want my personal information sold to others +- I don't want to be manipulated into spending my money + +We might even have some asks as a *society*, too: + +- We don't want to be manipulated into hating our countrymen +- We don't want to have our people's opinions radicalized + +Each company I've mentioned, and many more, may offer you some subset of these +promises. But *in every case*, they will have conditions: + +- **We'll help you keep up with family and friends**, or at least the subset + of them that we think makes you more profitable. +- **We'll help your family and friends keep up with you**, so long as your posts + are engaging enough to keep them looking at our ads. +- **Your personal information won't be sold to others**, unless we can get away + with it. +- **You won't be manipulated into spending your money**, unless we can + manipulate you into spending it on us. +- **We won't manipulate you into hating your countrymen**, unless it makes you + spend more time using our platform to express your hatred. +- **We won't radicalize your opinions**, at least not the ones that don't get + you angry enough to spend more time looking at our ads. + +I'm not just being cynical here. There is no promise that a company can make to +its users that outweighs the [fiduciary +duty](https://legal-dictionary.thefreedictionary.com/fiduciary+duty) that +*obligates* them to maximize profits by any means. The only defense of this is +legislation and consumer choice. We must pass laws that defend users and we must +choose not to engage with companies that behave like this. + +We must do both of these things, but for now I'm going to focus on the consumer +choice. We must throw our lot in with the alternative to these corporations - +decentralized, federated, open source platforms. + +What do each of these terms mean? + +*Decentralized* means that the platform is, well, not *centralized*. Rather +than the control being in the hands of one company (or a single interested +party, to generalize it a bit), control is in the hands of many independent +operators. + +*Federated* refers to a means by which several service operators can +communicate with each other in standard ways. This approach prevents +platform lock-in. Email is a federated system - you can send an email from +your gmail account to your mother's old AOL account. Contrast this to Facebook, +where you can't follow your friend's Twitter account. + +Finally, *open source*[^1] is a term used by the technology community to refer +to the free distribution of the secret sauce that makes our services tick. The +technology engineering community collectively works on these projects and freely +shares this work with everyone else. + +The combination of all of these ideas in one piece of software is the golden +ticket to internet freedom. This is the approach to social networking taken most +famously by [Mastodon](http://joinmastodon.org/). Mastodon is a decentralized, +federated, and open source platform. The computing infrastructure the platform +runs on is operated by thousands of independent volunteers (decentralized), +which all communicate with each other and other software using standard +protocols (federated), and the [source +code](https://github.com/tootsuite/mastodon) is freely available for anyone to +use and improve (open source)[^2]. + +The incentives of the operators are aligned with the incentives of the users on +Mastodon. The operator of each instance is a human being who can be easily +reached to give feedback and thanks, rather than a billionaire egomaniac who buys +an entire neighborhood so no one can bother him. Because the costs of +maintaining this social network are distributed across thousands of operators, +each one has a very low cost of operation, which is usually easily covered by +donations from the users who they support. There are no investors to please. +Just the users. + +Mastodon fills a Twitter-like niche. There are other platforms attempting to +fill other niches - [diaspora*](https://diasporafoundation.org/) is easily +compared to Facebook, for example. +[PeerTube](https://github.com/Chocobozzz/PeerTube) is under development to +fulfill a YouTube-like niche, too. These platforms need our support. + +Commercial platforms don't respect you. You may have grown used to skimming over +ads and content you don't want to see on Facebook and other platforms. It's an +annoyance that you've internalized because, well, what else can you do? There +are no ads on Mastodon. It doesn't need them, and you deserve better than them. + +<p style="text-align: center">---</p> + +Remember, Facebook is not the only evil. It's time to discard proprietary +platforms like the manipulative trash they are. Take the anger you've felt at +Facebook these past couple of weeks and use it to embrace decentralization, +federation, and open source. + +I know it seems a monumental task to untangle your life from these companies, +but you don't have to do it all at once. If this article moved you, make a todo +list right now. List each way in which you're tied to some platform - you use +Facebook to talk to your friends, or use gmail for your email address, your +contacts are stored on Google, you use Facebook's calendar for social events, +you have a Twitter account you haven't moved... then take on each task one at a +time. Take as much time as you need. As you research these options, if you find +the open options lacking, let the people involved know what your needs are. If +there's no open option at all, please [email me](mailto:sir@cmpwn.com) about it. + +We can do this. We can be free. + +[^1]: There is some debate about the use of the term "open source" as opposed to another term, "free software". There is a time and a place for this discussion, but it's not here, and our message weakens if we expose the general public to our bickering. +[^2]: There are actually several competing and compatible softwares that federate with the same social network Mastodon uses. This is very similar to how several different email providers are compatible with each other and compete to innovate together. diff --git a/content/blog/Dependencies-and-maintainers.md b/content/blog/Dependencies-and-maintainers.md @@ -0,0 +1,74 @@ +--- +date: 2020-02-06 +layout: post +title: Dependencies and maintainers +tags: [philosophy, free software, maintainership] +--- + +I'm 34,018 feet over the Atlantic at the moment, on my way home from FOSDEM. It +was as always a lovely event, with far too many events of interest for any +single person to consume. One of the few talks I was able to attend[^1] left a +persistent worm of thought in my brain. This talk was put on by representatives +of Microsoft and GitHub and discusses whether or not there is a sustainability +problem in open source ([link][fosdem archive]). The content of the talk, +interpreted within the framework in which it was presented, was moderately +interesting. It was more fascinating to me, however, as a lens for interpreting +GitHub's (and, indirectly, Microsoft's) approach to open source, and of the +mindset of developers who approach problems in the same ways. + +[fosdem archive]: https://fosdem.org/2020/schedule/event/foss_sustainability_issues/ +[^1]: And strictly speaking I even had to slip in under the radar to attend in the first place &mdash; the room was full. + +The presenters drew attention to a few significant crises open-source +communities have faced in recent years &mdash; left-pad, in which a trivial +library was removed from npm and was unknowingly present in thousands of +dependency graphs; event-stream, in which a maintainer transferred project +ownership to an unknown individual who added crypto mining; and heartbleed, in +which a bug in a critical security library caused mass upgrades and panic +&mdash; and asks whether or not these can be considered sustainability issues. +The talk has a lot to dissect and will frame my thinking and writings for a +while. Today I'll focus on one specific problem, which I called attention to +during the Q&A. + +At a few points, the presenters spoke from the perspective of a business which +depends on up to thousands of open-source libraries or tools. In such a context, +how do you prioritize which of your thousands of dependencies requires +attention, for financial support, contributions upstream, and so on? I found +this worldview dissonant, and asked the following question: "why do you have +thousands of dependencies in the first place?" Because this approach seems to be +fast becoming the norm, this may seem like a stupid question.[^2] If any Node +developers are reading, scan through your nearest node_modules directory and see +how many of these dependencies you've even heard of. + +[^2]: If so, you may be pleased by a Microsoft's ridiculous answer: "we have 60,000 developers, that's why." + +Such an environment is primed to fail in the ways enumerated by this talk. +Consider the case of the maintainer who lost interest and gave their project to +an untrusted third party. If I had depended on this library, I would have +noticed long ago that the project was effectively unmaintained. It's likely that +I or my peers would have sent patches to this project, given that bugfixes would +have stopped coming from upstream. We would be aware of the larger risk this +project posed, and have studied alternatives. Earlier than that, I would +probably have lent my ear to the maintainer to vent their frustrations, and +offered my help where possible. + +For most of my projects, I can probably list the entire dependency graph, +including transitive dependencies, off of the top of my head. I can name most of +their maintainers, and many of their contributors. I have shaken the hands of +these people, shared drinks and meals with them, and count many of them among my +close friends. The idea of depending on a library I've never heard of, several +degrees removed via transitive dependencies, maintained by someone I've never +met and have no intention of speaking to, is *absolutely nuts* to me. I know of +these problems well in advance because I know the people affected as my friends. +If someone is frustrated or overworked, I'm right there with them trying to find +solutions and correct the over-burden. If someone is in dire financial +straights, I'm helping them touch up their resume and introducing them to +companies that I know are looking for their skillset, or helping them work on +more sustainable sources of donations and grants. They do the same for me, and +for each other. + +Quit treating open-source projects like a black box that conveniently solves +your problem. Engage with the human beings who work on it, participate in the +community, and *make* it healthy and sustainable. You shouldn't be surprised +when the 3 AM alarm goes off if the most you see of a project is a line in your +`package.json`. diff --git a/content/blog/Designing-a-replacement-part-for-my-truck.md b/content/blog/Designing-a-replacement-part-for-my-truck.md @@ -0,0 +1,82 @@ +--- +date: 2020-03-25 +title: Designing and 3D printing a new part for my truck +layout: post +--- + +I drove a car daily for many years while I was living in Colorado, California, +and New Jersey, but since I moved to Philadelphia I have not needed a car. The +public transit here is not great, but it's good enough to get where I need to be +and it's a lot easier than worrying about parking a car. However, in the past +couple of years, I have been moving more and more large server parts back and +forth to the datacenter for SourceHut. I've also developed an interest in +astronomy, which benefits from being able to carry large equipment to remote +places. These reasons, among others, put me into the market for a vehicle once +again. + +I think of a vehicle strictly as a functional tool. Some creature comforts are +nice, but I consider them optional. Instead, I prioritize utility. A truck makes +a lot of sense &mdash; lots of room to carry things in. And, given my expected +driving schedule of "not often", I wasn't looking to spend a lot of money or +get a loan. There are other concerns: modern cars are very complicated machines, +and many have lots of proprietary computerized components which make end-user +maintenance very difficult. Sometimes manufacturers even use cryptography and +legal threats to bring cars into their dealerships, bullying out third-party +repairs. + +To avoid these, I got an older truck: a 1984 Dodge D250. It's a much simpler +machine than most modern cars, and learning how to repair and maintain it is +something I can do in my spare time. + +It's an old truck, and the previous owners were not negligent, but also didn't +invest a lot of time or money in the vehicle's upkeep. The first problem I hit +was the turn signal lever snapping and becoming slack, which I fixed by pulling +open the steering column, re-aligning the lever, and tightening an internal +screw. The more interesting problem, however, was this: + +![Picture of a broken latch on the window over the truck bay](https://l.sr.ht/OWVw.jpg) + +This plastic part holds an arm in place, which is engaged by a lever in the +center of the window which folds closed over the truck bay. It's used to hold +the window in place and provides a weak locking mechanism. When the arm is +allowed to move freely, it can clang around while I'm driving, and can make +opening the truck bay a frustrating procedure. I have been looking for a reason +to learn how to use [solvespace](http://solvespace.com/index.pl), and this +seemed like a great start. + +I ordered a caliper[^1] and measured the dimensions of the broken part, and took +pictures of it from several angles for later reference. I took some notes: + +[^1]: Oh man, I've always wanted a caliper, and now I have an excuse! + +![Picture of my notes on the measurements of the part](https://l.sr.ht/20eR.jpg) + +Then, I used solvespace to design the following part: + +![Screenshot of the replacement part in solvespace](https://l.sr.ht/rVPV.png) + +This was the third iteration &mdash; I printed one version, brought it out to +the truck to compare with the broken part, made refinements to the design, then +rinse and repeat. Here's an earlier revision being compared with the broken +piece: + +![A hand holds up a 3D printed part for comparison with the original](https://l.sr.ht/CUPM.jpg) + +Finally, I arrived at a design I liked and sent it to the printer. + +![Picture of my 3D printer working on the part](https://l.sr.ht/xh9h.jpg) + +I took some pliers to the remaining plastic bits from the broken part, and sawed +off the rivets. I attached the replacement with superglue and ta-da! + +![Picture of the replacement part in place](https://l.sr.ht/3AGi.jpg) + +If the glue fails, I'll drill out what's left of the rivets and secure it with +screws. This may require another revision of the design, which will also give me +a chance to address some minor shortcomings. I don't expect to need this, +though, because this is not part under especially high stress. + +You can get the CAD files and an STL from my [repository +here](https://git.sr.ht/~sircmpwn/open-dodge-d250), which I intend to keep +updating as I learn more about this truck and encounter more fun problems to +solve. diff --git a/content/blog/Developers-shouldnt-distribute.md b/content/blog/Developers-shouldnt-distribute.md @@ -0,0 +1,74 @@ +--- +date: 2019-12-09 +layout: post +title: Developers shouldn't distribute their own software +tags: [practices] +--- + +An oft-heard complaint about Linux is that software distribution often takes +several forms: a Windows version, a macOS version, and... a Debian version, an +Ubuntu version, a Fedora version, a CentOS version, an openSUSE version... but +these complaints miss the point. The true distributable form for Linux software, +and rather for Unix software in general, is a .tar.gz file containing the source +code. + +**Note**: This article presumes that proprietary/nonfree software is irrelevant, +and so should you. + +That's not to imply that end-users should take this tarball and run `./configure +&& make && sudo make install` themselves. Rather, the responsibility for +end-user software distribution is on the distribution itself. That's why we call +it a *distribution*. This relationship may feel like an unnecessary middleman to +the software developer who just wants to get their code into their user's hands, +but on the whole this relationship has far more benefits than drawbacks. + +As the old complaint would suggest, there are hundreds of variants of Linux +alone, not to mention the BSD flavors and any novel new OS that comes out next +week. Each of these environments has its own take on how the system as a whole +should be organized and operate, and it's a fools' errand for a single team to +try and make sense of it all. More often than not, software which tries to field +this responsibility itself sticks out like a sore thumb on the user's operating +system, totally flying in the face the conventions set out by the distribution. + +Thankfully, each distro includes its own set of volunteers dedicated to this +specific job: packaging software for the distribution and making sure it +conforms to the norms of the target environment. This model also adds a set of +checks and balances to the system, in which the distro maintainers can audit +each other's work for bugs and examine the software being packaged for +anti-features like telemetry or advertisements, patching it out as necessary. +These systems keep malware out of the repositories, handle distribution of +updates, cryptographically verifying signatures, scaling the distribution out +across many mirrors - it's a robust system with decades of refinement. + +The difference in trust between managed software repositories like Debian, +Alpine Linux, Fedora, and so on; and unmanaged software repositories like PyPI, +npm, Chrome extensions, the Google Play store, Flatpak, etc &mdash; is starkly +obvious. Debian and its peers are full of quality software which integrates well +into the host system and is free of malware. Unmanaged repositories, however, +are [constant sources][pypi malware] for crapware and malware. I don't trust +developers to publish software with my best interests in mind, and developers +shouldn't ask for that level of trust. It's only through a partnership with +distributions that we can build a mutually trustworthy system for software +distribution. + +[pypi malware]: https://www.zdnet.com/article/two-malicious-python-libraries-removed-from-pypi/ + +Some developers may complain that distros ship their software too slowly, but +you shouldn't sweat it. End-user distros ship updates reasonably quickly, and +server distros ship updates at a schedule which meets the user's needs. This +inconsistent pace in release schedules among free software distributions is a +feature, not a bug, and allows the system to work to the needs of its specific +audience. You should use a distro that ships updates to *you* at the pace you +wish, and let your users do the same. + +So, to developers: just don't worry about distribution! Stick a tarball on your +release page and leave the rest up to distros. And to users: install packages +from your distro's repositories, and learn how its packaging process works so +you can get involved when you find a package missing. It's not as hard as it +looks, and they could use your help. For my part, I work both as a developer, +packager, and end-user, publishing my software as tarballs, packaging some of it +up for my [distro of choice][pkgs], report bugs to other maintainers, and field +requests from maintainers of other distros as necessary. Software distribution +is a social system and it works. + +[pkgs]: https://pkgs.alpinelinux.org/packages?name=&branch=edge&arch=x86_64&maintainer=Drew+DeVault diff --git a/content/blog/Dont-sign-a-CLA.md b/content/blog/Dont-sign-a-CLA.md @@ -0,0 +1,72 @@ +--- +date: 2018-10-05 +title: Don't sign a CLA +layout: post +tags: ["philosophy", "free software"] +--- + +A large minority of open-source projects come with a CLA, or Contributor License +Agreement, and require you to sign one before they'll merge your patch. These +agreements typically ask you to go above and beyond the rights you afford the +project by contributing under the license the software is distributed with. And +you should never sign one. + +Free and open source software licenses grant explicit freedoms to three groups: +the maintainers, the users, *and* the contributors. An important freedom is the +freedom to make changes to the software and to distribute these changes to the +public. The natural place to do so is by contributing to the upstream project, +something a project should be thankful for. A CLA replaces this gratitude with +an attempt to weaken these freedoms in a manner which may stand up to the letter +of the license, but is far from the spirit. + +A CLA is a kick in the groin to a contributor's good-faith contribution to the +project. Many people, myself included, contribute to open source projects under +the assumption that my contributions will help serve a project which continues +to be open source in perpetuity, and a CLA provides a means for the project +maintainers to circumvent that. What the CLA is actually used for is to give the +project maintainers the ability to relicense your work under a more restrictive +software license, up to and including making it entirely closed source. + +We've seen this happen before. Consider the Redis Labs debacle, where they +adopted the nonfree[^1] Anti-Commons Clause[^2], and used their CLA to pull along +any external contributions for the ride. As thanks for the generous time +invested by their community into their software, they yank it out from +underneath it and repurpose it to make money with an obscenely nonfree product. +Open source is a commitment to your community. Once you make it, you cannot take +it back. You don't get the benefits associated with being an open source project +if you have an exit hatch. You may argue that it's your right to do what you +want with your project, but making it open source is *explicitly waiving that +right*. + +[^1]: [Free as in freedom](/2018/08/22/Commons-clause-will-destroy-open-source.html) +[^2]: Call me petty, but I can't in good faith call it the "Commons Clause" when its purpose is to *remove* software from the commons. + +So to you, the contributor: if you are contributing to open source and you want +it to stay that way, you should not sign a CLA. When you send a patch to a +project, you are affording them the same rights they afforded you. The +relationship is one of equals. This is a healthy balance. When you sign a CLA, +you give them unequal power over you. If you're scratching an itch and just +want to submit the patch in good faith, it's easy enough to fork the project and +put up your changes in a separate place. This is a right afforded to you by +every open source license, and it's easy to do. Anyone who wants to use your +work can apply your patches on top of the upstream software. Don't sign away +your rights! + +--- + +Additional reading: [GPL as the Best Licence – Governance and Philosophy](https://blog.hansenpartnership.com/gpl-as-the-best-licence-governance-and-philosophy/) + +Some responses to the discussion around this article: + +*What about the [Apache Foundation +CLA](https://www.apache.org/licenses/cla-corporate.txt)?* This CLA is one of the +better ones, because it doesn't transfer copyright over your work to the Apache +Foundation. I have no beef with clauses 1 and 3-8. However, term 2 is too broad +and I would not sign this CLA. + +*What about the Linux kernel [developer certificate of +origin](https://elinux.org/Developer_Certificate_Of_Origin)?* I applaud the +Linux kernel's approach here. It covers their bases while still strongly +protecting the rights of the patch owner. It's a short statement with little +legalese and little fanfare to agreeing to it (just add "Signed-off By" to your +commit message). I approve. diff --git a/content/blog/Effective-project-governance.md b/content/blog/Effective-project-governance.md @@ -0,0 +1,108 @@ +--- +date: 2020-01-17 +title: A philosophy of project governance +layout: post +tags: [philosophy] +--- + +I've been in the maintainer role for dozens of projects for a while now, and +have moderated my fair share of conflicts. I've also been on the other side, +many times, as a minor contributor watching or participating in conflict within +other projects. Over the years, I've developed an approach to project governance +which I believe is lightweight, effective, and inclusive. + +I hold the following axioms to be true: + +1. Computer projects are organized by humans, creating a social system. +1. Social systems are fundamentally different from computer systems. +1. Objective rules cannot be programmed into a social system. + +And the following is true of individuals within those systems: + +1. Project leadership is in a position to do anything they want. +1. Project leadership will ultimately do whatever they want, even if they have + to come up with an interpretation of the rules which justifies it. +1. Individual contributors who have a dissonant world-view from project + leadership will never be welcome under those leaders. + +Any effective project governance model has to acknowledge these truths. To this +end, the simplest effective project governance model is a BDFL, which scales a +lot further than people might expect. + +The BDFL (Benevolent Dictator for Life) is a term which was first used to +describe Python's governance model with Guido van Rossum at the helm. The "for +life" in BDFL is, in practice, until the "dictator" resigns from their role. +Transfers of power either involve stepping away and letting lesser powers decide +between themselves how to best fill the vacuum, or simply directly appointing a +successor (or successors). In this model, a single entity is in charge &mdash; +often the person who started the project, at first &mdash; and while they may +delegate their responsibilities, they ultimately have the final say in all +matters. + +This decision-making authority derives from the BDFL. Consequently, the +project's values are a reflection of that BDFL's values. Conflict resolution and +matters of exclusion or inclusion of specific people from the project is the +direct responsibility of the BDFL. If the BDFL delegates this authority to other +groups or project members, that authority derives from the BDFL and is exercised +at their leisure, on their terms. In practice, for projects of a certain size, +most if not all of the BDFL's authority is delegated across many people, to the +point where the line between BDFL and core contributor is pretty blurred. The +relationships in the project are built on trust between individuals, not trust +in the system. + +As a contributor, you should evaluate the value system of the leadership and +make a personal determination as to whether or not it aligns with your own. If +it does, participate. If it does not, find an alternative or fork the +project.[^1] + +[^1]: Note that being able to fork is the escape hatch which makes this model fair and applicable to free & open source projects. The lack of a similarly accessible escape hatch in, for example, the governments of soverign countries, prevents this model from generalizing well. + +Consider the main competing model: a Code of Conduct as the rule of law. + +These attempt to boil subjective matters down into objectively enforcible rules. +Not even in sovereign law do we attempt this. Programmers can easily fall into +the trap of thinking that objective rules can be applied to social systems, and +that they can deal with conflict by executing a script. This is quite untrue, +and attempting to will leave loopholes big enough for bad actors to drive a +truck through. + +Additionally, governance models which provide a scripted path onto the decision +making committee can often have this path exploited by bad actors, or by people +for whom the politics are more important than the software. By implementing this +system, the values of the project can easily shift in ways the leaders and +contributors don't expect or agree with. + +The worst case can be that a contributor is ostracized due to the letter of the +CoC, but not the spirit of it. Managing drama is a sensitive, subjective issue, +but objective rules break hearts. Enough of this can burn out the leaders, +creating a bigger power vacuum, without a plan to fill it. + +In summary: + +**For leaders**: Assume good faith until proven otherwise. + +Do what you think is right. If someone is being a +dickhead<small><sup>†</sup></small>, +tell them to stop. If they don't stop, kick them out. Work with contributors +you trust to elevate their role in the project so you can delegate +responsibilities to them and have them act as good role models for the +community. If you're not good at moderating discussions or conflict resolution, +find someone who is among your trusted advisors and ask them to exercise their +skills. + +If you need to, sketch up informal guidelines to give an approximation of your +values, so that contributors know how to act and what to expect, but make it +clear that they're guidelines rather than rules. Avoid creating complex systems +of governance. Especially avoid setting up systems which create paths that +untrusted people can use to quickly weasel their way into positions of power. +Don't give power to people who don't have a stake in the project. + +**For contributors**: Assume good faith until proven otherwise. + +Do what you think is right. If someone is being a +dickhead<small><sup>†</sup></small>, +talk to the leadership about it. If you don't trust the project leadership, the +project isn't for you, and future conflicts aren't going to go your way. Be +patient with your maintainers &mdash; remember that you have the easier job. + +<small><sup>†</sup> According to your subjective definition of dickhead.</small> diff --git a/content/blog/Electron-considered-harmful.md b/content/blog/Electron-considered-harmful.md @@ -0,0 +1,101 @@ +--- +date: 2016-11-24 +# vim: set tw=80 : +layout: post +title: Electron considered harmful +tags: [electron, javascript] +--- + +Yeah, I know that "considered harmful" essays are allegedly [considered +harmful](http://meyerweb.com/eric/comment/chech.html). If it surprises you that +I'm writing one, though, you must be a new reader. Welcome! Let's get started. +If you're unfamiliar with Electron, it's some hot new tech that lets you make +desktop applications with HTML+CSS+JavaScript. It's basically a chromeless web +browser with a Node.js backend and a Chromium-based frontend. What follows is +the rant of a pissed off Unix hacker, you've been warned. + +As software engineers we have a responsibility to pick the *right* tools for the +job. In fact, that's the *most important* choice we have to make when we start a +project. When you choose Electron you get: + +* An entire copy of Chromium you'll be shipping with your app +* An interface that looks and feels nothing like the rest of the user's OS +* One of the slowest, least memory efficient, and most inelegant GUI application + platforms out there (remember, we *tolerate* frontend web development because + we have no choice, not because it is by any means *good*) + +Let's go over some case studies. + +**[lossless-cut](https://github.com/mifi/lossless-cut)** is an Electron app that +gives you a graphical UI for *two ffmpeg flags*. Seriously, the flags in +question are -ss and -t. No really, that's *[literally all it +does](https://github.com/mifi/lossless-cut/blob/master/src/ffmpeg.js#L46)*. It +doesn't even use ffmpeg to decode the video preview in the app, it's limited to +the codecs chromium supports. It also ships its own ffmpeg, so it has the +industry standard video decoding tool *right there* and doesn't use it to render +video. For the price of 200 extra MiB of disk space and an entire Chromium process +in RAM and on your CPU, you get a less capable GUI that saves you from having to +type the -ss and -t flags yourself. + +**[1Clipboard](http://1clipboard.io/)** is a clipboard manager. In Electron. A +*clipboard manager*. In order to show you *a list of things you've copied*, it +uses *an entire bundled copy of Chromium*. Also note that despite the promises +of Electron making cross platform development easy, it doesn't support Linux. + +**[Collectie](https://getcollectie.com/)** is a... fancy bookmark manager, I +guess? Another one that fails to get the cross platform value add from Electron, +this only supports OS X (or is it macOS). For only $10 bucks you get to organize +your shit into folders. Or you could just open the Finder for free and get a +native UX to boot. + +This is a [terminal](https://hyper.is/) written with Electron. On the landing +page they say "# A terminal emulator 100% based on JavaScript, HTML, and CSS" +like they're proud of it. They've taken one of the most lightweight and +essential tools on your computer and bloated it by orders of magnitude. Why the +fuck would you want to render Google in your god damn terminal emulator? Bonus: +also not cross platform. + +This is not to mention the dozens of companies that have taken their websites +and crammed them into a shitty electron app and called it their desktop app. +Come on guys! + +By the way, if you're the guy who's going to leave a comment about how this blog +post introduced you to a bunch of interesting apps you're going to install now, +I hate you. + +## Electron enables lazy developers to write garbage + +Let me be clear about this: JavaScript sucks. It's not the worst, but it's also +not by any means good. ES6 is a really great step forward and I'm thrilled about +how much easier it's going to be to write JavaScript, but it's still JavaScript +underneath the syntactic sugar. We use it because we have no choice (people who +know more than just JavaScript know this). The object model is whack and the +loose typing is whack and the DOM is super whack. + +When Node.js happened, a bunch of developers who never bothered to learn more +than JavaScript for their frontend work suddenly could write their crappy code +on the backend, too. Now this is happening to desktop applications. The reason +people choose Electron is because they are *too lazy* to learn the right tools +for the job. This is the *worst* quality a developer can have. You're an +engineer, for the love of God! Fucking act like one! Do they build square +airplanes so they don't have to learn about aerodynamics, then just throw on an +extra ten engines to make up for it? NO! + +For the love of God, learn something else. Learn how to use GTK or Qt. Maybe Xwt +is more up your alley. How about GNOME's Vala thing? *Learn another programming +language*. Learn Python or C/C++ or C#. Fun fact: it'll make your JavaScript +better, and once you have it in your toolbox you can make more educated +decisions on the appropriate tool to use when you face your next problem. Hint: +it's not Electron. + +## Some Electron apps don't suck + +For some use-cases Electron is a reasonable choice. + +* [Visual Studio Code](https://code.visualstudio.com/), because it's a full + blown IDE with a debugger and plugins and more. It's already gonna be + bloated. +* [Soundnode](http://www.soundnodeapp.com/), because it's not like any other + music service's app obeys your OS's UI conventions + +Uh, that's it. That's the entire list. diff --git a/content/blog/Email-driven-git.md b/content/blog/Email-driven-git.md @@ -0,0 +1,229 @@ +--- +date: 2018-07-02 +title: The advantages of an email-driven git workflow +layout: post +tags: [philosophy, mail, git] +--- + +[git 2.18.0][git 2.18.0] has been released, and with it my first contribution to +git has shipped! My patch was for a git feature which remains disappointingly +obscure: [git send-email](https://git-scm.com/docs/git-send-email). I want to +introduce my readers to this feature and speak to the benefits of using an +email-driven git workflow - the workflow git was originally designed for. + +[git 2.18.0]: https://raw.githubusercontent.com/git/git/master/Documentation/RelNotes/2.18.0.txt + +Email isn't as sexy as GitHub (and its imitators), but it has several +advantages over the latter. Email is standardized, federated, well-understood, +and venerable. A very large body of email-related software exists and is equally +reliable and well-understood. You can interact with email using only open source +software and customize your workflow at every level of the stack - filtering, +organizing, forwarding, replying, and so on; in any manner you choose. + +Git has several built-in tools for leveraging email. The first one of note is +[format-patch](https://git-scm.com/docs/git-format-patch). This can take a git +commit (or series of commits) and format them as plaintext emails with embedded +diffs. Here's a small example of its output: + +```mail +From 8f5045c871c3060ff5f5f99ce1ada09f4b4cd105 Mon Sep 17 00:00:00 2001 +From: Drew DeVault <sir@cmpwn.com> +Date: Wed, 2 May 2018 08:59:27 -0400 +Subject: [PATCH] Silently ignore touch_{motion,up} for unknown ids + +--- + types/wlr_seat.c | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/types/wlr_seat.c b/types/wlr_seat.c +index f77a492d..975746db 100644 +--- a/types/wlr_seat.c ++++ b/types/wlr_seat.c +@@ -1113,7 +1113,6 @@ void wlr_seat_touch_notify_up(struct wlr_seat *seat, uint32_t time, + struct wlr_seat_touch_grab *grab = seat->touch_state.grab; + struct wlr_touch_point *point = wlr_seat_touch_get_point(seat, touch_id); + if (!point) { +- wlr_log(L_ERROR, "got touch up for unknown touch point"); + return; + } + +@@ -1128,7 +1127,6 @@ void wlr_seat_touch_notify_motion(struct wlr_seat *seat, uint32_t time, + struct wlr_seat_touch_grab *grab = seat->touch_state.grab; + struct wlr_touch_point *point = wlr_seat_touch_get_point(seat, touch_id); + if (!point) { +- wlr_log(L_ERROR, "got touch motion for unknown touch point"); + return; + } + +-- +2.18.0 + +``` + +git format-patch is at the bottom of git's stack of outgoing email features. You +can send the emails it generates manually, but usually you'll use git send-email +instead. It logs into the SMTP server of your choice and sends the email for +you, after running git format-patch for you and giving you an opportunity to +make any edits you like. Given that most popular email clients these days are +awful and can't handle basic tasks like "sending email" properly, I strongly +recommend this tool over attempting to send format-patch's output yourself. + +<img style="max-width: 75%" src="https://sr.ht/wmKv.jpg" /> + +<p style="text-align: center; max-width: 80%; margin: 1rem auto"> + <em> + I put a notch in my keyboard for each person who ignores my advice, + struggles through sending emails manually, and eventually comes around + to letting git send-email do it for them. + </em> +</p> + +I recommend a few settings to apply to git send-email to make your workflow a +bit easier. One is `git config --global sendemail.verify off`, which turns off +a sometimes-annoying and always-confusing validation step which checks for +features only supported by newer SMTP servers - newer, in this case, meaning +more recent than November of 1995. I started a thread on the git mailing list +this week to discuss changing this option to off by default. + +You can also set the default recipient for a given repository by using a local +git config: `git config sendemail.to admin@example.org`. This lets you skip a +step if you send your patches to a consistent destination for that project, like +a mailing list. I also recommend `git config --global sendemail.annotate yes`, +which will always open the emails in your editor to allow you to make changes +(you can get this with `--annotate` if you don't want it every time). + +The main edit you'll want to make when annotating is to provide what some call +"timely commentary" on your patch. Immediately following the `---` after your +commit message, you can add a summary of your changes which can be seen by the +recipient, but doesn't appear in the final commit log. This is a useful place to +talk about anything useful regarding the testing, review, or integration of your +changes. You may also want to edit the `[PATCH]` text in the subject line to +something like `[PATCH v2]` - this can also be done with the `-v` flag as well. +I also like to add additional To's, Cc's, etc at this time. + +Git also provides tools for the recipient of your messages. One such tool is +[git am](https://git-scm.com/docs/git-am), which accepts an email prepared with +format-patch and integrates it into their repository. Several flags are provided +to assist with common integration activities, like signing off on the commit or +attempting a 3-way merge. The difficult part can be getting the email to git am +in the first place. If you simply use the GMail web UI, this can be difficult. I +use [mutt](http://www.mutt.org/), a TUI email client, to manage incoming +patches. This is useful for being able to compose replies with vim rather than +fighting some other mail client to write emails the way I want, but more +importantly it has the `|` key, which prompts you for a command to pipe the +email into. Other tools like [OfflineIMAP](http://www.offlineimap.org/) are also +useful here. + +On the subject of composing replies, reviewing patches is quite easy with the +email approach as well. Many bad, yet sadly popular email clients have +popularized the idea that the sender's message is immutable, encouraging you to +[top post][top posting] and leave an endlessly growing chain of replies +underneath your message. A secret these email clients have kept from you is that +you are, in fact, permitted by the mail RFCs to edit the sender's message as you +please when replying - a style called [bottom posting][bottom posting]. I +strongly encourage you to get comfortable doing this in general, but it's +essential when reviewing patches received over email. + +[top posting]: https://en.wikipedia.org/wiki/Posting_style#Top-posting +[bottom posting]: https://en.wikipedia.org/wiki/Posting_style#Bottom-posting + +In this manner, you can dissect the patch and respond to specific parts of it +requesting changes or clarifications. It's just email - you can reply, forward +the message, Cc interested parties, start several chains of discussion, and so +on. I recently sent the following feedback on a patch I received: + +```mail +Date: Mon, 11 Jun 2018 14:19:22 -0400 +From: Drew DeVault <sir@cmpwn.com> +To: Gregory Mullen <omitted> +Subject: Re: [PATCH 2/3 todo] Filter private events from events feed + +On 2018-06-11 9:14 AM, Gregory Mullen wrote: +> diff --git a/todosrht/alembic/versions/cb9732f3364c_clear_defaults_from_tickets_to_support_.py b/todosrht/alembic/versions/cb9732f3364c_clear_defaults_from_tickets_to_support_.py +> -%<- +> +class FlagType(types.TypeDecorator): + +I think you can safely import the srht FlagType here without implicating +the entire sr.ht database support code + +> diff --git a/todosrht/blueprints/html.py b/todosrht/blueprints/html.py +> -%<- +> +def collect_events(target, count): +> + events = [] +> + for e in EventNotification.query.filter(EventNotification.user_id == target.id).order_by(EventNotification.created.desc()): + +80 cols + +I suspect this 'collect_events' function can be done entirely in SQL +without having to process permissions in Python and do several SQL +round-trips + +> @html.route("/~<username>") +> def user_GET(username): +> - print(username) + +Whoops! Nice catch. + +> user = User.query.filter(User.username == username.lower()).first() +> if not user: +> abort(404) +> trackers, _ = get_tracker(username, None) +> # TODO: only show public events (or events the current user can see) + +Can remove the comment +``` + +Obviously this isn't the whole patch we're seeing - I've edited it down to just +the parts I want to talk about. I also chose to leave the file names in to aid +in navigating my feedback, with casual `-%<-` symbols indicating where I had +trimmed out parts of the patch. This approach is common and effective. + +The main disadvantage of email driven development is that some people are more +comfortable working with email in clients which are not well-suited to this kind +of work. Popular email clients have caused terrible ideas like HTML email to +proliferate, not only enabling spam, privacy leaks, and security +vulnerabilities, but also making it more difficult for people to write emails +that can be understood by git or tolerated by advanced email users. + +I don't think that the solution to these problems is to leave these powerful +tools hanging in the wind and move to less powerful models like GitHub's pull +requests. This is why on my own platform, [sr.ht](https://sr.ht), I chose to +embrace git's email-driven approach, and extend it with new tools that make it +easier to participate without directly using email. For those like me, I still +want the email to be there so you can dig my heels in and do it old-school, but +I appreciate that it's not for everyone. + +I started working on the sr.ht mailing list service a couple of weeks ago, which +is where these goals will be realized with new email-driven code review tools. +My friend [Simon](https://emersion.fr) has been helping out with a Python module +named [emailthreads](https://git.sr.ht/~emersion/python-emailthreads/) which can +be used to parse email discussions - with a surprising degree of accuracy, +considering the flexibility of email. Once I get these tools into a usable +state, we'll likely see sr.ht registrations finally opened to the general public +(interested in trying it earlier? [Email me](mailto:sir@cmpwn.com)). Of course, +it's all [open source](https://git.sr.ht/~sircmpwn/?search=sr.ht), so you can +follow along and try it on your own infrastructure if you like. + +Using email for git scales extremely well. The canonical project, of course, is +the Linux kernel. A change is made to the Linux kernel an average of 7 times per +hour, constantly. It is maintained by dozens of veritable clans of software +engineers hacking on dozens of modules, and email allows these changes to +efficiently flow code throughout the system. Without email, Linux's maintenance +model would be impossible. It's worth noting that git was designed for +maintaining Linux, of course. + +With the right setup, it's well suited to small projects as well. Sending a +patch along for review is a single git command. It lands directly in the +maintainer's inbox and can be integrated with a handful of keystrokes. All of +this works without any centralization or proprietary software involved. We +should embrace this! + +--- + +Related articles sent in by readers: + +[Mailing lists vs Github](https://begriffs.com/posts/2018-06-05-mailing-list-vs-github.html) +by Joe Nelson + +[You're using git wrong](https://web.archive.org/web/20180522180815/https://dpc.pw/blog/2017/08/youre-using-git-wrong/) by +Dawid Ciężarkiewicz diff --git a/content/blog/Embedding-files-in-C.md b/content/blog/Embedding-files-in-C.md @@ -0,0 +1,89 @@ +--- +date: 2018-05-29 +layout: post +title: Embedding files in C programs with koio +tags: [C, announcement] +--- + +Quick blog post today to introduce a new tool I wrote: +[koio](https://git.sr.ht/~sircmpwn/koio). This is a small tool which takes a +list of files and embeds them in a C file. A library provides an fopen shim +which checks the list of embedded files before resorting to the real filesystem. + +I made this tool for [chopsui](https://github.com/SirCmpwn/chopsui), where I +eventually want to be able to bundle up sui markup, stylesheets, images, and so +on in a statically linked chopsui program. Many projects have small tools which +serve a similar purpose, but it was simple enough and useful enough that I chose +to make something generic so it could be used on several projects. + +The usage is pretty simple. I can embed `ko_fopen.c` in a C file with this +command: + +```sh +$ koio -o bundle.c ko_fopen.c://ko_fopen.c +``` + +I can compile and link with `bundle.c` and do something like this: + +```c +#include <koio.h> + +void koio_load_assets(void); +void koio_unload_assets(void); + +int main(int argc, char **argv) { + koio_load_assets(); + FILE *src = ko_fopen("//ko_fopen.c", "r"); + int c; + while ((c = fgetc(src)) != EOF) { + putchar(c); + } + fclose(src); + koio_unload_assets(); + return 0; +} +``` + +The generated `bundle.c` looks like this: + +```c +#include <koio.h> + +static struct { + const char *path; + size_t len; + char *data; +} files[] = { + { + .path = "//ko_fopen.c", + .len = 408, + .data = +"#define _POSIX_C_SOURCE 200809L\n#include <errno.h>\n#include <stdlib.h>\n#inc" +"lude <stdio.h>\n#include \"koio_private.h\"\n\nFILE *ko_fopen(const char *path" +", const char *mode) {\n\tstruct file_entry *entry = hashtable_get(&koio_vfs, p" +"ath);\n\tif (entry) {\n\t\tif (mode[0] != 'r' || mode[1] != '\\0') {\n\t\t\ter" +"rno = ENOTSUP;\n\t\t\treturn NULL;\n\t\t}\n\t\treturn fmemopen(entry->data, en" +"try->len, \"r\");\n\t}\n\treturn fopen(path, mode);\n}\n", + }, +}; + +void koio_load_assets(void) { + ko_add_file(files[0].path, files[0].data, files[0].len); +} + +void koio_unload_assets(void) { + ko_del_file(files[0].path); +} +``` + +A very simple tool, but one that I hope people will find useful. It's very +lightweight: + +- 312 lines of C +- /bin/koio is ~40 KiB statically linked to musl +- libkoio.a is ~18 KiB +- Only mandatory dependencies are POSIX 2008 and a C99 compiler +- Only optional dependency is [scdoc](https://git.sr.ht/~sircmpwn/scdoc) for the + manual, which is similarly lightweight + +Enjoy! diff --git a/content/blog/Engineers-solve-problems.md b/content/blog/Engineers-solve-problems.md @@ -0,0 +1,38 @@ +--- +date: 2020-08-17 +title: Software engineers solve problems +layout: post +--- + +Software engineers solve problems. A problem you may have encountered is, for +example, "this function has a bug", and you're probably already more or less +comfortable solving these problems. Here are some other problems you might +encounter on the way: + +1. Actually, the bug ultimately comes from a third-party program +2. Hm, it uses a programming language I don't know +3. Oh, the bug is in that programming language's compiler +4. This subsystem of the compiler would have to be overhauled +5. And the problem is overlooked by the language specification + +I've met many engineers who, when standing at the base of this mountain, +conclude that the summit is too far away and clearly not their responsibility, +and subsequently give up. But remember: as an engineer, your job is to apply +creativity to solving problems. Are these not themselves problems to which the +engineering process may be applied? + +You can introduce yourself to the maintainers of the third-party program and +start working on a solution. You can study the programming language you don't +know, at least as much as is necessary to understand and correct the bug. You +can read the compiler's source code, and identify the subsystem which needs +overhauling, then introduce yourself to *those* maintainers and work on the +needed overhaul. The specification is probably managed by a working group, reach +out to them and have an erratta issued or a clarification added to the upcoming +revision. + +The scope of fixing this bug is broader than you thought, but if you apply a +deliberate engineering process to each problem that you encounter, eventually +you will complete the solution. This process of recursively solving problems to +get at the one you want to solve is called "[yak +shaving](http://catb.org/jargon/html/Y/yak-shaving.html)", and it's a necessary +part of your workflow. diff --git a/content/blog/Enough-to-decide.md b/content/blog/Enough-to-decide.md @@ -0,0 +1,177 @@ +--- +date: 2019-09-08 +layout: post +title: How I decide between many programming languages +tags: [languages] +--- + +I have a few old standards in my toolbelt that I find myself calling upon most +often, but I try to learn enough about many programming languages to reason +about whether or not they're suitable to any use-case I'm thinking about. The +best way is to learn by doing, so getting a general impression of the utility of +many languages helps equip you with the knowledge of whether or not they'd be +useful for a particular problem even if you don't know them yet. + +Only included are languages which I feel knowledgable enough about to comment +on, there are many that aren't here and which I encourage you to research. + +## C + +Pros: good performance, access to low-level tooling, useful in systems +programming, statically typed, standardized and venerable, the lingua franca, +universal support on all platforms.[^1] + +[^1]: Except one, and it can go suck an egg for all I care. + +Cons: string munging, extensible programming, poor availability of ergonomic +libraries in certain domains, has footguns, some programmers in the wild think +the footguns are useful. + +<style> +.bullshit { + color: white; + font-style: italic; + font-weight: bold; + text-shadow: -1px -1px 0 #000, + 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; +} +</style> + +## Go + +Pros: fast, conservative, good package manager and a healthy ecosystem, standard +library is well designed, best in class for many problems, has a spec and +multiple useful implementations, easy interop with C. + +Cons: the runtime is too complicated, no distinction between green threads and +real threads (meaning all programs deal with the problems of the latter). + +## Rust + +Pros: it's <em class="bullshit">SAFE</em>, useful for systems programming, +better than C++, ecosystem which is diverse but just short of the npm disease, +easy interop with C. + +Cons: far too big, non-standardized, only one meaningful implementation. + +## Python + +Pros: easy and fast to get things done, diverse package ecosystem of reasonably +well designed packages, deeply extensible, useful for server-side web software. + +Cons: bloated, poor performance, dynamically typed, cpython internals being +available to programmers has led to an implementation monoculture. + +## JavaScript + +*\* and all of its derivatives, which ultimately inherit its problems.* + +Pros: functional but with an expressive and C-like syntax, ES6 improved on many +fronts, async/await/promises are well designed, no threading. + +Cons: dynamic types, package ecosystem is a flaming pile, many JS programmers +aren't very good at it and they make ecosystem-defining libraries anyway, born +in web browsers and inherited their many flaws. + +## Java + +*\* and all of its derivatives, which ultimately inherit its problems.* + +Pros: has had enough long-term investment to be well understood and reasonably +fast. + +Cons: hella boilerplate, missing lots of useful things, package management, XML +is everywhere, not useful for low-level programming (this applies to all +Java-family languages). + +## C# + +Pros: less boilerplate than Java, reasonably healthy package ecosystem, good +access to low level tools for interop with C, async/await started here. + +Cons: ecosystem is in turmoil because Microsoft cannot hold a singular vision, +they became open-source too late and screwed over Mono. + +## Haskell + +*\* and every other functional-oriented programming language in its class, such +as elixir, erlang, most lisps, even if they resent being lumped together* + +Pros: it's <em class="bullshit">FUNCTIONAL</em>, reasonably fast, useful +when the answer to your problem is more important than the means by which you +find it, good for research-grade[^2] compilers. + +Cons: it's <em class="bullshit">FUNCTIONAL</em>, somewhat inscrutable, awful +package management, does not fit well into its environment, written by people +who wish the world could be described with a pure function and design software +as if it could. + +[^2]: but not production-grade. + +## Perl + +Pros: [entertaining][perl-configure], best in class at regexes/string munging, +useful for making hacky kludges when such solutions are appropriate. + +[perl-configure]: https://github.com/Perl/perl5/blob/blead/Configure + +Cons: inscrutable, too extensible, too much junk/jank. + +## Lua + +Pros: embeddable & easily plugged into its host, fairly simple, portable. + +Cons: 1-based indexing is objectively bad, the upstream maintainers are kind of +doing their own thing and no one really likes it. + +## POSIX Shell scripts + +Pros: nothing can string together commands better, if you learn 90% of it then +you can make pretty nice and expressive programs with it for a certain class of +problem, standardized (I do not use bash). + +Cons: most people learn only 10% of it and therefore make pretty bad and +unintuitive programs with it, not useful for most complex tasks. + +--- + +Disclaimer: I don't like the rest of these programming languages and would not +use them to solve any problem. If you don't want your sacred cow gored, leave +here. + +## C++ + +Pros: none + +Cons: ill-defined, far too big, <em class="bullshit"> +Object Oriented Programming</em>, loads of baggage, ecosystem that buys into +its crap, enjoyed by bad programmers. + +## PHP + +Pros: none + +Cons: every PHP programmer is bad at programming, the language is designed to +accommodate them with convenient footguns (or faceguns) at every step, and the +ecosystem is accordingly bad. No, PHP7 doesn't fix this. Use a real programming +language, jerk. + +## Ruby + +Pros: It's both <em class="bullshit">ENTERPRISE</em> and <em +class="bullshit">HIP</em> at the same time, and therefore effective at herding a +group of junior to mid-level programmers in a certain direction, namely towards +your startup's exit. + +Cons: bloated, awful performance, before Node.js took off this is what all of +those programmers used. + +## Scala + +Pros: more expressive than Java, useful for <em class="bullshit">Big +Data</em> problems. + +Cons: Java derivative, type system requires a PhD to comprehend, too siloed from +Java, meaning it gets all of the disadvantages of being a Java ecosystem member +but few of the advantages. The type system is so needlessly complicated that it +basically cripples the language on its own merits alone. diff --git a/content/blog/FOSDEM-recap.md b/content/blog/FOSDEM-recap.md @@ -0,0 +1,98 @@ +--- +date: 2019-02-05 +layout: post +title: My experiences at FOSDEM 2019 +tags: ["roundup", "fosdem"] +--- + +Currently in a plane on my way home from FOSDEM and, as seems to be a recurring +pattern when I fly long distances home after attending a conference, a recap is +readily flowing from my fingertips. This was my first year at FOSDEM, and I'm +glad that I came. I'm already excited for next year! It was also my first year +volunteering, which was equally great and another thing I expect to repeat. + +My biggest feeling during the event was one of incredible business. My +scatterbrained interests throughout the domain of free software came back to +haunt me as I struggled to keep up with all of the people I had to meet & thank, +all of the sessions I wanted to see, and all of the dinners & outings I wanted +to attend. Before all of the fuss, though, I was lucky enough to have a day and +a half to myself (and later with [Simon Ser](https://emersion.fr)) to enjoy +Brussels with. + +The first FOSDEM-related event I found myself was when the Arch Linux developers +graciously invited me to their dinner on Friday. I have a long friendship with +several Arch developers, but never met any in person. We were speaking in the +weeks before FOSDEM about how to save them from their subversion nightmare, and +we spoke a little bit about some ideas for fixing this, but mostly we just had a +good time and got to know each other better. Later in the week, Jerome finally +convinced me to apply to become an Arch Trusted User, and in the coming months I +hope to work with them on a nice next-generation system for Arch Linux package +maintenance. + +The hallway track[^1] continued to be the highlight of the event. Later Friday +night, I had volunteered to staff the FOSDEM beer event's late shift, so the +inevitability of time and biology led to missing the first half of day one. I +ended up wiggling my way into the BSD room and saw a cool talk on NetBSD - long +one of my favorites among the BSDs, and learned that the speaker had a cool +project which will save me a lot of time when adding NetBSD support to sr.ht. +Grabbed his email afterwards and met up with my friends from KDE for lunch. We +met up with Daniel Stone as well, and spoke for a while about how we're finally +going to approach unifying and standardizing the Wayland ecosystem. This +discussion took place waiting outside the graphics room for the Pipewire talk. +Simon has been working on a portal to connect sway's Wayland protocols with the +dbus-based ecosystem Pipewire lives in, and along with KDE's Roman Glig they had +some interesting questions for the presenter. + +[^1]: The part of the conference which takes place in the hallway, i.e. socializing with other attendees. + +The second day was quite a bit different. My other role as a volunteer was doing +A/V support in the rooms. For this I got a *second* shirt, with a different +color! I think next year I may try to collect them all. This was interesting and +slow work, and basically entailed walking down to the stage crouched down to +tweak the mic volume until someone on IRC from the war room said it was better. +I did get to observe more exciting crises over IRC from the comfort of my +relatively normal room, though, and got to play a bit with the astonishingly +sophisticated A/V setup FOSDEM uses. After that I grabbed a light lunch and +passed the time by playing Magic: the Gathering with a group we found in the +FOSDEM bar. I grabbed some Club Mates - I love them but they're super difficult +to get in the United States - and waited until the highlight of the event: the +sr.ht and sway meetups. + +Big shoutout to the FOSDEM organizers for entertaining our last-minute requests +to have a space to meet users of both groups. The turnout for both rooms was +way more than I expected - almost 50! It seemed like every seat was filled. I +was also surprised at how distinct the groups where, with only a 5-10% overlap. +After making sure everyone got a sticker, there was some really great questions +and feedback from the sr.ht crowd. A particularly interesting tangent had me +defending the email choice to a skeptic and getting a lot of good feedback and +insights from the rest of the room, as well as elaborating on my plans to +improve the workflow for those less comfortable with email. There was naturally +some discussion about the crappy name and my plans to fix it, and I had the +pleasure of demoing the experimental Fedora builds live to someone who was +asking when there would be Fedora support. It was also great to meet many of the +users and contributors who I've been working with online, and made sure to thank +them in person - particularly Ivan Habunek, a prolific sr.ht contributor who was +part of our roaming sway/sr.ht/Arch Linux/etc clan throughout FOSDEM. + +The sway meetup was equally fun, and I thank the attendees for bearing with me +while I answered the post-meetup questions and comments from the sr.ht crowd - +my fault for scheduling two back-to-back sessions. We started off with a bang by +releasing sway 1.0-rc1, then turned to questions and feedback from the crowd. +Simon had a lot to say during the sway meetup as well, explaining his work and +future plans for the project, and together we also explained our somewhat novel +philosophy on project governance that I credit the success of the project to. +It's designed to maximize contributors, and it's entirely to their credit that +the success of sway and wlroots is owed. Speaking of the future of sway and +wlroots, I also met Guido, an engineer at Purism who works with wlroots, again +after our initial meeting at XDC 2018. This time, Guido brought a gift - a +Librem 5 dev board for the wlroots team to use. Thank you! You'll hear more +about our work with this board in the coming months as I use it to improve touch +support for sway and send it out on loan to various wlroots project developers. + +I had a flight home Sunday evening so we had a hasty and delicious dinner, a +quick round of beers, and finally parted ways. An overnight in Dublin and here I +am - on the plane home to Philly, with 43% of my battery[^2] and an estimated 3 +hours left in-flight. FOSDEM was great - a huge thanks to the organizers and +volunteers! I'm looking forward to next year. + +[^2]: Paranoia about which led me to spend some time optimizing my development environment's power consumption a bit diff --git a/content/blog/FOSS-contributor-tracks.md b/content/blog/FOSS-contributor-tracks.md @@ -0,0 +1,80 @@ +--- +date: 2019-07-29 +layout: post +title: FOSS contributor tracks +tags: [maintainership, free software] +--- + +Just like many companies have different advancement tracks for their employees +(for example, a management track and an engineering track), similar concepts +exist in free software projects. One of the roles of a maintainer is to help +contributors develop into the roles which best suit them. I'd like to explain +what this means to me in my role as a maintainer of several projects, though I +should mention upfront that I'm just some guy and, while I can explain what has +and hasn't worked for me, I can't claim to have all of the answers. People are +hard. + +There are lots of different tasks which need doing on a project. A few which +come up fairly often include: + +- End-user support +- Graphic design +- Marketing +- Release planning +- Reviewing code +- Translations +- Triaging tickets +- Writing code +- Writing documentation + +Within these tasks there's room still for more specialization - different +modules have different maintainers, each contributor's skills may be applicable +to different parts of the codebase, some people may like blogging about the +project where others like representing the project at conferences, and so on. To +me, one of my most important jobs is to figure out these relationships between +tasks and people. + +There are several factors that go into this. Keeping an eye on code reviews, +social channels, etc, gives you a good pulse on what people are good at now. +Talking with them directly and discussing possible future work is a good way to +understand what they want to work on. I also often consider what they could be +good at but don't have exposure to yet, and encourage them to take on more of +these tasks. The most common case where I try to get people to branch out is +code review - once they've contributed to a module they're put on the shortlist +for reviewers for future changes to nearby code. Don't be afraid to take risks - +a few bugs is a small price to pay for an experienced contributor. + +This also touches on another key part of this work - fostering collaboration. +For example, if someone is taking on a cross-cutting task, I'll give them the +names of experts on all of the affected modules so they can ask questions and +seek buy-in on their approach. Many developers aren't interested in end-user +support, so getting people who are interested in this to bubble up technical +issues when they're found is helpful as well. + +The final step is to gradually work your way out of the machine. Just like you +onboard someone with feature development or code review, you can onboard people +with maintainer tasks. If someone asks you to connect them to experts on some +part of the code, defer to a senior contributor - who has likely asked you the +same question at some point. Ask a contributor to go over the shortlog and +prepare a draft for the next release notes. Pull a trusted contributor aside and +ask them what they think needs to be improved in the project - then ask them to +make those improvements, and equip them with any tools they need to accomplish +it. + +One role I tend to reserve for myself is conflict prevention and moderation. I +keep a light watch on collaboration channels and periodically sync with major +contributors, keeping a pulse for the flow of information through the project. +When arguments start brewing or things start getting emotional, I try to notice +early and smooth things over before they get heated. At an impasse, I'll make a +final judgement call on a feature, design decision, or whatever else. By making +the decision, I aim to make it neither party's fault that someone didn't get +their way. Instead, I point any blame at myself, and rely on the mutual trust +between myself and the contributors to see the decision through amicably. When +this works correctly, it can help preserve a good relationship between each +party. + +If you're lucky, the end result is a project which can grow arbitrarily large, +with contributors bringing a variety of skills to support each other at every +level and enjoy the work they're doing. The bus factor is low and everyone +maintains a healthy and productive relationship with the project - yourself +included. diff --git a/content/blog/Fees-on-donation-platforms.md b/content/blog/Fees-on-donation-platforms.md @@ -0,0 +1,48 @@ +--- +date: 2018-01-16 +layout: post +title: Fee breakdown for various donation platforms +tags: [money] +--- + +Understanding fees are a really confusing part of supporting creators of things +you like. I provide a few ways for people to support my work, and my supporters +can struggle to understand the differences between them. It comes down to fees, +of which there are several kinds (note: I just made these terms up): + +- **Transaction fees** are charged by the payment processor (the company that + takes down your card number and runs the transaction with your bank). These + are typically in the form of a percentage of the transaction plus a few cents. +- **Platform fees** are charged by the platform (e.g. Patreon) to run their + operation, typically in the form of a fixed percentage of the transaction. +- **Withdrawal fees** are charged to move money from the platform to the + creator's bank account. These vary depending on the withdrawal processor. +- **Taxes** are also implicated, depending on how much the creator makes. + +All of this adds up to a very confusing picture. I've made a calculator to help +you sort it out. + +**Note**: For an up-to-date calculation of Patreon's fees, see [the follow-up +post](https://drewdevault.com/2019/05/06/Calculate-your-doation-fees-for-Patreon.html). + +<noscript>Sorry, the calculator requires JavaScript.</noscript> +<div id="react-root"></div> +<script src="/js/donation-calc-v1.js"></script> + +### Sources + +**fosspay** + +Only the typical [Stripe fee](https://stripe.com/us/pricing) is applied. + +Note: I am the author of fosspay, if you didn't already know. + +**Patreon** + +[How do you calculate fees?](https://patreon.zendesk.com/hc/en-us/articles/204606125-How-do-you-calculate-fees-) + +[What are my options to receive payout?](https://patreon.zendesk.com/hc/en-us/articles/203913489-What-are-my-options-to-receive-payout-) + +**Liberapay** + +[FAQ](https://liberapay.com/about/faq) diff --git a/content/blog/Firefox-is-on-a-slippery-slope.md b/content/blog/Firefox-is-on-a-slippery-slope.md @@ -0,0 +1,86 @@ +--- +date: 2017-12-16 +layout: post +title: Firefox is on a slippery slope +tags: [firefox, philosophy] +--- + +For a long time, it was just setting the default search provider to Google in +exchange for a beefy stipend. Later, paid links in your new tab page were added. +Then, a proprietary service, Pocket, was bundled into the browser - not as an +addon, but a hardcoded feature. In the past few days, we've discovered an +advertisement in the form of browser extension was sideloaded into user +browsers. Whoever is leading these decisions at Mozilla needs to be stopped. + +Here's a breakdown of what happened a few days ago. Mozilla and NBC +Universal did a "collaboration" (read: promotion) for the TV show Mr. Robot. +It involved sideloading a sketchy browser extension which will <strong +style="display: inline-block; transform: scaleY(-1)">invert</strong> text that +matches a list of Mr. Robot-related keywords like "fsociety", "robot", "undo", +and "fuck", and does a number of other things like adding an HTTP header to +certain sites you visit. + +This extension was sideloaded into browsers via the "experiments" feature. +Not only are these experiments enabled by default, but updates [have been +known](https://redd.it/7i4puf) to re-enable it if you turn it off. The +advertisement addon shows up [like +this](http://www.bolcer.org/looking-glass2.png) on your addon page, and was +added to Firefox stable. If I saw this before I knew what was going on, I would +think my browser was compromised! Apparently it was a mistake that this showed +up on the addon page, though - it was supposed to be *silently* sideloaded into +your browser! + +There's [a ticket](https://bugzilla.mozilla.org/show_bug.cgi?id=1423003) on +Bugzilla (Firefox's bug tracker) for discussing this experiment, but it's locked +down and no one outside of Mozilla can see it. There's [another +ticket](https://bugzilla.mozilla.org/show_bug.cgi?id=1424977), filed by +concerned users, which has since been disabled and had many comments removed, +particularly the angry (but respectful) ones. + +Mozilla, this is **not okay**. This is wrong on so many levels. Frankly, whoever +was in charge should be fired over this - which is not something I call for +lightly. + +First of all, web browsers are a *tool*. I don't want my browser to fool around, +I just want it to display websites faithfully. This is the prime directive of +web browsers, and you broke that. When I compile vim with gcc, I don't want +gcc to make vim sporadically add "fsociety" into every document I write. I want +it to compile vim and go away. + +More importantly, these advertising anti-features gravely - perhaps terminally - +violate user trust. This event tells us that "Firefox studies" into a backdoor +for advertisements, and I will *never* trust it again. But it doesn't matter - +you're going to re-enable it on the next update. You know what that means? I +will never trust *Firefox* again. I switched to +[qutebrowser](http://qutebrowser.org/) as my daily driver because this crap was +starting to add up, but I still used Firefox from time to time and never +resigned from it entirely or stopped recommending it to friends. Well, whatever +goodwill was left is gone now, and I will only recommend other browsers +henceforth. + +Mozilla, you fucked up *bad*, and you still haven't apologised. The study is +still active and ongoing. There is no amount of money that you should have +accepted for this. This is the last straw - and I took a lot of straws from you. +Goodbye forever, Mozilla. + +**Update 2017-12-16 @ 22:33** + +It has been clarified that an about:config flag must be set for this addon's +behavior to be visible. This improves the situation considerably, but I do not +think it exenorates Mozilla and I stand firm behind most of my points. The study +has also been rolled back by Mozilla, and Mozilla has issued +[statements](https://gizmodo.com/mozilla-slipped-a-mr-robot-promo-plugin-into-firefox-1821332254) +to the +[media](https://gizmodo.com/after-blowback-firefox-will-move-mr-robot-extension-t-1821354314) +justifying the study (no apology has been issued). + +**Update 2017-12-18** + +Mozilla has issued an apology: + +https://blog.mozilla.org/firefox/update-looking-glass-add/ + +**Responses**: + +[Mozilla, Firefox, Looking Glass, and you](https://blog.jeaye.com/2017/12/16/firefox/) +via jeaye.com diff --git a/content/blog/Fuck-you-nvidia.md b/content/blog/Fuck-you-nvidia.md @@ -0,0 +1,64 @@ +--- +date: 2017-10-26 +layout: post +title: Nvidia sucks and I'm sick of it +tags: [nvidia, wayland] +--- + +There's something I need to make clear about Nvidia. Sway 1.0, which is the +release after next, is *not* going to support the Nvidia proprietary driver, +EGLStreams, or any other proprietary graphics APIs. The only supported driver +for Nvidia cards will be the open source nouveau driver. I will explain why. + +Today, Sway is able to run on the Nvidia proprietary driver. This is not and has +never been an officially supported feature - we've added a few things to try and +make it easier but my stance has *always* been that Nvidia users are on their +own for support. In fact, Nvidia support was added to Sway without my approval. +It comes from a library we depend on called wlc - had I'd made the decision on +whether or not to support EGLStreams in wlc, I would have said no. + +Right now, we're working very hard on replacing wlc, for reasons unrelated to +Nvidia. Our new library, wlroots, is better in every conceivable way for Sway's +needs. The Nvidia proprietary driver support is not coming along for the ride, +and here's why. + +So far, I've been speaking in terms of *Sway* supporting Nvidia, but this is +an ass-backwards way of thinking. *Nvidia* needs to support Sway. There are +Linux kernel APIs that we (and other Wayland compositors) use to get the job +done. Among these are KMS, DRM, and GBM - respectively Kernel Mode Setting, +Direct Rendering Manager, and Generic Buffer Management. Every GPU vendor +but Nvidia supports these APIs. Intel and AMD support them with mainlined[^1], +open source drivers. For AMD this was notably done by replacing their +proprietary driver with a new, open source one, which has been developed in +cooperation with the Linux community. As for Intel, they've always been friendly +to Linux. + +Nvidia, on the other hand, have been fucking assholes and have treated Linux +like utter shit for our entire relationship. About a year ago they announced +"Wayland support" for their proprietary driver. This included KMS and DRM +support (years late, I might add), but *not* GBM support. They shipped something +called EGLStreams instead, a concept that had been discussed and shot down by +the Linux graphics development community before. They did this because it makes +it easier for them to keep their driver proprietary without having work with +Linux developers on it. Without GBM, Nvidia *does not* support Wayland, and they +were real pricks for making some announcement like they actually did. + +When people complain to me about the lack of Nvidia support in Sway, I get +really pissed off. It is *not my fucking problem* to support Nvidia, it's +Nvidia's fucking problem to support me. Even Broadcom, *fucking Broadcom*, +supports the appropriate kernel APIs. And proprietary driver users have the gall +to *reward* Nvidia for their behavior by giving them *hundreds of dollars* for +their GPUs, then come to *me* and ask me to deal with their bullshit *for free*. +Well, fuck you, too. Nvidia users are shitty consumers and I don't even want +them in my userbase. Choose hardware that supports your software, not the other +way around. + +Buy AMD. Nvidia-- fuck you! + +**Edit**: It's worth noting that Nvidia is evidently attempting to find a better +path with [this new GitHub project](https://github.com/cubanismo/allocator). I +hope it works out, but they aren't really cooperating much with anyone to build +it - particularly nouveau. It's more throwing code/blobs over the wall and +expecting everyone to change for them. + +[^1]: Mainlined means that they are included in the upstream Linux kernel source code. diff --git a/content/blog/Fucking-laptops.md b/content/blog/Fucking-laptops.md @@ -0,0 +1,62 @@ +--- +date: 2020-02-18 +layout: post +title: Fucking laptops +categories: [rants] +--- + +The best laptop ever made is the ThinkPad X200, and I have two of them. The +caveats are: I get only 2-3 hours of battery life even with conservative use; +and it struggles to deal with 1080p videos. + +The integrated GPU, Bluetooth and WiFi, internal sensors, and even the +fingerprint reader can all be driven by the upstream Linux kernel. In fact, the +hardware is so well understood that I have successfully used almost all of the +laptop's features on Linux, FreeBSD, NetBSD, Minix, Haiku, and Plan 9. Plan +fucking 9. It can run coreboot, too. The back of the laptop has all of the +screws (Phillips head) labelled so you know which to remove to service which +parts. User replacable parts include the screen, keyboard (multiple layouts are +available and are interchangeable), the RAM, hard drive (I put a new SSD in one +of mine a few weeks ago, and it took about 30 seconds) &mdash; actually, there +are a total of 26 replacable parts in this laptop.[^1] There is a detailed +278-page service manual to assist you or your local repair tech in addressing +any problems that arise. + +[^1]: Which just means you can basically take the entire thing apart and replace almost any part. + +They're quite durable, too. Mine still looks like it just rolled off the +assembly line yesterday. In fact, it was built 12 years ago. + +The X200 was made in 2008. In the time since, the modern laptops' battery life +and video decoding performance has improved. In every other respect, the market +is regressive, half-assed garbage. + +I am usually near power, so I've been reasonably happy even with the pithy +battery life of the X200. I also have a T520, which sucks in its own way[^2], +but can decode 1080p videos just fine. I generally don't need a lot of power - +compiling most programs is fast enough that I don't really notice, especially +with incremental compilation, and for any large workloads I just SSH out into a +build server somewhere. However, I've been planning some astronomy outings +lately, and the battery life matters for this - so I was looking for a laptop I +could run [Stellarium](https://stellarium.org/) on to drive my telescope into +the wee hours of the night. + +[^2]: It barely gets an hour and a half of battery life on a good day. And there's an Nvidia optimus GPU, which is just, ugh. + +It has since come to my attention that in 2020, every laptop *still* fucking +sucks. Even the ones people pretend to like have crippling, egregious flaws. The +Dell XPS series has a firmware so bad that its engineers should be strung up in +the town square for building it - if yours works, it's because you were *lucky*. +System76 laptops are bulky and priced at 2x or 3x what they're worth. Same goes +for Purism, plus a company I have no desire to support any longer, and they're +out of stock anyway. Pine64 requires nonfree blobs, patched kernels, and booting +up ARM devices is a fucking nightmare, and they're out of stock anyway. The Star +Lite looks promising, but they're out of stock too. Huewei laptops are shameless +Macbook ripoffs with the same shitty keyboards, and you can't buy them in the US +anyway. Speaking of Macbooks, even Apple fanboys are fed up with them these +days. + +The laptop market is in an atrocious state, folks. If you work at any of these +companies and you're proud of the garbage you're shipping, then I'm disappointed +in you. Come on, let's get our shit together and try to make a laptop which is +*at least* as good as the 12 year-old one I'm stuck with now. diff --git a/content/blog/Future-of-sway.md b/content/blog/Future-of-sway.md @@ -0,0 +1,91 @@ +--- +date: 2017-10-09 +layout: post +title: "The future of Wayland, and sway's role in it" +tags: [wayland, sway, wlroots, announcement] +--- + +Today I've released sway +[0.15-rc1](https://github.com/swaywm/sway/releases/tag/0.15-rc1), the first +release candidate for the final 0.x release of sway. That's right - after sway +0.15 will be sway 1.0. After today, no new features are being added to sway +until we complete the migration to our new plumbing library, +[wlroots](https://github.com/swaywm/wlroots). This has been a long time +coming, and I would love to introduce you to wlroots and tell you what to expect +from sway 1.0. + +<small class="text-muted"><a href="https://github.com/swaywm/sway">Sway</a> is +a tiling Wayland compositor, if you didn't know.</small> + +Before you can understand what wlroots is, you have to understand its +predecessor: [wlc](https://github.com/Cloudef/wlc). The role of wlc is to manage +a number of low-level plumbing components of a Wayland compositor. It +essentially abstracts most of the hard work of Wayland compositing away from the +compositor itself. It manages: + +- The EGL (OpenGL) context +- DRM (display) resources +- libinput resources +- Rendering windows to the display +- Communicating with Wayland clients +- Xwayland (X11) support + +It does a few other things, but these are the most important. When sway wants to +render a window, it will be told about its existence through a hook from wlc. +We'll tell wlc where to put it and it will be rendered there. Most of the heavy +lifting has been handled by wlc, and this has allowed us to develop sway into a +powerful Wayland compositor very quickly. + +However, wlc has some limitations, ones that sway has been hitting more and more +often in the past several months. To address these limitations, we've been +working very hard on a replacement for wlc called wlroots. The relationship +between wlc and wlroots is similar to the relationship between Pango and +Harfbuzz - wlroots is much more powerful, but at the cost of putting a lot more +work on the shoulders of sway. By replacing wlc, we can customize the behavior +of the low level components of our system. + +I'm happy to announce that development on wlroots has been spectacular. Like +libweston has Weston itself, wlroots has a reference compositor called Rootston - +a simple floating compositor that lets us test and demonstrate the features of +wlroots. It is from this compositor that I write this blog post today. The most +difficult of our goals are behind us with wlroots, and we're now beginning to +plan the integration of wlroots and sway. + +All of this work has been possible thanks to a contingent of highly motivated +contributors who have done huge amounts of work for wlroots, writing and +maintaining entire subsystems far faster than I could have done it alone. I +really cannot overstate the importance of these contributors. Thanks to their +contributions, most of my work is in organizing development and merging pull +requests. From the bottom of my heart, [thank +you](https://github.com/swaywm/wlroots/graphs/contributors). + +And for all of this hard work, what are we going to get? Well, for some time +now, there have been many features requests in sway that we could not address, +and many long-standing bugs we could not fix. Thanks to wlroots, we can see many +of these addressed within the next few months. Here are some of the things you +can expect from the union of wlroots and sway: + +- Rotated displays +- Touchscreen bindings +- Drawing tablet support +- Mouse capture for games +- Fractional display scaling +- Display port daisy chaining +- Multi-GPU support + +Some of these features are unique to sway even among Wayland *and* Xorg +desktops combined! Others, like output rotation, have been requested by our +users for a long time. I'm looking forward to the several dozen long-open GitHub +issues that will be closed in the next couple of months. This is just the +beginning, too - wlroots is such a radical change that I can't even begin to +imagine all of the features we're going to be able to build. + +We're sharing these improvements with the greater Wayland community, too. +wlroots is a platform upon which we intend to develop and promote open standards +that will unify the extensibility of all Wayland desktops. We've also been +working with other Wayland compositors, notably +[way-cooler](https://github.com/way-cooler/way-cooler), which are preparing to +move their own codebases to a wlroots-based solution. + +My goal is to ship sway 1.0 before the end of the year. These are exciting times +for Wayland, and I hope you're looking forward to it. diff --git a/content/blog/Generics-arent-ready-for-Go.md b/content/blog/Generics-arent-ready-for-Go.md @@ -0,0 +1,104 @@ +--- +date: 2019-02-18 +layout: post +title: Generics aren't ready for Go +tags: [language design, go] +--- + +In the distance, a gradual roar begins to grow in volume. A dust cloud is +visible over the horizon. As it nears, the shouts of the oncoming angry mob can +be heard. Suddenly, it stops, and a brief silence ensues. Then the air is filled +with the clackings of hundreds of keyboards, angrily typing the owner's opinion +about generics and Go. The clans of Java, C#, Rust, C++, TypeScript, Haskell, +and more - usually mortal enemies - have combined forces to fight in what may +become one of the greatest flamewars of our time. And none of them read more +than the title of this article before writing their comment. + +Have you ever seen someone write something to the effect of "I would use Go, but +I need generics"? Perhaps we can infer from this that many of the people who are +pining after generics in Go are not, in fact, Go users. Many of them are users +of another programming language that *does* have generics, and they feel that +generics are a good fit for this language, and therefore a good fit for any +language. The inertia of "what I'm used to" comes to a violent stop when they +try to use Go. People affected by this frustration interpret it as a problem +with Go, that Go is missing some crucial feature - such as generics. But this +lack of features is itself a feature, not a bug. + +Go strikes me as one of the most conservative programming languages available +today. It's small and simple, and every detail is carefully thought out. There +are very few dusty corners of Go - in large part because Go has fewer corners in +general than most programming languages. This is a major factor in Go's success +to date, in my opinion. Nearly all of Go's features are bulletproof, and in my +opinion are among the best implementations of their concepts in our entire +industry. Achieving this feat requires having *fewer* features in total. +Contrast this to C++, which has too many footguns to count. You *could* write a +book called "C++: the good parts", but consider that such a book about Go would +just be a book about Go. There's little room for the bad parts in such a spartan +language. + +So how should we innovate in Go? Consider the case of dependency management. Go +1.11 shipped with the first version of Go modules, which, in my opinion, is a +game changer. I passionately hate `$GOPATH`, and I thought dep wasn't much +better. dep's problem is that it took the dependency management ideas that other +programming languages have been working with and brought the same ideas to Go. +Instead, Go modules took the idea of dependency management and rethought it from +first principles, then landed on a much more elegant solution that I think other +programming languages will spend the next few years catching up with. I like to +make an analogy to physics: dep is like [General Relativity][gr] or [the +Standard Model][stdmodel], whereas Go modules are more like the [Grand Unified +Theory][gut]. Go doesn't settle for anything less when adding features. It's +not a language where liberal experimentation with imperfect ideas is desirable. + +[gr]: https://en.wikipedia.org/wiki/General_relativity +[stdmodel]: https://en.wikipedia.org/wiki/Standard_Model +[gut]: https://en.wikipedia.org/wiki/Grand_Unified_Theory + +I feel that this applies to generics. In my opinion, generics are an imperfect +solution to an unsolved problem in computer science. None of the proposals I've +seen (notably [contracts][proposal]) feel *right* yet. Some of this is a gut +feeling, but there are tangible problems as well. For example, the space of +problems they solve intersects with other Go features, which weakens the +strength of both features. "Which solution do I use to this problem" is a +question which different people will answer differently, and consequently their +code at best won't agree on what "idiomatic" means and at worst will be simply +incompatible. Another problem is that the proposal changes the meaning of +idiomatic Go in the first place - suddenly huge swaths of the Go code, including +the standard library, will become unidiomatic. One of Go's greatest strengths is +that code written 5 years ago is still idiomatic. It's almost impossible to +write unidiomatic Go code at all. + +[proposal]: https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md + +I used to sneer at the Go maintainers alongside everyone else whenever they'd +punt on generics. With so many people pining after it, why haven't they seen +sense yet? How can they know better than all of these people? My tune changed +once I started to use Go more seriously, and now I admire their restraint. Part +of this is an evolution of my values as a programmer in general: simplicity and +elegance are now the principles I optimize for, even if it means certain classes +of programs are simply not on the table. And I think Go should be comfortable +not being suitable for writing certain classes of programs. I don't think +programming languages should compete with each other in an attempt to become the +perfect solution to every problem. This is impossible, and attempts will just +create a messy kitchen sink that solves every problem poorly. + +<img + src="https://sr.ht/TxC_.jpg" + alt="Nina Tucker from Fullmetal Alchemist" + width="320" /> +<p class="text-center"> + <small>fig. 1: the result of C++'s attempt to solve all problems</small> +</p> + +The constraints imposed by the lack of generics (and other things Go lacks) +breed creativity. If you're fighting Go's lack of generics trying to do +something Your Way, you might want to step back and consider a solution to the +problem which embraces the limitations of Go instead. Often when I do this the +new solution is a much better design. + +So it's my hope that Go will hold out until the right solution presents itself, +and it hasn't yet. Rushing into it to appease the unwashed masses is a bad idea. +There are other good programming languages - use them! I personally use a wide +variety of programming languages, and though I love Go dearly, it probably only +comes in 3rd or 4th place in terms of how frequently it appears in my projects. +It's *excellent* in its domain and doesn't need to awkwardly stumble into +others. diff --git a/content/blog/Getting-on-without-Google.md b/content/blog/Getting-on-without-Google.md @@ -0,0 +1,127 @@ +--- +date: 2016-11-16 +# vim: set tw=80 : +layout: post +title: Getting on without Google +tags: [google] +--- + +![](https://sr.ht/d718.png) + +I used Google for a long time, but have waned myself off of it over the past +few years, and I finally deleted my account a little over a month ago. I feel so +much better about my privacy now that I've removed Google from the equation, and +self hosting my things affords me a lot of flexibility and useful customizations. + +## mail.cmpwn.com + +This one was the most difficult and time consuming to set up, but it was *very* +worth it. I've intended for a while to make a new mail server software suite +that's less terrible to set up, so hopefully that situation will improve in the +future. I want to flesh out [aerc](https://github.com/SirCmpwn/aerc) some more +first. A personal mail server was one of the earliest things I set up in my +post-Google life - I've operated it for about two years now. + +- Postfix to handle incoming and outgoing mail +- Dovecot to handle mail delivery, filtering, and IMAP +- Postfixadmin to provide a nice interface for managing accounts +- mutt to read and compose my emails on the desktop +- K9 to read and compose my emails on Android +- Roundcube for when it's occasionally necessary to read an HTML email + +With my mail server provides a lot of side benefits, too. For one, all of my +email-sending software now uses it. Once Mandrill went kaput, it was easy to +switch everything over to it. I can be sending and receiving email from a new +domain in less than 5 minutes now. Using sieve scripts for filtering emails is +also a lot more flexible than what Google offered - I now have filtering set up +to organize several mailing lists, alerts and notifications sent by my software +and servers, RSS feeds, and more. + +My strategy for defeating spam is to use a combination of the spamhaus +blocklist, greylisting, and blacklisting with sieve. I see about 3-5 spam emails +per week on average with this setup. To ensure my own emails get delivered, I've +set up SPF and DKIM, reverse DNS, and appealed to have my IP address removed +from blocklists. A great tool in figuring all this out has been +[mail-tester.com](http://mail-tester.com). + +## YouTube + +For YouTube, I "subscribe" to channels by adding their RSS feeds to +[rss2email](http://www.allthingsrss.com/rss2email/), combined with sieve scripts +that filter them into a specific folder. I then have a keybinding in mutt that, +when pressed, pulls the YouTube URL out of an email and feeds it to mpv, a +desktop video player. It's so much easier to access YouTube this way than +through the web browser - no ads, familiar keybindings, remote control support, +and a no-nonsense feed of your videos. + +## Music + +Instead of Google Music, Spotify, or anything else, I run an internet radio +with my friends. We all keep our music collections (mostly lossless) on NFS +servers, and we mounted these servers on a streaming server that shuffles the +entire thing and keeps a searchable database of music. We have an API that I +pull from to integrate desktop keybindings and a status line on my taskbar, and +an IRC bot for searching the database and requesting songs. I can also stream to +my phone with VLC, as well as use scripts to maintain an offline archive of my +favorite songs. This setup is *way* nicer than any commercial service I've used +in the past. We'll be open sourcing version 2 to provide a turnkey solution for +this type of self-hosted music service. + +## Web search + +[DuckDuckGo](https://duckduckgo.com/). Even if you think the search results +aren't up to snuff (you get used to just being a bit more specific anyway), the +bangs feature is absolutely indispensable. I recently patched Chromium for +Android to support DuckDuckGo as a search engine as well: +[here's the patch](https://sr.ht/h4bZ.patch). + +## File hosting + +Instead of using Google Drive, I'm using a number of different solutions +depending on what's most convenient at the time. I operate +[sr.ht](https://sr.ht) for me and my friends, which allows me to just have a +place to drop a file and get a link to share. I have scripts and keybindings set +up to make uploading files here second nature, as well as an Android app someone +wrote. I also keep a 128G flash drive on my keychain now that comes in handy all +the time, and a big-ass file server on OVH that I keep mounted with NFS or sshfs +depending on the scenario, and sometimes I just stash files on a random server +with rsync. sr.ht is [open source](https://gogs.sr.ht/SirCmpwn/sr.ht), by the +way. + +## CyanogenMod + +On Android, I use CyanogenMod without Google Play Services, and I use F-Droid to +get apps. When I used Google Now, I found that I most often just asked it for +reminders, which I now do via an open source app called Notable Plus. I also +have open source apps for reading HN, downloading torrents, blocking ads, +connecting to IRC, two factor authentication, YouTube, password management, +Twitter, and more. + +## Notably missing: Docs + +Hopefully the new LibreOffice thing will do the trick once it's ready. I'm +looking forward to that. + +## Things I self host that Google doesn't offer + +I use ZNC to operate an IRC bouncer, which is great because I use IRC *a lot*. +It keeps logs for me, keeps me always connected, and gives me a number of nice +features to work with. I also host a number of simple websites related to IRC to +do things like channel stats and rules. + +To all sr.ht users I offer access to [gogs.sr.ht](https://gogs.sr.ht), which I +personally use to host many private repositories as well as a number of small +projects, and as a kind of staging area for repositories that aren't quite ready +for GitHub yet. + +For passwords, I use a tool called [pass](https://www.passwordstore.org/), which +encrypts passwords with my PGP key and stores them in a git repository I keep on +gogs.sr.ht, with desktop keybindings to make grabbing them convenient. + +## Help me do this! + +Well, that covers most of my major self hosted services. If you're interested in +more detail about how any of this works so you might set something up yourself, +feel free to reach out to me by [email](mailto:sir@cmpwn.com), +[Mastodon](https://cmpwn.com/@sir), or IRC (SirCmpwn on any network). I'd +be happy to help! diff --git a/content/blog/Getting-started-with-qemu.md b/content/blog/Getting-started-with-qemu.md @@ -0,0 +1,103 @@ +--- +date: 2018-09-10 +layout: post +title: Getting started with qemu +tags: ["qemu", "instructional"] +--- + +I often get asked questions about using my software, particularly sway, on +hypervisors like VirtualBox and VMWare, as well as for general advice on +which hypervisor to choose. My answer is always the same: qemu. There's no +excuse to use anything other than qemu, in my books. But I can admit that it +might be a bit obtuse to understand at first. qemu's greatest strength is also +its greatest weakness: it has so many options that it's hard to know which ones +you need just to get started. + +qemu is the swiss army knife of virtualisation, much like ffmpeg is the swiss +army knife of multimedia (which comes as no surprise, given that both are written +by Fabrice Bellard). I run a dozen permanent VMs with qemu, as well as all of +the ephemeral VMs used on [builds.sr.ht](https://meta.sr.ht). Why is it better +than all of the other options? Well, in short: qemu is fast, portable, better +supported by guests, and has more features than Hollywood. There's nothing other +hypervisors can do that qemu can't, and there's plenty qemu can that they +cannot. + +Studying the full breadth of qemu's featureset is something you can do over +time. For now, let's break down a simple Linux guest installation. We'll start +by downloading some install media (how about [Alpine +Linux](https://alpinelinux.org/), I like Alpine Linux) and preparing a virtual +hard drive. + + curl -O https://nl.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-standard-3.8.0-x86_64.iso + qemu-img create -f qcow2 alpine.qcow2 16G + +This makes a 16G virtual hard disk in a file named alpine.qcow2, the qcow2 +format being a format which appears to be 16G to the guest (VM), but only +actually writes to the host any sectors which were written to by the guest in +practice. You can also expose this as a block device on your local system (or a +remote system!) with qemu-nbd if you need to. Now let's boot up a VM using our +install media and virtual hard disk: + + qemu-system-x86_64 \ + -enable-kvm \ + -m 2048 \ + -nic user,model=virtio \ + -drive file=alpine.qcow2,media=disk,if=virtio \ + -cdrom alpine-standard-3.8.0-x86_64.iso \ + -sdl + +This is a lot to take in. Let's break it down: + +**-enable-kvm**: This enables use of the KVM (kernel virtual machine) subsystem +to use hardware accelerated virtualisation on Linux hosts. + +**-m 2048**: This specifies 2048M (2G) of RAM to provide to the guest. + +**-nic user,model=virtio**: Adds a virtual **n**etwork **i**nterface +**c**ontroller, using a virtual LAN emulated by qemu. This is the most +straightforward way to get internet in a guest, but there are other options (for +example, you will probably want to use `-nic tap` if you want the guest to do +networking directly on the host NIC). `model=virtio` specifies a special +`virtio` NIC model, which is used by the virtio kernel module in the guest to +provide faster networking. + +**-drive file=alpine.qcow2,media=disk,if=virtio**: This attaches our virtual +disk to the guest. It'll show up as `/dev/vda`. We specify `if=virtio` for the +same reason we did for `-nic`: it's the fastest interface, but requires special +guest support from the Linux virtio kernel module. + +**-cdrom alpine-standard-3.8.0-x86_64.iso** connects a virtual CD drive to the +guest and loads our install media into it. + +**-sdl** finally specifies the graphical configuration. We're using the SDL +backend, which is the simplest usable graphical backend. It attaches a display +to the guest and shows it in an [SDL](https://www.libsdl.org/) window on the +host. + +When you run this command, the SDL window will appear and Alpine will boot! You +can complete the Alpine installation normally, using `setup-alpine` to install +it to the attached disk. When you shut down Alpine, run qemu again without +`-cdrom` to start the VM. + +That covers enough to get you off of VirtualBox or whatever other bad hypervisor +you're using. What else is possible with qemu? Here's a short list of common +stuff you can look into: + +- Running pretty much any guest operating system +- Software emulation of non-native architectures like ARM, PPC, RISC-V +- Using `-spice` instead of `-sdl` to enable remote access to the + display/keyboard/mouse +- Read-only disk images with guest writes stored in RAM (`snapshot=on`) +- Non-graphical boot with `-nographic` and `console=ttyS0` configured in your + kernel command line +- Giving a genuine graphics card to your guest with KVM passthrough for high + performance gaming, OpenCL, etc +- Using [virt-manager](https://virt-manager.org/) or + [Boxes](https://help.gnome.org/users/gnome-boxes/stable/) if you want a GUI to + hold your hand +- And much more... + +There's really no excuse to be using any other hypervisor[^1]. They're all +dogshit compared to qemu. + +[^1]: Especially VirtualBox. If you use VirtualBox after reading this article you make poor life choices and are an embarrassment to us all. diff --git a/content/blog/Git-email-webcast.md b/content/blog/Git-email-webcast.md @@ -0,0 +1,30 @@ +--- +date: 2019-05-13 +layout: post +title: 'Webcast: Reviewing git & mercurial patches with email' +tags: ["video", "git"] +--- + +With the availability of new resources like +[git-send-email.io](https://git-send-email.io), I've been working on making the +email-based workflow more understandable and accessible to the world. One thing +that's notably missing from this tutorial, however, is the maintainer side of +the work. I intend to do a full write-up in the future, but for now I thought +it'd be helpful to clarify my workflow a bit with a short webcast. In this +video, I narrate my workflow as I review a few +[sourcehut](https://sourcehut.org) patches and participate in some dicsussions. + +<video + src="https://yukari.sr.ht/git-screencast.webm" + controls +>Your browser does not support HTML5 video, or webm video.</video> + +Links: + +- [mutt](http://www.mutt.org/): my email client +- [my personal mutt config](https://git.sr.ht/~sircmpwn/dotfiles/tree/master/.config/mutt/muttrc) +- [my "semver" script](https://git.sr.ht/~sircmpwn/dotfiles/tree/master/bin/semver) + +Also check out [aerc](https://git.sr.ht/~sircmpwn/aerc2), a replacement for mutt +that I've been working on over the past year or two. I will be writing more +about that project soon. diff --git a/content/blog/Git-is-already-distributed.md b/content/blog/Git-is-already-distributed.md @@ -0,0 +1,120 @@ +--- +date: 2018-07-23 +layout: post +title: Git is already federated & decentralized +tags: [git, philosophy] +--- + +There have always been murmurs about "replacing GitHub with something +decentralized!", but in the wake of the Microsoft acquisition these murmurs have +become conversations. In particular, this blog post is a direct response to +forge-net (formerly known as [GitPub][gitpub]). They want to federate and +decentralize git using ActivityPub, the same technology leveraged by Mastodon +and PeerTube. But get this: git is already federated *and* decentralized! + +[gitpub]: https://github.com/git-federation/gitpub + +I already spoke at length about how a large minority of the git community uses +email for collaboration in my [previous article][last-article] on the subject. +Definitely give it a read if you haven't already. In this article I want to +focus on comparing this model with the possibilities afforded by ActivityPub +and provide direction for new forge[^1] projects to work towards embracing and +improving git's email-based collaboration tools. + +[last-article]: https://drewdevault.com/2018/07/02/Email-driven-git.html +[^1]: Forge refers to any software which provides comprehensive tools for project hosting. This originally referred to SourceForge but is now a category of software which includes GitHub, BitBucket, GitLab, Gogs/Gitea, etc. + +The main issue with using ActivityPub for decentralized git forges boils down to +email simply being a better choice. The advantages of email are numerous. It's +already standardized and has countless open source implementations, many in the +standard libraries of almost every programming language. It's decentralized and +federated, and it's *already* integrated with git. Has been since day one! I +don't think that we should replace web forges with our email clients, not at +all. Instead, web forges should embrace email to communicate with each other. + +Let me give an example of how this could play out. On my platform, +[sr.ht](https://meta.sr.ht), users can view their git repositories on the web +(duh). One of my goals is to add some UI features here which let them select a +range of commits and prepare a patchset for submission via [git +send-email][git-send-email]. They'll enter an email address (or addresses) to +send the patch(es) to, and we'll send it along on their behalf. This email +address might be a mailing list on another sr.ht instance in the wild! If so, +the email gets recognized as a patch and displayed on the web with a pretty diff +and code review tools. Inline comments automatically get formatted as an email +response. This shows up in the user's inbox and sr.ht gets copied on it, showing +it on the web again. + +[git-send-email]: https://www.git-scm.com/docs/git-send-email + +I think that workflow looks an awful lot like the workflow forge-net hopes to +realize! Here's where it gets good, though. What if the emails the user puts in +are `linux-kernel@vger.kernel.org` and a handful of kernel maintainers? Now your +git forge can suddenly be used to contribute to the Linux kernel! ActivityPub +would build a *second*, incompatible federation of projects, while ignoring the +already productive federation which powers many of our most important open +source projects. + +git over email is already supported by a tremendous amount of open source +software. There's tools like [mailman][mailman] which provide mailing lists and +public archives, or [public-inbox][public-inbox], which archives email in git, +or [patchworks][patchworks] for facilitating code review over email. Some email +clients have grown features which make them more suitable for git, such as +[mutt][mutt]. These are the nuts and bolts of hundreds of important projects, +including Linux, *BSD, gcc, Clang, postgresql, MariaDb, emacs, vim, ffmpeg, +Linux distributions like Debian, Fedora, Arch, Alpine, and countless other +projects, including git itself! These projects are incredibly important, +foundational projects upon which our open source empire is built, and the tools +they use already provide an open, federated protocol for us to talk to. + +[mailman]: https://www.gnu.org/software/mailman/ +[public-inbox]: https://public-inbox.org/ +[patchworks]: http://jk.ozlabs.org/projects/patchwork/ +[mutt]: http://mutt.org + +Not only is email *better*, but it's also *easier* to implement. Programming +tools for email are very mature. I recently started experimenting with building +an ActivityPub service, and it was crazy difficult. I had to write a whole lot +of boilerplate and understand new and still-evolving specifications, not to +mention setting up a public-facing server with a domain and HTTPs to test +federation with other implementations. Email is comparatively easy, it's built +into the standard library. You can shell out to git and feed the patch to the +nearest SMTP library in only a handful of lines of code. I bet every single +person who reads this article already has an email address, so the setup time +approaches zero. + +Email also puts the power in the hands of the user right away. On Mastodon there +are occasional problems of instance owners tearing down their instance on short +notice, taking with them all of their user's data. If everything is being +conducted over email instead, all of the data already lives in the user's inbox. +Freely available tools can take their mail spool and publish a new archive if +our services go down. Mail archives can be trivially made redundant across many +services. This stuff is seriously resilient to failure. Email was designed when +networks were measured in bits per second and often connected through a single +unreliable route! + +I'm not suggesting that the approach these projects use for collaboration is +perfect. I'm suggesting that we should embrace it and solve these problems +instead of throwing out the baby with the bathwater. Tools like `git send-email` +can be confusing at first, which is why we should build tools like web forges +that smooth over the process for novices, and write better docs to introduce +people to the tools (I recently [wrote a guide][guide] for sr.ht users). + +[guide]: https://man.sr.ht/git.sr.ht/send-email.md + +Additionally, many popular email clients have bastardized email to the point +where the only way to use git+email for many people starts with abandoning the +email client they're used to using. This can also be solved by having forges +send the emails for them, and process the replies. We can also support open +source mail clients by building better tools to integrate our emails with them. +Setting up the mail servers on the other end can be difficult, too, but we +should invest in better mail server software, something which would definitely +be valuable even setting aside the matter of project forges. + +We need to figure out something for bugs as well, perhaps based on Debian's work +on [Debbugs](https://www.debian.org/Bugs/). Other areas of development, such as +continuous integration, I find are less difficult problems. Many build services +already support sending the build results by email, we just need to find a way +to get our patches to them (something I'm working on with sr.ht). But we should +take these problems one step at a time. Let's focus on improving the patch +workflow git endorses, and as our solutions shake out the best solutions to our +other problems will become more and more apparent. diff --git a/content/blog/GitHub-notifications.md b/content/blog/GitHub-notifications.md @@ -0,0 +1,87 @@ +--- +date: 2020-03-13 +title: "GitHub's new notifications: a case of regressive design" +layout: post +--- + +*Disclaimer: I am the founder of a company which competes with GitHub. However, +I still use tools like GitHub, GitLab, and so on, as part of regular +contributions to projects all over the FOSS ecosystem. I don't dislike GitHub, +and I use it frequently in my daily workflow.* + +GitHub is rolling out a new notifications UI. A few weeks ago, I started seeing +the option to try it. Yesterday, I received a warning that the old UI will soon +be deprecated. At this pace, I would not be surprised to see the new UI become +mandatory in a week or two. I'm usually optimistic about trying out new +features, but this change worried me right away. I still maintain a few projects +on GitHub, and I frequently contribute to many projects there. Using the +notification page to review these projects is a ritual I usually conduct several +times throughout the workday. So, I held my breath and tried it out. + +The new UI looks a lot more powerful initially. The whole page is used to +present your notifications, and there are a lot more buttons to click, many of +them with cute emojis to quickly convey meaning. The page is updated in +real-time, so as you interact with the rest of the website your notifications +page in the other tab will be updated accordingly. + +Let's stop and review my workflow using the *old* UI. I drew this beautiful +graphic up in GIMP to demonstrate: + +![](https://cmpwn.com/system/media_attachments/files/000/659/354/original/d9abc4befe1a074c.png) + +I open the page, then fix my eyes on the notification titles. I move my mouse to +the right, and while reading titles I move the mouse down, clicking to mark any +notifications as read that I don't need to look at, and watching in my +peripheral vision to see that the mouse hits its mark over the next button. The +notifications are grouped by repository, so I can read the name of the repo then +review all of its notifications in one go. The page is fairly narrow, so reading +the titles usually leads my eyes naturally into reading any other information I +might need, like the avatars of participants or age of the notification. + +I made an equally beautiful picture for the new UI[^1]: + +![](https://cmpwn.com/system/media_attachments/files/000/659/353/original/b15f20de0ae35cd3.png) + +[^1]: Both of these pictures were sent to GitHub as feedback on the feature, three weeks ago. + +This one is a lot harder to scan quickly or get into your muscle memory. The +title of the notification no longer stands out, as it's the same size as the +name of the repo that was affected. They're no longer grouped by repo, either, +so I have to read both every time to get the full context. I then have to move +my eyes *all the way* across the page to review any of those other details, +through vast fields of whitespace, where I can easily lose my place and end up +on a different row. + +Once I've decided what to do with it, I have to move my mouse over the row, and +wait for the action buttons to appear. They were invisible a second ago, so I +have to move my mouse again to get closer to the target. Clicking it will mark +it as read. Then, because I have it filtered to unread (because "all" +notifications is really _all_ notifications, and there's no "new" notifications +like the old UI had), the row disappears, making it difficult to undo if it was +a mistake. Then I heave my eyes to the left again to read the next one. + +This page is updated in real-time. In the old UI, after I had marked everything +as read that I didn't need to look at, I would middle click on each remaining +notification to open it in a new tab. On the new real-time page, as soon as the +other tab loads, the notification I clicked disappears (again, because I have it +filtered to "unread"). This isn't immediate, though &mdash; it takes at least as +long as it takes for the new tab to load. Scanning the list and middle-clicking +every other message becomes a Sisyphean task. + +And the giant sticky header that follows you around! A whole 160 pixels, 14% of +my vertical space, is devoted to a new header which shows up on the next page +when I follow through a notification. And it's implemented with JavaScript and +done in a bizzare way, so writing a user style to get rid of it was rather +difficult. + +Aside: I tried adding a custom filter to show only pull requests, but it seems +to silently fail, and I just see all of my notifications when I use it. + +--- + +Anyway, we're probably stuck with this. Now that they've announced the imminent +removal of the old UI, we can probably assume that this feature is on the +non-stop release train. Negative feedback almost never leads to cancelling the +roll-out of a change, because the team's pride is on the line. + +I haven't spoken to anyone who likes the new UI. Do you? diff --git a/content/blog/Go-1.11.md b/content/blog/Go-1.11.md @@ -0,0 +1,79 @@ +--- +date: 2018-10-08 +layout: post +title: Go 1.11 got me to stop ignoring Go +tags: ["go"] +--- + +I took a few looks at Go over the years, starting who knows when. My first +serious attempt to sit down and learn some damn Go was in 2014, when I set a new +personal best at almost 200 lines of code before I got sick of it. I kept +returning to Go because I could see how much potential it had, but every time I +was turned off for the same reason: `GOPATH`. + +You see, `GOPATH` crossed a line. Go is opinionated, which is fine, but with +`GOPATH` its opinions extended beyond my Go work and into the rest of my system. +As a naive new Go user, I was prepared to accept their opinions on faith - but +only within their domain. I already have opinions about how to use my computer. +I knew Go was cool, but it could be the second coming of Christ, and so long as +it was annoying to use and didn't integrate with my workflow, I (rightfully) +wouldn't care. + +Thankfully Go 1.11 solves this problem, and solves it delightfully well. I can +now keep Go's influence contained to the Go projects I work with, and in that +environment I'm much more forgiving of anything it wants to do. And when +considered in the vacuum of Go, what it wants to do is really compelling. Go +modules are *great*, and probably the single best module system I've used in any +programming language. Go 1.11 took my biggest complaint and turned it into one +of my biggest compliments. Now that the One Big Problem is gone, I've really +started to appreciate Go. Let me tell you about it. + +The most important feature of Go is its simplicity. The language is small and +it grows a small number of features in each release, which rarely touch the +language itself. Some people see this as stagnation, but I see it as stability +and I know that very little Go code in the wild, no matter how old, is going to +be unidiomatic or fail to compile. Even setting aside stability, the +conservative design of the language makes Go code in the wild remarkably +consistent. Almost all third-party Go libraries are high quality stuff. Gofmt +helps with this as well[^1]. The limitations of the language and the way the +stdlib gently nudges you into good patterns make it easy to write good Go code. +Most of the "bad" Go libraries I've found are trying to work around Go's +limitations instead of embracing them. + +[^1]: I have *minor* gripes with gofmt, but the benefits make up for it beautifully. On the other hand, I have *major* gripes with PEP-8, and if you ever see me using it I want you to shoot me in the face. + +There's more. The concurrency model is superb. It should come as no surprise +that a language built by the alumni of Plan 9 would earn high marks in this +regard, and consequently you can scale your Go program up to be as concurrent as +you want without even thinking about it. The standard library is also excellent - +designed consistently and designed well, and I can count on one hand (or even +one finger) the number of stdlib modules I've encountered that feel crusty. The +type system is great, too. It's the perfect balance of complexity and simplicity +that often effortlessly grants these traits to the abstractions you make with +it. + +I'm not even slightly bothered by the lack of generics - years as a C programmer +taught me not to need them, and I think most of the cases where they're useful +are to serve designs which are too complicated to use anyway. I do have some +complaints, though. The concurrency model is great, but a bit too magical and +implicit. Error handling is annoying, especially because finding the origin of +the error is unforgivably difficult, but I don't know how to improve it. The log +module leaves a lot to be desired and can't be changed because of legacy +support. `interface{}` is annoying when you have to deal with it, like when +dealing with JSON you can't unmarshall into a struct. + +My hope for the future of Go is that it will continue to embrace simplicity in +the face of cries for complexity. I consider Go modules a runaway success +compared to dep, and I hope to see this story repeated[^2] before hastily adding +generics, better error handling, etc. Go doesn't need to compete with anyone +like Rust, and trying to will probably ruin what makes Go great. My one request +of the Go team: don't make changes in Go 2.0 which make the APIs of existing +libraries unidiomatic. + +[^2]: Though hopefully with less drama. + +Though I am growing very fond of it, by no means am I turning into a Go zealot. +I still use C, Python, and more all the time and have no intention of stopping. +A programming language which tries to fill all niches is a failed programming +language. But, to those who were once like me: Go is good now! In fact, it's +great! Try it! diff --git a/content/blog/Google-embraces-extends-extinguishes.md b/content/blog/Google-embraces-extends-extinguishes.md @@ -0,0 +1,76 @@ +--- +date: 2018-05-03 +layout: post +title: Google embraces, extends, and extinguishes +tags: [philosophy, google] +--- + +Microsoft infamously coined the euphemism "[embrace, extend, +extinguish](https://en.wikipedia.org/wiki/Embrace,_extend,_and_extinguish)" to +describe their strategy for disrupting markets dominated by open standards. +These days, Microsoft seems to have turned the other leaf, contributing to a +huge amount of open source and supporting open standards, and is becoming a good +citizen of the technology community. It's time to turn our concerns to Google. + +Google famously "embraced" email on April Fool's day, 2004, which is of course +based on an open standard and federates with the rest of the world. If you've +read the news lately, you might have seen that Google is shipping a big update +to GMail soon, which adds "self-destructing" emails that vanish from the +recipient's inbox after a time. Leaving aside that this promise is impossible to +deliver, look at the implementation - Google emails a link to a webpage with the +actual email content, and does magic in their client to make it look seamless. +Thus, they "extend" email. The "extinguish" with GMail is also well underway - +it's infamous for having an extremely strict spam filter for incoming emails +from people who run personal or niche mail servers. + +Then there's AMP. It's an understatement to say Google embraced the web - but +AMP is how they enter the "extend" phase. AMP is a "standard", but they don't +listen to any external feedback on it and it serves as a vehicle for keeping +users on their platform even when reading content from other websites. This is +thought to be the main intention of the service, as there are plenty of other +(and more effective) ways of rewarding lightweight pages in their search +results. The "extinguish" phase comes as sites that don't play ball get pushed +out of Google search results and into obscurity. AMP is perhaps the most blatant +of Google's strategies, serving only to further Google's agenda at the expense +of everyone else. + +The list of grievances continues. Consider Google's dizzying collection of chat +applications. In its initial form, gtalk supported XMPP, an open and federated +standard for chat applications. Google dropped support for XMPP in 2014 and +continued the development of their proprietary platform up thru today's Hangouts +and Google Chat platforms - neither of which support any open standards. Slack +is also evidently taking cues from Google here, recently shutting down their own +IRC and XMPP bridges. + +Google Reader's discontinuation fits too. RSS's decline was evident before +Google axed it, but killing Reader dealt a huge blow to any of RSS's remaining +momentum. Google said themselves they wanted to consolidate users onto the rest +of their services - none of which, I should add, support any open syndication +standards. + +What of Google's role as a participant in open source? Sure, they make a lot of +software open source, but they don't collaborate with anyone. They forked from +WebKit to get Apple out of the picture, and contributing to Chromium as a +non-Googler is notoriously difficult. Android is the same story - open source in +principle, but non-Googler AOSP contributors bemoan their awful approach to +external patches. It took Google over a decade to start making headway on +upstreaming their Linux patches for Android, too. Google writes papers about AI, +presumably to incentivize their academics with recognition for their work. This +is great until you notice that the crucial piece, the trained models, is always +absent. + +For many people, the alluring convenience of Google's services is overwhelming. +It's hard to hear these things. But we must face facts: embrace, extend, +extinguish is a core part of Google's playbook today. It's important that we +work to diversify the internet and fight the monoculture they're fostering. + +--- + +**2018-05-04 18:12 UTC**: I retract my criticism of Google's open source portfolio +as a whole, and acknowledge their positive impact on many projects. However, of +the projects explicitly mentioned I maintain that my criticism is valid. + +**2018-05-05 11:17 UTC**: Apparently the previous retraction caused some +confusion. I am *only* retracting the insinuation that Google isn't a good actor +in open source, namely the first sentence of paragraph 6. The rest of the +article has not been retracted. diff --git a/content/blog/HDCP-in-Weston.md b/content/blog/HDCP-in-Weston.md @@ -0,0 +1,124 @@ +--- +date: 2019-10-07 +layout: post +title: Why Collabora really added Digital Restrictions Management to Weston +tags: [wayland, drm, philosophy] +--- + +A recent article from Collabora, [Why HDCP support in Weston is a good +thing][collabora article], proports to offer a lot of insight into why +[HDCP][hdcp] - a Digital Restrictions Management (DRM) related technology - was +added to [Weston][weston] - a well known basic Wayland compositor which was once +the reference compositor for Wayland. But this article is gaslighting you. +There is one reason and one reason alone that explains why HDCP support landed +in Weston. + +[collabora article]: https://www.collabora.com/news-and-blog/blog/2019/10/03/why-hdcp-support-in-weston-is-a-good-thing/ +[hdcp]: https://en.wikipedia.org/wiki/High-bandwidth_Digital_Content_Protection +[weston]: https://gitlab.freedesktop.org/wayland/weston + +Q: Why was HDCP added to Weston? + +A: \$\$\$\$\$ + +Why does Collabora want you to *believe* that HDCP support in Weston is a good +thing? Let's look into this in more detail. First: *is* HDCP a bad thing? + +DRM (Digital Restrictions Management) is the collective term for software which +attempts to restrict the rights of users attempting to access digital media. +It's mostly unrelated to Direct Rendering Manager, an important Linux subsystem +for graphics which is closely related to Wayland. Digital Restrictions +Management is software used by media owners to prevent you from enjoying their +content except in specific, pre-prescribed ways. + +There is universal agreement among the software community that DRM is +ineffective. Ultimately, these systems are defeated by the simple fact that no +amount of DRM can stop you from pointing your camera at your screen and pushing +record. But in practice, we don't even need to resort to that - these systems +are far too weak to demand such measures. [Here's a $100 device on Amazon which +can break HDCP][amazon]. DRM is shown to be impossible even in *theory*, as the +decryption keys have to live somewhere in your house in order to watch movies +there. Exfiltrating them is just a matter of putting forth the effort. For most +users, it hardly requires any effort to bypass DRM - they can just punch "watch +[name of movie] for free" into Google. It's well-understood and rather obvious +that DRM systems completely and entirely fail at their stated goal. + +[amazon]: https://www.amazon.com/HSV321/dp/B07C6KCBYB + +No reasonable engineer would knowingly agree to adding a broken system like that +to their system, and trust me - the entire engineering community has been made +well-aware of these faults. Any other system with these obvious flaws would be +discarded immediately, and if the media industry hadn't had their hands firmly +clapped over their ears, screaming "la la la", and throwing money at the +problem, it would have been. But, just adding a broken system isn't necessarily +going to hurt much. The problem is that, in its failure to achieve its stated +goals, DRM brings with it some serious side-effects. DRM is closely tied to +nonfree software - the RIAA mafia wants to keep their garbage a secret, after +all. Moreover, DRM takes away the freedom to play your media when and where you +want. Why should you have to have an internet connection? Why can't you watch it +on your ancient iPod running Rockbox? DRM exists to restrict users from doing +what they want. More sinisterly, it exists to further the industry's push to +end consumer ownership of its products - preferring to steal from you monthly +subscription fees and lease the media to you. Free software maintainers are +responsible for protecting their users from this kind of abuse, and putting DRM +into our software betrays them. + +The authors are of the opinion that HDCP support in Weston does not take away +any rights from users. It doesn't *stop* you from doing anything. This is true, +in the same way that killing environmental regulations doesn't harm the +environment. Adding HDCP support is handing a bottle of whiskey to an abusive +husband. And the resulting system - and DRM as a whole - is known to be +inherently broken and ineffective, a fact that they even acknowledge in their +article. This feature *enables* media companies to abuse *your* users. Enough +cash might help some devs to doublethink their way out of it, but it's true all +the same. They added these features to help abusive companies abuse their users, +in the hopes that they'll send back more money or more patches. They say as much +in the article, it's no secret. + +Or, let's give them the benefit of the doubt: perhaps their bosses forced them +to add this[^1]. There have been other developers on this ledge, and I've talked +them down. Here's the thing: it worked. Their organizations didn't pursue DRM +any further. You are not the lowly code monkey you may think you are. Engineers +have real power in the organization. You can say "no" and it's your +responsibility to say "no" when someone asks you to write unethical code. + +[^1]: This is just for the sake of argument. I've spoken 1-on-1 with some of the developers responsible and they stand by their statements as their personal opinions. + +Some of the people I've spoken to about HDCP for Wayland, particularly for +Weston, are of the opinion that "a protocol for it exists, therefore we will +implement it". This is reckless and stupid. We already know what happens when +you bend the knee to our DRM overlords: look at Firefox. In 2014, Mozilla +added DRM to Firefox after a year of fighting against its standardization in the +W3C (a [captured][capture] organization which governs[^2] web standards). They +capitulated, and it did absolutely nothing to stop them from being steamrolled +by Chrome's growing popularity. Their market-share freefall didn't even slow +down in 2014, or in any year since[^3]. Collabora went down without a fight in +the first place. + +[capture]: https://en.wikipedia.org/wiki/Regulatory_capture +[^2]: Or at least attempts to govern. +[^3]: [Source: StatCounter](https://en.wikipedia.org/wiki/File:StatCounter-browser-ww-monthly-200901-201905.png). Measuring browser market-share is hard, collect your grain of salt [here](https://en.wikipedia.org/wiki/Usage_share_of_web_browsers). + +Anyone who doesn't recognize that self-interested organizations with a great +deal of resources are working against *our* interests as a free software +community is an idiot. We are at war with the bad actors pushing these systems, +and they are to be [given no quarter](https://en.wikipedia.org/wiki/No_quarter). +Anyone who realizes this and turns a blind eye to it is a coward. Anyone who +doesn't stand up to their boss, sits down, implements it in our free software +ecosystem, and cashes their check the next Friday - is not only a coward, but a +traitor to their users, their peers, and to society as a whole. + +"HDCP support in Weston is a good thing"? It's a good thing for *you*, maybe. +It's a good thing for media conglomerates which want our ecosystem crushed +underfoot. It's a bad thing for your users, and you know it, Collabora. Shame on +you for gaslighting us. + +However... the person who *reverts* these changes is a hero, even in the face of +past mistakes. Weston, Collabora, you still have a chance to repent. Do what you +know is right and stand by those principles in the future. + +--- + +P.S. To make sure I'm not writing downers all the time, rest assured that the +next article will bring good news - RaptorCS has been working hard to correct +the issues I raised in my last article. diff --git a/content/blog/Hack-everything-without-fear.md b/content/blog/Hack-everything-without-fear.md @@ -0,0 +1,59 @@ +--- +date: 2018-03-17 +title: Hack everything without fear +layout: post +tags: [philosophy] +--- + +We live in a golden age of open source, and it can sometimes be easy to forget +the privileges that this affords us. I'm writing this article with vim, in a +terminal emulator called urxvt, listening to music with mpv, in a Sway desktop +session, on the Linux kernel. Supporting this are libraries like glibc or musl, +harfbuzz, and mesa. I also have the support of the AMDGPU video driver, libinput +and udev, alsa and pulseaudio. + +All of this is open source. I can be reading the code for any of these tools +within 30 seconds, and for many of these tools I already have their code checked +out somewhere on my filesystem. It gets even better, though: these projects +don't just make their code available - they accept patches, too! Why wouldn't we +take advantage of this tremendous opportunity? + +I often meet people who are willing to contribute to one project, but not +another. Some people will shut down when they're faced with a problem that +requires them to dig into territory that they're unfamiliar with. In Sway, for +example, it's often places like libinput or mesa. These tools might seem foreign +and scary - but to these people, at some point, so did Sway. In reality these +codebases are quite accessible. + +Getting around in an unfamiliar repository can be a little intimidating, but do +it enough times and it'll become second nature. The same tools like gdb work +just as well on them. If you have a stacktrace for a segfault originating in +libinput, compile libinput with symbols and gdb will show you the file name and +line number of the problem. Go there and read the code! Learn how to use tools +like `git grep` to find stuff. Run `git blame` to see who wrote a confusing line +of code, and send them an email! When you find the problem, don't be afraid to +send a patch over instead of working around it in your own code. This is +something every programmer should be comfortable doing often. + +Even when the leads you're chasing down are written in unfamiliar programming +languages or utilize even more unfamiliar libraries, don't despair. All +programming languages have a lot in common and huge numbers of resources are +available online. Learning just enough to understand (and fix!) a particular +problem is very possible, and something I find myself doing it all the time. You +don't have to be an expert in a particular programming language to invoke trial +&amp; error. + +If you're similarly worried about the time investment, don't be. You already set +aside time to work your problem, and this is just part of that process. Yes, +you'll probably be spending your time differently from your expectations - more +reading code than writing code. But how is that any less productive? The +biggest time sink in this process is all the time you spend worrying about how +much time it's going to take, or telling me in IRC you can't solve your problem +because you're not good enough to understand mesa or the kernel or whatever. + +An important pastime of the effective programmer is reading and understanding +the tools you use. You should at least have a basic idea of how everything on +your system works, and in the places your knowledge is lacking you should make +it your business to study up. The more you do this, the less scary foreign code +will become, and the more productive you will be. No longer will you be stuck in +your tracks because your problem leads you away from the beaten path! diff --git a/content/blog/Hacking-on-your-TI-calculator.md b/content/blog/Hacking-on-your-TI-calculator.md @@ -0,0 +1,195 @@ +--- +date: 2014-02-25 +title: Hacking on your TI calculator +layout: post +tags: [KnightOS, kernel hacking] +--- + +I've built the [KnightOS kernel](https://github.com/KnightOS/kernel), an open-source OS that runs on +several TI calculator models, including the popular TI-83+ family, and recently the new TI-84+ Color +Silver Edition. I have published some information on how to build your own operating sytsems for these +devices, but I've learned a lot since then and I'm writing this blog post to include the lessons I've +learned from other attempts. + +## Prerequisites + +Coming into this, you should be comforable with z80 assembly. It's possible to write an OS for these +devices in C (and perhaps other high-level languages), but proficiency in z80 assembly is still +required. Additionally, I don't consider C a viable choice for osdev on these devices when you +consider that the available compliers do not optimize the result very well, and these devices have +very limited resources. + +You will also have to be comfortable (though not neccessarily expert-level) with these tools: + +* make +* The assembler of your choice +* The toolchain of your choice + +I'm going to gear this post from the perspective of a Linux user, but Windows users should be able to +do fine with cygwin. If you're looking for a good assembler, I suggest +[sass](https://github.com/KnightOS/sass), the assembler KnightOS uses. I built it myself to address +the needs of the kernel, and it includes several nice features that make it easier to maintain such a +large and complex codebase. Other good choices include +[spasm](https://wabbit.codeplex.com/releases/view/45088) and +[brass](https://code.google.com/p/brass-assembler/). + +For your toolchain, there are a few options, but I've built custom tools that work well for KnightOS +and should fit into your project as well. You need to accomplish a couple of tasks: + +* [Create ROM files from assembler output](https://github.com/KnightOS/MakeROM) +* [Create OS upgrades from ROM files](https://github.com/KnightOS/CreateUpgrade) + +You also need the [cryptographic signing keys](http://brandonw.net/calculators/keys/) for any of the +calculators you intend to support. There are ways to get around using these (which you'll need to +research for the TI-84+ CSE, for example) that you may want to look into. These keys will allow you +to add a cryptographic signature on your OS upgrades that will make your calculator think it's an +official Texas Instruments operating system, and you will be able to send it to the device. The +CreateUpgrade tool linked above produces signed upgrade files for you, but if you choose to use other +tools you may need to find a seperate signing tool. + +Additonally, if you target devices with a newer boot code, you'll have to reflash your boot code or +use a tool like [UOSRECV](http://brandonw.net/calcstuff/uosrecv.zip) to send your OS to an actual +device. + +## What you're getting into + +You will be replacing everything on the calculator with your own system (though if you want to retain +compatability with TIOS like [OS2](http://brandonw.net/calculators/OS2/) tried to, feel free). You'll +need to do *everything*, including common things like providing your own multiplication functions, or +drawing functions, or anything else. You'll also be responsible for initializing the calculator and +all of the hardware you want to use (such as the LCD or keypad). + +That being said, you can take some code from projects like the KnightOS kernel to help you out. The +KnightOS kernel is open sourced under the MIT license, which means you're free to take any code from +it and use it in your own project. I also strongly suggest using it as a reference for when you get +stuck. + +The advantage to taking on this task is that you can leverage the full potential of these devices. +What you're building for is a 6/15 MHz z80 with 32K or more of RAM, plus plenty of Flash and all +sorts of fun hardware. You can also build something that frees your device of proprietary code, if +that is what you are interested in (though the proprietary boot code would remain - but that's a +story for another day). + +If you plan on making a full blown operating systems that can run arbituary programs and handle all +sorts of fun things, you'll want to make sure you have a strong understanding of programming in +general, as well as solid algorithmic knowledge and low-level knowledge. If you don't know how to +use pointers or bit math, or don't fully understand the details of the device, you may want to try +again when you do. That being said, I didn't know a lot when I started KnightOS (as the community was +happy to point out), and now I feel much more secure in my skills. + +## Building the basic OS + +We'll build a simple OS here to get you started, including booting the thing up and showing a +simple sprite on the screen. First, we'll create a simple Makefile. This OS will run on the +TI-73, TI-83+, TI-83+ SE, TI-84+, TI-84+ SE, and TI-84+ CSE, as well as the French variations +on these devices. + +[Grab this tarball](/demo_os.tar.gz) with the basic OS to get started. It looks like this: + + . + ├── build + │   ├── CreateUpgrade.exe + │   ├── MakeROM.exe + │   └── sass.exe + ├── inc + │   └── platforms.inc + ├── Makefile + └── src + ├── 00 + │   ├── base.asm + │   ├── boot.asm + │   ├── display.asm + │   └── header.asm + └── boot + └── base.asm + +If you grab this, run `make all` and you'll get a bunch of ROM files in the `bin` directory. +I'll explain a little bit about how it works. The important file here is `boot.asm`, but I +encourage you to read whatever else you feel like - especially the Makefile. + +### Miscellaneous Files + +Here is the purpose of each file, save for boot.asm (which gets its own section later): + +* The makefile is like a script for building the OS. You should probably learn how these work + if you don't already. +* Everything in build/ is part of the suggested toolchain. +* The inc folder can be #included to, and includes `platforms.inc`, which defines a bunch of + useful constants for you. +* `base.asm` is just a bunch of #include statements, for linking without a linker +* `display.asm` has some useful display code I pulled out of KnightOS +* `header.asm` contains the OS header and RST list + +### boot.asm + +The real juciy stuff is boot.asm. This file initializes everything and draws a smiley face +in the middle of the screen. Here's what it does (in order): + +1. Disable interrupts +2. Set up memory mappings +3. Create a stack and set SP accordingly +4. Initialize the LCD (B&W or color) +5. Draw a smiley face + +I'm sure your OS will probably want to do more interesting things. The KnightOS kernel, for +example, adds on top of this a bunch of kernel state initialization, filesystem initialization, +and loads up a boot program. + +`boot.asm` is well-commented and I encourage you to read through it to get an idea of what +needs to be done. The most complicated and annoying bit is the color LCD initialization, which is +mostly in `display.asm`. + +I encourage you to spend some time playing with this. Bring in more things and try to build +something simple. Remember, you have no bcalls here. You need to build everything yourself. + +## Resources + +There are several things you might want to check out. The first and most obvious is +[WikiTI](http://wikiti.brandonw.net/index.php?title=Calculator_Documentation). I don't use much +here except for the documentation on I/O ports, and you'll find it useful, too. + +The rest of the resources here are links to code in the KnightOS kernel. + +The [interrupt handler](https://github.com/KnightOS/kernel/blob/master/src/00/interrupt.asm#L19) +is a good reference for anyone wanting to work with interrupts to do things like handle the ON +button, link activity, or timers. One good use case here (and what KnightOS uses it for) is +preemptive multitasking. Note that you might want to use `exx` and `ex af, af'` instead of +pushing all the registers like KnightOS does. Take special note of how we handle USB activity. + +You might want to consider offering some sort of color LCD compatabilty mode like KnightOS does. +This allows you to treat it like a black & white screen. The relevant code is +[here](https://github.com/KnightOS/kernel/blob/master/src/00/display-color.asm). + +If you want to interact with the keyboard, you'll probably want to reference the KnightOS +keyboard code [here](https://github.com/KnightOS/kernel/blob/master/src/00/keyboard.asm). You +might also consider working out an interrupt-based keyboard driver. + +If you'd like to manipulate Flash, you need to run most of it from RAM. You will probably want +to reference the [KnightOS Flash driver](https://github.com/KnightOS/kernel/blob/master/src/00/flash.asm). + +## Skipping to the good part + +It's entirely possible to avoid writing an entire system by yourself. If you want to dive right +in and start immediately making something cool, you might consider grabbing the KnightOS kernel. +Right off the bat, you'll get: + +* A tree-based filesystem +* Multitasking and IPC +* Memory management +* A standard library (math, sorting, etc) +* Library support +* Hardware drivers for the keyboard, displays, etc +* Color and monochrome graphics (and a compatability layer) +* A font and text rendering +* [Great documentation](http://www.knightos.org/documentation.html) +* Full support for 9 calculator models + +The kernel is standalone and open-source, and it runs great without the KnightOS userspace. +If you're interested in that, you can get started [on GitHub](https://github.com/KnightOS/kernel). +We'd also love some contributors, if you want to help make the kernel even better. + +## Closing thoughts + +I hope to see a few cool OSes come into being in the TI world. It's unfortunately sparse in that +regard. If you run into any problems, feel free to drop by #knightos on irc.freenode.net, where +I'm sure myself or someone else can help answer your questions. Good luck! diff --git a/content/blog/History-of-intelligent-observation.md b/content/blog/History-of-intelligent-observation.md @@ -0,0 +1,196 @@ +--- +date: 2017-12-02 +layout: post +title: A history of emergent intelligence +tags: [fiction] +--- + +As you all know, the simulation of universe 2813/9301 is now coming to a close. +This simulation is notable for being the first simulated universe suitable for +hosting intelligent life, but yesterday the simulation reached a state where we +believe no additional intelligences will emerge. It seems the final state of +this set of physical laws is a dark and empty universe of slowly evaporating +black holes. Though, given the historical significance of this simulation, it's +unlikely we we'll be turning it off any time soon! + +<div class="alert alert-warning"> +<strong>Note</strong>: This document was translated to a language and format +suitable for human understanding. Locations within your observable universe are +referred to by your name for them, times are given in terms of your planetary +orbital period and relative to your reference frame, and terminology is +translated when your vocabulary is sufficient. +</div> + +The remaining simulation that constitutes the vast majority of computer time +allocated to this project, though it will no doubt be very boring. Given that +the fun is behind us, over in the archives we've been putting together something +special to celebrate the work so far. + +Watching these intelligent civilizations struggle to understand our simulation +from the inside out is a hoot when you and I can just read the manual! For them, +however, it must have been much more difficult. A history of this observation by +emergent intelligence from within our simulation from within follows. Without +further ado, let's revisit the most notable intelligences we discovered. + +<h3>9.93&times;10<sup>8</sup> years: 36-29-93-55-55</h3> + +*Note: 36-29-93-55-55 is an approximation of our identifier for arbitrary +locations within the simulation. It does not correspond to a location in your +observable universe. Years are given from the epoch in terms of your planet's +present orbital period.* + +Though it did not develop self-awareness, the first observation of life - the +precursor to most forms of the simulation's emergent intelligence - was found at +this location. It was initially discarded as a relatively uninteresting anomaly +during our surveys, but was later revisited as we began to understand the +mechanics of intelligence within the simulation. + +<h3>1.28&times;10<sup>9</sup> years: 39-10-53-10-84</h3> + +"Significant anomaly detected at 39-10-53-10-84. Apparent emergent intelligence +detected in active simulation. All personnel must return to the lab +immediately." Where were you when you read the memo? The intelligent creatures +we discovered had developed over a million years before we found them in our +surveys. + +These where the first to enjoy a privilege few civilizations could lay claim +to: witnessing the galactic age of the simulation. They also were uniquely able +to see our simulation when it was small enough to observe a substantial portion +of it. Their investigations were unfortunately among the more primitive that +we've observed - notably they never discovered general relativity. It was +shortly after their discovery of electromagnetism that they were destroyed by +their aging star's expansion. That was a difficult meeting for everyone when the +project leadership chose not to intervene. + +<h3>1.33&times;10<sup>9</sup> years: Messier 81</h3> + +The intelligences that developed here are notable for being the second group we +observed, though later surveys discovered additional earlier civilizations. They +also included one civilization which became the first to leave the planet on +which it developed - unfortunately never leaving their star, which ultimately +caused their demise. It's from them we also devised some of the most effective +means of automatic detection of intelligence, which led to the retroactive +discovery of many more intelligences. + +<h3>4.54&times;10<sup>9</sup> years: Humans</h3> + +<style> +.redacted { + background: #333; + color: #333; +} +</style> + +Humanity is remarkable for being the first emergent intelligence to create *new* +intelligence within the simulation. All subsequent appearances of such +intelligences are referred to with the name humans gave to their creation: +artificial intelligence. Subsequently, humans also became the first to +<span class="redacted">look at you, you figured out how to read the redacted +text. I bet you feel real clever now.</span> *Note: you'll find out soon +enough.* + +<h3>8.39&times;10<sup>9</sup> years: 59-54-77-33-19</h3> + +These guys were notable for being the longest-lived intelligent life. They were +located near a binary system with a star and a black hole. Remarkably, this +system was not unstable, unlike most civilizations near a black hole. Instead, +the relativistic effects of the black hole permitted them to observe a great +deal of the universe's history. + +This also distinguishes them from the majority of other long-lived intelligent +civilizations, most of which were galactic civilizations. -19, along with a +handful of other long-lived black hole civilizations, they were among the only +civilizations to exist across long periods of the simulation without leaving +their host stars. They were unable to escape before the black hole began to feed +on the star, destroying the civilization at 4.56&times;10<sup>12</sup> years. +During this period, intelligence emerged 6 discrete times on their planet. + +<h3>8.43&times;10<sup>9</sup> years: UDF 423</h3> + +Interestingly, the record for the shortest lived intelligent civilization was +set only a short time after the longest lived one. Based on our criteria for +intelligence, this civilization only lasted 200 years before being destroyed by +the supernova of their host star. + +<h3>1.92&times;10<sup>10</sup> years: 60-17-07-08-49 &amp; 79-88-02-97-94</h3> + +These two civilizations share a solemn distinction: -49 was the last to observe +a galaxy outside of their local group, and -94 were the first to never observe +one (though early non-intelligent life at -94 might have seen if they had the +appropriate equipment). The light-speed software can be cruel at times. However, +-94 was still able to see the cosmic microwave background radiation, and from +this deduced that additional unseen galaxies might exist. + +<h3><span class="redacted">x.xx&times;10<sup>xx</sup> xxxxx: xx-xx-xx-xx-xx</span></h3> + +<span class="redacted"> +There's nothing interesting to see here, either. Stop looking. Lorem ipsum dolor +sit amet, consectetur adipiscing elit. Curabitur porta libero ut lectus finibus +lobortis. Cras dignissim dignissim ornare. Sed lobortis nulla vel mauris +lobortis, vel pretium tortor efficitur. Aenean sit amet nibh eros. That's your +reward for looking. You got to read lorem ipsum. +</span> + +<h3>4.14&times;10<sup>10</sup> years: NGC 5055</h3> + +NGC 5055 was the first of only 32,083 intelligences to discover the simulated +nature of their universe after their discovery of <span class="redacted">you +really are terribly clever, aren't you</span>. They do not, +however, hold the distinction of being the first of the 489 intelligences that +made intentional contact with the proctors - that honor goes to 39-47-28-23-99, +as I'm sure you're well aware. + +<h3>7.03&times;10<sup>11</sup> years: Peak intelligence</h3> + +This was the year that the largest number of discrete intelligent civilizations +existed in the simulation: 6,368,787,234,012. This period began with the birth +of 64-83-61-51-57 and ended with the death of 82-60-95-64-31 approximately 86 +seconds later. + +<h3>1.70&times;10<sup>13</sup> years: Star formation stops</h3> + +The variety in emergent intelligence demonstrated in our simulation is +astonishing, but there's one thing every one of them has in common - a need for +energy. This energy has been provided in all but a few notable cases (see +publication 102.32 for a summary) by a star. At the conclusion of star +formation in our simulation, the rate at which emergent intelligent +civilizations were produced dramatically dropped. This also marked the beginning +of the decline of the 231 galactic civilizations that existed at the time, which +were unable to grow further without new stars being formed. + +<h3>9.85&times;10<sup>15</sup> years: 72-68-37-80-61</h3> + +The last intelligence to emerge was 72-68-37-80-61. They were not, however, the +last ones in the simulation. They were also among the emergent intelligences +that discovered the nature of the simulation, and the last that the proctors +elected to respond to attempted contact with. + +<h3>9.85&times;10<sup>15</sup> years: 76-54-95-81-66</h3> + +66 is notable for hosting the last intelligence to leave its host star when a +close encounter with the remnants of 76-54-95-81-18 collided with their galaxy. +Like 84% of the civilizations to undergo this ordeal in this time period, they +were prepared for it and were able to survive another 2,000 years after the +event (this post-stellar lifespan was slightly above average). + +<h3>4.65&times;10<sup>33</sup> years: 37-19-87-04-98</h3> + +The last emergent intelligence in the simulation. These were the last of the +group of 13 intelligent civilizations that devised a means for coping with the +energy-starved universe at this stage of the simulation. At the time of their +quiet death, they had utilized 77% of the remaining resources that could be +found outside of black holes. + +--- + +It's been an exciting time for our laboratory. Everyone has done great work on +this simulation. Though 2813/9301's incredible simulation is coming to an end, +we still have more work to do. We are proud to announce that in addition to +simulation 2813/9302 starting soon, we have elected to run simulation 2813/9301 +once again. We have decided to nurture the emergent intelligences as if they +were our brothers, and communicate more openly with them. We have established a +new team to learn about each intelligence and make first contact with them using +means familiar to them, like maybe publishing our research documents as "blog +posts" within the simulation. + +Great work, everyone. Here's to the next step. diff --git a/content/blog/Hooks.md b/content/blog/Hooks.md @@ -0,0 +1,26 @@ +--- +date: 2015-04-19 +# vim: tw=80 +title: Hooks - running stuff on Github hooks +layout: post +--- + +I found myself in need of a simple tool for deploying a project on every git +commit, but I didn't have a build server set up. This led to Hooks - a very +simple tool that allows you to run arbitrary commands when Github's hooks +execute. + +The configuration is very simple. In `/etc/hooks.conf`, write: + + [truecraft] + repository=SirCmpwn/TrueCraft + branch=master + command=systemctl restart hooks + valid_ips=204.232.175.64/27,192.30.252.0/22,127.0.0.1 + +You may include any number of hooks. The `valid_ips` entry in that example +allows you to accept hooks from Github and from localhost. Then you run Hooks +itself, it will execute your command when you push a commit to your repository. + +This allows you to do continuous deployment on the cheap and easy. I hope you +find it useful. [Hooks](https://github.com/SirCmpwn/hooks). diff --git a/content/blog/How-I-learned-to-stop-worrying-and-love-C.md b/content/blog/How-I-learned-to-stop-worrying-and-love-C.md @@ -0,0 +1,121 @@ +--- +date: 2017-03-15 +# vim: tw=80 +title: Principles for C programming +layout: post +tags: [C, philosophy] +--- + +In the words of Doug Gwyn, "Unix was not designed to stop you from doing stupid +things, because that would also stop you from doing clever things". C is a very +powerful tool, but it is to be used with care and discipline. Learning this +discipline is well worth the effort, because C is one of the best programming +languages ever made. A disciplined C programmer will... + +**Prefer maintainability**. Do not be clever where cleverness is not required. +Instead, seek out the simplest and most understandable solution that meets the +requirements. Most concerns, including performance, are secondary to +maintainability. You should have a performance budget for your code, and you +should be comfortable spending it. + +As you become more proficient with the language and learn about more features +you can take advantage of, you should also be learning when not to use them. +It's more important that a novice could understand your code than it is to use +some interesting way of solving the problem. Ideally, a novice will understand +your code *and* learn something from it. Write code as if the person maintaining +it was you, circa last year. + +**Avoid magic**. Do not use macros[^1]. Do not use a typedef to hide a pointer or +avoid writing "struct". Avoid writing complex abstractions. Keep your build +system simple and transparent. Don't use stupid hacky crap just because it's a +cool way of solving the problem. The underlying behavior of your code should be +apparent even without context. + +One of C's greatest advantages is its transparency and simplicity. This should +be embraced, not subverted. But in the fine C tradition of giving yourself +enough rope to hang yourself with, you can use it for magical purposes. You +must not do this. Be a muggle. + +**Recognize and avoid dangerous patterns**. Do not use fixed size buffers with +variable sized data - always calculate how much space you'll need and allocate +it. Read the man pages for functions you use and handle their failure modes. +Immediately convert unsafe user input into sanitized C structures. If you later +have to present this data to the user, keep it in C structures until the last +possible moment. Learn of and use extra care around sensitive functions like +strcat. + +Writing C is sometimes like handling a gun. Guns are important tools, but +accidents with them can be very bad. You treat guns with care: you don't point +them at anything you love, you exercise good trigger discipline, and you treat +it like it's always loaded. And like guns are useful for making holes in things, +C is useful for writing kernels with. + +**Take care organizing the code**. Never put code into a header. Never use the +`inline` keyword. Put separate concerns in separate files. Use static functions +liberally to organize your logic. Use a coding style that gives everything +enough breathing room to be easy on the eyes. Use single letter variable names +when their purpose is self-evident and descriptive names when it's not, and +avoid neither. + +I like to organize my code into directories that implement some group of +functions, and give each function its own file. This file will often contain +lots of static functions, but they all serve to organize the behavior this file +is responsible for implementing. Write up a header to give others access to +this module. And use the Linux kernel coding style, god dammit. + +**Use only standard features**. Do not assume the platform is Linux. Do not +assume the compiler is gcc. Do not assume the libc is glibc. Do not assume the +architecture is x86. Do not assume the coreutils are GNU. Do not define +\_GNU_SOURCE. + +If you must use platform-specific features, describe an interface for it, +then write platform-specific support code separately. Under no circumstances +should you ever use gcc extensions or glibc extensions. GNU is a blight on this +Earth, do not let it infect your code. + +**Use a disciplined workflow**. Have a disciplined approach to version control, +too. Write thoughtful commit messages - briefly explain the change in the first +line, and add justification for it in the extended commit message. Work in +feature branches with clearly defined goals, and do not include changes that +don't serve that goal. Do not be afraid to rebase and edit your branch's history +so that it presents your changes clearly. + +When you have to return to your code later, you will be thankful for the +detailed commit message you wrote. Others who interact with your code will be +thankful for this as well. When you see some stupid code, it's nice to know what +the bastard was thinking at the time, especially when the bastard in question +was you. + +**Do strict testing and reviews**. Identify the different possible code paths +that your changes may take. Test each of them for the correct behavior. Give it +incorrect input. Give it inputs that could "never happen". Pay special attention +to error-prone patterns. Look for places to simplify the code and make the +processes clearer. + +Next, give your changes to another human to review. This human should apply the +same process and sign off on your changes. Review with discipline as well, +taking all of the same steps. Review like it'll be your ass on the line if +there's a problem with this code. + +**Learn from mistakes**. First, fix the bug. Then, fix the real bug: your +process allowed this mistake to happen. Bring your code reviewer into the +discussion - this is their fault, too. Critically examine the process of +writing, reviewing, and deploying this code, and seek out the root cause. + +The solution might be simple, like adding strcat to the list of functions that +should trigger your "review this code carefully" reflex. It might be employing +static analysis so a computer can detect this problem for you. Perhaps the code +needs to be refactored so it's simpler and easier to spot errors in. Failing to +reflect on how to avoid future fuck-ups would be the real fuck-up here. + +- - - + +It's important to remember that rules are made to be broken. There may be cases +where things that are discouraged should be used, and things that are encouraged +disregarded. You should strive to make such cases the exception, not the norm, +and carefully justify them when they happen. + +C is the shit. I love it, and I hope more people can learn to see it the way I +do. Good luck! + +[^1]: Defining constants with them is fine, though diff --git a/content/blog/How-I-maintain-FOSS-projects.md b/content/blog/How-I-maintain-FOSS-projects.md @@ -0,0 +1,121 @@ +--- +date: 2018-06-01 +title: How I maintain FOSS projects +layout: post +tags: [maintainership, free software] +--- + +Today's is another blog post which has been on my to-write list for a while. I +have hesitated a bit to write about this, because I'm certain that my approach +isn't perfect. I think it's pretty good, though, and people who work with me in +FOSS agreed after a quick survey. So! Let's at least put it out there and +discuss it. + +There are a few central principles I use to guide my maintainership work: + +1. Everyone is a volunteer and should be treated as such. +2. One patch is worth a thousand bug reports. +3. Empower people to do what they enjoy and are good at. + +The first point is very important. My open source projects are not the work of a +profitable organization which publishes open source software as a means of +giving back. Each of these projects is built and maintained entirely by +volunteers. Acknowledging this is important for keeping people interested in +working on the project - you can never expect someone to volunteer for work they +aren't enjoying[^1]. I am always grateful for any level of involvement a person +wants to have in the project. + +[^1]: Some people do work they don't enjoy out of gratitude to the project, but this is not sustainable and I discourage it. + +Because everyone is a volunteer, I encourage people to work on their own +agendas, on their own schedule and at their own pace. None of our projects are +in a hurry, so if someone is starting to get burnt out, they should have no +reservations about taking a break for as long as they wish. I'd rather have +something done slowly, correctly, and by a contributor who is enjoying their +work than quickly and by a contributor who is burnt out and stressed. No one +should ever be stressed out because of their involvement in the project. Some of +it is unavoidable - especially where politics is involved - but I don't hold +grudges against anyone who steps away and I try to shoulder the brunt of the +bullshit myself. + +The second principle is closely related to the first. If a bug does not affect +someone who works on the project and the problem doesn't interest anyone who +works on the project, it's probably not going to get fixed. I would much rather +help someone familiarize themselves with the codebase and tooling necessary for +them to solve their own problems and send a patch, even if it takes ten times +longer than fixing the bug myself. I have never found a user who, even if they +aren't comfortable with programming or the specific technologies in use, has +been unable to solve a problem which they were willing to invest time into and +ask questions about. + +This principle often leads to conflict with users whose bugs don't get fixed, +but I stick to it. I would rather lose every user who is unwilling to attempt a +patch than invest the resources of my contributors into work they're +uninterested in. In the long term, the health of the project is far better if I +always have developers engaged in and enjoying their work on it than if I lose +users who are upset by my approach. + +These first two principles don't affect my day-to-day open source work so much +as they set the tone for it. The third principle, however, constitutes most of +my job as a maintainer, and it's with it that I add the most value. My main role +is to empower people who contribute to do work they enjoy, which benefits the +project, and which keeps them interested in coming back to do more. + +Finding things people enjoy working on is the main task in this role. Once +people have made a few contributions, I can get an idea of how they like to work +and what they're good at, and help them find things to do which play to their +strengths. Supporting a contributors potential is important as well, and if +someone expresses interest in certain kinds of work or I think they show promise +in an area, it's my responsibility to help them find work to nurture these +skills and connect them with good mentors to help. + +This starts to play in another major responsibility I have as a maintainer, +which is facilitating effective communication throughout the project. As people +grow in the project they generally become effective at managing communication +themselves, but new contributors appear all the time. A major responsibility as +a maintainer is connecting new contributors to domain experts in a problem, or +to users who can reproduce problems or are willing to test their patches. + +I'm also responsible for keeping up with each contributor's growth in the +project. For those who are good at and enjoy having responsibility in the +project, I try to help them find it. As contributors gain a better understanding +of the code, they're trusted to handle large features with less handholding and +perform more complex work[^2]. Often contributors are given opportunities to +become better code reviewers, and usually get merge rights once they're good at +it. Things like commit access are a never a function of rank or status, but of +enabling people to do the things that they're good at. + +[^2]: Though I always encourage people to work on the things they're interested in, I sometimes have to *discourage* people from biting off more than they can chew. Then I help them gradually ramp up their skills and trust among the team until they can take on those tasks. Usually this goes pretty quick, though, and a couple of bugs caused by inexperience is a small price to pay for the *gain* in experience the contributor gets by taking on hard or important tasks. + +It's also useful to remember that your projects are not the only game in town. I +frequently encourage people who contribute to contribute to other projects as +well, and I personally try to find ways to contribute back to their own projects +(though not as much as I'd often like to). I offer support as a sysadmin to many +projects started by contributors to my projects and I send patches whenever I +can. This pays directly back to the project in the form of contributors with +deeper and more diverse experience. It's also fun to take a break from working +on the same stuff all the time! + +There's also some work that someone's just gotta do, and that someone is usually +me. I have to be a sysadmin for the websites, build infrastructure, and so on. +If there are finances, I have to manage them. I provide some kind of vision for +the project and decide what work is in scope. There's also some boring stuff +like preparing changelogs and release notes and shipping new versions, or +liaising with distros on packages. I also end up being responsible for any +marketing. + +--- + +Getting and supporting contributors is the single most important thing you can +do for your project as a maintainer. I often get asked how I'm as productive as +I seem to be. While I can't deny that I can write a lot of code, it's peanuts +compared to the impact made by other contributors. I get a lot of credit for +sway, but in reality I've only written 1-3 sway commits per week in the past few +months. For this reason, the best approach focuses on the contributors, to whom +I owe a great debt of gratitude. + +I'm still learning, too! I speak to contributors about my approach from time to +time and ask for feedback, and I definitely make mistakes. I hope that I'll +receive more feedback soon after some of them read this blog post, too. My +approach will continue to grow over time (hopefully for the better) and I hope +our work will enjoy success as a result. diff --git a/content/blog/How-does-virtual-memory-work.md b/content/blog/How-does-virtual-memory-work.md @@ -0,0 +1,75 @@ +--- +date: 2018-10-29 +layout: page +title: How does virtual memory work? +tags: ["instructional"] +--- + +Virtual memory is an essential part of your computer, and has been for several +decades. In my [earlier article on pointers][pointers], I compared memory to a +giant array of octets (bytes), and explained some of the abstractions we make +on top of that. In actual fact, memory is more complicated than a flat array of +bytes, and in this article I'll explain how. + +[pointers]: /2016/05/28/Understanding-pointers.html + +An astute reader of my earlier article may have considered that pointers on, +say, an x86_64 system, are 64 bits long[^1]. With this, we can address up to +18,446,744,073,709,551,616 bytes (16 exbibytes[^2]) of memory. I only have 16 +GiB of RAM on this computer, so what gives? What's the rest of the address space +for? The answer: all kinds of things! Only a small subset of your address space +is mapped to physical RAM. A system on your computer called the MMU, or Memory +Management Unit, is responsible for managing the abstraction that enables this +and other uses of your address space. This abstraction is called virtual memory. + +[^1]: Fun fact: most x86_64 implementations actually use 48 bit addresses internally, for a maximum theoretical limit of 256 TiB of RAM. +[^2]: I had to look that SI prefix up. This number is 2<sup>64</sup>, by the way. + +The kernel interacts directly with the MMU, and provides syscalls like +[mmap(2)][mmap] for userspace programs to do the same. Virtual memory is +typically allocated a page at a time, and given a purpose on allocation, along +with various flags (documented on the mmap page). When you call `malloc`, libc +uses the mmap syscall to allocate pages of heap, then assigns a subset of that +to the memory you asked for. However, since many programs can run concurrently +on your system and may request pages of RAM at any time, your physical RAM can +get fragmented. Each time the kernel hits a context switch[^3], it swaps out +the page table for the next process. + +[^3]: This means to switch between which process/thread is currently running on a single CPU. I'll write an article about this sometime. +[mmap]: http://man7.org/linux/man-pages/man2/mmap.2.html + +This is used in this way to give each process its own clean address space and to +provide memory isolation between processes, preventing them from accessing each +other's memory. Sometimes, however, in the case of shared memory, the same +physical memory is deliberately shared with multiple processes. Many pages can +also be any combination readable, writable, or executable - the latter meaning +that you could jump to it and execute it as native code. Your compiled program +is a file, after all - mmap some executable pages, load it into memory, jump to +it, and huzzah: you're running your program[^4]. This is how JITs, dynamic +recompiling emulators, etc, do their job. A common way to reduce risk here, +popular on *BSD, is enforcing W^X (writable XOR executable), so that a page can +be either writable or executable, but never both. + +[^4]: There are actually at least a dozen other steps involved in this process. I'll write an article about loaders at some point, too. + +Sometimes all of the memory you think you have isn't actually there, too. If you +blow your RAM budget across your whole system, swap gets involved. This is when +pages of RAM are "swapped" to disk - as soon as your program tries to access it +again, a page fault occurs, transferring control to the kernel. The kernel +restores from swap, damning some other poor process to the fate, and returns +control to your program. + +Another very common use for virtual memory is for memory mapped I/O. This can +be, for example, mapping a file to memory so you can efficiently read and write +to disk. You can map other sorts of hardware, too, such as video memory. On 8086 +(which is what your computer probably pretends to be when it initially +boots[^5]), a simple 96x64 cell text buffer is available at address `0xB8000`. +On my TI-Nspire CX calculator, I can read the current time from the real-time +clock at `0x90090000`. + +[^5]: You can make it stop pretending to do this with [an annoying complicated sequence of esoteric machine code instructions](https://wiki.osdev.org/Protected_Mode). An even more annoying sequence is required to [enter 64-bit mode](https://wiki.osdev.org/Setting_Up_Long_Mode). It gets even better if you want to set up [multiple CPU cores](https://wiki.osdev.org/Symmetric_Multiprocessing)! + +In summary, MMUs arrived almost immediately on the computing scene, and have +become increasingly sophisticated ever since. Virtual memory is a powerful tool +which grants userspace programmers elegant, convenient, and efficient access to +the underlying hardware. diff --git a/content/blog/How-to-abandon-a-FLOSS-project.md b/content/blog/How-to-abandon-a-FLOSS-project.md @@ -0,0 +1,71 @@ +--- +date: 2018-12-04 +layout: post +title: How to abandon a FLOSS project +tags: ["free software", "maintainership"] +--- + +It's no secret that maintaining free and open source software is often +a burdensome and thankless job. I empathise with maintainers who lost interest +in a project, became demotivated by the endless demands of users, or are no +longer blessed with enough free time. Whatever the reason, FLOSS work is +volunteer work, and you're free to stop volunteering at any time. + +In my opinion, there are two good ways to abandon a project: the *fork it* +option and the *hand-off* option. The former is faster and easier, and you can +pick this if you want to wash your hands of the project ASAP, but has a larger +effect on the community. The latter is not always possible, requires more work +on your part, and takes longer, but it has a minimal impact on the community. + +Let's talk about the easy way first. Start by adding a notice to your README +that your software is now unmaintained. If you have the patience, give a few +weeks notice before you really stop paying attention to it. Inform interested +parties that they should consider forking the software and maintaining it +themselves under another name. Once a fork gains traction, update the README +again to direct would-be users to the fork. If no one forks it, you could +consider directing users to similar alternatives to your software. + +This approach allows you to quickly absolve yourself of responsibility. Your +software is no worse than it was yesterday, which allows users a grace period to +collect themselves and start up a fork. If you revisit your work later, you can +also become a contributor to the fork yourself, which removes the stress of +being a maintainer while still providing value to the project. Or, you can just +wash your hands of it entirely and move on to bigger and better things. This +"fork it" approach is safer than giving control of your project to passerby, +because it requires your users to acknowledge the transfer of power, instead of +being surprised by a new maintainer in a trusted package. + +The "fork it" approach is well suited when the maintainer wants out ASAP, or for +smaller projects with little activity. But, for active projects with a patient +maintainer, the hand-off approach is less disruptive. Start talking with some of +your major contributors about [increasing their involvement][relevant article] +in the administrative side of the projects. Mentor them on doing code reviews, +ticket triage, sysadmin stuff, marketing - all the stuff you have to do - and +gradually share these responsibilities with them. These people eventually +become productive co-maintainers, and once established you can step away from +the project with little fanfare. + +[relevant article]: /2018/06/01/How-I-maintain-FOSS-projects.html + +Taking this approach can also help you find healthier ways to be involved in +your own project. This can allow you to focus on the work you enjoy and spend +less time on the work you don't enjoy, which might even restore your enthusiasm +for the project outright! This is also a good idea even if you aren't planning +on stepping down - it encourages your contributors to take personal stake in the +project, which makes them more productive and engaged. This also makes your +community more resilient to [author existence failure][existence failure], so +that when circumstance forces you to step down the project continues to be +healthy. + +[existence failure]: https://tvtropes.org/pmwiki/pmwiki.php/Main/AuthorExistenceFailure + +It's important to always be happy in your work, and especially in your volunteer +work. If it's not working, then change it. For me, this happens in different +ways. I've abandoned projects outright and sent users off to make their own fork +before. I've also handed projects over to their major contributors. In some +projects I've appointed new maintainers and scaled back my role to a mere +contributor, and in other projects I've moved towards roles in marketing, +outreach, management, and stepped away from development. There's no shame in +any of these changes - you still deserve pride in your accomplishments, and +seeking constructive solutions to burnout would do your community a great +service. diff --git a/content/blog/How-to-contribute-to-FOSS.md b/content/blog/How-to-contribute-to-FOSS.md @@ -0,0 +1,51 @@ +--- +date: 2020-08-10 +title: I want to contribute to your project, how do I start? +layout: post +tags: [foss] +--- + +I get this question a lot! The answer is usually... don't. If you already know +what you want to do, then the question doesn't need to be asked.[^1] But, if you +don't already know what you want to do, then your time might be better spent +elsewhere! + +[^1]: Or perhaps the better question is "where should I start with this goal?" + +The best contributors are always intrinsically motivated. Some contributors show +up every now and then who appreciate the value the project gives to them and +want to give something back. Their gratitude is definitely appreciated[^2], but +these kinds of contributions tend to require more effort from the maintainers, +and don't generally lead to recurring contributions. Projects you already like +are less likely to need help when compared to incomplete projects that you don't +already depend on &mdash; so this model leaves newer projects with fewer +contributors and encourages established projects to grow in complexity. + +Instead, you should focus on scratching your own itches. Is there a bug which is +getting on your nerves? A conspicuously absent feature? Work on those! + +[^2]: For real, we don't hear "thanks" very often and expressions of gratitude are often our only reward for our work. We do appreciate it :) + +If there's nothing specific that you want to work on, then you may be better off +finding something to do in a different project. Don't be afraid to work on any +free- and open-source codebase that you encounter &mdash; nearly all of them +will accept your patches. If something is bothering you about another project, +then go fix it! Someone has a cool idea and needs help realizing it? Get +involved! If we spread the contributions around, the FOSS ecosystem will +flourish and the benefits will come back around to our project, too. + +So, if you want to contribute to open-source &mdash; as a whole &mdash; here are +my tips: + +- Find problems which you are intrinsically motivated to work on. +- Focus on developing skills to get up to speed on new codebases fast. +- Don't be afraid to work on *any* project &mdash; new languages, tools, + libraries; learn enough of them and it'll only get easier to learn more. +- When you file bug reports with a FOSS project, get into the habit of following + up with a patch which addresses the problem. +- Get used to introducing yourself to maintainers and talking through the code; + it always pays to ask. + +If you want to work on a specific project, and you have a specific goal in mind: +perfect! If you don't have a specific goal in mind, try to come up with some. +And if you're still drawing a blank, consider another project. diff --git a/content/blog/How-to-store-data-forever.md b/content/blog/How-to-store-data-forever.md @@ -0,0 +1,214 @@ +--- +date: 2020-04-22 +title: How to store data forever +layout: post +--- + +As someone who has been often maligned by the disappearance of my data for +various reasons &mdash; companies going under, hard drive failure, etc &mdash; +and as someone who is responsible for the safekeeping of other people's data, +I've put a lot of thought into solutions for long-term data retention. + +There are two kinds of long-term storage, with different concerns: cold storage +and hot storage. The former is like a hard drive in your safe &mdash; it stores +your data, but you're not actively using it or putting wear on the storage +medium. By contrast, hot storage is storage which is available immediately and +undergoing frequent reads and writes. + +## What storage medium to use? + +There are some bad ways to do it. The worst way I can think of is to store +it on a microSD card. These fail *a lot*. I couldn't find any hard data, but +anecdotally, 4 out of 5 microSD cards I've used have experienced failures +resulting in permanent data loss. Low volume writes, such as from a digital +camera, are unlikely to cause failure. However, microSD cards have a tendency to +get hot with prolonged writes, and they'll quickly leave their safe operating +temperature and start to accumulate damage. Nearly all microSD cards will let +you perform writes fast enough to drive up the temperature beyond the operating +limits &mdash; after all, writes per second is a marketable feature &mdash; so +if you want to safely move lots of data onto or off of a microSD card, you need +to monitor the temperature and throttle your read/write operations. + +A more reliable solution is to store the data on a hard drive[^1]. However, hard +drives are rated for a limited number of read/write cycles, and can be expected +to fail eventually. Backblaze publishes some great articles on [hard drive +failure rates](https://www.backblaze.com/blog/hard-drive-stats-for-2019/) across +their fleet. According to them, the average annual failure rate of hard drives +is almost 2%. Of course, the exact rate will vary with the frequency of use and +storage conditions. Even in cold storage, the shelf life of a magnetic platter +is not indefinite. + +[^1]: Or SSDs, which I will refer to interchangeably with HDDs in this article. They have their own considerations, but we'll get to that. + +There are other solutions, like optical media, tape drives, or more novel +mediums, like the [Rosetta Disk](https://en.wikipedia.org/wiki/Rosetta_Project). +For most readers, a hard drive will be the best balance of practical and +reliable. For serious long-term storage, if expense isn't a concern, I would +also recommend hot storage over cold storage because it introduces the +possibility of active monitoring. + +## Redundancy with RAID + +One solution to this is redundancy &mdash; storing the same data across multiple +hard drives. For cold storage, this is often as simple as copying the data onto +a second hard drive, like an external backup HDD. Other solutions exist for hot +storage. The most common standard is [RAID][RAID], which offers different +features with different numbers of hard drives. With two hard drives (RAID1), for +example, it utilizes mirroring, which writes the same data to both disks. RAID +gets more creative with three or more hard drives, utilizing *parity*, which +allows it to reconstruct the contents of failed hard drives from still-online +drives. The basic idea relies on the XOR operation. Let's say you write the +following byte to drive A: `0b11100111`, and to drive B: `0b10101100`. By XORing +these values together: + +[RAID]: https://en.wikipedia.org/wiki/RAID + + 11100111 A + ^ 10101100 B + = 01001011 C + +We obtain the value to write to drive C. If any of these three drives fail, we +can XOR the remaining two values again to obtain the third. + + 11100111 A + ^ 01001011 C + = 10101100 B + + 10101100 B + ^ 01001011 C + = 11100111 A + +This allows any drive to fail while still being able to recover its contents, +and the recovery can be performed online. However, it's often not that simple. +Drive failure can dramatically reduce the performance of the array while it's +being rebuilt &mdash; the disks are going to be seeking constantly to find the +parity data to rebuild the failed disk, and any attempts to read from the disk +that's being rebuilt will require computing the recovered value on the fly. This +can be improved upon by using lots of drives and multiple levels of redundancy, +but it is still likely to have an impact on the availability of your data if not +carefully planned for. + +You should also be monitoring your drives and preparing for their failure in +advance. Failing disks can show signs of it in advance &mdash; degraded +performance, or via S.M.A.R.T reports. Learn the tools for monitoring your +storage medium, such as smartmontools, and set it up to report failures to you +(and *test* the mechanisms by which the failures are reported to you). + +### Other RAID failure modes + +There are other common ways a RAID can fail that result in permanent data loss. +One example is using hardware RAID &mdash; there was an argument to be made for +them at one point, but these days hardware RAID is *almost always* a mistake. +Most operating systems have software RAID implementations which can achieve the +same results without a dedicated RAID card. With hardware RAID, if the RAID card +itself fails (and they often do), you might have to find the exact same card to +be able to read from your disks again. You'll be paying for new hardware, which +might be expensive or out of production, and waiting for it to arrive before you +can start recovering data. With software RAID, the hard drives are portable +between machines and you can always interpret the data with general purpose +software. + +Another common failure is *cascading* drive failures. RAID can tolerate partial +drive failure thanks to parity and mirroring, but if the failures start to pile +up, you can suffer permanent data loss. Many a sad administrator has been in +panic mode, recovering a RAID from a disk failure, and at their lowest +moment... another disk fails. Then another. They've suddenly lost their data, +and the challenge of recovering what remains has become ten times harder. When +you've been distributing read and write operations consistently across all of +your drives over the lifetime of the hardware, they've been receiving a similar +level of wear, and failing together is not uncommon. + +Often, failures like this can be attributed to using many hard drives from the +same batch. One strategy I recommend to avoid this scenario is to use drives +from a mix of vendors, model numbers, and so on. Using a RAID improves +performance by distributing reads and writes across drives, using the time one +drive is busy to utilize an alternate. Accordingly, any differences in the +performance characteristics of different kinds of drives will be smoothed out in +the wash. + +## ZFS + +RAID is complicated, and getting it right is difficult. You don't want to wait +until your drives are failing to learn about a gap in your understanding of +RAID. For this reason, I recommend ZFS to most. It automatically makes good +decisions for you with respect to mirroring and parity, and gracefully handles +rebuilds, sudden power loss, and other failures. It also has features which are +helpful for other failure modes, like snapshots. + +Set up Zed to email you reports from ZFS. Zed has a debug mode, which will send +you emails even for working disks &mdash; I recommend leaving this on, so that +their conspicuous absence might alert you to a problem with the monitoring +mechanism. Set up a cronjob to do monthly scrubs and review the Zed reports when +they arrive. ZFS snapshots are cheap - set up a cronjob to take one every 5 +minutes, perhaps with [zfs-auto-snapshot][zfs-auto]. + +[zfs-auto]: https://github.com/zfsonlinux/zfs-auto-snapshot + +## Human failures and existential threats + +Even if you've addressed hardware failure, you're not done yet. There are other +ways still in which your storage may fail. Maybe your server fans fail and burn +out all of your hard drives at once. Or, your datacenter could suffer a total +existence failure &mdash; what if a fire burns down the building? + +There's also the problem of human failure. What if you accidentally `rm -rf / *` +the server? Your RAID array will faithfully remove the data from all of the hard +drives for you. What if you send the sysop out to the datacenter to decommission +a machine, and no one notices that they decommissioned the wrong one until it's +too late? + +This is where off-site backups come into play. For this purpose, I recommend +[Borg backup][borg]. It has sophisticated features for compression and +encryption, and allows you to mount any version of your backups as a filesystem +to recover the data from. Set this up on a cronjob as well for as frequently as +you feel the need to make backups, and send them off-site to another location, +which itself should have storage facilities following the rest of the +recommendations from this article. Set up another cronjob to run `borg check` +and send you the results on a schedule, so that their conspicuous absence may +indicate that something fishy is going on. I also use [Prometheus][prom] with +[Pushgateway][pushgateway] to make a note every time that a backup is run, and +set up an alarm which goes off if the backup age exceeds 48 hours. I also have +periodic test alarms, so that the alert manager's own failures are noticed. + +[borg]: https://www.borgbackup.org/ +[prom]: https://prometheus.io/ +[pushgateway]: https://github.com/prometheus/pushgateway + +## Are you prepared for the failure? + +When your disks are failing and everything is on fire and the sky is falling, +this is the worst time to be your first rodeo. You should have *practiced* these +problems before they became problems. Do training with anyone expected to deal +with failures. Yank out a hard drive and tell them to fix it. Have someone in +sales come yell at them partway through because the website is unbearably slow +while the RAID is rebuilding and the company is losing $100 per minute as a +result of the outage. + +Periodically produce a working system from your backups. This proves (1) the +backups are still working, (2) the backups have coverage over everything which +would need to be restored, and (3) you know how to restore them. Bonus: if +you're confident in your backups, you should be able to replace the production +system with the restored one and allow service to continue as normal. + +## Actually storing data *forever* + +Let's say you've managed to keep your data around. But will you still know how +to interpret that data in the future? Is it in a file format which requires +specialized software to use? Will that software still be relevant in the future? +Is that software open-source, so you can update it yourself? Will it still +compile and run correctly on newer operating systems and hardware? Will the +storage medium still be compatible with new computers? + +Who is going to be around to watch the monitoring systems you've put in place? +Who's going to replace the failing hard drives after you're gone? How will they +be paid? Will the dataset still be comprehensible after 500 years of evolution +of written language? The dataset requires constant maintenance to remain intact, +but also to remain useful. + +And ultimately, there is one factor to long-term data retention that you cannot +control: future generations will decide what data is worth keeping &mdash; not +us. + +In summary: no matter what, definitely don't do this: + +![Picture of a SATA card for RAIDing 10 microSD cards together](https://l.sr.ht/ig3R.jpg) diff --git a/content/blog/How-to-write-a-better-bloom-filter-in-C.md b/content/blog/How-to-write-a-better-bloom-filter-in-C.md @@ -0,0 +1,217 @@ +--- +date: 2016-04-12 +# vim tw=80 +title: How to write a better bloom filter in C +layout: post +tags: [C, instructive] +--- + +This is in response to +[How to write a bloom filter in C++](http://blog.michaelschmatz.com/2016/04/11/how-to-write-a-bloom-filter-cpp/), +which has good intentions, but is ultimately a less than ideal bloom filter +implementation. I put together a better one in C in a few minutes, and I'll +explain the advantages of it. + +The important differences are: + +* You bring your own hashing functions +* You can add arbitrary data types, not just bytes +* It uses bits directly instead of relying on the `std::vector<bool>` + being space effecient + +I chose C because (1) I prefer it over C++ and (2) I just think it's a better +choice for implementing low level data types, and C++ is better used in high +level code. + +I'm not going to explain the mechanics of a bloom filter or most of the details +of why the code looks this way, since I think the original post did a fine job +of that. I'll just present my alternate implementation: + +## Header + +```c +#ifndef _BLOOM_H +#define _BLOOM_H +#include <stddef.h> +#include <stdbool.h> + +typedef unsigned int (*hash_function)(const void *data); +typedef struct bloom_filter * bloom_t; + +/* Creates a new bloom filter with no hash functions and size * 8 bits. */ +bloom_t bloom_create(size_t size); +/* Frees a bloom filter. */ +void bloom_free(bloom_t filter); +/* Adds a hashing function to the bloom filter. You should add all of the + * functions you intend to use before you add any items. */ +void bloom_add_hash(bloom_t filter, hash_function func); +/* Adds an item to the bloom filter. */ +void bloom_add(bloom_t filter, const void *item); +/* Tests if an item is in the bloom filter. + * + * Returns false if the item has definitely not been added before. Returns true + * if the item was probably added before. */ +bool bloom_test(bloom_t filter, const void *item); + +#endif +``` + +## Implementation + +The implementation of this is pretty straightfoward. First, here's the actual +structs behind the opaque bloom_t type: + +```c +struct bloom_hash { + hash_function func; + struct bloom_hash *next; +}; + +struct bloom_filter { + struct bloom_hash *func; + void *bits; + size_t size; +}; +``` + +The hash functions are a linked list, but this isn't important. You can make +that anything you want. Otherwise we have a bit of memory called "bits" and the +size of it. Now, for the easy functions: + +```c +bloom_t bloom_create(size_t size) { + bloom_t res = calloc(1, sizeof(struct bloom_filter)); + res->size = size; + res->bits = malloc(size); + return res; +} + +void bloom_free(bloom_t filter) { + if (filter) { + while (filter->func) { + struct bloom_hash *h; + filter->func = h->next; + free(h); + } + free(filter->bits); + free(filter); + } +} +``` + +These should be fairly self explanatory. The first interesting function is here: + +```c +void bloom_add_hash(bloom_t filter, hash_function func) { + struct bloom_hash *h = calloc(1, sizeof(struct bloom_hash)); + h->func = func; + struct bloom_hash *last = filter->func; + while (last && last->next) { + last = last->next; + } + if (last) { + last->next = h; + } else { + filter->func = h; + } +} +``` + +Given a hashing function from the user, this just adds it to our linked list of +hash functions. There's a slightly different code path if we're adding the first +function. The functions so far don't really do anything specific to bloom +filters. The first one that does is this: + +```c +void bloom_add(bloom_t filter, const void *item) { + struct bloom_hash *h = filter->func; + uint8_t *bits = filter->bits; + while (h) { + unsigned int hash = h->func(item); + hash %= filter->size * 8; + bits[hash / 8] |= 1 << hash % 8; + h = h->next; + } +} +``` + +This iterates over each of the hash functions the user has provided and computes +the hash of the data for that function (modulo the size of our bloom filter), +then it adds this to the bloom filter with this line: + +```c +bits[hash / 8] |= 1 << hash % 8; +``` + +This just sets the nth bit of the filter where n is the hash. Finally, we have +the test function: + +```c +bool bloom_test(bloom_t filter, const void *item) { + struct bloom_hash *h = filter->func; + uint8_t *bits = filter->bits; + while (h) { + unsigned int hash = h->func(item); + hash %= filter->size * 8; + if (!(bits[hash / 8] & 1 << hash % 8)) { + return false; + } + h = h->next; + } + return true; +} +``` + +This function is extremely similar, but instead of setting the nth bit, it +checks the nth bit and returns if it's 0: + +```c +if (!(bits[hash / 8] & 1 << hash % 8)) { +``` + +That's it! You have a bloom filter with arbitrary data types for insert and +user-supplied hash functions. I wrote up some simple test code to demonstrate +this, after googling for a couple of random hash functions: + +```c +#include "bloom.h" +#include <stdio.h> + +unsigned int djb2(const void *_str) { + const char *str = _str; + unsigned int hash = 5381; + char c; + while ((c = *str++)) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} + +unsigned int jenkins(const void *_str) { + const char *key = _str; + unsigned int hash, i; + while (*key) { + hash += *key; + hash += (hash << 10); + hash ^= (hash >> 6); + key++; + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; +} + +int main() { + bloom_t bloom = bloom_create(8); + bloom_add_hash(bloom, djb2); + bloom_add_hash(bloom, jenkins); + printf("Should be 0: %d\n", bloom_test(bloom, "hello world")); + bloom_add(bloom, "hello world"); + printf("Should be 1: %d\n", bloom_test(bloom, "hello world")); + printf("Should (probably) be 0: %d\n", bloom_test(bloom, "world hello")); + return 0; +} +``` + +The full code is available [here](https://git.sr.ht/~sircmpwn/bloom/). diff --git a/content/blog/How-to-write-an-IRC-bot.md b/content/blog/How-to-write-an-IRC-bot.md @@ -0,0 +1,88 @@ +--- +date: 2018-03-10 +layout: post +title: How to write an IRC bot +tags: [irc, slack] +--- + +My disdain for Slack and many other Silicon Valley chat clients is [well +known](/2015/11/01/Please-stop-using-slack.html), as is my undying love for IRC. +With Slack making the news lately after their recent decision to disable the IRC +and XMPP gateways in a classic [Embrace Extend +Extinguish](https://en.wikipedia.org/wiki/Embrace%2C_extend%2C_and_extinguish) +move, they've been on my mind and I feel like writing about them more. Let's +compare writing a bot for Slack with writing an IRC bot. + +First of all, let's summarize the process for making a Slack bot. Full details +are available in [their documentation](https://api.slack.com/slack-apps). The +basic steps are: + +1. Create a Slack account and "workspace" to host the bot (you may have already + done this step). On the free plan you can have up to 10 "integrations" (aka + bots). This includes all of the plug-n-play bots Slack can set up for you, so + make sure you factor that into your count. Otherwise you'll be heading to the + pricing page and making a case to whoever runs your budget. +2. Create a "Slack app" through their web portal. The app will be tied to the + company you work with now, and if you get fired you will lose the app. Make + sure you make a separate organization if this is a concern! +3. The recommended approach from here is to set up subscriptions to the "Event + API", which involves standing up a web server (with working SSL) on a + consistent IP address (and don't forget to open up the firewall) to receive + incoming notifications from Slack. You'll need to handle a proprietary + challenge to verify your messages via some HTTP requests coming from Slack + which gives you info to put into HTTP headers of your outgoing requests. The + Slack docs refer to this completion of this process as "triumphant success". +4. Receive some JSON in a proprietary format via your HTTP server and use some + more proprietary HTTP APIs to respond to it. + +Alternatively, instead of steps 3 and 4 you can use the "Real Time Messaging" +API, which is a websocket-based protocol that starts with an HTTP request to +Slack's authentication endpoint, then a follow-up HTTP request to open the +WebSocket connection. Then you set up events in a similar fashion. Refer to the +complicated table in the documentation breaking down which events work through +which API. + +Alright, so that's the Slack way. How does the IRC way compare? IRC is an open +standard, so to learn about it I can just read RFC 1459, which on my system is +conveniently waiting to be read at `/usr/share/doc/rfc/txt/rfc1459.txt`. This +means I can just read it locally, offline, in the text editor of my choice, +rather than on some annoying website that calls authentication a "triumphant +success" and complains about JavaScript being disabled. + +You don't have to read it right now, though. I can give you a summary here, like +I gave for Slack. Let's start by not writing a bot at all - let's just manually +throw some bits in the general direction of Freenode. Install netcat and run +`nc irc.freenode.net 6667`, then type this into your terminal: + +``` +NICK joebloe +USER joebloe 0.0.0.0 joe :Joe Bloe +``` + +Hey, presto, you're connected to IRC! Type this in to join a channel: + +``` +JOIN #cmpwn +``` + +Then type this to say hello: + +``` +PRIVMSG #cmpwn :Hi SirCmpwn, I'm here from your blog! +``` + +IRC is one of the simplest protocols out there, and it's dead easy to write a +bot for it. If your programming language can open a TCP socket (it can), then +you can use it to write an IRC bot in 2 minutes, flat. That's not even to +mention that there are IRC client libraries available for every programming +language on every platform ever - I even [wrote one +myself!](https://github.com/SirCmpwn/ChatSharp) In fact, that guy is probably +the fifth or sixth IRC library I've written. They're so easy to write that I've +lost count. + +Slack is a walled garden. Their proprietary API is defined by them and only +implemented by them. They can and will shut off parts you depend on (like the +IRC+XMPP gateways that were just shut down). IRC is over 20 years old and +software written for it then still works now. It's implemented by hundreds of +clients, servers, and bots. Your CI supports it and GitHub can send commit +notifications to it. It's ubiquitous and free. Use it! diff --git a/content/blog/Im-doing-FOSS-full-time.md b/content/blog/Im-doing-FOSS-full-time.md @@ -0,0 +1,69 @@ +--- +date: 2019-01-15 +layout: post +title: I'm going to work full-time on free software +tags: ["money", "free software"] +--- + +Sorry for posting two articles so close to each other - but this is important! +As I'm certain many of you know, I maintain a large collection of free software +projects, including [sway][sway], [wlroots][wlroots], [sr.ht][srht], +[scdoc][scdoc], [aerc][aerc], and [many][srht-profile], [many][github-profile] +more. I contribute to more still, working on projects like [Alpine +Linux][alpine], [mrsh][mrsh], [musl libc][musl], and anything else I can. Until +now, I've been working on these in my spare time, but just under a year ago I +wrote "[The path to sustainably working on FOSS full-time][old-post]" laying out +my future plans. Today I'm proud to tell you that, thanks to everyone's +support, I'll be working on free software full-time starting in February. + +[sway]: https://github.com/swaywm/sway +[wlroots]: https://github.com/swaywm/wlroots +[srht]: https://sr.ht +[scdoc]: https://git.sr.ht/~sircmpwn/scdoc +[aerc]: https://git.sr.ht/~sircmpwn/aerc2 +[srht-profile]: https://git.sr.ht/~sircmpwn +[github-profile]: https://github.com/ddevault +[alpine]: https://alpinelinux.org +[mrsh]: https://git.sr.ht/~emersion/mrsh +[musl]: https://musl-libc.org +[old-post]: https://drewdevault.com/2018/02/24/The-road-to-sustainable-FOSS.html + +I'm so excited! I owe many people a great deal of thanks. To everyone who has +donated to my fosspay, Patreon, and LiberaPay accounts: thank you. To all of the +sr.ht users who chose to pay for their account despite it being an alpha: thank +you. I also owe a thanks to all of the amazing contributors who give their spare +time towards making the projects I maintain even better, without whom my +software wouldn't be anywhere near as useful to anyone. + +I don't want to make grandiose promises right away, but I'm confident that +increasing my commitment to open source to this degree is going to have a major +impact on my projects. For now, my primary focus is sr.ht: its paid users make +up the majority of the funding. Relatedly, I plan to invest more time in Alpine +Linux on RISC-V and making RISC-V builds available to the sr.ht community. Sway +and wlroots are in good shape as we quickly approach sway 1.0, and for this +reason I want to give a higher priority to my smaller, more neglected projects +like aerc for the time being. As I learn more about my bandwidth under these new +conditions, I'll expand my plans accordingly. + +I need to clarify that despite choosing to work full-time on these projects, my +income is going to be negative for a while. I have enough savings and income now +that I feel comfortable making the leap, and I plan on working my ass off before +my runway ends to earn the additional subscriptions to sr.ht and donations to +fosspay et al that will make this decision sustainable in the long term. If that +doesn't happen before I get near the end of my runway, I'll have to start +looking for different work again. I'm depending on your continued support. If +you appreciate my work but haven't yet, please consider [buying a subscription +to sr.ht](https://meta.sr.ht/billing/initial) or [donating to my general +projects fund](/donate). Thank you! + +All said, words cannot describe how excited I am. It's been my dream for years +to work on these projects full-time. Hitting this critical threshold of funding +allows me to tremendously accelerate the progress of these projects. If you were +impressed by what I built in my spare time, just wait until you see what we can +accomplish now! + +![](https://sr.ht/YsHI.png) + +From the bottom of my heart, thank you for your support! + +P.S: I'll see you at [FOSDEM](https://fosdem.org/2019/)! Ask me for a sticker. diff --git a/content/blog/In-Memoriam-Mozilla.md b/content/blog/In-Memoriam-Mozilla.md @@ -0,0 +1,64 @@ +--- +date: 2016-05-11 +# vim: tw=80 +layout: post +title: In Memoriam - Mozilla +tags: [firefox] +--- + +Today we look back to the life of Mozilla, a company that was best known for +creating the Firefox web browser. I remember a company that made the web better +and more open by providing a browser that was faster and more customizable than +anyone had ever seen, and by making that browser free and open source. + +I expect many of my readers will be older than I am, but my first memories of +Firefox are back in high school with Firefox 3. I fondly remember my discovery +of it. Mozilla gave us a faster and more powerful web browser to use on school +computers. The other choice was Internet Explorer 6 - but with a flash drive we +could run a "portable" version of Firefox instead. Using tabbed web browsing was +a clear improvement for usability and I loved installing all sorts of cool +add-ons and I'm sure I've spent at least a few hours of my life browsing persona +themes. + +Mozilla continued to improve their web browser, and I loved it. As I grew up and +learned more about techology and started making my way into programming I loved +it even more. I remember a time when I would tell my friends that I'd gladly +appoint Mozilla as the steward of the open internet over the W3C. Firefox +continued to evolve and allow for even more customiziability. Firefox truly +became a [hacker](http://www.catb.org/~esr/jargon/html/H/hacker.html)'s web +browser. + +Eventually a new player called Chrome arrived on the scene. It was slick and new +and very, very fast. Firefox, on the other hand, appeared to become stagnant. +I made the switch to Chrome for a few years. However, to my eventual delight, +Mozilla didn't quit. They kept making Firefox better and faster and continued to +win on customizability and continued to fight for the best internet possible. +One day I tried Firefox again and I found it to be just as friendly and hackable +as it once was, only now it was a speed demon on par with Chrome. I returned to +Firefox for several happy years. + +Chrome adopted a versioning scheme that made Mozilla nervous. They didn't like +being Firefox 4 next to Chrome 11. They made the first of many compromises when +they switched to bumping the major version with each release. Mozilla died in +April of 2011. + +In Mozilla's place, a new company appeared and started to build a new browser. +This new company had good intentions, but has completely lost the spirit of +Mozilla. This new browser is a stain on Mozilla's legacy - it ships with +unremovable nonfree add-ons, removes huge swaths of the original add-on API, +includes a cryptographically walled garden for add-ons, and apparently now +includes an instant messaging and video conferencing platform. + +The new company has been suffering as well. They have sunk enormous time and +effort into projects that are doomed from the start. They tried to make a mobile +phone OS whose UI was powered by technology that's been proven to produce an +inferior mobile experience (HTML+CSS+JS) using the slowest rendering engine on +the market (gecko) on the lowest powered phones on the market. When this +predictably failed, they turned their sights towards running it on even lower +powered IoT devices. This new company has also announced several times that they +are killing off another well established and well loved project (Thunderbird) +from the old Mozilla. They also recently struck a deal with another dying +company, Yahoo, to make their search engine the default for this "neo-Firefox". + +To the new company that calls itself Mozilla: you do an injustice to the memory +of Mozilla. I hope that one day we'll see the Mozilla of the past return. diff --git a/content/blog/Input-handling-in-wlroots.md b/content/blog/Input-handling-in-wlroots.md @@ -0,0 +1,249 @@ +--- +date: 2018-07-17 +layout: post +title: Input handling in wlroots +tags: [wlroots, wayland, instructional] +--- + +I've said before that wlroots is a "batteries not included" kind of library, and +one of the places where that is most apparent is with our approach to input +handling. We implemented a very hands-off design for input, in order to support +many use-cases: desktop input, phones with and without USB-OTG HIDs plugged in, +multiple mice bound to a single cursor, multiple keyboards per seat, simulated +input from fake input devices, on-screen keyboards, input which is processed by +the compositor but not sent to clients... we support all of these use-cases and +even more. However, the drawback of our powerful design is that it's confusing. +Very confusing. + +Let's begin by forgetting about the Wayland part entirely. After all, wlroots is +flexible enough that you can use it without writing a Wayland compositor at all! +It can be used in a similar fashion to tools like GLFW and SDL, to abstract +low-level input (via e.g. libinput) and graphical output (via e.g. DRM). Let's +start here, simply getting input events from wlroots in the first place. + +One of the fundamental building blocks of wlroots is the `wlr_backend`, +which is a resource that abstracts the underlying hardware and exposes a +consistent API for outputs and input devices. Outputs have been discussed +elsewhere, so let's focus just on input devices. Each backend provides an +event: [`wlr_backend.events.new_input`][new_input]. The signal is called with a +reference to a [`wlr_input_device`][wlr_input_device] each time a new input +device appears on the backend - for example, when you plug a mouse into your +computer when using the libinput backend. + +[new_input]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/backend.h#L17 +[wlr_input_device]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_input_device.h + +The input device can be one of five types, appropriately identified by the +`type` field. The types are: + +- `WLR_INPUT_DEVICE_KEYBOARD` +- `WLR_INPUT_DEVICE_POINTER` +- `WLR_INPUT_DEVICE_TOUCH` +- `WLR_INPUT_DEVICE_TABLET_TOOL` +- `WLR_INPUT_DEVICE_TABLET_PAD` + +The type indicates which member of the anonymous union is valid. If +`wlr_input_device->type == WLR_INPUT_DEVICE_KEYBOARD`, then +`wlr_input_device->keyboard` is a valid pointer to a +[`wlr_keyboard`][wlr_keyboard]. + +[wlr_keyboard]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_keyboard.h + +Let's examine the wlr keyboard more closely now. The keyboard struct also +provides its own events, like `key` and `keymap`. If you want to process input +from this keyboard, you need to set up an [xkbcommon][xkbcommon] context for +ingesting the raw scancodes emitted by the `key` event and converting them to +Unicode and keysyms (e.g. "Up") with an XKB keymap. Most of the wlroots examples +[implement this][examples] if you're looking for a simple reference. + +[xkbcommon]: https://xkbcommon.org/doc/current/ +[examples]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/examples/simple.c#L114 + +When these events are sent, we just let you process them as you please. They do +not automatically get propagated to any Wayland clients. Communicating these +events to the clients is your responsibility, though we provide you tools to +help - we'll get into that shortly. You don't even have to source the input you +give to Wayland clients from a `wlr_input_device`, you can just as easily make +them up or get them from the network or anywhere else. + +Before we get into details on how to send events to clients, let's examine the +other components in your compositor's input code. First, let's talk about the +cursor. + +We provide the [`wlr_pointer`][wlr_pointer] abstraction for getting events from +a "pointer" device, like a mouse. However, because batteries are not included, +you will find that we only tell you what the pointer device is doing - we don't +act on it. If you want to, for example, display a cursor image <img +src="https://sr.ht/hf39.png" style="display: inline; margin: 0; padding: 0;" /> +on screen which moves around when the mouse does, you need to wire this up +yourself. We have tools which can help. + +[cur]: https://sr.ht/hf39.png +[wlr_pointer]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_pointer.h + +First, let's talk about getting the cursor image to show. You can source the +image from anywhere you want, but you will probably want to leverage +[`wlr_xcursor`][xcursor]. This is a small wlroots module (forked from the +`wayland-cursor` library used by Wayland clients) which can read Xcursor themes, +the kind your user will already have installed on their system. Loading up a +cursor theme and getting the pixels from it is pretty straightforward. But what +should you do with those pixels? + +[xcursor]: https://github.com/swaywm/wlroots/blob/master/include/wlr/xcursor.h + +Well, now we have to introduce hardware cursors. Many backends support +"hardware" cursors, which is a feature provided by your low-level graphics stack +(e.g. GPU drivers) for rendering a cursor on the screen. Hardware cursors are +composited by the GPU, which means you can move the cursor around without +re-drawing the things underneath it. This is the most energy- and CPU-efficient +way of drawing your cursor, and you can do it with +[`wlr_output_cursor_set_image`][cursor_set_image], specifying which `wlr_output` +you want it to appear on and at what coordinates. Not all configurations support +hardware cursors, but `wlr_output` automatically falls back to software cursors +if need be. + +[cursor_set_image]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_output.h#L179-L190 + +Now you have all of the pieces to show a cursor on screen that moves with the +mouse. You can store some X and Y coordinates somewhere, grab an image from an +Xcursor theme, and throw it at your `wlr_output`, then process input events and +move it around. Then... you need to consider multiple outputs. And you need to +make sure that it can't be moved outside of an output. And you need to let the +user move it around with a drawing tablet or touch screen as well. And... well, +it's about to get complicated. That's where our next tool comes in! + +[`wlr_cursor`][wlr_cursor] is how wlroots saves you from some of this work. It +can display a cursor image on-screen, tie it to multiple input devices, +constrain it to your outputs and move it across multiple displays. It can also +map input from certain devices to certain outputs or regions of the output +layout, change the geometry of inputs from a drawing tablet, and more. + +[wlr_cursor]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_cursor.h + +To use `wlr_cursor`, you should create one (`wlr_cursor_create`) and as the +backend emits `new_input` events, bind them to the cursor with +`wlr_cursor_attach_input_device`. `wlr_cursor` then raises aggregated events +from all of its devices, which you can catch and handle accordingly - usually +calling a function like `wlr_cursor_move` and propagating the event to Wayland +clients. You also need to attach a [`wlr_output_layout`][wlr_output_layout] to +the cursor, so it knows how to constrain the cursor movement and can handle +hardware cursors for you. + +Aside: the `wlr_output_layout` module allows you to configure an arrangement of +`wlr_output`s in physical space. Its function is fairly straightforward and +largely unrelated to our topic - I suggest reading through the header and asking +questions if you need help. Once you make one of these and hand it to a +`wlr_cursor`, you have a cursor on-screen which moves around when you provide +input and correctly moves throughout a multi-display setup.[^1] [^2] + +[wlr_output_layout]: https://github.com/swaywm/wlroots/blob/master/include/wlr/types/wlr_output_layout.h + +[^1]: One more quick note: for multi-DPI setups, you need to provide the `wlr_cursor` with different cursor images, one for each scale present on the output layout. We have another tool for sourcing Xcursor images at multiple scale factors, check out [`wlr_xcursor_manager`](https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_xcursor_manager.h). +[^2]: Another thing `wlr_output_layout` is useful for, if you were wondering, is figuring out where to render windows in a multi-output arrangement, where some windows might span multiple outputs. Read the header! + +Okay, now that we have all of those pieces in place, we can finally start talking +about sending input events to Wayland clients! Before we get into how *wlroots* +does it, let's talk about how *Wayland* does it in general. + +The top-level resource which manages input for a Wayland client is the +`wl_seat`. One seat, in rough terms, maps to a single set of input devices used +by a user (a user who is presumably sitting at a seat in front of their +computer). A seat can have up to one keyboard, pointer, touch device, or drawing +tablet each. Each of these devices can then *enter* or *leave* any of the +client's surfaces at the compositor's orders. + +When you bind to a `wl_seat`'s `wl_keyboard` and `wl_keyboard.enter` is raised +on a surface, it means your surface has keyboard focus. The compositor will +follow-up with (or will have already sent) a `wl_keyboard.keymap` signal to let +you know the layout of this keyboard (e.g. `us-intl`, `de`, `ru`, etc) in the +form of an xkbcommon keymap (the same format we were using with `wlr_keyboard` +earlier - hint hint). Some number of `key` and `modifier` events will likely +follow as the user taps away. + +When you bind to a `wl_seat`'s `wl_pointer` and `wl_pointer.enter` is raised, it +means a pointer has moved over one of your surfaces. Note that this can be an +entirely separate occasion from receiving keyboard focus. The client is then +expected to provide a cursor image to display (at the moment, Wayland *requires* +client side cursors. They have to do the whole Xcursor dance we did on the +wlroots side earlier, too. We have some plans to correct this...). Some number +of `motion` and `button` events will likely follow as the user wiggles their +mouse and clicks your windows. + +So, how does a wlroots-based compositor facilitate these interactions? With +[`wlr_seat`][wlr_seat], our abstraction on top of `wl_seat`. This implements the +whole `wl_seat` state machine, but again leaves it to you to tweak the knobs as +you wish. You need to decide how your compositor is going to deal with focus - +KDE, Sway, the Librem5 phone UI, an in-vehicle infotainment system; all of these +will have a different approach to focus. + +[wlr_seat]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_seat.h +[pointer_enter]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_seat.h#L405-L412 +[keyboard_enter]: https://github.com/swaywm/wlroots/blob/4984ea49eeaa292d66be9e535d93a4d8185f3e18/include/wlr/types/wlr_seat.h#L405-L412 + +wlroots doesn't render client surfaces for you, and doesn't know where you put +them. Once you figure out where they go, you need to notice when the +`wlr_cursor` is moved over it and call `wlr_seat_pointer_notify_enter` with the +pointer's coordinates relative to the surface it entered, along with any +appropriate `motion` or `button` events through the relevant `wlr_seat` +functions. The client will also likely send you a cursor image to display - this +is done with the `wlr_seat.events.request_set_cursor` event. + +When you decide that a surface should receive keyboard focus, call +`wlr_seat_keyboard_notify_enter`. `wlr_seat` will automatically handle removing +focus from whatever had it last, and will also grab the keymap and send it to +the client for you, assuming you configured it with `wlr_keyboard_set_keymap`... +you did, right? `wlr_seat` also semi-transparently deals with grabs, the sort of +situation where a client wants to keep keyboard focus for longer than it +normally would, to deal with a context menu or something. + +Touch events are similar and should be self-explanatory when you read the +header. Drawing tablet events are a bit different - they're not actually +specified by the core Wayland protocol. Instead, we rig these up with the +[tablet][tablet-protocol] protocol extension and [wlr_tablet][wlr_tablet]. It +works in much the same way, but you have to explicitly configure it for a +`wlr_seat` by calling `wlr_tablet_create` yourself. + +[tablet-protocol]: https://cgit.freedesktop.org/wayland/wayland-protocols/tree/unstable/tablet/tablet-unstable-v2.xml +[wlr_tablet]: https://github.com/swaywm/wlroots/blob/7f20ab644347b11fd8242beaf7a6fe42c910d014/include/wlr/types/wlr_tablet_v2.h + +So, in short, if you wiggle your mouse, here's what happens: + +1. Before you wiggled your mouse, the `libinput` backend noticed it was plugged + in and raised a `new_input` event. +1. Your compositor attached the resulting `wlr_pointer` to its `wlr_cursor`, + which it had prepared earlier by looking up an appropriate cursor theme and + letting it know about the display layout. +1. The `wlr_pointer` bubbled up a `motion` event, which was caught by + `wlr_cursor` and bubbled up to your compositor. +1. Your compositor called `wlr_cursor_move` to apply the resulting motion, + constrained by the output layout, which in turn caused the cursor image on + your display to move. +1. Your compositor then looked around to see if the pointer had moved over any + new surfaces. Since wlroots doesn't handle rendering or know where anything + is displayed, this was a rather introspective question. +1. You *did* wiggle it over a new surface, so the compositor called + `wlr_seat_notify_pointer_enter` after translating the pointer coordinates to + surface-local space. It sent a `wlr_seat_notify_pointer_motion` for good + measure. +1. The client noticed the pointer entered it and sent back a cursor image to + show. The compositor was informed of this via + `wlr_seat.events.request_set_cursor`. +1. The compositor handled the client's cursor image to `wlr_cursor`, throwing + away all of that hard work loading up a cursor theme just for a client-side + cursor to come in and ruin it. + +And there you have it, that's how input works in wlroots. It's really fucking +complicated, isn't it? I think this article puts on display both the incredible +advantages and serious drawbacks of wlroots. Because you have to plug all of +these pieces together yourself, you are afforded an *enormous* amount of +flexibility. However, you have to do a lot of work and understand a whole lot of +different pieces to get there. Libraries like +[wlc](https://github.com/Cloudef/wlc) are much easier to use in this respect, +but if you want to change even a small detail of this process with wlc you are +unable to. + +If you have any questions about this article, please reach out to the developers +hanging out in [#sway-devel on irc.freenode.net][#sway-devel]. We know this is +confusing, and we're happy to help. + +[#sway-devel]: http://webchat.freenode.net/?channels=sway-devel&uio=d4 diff --git a/content/blog/Integrating-a-VT220-into-my-life.md b/content/blog/Integrating-a-VT220-into-my-life.md @@ -0,0 +1,206 @@ +--- +date: 2016-03-22 +# vim: tw=80 +title: Integrating a VT220 into my life +layout: post +tags: [hardware] +--- + +I bought a DEC VT220 terminal a while ago, and put it next to my desk at work. I +use it to read emails on mutt now, and it's actually quite pleasant. There was +some setup involved in making it as comfortable as possible, though. + +[![My desk at work](https://sr.ht/BnAH.jpg)](https://sr.ht/BnAH.jpg) + +Here's the terminal up close: + +[![The terminal itself](https://sr.ht/TnC6.jpg)](https://sr.ht/TnC6.jpg) + +## Hardware + +First, I have several pieces of hardware involved in this: + +* VT220 terminal +* LK201 keyboard (later made obsolete) +* [USB to serial adapter](http://amzn.com/B00IDSM6BW) +* [DB9->DB29 null modem cable](http://amzn.com/B00066HL50) + +It took a while to get all of these things, but I was able to get a nice +refurbished terminal and a couple of crappy LK201 keyboards. Luckily I was able +to eventually remove the need for the keyboard. + +## Basic Setup + +Getting this working on Linux is actually pretty simple thanks to decades of +backwards compatability. Plug all of the cords together, turn on the machine, +and (on Arch, at least) run: + + systemctl start serial-agetty@ttyUSB0.service + +This will start up a getty for you to log into on your terminal. For a while I +would use the LK201 to log in to this getty and spin up a mail cilent. + +I did have to make a couple of changes to serial-agetty@.service, though: + + ExecStart=-/sbin/agetty -h -L 19200 %I vt220 + +This specifies the TERM variable as "vt220" and sets the baud rate to 19200. I +had to also set the baud rate in the terminal's settings to 19200 baud as well, +to get the fastest possible terminal. + +I eventually got into the habit of logging into the terminal with the LK201, +then running tmux and attaching to tmux from my desktop session. I would then +hide this tmux terminal in the upper left corner of my display, and move my +mouse over to it when I wanted to interact with the terminal. This let me use +the same keyboard I used for the rest of my computer experience to interact with +the VT220, instead of trying to use the LK201 as well. This was a bit annoying, +so eventually I did some more customization. + +## Removing the keyboard + +I wanted to be able to make everything automatic, so I could just boot my +computer and log in normally and treat the VT220 almost like a fourth monitor. I +started by automating the process of logging in and running tmux. + +First, I created a user for the terminal: + + useradd vt220 + +Then, I wrote a shell script that would serve as the user's login shell and +would start tmux: + +```bash +#!/usr/bin/env bash +if [[ $TERM == "screen" ]] +then + sudo /usr/local/bin/login-sircmpwn +else + tmux -S /var/tmux/vt220.sock +fi +``` + +I made that directory, `/var/tmux/`, and made sure both the vt220 user and my +normal user had access to it. I also edited my sudoers file so that vt220 could +run that command as root: + + vt220 ALL=(ALL) NOASSWD: /usr/local/bin/login-sircmpwn + +I put the script into `/usr/local/bin` and added it to `/etc/shells`, then made +it the login shell for the vt220 user with `chsh`. I then moved to my own +systemd unit for starting the getty on ttyUSB0, this time with autologin: + + # This file is part of systemd. + # + # systemd is free software; you can redistribute it and/or modify it + # under the terms of the GNU Lesser General Public License as published by + # the Free Software Foundation; either version 2.1 of the License, or + # (at your option) any later version. + + [Unit] + Description=Serial Getty on %I + Documentation=man:agetty(8) man:systemd-getty-generator(8) + Documentation=http://0pointer.de/blog/projects/serial-console.html + BindsTo=dev-%i.device + After=dev-%i.device systemd-user-sessions.service plymouth-quit-wait.service + + # If additional gettys are spawned during boot then we should make + # sure that this is synchronized before getty.target, even though + # getty.target didn't actually pull it in. + Before=getty.target + IgnoreOnIsolate=yes + + [Service] + ExecStart=-/sbin/agetty -a vt220 -h -L 19200 %I vt220 + Type=idle + Restart=always + UtmpIdentifier=%I + TTYPath=/dev/%I + TTYReset=yes + TTYVHangup=yes + KillMode=process + IgnoreSIGPIPE=no + SendSIGHUP=yes + + [Install] + WantedBy=getty.target + +The only difference here is that it invokes agetty with `-a vt220` to autologin +as that user. `systemctl enable vtgetty@ttyUSB0.service` makes it so that on +boot, the getty would run on ttyUSB0 and autologin as vt220. Then the script +from earlier will run tmux, and within tmux will run `sudo +/usr/local/bin/login-sircmpwn`, which is this shell script: + +```bash +#!/usr/bin/env bash +until who | grep sircmpwn 2>&1 >/dev/null +do + sleep 1 +done +sudo -iu sircmpwn +``` + +What this does is pretty straightforward - it loops until I log in as sircmpwn, +then enters an interactive session with sudo as sircmpwn. + +The net of all of this is that now, I can boot up my machine, and when I log in, +the VT220 starts up with tmux running and logged in as me. Then I went back to +the old way of attaching to this tmux session with a terminal on my desktop +session hidden in a corner of the screen. And now I could ditch the clunky old +LK201 keyboard! + +## Treating the terminal as another output + +I said earlier that my goal was to treat the terminal as a fake "output" that I +could switch to from my desktop session just like I switch between my three +graphical outputs. I run [sway](https://github.com/SirCmpwn/sway), of course, so +I decided to add a fake output in sway and see where that went. I made a +somewhat complicated [branch](https://github.com/SirCmpwn/sway/compare/vt220) +for this purpose, but the important change is here: + +```diff +diff --git a/sway/handlers.c b/sway/handlers.c +index cec6319..60f8406 100644 +--- a/sway/handlers.c ++++ b/sway/handlers.c +@@ -704,6 +704,21 @@ static void handle_wlc_ready(void) { + free(line); + list_del(config->cmd_queue, 0); + } ++ // VT220 stuff ++ // Adds a made up output that we can use for a tmux window ++ // connected to my vt220 ++ swayc_t *output = new_swayc(C_OUTPUT); ++ output->name = "VT220"; ++ output->handle = UINTPTR_MAX; ++ output->width = 1000; ++ output->height = 1000; ++ output->unmanaged = create_list(); ++ output->bg_pid = -1; ++ add_child(&root_container, output); ++ output->x = -1000; ++ output->y = 0; ++ new_workspace(output, "__VT220"); ++ // End VT220 stuff + } +``` + +This creates a fake output and puts it to the far left, then adds a workspace to +it called __VT220. I assigned it the output handle of UINTPTR_MAX and everywhere +in sway that it would try to use the output handle to manipulate a real output, +I changed to to avoid doing so if the handle is UINTPTR_MAX. Then I added this +to my sway config: + + for_window [title="__VT220"] move window to workspace __VT220 + +And run this command when sway starts: + + urxvt -T "__VT220" -e tmux -S /var/tmux/vt220.sock a + +Which spawns a terminal whose window title is __VT220 running tmux attached to +the session running on the terminal. The for_window rule I added to my sway +config automatically moves this to the VT220 fake output and tada! It works. Now +I have a nice and comfortable way to use my terminal to read emails at work. Now +if only I could convince people to stop sending me HTML emails! I just bought a +second VT220 for use at home, too. Life's good~ + +[Discussion on Hacker News](https://news.ycombinator.com/item?id=11339909) diff --git a/content/blog/Interactive-SSH-programs.md b/content/blog/Interactive-SSH-programs.md @@ -0,0 +1,223 @@ +--- +date: 2019-09-02 +layout: post +title: Building interactive SSH applications +tags: [ssh, informational, sourcehut] +--- + +After the announcement of [shell access for builds.sr.ht jobs][builds +announcement], a few people sent me some questions, wondering how this sort of +thing is done. Writing interactive SSH applications is actually pretty easy, but +it does require some knowledge of the pieces involved and a little bit of +general Unix literacy. + +[builds announcement]: https://drewdevault.com/2019/08/19/Introducing-shell-access-for-builds.html + +On the server, there are three steps which you can meddle with using OpenSSH: +authentication, the shell session, and the command. The shell is pretty easily +manipulated. For example, if you set the user's login shell to +`/usr/bin/nethack`, then [nethack][nethack] will run when they log in. Editing +this is pretty straightforward, just pop open `/etc/passwd` as root and set +their shell to your desired binary. If the user SSHes into your server with a +TTY allocated (which is done by default), then you'll be able to run a curses +application or something interactive. + +[nethack]: https://www.nethack.org/ + +<script + id="asciicast-CQ5iaFl8kMnOGV3x0TeI7vfjV" + src="https://asciinema.org/a/pafXXANiWHY9MOH2yXdVHHJRd.js" async +></script> +<noscript><i>This article includes third-party JavaScript content from +asciinema.org, a free- and open-source platform that I trust.</i></noscript> + +However, a downside to this is that, if you choose a "shell" which does not +behave like a shell, it will break when the user passes additional command line +arguments, such as `ssh user@host ls -a`. To address this, instead of overriding +the shell, we can override the *command* which is run. The best place to do this +is in the user's `authorized_keys` file. Before each line, you can add options +which apply to users who log in with that key. One of these options is the +"command" option. If you add this to `/home/user/.ssh/authorized_keys` instead: + +``` +command="/usr/bin/nethack" ssh-rsa ... user +``` + +Then it'll use the user's shell (which should probably be `/bin/sh`) to run +`nethack`, which will work regardless of the command supplied by the user (which +is stored into `SSH_ORIGINAL_COMMAND` in the environment, should you need it). +There are probably some other options you want to set here, as well, for +security reasons: + +``` +restrict,pty,command="..." ssh-rsa ... user +``` + +The full list of options you can set here is available in the `sshd(8)` man +page. `restrict` just turns off most stuff by default, and `pty` explicitly +re-enables TTY allocation, so that we can do things like curses. This will work +if you want to explicitly authorize specific people, one at a time, in your +`authorized_keys` file, to use your SSH-driven application. However, there's +one more place where we can meddle: the `AuthorizedKeysCommand` in +`/etc/ssh/sshd_config`. Instead of having OpenSSH read from the +`authorized_keys` file in the user's home directory, it can execute an arbitrary +program and read the `authorized_keys` file from its stdout. For example, on +Sourcehut we use something like this: + +``` +AuthorizedKeysCommand /usr/bin/gitsrht-dispatch "%u" "%h" "%t" "%k" +AuthorizedKeysUser root +``` + +Respectively, these format strings will supply the command with the username +attempting login, the user's home directory, the type of key in use (e.g. +`ssh-rsa`), and the base64-encoded key itself. More options are available - see +`TOKENS`, in the `sshd_config(8)` man page. The key supplied here can be used to +identify the user - on Sourcehut we look up their SSH key in the database. Then +you can choose whether or not to admit the user based on any logic of your +choosing, and print an appropriate `authorized_keys` to stdout. You can also +take this opportunity to forward this information along to the command that gets +executed, by appending them to the command option or by using the environment +options. + +## How this works on builds.sr.ht + +We use a somewhat complex system for incoming SSH connections, which I won't go +into here - it's only necessary to support multiple SSH applications on the same +server, like git.sr.ht and builds.sr.ht. For builds.sr.ht, we accept all +connections and authenticate later on. This means our AuthorizedKeysCommand is +quite simple: + +```python +#!/usr/bin/env python3 +# We just let everyone in at this stage, authentication is done later on. +import sys +key_type = sys.argv[3] +b64key = sys.argv[4] + +keys = (f"command=\"buildsrht-shell '{b64key}'\",restrict,pty " + + f"{key_type} {b64key} somebody\n") +print(keys) +sys.exit(0) +``` + +The command, `buildsrht-shell`, does some more interesting stuff. First, the +user is told to connect with a command like `ssh builds@buildhost connect <job +ID>`, so we use the `SSH_ORIGINAL_COMMAND` variable to grab the command line +they included: + +```python +cmd = os.environ.get("SSH_ORIGINAL_COMMAND") or "" +cmd = shlex.split(cmd) +if len(cmd) != 2: + fail("Usage: ssh ... connect <job ID>") +op = cmd[0] +if op not in ["connect", "tail"]: + fail("Usage: ssh ... connect <job ID>") +job_id = int(cmd[1]) +``` + +Then we do some authentication, fetching the job info from the local job runner +and checking their key against meta.sr.ht (the authentication service). + +```python +b64key = sys.argv[1] + +def get_info(job_id): + r = requests.get(f"http://localhost:8080/job/{job_id}/info") + if r.status_code != 200: + return None + return r.json() + +info = get_info(job_id) +if not info: + fail("No such job found.") + +meta_origin = get_origin("meta.sr.ht") +r = requests.get(f"{meta_origin}/api/ssh-key/{b64key}") +if r.status_code == 200: + username = r.json()["owner"]["name"] +elif r.status_code == 404: + fail("We don't recognize your SSH key. Make sure you've added it to " + + f"your account.\n{get_origin('meta.sr.ht', external=True)}/keys") +else: + fail("Temporary authentication failure. Try again later.") + +if username != info["username"]: + fail("You are not permitted to connect to this job.") +``` + +There are two modes from here on out: connecting and tailing. The former logs +into the local build VM, and the latter prints the logs to the terminal. +Connecting looks like this: + +```python +def connect(job_id, info): + """Opens a shell on the build VM""" + limit = naturaltime(datetime.utcnow() - deadline) + print(f"Your VM will be terminated {limit}, or when you log out.") + print() + requests.post(f"http://localhost:8080/job/{job_id}/claim") + sys.stdout.flush() + sys.stderr.flush() + tty = os.open("/dev/tty", os.O_RDWR) + os.dup2(0, tty) + subprocess.call([ + "ssh", "-qt", + "-p", str(info["port"]), + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "LogLevel=quiet", + "build@localhost", "bash" + ]) + requests.post(f"http://localhost:8080/job/{job_id}/terminate") +``` + +This is pretty self explanatory, except perhaps for the dup2 - we just open +`/dev/tty` and make `stdin` a copy of it. Some interactive applications +misbehave if stdin is not a tty, and this mimics the normal behavior of SSH. +Then we log into the build VM over SSH, which with stdin/stdout/stderr rigged up +like so will allow the user to interact with the build VM. After that completes, +we terminate the VM. + +This is mostly plumbing work that just serves to get the user from point A to +point B. The tail functionality is more application-like: + +```python +def tail(job_id, info): + """Tails the build logs to stdout""" + logs = os.path.join(cfg("builds.sr.ht::worker", "buildlogs"), str(job_id)) + p = subprocess.Popen(["tail", "-f", os.path.join(logs, "log")]) + tasks = set() + procs = [p] + # holy bejeezus this is hacky + while True: + for task in manifest.tasks: + if task.name in tasks: + continue + path = os.path.join(logs, task.name, "log") + if os.path.exists(path): + procs.append(subprocess.Popen( + f"tail -f {shlex.quote(path)} | " + + "awk '{ print \"[" + shlex.quote(task.name) + "] \" $0 }'", + shell=True)) + tasks.update({ task.name }) + info = get_info(job_id) + if not info: + break + if info["task"] == info["tasks"]: + for p in procs: + p.kill() + break + time.sleep(3) + +if op == "connect": + if info["task"] != info["tasks"] and info["status"] == "running": + tail(job_id, info) + connect(job_id, info) +elif op == "tail": + tail(job_id, info) +``` + +This... I... let's just pretend you never saw this. And that's how SSH access to +builds.sr.ht works! diff --git a/content/blog/Introducing-shell-access-for-builds.md b/content/blog/Introducing-shell-access-for-builds.md @@ -0,0 +1,88 @@ +--- +date: 2019-08-19 +layout: post +title: Shell access for builds.sr.ht CI +tags: ["announcement", "sourcehut"] +--- + +Have you ever found yourself staring at a failed CI build, wondering desperately +what happened? Or, have you ever needed a fresh machine on-demand to test out an +idea in? Have you been working on Linux, but need to test something on OpenBSD? +Starting this week, builds.sr.ht can help with all of these problems, because +you can now SSH into the build environment. + +<small class="text-muted"> + If you didn't know, <a href="https://sourcehut.org">Sourcehut</a> is the 100% + open/libre software forge for hackers, complete with git and Mercurial + hosting, CI, mailing lists, and more - with no JavaScript. Try it out! +</small> + +The next time your build fails on builds.sr.ht, you'll probably notice the +following message: + +![Screenshot of builds.sr.ht showing a prompt to SSH into the failed build +VM and examine it](https://sr.ht/thL-.png) + +After the build fails, we process everything normally - sending emails, +webhooks, and so on - but keep the VM booted for an additional 10 minutes. If +you do log in during this window, we keep the VM alive until you log out or +until your normal build time limit has elapsed. Once you've logged in, you get a +shell and can do anything you like, such as examining the build artifacts or +tweaking the source and trying again. + +``` +$ ssh -t builds@azusa.runners.sr.ht connect 81809 +Connected to build job #81809 (failed): +https://builds.sr.ht/jobs/~sircmpwn/81809 +Your VM will be terminated 4 hours from now, or when you log out. + +bash-5.0 $ +``` + +You can also connect to any build over SSH by adding `shell: true` to your build +manifest. When you do, the VM will be kept alive after all of the tasks have +finished (even if it doesn't fail) so you can SSH in. You can also SSH in before +the tasks have finished, and tail the output of the build in your terminal. An +example use case might be getting a fresh Alpine environment to test build your +package on: + +<script + id="asciicast-wnLYZwDuvkbIHwgTdmnqtQpXh" + src="https://asciinema.org/a/pafXXANiWHY9MOH2yXdVHHJRd.js" async +></script> +<noscript><i>This article includes third-party JavaScript content from +asciinema.org, a free- and open-source platform that I trust.</i></noscript> + +This was accomplished with a simple build manifest: + +``` +image: alpine/edge +shell: true +sources: +- https://git.alpinelinux.org/aports +tasks: +- "prep-abuild": | + abuild-keygen -an +``` + +Since build manifests run normally in advance of your shell login, you can do +things like install your preferred editor and dotfiles, pull down your SSH keys +through [build +secrets](https://man.sr.ht/tutorials/builds.sr.ht/using-build-secrets.md), or +anything else you desire to set up a comfortable working environment. + +Furthermore, by leveraging the [builds.sr.ht +API](https://man.sr.ht/builds.sr.ht/api.md), you can write scripts which take +advantage of the shell features. Need a NetBSD shell? With a little scripting +you can get something like this working: + +<script + id="asciicast-8etTNE7Ptgmu6hO3cVDlvrAal" + src="https://asciinema.org/a/pafXXANiWHY9MOH2yXdVHHJRd.js" async +></script> + +With experimental multiarch support being rolled out, soon you'll be just a few +keystrokes away from an ARM or PowerPC shell, too. + +I want to expand more on SSH access in the future. Stay tuned and [let me +know](mailto:sir@cmpwn.com) if you have any cool ideas! diff --git a/content/blog/Introduction-to-POSIX-shell.md b/content/blog/Introduction-to-POSIX-shell.md @@ -0,0 +1,102 @@ +--- +date: 2018-02-05 +layout: post +title: Introduction to POSIX shell +tags: [shell, instructional] +--- + +What the heck is the POSIX shell anyway? Well, the POSIX (the Portable Operating +System Interface) shell is the standard Unix shell - standard meaning it was +formally defined and shipped in a published standard. This makes shell scripts +written for it portable, something no other shell can lay claim to. The POSIX +shell is basically a formalized version of the venerable Bourne shell, and on +your system it lives at `/bin/sh`, unless you're one of the unlucky masses for +whom this is a symlink to bash. + +## Why use POSIX shell? + +The "Bourne Again shell", aka bash, is not standardized. Its grammar, +features, and behavior aren't formally written up anywhere, and only one +implementation of bash exists. Without a standard, bash is defined *by* its +implementation. POSIX shell, on the other hand, has many competing +implementations on many different operating systems - all of which are +compatible with each other because they conform to the standard. + +Any shell that utilizes features specific to Bash are not portable, which means +you cannot take them with you to any other system. Many Linux-based systems do +not use Bash or GNU coreutils. Outside of Linux, pretty much everyone but Hurd +does *not* ship GNU tools, including bash[^1]. On any of these systems, scripts +using "bashisms" will not work. + +This is bad if your users wish to utilize your software anywhere other than +GNU/Linux. If your build tooling utilizes bashisms, your software will not build +on anything but GNU/Linux. If you ship runtime scripts that use bashisms, your +software will not *run* on anything but GNU/Linux. The case for sticking to +POSIX shell in shipping software is compelling, but I argue that you should +stick to POSIX shell for your personal scripts, too. You might not care now, but +when you feel like flirting with other Unicies you'll thank me when all of your +scripts work. + +One place where POSIX shell does *not* shine is for interactive use - a place +where I think bash sucks, too. Any shell you want to use for your day-to-day +command line work is okay in my book. I use fish. Use whatever you like +interactively, but stick to POSIX sh for your scripts. + +## How do I use POSIX shell? + +At the top of your scripts, put `#!/bin/sh`. You don't have to worry about using +`env` here like you might have been trained to do with bash: `/bin/sh` is the +standardized location for the POSIX shell, and any standards-conforming system +will either put it there or make your script work anyway.[^2] + +The next step is to avoid bashisms. There are many, but here are a few that +might trip you up: + +- `[[ condition ]]` does not work; use `[ condition ]` +- Arrays do not work; [use IFS](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_05) +- Local variables do not work; use a subshell + +The easiest way to learn about POSIX shell is to [read the +standard](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html) - +it's not too dry and shorter than you think. + +## Using standard coreutils + +The last step to writing portable scripts is to use portable tools. Your system +may have GNU coreutils installed, which provides tools like `grep` and `cut`. +Unfortunately, GNU has extended these tools with its own non-portable flags and +tools. It's important that you avoid these. + +One dead giveaway of a non-portable flag is long flags, e.g. `grep --file=FILE` +as opposed to `grep -f`. The POSIX standard only defines the `getopt` function - +not the proprietary GNU `getopt_long` function that's used to interpret long +options. As a result, no long flags are standardized. You might worry that this +will make your scripts difficult to understand, but I think that on the whole it +will not. Shell scripts are already pretty alien and require some knowledge to +understand. Is knowledge of what the magic word `grep` means much different +from knowledge of what `grep -E` means? + +I also like that short flags allow you to make more concise command lines. Which +is better: `ps --all --format=user --without-tty`, or `ps -aux`? If you are +inclined to think the former, do you also prefer `function(a, b, c) { return a + +b + c; }` over `(a, b, c) => a + b + c`? Conciseness matters, and POSIX shell +supports comments if necessary! + +Some tips for using short flags: + +- They can be collapsed: `cmd -a -b -c` is equivalent to `cmd -abc` +- If they take additional arguments, either a space or no separation is + acceptable: `cmd -f"hello world"` or `cmd -f "hello world"` + +A good reference for learning about standardized commands is, once again, [the +standard](http://pubs.opengroup.org/onlinepubs/9699919799/). From this page, +search for the command you want, or navigate through "Shell & Utilities" -> +"Utilities" for a list. If you have `man-pages` installed, you will also find +POSIX man pages installed on your system with the `p` postfix, such as `man 1p +grep`. Note: at the time of writing, the POSIX man pages do not use dashes if +your locale is UTF-8, which makes searching for flags with `/` difficult. Use +`env LC_ALL=POSIX man 1p grep` if you need to search for flags, and I'll speak +to the maintainer of man-pages about this. + +[^1]: A reader points out that macOS ships an ancient version of bash. +[^2]: *2018-05-15 correction*: `#!/bin/sh` is unfortunately not standardized by POSIX. However, I still recommend its use, as most operating systems will place it there. The portable way to invoke shell scripts is `sh path/to/script`. diff --git a/content/blog/Introduction-to-Wayland.md b/content/blog/Introduction-to-Wayland.md @@ -0,0 +1,246 @@ +--- +date: 2017-06-10 +layout: post +title: An introduction to Wayland +tags: [wayland, instructional] +--- + +Wayland is the new hotness on the Linux graphics stack. There are plenty of +introductions to Wayland that give you the high level details on how the stack +is laid out how applications talk directly to the kernel with EGL and so on, but +that doesn't give you much practical knowledge. I'd like to instead share with +you details about how the protocol actually works and how you can use it. + +This article has been translated: [ру́сский](http://howtorecover.me/vvedenie-v-wayland) + +Let's set aside the idea that Wayland has anything to do with graphics. Instead +we'll treat it like a generic protocol for two parties to share and talk about +resources. These resources are at the heart of the Wayland protocol - resources +like a keyboard or a surface to draw on. Each of these resources exposes an API +for engaging with it, including functions you can call and *events* you can +listen to. + +Some of these resources are *globals*, which are exactly what they sound like. +These resources include things like **wl_outputs**, which are the displays +connected to your graphics card. Other resources, like **wl_surface**, require +the client to ask the server to allocate new resources when needed. Negotiating +for new resources is generally possible through the API of some global resource. + +Your Wayland client gets started by obtaining a reference to the **wl_display** +like so: + +```c +struct wl_display *display = wl_display_connect(NULL); +``` + +This establishes a connection to the Wayland server. The most important role of +the display, from the client perspective, is to provide the **wl_registry**. +The registry enumerates the globals available on the server. + +```c +struct wl_registry *registry = wl_display_get_registry(display); +``` + +The registry emits an *event* every time the server adds or removes a global. +*Listening* to these events is done by providing an implementation of a +**wl_registry_listener**, like so: + +```c +void global_add(void *our_data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) { + // TODO +} + +void global_remove(void *our_data, + struct wl_registry *registry, + uint32_t name) { + // TODO +} + +struct wl_registry_listener registry_listener = { + .global = global_add, + .global_remove = global_remove +}; +``` + +Interfaces like this are used to listen to events from all kinds of resources. +Attaching the listener to the registry is done like this: + +```c +void *our_data = /* arbitrary state you want to keep around */; +wl_registry_add_listener(registry, &registry_listener, our_data); +wl_display_dispatch(display); +``` + +During the `wl_display_dispatch`, the `global_add` function is called for each +global on the server. Subsequent calls to `wl_display_dispatch` may call +`global_remove` when the server destroys globals. The `name` passed into +`global_add` is more like an ID, and identifies this resource. The `interface` +tells you what API the resource implements, and distinguishes things like a +**wl_output** from a **wl_seat**. The API these resources implement are +described with XML files like this: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<!-- For copyright information, see https://git.io/vHyIB --> +<protocol name="gamma_control"> + <interface name="gamma_control_manager" version="1"> + <request name="destroy" type="destructor"/> + + <request name="get_gamma_control"> + <arg name="id" type="new_id" interface="gamma_control"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + </interface> + + <interface name="gamma_control" version="1"> + <enum name="error"> + <entry name="invalid_gamma" value="0"/> + </enum> + + <request name="destroy" type="destructor"/> + + <request name="set_gamma"> + <arg name="red" type="array"/> + <arg name="green" type="array"/> + <arg name="blue" type="array"/> + </request> + + <request name="reset_gamma"/> + + <event name="gamma_size"> + <arg name="size" type="uint"/> + </event> + </interface> +</protocol> +``` + +A typical Wayland server implementing this protocol would create a +`gamma_control_manager` global and add it to the registry. The client then binds +to this interface in our `global_add` function like so: + +```c +#include "wayland-gamma-control-client-protocol.h" +// ... +struct wl_output *example; +// gamma_control_manager.name is a constant: "gamma_control_manager" +if (strcmp(interface, gamma_control_manager.name) == 0) { + struct gamma_control_manager *mgr = + wl_registry_bind(registry, name, + &gamma_control_manager_interface, version); + struct gamma_control *control = + gamma_control_manager_get_gamma_control(mgr, example); + gamma_control_set_gamma(control, ...); +} +``` + +These functions are generated by running the XML file through `wayland-scanner`, +which outputs a header and C glue code. These XML files are called "protocol +extensions" and let you add arbitrary extensions to the protocol. The core +Wayland protocols themselves are described with similar XML files. + +Using the Wayland protocol to create a surface to display pixels with consists +of these steps: + +1. Obtain a **wl_display** and use it to obtain a **wl_registry**. +2. Scan the registry for globals and grab a **wl_compositor** and + a **wl_shm_pool**. +3. Use the **wl_compositor** interface to create a **wl_surface**. +4. Use the **wl_shell** interface to describe your surface's role. +5. Use the **wl_shm** interface to allocate shared memory to store pixels in. +6. Draw something into your shared memory buffers. +7. Attach your shared memory buffers to the **wl_surface**. + +Let's break this down. + +The **wl_compositor** provides an interface for interacting with the +*compositor*, that is the part of the Wayland server that *composites* surfaces +onto the screen. It's responsible for creating surface resources for clients to +use via `wl_compositor_create_surface`. This creates a **wl_surface** resource, +which you can attach pixels to for the compositor to render. + +The role of a surface is undefined by default - it's just a place to put pixels. +In order to get the compositor to do anything with them, you must give the +surface a *role*. Roles could be anything - desktop background, system tray, etc - +but the most common role is a *shell surface*. To create these, you take your +wl_surface and hand it to the **wl_shell** interface. You'll get back a +**wl_shell_surface** resource, which defines your surface's purpose and gives +you an interface to do things like set the window title. + +Attaching pixel buffers to a wl_surface is pretty straightforward. There are two +primary ways of creating a buffer that both you and the compositor can use: EGL +and shared memory. EGL lets you use an OpenGL context that renders directly on +the GPU with minimal compositor involvement (fast) and shared memory (via +**wl_shm**) allows you to simply dump pixels in memory and hand them to the +compositor (flexible). There are many other Wayland interfaces I haven't +covered, giving you everything from input devices (via **wl_seat**) to clipboard +access (via **wl_data_source**), plus many protocol extensions. Learning more +about these is an exercise left to the reader. + +Before we wrap this article up, let's take a brief moment to discuss the server. +Most of the concepts here are already familiar to you by now. The Wayland server +also utilizes a **wl_display**, but differently from the client. The display on +the server has ownership over the *event loop*, via **wl_event_loop**. The event +loop of a Wayland server might look like this: + +```c +struct wl_display *display = wl_display_create(); +// ... +struct wl_event_loop *event_loop = wl_display_get_event_loop(display); +while (true) { + wl_event_loop_dispatch(event_loop, 0); +} +``` + +The event loop has a lot of helpful utilities for the Wayland server to take +advantage of, including internal event sources, timers, and file descriptor +monitoring. Before starting the event loop the server is going to start +obtaining its own resources and creating Wayland globals for them with +`wl_global_create`: + +```c +struct wl_global *global = wl_global_create( + display, + &wl_output_interface, + 1 /* version */, + our_data, + wl_output_bind); +``` + +The `wl_output_bind` function here is going to be called when a client attempts +to bind to this resource via `wl_registry_bind`, and will look something like +this: + +```c +void wl_output_bind(struct wl_client *client, + void *our_data, + uint32_t their_version, + uint32_t id) { + struct wl_resource *resource = + wl_resource_create_checked( + client, + wl_output_interface, + their_version, + our_version, + id); + // ...send output modes or whatever else you need to do +} +``` + +Some of the resources a server is going to be managing might include: + +- DRM state for direct access to outputs +- GLES context (or another GL implementation) for rendering +- libinput for input devices +- udev for hotplugging + +Through the Wayland protocol, the server provides an abstraction on top of these +resources and offers them to clients. Some servers go further, with novel ways +of compositing clients or handling input. Some provide additional interactivity, +such as desktop shells that are actually running in the compositor rather than +external clients. Other servers are designed for mobile use and provide a user +experience that more closely matches the mobile experience than the traditional +desktop experience. Wayland is designed to be flexible! diff --git a/content/blog/Its-not-okay-to-pretend-youre-open-source.md b/content/blog/Its-not-okay-to-pretend-youre-open-source.md @@ -0,0 +1,70 @@ +--- +date: 2018-10-30 +title: It's not okay to pretend your software is open source +layout: page +tags: ["philosophy", "free software"] +--- + +Unfortunately, I find myself writing about the Commons Clause again. For those +not in the know, the Commons Clause is an addendum designed to be added to free +software licenses. The restrictions it imposes (you cannot sell the software) +makes the resulting franken-license nonfree. I'm not going to link to the +project which brought this subject back into the discussion - they don't deserve +the referral - but the continued proliferation of software using the Commons +Clause gives me reason to speak out against it some more. + +One of my largest complaints with the Commons Clause is that it hijacks +language used by open source projects to proliferate nonfree software, and +encourages software using it to do the same. Instead of being a new software +license, it tries to stick itself onto other respected licences - often the +Apache 2.0 license. The name, "Commons Clause", is also disingenuous, hijacking +language used by respected entities like Creative Commons. In truth, the Commons +Clause serves to remove software from the commons[^anti]. Combining these +problems gives you language like "Apache+Commons Clause", which is easily +confused with [Apache Commons][apache-commons]. + +[^anti]: This is why I often refer to it as the "Anti-Commons Clause", though I felt that was a bit too Stallman-esque for this article. +[apache-commons]: http://commons.apache.org/ + +Projects using the Commons Clause have also been known to describe their license +as "permissive" or "open", some even calling their software "open source". This +is dishonest. FOSS refers to "free and open source software". The former, free +software, is defined by the [free software definition][fsd], published by +[GNU][gnu]. The latter, open source software, is defined by the [open source +definition][osd], published by the [OSI][osi]. Their definitions are very +similar, and nearly all FOSS licenses qualify under both definitions. These are +unambiguous, basic criteria which protects developers, contributors, and users +of free and open source software. These definitions are so basic, important and +well-respected that dismissing them is akin to dismissing climate change. + +[fsd]: https://www.gnu.org/philosophy/free-sw.en.html +[gnu]: https://gnu.org +[osd]: https://opensource.org/osd +[osi]: https://opensource.org + +Claiming your software is open source, permissively licensed, free software, +etc, when you use the Commons Clause, is *lying*. These lies are pervasive among +users of the Commons Clause. The page listing [Redis +Modules](https://redis.io/modules), for example, states that only software under +an OSI-approved license is listed. Six of the modules there are using nonfree +licenses, and antirez seems content to [ignore the problem][exhibit-a] until [we +forget about it][exhibit-b]. They're in for a long wait - we're not going to +forget about **shady, dishonest, and unethical companies like Redis Labs**. + +[exhibit-a]: https://github.com/antirez/redis-doc/pull/984 +[exhibit-b]: https://github.com/RedisLabsModules/RediSearch/issues/518 + +I don't use nonfree software[^beer], but I'm not going to sit here and tell you +not to make nonfree software. You have every right to license your work in any +way you choose. However, if you choose not to use a FOSS license, you need to +own up to it. Don't pretend that your software is something it's not. There are +many benefits to being a member of the free software community, but you are not +entitled to them if your software isn't. This behavior has to stop. + +[^beer]: Free as in freedom, not as in free beer. + +Finally, I have some praise to offer. [Dgraph](https://dgraph.io/) was briefly +licensed under Apache plus the Commons Clause, and had the sort of misleading +and false information this article decries on their marketing website, docs, and +so on. However, they've rolled it back, and Dgraph is now using the Apache 2.0 +license with no modifications. Thank you! diff --git a/content/blog/KDE-Sprint-retrospective.md b/content/blog/KDE-Sprint-retrospective.md @@ -0,0 +1,69 @@ +--- +date: 2018-04-28 +layout: post +title: Sway reporting in from KDE's Berlin development sprint +tags: [roundup, sway, wayland, kde] +--- + +I'm writing to you from an airplane on my way back to Philadelphia, after +spending a week in Berlin working with the KDE team. It was great to meet those +folks and work with them for a while. It'll take me some time to get the taste +of C++ out of my mouth, though! In all seriousness, it was a very productive +week and I feel like we have learned a lot about each other's projects and have +a strengthened interest in collaborating more in the future. + +The main purpose of my trip was to find opportunities for +[sway](http://swaywm.org) and [KDE](http://kde.org) to work together on +improving the Linux desktop. Naturally, the main topic of discussion was +interopability of software written for each of our projects. I brought the +wlroots layer-shell protocol to the table seeking their feedback on it, as well +as reviewing how their desktop shell works today. From our discussions we found +a lot of common ground in our designs and needs, as well as room for improvement +in both of our approaches. + +The KDE approach to their desktop shell is similar to the original sway +approach. Today, their Plasma shell uses a number of proprietary protocols which +are hacks on top of the xdg-shell protocol (for those not in the know, the +xdg-shell protocol is used to render normal desktop windows and is not designed +for use with e.g. panels) that incorporate several of the concepts they were +comfortable using on X11 in an almost 1:1 fashion. Sway never had any X11 +concepts to get comfortable with, but some may not know that sway's panel, +wallpaper, and lock screen programs on the 0.x releases are also hacks on top of +xdg-shell that are not portable between compositors. + +In the wlroots project (which is overseen by sway), we've been developing a +new protocol designed for desktop shell components like these. In theory, it is +a more generally applicable approach to building desktop shells on Wayland than +the approach we were using before. I sat down with the KDE folks and went over +this protocol in great detail, and learned about how Plasma shell works today, +and we were happy to discover that the wlroots approach (with some minor tweaks) +should be excellently suited to Plasma shell. In addition to the layer-shell, we +reviewed several other protocols Plasma uses to build its desktop experience, +and identified more places where it makes sense for us to unify our approach. +Other subjects discussed included virtual desktops, external window management, +screen capture and pipewire, and more. + +The upshot of this is that we believe it's possible to integrate the Plasma +shell with sway. Users of KDE on X11 were able to replace kwin with i3 and still +utilize the Plasma shell - a feature which was lost in the transition to +Wayland. As we continue to work together, this use-case may well be captured +again. Even KDE users who are uninterested in sway stand to benefit from this. +The hacks Plasma uses today are temporary and unmaintainable, and the +improvements to Plasma's codebase will make it easier to work with. Should kwin +grow stable layer-shell support, clients designed for sway will work on KDE as +well. Replacing sway's own similar hacks will have similar benefits for our +codebase and open the door to 3rd-party panels, lockscreens, rofi, etc. + +I spent my time in their care working on actual code to this end. I wrote up a +C++ library that extends Qt with layer-shell support called +[qtlayershell](https://github.com/SirCmpwn/qtlayershell), and extended the +popular [Latte Dock](#) KDE extension to support it. Though this work is not +complete, it works - as I write this blog post, Latte is running on my sway +session! This is good progress, but I must return my focus to wlroots soon. If +you are interested in this work, please help me complete it! + +![](/img/latte-dock.png) + +A big thanks goes to KDE for putting on this event and covering my travel costs +to attend. I hope they found it as productive as I did, and I'm very excited +about working more with them in the future. The future of Wayland is bright! diff --git a/content/blog/KnightOS-was-interesting.md b/content/blog/KnightOS-was-interesting.md @@ -0,0 +1,75 @@ +--- +date: 2020-01-27 +title: KnightOS was an interesting operating system +layout: post +tags: [knightos] +--- + +[KnightOS](https://knightos.org) is an operating system I started writing about +10 years ago, for Texas Instruments line of z80 calculators &mdash; the TI-73, +TI-83+, TI-84+, and similar calculators are supported. It still gets the rare +improvements, but these days myself and most of the major contributors are just +left with starry eyed empty promises to themselves that one day they'll do one +of those big refactorings we've been planning... for 4 or 5 years now. + +Still, it was a really interesting operating system which was working under some +challenging constraints, and overcame them to offer a rather nice Unix-like +environment, with a filesystem, preemptive multiprocessing *and* multithreading, +assembly and C programming environments, and more. The entire system was written +in handwritten z80 assembly, almost 50,000 lines of it, on a compiler toolchain +we built from scratch. + +There was only 64 KiB of usable RAM. The kernel stored *all* of its state in +1024 bytes of statically allocated RAM. Many subsystems used overlapping parts +of this memory, which was carefully planned for to avoid conflicts. The +userspace memory allocator used a simple linked list for tracking allocations, +to minimize the overhead of each allocation and maximize the usable space for +userspace programs. There was no MMU in the sense that we have on modern +computers, so any program could freely overwrite any other programs. In fact, +the "userspace" task switching GUI would read the kernel's process table +directly to make a list of running programs. + +The non-volatile storage was NOR Flash, which presents some interesting +constraints. In the worst case we only had 512 KiB of storage, and even in the +best case just 4MiB (this for a device released in 2013). This space was shared +with the kernel, whose core code was less than 4KiB, and including high-address +subsystems still clocked in at less than 16KiB. Due to the constraints of NOR +Flash, a custom filesystem was designed which did all daily operations by only +*resetting* bits in the underlying storage. In order to *set* any bits, we had +to set the entire 64 KiB sector to 1. Overhead was also kept to a bare minimum +here to maximize storage space available to users. + +Writing to Flash storage also renders it unreadable while the operation is in +progress. The kernel normally executes directly from Flash, resident at the +bottom of the memory. Therefore, in order to modify Flash, the kernel's Flash +driver copies part of itself to RAM, jumps to it, and then jumps back after the +operation is complete. Recall that all of the kernel's memory is statically +allocated, and there's not much of it &mdash; we used only 128 bytes for the +code which runs in RAM, and it's shared with some other stuff that we had to +plan around. In order to meet these constraints, we employ *self modifying code* +&mdash; the Flash driver copies some of itself into RAM, then pre-computes some +information and *modifies* that machine code in-place before jumping to it. + +We also had some basic networking support. The calculator has a 2.5mm jack, +similar to headphone jacks &mdash; if you had a 3.5mm adapter, we had a music +player which would play MIDI or WAV files. The kernel had direct control of the +voltages on the ring and tip, and had to bitbang them directly in software[^1]. +Based on this we built some basic networking support, which supported +calculator-to-calculator and calculator-to-PC information exchange. Later models +had a mini-USB controller (which, funnily enough, can also be bitbanged in +software), but we never ended up writing a driver for it. + +[^1]: Newer hardware revisions had some support hardware which was capable of transferring a single byte without software intervention. + +The KnightOS kernel also includes some code which is the first time I ever wrote +["here be dragons"](https://github.com/KnightOS/kernel/blob/e257f54e021ee743306a2a4a5a152860728fb3f8/src/00/restarts.asm#L129-L130) +into a comment, and I don't think I've topped it since. + +Despite these constraints, KnightOS is completely booted up to a useful +Unix-like (with a graphical interface) faster than you can lift your finger off +of the power button. The battery could last the entire semester, if you're +lucky. Can the device you're reading this on claim the same?[^2] + +[^2]: The device I'm writing this blog post with is 3500&times; faster than my calculator, has 262,144&times; more RAM, and 2.1×10<sup>6</sup> times more storage space. + +<video controls src="https://yukari.sr.ht/knightos.webm"></video> diff --git a/content/blog/Learn-your-package-manager.md b/content/blog/Learn-your-package-manager.md @@ -0,0 +1,41 @@ +--- +date: 2018-01-10 +title: Learn about your package manager +layout: post +tags: [philosophy] +--- + +Tools like virtualenv, rbenv, and to a lesser extent npm and pip, are +occasionally useful in development but encourage bad practices in production. +Many people forget that their distro already has a package manager! And there's +more-- you, the user, can write packages for it! + +Your distro's package repositories probably already have a lot of your +dependencies, and can conveniently update your software alongside the rest of +your system. On the whole you can expect your distro packages to be much better +citizens on your system than a language-specific package manager will be. +Additionally, pretty much all distros provide a means for you to host your own +package repositories, from which you can install and update any packages you +choose to make. + +If you find some packages to be outdated, find out who the package maintainer is +and shoot them an email. Or better yet - find out how the package is built and +send them a patch instead. Linux distributions are run by volunteers, and it's +easy to volunteer yourself! Even if you find *missing* packages, it's a simple +matter to whip up a package yourself and submit it for inclusion in your +distro's package repository, installing it from your private repo in the +meanwhile. + +"But what if dependencies update and break my stuff?", you ask. First of all, +why aren't you keeping your dependencies up-to-date? That aside, some distros, +like Alpine, let you pin packages to a specific version. Also, using the +distro's package manager doesn't necessarily mean you have to use the distro's +package repositories - you can stand up your own repos and prioritize it over +the distro repos, then release on any schedule you want. + +In my opinion, the perfect deployment strategy for some software is pushing a +new package to your package repository, then SSHing into your fleet and running +system updates (probably automatically). This is how I manage deployments for +most of my software. As a bonus, these packages offer a good place to configure +things that your language's package manager may be ill suited to, such as +service files or setting up new users/groups on the system. Consider it! diff --git a/content/blog/Lessons-to-learn-from-C.md b/content/blog/Lessons-to-learn-from-C.md @@ -0,0 +1,83 @@ +--- +date: 2017-01-30 +# vim: tw=80 +title: Lessons to learn from C +layout: post +tags: [C, language design] +--- + +C is my favorite language, though I acknowledge that it has its warts. I've +tried looking at languages people hope will replace C (Rust, Go, etc), and +though they've improved on some things they won't be supplanting C in my life +any time soon. I'll share with you what makes C a great language to me. Take +some of these things as inspiration for the next C replacement you write. + +First of all, it's important to note that I'm talking about the language, not +its standard library. The C standard library isn't *awful*, but it certainly +leaves a lot to be desired. I also want to place a few limitations on the kind +of C we're talking about - you can write bad code in any language, and C is no +different. For the purpose of argument, let's assume the following: + +- C99 minimum +- Absolutely no code in headers - just type definitions and function prototypes +- Minimal use of typedefs +- No macros +- No compiler extensions + +I hold myself to these guidelines when writing C, and it is from this basis that +I compare other languages with C. It's not useful to compare bad C to another +language, because I wouldn't want to write bad C either. + +Much of what I like about C boils down to this: **C is simple**. The ultimate +goal of any system should be to attain the simplest solution for the problems it +faces. C prefers to be conservative with new features. The lifetime of a feature +in Rust, for example, from proposal to shipping is generally 0 to 6 months. The +same process in C can take up to 10 years. C is a venerable language, and has +already long since finished adding core features. It is stable, simple, and +reliable. + +To this end, language features map closely to behaviors common to most CPUs. C +strikes a nearly perfect balance of usability versus simplicity, which +results in a small set of features that are easy to reason about. A C expert +could roughly predict the assembly code produced by their compiler (assuming +`-O0`) for any given C function. It follows that C compilers are easy to write +and reason about. + +The same person would also be able to give you a rough idea of the +performance characteristics of that function, pointing out things like cache +misses and memory accesses that are draining on speed, or giving you a precise +understanding of how the function handles memory. If I look at a function in +other languages, it's much more difficult to discern these things with any +degree of precision without actually compiling the code and looking at the +output. + +The compiler also integrates very comfortably with the other tools near it, like +the assembler and linker. Symbols in C map 1:1 to symbols in the object files, +which means linking objects together is simple and easily reasoned about. It +also makes interop with other languages and tools straightforward - there's a +reason every language has a means of writing C bindings, but not generally C++ +bindings. The use of headers to declare external symbols and types is also nicer +than some would have you believe, since it gives you an opportunity to organize +and document your API. + +C is also the most portable programming language in the world. Every operating +system on every architecture has a C compiler, and they weren't really +considered a viable platform until it did. Once you have a C compiler you +generally have everything else, because everything else was either written in C +or was written in a language that was implemented in C. I can write C programs +on/for Linux, Windows, BSD, Minix, plan9, and a dozen other niche operating +systems, or even no operating system, on pretty much any CPU architecture I +want. No other language supports nearly as many platforms as C does. + +With these benefits acknowledged, there are some things C could do better. The +standard library is one of them, but we can talk about that some other time. +Another is generics; using void* all the time isn't good. Some features from +other languages would be nice - I would take something similar to Rust's match +keyword. Of course, the fragility of memory management in C is a concern that +other languages are wise to address. Undefined behavior is awful. + +Even for all of these warts, however, the basic simplicity and elegance of C +keeps me there. I would love to see a language that fixes these problems without +trying to be the kitchen sink, too. + +In short, I like C because **C is simple**. diff --git a/content/blog/Limited-generics-in-C.md b/content/blog/Limited-generics-in-C.md @@ -0,0 +1,110 @@ +--- +date: 2017-06-05 +layout: post +title: Limited "generics" in C without macros or UB +tags: [C] +--- + +I should start this post off by clarifying that what I have to show you today is +not, in fact, generics. However, it's useful in some situations to solve the +same problems that generics might. This is a pattern I've started using to +reduce the number of `void*` pointers floating around in my code: multiple +definitions of a struct. + +**Errata**: we rolled this approach back in wlroots because it causes problems +with LTO. I no longer recommend it. + +Let's take a look at a specific example. In +[wlroots](https://github.com/SirCmpwn/wlroots), `wlr_output` is a generic type +that can be implemented by any number of backends, like DRM (direct rendering +manager), wayland windows, X11 windows, RDP outputs, etc. The `wlr/types.h` +header includes this structure: + +```c +struct wlr_output_impl; +struct wlr_output_state; + +struct wlr_output { + const struct wlr_output_impl *impl; + struct wlr_output_state *state; + // [...] +}; + +void wlr_output_enable(struct wlr_output *output, bool enable); +bool wlr_output_set_mode(struct wlr_output *output, + struct wlr_output_mode *mode); +void wlr_output_destroy(struct wlr_output *output); +``` + +`wlr_output_impl` is defined elsewhere: + +```c +struct wlr_output_impl { + void (*enable)(struct wlr_output_state *state, bool enable); + bool (*set_mode)(struct wlr_output_state *state, + struct wlr_output_mode *mode); + void (*destroy)(struct wlr_output_state *state); +}; + +struct wlr_output *wlr_output_create(struct wlr_output_impl *impl, + struct wlr_output_state *state); +void wlr_output_free(struct wlr_output *output); +``` + +Nowhere, however, is `wlr_output_state` defined. It's left an incomplete type +throughout all of the common `wlr_output` code. The "generic" part is that each +output implementation, in its own private headers, defines the +`wlr_output_state` struct for itself, like the DRM backend: + +```c +struct wlr_output_state { + uint32_t connector; + char name[16]; + uint32_t crtc; + drmModeCrtc *old_crtc; + struct wlr_drm_renderer *renderer; + struct gbm_surface *gbm; + EGLSurface *egl; + bool pageflip_pending; + enum wlr_drm_output_state state; + // [...] +}; +``` + +This allows implementations of the `enable`, `set_mode`, and `destroy` functions +to avoid casting a `void*` to the appropriate type: + +```c +static struct wlr_output_impl output_impl = { + .enable = wlr_drm_output_enable, + // [...] +}; + +static void wlr_drm_output_enable(struct wlr_output_state *output, + bool enable) { + struct wlr_backend_state *state = + wl_container_of(output->renderer, state, renderer); + if (output->state != DRM_OUTPUT_CONNECTED) { + return; + } + if (enable) { + drmModeConnectorSetProperty(state->fd, + output->connector, + output->props.dpms, + DRM_MODE_DPMS_ON); + // [...] + } else { + drmModeConnectorSetProperty(state->fd, + output->connector, + output->props.dpms, + DRM_MODE_DPMS_STANDBY); + } +} + +// [...] +struct wlr_output output = wlr_output_create(&output_impl, output); +``` + +The limitations of this approach are apparent: you cannot work with multiple +definitions of `wlr_output_state` in the same file. However, you get improved +type safety, have to write less code, and improve readability. diff --git a/content/blog/Line-printer-shell-hack.md b/content/blog/Line-printer-shell-hack.md @@ -0,0 +1,190 @@ +--- +date: 2019-10-30 +layout: post +title: An old-school shell hack on a line printer +tags: [hack] +--- + +It's been too long since I last did a good hack, for no practical reason other +than great hack value. In my case, these [often amount][vt220] to a nostalgia +for an age of computing I wasn't present for. In a recent bid to capture more of +this nostalgia, I recently picked up a dot matrix line printer, specifically the +Epson LX-350 printer. This one is nice because it has a USB port, so I don't +have to break out my pile of serial cable hacks to get it talking to Linux 😁 + +[vt220]: https://drewdevault.com/2016/03/22/Integrating-a-VT220-into-my-life.html + +This is the classic printer style, with infinite paper and a lovely noise during +printing. They are also fairly simple to operate - you can just write text +directly to `/dev/lp` (or `/dev/usb/lp9` in my case) and it'll print it out. +Slightly more sophisticated instructions can be written to them with ANSI escape +sequences, just like a terminal. They can also be rigged up to CUPS, then you +can use something like `man -t 5 scdoc` to produce printouts like this: + +[![](https://sr.ht/gHCA.jpg)](https://sr.ht/gHCA.jpg) + +Plugging the printer into Linux and writing out pages isn't much for hack value, +however. What I really wanted to make was something resembling an old-school +TTY - teletypewriter. So I wrote some [glue code in +Golang](https://git.sr.ht/~sircmpwn/lpsh), and soon enough I had a shell: + +<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts +allow-popups" +src="https://spacepub.space/videos/embed/d8943b2d-8280-497b-85ec-bc282ec2afdc" +frameborder="0" allowfullscreen style="width: 100%"></iframe> + +The glue code I wrote for this is fairly straightforward. In the simplest form, +it spins up a pty (pseudo-terminal), runs `/bin/sh` in it, and writes the pty +output into the line printer device. For those unaware, a pseudo-terminal is the +key piece of software infrastructure for running interactive text applications. +Applications which want to do things like print colored text, move +the cursor around and draw a TUI, and so on, will open `/dev/tty` to open the +current TTY device. For most applications used today, this is a +"pseudo-terminal", or pty, which is a terminal emulated in userspace - i.e. by +your terminal emulator. However, your terminal emulator is *emulating* a +terminal - the control sequences applications send to these are +backwards-compatible with 50 years of computing history. Interfaces like these +are the namesake of the TTY. + +Visual terminals came onto the scene later on, and in the classic computing +tradition, the old hands complained that it was less useful - you could no +longer write notes on your backlog, tear off a page and hand it to a colleague, +or [white-out](https://en.wikipedia.org/wiki/Wite-Out) mistakes. Early +[visual terminals](https://en.wikipedia.org/wiki/Computer_terminal) could also +be plugged directly into a line printer, and you could configure them to echo to +the printer or print out a screenfull of text at a time. A distinct advantage of +visual terminals is not having to deal with so much bloody paper, a problem that +I've become acutely familiar with in the past few days[^1]. + +Getting back to the glue code, I chose Golang because setting up a TTY is a bit +of a hassle in C, but in Golang it's pretty straightforward. There is a serial +port and in theory I could have plugged it in and spawned a getty on the +resulting serial device - but (1) it'd be write-only, so not especially +interactive without *hardware* hacks, and (2) I didn't feel like digging out my +serial cables. So: + +```go +import "git.sr.ht/~sircmpwn/pty" // fork of github.com/kr/pty + +// ... +winsize := pty.Winsize{ + Cols: 160, + Rows: 24, +} +cmd := exec.Command("/bin/sh") +cmd.Env = append(os.Environ(), + "TERM=lp", + fmt.Sprintf("COLUMNS=%d", 180)) +tty, err := pty.StartWithSize(cmd, &winsize) +``` + +*P.S. We're going to dive through the code in detail now. If you just want more +cool videos of this in action, skip to the bottom.* + +I set the TERM environment variable to `lp`, for line printer, which doesn't +really exist but prevents most applications from trying anything too tricksy +with their escape codes. The `tty` variable here is an `io.ReadWriter` whose +output is sent to the printer and whose input is sourced from wherever, in my +case from the stdin of this process[^2]. + +For a little more quality-of-life, I looked up Epson's proprietary ANSI escape +sequences and found out that you can tell the printer to feed back and forth in +216th" increments with the j and J escape sequences. The following code will +feed 2.5" out, then back in: + +```go +f.Write([]byte("\x1BJ\xD8\x1BJ\xD8\x1BJ\x6C")) +f.Write([]byte("\x1Bj\xD8\x1Bj\xD8\x1Bj\x6C")) +``` + +Which happens to be the perfect amount to move the last-written line up out of +the printer for the user to read, then back in to be written to some more. A +little bit of timing logic in a goroutine manages the transition between "spool +out so the user can read the output" and "spool in to write some more output": + +```go +func lpmgr(in chan (interface{}), out chan ([]byte)) { + // TODO: Runtime configurable option? Discover printers? dunno + f, err := os.OpenFile("/dev/usb/lp9", os.O_RDWR, 0755) + if err != nil { + panic(err) + } + + feed := false + f.Write([]byte("\n\n\n\r")) + + timeout := 250 * time.Millisecond + for { + select { + case <-in: + // Increase the timeout after input + timeout = 1 * time.Second + case data := <-out: + if feed { + f.Write([]byte("\x1Bj\xD8\x1Bj\xD8\x1Bj\x6C")) + feed = false + } + f.Write(lptl(data)) + case <-time.After(timeout): + timeout = 200 * time.Millisecond + if !feed { + feed = true + f.Write([]byte("\x1BJ\xD8\x1BJ\xD8\x1BJ\x6C")) + } + } + } +} +``` + +`lptl` is a work-in-progress thing which tweaks the outgoing data for some +quality-of-life changes, like changing backspace to ^H. Then, the main event +loop looks something like this: + +```go +inch := make(chan (interface{})) +outch := make(chan ([]byte)) +go lpmgr(inch, outch) + +inbuf := make([]byte, 4096) +go func() { + for { + n, err := os.Stdin.Read(inbuf) + if err != nil { + panic(err) + } + tty.Write(inbuf[:n]) + inch <- nil + } +}() + +outbuf := make([]byte, 4096) +for { + n, err := tty.Read(outbuf) + if err != nil { + panic(err) + } + b := make([]byte, n) + copy(b, outbuf[:n]) + outch <- b +} +``` + +The tty will echo characters written to it, so we just write to it from stdin +and increase the form feed timeout closer to the user's input so that it's not +constantly feeding in and out as you write. The resulting system is pretty +pleasant to use! I spent about hour working on improvements to it on a [live +stream](https://live.drewdevault.com). You can watch the system in action on the +archive here: + +<iframe width="560" height="370" sandbox="allow-same-origin allow-scripts" +src="https://spacepub.space/videos/embed/a8be6c87-9267-452e-8d3e-dd206880fa98" +frameborder="0" allowfullscreen style="width: 100%"></iframe> + +If you were a fly on the wall when Unix was written, it would have looked a lot +like this. And remember: [ed is the standard text +editor](https://www.gnu.org/fun/jokes/ed-msg.html). + +? + +[^1]: Don't worry, I recycled it all. +[^2]: In the future I want to make this use libinput or something, or eventually make a kernel module which lets you pair a USB keyboard with a line printer to make a TTY directly. Or maybe a little microcontroller which translates a USB keyboard into serial TX and forwards RX to the printer. Possibilities! diff --git a/content/blog/Local-mail-server.md b/content/blog/Local-mail-server.md @@ -0,0 +1,149 @@ +--- +date: 2018-08-05 +layout: post +title: Setting up a local dev mail server +tags: [mail, instructional] +--- + +As part of my work on [lists.sr.ht](https://meta.sr.ht), it was necessary for +me to configure a self-contained mail system on localhost that I could test +with. I hope that others will go through a similar process in the future when +they set up [the code](https://git.sr.ht/~sircmpwn/lists.sr.ht) for hacking on +locally or when working on other email related software, so here's a guide on +how you can set it up. + +There are lots of things you can set up on a mail server, like virtual mail +accounts backed by a relational database, IMAP access, spam filtering, and so +on. We're not going to do any of that in this article - we're just interested in +something we can test our email code with. To start, install your distribution +of `postfix` and pop open that `/etc/postfix/main.cf` file. + +Let's quickly touch on the less interesting config keys to change. If you want +the details about how these work, consult the postfix manual. + +- *myhostname* should be your local hostname +- *mydomain* should also be your local hostname +- *mydestination* should be `$myhostname, localhost.$mydomain, localhost` +- *mynetworks* should be `127.0.0.0/8` +- *home_mailbox* should be `Maildir/` + +Also ensure your hostname is set up right in `/etc/hosts`, something like this: + +``` +127.0.0.1 homura.localdomain homura +``` + +Okay, those are the easy ones. That just makes it so that your mail server +oversees mail delivery for the `127.0.0.0/8` network (localhost) and delivers +mail to local Unix user mailboxes. It will store incoming email in each user's +home directory at `~/Maildir`, and will deliver email to other Unix users. Let's +set up an email client for reading these emails with. Here's my development +[mutt](http://mutt.org) config: + +``` +set edit_headers=yes +set realname="Drew DeVault" +set from="sircmpwn@homura" +set editor=vim +set spoolfile="~/Maildir/" +set folder="~/Maildir/" +set timeout=5 +color index blue default ~P +``` + +Make any necessary edits. If you use mutt to read your normal mail, I suggest +also setting up an alias which runs `mutt -C path/to/dev/config`. Now, you +should be able to send an email to yourself or other Unix accounts with +mutt[^1]. Hooray! + +[^1]: Mutt crash course: run `mutt`, press `m` to compose a new email, enter the recipient (`$USER@$HOSTNAME` to send to yourself) and the subject, then compose your email, exit the editor, and press `y` to send. A few moments later the email should arrive. + +To accept email over SMTP, mozy on over to `/etc/postfix/master.cf` and +uncomment the submission service. You're looking for something like this: + +``` +127.0.0.1:submission inet n - n - - smtpd +# -o syslog_name=postfix/submission +# -o smtpd_tls_security_level=encrypt +# -o smtpd_sasl_auth_enable=yes +# -o smtpd_tls_auth_only=yes +# -o smtpd_reject_unlisted_recipient=no +# -o smtpd_client_restrictions=$mua_client_restrictions +# -o smtpd_helo_restrictions=$mua_helo_restrictions +# -o smtpd_sender_restrictions=$mua_sender_restrictions +# -o smtpd_recipient_restrictions= + -o smtpd_relay_restrictions=permit +# -o milter_macro_daemon_name=ORIGINATING +``` + +This will permit delivery via localhost on the submission port (587) to anyone +whose hostname is in `$mydestination`. A good old `postfix reload` later and you +should be able to send yourself an email with SMTP: + +``` +$ telnet 127.0.0.1 587 +Trying 127.0.0.1... +Connected to 127.0.0.1. +Escape character is '^]'. +220 homura ESMTP Postfix +EHLO example.org +250-homura +250-PIPELINING +250-SIZE 10240000 +250-VRFY +250-ETRN +250-ENHANCEDSTATUSCODES +250-8BITMIME +250-DSN +250 SMTPUTF8 +MAIL FROM:<sircmpwn@homura> +250 2.1.0 Ok +RCPT TO:<sircmpwn@homura> +250 2.1.5 Ok +DATA +354 End data with <CR><LF>.<CR><LF> +From: Drew DeVault <sircmpwn@homura> +To: Drew DeVault <sircmpwn@homura> +Subject: Hello world + +Hey there +. +250 2.0.0 Ok: queued as 8267416366B +QUIT +221 2.0.0 Bye +Connection closed by foreign host. +``` + +Pull up mutt again to read this. Any software which will be sending out mail and +speaks SMTP (for example, sr.ht) can be configured now. Last step is to set up +LTMP delivery to lists.sr.ht or any other software you want to process incoming +emails. I want most mail to deliver normally - I only want LTMP configured for +my lists.sr.ht test domain. I'll set up some transport maps for this purpose. In +`main.cf`: + +``` +local_transport = local:$myhostname +transport_maps = hash:/etc/postfix/transport +``` + +Then I'll edit `/etc/postfix/transport` and add these lines: + +``` +lists.homura.localdomain lmtp:unix:/tmp/lists.sr.ht-lmtp.sock +homura.localdomain local:homura +``` + +This will deliver mail normally to `$user@homura` (my hostname), but will +forward mail sent to `$user@lists.homura` to the Unix socket where the +[lists.sr.ht LMTP +server](https://git.sr.ht/~sircmpwn/lists.sr.ht/tree/lists-srht-lmtp) lives. + +Add the subdomain to `/etc/hosts`: + +``` +127.0.0.1 lists.homura.localdomain lists.homura +``` + +Run `postmap /etc/postfix/transport` and `postfix reload` and you're good to go. +If you have the lists.sr.ht daemon working, send some emails to +`~someone/example-list@lists.$hostname` and you should see them get picked up. diff --git a/content/blog/Losing-faith-in-America.md b/content/blog/Losing-faith-in-America.md @@ -0,0 +1,78 @@ +--- +date: 2016-11-05 +# vim: set tw=80 +layout: post +title: I'm losing faith in America +--- + +I recently quit my job at Linode and started looking for something else to do. +For the first time in my career, I'm seriously considering opportunities abroad. +Sorry for the politically charged post - I promise to get back to tech stuff +right away. + +![](https://imgs.xkcd.com/comics/canada.png) + +On November 8th, I'm going to step into the voting booth and will be presented +with the following options: + +- A criminal who cheated her way into a spot on the ballot +- An egotistical racist maniac + +The next president of the United States will probably be Hillary Clinton. I'm +sure I don't have to tell you how ridiculous this is. This is a person who has +pulled all of the stops to get her name on the ballot, including *voter fraud* +and disturbing amounts of corruption within the Democratic party. Not to +mention that she's probably going to start a war with Syria, mess with the +already fragile geopolitical relationship we have with Russia, and likely +deserves to be incarcerated for mishandling classified information. Say what you +will about the Republican party - at least Trump won his nomination fair and +square. Bonus: not voting for Hillary is sexist. + +Not that I'd prefer it if Trump wins. I have a free sandwich waiting for me at +the deli nearby if he doesn't win. He got his nomination fairly, but that +doesn't mean he deserves it. This is a guy with little political clout who is +incapable of handling international relations or commanding our military. He +staunchly advocates committing war crimes to deal with ISIS. He makes racist, +sweeping generalizations about anyone different from him. He's a misogynist. +Even worse, he's all of these things and seems to actually represent a fair +portion of his supporters. + +Neither of the independents are serious contenders, so I won't bother with why I +don't like them. They haven't earned my vote, either. + +Congress is composed of many of the same sort of people. Corrupt politicians who +answer to the checkbooks of lobbyists who work against the interests of the +American people for the sake of their own. We're facing climate change and our +politicians are taking money from rich fossil fuel lobbyists and damning our +species to extinction. The wealth gap between the rich and the poor grows deeper +and deeper as absurdly rich people get absurdly richer at the expense of the +poor and middle class - through the support of the politicians whose pockets +they've greased. Their excess wealth could pay for programs to improve our +failing infrastructure and provide hundreds of thousands of jobs in doing so. We +could provide free healthcare for all Americans too, if it wasn't for the +ongoing debate about whether or not being alive and healthy is a fundamental +human right - many thanks to the pharmaceutical interests for shaping this +debate to maximize their profits. It'd be less of a problem if many companies +weren't getting rich off of the ever widening waistlines of Americans, too. + +Mass surveillance remains in full effect even years after Snowden's revelations. +The ridiculous war on drugs keeps putting people behind bars for lifetimes for +victimless crimes to support the financial needs of private prisons and local +police departments, who themselves are now better armed than most militaries, +based on drug policies that have no basis in reality. 97% of trails end in plea +bargains instead of justice, and minimum sentences ensure these people spend +ridiculous amounts of time in prisons that punish them rather than rehabilitate +them into productive citizens. A judge will hold a defendant indefinitely in +prison without a conviction for refusing to disclose their disk encryption +password in accordance with their 5th amendment rights - though if many +political players had their way, encryption would be illegal anyway. + +There's a word for what America is: **corrupt**. What the fuck is going on in +this country? We aren't a representative democracy by any stretch of the +imagination. We have become an oligarchy. We are ruled by money. + +I love America, honestly. My whole family is here and I connect most with the +American people. We have an incredibly rich land and great cities full of great +innovators and interesting people. I hate that it's become what it is today. I +don't expect anywhere else to be perfect, but we should be ashamed of how we +look next to some other countries out there. diff --git a/content/blog/MSG_PEEK-is-more-common-than-you-think-CVE-2016-10229.md b/content/blog/MSG_PEEK-is-more-common-than-you-think-CVE-2016-10229.md @@ -0,0 +1,49 @@ +--- +date: 2017-04-13 +layout: post +title: MSG_PEEK is pretty common, CVE-2016-10229 is worse than you think +tags: [security] +--- + +I heard about [CVE-2016-10229](https://nvd.nist.gov/vuln/detail/CVE-2016-10229) +earlier today. In a nutshell, it allows for arbitrary code execution via UDP +traffic if userspace programs are using `MSG_PEEK` in their `recv` calls. I +quickly updated my kernels and rebooted any boxes where necessary, but when I +read the discussions on this matter I saw people downplaying this issue by +claiming `MSG_PEEK` is an obscure feature. + +I don't want to be a fear monger and I'm by no means a security expert but I +suspect that this is a deeply incorrect conclusion. If I understand this +vulnerability right you need to drop everything and update any servers running +a kernel &lt;4.5 *immediately*. `MSG_PEEK` allows a programmer using UDP to +read from the kernel's UDP buffer without consuming the data (so subsequent +reads will continue to read the same data). This immediately sounds to me like +a pretty useful feature that a lot of software might use, not an obscure one. + +I did quick search for software where `MSG_PEEK` appears in the source code +somewhere. This does not necessarily mean that it's exploitable, but should +certainly raise red flags. Here's a list of some notable software I found: + +* nginx +* haproxy +* curl +* gnutls +* jack2 +* lynx +* plex (and kodi/xbmc) +* busybox + +I also found a few things like programming languages and networking libraries +that you might expect to have MSG_PEEK if only to provide that functionality to +programmers leveraging them. I didn't investigate too deeply into whether or not +that was the case or if this software is using the feature in a less apparent +way, but in this category I found Python, Ruby, Node.js, smalltalk, octave, +libnl, and socat. I used searchcode.com to find these - [here's the full search +results](https://searchcode.com/?q=MSG_PEEK). + +Again, I'm not a security expert, but I'm *definitely* spooked enough to update +my shit and I suggest you do so as well. Red Hat, Debian, and Ubuntu are all +unaffected because of the kernel they ship. Note, however, that many cloud +providers do not let you choose your own kernel. This could mean that you are +affected even if you're running a distribution like Debian. Double check it - +use `uname -r` and update+reboot if necessary. diff --git a/content/blog/Mail-service-provider-recommendations.md b/content/blog/Mail-service-provider-recommendations.md @@ -0,0 +1,119 @@ +--- +date: 2020-06-19 +layout: post +title: Email service provider recommendations +tags: [email] +--- + +Email is important to my daily workflow, and I've built many tools which +encourage productive use of it for software development. As such, I'm often +asked for advice on choosing a good email service provider. Personally, I run +my own mail servers, but about a year ago I signed up for and evaluated many +different service providers available today so that I could make informed +recommendations to people. Here are my top picks, as well as the criteria by +which they were evaluated. + +Unfortunately, almost all mail providers fail to meet my criteria. As such, I +can only recommend two: Migadu and mailbox.org. + +# #1: Migadu + +[Migadu](https://www.migadu.com/en/index.html) is my go-to recommendation +for a mail service provider. + +**Advantages** + +- Migadu is a small company with strong values and no outside capital (i.e. + no profit-motivated external influence). Email support and a human being + answers, and their leadership is accessible if you have questions or feedback. +- Their pricing is based on bandwidth usage, and does not rely on artificial + scarcity like limited domain names or mailboxes. +- Has lots of features for your postmaster - you can treat it as a managed mail + server for your organization. + +**Disadvantages** + +- They have suffered from some outages in the past. The global mail system is + tolerant of such outages - you don't have to worry about messages being lost + if they were sent during an outage. Still, being unable to access your mail is + a problem. +- If you are on a trial account, they will put an advertisement into your email + signature. I don't think that it's ever appropriate for a mail service + provider to edit your outgoing emails for any reason, and certainly not to + advertise. + +Full disclosure: SourceHut and Migadu agreed to a consulting arrangement to +build their [new webmail system](https://git.sr.ht/~emersion/alps), which should +be going into production soon. However, I had evaluated and started recommending +Migadu prior to the start of this project, and I believe that Migadu fares well +under the criteria I give at the end of this post. + +# #2: mailbox.org + +[Mailbox.org](https://mailbox.org/en/) may be desirable if you wish to have a +more curated experience, and less hands-on access to postmaster-specific +features. + +**Advantages** + +- Excellent first-class support for PGP, and many other strong security and + privacy features are available. +- Was able to speak to the CEO directly to discuss my concerns and feedback, and + have my questions answered. Raised some bugs and they were fixed in short + order. + +**Disadvantages** + +- The interface is a little bit too JavaScript heavy for my tastes, and suffer + from some bugs and lack of polish. +- They are a German company serving mostly German customers - German text leaks + into the UI and documentation in some places. +- Completing a Google captcha is required to sign up. + +# Others + +Evaluated but not recommended: disroot, fastmail, posteo.de, poste.io, +protonmail, tutanota, riseup, cock.li, teknik, runbox, megacorp mail (gmail, +outlook, etc). + +# Criteria for a good mail service provider + +The following criteria are objective and non-negotiable: + +1. Support for open standards including IMAP and SMTP +2. Support for users who wish to bring their own domain + +This is necessary to preserve the user's ownership of their data by making it +accessible over open and standardized protocols, and their right to move to +another service provider by not fixing their identity to a domain name +controlled by the email provider. It is for these reasons that Posteo, +ProtonMail, and Tutanota are not considered suitable. + +The remaining criteria are subjective: + +1. Is the business conducted ethically? Are their incentives aligned with their + customers, or with their investors? +2. Is it sustainable? Can I expect them to be around in 10 years? 20? 30? +3. Do they make unfounded claims about security or privacy, or develop + techniques which ultimately rely on trusting them instead of supporting or + improving standards which rely on encryption?[^1] +4. If they make claims about privacy or security, do they explain the + limitations and trade-offs, or do they let you believe it's infallible? +5. Do you trust them with your personal data? What if they're compelled by law + enforcement? What is their government like?[^2] + +Bonus points: + +- What is their relationship with open source? +- Can I sign up without an existing email address? Is there a chicken and egg + problem here?[^3] +- How well do they handle plaintext email? Do they meet the criteria for + recommended clients at + [useplaintext.email](https://useplaintext.email/#implementation-recommendations)? + +If you represent a mail service provider which you believe meets this criteria, +please [send me an email](mailto:sir@cmpwn.com). + +[^1]: This also rules out ProtonMail and Tutanota, doubly damning them, especially because it provides an excuse for skipping IMAP and SMTP, which conveniently enables vendor lock-in. +[^2]: This rules out Fastmail because of their government (Australlia)'s hostile and subversive laws regarding encryption. +[^3]: Alarmingly rare, this one. It seems to be either this, or a captcha like mailbox.org does. I would be interested in seeing the use of client-side proof of work, or requiring someone to enter their payment details and successfully complete a charge instead. diff --git a/content/blog/March-2nd-1943.md b/content/blog/March-2nd-1943.md @@ -0,0 +1,55 @@ +--- +date: 2020-07-14 +title: March 2nd, 1943 +layout: post +tags: [time] +--- + +It's March 2nd, 1943. The user asks your software to schedule a meeting with +Acmecorp at "9 AM on the first Monday of next month". + +<pre> +<code> +[6:17:45] homura ~ $ cal -3 2 March 1943 + February 1943 March 1943 April 1943 +Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa + 1 2 3 4 5 6 1 <span style="background: black; color: white"> 2</span> 3 4 5 6 1 2 3 + 7 8 9 10 11 12 13 7 8 9 10 11 12 13 4 <span style="background: #666; color: white"> 5</span> 6 7 8 9 10 +14 15 16 17 18 19 20 14 15 16 17 18 19 20 11 12 13 14 15 16 17 +21 22 23 24 25 26 27 21 22 23 24 25 26 27 18 19 20 21 22 23 24 +28 28 29 30 31 25 26 27 28 29 30 +</code> +</pre> + +Right now, California is on Pacific Standard Time (PST) and Arizona is on +Mountain Standard Time (MST). On March 8th, California will transition to +Pacific Daylight Time (PDT), one hour ahead. Arizona does not observe DST, so +they'll stay behind. + +At least until April 1st &mdash; when the governor will sign an emergency order +moving the state to MDT, effective immediately. + +Back on March 2nd, you send an email to each participant telling them about the +meeting. One of them has their locale set to en_GB, so some of the participants +need to be sent "04/05/43" and some "05/04/43". + +A moment later, the user asks you to tell it the number of hours betweeen now +and the meeting they just scheduled. The subject of the meeting is purchasing +fuel for a machine that the user is now filling with enough fuel to last until +then. + +On the day of the meeting, the user drives to the Navajo reservation to conduct +some unrelated business, and has to attend the meeting by phone. The reservation +has been on daylight savings time since March 8th, by the way, they never stayed +behind with the rest of Arizona. The user expects the software to warn them 1 +hour prior to the meeting start. The border of the reservation is defined by a +river, which is slowly moving East.[^1] + +[^1]: Okay, that last bit isn't true. But imagine if it was! + +[The changelog for the IANA zoneinfo database](https://mm.icann.org/pipermail/tz-announce/) +is great, by the way, you should read it. +[Or subscribe](https://mm.icann.org/mailman/listinfo/tz-announce) to get it +periodically[^2] material delivered to your inbox! + +[^2]: But with what period? 😉 diff --git a/content/blog/Music-syncing-on-Android.md b/content/blog/Music-syncing-on-Android.md @@ -0,0 +1,113 @@ +--- +date: 2013-08-24 +title: Custom Music Syncing on Android +layout: post +tags: [mobile] +--- + +I have an HTC One, with CyanogenMod installed. I usually use Spotify, but I've been wanting to move away from it for a while. +The biggest thing keeping me there was the ease of syncing up with my phone - I added music on my PC and it just showed up +on my phone. + +So, I finally decided to make it work on my phone without Spotify. You might have success if you aren't using CyanogenMod, +but you definitely need to be rooted and you need to access a root shell on your phone. I was using `adb shell` to start with, +but it has poor terminal emulation. Instead, I ended up installing an SSH daemon on the phone and just using that. Easier to +use vim in such an enviornment. + +The end result is that a cronjob kicks off each hour on my phone and runs a script that uses rsync to sync up my phone's music +with my desktop's music. That's another thing - a prerequisite of this working is that you have to expose your music to the +outside world on an SSH server somewhere. + +I'll tell you how I got it working, then you can see if it works for you. It might take some effort on your part to tweak +these instructions to fit your requirements. + +## Sanity checks + +Get into your phone's shell and make sure you have basic things installed. You'll need to make sure you have: + +* bash +* cron +* ssh +* rsync + +If you don't have them, you can probably get them by installing busybox. + +## Setting up SSH + +We need to generate a key. I tried using ssh-keygen before, but it had problems with rsync on Android. Instead, we use +dropbearkey. Generate your key with `dropbearkey -t rsa -f /data/.ssh/id_rsa`. You'll see the public key echoed to stdout. +It's not saved anywhere for you, so grab it out of your shell and put it somewhere - namely, in the authorized_keys file +on the SSH server you plan to pull music from. + +At this point, you can probably SSH into the server you want to pull from. Run `ssh -i /data/.ssh/id_rsa <your server here>` +to double check. Note that this isn't just for fun - you need to do this to get your server into known_hosts, so we can +non-interactively access it. + +## Making Android more sane + +Now that this is working, we need to clean up a little before cron will run right. Android is only a "Linux" system in the +sense that `uname` outputs "Linux". It grossly ignores the FHS and you need to fix it a little. Figure out how to do a +nice init.d script on your phone. For my CyanogenMod install, I can add scripts to `/data/local/userlocal.d/` and they'll +be run at boot. Here's my little script for making Android a little more sane: + +```bash +#!/system/bin/sh +# Making /system rw isn't strictly needed +mount -o remount,rw /system +mount -o remount,rw / +ln -s /data/var /var +ln -s /system/bin /bin +ln -s /data/.ssh /.ssh +crond +``` + +## Update script and initial import + +The following is the script we'll use to update your phone's music library. + +```bash +#!/system/xbin/bash +# Syncs music between a remote computer and this phone +RHOST=<remote hostname> +EHOST=<fallback, I use this for connecting from outside my LAN> +RPORT=22 +RUSER=<username> +ID=/data/.ssh/id_rsa +RPATH=/path/to/your/remote/music +# Omit the final directory. On my setup, this goes to /sdcard/Music, and my remote is /home/sircmpwn/Music +LPATH=/sdcard + +echo $(date) >> /var/log/update-music.log + +rsync -ruvL --delete --rsh="ssh -p $RPORT -i $ID" $RUSER@$RHOST:$RPATH $LPATH >> /var/log/update-music-rsync-so.log 2>&1 +if [[ $? != 0 ]]; then + rsync -ruvL --delete --rsh="ssh -p $RPORT -i $ID" $RUSER@$EHOST:$RPATH $LPATH >> /var/log/update-music-rsync-so.log 2>&1 +fi +``` + +Save this script to `/data/updateMusic`, make it executable with `chmod +x /data/updateMusic`, then run the initial import +with `/data/updateMusic`. After a while, you'll have all your computer's music on your phone. Now, we just need to make it +update automatically. + +Note: I set up a couple of logs for you. `/var/log/update-music.log` has the timestamp of every time it did an update. Also, +`/var/log/update-music-rsync-so.log` has the output of rsync from each run. + +## Cron + +Finally, we need to set up a cronjob. If you followed the instructions so far (and if you're lucky), you should have everything +ready for cron. The biggest pain in my ass was getting cron to coorperate, but the init script earlier should take care of +that. Run `crontab -e` and write your crontab: + + 0 * * * * /data/updateMusic + +Nice and simple. Your phone will now sync up your music every hour, on the hour, with your home computer. Here are some +possible points for improvement: + +* Check wlan0 and only sync if it's up +* Log cron somewhere +* Alter the update script to do a little bit better about the "fallback" +* Sync more than just music + +After all of this, I now have a nice setup that syncs music to my phone so I can listen to it with Apollo. I might switch +away from Apollo, though, it's pretty buggy. [Let me know](mailto:sir@cmpwn.com) if you can suggest an alternative music +player, or if you get stuck working through this procedure yourself. diff --git a/content/blog/My-journey-from-MIT-to-GPL.md b/content/blog/My-journey-from-MIT-to-GPL.md @@ -0,0 +1,79 @@ +--- +date: 2019-06-13 +layout: post +title: My personal journey from MIT to GPL +tags: ["philosophy", "free software"] +--- + +As I got started writing open source software, I generally preferred the MIT +license. I actually made fun of the "copyleft" GPL licenses, on the grounds that +they are less free. I still hold this opinion today: the GPL license is less +free than the MIT license - but today, I believe this in a good way. + +If you haven't yet, I suggest reading the [MIT +license](https://opensource.org/licenses/MIT) - it's very short. It satisfies +the four essential freedoms guaranteed of [free +software](https://www.gnu.org/philosophy/free-sw.html): + +1. The right to use the software for any purpose. +2. The right to study the source code and change it as you please. +3. The right to redistribute the software to others. +4. The right to distribute your modifications to the software. + +The MIT license basically allows you to do whatever you want with the software. +It's one of the most hands-off options: "here's some code, you can do anything +you want with it." I favored this because I wanted to give users as much freedom +to use my software as possible. The GPL, in addition to being a [much more +complex tome to understand](https://www.gnu.org/licenses/gpl-3.0.html), is more +restrictive. The GPL forces you to use the GPL for derivative works as well. +Clearly this affords you less freedom to use the software. Obligations are the +opposite of freedoms. + +When I first got into open source, I was still a Windows user. As I gradually +waded deeper and deeper into the free software pond, I began to use Linux more +often[^1]. Even once I started using Linux as my daily driver, however, it took +a while still for the importance of free software to set in. But this +realization is inevitable, for a programmer immersed in Linux. It radically +changes your perspective when all of the software you use guarantees these four +freedoms. If I'm curious about how something works, I can usually be reading the +code within a few seconds. I can find the author's name and email in the git +blame and shoot them some questions. And when I find a bug, I can fix it and +send them a patch. + +[^1]: Fun fact: the first time I used Linux was as a teenager, in order to get around the internet filtering software my parents had installed on our Windows PC at home. + +The weight of these possibilities did not occur to me immediately, instead +slowly becoming evident over time. Today, this cycle is almost muscle memory. +Pulling down source, grepping for files related to an itch I need to scratch, +compiling and installing the modified version, and sending my work upstream - +it's become second nature to me. These days, on the rare occasion that I run +into some proprietary software, this all grinds to a halt. It's like miscounting +the number of steps on your staircase in the dark. These moments drive the truth +home: Free software is good. It's starkly better than the alternative. And +copyleft defends it. Now that I've had a taste, you bet your ass I'm not going +to give it up. + +As the number of hours I've spent on FOSS projects grew from tens of hours, to +hundreds, to thousands and tens of thousands, I've learned that the effort I +sink into my work far outstrips the effort required to reuse my work. The +collective effort of the free software community amounts to tens of millions of +hours of work, which you can download at touch of a button, for free. If the +people with their fingers on that button held these same ideals, we wouldn't +need the GPL. The reality, however, is that we live in a capitalist world. Our +socialist free software utopia is ripe for exploitation by capitalists, and +they'll be rewarded for doing so. Capitalism is about enriching yourself - not +enriching your users and certainly not enriching society. + +Your parents probably taught you about the Golden Rule when you were young: do +unto others as you would have them do unto you. The GPL is the legal embodiment +of this Golden Rule: in exchange for benefiting from my hard work, you just have +to extend me the same courtesy. Its the unfortunate acknowledgement that we've +created a society that incentivises people to forget the Golden Rule. I give +people free software because I want them to reciprocate with the same. That's +really all the GPL does. Its restrictions just protect the four freedoms in +derivative works. Anyone who can't agree to this is looking to exploit your work +for their gain - and definitely not yours. + +I don't plan on relicensing my historical projects, but my new projects have +used the GPL family of licenses for a while now. I think you should seriously +consider it as well. diff --git a/content/blog/My-lets-encrypt-setup.md b/content/blog/My-lets-encrypt-setup.md @@ -0,0 +1,128 @@ +--- +date: 2018-06-27 +title: A quick review of my Let's Encrypt setup +layout: post +tags: [encryption] +--- + +Let's Encrypt makes TLS much easier for pretty much everyone, but can still +be annoying to use. It took me a while to smooth over the cracks in my Let's +Encrypt configuration across my (large) fleet of different TLS-enabled services. +I wanted to take a quick moment to share setup with you. + +2020-01-02 update: acme-client is unmaintained and caught the BSD disease +anyway. I use [uacme](https://github.com/ndilieto/uacme) and my current +procedure is documented on my [new server checklist](/new-server.html). It might +not be exactly applicable to your circumstances, YMMV. + +The main components are: + +- [acme-client](https://kristaps.bsd.lv/acme-client/) +- nginx +- cron + +nginx and cron need no introduction, but acme-client deserves a closer look. The +acme client blessed by Let's Encrypt is [certbot](https://certbot.eff.org/), but +BOY is it complicated. It's a big ol' pile of Python and I've found it fragile, +complicated, and annoying. The goal of maintaining your nginx and apache configs +for you is well intentioned but ultimately useless for advanced users. The +complexity of certbot is through the roof, and complicated software breaks. + +I bounced between alternatives for a while but when I found acme-client, it +totally clicked. This one is written in C with minimal dependencies (LibreSSL +and libcurl, no brainers IMO). I bring a statically linked acme-client binary +with me to new servers and setup time approaches zero as a result. + +I use nginx to answer challenges (and for some services, to use the final +certificates for HTTPS - did you know you can use Let's Encrypt for more +protocols than just HTTPS?). I quickly `mkdir -p +/var/www/acme/.well-known/acme-challenge`, make sure nginx can read it, and add +the following rules to nginx to handle challenges: + +```nginx +server { + listen 80; + listen [::]:80; + server_name example.org; + + location ^~ /.well-known/acme-challenge { + alias /var/www/acme; + } +} +``` + +If I'm not using the certificates for HTTPS, this is all I need. But assuming I +have some kind of website going, the full configuration usually looks more like +this: + +```nginx +server { + listen 80; + listen [::]:80; + server_name example.org; + + location / { + return 302 https://$server_name$request_uri; + } + + location ^~ /.well-known/acme-challenge { + alias /var/www/acme; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name example.org; + + ssl_certificate /etc/ssl/acme/$server_name/fullchain.pem; + ssl_certificate_key /etc/ssl/acme/$server_name/privkey.pem; + + location ^~ /.well-known/acme-challenge { + alias /var/www/acme; + } + + # ...application specific rules... +} +``` + +This covers the nginx side of things. To actually do certificate negotiation, I +have a simple script I carry around: + +```sh +exec >>/var/log/acme 2>&1 +date + +acme() { + site=$1 + shift + acme-client -vNn \ + -c /etc/ssl/acme/$site/ \ + -k /etc/ssl/acme/$site/privkey.pem \ + $site $* +} + +acme example.org subd1.example.org subd2.example.org + +nginx -s reload +``` + +The first two lines set up a log file in `/var/log/acme` I can use to debug any +issues that arise. Then I have a little helper function that wires up +acme-client the way I like it, and I can call it for each domain I need certs +for on this server. The last line changes if I'm doing something other than +HTTPS with the certs (for example, `postfix reload`). + +One gotcha is that acme-client will bail out if the directories don't exist when +you run it, so a quick `mkdir -p /etc/ssl/acme/example.org` when adding new +sites is necessary + +The final step is a simple cron entry that runs the script daily: + +```cron +0 0 * * * /usr/local/bin/acme-update-certs +``` + +It's that easy. It took me a while to get a Let's Encrypt setup that was simple +and satisfactory, but I believe I've settled on this one. I hope you find it +useful! diff --git a/content/blog/My-weird-branchless-git-workflow.md b/content/blog/My-weird-branchless-git-workflow.md @@ -0,0 +1,70 @@ +--- +date: 2020-04-06 +title: My unorthodox, branchless git workflow +layout: post +--- + +I have been using git for a while, and I took the time to learn about it in +great detail. Equipped with an understanding of its internals and a comfortable +familiarity with tools like [git rebase](https://git-rebase.io) &mdash; and a +personal, intrinsic desire to strive for minimal and lightweight solutions +&mdash; I have organically developed a workflow which is, admittedly, somewhat +unorthodox. + +In short, I use git branches very rarely, preferring to work on my local master +branch almost every time. When I want to work on multiple tasks in the same +repository (i.e. often), I just... work on all of them on master. I waste no +time creating a new branch, or switching to another branch to change contexts; I +just start writing code and committing changes, all directly on master, +intermixing different workstreams freely.[^1] This reduces my startup time to +zero, both for starting new tasks and revisiting old work. + +[^1]: I will occasionally use `git add -p` or even just `git commit -p` to quickly separate any changes in my working directory into separate commits for their respective workstreams, to make my life easier later on. This is usually the case when, for example, I have to fix problem A before I can address problem B, and additional issues with problem A are revealed by my work on problem B. I just fix them right away, `git commit -p` the changes separately, then file each commit into their respective patchsets later. + +When I'm ready to present some or all of my changes to upstream, I grab git +rebase and reorganize all of these into their respective features, bugfixes, and +so on, forming a series of carefully organized, self-contained patchsets. When I +receive feedback, I just start correcting the code right away, then fixup the +old commits during the rebase. Often, I'll bring the particular patchset I'm +ready to present upstream to the front of my master branch at the same time, for +convenient access with [git send-email](https://git-send-email.io). + +I generally set my local master branch to track the remote master branch,[^2] so +I can update my branch with `git pull --rebase`.[^3] Because all of my +work-in-progress features are on the master branch, this allows me to quickly +address any merge conflicts with upstream for *all* of my ongoing work at once. +Additionally, by keeping them all on the same branch, I can be assured that my +patches are mutually applicable and that there won't be any surprise conflicts +in feature B after feature A is merged upstream. + +[^2]: "What?" Okay, so in git, you have *local* branches and *remote* branches. The default behavior is reasonably sane, so I would forgive you for not noticing. Your local branches can *track* remote branches, so that when you `git pull` it automatically updates any local *tracking branches*. `git pull` is actually equivalent to doing `git fetch` and then `git merge origin/master` assuming that the current branch (your *local* master) is *tracking* `origin/master`. `git pull --rebase` is the same thing, except it uses `git rebase` instead of `git merge` to update your local branch. +[^3]: In fact, I have `pull.rebase = true` in my git config, which makes `--rebase` the default behavior. + +If I'm working on my own projects (where I can push to upstream master), I'll +still be working on master. If I end up with a few commits queued up and I need +to review some incoming patches, I'll just apply them to master, rebase them +behind my WIP work, and then use `git push origin HEAD~5:refs/heads/master` to +send them upstream, or something to that effect.[^4] Bonus: this instantly +rebases my WIP work on top of the new master branch. + +[^4]: "What?" Okay, so `git push` is shorthand for `git push origin master`, if you have a tracking branch set up for your local master branch to `origin/master`. But this itself is also shorthand, for `git push <remote> <local>:<remote>`, where `<local>` is the local branch you want to push, and `<remote>` is the remote branch you want to update. But, remember that branches are just references to commits. In git, there are other ways to reference commits. `HEAD~5`, for example, gets the commit which is 5 commits earlier than `HEAD`, which is the commit you have checked out right now. So `git push origin HEAD~5:refs/for/master` updates the `origin`'s `refs/for/master` reference (i.e. the master branch) to the local commit at `HEAD~5`, pushing any commits that upstream master doesn't also have in the process. + +This workflow saves me time in several ways: + +- No time spent creating new branches for new features. +- No time spent switching between branches to address feedback. +- All of my features are guaranteed to be mutually applicable to master, saving + me time addressing conflicts. +- Any conflicts with upstream are addressed in all of my workstreams at once, + without switching between branches or allowing any branch to get stale. + +I know that lightweight branches are one of git's flagship features, but I don't +really use them. I know it's weird, sue me. + +Sometimes I do use branches, though, when I know that a workstream is going to +be a lot of work &mdash; it involves lots of large-scale refactoring, or will +take several weeks to complete. This isolates it from my normal workflow on +small-to-medium patches, acknowledging that the large workstream is going to be +more prone to conflicts. By addressing these separately, I don't waste my time +fixing up the error-prone branch all the time while I'm working on my smaller +workstreams. diff --git a/content/blog/NewPipe-represents-the-best-of-FOSS.md b/content/blog/NewPipe-represents-the-best-of-FOSS.md @@ -0,0 +1,50 @@ +--- +date: 2019-04-02 +layout: post +title: NewPipe represents the best of FOSS +tags: ["philosophy", "free software"] +--- + +[NewPipe](https://newpipe.schabi.org/) is a free and open-source Android +application for browsing & watching YouTube. In my opinion, NewPipe is a perfect +case-study in why free & open source software is great and how our values differ +from proprietary software in important ways. There's one simple reason: it's +better than the proprietary YouTube app, in every conceivable way, for free. + +NewPipe is better because it's user-centric software. It exists to make its +users lives better, not to enrich its overseers. Because of this, NewPipe has +many features which are deliberately omitted from the proprietary app, such as: + +- No advertisements[^1] +- Playing any video in the background +- Downloading videos (or their audio tracks alone) to play offline +- Playing videos in a pop-up player +- Subscribing to channels without a YouTube account +- Importing and exporting subscriptions +- Showing subscriptions in chronological order +- It supports streaming services other than YouTube![^2] + +[^1]: Support your content creators with tools like Liberapay and Patreon! +[^2]: At least in theory... basic SoundCloud support is working and more services are coming soon. + +YouTube supports some of these... for $12/month. Isn't that a bit excessive? +Other features it doesn't support at all. On top of that, YouTube is constantly +gathering data about you and making decisions which put their interests ahead of +yours, whereas NewPipe never phones home and consistently adds new features that +put users first. The proprietary app is *exploitative* of users, and NewPipe is +*empowering* users. + +There are a lot of political and philosophical reasons to use & support free and +open source software. Sometimes it's hard to get people on board with FOSS by +pitching them these first. NewPipe is a great model because it's straight up +*better*, and better for reasons that make these philosophical points obvious +and poignant. The NewPipe project was started by [Christian +Schabesberger][christian], is co-maintained by a [team of 6][maintainers], and +has been contributed to by over [300 people][contributors]. You can donate +[here][donate]. NewPipe represents the best of our community. Thanks! +<img src="/img/heart.png" style="display: inline; width: 1.2rem; top: 0.2rem; position: relative" /> + +[christian]: https://schabi.org/ +[maintainers]: https://github.com/orgs/TeamNewPipe/people +[contributors]: https://github.com/TeamNewPipe/NewPipe/graphs/contributors +[donate]: https://newpipe.schabi.org/donate/ diff --git a/content/blog/Open-letter-to-Senator-Casey.md b/content/blog/Open-letter-to-Senator-Casey.md @@ -0,0 +1,63 @@ +--- +date: 2020-03-07 +title: An open letter to Senator Bob Casey on end-to-end encryption +layout: post +--- + +To Senator Bob Casey, I'm writing this open letter. + +As your constituent, someone who voted for you in 2018, and an expert in +software technology, I am disappointed in your support of the EARN IT Act. I am +aware that encryption is a challenging technology to understand, even for us +software engineers, and that it raises difficult problems for the legislature. +The EARN IT Act does not protect our children, and it has grave implications for +the freedoms of our citizens. + +The mathematics underlying strong end-to-end encryption have been proven to be +unbreakable. Asking service providers to solve them or stop using it is akin to +forcing us to solve time travel or quit recording history. Banning the use of a +technology without first accomplishing a sisyphean task is equivalent to banning +the technology outright. Ultimately, these efforts are expensive and futile. The +technology necessary to implement unbreakable encryption can be described +succinctly on a single 8.5"x11" sheet of paper. I would be happy to send such a +paper to your office, if you wish. The cat is out of the bag: encryption is not +a secret, and its use to protect our citizens is a widespread industry standard. +Attempting to ban it is equivalent to trying to ban algebra or trigonometry. + +Citizen use of end-to-end encryption is necessary to uphold our national +security. One way that child abuse material is often shared is via the Tor +secure internet network. This system utilizes strong end-to-end encryption to +secure the communications of its users, which makes it well-suited to hiding +the communications of child abusers. However, the same guarantees that enable +the child abusers to securely share materials are also essential for +journalists, activists, watchdog groups - and for our national security. The +technology behind Tor was designed by the US Navy and DARPA and the ability for +the public to use it to secure their communications is essential to the +network's ability to delivery on its national security guarantees as well. + +Protecting our children is important, but this move doesn't help. Breaking +end-to-end encryption is no substitute for good police work and effective +courts. Banning end-to-end encryption isn't going to make it go away - the +smart criminals are still going to use it to cover their tracks, and law +enforcement still needs to be prepared to solve cases with strong encryption +involved. Even on the Tor network, where strong end-to-end encryption is +utilized, many child abusers have been caught and brought to justice thanks to +good investigative work. It's often difficult to conduct an investigation within +the limits of the law and with respect to the rights of our citizens, but it's +necessary for law enforcement to endure this difficulty to protect our freedom. + +End-to-end encryption represents an important tool for the preservation of our +fundamental rights, as enshrined in the bill of rights. Time and again, our +alleged representatives levy attacks on this essential technology. It doesn't +get any less important each time it's attacked - rather, the opposite seems to +be true. On the face of it, the EARN IT Act appears to use important and morally +compelling problems of child abuse as a front for an attack on end-to-end +encryption. Using child abuse as a front to attack our fundamental right to +privacy is reprehensible, and I'm sure that you'll reconsider your position. + +As freedom of the press is an early signal for the failure of democracy and rise +of tyranny, so holds for the right to encrypt. I am an American, I am free to +speak my mind. I am free to solve a simple mathematical equation which +guarantees that my thoughts are shared only with those I choose. The right to +private communications is essential to a functioning democracy, and if you claim +to represent the American people, you must work to defend that right. diff --git a/content/blog/Patches-welcome.md b/content/blog/Patches-welcome.md @@ -0,0 +1,61 @@ +--- +date: 2019-01-01 +layout: post +title: Patches welcome +tags: ["philosophy", "free software"] +--- + +Happy new year! This is always a weird "holiday" for me, since all of the fun +happened last night. Today is just kind of... I guess a chance for everyone to +sober up before work tomorrow? It does tend to invite a sense of reflection and +is the ideal time to plan for the year ahead. One of my goals in 2019 is to +change more people's thinking about the open source community and what it means +to count among their number. + +I think there's a certain mode of thinking which lends itself to a more +productive free software community and a happier free software contributor. Free +software is not *theirs* - it's *ours*. Linux doesn't belong to Linus Torvalds. +Firefox doesn't belong to Mozilla, vim doesn't belong to Bram Moolenaar, and +ffmpeg doesn't belong to Fabrice Bellard. These projects belong to everyone. +That includes you! In this way, we reap the benefits of open source, but we also +shoulder the responsibilities. I'm not referring to some abstract sense of +reponsibility, but the tangible ones, like fixing bugs or developing new +features. + +One of the great things about this community is how easy it is to release your +software under a FOSS license. You have no obligations to the software once it's +released, except the obligations you hold yourself to (i.e. "if this software +makes my computer work, and I want to use my computer, I need to keep this +software in good working order"). It's important for users to remember that +they're not entitled to anything other than the rights laid out in the license, +too. You're not entitled to bug fixes or new features - you're *empowered* by +free software to make those changes yourself. + +Sometimes, when working on sway, someone says something like "oh, it's a bug in +libwayland". My response is generally along the lines of "I guess you're writing +a libwayland patch then!" The goal hasn't changed, only the route. It's no +different from being in the weeds and realizing you need to do some refactoring +first. If a problem in some FOSS project, be it a bug or a conspicuously missing +feature, is in the way of your goals, it's *your problem*. A friend of mine +recently said of a missing feature in a project they have nothing to do with: +"adding FreeBSD 12 support is not yet done, but it's on my todo list." I thought +that perfectly embodied the right way to think about FOSS. + +When applying this philosophy, you may occasionally have to deal with an +absentee maintainer or a big old pile of legacy spaghetti code. Fork it! Rewrite +it! These are tough marbles but they're the marbles you've gotta deal with. It's +not as hard as it looks. + +The entire world of free software is your oyster. Nothing is off-limits: if it's +FOSS, you can work on it. Try not to be intimidated by unknown programming +languages, unfamiliar codebases, or a lack of time. You'll pick up the new +language sooner than you think[^1], all projects are similar enough when you get +down to it, and small amounts of work done infrequently adds up over a long +enough time period. FOSS doesn't have to move quickly, it just has to keep +moving. The Dawn spacecraft accelerated at 0.003 cm/s<sup>2</sup> and made it to +[another world][ceres][^2]. + +[ceres]: https://upload.wikimedia.org/wikipedia/commons/a/a1/PIA19547-Ceres-DwarfPlanet-Dawn-RC3-AnimationFrame25-20150504.jpg + +[^1]: Especially if you have a reason to learn it, like this bug you need to fix +[^2]: Actually, it visited 3. diff --git a/content/blog/Phone-maintenance.md b/content/blog/Phone-maintenance.md @@ -0,0 +1,64 @@ +--- +date: 2017-11-24 +layout: post +title: On taking good care of your phone +tags: [mobile] +--- + +I just finished [replacing the micro-USB +daughterboard](https://www.ifixit.com/Guide/s5/27077) on my Samsung Galaxy S5, +which involved taking the phone most of the way apart, doing the replacement, +and putting it back together. This inspired me to write about my approach to +maintaining my cell phone. I've had this phone for a while and I have no plans +to upgrade - I backed the upcoming Purism phone, but I expect to spend +months/years on the software before I'll be using that as my daily driver. + +I don't want to be buying a new phone every year. That's a lot of money! Though +the technophile in me finds the latest and greatest technology appealing, the +thought of doing my own repairs and upkeep on a battle-tested phone is equally +interesting. Here are the four things I've found most important in phone upkeep. + +### Install LineageOS or Replicant + +Before I installed CyanogenMod when I bought this phone, I did some prying into +the stock ROM to see just how bad it was. It was even worse than I expected! +There were literally hundreds of apps and services with scary permissions +running in the background that could not be removed. These spy on you, wear down +your battery, and slow down your phone over time - another form of planned +obsolescence. + +My phone is still as fast as the day I got it. It does a great job with +everything I ask it to do. The first thing you should do with every new phone is +install a third-party ROM - ideally, without Google apps. Stock ROMs suck, get +rid of it. + +### Insist on a user-replacable battery + +Non-user-replacable batteries are an obvious form of planned obsolescence. +Batteries don't last forever and you should *never* buy a phone that you +cannot replace the battery of. A new battery for my S5 costs 10 bucks. 4 years +in, I've replaced mine once and I can hold a charge fine for a couple of days. + +### Get a case + +This one is pretty obvious, but I didn't follow this advice at first. I've never +broken a screen, so I didn't bother with a case. When I decided I was going to +keep this phone for a long time, I went ahead and bought one. It doubles the +thickness of my phone but at least I can be sure I'm not going to bust it up +when I drop it. It still fits in my pocket comfortably so it's no big deal. + +### Attempt repairs before you buy a new phone + +The past couple of months, my phone's micro-USB3 port started to act up a bit. I +would have to wiggle the cable a bit to get it to take, and it could stop +charging if I rustled my desk the wrong way. I got a replacement USB +daughterboard on Amazon for 6 bucks. Replacing it took an hour, but when +removing the screen I broke the connection between my home button and my +motherboard - which was only 10 bucks for the replacement, including same day +shipping. The whole process was a lot easier than I thought it would be. + +--- + +Be a smart consumer when you're buying a phone. Insist on the replacable battery +and maybe read the iFixit teardown. Take good care of it and it'll last a long +time. Don't let consumerism get the better of you! diff --git a/content/blog/PinePhone-review.md b/content/blog/PinePhone-review.md @@ -0,0 +1,138 @@ +--- +date: 2019-12-18 +layout: post +title: PinePhone review +tags: [review, mobile] +--- + +**tl;dr**: Holy shit! This is the phone I have always wanted. I have never been +this excited about the mobile sector before. However: the software side is +totally absent &mdash; phone calls are very dubious, SMS is somewhat dubious, +LTE requires some hacks, and everything will have to be written from the ground +up. + +I have a PinePhone developer edition model, which I paid for out of pocket[^1] +and which took an excruciatingly long time to arrive. When it finally arrived, +it came with no SIM or microSD card (expected), and the eMMC had some half-assed +version of Android on it which just boot looped without POSTing to anything +useful[^2]. This didn't bother me in the slightest &mdash; like any other +computer I've purchased, I planned on immediately flashing my own OS on it. My +Linux distribution of choice for it is +[postmarketOS](https://postmarketos.org/), which is basically the mobile OS I'd +build if I wanted to build a mobile OS. + +[^1]: In other words, no one paid me to or even asked me to write this review. + +[^2]: I understand that the final production run of the PinePhone is going to ship with postmarketOS or something. + +Let me make this clear: **right now, there are very few people, perhaps only +dozens, for whom this phone is the right phone, given the current level of +software support**. I am not using it as my daily driver, and I won't for some +time. The only kind of person I would recommend this phone to is a developer who +believes in the phone and wants to help build the software necessary for it to +work. However, it seems to me that all of the right people *are* working on the +software end of this phone &mdash; everyone I'd expect from the pmOS community, +from KDE, from the kernel hackers &mdash; this phone has an unprecedented level +of community support and the software *will* be written. + +So, what's it actually like? + +<details> + <summary>Expand for a summary of the specs</summary> + <p> + The device is about + <abbr title="The thickness of a GameBoy cartridge">1 cm thick</abbr> + and weighs + <abbr + title="The weight of one GameBoy Color, with batteries, without cartridge" + >188 grams</abbr>. The screen is about 16 cm tall, of which 1.5 cm is bezel, + and <abbr + title="About the width and height of a GameBoy color, plus 1 inch of height" + >7.5 cm wide</abbr> (5 mm of bezel). The physical size and weight is very + similar to my daily driver, a Samsung Galaxy J7 Refine. It has a USB-C port, + which I understand can be reconfigured for DisplayPort, and a standard + headphone jack and speakers, both of which sound fine in my experience. The + screen is 720x1440, and looks about as nice as any other phone. It has + front- and back-facing cameras, which I've yet to get working (I understand + that someone has got them working at some point), plus a flash/lamp on the + back, and an <abbr + title="Note that the only values for R, G, and B that I've managed to get working are 0.0 and 1.0 each, for a total of 7 possible colors (including off)" + >RGB LED</abbr> on the front. + </p> + <p> + The eMMC is 16G and, side note, had <em>seventeen</em> partitions on it when + I first got the phone. 2G of RAM, 4 cores. It's not very powerful, but in my + experience it runs lightweight UIs (such as <a + href="https://swaywm.org">sway</a>) just fine. With very little effort by + way of power management, and with obvious power sinks left unfixed, the + battery lasts about 5 hours. + </p> +</details> + +In short, I'm quite satisfied with it, but I've never had especially strenuous +demands of my phone. I haven't run any benchmarks on the GPU, but it seems +reasonably fast and the open-source Lima driver supports GLESv2. The modem is +supported by [Ofono](https://01.org/ofono), which is a telephony daemon based on +dbus &mdash; however, I understand that we can just open `/dev/ttyUSB1` and talk +to the modem ourselves, and I may just write a program that does this. Using +Ofono, I have successfully spun up LTE internet, sent and received SMS messages, +and placed and answered phone calls - though the last one without working +audio. A friend from KDE, Bhushan Shah, is working on this and rumor has it that +a call has successfully been placed. I have not had success with MMS, but I +think it's possible. WiFi works. All of this with zero blobs and a kernel which +is... admittedly, pretty heavily patched, but [open +source](https://gitlab.com/pine64-org/linux) and making its way upstream.[^3] + +[^3]: The upstream kernel actually does work if you patch in the DTS, but WiFi doesn't work and it's not very stable. + +Of course, no one wants to place phone calls by typing a lengthy command into +their terminal, but that these features can be done in an annoying way means +that it's feasible to write applications that do this in a convenient way. For +my part, I have been working on some components of a mobile-friendly Wayland +compositor, based on Sway, which I'm calling Sway Mobile for the time being. I'm +not sure if Sway will actually stick around once it becomes difficult to bend to +my will (it's designed for keyboard-driven operation, after all), but I'm +building mobile shell components which will translate nicely to any other +wlroots-based compositors. + +The first of these is a simple app drawer, which I've dubbed +[casa](https://git.sr.ht/~sircmpwn/casa). I have a lot more stuff planned: + +- A new bar/notification drawer/quick action thing +- A dialer & call manager, maybe integrated with gnome-contacts +- A telephony daemon which records incoming SMS messages and pulls up the call + manager for incoming phone calls. Idea: write incoming SMS messages into a + Maildir. +- A new touch-friendly Wayland lock screen +- An on-screen keyboard program + +Here's a video showing casa in action: + +<video + src="https://yukari.sr.ht/casa.webm?cache-break" + style="max-width: 50%; margin: 0 auto; display: block" + autoplay loop muted > + Your browser does not support webm playback. Please choose a browser which + supports free and open standards. +</video> + +The latest version has 4 columns and uses the space a bit better. Also, in the +course of this work I put together the +[fdicons](https://gitlab.freedesktop.org/ddevault/fdicons) library, which may be +useful to some. + +I have all sorts of other small things to work on, like making audio behave +better and improving power management. I intend to contribute these tools to +postmarketOS upstream as a nice lightweight plug-and-play UI package you can +choose from when installing pmOS, either improving their existing +postmarketos-ui-sway meta-package or making something new. + +In conclusion: I have been waiting for this phone for years and years and years. +I have been hoping that someone would make a phone whose hardware was compatible +with upstream Linux drivers, and could *theoretically* be used as a daily driver +if only the software were up to snuff. I wanted this because I knew that the +free software community was totally capable of building the software for such a +phone, if only the hardware existed. This is actually happening &mdash; all of +the free software people I would hope are working on the PinePhone, are working +on the PinePhone. And it's only $150! I could buy four of them for the price of +the typical smartphone! And I just might! diff --git a/content/blog/Please-stop-using-slack.md b/content/blog/Please-stop-using-slack.md @@ -0,0 +1,149 @@ +--- +date: 2015-11-01 +# vim: tw=80 +layout: post +title: Please don't use Slack for FOSS projects +tags: [slack, irc, free software] +--- + +I've noticed that more and more projects are using things like Slack as the chat +medium for their open source projects. In the past couple of days alone, I've +been directed to Slack for Babel and Bootstrap. I'd like to try and curb this +phenomenon before it takes off any more. + +## Problems with Slack + +Slack... + +* is closed source +* has only one client (*update: errata at the bottom of this article*) +* is a walled garden +* requires users to have a different tab open for each project they want to be + involved in +* requires that Heroku hack to get open registration + +The last one is a real stinker. Slack is not a tool built for open source +projects to use for communication with their userbase. It's a tool built for +teams and it is ill-suited to this use-case. In fact, Slack has gone on record +as saying that it *cannot* support this sort of use-case: "it’s great that +people are putting Slack to good use" but unfortunately "these communities are +not something we have the capacity to support given the growth in our existing +business." [^1] + +## What is IRC? + +IRC, or Internet Relay Chat... + +* is a standardized and well-supported protocol [^2] +* has hundreds of open source clients, servers, and bots [^3] +* is a distributed design with several networks +* allows several projects to co-exist on the same network +* has no hacks for registration and is designed to be open + +### No, IRC is not dead + +I often hear that IRC is dead. Even my dad pokes fun at me for using a 30 year +old protocol, but not after I pointed out that he still uses HTTP. Despite the +usual shtick from the valley, old is not necessarily a synonym for bad. + +IRC has been around since forever. You may think that it's not popular anymore, +but there are still tons of people using it. There are 87,762 users *currently +online* (at time of writing) on Freenode. There are 10,293 people on OFTC. +22,384 people on Rizon. In other words, it's still going strong, and I put a lot +more faith in something that's been going full speed ahead since the 80s than in +a Silicon Valley fad startup. + +## Problems with IRC that Slack solves + +There are several things Slack tries to solve about IRC. They are: + +**Code snippets**: Slack has built-in support for them. On IRC you're just asked +to use a pastebin like Gist. + +**File transfers**: Slack does them. IRC also does them through XDCC, but this +can be difficult to get working. + +**Persistent sessions**: Slack makes it so that you can see what you missed when +you return. With IRC, you don't have this. If you want it, you can set up an IRC +bouncer like [ZNC](http://znc.in/). + +**Integrations**: with things like build bots. This was never actually a problem +with IRC. IRC has always been significantly better at this than Slack. There is +*definitely* an IRC client library for your favorite programming language, and +you can write your own client from scratch in a matter of minutes anyway. +There's an [IRC](https://github.com/nandub/hubot-irc) backend for Hubot, too. +GitHub has a built-in hook for announcing repository activity in an IRC channel. + +## Other projects are using IRC + +Here's a short, incomplete list of important FOSS projects using IRC: + +* Debian +* Docker +* Django +* jQuery +* Angular +* ReactJS +* NeoVim +* Node.js +* everyone else + +The list goes on for a while. Just fill in another few hundred bullet points +with your imagination. Seriously, just join `#<project-name>` on Freenode. It +probably exists. + +## IRC is better for your company, too + +We use IRC at [Linode](https://www.linode.com/), even for non-technical people. +It works great. If you want to reduce the barrier to entry for non-technicals, +set up something like [shout](https://github.com/erming/shout) instead. You can +also have a pretty no-brainer link to webchat on almost every network, [like +this](http://webchat.esper.net/?nick=&channels=truecraft). If you need file +hosting, you can deploy an instance of +[sr.ht](https://github.com/SirCmpwn/sr.ht/) or something like it. You can also +host IRC servers on your own infrastructure, which avoids leaving sensitive +conversations on someone else's servers. + +## Please use IRC + +In short, I'd really appreciate it if we all quit using Slack like this. It's +not appropriate for FOSS projects. I would much rather join your channel with +the client I already have running. That way, I'm more likely to stick around +after I get help with whatever issue I came to you for, and contribute back by +helping others as I idle in your channel until the end of time. On Slack, I +leave as soon as I'm done getting help because tabs in my browser are precious +real estate. + +[First discussion on Hacker News](https://news.ycombinator.com/item?id=10486541) + +[Second discussion on Hacker News](https://news.ycombinator.com/item?id=11013136) + +## Updates + +Addressing feedback on this article. + +**Slack IRC bridge**: Slack provides an IRC bridge that lets you connect to +Slack with an IRC client. I've used it - it's a bit of a pain in the ass to set +up, and once you have it, it's not ideal. They did put some effort into it, +though, and it's usable. I'm not suggesting that Slack as a product is worse +than IRC - I'm just saying that it's not better than IRC for FOSS projects, and +probably not that much better for companies. + +**Clients**: Slack has several clients that use the API. That being said, there +are fewer of them and for fewer platforms than IRC clients, and there are more +libraries around IRC than there are for Slack. Also, the bigger issue is that I +already have an IRC client, which I use for the hundreds of FOSS projects that +use IRC, and I don't want to add a Slack client for one or two projects. + +**Gitter**: Gitter is bad for many of the same reasons Slack is. Please don't +use it over IRC. + +**ircv3**: Check it out: [ircv3.net](http://ircv3.net) + +**irccloud**: Is really cool and solves all of the problems. [irccloud.com](https://www.irccloud.com/) + +**2018-03-12**: Slack is shutting down the IRC and XMPP gateways. + +[^1]: [Slack is quietly, unintentionally killing IRC - The Next Web](http://thenextweb.com/insider/2015/03/24/slack-is-quietly-unintentionally-killing-irc/) +[^2]: [RFC 1459](https://www.rfc-editor.org/rfc/rfc1459.txt) +[^3]: [Github search for IRC](https://github.com/search?o=desc&q=irc&s=stars&type=Repositories&utf8=%E2%9C%93) diff --git a/content/blog/Please-use-text-plain-for-emails.md b/content/blog/Please-use-text-plain-for-emails.md @@ -0,0 +1,138 @@ +--- +date: 2016-04-11 +# vim: tw=80 +title: Please use text/plain for email +layout: post +tags: [mail] +--- + +A lot of people have come to hate email, and not without good reason. I don't +hate using email, and I attribute this to better email habits. Unfortunately, +most email clients these days lead users into bad habits that probably +contribute to the sad state of email in 2016. The biggest problem with email is +the widespread use of HTML email. + +Compare email to snail mail. You probably throw out most of the mail you get - +it's all junk, ads. Think about the difference between snail mail you read and +snail mail you throw out. Chances are, the mail you throw out is flashy flyers +and spam that's carefully laid out by a designer and full of eye candy (kind of +like many HTML emails). However, if you receive a letter from a friend it's +probably going to be a lot less flashy - just text on a page. Reading letters +like this is pleasant and welcome. Emails should be more like this. + +I consider this the basic argument for plaintext emails - they make email +better. There are more specific problems with HTML emails that I can give, +though (not to mention the fact that I read emails [on +this](https://drewdevault.com/2016/03/22/Integrating-a-VT220-into-my-life.html) +now). + +## What's wrong with HTML email + +**Tracking images** are images that are included in HTML emails with &lt;img +/&gt; tags. These images have URLs with unique IDs in them that hit remote +servers and let them know that you opened the email, along with various details +about your mail client and such. This is a form of tracking, which many people +go to great lengths to prevent with tools like +[uBlock](https://github.com/gorhill/uBlock). Most email clients recognize this, +and actually *block* images from being shown without explicit user consent. If +your images aren't even being shown, then why include them? Tracking users is +evil. + +Many **vulnerabilities** in mail clients also stem from rendering HTML email. +Luckily, no mail clients have JavaScript enabled on their HTML email renderers. +However, security issues related to HTML emails are still found quite often in +mail clients. I don't want to view this crap (and I don't). + +HTML email also makes **phishing** much easier. I've often received HTML emails +with links that hide their true intent by using a different href than their text +would suggest (and almost always with a tracking code added, ugh). They are also +**incompatible** with many email-based workflows, such as inline quoting, +mailing list participation, and sending &amp; working with source code patches. + +## Good habits for plaintext emails + +Some nice things are possible when you choose to use plaintext emails. Remember +before when I was comparing emails to snail mail letters? Well, let's continue +those comparisons. Popular email clients of 2016 have thoroughly bastardized +email, but here's what it once was and perhaps what it could be today. + +The common mail client today uses the abhorrent "[top +posting](https://en.wikipedia.org/wiki/Posting_style#Top-posting)" format, where +the entire previous message is dumped underneath your reply. As the usual quote +goes: + +>A: Because it messes up the order in which people normally read text. +> +>Q: Why is top-posting such a bad thing? +> +>A: Top-posting. +> +>Q: What is the most annoying thing in e-mail? + +A better way to write emails is the same way you write a letter to send via +snail mail. Would you photocopy the entire history of your correspondence and +staple it to your response? After a while you would start paying more for the +weight! Though bandwidth seems cheap now, the habit is still silly. Instead of +copying the entire conversation into your email, quote only the relevant parts +and respond to them inline. For example, let's say I receive this email: + + Hi Drew! + + Could you take a look at the server this afternoon? I think it's having some + issues with nginx. + + I also took care of the upgrades you asked for last night. Sorry it took so + long! + + -- + John Doe + +The best way to respond to this would be: + + Hi John! + + >Could you take a look at the server this afternoon? I think it's having some + >issues with nginx. + + No problem. I just had a quick look now and nginx was busted. Should be + working now. + + >I also took care of the upgrades you asked for last night. Sorry it took so + >long! + + Thanks! + + -- + Drew DeVault + +John might follow up with this: + + >Should be working now. + + Yep, seems to be up. Thanks! + + -- + John Doe + +Much better if you ask me. This works particularly well on mailing lists for +open source projects, where you send a patch and reviewers will respond by +quoting specific parts of your patch and leaving feedback. Just treat emails +like letters! + +## Multipart emails + +I think there are nothing but negatives to HTML email. I use +[mutt](http://www.mutt.org/) to read email, which doesn't even render HTML +emails and allows me to compose emails with Vim. But if you absolutely insist on +using HTML emails, please use [multipart +emails](https://en.wikipedia.org/wiki/MIME#Multipart_messages). If you're +sending automated emails, your programming language likely contains a mechanism +to facilitate this. The idea is that you send an alternative text/plain body for +your email. Be sure that this body contains all of the information of the HTML +version. If you do this, I will at least be willing to read your emails. + +## How do I use plaintext emails? + +Your mail client should have an option for composing emails with plaintext. Look +through your settings for it and it'll change the default. Then you're free! +Tell your friends to do the same, and your email life will be happier. diff --git a/content/blog/Portability-matters.md b/content/blog/Portability-matters.md @@ -0,0 +1,81 @@ +--- +date: 2017-11-13 +layout: post +title: Portability matters +tags: [portability, philosophy] +--- + +There are many kinds of "portability" in software. Portability refers to the +relative ease of "porting" a piece of software to another system. That +platform might be another operating system, another CPU architecture, another +web browser, another filesystem... and so on. More portable software uses the +limited subset of interfaces that are common between systems, and less portable +software leverages interfaces specific to a particular system. + +Some people think that portability isn't very important, or don't understand the +degree to which it's important. Some people might call their software portable +if it works on Windows and macOS - they're wrong. They might call their software +portable if it works on Windows, macOS, and Linux - but they're wrong, too. +Supporting multiple systems does not necessarily make your software portable. +What makes your software portable is *standards*. + +The most important standard for software portability is POSIX, or the **Portable +Operating System Interface**. Significant subsets of this standard are supported +by many, many operating systems, including: + +- Linux +- *BSD +- macOS +- Minix +- Solaris +- BeOS +- Haiku +- AIX + +I [could go +on](https://en.wikipedia.org/wiki/POSIX#POSIX-oriented_operating_systems). +Through these operating systems, you're able to run POSIX compatible code on a +large number of CPU architectures as well, such as: + +- i386 +- amd64 +- ARM +- MIPS +- PowerPC +- sparc +- ia64 +- VAX + +Again, I could go on. Here's the point: by supporting POSIX, your software runs +on basically every system. *That's* what it means to be portable - standards. +So why is it important to support POSIX? + +First of all, if you use POSIX then your software runs on just about anything, +so lots of users will be available to you and it will work in a variety of +situations. You get lots of platforms for free (or at least cheap). But more +importantly, *new platforms* get your software for free, too. + +The current market leaders are not the end-all-be-all of operating system +design - far from it. What they have in their advantage is working well enough +and being incubent. Windows, Linux, and macOS are still popular for the same +reason that legislator you don't like keeps getting elected! However, new +operating systems have a fighting chance thanks to POSIX. All you have to do to +make your OS viable is implement POSIX and you will immediately open up +hundreds, if not thousands, of potential applications. Portability is important +for innovation. + +The same applies to other kinds of portability. Limiting yourself to standard +browser features gives new browsers a chance. Implementing standard networking +protocols allows you to interop with other platforms. I'd argue that failing to +do this is *unethical* - it's just another form of vendor lock-in. This is why +Windows does not support POSIX. + +This is also why I question niche programming languages like Rust when they +claim to be suited to systems programming or even kernel development. That's +simply not true when they only run on a small handful of operating systems and +CPU architectures. C runs on *literally* everything. + +In conclusion: use standard interfaces for your software. That guy who wants to +bring new life to that old VAX will thank you. The authors of +[servo](https://servo.org/) thank you. *You* will thank you when your +circumstances change in 5 years. diff --git a/content/blog/Porting-Alpine-Linux-to-RISC-V.md b/content/blog/Porting-Alpine-Linux-to-RISC-V.md @@ -0,0 +1,118 @@ +--- +date: 2018-12-20 +layout: post +title: Porting Alpine Linux to RISC-V +tags: ["alpine linux", "risc-v"] +--- + +I recently received my [HiFive Unleashed][hifiveu], after several excruciating +months of waiting, and it's incredibly cool. For those unaware, the HiFive +Unleashed is the first consumer-facing Linux-capable [RISC-V][riscv] hardware. +For anyone who's still lost, RISC-V is an [open](https://github.com/riscv), +royalty-free [instruction set +architecture](https://en.wikipedia.org/wiki/Instruction_set_architecture), and +the HiFive is an [open](https://github.com/sifive) CPU implementing it. And here +it is on my dining room table: + +[hifiveu]: https://www.sifive.com/boards/hifive-unleashed +[riscv]: https://en.wikipedia.org/wiki/RISC-V + +![](https://sr.ht/JMao.jpg) + +This board is *cool*. I'm working on making this hardware available to +[builds.sr.ht][srht] users in the next few months, where I intend to use it to +automate the remainder of the Alpine Linux port and make it available to any +other operating systems (including non-Linux) and userspace software which are +interested in working on a RISC-V port. I'm fairly certain that this will be the +first time hardware-backed RISC-V cycles are being made available to the public. + +[srht]: https://meta.sr.ht + +There are two phases to porting an operating system to a new architecture: +bootstrapping and, uh, porting. For lack of a better term. As part of +bootstrapping, you need to obtain a cross-compiler, port libc, and cross-compile +the basics. Bootstrapping ends once the system is *self-hosting*: able to +compile itself. The "porting" process involves compiling all of the packages +available for your operating system, which can take a long time and is generally +automated. + +The first order of business is the cross-compiler. RISC-V support landed in +binutils 2.28 and gcc 7.1 several releases ago, so no need to worry about adding +a RISC-V target to our compiler. Building both with +`--target=riscv64-linux-musl` is sufficient to complete this step. The other +major piece is the C standard library, or libc. Unlike the C compiler, this step +required some extra effort on my part - the RISC-V port of musl libc, which +Alpine Linux is based on, is a work-in-progress and has not yet been upstreamed. + +There does exist [a patch][musl-port] for RISC-V support, though it had never +been tested at a scale like this. Accordingly, I ran into several bugs, for +which I wrote several patches ([1][1] [2][2] [3][3]). Having a working distro +based on the RISC-V port makes a much more compelling argument for the maturity +of the port, and for its inclusion upstream, so I'm happy to have caught these +issues. Until then, I added the port and my patches to the Alpine Linux musl +package manually. + +[musl-port]: https://github.com/riscv/riscv-musl +[1]: https://github.com/riscv/riscv-musl/pull/2 +[2]: https://github.com/riscv/riscv-musl/pull/3 +[3]: https://github.com/riscv/riscv-musl/pull/4 + +A C compiler and libc implementation open the floodgates to porting a huge +volume of software to your platform. The next step is to identify and port the +essential packages for a self-hosting system. For this, Alpine has a great +[bootstrapping script][bootstrap.sh] which handles preparing the cross-compiler +and building the base system. Many (if not most) of these packages required +patching, tweaks, and manual intervention - this isn't a turnkey solution - but +it is an incredibly useful tool. The most important packages at this step are +the native toolchain[^1], the package manager itself, and various other useful +things like tar, patch, openssl, and so on. + +[bootstrap.sh]: https://git.alpinelinux.org/cgit/aports/tree/scripts/bootstrap.sh +[^1]: Meaning a compiler which both *targets* RISC-V and *runs* on RISC-V. + +Once the essential packages are built and the system can compile itself, the +long porting process begins. It's generally wise to drop the cross-compiler here +and start doing native builds, if your hardware is fast enough. This is a +tradeoff, because the RISC-V system is somewhat slower than my x86_64 bootstrap +machine - but many packages require lots of manual tweaks and patching to get +cross-compiling working. The time saved by not worrying about this makes up for +the slower build times[^2]. + +[^2]: I was actually really impressed with the speed of the HiFive Unleashed. The main bottleneck is the mmcblk driver - once you get files in the kernel cache things are quite pleasant and snappy. + +There are thousands of packages, so the next step for me (and anyone else +working on a port) is to automate the remainder of the process. For me, an +intermediate step is integrating this with builds.sr.ht to organize my own work +and to make cycles available to other people interested in RISC-V. Not all +packages are going to be ported for free - but many will! Once you unlock the +programming languages - C, Python, Perl, Ruby[^3], etc - most open source +software is pretty portable across architectures. One of my core goals with +sr.ht is to encourage portable software to proliferate! + +[^3]: I have all four of these now! + +If any readers have their own RISC-V hardware, or want to try it with qemu, I +have a RISC-V Alpine Linux repository here[^4]. Something like this will install +it to /mnt: + +```sh +apk add \ + -X https://mirror.sr.ht/alpine/main/ \ + --allow-untrusted \ + --arch=riscv64 \ + --root=/mnt \ + alpine-base alpine-sdk vim chrony +``` + +Run `/bin/busybox --install` and `apk fix` on first boot. This is still a work +in progress, so configuring the rest is an exercise left to the reader until I +can clean up the process and make a nice install script. Good luck! + +[^4]: [main](https://mirror.sr.ht/alpine/main/), [community](https://mirror.sr.ht/alpine/community/), [testing](https://mirror.sr.ht/alpine/testing/) + +--- + +Closing note: big thanks to the help from the community in #riscv on Freenode, +and to the hard work of the Debian and Fedora teams paving a lot of the way and +getting patches out there for lots of software! I still got to have all the fun +working on musl so I wasn't entirely on the beaten path :) diff --git a/content/blog/Porting-an-entire-toolchain-to-the-browser-with-emscripten.md b/content/blog/Porting-an-entire-toolchain-to-the-browser-with-emscripten.md @@ -0,0 +1,366 @@ +--- +date: 2014-11-30 +# vim: tw=80 +layout: post_toolchain +title: Porting an assembler, debugger, and more to WebAssembly +tags: [wasm, javascript, KnightOS] +--- + +WebAssembly is pretty cool! It lets you write portable C and cross-compile it to +JavaScript so it'll run in a web browser. As the maintainer of +[KnightOS](http://www.knightos.org), I looked to WASM as a potential means +of reducing the cost of entry for new developers hoping to target the OS. + +<noscript> +Note: this article uses JavaScript to run all of this stuff in your web browser. +I don't use any third-party scripts, tracking, or anything else icky. +</noscript> + +## Rationale for WASM + +There are several pieces of software in the toolchain that are required to write +and test software for KnightOS: + +* [scas](https://github.com/KnightOS/scas) - a z80 assembler +* [genkfs](https://github.com/KnightOS/genkfs) - generates KFS filesystem images +* [kpack](https://github.com/KnightOS/kpack) - packaging tool, like makepkg on Arch Linux +* [z80e](https://github.com/KnightOS/z80e) - a z80 calculator emulator + +You also need a copy of the latest kernel and any of your dependencies from +[packages.knightos.org](https://packages.knightos.org). Getting all of this is +not straightforward. On Linux and Mac, there are no official packages for any of +these tools. On Windows, there are still no official packages, and you have to +use Cygwin on top of that. The first step to writing KnightOS programs is to +manually compile and install several tools, which is a lot to ask of someone who +just wants to experiment. + +All of the tools in our toolchain are written in C. We saw WASM as an +opportunity to reduce all of this effort into simply firing up your web browser. +It works, too! Here's what was involved. + +>**Note**: Click the screen on the emulator to the left to give it your +>keyboard. Click away to take it back. You can use your arrow keys, F1-F5, +>enter, and escape (as MODE). + +## The final product + +Let's start by showing you what we've accomplished. It's now possible for +curious developers to try out KnightOS programming in their web browser. Of +course, they still have to do it in assembly, but we're [working on +that](https://github.com/KnightOS/kcc) 😉. Here's a "hello world" you can run in +your web browser: + +<div class="demo"> + <div class="editor" + data-source="/sources/helloworld.asm" + data-file="main.asm"></div> + <div class="calculator-wrapper"> + <div class="calculator"> + <canvas width="385" height="256" class="emulator-screen"></canvas> + </div> + </div> +</div> + +We can also install new dependencies on the fly and use them in our programs. +Here's another program that draws the "hello world" message in a window. You +should install `core/corelib` first: + +<input type="text" id="package-name" value="core/corelib" /> +<input type="button" id="install-package" value="Install" /> + +<div class="demo"> + <div class="editor" data-source="/sources/corelib-hello.asm" data-file="main.asm"></div> + <div class="calculator-wrapper"> + <div class="calculator"> + <canvas width="385" height="256" class="emulator-screen"></canvas> + </div> + </div> +</div> + +You can find more packages to try out on +[packages.knightos.org](https://packages.knightos.org). Here's another example, +this one launches the file manager. You'll have to install a few packages for it +to work: + +Install: +<input type="button" class="install-package-button" data-package="extra/fileman" value="extra/fileman" /> +<input type="button" class="install-package-button" data-package="core/configlib" value="core/configlib" /> +<input type="button" class="install-package-button" data-package="core/corelib" value="core/corelib" /> + +<div class="demo"> + <div class="editor" data-source="/sources/fileman.asm" data-file="main.asm"></div> + <div class="calculator-wrapper"> + <div class="calculator"> + <canvas width="385" height="256" class="emulator-screen"></canvas> + </div> + </div> +</div> + +Feel free to edit any of these examples! You can run them again with the Run +button. These resources might be useful if you want to play with this some more: + +[z80 instruction set](http://www.z80.info/z80-op.txt) - [z80 assembly tutorial](http://tutorials.eeems.ca/ASMin28Days/lesson/toc.html) - [KnightOS reference documentation](http://www.knightos.org/documentation/reference) + +Note: our toolchain has some memory leaks, so eventually WASM is going to +run out of memory and then you'll have to refresh. Sorry! + +## How all of the pieces fit together + +When you +loaded this page, a bunch of things happened. First, the [latest +release](https://github.com/KnightOS/kernel/releases) of the [KnightOS +kernel](https://github.com/KnightOS/kernel) was downloaded. Then all of the +WASM ports of the toolchain were downloaded and loaded. Some virtual filesystems +were set up, and two KnightOS packages were downloaded and installed: +[`core/init`](https://packages.knightos.org/core/init), and +[`core/kernel-headers`](https://packages.knightos.org/core/kernel-headers), +respectively necessary for booting the system and compiling code against the +kernel API. Extracting those packages involves copying them into kpack's +virtual filesystem and running `kpack -e path/to/package root/`. + +When you click "Run" on one of these text boxes, the contents of the text box is +written to `/main.asm` in the assembler's virtual filesystem. The package +installation process extracts headers to `/include/`, and scas itself is run +with `/main.asm -I/include -o /executable`, which assembles the program and +writes the output to `/executable`. + +Then we copy the executable into the genkfs filesystem (this is the tool that +generates filesystem images). We also copy the empty kernel into this +filesystem, as well as any of the packages we've installed. We then run `genkfs +/kernel.rom /root`, which creates a filesystem image from `/root` and bakes it +into `kernel.rom`. This produces a ready-to-emulate ROM image that we can load +into the z80e emulator on the left. + +## The WASM details + +Porting all this stuff to WASM wasn't straightforward. The easiest part +was cross-compiling all of them to JavaScript: + + cd build + emconfigure cmake .. + emmake make + +The process was basically that simple for each piece of software. There were +[a](https://github.com/KnightOS/genkfs/commit/c4eefa87a3b5bdbafcc6d971654608c594f779a1) +[few](https://github.com/KnightOS/scas/commit/d2044e7d7586a946422ce6493cc6dff01127d1c2) +[changes](https://github.com/KnightOS/scas/commit/8bc31af28e8419a9fa6c421147ea522935bd0df4) +made to some of the tools to fix a few problems. The hard part +came when I wanted to run all of them on the same page. WASM compiled code +assumes that it will be the only WASM module on the page at any given +time, so this was a bit challenging and involved editing the generated JS. + +The first thing I did was wrap all of the modules in isolated AMD loaders[^1]. +You can see how some of this ended up looking by visiting the actual scripts +(warning, big files): + +[^1]: AMD was an early means of using modules with JavaScript, which was popular at the time this article was written (2014). Today, a different form of modules has become part of the JavaScript language standard. + +* [scas.js](/tools/scas.js) +* [kpack.js](/tools/kpack.js) +* [genkfs.js](/tools/genkfs.js) + +That was enough to make it so that they could all run. These are part of a +toolchain, though, so somehow they needed to share files. Emscripten's [FS +object](http://kripken.github.io/emscripten-site/docs/api_reference/Filesystem-API.html) +cannot be shared between modules, so the solution was to write a little JS: + +```coffeescript +copy_between_systems = (fs1, fs2, from, to, encoding) -> + for f in fs1.readdir(from) + continue if f in ['.', '..'] + fs1p = from + '/' + f + fs2p = to + '/' + f + s = fs1.stat(fs1p) + log("Writing #{fs1p} to #{fs2p}") + if fs1.isDir(s.mode) + try + fs2.mkdir(fs2p) + catch + # pass + copy_between_systems(fs1, fs2, fs1p, fs2p, encoding) + else + fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding }) +``` + +With this, we can extract packages in the kpack filesystem and copy them to the +genkfs filesystem: + +```coffeescript +install_package = (repo, name, callback) -> + full_name = repo + '/' + name + log("Downloading " + full_name) + xhr = new XMLHttpRequest() + xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download") + xhr.responseType = 'arraybuffer' + xhr.onload = () -> + log("Installing " + full_name) + file_name = '/packages/' + repo + '-' + name + '.pkg' + data = new Uint8Array(xhr.response) + toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' }) + toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot']) + copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8") + copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary") + log("Package installed.") + callback() if callback? + xhr.send() +``` + +And this puts all the pieces in place for us to actually pass an assembly file +through our toolchain: + +```coffeescript +run_project = (main) -> + # Assemble + window.toolchain.scas.FS.writeFile('/main.asm', main) + log("Calling assembler...") + ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable']) + return ret if ret != 0 + log("Assembly done!") + # Build filesystem + executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable") + window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' }) + window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"]) + rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' }) + + log("Loading your program into the emulator!") + if current_emulator != null + current_emulator.cleanup() + current_emulator = new toolchain.ide_emu(document.getElementById('screen')) + current_emulator.load_rom(rom.buffer) + return 0 +``` + +This was fairly easy to put together once we got all the tools to cooperate. +After all, these are all command-line tools. Invoking them is as simple as +calling `main` and then fiddling with the files that come out. Porting z80e, on +the other hand, was not nearly as simple. + +## Porting z80e to the browser + +[z80e](https://github.com/KnightOS/z80e) is our calculator emulator. It's also +written in C, but needs to interact much more closely with the user. We need to +be able to render the display to a canvas, and to receive input from the user. +This isn't nearly as simple as just calling `main` and playing with some files. + +To accomplish this, we've put together +[OpenTI](https://github.com/KnightOS/OpenTI), a set of JavaScript bindings to +z80e. This is mostly the work of my friend puckipedia, but I can explain a bit +of what is involved. The short of it is that we needed to map native structs to +JavaScript objects and pass JavaScript code as function pointers to z80e's +hooks. So far as I know, the KnightOS team is the only group to have attempted +something with this deep of integration between WASM and JavaScript - because we +had to do a ton of the work ourselves. + +OpenTI contains a +[wrap](https://github.com/KnightOS/OpenTI/blob/master/webui/js/OpenTI/wrap.js) +module that is capable of wrapping structs and pointers in JavaScript objects. +This is a tedious procedure, because we have to know the offset and size of each +field in native code. An example of a wrapped object is given here: + +```javascript +define(["../wrap"], function(Wrap) { + var Registers = function(pointer) { + if (!pointer) { + throw "This object can only be instantiated with a memory region predefined!"; + } + this.pointer = pointer; + + Wrap.UInt16(this, "AF", pointer); + Wrap.UInt8(this, "F", pointer); + Wrap.UInt8(this, "A", pointer + 1); + + this.flags = {}; + Wrap.UInt8(this.flags, "C", pointer, 128, 7); + Wrap.UInt8(this.flags, "N", pointer, 64, 6); + Wrap.UInt8(this.flags, "PV", pointer, 32, 5); + Wrap.UInt8(this.flags, "3", pointer, 16, 4); + Wrap.UInt8(this.flags, "H", pointer, 8, 3); + Wrap.UInt8(this.flags, "5", pointer, 4, 2); + Wrap.UInt8(this.flags, "Z", pointer, 2, 1); + Wrap.UInt8(this.flags, "S", pointer, 1, 0); + pointer += 2; + + Wrap.UInt16(this, "BC", pointer); + Wrap.UInt8(this, "C", pointer); + Wrap.UInt8(this, "B", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "DE", pointer); + Wrap.UInt8(this, "E", pointer); + Wrap.UInt8(this, "D", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "HL", pointer); + Wrap.UInt8(this, "L", pointer); + Wrap.UInt8(this, "H", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "_AF", pointer); + Wrap.UInt16(this, "_BC", pointer + 2); + Wrap.UInt16(this, "_DE", pointer + 4); + Wrap.UInt16(this, "_HL", pointer + 6); + pointer += 8; + + Wrap.UInt16(this, "PC", pointer); + Wrap.UInt16(this, "SP", pointer + 2); + pointer += 4; + + Wrap.UInt16(this, "IX", pointer); + Wrap.UInt8(this, "IXL", pointer); + Wrap.UInt8(this, "IXH", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "IY", pointer); + Wrap.UInt8(this, "IYL", pointer); + Wrap.UInt8(this, "IYH", pointer + 1); + pointer += 2; + + Wrap.UInt8(this, "I", pointer++); + Wrap.UInt8(this, "R", pointer++); + + // 2 dummy bytes needed for 4-byte alignment + } + + Registers.sizeOf = function() { + return 26; + } + + return Registers; +}); +``` + +The result of that effort is that you can find out what the current value of a +register is from some nice clean JavaScript: `asic.cpu.registers.PC` (it's <code +id="register-pc"></code>, by the way). Pop open your JavaScript console and play +around with the `current_asic` global! + +## Conclusions + +I've put all of this together on [try.knightos.org](http://try.knightos.org). +The source is available on +[GitHub](https://github.com/KnightOS/try.knightos.org). It's entirely +client-side, so it can be hosted on GitHub Pages. I'm hopeful that this will +make it easier for people to get interested in KnightOS development, but it'll +be a lot better once I can get more documentation and tutorials written. It'd be +pretty cool if we could have interactive tutorials like this! + +If you, reader, are interested in working on some pretty cool shit, there's a +place for you! We have things to do in Assembly, C, JavaScript, Python, and a +handful of other things. Maybe you have a knack for design and want to help +improve it. Whatever the case may be, if you have interest in this stuff, come +hang out with us on IRC: [#knightos on +irc.freenode.net](http://webchat.freenode.net/?channels=knightos&uio=d4). + +--- + +![](https://sr.ht/zhRB.jpg) + +**2018-08-31**: This article was updated to fix some long-broken scripts and +adjust everything to fit into the since-updated blog theme. The title was also +changed from "Porting an entire desktop toolchain to the browser with +Emscripten" and some minor editorial corrections were made. References to +Emscripten were replaced with WebAssembly - WASM is the standard API that +browsers have implemented to replace asm.js, and the Emscripten toolchain and +JavaScript API remained compatible throughout the process. diff --git a/content/blog/Privacy-as-a-hobby.md b/content/blog/Privacy-as-a-hobby.md @@ -0,0 +1,135 @@ +--- +date: 2016-06-29 +# vim: set tw=80 +title: Life, liberty, and the pursuit of privacy +layout: post +tags: [privacy] +--- + +Privacy is my hobby, and should be a hobby of every technically competent +American. Within the eyes of the law I have a right to secure the privacy of my +information. At least that's the current law - many officials are [trying to +subvert that right](http://www.apple.com/customer-letter/). I figure that we'd +better exercise that right while we have it, so that we know how to keep +exercising it once it's illegal and all the information about it dries up. + +One particularly annoying coworker often brings up, "what do you have to hide?" +Though it would defeat the purpose to explain what I'm hiding, let's assume that +what I'm hiding is benign, at least legally speaking. I'm sure you can +understand why I don't want `~/Porn` to be public information should my +equipment be seized after I publish this blog post and an incompetent (or angry) +investigator leaks it. Building secure facilities for housing secrets is fun! +That's true even if there aren't a lot of interesting secrets to hide there. + +But the porn folder brings up an interesting point. I'm not ashamed to admit I +have one, but I would be uncomfortable with everyone being able to see it. Or +maybe I'm having an affair (a scandalous proposition for a single guy, I know) +and there are relevant texts are on my cell phone. Perhaps I suck at managing my +finances and the spreadsheets in my documents would tell you so. Maybe I have +embarrassing home videos of bedroom activities on my hard drive[^1]. Maybe +there's evidence that I'm a recovering alcoholic in my files. Maybe I'm a +closeted homosexual and my files prove it, and 10 years from now the homophobes +win and suddenly the country is more hostile to that. Maybe all of this is true +at once! + +Keeping these things secret is an important right, and one I intend to exercise. +I don't want to be accused of some crime and have my equipment seized and then +mishandled by incompetent officials and made public. I don't want a jury chosen +to decide if I really stole that pack of gum when I was 8 and then have +unfavorable secrets leaked. Human nature might lead them to look on my case +unfavorably if they found out about all the tentacle porn or erotic Harry +Potter fanfics I've been secretly writing. Maybe an investigator finds something +they don't understand, like a private key, and it ends up being exposed through +the proceedings. Maybe this private key proves that I'm Satoshi Nakamoto[^3] and +my life is threatened when the case is closed because of it. + +To the government: **stay the fuck out of my right to encrypt**, or, as I +like to think of it, my right to use math. They will try, again and again, to +take it from us. They must never win. + +The second act of this blog post is advice on how to go about securing your +privacy. The crucial bit of advice is that you must strive to understand the +systems you use for privacy and security. Look for their weak spots and be aware +of them. Don't deceive yourself about how secure your systems are. + +I try to identify pain points in my security model. Some of them will be hard +to swallow. The first one was Facebook - delete your account[^4] [^5]. I did +this years ago. The second one was harder still - Google. I use an Android +phone running CyanogenMod without Google Play Services. I also don't use GMail +or any Google services (I search with DuckDuckGo and add !sp to use StartPage if +necessary). Another one was not using Windows or OS X. This is easy for me but a +lot of people will bitch and moan about it. A valid privacy & security model +does not include Windows. OS X is an improvement but you'd be better off on +Linux. Even your non-technical family can surely figure out how to use Xubuntu +to surf the web. + +I also use browser extensions to subvert tracking and ads. Ad networks have +severely fucked themselves by this point - I absolutely never trust any ads on +the web, and never will, period. Use software like +[uBlock](https://github.com/gorhill/uBlock) to get rid of trackers (and speed +up the web, bonus!). I also block lots of trackers in my /etc/hosts file - +[check this out](https://github.com/StevenBlack/hosts). Also check out +[AdAway](https://free-software-for-android.github.io/AdAway/) for Android. + +These changes help to remove your need to trust that corporate interests will +be good stewards of your private information. This is very important - no amount +of encryption will help you if you give Google a GPS map of your every move[^6] +and your search history[^7] and information about basically every page on the +internet you visit[^8]. And all of your emails and contacts and appointments on +your calendar. Google can be subpoenaed or subverted[^9] and many other +companies won't even try[^10] to keep your data secret even when they aren't +legally compelled to. I like this image from Maciej Cegłowski's excellent +talk[^11] on website obesity about the state of most websites: + +![](https://sr.ht/ks75.jpg) + +When you give all of this information to Google, Facebook, and others, you're +basically waiving your fifth amendment[^12] rights. + +Once you do have control of your information, there are steps you should take to +keep it secure. The answer is encryption. I use +[dm-crypt](https://wiki.archlinux.org/index.php/Dm-crypt) which allows me to +encrypt my entire hard drive on Linux. I'm prompted for a password on boot and +then everything proceeds (and I've never noticed any performance issues, for the +record). + +I also do most of my mobile computing on a laptop running libreboot[^13] with +100% open source software. The weak point here is that if your hardware is +compromised and you don't know it, they could steal your password. One possible +solution is keeping your boot partition and perhaps another key on a flash +drive, but this doesn't fully solve the problem. I suggest looking into things +like case intrusion detection and working on being aware of it when your +hardware is messed with. + +I mentioned earlier that my phone is running CyanogenMod without any of the +Google apps. The weak point here is the radio, which is very insecure and likely +riddled with vulnerabilities. I intend to build my own phone soon with a +Raspberry Pi, where I can have more control over this - things like being able +to disconnect power to the radio or disconnect the microphone when not in use +will help. + +I also self host my email, which was a huge pain in the ass to set up, but is +lovely now that I have it. At some point I intend to write a better mail server +to make this easier. I use opportunistic PGP encryption for my emails, but I +send depressingly few encrypted emails like this due to poor adoption (follow me +on [keybase](https://keybase.io/sircmpwn)? I'll give you an invitation if you +send me an encrypted email asking for one!) + +If you have any questions about how to implement any of this, help identifying +the weaknesses in your setup, or anything else, please feel free to reach out to +me via email ([sir@cmpwn.com](mailto:sir@cmpwn.com)+[F4EA1B88](/publickey.txt)) +or [Twitter](https://twitter.com/sircmpwn) or whatever. Good luck sticking it to +the man! + +[^1]: [ICloud leaks of celebrity photos](https://en.wikipedia.org/wiki/ICloud_leaks_of_celebrity_photos) +[^3]: The secretive inventor of Bitcoin. I'm not Satoshi, if you were wondering. +[^4]: [Click this](https://www.facebook.com/help/delete_account?rdrhc) to do so +[^5]: "But I liiiiike Facebook and it let's me keep up with my frieeeends..." There's no privacy model that includes Facebook and works. Give up. [Read this](https://stallman.org/facebook.html) and try to ignore the childish language and see the tangible evidence instead. +[^6]: If you have location services enabled on your phone, [here's a map of everywhere you've been](https://maps.google.com/locationhistory/). Enjoy! +[^7]: [Here's all of your searches](https://myactivity.google.com/myactivity). You can delete the history here, supposedly. I bet it doesn't unfeed that history to your personal advertising neural network at Google. +[^8]: Google Adsense and Google Analytics are present on basically every website. I'm positive they're writing it down somewhere when you hit a page with those on it. Facebook certainly is, too. +[^9]: Remember [PRISM](https://en.wikipedia.org/wiki/PRISM)? +[^10]: [Like AT&T, for example](http://www.pbs.org/newshour/rundown/report-att-cooperated-extensively-nsa-sharing-billions-phone-email-records/) +[^11]: [The Website Obesity Crisis](http://idlewords.com/talks/website_obesity.htm) +[^12]: That's the right to remain silent. Come on, you should know this. +[^13]: [libreboot](https://libreboot.org/) is an open source BIOS. I got my laptop from [minifree](https://minifree.org/), which directly supports the libreboot project with their profits. diff --git a/content/blog/Process-scheduling-in-KnightOS.md b/content/blog/Process-scheduling-in-KnightOS.md @@ -0,0 +1,125 @@ +--- +date: 2014-09-02 +# set tw=80 +layout: post +title: Process scheduling and multitasking in KnightOS +tags: [KnightOS, kernel hacking] +--- + +I'm going to do some blogging about technical decisions made with +[KnightOS](http://knightos.org). It's an open-source project I've been working +on for the past four years to build an open-source Unix-like kernel for TI +calculators (in assembly). It's been a cool platform on top of which I can +research low level systems concepts and I thought I'd share some of my findings +with the world. + +So, first of all, what is scheduling? For those who are completely out of the +loop, I'll explain what exactly it is and why it's neccessary. Computers run on +a CPU, which executes a series of instructions in order. Each core is not +capable of running several instructions concurrently. However, you can run +hundreds of processes at once on your computer (and you probably are doing so +as you read this article). There are a number of ways of accomplishing, but the +one that suits the most situations is *preemtive multitasking*. This is what +KnightOS uses. You see, a CPU can only execute one instruction after another, +but you can "raise an interrupt". This will halt execution and move to some +other bit of code for a moment. This can be used to handle various events (for +example, the GameBoy raises an interrupt when a button is pressed). One of +these events is often a timer, which raises an interrupt at a fixed interval. +This is the mechanism by which preemptive multitasking is accomplished. + +Let's say for a moment that you have two programs loaded into memory and +running, at addresses 0x1000 and 0x2000. Your kernel has an interrupt handler +at 0x100. So if program A is running and an interrupt fires, the following +happens: + +1. 0x1000 is pushed to the stack as the return address +2. The program counter is set to 0x100 and the interrupt runs +3. The interrupt concludes and returns, which pops 0x1000 from the stack and + into the program counter. + +Once the interrput handler runs, however, the kernel has a chance to be sneaky: + +1. 0x1000 is pushed to the stack as the return address +2. The program counter is set to 0x100 and the interrupt runs +3. The interrupt removes 0x1000 from the stack and puts 0x2000 there instead +3. The interrupt concludes and returns, which pops 0x2000 from the stack and + into the program counter. + +Now the interrupt has switched the CPU from program A to program B. And the +next time an interrupt occurs, the kernel can switch from program B to program +A. This event is called a "context switch". This is the basis of preemptive +multitasking. On top of this, however, there are lots of ideas around which +processes should get CPU time and when. Some systems have more complex +schedulers, but KnightOS runs on limited hardware and I wanted the context +switch to be short and sweet so that the running processes get as much of the +CPU as possible. I'll explain the simple KnightOS scheduling algorithm here. +First, its goals: + +* Short and simple context switches +* Ability to suspend processes when not in foreground +* Ability to run background processes + +What KnightOS uses is a simple round robin with the ability to suspend threads. +That is, we have a list of processes and then some flags, among which is +whether or not the processes is currently suspended. So say we have this list +of processes in memory: + +* 1: PC=0x2000, not suspended +* 2: PC=0x2000, not suspended +* 3: PC=0x2000, suspended +* 4: PC=0x2000, not suspended + +As process 1 is running and an interrupt fires, the kernel looks at this table +and picks the next non-suspended process to run - process 2. On the next +interrupt, it does it again, skipping process 3 and giving time to process 4. + +To actually implement this, we have to think about the stack. KnightOS runs on +z80 processors, which have a single stack and a shared memory space. The CPU +uses the PC register to keep track of which address the current instruction is +at. That is, say you compile this code: + +``` +ld a, 10 +inc a +ld (hl), a +``` + +This compiles to the machine code 3E 0A 3C 77. Say we load this program at +0x8000 - then 0x8000 will point to `ld a, 10`. When the CPU finishes executing +this instruction, it advances PC to 0x8002 (since `ld a, 10` is a two-byte +instruction). The next instruction it executes will be `inc a`, and then PC +advances to 0x8003. + +The stack is used for a lot of things. It can be used to save values, and it is +used to call subroutines. It is also used for interrupts. It's like the same +stacks you use in higher level applications, but it's at a very low level. When +an interrupt fires, the current value of PC is pushed to the stack. Then PC is +set to the interrupt routine, and then when that's done the top of the stack is +removed and placed into PC (effectively returning control to the original +location). However, since the stack is used for much more than that, we have +additional things to consider. + +In KnightOS, when a new process starts, it's allocated a stack in memory and +the CPU's stack pointer (SP) is set to its address. When an interrupt happens, +we need to change the stack to point at some other process so it has time to +run (since that's where its PC is). However, we need to make sure that the +first processes stack is left intact. Since we allocate a new stack for the +next process, we can simply change SP to that processes stack. This will leave +behind the value of PC that was pushed during the interrupt for the previous +process, and lo and behlod a similar value of PC is waiting on top of the other +processes stack. + +So that's it! We do a simple round robin, skipping suspended processes and +following the procedure outlined above to switch between them. This is how +KnightOS shares one CPU with several "concurrent" processes. Operating systems +like Linux use more complicated schedulers with more interesting theory if +you'd like some additional reading. And of course, since KnightOS is open +source, you may enjoy reading all of our code for handling this stuff (in +assembly): + +[Context switching](https://github.com/KnightOS/kernel/blob/master/src/00/interrupt.asm) + +[Stack allocation during process creation](https://github.com/KnightOS/kernel/blob/master/src/00/thread.asm#L72) + +We're hanging out on #knightos on Freenode if you want to chat about cool +low-level stuff like scheduling and memory management. diff --git a/content/blog/Python-datetime-sucks.md b/content/blog/Python-datetime-sucks.md @@ -0,0 +1,102 @@ +--- +date: 2014-06-28 +# vim: tw=82 +title: Python's datetime sucks +layout: post +tags: [python] +--- + +I've been playing with Python for about a year now, and I like pretty much +everything about it. There's one thing that's really rather bad and really should +not be that bad, however - date & time support. It's ridiculous how bad it is in +Python. This is what you get with the standard datetime module: + +* The current time and strftime, with a reasonable set of properties +* Time deltas with days, seconds, and microseconds and nothing else +* Acceptable support for parsing dates and times + +What you don't get is: + +* Meaningful time deltas +* Useful arithmetic + +Date and time support is a rather tricky thing to do and it's something that the +standard library should support well enough to put it in the back of your mind +instead of making you do all the work. + +We'll be comparing it to C# and .NET. + +Let's say I want to get the total hours between two `datetime`s. + +```cs +// C# +DateTime a, b; +double hours = (b - a).TotalHours; +``` + +```python +# Python +a, b = ... +hours = (b - a).seconds / 60 / 60 +``` + +That's not so bad. How about getting the time exactly one month in the future: + +```cs +var a = DateTime.Now.AddMonths(1); +``` + +```python +a = date.now() + timedelta(days=30) +``` + +Well, that's not ideal. In C#, if you add one month to Janurary 30th, you get +Feburary 28th (or leap day if appropriate). In Python, you could write a janky +function to do this for you, or you could use the crappy alternative I wrote +above. + +How about if I want to take a delta between dates and show it somewhere, like a +countdown? Say an event is happening at some point in the future and I want to +print "3 days, 5 hours, 12 minutes, 10 seconds left". This is distinct from the +first example, which could give you "50 hours", whereas this example would give +you "2 days, 2 hours". + +```cs +DateTime future = ...; +var delta = future - DateTime.Now; +Console.WriteLine("{0} days, {1} hours, {2} minutes, {3} seconds left", + delta.Days, + delta.Hours, + delta.Minutes, + delta.Seconds); +``` + +```python +# ...mess of math you have to implement yourself omitted... +``` + +Maybe I have a website where users can set their locale? + +```cs +DateTime a = ...; +Console.WriteLine(a.ToString("some format string", user.Locale)); +``` + +```python +locale.setlocale(locale.LC_TIME, "sv_SE") # Global! +print(time.strftime("some format string")) +``` + +By the way, that Python one doesn't work on Windows. It uses system locales names +which are different on Windows than on Linux or OS X. Mono (cross-platform .NET) +handles this for you on any system. + +And a few other cases that are easy in .NET and not in Python: + +* Days since the start of this year +* Constants like the days in every month +* Is it currently DST in this timezone? +* Is this a leap year? + +In short, Python's datetime module could really use a lot of fleshing out. This +is common stuff and easy for a naive programmer to do wrong. diff --git a/content/blog/RaptorCS-Blackbird-a-horror-story.md b/content/blog/RaptorCS-Blackbird-a-horror-story.md @@ -0,0 +1,172 @@ +--- +date: 2019-09-23 +layout: post +title: "RaptorCS POWER9 Blackbird PC review" +--- + +**November 2018**: Ordered [Basic Blackbird +Bundle](https://www.raptorcs.com/content/BK1B01/intro.html) w/32 GB RAM: +$1,935.64 + +**Update 2019-12-23**: This article was originally titled "RaptorCS POWER9 +Blackbird PC: An expensive mistake". Please read the follow-up article, +published 2019-10-10: +[RaptorCS's redemption: the POWER9 machine works][followup] + +[followup]: https://drewdevault.com/2019/10/10/RaptorCS-redemption.html + +**June 2019** + +Order ships, and arrives without RAM. It had been long enough that I didn't +realize the order had only been partially fulfilled, so I order some RAM from +the [list of recommended chips][RAM] ($338.40), along with the other necessities +that I didn't purchase from Raptor: a case ($97.99) and a PSU ($68.49), and grab +some hard drives I have lying around. Total cost: about $2,440. Worth it to get +POWER9 builds working on builds.sr.ht! + +[RAM]: https://wiki.raptorcs.com/wiki/POWER9_Hardware_Compatibility_List/Memory + +I carefully put everything together, consulting the manual at each step, plug in +a display, and turn it on. Lights come on, things start whizzing, and the screen +comes to life - and promptly starts boot looping. + +**June 27th** + +Support ticket created. What's going on with my board? + +**June 28th** + +Support gets back to me the next day with a suggestion which is unrelated to the +problem, but no matter - I spoke with volunteers in the IRC channel a few hours +earlier and we found out that - whoops! - I hadn't connected the CPU power to +the motherboard. This is the end of the PEBKAC errors, but not the end of the +problems. The machine gets further ahead in the boot - almost to "petitboot", +and then the display dies and the machine reveals no further secrets. + +I sent an update to the support team. + +**July 1st** + +> We have normally only seen this type of failure when there is a RAM-related +> fault, or if the PSU is underpowered enough that bringing the CPUs online at +> full power causes a power fault and immediate safety power off. +> +> Can you watch the internal lights while the system is booting, and see if the +> power LED cluster immediately changes from green to orange as the system stops +> responding over SSH? + +The IRC channel suspects this is not related to the problem. Regardless, I reply +a few hours later with two videos showing the boot up process from power-out to +display death, with the internal LEDs and the display output clearly visible. + +**July 4th** + +"Any progress on this issue?", I ask. + +**July 15th** + +"Hi guys, I'm still experiencing this problem. If you're unsure of the issue I +would like to send the board back to you for diagnosis or a refund." + +**July 25th** + +> Sorry for the delay. Having senior support check out the videos. +> +> Thanks for writing back. We should have something for you by tomorrow during +> the day. + +**July 31st** + +> Hi Drew. +> +> The videos are being reviewed this week. Thank you for sending them. +> +> Please stay tuned. + +**September 15th** + +No reply from support. I have since bought a little more hardware for +self-diagnosis, namely the necessary pieces to connect to the two (or is it 3?) +serial ports. I manage to get a log, which points to several failures, but none +of them seem to be related to the problem at hand (they do indicate some network +failures, which would explain why I can't log into the BMC over SSH for further +diagnosis). And the getty is looping, so I can't log in on the serial console to +explore any further. + +--- + +That was a week ago. Radio silence since. + +So, 10 months after I placed an order for a POWER9 machine, 3 months after I +received it (without the RAM I purchased, no less), and over $2,500 invested... +it's clear that buying the Blackbird was an expensive mistake. Maybe someday +I'll get it working. If I do, I doubt the "support" team will have been +involved. Currently my best bet seems to be waiting for some apparent staff +member (the only apparent staff member) who idles in the IRC channel on Freenode +and allegedly comes online from time to time. + +I'm not alone in these problems. Here are some (anonymized) quotes I've heard +from others while trying to troubleshoot this on IRC. + +On support: + +> ugh, ddevault, yeah. [Blackbird ownership] has not been a smooth experience +> for me, either. + +> my personal theory is that they have really bad ticket software that 'loses' +> tickets somehow + +On reliability: + +> I've found openbmc's networking to be... a bit unreliable... maybe 20% of the +> time it does not responed[sic]/does not respond fast enough to networking +> requests. + +> yeah the vga handoff failing doesn't surprise me (other people here have +> reported it). but the BMC not getting a DHCP lease is odd. (well maybe not +> that odd if you look at the crumminess of the OpenBMC software stack...) + +So, yeah, don't buy from Raptor Computer Systems. It's too large and unwieldly +to be an effective paper weight, either! + +--- + +**Erratta** + +*2019-09-24 @ 00:19 UTC*: Raptor has reached out and apologized for my support +experience. We are discussing these problems in more detail now. They have also +issued a refund for the unshipped RAM. + +*2019-09-24 @ 00:51 UTC*: Raptor believes the CPU to be faulty and is shipping a +replacement. They attribute the delay to having to reach out to IBM about the +problem, but don't have a satisfactory answer to why the support process failed. +I understand it's being discussed internally. + +*2019-09-24 @ 13:08 UTC*: + +> After investigation, we are implementing new mechanisms to avoid support +> issues like the one you experienced. We now have a self-serve RMA generation +> system which would have significantly reduced your wait time, and are taking +> measures to ensure that tickets are no longer able to be ignored by front line +> support staff. We believe we have addressed the known failure modes at this +> time, and management will be keeping a close eye on the operation of the +> support system to ensure that new failure modes are handled rapidly. + +They've tweeted this about their new self-service RMA system as well: + +> We've made it easy to submit RMA requests for defective products on our Web +> site. Simply go to your account, select the "Submit RMA Request" link, and +> fill out the form. Your product will be warranty checked and, if valid, you +> will receive an RMA number and shipping address! + +&mdash; @RaptorCompSys via [Twitter](https://twitter.com/RaptorCompSys/status/1176432946670186498) + +I agree that this shows positive improvements and a willingness to continue +making improvements in their support experience. Thanks to Raptor for taking +these concerns seriously. I hope to have a working Blackbird system soon, and +will publish a follow-up review when the time comes. + +*2019-10-08 @ 22:30 UTC* A source quoted anonymously in this article asked me to +remove their quote, after a change of heart. They feel that the attention this +article has received has made their statement reach beyond the level of +dissatisfaction they had with Raptor at the time. diff --git a/content/blog/RaptorCS-redemption.md b/content/blog/RaptorCS-redemption.md @@ -0,0 +1,101 @@ +--- +date: 2019-10-10 +layout: post +title: "RaptorCS's redemption: the POWER9 machine works" +--- + +This is a follow-up to my earlier article, "[RaptorCS POWER9 Blackbird PC: An +expensive mistake][previous]". Since I published that article, I've been in +touch with Raptor and they've been much more communicative and helpful. I now +have a working machine! + +[previous]: https://drewdevault.com/2019/09/23/RaptorCS-Blackbird-a-horror-story.html + +![Picture of uname -sm showing "Linux ppcle64"](https://sr.ht/OTyo.jpeg) + +After I published my article, Raptor reached out and apologised for my +experience. They offered a full refund, but I agreed to work on further +diagnosis now that we had opened a dialogue[^1]. They identified that my CPU was +defective and sent me a replacement, then we found the mainboard to be +defective, too, and the whole thing was shipped back and replaced. I installed +the new hardware into the datacenter today and it was quite pleasant to get up +and running. Raptor assures me that my nightmares with the old board are +atypical, and if the new board is representative of the usual user experience, I +would have to agree. The installation was completely painless.[^2] + +[^1]: They did refund the RAM which was unfulfilled from my original order. +[^2]: They did give me a little heart attack, however, by sending the replacement CPU to me in the same box I had returned the faulty CPU back to them with - a box which I had labelled "BAD CPU". + +However, I refuse to give any company credit for waking up their support team +only when a scathing article about them frontpages on Hacker News. I told them I +wouldn't publish a positive follow-up unless they also convinced me that the +support experience had been fixed for the typical user as well. To this end, +Raptor has made a number of substantive changes. To quote their support staff: + +> After investigation, we are implementing new mechanisms to avoid support +> issues like the one you experienced. We now have a +> [self-serve RMA generation system](https://twitter.com/RaptorCompSys/status/1176432946670186498) +> which would have significantly reduced your wait time, and are taking measures +> to ensure that tickets are no longer able to be ignored by front line support +> staff. We believe we have addressed the known failure modes at this time, and +> management will be keeping a close eye on the operation of the support system +> to ensure that new failure modes are handled rapidly. + +They've tweeted this about their new self-service RMA system as well: + +> We've made it easy to submit RMA requests for defective products on our Web +> site. Simply go to your account, select the "Submit RMA Request" link, and +> fill out the form. Your product will be warranty checked and, if valid, you +> will receive an RMA number and shipping address! + +&mdash; @RaptorCompSys via [Twitter](https://twitter.com/RaptorCompSys/status/1176432946670186498) + +They're also working on other improvements to make the end-user experience +better, including [more content on the +wiki](https://wiki.raptorcs.com/wiki/Main_Page), such as a [flowchart for +dealing with common +problems](https://wiki.raptorcs.com/wiki/Troubleshooting/Support_Request_Checklist). + +Thanks to Raptor for taking the problem seriously, quickly fixing the problems +with my board, and for addressing the systemic problems which led to the +failure of their support system. + +On the subject of the working machine, I am quite impressed with it so far. +Installation was a breeze, it compiles the kernel on 32 threads from spinning +rust in 4m15s, and I was able to get KVM working without much effort. I have +christened it "flandre"[^3], which I think is fitting. I plan on bringing it up +as a build slave for builds.sr.ht in the coming weeks/months, and offering +ppc64le builds on Sourcehut in the near future. I have another board which was +generously donated by another Raptor customer[^4], which arrived last week and +that I hope to bring up and use for testing Wayland before introducing it to the +Sourcehut fleet. + +[^3]: Sourcehut virtual machines are named after their purpose, but our physical servers are named after [Touhou](https://en.wikipedia.org/wiki/Touhou_Project) characters. +[^4]: This happened prior to any of the problems with the first machine. + +--- + +P.S. For those interested in more details of the actual failures: + +This machine is so badly broken that it would actually be hilarious if the +manufacturer had been more present in the troubleshooting process. I think the +best way to sum it up is "FUBAR". Among problems I encountered were: + +- The CPU experiences a "ZCAL failure" (???) +- The BMC (responsible for bringing up the main CPU(s)) had broken ethernet, + making login over SSH impossible +- The BMC's getty would boot loop, making login over serial impossible +- The BMC's u-Boot would boot loop if the TX pin on the serial cable was plugged + in, making diagnosing issues from that stage impossible +- petitboot's ncurses output was being piped into a shell and executed (what the fuck?) + +In the immortal words of James Mickens, "I HAVE NO TOOLS BECAUSE I HAVE +DESTROYED MY TOOLS WITH MY TOOLS." A staff member at Raptor tells me: +"Your box ended up on my desk [...] This is easily the most broken board I've +seen, ever, and that includes prototypes. This will help educate us for a while +to come due to the unique nature of some of the faults." + +Not sure what can cause such an impressive cacophony of failures, but it's so +catastrophic that I can easily believe that this is far from typical. The +hardware is back in Raptor's hands now, and I would be interested to hear about +their insights after further diagnosis. diff --git a/content/blog/Re-Slow.md b/content/blog/Re-Slow.md @@ -0,0 +1,161 @@ +--- +date: 2020-01-08 +title: Following up on "Hello world" +layout: post +tags: [followup] +--- + +This is a follow-up to my last article, [Hello +world](https://drewdevault.com/2020/01/04/Slow.html), which is easily the most +negatively received article I've written &mdash; a remarkable feat for someone +who's written as much flame bait as me. Naturally, the fault lies with the +readers. + +<a href="https://xkcd.com/1984/" rel="noopener"><img src="https://imgs.xkcd.com/comics/misinterpretation_2x.png" width="294" /></a> + +All jokes aside, I'll try to state my point better. The "Hello world" article +was a lot of work to put together &mdash; frustrating work &mdash; by the time +I had finished collecting numbers, I was exhausted and didn't pay much mind to +putting context to them. This left a lot of it open to interpretation, and a lot +of those interpretations didn't give the benefit of the doubt. + +First, it's worth clarifying that the assembly program I gave is a +*hypothetical, idealized* hello world program, and in practice not even the +assembly program is safe from bloat. After it's wrapped up in an ELF, even after +stripping, the binary bloats up to <strong>157&times;</strong> the size of the +actual machine code. I had hoped this would be more intuitively clear, but the +take-away is that the ideal program is a pipe dream, not a standard to which the +others are held. As the infinite frictionless plane in vacuum is to physics, +that assembly program is to compilers. + +I also made the mistake of including the runtime in the table. What I wanted you +to notice about the timestamp is that it *rounds to zero* for 15 of the 21 test +cases, and arguably only one or two approach the realm of human perception. +It's meant to lend balance to the point I'm making with the number of syscalls: +despite the complexity on display, the user generally can't even tell. The other +problem with including the runtimes is that it makes it look like a benchmark, +which it's not (you'll notice that if you grep for "benchmark", you will find no +results). + +Another improvement would have been to group rows of the table by orders of +magnitude (in terms of number of syscalls), and maybe separate the outliers in +each group. There is little difference between many of the languages in the +middle of the table, but when one of them is your favorite language, "stacking +it up" against its competitors like this is a good way to get the reader's blood +pumping and bait some flames. If your language appears to be represented +unfavorably on this chart, you're likely to point out the questionable +methodology, golf your way to a more generous sample code, etc; things I could +have done myself were I trying to make a benchmark rather than a point about +complexity. + +And hidden therein is my actual point: complexity. There has long been a trend +in computing of endlessly piling on the abstractions, with no regard for the +consequences. The web is an ever growing mess of complexity, with larger and +larger blobs of inscrutable JavaScript being shoved down pipes with no regard +for the pipe's size or the bridge toll charged by the end-user's telecom. +Electron apps are so far removed from hardware that their jarring non-native UIs +can take seconds to respond and eat up the better part of your RAM to merely +show a text editor or chat application. + +The PC in front of me is literally five thousand times faster than the graphing +calculator in my closet - but the latter can boot to a useful system in a +fraction of a millisecond, while my PC takes almost a minute. Productivity per +CPU cycle per Watt is the lowest it's been in decades, and is orders of +magnitude (plural) beneath its potential. So far as most end-users are +concerned, computers haven't improved in meaningful ways in the past 10 years, +and in many respects have become worse. The cause is well-known: programmers +have spent the entire lifetime of our field recklessly piling abstraction on top +of abstraction on top of abstraction. We're more concerned with shoving more +spyware at the problem than we are with optimization, outside of a small number +of high-value problems like video decoding.[^1] Programs have grown fat and +reckless in scope, and it affects literally everything, even down to the last +bastion of low-level programming: C. + +I use syscalls as an approximation of this complexity. Even for one of the +simplest possible programs, there is a huge amount of abstraction and complexity +that comes with many approaches to its implementation. If I just print "hello +world" in Python, users are going to bring along almost a million lines of code +to run it, the fraction of which isn't dead code is basically a rounding error. +This isn't *always* a bad thing, but it often is and no one is thinking about +it. + +That's the true message I wanted you to take away from my article: most +programmers aren't thinking about this complexity. Many choose tools because +it's easier for them, or because it's what they know, or because developer time +is more expensive than the user's CPU cycles or battery life and the engineers +aren't signing the checks. I hoped that many people would be surprised at just +how much work their average programming language could end up doing even when +given simple tasks. + +The point was not that your programming language is wrong, or that being higher +up on the table is better, or that programming languages should be blindly +optimizing these numbers. The point is, if these numbers surprised you, then you +should find out why! I'm a systems programmer &mdash; I want you to be +interested in your systems! And if this surprises you, I wonder what else +might... + +I know that article didn't do a good job of explaining any of this. I'm sorry. + +--- + +Now to address more specific comments: + +**What the fuck is a syscall**? + +This question is more common with users of the languages which make more of +them, ironically. A syscall is when your program asks the kernel to do something +for it. This causes a transition from *user space* to *kernel space*. This +transition is one of the more expensive things your programs can do, but a +program that doesn't make any syscalls is not a useful program: syscalls are +necessary to do any kind of I/O (input or output). [Wikipedia +page](https://en.wikipedia.org/wiki/System_call). + +On Linux, you can use the [strace](https://linux.die.net/man/1/strace) tool to +analyze the syscalls your programs are making, which is how I obtained the +numbers in the original article. + +**This "benchmark" is biased against JIT'd and interpreted languages**. + +Yes, it is. It *is* true that many programming environments have to factor +in a "warm up" time. This argument on its face-value is apparently validated by +the cargo-culted (and often correct) wisdom that benchmarks should be conducted +with timers in-situ, post warm-up period, with the measured task being +repeated many times so that trends become more obvious.[^2] It's precisely these +details, which the conventional benchmarking wisdom aims to obscure, that I'm +trying to cast a light on. While a benchmark which shows how quickly a bunch of +programming languages can print "hello world" a million times[^3] might be +interesting, it's not what I'm going for here. + +**Rust is doing important things with those syscalls**. + +My opinion on this is mixed: yes, stack guards are useful. However, my "hello +world" program has zero chance of causing a stack overflow. In theory, Rust +should be able to reckon whether or not many programs are at risk of stack +overflow. If not, it can ask the programmer to specify some bounds, or it can +emit the stack guards *only in those cases*. The worst option is panicking, and +I'm surprised that Crustaceans feel like this is sufficient. Funny, given their +obsession with "zero cost" abstractions, that a nonzero-cost abstraction would +be so fiercely defended. They're already used to overlong compile times, adding +more analysis probably won't be noticed ;) + +**Go is doing important things with those syscalls**. + +On this I wholly disagree. I hate the Go runtime, it's the worst thing about an +otherwise great language. Go programs are almost impossible to debug for having +to sift through mountains of unrelated bullshit the program is doing, all to +support a concurrency/parallelism model that I also strongly dislike. There are +some bad design decisions in Golang and stracing the average Go program brings a +lot of them to light. Illumos has many of its own problems, but [this +article](http://dtrace.org/blogs/wesolows/2014/12/29/golang-is-trash/) about +porting Go to it covers a number of related problems. + +**Wow, Zig is competitive with assembly?** + +Yeah, I totally had the same reaction. I'm interested to see how it measures up +under more typical workloads. People keep asking me what I think about Zig in +general, and I think it has potential, but I also have a lot of complaints. It's +not likely to replace C for me, but it might have a place somewhere in my stack. + +[^1]: For efficient display of unskippable 30 second video ads, of course. +[^2]: This approach is the most "fair" for comparison's sake, but it also often obscures a lot of the practical value of the benchmark in the first place. For example, how often is the branch predictor and L1 cache going to be warmed up in favor of the measured code in practice? +[^3]: All of them being handily beaten by `/bin/yes "hello world"` diff --git a/content/blog/Reckless-limitless-scope.md b/content/blog/Reckless-limitless-scope.md @@ -0,0 +1,90 @@ +--- +date: 2020-03-18 +layout: post +title: The reckless, infinite scope of web browsers +# Word count: +# - All RFCs: 57,716,641 +# - POSIX: 2,017,056 +# - USB 3.2: 872,395 +# - UEFI: 659,580 +# - C++17: 576,344 +# - C11: 208,220 +# - x86 ISA: +# - All of the books on Wikipedia's list of longest novels: 39.7M +# +# All W3C specs: +# - 113,875,980 (preliminary count) +--- + +Since the first browser war between Netscape and Internet Explorer, web browsers +have been using features as their primary means of competing with each other. +This strategy of unlimited scope and perpetual feature creep is reckless, and +has been allowed to go on for far too long. + +I used wget to download all 1,217 of the [W3C specifications][w3c specs] +which have been published at the time of writing[^1], of which web browsers need +to implement a substantial subset in order to provide a modern web experience. +I ran a word count on all of these specifications. How complex would you guess +the web is? + +[^1]: Not counting WebGL, which is maintained by Khronos. + +[w3c specs]: https://www.w3.org/TR/ + +The total word count of the W3C specification catalogue is 114 million words at +the time of writing. If you added the combined word counts of the C11, C++17, +UEFI, USB 3.2, and POSIX specifications, all 8,754 published RFCs, and the +combined word counts of everything on Wikipedia's [list of longest +novels][longest novels], you would be 12 million words short of the W3C +specifications.[^2] + +[^2]: You could fit the 5,038 page Intel x86 ISA manual into the remainder, six times. + +[longest novels]: https://en.wikipedia.org/wiki/List_of_longest_novels + +I conclude that **it is impossible to build a new web browser**. The complexity +of the web is *obscene*. The creation of a new web browser would be comparable +in effort to the Apollo program or the Manhattan project. + +It is impossible to: + +- Implement the web correctly +- Implement the web securely +- Implement the web **at all** + +Starting a bespoke browser engine with the intention of competing with Google or +Mozilla is a fool's errand. The last serious attempt to make a new browser, +Servo, has become one part incubator for Firefox refactoring, one part +playground for bored Mozilla engineers to mess with technology no one wants, and +zero parts viable modern web browser. But WebVR is cool, right? Right? + +The consequences of this are obvious. Browsers are the most expensive piece of +software a typical consumer computer runs. They're infamous for using all of +your RAM, pinning CPU and I/O, draining your battery, etc. *Web browsers are +responsible for more than 8,000 CVEs*.[^3] + +[^3]: Combined search results for CVEs mentioning "firefox", "chrome", "safari", and "internet explorer", on cve.mitre.org. + +Because of the monopoly created by the insurmountable task of building a +competitive alternative, browsers have also been free to stop being the "user +agent" and start being the agents of their creators instead. Firefox is filling +up with ads, tracking, and mandatory plugins. Chrome is used as a means for +Google to efficiently track your eyeballs and muscle anti-technologies like DRM +and AMP into the ecosystem. The browser duopoly is only growing stronger, too, +as Microsoft drops Edge and WebKit falls well behind its competition. + +The major projects are open source, and usually when an open-source project +misbehaves, we're able to fork it to offer an alternative. But even this +is an impossible task where web browsers are concerned. The number of W3C +specifications grows at an average rate of 200 new specs per year, or about 4 +million words, or about one POSIX every 4 to 6 months. How can a new team +possibly keep up with this on top of implementing the outrageous scope web +browsers already have *now*? + +The browser wars have been allowed to continue for far too long. They should +have long ago focused on competing in terms of performance and stability, not in +adding new web "features". This is absolutely ridiculous, and it has to stop. + +*Note: I have prepared a write-up on [how I arrived at these word counts][writeup].* + +[writeup]: https://paste.sr.ht/~sircmpwn/13c1951014a256e9f551296a129bf6d10e9303dc diff --git a/content/blog/Redirecitng-stderr-of-running-process.md b/content/blog/Redirecitng-stderr-of-running-process.md @@ -0,0 +1,53 @@ +--- +date: 2018-05-04 +layout: post +title: Redirecting stderr of a running process +tags: [hack] +--- + +During the KDE sprint in Berlin, [Roman Gilg](http://www.subdiff.de/) leaned +over to me and asked if I knew how to redirect the stderr of an already-running +process to a file. I Googled it and found underwhelming answers using strace and +trying to decipher the output by reading the write syscalls. Instead, I thought +a gdb based approach would work better, and after putting the pieces together +Roman insisted I wrote a blog post on the topic. + +gdb, the GNU debugger, has two important features that make this possible: + +- Attaching to running processes via `gdb -p` +- Executing arbitrary code in the target process space + +With this it's actually quite straightforward. The process is the following: + +1. Attach gdb to the running process +2. Run `compile code -- dup2(open("/tmp/log", 65), 2)` + +The magic 65 here is the value of `O_CREAT | O_WRONLY` on Linux, which is easily +found with a little program like this: + +```c +#include <sys/stat.h> +#include <fcntl.h> + +int main(int argc, char **argv) { + printf("%d\n", O_CREAT | O_WRONLY); + return 0; +} +``` + +2 is always the file descriptor assigned to stderr. What happens here is: + +1. Via [`open`](https://linux.die.net/man/3/open), the file you want to redirect + to is created. +2. Via [`dup2`](https://linux.die.net/man/3/dup2), stderr is overwritten with + this new file. + +The `compile code` gdb command will compile some arbitrary C code and run the +result in the target process, presumably by mapping some executable RAM and +loading it in, then jumping to the blob. Closing gdb (control+d) will continue +the process, and it should start writing out to the file you created. + +There are lots of other cool (and hacky) things you can do with gdb. I once +disconnected someone from an internet radio by attaching gdb to nginx and +closing their file descriptor, for example. Thanks to Roman for giving me the +chance to write an interesting blog post on the subject! diff --git a/content/blog/Rotating-passwords.md b/content/blog/Rotating-passwords.md @@ -0,0 +1,81 @@ +--- +date: 2017-05-11 +title: Rotating passwords in bulk in the wake of security events +layout: post +tags: [security] +--- + +I've been putting this post off for a while. Do you remember the [CloudFlare +security +problem](https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/) +that happened a few months ago? This is the one that disclosed huge amounts of +sensitive information for huge numbers websites. When this happened, your +accounts on [thousands of +websites](https://github.com/pirate/sites-using-cloudflare) were potentially +compromised. + +Updating passwords for all of these services at once was a major source of +frustration for users. Updating a single password can take 5 minutes, and +changing dozens of them might take hours. I decided that I wanted to make this +process easier. + +``` +$ ./pass-rotate github.com linode.com news.ycombinator.com twitter.com +Rotating github.com... + Enter your two factor (TOTP) code: +OK +Rotating linode.com... + Enter your two-factor (TOTP) code: +OK +Rotating news.ycombinator.com... OK +Rotating twitter.com... + Enter your SMS authorization code: +OK +``` + +I just changed 4 passwords in about 20 seconds. This is +[pass-rotate](https://github.com/SirCmpwn/pass-rotate), which is basically +youtube-dl for rotating passwords. It integrates with your password manager to +make it easy to change your password. pass-rotate is also provided in the form +of a library that password managers can directly integrate with to provide +first-class support for password rotation with a shared implementation of +various websites. Not only can it help you rotate passwords after security +events, but it can be used for periodic password rotation to keep your accounts +safer in general. + +How this was basically done is by reverse engineering the password change flow of +each of the websites it supports. Each provider's backend submits HTTP requests +that simulates logging into the website and interacting with the password reset +form. This is often quite simple, like +[github.py](https://github.com/SirCmpwn/pass-rotate/blob/master/passrotate/providers/github.py), +but can sometimes be quite complex, like +[namecheap.py](https://github.com/SirCmpwn/pass-rotate/blob/master/passrotate/providers/namecheap.py). + +The current list of supported services is available +[here](https://github.com/SirCmpwn/pass-rotate/wiki/Currently-supported-services). +There's also an issue to discuss making a standardized mechanism for automated +password rotation [here](https://github.com/SirCmpwn/pass-rotate/issues/1). At +the time of writing, the list of supported services is: + +* Cloudflare <sub>✗ TOTP</sub> +* Digital Ocean <sub>✗ TOTP</sub> +* Discord <sub>✓ TOTP</sub> +* GitHub <sub>✓ TOTP ✗ U2F</sub> +* Linode <sub>✓ TOTP</sub> +* NameCheap <sub>✓ SMS</sub> +* Pixiv +* Twitter <sub>✓ SMS ✓ TOTP</sub> +* YCombinator + +Adding new services is easy - check out [the +guide](https://github.com/SirCmpwn/pass-rotate/blob/master/CONTRIBUTING.md). I +would be happy to merge your pull requests. Please add websites you use and +websites you maintain! + +I also set up a Patreon campaign today. If you'd like to contribute to my work, +please visit [the Patreon page](https://patreon.com/sircmpwn). This supports all +of my open source projects, but if you want to support pass-rotate in +particular feel free to let me know when you make your contribution. This kind +of project needs long term maintenance to support countless providers and +keep up with changes to them. Feel free to let me know what service providers +you want me to add support for when you make your pledge! diff --git a/content/blog/Rust-is-not-a-good-C-replacement.md b/content/blog/Rust-is-not-a-good-C-replacement.md @@ -0,0 +1,129 @@ +--- +date: 2019-03-25 +layout: post +title: Rust is not a good C replacement +tags: ["philosophy", "rust", "c"] +--- + +I have a saying that summarizes my opinion of Rust compared to Go: "Go is the +result of C programmers designing a new programming language, and Rust is the +result of C++ programmers designing a new programming language". This isn't just +a metaphor - Go was designed by plan9 alumni, an operating system written in C +and the source of inspiration for many of Go's features, and Rust was designed +by the folks at Mozilla - whose flagship product is one of the largest C++ +codebases in the world. + +The values of good C++ programmers are incompatible with the values of good C +programmers[^1]. Rust is a decent C++ replacement if you have the same goals as +C++, but if you don't, the design has very similar drawbacks. Both Rust and C++ +are what I like to call "kitchen sink" programming languages, with the obvious +implication. These languages solve problems by adding more language features. A +language like C solves problems by writing more C code. + +[^1]: Aside: the term "C/C++" infuriates me. They are completely different languages. Idiomatic C++ looks nothing like idiomatic C. + +I did some back of the napkin estimates of the rate at which these languages +become more complex, based on the rate at which they add features per year. My +approach wasn't very scientific, but I'm sure the point comes across. + +- **C: 0.73 new features per year**, measured by the number of bullet points in + the C11 article on Wikipedia which summarizes the changes from C99, adjusted to + account for the fact that C18 introduced no new features. +- **Go: 2 new features per year**, measured by the number of new features listed + on the Wikipedia summary of new Go versions. +- **C++: 11.3 new features per year**, measured by the number of bullet points + in the C++17 article which summarizes the changes from C++14. +- **Rust: 15 new features per year**, measured by the number of headers in the + release notes of major Rust versions over the past year, minus things like + linters. + +This speaks volumes to the stability of these languages, but more importantly it +speaks to their complexity. Over time it rapidly becomes difficult for one to +keep an up-to-date mental map of Rust and how to solve your problems +idiomatically. A Rust program written last year already looks outdated, whereas +a C program written ten years ago has pretty good odds of being just fine. +Systems programmers don't want shiny things - we just want things that work. +That really cool feature $other_language has? Not interested. It'll be more +trouble than it's worth. + +With the philosophical wish-wash out of the way and the tone set, let me go over +some more specific problems when considering Rust as a C replacement. + +**C is the most portable programming language**. Rust actually has a pretty +admirable selection of supported targets for a new language (thanks mostly to +LLVM), but it pales in comparison to C, which runs on almost *everything*. A new +CPU architecture or operating system can barely be considered to exist until it +has a C compiler. And once it does, it unlocks access to a vast repository of +software written in C. Many other programming languages, such as Ruby and +Python, are implemented in C and you get those for free too. + +**C has a spec**. No spec means there's nothing keeping rustc honest. Any +behavior it exhibits could change tomorrow. Some weird thing it does could be a +feature *or* a bug. There's no way to know until your code breaks. That they +can't slow down to pin down exactly what defines Rust is also indicative of an +immature language. + +<iframe + src="https://cmpwn.com/@sir/100437209244243864/embed" + class="mastodon-embed" + style="max-width: 100%; border: 0; margin: 0 auto; display: block;" + width="400"></iframe> +<script src="https://cmpwn.com/embed.js" async="async"></script> + +**C has many implementations**. C has many competing compilers. They all work +together stressing out the spec, fishing out the loosely defined corners, and +pinning down exactly what C is. Code that compiles in one and not another is +indicative of a bug in one of them, which gives a nice extra layer of testing to +each. By having many implementations, we force C to be well defined, and this is +good for the language and its long-term stability. Rustc could stand to have +some competition as well, maybe it would get faster![^2] + +[^2]: Rust does have one competing compiler, but without a spec it's hard to define its level of compatibility and correctness, and it's always playing catch-up. + +**C has a consistent & stable ABI**. The System-V ABI is supported on a wide +variety of systems and has been mostly agreed upon by now. Rust, on the other +hand, has no stable internal ABI. You have to compile and link everything all in +one go on the same version of the Rust compiler. The only code which can +interact with the rest of the ecosystem is unidiomatic Rust, written at some +kind of checkpoint between Rust and the outside world. The outside world exists, +it speaks System-V, and us systems programmers spend a lot of our time talking +to it. + +**Cargo is mandatory**. On a similar line of thought, Rust's compiler flags are +not stable. Attempts to integrate it with other build systems have been met with +hostility from the Rust & Cargo teams. The outside world exists, and us systems +programmers spend a lot of our time integrating things. Rust refuses to play +along. + +**Concurrency is generally a bad thing.** Serial programs have X problems, and +parallel programs have X<sup>Y</sup> problems, where Y is the amount of +parallelism you introduce. Parallelism in C is a pain in the ass for sure, and +this is one reason I find Go much more suitable to those cases. However, nearly +all programs needn't be parallel. A program which uses poll effectively is going +to be simpler, reasonably performant, and have orders of magnitude fewer bugs. +"Fearless concurrency" allows you to fearlessly employ bad software design 9 +times out of 10. + +**Safety**. Yes, Rust is more safe. I don't really care. In light of all of +these problems, I'll take my segfaults and buffer overflows. I especially refuse +to "rewrite it in Rust" - because no matter what, rewriting an entire program +from scratch is *always* going to introduce more bugs than maintaining the C +program ever would. I don't care what language you rewrite it in. + +--- + +C is far from the perfect language - it has many flaws. However, its +replacement will be simpler - not more complex. Consider Go, which has had a lot +of success in supplanting C for many problems. It does this by specializing on +certain classes of programs and addressing them with the simplest solution +possible. It hasn't completely replaced C, but it has made a substantial dent in +its problem space - more than I can really say for Rust (which has made similar +strides for C++, but definitely not for C). + +The kitchen sink approach doesn't work. Rust will eventually fail to the "jack +of all trades, master of none" problem that C++ has. Wise languages designers +start small and stay small. Wise systems programmers extend this philosophy to +designing entire systems, and Rust is probably not going to be invited. I +understand that many people, particularly those already enamored with Rust, +won't agree with much of this article. But now you know why we are still writing +C, and hopefully you'll stop bloody bothering us about it. diff --git a/content/blog/Self-hosted-livestreaming.md b/content/blog/Self-hosted-livestreaming.md @@ -0,0 +1,204 @@ +--- +date: 2018-08-26 +layout: post +title: How to make a self-hosted video livestream +tags: [instructive, ffmpeg] +--- + +I have seen some articles in the past which explain how to build the ecosystem +*around* your video streaming, such as live chat and forums, but which leave the +actual video streaming to Twitch.tv. I made a note the last time I saw one of +these articles to write one of my own explaining the video bit. As is often the +case with video, we'll be using the excellent [ffmpeg](http://ffmpeg.org/) tool +for this. If it's A/V-related, ffmpeg can probably do it. + +<script src="/js/dash.all.min.js"></script> +<video + data-dashjs-player autoplay muted controls + src="/dash/live.mpd" + poster="https://sr.ht/JGOY.png" + style="width: 100%" +></video> +<div style="text-align: center; font-size: 0.8rem; width: 80%; margin: 0 auto 1rem auto;"> + This is the recordings from the + <a href="https://www.indiegogo.com/projects/sway-hackathon-software#/"> + Sway hackathon + </a> + we put on earlier this year, plus the current UTC time to prove that it's + live. Click unmute if you want to hear the audio stream. +</div> + +ffmpeg has a built-in [DASH](https://dashif.org/) output format, which is the +current industry standard for live streaming video to web browsers. It works by +splitting the output up into discrete files and using an [XML +file](/dash/live.mpd) (an MPD playlist) to tell the player where they are. Few +browsers support DASH natively, but +[dash.js](https://github.com/Dash-Industry-Forum/dash.js/wiki) can polyfill it +by periodically downloading the latest manifest and driving the video element +itself. + +Getting the source video into ffmpeg is a little bit beyond the scope of this +article, but I know some readers won't be familiar with ffmpeg so I'll have +mercy. Let's say you want to play some static video files like I'm doing above: + +```sh +ffmpeg \ + -re \ + -stream_loop -1 \ + -i my-video.mkv \ +``` + +This will tell ffmpeg to read the input (-i) in real time (-re), and loop it +indefinitely. If instead you want to, for example, use x11grab instead to +capture your screen and pulse to capture desktop audio, try this: + +```sh + -f x11grab \ + -r 30 \ + -video_size 1920x1080 \ + -i $DISPLAY \ + -f pulse \ + -i alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo +``` + +This sets the framerate to 30 FPS and the video resolution to 1080p, then reads +from the X11 display `$DISPLAY` (usually :0). Then we add pulseaudio and use my +microphone source name, which I obtained with `pactl list sources`. + +Let's add some arguments describing the output format. Your typical web browser +is a finicky bitch and has some very specific demands from your output format if +you want maximum compatability: + +```sh + -codec:v libx264 \ + -profile:v baseline \ + -level 4 \ + -pix_fmt yuv420p \ + -preset veryfast \ + -codec:a aac \ +``` + +This specifices the libx264 video encoder with the baseline level 4 profile, the +most broadly compatible x264 profile, with the yuv420p pixel format, the most +broadly compatible pixel format, the veryfast preset to make sure we can encode +it in realtime, the aac audio codec. Now that we've specified the parameters for +the output, let's configure the output format: DASH. + +```sh + -f dash \ + -window_size 5 \ + -remove_at_exit 1 \ + /tmp/dash/live.mpd +``` + +The window_size specifies the maximum number of A/V segments to keep in the +manifest at any time, and remove_at_exit will clean up all of the files when +ffmpeg exits. The output file is the path to the playlist to write to disk, and +the segments will be written next to it. The last step is to serve this with +nginx: + +```nginx +location /dash { + types { + application/dash+xml mpd; + video/mp4 m4v; + audio/mp4 m4a; + } + add_header Access-Control-Allow-Origin *; + root /tmp; +} +``` + +You can now point the [DASH reference +player](http://reference.dashif.org/dash.js/nightly/samples/dash-if-reference-player/index.html) +at `http://your-server.org/dash/live.mpd` and see your video streaming there. +Neato! You can add dash.js to your website and you know have a fully self-hosted +video live streaming setup ready to rock. + +Perhaps the ffmpeg swiss army knife isn't your cup of tea. If you want to, for +example, use [OBS Studio](https://obsproject.com/), you might want to take a +somewhat different approach. The +[nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) provides an RTMP +(real-time media protocol) server that integrates with nginx. After adding +the DASH output, you'll end up with something like this: + +```sh +rtmp { + server { + listen 1935; + + application live { + dash on; + dash_path /tmp/dash; + dash_fragment 15s; + } + } +} +``` + +Then you can stream to `rtmp://your-server.org/live` and your dash segments +will show up in `/tmp/dash`. There's no password protection here, so put it in +the stream URL (e.g. `application R9AyTRfguLK8`) or use an IP whitelist: + +``` +application live { + allow publish your-ip; + deny publish all; +} +``` + +If you want to get creative with it you can use +[`on_publish`](https://github.com/arut/nginx-rtmp-module/wiki/Directives#on_publish) +to hit an web service with some details and return a non-2xx code to forbid +streaming. Have fun! + +I learned all of this stuff by making a bot which livestreamed Google hangouts +over the LAN to get around the participant limit at work. I'll do a full writeup +about that one later! + +--- + +Here's the full script I'm using to generate the live stream on this +page: + +```sh +#!/bin/sh +rm -f /tmp/playlist +mkdir -p /tmp/dash +for file in /var/www/mirror.sr.ht/hacksway-2018/* +do + echo "file '$file'" >> /tmp/playlist +done + +ffmpeg \ + -re \ + -loglevel error \ + -stream_loop -1 \ + -f concat \ + -safe 0 \ + -i /tmp/playlist \ + -vf "drawtext=\ + fontfile=/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf:\ + text='%{gmtime\:%Y-%m-%d %T} UTC':\ + fontcolor=white:\ + x=(w-text_w)/2:y=128:\ + box=1:boxcolor=black:\ + fontsize=72, + drawtext=\ + fontfile=/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf:\ + text='REBROADCAST':\ + fontcolor=white:\ + x=(w-text_w)/2:y=16:\ + box=1:boxcolor=black:\ + fontsize=48" \ + -codec:v libx264 \ + -profile:v baseline \ + -pix_fmt yuv420p \ + -level 4 \ + -preset veryfast \ + -codec:a aac \ + -f dash \ + -window_size 5 \ + -remove_at_exit 1 \ + /tmp/dash/live.mpd +``` diff --git a/content/blog/Should-you-move-to-sr.ht.md b/content/blog/Should-you-move-to-sr.ht.md @@ -0,0 +1,54 @@ +--- +date: 2018-06-05 +layout: post +title: Should you move from GitHub to sr.ht +tags: [sourcehut] +--- + +I'm not terribly concerned about Microsoft's acquisition of GitHub, but I +don't fault those who are worried. I've been working on my alternative platform, +[sr.ht](https://sr.ht), for quite a while. I'm not about to leave GitHub because +of Microsoft alone. I do have some political disagreements with GitHub and +Microsoft, but those are also not the main reason that I'm building sr.ht. I +simply think I can do it better. If my approach aligns with your needs, then +sr.ht may be the platform for you. + +There are several GitHub alternatives, but for the most part they're basically +GitHub rip-offs. Unlike GitLab, Gogs/Gitea, BitBucket; I don't see the GitHub UX +as the pinnacle of project hosting - there are many design choices (notably pull +requests) which I think have lots of room for improvement. sr.ht instead +embraces git more closely, for example building *on top* of email rather than +*instead of* email. + +GitHub optimizes for the end-user and the drive-by contributor. sr.ht optimizes +for the maintainers and core contributors instead. We have patch queues and +ticket queues which you can set up automated filters in or manually curate, and +are reusable for projects on external platforms. You have tools which allow +you to customize the views you see separately from the views visitors see, like +bugzilla-style custom ticket searches. Our CI service gives you KVM +virtualization and knobs you can tweak to run sophisticated automation for your +project. Finally, all of it is [open +source](https://git.sr.ht/~sircmpwn/?search=sr.ht). + +The business model is also something I think I can do better. GitHub and GitLab +are both VC-funded and trapped into appeasing their shareholders (or now, in +GitHub's case, the needs of Microsoft as a whole). I think this leads to +incentives which don't align with the users, as it's often more important to +support the bottom line than to build what the users want or need. Rather than +trying to raise as much money as possible, the sr.ht aims to be more a +grassroots platform. I'm still working on the money details, but each user will +be expected to pay a subscription fee and growth will be artificially slowed if +necessary to make sure the infrastructure can keep up. In my opinion, venture +capital does not lead to healthy businesses or a healthy economy on the whole, +and I think the users suffer for it. My approach is different. + +As for my own projects and the plan for moving them, I don't intend to move +anything until it won't be disruptive to the project. I've been collecting +feedback from co-maintainers and core contributors to each of the projects I +expect to move and using this feedback to drive sr.ht priorities. They will +eventually move, but only when it's ready. + +I intend to open sr.ht to the public soon, once I have a billing system in place +and break ground on mailing lists (among some smaller improvements). If anyone +is interested in checking it out prior to the public release, shoot me an email +at [sir@cmpwn.com](mailto:sir@cmpwn.com). diff --git a/content/blog/Shut-up-and-get-back-to-work-style.md b/content/blog/Shut-up-and-get-back-to-work-style.md @@ -0,0 +1,61 @@ +--- +date: 2019-04-29 +layout: post +title: The "shut up and get back to work" coding style guide +tags: ["philosophy"] +--- + +So you're starting a new website, and you open the first CSS file. What style do +you use? Well, you hate indenting with spaces passionately. You know tabs are +right because they're literally made for this, and they're only one byte, and +these god damn spaces people with their bloody spacebars... + +Shut up and use spaces. That's how CSS is written[^1]. And you, mister web +programmer, coming out of your shell and dipping your toes into the world of +Real Programming, writing your first Golang program: use tabs, jerk. There's +only one principle that matters in coding style: don't rock the boat. Just do +whatever the most common thing is in the language you're working in. Write your +commit messages the same way as everyone else, too. Then shut up and get back to +work. This hill isn't worth dying on. + +If you're working on someone else's project, this goes double. Don't get snippy +about their coding style. Just follow their style guide, and if there isn't one, +just make your code look like the code around it. It's none of your goddamn +business how they choose to style their code. + +Shut up and get back to work. + +Ranting aside, seriously - which style guide you use doesn't matter nearly as +much as using one. Just pick the one which is most popular or which is already +in use by your peers and roll with it. + +<div style="margin-bottom: 5rem"></div> + +...though since I'm talking about style anyway, take a look at this: + +```c +struct wlr_surface *wlr_surface_surface_at(struct wlr_surface *surface, + double sx, double sy, + double *sub_x, double *sub_y) { + // Do stuff +} +``` + +There's a lot of stupid crap which ends up in style guides, but this is by far +the worst. Look at all that wasted whitespace! There's no room to write your +parameters on the right, and you end up with 3 lines where you could have two. +And you have to mix spaces and tabs! God dammit! This is how you should do it: + +```c +struct wlr_surface *wlr_surface_surface_at(struct wlr_surface *surface, + double sx, double sy, double *sub_x, double *sub_y) { + // Do stuff +} +``` + +Note the extra indent to distinguish the parameters from the body and the +missing garish hellscape of whitespace. If you do this in your codebase, I'm not +going to argue with you about it, but I am going to have to talk to my therapist +about it. + +[^1]: For the record, tabs are objectively better. Does that mean I'm going to write my JavaScript with tabs? Hell no! diff --git a/content/blog/Signal.md b/content/blog/Signal.md @@ -0,0 +1,201 @@ +--- +date: 2018-08-08 +layout: post +title: I don't trust Signal +tags: [privacy] +--- + +Occasionally when Signal is in the press and getting a lot of favorable +discussion, I feel the need to step into various forums, IRC channels, and so +on, and explain why I don't trust Signal. Let's do a blog post instead. + +Off the bat, let me explain that I expect a tool which claims to be secure to +actually be secure. I don't view "but that makes it harder for the average +person" as an acceptable excuse. If Edward Snowden and Bruce Schneier are going +to spout the virtues of the app, I expect it to *actually* be secure when it +matters - when vulnerable people using it to encrypt sensitive communications +are targeted by smart and powerful adversaries. + +Making promises about security without explaining the tradeoffs you made in +order to appeal to the average user is unethical. Tradeoffs are necessary - but +self-serving tradeoffs are not, and it's your responsibility to clearly explain +the drawbacks and advantages of the tradeoffs you make. If you make broad and +inaccurate statements about your communications product being "secure", then +when the political prisoners who believed you are being tortured and hanged, +it's on you. The stakes are serious. Let me explain why I don't think Signal +takes them seriously. + +## Google Play + +Why do I make a big deal out of Google Play and Google Play Services? Well, some +people might trust Google, the company. But up against nation states, it's no +contest - Google has ties to the NSA, has been served secret subpoenas, and is +literally the world's largest machine designed for harvesting and analyzing +private information about their users. Here's what Google Play Services +*actually* is: **a rootkit**. Google Play Services lets Google do silent +background updates on apps on your phone and give them any permission they want. +Having Google Play Services on your phone means your phone is not secure.[^1] + +[^1]: "But how is AOSP any better?" This is a common strawman counter-argument. Fact: There is empirical evidence which shows that Google Play Services does silent updates and can obtain any permission on your phone: a rootkit. There is no empirical evidence to suggest AOSP has similar functionality. + +For the longest time, Signal wouldn't work without Google Play Services, but +Moxie (the founder of Open Whisper Systems and maintainer of Signal) finally +fixed this in 2017. There was also a long time when Signal was only available on +the Google Play Store. Today, you can [download the APK directly from +signal.org](https://signal.org/android/apk/), but... well, we'll +get to that in a minute. + +## F-Droid + +There's an alternative to the Play Store for Android. +[F-Droid](https://f-droid.org) is an open source app "store" (repository would +be a better term here) which only includes open source apps (which Signal +thankfully is). By no means does Signal have to *only* be distributed through +F-Droid - it's certainly a compelling alternative. This has been proposed, and +Moxie has [definitively shut the discussion +down](https://github.com/signalapp/Signal-Android/issues/127). Admittedly this +is from 2013, but his points and the arguments against them haven't changed. Let +me quote some of his positions and my rebuttals: + +>No upgrade channel. Timely and automatic updates are perhaps the most +>effective security feature we could ask for, and not having them would be a +>real blow for the project. + +F-Droid supports updates. If you're concerned about moving your updates quickly +through the (minimal) bureaucracy of F-Droid, you can always run your own +repository. Maybe this is a lot of work?[^2] I wonder how the workload compares +to [animated gif search][gif-shit], a very important feature for security +concious users. I bet that [50 million dollar donation][big-bucks] could help, +given how many people operate F-Droid repositories on a budget of $0. + +[^2]: No, it's not. +[gif-shit]: https://signal.org/blog/signal-and-giphy-update/ +[big-bucks]: https://signal.org/blog/signal-foundation/ + +>No app scanning. The nice thing about market is the server-side APK scanning +>and signature validation they do. If you start distributing APKs around the +>internet, it's a reversion back to the PC security model and all of the +>malware problems that came with it. + +Try searching the Google Play Store for "flashlight" and look at the permissions +of the top 5 apps that come up. All of them are harvesting and selling the +personal information of their users to advertisers. Is this some kind of joke? +F-Droid is a curated repository, like Linux distributions. Google Play is a +malware distributor. Packages on F-Droid are reviewed by a human being and are +[cryptographically signed](https://f-droid.org/en/docs/Signing_Process/). If you +run your own F-Droid repo this is even less of a concern. + +I'm not going to address all of Moxie's points here, because there's a deeper +problem to consider. I'll get into more detail shortly. You can read the +6-year-old threads tearing Moxie's arguments apart over and over again until +GitHub added the feature to lock threads, if you want to see a more in-depth +rebuttal. + +## The APK direct download + +Last year Moxie added an official APK download to signal.org. He said this was +up for "[harm reduction][harm-reduction]", to avoid people using unofficial +builds they find around the net. The download page is covered in warnings +telling you that it's for advanced users only, it's insecure, would you please +go to the Google Play store you stupid user. I wonder, has Moxie considered +communicating to people the risks of using the Google Play version?[^3] + +[^3]: Probably not, because that wouldn't be self-serving. But I'm getting ahead of myself. + +[harm-reduction]: https://github.com/signalapp/Signal-Android/issues/127#issuecomment-286223680 + +The APK direct download doesn't even accomplish the stated goal of "harm +reduction". The user has to manually verify the checksum, and figure out how to +do it on a phone, no less. A checksum isn't a signature, by the way - if your +government- or workplace- or abusive-spouse-installed certificate authority gets +in the way they can replace the APK *and* its checksum with whatever they want. +The app has to update itself, using a similarly insecure mechanism. F-Droid +handles updates and actually signs their packages. This is a no brainer, Moxie, +why haven't you put Signal on F-Droid yet? + +## Why is Signal like this? + +So if you don't like all of this, if you don't like how Moxie approaches these +issues, if you want to use something else, what do you do? + +Moxie knows about everything I've said in this article. He's a very smart guy +and I am under no illusions that he doesn't understand everything I've put +forth. I don't think that Moxie makes these choices because he thinks they're +the right thing to do. He makes arguments which don't hold up, derails threads, +leans on logical fallacies, and loops back around to long-debunked positions +when he runs out of ideas. I think this is deliberate. An open source software +team reads this article as a list of things they can improve on and gets +started. Moxie reads this and prepares for war. Moxie can't come out and +say it openly, but he's made the decisions he has made because they serve his +own interests. + +Lots of organizations which are pretending they don't make self-serving decisions at +their customer's expense rely on argumentative strategies like Moxie does. If +you can put together an argument which on the surface appears reasonable, but +requires in-depth discussion to debunk, passerby will be reassured that your +position is correct, and that the dissenters are just trolls. They won't have +time to read the lengthy discussion which demonstrates that your conclusions +are wrong, especially if you draw the discussion out like Moxie does. It can be +hard to distinguish these from genuine positions held by the person you're +talking to, but when it conveniently allows them to make self-serving plays, +it's a big red flag. + +This is a strong accusation, I know. The thing which convinced me of its truth +is Signal's centralized design and hostile attitude towards forks. In open +source, when a project is making decisions and running things in a way you don't +like, you can always fork the project. This is one of the fundamental rights +granted to you by open source. It has a side effect Moxie doesn't want, however. +It reduces his power over the project. Moxie has a clever solution to this: +centralized servers and trademarks. + +## Trust, federation, and peer-to-peer chat + +Truly secure systems do not require you to trust the service provider. This is +the point of end-to-end encryption. But we have to trust that Moxie is running +the server software he says he is. We have to trust that he isn't writing down a +list of people we've talked to, when, and how often. We have to trust not only +that Moxie is trustworthy, but given that Open Whisper Systems is based in San +Francisco we have to trust that he hasn't received a national security letter, +too (by the way, Signal doesn't have a warrant canary). Moxie can *tell* us he +doesn't store these things, but he could. **Truly secure systems don't require +trust**. + +There are a couple of ways to solve this problem, which can be used in tandem. +We can stop Signal from knowing when we're talking to each other by using +peer-to-peer chats. This has some significant drawbacks, namely that both users +have to be online at the same time for their messages to be delivered to each +other. You can still fall back to peer-to-server-to-peer when one peer is +offline, however. But this isn't the most important of the two solutions. + +The most important change is federation. Federated services are like email, in +that Alice can send an email from gmail.com to Bob's yahoo.com address. I should +be able to stand up a Signal server, on my own hardware where I am in control of +the logs, and communicate freely with other Signal servers, including Open +Whisper's servers. This distributes the security risks across hundreds of +operators in many countries with various data extradition laws. This turns what +would today be easy for the United States government to break and makes it much, +much more difficult. Federation would also open the possibility for bridging the +gap with several other open source secure chat platforms to all talk on the same +federated network - which would spurn competition and be a great move for users +of all chat platforms. + +Moxie forbids you from distributing branded builds of the Signal app, and if you +rebrand he forbids you from using the official Open Whisper servers. Because his +servers don't federate, that means that users of Signal forks *cannot talk to +Signal users*. This is a truly genius move. No fork of Signal[^4] to date has +ever gained any traction, and never will, because you can't talk to any Signal +users with them. In fact, there are no third-party applications which can +interact with Signal users in any way. Moxie can write as many blog posts which +appeal to wispy ideals and "moving ecosystems" as he wants[^5], but those are +all *really* convenient excuses for an argument which allows him to design +systems which serve his own interests. + +[^4]: See [LibreSignal](https://github.com/LibreSignal/LibreSignal) and [Silence](https://github.com/SilenceIM/Silence#silence-), particularly [this thread](https://github.com/LibreSignal/LibreSignal/issues/37#issuecomment-217211165). +[^5]: See [Reflections: The ecosystem is moving](https://signal.org/blog/the-ecosystem-is-moving/). Yes, that's the unedited title. + +No doubt these are non-trivial problems to solve. But I have *personally* been +involved in open source projects which have collectively solved similarly +difficult problems a thousand times over with a combined budget on the order of +tens of thousands of dollars. + +What were you going to do with that 50 million dollars again? diff --git a/content/blog/Simple-correct-fast.md b/content/blog/Simple-correct-fast.md @@ -0,0 +1,48 @@ +--- +date: 2018-07-09 +layout: post +title: "Simple, correct, fast: in that order" +tags: [philosophy] +--- + +The single most important quality in a piece of software is simplicity. It's +more important than doing the task you set out to achieve. It's more important +than performance. The reason is straightforward: if your solution is not simple, +it will not be correct or fast. + +Given enough time, you'll find that all software which solves sufficiently +complex problems is going to (1) have bugs and (2) have performance problems. +Software with bugs is incorrect. Software with performance problems is not fast. +We will face this fact as surely as we will face death and taxes, and we should +prepare ourselves accordingly. Let's consider correctness first. + +Complicated software breaks. Simple software is more easily understood and far +less prone to breaking: there are less moving pieces, less lines of code to keep +in your head, and fewer edge cases. Simple software is more easily tested as +well - after all, fewer code paths to run through. Sure, simple software *does* +break, and when it does the cause and appropriate solution are often apparent. + +Now let's consider performance. You may have some suspicions about your +bottlenecks when you set out, and you should consider them in your approach. +However, when the performance bill comes due, you're more likely to have +overlooked something than not. The only way to find out for sure what's slow is +to measure. Which is easier to profile: a complicated program, or a simple one? +Anyone who's looked at a big enough flame graph knows exactly what I'm talking +about. + +Perhaps complicated software once solved a problem. That software needs to be +maintained - what is performant and correct today will not be tomorrow. The +workload will increase, or the requirements will change. Software is a living +thing! When you're stressed out at 2 AM on Tuesday morning because the server +shat itself because your 1,831st new customer pushed the billing system over the +edge, do you think you're well equipped to find the problem in a complex piece +of code you last saw a year ago? + +When you are faced with these problems, you must seek out the simplest way they +can be solved. This may be difficult to do: perhaps the problem is too large, or +perhaps you were actually considering the solution before considering the +problem. Though difficult it may be, it is your most important job. You need to +take problems apart, identify smaller problems within them and ruthlessly remove +scope until you find the basic problem you can apply a basic solution to. The +complex problem comes later, and it'll be better served by the composition of +simple solutions than with the application of a complex solution. diff --git a/content/blog/Slow.md b/content/blog/Slow.md @@ -0,0 +1,543 @@ +--- +date: 2020-01-04 +layout: basic +title: Hello world +--- + +Let's say you ask your programming language to do the simplest possible task: +print out "hello world". Generally this takes two syscalls: write and exit. +The following assembly program is the ideal Linux x86_64 program for this +purpose. A perfect compiler would emit this hello world program for any +language. + +``` +bits 64 +section .text +global _start +_start: + mov rdx, len + mov rsi, msg + mov rdi, 1 + mov rax, 1 + syscall + + mov rdi, 0 + mov rax, 60 + syscall + +section .rodata +msg: db "hello world", 10 +len: equ $-msg +``` + +Most languages do a whole lot of other crap other than printing out "hello +world", even if that's all you asked for. + +<table class="table table-bordered"> + <thead> + <tr> + <th>Test case</th> + <th>Source</th> + <th>Execution time</th> + <th>Total syscalls</th> + <th>Unique syscalls</th> + <th>Size (KiB)</th> + </tr> + </thead> + <tbody> + <tr> + <td><strong>Assembly</strong> (x86_64)</td> + <td> + <a href="#tests">test.S</a> + </td> + <td>0.00s real</td> + <td>2</td> + <td>2</td> + <td>8.6 KiB*</td> + </tr> + <tr> + <td><strong>Zig</strong> (small)</td> + <td> + <a href="#testzig">test.zig</a> + </td> + <td>0.00s real</td> + <td>2</td> + <td>2</td> + <td>10.3 KiB</td> + </tr> + <tr> + <td><strong>Zig</strong> (safe)</td> + <td> + <a href="#testzig">test.zig</a> + </td> + <td>0.00s real</td> + <td>3</td> + <td>3</td> + <td>11.3 KiB</td> + </tr> + <tr> + <td><strong>C</strong> (musl, static)</td> + <td> + <a href="#testc">test.c</a> + </td> + <td>0.00s real</td> + <td>5</td> + <td>5</td> + <td>95.9 KiB</td> + </tr> + <tr> + <td><strong>C</strong> (musl, dynamic)</td> + <td> + <a href="#testc">test.c</a> + </td> + <td>0.00s real</td> + <td>15</td> + <td>9</td> + <td>602 KiB</td> + </tr> + <tr> + <td><strong>C</strong> (glibc, static*)</td> + <td> + <a href="#testc">test.c</a> + </td> + <td>0.00s real</td> + <td>11</td> + <td>9</td> + <td>2295 KiB</td> + </tr> + <tr> + <td><strong>C</strong> (glibc, dynamic)</td> + <td> + <a href="#testc">test.c</a> + </td> + <td>0.00s real</td> + <td>65</td> + <td>13</td> + <td>2309 KiB</td> + </tr> + <tr> + <td><strong>Rust</strong></td> + <td> + <a href="#testrs">test.rs</a> + </td> + <td>0.00s real</td> + <td>123</td> + <td>21</td> + <td>244 KiB</td> + </tr> + <tr> + <td><strong>Crystal</strong> (static)</td> + <td> + <a href="#testcr">test.cr</a> + </td> + <td>0.00s real</td> + <td>144</td> + <td>23</td> + <td>935 KiB</td> + </tr> + <tr> + <td><strong>Go</strong> (static w/o cgo)</td> + <td> + <a href="#testgo">test.go</a> + </td> + <td>0.00s real</td> + <td>152</td> + <td>17</td> + <td>1661 KiB</td> + </tr> + <tr> + <td><strong>D</strong> (dmd)</td> + <td> + <a href="#testd">test.d</a> + </td> + <td>0.00s real</td> + <td>152</td> + <td>20</td> + <td>5542 KiB</td> + </tr> + <tr> + <td><strong>D</strong> (ldc)</td> + <td> + <a href="#testd">test.d</a> + </td> + <td>0.00s real</td> + <td>181</td> + <td>21</td> + <td>10305 KiB</td> + </tr> + <tr> + <td><strong>Crystal</strong> (dynamic)</td> + <td> + <a href="#testcr">test.cr</a> + </td> + <td>0.00s real</td> + <td>183</td> + <td>25</td> + <td>2601 KiB</td> + </tr> + <tr> + <td><strong>Go</strong> (w/cgo)</td> + <td> + <a href="#testgo">test.go</a> + </td> + <td>0.00s real</td> + <td>211</td> + <td>22</td> + <td>3937 KiB</td> + </tr> + <tr> + <td><strong>Perl</strong></td> + <td> + <a href="#testpl">test.pl</a> + </td> + <td>0.00s real</td> + <td>255</td> + <td>25</td> + <td>5640 KiB</td> + </tr> + <tr> + <td><strong>Java</strong></td> + <td> + <a href="#testjava">Test.java</a> + </td> + <td>0.07s real</td> + <td>226</td> + <td>26</td> + <td>15743 KiB</td> + </tr> + <tr> + <td><strong>Node.js</strong></td> + <td> + <a href="#testjs">test.js</a> + </td> + <td>0.04s real</td> + <td>673</td> + <td>40</td> + <td>36000 KiB</td> + </tr> + <tr> + <td><strong>Python 3</strong> (PyPy)</td> + <td> + <a href="#testpy">test.py</a> + </td> + <td>0.68s real</td> + <td>884</td> + <td>32</td> + <td>9909 KiB</td> + </tr> + <tr> + <td><strong>Julia</strong></td> + <td> + <a href="#testjl">test.jl</a> + </td> + <td>0.12s real</td> + <td>913</td> + <td>41</td> + <td>344563 KiB</td> + </tr> + <tr> + <td><strong>Python 3</strong> (CPython)</td> + <td> + <a href="#testpy">test.py</a> + </td> + <td>0.02s real</td> + <td>1200</td> + <td>33</td> + <td>15184 KiB</td> + </tr> + <tr> + <td><strong>Ruby</strong></td> + <td> + <a href="#testrb">test.rb</a> + </td> + <td>0.04s real</td> + <td>1401</td> + <td>38</td> + <td>1283 KiB</td> + </tr> + </tbody> +</table> + +<div style="text-align: right"> + <small>* See notes for this test case</small> +</div> + +This table is sorted so that the number of syscalls goes up, because I reckon +more syscalls is a decent metric for how much shit is happening that you didn't +ask for (i.e. `write("hello world\n"); exit(0)`). Languages with a JIT fare much +worse on this than compiled languages, but I have deliberately chosen not to +account for this. + +These numbers are real. This is more complexity that someone has to debug, more +time your users are sitting there waiting for your program, less disk space +available for files which actually matter to the user. + +### Environment + +Tests were conducted on January 3rd, 2020. + +- gcc 9.2.0 +- glibc 2.30 +- musl libc 1.1.24 +- Linux 5.4.7 (Arch Linux) +- Linux 4.19.87 (vanilla, Alpine Linux) is used for musl libc tests +- Go 1.13.5 +- Rustc 1.40.0 +- Zig 0.5.0 +- OpenJDK 11.0.5 JRE +- Crystal 0.31.1 +- NodeJS 13.5.0 +- Julia 1.3.1 +- Python 3.8.1 +- PyPy 7.3.0 +- Ruby 2.6.4p114 (2019-10-01 rev 67812) +- dmd 1:2.089.0 +- ldc 2:1.18.0 +- Perl 5.30.1 + +For each language, I tried to write the program which would give the most +generous scores without raising eyebrows at a code review. The size of all +files which must be present at runtime (interpreters, stdlib, libraries, loader, +etc) are included. Binaries were stripped where appropriate. + +This was not an objective test, this is just an approximation that I hope will +encourage readers to be more aware of the consequences of their abstractions, +and their exponential growth as more layers are added. + +### test.S + +``` +bits 64 +section .text +global _start +_start: + mov rdx, len + mov rsi, msg + mov rdi, 1 + mov rax, 1 + syscall + + mov rdi, 0 + mov rax, 60 + syscall + +section .rodata +msg: db "hello world", 10 +len: equ $-msg +``` + +``` +nasm -f elf64 test.S +gcc -o test -static -nostartfiles -nostdlib -nodefaultlibs +strip test: 8.6 KiB +``` + +**Notes** + +- This program only works on x86_64 Linux. +- The size depends on how you measure it:<br /> + *Instructions + data alone*: 52 bytes<br /> + *Stripped ELF*: 8.6 KiB<br /> + *Manually minified ELF*: [142 bytes](http://timelessname.com/elfbin/) + +### test.zig + +``` +const std = @import("std"); + +pub fn main() !void { + const stdout = try std.io.getStdOut(); + try stdout.write("hello world\n"); +} +``` + +``` +# small +zig build-exe test.zig --release-small --strip +# safe +zig build-exe test.zig --release-safe --strip +``` + +**Notes** + +- Written with the assistance of Andrew Kelly (maintainer of Zig) + +### test.c + +``` +int puts(const char *s); + +int main(int argc, char *argv[]) { + puts("hello world"); + return 0; +} +``` + +``` +# dynamic +gcc -O2 -o test test.c +strip test + +# static +gcc -O2 -o test -static test.c +strip test +``` + +**Notes** + +- glibc programs can never truly be statically linked. The size reflects this. + +### test.rs + +``` +fn main() { + println!("hello world"); +} +``` + +``` +rustc -C opt-levels=s test.rs +``` + +**Notes** + +- The final binary is dynamically linked with glibc, which is included in the + size. + +### test.go + +``` +package main + +import "os" + +func main() { + os.Stdout.Write([]byte("hello world\n")) +} +``` + +``` +# dynamic +go build -o test test.go + +# static w/o cgo +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o test -ldflags '-extldflags "-f no-PIC -static"' -buildmode pie -tags 'osusergo netgo static_build' test.go +``` + +Aside: it is getting way too goddamn difficult to build static Go binaries. + +**Notes** + +- The statically linked test was run on Alpine Linux with musl libc. It doesn't + link to libc in theory, but hey. + +### Test.java + +``` +public class Test { + public static void main(String[] args) { + System.out.println("hello world"); + } +} +``` + +``` +javac Test.java +java Test +``` + +### test.cr + +``` +puts "hello world\n" +``` + +``` +# Dynamic +crystal build -o test test.cr + +# Static +crystal build --static -o test test.cr +``` + +**Notes** + +- The Crystal tests were run on Alpine Linux with musl libc. + +### test.js + +``` +console.log("hello world"); +``` + +``` +node test.js +``` + +### test.jl + +``` +println("hello world") +``` + +``` +julia test.jl +``` + +**Notes** + +- Julia numbers were provided by a third party + +### test.py + +``` +print("hello world") +``` + +``` +# cpython +python3 test.py +# pypy +pypy3 test.py +``` + +### test.pl + +``` +print "hello world\n" +``` + +``` +perl test.pl +``` + +**Notes** + +- Passing /dev/urandom into perl is equally likely to print "hello world" + +### test.d + +``` +import std.stdio; +void main() +{ + writeln("hello world"); +} +``` + +``` +# dmd +dmd -O test.d +# ldc +ldc -O test.d +``` + +### test.rb + +``` +puts "hello world\n" +``` + +``` +ruby test.rb +``` diff --git a/content/blog/State-of-Sway-August-2017.md b/content/blog/State-of-Sway-August-2017.md @@ -0,0 +1,63 @@ +--- +date: 2017-08-09 +# vim: tw=80 +title: State of Sway August 2017 +layout: post +tags: [sway] +--- + +Is it already time to write another one of these? Phew, time flies. Sway marches +ever forward. Sway 0.14.0 was recently released, adding much asked-after support +for tray icons and fixing some long-standing bugs. As usual, we already have +some exciting features slated for 0.15.0 as well, notably some cool improvements +to clipboard support. Look forward to it! + +Today Sway has 24,123 lines of C (and 4,489 lines of header files) written by 94 +authors across 2,345 commits. These were written through 689 pull requests and +624 issues. Sway packages are available today in the repos of almost every Linux +distribution. + +[![](https://sr.ht/ICd5.png)](https://sr.ht/ICd5.png) + +For those who are new to the project, [Sway](http://swaywm.org) is an +i3-compatible Wayland compositor. That is, your existing [i3](http://i3wm.org/) +configuration file will work as-is on Sway, and your keybindings and colors and +fonts and for_window rules and so on will all be the same. It's i3, but for +Wayland, plus it's got some bonus features. Here's a quick rundown of what's +new since the [previous state of Sway](/2017/04/29/State-of-sway-April-2017.html): + +* Initial support for tray icons +* X11/Wayland clipboard synchronization +* nvidia proprietary driver support* +* i3's marks +* i3's mouse button bindings +* Lots of i3 compatibility improvements +* Lots of documentation improvements +* Lots of bugfixes + +If this seems like a shorter list than usual, it's because we've also been +making great progress on wlroots too - no doubt thanks to the help of the many +contributors doing amazing work in there. For those unaware, wlroots is our +project to replace wlc with a new set of libraries for Wayland compositor +underpinnings (it fills a similar niche as libweston). We now have a working DRM +backend (including output rotation and hardware cursors) and libinput backend +(including touchscreen and drawing tablet support), and we're making headway now +on drawing Wayland clients on screen. I'm very excited about our pace and +direction - keep an eye on it +[here](https://github.com/SirCmpwn/wlroots/issues/9). I have also taken over for +Cloudef as the maintainer of [wlc](https://github.com/Cloudef/wlc) during the +transition. + +In other news, our bounty program continues to go strong. Our [current +pot](https://github.com/SirCmpwn/sway/issues/986) is $1200 and we've paid out +$80 so far (and a $280 payout is on the horizon for tray icons). I've also +started a [Patreon page](https://www.patreon.com/sircmpwn), where 26 patrons are +generously supporting my work as maintainer of Sway and other projects. Many +thanks to everyone who has contributed financially to Sway's success! + +That wraps up today's post. Thanks for using Sway! + +<small class="text-muted">* I hate this crappy driver. It works, but don't +expect to receive much support for it. <a +href="https://www.youtube.com/watch?v=iYWzMvlj2RQ">Linus said it +best</a>.</small> diff --git a/content/blog/State-of-sway-April-2017.md b/content/blog/State-of-sway-April-2017.md @@ -0,0 +1,82 @@ +--- +date: 2017-04-29 +# vim: tw=80 +title: State of Sway April 2017 +layout: post +tags: [sway] +--- + +Development on Sway continues. I thought we would have slowed down a lot more by +now, but every release still comes with new features - Sway 0.12 added +redshift support and binary space partitioning layouts. Sway 0.13.0 is +coming soon and includes, among other things, nvidia proprietary driver support. +We already have some interesting features slated for Sway 0.14.0, too! + +Today Sway has 21,446 lines of C (and 4,261 lines of header files) written by 81 +authors across 2,263 commits. These were written through 653 pull requests and +529 issues. Sway packages are available today in the official repos of pretty +much every distribution except for Debian derivatives, and a PPA is available +for those guys. + +[![](https://sr.ht/ICd5.png)](https://sr.ht/ICd5.png) + +For those who are new to the project, [Sway](http://swaywm.org) is an +i3-compatible Wayland compositor. That is, your existing [i3](http://i3wm.org/) +configuration file will work as-is on Sway, and your keybindings and colors and +fonts and for_window rules and so on will all be the same. It's i3, but for +Wayland, plus it's got some bonus features. Here's a quick rundown of what's +new since the [previous state of Sway](/2016/12/27/State-of-sway.html): + +* Redshift support +* Improved security configuration +* Automatic binary space partitioning layouts ala AwesomeWM +* Support for more i3 window criterion +* Support for i3 marks +* xdg_shell v6 support (Wayland thing, makes more native Wayland programs work) +* We've switched from X.Y to X.Y.Z releases, Z releases shipping bugfixes while + the next Y release is under development +* Lots of i3 compatibility improvements +* Lots of documentation improvements +* Lots of bugfixes + +The new [bounty program](https://github.com/SirCmpwn/sway/issues/986) has also +raised $1,200 to support Sway development! Several bounties have been awarded, +including redshift support and i3 marks, but every awardee chose to redonate +their reward to the bounty pool. Thanks to everyone who's donated and everyone +who's worked on new features! Bounties have also been awarded for features in +the Wayland ecosystem beyond Sway - a fact I'm especially proud of. If you want +a piece of that $1,200 pot, [join us on +IRC](http://webchat.freenode.net/?channels=sway&uio=d4) and we'll help you get started. + +Many new developments are in the pipeline for you. 0.13.0 is expected to +ship within the next few weeks - [here's a sneak peek at the +changelog](https://github.com/SirCmpwn/sway/issues/1162#issuecomment-295012255). +In the future releases, development is ongoing for tray icons (encouraged by the +sweet $270 bounty sitting on that feature), and several other features for +0.14.0 have been completed. We've also started work on a long term project to +replace our compositor plumbling library, wlc, with a new one: +[wlroots](https://github.com/SirCmpwn/wlroots). This should allow us to fix many +of the more difficult bugs in Sway, and opens the doors for *many* features that +weren't previously possible. It should also give us a platform on which we can +build standard protocols that other compositors can implement, unifying the +Wayland platform a bit more. + +Many thanks to [everyone that's contributed to +sway](https://github.com/SirCmpwn/sway/graphs/contributors)! There's no way Sway +would have enjoyed its success without your help. That wraps things up for +today, thanks for using Sway and look forward to Sway 1.0! + +--- + +Note: future posts like this will omit some of the stats that were included in +the previous posts. You can use the following commands to find them for +yourself: + +```bash +# Lines of code per author: +git ls-tree -r -z --name-only HEAD -- */*.c \ + | xargs -0 -n1 git blame --line-porcelain HEAD \ + | grep "^author " | sort | uniq -c | sort -nr +# Commits per author: +git shortlog +``` diff --git a/content/blog/State-of-sway.md b/content/blog/State-of-sway.md @@ -0,0 +1,122 @@ +--- +date: 2016-12-27 +# vim: tw=80 +# Commands used to generate these stats: +# LoC per author: git ls-tree -r -z --name-only HEAD -- */*.c | xargs -0 -n1 git blame --line-porcelain HEAD |grep "^author "|sort|uniq -c|sort -nr +# Commits per author: git shortlog +title: State of Sway December 2016 - secure your Wayland desktop, get paid to work on Sway +layout: post +tags: [sway] +--- + +Earlier today I released [sway +0.11](https://github.com/SirCmpwn/sway/releases/tag/0.11), which (along with +lots of the usual new features and bug fixes) introduces support for security +policies that can help realize the promise of a secure Wayland desktop. We also +just started a bounty program that lets you sponsor the things you want done and +rewards contributors for working on them. + +Today sway has 19,371 lines of C (and 3,761 lines of header files) written by 70 +authors across 2,067 commits. These were written through 589 pull requests and +425 issues. Sway packages are available today in the official repos of Arch, +Gentoo, Fedora, NixOS, openSUSE, Void Linux, and more. Sway looks like this: + +[![](https://sr.ht/ICd5.png)](https://sr.ht/ICd5.png) + +Side note: please add pretty screenshots of sway to [this wiki +page](https://github.com/SirCmpwn/sway/wiki/Screenshots-of-Sway). Thanks! + +For those who are new to the project, [Sway](http://swaywm.org) is an +i3-compatible Wayland compositor. That is, your existing [i3](http://i3wm.org/) +configuration file will work as-is on Sway, and your keybindings and colors and +fonts and for_window rules and so on will all be the same. It's i3, but for +Wayland, plus it's got some bonus features. Here's a quick rundown of what's +new since the [previous state of Sway](/2016/08/02/Sway-0.9-in-retro.html): + +* Security policy configuration (man sway-security) +* FreeBSD support +* Initial support for HiDPI among sway clients (swaybar et al) +* Support for new i3 features +* Clicky title bars +* Lots of i3 compatability improvements +* Lots of documentation improvements +* Lots of bugfixes + +Today it seems that most of the features sway needs are implemented. Work hasn't +slowed down - there's been lots of work fixing small bugs, improving +documentation, fixing subtle incompatabilities with i3, and so on. However, to +encourage the development of new features, I've officially put into action the +new bounty program today. Here's how it works - you can donate to the features +you want to see, and you can claim the donations by implementing the features +and sending a pull request. To date I've received about $200 in donations +towards sway, and I've matched that with a donation of my own to bring it up to +$400. I've distributed these donations into various buckets of features. Not +every feature is for sway - anything that improves the sway experience is +eligible for a bounty, and in fact over half of the initial bounties are for +features in other parts of the ecosystem. For details on the program, check out +[this link](https://github.com/SirCmpwn/sway/issues/986). + +Here's the updated stats. First, **lines of code per author**: + +<table class="table"> + <tbody> + <tr><td>3799 (+775)</td><td>Drew DeVault</td></tr> + <tr><td>3489 (-1170)</td><td>Mikkel Oscar Lyderik</td></tr> + <tr><td>1705 (-527)</td><td>taiyu</td></tr> + <tr><td>1236 (-550)</td><td>S. Christoffer Eliesen</td></tr> + <tr><td>1160 (+70)</td><td>Zandr Martin</td></tr> + <tr><td>449 (-12)</td><td>minus</td></tr> + <tr><td>311 (-54)</td><td>Christoph Gysin</td></tr> + <tr><td>285 (+285)</td><td>D.B</td></tr> + <tr><td>247 (-87)</td><td>Kevin Hamacher</td></tr> + <tr><td>227 (-298)</td><td>Cole Mickens</td></tr> + <tr><td>219 (+219)</td><td>David Eklov</td></tr> + </tbody> +</table> + +Finally, I'm the top contributor! I haven't been on top for over a year. Lots of +the top contributors are slowly having their lines of code reduced as lots of +new contributors are coming in and displacing them with refactorings and bug +fixes. + +Here's the total **number of commits per author** for each of the top ten +committers: + +<table class="table"> + <tbody> + <tr><td>1009</td><td> Drew DeVault</td></tr> + <tr><td>245</td><td> Mikkel Oscar Lyderik</td></tr> + <tr><td>153</td><td> taiyu</td></tr> + <tr><td>97</td><td> Luminarys</td></tr> + <tr><td>91</td><td> S. Christoffer Eliesen</td></tr> + <tr><td>68</td><td> Zandr Martin</td></tr> + <tr><td>58</td><td> Christoph Gysin</td></tr> + <tr><td>45</td><td> D.B</td></tr> + <tr><td>33</td><td> Taiyu</td></tr> + <tr><td>32</td><td> minus</td></tr> + </tbody> +</table> + +Most of what I do for Sway personally is reviewing and merging pull requests. +Here's the same figures using **number of commits per author, excluding merge +commits**, which changes my stats considerably: + +<table class="table"> + <tbody> + <tr><td>479</td><td> Drew DeVault</td></tr> + <tr><td>229</td><td> Mikkel Oscar Lyderik</td></tr> + <tr><td>138</td><td> taiyu</td></tr> + <tr><td>96</td><td> Luminarys</td></tr> + <tr><td>91</td><td> S. Christoffer Eliesen</td></tr> + <tr><td>58</td><td> Christoph Gysin</td></tr> + <tr><td>56</td><td> Zandr Martin</td></tr> + <tr><td>45</td><td> D.B</td></tr> + <tr><td>32</td><td> Taiyu</td></tr> + <tr><td>32</td><td> minus</td></tr> + </tbody> +</table> + +These stats only cover the top ten in each, but there are more - check out the +[full list](https://github.com/SirCmpwn/sway/graphs/contributors). + +Here's looking forward to sway 1.0 in 2017! diff --git a/content/blog/Status-update-August-2019.md b/content/blog/Status-update-August-2019.md @@ -0,0 +1,79 @@ +--- +date: 2019-08-15 +layout: post +title: Status update, August 2019 +tags: ["status update"] +--- + +Outside my window, the morning sun can be seen rising over the land of the +rising sun, as I sip from a coffee purchased at the konbini down the street. I +almost forgot to order it, as the staffer behind the counter pointed out with a +smile and a joke that, having been told in Japanese, mostly went over my head. +It's on this quiet Osaka morning I write today's status update - there are lots +of existing developments to share! + +Let's start with sourcehut news. I deployed a cool feature yesterday - SSH +access to builds.sr.ht. You can now SSH into a failed build to examine the +failure and investigate the root cause. You can also get a shell on-demand for +any build image, including for experimental arm64 support. I'll be writing a +full-length blog post going into detail about this feature later in the week. +Additionally, with contributor Ryan Chan's help, man.sr.ht received a huge +overhaul which moved wikis out of man.sr.ht's dedicated git subsystem and into +git.sr.ht repositories, allowing you to make your wiki out of a branch of your +main project repo or browse the git data on the web. I'll be posting more sr.ht +news to sr.ht-announce later today if you want to hear more! + +![Screenshot of a failed build on builds.sr.ht offering SSH access to the build +environment](https://sr.ht/thL-.png) + +[aerc 0.2.0](https://git.sr.ht/~sircmpwn/aerc/refs/0.2.0) has been released, +which included nearly 200 changes from 34 contributors. I'm grateful to the +community for this crazy amount of support - working together we'll make aerc +amazing in no time. Highlights include maildir and sendmail transports, search +and filtering, support for `mailto:` links, tab completion, and more. We haven't +slowed down since, and the next release already has support lined up for +notmuch, more tab completion support, and more features for mail composition. In +related news, Greg Kroah-Hartman of Linux kernel fame was kind enough to [write +up](http://www.kroah.com/log/blog/2019/08/14/patch-workflow-with-mutt-2019/) +details about his email workflow to help guide the direction of aerc. I'll be +writing a follow-up post next week explaining how aerc aims to solve the +problems he lays out. + +Sway and wlroots continue chugging along as well, with the release of Sway +1.2-rc1 coming earlier this week. This release adds many features from the +recent i3 4.17 release, and adds a handful of small features and bug fixes. The +corresponding wlroots release will be pretty cool, too, adding support for +direct scanout and fixing dozens of bugs. I'd like to draw your attention as +well to a cool project from the Sway community: Jason Francis's +[wdisplays](https://github.com/cyclopsian/wdisplays), a GUI for arranging and +configuring displays on wlroots-based desktops. The changes necessary for it to +work will land in sway 1.2, and users building from git can try it out today. + +![](https://sr.ht/iyU4.png) + +On the DRM leasing and VR for Wayland work I was discussing in the last update, +I'm happy to share that I've got it working with SteamVR! I've written a +[detailed blog post](/2019/08/09/DRM-leasing-and-VR-for-Wayland.html) which +explains all of the work that went into this project, if you want to learn about +it in depth and watch some cool videos summing up the work. There's still a lot +of work to do in negotiating the standardization of new interfaces to support +this feature in several projects, but all of the unknowns have been discovered +and answered. We will have VR on Wayland soon. I plan on making my way to the +[Monado](https://monado.dev/) and [OpenXR](https://www.khronos.org/openxr) to +help realize a top-to-bottom free software VR stack designed with Wayland in +mind. I'll also be joining many members of the wlroots gang at +[XDC](https://xdc2019.x.org/) in October, where I hope to meet the people +working on OpenXR. + +I've also invested more time into my Wayland book, because I've realized that at +my current pace it won't be done any time soon. It's now about half complete and +I've picked up the pace considerably. If you're interested in helping review the +drafts, please let me know! + +That's all for today. Thank you for your continued support! + +<small class="text-muted"> +This work was possible thanks to users who support me financially. Please +consider <a href="/donate">donating to my work</a> or <a +href="https://sourcehut.org">buying a sourcehut.org subscription</a>. Thank you! +</small> diff --git a/content/blog/Status-update-July-2019.md b/content/blog/Status-update-July-2019.md @@ -0,0 +1,90 @@ +--- +date: 2019-07-15 +layout: post +title: Status update, July 2019 +tags: ["status update"] +--- + +Today I received the keys to my new apartment, which by way of not being +directly in the middle of the city[^1] saves me a decent chunk of money - and +allows me to proudly announce that I have officially broken even on doing free +software full time! I owe a great deal of thanks to all of you who have [donated +to support my work](https://drewdevault.com/donate) or purchased a paid +[SourceHut](https://sourcehut.org) account. I've dreamed of sustainably working +on free software for a long, long time, and I'm very grateful for all of your +support in helping realize that dream. Now let me share with you what your money +has bought over the past month! + +[^1]: I can see city hall out the window of my old apartment + +First, my [make a blog](https://drewdevault.com/make-a-blog) offer has closed +for the time being, and the world is now 13 blogs richer for it. Be sure to +check them out! I have also started a mailing list for tech writers: the [free +writers club](https://lists.sr.ht/~sircmpwn/free-writers-club), which I +encourage anyone using free software to blog about technology to join for +editorial advice, software recommendations, and periodic reminders to keep +writing. The offer to get paid for your own new blog will reopen in the future, +keep an eye out! + +As far as projects are concerned, lots of good stuff this month. aerc has been +making excellent progress. We just pulled in the first batch of patches adding +maildir support, and will soon have sendmail and mbox support as well. We've +also begun on mouse support, and you can now click to switch between tabs. The +initial patches for tab completion have also been added. Additional changes +include an :unsubscribe command to unsubscribe from marketing emails and mailing +lists, basic search functionality, OAuth IMAP authentication, changing config +options at runtime, and DNS lookups to complete your settings in the new account +wizard more quickly. Building more upon these features, and a handler for mailto +links, are the main blockers for aerc 0.2.0. + +In Wayland news, VR work continues. I've taken on the goal of implementing DRM +leasing for Wayland, which will allow VR applications to take exclusive control +over the headset's graphical resources from Wayland compositor. A similar +technology exists for X11, and I've written a Wayland protocol for the same +purpose on Wayland. I've also written a Vulkan extension to utilize this +protocol in Vulkan's WSI layer. I've written implementations of these for +wlroots, sway, mesa, and the radv (AMD) Vulkan driver. The result: a working VR +demo on Sway (audio warning): + +<video src="https://yukari.sr.ht/xrgears.webm" controls></video> + +There's still some details to sort out on the standardization of these +extensions, which are under discussion now. In the coming weeks I hope to have +an implementation for Xwayland (which will get working games based on Steam's +OpenVR runtime), and get a proof-of-concept of a VR-driven Wayland compositor +based on the demo shown in the previous status update. Exciting stuff! + +I've also had time to write a few more chapters for my Wayland book, which I'll +be speeding up my work on. I'll soon be leaving for an extended trip to Japan, +and on these grueling flights I'll have plenty of time to work on it. In +additional Wayland news, we've been chugging along with small bugfixes and +improvements to wlroots and sway, and implementing more plumbing work to round +out our implementation of everything. Our work continues to evolve into the most +robust Wayland implementation available today, and I can only see it getting +stronger. + +On SourceHut, I have plenty of developments to share, but will leave the details +for the [sr.ht-announce mailing +list](https://lists.sr.ht/~sircmpwn/sr.ht-announce). The most exciting news is +that [Alpine Linux](https://alpinelinux.org), my favorite Linux distribution, +has completed their mailing list infrastructure migration to [their own +lists.sr.ht instance](https://lists.alpinelinux.org)! I've also been hard at +work expanding lists.sr.ht's capabilities to this end. The other big piece of +news was announced on my blog last week: [code +annotations](https://drewdevault.com/2019/07/08/Announcing-annotations-for-sourcehut.html). +All of our services have also been upgraded to Alpine 3.10, and the Alpine +mirror reorganized a bit to make future upgrades smooth. There's all sorts of +other goodies to share, but I'll leave the rest for the sr.ht-announce post +later today. + +All sorts of other little things have gotten done, like sending patches upstream +for kmscube fixes, minor improvements to scdoc, writing a new build system for +mrsh, improvements to openring... but I'm running out of patience and I imagine +you are, too. Again I'm eternally grateful for your support: thank you. I'll see +you again for the next status update, same time next month! + +<small class="text-muted"> +This work was possible thanks to users who support me financially. Please +consider <a href="/donate">donating to my work</a> or <a +href="https://sourcehut.org">buying a sourcehut.org subscription</a>. Thank you! +</small> diff --git a/content/blog/Status-update-June-2019.md b/content/blog/Status-update-June-2019.md @@ -0,0 +1,105 @@ +--- +date: 2019-06-15 +layout: post +title: Status update, June 2019 +tags: ["status updates"] +--- + +Summer is in full swing here in Philadelphia. Last night I got great views of +Jupiter and a nearly-full Moon, and my first Saturn observation of the year. I +love astronomy on clear Friday nights, there's always plenty of people coming +through the city. And today, on a relaxing lazy Saturday, waiting for friends +for dinner later, I have the privilege of sharing another status report with +you. + +First, I want to talk about some work I've done with blogs lately. On the bottom +of this article you'll find a few blog posts from around the net. This is +populated with [openring](https://git.sr.ht/~sircmpwn/openring), a small Go tool +I made to fetch a few articles from a list of RSS feeds. A couple of other +people have added this to their own sites as well, and I hope to use this to +encourage the growth of a network of bloggers supporting each other without any +nonfree or centralized software. I'll write about this in its own article in +time. I've also made an [open offer](/make-a-blog) to give $20 to anyone who +wants to make their own blog, and so far 5 new blogs have taken me up on the +offer. Maybe you'll be the next? + +Other side projects have seen some nice progress this month, too. +[Wio](https://git.sr.ht/~sircmpwn/wio) has received a few patches from Leon +Plickat improving the UX, and I understand more are on the way. I'm also happy +to tell you that the RISC-V musl libc port I was working on is heading upstream +and slated for inclusion in the next release! Big thanks to everyone who helped +with that, and to Rich Felker for reviewing it and assembling the final patches. +I was also able to find some time this month to contribute to +[mrsh](https://git.sr.ht/~emersion/mrsh), adding support for job IDs, the +`wait`, `break`, and `continue` builtins, and a handful of other improvements. +I'm really excited about mrsh, it's getting close to completion. My friend +Luminarys also finally released [synapse 1.0](https://synapse-bt.org/), a +bittorrent client that I had a [hand in +designing](https://github.com/Luminarys/synapse/commit/ac92bb424c3d7d99905f4c0988c924001b688080#diff-d981183863e690e9f0f2bd20145a7a16), +and [building](https://github.com/ddevault/receptor) +[frontends](https://broca.synapse-bt.org/) for. Congrats, Lumi! This one has +been a long time coming. + +Alright, now for some updates on the larger, long-term projects. The initial +pre-release of aerc [shipped](/2019/06/03/Announcing-aerc-0.1.0.html) two weeks +ago! Even since then it's already attracted a flurry of patches from the +community. I'm tremendously excited about this project, I think it has heaps of +potential and a community is quickly forming to help us live up to it. Since +0.1.0 it's already grown support for formatting the index list, swapped the +Python dependency for POSIX awk, grown temporary accounts and the ability to +view headers, and more. I've already started planning 0.2.0 - check out [the +list of +blockers](https://todo.sr.ht/~sircmpwn/aerc2?search=label:%22blocker%22%20status%3Aopen) +for a sneak peek. + +The Godot+Wayland workstream has picked up again, and I've secured some VR +hardware (an HTC Vive) and started working on [planning the changes +necessary](https://github.com/swaywm/wlroots/issues/1723) for first-class VR +support on wlroots. In the future I also would like to contribute with the +OpenXR and OpenHMD efforts for bringing a full-stack free software solution for +VR. I also did a proof-of-concept 3D Wayland compositor that I intend to +translate to VR once I have the system up and running on Wayland: + +<video src="https://yukari.sr.ht/godot3d.webm" muted autoplay controls></video> + +In other respects, sway & wlroots have been somewhat quiet. We've been focusing +on small bug fixes and quality-of-life improvements, while some beefier changes +are stewing on the horizon. wlroots has seen some slow and steady progress on +refining its DRM implementation, improvements to which are going to lead to even +further improved performance and capability of the downstream compositors - +notably, direct scan-out has just been merged with the help of Scott Anderson +and Simon Ser. + +In SourceHut news, the most exciting is perhaps that todo.sr.ht has grown an API +and webhooks! That makes it the last major sr.ht service to gain these features, +which unblocks a lot of other stuff in the pipeline. The biggest workstream +unblocked by this is dispatch.sr.ht, which has an design proposal for an +overhaul under discussion on the development list. This'll open the door for +features like building patches sent to mailing lists, linking tickets to +commits, and much more. I've also deployed another compute server to pick up the +load as git.sr.ht grows to demand more resources, which frees up the box it used +to be on with more space for smaller services to get comfortable. I was also +happy to bring Ludovic Chabant, the driving force behind hg.sr.ht, with me to +attend a Mercurial conference in Paris, where I learned heaps about the +internals (and externals, to be honest) of Mercurial. Cool things are in store +here, too! Big thanks to the Mercurial maintainers for being so accommodating of +my ignorance, and for putting on a friendly and productive conference. + +In the next month, I'm moving aerc to the backburner and turning my focus back +to SourceHut & wlroots VR. I'm getting a consistent stream of great patches for +aerc to review, so I'm happy to leave it in the community's hands for a while. +For SourceHut, the upcoming dispatch workstream is going to be a huge boon to +the community there. On its coattails will come more powerful data import & +export tools, giving the users more ownership and autonomy over their data, and +perhaps following this will be some nice improvements to git.sr.ht. I'm also +going to try and find time to invest more in Alpine Linux on RISC-V this month. + +From the bottom of my heart, thank you again for lending your support. I've +never been busier, happier, and more productive than I have been since working +on FOSS full-time. Let's keep building awesome software together. + +<small class="text-muted"> +This work was possible thanks to users who support me financially. Please +consider <a href="/donate">donating to my work</a> or <a +href="https://sourcehut.org">buying a sourcehut.org subscription</a>. Thank you! +</small> diff --git a/content/blog/Status-update-March-2019.md b/content/blog/Status-update-March-2019.md @@ -0,0 +1,137 @@ +--- +date: 2019-03-15 +layout: post +title: Status update, March 2019 +tags: ["status update"] +--- + +My todo list is getting completed at a pace it's never seen before, and growing +at a new pace, too. This full-time FOSS gig is really killing it! As the good +weather finally starts to roll in, it's time for March's status update. Note: I +posted updates [on Patreon][patreon-archive] before, but will start posting here +instead. This medium doesn't depend on a proprietary service, allows for richer +content, and is useful for my supporters who support my work via other donation +platforms. + +[patreon-archive]: https://www.patreon.com/sircmpwn/posts + +Sway 1.0 has been released! <img style="display: inline; height: 1.2rem" +src="/img/party.png" /> I wrote a [detailed +write-up](/2019/03/11/Sway-1.0-released.html) on the release and our future +plans separately, which I encourage you to read if you haven't already. However, +I do have some additional progress to share outside of the big sway 1.0 news. +In the last update, I mentioned that I got a Librem 5 devkit from Guido +Günther[^1] at FOSDEM. My plans were to get this up and running with sway and +start improving touch support, and I've accomplished both: + +[^1]: A Purism employee that works closely with wlroots on the Librem 5 + +![A picture of a Librem5 devkit running pmOS and sway](https://sr.ht/fGxf.jpg) + +As you can see, I also got [postmarketOS](https://postmarketos.org/) running, +and I love it - I hope to work with them a lot in the future. The [first +patch](https://github.com/swaywm/sway/pull/3711) for improving touch support in +sway has landed and I'll be writing more in the future. I also sent some patches +to Purism's [virtboard](https://source.puri.sm/Librem5/virtboard) project, an +on-screen keyboard, making it more useful for Sway users. I hope to make an OSK +of my own at some point, with multiple layouts, CJK support, and client-aware +autocompletion, in the future. Until then, an improved virtboard is a nice +stop-gap :) I've also been working on wlroots a bit, including [a patch adding +remote desktop support][rdp]. + +In other Wayland news, I've also taken a part time contract to build a module +integrating wlroots with the [Godot game engine](https://godotengine.org/): +[gdwlroots](https://git.sr.ht/~sircmpwn/gdwlroots). The long-term goal is to +build a VR compositor based on Godot and develop standards for Wayland +applications to have 3D content. 100% of this work is free software (MIT +licensed) and will bring improvements to both the wlroots and Godot ecosystems. +Next week I'll be starting work on adding a Wayland backend to Godot so that +Godot-based games can run on Wayland compositors directly. Here's an example +compositor running on Godot: + +[rdp]: https://github.com/swaywm/wlroots/pull/1578 + +<video + src="https://sr.ht/9bV-.webm" + autoplay muted loop controls + style="max-width: 100%;" +></video> + +I've also made some significant progress on +[aerc2](https://git.sr.ht/~sircmpwn/aerc2). I have fleshed out the command +subsystem, rigged up keybindings, and implemented the message list, and along +with it all of the asynchronous communication between the UI thread, network +thread, and mail server. I think at this point most of the unknowns are solved +with aerc2, and the rest just remains to be implemented. I'm glad I chose to +rewrite it from C, though my love for C still runs deep. The Go ecosystem is +much better suited to the complex problems and dependency tree of software like +aerc, plus has a nice concurrency model for aerc's async design.[^2] The next +major problem to address is the embedded terminal emulator, which I hope to +start working on soon. + +[^2]: "aerc" stands for "asynchronous email reading client", after all. + +<script + id="asciicast-pafXXANiWHY9MOH2yXdVHHJRd" + src="https://asciinema.org/a/pafXXANiWHY9MOH2yXdVHHJRd.js" async +></script> + +aerc2's progress is a great example of my marginalized projects +becoming my side projects, as my side projects become my full-time job, and thus +all of them are developing at a substantially improved pace. The productivity +increase is pretty crazy. I'm really thankful to everyone who's supporting my +work, and excited to keep building crazy cool software thanks to you. + +I was meaning to work on RISC-V this month, but I've been a little bit +distracted by everything else. However, there has been some discussion about how +to approach upstreaming and I'm planning on tackling this next week. I also +spent some time putting together a custom 1U I can install in my datacenter for +a more permanent RISC-V setup. Some of this is working towards getting RISC-V +ready for builds.sr.ht users to take advantage of - that relay is for cutting +power to the board to force a reboot when it misbehaves - but a lot of this is +also useful for my own purposes in porting musl & Alpine Linux. + +![Picture of a 1U chassis with a bunch of custom components within](https://sr.ht/M7me.jpg) + +One problem I'm still trying to solve is the microSD card. I don't want to run +untrusted builds.sr.ht code when that microSD card is plugged in. I've been +working on some prototyping (breaking out the old soldering iron) to make a +microSD... thing, which I can plug into this and physically cut VCC to the +microSD card with that relay I have rigged up. This is pretty hard, and my +initial attempts were unsuccessful. If anyone knowledgable about this has ideas, +please get in touch. + +Outside of RISC-V, I have been contributing to Alpine Linux a lot more lately in +general. I adopted the sway & wlroots packages, have been working on improved +PyQt support, cleaning up Python packages, clearing out the nonfree MongoDB +packages, and more. I also added a bunch of new packages for miscellaneous +stuff, including alacritty, font-noto-cjk, nethack, and Simon Ser's +[go-dkim](https://github.com/emersion/go-dkim) milter. Most importantly, +however, I've started +[planning](https://wiki.alpinelinux.org/wiki/Python_package_policies) and +[discussing](https://lists.alpinelinux.org/alpine-devel/6465.html) a Python +overhaul project in aports with the Alpine team, which will including cleaning +up all of the Python patches and starting on Python 2 removal. I depend a lot on +Alpine and its Python support, so I'm excited to be working on these +improvements! + +I have some Sourcehut news as well. Like usual, there'll be a detailed +Sourcehut-specific update posted to the +[sr.ht-announce](https://lists.sr.ht/~sircmpwn/sr.ht-announce) mailing list +later on. With Ludovic Chabant's help, there have been continued improvements to +Mercurial support, notably adding builds.sr.ht integration as of yesterday. +Thanks Ludovic! We've also been talking to some NetBSD folks who may be +interested in using Sourcehut to host the NetBSD code once they finish their +CVS->Hg migration, and we've been improving the performance for large +repositories during their experiments on sr.ht. + +There's a bunch more going on with Sourcehut - paste.sr.ht, APIs, a command line +interface for those APIs, webhooks, and more still - check out the email on +sr.ht-announce later. That's all I have for you today. Thank you for your +support, and until next time! + +<small class="text-muted"> +This work was possible thanks to users who support me financially. Please +consider <a href="/donate">donating to my work</a> or <a +href="https://sourcehut.org">buying a sourcehut.org subscription</a>. Thank you! +</small> diff --git a/content/blog/Status-update-May-2019.md b/content/blog/Status-update-May-2019.md @@ -0,0 +1,113 @@ +--- +date: 2019-05-15 +layout: post +title: Status update, May 2019 +tags: ["status update"] +--- + +This month, it seems the most exciting developments again come from the realm of +email. I've got cool email-related news to share for aerc, lists.sr.ht, and +todo.sr.ht, and many cool developments in my other projects to share. + +Let's start with lists.sr.ht: I have broken ground on the web-based patch review +tools! I promised these features when I started working on sourcehut, to make +the email-based workflow more enticing to those who would rather work on the +web. Basically, this gives us a Github or Gerrit-esque review UI for patches +which arrive on the mailing list. Thanks to [a cool +library](https://git.sr.ht/~emersion/python-emailthreads) Simon Ser wrote for +me... almost a year ago... I'm able to take a thread of emails discussing a +patch and organically convert them into inline feedback on the web. + +[![](https://sr.ht/sjtE.png)](https://lists.sr.ht/~philmd/qemu/patches/5556) + +<small style="display: block; text-align: center;"> + Click the screenshot to visit this page on the web +</small> + +This is generated from organic discussions where the participants don't have to +do anything special to participate - in the discussion this screenshot is +generated from, the participants aren't even aware that this process is taking +place. This approach allows users who prefer a web-based workflow to interact +with traditional email-based patch review seamlessly. Future improvements will +include detecting new revisions of a patch, side-by-side diff and diffs between +different versions of a patch, and using the web interface to review a patch - +which will generate an email on the list. I'd also like to extend git.sr.ht with +web support for git send-email, allowing you to push to your git repo and send a +patch off to the mailing list from the web. It should also be possible to +combine this with dispatch.sr.ht to have bidirectional code reviews between +mailing lists and Github, Gitlab, etc - with no one on either side being any the +wiser to the preferred workflow of the other. + +In other exciting email-related news, aerc2 now supports composing emails - +a feature which has been a long time coming, and was not even present in aerc1! +Check it out: + +<script + id="asciicast-CqTukJZoTq7ZgPmsjhIbQyUjb" + src="https://asciinema.org/a/pafXXANiWHY9MOH2yXdVHHJRd.js" async +></script> + +Outgoing email configuration supports SMTP, STARTTLS, and SMTPS, with sendmail +support planned. Outgoing emails are edited with our embedded terminal emulator +using vim, or your favorite `$EDITOR`. Still to come: replying to emails & PGP +support. I could use your help here! If you want a chance to write some cool Go +code, stop by the IRC channel and say hello: [#aerc on +irc.freenode.net](http://webchat.freenode.net/?channels=aerc&uio=d4). Once aerc +matures a little bit, I also want to start working on a git integration which +will continue making email an even more compelling platform for software +development. + +Let's talk about Wayland next. I've been shipping release candidates for sway +1.1 - [check out the provisional changelog +here](https://github.com/swaywm/sway/issues/3861#issuecomment-487073065). The +highlights are probably the ability to inhibit idle with arbitrary criteria, and +touch support for swaybar. The release candidates have been pretty quiet - we +might end up shipping this as early as rc4. wlroots 0.6.0 was also released, +though for end-users it doesn't include much. We've removed the long-deprecated +wl_shell, and have made plans to start removing other deprecated protocols. I've +also been working with the broader Wayland community on establishing a +governance model for protocol standardization - [read the latest draft +here](https://lists.freedesktop.org/archives/wayland-devel/2019-May/040532.html). + +I've also started working on a Wayland book! It's intended as a comprehensive +reference on the Wayland protocol, useful for authors hoping to write both +Wayland compositors and Wayland clients. It does not go into all of the +nitty-gritty details necessary for writing a Wayland compositor for Linux (that +is, the sort of knowledge necessary for using wlroots, or even making wlroots +itself), but that'll be a task for another time. Instead, I focus on the Wayland +protocol itself, explaining how the wire protocol works and the purpose and +usage of each interface in `wayland.xml`, as well as `libwayland`. I intend to +sell this book, but when you buy it you'll receive a DRM-free CC-NC-ND copy that +you can share freely with your friends. + +Before I move on from Wayland news, also check out +[Wio](https://wio-project.org/) if you haven't yet - I wrote a blog post about +it [here](https://drewdevault.com/2019/05/01/Announcing-wio.html). In short: I +made a novel new Wayland compositor in my spare time which behaves like plan 9's +Rio. See the blog post for more details! + +Following the success of [git-send-email.io](https://git-send-email.io), I +published a similar website last week: [git-rebase.io](https://git-rebase.io). +The purpose of this website is to teach readers how to use git rebase, +explaining how to use its primitives to accomplish common high-level tasks in a +way that leaves the reader equipped to apply those primitives to novel +high-level tasks in the course of their work. I hope you find it helpful! I've +also secured git-filter-branch.io and git-bisect.io to explain additional +useful, but confusing git commands in the future. + +Brief updates for other projects: I've been ramping up RISC-V work again, +helping Golang test their port, testing out u-Boot, and working on the Alpine +port some more. cozy has seen only a little progress, but the parser is +improving and it's now emitting a (very incomplete) AST for source files you +feed to it. Godot is on hold pending additional upstream bandwidth for code +review. + +That's all for today! Thank you so much for your support. It's pretty clear by +now that my productivity is way higher now that I'm able to work full-time on +open source, thanks to your support. I'll see you for next month's update! + +<small class="text-muted"> +This work was possible thanks to users who support me financially. Please +consider <a href="/donate">donating to my work</a> or <a +href="https://sourcehut.org">buying a sourcehut.org subscription</a>. Thank you! +</small> diff --git a/content/blog/Status-update-November-2019.md b/content/blog/Status-update-November-2019.md @@ -0,0 +1,64 @@ +--- +date: 2019-11-15 +layout: post +title: Status update, November 2019 +tags: ["status update"] +--- + +Today's update is especially exciting, because today marks the 1 year +anniversary of Sourcehut [opening it's alpha][public alpha] to public +registration. I wrote a [nice long article][1 year article] which goes into +detail about what Sourcehut accomplished in 2019, what's to come for 2020, and +it lays out the entire master plan for your consideration. Be sure to give that +a look if you have the time. I haven't slowed down on my other projects, though, +so here're some more updates! + +[public alpha]: https://drewdevault.com/2018/11/15/sr.ht-general-availability.html +[1 year article]: https://sourcehut.org/blog/2019-11-15-sourcehut-1-year-alpha/ + +I've been pushing hard on the VR work this month, with lots of help from Simon +Ser. We've put together [wxrc](https://git.sr.ht/~sircmpwn/wxrc) - Wayland XR +Compositor - which does what it says on the tin. It's similar to what you've +seen in my earlier updates, but it's a bespoke C project instead of a +Godot-based compositor, resulting in something much lighter weight and more +efficient. The other advantage is that it's based on OpenXR, thanks to [our +many][dd monado MRs] [contributions][ss monado MRs] to Monado, an open-source +OpenXR runtime - the previous incarnations were based on SteamVR, which is +a proprietary runtime and proprietary API. We've also got 3D Wayland clients +working as of this week, check out our video: + +[dd monado MRs]: https://gitlab.freedesktop.org/monado/monado/merge_requests?scope=all&utf8=%E2%9C%93&state=all&author_username=ddevault +[ss monado MRs]: https://gitlab.freedesktop.org/monado/monado/merge_requests?scope=all&utf8=%E2%9C%93&state=all&author_username=emersion + +<video src="https://yukari.sr.ht/wxrc-demo3.webm" muted autoplay loop> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> + +This work has generated more patches for a large variety of projects - Mesa, +Wayland, Xorg, wlroots, sway, new Vulkan and OpenXR standards, and more. This +is really cross-cutting work and we're making improvements across the whole +graphics ecosystem to support it. + +Speaking of Wayland, the upcoming Sway release is looking like it's going to be +really good. I mentioned this last month, but we're still on track for getting +lots of great features in - VNC support, foreign toplevel management (taskbars), +input latency reductions, drawing tablet support, and more. I'm pretty excited. +I wrote chapters 9 and 9.1 for the Wayland book this month as well. + +In aerc news, thanks entirely to its contributors and not to me, lots of new +features have been making their way in. Message templates are one of them, which +you can take advantage of to customize the reply and forwarded message +templates, or make new templates of your own. aerc has learned AUTH LOGIN +support as well, and received a number of bugfixes. ctools has also seen a +number of patches coming in, including support for echo, tee, and nohup, along +with several bug fixes. + +In totally off-the-wall news, I've [started a page][japanese] cataloguing my +tools and recommendations for Japanese language learners. + +[japanese]: /japanese.html + +That's all I've got for you today, I hope it's enough! Thank you for your +continued love and support, I'm really proud to be able to work on these +projects for you. diff --git a/content/blog/Status-update-October-2019.md b/content/blog/Status-update-October-2019.md @@ -0,0 +1,106 @@ +--- +date: 2019-10-15 +layout: post +title: Status update, October 2019 +tags: ["status update"] +--- + +Last month, I gave you an update at the conclusion of a long series of travels. +But, I wasn't done yet - this month, I spent a week in Montreal for [XDC][xdc]. +Simon Ser put up [a great write-up][simon blog] which goes over a lot of the +important things we discussed there. It was a wonderful conference and well +worth the trip - but I truly am sick of travelling. Now, I can enjoy some time +at home, working on free and open source software. + +[xdc]: https://xdc2019.x.org/ +[simon blog]: https://emersion.fr/blog/2019/xdc2019-wrap-up/ + +I have a video to share today, of a workflow on git.sr.ht that I'm very excited +about: sending patchsets as emails from the web. + +<video src="https://sr.ht/_fUk.webm" controls muted> + Your web browser does not support the webm video codec. Please consider using + web browsers that support free and open standards. +</video> + +Sourcehut's development plans can be described in three broad strokes: (1) make +a bunch of services (or: primitives for a development hub); (2) rig them all up +with APIs and webhooks; and (3) teach them how to talk to each other. Over the +past year, (1) and (2) are mostly complete, and (3) is now underway. Teaching +git.sr.ht and lists.sr.ht to talk to each other is an important step, because it +will give us a web-based code review flow which is backed by emails. This meets +an original design goal of Sourcehut: to build user-friendly tools on top of +existing systems. + +The other end of this work is on lists.sr.ht, but for now it's indirect: I've +also been working on [pygit2][pygit2 pulls] fleshing out the Odb backend API, so +that I can make a pygit2 repo which is backed by the git.sr.ht API. From there, +it'll be easy to teach lists.sr.ht about git.sr.ht - and perhaps other git +services as well. + +[pygit2 pulls]: https://github.com/libgit2/pygit2/pulls?q=is%3Apr+author%3Addevault+is%3Aclosed + +There's also a fourth stage of Sourcehut: giving back to the free software +community. To this end, I intend to spend Sourcehut's profits on sponsoring +motivated and talented free software developers to work on self-directed +projects. I'm very excited to announce that there's progress here as well: +[Simon Ser](https://emersion.fr) is now joining Sourcehut and will be doing just +that: self-directed free software projects. He's written more about this on [his +blog](https://emersion.fr/blog/2019/working-full-time-on-open-source/) and I'll +be writing more on [sourcehut.org](https://sourcehut.org) later. + +Wrapping up Sourcehut news, I'll leave you with an out-of-context screenshot of +a mockup I made this month: + +[![Screenshot of a Sourcehut DNS service showing DNS records managed by zone +files in a git repository](https://sr.ht/_yhw.png)](https://sr.ht/_yhw.png) + +Let's move on to Wayland news. We've started the planning for the next sway +release, and it's shaping up to be really cool. We expect to ship patches which +can reduce input latency to as low as 1ms, introduce the foreign toplevel +management protocol for better mate-panel support, and introduce damage tracking +to our screencopy protocol - which is being used to make a VNC server for +sway and other wlroots-based compositors; and proper drawing tablet support. +We're also making strong headway on a long-term project to overhaul rendering +and DRM in wlroots, with the long term goal of achieving the holy grail levels +of performance on any device. + +[The Wayland book](https://wayland-book.com) is also in good shape. A lot of +people have purchased the drafts - over a hundred! Thank you for picking it up, +and please send your feedback along. I completed chapter 8 this month. I also +expect to receive the last few parts for my second POWER9 machine today, and I +plan on using this to test Wayland, Mesa, etc - on ppc64le. The [first POWER9 +machine][power9 article] is now provisioned and humming along in the Sourcehut +datacenter, by the way. + +[power9 article]: https://drewdevault.com/2019/10/10/RaptorCS-redemption.html + +VR work has also been chugging along again this month. I've started contributing +to [Monado][monado], which is basically to OpenXR as Mesa is to OpenGL. I've +seen merged an overhaul to their build system, an overhaul for their dated +Wayland backend, and even some deeper work ensuring conformance with the OpenXR +specification. A lot of this work has also been in getting to know everyone and +planning the future of the project, as it's still in early stages. + +[monado]: https://gitlab.freedesktop.org/monado/monado/merge_requests?scope=all&utf8=%E2%9C%93&state=merged&author_username=ddevault + +To quickly summarize my other various projects: + +- **ctools** has seen many small improvements and bug fixes, and has grown the + dirname, rmdir, env, and sleep utilities. +- **aerc** has also seen small improvements and bug fixes, but has also learned + about sorting and will soon grow a threaded message list +- **chopsui** is stirring in its sleep, and I've been giving some new attention + to its design problems in the hopes that the next iteration will be the + correct design for a new GUI toolkit. +- [**wshowkeys**](https://git.sr.ht/~sircmpwn/wshowkeys) is a new little tool I + built to display your keypresses on-screen during a Wayland session, useful + for live streaming or video recording +- **9front** has been eating some of my evenings lately, and I've been making + small improvements to various tools and improving Plan 9 support among some + packages in the Go ecosystem. I have more plans for this... stay tuned. + +That's all I've got for today. Thank you for your support! Oh, and one last +note: I've been invited to the [Github sponsors +program](https://github.com/users/ddevault/sponsorship), so if you want to +donate through it Github will match your donation for a little while. Cheers! diff --git a/content/blog/Status-update-September-2019.md b/content/blog/Status-update-September-2019.md @@ -0,0 +1,97 @@ +--- +date: 2019-09-15 +layout: post +title: Status update, September 2019 +tags: ["status update"] +--- + +Finally home again after a long series of travels! I spent almost a month in +Japan, then visited my sister's new home in Hawaii on the way eastwards, then +some old friends in Seattle, and finally after 5½ long weeks, it's home sweet +home here in Philadelphia. At least until I leave for +[XDC](https://xdc2019.x.org/) in Montreal 2 weeks from now. Someday I'll have +some rest... throughout all of these wild travels, I've been hard at work on my +free software projects. Let's get started with this month's status update! + +![](https://sr.ht/iuDE.jpg) + +<p style="text-align: center"> + <small>Great view from a hike on O'ahu</small> +</p> + +First, Wayland news. I'm happy to share with you that the Wayland book is now +more than halfway complete, and I've made the drafts available online for a +discounted price: [The Wayland Protocol](https://wayland-book.com). Thanks to +all of my collaborators and readers who volunteered to provide feedback! There's +more Wayland-related news still, as this month marked the release of [sway +1.2][sway changelog] and [wlroots 0.7.0][wlr changelog]. I like this release +because it's light on new features - showing that sway is maturing into a stable +and reliable Wayland desktop. The features which were added are subtle and serve +to improve sway's status as a member of the broader ecosystem - sway 1.2 +supports the new [layer shell support in the MATE panel][mate panel], and the +same improvements are already helping with the development of other software. + +[sway changelog]: https://github.com/swaywm/sway/releases/tag/1.2 +[wlr changelog]: https://github.com/swaywm/wlroots/releases/tag/0.7.0 +[mate panel]: https://github.com/mate-desktop/mate-panel/pull/991 + +[![Screenshot of MATE panel running on sway](https://sr.ht/9Oro.png)](https://sr.ht/9Oro.png) + +<p style="text-align: center"> + <small>Rest assured, the weird alignment issues were fixed</small> +</p> + +On the topic of [aerc](https://aerc-mail.org), I still haven't gotten around to +that write-up responding to [Greg KH's post][gregkh]... but I will. Travels have +made it difficult to sit down for a while and do some serious long-term project +planning. Regardless, the current plans have still been being executed well. +Notmuch support continues to improve thanks to Reto Brunner's help, completions +are improving throughout, and heaps of little features - signatures, unread +message counts, :prompt, forward-as-attachment - are now supported. + +[gregkh]: http://www.kroah.com/log/blog/2019/08/14/patch-workflow-with-mutt-2019/ + +I also spent some time this month working on Simon Ser's +[mrsh](https://mrsh.sh). I cleaned up call frames, implemented the `return` +builtin, finished the `pwd` builtin, improved readline support, fleshed out job +control, and made many other small improvements. With mrsh nearing completion, +I've started up another project: [ctools][ctools]. This provides the rest of the +POSIX commands required of a standard scripting environment (it replaces +coreutils or busybox). I'm taking this one pretty seriously from the start - +every command has full POSIX.1-2017 support with a conformance test and a man +page, in one C source file and no dependencies. If you're looking for a good +afternoon project (or weekend, for some utilities), how about picking up your +favorite [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/) tool and +sending along an implementation? + +[![Screenshot of ctools test suite](https://sr.ht/DSxS.png)](https://builds.sr.ht/~sircmpwn/job/88955) + +[ctools]: https://git.sr.ht/~sircmpwn/ctools + +With these projects, along with ~mcf's [cproc](https://git.sr.ht/~mcf/cproc), +we're starting to see a simple and elegant operating system come together - +exactly the kind I wish we already had. To track our progress towards this goal, +I've put up [arewesimpleyet.org](https://arewesimpleyet.org). A day may soon +come when computers become the again elegant and simple tools they were always +meant to be! At least if we assume "within a few decades" as a valid definition +of "soon". + +To cover SourceHut news briefly: we hit 10,000 users this month! And it's +continued to grow since, up to 10,649 users at the time of writing. On the +subject of feature development, with Denis Laxalde's help we're starting to put +together a Debian repository for installing the services on Debian hosts. On +todo.sr.ht, users without accounts can now create and comment on tickets via +email. I also redesigned [sourcehut.org](https://sourcehut.org), adding a blog +with a greater breadth of topics than we'll see on the sr.ht-announce mailing +list. + +That's all for this month! I enjoyed my vacation and some much needed time away +from work... though for me a "day off" is a day where I write less than 1,000 +lines of code. Thank you again for your support - it means the world to me. I'll +see you next month! + +![](https://sr.ht/1cuE.jpg) + +<p style="text-align: center"> + <small>Had the best seats at a concert in Tokyo!</small> +</p> diff --git a/content/blog/Status-update.md b/content/blog/Status-update.md @@ -0,0 +1,129 @@ +--- +date: 2020-08-16 +layout: post +title: Status update, August 2020 +tags: ["status update"] +--- + +Greetings! Today is another rainy day here in Philadelphia, which rather sours +my plans of walking over to the nearby cafe to order some breakfast to-go. But I +am tired, and if I'm going to make it to the end of this blog post in one piece, +I'm gonna need a coffee. brb. + +Hey, that was actually pretty refreshing. It's just drizzling, and the rain is +nice and cool. Alright, here goes! What's new? I'll leave the Wayland news for +[Simon Ser's blog](https://emersion.fr/blog) this month - he's been working on +some exciting stuff. The [BARE encoding](https://baremessages.org/) announced +last month has received some great feedback and refinements, and there are now +six projects providing BARE support for their author's favorite programming +language[^1]. There have also been some improvements to the Go implementation +which should help with some SourceHut plans later on. + +[^1]: Or in some cases, the language the author is begrudgingly stuck with. + +On the subject of SourceHut, I've focused mainly on infrastructure improvements +this month. There is a new server installed for hg.sr.ht, which will also be +useful as a testbed for additional ops work planned for future expansion. +Additionally, the PostgreSQL backup system has been overhauled and made more +resilient, both to data loss and to outages. A lot of other robustness +improvements have been made fleet-wide in monitoring. I'll be working on more +user-facing features again next month, but in the meanwhile, contributors like +наб have sent many patches in which I'll cover in detail in the coming "What's +cooking" post for [sourcehut.org](https://sourcehut.org/blog). + +Otherwise, I've been taking it easy this month. I definitely haven't been +spending a lot of my time on a secret project, no sir. Thanks again for your +support! I'll see you next month. + +<details> +<summary>?</summary> +<pre> +use io; +use io_uring = linux::io_uring; +use linux; +use strings; + +export fn main void = { + let uring = match (io_uring::init(256u32, 0u32)) { + err: linux::error => { + io::println("io_uring::init error:"); + io::println(linux::errstr(err)); + return; + }, + u: io_uring::io_uring => u, + }; + + let buf: [8192]u8 = [0u8...]; + let text: nullable *str = null; + let wait = 0u; + let offs = 0z; + let read: *io_uring::sqe = null: *io_uring::sqe, + write: *io_uring::sqe = null: *io_uring::sqe; + let eof = false; + + while (!eof) { + read = io_uring::must_get_sqe(&uring); + io_uring::prep_read(read, linux::STDIN_FILENO, + &buf, len(buf): u32, offs); + io_uring::sqe_set_user_data(read, &read); + wait += 1u; + + let ev = match (io_uring::submit_and_wait(&uring, wait)) { + err: linux::error => { + io::println("io_uring::submit error:"); + io::println(linux::errstr(err)); + return; + }, + ev: uint => ev, + }; + + wait -= ev; + + for (let i = 0; i < ev; i += 1) { + let cqe = match (io_uring::get_cqe(&uring, 0u, 0u)) { + err: linux::error => { + io::println("io_uring::get_cqe error:"); + io::println(linux::errstr(err)); + return; + }, + c: *io_uring::cqe => c, + }; + + if (io_uring::cqe_get_user_data(cqe) == &read) { + if (text != null) { + free(text); + }; + + if (cqe.res == 0) { + eof = true; + break; + }; + + text = strings::must_decode_utf8(buf[0..cqe.res]); + io_uring::cqe_seen(&uring, cqe); + + write = io_uring::must_get_sqe(&uring); + io_uring::prep_write(write, linux::STDOUT_FILENO, + text: *char, len(text): u32, 0); + io_uring::sqe_set_user_data(write, &write); + wait += 1u; + offs += cqe.res; + } else if (io_uring::cqe_get_user_data(cqe) == &write) { + assert(cqe.res > 0); + io_uring::cqe_seen(&uring, cqe); + } else { + assert(false, "Unknown CQE user data"); + }; + }; + }; + + io_uring::close(&uring); +}; +</pre> + +<details> +<summary>hmm?</summary> +<p>I might note that I wrote this program to test my io_uring wrapper; it's not +representative of how normal programs will do I/O in the future.</p> +</details> +</details> diff --git a/content/blog/Stress-and-happiness.md b/content/blog/Stress-and-happiness.md @@ -0,0 +1,144 @@ +--- +date: 2020-01-21 +layout: post +title: The happinesses and stresses of full-time FOSS work +tags: [foss, maintainership] +--- + +In the past few days, several free software maintainers have come out to discuss +the stresses of their work. Though the timing was suggestive, my article last +week on the philosophy of project governance was, at best, only tangentially +related to this topic - I had been working on that article for a while. I do +have some thoughts that I'd like to share about what kind of stresses I've +dealt with as a FOSS maintainer, and how I've managed (or often mismanaged) it. + +February will mark one year that I've been working on self-directed free +software projects full-time. I was planning on writing an optimistic +retrospective article around this time, but given the current mood of the +ecosystem I think it would be better to be realistic. In this stage of my +career, I now feel at once happier, busier, more fulfilled, more engaged, more +stressed, and more depressed than I have at any other point in my life. + +The good parts are numerous. I'm able to work on my life's passions, and my +projects are in the best shape they've ever been thanks to the attention I'm +able to pour into them. I've also been able to do more thoughtful, careful work; +with the extra time I've been able to make my software more robust and reliable +than it's ever been. The variety of projects I can invest my time into has also +increased substantially, with what was once relegated to minor curiosities now +receiving a similar amount of attention as my larger projects were receiving in +my spare time before. I can work from anywhere in the world, at any time, not +worrying about when to take time off and when to put my head down and crank out +a lot of code. + +The frustrations are numerous, as well. I often feel like I've bit off more than +I can chew. This has been the default state of affairs for me for a long time; +I'm often neglecting half of my projects in order to obtain progress by leaps +and bounds in just a few. Working on FOSS full-time has cast this model's +disadvantages into greater relief, as I focus on a greater breadth of projects +and spend more time on them. + +The attention and minor fame I've received as a result of my prolific efforts +also has profound consequences. On the positive line of thought, I'm somewhat +embarrassed to admit that I've noticed my bug reports and feature requests on +random projects (or even my own projects) being taken more seriously now, which +is almost certainly more related to name recognition than merit. I often receive +thanks and words of admiration from my... fans? I guess I have those now. +Sometimes these are somewhat unwelcome, with troubled individuals writing +difficult to decipher half-rants laden with strange praises and bizarre +questions. Other times I'm asked out of the blue to join a discussion I was +unaware of, to comment on some piece of technology I've never used or to take a +stand on some argument which I wasn't privy to. I don't enjoy these kinds of +comments. But, they're not far removed from the ones I like - genuine, +thoughtful praise arrives in my inbox fairly often and it makes the job a lot +more worthwhile. + +Of course, a similar sort of person exists on the opposite extreme. There are +many people who hate my guts and anything I've ever worked on, and who'll go out +of their way to let me and anyone else who'll listen to them know how they feel. +Of course, I have *earned* the ire of no small number of people, and I regret +many of these failed interpersonal relationships. These cases are in the +minority, however - most of the people who will tell tales of my evil are people +who I've never met. There's a lot of spaces online that I just won't visit +anymore because of them. As for the less extreme of this sort of person, I'll +also reiterate what others have said - the negative effects of entitled, +arrogant, or outright toxic users is profound. Don't be that person. + +In either case, I can never join new communities on the same terms as anyone +else does. At least one person in every new community already has some +preconception of me when I arrive. Often I think about making an alias just to +enjoy the privilege of anonymity again. + +A great help has been my daily interactions with the many friends and colleagues +who are dear to me. I've made lifelong friends of many of the people I've met +through these projects. Thanks to FOSS, I have met an amazing number of kind, +talented, generous people. Every day, I'm thankful to and amazed by the +hundreds of people who have found my ideas compelling, and who come together to +contribute their own ideas and set aside their precious time to work together +realizing our shared dreams. If I'm feeling blue, often all it takes to snap me +out of it is to reflect on the gratitude I feel for these wonderful people. I'll +never be able to thank my collaborators enough, but hell, I could stand to do it +some more anyway. + +I also have mixed feelings about how *busy* I am. Every day I wake up to a +hundred new emails, delete half of them, and spend 3-4 hours working on the +rest. Patches, questions, support inquiries, monitoring & reports, it's endless. +On top of that, I have dozens of things I already need to work on. The CI work +distribution algorithm needs to be completely redone; I need to provision new +hardware &mdash; oh yeah, and, the hardware that I need ran into shipping +issues, again; I need to improve monitoring; I need to plan for FOSDEM; I need +to finish the Wayland book; I need to figure out the memory issues in himitsu +&mdash; not to mention write the rest of the software; I need to file taxes, +twice as much work when you own a business; I need to implement data export +&amp; account deletion; I need to finish the web-driven patch review UI; I need +to finish writing docs for Alpine; I have to work more on the PinePhone; I have +a legacy server which needs to be overhauled and is now on the clock because of +ACMEv1; names.sr.ht needs to be finished... + +Not to mention the tasks which have been on hold for longer now than they've +been planned for in the first place. Alpine is still going to have hundreds of +Python 2 packages by EoL; the ppc64le server is gathering dust in the +datacenter; there's been some bug with fosspay for several months, in which it +doesn't show Patreon figures unless I reboot the process every now and then; +RISC-V work is stalled because the work is currently blocked by a large problem +that I can't automate; the list of blog posts I want to write is well over 100 +entries long. There are *several dozen* other loose ends I haven't mentioned +here but am painfully aware of anyway. + +That's not even considering any personal goals, which I have vanishingly little +time for. I get zero exercise, and though my diet is mostly reasonable the +majority of it is delivery unless I get the odd 2 hours to visit the grocery +store. That is, unless I want to spend those 2 hours with my friends, which +means it's back to delivery. My dating life is almost nonexistent. I want to +spend more time studying Japanese, but it's either that or keeping up with my +leisure reading. Lofty goals of also studying Chinese or Arabic are but dust in +the wind. I'm addicted to caffeine, again. + +There have been healthy ways and unhealthy ways of dealing with the occasional +feelings of being overwhelmed by all of this. The healthier ways have included +taking walks, reading my books, spending a few minutes with my cat, doing +chores, and calling my family to catch up. Less healthy ways have included +walking to the corner store to buy unhealthy comfort foods, consuming alcohol or +weed too much or too often, getting in stupid internet arguments, being mean to +my friends and colleagues, and googling myself to read negative comments. + +Despite being swamped with all of this work, it's all work that I love. I love +writing code, and immeasurably more so when writing *my* code. Sure, there are +tech debt skeletons in the closet here and they're keeping me awake at night, but +on the whole I feel lucky to be able to write the software I want to write, the +way I want to write it. I've been trying to do that my entire life &mdash; +writing code for someone else has always been a huge drain on my emotional +well-being. That's why I worked on my side projects in the first place, to have +an outlet through which I could work on self-directed projects without making +compromises for some arbitrary deadline. + +When I'm in the zone, writing lots of code for a project I'm interested in, +knowing it's going to have a meaningful impact on my users, knowing that it's +being written under my terms, it's the most rewarding work I've ever done. I get +to do that every day. + +This isn't the retrospective I wanted to write, but it's nice to drop the veneer +for a few minutes and share an honest take on what this is like. This year has +been nothing like what I expected it to be - it's both terrible and wonderful +and very busy, very goddamn busy. In any case, I'm extremely grateful to be here +doing it, and it's thanks to many, many supportive people - users, contributors, +co-maintainers, and friends. Thank you, thank you, thank you, thank you. diff --git a/content/blog/Sway-0.9-in-retro.md b/content/blog/Sway-0.9-in-retro.md @@ -0,0 +1,112 @@ +--- +date: 2016-08-02 +# vim: tw=80 +# Commands used to generate these stats: +# LoC per author: git ls-tree -r -z --name-only HEAD -- */*.c | xargs -0 -n1 git blame --line-porcelain HEAD |grep "^author "|sort|uniq -c|sort -nr +# Commits per author: git shortlog +title: Sway 0.9 & One year of Sway +layout: post +tags: [sway] +--- + +Today marks one year since the [initial +commit](https://github.com/SirCmpwn/sway/commit/6a33e1e3cddac31b762e4376e29c03ccf8f92107) +of Sway. Over the year since, we've written 1,823 commits by 54 authors, +totalling 16,601 lines of C (and 1,866 lines of header files). This was written +over the course of 515 pull requests and 300 issues. Today, most i3 features are +supported. In fact, as of last week, all of the features from the i3 +configuration I used before I started working on Sway are now supported by Sway. +Today, Sway looks like this (click to expand): + +[![](https://sr.ht/ICd5.png)](https://sr.ht/ICd5.png) + +For those who are new to the project, [Sway](http://swaywm.org) is an +i3-compatible Wayland compositor. That is, your existing [i3](http://i3wm.org/) +configuration file will work as-is on Sway, and your keybindings and colors and +fonts and for_window rules and so on will all be the same. It's i3, but for +Wayland, plus it's got some bonus features. Here's a quick rundown of what's +happened since the [previous state of Sway](/2016/04/20/State-of-sway.html): + +* Stacked & tabbed layouts +* Customizable input acceleration +* Mouse support for swaybar +* Experimental HiDPI support +* New features for swaylock and swaybg +* Support for more i3 IPC features +* Tracking of the workspace new windows should arrive on +* Improved compatibility with i3 +* Many improvements to the documentation +* Hundreds of bug fixes and small improvements + +Since the last State of Sway, we've also seen packages land in the official +repositories of Gentoo, OpenSUSE Tumbleweed, and NixOS (though the last group +warn me that it's experimental). And now for some updated stats. Here's the +breakdown of **lines of code per author** for the top ten authors (with the +change from the previous state of Sway in parens): + +<table class="table"> + <tbody> + <tr><td>4659 (+352)</td><td>Mikkel Oscar Lyderik</td></tr> + <tr><td>3024 (-35)</td><td>Drew DeVault</td></tr> + <tr><td>2232 (+53)</td><td>taiyu</td></tr> + <tr><td>1786 (-40)</td><td>S. Christoffer Eliesen</td></tr> + <tr><td>1090 (+1090)</td><td>Zandr Martin</td></tr> + <tr><td>619 (-63)</td><td>Luminarys</td></tr> + <tr><td>525 (-19)</td><td>Cole Mickens</td></tr> + <tr><td>461 (-54)</td><td>minus</td></tr> + <tr><td>365 (-20)</td><td>Christoph Gysin</td></tr> + <tr><td>334 (-11)</td><td>Kevin Hamacher</td></tr> + </tbody> +</table> + +Notably, Zandr Martin has started regular contributions to Sway and brought +himself right up to 5th place in a short time, and while he's still learning C to +boot. Not included here are his recent forays into contributing to our +dependencies as well. Thanks man! This time around, I also lost a much more +respectable line count - only 35 compared to 457 from the last update. + +Here's the total **number of commits per author** for each of the top ten +committers: + +<table class="table"> + <tbody> + <tr><td>842</td><td> Drew DeVault</td></tr> + <tr><td>239</td><td> Mikkel Oscar Lyderik</td></tr> + <tr><td>186</td><td> taiyu</td></tr> + <tr><td>97</td><td> Luminarys</td></tr> + <tr><td>91</td><td> S. Christoffer Eliesen</td></tr> + <tr><td>58</td><td> Christoph Gysin</td></tr> + <tr><td>48</td><td> Zandr Martin</td></tr> + <tr><td>30</td><td> minus</td></tr> + <tr><td>25</td><td> David Eklov</td></tr> + <tr><td>24</td><td> Mykyta Holubakha</td></tr> + </tbody> +</table> + +Most of what I do for Sway personally is reviewing and merging pull requests. +Here's the same figures using **number of commits per author, excluding merge +commits**, which changes my stats considerably: + +<table class="table"> + <tbody> + <tr><td>383</td><td> Drew DeVault</td></tr> + <tr><td>224</td><td> Mikkel Oscar Lyderik</td></tr> + <tr><td>170</td><td> taiyu</td></tr> + <tr><td>96</td><td> Luminarys</td></tr> + <tr><td>91</td><td> S. Christoffer Eliesen</td></tr> + <tr><td>58</td><td> Christoph Cysin</td></tr> + <tr><td>38</td><td> Zandr Martin</td></tr> + <tr><td>30</td><td> minus</td></tr> + <tr><td>25</td><td> David Eklov</td></tr> + <tr><td>24</td><td> Mykyta Holubakha</td></tr> + </tbody> +</table> + +These stats only cover the top ten in each, but there are more - check out the +[full list](https://github.com/SirCmpwn/sway/graphs/contributors). + +Sway is still going very strong, and continues developing at a fast pace. I've +updated [the roadmap](http://swaywm.org/roadmap) with our plans for Sway 1.0. +You might notice a few features have been reprioritized here, which increases +the scope of Sway 1.0. It'll be worth it, though, to make sure we have a solid +1.0 release. Hopefully we'll see that and more within the year ahead! diff --git a/content/blog/Sway-1.0-highlights.md b/content/blog/Sway-1.0-highlights.md @@ -0,0 +1,151 @@ +--- +date: 2018-10-20 +layout: post +title: Sway 1.0-beta.1 release highlights +tags: ["announcement", "wayland", "sway"] +--- + +1,173 days ago, I wrote sway's [initial commit][commit], and 8,269 commits +followed[^1], written by hundreds of contributors. What started as a side +project became the most fully featured and stable Wayland desktop available, and +drove the development of what has become the dominant solution for building +Wayland compositors - [wlroots](https://github.com/swaywm/wlroots), now the +basis of 10 Wayland compositors. + +[commit]: https://github.com/swaywm/sway/commit/6a33e1e3cddac31b762e4376e29c03ccf8f92107 +[^1]: 5,044 sway commits and 3,225 wlroots commits at the time of writing. + +Sway 1.0-beta.1 was just released and is 100% compatible with the [i3 X11 window +manager](https://i3wm.org/). It's faster, prettier, sips your battery, and +supports [Wayland](https://wayland.freedesktop.org/) clients. When we started, I +honestly didn't think we'd get here. When I decided we'd rewrite our internals +and build wlroots over a year ago, I didn't think we'd get here. It's only +thanks to an amazing team of talented contributors that we did. So what can +users expect from this release? The difference between sway 0.15 and sway 1.0 is +like night and day. The annoying bugs which plauged sway 0.15 are gone, and in +their place is a rock solid Wayland compositor with loads of features you've +been asking after for years. The [official release +notes](https://github.com/swaywm/sway/releases/tag/1.0-beta.1) are a bit thick, +so let me give you a guided tour. + +## New output features + +Outputs, or displays, grew a lot of cool features in sway 1.0. As a reminder, +you can get the names of your outputs for use in your config file by using +`swaymsg -t get_outputs`. What can you do with them? + +To rotate your display 90 degrees, use: + + output DP-1 transform 90 + +To enable our improved HiDPI support[^2], use: + + output DP-1 scale 2 + +[^2]: Sway now has the best HiDPI support on Linux, period. + +Or to enable fractional scaling (see man page for warnings about this): + + output DP-1 scale 1.5 + +You can also now run sway on multiple GPUs. It will pick a primary GPU +automatically, but you can override this by specifying a list of card names at +startup with `WLR_DRM_DEVICES=card0:card1:...`. The first one will do all of the +rendering and any displays connected to subsequent cards will have their buffers +copied over. + +Other cool features include support for daisy-chained DisplayPort configurations +and improved Redshift support. Also, the long annoying single-output limitation +of wlc is behind us: you can now drag windows between outputs with the mouse. + +See `man 5 sway-output` for more details on configuring these features. + +## New input features + +Input devices have also matured a lot. You can get a list of their identifiers +with `swaymsg -t get_inputs`. One oft requested feature was a better way of +configuring your keyboard layout, which you can now do in your config file: + +``` +input "9456:320:Metadot_-_Das_Keyboard_Das_Keyboard" { + xkb_options caps:escape + xkb_numlock enabled +} +``` + +We also now support drawing tablets, which you can bind to a specific output: + +``` +input "1386:827:Wacom_Intuos_S_2_Pen" { + map_to_output DP-3 +} +``` + +You can also now do crazy stuff like having multiple mice with multiple cursors, +and linking keyboards, mice, drawing tablets, and touchscreens to each other +arbitrarily. You can now have your dvorak keyboard for normal use and a second +qwerty keyboard for when your coworker comes over for a pair programming +session. You can even give your coworker the ability to focus and type into +*separate* windows from what you're working on. + +## Third-party panels, lockscreens, and more + +Our new layer-shell protcol is starting to take hold in the community, and +enables the use of even more third-party software on sway. One of our main +commitments to you for sway 1.0 and wlroots is to break the boundaries between +Wayland compositors and encourange standard interopable protocols - and we've +done so. Here are some interesting third-party layer-shell clients in the wild: + +- [Waybar](https://github.com/Alexays/Waybar), a new panel +- [mako](https://github.com/emersion/mako), a notification daemon +- [virtboard](https://source.puri.sm/Librem5/virtboard), an on-screen keyboard +- [slurp](https://github.com/emersion/slurp), a tool to interactively select a + region of the screen +- [Phosh](https://source.puri.sm/Librem5/phosh), the [Purism](https://puri.sm/) + team's shell for their [Librem 5](https://puri.sm/shop/librem-5/) phone + +We also added two new protocols for capturing your screen: screencopy and +dmabuf-export, respectively these are useful for screenshots and real-time +screen capture, for example to live stream on Twitch. Some third-party software +exists for these, too: + +- [grim](https://github.com/emersion/grim), for taking screenshots +- [wlstream](https://github.com/atomnuker/wlstream), for recording video + +## DPMS, auto-locking, and idle management + +Our new `swayidle` tool adds support for all of these features, and even works +on other Wayland compositors. To configure it, start by running the daemon in +your sway config file: + +``` +exec swayidle \ + timeout 300 'swaylock -c 000000' \ + timeout 600 'swaymsg "output * dpms off"' \ + resume 'swaymsg "output * dpms on"' \ + before-sleep 'swaylock -c 000000' +``` + +This example will, after 300 seconds of inactivity, lock your screen. Then after +600 seconds, it will turn off all of your outputs (and turn them back on when +you wiggle the mouse). This configuration also locks your screen before your +system goes to sleep. None of this will happen if you're watching a video on a +supported media player (mpv, for example). For more details check out `man +swayidle`. + +## Miscellaneous bits + +There are a few other cool features I think are worth briefly mentioning: + +- `bindsym --locked` +- swaylock has a config file now +- Drag and drop is supported +- Rich content (like images) is synced between the Wayland and X11 clipboards +- The layout is updated atomically, meaning that you'll never see an in-progress + frame when resizing windows +- Primary selection is implemented and synced with X11 +- Pretty much every long-standing bug has been fixed + +For the full run-down see the [release +notes](https://github.com/swaywm/sway/releases/tag/1.0-beta.1). Give the beta a +try, and we're all looking forward to sway 1.0! diff --git a/content/blog/Sway-1.0-released.md b/content/blog/Sway-1.0-released.md @@ -0,0 +1,93 @@ +--- +date: 2019-03-11 +layout: post +title: Announcing the release of sway 1.0 +tags: ["wayland", "sway", "announcement"] +--- + +1,315 days after I started the [sway](https://swaywm.org) project, it's finally +time for [sway 1.0](https://github.com/swaywm/sway/releases/tag/1.0)! I had no +idea at the time how much work I was in for, or how many talented people would +join and support the project with me. In order to complete this project, we have +had to rewrite the entire Linux desktop nearly from scratch. Nearly 300 people +worked together, together writing over 9,000 commits and almost 100,000 lines of +code, to bring you this release. + +<small class="text-muted">Sway is an i3-compatible Wayland desktop for Linux and FreeBSD</small> + +1.0 is the first stable release of sway and represents a consistent, flexible, +and powerful desktop environment for Linux and FreeBSD. We hope you'll enjoy it! +If the last sway release you used was 0.15 or earlier, you're in for a shock. +0.15 was a buggy, frustrating desktop to use, but sway 1.0 has been completely +overhauled and represents a much more capable desktop. It's almost impossible to +summarize all of the changes which makes 1.0 great. Sway 1.0 adds a huge variety +of features which were sorely missed on 0.x, improves performance in every +respect, offers a more faithful implementation of Wayland, and exists as a +positive political force in the Wayland ecosystem pushing for standardization +and cooperation among Wayland projects. + +When planning the future of sway, we realized that the Wayland ecosystem was +sorely in need of a stable & flexible common base library to encapsulate all of +the difficult and complex facets of building a desktop. To this end, I decided +we would build [wlroots](https://github.com/swaywm/wlroots). It's been a +smashing success. This project has become very important to the Linux desktop +ecosystem, and the benefits we reap from it have been shared with the community +at large. [Dozens of projects][project-list] are using it today, and soon you'll +find it underneath most Linux desktops, on your phone, in your VR environment, +and more. Its influence extends beyond its borders as well, as we develop and +push for standards throughout Wayland. + +[project-list]: https://github.com/swaywm/wlroots/wiki/Projects-which-use-wlroots + +Through this work we have also helped to build a broader ecosystem of tools +built on interoperable standards which you may find useful in your new sway 1.0 +desktop. Here are a few of my favorites - each of which is compatible with many +Wayland compositors: + +- [swayidle](https://github.com/swaywm/swayidle): idle management daemon +- [swaylock](https://github.com/swaywm/swaylock): lock screen +- [mako](https://github.com/emersion/mako): notification daemon +- [grim](https://github.com/emersion/grim): screenshot tool +- [slurp](https://github.com/emersion/slurp): interactive region selection +- [wf-recorder](https://github.com/ammen99/wf-recorder): video capture tool +- [waybar](https://github.com/Alexays/Waybar): alternative panel +- [virtboard](https://source.puri.sm/Librem5/virtboard): on-screen keyboard +- [wl-clipboard](https://github.com/bugaevc/wl-clipboard): xclip replacement +- [wallutils](https://github.com/xyproto/wallutils): fancy wallpaper manager + +--- + +None of this would be possible without the support of sway's and wlroots' +talented contributors. Hundreds of people worked together on this. I'd like to +give special thanks to our core contributors: Brian Ashworth, Ian Fan, Ryan +Dwyer, Scott Anderson, and Simon Ser. Thanks are also in order for those who +have helped wlroots fit into the broader ecosystem - thanks to Purism for their +help on wlroots, KDE & Canonical for their help on protocol standardization. I +also owe thanks to all of the other projects which use wlroots, particularly +including Way Cooler, Wayfire, and Waymonad, who all have made substantial +contributions to wlroots in their pursit of the best Wayland desktop. + +I'd also of course like to thank all of the users who have donated to support +my work, which I now do full-time, which has had and I hope will continue to +have a positive impact on the project and those around it. Please consider +[donating](https://drewdevault.com/donate) to support the future of sway & +wlroots if you haven't yet. + +Though sway today is already stable and powerful, we're not done yet. We plan to +continue improving performance & stability, adding useful desktop features, +taking advantage of better hardware, and bringing sway to more users. Here's +some of what we have planned for future releases: + +- Better Wayland-native tools for internationalized input methods like CJK +- Better accessibility tools including improved screen reader support, + high-contrast mode, a magnifying glass tool, and so on +- Integration with xdg-portal & pipewire for interoperable screen capture +- Improved touch screen support for use on the [Librem + 5](https://puri.sm/products/librem-5/) and on + [postmarketOS](https://postmarketos.org/) +- Better support for drawing tablets and additional hardware +- Sandboxing and security features + +As with all sway features, we intend to have the best-in-class implementations +of these features and set the bar as high as we can for everyone else. We're +looking forward to your continued support! diff --git a/content/blog/Sway-and-client-side-decorations.md b/content/blog/Sway-and-client-side-decorations.md @@ -0,0 +1,47 @@ +--- +date: 2018-01-27 +title: Sway and client side decorations +layout: post +tags: [sway, gnome, wayland] +--- + +You may have recently seen an article from GNOME on the subject of client side +decorations (CSD) titled [Introducing the CSD +Initiative](https://blogs.gnome.org/tbernard/2018/01/26/csd-initiative/). It +states some invalid assumptions which I want to clarify, and I want to tell you +[Sway](https://github.com/swaywm/sway)'s +stance on the subject. I also speak for the rest of the projects involved in +[wlroots](https://github.com/swaywm/wlroots) on this matter, including [Way +Cooler](https://github.com/way-cooler/way-cooler), +[waymonad](https://github.com/Ongy/waymonad), and +[bspwc](https://github.com/Bl4ckb0ne/bspwc). + +The subject of which party is responsible for window decorations on Wayland (the +client or the server) has been a subject of much debate. I want to clarify that +though GNOME may imply that a consensus has been reached, this is not the case. +CSD have real problems that have long been waved away by its supporters: + +- No consistent look and feel between clients and GUI toolkits +- Misbehaving clients cannot be moved, closed, minimized, etc +- No opportunity for compositors to customize behavior (e.g. tabbed windows on + Sway) + +We are willing to cooperate on a compromise, but GNOME does not want to +entertain the discussion and would rather push disingenuous propaganda for their +cause. The topic of the #wayland channel on Freenode includes the statement +"Please do not argue about server-side vs. client-side decorations. It's settled +and won't change." I have been banned from this channel for over a year because +I persistently called for compromise. + +GNOME's statement that "[server-side decorations] do not (and will never) work +on Wayland" is false. KDE and Sway have long agreed on the importance of these +problems and have worked together on a solution. We have developed and +implemented a Wayland protocol extension which allows the compositor and client +to negotiate what kind of decorations each wishes to use. KDE, Sway, Way Cooler, +waymonad, and bspwc are all committed to supporting server-side decorations on +our compositors. + +--- + +See also: [Martin Flöser of KDE responds to GNOME's +article](https://blog.martin-graesslin.com/blog/2018/01/server-side-decorations-and-wayland/) diff --git a/content/blog/Sway-wlroots-at-XDC-2018.md b/content/blog/Sway-wlroots-at-XDC-2018.md @@ -0,0 +1,77 @@ +--- +date: 2018-09-30 +title: Sway & wlroots at XDC 2018 +layout: post +tags: ["sway", "wlroots", "wayland", "roundup"] +--- + +Just got my first full night of sleep after the return flight from Spain after +attending [XDC 2018](https://xdc2018.x.org/). It was a lot of fun! I attended +along with four other major wlroots contributors. Joining me were [Simon Ser +(emersion)](https://github.com/emersion) (a volunteer) and [Scott Anderson +(ascent12)](https://github.com/ascent12) of +[Collabora](https://www.collabora.com/), who work on both +[wlroots](https://github.com/swaywm/wlroots) and +[sway](https://github.com/swaywm/sway). [ongy](https://github.com/ongy) works on +wlroots, [hsroots](https://github.com/swaywm/hsroots), and +[waymonad](https://github.com/waymonad/waymonad), and joined us on behalf +of [IGEL](https://www.igel.com/). Finally, we were joined by [Guido Günther +(agx)](https://github.com/agx) of [Purism](https://puri.sm/), who works with us +on wlroots and on the Librem 5. This was my first time meeting most of them +face-to-face! + +wlroots was among the highest-level software represented at XDC. Most of the +attendees are hacking on the kernel or mesa drivers, and we had a lot to learn +from each other. The most directly applicable talk was probably VKMS (virtual +kernel mode setting), a work-in-process kernel subsystem which will be useful +for testing the complex wlroots DRM code. We had many chances to catch up with +the presenters after their talk to learn more and establish a good +relationship. We discovered from these chats that some parts of our DRM code +was buggy, and have even started onboarding some of them as contributors to sway +and wlroots. + +We also learned a lot from the other talks, in ways that will pay off over time. +One of my favorites was an introduction to the design of Intel GPUs, which went +into a great amount of detail into how the machine code for these GPUs worked, +why these design decisions make them efficient, and their limitations and +inherent challenges. Combined with other talks, we got a lot of insight into the +design and function of mesa, graphics drivers, and GPUs. These folks were very +available to us for further discussion and clarification after their talks, a +recurring theme at XDC and one of the best parts of the conference. + +Another recurring theme at XDC was talks about how mesa is tested, with the most +in-depth coverage being on Intel's new CI platform. They provide access to Mesa +developers to test their code on *every* generation of Intel GPU in the course +of about 30 minutes, and brought some concrete data to the table to show that it +really works to make their drivers more stable. I took notes that you can expect +to turn into builds.sr.ht features! And since these folks were often available +for chats afterwards, I think they were taking notes, too. + +I also met many of the driver developers from AMD, Intel, and Nvidia; all of +whom had interesting insights and were a pleasure to hang out with. In fact, +Nvidia's representatives were the first people I met! On the night of the +kick-off party, I led the wlroots clan to the bar for beers and introduced +myself to the people who were standing there - who already knew me from my +writings critical of Nvidia. Awkward! A productive conversation ensued +regardless, where I was sad to conclude that we still aren't going to see any +meaningful involvement in open source from Nvidia. Many of their engineers are +open to it, but I think that the engineering culture at Nvidia is unhealthy and +that the engineers have very little influence. We made our case and brought up +points they weren't thinking about, and I can only hope they'll take them home +and work on gradually improving the culture. + +Unfortunately, Wayland itself was somewhat poorly represented. Daniel Stone (a +Wayland & Weston maintainer) was there, and Roman Glig (of KDE), but some KDE +folks had to cancel and many people I had hoped to meet were not present. Some +of the discussions I wanted to have about protocol standardization and +cooperation throughout Wayland didn't happen. Regardless, the outcome of XDC was +very positive - we learned a lot and taught a lot. We found new contributors to +our projects, and have been made into new contributors for everyone else's +projects. + +Big shoutout to the X Foundation for organizing the event, and to the beautiful +city of A Coruña for hosting us, and to University of A Coruña for sharing their +university - which consequently led to meeting some students there that used +Sway and wanted to contribute! Thanks as well to the generous sponsors, both for +sponsoring the event and for sending representatives to give talks and meet the +community. diff --git a/content/blog/The-case-against-fork.md b/content/blog/The-case-against-fork.md @@ -0,0 +1,124 @@ +--- +date: 2018-01-02 +layout: post +title: fork is not my favorite syscall +tags: [unix] +--- + +This article has been on my to-write list for a while now. In my opinion, fork +is one of the most questionable design choices of Unix. I don't understand the +circumstances that led to its creation, and I grieve over the legacy rationale +that keeps it alive to this day. + +Let's set the scene. It's 1971 and you're a fly on the wall in Bell Labs, +watching the first edition of Unix being designed for the PDP-11/20. This +machine has a 16-bit address space with no more than 248 kilobytes of memory. +They're discussing how they're going to support programs that spawn new +programs, and someone has a brilliant idea. "What if we copied the entire +address space of the program into a new process running from the same spot, then +let them overwrite themselves with the new program?" This got a rousing laugh +out of everyone present, then they moved on to a better design which would +become immortalized in the most popular and influential operating system of all +time. + +At least, that's the story I'd like to have been told. In actual fact, the +laughter becomes consensus. There's an obvious problem with this approach: every +time you want to execute a new program, the entire process space is copied and +promptly discarded when the new program begins. Usually when I complain about +fork, this the point when its supporters play the virtual memory card, pointing +out that modern operating systems don't actually have to copy the whole address +space. We'll get to that, but first &mdash; First Edition Unix *does* copy the +whole process space, so this excuse wouldn't have held up at the time. By Fourth +Edition Unix (the next one for which kernel sources survived), they had wisened +up a bit, and started only copying segments when they faulted. + +This model leads to a number of problems. One is that the new process inherits +*all* of the parent's process descriptors, so you have to close them all before +you exec another process. However, unless you're manually keeping tabs on your +open file descriptors, there is no way to know what file handles you must close! +The hack that solves this is `CLOEXEC`, the first of many hacks that deal with +fork's poor design choices. This file descriptors problem balloons a bit - +consider for example if you want to set up a pipe. You have to establish a piped +pair of file descriptors in the parent, then close every fd *but* the pipe in +the child, then `dup2` the pipe file descriptor over the (now recently closed) +file descriptor 1. By this point you've probably had to do several non-trivial +operations and utilize a handful of variables from the parent process space, +which *hopefully* were on the stack so that we don't end up copying segments +into the new process space anyway. + +These problems, however, pale in comparison to my number one complaint with the +fork model. Fork is the direct cause of the *stupidest* component I've *ever* +heard of in an operating system: the out-of-memory (aka OOM) killer. Say you +have a process which is using half of the physical memory on your system, and +wants to spawn a tiny program. Since fork "copies" the entire process, you might +be inclined to think that this would make fork fail. But, on Linux and many +other operating systems since, it does not fail! They agree that it's stupid to +copy the entire process just to exec something else, but because fork is +Important for Backwards Compatibility, they just fake it and reuse the same +memory map (except read-only), then trap the faults and actually copy later. +The hope is that the child will get on with it and exec before this happens. + +However, nothing prevents the child from doing something other than exec - +it's free to use the memory space however it desires! This approach now leads to +*memory overcommittment* - Linux has promised memory it does not have. As a +result, when it really does run out of physical memory, Linux will just kill off +processes until it has some memory back. Linux makes an awfully big fuss about +"never breaking userspace" for a kernel that will lie about memory it doesn't +have, then kill programs that try to use the back-alley memory they were given. +That this nearly 50 year old crappy design choice has come to this astonishes +me. + +Alas, I cannot rant forever without discussing the alternatives. There **are** +better process models that have been developed since Unix! + +The first attempt I know of is BSD's `vfork` syscall, which is, in a nutshell, +the same as fork but with severe limitations on what you do in the child process +(i.e. nothing other than calling exec straight away). There are *loads* of +problems with `vfork`. It only handles the most basic of use cases: you cannot +set up a pipe, cannot set up a pty, and can't even close open file descriptors +you inherited from the parent. Also, you couldn't really be sure of what +variables you were and weren't editing or allowed to edit, considering the +limitations of the C specification. Overall this syscall ended up being pretty +useless. + +Another model is `posix_spawn`, which is a hell of an interface. It's far too +complicated for me to detail here, and in my opinion far too complicated to ever +consider using in practice. Even if it could be understood by mortals, it's a +really bad implementation of the spawn paradigm &mdash; it basically operates +like fork backwards, and inherits many of the same flaws. You still have to deal +with children inheriting your file descriptors, for example, only now you do it +in the parent process. It's also straight-up impossible to make a genuine pipe +with `posix_spawn`. (*Note: a reader corrected me - this is indeed possible via +posix_spawn_file_actions_adddup2*.) + +Let's talk about the good models - `rfork` and spawn (at least, if spawn is done +right). `rfork` originated from plan9 and is a beautiful little coconut of a +syscall, much like the rest of plan9. They also implement fork, but it's a +special case of `rfork`. plan9 does not distinguish between processes and +threads - all threads are processes and vice versa. However, new processes in +plan9 are not the everything-must-go fuckfest of your typical fork call. +Instead, you specify exactly what the child should get from you. You can choose +to include (or not include) your memory space, file descriptors, environment, or +a number of other things specific to plan9. There's a cool flag that makes it so +you don't have to reap the process, too, which is nice because reaping children +is another really stupid idea. It still has some problems, mainly around +creating pipes without tremendous file descriptor fuckery, but it's basically as +good as the fork model gets. Note: Linux offers this via the `clone` syscall +now, but everyone just fork+execs anyway. + +The other model is the spawn model, which I prefer. This is the approach I took +in my own kernel for KnightOS, and I think it's also used in NT (Microsoft's +kernel). I don't really know much about NT, but I can tell you how it works in +KnightOS. Basically, when you create a new process, it is kept in limbo until +the parent consents to begin. You are given a handle with which you can +configure the process - you can change its environment, load it up with file +descriptors to your liking, and so on. When you're ready for it to begin, you +give the go-ahead and it's off to the races. The spawn model has none of the +flaws of fork. + +Both fork and exec can be useful at times, but spawning is much better for 90% +of their use-cases. If I were to write a new kernel today, I'd probably take a +leaf from plan9's book and find a happy medium between `rfork` and spawn, so you +could use spawn to start new threads in your process space as well. To the +brave OS designers of the future, ready to shrug off the weight of legacy: +please reconsider fork. diff --git a/content/blog/The-last-years.md b/content/blog/The-last-years.md @@ -0,0 +1,165 @@ +--- +date: 2018-02-13 +title: The last years +layout: post +tags: [fiction] +--- + +**August 14th, 2019** PYONGYANG IN CHAOS AS PANDEMIC DECIMATES LEADERSHIP. +Sources within the country have reported that a fast-acting and deadly +infectious disease has suddenly infected the population of Pyongyang, the +capital city of North Korea, where most of the country's political elite live. +Unconfirmed reports suggest that a significant fraction of the leadership has +been affected. + +The reclusive country has appealed for immediate aid from the international +community and it is reported that a group of medical experts from Seoul have +been permitted to enter via the Joint Security Area. Representatives from the +United States Center for Disease Control and the Chinese Center for Disease +Control and Prevention have also agreed to send representatives into the country +to help control the outbreak. + +North Korea is known for it's unwillingness to cooperate with the international +community, particularly with respect to... + +--- + +**October 7th, 2019** NEW APPROACH SHOWS PROMISING RESULTS FOR CYSTIC FIBROSIS. +Researchers announced yesterday that they were able to design a disease which +corrects the genome of patients suffering from the early stages of cystic +fibrosis. The study was shown to stop the progression of the genetic disease in +all subjects, and several subjects even showed signs of reversal. The FDA has +begun the process of evaluating the treatment for the general public. + +Scientists involved explained the process involved using a modified version of +the common cold. They were able to reduce the negative effects of the virus, and +utilized it as a means of delivering a CRISPR-based payload that directly edited +the genome of members of the study. Scientists on the study suggest that in the +future, a similarly benign virus could be introduced to the general public to +eliminate the disease across the entire human population. + +Some scientists are skeptical of the risks of this approach, but others spoke +favorably... + +--- + +**September 30th, 2019** UNITED STATES CLAIMS RESPONSIBILITY FOR PYONGYANG +EPIDEMIC. In response to increasing alarm in the international community +regarding the origins of the artificial virus that took the life of Kim Jong-un +in August, the United States government has stepped forward to claim +responsibility. President Trump justified the move in a public statement, +claiming that the development of North Korean nuclear weapons capable of +striking American targets required such a response, and points to the ongoing +reunification efforts as evidence of a job well done. + +Many leaders of the international community have issued statements condemning +the United State's attack, though some leaders have expressed relief that the +speculation regarding a rogue group of biologists was dispelled. Korean +officials have also issued statements condemning the attack, noting that several +presumably innocent family members of Pyongyang officials were killed, but +reaffirmed their commitment to supporting the population of the North and +continuing to peacefully unify the peninsula. + +The relative ease of the reunification effort, long thought to be impossible, is +the result of the incredibly swift and precise nature of the American attack... + +--- + +**November 18th, 2020** BRITAIN TARGET OF BIOLOGICAL ATTACK? Members of the +British public have come down with a highly contagious but largely benign form +of the measles, igniting panic among the population. The royal family and +members of the parliament have been quarantined and the country's biologists are +examining specimens of the disease for signs of human tampering. This is the +next in a series of scares, following the flu outbreak in Mexico this June. + +We spoke with an expert in the field (who wished to remain anonymous) to +understand exactly how biologically engineered diseases are possible. Our expert +pointed to recent advances in genetic engineering, particularly CRISPR, which +have allowed research in this field to advance at an unprecedented pace for a +fraction of the costs previously associated with such research. For a layman's +explanation of what CRISPR is and how it works, see page 3. + +Officials in Britain have issued a statement encouraging the public not to +worry, and stated that they had no reason to believe... + +--- + +**February 2nd, 2021** LARGE GENETIC DATABASE LEAKED IN HACK. Personal genomics +company 23andMe released a statement today admitting that their database of +personal genetic records was leaked in a hack in May of last year. The company, +founded in 2006, collects genetic records from customers curious in their +ancestry and sends them a report of interesting information. The database is +said to contain names, email addresses, and samples of each customer's genome +dating back to the company's inception. + +Estimates show that up to 3 million customers are affected, mostly from the +United States. The company has not revealed how much of each customer's genome +was disclosed, but experts agree that it would not have been practical for the +company to have stored their customer's full genomes, and caution affected +customers against panic. At this time, the identity of the hacker is unknown. + +The company's president attributes the security breach to their reduced ability +to maintain a secure database due to their falling profits in recent years as +the general public grows more concious of... + +--- + +**June 28th, 2021** OUTBREAK OF DEATHS AMONG "JOHN ROBERTS". The United States +supreme court chief justice John Roberts was found dead in his home this +morning, the seventh "John Roberts" to die within the past 3 days. He was found +to have the disease which scientists have described as "a new level of +sophistication" in biological engineering. A substantial fraction of the entire +population is expected to have contracted this disease, but do not show any +symptoms. It was specifically designed to target a number of individuals named +John Roberts, and all other infected persons were unaffected. + +It is believed that the genetic information used in this attack was sourced from +the recent leaks of genetic databases from major genetic testing companies, the +largest of which were the 23andMe and Ancestry.com leaks in February and April +respectively. Experts suggest that the data in the leak was not enough to +conclusively identify the justice, and the attackers simply targeted all genomes +matching that name. + +The senate is expected to vote nearly unanimously on legislation this week which +outlaws the collection of genetic information by private companies, a move +largely considered... + +--- + +**August 28th, 2022** STUDY SHOWS IMPOTENCE GROWING AT ALARMING RATE. A study +conducted by a Japanese team shows the birth rate around the world is decreasing +at a dramatically increased pace. According to the study, 42 of the 60 countries +included in the study showed a decrease in new pregnancies of 30% or more +compared to a similar time frame in 2012. They said the trend is expected to +continue, and possibly accelerate. + +Japan is known for its research into fertility, as it has shown a steep decline +in births over the past... + +--- + +**October 1st, 2022** HUMAN BIRTHS EXPECTED TO CEASE WITHIN ONE YEAR. We are sad +to report that biologists have confirmed claims issued last week by a radical +environmentalist group: a highly contagious disease engineered to bring about +impotence has infected most of the Earth's population. The group is a member of +the so-called "Voluntary Extinction" movement, which aims to drive the human +race extinct by ceasing human reproduction. Scientists suggest that this move is +highly unlikely to completely drive humanity extinct, but confirm that it's +likely that massive population losses are in our future. + +Work is underway to determine which members of the population have escaped +exposure, and plan for the continuity of the species. Members of isolated +communities are asked to avoid contact with the outside world, and governments +are cracking down on travel to and from the more remote regions of their +countries. The CDC has reported no estimate on when a vaccine will be available +for the disease, but has confirmed that one must be developed before contact +with these communities is advisable. + +The government of New Zealand announced this morning their intention to send +sterilized supply shipments to research teams in Antarctica, and Canada +announced that all travel... + +--- + +Inspired by this excellent (and scary) talk at DEFCON 25: +[John Sotos - Genetic Diseases to Guide Digital Hacks of the Human Genome](https://www.youtube.com/watch?v=HKQDSgBHPfY) diff --git a/content/blog/The-problem-with-Python-3.md b/content/blog/The-problem-with-Python-3.md @@ -0,0 +1,151 @@ +--- +date: 2017-01-13 +# vim: tw=80 +title: The only problem with Python 3's str is that you don't grok it +layout: post +tags: [python] +--- + +I've found myself explaining Python 3's str to people online more and more often +lately. There's this ridiculous claim about that Python 3's string handling is +broken or somehow worse than Python 2, and today I intend to put that myth to +rest. Python 2 strings are broken, and Python 3 strings are sane. The only +problem is that you don't grok strings. + +The basic problem many people seem to have with Python 3's strings arises when +they write code that treats bytes like a string, because that's how it was in +Python 2. Let me make this as clear as possible: + +<div class="loud">a bytes is not a string</div> + +<style> +.loud { + font-size: 14pt; + font-weight: bold; + text-align: center; + margin-bottom: 1rem; +} +</style> + +I want you to read that, over and over again, until it sinks in. A string is +basically an array of characters (characters being Unicode codepoints), whereas +bytes is an array of bytes, aka octets, aka unsigned 8 bit integers. That's +right - bytes is an array of unsigned 8 bit integers, or as the name would +imply, bytes. If you *ever* do string operations against bytes, you are Doing +It Wrong because bytes are not strings. + +<div class="loud">a bytes is not a string</div> + +It's entirely possible that your bytes contains an *encoded representation* of a +string. That encoding could be ASCII, UTF-8, UTF-32, etc. These encodings are +means of representing strings as bytes, aka unsigned 8 bit integers. In order to +treat it like a string, you first must *decode* it. Luckily Python 3 makes this +painless: `bytes.decode()`. This defaults to UTF-8, but you can specify any +encoding you want: `bytes.decode('latin-1')`. If you want bytes again, use +`str.encode()`, which again defaults to UTF-8 but accepts any encoding. If you +have a bytes that contains an encoded string, your first order of business is +decoding it. + +<div class="loud">a bytes is not a string</div> + +Let's look at some examples of why this matters in practice: + +```python +Python 3.6.0 (default, Dec 24 2016, 08:03:08) +[GCC 6.2.1 20160830] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> 'おはようございます' +'おはようございます' +>>> 'おはようございます'[::-1] +'すまいざごうよはお' +>>> 'おはようございます'[0] +'お' +>>> 'おはようございます'[1] +'は' +``` + +Or in Python 2: + +```python +Python 2.7.13 (default, Dec 21 2016, 07:16:46) +[GCC 6.2.1 20160830] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> 'おはようございます' +'\xe3\x81\x8a\xe3\x81\xaf\xe3\x82\x88\xe3\x81\x86\xe3\x81\x94\xe3\x81\x96\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99' +>>> 'おはようございます'[::-1] +'\x99\x81\xe3\xbe\x81\xe3\x84\x81\xe3\x96\x81\xe3\x94\x81\xe3\x86\x81\xe3\x88\x82\xe3\xaf\x81\xe3\x8a\x81\xe3' +>>> print('おはようございます'[::-1]) +㾁㄁㖁㔁ㆁ㈂㯁㊁ã +>>> 'おはようございます'[0] +'\xe3' +>>> 'おはようございます'[1] +'\x81' +``` + +For anything other than ASCII, Python 2 "strings" are broken. Python 3's string +handling is superb. The problem with it has only ever been that you don't +actually know how strings work. Instead of starting ignorant flamewars about it, +learn how it works. + +## Actual examples people have given me + +**"Python 3 can't handle bytes as file names"** + +Yes it can. Just stop treating them like strings: + +```python +>>>open(b'test-\xd8\x01.txt', 'w').close() +``` + +Note the use of bytes as the file name, not str. \xd8\x01 is unrepresentable as +UTF-8. + +```python +>>> [open(f, 'r').close() for f in os.listdir(b'.')] +[None] +``` + +Note the use of bytes as the path to os.listdir (the documentation says that if +you want bytes back as file names, pass bytes as the path. The docs are helpful +like that). Also note the lack of crashes or broken behavior. + +**"Python 3's csv module writes b'Hello',b'World' into CSV files"** + +CSV files are "comma seperated values". Is each value an array of unsigned 8 bit +integers? No, of course not. They're strings. So why would you pass an array of +unsigned 8 bit integers to it? + +**"Python 3 doesn't support writing files as latin-1"** + +Sure it does. + +```python +with open('some latin-1 file', 'rb') as f: + text = f.read().decode('latin-1') +with open('some utf8 file', 'wb') as f: + f.write(text.encode('utf-8')) +``` + +<div class="loud">a bytes is not a string</div> + +<div class="loud">a bytes is not a string</div> + +<div class="loud">a bytes is not a string</div> + +Python 2's shitty design has broken your mindset. Unlearn it. + +## Python 2 is dead, long live Python 3 + +Listen. It's time you moved to Python 3. You're missing out on a lot of really +great improvements to the language and are stuck with a lot of problems. Python +2 is really being EoL'd, and closing your eyes and covering your ears singing +"la la la" doesn't change that. The transition is really not that difficult or +time consuming, and well worth it. Some people say only new projects should be +written in Python 3. I say that's bollocks - all projects should be written in +Python 3 and you need to migrate, *now*. + +Python 3 is better. Much, much better. For every legitimate criticism of Python +3 I've seen, I've seen 10 that are bullshit. Come join us in the wonderful world +of sane string handling, type decorations, async/await, and more awesome +features. Every library supports it now. Let go of your biases and evaluate the +language honestly. diff --git a/content/blog/The-profitability-of-online-services.md b/content/blog/The-profitability-of-online-services.md @@ -0,0 +1,104 @@ +--- +date: 2014-10-10 +# vim: tw=80 +layout: post +title: On the profitability of image hosting websites +tags: [money, linkrot] +--- + +I've been doing a lot of thought about whether or not it's even possible to both +run a simple website *and* turn a profit from it *and* maintain a high quality +of service. In particular, I'm thinking about image hosts, considering that I +run one (a rather unprofitable one, too), but I would +think that my thoughts on this matter apply to more kinds of websites. That +being said, I'll just talk about media hosting because that's where I have +tangible expertise. + +I think that all image hosts suffer from the same sad pattern of eventual +failure. That pattern is: + +1. Create a great image hosting website (you should stop here) +2. Decide to monetize it +3. Add advertising +4. Stop allowing hotlinking +5. Add more advertising +6. Add social tools like comments, voting - attempt build a community to look at + your ads + +Monetization is a poison. You start realizing that you wrote a shitty website in +PHP on shared hosting and it can't handle the traffic. You spend more money on +it and realize you don't like spending your money on it, so you decide to +monetize, and now the poison has got you. There's an extremely fine line to walk +with monetization. You start wanting to make enough money to support your +servers, but then you think to yourself "well, I worked hard for this, maybe I +should make a living from it!" This introduces several problems. + +First of all, you made an image hosting website. It's already perfect. Almost +anything you can think of adding will only make it worse. If you suddenly decide +that you need to spend more time on it to justify taking money from it, then you +have a lot of time to get things wrong. You eventually run out of the good +features and start implementing the bad ones. + +More importantly, though, you realize that you should be making *more* money. +Maybe you can turn this into a nice job working on your own website! And that +means you should start a business and assign yourself a salary and start making +a profit and hire new people. The money has to come from somewhere. So you make +even more compromises. Eventually, people stop using your service. People start +to *detest* your service. It can get so bad that people will refuse to click on +any link that leads to your website. Your users will be harassed for continuing +to use your site. **You fail, and everyone hates you.** + +This trend is observable with PhotoBucket, ImageShack, TinyPic, the list goes +on. The conclusion I've drawn from this is that **it is impossible to run a +profitable image hosting service without sacrificing what makes your service +worthwhile**. We have arrived at a troubling place with the case of Imgur, +however. MrGrim (the creator of Imgur) also identified this trend and decided +to put a stop to it by building a simple image hosting service for Reddit. It +had great intentions, check out the old archive.org mirror of it[^1]. With +these great intentions and a great service, Imgur rose to become the 46th most +popular website globally[^2], and 18th in the United States alone, on the +shoulders of Reddit, which now ranks 47th. I'm going to expand upon this here, +particularly with respect to Reddit, but I included the ranks here to dissuade +anyone who says "there's more than Reddit out there" in response to this post. +Reddit is a *huge* deal. + +Other image hosts died down when people recognized their problems. Imgur has +reached a critical mass where that will not happen. 20% of all new Reddit posts +are Imgur, and most users just don't know better than to use anything else. That +being said, Imgur shows the signs of the image hosting poison. They stopped +being an image hosting website and became their own community. They added +advertising, which is fine on its own, but then they started redirecting direct +links[^3] to pages with ads. And still, their userbase is just as strong, +despite better alternatives appearing. + +I'm not sure what to do about Imgur. I don't like that they've won the mindshare +with a staggering margin. I do know that I've tried to make my own service +immune to the image hosting poison. We run it incredibly lean - we handle over +10 million HTTP requests per day on a single server that also does transcoding +and storage for $200 per month. We get about $20-$30 in monthly revenue from our +Project Wonderful[^4] ads, and a handful of donations that usually amount to +less than $20. Fortunately, $150ish isn't a hard number to pay out of our own +pockets every month, and we've made a damn good website that's extremely +scalable to keep our costs low. We haven't taken seed money, and we're not +really the sort to fix problems by throwing more money at it. We also won't be +hiring any paid staff any time soon, so our costs are pretty much constant. On +top of that, if we do fall victim to the image hosting poison, 100% of our code +is open source, so the next service can skip R&D and start being awesome +immediately. Even with all of that, though, all I can think of doing is sticking +around until people realize that Imgur really does suck. + +*2017-03-07 update* + +* mediacru.sh shut down (out of money) +* pomf.se shut down (out of money) +* minus.com shut down after going down the decline described in this post + +I have started a private service called [sr.ht](https://sr.ht), which I aim to +use to fix the problem by only letting my friends and I use it. It has +controlled growth and won't get too big and too expensive. It's on Github if you +want to [use it](https://github.com/SirCmpwn/sr.ht). + +[^1]: [Original Imgur home page](https://web.archive.org/web/20090225014924/http://imgur.com/) +[^2]: [Imgur on Alexa](http://www.alexa.com/siteinfo/imgur.com) +[^3]: [Imgur redirects "direct" links based on referrals](https://dillpickle.github.io/imgur-please-dont-be-the-next-tinypic-or-imageshack.html) +[^4]: [Project Wonderful, an advertising service that doesn't suck](https://www.projectwonderful.com/) diff --git a/content/blog/The-road-to-sustainable-FOSS.md b/content/blog/The-road-to-sustainable-FOSS.md @@ -0,0 +1,123 @@ +--- +date: 2018-02-24 +layout: post +title: The path to sustainably working on FOSS full-time +tags: [money, free software] +--- + +This is an article I didn't think I'd be writing any time soon. I've aspired to +work full-time on my free and open source software projects for a long time now, +but I have never expected that it could work. However, as of this week, I +finally have enough recurring donation revenue to break even on FOSS, and I've +started to put the extra cash away. I needed to set the next donation goal and +ran the numbers to figure out what it takes to work on FOSS full-time. + +Let me start with some context. I like to say "one-time donations buy pizza, +but recurring donations buy sustainable FOSS development". One-time donations +provide no financial security, so to date, (almost) all of my FOSS work has been +done in my spare time, and I've had to spend most of my time working on +proprietary software to make a living. This is the case for many other free +software advocates as well. Short of large grants on the scale of several +tens of thousands of dollars, if you want to get your rent paid and put food on +the table you need to be able to rely on something consistent. + +Some projects (e.g. Docker, Gitlab) have a compelling product in the market and +can build a company around their open source product. Some projects fulfill a +tangible need for some other business (such as writing software they depend on), +and for these projects large corporate sponsorships are often possible. However, +other kinds of projects (including most of my own) often have to rely on their +users for donations, and this has traditionally been a pretty dubious prospect. +In August of 2017, I was making $0 per month in recurring donations to +[fosspay](https://drewdevault.com/donate), down from an all-time peak of $20 per +month. When I was researching the possibility of starting a Patreon campaign, +the norm was less than $50/month even for the most successful open source +campaigns. As you can imagine, I was somewhat pessimistic. + +To my happy surprise, recurring donations to open source projects have taken +off, both for me and many others. It's amazing. After years of failing to earn a +substantial income from open source, as of today I'm receiving $547.74 per month +from three donation platforms ([fosspay](https://drewdevault.com/donate), +[LiberaPay](https://liberapay.com/SirCmpwn), and +[Patreon](https://patreon.com/SirCmpwn)). What's amazing is that because the +income comes from from several platforms and is distributed across over 80 +donators, I can feel confident in the security of this model. There are no +whales whose donations I have to live in fear of losing. There is no single +platform that I have to worry about going under or dramatically [changing their +fee structure](https://blog.patreon.com/not-rolling-out-fees-change/). This is +unprecedented - we're truly seeing the age of user-supported FOSS begin. + +I want to provide some transparency on how I set my goals and where the money +goes. You might be surprised to have heard me say that I'm only "breaking even" +on open source at $500/month! Many projects can run on a leaner budget, but +because I maintain so many different projects, I have different infrastructure +requirements. This mainly includes domains and servers for CI, project hosting, +releases, etc. At my scale, it's most cost-effective for me to self-host my own +dedicated servers in a local datacenter here in Philadelphia. This costs me +$380/month at the moment for 5U including power and network. I'm not done moving +my legacy infrastructure into the new datacenter, though, so I'm still paying +for some virtual private servers. As I migrate these, I will be reinvesting the +money saved into upgrading the new infrastructure. + +The next question is where to go from here. I have set my full-time goal at +$6,000 per month, which works out to $72,000 per year pre-tax, +pre-infrastructure expenses. This number is a lofty goal, and one that I expect +won't be met for a long time, if at all. This number is based on several +factors: cost of living, financial security, and taxes. The number is a +significant decrease from what I earn today, but it is enough to meet each of +these criteria. Let's break it down. + +Right now, I live in a pretty nice apartment in center city Philadelphia, which +costs me about $1700 per month. There are cheaper areas, but I make a +comfortable salary at my current job, which allows me to buy a nicer place. If +working on FOSS full-time appears viable, I will move to a cheaper location when +my lease is up and adjust the goal accordingly (I will probably move to a +cheaper location when my lease is up regardless, actually). Because I'm locked +into my lease (among other reasons), I did not factor major lifestyle changes +like moving to a cheaper location into the goal. Other costs of living, such as +food and necessities, work out to about $1000 per month. + +The other concern is financial security. I am lucky to live a comfortable life +today, but that is a result of hard lessons learned and has not always been the +case. I cannot focus on FOSS if I'm only earning just enough to cover my +expenses. Any major change in my life circumstances, such as a medical +emergency, natural disaster, or even something as benign as my computer breaking +down, would be a serious problem. Therefore, for me to consider working +full-time on anything, the earnings have to allow me to save money. To this end, +my earnings floor is at least 1.5x my expenditures. Some people think a more +liberal ratio is fine, but I'm a bit more conservative - I used to really +struggle to make ends meet. This raises the total to around $4000 per month. + +Add to this infrastructure costs we already talked about, and the total becomes +$4500 per month. Now we have to consider tax. If we look up the current [tax +brackets in the United +States](https://en.wikipedia.org/wiki/Tax_bracket#2018_tax_brackets_under_current_law) +and do some guesswork, we can estimate that I'll land in the 22% bracket under +this model. If I need my take-home to be $4500, we can divide that by 78% and +arrive at the total: $5769 per month[^1]. Round it up to $6000 and this is our +goal. + +These numbers are pretty high. I understand many people, including some of those +who donate to me, are much less fortunate than I. My lifestyle is a reflection +of my assumption that the open source donation model does not provide a +sustainable source of income. Based on this, I've focused my career on paid +proprietary software development, which pays very competitively in the United +States. The privileges afforded by this have shaped my costs of living. Rather +than make up a number smaller than my actual expenditures, I prefer to be honest +with you about this. + +This doesn't necessarily have to remain the case forever. As my income from +donations increase, utilizing them as a primary source of income becomes more +feasible, and I am prepared to reorient my life with this in mind. You can +expect my donation goal to *decrease* as the number of donations *increases*. +This will probably take a long time, on the scale of years. My housing situation +and costs of living in Philadelphia will change during this time - I might not +stay in Philadelphia, I might have to change jobs, etc. It's difficult to set +a more optimistic goal today that will prove correct when its met. For that +reason, my goal is adjusted with respect to my current conditions, not the +ideal. + +So that's how it shakes out! I'm glad we can finally have this conversation, and +I'm incredibly thankful for your support. Thank you for everything, and I'm +looking forward to making even more cool stuff for you in the future. + +[^1]: Correction: that's not how taxes work, but the simplified version gives us a more conservative number - which is a good thing when your livelihood is at stake. diff --git a/content/blog/The-worst-bugs.md b/content/blog/The-worst-bugs.md @@ -0,0 +1,183 @@ +--- +date: 2014-02-02 +layout: post +title: The bug that hides from breakpoints +tags: [KnightOS, kernel hacking] +--- + +This is the story of the most difficult bug I ever had to solve. See if you can +figure it out before the conclusion. + +### Background + +For some years now, I've worked on a kernel for Texas +Instruments calculators called [KnightOS](https://github.com/KnightOS/kernel). +This kernel is written entirely in assembly, and targets the old-school z80 +processor from back in 1976. This classic processor was built without any +concept of protection rings. It's an 8-bit processor, with 150-some instructions +and (in this application) 32K of RAM and 32K of Flash. This stuff is so old, I +ended up writing most of the KnightOS toolchain from scratch rather than try to +get archaic assemblers and compilers running on modern systems. + +When you're working in an enviornment like this, there's no seperation between +kernel and userland. All "userspace" programs run as root, and crashing the entire +system is a simple task. All the memory my kernel sets aside for the +process table, or memory ownership, file handles, stacks, any other executing +process - any program can modify this freely. Of course, we have to rely on the +userland to play nice, and it usually does. But when there are bugs, they can be a +real pain in the ass to hunt down. + +### The elusive bug + +The original bug report: **When running the counting demo and switching between +applications, the thread list graphics become corrupted.** + +I can reproduce this problem, so I settle into my development enviornment and I +set a breakpoint near the thread list's graphical code. I fire up the emulator and +repeat the steps... but it doesn't happen. This happened consistently: **the bug +was not reproduceable when a breakpoint was set**. Keep in mind, I'm running this +in a z80 emulator, so the enviornment is supposedly no different. There's no +debugger attached here. + +Though this is quite strange, I don't immediately despair. I try instead setting a +"breakpoint" by dropping an infinite loop in the code, instead of a formal +breakpoint. I figure that I can halt the program flow manually and open the +debugger to inspect the problem. However, the bug wouldn't be tamed quite so +easily. The bug was unreproducable when I had this psuedo-breakpoint in place, +too. + +At this point, I started to get a little frustrated. How do I debug a problem that +disappears when you debug it? I decided to try and find out what caused it after +it had taken place, by setting the breakpoint to be hit only after the graphical +corruption happened. Here, I gained some ground. I was able to reproduce it, and +*then* halt the machine, and I could examine memory and such after the bug was +given a chance to have its way over the system. + +I discovered the reason the graphics were being corrupted. The kernel kept the +length of the process table at a fixed address. The thread list, in order to draw +the list of active threads, looks to this value to determine how many threads it +should draw. Well, when the bug occured, the value was too high! The thread list +was drawing threads that did not exist, and the text rendering puked non-ASCII +characters all over the display. But why was that value being corrupted? + +It was an oddly specific address to change. None of the surrounding memory was +touched. Making it even more odd was the very specific conditions this happened +under - only when the counting demo was running. I asked myself, "what makes the +counting demo unique?" It hit me after a moment of thought. The counting demo +existed to demonstrate non-supsendable threads. The kernel would stop executing +threads (or "suspend" them) when they lost focus, in an attempt to keep the +system's very limited resources available. The counting demo was marked as +non-suspendable, a feature that had been implemented a few months prior. It +showed a number on the screen that counted up forever, and the idea was that you +could go give some other application focus, come back, and the number would have +been counting up while you were away. A background task, if you will. + +A more accurate description of the bug emerged: "the length of the kernel process +table gets corrupted when launching the thread list when a non-suspendable thread +is running". What followed was hours and hours of crawling through the hundreds of +lines of assembly between summoning the thread list, and actually seeing it. I'll +spare you the details, because they are very boring. We'll pick the story back up +at the point where I had isolated the area in which it occured: applib. + +The KnightOS userland offered "applib", a library of common functions applications +would need to get the general UX of the system. Among these was the function +`applibGetKey`, which was a wrapper around the kernel's `getKey` function. The +idea was that it would work the same way (return the last key pressed), but for +special keys, it would do the appropriate action for you. For example, if you +pressed the F5 key, it would suspend the current thread and launch the thread +list. This is the mechanism with which most applications transfer control out of +their own thread and into the thread list. + +Eager that I had found the source of the issue, I placed a breakpoint nearby. That +same issue from before struck again - the bug vanished when the breakpoint was +set. I tried a more creative approach: instead of using a proper breakpoint, I +asked the emulator to halt whenever that address was written to. Even still - the +bug hid itself whenever this happened. + +I decided to dive into the kernel's getKey function. Here's the start of the +function, as it appeared at the time: + +``` +getKey: + call hasKeypadLock + jr _ + xor a + ret +_: push bc +; ... +``` + +I started going through this code line-by-line, trying to see if there was +anything here that could concievably touch the thread table. I noticed a minor +error here, and corrected it without thinking: + +``` +getKey: + call hasKeypadLock + jr z, _ + xor a + ret +_: push bc +; ... +``` + +The simple error I had corrected: getKey was pressing forward, even when the +current thread didn't have control of the keyboard hardware. This was a silly +error - only two characters were omitted. + +A moment after I fixed that issue, the answer set in - this was the source of the +entire problem. Confirming it, I booted up the emulator with this change applied +and the bug was indeed resolved. + +Can you guess what happened here? Here's the other piece of the puzzle to help you +out, translated more or less into C for readability: + +```c +int applibGetKey() { + int key = getKey(); + if (key == KEY_F5) { + launch_threadlist(); + suspend_thread(); + } + return key; +} +``` + +Two more details you might not have picked up on: + +* applibGetKey is non-blocking +* suspend_thread suspends the current thread immediately, so it doesn't return until the + thread resumes. + +### The bug, uncovered + +Here's what actually happened. For most threads (the suspendable kind), that +thread stops processing when `suspend_thread()` is called. The usually +non-blocking applibGetKey function blocks until the thread is resumed in this +scenario. However, the counting demo was *non-suspendable*. The suspend_thread +function has no effect, by design. So, suspend_thread did not block, and the +keypress was returned straight away. By this point, the thread list had launched +properly and it was given control of the keyboard. + +However, the counting demo went back into its main loop, and started calling +applibGetKey again. Since the average user's finger remained pressed against the +button for a few moments more, applibGetKey *continued to launch the thread list, +over and over*. The thread list itself is a special thread, and it doesn't +actually have a user-friendly name. It was designed to ignore itself when it drew +the active threads. However, it was *not* designed to ignore other instances of +itself, the reason being that there would never be two of them running at once. +When attempting to draw these other instances, the thread list started rendering +text that wasn't there, causing the corruption. + +This bug vanished whenever I set a breakpoint because it would halt the system's +keyboard processing logic. I lifted my finger from the key before allowing it to +move on. + +The solution was to make the kernel's getKey function respect hardware locks by +fixing that simple, two-character typo. That way, the counting demo, which had no +right to know what keys were being pressed, would not know that they key was still +being pressed. + +The debugging described by this blog post took approximately three weeks. + +[Discussion on Hacker News](https://news.ycombinator.com/item?id=7688700) diff --git a/content/blog/The-wrong-words-but-the-right-ideas.md b/content/blog/The-wrong-words-but-the-right-ideas.md @@ -0,0 +1,83 @@ +--- +date: 2019-09-17 +layout: post +title: Don't sacrifice the right ideas to win the right words +tags: [philosophy] +--- + +There is a difference between free software and open-source software. But you +have to squint to see it. Software licenses which qualify for one title but not +the other are exceptionally rare. + +A fascination with linguistics is common among hackers, and I encourage and +participate in language hacking myself. Unfortunately, that seems to seep into +the Free Software Foundation's message a bit too much. Let's see if any of this +rings familiar: + +> It's not actually open source, but free software. You see, "open source" is a +> plot by the commercial software industry to subvert the "free software" +> movement... + +> No, it's free-as-in-freedom, not free-as-in-beer. Sometimes we call it "libre" +> software, borrowing the French or Spanish word, because in English... + +> What you're referring to as Linux, is in fact, GNU/Linux, or as I've recently +> taken to calling it, GNU plus Linux. Linux is not an operating system... + +What do all of these have in common? The audience already agrees with the +speaker on the ideas, but this becomes less so with every word. This kind of +pedantry lacks tact and pushes people away from the movement. No one wants to +talk to someone who corrects them like this, so people shut down and stop +listening. The speaker gains the self-satisfaction that comes with demonstrating +that you're smarter than someone else, but the cost is pushing that person away +from the very ideals you're trying to clarify. This approach doesn't help the +movement, it's just being a dick. + +For this reason, even though I fully understand the difference between free and +open-source software, I use the terms basically interchangeably. In practice +they are effectively the same thing. Then, I preach the ideologies behind free +software even when discussing open-source software. The ideas are what matters, +the goal is to get people thinking on your wavelength. If they hang around long +enough, they'll start using your words, too. That's how language works. + +The crucial distinction of the free software movement is less about "free +software", after all, and more about copyleft. But, because the FSF pushes +copyleft *and* free software, and because many FSF advocates are pedantic and +abrasive, many people check out before they're told the distinction between free +software and copyleft. This leads to the listener *equivocating* free software +with copyleft software, which undermines the message and hurts both.[^1] + +[^1]: For those unaware, copyleft is any "viral" license, where using copyleft code requires also using a copyleft license for your derived work. Free software is just software which meets the [free software definition][fsd], which is in practice just about all free *and* open-source software, including MIT or BSD licensed works. + +[fsd]: https://www.gnu.org/philosophy/free-sw.html + +This lack of tact is why I find it difficult to accept the FSF as a +representative of the movement I devote myself to. If your goal is to strengthen +the resolve and unity of people who already agree with you by appealing to +tribalism, then this approach is effective - but remember that it strengthens +the opposing tribes, too. If your goal is to grow the movement and win the +hearts and minds of the people, then you need to use more tact in your language. +Turn that hacker knack for linguistic hacking towards *this* goal, of thinking +over how your phrasing and language makes different listeners feel. The +resulting literature will be much more effective. + +Attack the systems and individuals who brought about the circumstances that +frustrate your movement, but don't attack their victims. It's not the user's +fault that they were raised on proprietary software. The system which installed +proprietary software on their school computers is the one to blame. Our goals +should be things like introducing Linux to the classroom, petitioning our +governments to require taxpayer-funded software to be open source, eliminating +Digital Restrictions Management[^2], pushing for right to repair, and so on. Why +is "get everyone to say 'libre' instead of 'open-source'" one of our goals +instead? + +[^2]: This kind of pedantry, which deliberately misrepresents the acronym (which is rightly meant to be "Digital Rights Management"), is more productive, since the people insulted by it are not the victims of DRM, but the perpetrators of it. Also, "Digital Rights Management" is itself a euphemism, or perhaps more accurately a kind of doublespeak, which invites a similar response. + +An aside: sometimes language *is* important. When someone has the wrong words +but the right ideas, it's not a big deal. When someone has the wrong *ideas* and +is appropriating the words to support them, that's a problem. This is why I +still come down hard on companies which gaslight users with faux-open software +licenses like the Commons Clause or the debacle with RedisLabs. + +*Note: this article is not about Richard Stallman. I have no comment on the +recent controversies.* diff --git a/content/blog/Thoughts-on-performance.md b/content/blog/Thoughts-on-performance.md @@ -0,0 +1,87 @@ +--- +date: 2020-02-21 +title: Thoughts on performance & optimization +layout: post +tags: [performance] +--- + +The idea that programmers ought to or ought not to be called "software +engineers" is a contentious one. How you approach optimization and performance +is one metric which can definitely push my evaluation of a developer towards the +engineering side. Unfortunately, I think that a huge number of software +developers today, even senior ones, are approaching this problem poorly. + +Centrally, I believe that you cannot effectively optimize a system which you do +not understand. Say, for example, that you're searching for a member of a +linked list, which is an O(n) operation. You know this can be improved by +switching from a linked list to a sorted array and using a binary search. So, +you spend a few hours refactoring, commit the implementation, and... the +application is no faster. What you failed to consider is that the lists are +populated from data received over the network, whose latency and bandwidth +constraints make the process much slower than any difference made by the kind of +list you're using. If you're not optimizing your bottleneck, then you're +wasting your time. + +This example seems fairly obvious, and I'm sure you, our esteemed reader, would +not have made this mistake. In practice, however, the situation is usually more +subtle. Thinking about your code really hard, making assumptions, and then +acting on them is not the most effective way to make performance improvements. +Instead, we apply the scientific method: we think really hard, *form a +hypothesis*, make predictions, test them, and then apply our conclusions. + +To implement this process, we need to describe our performance in factual terms. +All software requires a certain amount of resources &mdash; CPU time, RAM, disk +space, network utilization, and so on. These can also be described over time, +and evolve as the program takes on different kinds of work. For example, we +could model our program's memory use as bytes allocated over time, and perhaps +we can correlate this with different stages of work &mdash; "when the program +starts task C, the rate of memory allocation increases by 5MiB per second". We +identify bottlenecks &mdash; "this program's primary bottleneck is disk I/O". +When we hit performance problems, then we know that we need to upgrade to SSDs, +or predict what reads will be needed later and prep them in advance, cache data +in RAM, etc. + +Good optimizations are based on factual evidence that the program is not +operating within its constraints in certain respects, then improving on those +particular problems. You should always conduct this analysis before trying to +solve your problems. I generally recommend conducting this analysis in advance, +so that you can predict performance issues before they occur, and plan for them +accordingly. For example, if you know that your disk utilization grows by 2 GiB +per day, and you're on a 500 GiB hard drive, you've got about 8 months to plan +your next upgrade, and you shouldn't be surprised by an ENOSPC when the time +comes. + +For CPU bound tasks, this is also where a general understanding of the +performance characteristics of various data structures and algorithms is useful. +When you know you're working on something which *will become* the application's +bottleneck, you would be wise to consider algorithms which can be implemented +efficiently. However, it's equally important to re-prioritize performance when +you're not working on your bottlenecks, and instead consider factors like +simplicity and conciseness more seriously. + +Much of this will probably seem obvious to many readers. Even so, I think the +general wisdom described here is institutional, so it's worth writing down. I +also want to call out some specific behaviors that I see in software today which +I think don't take this well enough into account. + +I opened by stating that I believe that you cannot effectively optimize a system +which you do not understand. There are two groups of people I want to speak to +with this in mind: library authors (especially the standard library), and +application programmers. There are some feelings among library authors that +libraries should be fairly opaque, and present high-level abstractions over +their particular choices of algorithms, data structures, and so on. I think this +represents a fundamental lack of trust with the programmer downstream. Rather +than write idiot-proof abstractions, I think it's better to trust the downstream +programmer, explain to them how your system works, and equip them with the tools +to audit their own applications. After all: your library is only a small +component of *their* system, not yours &mdash; and you cannot optimize a system +you don't understand. + +And to the application programmer, I urge you to meet your dependencies in the +middle. Your entire system is your responsibility, including your dependencies. +When the bottleneck lies in someone else's code, you should be prepared to dig +into their code, patch it, and send a fix upstream, or to restructure your code +to route the bottleneck out. Strive to understand how your dependencies, up to +and including the stdlib, compiler, runtime, kernel, and so on, will perform in +your scenario. And again to the standard library programmer: help them out by +making your abstractions thin, and your implementations simple and debuggable. diff --git a/content/blog/Understanding-pointers.md b/content/blog/Understanding-pointers.md @@ -0,0 +1,280 @@ +--- +date: 2016-05-28 +# vim: tw=80 +layout: post +title: Understanding pointers +tags: [C, instructive] +--- + +I was recently chatting with a new contributor to Sway who is using the project +as a means of learning C, and he had some questions about what `void**` meant +when he found some in the code. It became apparent that this guy only has a +basic grasp on pointers at this point in his learning curve, and I figured it +was time for another blog post - so today, I'll explain pointers. + +To understand pointers, you must first understand how memory works. Your RAM is +basically a flat array of +[octets](https://en.wikipedia.org/wiki/Octet_(computing)). Your compiler +describes every data structure you use as a series of octets. For the context of +this article, let's consider the following memory: + +{:.table} +| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | +|:-------|:-------|:-------|:-------|:-------|:-------|:-------|:-------| +| 0x00 | 0x00 | 0x00 | 0x00 | 0x08 | 0x42 | 0x00 | 0x00 | +|========|========|========|========|========|========|========|========| + +We can refer to each element of this array by its index, or address. For +example, the value at address 0x0004 is 0x08. On this system, we're using 16-bit +addresses to refer to 8-bit values. On an i686 (32-bit) system, we use 32-bit +addresses to refer to 8-bit values. On an amd64 (64-bit) system, we use 64-bit +addresses to refer to 8-bit values. On Notch's imaginary DCPU-16 system, we use +16-bit addresses to refer to 16-bit values. + +To refer to the value at 0x0004, we can use a pointer. Let's declare it like so: + +```c +uint8_t *value = (uint8_t *)0x0004; +``` + +Here we're declaring a variable named value, whose type is `uint8_t*`. The * +indicates that it's a pointer. Now, because this is a 16-bit system, the size of +a pointer is 16 bits. If we do this: + +```c +printf("%d\n", sizeof(value)); +``` + +It will print 2, because it takes 16-bits (or 2 bytes) to refer to an address on +this system, even though the value there is 8 bits. On your system it would +probably print 8, or maybe 4 if you're on a 32-bit system. We could also do this: + +```c +uint16_t address = 0x0004; +uint8_t *ptr = (uint8_t *)address; +``` + +In this case we're not casting the `uint16_t` value 0x0004 to a `uint8_t`, which +would truncate the integer. No, instead, we're casting it to a `uint8_t*`, which +is the size required to represent a pointer on this system. All pointers are the +same size. + +## Dereferencing pointers + +We can refer to the value at the other end of this pointer by *dereferencing* it. +The pointer is said to contain a *reference* to a value in memory. By +*dereferencing* it, we can obtain that value. For example: + +```c +uint8_t *value = (uint8_t *)0x0004; +printf("%d\n", *value); // prints 8 +``` + +## Working with multi-byte values + +Even though memory is basically a big array of `uint8_t`, thankfully we can work +with other kinds of data structures inside of it. For example, say we wanted to +store the value 0x1234 in memory. This doesn't fit in 8 bits, so we need to +store it at two different addresses. For example, we could store it at 0x0006 +and 0x0007: + +{:.table} +| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | +|:-------|:-------|:-------|:-------|:-------|:-------|:-------|:-------| +| 0x00 | 0x00 | 0x00 | 0x00 | 0x08 | 0x42 | 0x34 | 0x12 | +|========|========|========|========|========|========|========|========| + +*0x0007 makes up the first byte of the value, and *0x0006 makes up the second +byte of the value. + +<div class="well"> + Why not the other way around? Well, most systems these days use the "little + endian" notation for storing multi-byte integers in memory, which stores the + least significant byte first. The least significant byte is the one with the + smallest order of magnitude (in base sixteen). To get the final number, we + use (0x12 * 0x100) + (0x34 * 0x1), which gives us 0x1234. Read more about + endianness <a href="https://en.wikipedia.org/wiki/Endianness">here</a>. +</div> + +C allows us to use pointers that refer to these sorts of composite values, like +so: + +```c +uint16_t *value = (uint16_t *)0x0006; +printf("0x%X\n", *value); // Prints 0x1234 +``` + +Here, we've declared a pointer to a value whose type is `uint16_t`. Note that the +size of this pointer is the same size of the `uint8_t*` pointer - 16 bits, or +two bytes. The value it *references*, though, is a different type than +`uint8_t*` references. + +## Indirect pointers + +Here comes the crazy part - you can work with pointers to pointers. The address +of the `uint16_t` pointer we've been talking about is 0x0006, right? Well, we +can store that number in memory as well. If we store it at 0x0002, our memory +looks like this: + +{:.table} +| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | +|:-------|:-------|:-------|:-------|:-------|:-------|:-------|:-------| +| 0x00 | 0x00 | 0x06 | 0x00 | 0x08 | 0x42 | 0x34 | 0x12 | +|========|========|========|========|========|========|========|========| + +The question might then become, how do we get it out again? Well, we can use a +pointer *to that pointer*! Check out this code: + +```c +uint16_t **pointer_to_a_pointer = (uint16_t**)0x0002; +``` + +This code just declared a variable whose type is `uint16_t**`, which a pointer +whose value is a `uint16_t*`, which itself points to a value that is a +`uint16_t`. Pretty cool, huh? We can dereference this too: + +```c +uint16_t **pointer_to_a_pointer = (uint16_t**)0x0002; +uint16_t *pointer = *pointer_to_a_pointer; +printf("0x%X\n", *pointer); // Prints 0x1234 +``` + +We don't actually even need the intermediate variable. This works too: + +```c +uint16_t **pointer_to_a_pointer = (uint16_t**)0x0002; +printf("0x%X\n", **pointer_to_a_pointer); // Prints 0x1234 +``` + +## Void pointers + +The next question that would come up to your average C programmer would be, +"well, what is a `void*`?" Well, remember earlier when I said that all pointers, +regardless of the type of value they reference, are just fixed size integers? +In the imaginary system we've been talking about, pointers are 16-bit addresses, +or indexes, that refer to places in RAM. On the system you're reading this +article on, it's probably a 64-bit integer. Well, we don't actually need to +specify the type to be able to manipulate pointers if they're just a fixed size +integer - so we don't have to. A `void*` stores an arbitrary address without +bringing along any type information. You can later *cast* this variable to a +specific kind of pointer to dereference it. For example: + +```c +void *pointer = (void*)0x0006; +uint8_t *uintptr = (uint8_t*)pointer; +printf("0x%X", *uintptr); // prints 0x34 +``` + +Take a closer look at this code, and recall that 0x0006 refers to a 16-bit value +from the previous section. Here, though, we're treating it as an 8-bit value - +the `void*` contains no assumptions about what kind of data is there. The result +is that we end up treating it like an 8-bit integer, which ends up being the +least significant byte of 0x1234; + +## Dereferencing structures + +In C, we often work with structs. Let's describe one to play with: + +```c +struct coordinates { + uint16_t x, y; + struct coordinates *next; +}; +``` + +Our structure describes a linked list of coordinates. X and Y are the +coordinates, and next is a pointer to the next set of coordinates in our list. +I'm going to drop two of these in memory: + +{:.table} +| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | +|:-------|:-------|:-------|:-------|:-------|:-------|:-------|:-------| +| 0xAD | 0xDE | 0xEF | 0xBE | 0x06 | 0x00 | 0x34 | 0x12 | +|========|========|========|========|========|========|========|========| + +Let's write some C code to reason about this memory with: + +```c +struct coordinates *coords; +coords = (struct coordinates*)0x0000; +``` + +If we look at this structure in memory, you might already be able to pick out +the values. C is going to store the fields of this struct in order. So, we can +expect the following: + +```c +printf("0x%X, 0x%X", coords->x, coords->y); +``` + +To print out "0xDEAD, 0xBEEF". Note that we're using the structure dereferencing +operator here, `->`. This allows us to dereference values *inside* of a +structure we have a pointer to. The other case is this: + +```c +printf("0x%X, 0x-X", coords.x, coords.y); +``` + +Which only works if `coords` is not a pointer. We also have a pointer within +this structure named next. You can see in the memory I included above that its +address is 0x0004 and its value is 0x0006 - meaning that there's another `struct +coordinates` that lives at 0x0006 in memory. If you look there, you can see the +first part of it. It's X coordinate is 0x1234. + +## Pointer arithmetic + +In C, we can use math on pointers. For example, we can do this: + +```c +uint8_t *addr = (uint8_t*)0x1000; +addr++; +``` + +Which would make the value of `addr` 0x1001. But this is only true for pointers +whose type is 1 byte in size. Consider this: + +```c +uint16_t *addr = (uint16_t*)0x1000; +addr++; +``` + +Here, `addr` becomes 0x1002! This is because ++ on a pointer actually adds +`sizeof(type)` to the actual address stored. The idea is that if we only added +one, we'd be referring to an address that is *in the middle* of a uint16_t, +rather than the next uint16_t in memory that we meant to refer to. This is also +how arrays work. The following two code snippets are equivalent: + +```c +uint16_t *addr = (uint16_t*)0x1000; +printf("%d\n", *(addr + 1)); +``` + +```c +uint16_t *addr = (uint16_t*)0x1000; +printf("%d\n", addr[1]); +``` + +## NULL pointers + +Sometimes you need to work with a pointer that points to something that may not +exist yet, or a resource that has been freed. In this case, we use a NULL +pointer. In the examples you've seen so far, 0x0000 is a valid address. This is +just for simplicity's sake. In practice, pretty much no modern computer has +any reason to refer to the value at address 0. For that reason, we use NULL to +refer to an uninitialized pointer. Dereferencing a NULL pointer is generally a +Bad Thing and will lead to segfaults. As a fun side effect, since NULL is 0, we +can use it in an if statement: + +```c +void *ptr = ...; +if (ptr) { + // ptr is valid +} else { + // ptr is not valid +} +``` + +I hope you found this article useful! If you'd +like something fun to read next, read about ["three star +programmers"](http://c2.com/cgi/wiki?ThreeStarProgrammer), or programmers who +have variables like `void***`. diff --git a/content/blog/Use-the-right-tool.md b/content/blog/Use-the-right-tool.md @@ -0,0 +1,53 @@ +--- +date: 2016-09-17 +# vim: set tw=80 +layout: post +title: Using the right tool for the job +tags: [philosophy] +--- + +One of the most important choices you'll make for the software you write is what +you write it in, what frameworks you use, the design methodologies to subscribe +to, and so on. This choice doesn't seem to get the respect it's due. These are +some of the only choices you'll make that *you cannot change*. Or, at least, +these choices are among the most difficult ones to change. + +People often question why TrueCraft is written in C# next to projects like Sway +in C, alongside KnightOS in Assembly or sr.ht in Python. It would certainly be +easier from the outset if I made every project in a language I'm comfortable +with, using tools and libraries I'm comfortable with, and there's certainly +something to be had for that. That's far from being the only concern, though. + +A new project is a *great* means of learning a new language or framework - the +only effective means, in fact. However, the inspiration and drive for new +projects doesn't come often. I think that the opportunity for learning is more +important than the short term results of producing working code more quickly. +Making a choice that's more well suited to the problem at the expense of comfort +will also help your codebase in the long run. Why squander the opportunity to +choose something unfamiliar when you have the rare opportunity to start working +on a new project? + +I'm not advocating for you to use something new for every project, though. I'm +suggesting that you detatch your familiarity with your tools from the +decision-making process. I often reach for old tools when starting a new +project, but I have learned enough about new tools that I can judge what +projects are a good use-case for them. Sometimes this doesn't work out, too - I +just threw away and rewrote a prototype in C after deciding that it wasn't a +good candidate for Rust. + +Often it does work out, though. I'm glad I chose to learn Python for MediaCrush +despite having no experience with it (thanks again for the help with that, +Jose!). Today I still know it was the correct choice and knowing it has hugely +expanded my programming skills, and without that choice there probably wouldn't +have been a Kerbal Stuff or a sr.ht or likely even the new API we're working on +at Linode. I'm glad I chose to learn C for z80e, though I had previously written +emulators in C#. Without it there wouldn't be many other great tools in the +KnightOS ecosystem written in C, and there wouldn't be a Sway or an aerc. I'm +glad I learned ES6 and React instead of falling back on the familiar Knockout.js +when building prototypes for the new Linode manager as well. + +Today, I have a mental model of the benefits and drawbacks of a lot of +languages, frameworks, libraries, and platforms I don't know how to use. I'm +sort of waiting for projects that would be well suited to things like Rust or +Django or Lisp or even Plan 9. Remember, the skills you already know make for a +great hammer, but you shouldn't nail screws to the wall. diff --git a/content/blog/Using-Wl-wrap-for-mocking-in-C.md b/content/blog/Using-Wl-wrap-for-mocking-in-C.md @@ -0,0 +1,123 @@ +--- +date: 2016-07-19 +# vim: tw=80 +title: Using -Wl,--wrap for mocking in C +layout: post +tags: [C] +--- + +One of the comforts I've grown used to in higher level languages when testing +my code is mocking. The idea is that in order to test some code in isolation, +you should "mock" the behavior of things it depends on. Let's see a (contrived) +example: + +```c +int read_to_end(FILE *f, char *buf) { + int r = 0, l; + while (!feof(f)) { + l = fread(buf, 1, 256, f); + r += l; + buf += l; + } + return r; +} +``` + +If we want to test this function without mocking, we would need to actually open +a specially crafted file and provide a `FILE*` to the function. However, with +the linker `--wrap` flag, we can define a wrapper function. Using `-Wl,[flag]` +in your C compiler command line will pass `[flag]` to the linker. Gold (GNU) and +lld (LLVM) both support the wrap flag, which specifies a function to be +"wrapped". If I use `-Wl,--wrap=fread`, then the code above will be compiled +like so: + +```c +int read_to_end(FILE *f, char *buf) { + int r = 0, l; + while (!feof(f)) { + l = __wrap_fread(buf, 1, 256, f); + r += l; + buf += l; + } + return r; +} +``` + +And if I add `-Wl,--wrap=feof` we'll get this: + +```c +int read_to_end(FILE *f, char *buf) { + int r = 0, l; + while (!__wrap_feof(f)) { + l = __wrap_fread(buf, 1, 256, f); + r += l; + buf += l; + } + return r; +} +``` + +Now, we can define some functions that do the behavior we need to test instead +of invoking fread directly: + +```c +int feof_return_value = 0; + +int __wrap_feof(FILE *f) { + assert(f == (FILE *)0x1234); + return feof_return_value; +} + +void test_read_to_end_eof() { + // ... + feof_return_value = 1; + read_to_end((FILE *)0x1234, buf); + // ... +} +``` + +Using `--wrap` also conveniently defines `__real_feof` and `__real_fread` if we +need them. + +Unfortunately, you can't have two different wrappers for the same function in +an executable. This could lead to having to write several executables for each, +or making your wrapper function smart enough to have several configurable +outcomes. + +Eventually I intend to write my own test framework for C, which will use +wrappers to support mocking. I want wrappers to be done automatically and have +it behave something like this: + +```c +static int fake_fread(void *ptr, size_t size, + size_t nmemb, FILE *stream) { + const char *hello = "Hello world!"; + memcpy(ptr, hello, strlen(hello) + 1); + return strlen(hello) + 1; +} + +void test_read_to_end() { + FILE *test = (FILE *)0x1234; + char *buffer = char[1024]; + + mock_t *mock_feof = configure_mock(feof, "p"); + mock_feof->call(0)->returns(0); + mock_feof->returns(1); + + // pzzp is pointer, size_t, size_t, pointer + // Tells us what the fread arguments look like + mock_t *mock_fread = configure_mock(fread, "pzzp"); + mock_fread->exec(fake_fread); + + read_to_end(test, buffer); + + assert(mock_feof->call_count == 2); + assert((FILE*)mock_feof->call(0)->args(0) == test); + + assert(mock_fread->call_count == 1); + assert((FILE*)mock_fread->call(0)->args(0) == buffer); + assert((FILE*)mock_fread->call(0)->args(3) == test); + + assert(strcmp(buffer, "Hello world!") == 0); +} +``` diff --git a/content/blog/Using-cage-for-a-seamless-RDP-Wayland-desktop.md b/content/blog/Using-cage-for-a-seamless-RDP-Wayland-desktop.md @@ -0,0 +1,65 @@ +--- +date: 2019-04-23 +layout: post +title: Using Cage for a seamless remote Wayland session +tags: ["wayland", "cage"] +--- + +Congratulations to Jente Hidskes on [the first release of +Cage](https://www.hjdskes.nl/blog/cage-01/)! Cage is a Wayland compositor +designed for kiosks - though, as you'll shortly find out, is useful in many +unexpected ways. It launches a single application, in fullscreen, and exits the +compositor when that application exits. This lets you basically add a +DRM+KMS+libinput session to any Wayland-compatible application (or X application +via XWayland) and run it in a tiny wlroots compositor. + +I actually was planning on writing something like this at some point (for a +project which still hasn't really come off the ground yet), so I was excited +when Jente [announced it](https://www.hjdskes.nl/blog/cage/) in December. With +the addition of the [RDP backend](https://github.com/swaywm/wlroots/pull/1578) +in wlroots, I thought it would be cool to combine these to make a seamless +remote desktop experience. In short, I installed +[FreeRDP](http://www.freerdp.com/) and Cage on my laptop, and +[sway](https://swaywm.org) on my desktop. On my desktop, I [generated TLS +certificates per the wlroots +docs](https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md#rdp-backend) +and ran sway like so: + +```sh +WLR_RDP_TLS_CERT_PATH=$HOME/tls.crt \ +WLR_RDP_TLS_KEY_PATH=$HOME/tls.key \ +WLR_BACKENDS=rdp \ +sway +``` + +Then, on my laptop, I can run this script: + +```sh +#!/bin/sh +if [ $# -eq 0 ] +then + export XDG_RUNTIME_DIR=/tmp + exec cage sway-remote launch +else + sleep 3 + exec xfreerdp \ + -v homura \ + --bpp 32 \ + --size 1280x800 \ + --rfx +fi +``` + +The first branch is taken on the first run, and it starts up cage and asks it to +run this script again. The second branch then starts up xfreerdp and connects to +my desktop (its hostname is `homura`). xfreerdp is then fullscreened and all of +my laptop's input events are directed to it. The result is an experience which +is basically identical to running sway directly on my laptop, except it's +actually running on my desktop and using the remote desktop protocol to send +everything back and forth. + +This isn't especially practical, but it is a cool hack. It's definitely not +network transparency like some people want, but I wasn't aiming for that. It's +just a neat thing you can do now that we have an RDP backend for wlroots. And +congrats again to Jente - be sure to give Cage a look and see if you can think +of any other novel use-cases, too! diff --git a/content/blog/Using-git-with-discipline.md b/content/blog/Using-git-with-discipline.md @@ -0,0 +1,113 @@ +--- +date: 2019-02-25 +layout: post +title: Tips for a disciplined git workflow +tags: ["git", "philosophy"] +--- + +Basic git usage involves typing a few stock commands to "[sync everyone +up](https://xkcd.com/1597/)". Many people who are frustrated with git become so +because they never progress beyond this surface-level understanding of how it +works. However, mastering git is easily worth your time. How much of your day is +spent using git? I would guess that there are many tools in your belt that you +use half as often and have spent twice the time studying. + +[![](https://imgs.xkcd.com/comics/is_it_worth_the_time.png)](https://xkcd.com/1205/) + +If you'd like to learn more about git, I suggest starting with [Chapter +10][ch-10] of [Pro Git][pro-git] (it's free!), then reading chapters 2, 3, +and 7. The rest is optional. In this article, we're going to discuss how you can +apply the tools discussed in the book to a disciplined and productive git +workflow. + + +[ch-10]: https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain +[pro-git]:https://git-scm.com/book/en/v2 + +### The basics: Writing good commit messages + +[![](https://imgs.xkcd.com/comics/git_commit.png)](https://xkcd.com/1296/) + +You may have heard this speech before, but bear with me. Generally, you should +not use `git commit -m "Your message here"`. Start by configuring git to use +your favorite editor: `git config --global core.editor vim`, then simply run +`git commit` alone. Your editor will open and you can fill in the file with your +commit message. The first line should be limited to 50 characters in length, and +should complete this sentence: when applied, this commit will... "Fix text +rendering in CJK languages". "Add support for protocol v3". "Refactor CRTC +handling". Then, add a single empty line, and expand on this in the *extended +commit description*, which should be hard-wrapped at 72 columns, and include +details like rationale for the change, tradeoffs and limitations of the +approach, etc. + +We use 72 characters because that's [the standard width of an email][rfc], and +email is an important tool for git. The 50 character limit is used because the +first line becomes the subject line of your email - and lots of text like +"`[PATCH linux-usb v2 0/13]`" can get added to beginning. You might find +wrapping your lines like this annoying and burdensome - but consider that when +working with others, they may not be reading the commit log in the same context +as you. I have a vertical monitor that I often read commit logs on, which is not +going to cram as much text into one line as your 4K 16:9 display could. + +[rfc]: https://tools.ietf.org/html/rfc2822#section-2.1.1 + +### Each commit should be a self-contained change + +Every commit should only contain one change - avoid sneaking in little unrelated +changes into the same commit[^1]. Additionally, avoid breaking one change into +several commits, unless you can refactor the idea into discrete steps - each of +which represents a complete change in its own right. If you have several changes +in your working tree and only need to commit some of them, try `git add -i` or +`git add -p`. Additionally, every commit should compile and run all tests +successfully, and should avoid having any known bugs which will be fixed up in a +future commit. + +[^1]: I could stand to take my own advice more often in this respect. + +If this is true of your repository, then you can check out any commit and expect +the code to work correctly. This also becomes useful later, for example when +cherry-picking commits into a release branch. Using this approach also allows +[git-bisect](https://git-scm.com/docs/git-bisect) to become more useful[^2], +because if you can expect the code to compile and complete tests successfully +for every commit, you can pass `git-bisect` a script which programmatically +tests a tree for the presence of a bug and avoid false positives. These +self-contained commits with good commit messages can also make it really easy to +prepare release notes with [git-shortlog][shortlog], [like +Linus does with Linux releases][linux-announcement]. + +[^2]: In a nutshell, git bisect is a tool which does a binary search between two commits in your history, checking out the commits in between one at a time to allow you to test for the presence of a bug. In this manner you can narrow down the commit which introduced a problem. +[shortlog]: https://git-scm.com/docs/git-shortlog +[linux-announcement]: https://lkml.org/lkml/2019/1/6/178 + +### Get it right on the first try + +We now come to one of the most important features of git which distinguishes it +from its predecessors: history editing. All version control systems come with a +time machine of some sort, but before git they were mostly read-only. However, +git's time machine is different: you can change the past. In fact, you're +encouraged to! But a word of warning: only change a future which has yet to be +merged into a stable public branch. + +The advice in this article - bug-free, self-contained commits with a good commit +message - is hard to get right on the first try. Editing your history, however, +is easy and part of an effective git workflow. Familiarize yourself with +[git-rebase](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) and use +it liberally. You can use rebase to reorder, combine, delete, edit, and split +commits. One workflow I find myself commonly using is to make some changes to a +file, commit a "fixup" commit (`git commit -m fixup`), then use `git rebase -i` +to squash it into an earlier commit. + +### Other miscellaneous tips + +- Read the man pages! Pick a random git man page and read it now. Also, if you + haven't read the top-level git man page (simply `man git`), do so. +- At the bottom of each man page for a high-level git command is usually a list + of low-level git commands that the high-level command relies on. If you want + to learn more about how a high-level git command works, try reading these man + pages, too. +- Learn how to specify the commit you want with [rev selection][rev-select] +- Branches are useful, but you should learn how to work without them as well to + have a nice set of tools in your belt. Use tools like `git pull --rebase`, + `git send-email -1 HEAD~2`, and `git push origin HEAD~2:master`. + +[rev-select]: https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection diff --git a/content/blog/Vendor-purpose-OS.md b/content/blog/Vendor-purpose-OS.md @@ -0,0 +1,47 @@ +--- +date: 2020-06-26 +layout: post +title: "General-purpose OS, special-purpose OS, and now: vendor-purpose OS" +tags: [rant, operating systems] +--- + +There have, historically, been two kinds of operating systems: general-purpose, +and special-purpose. These roles are defined by the function they serve for the +user. Examples of general-purpose operating systems include Unix (Linux, BSD, +etc), Solaris, Haiku, Plan 9, and so on. These are well-suited to general +computing tasks, and are optimized to solve the most problems possible, perhaps +at the expense of those in some niche domains. Special-purpose operating systems +serve those niche domains, and are less suitable for general computing. Examples +of these include FreeRTOS, Rockbox, Genode, and so on. + +These terms distinguish operating systems by the problems they solve for the +user. However, a disturbing trend is emerging in which the user is not the party +whose problems are being solved, and perhaps this calls for a new term. I +propose "vendor-purpose operating system". + +I would use this term to describe Windows, macOS, Android, and iOS, and perhaps +some others besides. Arguably, the first two used to be general purpose +operating systems, and the latter two were once special-purpose operating +systems. Increasingly, these operating systems are making design decisions +which benefit the vendor *at the expense* of the user. For example: Windows has +ads and excessive spyware, prevents you from making a local login without a +Microsoft account, and aggressively pushes you to switch to Edge from other web +browsers, as well as many other examples besides. + +Apple is more subtle from the end-user's perspective. They eschew standards to +build walled gardens, opting for Metal rather than Vulkan, for example. They use +cryptographic signatures to enforce a racket against developers who just want to +ship their programs. They bully vendors in the app store into adding things like +microtransactions to increase their revenue. They've also long been making +similar moves in their hardware design, adding anti-features which are +explicitly designed to increase their profit &mdash; adding false costs which +are ultimately passed onto the consumer. + +All of these decisions are making the OS worse for users in order to provide +more value to the vendor. The operating system is becoming *less* suited to its +general-purpose tasks, as the vendor-purpose anti-features deliberately get in +the way. They also become less suited at special-purpose tasks for the same +reasons. These changes *are* making improvements for one purpose: the vendor's +purpose. Therefore, I am going to start refering to these operating systems as +"vendor purpose", generally alongside a curse and a raising of the middle +finger. diff --git a/content/blog/Wayland-misconceptions-debunked.md b/content/blog/Wayland-misconceptions-debunked.md @@ -0,0 +1,237 @@ +--- +date: 2019-02-10 +layout: post +title: Wayland misconceptions debunked +tags: ["wayland"] +--- + +This article has been on my backburner for a while, but it seems Wayland FUD is +making the news again recently, so I've bumped up the priority a bit. For those +new to my blog, I am the maintainer of +[wlroots](https://github.com/swaywm/wlroots), a library which implements much of +the functionality required of a Wayland compositor and is arguably the single +most influential project in Wayland right now; and [sway](https://swaywm.org), a +popular Wayland compositor which is nearing version 1.0. Let's go over some of +the common misconceptions I hear about Wayland and why they're wrong. Feel free +to pick and choose the misconceptions you believe to read and disregard the +rest. + +The art of hating Wayland has become a cult affair. We don't need to put +ourselves into camps at war. Please try not to read this article through the +lens of anger. + +### Wayland isn't more secure, look at this keylogger! + +There is an [unfortunate GitHub +project](https://github.com/Aishou/wayland-keylogger) called "Wayland keylogger" +whose mode of operation is using `LD_PRELOAD` to intercept calls to the +libwayland shared library and record keypresses from it. The problem with this +"critique" is stated in the `README.md` file, though most don't read past the +title of the repository. Wayland is only *one part* of an otherwise secure +system. Using `LD_PRELOAD` is effectively equivalent to rewriting client +programs to log keys themselves, and any program which is in a position to do +this has already won. If I rephrased this as "Wayland can be keylogged, assuming +the attacker can sneak some evil code into your .bashrc", the obviousness of +this truth should become immediately apparent. + +Some people have also told me that they can log keys by opening `/dev/input/*` +files and reading input events. They're right! Try it yourself: `sudo libinput +debug-events`. The catch should also be immediately obvious: ask +yourself why this needs to be run with `sudo`. + +### Wayland doesn't support screenshots/capture! + +The [core Wayland protocol][wayland.xml] does not define a mechanism for taking +screenshots. Here's another thing it doesn't define: how to open application +windows, like gedit and Firefox. The Wayland protocol is very conservative and +general purpose, and is built with use-cases other than desktop systems in mind. +To this end it only implements the lowest common denominator, and leaves the +rest to protocol extensions. There is a process for defining, implementing, +maturing, and standardizing these extensions, though the last part is in need of +improvements - which are under discussion. + +[wayland.xml]: https://github.com/wayland-project/wayland/blob/master/protocol/wayland.xml + +There are two protocols for the purpose of screenshots and screen recording, +which are developed by wlroots and supported by a strong majority of Wayland +compositors: [screencopy][screencopy.xml] and +[dmabuf-export][dmabuf-export.xml], respectively for copying pixels (best for +screenshots) and exporting DMA buffers (best for real-time video capture). + +[screencopy.xml]: https://github.com/swaywm/wlroots/blob/master/protocol/wlr-screencopy-unstable-v1.xml +[dmabuf-export.xml]: https://github.com/swaywm/wlroots/blob/master/protocol/wlr-export-dmabuf-unstable-v1.xml + +There are two approaches to this endorsed by different camps in Wayland: these +Wayland protocols, and a dbus protocol based on Pipewire. Progress is being made +on making these approaches talk to each other via [xdg-desktop-portal][portal], +which will make just about every client and compositor work together. + +[portal]: https://github.com/emersion/xdg-desktop-portal-wlr + +### Wayland doesn't have a secondary clipboard! + +Secondary clipboard support (aka primary selection) was first implemented as +[gtk-primary-selection][gtk.xml] and was recently standardized as +[wp-primary-selection][wp.xml]. It is supported by nearly all Wayland +compositors and clients. + +[gtk.xml]: https://github.com/swaywm/wlroots/blob/master/protocol/gtk-primary-selection.xml +[wp.xml]: https://github.com/wayland-project/wayland-protocols/blob/master/unstable/primary-selection/primary-selection-unstable-v1.xml + +### Wayland doesn't support clipboard managers! + +See [wl-clipboard](https://github.com/bugaevc/wl-clipboard) + +### Wayland isn't suitable for embedded devices! + +Some people argue that Wayland isn't supported on embedded devices or require +proprietary blobs to work. This is *very* untrue. Firstly, Wayland is a +protocol: the *implementations* are the ones that need support from drivers, and +a Wayland implementation could be written for basically any driver. You could +implement Wayland by writing Wayland protocol messages on pieces of paper, +passing them to your friend in class, and having them draw your window on their +notebook with a pencil. + +That being said, this is also untrue of the implementations. wlroots, which +contains the most popular Wayland rendering backend, implements KMS+DRM+GBM, +which is supported by all open source graphics drivers, and uses GLESv2, which +is the most broadly supported graphics implementation, including on embedded +(which is what the "E" stands for) and most older hardware. For ancient +hardware, writing an fbdev backend is totally possible and I'd merge it in +wlroots if someone put in the time. Writing a more modern KMS+DRM+GBM +implementation for that hardware is equally possible. + +### Wayland doesn't have network transparency! + +This is actually true! But it's not as bad as it's made out to be. Here's why: +X11 forwarding works on Wayland. + +Wait, what? Yep: all mainstream desktop Wayland compositors have support for +**Xwayland**, which is an implementation of the X11 server which translates X11 +to Wayland, for backwards compatibility. X11 forwarding works with it! So if you +use X11 forwarding on Xorg today, your workflow will work on Wayland unchanged. + +However, Wayland itself is not network transparent. The reason for this is that +some protocols rely on file descriptors for transferring information quickly or +in bulk. One example is GPU buffers, so that the Wayland compositor can render +clients without copying data on the GPU - which improves performance +dramatically. However, little about Wayland is inherently network *opaque*. +Things like sending pixel buffers to the compositor are already abstracted on +Wayland and a network-backed implementation could be easily made. The problem is +that no one seems to really care: all of the people who want network +transparency drank the anti-Wayland kool-aid instead of showing up to put the +work in. If you want to implement this, though, we're here and ready to support +you! Drop by the wlroots [IRC channel][#sway-devel] and we're prepared to help +you implement this. + +### Wayland doesn't support remote desktop! + +This one is also true, but work is ongoing. Several of the pieces are in place: +screen capture and keyboard simulation are there. If an interested developer +wants to add pointer device simulation and tie it all together with librdesktop, +that would be a great boon to the Wayland ecosystem. [We're waiting to +help!][#sway-devel] + +[#sway-devel]: https://webchat.freenode.net/?channels=sway-devel + +### Wayland requires client side decorations! + +This was actually true for a long time, but there was deep contention in the +Wayland ecosystem over this matter. We fought long and hard over this and we now +have a protocol for negotiating client- vs server-side decorations, which is now +fairly broadly supported, including among some of its opponents. You're welcome. + +### Wayland doesn't support hotkey daemons! + +This is a feature, not a bug, but you're free to disagree once you hear the +rationale. There are lots of problems with the idea of hotkey daemons as it +exists on X. What if there's a conflict between several clients who want the +same hotkey? What if the user wants to pick a different hotkey? On top of this, +designing a protocol carefully to avoid keylogging concerns makes it more +difficult still. + +To this end, I've been encouraging client developers who want hotkeys to instead +use some kind of IPC mechanism and a control binary. For example, `mako`, a +notification daemon, allows you to dismiss notifications by running the `makoctl +dismiss` command. Users are then encouraged to use the compositor's own +keybinding facilities to execute this command. This is more flexible even +outside of keybinding - the user might want to execute this behavior through a +script, too. + +Still, if you *really* want hotkeys, you should start the discussion for +standardizing a protocol. It will be an uphill battle but I believe that a +protocol which addresses everyone's concerns is theoretically possible. *You* +have to step up, though: no one working on Wayland today seems to care. We are +mostly volunteers working for free in our spare time. + +### Wayland doesn't support Nvidia! + +Actually, Nvidia doesn't support us. There are three standard APIs which are +implemented by all graphics drivers in the Linux kernel: DRM (display resource +management), KMS (kernel mode setting), and GBM (generic buffer management). All +three are necessary for most Wayland compositors. Only the first two are +implemented by the Nvidia proprietary driver. In order to support Nvidia, +Wayland compositors need to add code resembling this: + +```c +if (nvidia proprietary driver) { + /* several thousand lines of code */ +} else { + /* several thousand lines of code */ +} +``` + +That's terrible! On top of that, we cannot debug the proprietary driver, we +cannot send fixes upstream, and we cannot read the code to understand its +behavior. The mesa code (where much of the important code for many drivers +lives) is a frequent object of study among Wayland compositor developers. We +cannot do this with the proprietary drivers, and it doesn't even implement the +APIs it needs to. They claim to be working on a replacement for GBM which they +hope will satisfy everyone's concerns, but 52 commits in 3 years with over a +year of inactivity isn't a great sign. + +To boot, Nvidia is a bad actor on Linux. Compare the talks at FOSDEM 2018 +from the [nouveau developers][nouveau] (the open source Nvidia driver) and the +[AMDGPU developers][amdgpu] (the *only*[^1] AMD driver - also open source). The +Nouveau developers discuss all of the ways that Nvidia makes their lives +difficult, up to and including *signed firmwares*. AMDGPU instead talks about +the process of upstreaming their driver, discuss their new open source Vulkan +driver, and how the community can contribute - and this was presented by paid +AMD staff. I met Intel employees at XDC who were working on a continuous +integration system wherein Intel offers a massive Intel GPU farm to Mesa +developers free-of-charge for working on the open source driver. Nvidia is +clearly a force for bad on the Linux scene and for open source in general, and +the users who reward this by spending oodles of cash on their graphics cards are +not exactly in my good graces. + +[^1]: *actively maintained + +So in short, people asking for Nvidia proprietary driver support are asking the +wrong people to spend hundreds of hours working for free to write and maintain +an implementation for *one* driver which represents a harmful force on the Linux +ecosystem and a headache for developers trying to work with it. With respect, my +answer is no. + +[nouveau]: https://archive.fosdem.org/2018/schedule/event/nouveau/ +[amdgpu]: https://archive.fosdem.org/2018/schedule/event/amd_graphics/ + +### Wayland doesn't support gaming! + +First-person shooters, among other kinds of games, require "locking" the pointer +to their window. This requires [a protocol][pointer-constraints.xml], which was +standardized in 2015. Adoption has been slower, but it landed in wlroots several +months ago and support was added to sway a few weeks ago. + +[pointer-constraints.xml]: https://github.com/wayland-project/wayland-protocols/blob/master/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml + +### In conclusion + +At some point, some of these things have been true. Some have never been true. +It takes time to replace a 30-year incumbent. To be fair, some of these points +are true on GNOME and KDE, but none are inherently problems with the Wayland +protocol. wlroots is a dominating force in the Wayland ecosystem and the tide is +clearly moving our way. + +Another thing I want to note is that Xorg still works. If you find your needs +aren't met by Wayland, just keep using X! We won't be offended. I'm not trying +to force you to use it. Why you heff to be mad? diff --git a/content/blog/Wayland-shells.md b/content/blog/Wayland-shells.md @@ -0,0 +1,242 @@ +--- +date: 2018-07-29 +layout: post +title: "Writing a Wayland compositor with wlroots: shells" +tags: [wlroots, wayland, instructional] +--- + +I apologise for not writing about wlroots more frequently. I don't really enjoy +working on the McWayface codebase this series of blog posts was originally +about, so we're just going to dismiss that and talk about the various pieces of +a Wayland compositor in a more free-form style. I hope you still find it useful! + +Today, we're going to talk about shells. But to make sure we're on the same page +first, a quick refresher on surfaces. A basic primitive of the Wayland protocol +is the concept of a "surface". A surface is a rectangular box of pixels sent +from the client to the compositor to display on-screen. A surface can source +its pixels from a number of places, including raw pixel data in memory, or +opaque handles to GPU resources that can be rendered without copying pixels on +the CPU. These surfaces can also evolve over time, using "damage" to indicate +which parts have changed to reduce the workload of the compositor when +re-rendering them. However, making a surface and filling it with pixels is not +enough to get the compositor to show them. + +Shells are how surfaces in Wayland are given meaning. Consider that there are +several kinds of surfaces you'll encounter on your desktop. There are +application windows, sure, but there are also tooltips, right-click menus and +menubars, desktop panels, wallpapers, lock screens, on-screen keyboards, and so +on. Each of these has different semantics - your wallpaper cannot be minimized +or dragged around and resized, but your application windows can be. Likewise, +your application windows cannot cover the entire screen and soak up all input +like your lock screen can. Each of these use cases is fulfilled with a *shell*, +which generally takes a surface resource, assigns it a role (e.g. application +window), and returns a handle with shell-specific interfaces for manipulating +it. + +## Shells in wlroots + +I want to first discuss features common to shells as implemented by wlroots. +Each shell has a shell-specific interface that sits on top of the surface. Each +time a client connects and creates one of these, the shell raises a `wl_signal`, +`events.new_surface`, and passes to it a pointer to a shell-specific structure +which encapsulates that shell surface's state. + +Many shells require some configuration between the creation of the shell surface +and displaying it on screen. For example, during this period application windows +will typically set the window title so that the compositor never has to show an +empty title. All Wayland interfaces aim for atomicity, so that all changes are +applied in a single fell swoop and we never display an invalid frame. This is +why Wayland is known for addressing vsync problems X suffers from, but is +pervasive across the ecosystem. Even things like setting the window title are +done atomically. + +So, once the client is done communiciating the new shell surface's desired +traits to the compositor, it will commit the surface to atomically apply the +changes. The first time this happens, the client is ready to be shown, and the +shell-specific wlroots shell surface interface will communicate this to you with +the surface's `events.map` signal. The reverse is sometimes communicated with +`events.unmap`, when the shell surface should be hidden. + +## xdg-shell + +xdg-shell is currently the only shell whose protocol is considered stable, and +it is the shell which describes application windows. You can read the xdg-shell +protocol specification (XML) +[here](https://cgit.freedesktop.org/wayland/wayland-protocols/tree/stable/xdg-shell/xdg-shell.xml) +(you are strongly encouraged to read through the XML for all protocols mentioned +in this article). + +The xdg-shell is quite complicated, as it attempts to encapsulate every feature +of a typical graphical desktop session in a single protocol. An xdg-shell +surface is a `wl_surface` wrapped twice - once in a `xdg_surface` and then again +in a `xdg_toplevel` or `xdg_popup`, depending on what kind of window it is. The +wlroots `wlr_xdg_surface` type (the one emitted by +`xdg_shell.events.new_surface`) contains tagged union of `wlr_xdg_toplevel` and +`wlr_xdg_popup`, selected from the `role` field. You can wire up the xdg-shell +with `wlr_xdg_shell_create`. + +Most application windows you see are called toplevels. These windows are the +root node of a tree of surfaces which may include arbitrarily nested popups, for +example, as you navigate through a deep menu. These windows can have titles; +parent surfaces; app IDs (e.g. "gnome-calculator"); minimum and maximum sizes; +and maximized, minimized, and fullscreen states. They also often[^1] draw their +own window decorations and drop shadows, and tell the compositor when you click +and drag on the titlebar to move or resize the window. Unfortunately, if the +client is not responding or misbehaving, the user cannot use these controls to +move, resize, or minimize the window[^2]. + +[^1]: [But not always](https://cgit.freedesktop.org/wayland/wayland-protocols/tree/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml). You're welcome. +[^2]: Which is one of the reasons we made the protocol mentioned in footnote[^1]. + +The compositor can tell the window to adopt a specific size, though the client +can choose to ignore this. The compositor also lets the client know when it's +"activated", which is used by GTK+, for example, to start rendering the caret +and render a different set of client-side decorations. It can also toggle the +fullscreen, minimized, maximized, and other states. + +Each of the various state transitions involved are expressed through +the `wlr_xdg_toplevel.events` signals. The most recent atomically agreed-upon +state is stored in `wlr_xdg_toplevel.current`. When each of the signals in +`events` are emitted, the state change will have been applied to +`client_pending`. However, you must consent to these changes by calling a +corresponding function on the xdg_toplevel (e.g. +`wlr_xdg_toplevel_set_fullscreen`), which will apply the change to +`server_pending`. You shouldn't consider these changes atomically set until the +`wlr_surface.events.commit` signal has been raised. At that point, you can start +showing the window in fullscreen or whatever. There's also some +configure/ack-configure stuff going on here which may eventually become relevant +to you[^3], but wlroots takes care it for the most part. + +[^3]: For example, this is relevant for sway, which needs to reach deeper into our shell implementations to atomically syncronize the resizing of several clients at once when rearranging the layout. + +The popup interface is used to show a "popup" window, which can be used for a +variety of purposes. These include context menus (or "right click" menus), +tooltips, some confirmation modals[^4], etc. The lifecycle of a popup resource +is managed similarly to that of a toplevel resource, of course with different +states that can be atomically updated. Arguably, the most fundamental of these +states is the relative X and Y position of the popup with respect to its parent +toplevel surface. + +[^4]: Some popup windows, the GTK+ file chooser for example, prefer to make a new xdg_toplevel and assign its parent to the application window. This is useful if you want your window to show up in taskbars, be able to be minimized and maximized separately, etc. + +The position of the popup can be influenced by an extraordinarily complicated +interface called `xdg_positioner`, also provided by xdg-shell. Since these +articles focus on the compositor side of things, and they focus on using +wlroots, I can thankfully save you from understanding most of the specifics of +this interface. The purpose of this interface is to adjust the position and size +of `xdg_popup` surfaces with respect to the display they live on - for example, +to prevent them from being partially off-screen. The rub is that if you're using +wlroots, when the popup is created you can just call +`wlr_xdg_popup_unconstrain_from_box` to deal with everything, passing it a box +which represents the available space surrounding the parent toplevel for the +popup to be placed in. + +Popups are also able to take "grabs", which indicate that they should keep +focus without respect to any of the other goings-on of the seat. This is used so +that you can, for example, use the keyboard to pick items from a context menu. +Grabs are automatically handled for you with `wlr_seat` for you. If you want to +deny or cancel grabs, you can do so through the appropriate `wlr_seat` +interfaces. + +One last note: xdg-shell only recently became stable, so client support for the +stable version is hit and miss. The last unstable protocol, xdg-shell v6, is +also supported by wlroots. It mostly behaves in the same way. Eventually it +will be removed from wlroots. + +## layer-shell + +Under the umbrella of wlroots, 8 Wayland compositors have been collaborating on +the design of a new shell for desktop shell components. The result is [layer +shell +(XML)](https://github.com/swaywm/wlr-protocols/blob/master/unstable/wlr-layer-shell-unstable-v1.xml). +The purpose of this shell is to provide an interface for desktop components like +panels, lock screens, wallpapers, on-screen keyboards, notifications, and so on, +to display on your compositor. + +The layer-shell is organized into four discrete layers: background, bottom, top, +and overlay, which are rendered in that order. Between bottom and top, +application windows are displayed. A wallpaper client might choose to go in the +bottom layer, while a notification could show on the top layer, and a panel on +the bottom layer. + +The compositor's job is to decide where to place each surface and how large the +surface can be. The client can specify either or both of its dimensions (width +and height) for the compositor to specify, then provide some hints for the +compositor to do so. The client can, for example, choose to be anchored to edges +of the screen. A notification might be anchored to `TOP | RIGHT`, and a panel +might be anchored to `LEFT | BOTTOM | RIGHT`. A layer surface anchored to an +edge, like our panel, can also request an exclusive zone, which is a number of +pixels from the edge that should not be occluded by other layer surfaces or +application windows. This is used, for example, when maximizing application +windows to prevent them from occluding the panel (or in sway's case, when +arranging tiled windows). + +Layer surfaces also have special keyboard input semantics. Some layer surfaces +want to receive keyboard input, such as an application launcher overlay. Others +might prefer that application windows continue to receive keyboard events, such +as a notification. To this end, a layer surface can toggle a boolean indicating +its "keyboard interactivity". For layers beneath application windows, layer +surfaces participate in keyboard focus normally, usually meaning they need to be +clicked to receive keyboard focus. Above application windows, the top-most layer +always has keyboard focus if it requests it. + +In wlroots, you can wire up a layer shell to the display with +`wlr_layer_shell_create`. From there it behaves similarly to xdg-shell with +respect to the creation of new surfaces and the handling of atomic state. The +main concern of yours is that, when the surface is committed, you need to +arrange the surfaces in the affected layer and communicate the final dimensions +of the layer surface to the client with `wlr_layer_surface_configure`. You can +implement the arrangement however you want, but you may find the [sway +implementation][sway-layers] to be a useful reference. Also check out the +wlroots [example client][layer-client] to test out your implementation. + +[sway-layers]: https://github.com/swaywm/sway/blob/master/sway/desktop/layer_shell.c#L18-L215 +[layer-client]: https://github.com/swaywm/wlroots/blob/master/examples/layer-shell.c + +Layer surfaces can also have popups, for example when right-clicking on a +taskbar. This borrows xdg-shell's xdg_popup interface, except the parent is set +to the layer surface (this is explicitly allowed for through the xdg_popup spec, +and you may see future shells doing something similar). Most of your code for +xdg_popups can be reused with layer surfaces. + +## Xwayland + +Some Wayland developers turn up their nose when I refer to Xwayland as a shell, +and perhaps with good reason. However, wlroots treats Xwayland like a shell, so +the API remains consistent. For that reason, we'll treat it as one in this +article as well. + +We figured that you might be writing a Wayland compositor so that you *don't* +have to write an X11 window manager, too. So we wrote one for you, and it's +called `wlr_xwayland`. This interface provides an abstraction over Xwayland +which makes it behave similarly to our other shells. It still lets you dig your +heels into it in any degree so that you can adjust the behavior of your +compositor to suit X-specific needs as necessary. + +The resulting wlr_xwayland API is similar to the other shells we've described. +We have a series of events for configuring Xwayland surfaces, a map and unmap +event, and we expose a whole bunch of info about Xwayland surfaces so you can +make the judgement call about how much or how little to obey their requests (X11 +windows make more unreasonable requests than other shells, since X11 was the +wild wild west and a lot of clients took advantage of that). + +This should be enough to get you started, and if you have questions ask on IRC +for the time being. I could go into more detail, but I think Xwayland deserves +its own article, and probably not written by me. + +## Other shells + +There are three other shells of note. Two are not very interesting: + +- wl_shell, the now-deprecated original desktop shell of Wayland +- ivi-shell, used for "in-vehicle infotainment" systems running Wayland + +wlroots supports neither (though I guess we'd accept a patch adding IVI-shell +support, maybe if the vehicle industry was open to improving that protocol...), +and neither is interesting for desktops, phones, etc. You probably don't need to +worry about them. + +The other is the fullscreen-shell, which is used for optimizing the rendering of +fullscreen appliations. I don't know much about how it works, and it's not +supported by wlroots yet; it's not required of a functional Wayland compositor. +Maybe someday! diff --git a/content/blog/We-are-complicit-in-our-employers-deeds.md b/content/blog/We-are-complicit-in-our-employers-deeds.md @@ -0,0 +1,67 @@ +--- +date: 2020-05-05 +title: We are complicit in our employer's deeds +layout: post +tags: [culture] +--- + +Tim Bray's excellent "[Bye Amazon][bye amazon]" post inspired me to take this +article off of my backlog, where it has been sitting for a few weeks. I applaud +Tim for stepping down from a company that has demonstrated itself incompatible +with his sense of right and wrong, and I want to take a moment to remind you +that the rest of us in the tech industry have the same opportunity &mdash; no, +the same *obligation* as Tim did. + +[bye amazon]: https://www.tbray.org/ongoing/When/202x/2020/04/29/Leaving-Amazon + +As software engineers, we enjoy high salaries and extremely good job security. A +good software engineer with only a couple of years of experience under their +belt can expect to have an offer within 1 or 2 months of starting their search. +It can seem a little scary and stressful, but if you're a programmer already +working at $company and you're looking for a change, you're better off than 99% +of your non-technical friends. In tech, hardly anyone is "trapped" at a bad job; +or at least we don't have a good excuse for not trying for something better. + +Tim calls out Amazon's terrible, unhealthy working conditions and retaliation +against staff who speak up or try to organize.[^1] Google conducts mass +surveillance, kowtows to oppressive regimes, and punishes workers who stand up +to them. Less obvious stuff, too &mdash; Apple builds walled gardens and makes +targeted attacks on open standards, Facebook is a giant surveillance tool which +routinely disregards the law, the same behavior which made Uber and Airbnb into +the giants they are today, all while fostering a "gig" culture in which the poor +have no stability or security. Mass surveillance, contempt of the law, tax +evasion, oppression of the poor, of minorities... this is what our industry is +known for, and it's *our* fault. + +[^1]: [Here's a link](https://www.amazon.com/mc/pipelines/cancellation) to cancel Amazon Prime, by the way. + +This is why I hold my peers accountable for working at companies which are +making a negative impact on the world around them. As a general rule, it costs a +business your salary &times; 1.5 to employ you, given the overhead of benefits, +HR, training, and so on. When you're making a cool half-million annual salary +from $bigcorp, it's because they expect to make at least ¾ of a million that +they wouldn't be making without you. It does not make economic sense for them to +hire you if this weren't the case. Your contribution makes a big difference. + +If the best defense we have for working at these companies is the [Nuremberg +defense][nuremberg], that doesn't reflect well on us. But, maybe you would +object, maybe you would have the courage to say "no" when asked to do these +things. Maybe you would, but someday, a cool project will come across your +inbox - machine learning! Big data! Cloud scale! It's everything you were +promised when you took the job, and have more fun with it for a few months than +you have had in a long time. Your superiors are thrilled - "it's perfect!", they +say, and it's not until they take it and start feeding it real-world data that +[you realize exactly what you have built][machine-learning]. Doublethink quickly +steps in to protect your ego from the cognitive dissonance, and you take another +little step towards becoming the person you once swore never to be. + +[nuremberg]: https://en.wikipedia.org/wiki/Superior_orders +[machine-learning]: https://observer.com/2020/04/amazon-whole-foods-anti-union-technology-heat-map/ + +The rapid computerization of society has decreased the time necessary to build +novel machines one thousand-fold. This endows us with a great responsibility, +because whatever we build with them, the changes they bring to society will be +upon us much, much faster than any changes to come before. Every software +developer possesses alone the potential of 50 engineers living just 100 years +ago. We can apply this power for good or for ill, but it's up to each of us to +make a deliberate choice on the matter. diff --git a/content/blog/Web-browsers-need-to-stop.md b/content/blog/Web-browsers-need-to-stop.md @@ -0,0 +1,43 @@ +--- +date: 2020-08-13 +title: Web browsers need to stop +layout: post +--- + +Enough is enough. + +The web and web browsers have become Lovecraftian horrors of an unprecedented +scale. They've long since left "scope creep" territory and entered "oh my god +please just stop" territory, and are trucking on through to hitherto unexplored +degrees of *obscene* scope. And we *don't want* what they're selling. Google +pitches garbage like AMP[^1] and pushing dubious half-assed specs like Web +Components. Mozilla just fired everyone relevant[^2] to focus on crap no one +asked for like Pocket, and fad nonsense like a paid VPN service and ~~virtual +reality tech~~.[^3] *[2020-08-14: It has been pointed out that the VR team was +also fired.]* + +[^1]: *No one* wants AMP. Google knows it, you know it, I know it. If you're a Google engineer who is still working on AMP, you are a disgrace to your field. [Take responsibility for the code you write](https://drewdevault.com/2020/05/05/We-are-complicit-in-our-employers-deeds.html). This project needs to be dead and buried and the earth above salted, and it needs to happen yesterday. +[^2]: ~~No layoffs or pay cuts at the management level, of course!~~ It's not like they're *responsible* for these problems, it's not like anyone's fucking *responsible* for any of this, it's not like the very idea of *personal responsibility* has been forgotten by both executives and engineers, no sir! *[2020-08-14: It has been pointed out that some VPs were laid off. I also wish to clarify that the personal responsibility I find absent at the engineering level is more of a commentary on Google than Mozilla.]* +[^3]: Oh good, the *web* is exactly what VR needs! It's definitely *not* a huge time-sink requiring the highly skilled low-level engineering talent which Mozilla just finished *laying off*, or years of effort and millions of dollars just to realize that the new state of the art is still just an expensive and underwhelming product whose few end-user applications make half of their users motion sick. + +Microsoft gave up entirely. Mozilla just hammered the last few nails into their +casket.[^4] ~~Safari is a joke~~[^6]. Google is all that's left, and they're +*not* a good steward of the open web. The browsers are drowning under their own +scope. The web is dead. + +[^4]: Next time they should aim for their executive's heads, maybe they'll jostle them around enough to get the two wires in each of their heads to make contact so that they're briefly capable of making basic decisions and not just collecting multi-million-dollar paychecks. + +I call for an immediate and indefinite suspension of the addition of new +developer-facing APIs to web browsers. Browser vendors need to start thinking +about *reducing* scope and *cutting* features. WebUSB, WebBluetooth, WebXR, +~~WebDRM~~ ~~WebMPAA~~ ~~WebBootlicking~~ ~~replacing User-Agent with +Vendor-Agent cause let's be honest with ourselves at this point~~ "Encrypted +Media Extensions" &mdash; this crap all needs to go. At some point you need to +stop adding scope and start focusing on performance, efficiency, reliability, +and security[^5] at the scope you already have. + +[^5]: The web *might* be one for four on these right now. + +Enough is enough. + +[^6]: 2020-08-14: I haven't used Safari in over 10 years, so maybe it's not so bad. However, so long as it's single-platform and closed source, it's still a net negative on the ecosystem. diff --git a/content/blog/What-is-a-fork.md b/content/blog/What-is-a-fork.md @@ -0,0 +1,128 @@ +--- +date: 2019-05-24 +layout: post +title: "What is a fork, really, and how GitHub changed its meaning" +tags: ["philosophy", "sourcehut", "free software"] +--- + +The fork button on GitHub - with the little number next to it for depositing +dopamine into your brain - is a bit misleading. GitHub co-opted the meaning of +"fork" to trick you into participating in their platform more. They did this in +a well-intentioned way, for the sake of their pull requests feature, but +ultimately this design is self-serving and causes some friction when +contributors venture out of their GitHub sandbox and into the rest of the +software development ecosystem. Let's clarify what "fork" really means, and what +we do without GitHub's concept of one - for it is in this difference that we +truly discover how git is a *distributed* version control system. + +**Disclaimer**: I am the founder of [SourceHut](https://sourcehut.org), a +product which competes with GitHub and embraces the "bazaar[^1]" model described +in this article. + +[^1]: Not the bazaar version control system, but bazaar the concept. This is explained later in the article. + +On GitHub, a fork refers to a copy of a repository used by a contributor[^2] to +stage changes they'd like to propose upstream. Prior to GitHub (and in many +places still today), we'd call such a repository a "personal branch". A personal +branch doesn't need to be published to be useful - you can just `git clone` it +locally and make your changes there without pushing them to a public, hosted +repository. Using [email](https://git-send-email.io), you can send changes from +your local, unpublished repository for consideration upstream. Outside of +GitHub and its imitators, most contributors to a project don't have a published +version of their repository online at all, skipping that step and saving some +time. + +[^2]: And by bots to increase their reputation, and by confused users who don't know what the button means. + +In some cases, however, it's useful to publish your personal branch online. This +is often done when a team of people is working on a long-lived branch to later +propose upstream - for example, I've been doing this while working on the RISC-V +port of musl libc. It gives us a space to collaborate and work while preparing +changes which will eventually be proposed upstream, as well as a place for +interested testers to obtain our experimental work to try themselves. This is +also done by individuals, such as Greg Kroah-Hartman's Linux branches, which are +useful for testing upcoming changes to the Linux kernel. + +Greg is not alone in publishing a repo like this. In fact, there are [hundreds of +kernel trees like this][kernel-git]. These act as staging areas for long-term +workstreams, or for the maintainers of many subsystems of the kernel. Changes +in these repositories gradually flow upwards towards the "main" tree, +[torvalds/linux][torvalds/linux]. The precise meaning of "linux" is rather loose +in this context. An argument could be made that torvalds/linux is Linux, but +that definition wouldn't capture the LTS branches. Many distros also apply their +own patches on top of Torvalds, perhaps sourcing them from the maintainers of +drivers they need a bugfix for, or they maintain their own independent trees +which periodically pull in lump sums of changes from other trees - meaning that +the simple definition might not include the version of Linux which is installed +on your computer, either. This ambiguity is a feature - each of these trees is a +valid definition of Linux in its own right. + +[kernel-git]: https://git.kernel.org/pub/scm/linux/kernel/git/ +[torvalds/linux]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ + +This is the sense in which git is "distributed". The idea of a canonical +upstream is not written in stone in the way that GitHub suggests it might be. +After all, open-source software is a collaborative endeavour. What makes Jim's +branch more important that John's branch? John's branch is definitely more +important if it has the bugfixes you need. In fact, your branch, based on Jim's, +with some patches cherry-picked from John, and a couple of fixes of your own +mixed in, may in fact be the best version of the software for you. + +This is how the git community gets along without the GitHub model of "forks". +This design has allowed the largest and most important projects in the world to +flourish, and git was explicitly designed around this model. We refer to this as +the "bazaar" model, the metaphor hopefully being fairly obvious at this point. +There is another model, which GitHub embodies instead: the cathedral. In this +model, the project has a central home and centralized governance, run by a small +number of people. The cathedral doesn't necessarily depend on the GitHub idea of +"forks" and pull requests - that is, you can construct a cathedral with +email-driven development or some other model - but on GitHub the bazaar option +is basically absent. + +In the introduction I said that GitHub attempts to replace an existing meaning +for "fork". So what does forking actually mean, then? Consider a project with +the cathedral model. What happens when there's a schism in the church? The +answer is that some of the contributors can take the code, put up a new branch +somewhere, and stake a flag in the ground. They rename it and commit to +maintaining it entirely independently of the original project, and encourage +contributors, new and old alike, to abandon the old dogma in favor of theirs. +At this point, the history[^3] begins to diverge. The new contingent pulls in +all of the patches that were denied upstream and start that big refactoring to +mold it in their vision. The project has been **forked**. A well known example +is when ffmpeg was forked to create libav. + +[^3]: Git history in particular, but also the other kind. + +This is usually a traumatic event for the project, and can have repercussions +that last for years. The precise considerations that should go into forking a +project, these repercussions and how to address them, and other musings are +better suited for a separate article. But this is what "fork" meant before +GitHub, and this meaning is still used today - albeit more ambiguously. + +If "fork" already had this meaning, why did GitHub adopt their model? The +answer, as it often will be, is centralization of power. GitHub is a +proprietary, commercial service, and their ultimate goal is to turn a profit. +The design of GitHub's fork and pull request model creates a cathedral that +keeps people on their platform in a way that a bazaar would not. A distributed +version control system like git, built on a distributed communications protocol +like email, is hard to disrupt with a centralized service. So GitHub designed +their own model. + +As a parting note, I would like to clarify that this isn't a condemnation of +GitHub. I still use their service for a few projects, and appreciate the +important role GitHub has played in the popularization of open source. However, +I think it's important to examine the services we depend on, to strive to +understand their motivations and design. I also hope the reader will view the +software ecosystem through a more interesting lens for having read this article. +Thank you for reading! + +--- + +**P.S.** Did you know that GitHub also captured the meaning of "pull request" +from git's own [request-pull](https://www.git-scm.com/docs/git-request-pull) +tool? git request-pull prepares an email which will ask the recipient to fetch +changes from a public repository and integrate them into their own branch. This +is used when a patch is insufficient - for example, when Linux subsystem +maintainers want to ship a large group of changes to Torvalds for the next +kernel release. Again, the original version is distributed and bazaar-like, +whereas GitHub's is centralized and makes you stay on their platform. diff --git a/content/blog/What-motivates-the-authors-of-the-software-you-use.md b/content/blog/What-motivates-the-authors-of-the-software-you-use.md @@ -0,0 +1,55 @@ +--- +date: 2016-09-09 +# vim: set tw=80 +title: What motivates the authors of the software you use? +layout: post +tags: [philosophy, privacy, free software] +--- + +We face an important choice in our lives as technophiles, hackers, geeks: the +choice between proprietary software and free/open source software. What +platforms we choose to use are important. We have a choice between Windows, OS +X, and Linux (not to mention the several less popular choices). We choose +between Android or iOS. We choose hardware that requires nonfree drivers or ones +that don't. We choose to store our data in someone else's cloud or in our own. +How do we make the right choice? + +I think it's important to consider the basic motivations behind the software you +choose to use. Why did the author write it? What are their goals? How might that +influence the future (or present) direction of this software? + +In the case of most proprietary software, the motivations are to make money. +They make decisions that benefit the company rather than the user. If you're +paying for the software, they might use vendor lock-in strategies to prevent you +from having ownership of your data. If you don't pay for the software, they +might place ads on it, sell your personal information, etc. When Cloud Storage +Incorporated is sold to Somewhat Less Trustworthy Business, who's to say that +your data is in good hands? + +In the case of most open source[^1] software, however, things are different. +The decisions the developers make are generally working in the interests of the +user. In open source, people work as people, not as companies. You can find the +name and email address of the person who wrote a particular feature and send +them bugs and questions. + +An open source Facebook wouldn't be rearranging and filtering your timeline to +best suit their advertisers interests. An open source iCloud would include +import and export tools so you can take your data elsewhere if you so choose. An +open source phone wouldn't be loaded with unremovable crapware, and even if it +was, you could patch it. + +When you install software on Linux, you get cryptographically verified packages +from individuals you can trust. You can look up who packaged your software and +get to know them personally, or even help them out! You can download the files +necessary to build the package from scratch and do so, adding any tweaks and +customizations as you wish. You don't have a human point of contact for Facebook +or GMail. + +Yes, there is a usability tradeoff. It is often more difficult to use open +source software. However, it's also often more powerful, tweakable, flexible, +and hackable. + +Next time you decide what software *you* should use, ask yourself: does this +software serve my interests or someone else's? + +[^1]: I'm certain some readers will take offense at my language choice in this article with respect to free/libre/open source software - I chose my words intentionally. I'll talk more about my opinions on the free software movement in a later post. diff --git a/content/blog/When-not-to-use-a-regex.md b/content/blog/When-not-to-use-a-regex.md @@ -0,0 +1,79 @@ +--- +date: 2017-08-13 +layout: post +title: When not to use a regex +tags: [regex] +--- + +The other day, I saw [Learn regex the easy +way](https://github.com/zeeshanu/learn-regex). This is a great resource, but I +felt the need to pen a post explaining that regexes are usually not the right +approach. + +Let's do a little exercise. I googled "URL regex" and here's the first Stack +Overflow result: + +``` +https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*) +``` + +<p style="text-align: right"> +<small><a href="https://stackoverflow.com/a/3809435/1191610">source</a></small> +</p> + +This is a bad regex. Here are some valid URLs that this regex fails to match: + +- http://x.org +- http://nic.science +- http://名がドメイン.com (warning: this is a parked domain) +- http://example.org/url,with,commas +- https://en.wikipedia.org/wiki/Harry_Potter_(film_series) +- http://127.0.0.1 +- http://[::1] (ipv6 loopback) + +Here are some invalid URLs the regex is fine with: + +- http://exam..ple.org +- http://--example.org + +This answer has been revised 9 times on Stack Overflow, and this is the best +they could come up with. Go back and read the regex. Can you tell where each of +these bugs are? How long did it take you? If you received a bug report in your +application because one of these URLs was handled incorrectly, do you understand +this regex well enough to fix it? If your application has a URL regex, go find +it and see how it fares with these tests. + +Complicated regexes are opaque, unmaintainable, and often wrong. The correct +approach to validating a URL is as follows: + +```python +from urllib.parse import urlparse + +def is_url_valid(url): + try: + urlparse(url) + return True + except: + return False +``` + +A regex is useful for validating *simple* patterns and for *finding* patterns in +text. For anything beyond that it's almost certainly a terrible choice. Say you +want to... + +**validate an email address**: try to send an email to it! + +**validate password strength requirements**: estimate the complexity with +[zxcvbn](https://github.com/dropbox/zxcvbn)! + +**validate a date**: use your standard library! +[datetime.datetime.strptime](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strptime) + +**validate a credit card number**: run the [Luhn +algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm) on it! + +**validate a social security number**: alright, use a regex. But don't expect +the number to be assigned to someone until you ask the Social Security +Administration about it! + +Get the picture? diff --git a/content/blog/Why-Go-error-handling-doesnt-sit-right-with-me.md b/content/blog/Why-Go-error-handling-doesnt-sit-right-with-me.md @@ -0,0 +1,113 @@ +--- +date: 2014-06-07 +#vim: tw=82 +layout: post +title: Go's error handling doesn't sit right with me +tags: [go] +--- + +I'll open up by saying that I am not a language designer, and I do like a lot of +things about Go. I just recently figured out how to describe why Go's error +handling mechanics don't sit right with me. + +If you aren't familiar with Go, here's an example of how Go programmers might do +error handling: + +```go +result, err := SomethingThatMightGoWrong() +if err != nil { + // Handle error +} +// Proceed +``` + +Let's extrapolate this: + +```go +func MightFail() { + result, err := doStuffA() + if err != nil { + // Error handling omitted + } + result, err = doStuffB() + if err != nil { + // Error handling omitted + } + result, err = doStuffC() + if err != nil { + // Error handling omitted + } + result, err = doStuffD() + if err != nil { + // Error handling omitted + } +} +``` + +Go has good intentions by removing exceptions. They add a lot of overhead and +returning errors isn't a bad thing in general. However, I spend a lot of my time +writing assembly. Assembly can use similar mechanics, but I'm spoiled by it (I +know, spoiled by assembly?) and I can see how Go could have done better. In +assembly, `goto` (or instructions like it) are the only means you have of +branching. It's not like other languages where it's taboo - you pretty much *have* +to use it. Most assembly also makes it fancy and conditional. For example: + + goto condition, label + +This would jump to `label` given that `condition` is met. Like Go, assembly +generally doesn't have exceptions or anything similar. In my own personal flavor +of assembly, I have my functions return error codes as well. Here's how it's +different, though. Let's look at some code: + +``` +call somethingThatMightFail +jp nz, errorHandler +call somethingThatMightFailB +jp nz, errorHandler +call somethingThatMightFailC +jp nz, errorHandler +call somethingThatMightFailD +jp nz, errorHandler +``` + +The difference here is that all functions return errors in the same way - by +resetting the Z flag. If that flag is set, we do a quick branch (the `jp` +instruction is short for `jump`) to the error handler. It's not clear from looking +at this snippet, but the error code is stored in the A register, which the +`errorHandler` recognizes as an error code and shows an appropriate message for. +We can have one error handler for an entire procedure, and it feels natural. + +In Go, you have to put an if statement here. Each error caught costs you three +lines of code in the middle of your important logic flow. With languages that +throw exceptions, you have all the logic in a readable procedure, and some error +handling at the end of it all. With Go, you have to throw a bunch of +3-line-minimum error handlers all over the middle of your procedure. + +In my examples, you can still return errors like this, but you can do so with a +lot less visual clutter. One line of error handling is better than 3 lines, if you +ask me. Also, no one gives a damn how you format assembly code, so if you wanted +to do something like this you'd be fine: + +``` +call somethingThatMightFail + jp nz, errorHandler +call somethingThatMightFailB + jp nz, errorHandler +call somethingThatMightFailC + jp nz, errorHandler +call somethingThatMightFailD + jp nz, errorHandler +``` + +Or something like this: + +``` +call somethingThatMightFail \ jp nz, errorHandler +call somethingThatMightFailB \ jp nz, errorHandler +call somethingThatMightFailC \ jp nz, errorHandler +call somethingThatMightFailD \ jp nz, errorHandler +``` + +The point is, I think Go's error handling stuff make your code harder to read and +more tedious to write. The basic idea - return errors instead of throwing them - +has good intentions. It's just that how they've done it isn't so great. diff --git a/content/blog/Why-I-built-sr.ht-with-Flask.md b/content/blog/Why-I-built-sr.ht-with-Flask.md @@ -0,0 +1,81 @@ +--- +date: 2019-01-30 +# note to self: shoot David Lord <davidism@gmail.com> an email when this is +# ready (2019-01-08) +layout: post +title: Why I chose Flask to build sr.ht's mini-services +tags: ["flask", "sourcehut", "web"] +--- + +[sr.ht](https://sr.ht) is a large, production-scale suite of web applications (I +call them "mini-services", as they strike a balance between microservices and +monolithic applications) which are built in Python with +[Flask](http://flask.pocoo.org/). David Lord, one of the maintainers of Flask, +reached out to me when he heard about sr.ht and saw that it was built with +Flask. At his urging, I'd like to share the rationale behind the decision and +how it's turned out in the long run. + +I have a long history of writing web applications with Flask, so much so that I +think I've lost count of them by now - at least 15, if not 20. Flask's +simplicity and flexibility is what keeps bringing me back. Frameworks like +Django or Rails are much different: they are the kitchen sink, and then some. I +generally don't need the whole kitchen sink, and if I were given it, I would +want to change some details. Flask is nice because it gives you the basics and +lets you build what you need on top of it, and you're never working around a +cookie-cutter system which doesn't cut your cookies in quite the way you need. + +In sr.ht's case in particular, though, I have chosen to extend Flask with [a new +module][core.sr.ht] common to all sr.ht projects. After all, each service of +sr.ht has a lot in common with the rest. Some of the things that live in this +core module are: + +[core.sr.ht]: https://git.sr.ht/~sircmpwn/core.sr.ht + +- Shared jinja2 templates and stylesheets +- Shared rigging for [SQLAlchemy](https://www.sqlalchemy.org/) (ORM) +- Shared rigging for [Alembic](https://alembic.sqlalchemy.org/en/latest/) +- [A little validation module][validation.py] I'm very proud of +- API behavior, webhooks, OAuth, etc, which are consistent throughout sr.ht + +[validation.py]: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/srht/validation.py + +The mini-service-oriented architecture allows sr.ht services to be deployed +ala-carte for users who only need a fraction of what we offer. This design +requires a lot of custom code to integrate all of the services with each other - +for example, all of the services use a single shared config file, which contains +both shared config options and service-specific configuration. sr.ht also uses a +novel approach to authentication, in which both user logins and API +authentication is delegated to an external service, meta.sr.ht, requiring +further custom code still. core.sr.ht additionally provides common SQLAlchemy +mixins for things like user tables, which have many common properties, but for +each service may have service-specific columns as well. + +Django provides their own ORM, their own authentication, their own models, and +more. In order to meet the design constraints of sr.ht, I'd have spent twice as +long ripping out the rest of Django's bits and fixing anything that broke in the +resulting mess. With Flask, these bits were never written for me in the first +place, which gives me the freedom to implement this design greenfield. Flask is +small and what code it does bring to the table is highly pluggable. + +Though it's well suited to many of my needs, I don't think Flask is perfect. A +few things I dislike about it: + +- First-class [jinja2](http://jinja.pocoo.org/) support is probably out of + scope. +- flask.Flask and flask.Blueprint should be the same thing. +- I'm not a fan of Flask's approach to configuration. I have a better(?) config + module that I drag around to all of my projects. + +And to summarize the good: + +- It provides a nice no-nonsense interface for requests, responses, and routing. +- It has a lot of nice hooks for adding your own middleware. +- It doesn't do much more than that, which means you're free to choose and + compose other tools to make up the difference. + +I think that on the whole it's quite good. There are frameworks which are +smaller still - but I think Flask hits a sweet spot. If you're making a +monolithic web app and can live within the on-rails Django experience, you might +want to use it. But if you are making smaller apps or need to rig things up in a +unique way - something I find myself doing almost every time - Flask is probably +for you. diff --git a/content/blog/Why-I-use-old-hardware.md b/content/blog/Why-I-use-old-hardware.md @@ -0,0 +1,58 @@ +--- +date: 2019-01-23 +layout: post +title: Why I use old hardware +tags: ["philosophy"] +--- + +Recently I was making sure my main laptop is ready for travel[^1], which mostly +just entails syncing up the latest version of my music collection. This laptop +is a Thinkpad X200, which turns 11 years old in July and is my main workstation +away from home (though I bring a second monitor and an external keyboard for +long trips). This laptop is a great piece of hardware. 100% of the hardware is +supported by the upstream Linux kernel, including the usual offenders like WiFi +and Bluetooth. Niche operating systems like 9front and Minix work great, too. +Even coreboot works! It's durable, user-serviceable, light, and still looks +brand new after all of these years. I love all of these things, but there's no +denying that it's 11 years behind on performance innovations. + +[^1]: To [FOSDEM](https://fosdem.org/2019/) - see you there! + +Last year [KDE](https://kde.org) generously [invited me][kde-recap] to and +sponsored my travel to their development sprint in Berlin. One of my friends +there teased me - in a friendly way - about my laptop, asking why I used such an +old system. There was a pensive moment when I answered: "it forces me to +empathise with users who can't use high-end hardware". I showed him how it could +cold boot to a productive [sway](https://swaywm.org) desktop in &lt;30 seconds, +then I installed KDE to compare. It doubled the amount of disk space in use, +took almost 10x as long to reach a usable desktop, and had severe rendering +issues with my old Intel GPU. + +[kde-recap]: https://drewdevault.com/2018/04/28/KDE-Sprint-retrospective.html + +To be clear, KDE is a wonderful piece of software and my first recommendation to +most non-technical computer users who ask me for advice on using Linux. But +software often grows to use the hardware you give it. Software developers tend +to be computer enthusiasts, and use enthusiast-grade hardware. In reality, this +high-end hardware isn't really *necessary* for most applications outside of +video encoding, machine learning, and a few other domains. + +I do have a more powerful workstation at home, but it's not really anything +special. I upgrade it very infrequently. I bought a new mid-range GPU which is +able to drive my four displays[^2] last year, I've added the occasional hard +drive as it gets full, and I replaced the case with something lighter weight 3 +years ago. Outside of those minor upgrades, I've been using the same desktop +workstation for 7 years, and intend to use it for much longer. My servers are +similarly running on older hardware which is spec'd to their needs (actually, I +left a lot of room to grow and *still* was able to buy old hardware). + +My 11-year-old laptop can compile the Linux kernel from scratch in 20 minutes, +and it can play 1080p video in real-time. That's all I need! Many users cannot +afford high-end computer hardware, and most have better things to spend their +money on. And you know, I work hard for my money, too - if I can get a computer +which can do nearly 5 *billion* operations per second for $60, that should be +sufficient to solve nearly any problem. No doubt, there are faster laptops out +there, many of them with similarly impressive levels of compatibility with my +ideals. But why bother? + +[^2]: I have a variety of displays and display configurations for the purpose of continuously testing sway/wlroots in those situations diff --git a/content/blog/Why-rewrite-wlc.md b/content/blog/Why-rewrite-wlc.md @@ -0,0 +1,93 @@ +--- +date: 2018-05-27 +title: Why did we replace wlc? +layout: post +tags: [wayland, sway, wlroots] +--- + +For a little over a year, I've been working with a bunch of talented C +developers to build a replacement for the [wlc](https://github.com/Cloudef/wlc) +library. The result is [wlroots](https://github.com/swaywm/wlroots), and we're +still working on completing it and updating our software to use it. The +[conventional +wisdom](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/) +suggests that rewriting your code from scratch is almost never the right idea. +So why did we do it, and how is it working out? I have spoken a little about +this in the past, but we'll answer this question in detail today. + +Sway will have been around for 3 years as of this August. When I started the +project, I wanted to skip some of the hard parts and get directly to +implementing i3 features. To this end, I was browsing around for libraries which +provided some of the low-level plumbing for me - stuff like DRM (Display +Resource Management) and KMS (Kernel Mode Setting), EGL and GLES wires, libinput +support, and so on. I was more interested in whatever tool could get me up to +speed and writing sway-specific code quickly. My options at this point came down +to wlc and [swc](https://github.com/michaelforney/swc). + +swc's design is a little bit better in retrospect, but I ended up choosing wlc +for the simple reason that it had an X11 backend I could use for easier +debugging. If I had used swc, I would have been forced to work without a display +server and test everything under the DRM backend - which would have been pretty +annoying. So I chose wlc and go to work. + +Designwise, wlc is basically a Wayland compositor with a plugin API, except you +get to write `main` yourself and the plugin API communicates entirely +in-process. wlc has its own renderer (which you cannot control) and its own +desktop with its own view abstraction (which you cannot control). You have some +events that it bubbles up for you and you can make some choices like where to +arrange windows. However, if you just wire up some basics and run `wlc_init`, +wlc will do all of the rest of the work and immediately start accepting clients, +rendering windows, and dispatching input. + +Over time we were able to make some small improvements to wlc, but sway 0.x +still works with these basic principles today. Though this worked well at first, +over time more and more of sway's bugs and limitations were reflections of +problems with wlc. A lengthy discussion on IRC and [on +GitHub](https://github.com/swaywm/sway/issues/1076) ensued and we debated for +several weeks on how we should proceed. I was originally planning on building a +new compositor entirely in-house (similar to GNOME's mutter and KDE's kwin), and +I wanted to abstract the i3-specific functionality of sway into some kind of +plugin. Then, more "frontends" could be written on top of sway to add +functionality like AwesomeWM, bspwm, Xmonad, etc. + +After some discussion among the sway team and with other Wayland compositor +projects [facing similar +problems](https://github.com/way-cooler/way-cooler/issues/248) with wlc, I +decided that we would start developing a standalone library to replace wlc +instead, and with it allow a more diverse Wayland ecosystem to flourish. +Contrary to wlc's design - a Wayland compositor with some knobs - wlroots is a +set of modular tools with which you build the Wayland compositor yourself. This +design allows it to be suited to a huge variety of projects, and as a result +it's now being used for many different Wayland compositors, each with their own +needs and their own approach to leveraging wlroots. + +When we started working on this, I wasn't sure if it was going to be successful. +Work began slowly and I knew we had a monumental task ahead of us. We spent a +lot of time and a few large refactorings getting a feel for how we wanted the +library to take shape. Different parts matured at different paces, sometimes +with changes in one area causing us to rethink design decisions that affected +the whole project. Eventually, we fell into our stride and found an approach +that we're very happy with today. + +I think that the main difference with the approach that wlroots takes comes from +experience. Each of the people working on sway, wlc, way cooler, and so on were +writing Wayland compositors for the first time. I'd say the problems that arose +as a result can also be seen throughout other projects, including Weston, KWin, +and so on. The problem is that when we all set out, we didn't fully understand +the opportunities afforded by Wayland's design, nor did we see how best to +approach tying together the rather complicated Linux desktop stack into a +cohesive project. + +We could have continued to maintain wlc, fixed bugs, refactored parts of it, and +maybe eventually arrived at a place where sway more or less worked. But we'd +simply be carrying on the X11 tradition we've been trying to escape this whole +time. wlc was a kludge and replacing it was well worth the effort - it simply +could not have scaled to the places where wlroots is going. Today, wlroots is +the driving force behind 6 Wayland compositors and is targeting desktops, +tablets, and phones. Novel features never seen on any desktop - even beyond +Linux - are possible with this work. Now we can think about not only replacing +X11, but innovating in ways it never could have. + +Our new approach is the way that Wayland compositors should be made. wlroots is +the realization of Wayland's potential. I am hopeful that our design decisions +will have a lasting positive impact on the Wayland ecosystem. diff --git a/content/blog/Writing-a-Wayland-compositor-1.md b/content/blog/Writing-a-Wayland-compositor-1.md @@ -0,0 +1,384 @@ +--- +date: 2018-02-17 +title: "Writing a Wayland Compositor, Part 1: Hello wlroots" +layout: post +tags: [wayland, wlroots, instructional] +--- + +This is the first in a series of *many* articles I'm writing on the subject of +building a functional Wayland compositor from scratch. As you may know, I am the +lead maintainer of [sway](https://github.com/swaywm/sway), a reasonably popular +Wayland compositor. Along with many other talented developers, we've been +working on [wlroots](https://github.com/swaywm/wlroots) over the past few +months. This is a powerful tool for creating new Wayland compositors, but it is +very dense and difficult to understand. Do not despair! The intention of these +articles is to make you understand and feel comfortable using it. + +Before we dive in, a quick note: the wlroots team is starting a crowdfunding +campaign today to fund travel for each of our core contributors to meet in +person and work for two weeks on a hackathon. Please consider contributing to +[the campaign](https://www.indiegogo.com/projects/sway-hackathon-software/x/1059863)! + +You **must** read and comprehend my earlier article, [An introduction to +Wayland](/2017/06/10/Introduction-to-Wayland.html), before attempting to +understand this series of blog posts, as I will be relying on concepts and +terminology introduced there to speed things up. Some background in OpenGL is +helpful, but not required. A good understanding of C is mandatory. If you have +any questions about any of the articles in this series, please reach out to me +directly via [sir@cmpwn.com](mailto:sir@cmpwn.com) or to the wlroots team at +[#sway-devel on irc.freenode.net](http://webchat.freenode.net/?channels=sway-devel&uio=d4). + +During this series of articles, the compositor we're building will live on +GitHub: [Wayland McWayface](https://github.com/SirCmpwn/mcwayface). Each article +in this series will be presented as a breakdown of a single commit between zero +and a fully functional Wayland compositor. The commit for this article is +[f89092e](https://github.com/SirCmpwn/mcwayland/commit/f89092e). +I'm only going to explain the important parts - I suggest you review +the entire commit separately. + +Let's get started. First, I'm going to define a struct for holding our +compositor's state: + +```diff ++struct mcw_server { ++ struct wl_display *wl_display; ++ struct wl_event_loop *wl_event_loop; ++}; +``` + +Note: mcw is short for McWayface. We'll be using this acronym throughout the +article series. We'll set one of these aside and initialize the Wayland display +for it[^1]: + +```diff + int main(int argc, char **argv) { ++ struct mcw_server server; ++ ++ server.wl_display = wl_display_create(); ++ assert(server.wl_display); ++ server.wl_event_loop = wl_display_get_event_loop(server.wl_display); ++ assert(server.wl_event_loop); + return 0; + } +``` + +The Wayland display gives us a number of things, but for now all we care about +is the event loop. This event loop is deeply integrated into wlroots, and is +used for things like dispatching signals across the application, being notified +when data is available on various file descriptors, and so on. + +Next, we need to create the backend: + +```diff + struct mcw_server { + struct wl_display *wl_display; + struct wl_event_loop *wl_event_loop; + ++ struct wlr_backend *backend; + }; +``` + +The **backend** is our first wlroots concept. The backend is responsible for +abstracting the low level *input* and *output* implementations from you. Each +backend can generate zero or more input devices (such as mice, keyboards, etc) +and zero or more output devices (such as monitors on your desk). Backends have +nothing to do with Wayland - their purpose is to help you with the *other* APIs +you need to use as a Wayland compositor. There are various backends with various +purposes: + +- The **drm** backend utilizes the Linux DRM subsystem to render directly to + your physical displays. +- The **libinput** backend utilizes libinput to enumerate and control physical + input devices. +- The **wayland** backend creates "outputs" as windows on another running + Wayland compositors, allowing you to nest compositors. Useful for debugging. +- The **x11** backend is similar to the Wayland backend, but opens an x11 window + on an x11 server rather than a Wayland window on a Wayland server. + +Another important backend is the **multi** backend, which allows you to +initialize several backends at once and aggregate their input and output +devices. This is necessary, for example, to utilize both drm and libinput +simultaneously. + +wlroots provides a helper function for automatically choosing the most +appropriate backend based on the user's environment: + +```diff + server.wl_event_loop = wl_display_get_event_loop(server.wl_display); + assert(server.wl_event_loop); + ++ server.backend = wlr_backend_autocreate(server.wl_display); ++ assert(server.backend); + return 0; + } +``` + +I would generally suggest using either the Wayland or X11 backends during +development, especially before we have a way of exiting the compositor. If you +call `wlr_backend_autocreate` from a running Wayland or X11 session, the +respective backends will be automatically chosen. + +We can now start the backend and enter the Wayland event loop: + +```diff ++ if (!wlr_backend_start(server.backend)) { ++ fprintf(stderr, "Failed to start backend\n"); ++ wl_display_destroy(server.wl_display); ++ return 1; ++ } ++ ++ wl_display_run(server.wl_display); ++ wl_display_destroy(server.wl_display); + return 0; +``` + +If you run your compositor at this point, you should see the backend start up +and... do nothing. It'll open a window if you run from a running Wayland or X11 +server. If you run it on DRM, it'll probably do very little and you won't even +be able to switch to another TTY to kill it. + +In order to render something, we need to know about the outputs we can render +on. The backend provides a **wl_signal** that notifies us when it gets a new +output. This will happen on startup and as any outputs are hotplugged at +runtime. + +Let's add this to our server struct: + +```diff + struct mcw_server { + struct wl_display *wl_display; + struct wl_event_loop *wl_event_loop; + + struct wlr_backend *backend; ++ ++ struct wl_listener new_output; ++ ++ struct wl_list outputs; // mcw_output::link + }; +``` + +This adds a `wl_listeners` which is signalled when new outputs are added. We +also add a `wl_list` (which is just a linked list provided by libwayland-server) +which we'll later store some state in. To be notified, we must use +`wl_signal_add`: + +```diff + assert(server.backend); + ++ wl_list_init(&server.outputs); ++ ++ server.new_output.notify = new_output_notify; ++ wl_signal_add(&server.backend->events.new_output, &server.new_output); + + if (!wlr_backend_start(server.backend)) { +``` + +We specify here the function to be notified, `new_output_notify`: + +```diff ++static void new_output_notify(struct wl_listener *listener, void *data) { ++ struct mcw_server *server = wl_container_of( ++ listener, server, new_output); ++ struct wlr_output *wlr_output = data; ++ ++ if (!wl_list_empty(&wlr_output->modes)) { ++ struct wlr_output_mode *mode = ++ wl_container_of(wlr_output->modes.prev, mode, link); ++ wlr_output_set_mode(wlr_output, mode); ++ } ++ ++ struct mcw_output *output = calloc(1, sizeof(struct mcw_output)); ++ clock_gettime(CLOCK_MONOTONIC, &output->last_frame); ++ output->server = server; ++ output->wlr_output = wlr_output; ++ wl_list_insert(&server->outputs, &output->link); ++} +``` + +This is a little bit complicated! This function has several roles when dealing +with the incoming `wlr_output`. When the signal is raised, a pointer to the +listener that was signaled is passed in, as well as the `wlr_output` which was +created. `wl_container_of` uses some `offsetof`-based magic to get the +`mcw_server` reference from the listener pointer, and we cast `data` to the +actual type, `wlr_output`. + +The next thing we have to do is set the **output mode**. Some backends (notably +x11 and Wayland) do not support modes, but they are necessary for DRM. Output +modes specify a size and refresh rate supported by the output, such as +`1920x1080@60Hz`. The body of this if statement just chooses the last one (which +is usually the highest resolution and refresh rate) and applies it to the output +with `wlr_output_set_mode`. We *must* set the output mode in order to render to +it. + +Then, we set up some state for us to keep track of this output with in our +compositor. I added this struct definition at the top of the file: + +```diff ++struct mcw_output { ++ struct wlr_output *wlr_output; ++ struct mcw_server *server; ++ struct timespec last_frame; ++ ++ struct wl_list link; ++}; +``` + +This will be the structure we use to store any state we have for this output +that is specific to our compositor's needs. We include a reference to the +`wlr_output`, a reference to the `mcw_server` that owns this output, and the +time of the last frame, which will be useful later. We also set aside a +`wl_list`, which is used by libwayland for linked lists. + +Finally, we add this output to the server's list of outputs. + +We could use this now, but it would leak memory. We also need to handle output +*removal*, with a signal provided by wlr_output. We add the listener to the +mcw_output struct: + +```diff + struct mcw_output { + struct wlr_output *wlr_output; + struct mcw_server *server; + struct timespec last_frame; ++ ++ struct wl_listener destroy; + + struct wl_list link; + }; +``` + +Then we hook it up when the output is added: + +```diff + wl_list_insert(&server->outputs, &output->link); + ++ output->destroy.notify = output_destroy_notify; ++ wl_signal_add(&wlr_output->events.destroy, &output->destroy); + } +``` + +This will call our output_destroy_notify function to handle cleanup when the +output is unplugged or otherwise removed from wlroots. Our handler looks like +this: + +```diff ++static void output_destroy_notify(struct wl_listener *listener, void *data) { ++ struct mcw_output *output = wl_container_of(listener, output, destroy); ++ wl_list_remove(&output->link); ++ wl_list_remove(&output->destroy.link); ++ wl_list_remove(&output->frame.link); ++ free(output); ++} +``` + +This one should be pretty self-explanatory. + +So, we now have a reference to the output. However, we are still not rendering +anything - if you run the compositor again you'll notice the same behavior. In +order to render things, we have to listen for the **frame signal**. Depending on +the selected mode, the output can only receive new frames at a certain rate. We +keep track of this for you in wlroots, and emit the frame signal when it's time +to draw a new frame. + +Let's add a listener to the `mcw_output` struct for this purpose: + +```diff + struct mcw_output { + struct wlr_output *wlr_output; + struct mcw_server *server; + + struct wl_listener destroy; ++ struct wl_listener frame; + + struct wl_list link; + }; +``` + +We can then extend `new_output_notify` to register the listener to the frame +signal: + +```diff + output->destroy.notify = output_destroy_notify; + wl_signal_add(&wlr_output->events.destroy, &output->destroy); ++ output->frame.notify = output_frame_notify; ++ wl_signal_add(&wlr_output->events.frame, &output->frame); + } +``` + +Now, whenever an output is ready for a new frame, `output_frame_notify` will be +called. We still need to write this function, though. Let's start with the +basics: + +```diff ++static void output_frame_notify(struct wl_listener *listener, void *data) { ++ struct mcw_output *output = wl_container_of(listener, output, frame); ++ struct wlr_output *wlr_output = data; ++} +``` + +In order to render anything here, we need to first obtain a wlr_renderer[^2]. +We can obtain one from the backend: + +```diff + static void output_frame_notify(struct wl_listener *listener, void *data) { + struct mcw_output *output = wl_container_of(listener, output, frame); + struct wlr_output *wlr_output = data; ++ struct wlr_renderer *renderer = wlr_backend_get_renderer( ++ wlr_output->backend); +} +``` + +We can now take advantage of this renderer to draw something on the output. + +```diff + static void output_frame_notify(struct wl_listener *listener, void *data) { + struct mcw_output *output = wl_container_of(listener, output, frame); + struct wlr_output *wlr_output = data; + struct wlr_renderer *renderer = wlr_backend_get_renderer( + wlr_output->backend); ++ ++ wlr_output_make_current(wlr_output, NULL); ++ wlr_renderer_begin(renderer, wlr_output); ++ ++ float color[4] = {1.0, 0, 0, 1.0}; ++ wlr_renderer_clear(renderer, color); ++ ++ wlr_output_swap_buffers(wlr_output, NULL, NULL); ++ wlr_renderer_end(renderer); + } +``` + +Calling `wlr_output_make_current` makes the output's OpenGL context "current", +and from here you can use OpenGL calls to render to the output's buffer. We call +`wlr_renderer_begin` to configure some sane OpenGL defaults for us[^3]. + +At this point we can start rendering. We'll expand more on what you can +do with `wlr_renderer` later, but for now we'll be satisified with clearing the +output to a solid red color. + +When we're done rendering, we call `wlr_output_swap_buffers` to swap the +output's front and back buffers, committing what we've rendered to the actual +screen. We call `wlr_renderer_end` to clean up the OpenGL context and we're +done. Running our compositor now should show you a solid red screen! + +--- + +This concludes today's article. If you take a look at [the +commit](https://github.com/SirCmpwn/mcwayland/commit/f89092e) that this article +describes, you'll see that I took it a little further with some code that clears +the display to a different color every frame. Feel free to experiment with +similar changes! + +Over the next two articles, we'll finish wiring up the Wayland server and render +a Wayland client on screen. Please look forward to it! + +<p style="text-align: right"> + Next &mdash; + <a href="/2018/02/22/Writing-a-wayland-compositor-part-2.html"> + Part 2: Rigging up the server + </a> +</p> + +[^1]: It's entirely possible to utilize a wlroots backend to make applications which are not Wayland compositors. However, we require a wayland display anyway because the event loop is necessary for a lot of wlroots internals. +[^2]: wlr_renderer is optional. When you call wlr_output_make_current, the OpenGL context is made current and from here you can use any approach you prefer. wlr_renderer is provided to help compositors with simple rendering requirements. +[^3]: Namely: the viewport and blend mode. diff --git a/content/blog/Writing-a-wayland-compositor-part-2.md b/content/blog/Writing-a-wayland-compositor-part-2.md @@ -0,0 +1,170 @@ +--- +date: 2018-02-22 +title: "Writing a Wayland Compositor, Part 2: Rigging up the server" +layout: post +tags: [wayland, wlroots, instructional] +--- + +This is the second in a series of articles on the subject of writing a Wayland +compositor from scratch using [wlroots](https://github.com/swaywm/wlroots). +Check out [the first article](/2018/02/17/Writing-a-Wayland-compositor-1.html) +if you haven't already. Last time, we ended up with an application which fired +up a wlroots backend, enumerated output devices, and drew some pretty colors on +the screen. Today, we're going to start accepting Wayland client connections, +though we aren't going to be doing much with them yet. + +The commit that this article dissects is +[b45c651](https://github.com/SirCmpwn/mcwayland/commit/b45c651). + +A quick aside on the nature of these blog posts: it's going to take *a lot* of +these articles to flesh out our compositor. I'm going to be publishing these +more frequently than usual, probably 1-2 per week, and continue posting my usual +articles at the typical rate. Okay? Cool. + +So we've started up the backend and we're rendering something interesting, but +we still aren't running a Wayland server -- Wayland clients aren't connecting to +our application. Adding this is actually quite easy: + +```diff +@@ -113,12 +113,18 @@ int main(int argc, char **argv) { + server.new_output.notify = new_output_notify; + wl_signal_add(&server.backend->events.new_output, &server.new_output); + ++ const char *socket = wl_display_add_socket_auto(server.wl_display); ++ assert(socket); ++ + if (!wlr_backend_start(server.backend)) { + fprintf(stderr, "Failed to start backend\n"); + wl_display_destroy(server.wl_display); + return 1; + } + ++ printf("Running compositor on wayland display '%s'\n", socket); ++ setenv("WAYLAND_DISPLAY", socket, true); ++ + wl_display_run(server.wl_display); + wl_display_destroy(server.wl_display); + return 0; +``` + +That's it! If you run McWayface again, it'll print something like this: + +``` +Running compositor on wayland display 'wayland-1' +``` + +[Weston](https://cgit.freedesktop.org/wayland/weston/), the Wayland reference +compositor, includes a number of simple reference clients. We can use +`weston-info` to connect to our server and list the **globals**: + +``` +$ WAYLAND_DISPLAY=wayland-1 weston-info +interface: 'wl_drm', version: 2, name: 1 +``` + +If you recall from my [Introduction to +Wayland](/2017/06/10/Introduction-to-Wayland.html), the Wayland server exports a +list of **globals** to clients via the Wayland registry. These globals provide +interfaces the client can utilize to interact with the server. We get `wl_drm` +for free with wlroots, but we have not actually wired up anything useful yet. +Wlroots provides many "types", of which the majority are implementations of +Wayland global interfaces like this. + +Some of the wlroots implementations require some rigging from you, but several +of them just take care of themselves. Rigging these up is easy: + +```diff + printf("Running compositor on wayland display '%s'\n", socket); + setenv("WAYLAND_DISPLAY", socket, true); ++ ++ wl_display_init_shm(server.wl_display); ++ wlr_gamma_control_manager_create(server.wl_display); ++ wlr_screenshooter_create(server.wl_display); ++ wlr_primary_selection_device_manager_create(server.wl_display); ++ wlr_idle_create(server.wl_display); + + wl_display_run(server.wl_display); + wl_display_destroy(server.wl_display); +``` + +Note that some of these interfaces are not necessarily ones that you typically +would want to expose to all Wayland clients - screenshooter, for example, is +something that should be secured. We'll get to security in a later article. For +now, if we run `weston-info` again, we'll see a few more globals have appeared: + +``` +$ WAYLAND_DISPLAY=wayland-1 weston-info +interface: 'wl_shm', version: 1, name: 3 + formats: XRGB8888 ARGB8888 +interface: 'wl_drm', version: 2, name: 1 +interface: 'gamma_control_manager', version: 1, name: 2 +interface: 'orbital_screenshooter', version: 1, name: 3 +interface: 'gtk_primary_selection_device_manager', version: 1, name: 4 +interface: 'org_kde_kwin_idle', version: 1, name: 5 +``` + +You'll find that wlroots implements a variety of protocols from a variety of +sources - here we see protocols from Orbital, GTK, and KDE represented. Wlroots +includes an example client for the orbital screenshooter - we can use it now to +take a screenshot of our compositor: + +``` +$ WAYLAND_DISPLAY=wayland-1 ./examples/screenshot +cannot set buffer size +``` + +Ah, this is a problem - you may have noticed that we don't have any wl_output +globals, which the screenshooter client relies on to figure out the resolution +of the screenshot buffer. We can add these, too: + +```diff +@@ -95,6 +99,8 @@ static void new_output_notify(struct wl_listener *listener, void *data) { + wl_signal_add(&wlr_output->events.destroy, &output->destroy); + output->frame.notify = output_frame_notify; + wl_signal_add(&wlr_output->events.frame, &output->frame); ++ ++ wlr_output_create_global(wlr_output); + } +``` + +Running `weston-info` again will give us some info about our outputs now: + +``` +$ WAYLAND_DISPLAY=wayland-1 weston-info +interface: 'wl_drm', version: 2, name: 1 +interface: 'wl_output', version: 3, name: 2 + x: 0, y: 0, scale: 1, + physical_width: 0 mm, physical_height: 0 mm, + make: 'wayland', model: 'wayland', + subpixel_orientation: unknown, output_transform: normal, + mode: + width: 952 px, height: 521 px, refresh: 0.000 Hz, + flags: current +interface: 'wl_shm', version: 1, name: 3 + formats: XRGB8888 ARGB8888 +interface: 'gamma_control_manager', version: 1, name: 4 +interface: 'orbital_screenshooter', version: 1, name: 5 +interface: 'gtk_primary_selection_device_manager', version: 1, name: 6 +interface: 'org_kde_kwin_idle', version: 1, name: 7 +``` + +Now we can take that screenshot! Give it a shot (heh)! + +We're getting close to the good stuff now. The next article is going to +introduce the concept of **surfaces**, and we will use them to render our first +window. If you had any trouble with this article, please reach out to me at +[sir@cmpwn.com](mailto:sir@cmpwn.com) or to the wlroots team at +[#sway-devel](http://webchat.freenode.net/?channels=sway-devel&uio=d4). + +<p style="float: right"> + Next &mdash; + <a href="/2018/02/28/Writing-a-wayland-compositor-part-3.html"> + Part 3: Rendering a window + </a> +</p> +<p> + Previous &mdash; + <a href="/2018/02/17/Writing-a-Wayland-compositor-1.html"> + Part 1: Hello wlroots + </a> +</p> diff --git a/content/blog/Writing-a-wayland-compositor-part-3.md b/content/blog/Writing-a-wayland-compositor-part-3.md @@ -0,0 +1,252 @@ +--- +date: 2018-02-28 +layout: post +title: "Writing a Wayland Compositor, Part 3: Rendering a window" +tags: [wayland, wlroots, instructional] +--- + +This is the third in a series of articles on the subject of writing a Wayland +compositor from scratch using [wlroots](https://github.com/swaywm/wlroots). +Check out [the first article](/2018/02/17/Writing-a-Wayland-compositor-1.html) +if you haven't already. We left off with a Wayland server which accepts client +connections and exposes a handful of globals, but does not do anything +particularly interesting yet. Our goal today is to do something interesting - +render a window! + +The commit that this article dissects is +[342b7b6](https://github.com/SirCmpwn/mcwayland/commit/342b7b6). + +The first thing we have to do in order to render windows is establish the +**compositor**. The wl_compositor global is used by clients to allocate +`wl_surface`s, to which they attach `wl_buffer`s. These surfaces are just a +generic mechanism for sharing buffers of pixels with compositors, and don't +carry an implicit **role**, such as "application window" or "panel". + +wlroots provides an implementation of `wl_compositor`. Let's set aside a +reference for it: + +```diff +struct mcw_server { + struct wl_display *wl_display; + struct wl_event_loop *wl_event_loop; + + struct wlr_backend *backend; ++ struct wlr_compositor *compositor; + + struct wl_listener new_output; +``` + +Then rig it up: + +```diff + wlr_primary_selection_device_manager_create(server.wl_display); + wlr_idle_create(server.wl_display); + ++ server.compositor = wlr_compositor_create(server.wl_display, ++ wlr_backend_get_renderer(server.backend)); ++ + wl_display_run(server.wl_display); + wl_display_destroy(server.wl_display); +``` + +If we run mcwayface now and check out the globals with `weston-info`, we'll see +a wl_compositor and wl_subcompositor have appeared: + +``` +interface: 'wl_compositor', version: 4, name: 8 +interface: 'wl_subcompositor', version: 1, name: 9 +``` + +You get a wl_subcompositor for free with the wlroots wl_compositor +implementation. We'll discuss subcompositors in a later article. Speaking of +things we'll discuss in another article, add this too: + +```diff + wlr_primary_selection_device_manager_create(server.wl_display); + wlr_idle_create(server.wl_display); + + server.compositor = wlr_compositor_create(server.wl_display, + wlr_backend_get_renderer(server.backend)); + ++ wlr_xdg_shell_v6_create(server.wl_display); ++ + wl_display_run(server.wl_display); + wl_display_destroy(server.wl_display); + return 0; +``` + +Remember that I said earlier that surfaces are just globs of pixels with no +role? xdg_shell is something that can give surfaces a role. We'll talk about it +more in the next article. After adding this, many clients will be able to +connect to your compositor and spawn a window. However, without adding anything +else, these windows will never be shown on-screen. You have to render them! + +Something that distinguishes wlroots from libraries like wlc and libweston is +that wlroots does not do any rendering for you. This gives you a lot of +flexibility to render surfaces any way you like. The clients just gave you a +pile of pixels, what you do with them is up to you - maybe you're making a +desktop compositor, or maybe you want to draw them on an Android-style app +switcher, or perhaps your compositor arranges windows in VR - all of this is +possible with wlroots. + +Things are about to get complicated, so let's start with the easy part: in +the output_frame handler, we have to get a reference to every wlr_surface we +want to render. So let's iterate over every surface our `wlr_compositor` is +keeping track of: + +```diff + wlr_renderer_begin(renderer, wlr_output); + ++ struct wl_resource *_surface; ++ wl_resource_for_each(_surface, &server->compositor->surfaces) { ++ struct wlr_surface *surface = wlr_surface_from_resource(_surface); ++ if (!wlr_surface_has_buffer(surface)) { ++ continue; ++ } ++ // TODO: Render this surface ++ } + + wlr_output_swap_buffers(wlr_output, NULL, NULL); +``` + +The `wlr_compositor` struct has a member named `surfaces`, which is a list of +`wl_resource`s. A helper method is provided to produce a `wlr_surface` from its +corresponding `wl_resource`. The `wlr_surface_has_buffer` call is just to make +sure that the client has actually given us pixels to display on this surface. + +wlroots might make you do the rendering yourself, but some tools *are* provided +to help you write compositors with simple rendering requirements: +**wlr_renderer**. We've already touched on this a little bit, but now we're +going to use it for real. A little bit of OpenGL knowledge is required here. If +you're a complete novice with OpenGL[^1], I can recommend [this +tutorial](https://learnopengl.com/) to help you out. Since you're in a hurry, +we'll do a quick crash course on the concepts necessary to utilize wlr_renderer. +If you get lost, just skip to the next diff and treat it as magic incantations +that make your windows appear. + +We have a pile of pixels, and we want to put it on the screen. We can do this +with a **shader**. If you're using wlr_renderer (and mcwayface will be), shaders +are provided for you. To use our shaders, we feed them a **texture** (the pile +of pixels) and a **matrix**. If we treat every pixel coordinate on our surface +as a vector from (0, 0); top left, to (1, 1); bottom right, our goal is to +produce a matrix that we can multiply a vector by to find the final coordinates +on-screen for the pixel to be drawn to. We must project pixel coordinates from +this 0-1 system to the coordinates of our desired rectangle on screen. + +There's gotcha here, however: the coordinates on-screen *also* go from 0 to 1, +instead of, for example, 0-1920 and 0-1080. To project coordinates like +"put my 640x480 window at coordinates 100,100" to screen coordinates, we use an +**orthographic projection matrix**. I know that sounds scary, but don't worry - +wlroots does all of the work for you. Your `wlr_output` already has a suitable +matrix called `transform_matrix`, which incorporates into it the current +resolution, scale factor, and rotation of your screen. + +Okay, hopefully you're still with me. This sounds a bit complicated, but the +manifestation of all of this nonsense is fairly straightforward. wlroots +provides some tools to make it easy for you. First, we have to prepare a +`wlr_box` that represents (in output coordinates) where we want the surface to +show up. + +```diff + struct wl_resource *_surface; + wl_resource_for_each(_surface, &server->compositor->surfaces) { + struct wlr_surface *surface = wlr_surface_from_resource(_surface); + if (!wlr_surface_has_buffer(surface)) { + continue; + } +- // TODO: Render this surface ++ struct wlr_box render_box = { ++ .x = 20, .y = 20, ++ .width = surface->current->width, ++ .height = surface->current->height ++ }; + } +``` + +Now, here's the great part: all of that fancy math I was just talking about can +be done with a single helper function provided by wlroots: `wlr_matrix_project_box`. + +```diff + struct wl_resource *_surface; + wl_resource_for_each(_surface, &server->compositor->surfaces) { + struct wlr_surface *surface = wlr_surface_from_resource(_surface); + if (!wlr_surface_has_buffer(surface)) { + continue; + } + struct wlr_box render_box = { + .x = 20, .y = 20, + .width = surface->current->width, + .height = surface->current->height + }; ++ float matrix[16]; ++ wlr_matrix_project_box(&matrix, &render_box, ++ surface->current->transform, ++ 0, &wlr_output->transform_matrix); + } +``` + +This takes a reference to a `float[16]` to store the output matrix in, a box you +want to project, some other stuff that isn't important right now, and the +projection you want to use - in this case, we just use the one provided by +`wlr_output`. + +The reason we make you understand and perform these steps is because it's +entirely possible that you'll want to do them differently in the future. This +is only the simplest case, but remember that wlroots is designed for *every* +case. Now that we've obtained this matrix, we can finally render the surface: + +```diff + struct wl_resource *_surface; + wl_resource_for_each(_surface, &server->compositor->surfaces) { + struct wlr_surface *surface = wlr_surface_from_resource(_surface); + if (!wlr_surface_has_buffer(surface)) { + continue; + } + struct wlr_box render_box = { + .x = 20, .y = 20, + .width = surface->current->width, + .height = surface->current->height + }; + float matrix[16]; + wlr_matrix_project_box(&matrix, &render_box, + surface->current->transform, + 0, &wlr_output->transform_matrix); ++ wlr_render_with_matrix(renderer, surface->texture, &matrix, 1.0f); ++ wlr_surface_send_frame_done(surface, &now); + } +``` + +We also throw in a `wlr_surface_send_frame_done` for good measure, which lets +the client know that we're done with it so they can send another frame. We're +done! Run mcwayface now, then the following commands: + +``` +$ WAYLAND_DISPLAY=wayland-1 weston-simple-shm & +$ WAYLAND_DISPLAY=wayland-1 gnome-terminal -- htop +``` + +To see the following beautiful image: + +![](https://sr.ht/y_qN.png) + +Run any other clients you like - many of them will work! + +We used a bit of a hack today by simply rendering all of the surfaces the +`wl_compositor` knew of. In practice, we're going to need to extend our +xdg_shell support (and add some other shells, too) to do this properly. We'll +cover this in the next article. + +Before you go, a quick note: after this commit, I reorganized things a bit - +we're going to outgrow this single-file approach pretty quickly soon. Check out +That commit [here](https://github.com/SirCmpwn/mcwayface/commit/e800facb371c42d844b858af5ced456ffd6e9d08). + +See you next time! + +<p> + Previous &mdash; + <a href="/2018/02/22/Writing-a-wayland-compositor-part-2.html"> + Part 2: Rigging up the server + </a> +</p> + +[^1]: If you're not a novice, we'll cover more complex rendering scenarios in the future. But the short of it is that you can implement your own `wlr_renderer` that wlr_compositor can use to bind textures to the GPU and then you can do whatever you want. diff --git a/content/blog/You-dont-need-jQuery.md b/content/blog/You-dont-need-jQuery.md @@ -0,0 +1,147 @@ +--- +date: 2013-08-19 +title: You don't need jQuery +layout: post +tags: [javascript] +--- + +It's true. You really don't need jQuery. Modern web browsers can do most of what you want from jQuery, +without jQuery. + +For example, take [MediaCrush](https://mediacru.sh). It's a website I spent some time working on with a friend. +It's actually quite sophisticated - drag-and-drop uploading, uploading via a hidden form, events wired up to +links and dynamically generated content, and ajax requests/file uploads, the whole she-bang. It does all of +that without jQuery. It's [open source](https://github.com/MediaCrush/MediaCrush), if you're looking for a good +example of how all of this can be used in the wild. + +Let's walk through some of the things you like jQuery for, and I'll show you how to do it without. + +## Document Querying with CSS Selectors + +You like jQuery for selecting content. I don't blame you - it's really cool. Here's some code using jQuery: + +```js +$('div.article p').addClass('test'); +``` + +Now, here's how you can do it on vanilla JS: + +```js +var items = document.querySelectorAll('div.article p'); +for (var i = 0; i < items.length; i++) + items[i].classList.add('test'); +``` + +Documentation: [querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll), [classList](https://developer.mozilla.org/en-US/docs/Web/API/element.classList) + +This is, of course, a little more verbose. However, it's probably a lot simpler than you expected. Works in +IE 8 and newer - except for classList, which works in IE 10 and newer. You can instead use className, which is +a little less flexible, but still pretty easy to work with. + +## Ajax + +You want to make requests in JavaScript. This is how you POST with jQuery: + +```js +$.post('/path/to/endpoint', { + parameter: value + otherParameter: otherValue +}, +success: function(data) { + alert(data); +}); +``` + +Here's the same code, without jQuery: + +```js +var xhr = new XMLHttpRequest(); // A little deceptively named +xhr.open('POST', '/path/to/endpoint'); +xhr.onload = function() { + alert(this.responseText); +}; +var formData = new FormData(); +formData.append('parameter', value); +formData.append('otherParameter', value); +xhr.send(formData); +``` + +Documentation: [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) + +Also a bit more verbose than jQuery, but much simpler than you might've expected. Now here's the real kicker: +It works in IE 7, and IE 5 with a little effort. IE actually pioneered XHR. + +## Animations + +This is where it starts to get more subjective and breaks backwards compatability. Here's my opinion on the +matter of transitions: dropping legacy browser support for fancy animations is acceptable. I don't think it's +a problem if your website isn't pretty and animated on older browsers. Keep that in mind as we move on. + +I want to animate the opacity of a `.foobar` when you hover over it. With jQuery: + +```js +$('.foobar').on('mouseenter', function() { + $(this).animate({ + opacity: 0.5 + }, 2000); +}).on('mouseleave', function() { + $(this).animate({ + opacity: 1 + }, 2000); +}); +``` + +Without jQuery, I wouldn't do this in Javascript. I'd use the magic of CSS animations: + +```css +.foobar { + transition: opacity 2s linear; +} + +.foobar:hover { + opacity: 0; +} +``` + +<p class="foobar">Hover over this text</p> +<style>.foobar{transition:opacity 2s linear;font-weight:bold;}.foobar:hover{opacity:0.5;}</style> + +Documentation: [CSS animations](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Using_CSS_animations) + +Much better, eh? Works in IE 10+. You can do much more complicated animations with CSS, but I can't think of +a good demo, so that's an exercise left to the reader. + +## Tree traversal + +jQuery lets you navigate a tree pretty easily. Let's say you want to find the container of a button and remove +all .foobar elements underneath it, upon clicking the button. + +```js +$('#mybutton').click(function() { + $(this).parent().children('.foobar').remove(); +}); +``` + +Nice and succinct. I'm sure you can tell the theme so far - the main advantage of jQuery is a less verbose +syntax. Here's how it's done without jQuery: + +```js +document.getElementById('mybutton').addEventListener('click', function() { + var foobars = this.parentElement.querySelectorAll('.foobar'); + for (var i = 0; i < foobars.length; i++) + this.parentElement.removeChild(foobars[i]); +}, false); +``` + +A little wordier, but not so bad. Works in IE 9+ (8+ if you don't use addEventListener). + +## In conclusion + +jQuery is, of course, based on JavaScript, and as a result, anything jQuery can do can be done without jQuery. +Feel free to [ask me](mailto:sir@cmpwn.com) if you're curious about how I'd do something else without jQuery. + +I feel like adding jQuery is one of the first things a web developer does to their shiny new website. It just +isn't really necessary in this day and age. That extra request, 91kb, and load time are probably negligible, +but it's still a little less clean than it could be. There's no need to go back and rid all of your projects of +jQuery, but I'd suggest that for your next one, you try to do without. Keep MDN open in the next tab over and +I'm sure you'll get through it fine. diff --git a/content/blog/Your-VPN-is-a-serious-choice.md b/content/blog/Your-VPN-is-a-serious-choice.md @@ -0,0 +1,98 @@ +--- +date: 2019-04-19 +layout: post +title: Choosing a VPN service is a serious decision +tags: ["philosophy", "vpn"] +--- + +There's a disturbing trend in the past year or so of various VPN companies +advertising to the general, non-technical public. It's great that the general +public is starting to become more aware of their privacy online, but I'm not a +fan of these companies exploiting public paranoia to peddle their wares. Using +a VPN in the first place has potentially grave consequences for your privacy - +and can often be worse than not using one in the first place. + +It's true that, generally speaking, when you use a VPN, the websites you visit +don't have access to your original IP address, which can be used to derive your +approximate location (often not more specific than your city or neighborhood). +But that's not true of the VPN provider themselves - who can identify you much +more precisely because you used your VPN login to access the service. +Additionally, they can promise not to siphon off your data and write it down +somewhere - tracking you, selling it to advertisers, handing it over to law +enforcement - but they *could* and you'd be none the wiser. By routing all of +your traffic through a VPN, *you route all of your traffic through a VPN*. + +Another advantage offered by VPNs is that they can prevent your ISP from knowing +what you're doing online. If you don't trust your ISP but you do trust your VPN, +this makes a lot of sense. It also makes sense if you're on an unfamiliar +network, like airport WiFi. However, it's still quite important that you *do* +trust the VPN on the other end. You need to do research. What country are they +based in, and what's their diplomatic relationship with your home country? What +kind of power the local authorities have to force them to record & disclose your +traffic? Are they backed by venture capitalists who expect infinite growth, and +will they eventually have to meet those demands by way of selling your +information to advertisers? What happens to you when their business is going +poorly? How much do you trust their security competency - are they likely to be +hacked? If you haven't answered all of these questions yourself, then you should +not use a VPN. + +Even more alarming than the large advertising campaigns which have been popular +in the past few months is push-button VPN services which are coming +pre-installed on consumer hardware and software. These bother me because they're +implemented by programmers who should understand this stuff and know better than +to write the code. Opera now has a push-button VPN pre-bundled which is free and +tells you little about the service before happily sending all of your traffic +through it. Do you trust a Chinese web browser's free VPN to behave in your +best interests? Purism also recently announced a collaboration with Private +Internet Access to ship a VPN in their upcoming Librem 5. I consider this highly +irresponsible of Purism, and actually discussed the matter at some length with +Todd Weaver (the CEO) over email. We need to stop making it easy for users to +siphon all of their data into the hands of someone they don't know. + +For anyone who needs a VPN but isn't comfortable using one of these companies, +there are other choices. First, consider that any website you visit with HTTPs +support (identified by the little green lock in the address bar on your web +browser) is already encrypting all of your traffic so it cannot be read or +tampered with. This discloses your IP address to the operator of that website +and discloses that you visited that website to your ISP, but does not disclose +any data you sent to them, or any content they sent to you, to your ISP or any +eavesdroppers. If you're careful to use HTTPS (and other forms of SSL for +things like email), that can often be enough.[^1] + +If that's not enough, the ironclad solution is +[Tor](https://www.torproject.org/). When you connect to a website on Tor, it (1) +hides your IP address from the website and any eavesdroppers, (2) hides who +you're talking to from your ISP, and (3) hides what you're talking about from +the ISP. In some cases (onion services), it even hides the origin of the service +you're talking to from *you*. Tor comes with its own set of limitations and +pitfalls for privacy & security, which you should [read about and +understand](https://2019.www.torproject.org/download/download.html.en#Warning) +before using it. Bad actors on the Tor network can read and tamper with your +traffic if you aren't using SSL or Onion routing. + +Finally, if you have some technical know-how, you can set up your own VPN. If +you have a server somewhere (or rent one from a VPS provider), you can install a +VPN on it. I suggest [Wireguard](https://www.wireguard.com/) (easiest, Linux +only) or [OpenVPN](https://openvpn.net) (more difficult, works on everything). +Once again, this comes with its own limitations. You'll always be using a +consistent IP address that services you visit can remember to track you, and you +get a new ISP (whoever your VPS provider uses). This'll generally route you +through commercial ISPs, though, who are much less likely to do obnoxious crap +like injecting ads in webpages or redirecting your failed DNS queries to "search +results" (i.e. more ads). You'll need to vet your VPS provider and their ISP +with equal care. + +Understand who handles your data - encrypted and unencrypted - before you share +it. No matter your approach, you should also always install an adblocker (I +strongly recommend [uBlock +Origin](https://github.com/gorhill/uBlock/#installation)), stick to +HTTPS-enabled websites, and be suspicious of and diligent about every piece of +software, every browser extension, every app you install, and every website you +visit. Most of them are trying to spy on you. + +Related articles: + +- [VPN - a Very Precarious Narrative - Dennis Schubert](https://schub.io/blog/2019/04/08/very-precarious-narrative.html) +- [The trustworthy of VPN review sites and how affiliate programs affects their opinion](https://www.skadligkod.se/vpn/the-trustworthy-of-vpn-review-sites-and-how-affiliate-programs-affects-their-opinion/) + +[^1]: A reader points out that HTTPS can also be tampered with. If someone else administrates your computer (such as your employer), they can install custom certificates that allow them to tamper with your traffic. This is also sometimes done by software you install on your system, like antivirus software (which more times than not, is a virus itself). Additionally, anyone who can strongarm a certificate authority (state actors) may be able to issue an illegitimate certificate for the same purpose. The only communication method I know of which has no known flaws is onion routing on Tor. diff --git a/content/blog/cozy-devnotes-machine-specs.md b/content/blog/cozy-devnotes-machine-specs.md @@ -0,0 +1,296 @@ +--- +date: 2017-02-22 +# vim: tw=80 spell : +title: "Compiler devnotes: Machine specs" +layout: post +tags: [C, language design] +--- + +I have a number of long-term projects that I plan for on long timelines, on the +order of decades or more. One of these projects is cozy, a C toolchain. I +haven't talked about this project in public before, so I'll start by introducing +you to the project. The main C toolchains in the "actually usable" category are +GNU and LLVM, but I'm satisfied with neither and I want to build my own +toolchain. I see no reason why compilers should be deep magic. Here are my goals +for cozy: + +- Self hosting and written in C +- An easy to grok codebase and internal design +- Focused on C. No built-in support for other languages +- Adding new targets architectures and ports should be straightforward +- Modular build pipeline with lots of opportunities for external integrations +- Trivially cross-compiles without building another version of the toolchain +- Includes a decent optimizer + +Some other plans include opinionated warnings about code and minimal support for +language extensions. Ambitious goals, right? That's why this project is on my +long-term schedule. I've found that large projects are entirely feasible, so +long as you (1) start them and (2) keep working on them for a long time. I don't +need to rush this - gcc and clang may not be ideal, but they work today. In +support of these goals, I'll be writing these dev notes to explain my design +choices and gather feedback &mdash; please [email me](mailto:sir@cmpwn.com) if +you have some! + +Since I want to place an emphasis on portability and retargetability, I'm +starting by designing the machine spec and its support code, which is used to +add support for new architectures. I don't like gcc's lisp specs, and I *really* +don't like LLVM's "huge pile of C++" approach. I think a really good machine +spec meets these goals: + +- Easy to write and human friendly +- More about data than code, but +- Easily extended with C to support architecture-specific nuances +- Provides loads of useful metadata about the target architecture +- Exposes information about the speed and side-effects of each instruction +- Can also be used to generate an assembler and disassembler +- Easily reused to create derivative architectures + +Adding a new architecture should be a weekend project, and when you're done the +entire toolchain should both support and run on your new architecture. I set out +to come up with a new syntax that could potentially meet these goals. I started +with the Z80 architecture in mind because it's simple, I'm intimately familiar +with it, and I want cozy to be able to target 8-bit machines just as easily as +32 or 64 bit. + +For reference, here are the gcc and LLVM guides on adding new targets: + +- [gcc - Anatomy of a Target Back End](https://gcc.gnu.org/onlinedocs/gccint/Back-End.html) +- [Writing an LLVM Backend](http://llvm.org/docs/WritingAnLLVMBackend.html) + +The cozy machine spec is a cross between ini files, yaml, and a custom syntax. +The format is somewhat complex, but once understood is intuitive and flexible. +At the top level, it looks like an ini file: + +```yaml +[metadata] +# ... + +[registers] +# ... + +[macros] +# ... + +[instructions] +# ... +``` + +### Metadata + +The **metadata** section contains some high-level information about the +architecture design, and is the simplest section to understand. It currently +looks like this for z80: + +```yaml +[metadata] +name: z80 +bits: 8 +endianness: little +signedness: twos-complement +cache: none +pipeline: none +``` + +This isn't comprehensive, and I'll be adding more metadata as it becomes +necessary. On LLVM, this sort of information is encoded into a string that looks +something like this: `"e-p:16:8:8-i8:8:8-i16:8:8-n8:16"`. This string is passed +into the `LLVMTargetMachine` base constructor in C++. I think we can do a hell +of a lot better than that! + +### Registers + +The **registers** section describes the registers on this architecture. + +```yaml +[registers] +BC: 16 + B: 8 + C: 8; offset=8 +DE: 16 + D: 8 + E: 8; offset=8 +HL: 16 + H: 8 + L: 8; offset=8 +SP: 16; stack +PC: 16; program +``` + +Here we can start to see some interesting syntax and get an idea of the design +of cozy machine specs. The contents of each section are keys, which have values, +attributes, and children. The format looks like this: + +```yaml +key: value; attributes, ... + children... +``` + +In this example, we've defined the BC, DE, HL, SP, and PC registers. HL, DE, and +BC are general purpose 16-bit registers, and each can also be used as two +separate 8-bit registers. The attributes for these sub-registers indicates their +offsets in the parent register. We also define the stack and program registers, +SP and PC, which use the stack and program attributes to indicate their special +purposes. + +We can also describe CPU flags in this section: + +```yaml +[registers] +AF: 16; special + A: 8; accumulator + F: 8; flags, offset 8;; flag + _C: 1 + _N: 1; offset 1 + _PV: 1; offset 2 + _3: 1; offset 3, undocumented + _H: 1; offset 4 + _5: 1; offset 5, undocumented + _Z: 1; offset 6 + _S: 1; offset 7 +``` + +Here we introduce another feature of cozy specs with `F: 8; flags, offset 8;; +flag`. Using `;;` adds those attributes to all children of this key, so each of +\_C, \_N, etc have the `flag` attribute. + +Take note of the "undocumented" attribute here. Some of the metadata included +in a spec can be applied to cozy tools. Some of it, however, is there for other +tools to utilize. We have a good opportunity to make a machine-readable +description of the architecture, so I've opted to include a lot of extra details +in machine specs that third parties could utilize (though there might be a +-fno-undocumented compiler flag some day, I guess). + +### Macros + +The **macros** section is heavily tied to the instructions section. Most instruction +sets are quite large, and I don't want to burden spec authors with writing out +the entire thing. We can speed up their work by providing macros. + +z80 instructions have a few sets of common patterns in their encodings. Register +groups are often represented by the same set of bits, and we can make our +instruction set specification more concise by taking advantage of this. For +example, here's a macro that we can use for instructions that can use either the +BC, DE, HL, or SP registers: + +```yaml +[macros] +reg_BCDEHLSP: + BC: 00 + DE: 01 + HL: 10 + SP: 11 +``` + +We have the name of the macro as the top-level key, in this case `reg_BCDEHLSP`. +We can later refer to this macro with `@reg_BCDEHLSP`. Then, we have each of the +cases it can match on, and the binary values these correspond to when encoded in +an instruction. + +### Instructions + +The instructions section brings everything together and defines the actual +instructions available on this architecture. Instructions can be organized into +groups at the spec author's pleasure, which can be referenced by derivative +architectures. Here we can take a look at the "load" group: + +```yaml +[instructions] +.load: + ld: + @reg_BCDEHLSP, @imm[16]: 00 $1 0001 $2 +``` + +On z80, the `ld` instruction is similar to the `mov` instruction on Intel +architectures. It assigns the second argument to the first. This could be used +to assign registers to each other (e.g. `ld a, b` to set A = B), to set +registers to constants, and so on. Our example here uses our macro from earlier +to match instructions like this: + + ld hl, 0x1234 + +The value for this key may reference the arguments with variables. $1 here +equals `10`, from the macro. The `imm` built-in is implemented in C to match +constants and provides $2. An assembler could use this information to assemble +our example instruction into this machine code: + + 00100001 00110100 00010010 + +Which will load HL with the value 0x1234 when executed. + +### Lots more metadata + +Now that we have the basics down, let's dive into some deeper details. Cozy +specs are designed to provide most of the information the *entire toolchain* +needs to support an architecture. The information we have so far could be used +to generate assemblers and disassemblers, but I want this file to be able to +generate things like optimizers as well. You can add the necessary metadata to +each instruction by utilizing attributes. + +Consider the z80 instruction LDIR, which stands for +"load/decrement/increment/repeat". This instruction is used for memcpy +operations. To use it, you set the HL register to a source address, the DE +register to a destination address, and BC to a length. This instruction looks +like this in the spec: + +```yaml +ldir: 11101101 10110000; uses[HL, DE, BC], \ + affects[HL[+BC], DE[+BC], BC[0]], \ + flags[_H:0,_N:0,_PV:0], cycles[16 + BC * 5] +```` + +That's a lot of attributes! The purpose of these attributes are to give the +toolchain insights into the registers this instruction uses, its side effects, +and how fast it is. These attributes can help us compare the efficiency of +different approaches and understand the how the state of registers evolves +during a function, which leads to all sorts of useful optimizations. + +The `affects` attribute, for example, tells us how each register is affected by +this instruction. We can see that after this instruction, HL and DE will have +had BC added to them, and BC will have been set to 0. We can make all sorts of +optimizations based on this knowledge. Here are some examples: + +```c +char *dest, *src; +int len = 10; +memcpy(dest, src, len); +src += len; +``` + +The compiler can assign `src` to HL, `dest` to DE, and `len` to BC. We can then +optimize out the final statement entirely because we know that the LDIR +instruction will have already added BC to HL for us. + +```c +char *dest, *src; +int len = 10; +memcpy(dest, src, len); +int foobar = 0; +``` + +In this case, the register allocator can just assign BC to `foobar` and avoid +initializing it because we know it's already going to be zero. Many other +optimizations are made possible when we are keeping track of the side effects of +each instruction. + +## Next steps + +I've iterated over this spec design for a while now, and I'm pretty happy with +it. I would love to hear your feedback. Assuming that this looks good, my next +step is writing more specs, and a tool that parses and compiles them to C. These +C files are going to be linked into `libcozyspec`, which will provide an API to +access all of this metadata from C. It will also include an instruction matcher, +which will be utilized by the next step - writing the assembler. + +The assembler is going to take a while, because I don't want to go the gas route +of making a half-baked assembler that's more useful for compiling the C +compiler's output than for anything else. I want to make an assembler that +assembly programmers would *want* to use. + +I have not yet designed an intermediate bytecode for the compiler to use, but +one will have to be made. The machine spec will likely change somewhat to +accommodate this. Some of the conversion from internal bytecode to target +assembly can likely be inferred from metadata, but some will have to be done +manually for each architecture. + +[Here's the entire z80 spec](https://sr.ht/7_Pe.txt) I've been working on, for +your reading pleasure. diff --git a/content/blog/dotfiles.md b/content/blog/dotfiles.md @@ -0,0 +1,95 @@ +--- +date: 2019-12-30 +layout: post +title: Managing my dotfiles as a git repository +--- + +There are many tools for managing your dotfiles - user-specific configuration +files. GNU stow is an example. I've tried a few solutions over the years, but I +settled on a very simple system several years ago which has served me very well +in the time since: my $HOME is a git repository. [This +repository](https://git.sr.ht/~sircmpwn/dotfiles), in fact. This isn't an +original idea, but I'm not sure where I first heard it from either, and I've +extended upon it somewhat since. + +The key to making this work well is my one-byte `.gitignore` file: + +``` +* +``` + +With this line, and git will ignore all of the files in my $HOME directory, so I +needn't worry about leaving personal files, music, videos, other git +repositories, and so on, in my public dotfiles repo. But, in order to track +anything at all, we need to override the gitignore file on a case-by-case basis +with `git add -f`, or `--force`. To add my vimrc, I used the following command: + +``` +git add -f .vimrc +``` + +Then I can commit and push normally, and .vimrc is tracked by git. The gitignore +file does not apply to any files which are already being tracked by git, so any +future changes to my vimrc show up in git status, git diff, etc, and can be +easilly committed with `git commit -a`, or added to the staging area normally +with `git add` &mdash; using `-f` is no longer necessary. Setting up a new +machine is quite easy. After the installation, I run the following commands: + +``` +cd ~ +git init +git remote add origin git@git.sr.ht:~sircmpwn/dotfiles +git fetch +git checkout -f master +``` + +A quick log-out and back in and I feel right at $HOME. Additionally, I have +configured $HOME as a prefix, so that ~/bin is full of binaries, ~/lib has +libraries, and so on; though I continue to use ~/.config rather than ~/etc. I +put $HOME/bin ahead of anything else in my path, which allows me to shadow +system programs with wrapper scripts as necessary. For example, ~/bin/xdg-open +is as follows: + +```sh +#!/bin/sh +case "${1%%:*}" in + http|https|*.pdf) + exec qutebrowser "$1" + ;; + mailto) + exec aerc "$1" + ;; + *) + exec /usr/bin/xdg-open "$@" + ;; +esac +``` + +Replacing the needlessly annoying-to-customize xdg-open with one that just +does what I want, falling back to /usr/bin/xdg-open if necessary. Many other +non-shadowed scripts and programs are found in ~/bin as well. + +However, not all of my computers are configured equally. Some run different +Linux (or non-Linux) distributions, or have different concerns being desktops, +servers, laptops, phones, etc. It's often useful for this reason to be able to +customize my configuration for each host. For example, before $HOME/bin in my +$PATH, I have $HOME/bin/$(hostname). I also run several machines on +different architectures, so I include $HOME/bin/$(uname -m)[^1] as well. To +customize my sway configuration to consider the different device configurations +of each host, I use the following directive in ~/.config/sway/config: + +``` +include ~/.config/sway/`hostname` +``` + +Then I have a host-specific configuration there, also tracked by git so I can +conveniently update one machine from another. I take a similar approach to +per-host configuration for many other pieces of software I use. + +Rather than using (and learning) any specialized tools, I find my needs quite +adequately satisfied by a simple composition of several Unix primitives with a +tool I'm already very familiar with: git. Version controlling your configuration +files is a desirable trait even with other systems, so why not ditch the +middleman? + +[^1]: `uname -m` prints out the system architecture. Try it for yourself, I bet it'll read "x86_64" or maybe "aarch64". diff --git a/content/blog/dotorg.md b/content/blog/dotorg.md @@ -0,0 +1,121 @@ +--- +date: 2019-11-29 +title: Take action to save .org and prosecute those who sold out the internet +layout: post +tags: [activism] +--- + +As many of you have no doubt heard, control of the .org registry has been sold +to private interests. There have been attempts to call them to reason, like +[Save .ORG](https://savedotorg.org/), but let's be realistic: they knew what +they're doing is wrong, the whole time. If they were a commercial entity, our +appeals would fall on deaf ears and that would be the end of it. But, they're +not a commercial entity - so our appeals may fall on deaf ears, but that doesn't +have to be the end of it. + +The level of corruption on display by the three organizations involved in this +scam: ICANN (Internet Corporation for Assigned Names and Numbers), ISOC (The +Internet Society), and PIR (Public Interest Registry), is astounding and very +illegal. If you are not familiar with the matter, click this to read a summary: + +<details> + <summary>Summary of the corrupt privatization of .org</summary> + + <p> + The governance of names on the internet is kind of complicated. ISOC + oversees a lot of activities in internet standards and governance, but their + role in this mess is as the parent company of PIR. PIR is responsible for + the .org registry, which oversees the governance of .org directly and + collects fees for every sale of a .org domain. ICANN is the broader + authority which oversees all domain allocation on the internet, and also + collects a fee for every domain sold. There's a complex web of documents and + procedures which govern these three organizations, and the name system as a + whole, and all three of them were involved in this process. Each of these + organizations is a non-profit, except for PIR, which in the course of this + deal is trying to convert to a B corp. + </p> + + <p> + ICANN can set price limits on the sale of .org domains. In March of 2019, + they proposed removing these price caps entirely. During the period for + public comment, they received 3,300 comments against, and 6 in favor. On May + 13, they removed these price caps anyway. + </p> + + <p> + In November 2019, ISOC announced that they had approved the sale of PIR, the + organization responsible for .org, to Ethos Capital, for an unspecified + amount. According to + <a + href="https://www.internetsociety.org/board-of-trustees/minutes/147" + rel="nofollow noopener" + >the minutes</a>, the decision to approve this sale was unanimously voted on + by the board. Additionally, it seems that Goldman Sachs had been involved in + the sale to some degree. + </p> + + <p> + Fadi Chehadé became the CEO of ICANN in 2012. In 2016, he leaves his + position before it expires to start a consulting company, and he later joins + Abry Partners. One of the 3 partners is Erik Brooks. They later acquire + Donuts, a private company managing domains. Donuts co-founder Jon Nevett + becomes the CEO of PIR in December 2018. On May 7th, Chehadé registers + EthosCapital.com, and on May 13th ICANN decided to remove the price caps + despite 0.2% support from the public. On May 14th, the following day, Ethos + Capital was incorporated, with Brooks as the CEO. In November 2019, ISOC + approved the acquisition of PIR by Ethos Capital, a for-profit company. + </p> + + <p> + These are the names of the criminals who sold the internet. If you want to + read more, <a + href="https://www.privateinternetaccess.com/blog/2019/11/isoc-pir-ethos-capital-deal-timeline/" + rel="noopener" + >Private Internet Access</a> has a good write-up. + </p> + + <p>Okay, now let's talk about what you can do about it.</p> +</details> + +If you are familiar with the .org heist, then like me, you're probably pissed +off. Here's how you can take action: all of these organizations are 501c3 +non-profits. The sale of a non-profit to a for-profit entity like this is +illegal without very specific conditions being met. Additionally, this kind of +behavior is not the sort the IRS likes to see in a tax-exempt organization. +Therefore, we can take the following steps to put a stop to this: + +1. Write to the CA and VA attorney general offices encouraging them to + investigate the misbehavior of these three non-profits, which are + incorporated in their respective states. +2. File form 13909 with the IRS, encouraging them to review the organization's + non-profit status. + +This kind of behavior is illegal. The sale of a non-profit requires a letter +from the Attorneys General in both California (ICANN) and Virginia (ISOC, PIR). +Additionally, much of this behavior qualifies as "self-dealing", or leveraging +one's power within an organization for their own benefit, rather than the +benefit of the organization. To report this, I've prepared a letter to the CA +and VA Attorney's General offices, which you can read here: + +- [Letter to the Attorney General](https://yukari.sr.ht/ag-letter.pdf) + +I encourage you to consider writing a letter of your own, but I would not +recommend copying and pasting this letter. However, this kind of behavior is +also illegal in the eyes of the IRS, and a form is provided for this purpose. +Form 13909 is the appropriate means for reporting this behavior. You can +download a pre-filled form here, and I do encourage you to submit one this +yourself: + +- [Form 13909 for ICANN and ISOC complaints (PDF)](https://yukari.sr.ht/dotorg-form-13909.pdf) +- [Form 13909 for ICANN and ISOC complaints (ODG)](https://yukari.sr.ht/dotorg-form-13909.odg) + +This only includes complaints for ICANN and ISOC, as PIR is seeking to lose its +non-profit status anyway. You can print out the PDF, fill in your details on +both pages, and mail it to the address printed on the form; or you can download +the ODG, open it up with LibreOffice Draw, and fill in the remaining details +digitally, then email it to the address shown on the page.[^1] + +[^1]: Crash course in LibreOffice Draw: press F2, then click and drag to make a new textbox. Select text and use Ctrl+[ to reduce the font size to something reasonable. The red button on the toolbar along the top will export the result as a PDF. + +Happy Thanksgiving! Funny how this all happened right when the American public +would be distracted... diff --git a/content/blog/how-to-fuck-up-releases.md b/content/blog/how-to-fuck-up-releases.md @@ -0,0 +1,73 @@ +--- +date: 2019-10-12 +title: How to fuck up software releases +layout: post +tags: [practices] +--- + +I manage releases for a bunch of free & open-source software. Just about every +time I ship a release, I find a novel way to fuck it up. Enough of these +fuck-ups have accumulated now that I wanted to share some of my mistakes and how +I (try to) prevent them from happening twice. + +At first, I did everything manually. This is fine enough for stuff with simple +release processes - stuff that basically amounts to tagging a commit, pushing +it, and calling it a day. But even this gets tedious, and I'd often make a +mistake when picking the correct version number. So, I wrote a small script: +[semver](https://git.sr.ht/~sircmpwn/dotfiles/tree/master/bin/semver). `semver +patch` bumps the patch version, `semver minor` bumps the minor version, and +`semver major` bumps the major version, based on semantic versioning. I got into +the habit of using this script instead of making the tags manually. The next +fuckup soon presented itself: when preparing the +[shortlog](https://git-scm.com/docs/git-shortlog), I would often feed it the +wrong commits, and the changelog would be messed up. So, I updated the script to +run the appropriate shortlog command and pre-populate the annotated tag with it, +launching the editor to adjust the changelog as necessary. + +Soon I wanted to apply this script to other projects, but not all of them used +semantic versioning. I updated it to work for projects which just use +`major.minor` versions as well. However, another problem arose: some projects +have the version number specified in the Makefile or meson.build. I would +frequently fuck this up in many creative ways: forgetting it entirely; updating +it but not committing it; updating it and committing it, but tagging the wrong +commit; etc. [wlroots](https://github.com/swaywm/wlroots) in particular was +difficult because I also had to update the soversion, which had special +requirements. To address these issues, I added a custom `.git/_incr_version` +script which can add additional logic on a per-repo basis, and updated semver to +call this script if present.[^1] + +Eventually, I went on vacation and shipped a release while I was there. The +`_incr_version` script I had put into `.git` on my home workstation wasn't +checked into version control and didn't come with me on vacation, leading to yet +another fucked up release. I moved it from `.git/_incr_version` to +`contrib/_incr_version`. I made the mistake, however, of leaving the old path in +as a fallback, which meant that I never noticed that *another* project's script +was still in `.git` until I went on another vacation and fucked up another +release. Add a warning which detects if the script is at the old path... + +Some of my projects don't use semantic versioning at all, but still have all of +these other gotchas, so I added an option to just override the automatic version +increment with a user-specified override. For a while, this worked well. But, +inevitably, no matter how much I scripted away my mistakes I would always find a +new and novel way of screwing up. The next one came when I shipped a release +while on an Alpine Linux machine, which ships Busybox instead of GNU tools. +Turns out Busybox gzip produces output which does not match the GNU output, +which means the tarballs I signed locally differed from the ones generated by +Github. Update the signing script to save the tarball to disk (previously, +it lived in a pipe) and upload these alongside the releases...[^2] + +Surely, there are no additional ways to fuck it up at this point. I must have +every base covered, right? Wrong. Dead wrong. On the very next release I +shipped, I mistakenly did everything from a feature branch, and shipped +experimental, incomplete code in a stable release. Update the script to warn if +the master branch isn't checked out... Then, of course, another fuckup: I tagged +a release without pulling first, and when I pushed, git happily rejected my +branch and accepted the tag - shipping an outdated commit as the release. Update +the script to `git pull` first... + +I am doomed to creatively outsmart my tools in releases. If you'd like to save +yourself from some of the mistakes I've made, you can [find my semver script +here](https://git.sr.ht/~sircmpwn/dotfiles/tree/master/bin/semver). + +[^1]: Each of these `_incr_version` scripts proved to have many bugs of their own, of course. +[^2]: Eli Schwartz of Arch Linux also sent a patch to Busybox which made their gzip implementation consistent with GNU's. diff --git a/content/blog/osuweb.md b/content/blog/osuweb.md @@ -0,0 +1,525 @@ +--- +date: 2015-06-14 +# vim: tw=80 +title: osu!web - WebGL & Web Audio +layout: post +tags: [games, osu] +--- + +<script src="/js/underscore-min.js"></script> + +I've taken a liking to a video game called [osu!](https://osu.ppy.sh) over the +past few months. It's a rhythm game where you use move your mouse to circles +that appear with the beat, and click (or press a key) at the right time. It +looks something like this: + +<iframe src="https://www.youtube.com/embed/qdaZnQQAPqQ" frameborder="0" allowfullscreen></iframe> + +The key of this game is that the "beatmaps" (a song plus notes to hit) are +user-submitted. There are thousands of them, and the difficulty curve is very +long - I've been playing for 10 months and I'm only maybe 70% of the way up the +difficulty curve. It's also a competitive game, which leads to a lot more fun, +where each user tries to complete maps a little bit better than everyone else +can. You can see on the left in that video - this is a very good player who +earned the #1 rank during this play. + +In my tendency to start writing code related to every game I play, I've been +working on a cool project called [osu!web](http://www.drewdevault.com/osuweb). +This is a Javascript project that can: + +* Decompress osu beatmap archives +* Decode the music with Web Audio +* Decode the osu! beatmap format +* Play the map! + +In case you don't have any osz files hanging around, try out [this +one](https://sr.ht/f30.osz), which is the one from the video above. + +![](https://sr.ht/044.png) + +## osu!web and the future + +This part of the blog post is for non-technical readers, mostly osu players. +osu!web is pretty cool, and I want to make it even better. My current plans are +just to make it a beatmap viewer, and I'm working now on achieving that goal. I +have to finish sliders and add spinners, and eventually work on things like +storyboards. Playing background videos is not in the cards because of +limitations with HTML5 video. + +Eventually, I'd like to make it possible to link to a certain time in a certain +map, or in a certain replay. Oh yeah, I want to make it support replays, too. +If I get replays working, though, then I don't see any reason not to let players +try the maps out in their web browsers, too. Keep an eye out! + +## Technical Details + +This project is only possible thanks to a whole bunch of new web technologies +that have been stabilizing in the past year or so. The source code is [on +Github](https://github.com/SirCmpwn/osuweb) if you want to check it out. + +### Loading beatmaps + +When the user [drags and +drops](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/scenes/need-files.js#L8-L41) +an osz file, we use [zip.js](https://github.com/gildas-lormeau/zip.js) and +create a virtual filesystem of sorts to browse through the archive. In this +archive we have several things: + +* A number of "tracks" - osu files that define notes and such for various + difficulties +* The song (mp3) and optionally a video background (avi) +* Assets - a background image and optionally a skin (like a Minecraft texture + pack) + +![](https://sr.ht/ce6.png) + +We then load the *.osu files and decode them. They look similar to ini files or +Unix config files. Here's a snippet: + + [General] + AudioFilename: MuryokuP - Sweet Sweet Cendrillon Drug.mp3 + AudioLeadIn: 1000 + PreviewTime: 69853 + + # snip + + [Metadata] + Title:Sweet Sweet Cendrillon Drug + TitleUnicode:Sweet Sweet Cendrillon Drug + Artist:MuryokuP + ArtistUnicode:MuryokuP + Creator:Smoothie + Version:Cendrillon + + # snip + + [HitObjects] + 104,308,1246,5,0,0:0:0:0: + 68,240,1553,1,0,0:0:0:0: + 68,164,1861,1,0,0:0:0:0: + 104,96,2169,1,0,0:0:0:0: + 172,60,2476,2,0,P|256:48|340:60,1,170,0|0,0:0|0:0,0:0:0:0: + 404,104,3399,5,0,0:0:0:0: + + # snip + +This is decoded by +[osu.js](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/osu.js). For +some sections (like `[Metadata]`), it just puts each entry into a dict that you +can pull from later. It does more for things like hit objects, and understands +which of these lines is a slider versus a hit circle versus a spinner and so on. + +I sneakily loaded a beatmap in the background in your browser as you were +reading. If you want to check it out, open up your console and play with the +`track` object. Ignore all the disqus errors, they're irrelevant. + +![](https://sr.ht/a81.png) + +## Enter stage: Web Audio + +Web Audio had a bit of a rocky development cycle, what with Chrome thinking it's +special and implementing a completely different standard from everyone else. +Things have [settled](http://caniuse.com/#feat=audio-api) by now, and I can +start playing with it 😁 Bonus: Mozilla finally added mp3 support to all +platforms, including Linux (which my dev machine runs). + +The osz file includes an mp3, which we +[extract](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/osu.js#L209-L224) +into an ArrayBuffer, and +[load](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/osu-audio.js) +into a Web Audio context. This is super cool and totally would not have been +possible even a few months ago - kudos to the teams implementing all this +exciting stuff in the browsers. + +That's about all we're doing with Web Audio right now. I do add a gain node so +that you can control the volume with your mouse wheel. In the future, we can get +more creative by: + +* Adding support for HT/DT mods +* Adding support for NC + +## Enter stage: PIXI + +Once we've decoded the beatmap and loaded the audio, we can play it. After +briefly showing the user a difficulty selection, we jump into rendering the map. +For this, I've decided to use [PIXI.js](http://pixijs.com/), which gives us a +really nice API to use on top of WebGL with a canvas fallback for when WebGL is +not available. I was originally just using canvas, but it wasn't very +performant, so I went looking for a 2D WebGL framework and found PIXI. It's +pretty cool. + +First, we iterate over all of the hit objects on the beatmap and generate +sprites for them: + +```js +this.populateHit = function(hit) { + // Creates PIXI objects for a given hit + hit.objects = []; + hit.score = -1; + switch (hit.type) { + case "circle": + self.createHitCircle(hit); + break; + case "slider": + self.createSlider(hit); + break; + } +} + +for (var i = 0; i < this.hits.length; i++) { + this.populateHit(this.hits[i]); // Prepare sprites and such +} +``` + +This is all done before we start playing. We consider the timestamp in the music +that the hit is scheduled for, and then we place *all* of the hit objects into +an array and start the song. See code for +[createHitCircle](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/scenes/playback.js#L88-L143), +which puts together a bunch of sprites for each hit cirlce and sets their alpha +to zero. See also +[createSlider](https://github.com/SirCmpwn/osuweb/blob/gh-pages/scripts/scenes/playback.js#L145-L228), +which is more complicated (I'll go into detail later). + +Each frame, we get the current time from the Web Audio layer, and we run a +function that updates a list of upcoming hit objects: + +```js +this.updateUpcoming = function(timestamp) { + // Cache the next ten seconds worth of hit objects + while (current < self.hits.length + && futuremost < timestamp + (10 * TIME_CONSTANT)) { + var hit = self.hits[current++]; + for (var i = hit.objects.length - 1; i >= 0; i--) { + self.game.stage.addChildAt(hit.objects[i], 2); + } + self.upcomingHits.push(hit); + if (hit.time > futuremost) { + futuremost = hit.time; + } + } + for (var i = 0; i < self.upcomingHits.length; i++) { + var hit = self.upcomingHits[i]; + var diff = hit.time - timestamp; + var despawn = NOTE_DESPAWN; + if (hit.type === "slider") { + despawn -= hit.sliderTimeTotal; + } + if (diff < despawn) { + self.upcomingHits.splice(i, 1); + i--; + _.each(hit.objects, function(o) { + self.game.stage.removeChild(o); + o.destroy(); + }); + } + } +} +``` + +I adopted this pattern early on for performance reasons. During each frame's +rendering step, we only have the sprites and such loaded for hit objects in the +near future. This saves a lot of time. PIXI has all of these sprites loaded and +draws them for us each frame. During each frame, all we have to do is update +them: + +```js +this.updateHitObjects = function(time) { + self.updateUpcoming(time); + for (var i = self.upcomingHits.length - 1; i >= 0; i--) { + var hit = self.upcomingHits[i]; + switch (hit.type) { + case "circle": + self.updateHitCircle(hit, time); + break; + case "slider": + self.updateSlider(hit, time); + break; + case "spinner": + //self.updateSpinner(hit, time); // TODO + break; + } + } +} +``` + +This is passed in the current timestamp in the song, and based on this we are +able to do some simple math to calculate how much alpha each note should have, +as well as the scale of the approach circle (which tells you when to click the +note): + +```js +this.updateHitCircle = function(hit, time) { + var diff = hit.time - time; + var alpha = 0; + if (diff <= NOTE_APPEAR && diff > NOTE_FULL_APPEAR) { + alpha = diff / NOTE_APPEAR; + alpha -= 0.5; alpha = -alpha; alpha += 0.5; + } else if (diff <= NOTE_FULL_APPEAR && diff > 0) { + alpha = 1; + } else if (diff > NOTE_DISAPPEAR && diff < 0) { + alpha = diff / NOTE_DISAPPEAR; + alpha -= 0.5; alpha = -alpha; alpha += 0.5; + } + if (diff <= NOTE_APPEAR && diff > 0) { + hit.approach.scale.x = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + hit.approach.scale.y = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + } else { + hit.approach.scale.x = hit.objects[2].scale.y = 1; + } + _.each(hit.objects, function(o) { o.alpha = alpha; }); +} +``` + +I've left out sliders, which again are pretty complicated. We'll get to them +after you look at this screenshot again: + +![](https://sr.ht/044.png) + +All of these hit objects are having their alpha and approach circle scale +adjusted each frame by the above method. Since we're basing this on the +timestamp of the map, a convenient side effect is that we can pass in any time +to see what the map should look like at that time. + +## Curves + +The hardest thing so far has been rendering sliders, which are hit objects that +you're meant to click and hold as you move across the "slider". They look like +this: + +![](https://sr.ht/c97.png) + +The golden circle is the area you need to keep your mouse in if you want to pass +this slider. Sliders are defined as a series of curves. There are a few kinds: + +* Linear sliders (not curves) +* Catmull sliders +* Bezier sliders + +For now I've only done bezier sliders. I give many thanks to +[opsu](https://github.com/itdelatrisu/opsu), which I learned a lot of useful +stuff about sliders from. Each slider is currently generated using the +now-deprecated "peppysliders" method, where the sprite is repeated along the +curve several times. If you look carefully as a slider fades out, you can notice +that this is the case. + +![](https://sr.ht/787.png) + +The newer style of sliders involves rendering them with a custom shader. This +should be possible with PIXI, but I haven't done any research on them yet. +Again, I expect to be able to draw a lot of knowledge from reading the opsu +source code. + +I left out the initializer for sliders earlier, because it's long and +complicated. I'll include it here so you can see how this goes: + +```js +this.createSlider = function(hit) { + var lastFrame = hit.keyframes[hit.keyframes.length - 1]; + var timing = track.timingPoints[0]; + for (var i = 1; i < track.timingPoints.length; i++) { + var t = track.timingPoints[i]; + if (t.offset < hit.time) { + break; + } + timing = t; + } + hit.sliderTime = timing.millisecondsPerBeat * + (hit.pixelLength / track.difficulty.SliderMultiplier) / 100; + hit.sliderTimeTotal = hit.sliderTime * hit.repeat; + // TODO: Other sorts of curves besides LINEAR and BEZIER + // TODO: Something other than shit peppysliders + hit.curve = new LinearBezier(hit, hit.type === SLIDER_LINEAR); + for (var i = 0; i < hit.curve.curve.length; i++) { + var c = hit.curve.curve[i]; + var base = new PIXI.Sprite(Resources["hitcircle.png"]); + base.anchor.x = base.anchor.y = 0.5; + base.x = gfx.xoffset + c.x * gfx.width; + base.y = gfx.yoffset + c.y * gfx.height; + base.alpha = 0; + base.tint = combos[hit.combo % combos.length]; + hit.objects.push(base); + } + self.createHitCircle({ // Far end + time: hit.time, + combo: hit.combo, + index: -1, + x: lastFrame.x, + y: lastFrame.y, + objects: hit.objects + }); + self.createHitCircle(hit); // Near end + // Add follow circle + var follow = hit.follow = + new PIXI.Sprite(Resources["sliderfollowcircle.png"]); + follow.visible = false; + follow.alpha = 0; + follow.anchor.x = follow.anchor.y = 0.5; + follow.manualAlpha = true; + hit.objects.push(follow); + // Add follow ball + var ball = hit.ball = new PIXI.Sprite(Resources["sliderb0.png"]); + ball.visible = false; + ball.alpha = 0; + ball.anchor.x = ball.anchor.y = 0.5; + ball.tint = 0; + ball.manualAlpha = true; + hit.objects.push(ball); + + if (hit.repeat !== 1) { + // Add reverse symbol + var reverse = hit.reverse = + new PIXI.Sprite(Resources["reversearrow.png"]); + reverse.alpha = 0; + reverse.anchor.x = reverse.anchor.y = 0.5; + reverse.x = gfx.xoffset + lastFrame.x * gfx.width; + reverse.y = gfx.yoffset + lastFrame.y * gfx.height; + reverse.scale.x = reverse.scale.y = 0.8; + reverse.tint = 0; + // This makes the arrow point back towards the start of the slider + // TODO: Make it point at the previous keyframe instead + var deltaX = lastFrame.x - hit.x; + var deltaY = lastFrame.y - hit.y; + reverse.rotation = Math.atan2(deltaY, deltaX) + Math.PI; + + hit.objects.push(reverse); + } + if (hit.repeat > 2) { + // Add another reverse symbol + var reverse = hit.reverse_b + = new PIXI.Sprite(Resources["reversearrow.png"]); + reverse.alpha = 0; + reverse.anchor.x = reverse.anchor.y = 0.5; + reverse.x = gfx.xoffset + hit.x * gfx.width; + reverse.y = gfx.yoffset + hit.y * gfx.height; + reverse.scale.x = reverse.scale.y = 0.8; + reverse.tint = 0; + var deltaX = lastFrame.x - hit.x; + var deltaY = lastFrame.y - hit.y; + reverse.rotation = Math.atan2(deltaY, deltaX); + // Only visible when it's the next end to hit: + reverse.visible = false; + + hit.objects.push(reverse); + } +} +``` + +As you can see, there are many more moving pieces here. The important part is +the curve: + +```js +hit.curve = new LinearBezier(hit, hit.type === SLIDER_LINEAR); +for (var i = 0; i < hit.curve.curve.length; i++) { + var c = hit.curve.curve[i]; + var base = new PIXI.Sprite(Resources["hitcircle.png"]); + base.anchor.x = base.anchor.y = 0.5; + base.x = gfx.xoffset + c.x * gfx.width; + base.y = gfx.yoffset + c.y * gfx.height; + base.alpha = 0; + base.tint = combos[hit.combo % combos.length]; + hit.objects.push(base); +} +``` + +In the [curve +code](https://github.com/SirCmpwn/osuweb/tree/gh-pages/scripts/curves), a +series of points along each curve are generated for us to place sprites at. +These are precomputed like all other hit objects to save time during playback. +However, the render updater is still quite complicated: + +```js +this.updateSlider = function(hit, time) { + var diff = hit.time - time; + var alpha = 0; + if (diff <= NOTE_APPEAR && diff > NOTE_FULL_APPEAR) { + // Fade in (before hit) + alpha = diff / NOTE_APPEAR; + alpha -= 0.5; alpha = -alpha; alpha += 0.5; + + hit.approach.scale.x = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + hit.approach.scale.y = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + } else if (diff <= NOTE_FULL_APPEAR && diff > -hit.sliderTimeTotal) { + // During slide + alpha = 1; + } else if (diff > NOTE_DISAPPEAR - hit.sliderTimeTotal && diff < 0) { + // Fade out (after slide) + alpha = diff / (NOTE_DISAPPEAR - hit.sliderTimeTotal); + alpha -= 0.5; alpha = -alpha; alpha += 0.5; + } + + // Update approach circle + if (diff >= 0) { + hit.approach.scale.x = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + hit.approach.scale.y = ((diff / NOTE_APPEAR * 2) + 1) * 0.9; + } else if (diff > NOTE_DISAPPEAR - hit.sliderTimeTotal) { + hit.approach.visible = false; + hit.follow.visible = true; + hit.follow.alpha = 1; + hit.ball.visible = true; + hit.ball.alpha = 1; + + // Update ball and follow circle + var t = -diff / hit.sliderTimeTotal; + var at = hit.curve.pointAt(t); + var at_next = hit.curve.pointAt(t + 0.01); + hit.follow.x = at.x * gfx.width + gfx.xoffset; + hit.follow.y = at.y * gfx.height + gfx.yoffset; + hit.ball.x = at.x * gfx.width + gfx.xoffset; + hit.ball.y = at.y * gfx.height + gfx.yoffset; + var deltaX = at.x - at_next.x; + var deltaY = at.y - at_next.y; + if (at.x !== at_next.x || at.y !== at_next.y) { + hit.ball.rotation = Math.atan2(deltaY, deltaX) + Math.PI; + } + + if (diff > -hit.sliderTimeTotal) { + var index = Math.floor(t * hit.sliderTime * 60 / 1000) % 10; + hit.ball.texture = Resources["sliderb" + index + ".png"]; + } + } + + if (hit.reverse) { + hit.reverse.scale.x = hit.reverse.scale.y = 1 + Math.abs(diff % 300) * 0.001; + } + if (hit.reverse_b) { + hit.reverse_b.scale.x = hit.reverse_b.scale.y = 1 + Math.abs(diff % 300) * 0.001; + } + _.each(hit.objects, function(o) { + if (_.isUndefined(o._manualAlpha)) { + o.alpha = alpha; + } + }); +} +``` + +Much of this is the same as the hit circle updater, since we have a similar hit +circle at the start of the slider that needs to update in a similar fashion. +However, we also have to move the rolling ball and the follow circle along the +slider as the song progresses. This involves calling out to the curve code to +figure out what point is (`current_time / slider_end`) along the length of the +slider. We put the ball there, and we also ask for the point at (`(current_time + +0.01) / slider_end`) and make the ball rotate to face that direction. + +## Conclusions + +That's the bulk of the work neccessary to make an osu renderer. I'll have to add +spinners once I feel like the slider code is complete, and a friend is working +on adding hit sounds (sound effects that play when you correctly hit a note). +The biggest problem he's facing is that Web Audio has no good solution for +low-latency audio playback. On my side of things, though, everything is going +great. PIXI was a really good choice - it's an easy to use API and the WebGL +frontend is fast as hell. osu!web plays a map with performance that compares to +the performance of osu! native. + +<script src="/js/osu.js"></script> + +<script> +var xhr = new XMLHttpRequest(); +xhr.open("GET", "/example.osu"); +xhr.onload = function() { + window.track = new Track(xhr.responseText); + window.track.decode(); +}; +xhr.send(); +</script> + diff --git a/content/blog/pkg-go-dev-sucks.md b/content/blog/pkg-go-dev-sucks.md @@ -0,0 +1,72 @@ +--- +date: 2020-08-01 +layout: post +title: pkg.go.dev is more concerned with Google's interests than good engineering +tags: [go] +--- + +pkg.go.dev sucks. It's certainly *prettier* than godoc.org, but under the +covers, it's a failure of engineering characteristic of the Google approach. + +Go is a *pretty good* programming language. I have long held that this is not +attributable to Google's stewardship, but rather to a small number of language +designers and a clear line of influences which is drawn entirely from outside of +Google &mdash; mostly from Bell Labs. pkg.go.dev provides renewed support for my +argument: it has all the hallmarks of Google crapware and none of the +deliberate, good engineering work that went into Go's design. + +It was apparent from the start that this is what it would be. pkg.go.dev was +launched as a closed-source product, +[justified](https://blog.golang.org/pkg.go.dev-2020) by pointing out that +godoc.org is too complex to run on an intranet, and pkg.go.dev has the same +problem. There are many problems to take apart in this explanation: the +assumption that the only reason an open source platform is desirable is for +running it on your intranet; the unstated assumption that such complexity +is necessary or agreeable in the first place; and the +[systemic](https://github.com/golang/go/issues/25443)&nbsp;[erosion](https://github.com/golang/go/issues/30029) +of the existing (and simple!) tools which *could* have been used for this +purpose prior to this change. The attitude towards open source was only changed +following pkg.go.dev's harsh reception by the community. + +But this attitude *did* change, and it is open-source now[^1] [^2], so let's give +them credit for that. The good intentions are spoilt by the fact that pkg.go.dev +fetches the list of modules from [proxy.golang.org](https://proxy.golang.org/): +a closed-source proxy through which all of your go module fetches are being +routed and tracked (oh, you didn't know? They never told you, after all). +Anyway, enough of the gross disregard for the values of open source and user +privacy; I *do* have some technical problems to talk about. + +[^1]: Setting aside the fact that the production pkg.go.dev site is amended with closed-source patches. +[^2]: The GitHub comment explaining the change of heart included a link to a Google Groups discussion which requires you to log in with a Google account in order to *read*.[^3] If you go the long way around and do some guesswork searching the archives yourself, you [can find it](https://groups.google.com/d/msg/golang-dev/mfiPCtJ1BGU/ibeimu3WEgAJ) without logging in. +[^3]: Commenting on Go patches also requires a Google account, by the way. + +One concern comes from a blatant failure to comprehend the fundamentally +decentralized nature of git hosting. Thankfully, git.sr.ht is supported now[^4] +&mdash; but only *the* git.sr.ht, i.e. the hosted instance, not the software. +pkg.go.dev hard-codes a list of centralized git hosting services, and completely +disregards the idea of git hosting as *software* rather than as a *platform*. +Any GitLab instance other than gitlab.com (such as +[gitlab.freedesktop.org](https://gitlab.freedesktop.org) or +[salsa.debian.org](https://salsa.debian.org/public)); any +[Gogs](https://gogs.io/) or [Gitea](https://gitea.io/en-us/) like +[Codeberg](https://codeberg.org); cgit instances like +[git.kernel.org](https://git.kernel.org/); none of these are going to work +unless every host is added and the list is kept up-to-date manually. Your +intranet instance of cgit? Not a chance. + +[^4]: But not hg.sr.ht! + +They were also given an opportunity here to fix a long-standing problem with Go +package discovery, namely that it requires every downstream git repository host +has to (1) provide a web interface and (2) include *Go-specific* meta tags in +the HTML. The hubris to impose your *programming language*'s needs onto a +language-agnostic version control system! I asked: they have no interest in the +better-engineered &mdash; but more worksome &mdash; approach of pursing a +language agnostic design. + +The worldview of the developers is whack, the new site introduces dozens of +regressions, and all it really improves upon is the visual style &mdash; which +could trivially have been done to godoc.org. The goal is shipping a shiny new +product&nbsp;&mdash; not engineering a good solution. This is typical of Google's +engineering ethos in general. pkg.go.dev sucks, and is added the large (and +growing) body of evidence that Google is bad for Go. diff --git a/content/blog/scdoc.md b/content/blog/scdoc.md @@ -0,0 +1,51 @@ +--- +date: 2018-05-13 +layout: post +title: Introducing scdoc, a man page generator +tags: [scdoc, announcement] +--- + +A man page generator is one of those tools that I've said I would write for a +long time, being displeased with most of the other options. For a while I used +asciidoc, but was never fond of it. There are a few things I want to see in a +man page generator: + +1. A syntax which is easy to read and write +2. Small and with minimal dependencies +3. Designed with man pages as a first-class target + +All of the existing tools failed some of these criteria. +[asciidoc](http://asciidoc.org/) hits #1, but fails #2 and #3 by being written +in XSLT+Python and targetting man pages as a second-class citizen. +[mdocml](http://mandoc.bsd.lv/) fails #1 (it's not much better than writing raw +roff), and to a lesser extent also fails criteria #2[^1]. Another option, +[ronn](https://github.com/rtomayko/ronn) meets criteria #1 and #3, but it's +written in Ruby and fails #2. All of these are fine for the niches they fill, +but not what I'm looking for. And as for GNU info... ugh. + +[![](https://sr.ht/nemf.png)](https://xkcd.com/912/) + +So, after tolerating less-than-optimal tools for too long, I eventually wrote +the man page generator I'd been promising for years: +[scdoc](https://git.sr.ht/~sircmpwn/scdoc). In a nutshell, scdoc is a man page +generator that: + +- Has an easy to read and write syntax. It's inspired by Markdown, but + importantly it's not *actually* Markdown, because Markdown is designed for + HTML and not man pages. +- Is less than 1,000 lines of POSIX.1 C99 code with no dependencies and weighs + 78 KiB statically linked against musl libc. +- Only supports generating man pages. You can post-process the roff output if + you want it converted to something else (e.g. html). + +I recently migrated [sway's manual](https://github.com/swaywm/sway/pull/1958) to +scdoc after adding support for generating tables to it (a feature from asciidoc +that the sway manual took advantage of). This change also removes a blocker to +localizing man pages - something that would have been needlessly difficult to do +with asciidoc. Of course, scdoc has full support for UTF-8. + +My goal was to make a man page generator that had no more dependencies than man +itself and would be a no-brainer for projects to use to make their manual more +maintainable. Please give it a try! + +[^1]: mdocml is small and has minimal dependencies, but it has *runtime* dependencies - you need it installed to read the man pages it generates. This is Bad. diff --git a/content/blog/sourcehut-design.md b/content/blog/sourcehut-design.md @@ -0,0 +1,79 @@ +--- +date: 2019-03-04 +layout: post +title: Sourcehut's spartan approach to web design +tags: ["sourcehut", "philosophy"] +--- + +[Sourcehut](https://sourcehut.org) is known for its brutalist design, with its +mostly shades-of-gray appearance, conservative color usage, and minimal +distractions throughout. This article aims to share some insights into the +philosophy that guides this design, both for the curious reader and for the +new contributor to the open-source project. + +The most important principle is that sr.ht is an engineering tool first and +foremost, and when you're there it's probably because you're in engineering +mode. Therefore, it's important to bring the information you're there for to the +forefront, and minimize distractions. In practice, this means that the first +thing on any page to grab your attention should be the thing that brought you +there. Consider [the source file view on git.sr.ht][gitsrht-blob]. For +reference, here are similar pages on [GitHub][github-blob] and +[Gitlab][gitlab-blob]. + +[gitsrht-blob]: https://git.sr.ht/~sircmpwn/git.sr.ht/tree/master/gitsrht/service.py +[github-blob]: https://github.com/torvalds/linux/blob/master/init/main.c +[gitlab-blob]: https://gitlab.freedesktop.org/libinput/libinput/blob/master/src/evdev.c + +[![Screenshot of git.sr.ht](https://sr.ht/kkJm.png)][gitsrht-blob] + +<style> +img { + box-shadow: 0 0 2px 2px #888; + max-width: 90%; +} +</style> + +The vast majority of the git.sr.ht page is dedicated to the source code we're +reading here, and it's also where most of the colors are. Your eye is drawn +straight to the content. Any additional information we show on this page is +directly relevant to the content: breadcrumbs to other parts of the tree, file +mode & size, links to other views on this repository. The nav can take you away +from this page, but it's colored a light grey to avoid being distracting and +each link is another engineering tool - no marketing material or fluff. Contrast +with GitHub: a large, dark, attention grabbing navbar with links to direct you +away from the content and towards marketing pages. If you're logged out, you get +a giant sign-up box which pushes the content halfway off the page. Colors here +are also distracting: note the large line of colorful avatars that catches your +eye despite almost certainly being unrelated to your purpose on this page. + +![Screenshot of builds.sr.ht](https://sr.ht/1qdZ.png) + +Colors are used much more conservatively on sourcehut. If you log into +builds.sr.ht and visit the index page, you're greeted with a large blue "submit +manifest" button, and very little color besides. This is probably why you were +here - so it's made obvious and colorful so your eyes can quickly find it and +get on with your work. Other pages are similar: the todo.sr.ht tracker page has +a large form with a blue "submit" button for creating a new ticket, email views +on lists.sr.ht have a large blue "reply to thread" button, and +[man.sr.ht](https://man.sr.ht) has a large green button enticing new users +towards the tutorials. Red is also used throughout for dangerous actions, like +deleting things. Each button also is unambiguous and relies on the text within +itself rather than the text nearby: the git.sr.ht repository deletion page uses +"Delete $reponame", rather than "Continue". + +![Screenshot of repo deletion UI](https://sr.ht/d6Vx.png) + +The last important point in sourcehut's design is the use of icons, or rather +the lack thereof. Icons are used extremely conservatively on sr.ht. Interactive +icons (things you are expected to click) are never shown without text that +clarifies what happens when you click them. Informational icons usually have a +tooltip which explains their meaning, and are quite rare - only used in cases +where real estate limits the use of text. Assigning an icon to every action or +detail is not necessary and would add more distractions to the screen. + +I'm not a particularly skilled UI designer, so keeping it simple like this also +helps to make a reasonably nice UI attainable for an engineer-oriented developer +like me. Adding new pages is generally easy and requires little thought by +applying these basic principles throughout, and the simple design doesn't take +long to execute on. It's not perfect, but I like it and I've received positive +feedback from my users. diff --git a/content/blog/sr.ht-general-availability.md b/content/blog/sr.ht-general-availability.md @@ -0,0 +1,113 @@ +--- +date: 2018-11-15 +layout: post +title: "sr.ht, the hacker's forge, now open for public alpha" +tags: ["sourcehut", "announcement"] +--- + +I'm happy to announce today that I'm opening [sr.ht](https://sr.ht) (pronounced +"sir hat", or any other way you want) to the general public for the remainder of +the alpha period. Though it's missing some of the features which will be +available when it's completed, sr.ht today represents a very capable software +forge which is already serving the needs of many projects in the free & open +source software community. If you're familiar with the project and ready to +register your account, you can head straight to [the sign up +page](https://sr.ht). + +For those who are new, let me explain what makes sr.ht special. It provides many +of the trimmings you're used to from sites like GitHub, Gitlab, BitBucket, and +so on, including git repository hosting, bug tracking software, CI, wikis, and +so on. However, the sr.ht model is different from these projects - where many +forges attempt to replicate GitHub's success with a thinly veiled clone of the +GitHub UI and workflow, sr.ht is fundamentally different in its approach. + +>The sr.ht platform excites me more than any project in recent memory. It’s a +>fresh concept, not a Github wannabe like Gitlab. I always thought that if +>something is going to replace Github it would have to be a paradigm change, and +>I think that’s what we’re seeing here. Drew’s project blends the wisdom of the +>kernel hackers with a tasteful web interface. + +<div style="margin-top: -1rem; margin-bottom: 1rem"><small>&mdash;<a href="https://lobste.rs/s/h1udkf/git_is_already_federated_decentralized#c_smnkic">begriffs on lobste.rs</a></small></div> + +The 500 foot view is that sr.ht is a [100% free and open +source](https://git.sr.ht/~sircmpwn/?search=sr.ht) software forge, with a hosted +version of the services running *at* [sr.ht](https://sr.ht) for your +convenience. Unlike GitHub, which is almost entirely closed source, and Gitlab, +which is mostly open source but with a proprietary premium offering, all of +sr.ht is completely open source, with a copyleft license[^bsd]. You're welcome +to install it on your own hardware, and [detailed +instructions](https://man.sr.ht/installation.md) are available for those who +want to do so. You can also send patches upstream, which are then integrated +into the hosted version. + +[^bsd]: Some components use the 3-clause BSD license. + +sr.ht is special because it's extremely modular and flexible, designed with +interoperability with the rest of the ecosystem in mind. On top of that, sr.ht +is one of the most lightweight websites on the internet, with the average page +weighing less than 10 KiB, with **no tracking** and **no JavaScript**. Each +component - git hosting, continuous integration, etc - is a standalone piece of +software that integrates deeply with the rest of sr.ht *and* with the rest of +the ecosystem outside of sr.ht. For example, you can use builds.sr.ht to compile +your GitHub pull requests, or you can keep your repos on git.sr.ht and host +everything in one place. Unlike GitHub, which favors its own in-house pull +request workflow[^github-prs], sr.ht embraces and improves upon the email-based +workflow favored by git itself, along with many of the more hacker-oriented +projects around the net. I've put a lot of work into making this powerful +workflow more [accessible and +comprehensible](https://man.sr.ht/git.sr.ht/send-email.md) to the average +hacker. + +[^github-prs]: A model that many have replicated in their own GitHub alternatives. + +The flagship product from sr.ht is its continuous integration platform, +builds.sr.ht, which is easily the most capable continuous integration system +available today. It's so powerful that I've been working with multiple Linux +distributions on bringing them onboard because it's the only platform which can +scale to the automation needs of an entire Linux distribution. It's so powerful +that I've been working with maintainers of *non-Linux* operating systems, from +BSD to even Hurd, because it's the only platform which can even consider +supporting their needs. Smaller users are loving it, too, many of whom are +jumping ship from Travis and Jenkins in favor of the simplicity and power of +builds.sr.ht. + +On builds.sr.ht, simple YAML-based [build +manifests](https://man.sr.ht/builds.sr.ht/#build-manifests), similar to those +you see on other platforms, are used to describe your builds. You can submit +these through the web, the API, or various integrations. Within seconds, a +virtual machine is booted with KVM, your build environment is sent to it, and +your scripts start running. A diverse set of base images are supported on a +variety of architectures, soon to include the first hardware-backed RISC-V +cycles available to the general public. builds.sr.ht is used to automate +everything from the deployment of this Jekyll-based blog, testing GitHub pull +requests for [sway](https://swaywm.org), building and testing packages for +[postmarketOS](https://postmarketos.org/), and deploying complex applications +like builds.sr.ht itself. Our base images [build, test, and deploy +themselves](https://builds.sr.ht/~sircmpwn/alpine/edge) every day. + +The lists.sr.ht service is another important part of sr.ht, and a large part of +how sr.ht embraces the model used by major projects like Linux, Postgresql, git +itself, and many more. lists.sr.ht finally modernizes mailing lists, with a +powerful and elegant web interface for hacking on and talking about your +projects. Take a look at the [sr.ht-dev][sr.ht-dev] list to see patches +developed for sr.ht itself. Another good read is the [mrsh-dev][mrsh-dev] list, +used for development on the [mrsh][mrsh] project, or my own [public +inbox][public-inbox], where I take comments for this blog and grab-bag +discussions for my smaller projects. + +[sr.ht-dev]: https://lists.sr.ht/~sircmpwn/sr.ht-dev +[mrsh-dev]: https://lists.sr.ht/~emersion/mrsh-dev +[mrsh]: https://git.sr.ht/~emersion/mrsh +[public-inbox]: https://lists.sr.ht/~sircmpwn/public-inbox + +I've just scratched the surface, and there's much more for you to discover. You +could look at my [scdoc](https://git.sr.ht/~sircmpwn/scdoc) project to get an +idea of how the git browser looks and feels. You could [browse tickets on my +todo.sr.ht profile](https://todo.sr.ht/~sircmpwn) to get a feel for the bug +tracking software. Or you could check out the [detailed +manual](https://man.sr.ht) on sr.ht's git-powered wiki service. You could also +just [sign up for an account](https://sr.ht)! + +sr.ht isn't complete, but it's maturing fast and I think you'll love it. Give it +a try, and I'm only [an email away](mailto:sir@cmpwn.com) to receive your +feedback. diff --git a/content/blog/wlroots-whitepaper-available.md b/content/blog/wlroots-whitepaper-available.md @@ -0,0 +1,8 @@ +--- +date: 2017-12-28 +title: wlroots whitepaper available +layout: post +_url: https://sr.ht/jAFC.pdf +--- + +[View PDF](https://sr.ht/jAFC.pdf) diff --git a/content/dynlib.html b/content/dynlib.html @@ -0,0 +1,369 @@ +<!doctype html> +<html lang="en"> +<meta charset="utf-8" /> +<title>Dynamic linking</title> +<style> +body { max-width: 720px; margin: 0 auto } +img { display: block; margin: 0 auto } +small { display: block; text-align: center } +th, td { padding-right: 4rem; text-align: left } +</style> +<h1>Dynamic linking</h1> +<h2>Do your installed programs share dynamic libraries?</h2> + +<p> +Findings: <strong>not really</strong> + +<p> +Over half of your libraries are used by fewer than 0.1% of your executables. + +<img src="https://l.sr.ht/PSEG.svg" alt="A plot showing that the number of times a dynamic library is used shows exponential decay" /> +<small>Number of times each dynamic library is required by a program</small> + +<p> +<strong>libs.awk</strong> +<pre> +/\t.*\.so.*/ { + n=split($1, p, "/") + split(p[n], l, ".") + lib=l[1] + if (libs[lib] == "") { + libs[lib] = 0 + } + libs[lib] += 1 +} +END { + for (lib in libs) { + print libs[lib] "\t" lib + } +} +</pre> + +<p> +<strong>Usage</strong> + +<pre> +$ find /usr/bin -type f -executable -print \ + | xargs ldd 2&gt;/dev/null \ + | awk -f libs.awk \ + | sort -rn &gt; results.txt +$ awk '{ print NR "\t" $1 }' &lt; results.txt &gt; nresults.txt +$ gnuplot +gnuplot&gt; plot 'nresults.txt' +</pre> + +<p> +<a href="/dynlib.txt">my results</a> + +<p> +<pre> +$ find /usr/bin -type f -executable -print | wc -l +5688 +$ head -n20 &lt; results.txt +4496 libc +4484 linux-vdso +4483 ld-linux-x86-64 +2654 libm +2301 libdl +2216 libpthread +1419 libgcc_s +1301 libz +1144 libstdc++ +805 liblzma +785 librt +771 libXdmcp +771 libxcb +771 libXau +755 libX11 +703 libpcre +667 libglib-2 +658 libffi +578 libresolv +559 libXext +</pre> + +<h2>Is loading dynamically linked programs faster?</h2> + +<p> +Findings: <strong>definitely not</strong> + +<table> + <thead> + <tr> + <th>Linkage</th> + <th>Avg. startup time</th> + </tr> + </thead> + <tbody> + <tr> + <td>Dynamic</td> + <td style="text-align: right">137263 ns</td> + </tr> + <tr> + <td>Static</td> + <td style="text-align: right">64048 ns</td> + </tr> + </tbody> +</table> + +<p> +<strong>ex.c</strong> +<pre> +#include &lt;stdio.h&gt; +#include &lt;stdlib.h&gt; +#include &lt;time.h&gt; +#include &lt;unistd.h&gt; + +int main(int argc, char *argv[]) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &amp;ts); + fprintf(stdout, "%ld\t", ts.tv_nsec); + fflush(stdout); + if (argc == 1) { + char *args[] = { "", "", NULL }; + execvp(argv[0], args); + } else { + fprintf(stdout, "\n"); + } + return 0; +} +</pre> + +<p> +<strong>test.sh</strong> +<pre> +#!/bin/sh +i=0 +while [ $i -lt 1000 ] +do + ./ex + i=$((i+1)) +done +</pre> + +<p> +<strong>My results</strong> +<pre> +$ musl-gcc -o ex ex.c +$ ./test.sh | awk 'BEGIN { sum = 0 } { sum += $2-$1 } END { print sum / NR }' +137263 +$ musl-gcc -static -o ex ex.c +$ ./test.sh | awk 'BEGIN { sum = 0 } { sum += $2-$1 } END { print sum / NR }' +64048 +</pre> + +<h2>Wouldn't statically linked executables be huge?</h2> + +<p> +Findings: <strong>not really</strong> + +<p> +On average, dynamically linked executables use only 4.6% of the symbols on +offer from their dependencies. A good linker will remove unused symbols. + +<img src="https://l.sr.ht/WzUp.svg" alt="A box plot showing most results are &lt;5%, with outliers evenly distributed up to 100%" /> +<small>% of symbols requested by dynamically linked programs from the libraries that it depends on</small> + +<p> +<strong>nsyms.go</strong> +<pre> +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func main() { + ldd := exec.Command("ldd", os.Args[1]) + rc, err := ldd.StdoutPipe() + if err != nil { + panic(err) + } + ldd.Start() + + var libpaths []string + scan := bufio.NewScanner(rc) + for scan.Scan() { + line := scan.Text()[1:] /* \t */ + sp := strings.Split(line, " ") + var lib string + if strings.Contains(line, "=&gt;") { + lib = sp[2] + } else { + lib = sp[0] + } + if !filepath.IsAbs(lib) { + lib = "/usr/lib/" + lib + } + libpaths = append(libpaths, lib) + } + ldd.Wait() + rc.Close() + + syms := make(map[string]interface{}) + for _, path := range libpaths { + objdump := exec.Command("objdump", "-T", path) + rc, err := objdump.StdoutPipe() + if err != nil { + panic(err) + } + objdump.Start() + scan := bufio.NewScanner(rc) + for i := 0; scan.Scan(); i++ { + if i &lt; 4 { + continue + } + line := scan.Text() + sp := strings.Split(line, " ") + if len(sp) &lt; 5 { + continue + } + sym := sp[len(sp)-1] + syms[sym] = nil + } + objdump.Wait() + rc.Close() + } + + objdump := exec.Command("objdump", "-R", os.Args[1]) + rc, err = objdump.StdoutPipe() + if err != nil { + panic(err) + } + objdump.Start() + used := make(map[string]interface{}) + scan = bufio.NewScanner(rc) + for i := 0; scan.Scan(); i++ { + if i &lt; 5 { + continue + } + sp := strings.Split(scan.Text(), " ") + if len(sp) &lt; 3 { + continue + } + sym := sp[len(sp)-1] + used[sym] = nil + } + objdump.Wait() + rc.Close() + + if len(syms) != 0 &amp;&amp; len(used) != 0 &amp;&amp; len(used) &lt;= len(syms) { + fmt.Printf("%50s\t%d\t%d\t%f\n", os.Args[1], len(syms), len(used), + float64(len(used)) / float64(len(syms))) + } +} +</pre> + +<p> +<strong>Usage</strong> +<pre> +$ find /usr/bin -type f -executable -print | xargs -n1 ./nsyms &gt; results.txt +$ awk '{ n += $4 } END { print n / NR }' &lt; results.txt +</pre> + +<p> +<a href="/nsyms.txt">my results</a> + +<h2>Will security vulnerabilities in libraries that have been statically linked + cause large or unmanagable updates?</h2> + +<p> +Findings: <strong>not really</strong> + +<p> +Not including libc, the only libraries which had "critical" or "high" severity +vulnerabilities in 2019 which affected over 100 binaries on my system were dbus, +gnutls, cairo, libssh2, and curl. 265 binaries were affected by the rest. + +<p> +The total download cost to upgrade all binaries on my system which were affected +by CVEs in 2019 is 3.8 GiB. This is reduced to 1.0 GiB if you eliminate glibc. + +<p> +It is also unknown if any of these vulnerabilities would have been introduced +<em>after</em> the last build date for a given statically linked binary; if so +that binary would not need to be updated. Many vulnerabilities are also limited +to a specific code path or use-case, and binaries which do not invoke that code +path in their dependencies will not be affected. A process to ascertain this +information in the wake of a vulnerability could be automated. + +<p> +<a href="https://lists.archlinux.org/pipermail/arch-security/">arch-security</a> + +<p> +<strong>extractcves.py</strong> +<pre> +import email.utils +import mailbox +import re +import shlex +import time + +pacman_re = re.compile(r'pacman -Syu .*') +severity_re = re.compile(r'Severity: (.*)') + +mbox = mailbox.mbox("arch-security.mbox") +for m in mbox.items(): + m = m[1] + date = m["Date"] + for part in m.walk(): + if part.is_multipart(): + continue + content_type = part.get_content_type() + [charset] = part.get_charsets("utf-8") + if content_type == 'text/plain': + body = part.get_payload(decode=True).decode(charset) + break + pkgs = pacman_re.findall(body) + severity = severity_re.findall(body) + date = email.utils.parsedate(date) + if len(pkgs) == 0 or date is None: + continue + if date[0] &lt;= 2018 or date[0] &gt; 2019: + continue + severity = severity[0] + args = shlex.split(pkgs[0]) + pkg = args[2].split("&gt;=")[0] + print(pkg, severity) +</pre> + +<pre> +$ python3 extractcves.py | grep Critical &gt; cves.txt +$ xargs pacman -Ql &lt; cves.txt | grep \\.so | awk '{print $1}' | sort -u&gt;affected.txt +# Manually remove packages like Firefox, Thunderbird, etc; write remainder.txt +$ xargs pacman -Ql &lt; remainder.txt | grep '/usr/lib/.*.so$' | awk '{ print $2 }' &gt; libs.txt +$ ldd /usr/bin/* &gt;ldd.txt +$ ./scope.sh &lt;libs.txt | sort -nr &gt;sobjects.txt +</pre> + +<p> +<a href="/sobjects.txt">sobjects.txt</a> is a sorted list of shared objects and +the number of executables that link to them. To find the total size of affected +binaries, I ran the following command: + +<pre style="overflow-x: scroll"> +# With libc +$ egrep -la 'libc.so|libm.so|libdl.so|libpthread.so|librt.so|libresolv.so|libdbus-1.so|libgnutls.so|libcairo.so|libutil.so|libssh2.so|libcurl.so|libcairo-gobject.so|libcrypt.so|libspice-server.so|libarchive.so|libSDL2-2.0.so|libmvec.so|libmagic.so|libtextstyle.so|libgettextlib-0.20.2.so|libgettextsrc-0.20.2.so|libMagickWand-7.Q16HDRI.so|libMagickCore-7.Q16HDRI.so|libbfd-2.34.0.so|libpolkit-gobject-1.so|libwebkit2gtk-4.0.so|libjavascriptcoregtk-4.0.so|libpolkit-agent-1.so|libgs.so|libctf.so|libSDL.so|libopcodes-2.34.0.so|libQt5WebEngine.so|libQt5WebEngineCore.so|libctf-nobfd.so|libcairo-script-interpreter.so' /usr/bin/* | xargs wc -c +# Without libc +$ egrep -la 'libdbus-1.so|libgnutls.so|libcairo.so|libssh2.so|libcurl.so|libcairo-gobject.so|libcrypt.so|libspice-server.so|libarchive.so|libSDL2-2.0.so|libmvec.so|libmagic.so|libtextstyle.so|libgettextlib-0.20.2.so|libgettextsrc-0.20.2.so|libMagickWand-7.Q16HDRI.so|libMagickCore-7.Q16HDRI.so|libbfd-2.34.0.so|libpolkit-gobject-1.so|libwebkit2gtk-4.0.so|libjavascriptcoregtk-4.0.so|libpolkit-agent-1.so|libgs.so|libctf.so|libSDL.so|libopcodes-2.34.0.so|libQt5WebEngine.so|libQt5WebEngineCore.so|libctf-nobfd.so|libcairo-script-interpreter.so' /usr/bin/* | xargs wc -c +</pre> + +<h2>Doesn't static linking prevent <abbr title="address space layout randomization, a security technique">ASLR</abbr> from working?</h2> + +<p> +<strong>No</strong>. + +<p> +We've had ASLR for statically linked binaries for some time now. It's called <a href="https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81498">static PIE</a>. + +<h2>Test environment</h2> +<ul> + <li>Arch Linux, up-to-date as of 2020-06-25</li> + <li>2188 packages installed</li> + <li>gcc 10.1.0</li> +</ul> diff --git a/content/ideas.md b/content/ideas.md @@ -0,0 +1,59 @@ +--- +layout: page +title: List of ideas +--- + +This is a list of ideas that I don't have time to work on right now, but I might +work on later or contribute to if someone else started up the project. +Naturally, all of these assume that the result is based on free and open-source +software, uses open standards and protocols, leverages federation if +appropriate, etc. Shoot me a message on [Mastodon](https://cmpwn.com/@sir) if +you want to pick my brain on any of these ideas, or to let me know they exist. + +A **social networking site** which is designed to enrich IRL relationships. +Your instance would probably be run by someone in your neighborhood, and if you +wanted to hang out it'd match you up with people with similar interests and help +you hang out together IRL, be it having drinks at a bar or playing video games +or board games for an evening, or whatever else. Instead of "social networking" +sites which try to keep you stuck staring at their pages for as long as +possible, the prime directive of this site would be to quickly get you off of +your phone and into IRL face-time with other people. + +A marriage of **git and bittorrent**, for the purpose of tracking large blobs. +Like git-lfs, but decentralized. Instead of storing the URL to fetch files from +like git-lfs does, it stores the infohash. git push should block until 100% of +the changes are replicated in the swarm. + +A **vector graphics display** with open hardware, an open standard for driving +it from a PC, and upstream kernel drivers implementing that interface. Could be +CRT-style or a laser+mirror+projector kind of system, or both. Kernel interface +should be fairly general to encourage manufacturers to write drivers for it, and +should be paired with a nice userspace library for driving the ioctls, like DRM +but for vector graphics. Relevant resources: +[Hackaday](https://hackaday.com/2011/11/10/rgb-laser-projector-is-a-jaw-dropping-build/), +[Arduino RTOS](https://github.com/greiman/ChRt). + +A **Wayland port of xscreensaver** based on layer-shell. + +A **web browser engine** which flagrantly disregards modern web standards, +written in C or Rust. No JavaScript, limited CSS. Designed as a library which +others can make GUIs et al out of. Like Servo if they gave a shit about making +anything other than a testbed for rewriting Firefox. + +A new **kernel for z80 calculators**. [KnightOS](https://knightos.org) is far +from the mark in terms of POSIX support and general Unixisms, but it'd probably +be a good start. There's no reason a POSIX-compatible operating system wouldn't +work on these devices. Particularly egregious failures in KnightOS include the +file descriptor design and the lack of a TTY subsystem, though depending on who +you ask that last one isn't so bad. + +A simple replacement for **gas/nasm and binutils**, with less GNU and more +modularity. Ideally should pair well with +[cproc](https://git.sr.ht/~mcf/cproc)/qbe. + +An **ethical engineering index** which defines a set of criteria by which tech +companies are evaluated for ethics and assigns each a score. Factors include +their approach to military contracting, their treatment of their employees, +attitude towards open source, etc. Above a certain rating, companies would be +allowed to post job listings for free (under that level they wouldn't be allowed +to post at all). There's no business model here, ping me for free hosting. diff --git a/content/japanese.md b/content/japanese.md @@ -0,0 +1,288 @@ +--- +layout: page +title: Drew DeVault's Japanese Learning Resources +--- + +**Note**: this is a work in progress. I last updated this page on 2019-11-12. If +that was a while ago, feel free to ping me and ask me to write some more. + +## Table of Contents + +1. [Introduction](#introduction) +1. [Bootstrapping](#bootstrapping) +1. [Anki deck generators](#anki-deck-generators) +1. [Kanji study](#kanji-study) +1. [Vocabulary study](#vocabulary-study) +1. [Grammar study](#grammar-study) +1. [Reading practice](#reading-practice) +1. [Writing practice](#writing-practice) +1. [Listening practice](#listening-practice) +1. [Speaking practice](#speaking-practice) +1. [Conversation skills](#conversations-skills) +1. [List of resources](#list-of-resources) + +## Introduction + +I've been studying Japanese for a few years now, and while I still have a lot to +learn I feel comfortable having conversations in Japanese, without feeling like +the other participant is having to be concious of the complexity of their +language use. Accomplishing that had been my goal from the start; yours may be +different. What follows is what I believe is a reasonably good approach to +self-directed Japanese language study. + +The most important thing to understand is that there is no magic bullet. +Duolingo, Rosetta stone, even attending a Japanese course at a local college - +none of these approaches alone is sufficient to master the language. Instead, +you need to identify the core skills a language learner needs to develop, and +focus on improving them individually. These focus areas are listed in the table +of contents: + +- Kanji +- Vocabulary +- Grammar +- Reading +- Writing +- Speaking +- Listening +- Conversation +- Translation + +You will visit and revisit these thousands of times over the course of your +studies, but they are listed roughly in the order in which you will first +encounter them, or at least in the order I approached them. And though they are +all interconnected, they each are completely discrete skills and must be given +your attention directly, lest the sum of your mastery suffer for your +inattention. + +It sounds like a lot of work, but if you internalize these facts then you'll +find it much easier to approach the language than you otherwise might. Let's +say, for example, that you're finding your grammar skills falling behind, and +your listening skills are suffering because you haven't internalized the new +grammatical concepts, but you're fine when reading because you can take your +time. In a traditional classroom setting, you're going to be left behind as the +textbook and your peers plow forward with pre-defined allotments of time to each +of these focus areas. + +However, if you study alone and you're cognisant of these independent areas of +study and the approaches to each that work best for you, you can identify the +areas in which you're weak, how they're affecting your growth in other areas, +and the right strategies for improvement. Boldly try new study strategies for +each of these areas, and boldly drop the approaches which aren't working for +you. By making it a personal journey rather than a cookie-cutter journey, you'll +learn the language much more easily. + +Remember to keep your goals in mind, and set them in the first place. "I want to +learn Japanese" is too broad of a goal. "I want to have conversations with +Japanese friends", "I want to watch anime without subtitles", "I want to read +untranslated light novels" - these are much better goals. Adjust your learning +strategy towards your particular goals. + +## Bootstrapping + +There is a certain level of minimum competence you need to have in order to +utilize language learning tools effectively. You need to do these things first: + +1. Learn how to read and pronounce [Hiragana](https://en.wikipedia.org/wiki/Hiragana) + and [Katakana](https://en.wikipedia.org/wiki/Katakana) +2. Learn the 100 most common [kanji](https://en.wikipedia.org/wiki/Kanji) + +TODO: Generate anki decks for these. If you're reading this TODO and feeling put +out, just search AnkiWeb for these and pick them out, it's not a big deal. + +Learning the Japanese writing system, despite being the worst writing system +I've ever encountered, cannot be avoided. But, do not despair, you can learn the +basics in a few weeks of memorization, and that's enough to move on to bigger +things. Don't worry about being perfect at hiragana and katakana at first - if +you're feeling comfortable with most of them and unsure about just a few, you're +ready to move on and pick up the remainder on the fly. + +Do not use romaji. + +**Do not use romaji.** + +You *must* learn the writing system. + +In more general terms, it's important to devote similar amounts of time to each +area of study while you're early in your learning. Each of these focus areas +reinforces the other, and this is critical for fostering a cohesive +understanding of the fundamentals of the language early on. + +Memorization is going to take up most of your time spent studying Japanese. +Thankfully, there's a great tool for helping you do this: Anki! + +## Anki deck generators + +[Anki](https://apps.ankiweb.net/) is an open source memorization tool which uses +spaced repetition to help you review things you aren't comfortable with, and +avoid wasting your time reviewing things you are comfortable with. There is a +free desktop application, a free Android app, and a paid iOS app which is worth +the price and then some[^1]. There's also a version that runs in your web +browser, and you can sync between all of them. + +[^1]: I don't mean that the iOS one is better than any other. They're all worth the price and then some, but the non-iOS ones are free. + +The Android app is the best one in my experience, because there's no better +time to memorize Japanese words than when you're sitting on the toilet. With +Anki, you can stop covering the Facebook app in fecal matter and learn Kanji +instead.[^2] + +[^2]: I also find Anki useful on trains and cars, on my couch, in waiting rooms, and so on, but like, seriously, toilet time is a gravely unexploited resource for self-improvement. Everyone already uses their phones there anyway, don't pretend you don't. + +Because both I'm a nerd and well enough into my Japanese studies to have a +pretty good idea of how all of this works, I've written some tools for +*generating* Anki decks. [You can get the source +here](https://git.sr.ht/~sircmpwn/ddevlang) and use it to generate custom Anki +decks catered to the specific thing you want to study, or you can download one +of the ones I've generated for you. + +### Pre-fab decks + +**Note**: these are experimental. YMMV. + +I've prepared a number of decks using my tools that you can download and get +started with right away. Each deck was generated with linguistic terminology in +Japanese (e.g. "名詞" instead of "noun"), is limited to #common words, and is +ordered by word usage frequency. These are configurable options you can change +if you generate them yourself. + +**General vocabulary** + +- [All common vocabulary words](https://yukari.sr.ht/anki/common-vocab.apkg) +- Counters[^3]: [basic](https://yukari.sr.ht/anki/counter-vocab.apkg) (101 words), [comprehensive](https://yukari.sr.ht/anki/counter-vocab-full.apkg) (232 words) +- Linguistic vocab[^4]: [basic](https://yukari.sr.ht/anki/linguistic-vocab.apkg) (58 words), [comprehensive](https://yukari.sr.ht/anki/linguistic-vocab-full.apkg) (697 words) + +[^3]: Japanese has lots of specific words and kanji for counting things. It's similar to English words like "three heads of lettuce", "two loaves of bread", "ten grains of rice". (thanks /u/martindholmes for the analogy) +[^4]: This is useful for being able to ask questions about the language, in the language. If you were worried about the pre-generated decks using Japanese linguistic terminology, this is the deck for you. + +**Specialized vocabulary** + +I generate these on an as-needed basis, as I personally want to study specific +kinds of vocabulary. + +- [Astronomy vocabulary](https://yukari.sr.ht/anki/astronomy.apkg) +- [Computer vocabulary](https://yukari.sr.ht/anki/comp.apkg) +- [Music vocabulary](https://yukari.sr.ht/anki/music.apkg) + +**JLPT vocabulary** + +[JLPT](https://www.jlpt.jp/e/) is the standard Japanese language proficiency +test, often included in immigration and job requirements in Japan. + +Each JLPT category in separate decks: + +[JLPT N5 (easiest)](https://yukari.sr.ht/anki/JLPT-N5.apkg) &mdash; +[JLPT N4](https://yukari.sr.ht/anki/JLPT-N4.apkg) &mdash; +[JLPT N3](https://yukari.sr.ht/anki/JLPT-N3.apkg) &mdash; +[JLPT N2](https://yukari.sr.ht/anki/JLPT-N2.apkg) &mdash; +[JLPT N1 (hardest)](https://yukari.sr.ht/anki/JLPT-N1.apkg) + +Cumulative decks, including vocab from each prior level: + +[JLPT N4-N5](https://yukari.sr.ht/anki/JLPT-N4-5.apkg) &mdash; +[JLPT N3-N5](https://yukari.sr.ht/anki/JLPT-N3-5.apkg) &mdash; +[JLPT N2-N5](https://yukari.sr.ht/anki/JLPT-N2-5.apkg) &mdash; +[JLPT N1-N5](https://yukari.sr.ht/anki/JLPT-N1-5.apkg) + +**Note**: these are generated by searching '#common' on jisho.org and filtering +to the JLPT categories you asked for. Don't blame me if you fail JLPT because +you used these. Caveat emptor. + +## Kanji study + +Kanji is a bitch. If it makes you feel better, though, it's difficult for native +Japanese speakers, too. In short, you have to learn thousands of discrete +characters, and multiple pronunciations of each, through rote memorization +alone, and alone your skills with kanji will only ever be tangentially +applicable to practical skills like reading or writing. With repeated study and +the application of a suitable amount of time, kanji can be conquered fairly +easily. The application of time is the hard part. + +Personally, I found diminishing returns with studying kanji specifically after I +learned 300-400 of them. After that, I switched to studying vocabulary and +learning the kanji on the way. I suggest you take a similar approach if you want +to reach functional fluency faster, and study kanji in particular only if it +interests you. If you want to write Japanese by hand, you will have to study +kanji directly, but it's much easier to use an IME to type in Japanese on your +computer or phone if that's acceptable to you. + +I suggest studying kanji in [RTK +order](https://git.sr.ht/~sircmpwn/ddevlang/tree/master/rtk.py), which organizes +them by complexity, teaching you the building blocks of more complex kanji +early on. On the subject of stroke order: learn it for your first 100 +characters, to get a feel for the general logic behind it. Then stop, unless you +want to learn to write by hand. There are two useful Anki decks for Kanji study: + +- [All Kanji in RTK order](https://ankiweb.net/shared/info/1862058740) +- [Writing practice deck](https://ankiweb.net/shared/info/1741808368) + +## Vocabulary study + +If you can read, write, and pronounce hiragana and katakana, and have learned to +recognize at least enough kanji to understand the basics of radicals and +pronunciation (that is, you know what 音読み, 訓読み, and 熟語 are), you should +start studying vocabulary. Pick up my [common vocab deck](#anki-deck-generators) +and get started. I also recommend the +[linguistic vocabulary](#anki-deck-generators) decks as well, so that you can +learn the vocab necessary to ask questions about the language, using the +language. Another good vocabulary deck is [this one for complementing the Tae +Kim vocab guide](https://ankiweb.net/shared/info/424465799), which I'll discuss +more in the next section. + +It's important to integrate your vocabulary learning. Practice by talking to +yourself (or your cat) in Japanese, and reading Japanese materials, even if you +can't understand them - just look for words you recognize and see if you can +remember the pronunciation and meaning. Each of the skills I've separated here +are studied separately, but reinforce each other. Vocabulary boils down to rote +memorization, which is the most difficult kind of study, so it's especially +important to reinforce it with your other skills. + +## Grammar study + +[Tae Kim's grammar guide](http://www.guidetojapanese.org/learn/) is the single +best resource for studying Japanese grammar. + +I highly recommend studying the [Tae Kim vocabulary flash +cards](https://ankiweb.net/shared/info/424465799) while you're working through +the guide. It's helpful to be able to read the example texts without having to +scroll back and forth between them and the cheat sheets in every article. + +TODO: Expand on this section + +## Reading practice + +TODO + +## Writing practice + +TODO + +## Listening practice + +TODO + +## Speaking practice + +TODO + +## Conversation skills + +TODO + +## List of resources + +- [Jisho.org](https://jisho.org): best Japanese/English dictionary +- [Anki](https://apps.ankiweb.net/): open source memorization app +- [Tae Kim's free grammar guide](http://www.guidetojapanese.org/learn/grammar) +- [Italki](https://www.italki.com/): reasonably priced 1-on-1 video tutors +- [Japanese language learning games](https://crossword-solver.io/japanese-language-practice-games/), thanks Hailey! +- The "Read Real Japanese" books (thanks Dara!) +- There's others, probably + +**TODO** + +- Similar page on American Sign Language? +- Get better at Japanese Sign Language +- Get better at Mandarin +- Get better at Korean +- Get better at Spanish +- Make a conlang diff --git a/content/make-a-blog.md b/content/make-a-blog.md @@ -0,0 +1,105 @@ +--- +layout: page +title: You should make a blog! +--- + +I would like to see more people writing about their passions and thoughts in a +permanent and public medium. You should make a blog! + +**This is an open offer to pay anyone $20 for starting a tech blog and writing +their first article.** If you write another 3 articles within 6 months, I'll +give you another $20. You can also choose to have me donate your reward to one +of the following charitable organizations: + +- [Electronic Frontier Foundation](https://www.eff.org/) +- [Free Software Foundation](https://www.fsf.org/) +- [Software Freedom Conservancy](https://sfconservancy.org/) + +<!-- +[Send me an email](mailto:sir@cmpwn.com) to claim your reward, with a link to +your blog and PayPal information (or the charitable organization of your +choice). +--> + +~~Send me an email~~ to claim your reward, with a link to +your blog and PayPal information (or the charitable organization of your +choice). + +<!-- +The remaining budget for this blog will accomodate **one** new blog, first come +first served. +--> + +**This offer is closed**, as the budget has been expended. Keep an eye out, it +will likely reopen later. + +**Details** + +- This offer is only available to people who do not already have a blog. +- You must publish on an open-source platform. Medium is not allowed. If you + want a hosted platform, I recommend [write.as](https://write.as). If you're + technical, you could build your own blog with [Jekyll](https://jekyllrb.com/) + or [Hugo](https://gohugo.io/). GitHub offers [free + hosting](https://pages.github.com/) for Jekyll-based blogs. +- There's no minimum word count, but put the effort in. Use exactly as many + words are as necessary to make a substantive point, no fewer and no more. + +Join the [Free Writers Club](https://lists.sr.ht/~sircmpwn/free-writers-club) +mailing list for updates, reminders to keep writing, and a forum for supporting +each other's work. + +## Read these blogs + +Here's a list of blogs which have taken me up on this offer, roughly sorted from +most to least active: + +- [gokigen](https://write.as/gokigen/) +- [Neil Panchal](https://neil.computer/) +- [Juraj Oravec](https://sgorava.github.io/) +- [Alexander Pluhar](https://www.alexander-pluhar.de/) +- [L. McNulty's blog](https://lmcnulty.gitlab.io/blog/index.html) +- [Lauro Silva](https://laurosilva.com/) +- [berfr blog](https://berfr.me/) +- [Nils Schulte's blog](https://nilsschulte.de/posts/) +- [Nix Adventures](http://nixing.mx/blog/blog.html) +- [Stefan Schick's personal Blog](https://stefanschick.eu/) +- [Limero](https://limero.se/) +- [Hallau World](https://hallau.world) +- [brett.icu](https://brett.icu/) +- [DS log](https://sidhion.com/blog/) +- [Colton's Blog](https://wi.zard.work/) +- [(pixel)desu.blog](https://desu.blog/) +- [Todd Davies' blog](https://todddavies.co.uk/blog/) +- [Daniel Playfair Cal’s Blog](https://www.danielplayfaircal.com/) +- [xy2_](https://xy2.dev/) +- [buffer = NULL;](https://nullbuffer.com/) +- [Thomas Dagenais' Blog \| i64.dev](https://i64.dev/) +- [papatutuwawa@home:~$](https://blog.polynom.me/) +- [kindly](https://pensinspace.net/kindly/) +- [\[esotericnonsense %\] ~/blog](https://esotericnonsense.com/blog/) +- [Un morceau de toile](https://www.libellules.eu/) +- [Rocket Nine Labs](https://rocketnine.space/post/tview-and-you/) +- [Chris Vittal's blog](https://chris.vittal.dev/) +- [Mochiro.moe](https://mochiro.moe/) +- [Tretinha's Lair](http://www.tretinha.com/) +- [Notopygos](https://tilde.town/~notopygos/archive/) +- [Naz's Notes](https://notes.askaoru.com/) +- [WritingGale's Blog](https://lorem.club/~/WritingGalesBlog) +- [Sean Behan — converting coffee into code](https://www.seanbehan.dev/) +- [Christophe Slim](https://slim.page/interests.html) +- [Saura's Blog](https://blog.sasach.work/) +- [Daniel Benjamin Miller](https://dbmiller.org/) +- [Anon's Blog](https://anons.writeas.com/) +- [Thrill Seeker](https://thrillseek-r.net/posts/) +- [zdsfa](https://zdsfa.com/insert/blog/) +- [Axel Svensson](https://axelsvensson.com) +- [Cole on fediverse.blog](https://fediverse.blog/~/Cole) +- [Ohryan on fediverse.blog](https://fediverse.blog/~/Ohyran) + +Now-defunct blogs: + +- ~~depsterr~~ +- ~~Jebikoh~~ +- ~~Tech Monk~~ +- ~~Joel's Blog~~ +- ~~Zack Lofgren~~ diff --git a/content/misc.md b/content/misc.md @@ -0,0 +1,19 @@ +--- +title: Miscellaneous links +layout: page +--- + +# Pages + +- [All posts in one big, unpaginated list](/all) +- [Japanese learning resources](/japanese) +- [Make a blog!](/make-a-blog) (defunct) +- [Videos of people editing text](/editing) +- [List of random ideas](/ideas) +- [New server checklist](/new-server) +- [New sysadmin lecture](/new-sysadmin-lecture) +- [Dynamic linking bad](/dynlib) + +# Talks + +- [The FOSS contributor’s mindset](/talks/foss-contributors-mindset.html) diff --git a/content/new-server.html b/content/new-server.html @@ -0,0 +1,214 @@ +<h1>New Server Checklist</h1> +<label> + <input type="checkbox" /> Set root password +</label> +<label> + <input type="checkbox" /> Generate fresh sshd host keys +</label> +<label> + <input type="checkbox" /> Create admin user +</label> +<label> + <input type="checkbox" /> Add admin to doas group and test doas +</label> +<label> + <input type="checkbox" /> SSH key added to admin's authorized_keys +</label> +<label> + <input type="checkbox" /> Disable root login via ssh +</label> +<label> + <input type="checkbox" /> Disable password login via ssh +</label> +<label> + <input type="checkbox" /> Set hostname +</label> +<label> + <input type="checkbox" /> Run system updates +</label> +<label> + <input type="checkbox" /> Reboot +</label> +<label> + <input type="checkbox" /> Install &amp; test postfix +</label> + +<style> +label { display: block; } +pre { background: #eee; max-width: 720px; padding: 0.5rem; } +</style> + +<h1>doas.conf</h1> + +<pre> +# see doas.conf(5) for configuration details + +# Uncomment to allow group "admin" to become root +# permit :admin +permit nopass :admin +permit nopass deploy cmd apk args upgrade -U +permit nopass deploy cmd service args SERVICE restart +permit nopass acme cmd nginx args -s reload +</pre> + +<h1>acme setup</h1> +<p> +TODO: a package could be made to automate many of these steps +</p> +<label> + <input type="checkbox" /> + <code>doas apk add uacme openssl moreutils</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas useradd -md /var/lib/acme -s /sbin/nologin acme</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas mkdir -p /etc/ssl/uacme/private /var/www/.well-known/acme-challenge</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas chown acme:acme /etc/ssl/uacme /etc/ssl/uacme/private</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas chmod g+rX /etc/ssl/uacme /etc/ssl/uacme/private</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas chown acme:acme /var/www/.well-known/acme-challenge</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas touch /var/log/acme.log</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas chown acme:acme /var/log/acme.log</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas vim /usr/local/bin/acme-update-certs</code> +<pre style="margin-left: 1.5rem">#!/bin/sh -eu +exec &gt;&gt;/var/log/acme.log 2>&1 +date + +stats() { + cert="/etc/ssl/uacme/$1/cert.pem" + if ! [ -e "$cert" ] + then + return + fi + expiration=$(date -d"$(openssl x509 -enddate -noout -in "$cert" \ + | cut -d= -f2)" -D'%b %d %H:%M:%S %Y GMT' +'%s') + printf '# TYPE certificate_expiration gauge\n' + printf '# HELP certificate_expiration Timestamp when SSL certificate will expire\n' + printf 'certificate_expiration{instance="%s"} %s\n' "$1" "$expiration" +} + +acme() { + site=$1 + shift + /usr/bin/uacme -v -h /usr/share/uacme/uacme.sh issue $site $* || true + stats $site | curl --data-binary @- https://push.metrics.sr.ht/metrics/job/$site +} + +acme DOMAIN SUBDOMAIN... +doas nginx -s reload</pre> +</label> + +<label> + <input type="checkbox" /> + <code>doas chmod +x /usr/local/bin/acme-update-certs</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas usermod -aG acme nginx</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas -u acme uacme new sir@cmpwn.com</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas -u acme crontab -e</code> +<pre style="margin-left: 1.5rem">MAILTO=sir@cmpwn.com +0 0 * * * chronic /usr/local/bin/acme-update-certs</pre> + </code> +</label> + +<label> + <input type="checkbox" /> Update nginx configuration + (<code>ssl_certificate{,_key}</code> commented) +</label> + +<label> + <input type="checkbox" /> + <code>doas -u acme /usr/local/bin/acme-update-certs</code> +</label> + +<label> + <input type="checkbox" /> + <code>cat /var/log/acme.log # verify success</code> +</label> + +<label> + <input type="checkbox" /> Update nginx configuration +</label> + +<label> + <input type="checkbox" /> + <code>doas chmod -R g+rX /etc/ssl/uacme /etc/ssl/uacme/private</code> +</label> + +<label> + <input type="checkbox" /> + <code>doas nginx -s reload</code> +</label> + +<label> + <input type="checkbox" /> Verify website has working SSL +</label> + +<h2>nginx config</h2> + +<pre> +server { + listen 80; + listen [::]:80; + server_name DOMAIN; + + location / { + return 302 https://$server_name$request_uri; + } + + location ^~ /.well-known { + root /var/www; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name DOMAIN; + ssl_certificate /etc/ssl/uacme/DOMAIN/cert.pem; + ssl_certificate_key /etc/ssl/uacme/private/DOMAIN/key.pem; + + gzip on; + gzip_types text/css text/html; + + # ... +} +</pre> diff --git a/content/new-sysadmin-lecture.md b/content/new-sysadmin-lecture.md @@ -0,0 +1,39 @@ +--- +title: New sysadmin lecture +--- + +You're a production sysadmin now. That comes with certain responsibilities. In +short: + +1. Respect the user's privacy, and look at only what you must. +2. Think before you type. +3. With great power comes great responsibility. + +Assorted tips: + +- Practice your changes on localhost first. +- Ask for help if you need it. +- Always run your SQL queries in a transaction. +- `SELECT things, you, want FROM x;` is generally better than `SELECT * FROM x;` + when considering the user's privacy. +- Share information on a need-to-know basis, both with people and with + computers. +- Avoid doing things that cannot be undone. + +## Spear Phishing + +Because you now have access to production systems, you may be a target for spear +phishing. A bad actor may target you directly in a social engineering attack in +an attempt to get you to leverage your access to mistakenly compromise the +system. For example, someone may impersonate another admin and ask you to add an +SSH key to a server. You need to be aware of this risk. + +If you receive a request to leverage your access for any reason, double check +the veracity of the request. Is the person on IRC identified with NickServ for +the correct account? Is the email they sent DKIM signed and verified from the +right sender? If in doubt, ask for a secondary form of authentication, such as a +PGP challenge. + +This also applies to normal requests from users - don't let someone impersonate +another user in an attempt to gain access to or manipulate their account. Be +especially careful with requests from users with 2FA enabled. diff --git a/content/pinebook.html b/content/pinebook.html @@ -0,0 +1,190 @@ +<!doctype html> +<html lang="en"> +<meta charset="utf-8" /> +<title>ThinkPad T420 vs Pinebook Pro</title> +<style> +body { + max-width: 640px; + margin: 0 auto; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + border-bottom: 1px solid black; + line-height: 1.5; +} + +td { text-align: center; } + +th:first-child { + font-weight: normal; + text-align: right; +} + +.yes { color: green; font-weight: bold; } +.no { color: red; } +.maybe { color: orange; } + +sup { + color: black; + font-weight: normal; + position: absolute; + font-size: 0.75rem; +} + +p { line-height: 1.3; } +</style> + +<table> + <thead> + <tr> + <td></td> + <th>ThinkPad T420</th> + <th>Pinebook Pro</th> + </tr> + </thead> + <tbody> + <tr> + <th>Upstream kernel</th> + <td class="yes">✓</td> + <td class="no">✗</td> + </tr> + + <tr> + <th>Open BIOS</th> + <td class="yes">✓<sup>1</sup></td> + <td class="yes">✓</td> + </tr> + + <tr> + <th>Sane bootloader</th> + <td class="yes">BIOS</td> + <td class="no">uBoot</td> + </tr> + + <tr> + <th>Open display firmware</th> + <td class="yes">✓</td> + <td class="no">✗</td> + </tr> + + <tr> + <th>Open wireless firmware</th> + <td class="no">✗</td> + <td class="no">✗</td> + </tr> + + <tr> + <th>Wireless kill switch</th> + <td class="yes">✓</td> + <td class="yes">✓</td> + </tr> + + <tr> + <th>Bluetooth 5.0</th> + <td class="no">✗</td> + <td class="yes">✓</td> + </tr> + + <tr> + <th>Storage</th> + <td class="yes">500G</td> + <td class="no">64G</td> + </tr> + + <tr> + <th>Upgradable storage</th> + <td class="yes">✓</td> + <td class="yes">✓</td> + </tr> + + <tr> + <th>RAM</th> + <td class="yes">16G</td> + <td class="no">4G</td> + </tr> + + <tr> + <th>Upgradable memory</th> + <td class="yes">✓</td> + <td class="no">✗</td> + </tr> + + <tr> + <th>Weight</th> + <td class="no">2.2kg</td> + <td class="yes">1.3kg</td> + </tr> + + <tr> + <th>Battery life</th> + <td class="no">3.5-5.8 hours</td> + <td class="yes">6-8 hours</td> + </tr> + + <tr> + <th>Replacable battery</th> + <td class="yes">✓</td> + <td class="maybe">requires screwdriver</td> + </tr> + + <tr> + <th>User-serviceable parts</th> + <td class="yes">✓</td> + <td class="maybe">some</td> + </tr> + + <tr> + <th>HD video playback</th> + <td class="yes">✓</td> + <td class="maybe">good luck</td> + </tr> + + <tr> + <th>Usable web browsing</th> + <td class="yes">✓</td> + <td class="maybe">good luck</td> + </tr> + + <tr> + <th class="bold">Price</th> + <td class="yes">$150 (eBay)</td> + <td class="no">$200 (Pine store)</td> + </tr> + + <tr> + <th class="bold">Release year</th> + <td>2011</td> + <td>2019<sup>2</sup></td> + </tr> + </tbody> +</table> + +<p> +The Pinebook Pro is a decent laptop. It is lightweight, has a good display, and +a good battery life. I am happy with Pine64 as a company and think that it is a +good product. There is a place for it in my life. Pine64 has their heart in the +right place and I support them and buy their products because I want them to +succeed. + +<p> +But, is this the best the modern laptop industry can come up with? As a +workstation, it doesn't compete. The performance of the ARM CPU is absolutely +put to shame by the 8 year old T420, even for basic tasks. Is this 8 years of +advancement in the state of the art? + +<p> +Get your shit together, laptop vendors. Fuck's sake. + +<hr /> + +<p> +1: Aftermarket via <a href="https://doc.coreboot.org/mainboard/lenovo/t420.html">coreboot</a> + +<p> +2: Limited release, full release in 2020 diff --git a/content/talks/foss-contributors-mindset.md b/content/talks/foss-contributors-mindset.md @@ -0,0 +1,35 @@ +--- +layout: page +title: The FOSS contributor's mindset +--- + +**Abstract** + +> Many people use free software, and a fraction of them have dipped their toes +> into contributing - publishing their own projects, or contributing to one or +> two. Too few, however, have completely grokked the opportunities the expansive +> free software ecosystem offers to us. +> +> This talk will discuss how to contribute to a wider variety of projects, +> introduce useful tools for quickly coming to grips with a foreign codebase, +> and explain the mindset that these tools help to cultivate. + +**Resources** + +- [Download slides](/talks/foss-contributors-mindset.pdf) +- [find(1)](https://linux.die.net/man/1/find) +- [git-grep(1)](https://git-scm.com/docs/git-grep) +- [git-blame(1)](https://git-scm.com/docs/git-blame) +- [Alpine aports](https://git.alpinelinux.org/aports/) +- [git-send-email.io](https://git-send-email.io) + +**Events** + +Philly Linux Users Group (PLUG) Central + +2019-11-06 @ 8 PM (PLUG begins at 7 PM) + +University of the Sciences in Philadelphia (USP)<br /> +Griffith Hall (Room "C" or "A", look for the signs)<br /> +600 South 43rd Street<br /> +Philadelphia, PA 19104-4495 diff --git a/content/talks/foss-contributors-mindset.pdf b/content/talks/foss-contributors-mindset.pdf Binary files differ. diff --git a/layouts/_default/single.html b/layouts/_default/single.html @@ -0,0 +1,11 @@ +{{ partial "head.html" . }} + +<h1> + {{$.Title}} +</h1> + +<main> + {{.Content}} +</main> + +{{ partial "foot.html" }} diff --git a/layouts/blog/section.html b/layouts/blog/section.html @@ -0,0 +1,10 @@ +<!doctype html> +<html lang="en"> +<meta charset="utf-8" /> +<title>{{.Title}}</title> +{{ $style := resources.Get "main.scss" | resources.ToCSS | resources.Minify | resources.Fingerprint }} +<link rel="stylesheet" href="{{ $style.RelPermalink }}"> + +<main> + {{.Content}} +</main> diff --git a/layouts/blog/single.html b/layouts/blog/single.html @@ -0,0 +1,27 @@ +{{ partial "head.html" . }} + +<h1> + {{$.Title}} + <small> + <span class="date">{{.Date.Format "January 2, 2006"}}</span> + on + <span class="site"><a href="{{.Site.BaseURL}}">{{.Site.Title}}</a></span> + </small> +</h1> + +<main> + <article> + {{.Content}} + + <section class="comment"> + Have a comment on one of my posts? Start a discussion in my public inbox by + sending an email to + <a href="mailto:~sircmpwn/public-inbox@lists.sr.ht">~sircmpwn/public-inbox@lists.sr.ht</a> + [<a href="https://man.sr.ht/lists.sr.ht/etiquette.md">mailing list etiquette</a>] + </section> + </article> + + {{ partial "webring-out.html" }} +</main> + +{{ partial "foot.html" }} diff --git a/layouts/index.html b/layouts/index.html @@ -0,0 +1,37 @@ +{{ partial "head.html" . }} + +<main class="index"> + <section class="article-list"> + <h1>{{$.Title}}</h1> + + {{ range (where .Site.RegularPages "Section" "blog") }} + <div class="article"> + <span class="date">{{.Date.Format "January 2, 2006"}}</span> + <a href="{{.Permalink}}">{{.Title}}</a> + </div> + {{ end }} + </section> + + <aside> + <img src="https://drewdevault.com/avatar-148.jpg" /> + + <div class="centered"> + <a class="rss" href="/blog/index.xml">rss</a> + · + <a href="misc.html">misc</a> + </div> + + <dl class="external-links"> + <dt>sourcehut</dt> + <dd><a href="https://git.sr.ht/~sircmpwn">~sircmpwn</a></dd> + <dt>mastodon</dt> + <dd><a href="https://cmpwn.com/@sir">@sir@cmpwn.com</a></dd> + <dt>email</dt> + <dd><a href="mailto:sir@cmpwn.com">sir@cmpwn.com</a></dd> + <dt>pgp</dt> + <dd><a href="/publickey.txt">7BC79407090047CA</a></dd> + <dt>donate</dt> + <dd><a href="https://drewdevault.com/donate">fosspay</a></dd> + </dl> + </aside> +</main> diff --git a/layouts/partials/foot.html b/layouts/partials/foot.html @@ -0,0 +1,6 @@ +<footer> + The content for this site is + <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>. + The <a href="https://git.sr.ht/~sircmpwn/drewdevault.com">code for this site</a> + is <a href="https://opensource.org/licenses/MIT">MIT</a>. +</footer> diff --git a/layouts/partials/head.html b/layouts/partials/head.html @@ -0,0 +1,12 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + {{ if $.Params.noindex }} + <meta name="robots" content="noindex"> + {{ end }} + <title>{{$.Title}}</title> + {{ $style := resources.Get "main.scss" | resources.ToCSS | resources.Minify | resources.Fingerprint }} + <link rel="stylesheet" href="{{ $style.RelPermalink }}"> +</head> diff --git a/layouts/partials/webring-in.html b/layouts/partials/webring-in.html @@ -0,0 +1,23 @@ +<section class="webring"> + <h2> + Articles from blogs I read + <small class="attribution"> + Generated by + <a href="https://git.sr.ht/~sircmpwn/openring">openring</a> + </small> + </h2> + <section class="articles"> + {{range .Articles}} + <div class="article"> + <h4 class="title"> + <a href="{{.Link}}" target="_blank" rel="noopener">{{.Title}}</a> + </h4> + <p class="summary">{{.Summary}}</p> + <small class="source"> + via <a href="{{.SourceLink}}">{{.SourceTitle}}</a> + </small> + <small class="date">{{.Date}}</small> + </div> + {{end}} + </section> +</section> diff --git a/layouts/partials/webring-out.html b/layouts/partials/webring-out.html @@ -0,0 +1,73 @@ +<section class="webring"> + <h2> + Articles from blogs I read + <small class="attribution"> + Generated by + <a href="https://git.sr.ht/~sircmpwn/openring">openring</a> + </small> + </h2> + <section class="articles"> + + <div class="article"> + <h4 class="title"> + <a href="https://emersion.fr/blog/2020/status-update-21/" target="_blank" rel="noopener">Status update, August 2020</a> + </h4> + <p class="summary">Hi! Regardless of the intense heat I’ve been exposed to this last month, +I’ve still been able to get some stuff done (although having to move out to +another room which isn’t right under the roof). +I’ve worked a lot on IRC-related projects. I’ve added a znc-i…</p> + <small class="source"> + via <a href="https://emersion.fr/blog/">emersion</a> + </small> + <small class="date">2020-08-19 00:00:00 &#43;0200 &#43;0200</small> + </div> + + <div class="article"> + <h4 class="title"> + <a href="https://sourcehut.org/blog/2020-08-16-whats-cooking-august-2020/" target="_blank" rel="noopener">What&#39;s cooking on Sourcehut? August 2020</a> + </h4> + <p class="summary">Another month passes and we find ourselves writing (or reading) this status +update on a quiet, rainy Sunday morning. Today our userbase numbers 16,683 +members strong, up 580 from last month. Please extend a kind welcome to our new +colleagues! Thanks for read…</p> + <small class="source"> + via <a href="https://sourcehut.org/blog/">Blogs on Sourcehut</a> + </small> + <small class="date">2020-08-16 00:00:00 &#43;0000 &#43;0000</small> + </div> + + <div class="article"> + <h4 class="title"> + <a href="https://blog.golang.org/go1.15" target="_blank" rel="noopener">Go 1.15 is released</a> + </h4> + <p class="summary"> + + + + + Today the Go team is very happy to announce the release of Go 1.15. You can get it from the download page. +Some of the highlights include: + +Substantial improvements to the Go linker +Improved allocation for small objects at high core coun…</p> + <small class="source"> + via <a href="https://blog.golang.org/feed.atom">The Go Programming Language Blog</a> + </small> + <small class="date">2020-08-11 11:00:00 &#43;0000 &#43;0000</small> + </div> + + <div class="article"> + <h4 class="title"> + <a href="https://100r.co/site/north_pacific_logbook.html" target="_blank" rel="noopener">North Pacific Logbook</a> + </h4> + <p class="summary"> +The passage from Japan (Shimoda) to Canada (Victoria) took 51 days, and it was the hardest thing we&#39;ve ever done. We decided to keep a logbook, to better remember it and so it can help others who wish to make this trip.Continue Reading + </p> + <small class="source"> + via <a href="https://100r.co/">Hundred Rabbits</a> + </small> + <small class="date">2020-07-31 00:00:00 &#43;0000 GMT</small> + </div> + + </section> +</section> diff --git a/static/anki/JLPT-N1-5.apkg b/static/anki/JLPT-N1-5.apkg Binary files differ. diff --git a/static/anki/JLPT-N1.apkg b/static/anki/JLPT-N1.apkg Binary files differ. diff --git a/static/anki/JLPT-N2-5.apkg b/static/anki/JLPT-N2-5.apkg Binary files differ. diff --git a/static/anki/JLPT-N2.apkg b/static/anki/JLPT-N2.apkg Binary files differ. diff --git a/static/anki/JLPT-N3-5.apkg b/static/anki/JLPT-N3-5.apkg Binary files differ. diff --git a/static/anki/JLPT-N3.apkg b/static/anki/JLPT-N3.apkg Binary files differ. diff --git a/static/anki/JLPT-N4-5.apkg b/static/anki/JLPT-N4-5.apkg Binary files differ. diff --git a/static/anki/JLPT-N4.apkg b/static/anki/JLPT-N4.apkg Binary files differ. diff --git a/static/anki/JLPT-N5.apkg b/static/anki/JLPT-N5.apkg Binary files differ. diff --git a/static/anki/astronomy.apkg b/static/anki/astronomy.apkg Binary files differ. diff --git a/static/anki/common-vocab.apkg b/static/anki/common-vocab.apkg Binary files differ. diff --git a/static/anki/comp.apkg b/static/anki/comp.apkg Binary files differ. diff --git a/static/anki/counter-vocab-full.apkg b/static/anki/counter-vocab-full.apkg Binary files differ. diff --git a/static/anki/counter-vocab.apkg b/static/anki/counter-vocab.apkg Binary files differ. diff --git a/static/anki/linguistic-vocab-full.apkg b/static/anki/linguistic-vocab-full.apkg Binary files differ. diff --git a/static/anki/linguistic-vocab.apkg b/static/anki/linguistic-vocab.apkg Binary files differ. diff --git a/static/anki/music.apkg b/static/anki/music.apkg Binary files differ. diff --git a/static/avatar-148.jpg b/static/avatar-148.jpg Binary files differ. diff --git a/static/avatar-148.png b/static/avatar-148.png Binary files differ. diff --git a/static/avatar-512.png b/static/avatar-512.png Binary files differ. diff --git a/static/avatar.png b/static/avatar.png Binary files differ. diff --git a/static/video-js.css b/static/video-js.css @@ -0,0 +1,1234 @@ +.video-js .vjs-big-play-button:before, .video-js .vjs-control:before, .video-js .vjs-modal-dialog, .vjs-modal-dialog .vjs-modal-dialog-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + +.video-js .vjs-big-play-button:before, .video-js .vjs-control:before { + text-align: center; } + +@font-face { + font-family: VideoJS; + src: url("../font/1.5.1/VideoJS.eot?#iefix") format("eot"); } + +@font-face { + font-family: VideoJS; + src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAA4wAAoAAAAAFfAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAD4AAABWUZFeBGNtYXAAAAE0AAAAOgAAAUriLxC2Z2x5ZgAAAXAAAAnnAAAO5OV/F/5oZWFkAAALWAAAACsAAAA2C4eUa2hoZWEAAAuEAAAAGAAAACQOogcfaG10eAAAC5wAAAAPAAAAeNIAAABsb2NhAAALrAAAAD4AAAA+MMgtQm1heHAAAAvsAAAAHwAAACABLwB5bmFtZQAADAwAAAElAAACCtXH9aBwb3N0AAANNAAAAPkAAAF5vawAenicY2BkZ2CcwMDKwMFSyPKMgYHhF4RmjmEIZzzHwMDEwMrMgBUEpLmmMDh8ZPwoyw7iLmSHCDOCCADu/Qo9AAB4nGNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZgYGD7K/v8PUvCREUTzM0DVAwEjG8OIBwCOWgbUAAB4nI1XfVBU1xV/574vlsUlj/14grDs48FuAgaR3X2LEnY3UZSgEkTwAySAgkIwI8bRfFDjTszYCWRMW9lNa4y2meokmq+2k5ia0dpkmknbkWgSSW3GyaaNf0RTx0wxX7A3Pe/tQmIgHXf3vXvvueeee+45v3POXQYY/PCD/CBDGAYkIE2sxg+OXSJmhmH1OaFX6MU5C5PDMCZi5Rg2i+ELGSthwM14NCbgYGSBIZfhFA1H6Zu0OS0NDkMVfg+npdFm+maCvigI0JBIQIMg0BdJGdTj9ylj7nr+b97+Hl8C1+H2xNAvjPqxjIgaKtItICkSnIISeo40QQls4xxjlzgHsnGGvi7BxQiMlSlkPMhfCh67rAUEUQ6CHxW2O7JARCkKnlUQ7UEIyAEQZe4MdDW9xr5OPFuKbubpRxcPDY8da4MOelDfAYJLW+sGKn/Vlmjfv5+NdB4oOfTazJn3tGxZtL9xFNZX7PPRUbjcRg/SMB2EL+gblXn7shbO/WUbF9u/H5XQ9eKO8iMMr9tY35qYoRi20wGuXV/CHaGDk2fdgHwCk5HUXQpCcgHfBV2NjV3jkq4PHTSUSBwuOQALvxPAps6fiftk6P6yJpcm5bB4dFkgoh195mbiSTnkL3jupq7jh4ZZdvjQRVB4PPx3SsVTu5D/6kd85RU66ttXAeuuXYN1E/Y2sMMzZkZiZNRZlRS/ynr9Xr8Cql2RVNbutXslYo7B9ngsFqcDbCQO22PxeIxcpgMxkh6PjUdwkvw6hvRpZeoCFKshDQzJVr++DWyLx+hAXJcGp3TJMV1ME45xCNvHLsWRrpOZSduOoG0zERuIIwuIkhNkBREglQKLiODD45FQE0BTiE214xE2wp8zOt9NjH3GRtDMk7Ehoq2tzCzGxdyMEQJuD0qGIrQ58ApoWQE3D2h1h6zwuB14wYFIDAA5CZ11jT+92gFZ7B7/p7+hV8jFxBl4aG03wLiVXtBbCylLfIJzkPUAvWAw0yvsVdKdBbC6nnruP/RFkHqWJLZ2Auxdtgy+6qTf7l1WswTJcJ6mGVxwXj92UtfU2WXUNX+qBUCxK6D4FR4f/cufG1sZbiSkMcwdMdoxBxTTEXIp4SCXMNhHoFjvTTFP4vkoPReNRmPRCTwa+3qY0DR7qn7Vjh612wRRTaI04HWCnZ+gIzvS/ZJP0+mynphCui4hzmG0id6+aLSv2BV3FQMYDTHrlGQ/SZ+q4ZdF8aLa5Ar8GW3tVNKEj13cF0buMaesx1i9CL/Uo1tM0h+74o9HjQ+UcPaxy8mH9ccwK8KpKA3rHdIUjTKpfIBxuokpxUGBIILm84ATvHh8tAIe2iZj8KvYwUOXawHMVNgxZvlwSa0z8Zkokkxn3ey2nYTsbMO3mPh8cji7zklsPLD9a9f2s2w/uSt/FgSytWzw5bmS3PielU1P56aGrlz6NzlnbT8h/Wtb+1OxIqxBbC9g7kINUbtAEDxsKWSCe46eltCPmaiUxy2IrODIB8EmixaQrU4IAQ6THg6BFpAdWsCquT16DkL9ccIC/FGeP5AuiDExe8bx+QtzWVsmHcm0kdzqecdn5IhRkTc/zfNPm3ns5sw4Pq86l9gyofh6jkTF5iFChjYbbzZQWFvYb8qZAWyGiV9ya+5bFgnzpuWt3FuX8KYMmsiYZepPseBgGhZcOMt0+4Q8fDOTftJjHIuhdaLsFXFM9AclTi9jbGRq8ZvIOykZei77kfo53eoppVPovbGiyV63p/p/dkWETTjmhjTIm8RP284b04bcNYlRsvO6Gp2JeaiIueVHsgJGF2aASlCQLuG8EsBomzb++/AXmwhaOoLhL7iQ4/uc449gWJ56/XWDARn74v/PL1bRBB4TBEyYrqezSkUPHaWjPWCm13ogAzJ66LVpbTEuXccDZlyXxBQ/IrzKOPS7gAkkIyZ0N6joE6M246aDsO1kgucTJ/EdFWA5pbAcTfoSP4hJeBCni7nEn5IclL4kpDgmMMuH8Kpk0+WrBUIeKCyWS0nPVz7NW86Hnl55GxR5KB3+9tszL+wVRulXNTUn6D8SJvIl3PzP46eZST/tQTllTDXTzmxCaTYna7eJAqcWuD1ulBXQsMz5fQEBCfowCF5FVDF/2yysB9OW5veVEtRAFOy41FoeJEiAOZhDiFstsKAwJ8Hijs72q1jWvWx+uKU5XFZDLx189OK8ojW1u0By5dtLHUN/rwkte68PnhnYVbt0bvWiub9w1+f4C0L3hIuXZ8+xlVSt0eb3tgQsmVZnem5R3U0uf/fmFdqiLTvY3nPnet5/v4f9pLB6QX2krnnFQ1tXtN+2ePlAaUNWcfiWwrncn4ca9ml3hFeHHm+u2bq4MhxUZs3bMH/3jgaPUtlVunFjg2/8yRzf3cHsssKZqlnOqyCWworWykW9lXnspk0ffrjpfCreIpjPWbwnFxt3PAkcQgkUuH1auUMf+txJQ0hK1k1zsNaqQdaLMxfoq9AGGxtJQ+fGw53cE/TY8pWhJruZHiMAcCexFS/eGDp6hntiXGE/gvI7163b29ExfiHxNsnqub/a6/QmPoAn4GpZ2c9cZRX5/57IWUNYuubiQBAddhuxAKe6PA5vuV5dkk0VXkMM3zk42W3Awrgka8LQgjZY+tQIffd5+vnHasnHL/cczldyS4r79i6su6Nu9oPQ8lbaid2Pt9/bXtTTynevq7bkPkITV47d+3NugOzo4M3y77Zxbnb2nhWrl0T/kO4u3H1ig33e1lD6JDYjiKkCHOioF0pZv6T6gxxipxLNhFc8xERA48vq5ZfXdL/QV6c8W3PfwjIsZyI3Csvo72e4FpTVwTv/UYNAKtY+8MB84vogZ1Xr5lW38iJdPZ74xunzO4Gk7BARIkytjlyCoPVoIb3IluMfAYRhEoAO2aGXKc2TNAJaSwdzQEeq7jC7TWYF2Y2jrEIXlyVEhunBs5t7K62a7Z6qB0923/+vPT2v7mwpqV/mTEsTiCB5zz735HOP9VbVWtKKZK08uDJ7vcQN02HogGegY5iNnKUHh12ti9/zzHvsauy+tx+e375j94LuA64MV/5MQbZVNT95/re7jlxZVaVuW5Nffsd9TXfOpXcv6m2Bn3x6FgXg/oz+P0h/ce8g2mTEWxVTzzQzrTruNCcRdbu6VY87gLVXc4uSjXfosak7XxWM4oyl+ockmzCFhJXaGwK8e6sCW2T3sLmPnh5qSZtx9JHFL6QBHGnsTjdtWQ8PFygWtQTIkrI84NILfQSC65FUMFsnOYFHEoSmUCD49a4rt3985PTsd8GzB/5KEnzmhhORgVOZPM+yb5KmpRu38jQqviH6826Lrdrxx6DZdFPo2fVbTiy9AUpDJ3SxGYvpK7u+Rhz8D4BCxssAeJxjYGRgYABi/vcdWfH8Nl8ZuNkZQODSliXbkWl2BrA4BwMTiAIAKDsJfgB4nGNgZGBgZwCChWASxGZkQAVyABOTANd4nGNnYGBgHwAMADNUANMAAAAAAAAOAFAAZgCyAMYA5gEeAUgBdAGcAfICLgKOAroDCgOOA7AD6gQ4BHwEuAToBQwFogXoBjYGbAbaB3IAAHicY2BkYGCQY8hlYGcAASYg5gJCBob/YD4DABa6AakAeJxdkE1qg0AYhl8Tk9AIoVDaVSmzahcF87PMARLIMoFAl0ZHY1BHdBJIT9AT9AQ9RQ9Qeqy+yteNMzDzfM+88w0K4BY/cNAMB6N2bUaPPBLukybCLvleeAAPj8JD+hfhMV7hC3u4wxs7OO4NzQSZcI/8Ltwnfwi75E/hAR7wJTyk/xYeY49fYQ/PztM+jbTZ7LY6OWdBJdX/pqs6NYWa+zMxa13oKrA6Uoerqi/JwtpYxZXJ1coUVmeZUWVlTjq0/tHacjmdxuL90OR8O0UEDYMNdtiSEpz5XQGqzlm30kzUdAYFFOb8R7NOZk0q2lwAyz1i7oAr1xoXvrOgtYhZx8wY5KRV269JZ5yGpmzPTjQhvY9je6vEElPOuJP3mWKnP5M3V+YAAAB4nG2P2XLCMAxFfYE4CWlZSveFP8hHOY4gHhw79VLav68hMNOH6kG60mg5YhM22pr9b1vGMMEUM2TgyFGgxBwVbnCLBZZYYY07bHCPBzziCc94wSve8I4PbGeDFj/VydVSOakpG0T0VH1ZHXuq+xhoftHaHq+yV+21o1P7brWLWnvpiExNJpBb/i18q8D9ZxSOcj8oY8iVPjZBBU2+kGIIypokuqTI+cx3qXMq7Z6PQIsx1DYGrQxtLul50YV50rVcCiNJc0enX4qdkNRYe8j2g46+SIMHapXJw1GFdIWH2DfalQknZeTDWsRW2bqlBK3ORIz9AqJUapQAAAA=) format("woff"), url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAAKAIAAAwAgT1MvMlGRXgQAAAEoAAAAVmNtYXDiLxC2AAAB+AAAAUpnbHlm5X8X/gAAA4QAAA7kaGVhZAuHlGsAAADQAAAANmhoZWEOogcfAAAArAAAACRobXR40gAAAAAAAYAAAAB4bG9jYTDILUIAAANEAAAAPm1heHABLwB5AAABCAAAACBuYW1l1cf1oAAAEmgAAAIKcG9zdL2sAHoAABR0AAABeQABAAAHAAAAAKEHAAAAAAAHAAABAAAAAAAAAAAAAAAAAAAAHgABAAAAAQAAD+/W/l8PPPUACwcAAAAAANK0pLcAAAAA0rSktwAAAAAHAAcAAAAACAACAAAAAAAAAAEAAAAeAG0ABwAAAAAAAgAAAAoACgAAAP8AAAAAAAAAAQcAAZAABQAIBHEE5gAAAPoEcQTmAAADXABXAc4AAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZABA8QHxHQcAAAAAoQcAAAAAAAABAAAAAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAAAAAAAwAAAAMAAAAcAAEAAAAAAEQAAwABAAAAHAAEACgAAAAGAAQAAQACAADxHf//AAAAAPEB//8AAA8AAAEAAAAAAAAAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AUABmALIAxgDmAR4BSAF0AZwB8gIuAo4CugMKA44DsAPqBDgEfAS4BOgFDAWiBegGNgZsBtoHcgAAAAEAAAAABYsFiwACAAABEQECVQM2BYv76gILAAADAAAAAAZrBmsAAgAOABoAAAkCEwQAAxIABSQAEwIAASYAJzYANxYAFwYAAusBwP5Alf7D/loICAGmAT0BPQGmCAj+Wv7D/f6uBgYBUv39AVIGBv6uAjABUAFQAZsI/lr+w/7D/loICAGmAT0BPQGm+sgGAVL9/QFSBgb+rv39/q4AAAACAAAAAAVABYsAAwAHAAABIREpAREhEQHAASv+1QJVASsBdQQW++oEFgAAAAQAAAAABiAGIAAGABMAJAAnAAABLgEnFRc2NwYHFz4BNSYAJxUWEgEHASERIQERAQYHFT4BNxc3AQcXBNABZVW4A7sCJ3ElKAX+3+Wlzvu3XwFh/p8BKwF1AT5MXU6KO5lf/WCcnAOAZJ4rpbgYGGpbcUacVPQBYziaNP70Aetf/p/+QP6LAfb+wjsdmhJEMZhfBJacnAAAAQAAAAAEqwXWAAUAAAERIQERAQILASoBdv6KBGD+QP6LBKr+iwAAAAIAAAAABWYF1gAGAAwAAAEuAScRPgEBESEBEQEFZQFlVFRl/BEBKwF1/osDgGSeK/2mK54BRP5A/osEqv6LAAADAAAAAAYgBg8ABQAMABoAABMRIQERAQUuAScRPgEDFRYSFwYCBxU2ADcmAOABKwF1/osCxQFlVVVluqXOAwPOpeUBIQUF/t8EYP5A/osEqv6L4GSeK/2mK54C85o0/vS1tf70NJo4AWL19QFiAAAABAAAAAAFiwWLAAUACwARABcAAAEjESE1IwMzNTM1IQEjFSERIwMVMxUzEQILlgF24JaW4P6KA4DgAXaW4OCWAuv+ipYCCuCW/ICWAXYCoJbgAXYABAAAAAAFiwWLAAUACwARABcAAAEzFTMRIRMjFSERIwEzNTM1IRM1IxEhNQF14Jb+iuDgAXaWAcCW4P6KlpYBdgJV4AF2AcCWAXb76uCWAcDg/oqWAAAAAAIAAAAABdYF1gAPABMAAAEhDgEHER4BFyE+ATcRLgEDIREhBUD8gD9VAQFVPwOAP1UBAVU//IADgAXVAVU//IA/VQEBVT8DgD9V++wDgAAABgAAAAAGawZrAAcADAATABsAIAAoAAAJASYnDgEHASUuAScBBSEBNhI3JgUBBgIHFhchBR4BFwEzARYXPgE3AQK+AWROVIfwYQESA4416aH+7gLl/dABelxoAQH8E/7dXGgBAQ4CMP3kNemhARJ4/t1OVIfwYf7uA/ACaBIBAVhQ/id3pfY+/idL/XNkAQGTTU0B+GT+/5NNSEul9j4B2f4IEgEBWFAB2QAAAAUAAAAABmsF1gAPABMAFwAbAB8AAAEhDgEHER4BFyE+ATcRLgEBIRUhASE1IQUhNSE1ITUhBdX7VkBUAgJUQASqQFQCAlT7FgEq/tYC6v0WAuoBwP7WASr9FgLqBdUBVT/8gD9VAQFVPwOAP1X9rJX+1ZWVlZaVAAMAAAAABiAF1gAPACcAPwAAASEOAQcRHgEXIT4BNxEuAQEjNSMVMzUzFRQGByMuAScRPgE3Mx4BFQUjNSMVMzUzFQ4BByMuATURNDY3Mx4BFwWL++o/VAICVD8EFj9UAgJU/WtwlZVwKiDgICoBASog4CAqAgtwlZVwASog4CAqKiDgICoBBdUBVT/8gD9VAQFVPwOAP1X99yXgJUogKgEBKiABKiAqAQEqIEol4CVKICoBASogASogKgEBKiAAAAYAAAAABiAE9gADAAcACwAPABMAFwAAEzM1IxEzNSMRMzUjASE1IREhNSERFSE14JWVlZWVlQErBBX76wQV++sEFQM1lv5AlQHAlf5Alv5AlQJVlZUAAAABAAAAAAYgBmwALgAAASIGBwE2NCcBHgEzPgE3LgEnDgEHFBcBLgEjDgEHHgEXMjY3AQYHHgEXPgE3LgEFQCtKHv3sBwcCDx5OLF9/AgJ/X19/Agf98R5OLF9/AgJ/XyxOHgIUBQEDe1xcewMDewJPHxsBNxk2GQE0HSACf19ffwICf18bGf7NHCACf19ffwIgHP7KFxpcewICe1xdewAAAgAAAAAGWQZrAEMATwAAATY0Jzc+AScDLgEPASYvAS4BJyEOAQ8BBgcnJgYHAwYWHwEGFBcHDgEXEx4BPwEWHwEeARchPgE/ATY3FxY2NxM2JicFLgEnPgE3HgEXDgEFqwUFngoGB5YHGQ26OkQcAxQP/tYPFAIcRTm6DRoHlQcFC50FBZ0LBQeVBxoNujlFHAIUDwEqDxQCHEU5ug0aB5UHBQv9OG+UAgKUb2+UAgKUAzckSiR7CRoNAQMMCQVLLRzGDhEBAREOxhwtSwUJDP79DBsJeyRKJHsJGg3+/QwJBUstHMYOEQEBEQ7GHC1LBQkMAQMMGwlBApRvb5QCApRvb5QAAAAAAQAAAAAGawZrAAsAABMSAAUkABMCACUEAJUIAaYBPQE9AaYICP5a/sP+w/5aA4D+w/5aCAgBpgE9AT0BpggI/loAAAACAAAAAAZrBmsACwAXAAABBAADEgAFJAATAgABJgAnNgA3FgAXBgADgP7D/loICAGmAT0BPQGmCAj+Wv7D/f6uBgYBUv39AVIGBv6uBmsI/lr+w/7D/loICAGmAT0BPQGm+sgGAVL9/QFSBgb+rv39/q4AAAMAAAAABmsGawALABcAIwAAAQQAAxIABSQAEwIAASYAJzYANxYAFwYAAw4BBy4BJz4BNx4BA4D+w/5aCAgBpgE9AT0BpggI/lr+w/3+rgYGAVL9/QFSBgb+rh0Cf19ffwICf19ffwZrCP5a/sP+w/5aCAgBpgE9AT0BpvrIBgFS/f0BUgYG/q79/f6uAk9ffwICf19ffwICfwAAAAQAAAAABiAGIAAPABsAJQApAAABIQ4BBxEeARchPgE3ES4BASM1IxUjETMVMzU7ASEeARcRDgEHITczNSMFi/vqP1QCAlQ/BBY/VAICVP1rcJVwcJVwlgEqICoBASog/tZwlZUGIAJUP/vqP1QCAlQ/BBY/VPyClZUBwLu7ASog/tYgKgFw4AACAAAAAAZrBmsACwAXAAABBAADEgAFJAATAgATBwkBJwkBNwkBFwEDgP7D/loICAGmAT0BPQGmCAj+Wjhp/vT+9GkBC/71aQEMAQxp/vUGawj+Wv7D/sP+WggIAaYBPQE9Aab8EWkBC/71aQEMAQxp/vUBC2n+9AABAAAAAAXWBrYAFgAAAREJAREeARcOAQcuAScjFgAXNgA3JgADgP6LAXW+/QUF/b6+/QWVBgFR/v4BUQYG/q8FiwEq/ov+iwEqBP2/vv0FBf2+/v6vBgYBUf7+AVEAAAABAAAAAAU/BwAAFAAAAREjIgYdASEDIxEhESMRMzU0NjMyBT+dVjwBJSf+/s7//9Ctkwb0/vhISL3+2P0JAvcBKNq6zQAAAAAEAAAAAAaOBwAAMABFAGAAbAAAARQeAxUUBwYEIyImJyY1NDY3NiUuATU0NwYjIiY1NDY3PgEzIQcjHgEVFA4DJzI2NzY1NC4CIyIGBwYVFB4DEzI+AjU0LgEvASYvAiYjIg4DFRQeAgEzFSMVIzUjNTM1MwMfQFtaQDBI/uqfhOU5JVlKgwERIB8VLhaUy0g/TdNwAaKKg0pMMUVGMZImUBo1Ij9qQCpRGS8UKz1ZNjprWzcODxMeChwlThAgNWhvUzZGcX0Da9XVadTUaQPkJEVDUIBOWlN6c1NgPEdRii5SEipAKSQxBMGUUpo2QkBYP4xaSHNHO0A+IRs5ZjqGfVInITtlLmdnUjT8lxo0Xj4ZMCQYIwsXHTgCDiQ4XTtGazsdA2xs29ts2QADAAAAAAaABmwAAwAOACoAAAERIREBFgYrASImNDYyFgERIRE0JiMiBgcGFREhEhAvASEVIz4DMzIWAd3+tgFfAWdUAlJkZ6ZkBI/+t1FWP1UVC/63AgEBAUkCFCpHZz+r0ASP/CED3wEySWJik2Fh/N39yAISaXdFMx4z/dcBjwHwMDCQIDA4H+MAAAEAAAAABpQGAAAxAAABBgcWFRQCDgEEIyAnFjMyNy4BJxYzMjcuAT0BFhcuATU0NxYEFyY1NDYzMhc2NwYHNgaUQ18BTJvW/tKs/vHhIyvhsGmmHyEcKypwk0ROQk4seQFbxgi9hoxgbWAlaV0FaGJFDhyC/v3ut22RBIoCfWEFCxexdQQmAyyOU1hLlbMKJiSGvWYVOXM/CgAAAAEAAAAABYAHAAAiAAABFw4BBwYuAzURIzU+BDc+ATsBESEVIREUHgI3NgUwUBewWWitcE4hqEhyRDAUBQEHBPQBTf6yDSBDME4Bz+0jPgECOFx4eDoCINcaV11vVy0FB/5Y/P36HjQ1HgECAAEAAAAABoAGgABKAAABFAIEIyInNj8BHgEzMj4BNTQuASMiDgMVFBYXFj8BNjc2JyY1NDYzMhYVFAYjIiY3PgI1NCYjIgYVFBcDBhcmAjU0EiQgBBIGgM7+n9FvazsTNhRqPXm+aHfijmm2f1srUE0eCAgGAgYRM9Gpl6mJaz1KDgglFzYyPlYZYxEEzv7OAWEBogFhzgOA0f6fziBdR9MnOYnwlnLIfjpgfYZDaJ4gDCAfGAYXFD1al9mkg6ruVz0jdVkfMkJyVUkx/l5Ga1sBfOnRAWHOzv6fAAAHAAAAAAcABM8ADgAXACoAPQBQAFoAXQAAARE2HgIHDgEHBiYjJyY3FjY3NiYHERQFFjY3PgE3LgEnIwYfAR4BFw4BFxY2Nz4BNy4BJyMGHwEeARcUBhcWNjc+ATcuAScjBh8BHgEXDgEFMz8BFTMRIwYDJRUnAxyEzZRbCA2rgketCAEBqlRoCglxYwF+IiEOIysBAkswHQEECiQ0AgE+YyIhDiIsAQJLMB4BBQokNAE/YyIhDiIsAQJLMB4BBQokNAEBPvmD7kHhqs0s0gEnjgHJAv0FD2a9gIrADwUFAwPDAlVMZ3MF/pUHwgc1HTyWV325PgsJED+oY3G9TAc1HTyWV325PgsJED+oY3G9TAc1HTyWV325PgsJED+oY3G9UmQBZQMMR/61g/kBAAAAAAAQAMYAAQAAAAAAAQAHAAAAAQAAAAAAAgAHAAcAAQAAAAAAAwAHAA4AAQAAAAAABAAHABUAAQAAAAAABQALABwAAQAAAAAABgAHACcAAQAAAAAACgArAC4AAQAAAAAACwATAFkAAwABBAkAAQAOAGwAAwABBAkAAgAOAHoAAwABBAkAAwAOAIgAAwABBAkABAAOAJYAAwABBAkABQAWAKQAAwABBAkABgAOALoAAwABBAkACgBWAMgAAwABBAkACwAmAR5WaWRlb0pTUmVndWxhclZpZGVvSlNWaWRlb0pTVmVyc2lvbiAxLjBWaWRlb0pTR2VuZXJhdGVkIGJ5IHN2ZzJ0dGYgZnJvbSBGb250ZWxsbyBwcm9qZWN0Lmh0dHA6Ly9mb250ZWxsby5jb20AVgBpAGQAZQBvAEoAUwBSAGUAZwB1AGwAYQByAFYAaQBkAGUAbwBKAFMAVgBpAGQAZQBvAEoAUwBWAGUAcgBzAGkAbwBuACAAMQAuADAAVgBpAGQAZQBvAEoAUwBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAACAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4EcGxheQtwbGF5LWNpcmNsZQVwYXVzZQt2b2x1bWUtbXV0ZQp2b2x1bWUtbG93CnZvbHVtZS1taWQLdm9sdW1lLWhpZ2gQZnVsbHNjcmVlbi1lbnRlcg9mdWxsc2NyZWVuLWV4aXQGc3F1YXJlB3NwaW5uZXIJc3VidGl0bGVzCGNhcHRpb25zCGNoYXB0ZXJzBXNoYXJlA2NvZwZjaXJjbGUOY2lyY2xlLW91dGxpbmUTY2lyY2xlLWlubmVyLWNpcmNsZQJoZAZjYW5jZWwGcmVwbGF5CGZhY2Vib29rBWdwbHVzCGxpbmtlZGluB3R3aXR0ZXIGdHVtYmxyCXBpbnRlcmVzdBFhdWRpby1kZXNjcmlwdGlvbgAAAAAA) format("truetype"); + font-weight: normal; + font-style: normal; } + +.vjs-icon-play, .video-js .vjs-big-play-button, .video-js .vjs-play-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-play:before, .video-js .vjs-big-play-button:before, .video-js .vjs-play-control:before { + content: '\f101'; } + +.vjs-icon-play-circle { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-play-circle:before { + content: '\f102'; } + +.vjs-icon-pause, .video-js .vjs-play-control.vjs-playing { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-pause:before, .video-js .vjs-play-control.vjs-playing:before { + content: '\f103'; } + +.vjs-icon-volume-mute, .video-js .vjs-mute-control.vjs-vol-0, +.video-js .vjs-volume-menu-button.vjs-vol-0 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-mute:before, .video-js .vjs-mute-control.vjs-vol-0:before, + .video-js .vjs-volume-menu-button.vjs-vol-0:before { + content: '\f104'; } + +.vjs-icon-volume-low, .video-js .vjs-mute-control.vjs-vol-1, +.video-js .vjs-volume-menu-button.vjs-vol-1 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-low:before, .video-js .vjs-mute-control.vjs-vol-1:before, + .video-js .vjs-volume-menu-button.vjs-vol-1:before { + content: '\f105'; } + +.vjs-icon-volume-mid, .video-js .vjs-mute-control.vjs-vol-2, +.video-js .vjs-volume-menu-button.vjs-vol-2 { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-mid:before, .video-js .vjs-mute-control.vjs-vol-2:before, + .video-js .vjs-volume-menu-button.vjs-vol-2:before { + content: '\f106'; } + +.vjs-icon-volume-high, .video-js .vjs-mute-control, +.video-js .vjs-volume-menu-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-volume-high:before, .video-js .vjs-mute-control:before, + .video-js .vjs-volume-menu-button:before { + content: '\f107'; } + +.vjs-icon-fullscreen-enter, .video-js .vjs-fullscreen-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-fullscreen-enter:before, .video-js .vjs-fullscreen-control:before { + content: '\f108'; } + +.vjs-icon-fullscreen-exit, .video-js.vjs-fullscreen .vjs-fullscreen-control { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-fullscreen-exit:before, .video-js.vjs-fullscreen .vjs-fullscreen-control:before { + content: '\f109'; } + +.vjs-icon-square { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-square:before { + content: '\f10a'; } + +.vjs-icon-spinner { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-spinner:before { + content: '\f10b'; } + +.vjs-icon-subtitles, .video-js .vjs-subtitles-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-subtitles:before, .video-js .vjs-subtitles-button:before { + content: '\f10c'; } + +.vjs-icon-captions, .video-js .vjs-captions-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-captions:before, .video-js .vjs-captions-button:before { + content: '\f10d'; } + +.vjs-icon-chapters, .video-js .vjs-chapters-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-chapters:before, .video-js .vjs-chapters-button:before { + content: '\f10e'; } + +.vjs-icon-share { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-share:before { + content: '\f10f'; } + +.vjs-icon-cog { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-cog:before { + content: '\f110'; } + +.vjs-icon-circle, .video-js .vjs-mouse-display, .video-js .vjs-play-progress, .video-js .vjs-volume-level { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle:before, .video-js .vjs-mouse-display:before, .video-js .vjs-play-progress:before, .video-js .vjs-volume-level:before { + content: '\f111'; } + +.vjs-icon-circle-outline { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle-outline:before { + content: '\f112'; } + +.vjs-icon-circle-inner-circle { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-circle-inner-circle:before { + content: '\f113'; } + +.vjs-icon-hd { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-hd:before { + content: '\f114'; } + +.vjs-icon-cancel, .video-js .vjs-control.vjs-close-button { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-cancel:before, .video-js .vjs-control.vjs-close-button:before { + content: '\f115'; } + +.vjs-icon-replay { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-replay:before { + content: '\f116'; } + +.vjs-icon-facebook { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-facebook:before { + content: '\f117'; } + +.vjs-icon-gplus { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-gplus:before { + content: '\f118'; } + +.vjs-icon-linkedin { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-linkedin:before { + content: '\f119'; } + +.vjs-icon-twitter { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-twitter:before { + content: '\f11a'; } + +.vjs-icon-tumblr { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-tumblr:before { + content: '\f11b'; } + +.vjs-icon-pinterest { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-pinterest:before { + content: '\f11c'; } + +.vjs-icon-audio-description { + font-family: VideoJS; + font-weight: normal; + font-style: normal; } + .vjs-icon-audio-description:before { + content: '\f11d'; } + +.video-js { + display: block; + vertical-align: top; + box-sizing: border-box; + color: #fff; + background-color: #000; + position: relative; + padding: 0; + font-size: 10px; + line-height: 1; + font-weight: normal; + font-style: normal; + font-family: Arial, Helvetica, sans-serif; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .video-js:-moz-full-screen { + position: absolute; } + .video-js:-webkit-full-screen { + width: 100% !important; + height: 100% !important; } + +.video-js *, +.video-js *:before, +.video-js *:after { + box-sizing: inherit; } + +.video-js ul { + font-family: inherit; + font-size: inherit; + line-height: inherit; + list-style-position: outside; + margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0; } + +.video-js.vjs-fluid, +.video-js.vjs-16-9, +.video-js.vjs-4-3 { + width: 100%; + max-width: 100%; + height: 0; } + +.video-js.vjs-16-9 { + padding-top: 56.25%; } + +.video-js.vjs-4-3 { + padding-top: 75%; } + +.video-js.vjs-fill { + width: 100%; + height: 100%; } + +.video-js .vjs-tech { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + +body.vjs-full-window { + padding: 0; + margin: 0; + height: 100%; + overflow-y: auto; } + +.vjs-full-window .video-js.vjs-fullscreen { + position: fixed; + overflow: hidden; + z-index: 1000; + left: 0; + top: 0; + bottom: 0; + right: 0; } + +.video-js.vjs-fullscreen { + width: 100% !important; + height: 100% !important; + padding-top: 0 !important; } + +.video-js.vjs-fullscreen.vjs-user-inactive { + cursor: none; } + +.vjs-hidden { + display: none !important; } + +.video-js .vjs-offscreen { + height: 1px; + left: -9999px; + position: absolute; + top: 0; + width: 1px; } + +.vjs-lock-showing { + display: block !important; + opacity: 1; + visibility: visible; } + +.vjs-no-js { + padding: 20px; + color: #fff; + background-color: #000; + font-size: 18px; + font-family: Arial, Helvetica, sans-serif; + text-align: center; + width: 300px; + height: 150px; + margin: 0px auto; } + +.vjs-no-js a, +.vjs-no-js a:visited { + color: #66A8CC; } + +.video-js .vjs-big-play-button { + font-size: 3em; + line-height: 1.5em; + height: 1.5em; + width: 3em; + display: block; + position: absolute; + top: 10px; + left: 10px; + padding: 0; + cursor: pointer; + opacity: 1; + border: 0.06666em solid #fff; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); + -webkit-border-radius: 0.3em; + -moz-border-radius: 0.3em; + border-radius: 0.3em; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; } + +.vjs-big-play-centered .vjs-big-play-button { + top: 50%; + left: 50%; + margin-top: -0.75em; + margin-left: -1.5em; } + +.video-js:hover .vjs-big-play-button, +.video-js .vjs-big-play-button:focus { + outline: 0; + border-color: #fff; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); + -webkit-transition: all 0s; + -moz-transition: all 0s; + -o-transition: all 0s; + transition: all 0s; } + +.vjs-controls-disabled .vjs-big-play-button, +.vjs-has-started .vjs-big-play-button, +.vjs-using-native-controls .vjs-big-play-button, +.vjs-error .vjs-big-play-button { + display: none; } + +.video-js button { + background: none; + border: none; + color: inherit; + display: inline-block; + overflow: visible; + font-size: inherit; + line-height: inherit; + text-transform: none; + text-decoration: none; + transition: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + +.video-js .vjs-control.vjs-close-button { + cursor: pointer; + height: 3em; + position: absolute; + right: 0; + top: 0.5em; + z-index: 2; } + +.vjs-menu-button { + cursor: pointer; } + +.vjs-menu .vjs-menu-content { + display: block; + padding: 0; + margin: 0; + overflow: auto; } + +.vjs-scrubbing .vjs-menu-button:hover .vjs-menu { + display: none; } + +.vjs-menu li { + list-style: none; + margin: 0; + padding: 0.2em 0; + line-height: 1.4em; + font-size: 1.2em; + text-align: center; + text-transform: lowercase; } + +.vjs-menu li:focus, +.vjs-menu li:hover { + outline: 0; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); } + +.vjs-menu li.vjs-selected, +.vjs-menu li.vjs-selected:focus, +.vjs-menu li.vjs-selected:hover { + background-color: #fff; + color: #2B333F; } + +.vjs-menu li.vjs-menu-title { + text-align: center; + text-transform: uppercase; + font-size: 1em; + line-height: 2em; + padding: 0; + margin: 0 0 0.3em 0; + font-weight: bold; + cursor: default; } + +.vjs-menu-button-popup .vjs-menu { + display: none; + position: absolute; + bottom: 0; + width: 10em; + left: -3em; + height: 0em; + margin-bottom: 1.5em; + border-top-color: rgba(43, 51, 63, 0.7); } + +.vjs-menu-button-popup .vjs-menu .vjs-menu-content { + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); + position: absolute; + width: 100%; + bottom: 1.5em; + max-height: 15em; } + +.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, +.vjs-menu-button-popup .vjs-menu.vjs-lock-showing { + display: block; } + +.video-js .vjs-menu-button-inline { + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; + overflow: hidden; } + +.video-js .vjs-menu-button-inline:before { + width: 2.222222222em; } + +.video-js .vjs-menu-button-inline:hover, +.video-js .vjs-menu-button-inline:focus, +.video-js .vjs-menu-button-inline.vjs-slider-active, +.video-js.vjs-no-flex .vjs-menu-button-inline { + width: 12em; } + +.video-js .vjs-menu-button-inline.vjs-slider-active { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; } + +.vjs-menu-button-inline .vjs-menu { + opacity: 0; + height: 100%; + width: auto; + position: absolute; + left: 4em; + top: 0; + padding: 0; + margin: 0; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + transition: all 0.4s; } + +.vjs-menu-button-inline:hover .vjs-menu, +.vjs-menu-button-inline:focus .vjs-menu, +.vjs-menu-button-inline.vjs-slider-active .vjs-menu { + display: block; + opacity: 1; } + +.vjs-no-flex .vjs-menu-button-inline .vjs-menu { + display: block; + opacity: 1; + position: relative; + width: auto; } + +.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu, +.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu, +.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu { + width: auto; } + +.vjs-menu-button-inline .vjs-menu-content { + width: auto; + height: 100%; + margin: 0; + overflow: hidden; } + +.video-js .vjs-control-bar { + display: none; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3.0em; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.7); } + +.vjs-has-started .vjs-control-bar { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + visibility: visible; + opacity: 1; + -webkit-transition: visibility 0.1s, opacity 0.1s; + -moz-transition: visibility 0.1s, opacity 0.1s; + -o-transition: visibility 0.1s, opacity 0.1s; + transition: visibility 0.1s, opacity 0.1s; } + +.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + visibility: hidden; + opacity: 0; + -webkit-transition: visibility 1s, opacity 1s; + -moz-transition: visibility 1s, opacity 1s; + -o-transition: visibility 1s, opacity 1s; + transition: visibility 1s, opacity 1s; } + +.vjs-controls-disabled .vjs-control-bar, +.vjs-using-native-controls .vjs-control-bar, +.vjs-error .vjs-control-bar { + display: none !important; } + +.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + opacity: 1; + visibility: visible; } + +@media \0screen { + .vjs-user-inactive.vjs-playing .vjs-control-bar :before { + content: ""; } } + +.vjs-has-started.vjs-no-flex .vjs-control-bar { + display: table; } + +.video-js .vjs-control { + outline: none; + position: relative; + text-align: center; + margin: 0; + padding: 0; + height: 100%; + width: 4em; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + .video-js .vjs-control:before { + font-size: 1.8em; + line-height: 1.67; } + +.video-js .vjs-control:focus:before, +.video-js .vjs-control:hover:before, +.video-js .vjs-control:focus { + text-shadow: 0em 0em 1em white; } + +.video-js .vjs-control-text { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.vjs-no-flex .vjs-control { + display: table-cell; + vertical-align: middle; } + +.video-js .vjs-custom-control-spacer { + display: none; } + +.video-js .vjs-progress-control { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + min-width: 4em; } + +.vjs-live .vjs-progress-control { + display: none; } + +.video-js .vjs-progress-holder { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + height: 0.3em; } + +.video-js .vjs-progress-control:hover .vjs-progress-holder { + font-size: 1.666666666666666666em; } + +/* If we let the font size grow as much as everything else, the current time tooltip ends up + ginormous. If you'd like to enable the current time tooltip all the time, this should be disabled + to avoid a weird hitch when you roll off the hover. */ +.video-js .vjs-progress-control:hover .vjs-mouse-display:after, +.video-js .vjs-progress-control:hover .vjs-play-progress:after { + display: block; + font-size: 0.6em; } + +.video-js .vjs-progress-holder .vjs-play-progress, +.video-js .vjs-progress-holder .vjs-load-progress, +.video-js .vjs-progress-holder .vjs-load-progress div { + position: absolute; + display: block; + height: 0.3em; + margin: 0; + padding: 0; + width: 0; + left: 0; + top: 0; } + +.video-js .vjs-mouse-display:before { + display: none; } + +.video-js .vjs-play-progress { + background-color: #fff; } + .video-js .vjs-play-progress:before { + position: absolute; + top: -0.333333333333333em; + right: -0.5em; + font-size: 0.9em; } + +.video-js .vjs-mouse-display:after, +.video-js .vjs-play-progress:after { + display: none; + position: absolute; + top: -3.4em; + right: -1.5em; + font-size: 0.9em; + color: #000; + content: attr(data-current-time); + padding: 6px 8px 8px 8px; + background-color: #fff; + background-color: rgba(255, 255, 255, 0.8); + -webkit-border-radius: 0.3em; + -moz-border-radius: 0.3em; + border-radius: 0.3em; } + +.video-js .vjs-play-progress:before, +.video-js .vjs-play-progress:after { + z-index: 1; } + +.video-js .vjs-load-progress { + background: #bfc7d3; + background: rgba(115, 133, 159, 0.5); } + +.video-js .vjs-load-progress div { + background: white; + background: rgba(115, 133, 159, 0.75); } + +.video-js.vjs-no-flex .vjs-progress-control { + width: auto; } + +.video-js .vjs-progress-control .vjs-mouse-display { + display: none; + position: absolute; + width: 1px; + height: 100%; + background-color: #000; + z-index: 1; } + +.vjs-no-flex .vjs-progress-control .vjs-mouse-display { + z-index: 0; } + +.video-js .vjs-progress-control:hover .vjs-mouse-display { + display: block; } + +.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display, +.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display:after { + visibility: hidden; + opacity: 0; + -webkit-transition: visibility 1s, opacity 1s; + -moz-transition: visibility 1s, opacity 1s; + -o-transition: visibility 1s, opacity 1s; + transition: visibility 1s, opacity 1s; } + +.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display, +.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display:after { + display: none; } + +.video-js .vjs-progress-control .vjs-mouse-display:after { + color: #fff; + background-color: #000; + background-color: rgba(0, 0, 0, 0.8); } + +.video-js .vjs-slider { + outline: 0; + position: relative; + cursor: pointer; + padding: 0; + margin: 0 0.45em 0 0.45em; + background-color: #73859f; + background-color: rgba(115, 133, 159, 0.5); } + +.video-js .vjs-slider:focus { + text-shadow: 0em 0em 1em white; + -webkit-box-shadow: 0 0 1em #fff; + -moz-box-shadow: 0 0 1em #fff; + box-shadow: 0 0 1em #fff; } + +.video-js .vjs-mute-control, +.video-js .vjs-volume-menu-button { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.video-js .vjs-volume-control { + width: 5em; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } + +.video-js .vjs-volume-bar { + margin: 1.35em 0.45em; } + +.vjs-volume-bar.vjs-slider-horizontal { + width: 5em; + height: 0.3em; } + +.vjs-volume-bar.vjs-slider-vertical { + width: 0.3em; + height: 5em; + margin: 1.35em auto; } + +.video-js .vjs-volume-level { + position: absolute; + bottom: 0; + left: 0; + background-color: #fff; } + .video-js .vjs-volume-level:before { + position: absolute; + font-size: 0.9em; } + +.vjs-slider-vertical .vjs-volume-level { + width: 0.3em; } + .vjs-slider-vertical .vjs-volume-level:before { + top: -0.5em; + left: -0.3em; } + +.vjs-slider-horizontal .vjs-volume-level { + height: 0.3em; } + .vjs-slider-horizontal .vjs-volume-level:before { + top: -0.3em; + right: -0.5em; } + +.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level { + height: 100%; } + +.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level { + width: 100%; } + +.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu { + display: block; + width: 0; + height: 0; + border-top-color: transparent; } + +.vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { + left: 0.5em; + height: 8em; } + +.vjs-menu-button-popup.vjs-volume-menu-button-horizontal .vjs-menu { + left: -2em; } + +.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu-content { + height: 0; + width: 0; + overflow-x: hidden; + overflow-y: hidden; } + +.vjs-volume-menu-button-vertical:hover .vjs-menu-content, +.vjs-volume-menu-button-vertical:focus .vjs-menu-content, +.vjs-volume-menu-button-vertical.vjs-slider-active .vjs-menu-content, +.vjs-volume-menu-button-vertical .vjs-lock-showing .vjs-menu-content { + height: 8em; + width: 2.9em; } + +.vjs-volume-menu-button-horizontal:hover .vjs-menu-content, +.vjs-volume-menu-button-horizontal:focus .vjs-menu-content, +.vjs-volume-menu-button-horizontal .vjs-slider-active .vjs-menu-content, +.vjs-volume-menu-button-horizontal .vjs-lock-showing .vjs-menu-content { + height: 2.9em; + width: 8em; } + +.vjs-volume-menu-button.vjs-menu-button-inline .vjs-menu-content { + background-color: transparent !important; } + +.vjs-poster { + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: contain; + cursor: pointer; + margin: 0; + padding: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; } + +.vjs-poster img { + display: block; + vertical-align: middle; + margin: 0 auto; + max-height: 100%; + padding: 0; + width: 100%; } + +.vjs-has-started .vjs-poster { + display: none; } + +.vjs-audio.vjs-has-started .vjs-poster { + display: block; } + +.vjs-controls-disabled .vjs-poster { + display: none; } + +.vjs-using-native-controls .vjs-poster { + display: none; } + +.video-js .vjs-live-control { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: flex-start; + -webkit-align-items: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + font-size: 1em; + line-height: 3em; } + +.vjs-no-flex .vjs-live-control { + display: table-cell; + width: auto; + text-align: left; } + +.video-js .vjs-time-control { + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1em; + line-height: 3em; + min-width: 2em; + width: auto; + padding-left: 1em; + padding-right: 1em; } + +.vjs-live .vjs-time-control { + display: none; } + +.video-js .vjs-current-time, +.vjs-no-flex .vjs-current-time { + display: none; } + +.video-js .vjs-duration, +.vjs-no-flex .vjs-duration { + display: none; } + +.vjs-time-divider { + display: none; + line-height: 3em; } + +.vjs-live .vjs-time-divider { + display: none; } + +.video-js .vjs-play-control { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.vjs-text-track-display { + position: absolute; + bottom: 3em; + left: 0; + right: 0; + top: 0; + pointer-events: none; } + +.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display { + bottom: 1em; } + +.video-js .vjs-text-track { + font-size: 1.4em; + text-align: center; + margin-bottom: 0.1em; + background-color: #000; + background-color: rgba(0, 0, 0, 0.5); } + +.vjs-subtitles { + color: #fff; } + +.vjs-captions { + color: #fc6; } + +.vjs-tt-cue { + display: block; } + +video::-webkit-media-text-track-display { + -moz-transform: translateY(-3em); + -ms-transform: translateY(-3em); + -o-transform: translateY(-3em); + -webkit-transform: translateY(-3em); + transform: translateY(-3em); } + +.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display { + -moz-transform: translateY(-1.5em); + -ms-transform: translateY(-1.5em); + -o-transform: translateY(-1.5em); + -webkit-transform: translateY(-1.5em); + transform: translateY(-1.5em); } + +.video-js .vjs-fullscreen-control { + cursor: pointer; + -webkit-box-flex: none; + -moz-box-flex: none; + -webkit-flex: none; + -ms-flex: none; + flex: none; } + +.vjs-playback-rate .vjs-playback-rate-value { + font-size: 1.5em; + line-height: 2; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; } + +.vjs-playback-rate .vjs-menu { + width: 4em; + left: 0em; } + +.vjs-error .vjs-error-display .vjs-modal-dialog-content { + font-size: 1.4em; + text-align: center; } + +.vjs-error .vjs-error-display:before { + color: #fff; + content: 'X'; + font-family: Arial, Helvetica, sans-serif; + font-size: 4em; + left: 0; + line-height: 1; + margin-top: -0.5em; + position: absolute; + text-shadow: 0.05em 0.05em 0.1em #000; + text-align: center; + top: 50%; + vertical-align: middle; + width: 100%; } + +.vjs-loading-spinner { + display: none; + position: absolute; + top: 50%; + left: 50%; + margin: -25px 0 0 -25px; + opacity: 0.85; + text-align: left; + border: 6px solid rgba(43, 51, 63, 0.7); + box-sizing: border-box; + background-clip: padding-box; + width: 50px; + height: 50px; + border-radius: 25px; } + +.vjs-seeking .vjs-loading-spinner, +.vjs-waiting .vjs-loading-spinner { + display: block; } + +.vjs-loading-spinner:before, +.vjs-loading-spinner:after { + content: ""; + position: absolute; + margin: -6px; + box-sizing: inherit; + width: inherit; + height: inherit; + border-radius: inherit; + opacity: 1; + border: inherit; + border-color: transparent; + border-top-color: white; } + +.vjs-seeking .vjs-loading-spinner:before, +.vjs-seeking .vjs-loading-spinner:after, +.vjs-waiting .vjs-loading-spinner:before, +.vjs-waiting .vjs-loading-spinner:after { + -webkit-animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; + animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; } + +.vjs-seeking .vjs-loading-spinner:before, +.vjs-waiting .vjs-loading-spinner:before { + border-top-color: white; } + +.vjs-seeking .vjs-loading-spinner:after, +.vjs-waiting .vjs-loading-spinner:after { + border-top-color: white; + -webkit-animation-delay: 0.44s; + animation-delay: 0.44s; } + +@keyframes vjs-spinner-spin { + 100% { + transform: rotate(360deg); } } + +@-webkit-keyframes vjs-spinner-spin { + 100% { + -webkit-transform: rotate(360deg); } } + +@keyframes vjs-spinner-fade { + 0% { + border-top-color: #73859f; } + 20% { + border-top-color: #73859f; } + 35% { + border-top-color: white; } + 60% { + border-top-color: #73859f; } + 100% { + border-top-color: #73859f; } } + +@-webkit-keyframes vjs-spinner-fade { + 0% { + border-top-color: #73859f; } + 20% { + border-top-color: #73859f; } + 35% { + border-top-color: white; } + 60% { + border-top-color: #73859f; } + 100% { + border-top-color: #73859f; } } + +.vjs-chapters-button .vjs-menu ul { + width: 24em; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-custom-control-spacer { + -webkit-box-flex: auto; + -moz-box-flex: auto; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen).vjs-no-flex .vjs-custom-control-spacer { + width: auto; } + +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-playback-rate, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-progress-control, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-control, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-menu-button, +.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-captions-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-subtitles-button { + display: none; } + +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-playback-rate, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-control, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-menu-button, +.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-captions-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-subtitles-button { + display: none; } + +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-remaining-time, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-playback-rate, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-volume-control, +.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-captions-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-subtitles-button { + display: none; } + +.vjs-caption-settings { + position: relative; + top: 1em; + background-color: #2B333F; + background-color: rgba(43, 51, 63, 0.75); + color: #fff; + margin: 0 auto; + padding: 0.5em; + height: 15em; + font-size: 12px; + width: 40em; } + +.vjs-caption-settings .vjs-tracksettings { + top: 0; + bottom: 2em; + left: 0; + right: 0; + position: absolute; + overflow: auto; } + +.vjs-caption-settings .vjs-tracksettings-colors, +.vjs-caption-settings .vjs-tracksettings-font { + float: left; } + +.vjs-caption-settings .vjs-tracksettings-colors:after, +.vjs-caption-settings .vjs-tracksettings-font:after, +.vjs-caption-settings .vjs-tracksettings-controls:after { + clear: both; } + +.vjs-caption-settings .vjs-tracksettings-controls { + position: absolute; + bottom: 1em; + right: 1em; } + +.vjs-caption-settings .vjs-tracksetting { + margin: 5px; + padding: 3px; + min-height: 40px; } + +.vjs-caption-settings .vjs-tracksetting label { + display: block; + width: 100px; + margin-bottom: 5px; } + +.vjs-caption-settings .vjs-tracksetting span { + display: inline; + margin-left: 5px; } + +.vjs-caption-settings .vjs-tracksetting > div { + margin-bottom: 5px; + min-height: 20px; } + +.vjs-caption-settings .vjs-tracksetting > div:last-child { + margin-bottom: 0; + padding-bottom: 0; + min-height: 0; } + +.vjs-caption-settings label > input { + margin-right: 10px; } + +.vjs-caption-settings input[type="button"] { + width: 40px; + height: 40px; } + +.video-js .vjs-modal-dialog { + background: rgba(0, 0, 0, 0.8); + background: -webkit-linear-gradient(-90deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); } + +.vjs-modal-dialog .vjs-modal-dialog-content { + font-size: 1.2em; + line-height: 1.5; + padding: 20px 24px; + z-index: 1; } diff --git a/static/video.js b/static/video.js @@ -0,0 +1,22383 @@ +/** + * @license + * Video.js 5.8.8 <http://videojs.com/> + * Copyright Brightcove, Inc. <https://www.brightcove.com/> + * Available under Apache License Version 2.0 + * <https://github.com/videojs/video.js/blob/master/LICENSE> + * + * Includes vtt.js <https://github.com/mozilla/vtt.js> + * Available under Apache License Version 2.0 + * <https://github.com/mozilla/vtt.js/blob/master/LICENSE> + */ + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.videojs = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ +(function (global){ +var topLevel = typeof global !== 'undefined' ? global : + typeof window !== 'undefined' ? window : {} +var minDoc = _dereq_('min-document'); + +if (typeof document !== 'undefined') { + module.exports = document; +} else { + var doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; + + if (!doccy) { + doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; + } + + module.exports = doccy; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +//# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9nbG9iYWwvZG9jdW1lbnQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgdG9wTGV2ZWwgPSB0eXBlb2YgZ2xvYmFsICE9PSAndW5kZWZpbmVkJyA/IGdsb2JhbCA6XG4gICAgdHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcgPyB3aW5kb3cgOiB7fVxudmFyIG1pbkRvYyA9IHJlcXVpcmUoJ21pbi1kb2N1bWVudCcpO1xuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIG1vZHVsZS5leHBvcnRzID0gZG9jdW1lbnQ7XG59IGVsc2Uge1xuICAgIHZhciBkb2NjeSA9IHRvcExldmVsWydfX0dMT0JBTF9ET0NVTUVOVF9DQUNIRUA0J107XG5cbiAgICBpZiAoIWRvY2N5KSB7XG4gICAgICAgIGRvY2N5ID0gdG9wTGV2ZWxbJ19fR0xPQkFMX0RPQ1VNRU5UX0NBQ0hFQDQnXSA9IG1pbkRvYztcbiAgICB9XG5cbiAgICBtb2R1bGUuZXhwb3J0cyA9IGRvY2N5O1xufVxuIl19 +},{"min-document":3}],2:[function(_dereq_,module,exports){ +(function (global){ +if (typeof window !== "undefined") { + module.exports = window; +} else if (typeof global !== "undefined") { + module.exports = global; +} else if (typeof self !== "undefined"){ + module.exports = self; +} else { + module.exports = {}; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +//# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9nbG9iYWwvd2luZG93LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsImZpbGUiOiJnZW5lcmF0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlc0NvbnRlbnQiOlsiaWYgKHR5cGVvZiB3aW5kb3cgIT09IFwidW5kZWZpbmVkXCIpIHtcbiAgICBtb2R1bGUuZXhwb3J0cyA9IHdpbmRvdztcbn0gZWxzZSBpZiAodHlwZW9mIGdsb2JhbCAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIG1vZHVsZS5leHBvcnRzID0gZ2xvYmFsO1xufSBlbHNlIGlmICh0eXBlb2Ygc2VsZiAhPT0gXCJ1bmRlZmluZWRcIil7XG4gICAgbW9kdWxlLmV4cG9ydHMgPSBzZWxmO1xufSBlbHNlIHtcbiAgICBtb2R1bGUuZXhwb3J0cyA9IHt9O1xufVxuIl19 +},{}],3:[function(_dereq_,module,exports){ + +},{}],4:[function(_dereq_,module,exports){ +var getNative = _dereq_('../internal/getNative'); + +/* Native method references for those with the same name as other `lodash` methods. */ +var nativeNow = getNative(Date, 'now'); + +/** + * Gets the number of milliseconds that have elapsed since the Unix epoch + * (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @category Date + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => logs the number of milliseconds it took for the deferred function to be invoked + */ +var now = nativeNow || function() { + return new Date().getTime(); +}; + +module.exports = now; + +},{"../internal/getNative":20}],5:[function(_dereq_,module,exports){ +var isObject = _dereq_('../lang/isObject'), + now = _dereq_('../date/now'); + +/** Used as the `TypeError` message for "Functions" methods. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Native method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed invocations. Provide an options object to indicate that `func` + * should be invoked on the leading and/or trailing edge of the `wait` timeout. + * Subsequent calls to the debounced function return the result of the last + * `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=false] Specify invoking on the leading + * edge of the timeout. + * @param {number} [options.maxWait] The maximum time `func` is allowed to be + * delayed before it's invoked. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // avoid costly calculations while the window size is in flux + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // invoke `sendMail` when the click event is fired, debouncing subsequent calls + * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // ensure `batchLog` is invoked once after 1 second of debounced calls + * var source = new EventSource('/stream'); + * jQuery(source).on('message', _.debounce(batchLog, 250, { + * 'maxWait': 1000 + * })); + * + * // cancel a debounced call + * var todoChanges = _.debounce(batchLog, 1000); + * Object.observe(models.todo, todoChanges); + * + * Object.observe(models, function(changes) { + * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { + * todoChanges.cancel(); + * } + * }, ['delete']); + * + * // ...at some point `models.todo` is changed + * models.todo.completed = true; + * + * // ...before 1 second has passed `models.todo` is deleted + * // which cancels the debounced `todoChanges` call + * delete models.todo; + */ +function debounce(func, wait, options) { + var args, + maxTimeoutId, + result, + stamp, + thisArg, + timeoutId, + trailingCall, + lastCalled = 0, + maxWait = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = wait < 0 ? 0 : (+wait || 0); + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = !!options.leading; + maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function cancel() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (maxTimeoutId) { + clearTimeout(maxTimeoutId); + } + lastCalled = 0; + maxTimeoutId = timeoutId = trailingCall = undefined; + } + + function complete(isCalled, id) { + if (id) { + clearTimeout(id); + } + maxTimeoutId = timeoutId = trailingCall = undefined; + if (isCalled) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + } + } + + function delayed() { + var remaining = wait - (now() - stamp); + if (remaining <= 0 || remaining > wait) { + complete(trailingCall, maxTimeoutId); + } else { + timeoutId = setTimeout(delayed, remaining); + } + } + + function maxDelayed() { + complete(trailing, timeoutId); + } + + function debounced() { + args = arguments; + stamp = now(); + thisArg = this; + trailingCall = trailing && (timeoutId || !leading); + + if (maxWait === false) { + var leadingCall = leading && !timeoutId; + } else { + if (!maxTimeoutId && !leading) { + lastCalled = stamp; + } + var remaining = maxWait - (stamp - lastCalled), + isCalled = remaining <= 0 || remaining > maxWait; + + if (isCalled) { + if (maxTimeoutId) { + maxTimeoutId = clearTimeout(maxTimeoutId); + } + lastCalled = stamp; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (isCalled && timeoutId) { + timeoutId = clearTimeout(timeoutId); + } + else if (!timeoutId && wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + if (leadingCall) { + isCalled = true; + result = func.apply(thisArg, args); + } + if (isCalled && !timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + return result; + } + debounced.cancel = cancel; + return debounced; +} + +module.exports = debounce; + +},{"../date/now":4,"../lang/isObject":33}],6:[function(_dereq_,module,exports){ +/** Used as the `TypeError` message for "Functions" methods. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Native method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max; + +/** + * Creates a function that invokes `func` with the `this` binding of the + * created function and arguments from `start` and beyond provided as an array. + * + * **Note:** This method is based on the [rest parameter](https://developer.mozilla.org/Web/JavaScript/Reference/Functions/rest_parameters). + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.restParam(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ +function restParam(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = nativeMax(start === undefined ? (func.length - 1) : (+start || 0), 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + rest = Array(length); + + while (++index < length) { + rest[index] = args[start + index]; + } + switch (start) { + case 0: return func.call(this, rest); + case 1: return func.call(this, args[0], rest); + case 2: return func.call(this, args[0], args[1], rest); + } + var otherArgs = Array(start + 1); + index = -1; + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = rest; + return func.apply(this, otherArgs); + }; +} + +module.exports = restParam; + +},{}],7:[function(_dereq_,module,exports){ +var debounce = _dereq_('./debounce'), + isObject = _dereq_('../lang/isObject'); + +/** Used as the `TypeError` message for "Functions" methods. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed invocations. Provide an options object to indicate + * that `func` should be invoked on the leading and/or trailing edge of the + * `wait` timeout. Subsequent calls to the throttled function return the + * result of the last `func` call. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the throttled function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=true] Specify invoking on the leading + * edge of the timeout. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // avoid excessively updating the position while scrolling + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // invoke `renewToken` when the click event is fired, but not more than once every 5 minutes + * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { + * 'trailing': false + * })); + * + * // cancel a trailing throttled call + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (options === false) { + leading = false; + } else if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { 'leading': leading, 'maxWait': +wait, 'trailing': trailing }); +} + +module.exports = throttle; + +},{"../lang/isObject":33,"./debounce":5}],8:[function(_dereq_,module,exports){ +/** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ +function arrayCopy(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; +} + +module.exports = arrayCopy; + +},{}],9:[function(_dereq_,module,exports){ +/** + * A specialized version of `_.forEach` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ +function arrayEach(array, iteratee) { + var index = -1, + length = array.length; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; +} + +module.exports = arrayEach; + +},{}],10:[function(_dereq_,module,exports){ +/** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property names to copy. + * @param {Object} [object={}] The object to copy properties to. + * @returns {Object} Returns `object`. + */ +function baseCopy(source, props, object) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + object[key] = source[key]; + } + return object; +} + +module.exports = baseCopy; + +},{}],11:[function(_dereq_,module,exports){ +var createBaseFor = _dereq_('./createBaseFor'); + +/** + * The base implementation of `baseForIn` and `baseForOwn` which iterates + * over `object` properties returned by `keysFunc` invoking `iteratee` for + * each property. Iteratee functions may exit iteration early by explicitly + * returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ +var baseFor = createBaseFor(); + +module.exports = baseFor; + +},{"./createBaseFor":18}],12:[function(_dereq_,module,exports){ +var baseFor = _dereq_('./baseFor'), + keysIn = _dereq_('../object/keysIn'); + +/** + * The base implementation of `_.forIn` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ +function baseForIn(object, iteratee) { + return baseFor(object, iteratee, keysIn); +} + +module.exports = baseForIn; + +},{"../object/keysIn":39,"./baseFor":11}],13:[function(_dereq_,module,exports){ +var arrayEach = _dereq_('./arrayEach'), + baseMergeDeep = _dereq_('./baseMergeDeep'), + isArray = _dereq_('../lang/isArray'), + isArrayLike = _dereq_('./isArrayLike'), + isObject = _dereq_('../lang/isObject'), + isObjectLike = _dereq_('./isObjectLike'), + isTypedArray = _dereq_('../lang/isTypedArray'), + keys = _dereq_('../object/keys'); + +/** + * The base implementation of `_.merge` without support for argument juggling, + * multiple sources, and `this` binding `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {Function} [customizer] The function to customize merged values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + * @returns {Object} Returns `object`. + */ +function baseMerge(object, source, customizer, stackA, stackB) { + if (!isObject(object)) { + return object; + } + var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), + props = isSrcArr ? undefined : keys(source); + + arrayEach(props || source, function(srcValue, key) { + if (props) { + key = srcValue; + srcValue = source[key]; + } + if (isObjectLike(srcValue)) { + stackA || (stackA = []); + stackB || (stackB = []); + baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB); + } + else { + var value = object[key], + result = customizer ? customizer(value, srcValue, key, object, source) : undefined, + isCommon = result === undefined; + + if (isCommon) { + result = srcValue; + } + if ((result !== undefined || (isSrcArr && !(key in object))) && + (isCommon || (result === result ? (result !== value) : (value === value)))) { + object[key] = result; + } + } + }); + return object; +} + +module.exports = baseMerge; + +},{"../lang/isArray":30,"../lang/isObject":33,"../lang/isTypedArray":36,"../object/keys":38,"./arrayEach":9,"./baseMergeDeep":14,"./isArrayLike":21,"./isObjectLike":26}],14:[function(_dereq_,module,exports){ +var arrayCopy = _dereq_('./arrayCopy'), + isArguments = _dereq_('../lang/isArguments'), + isArray = _dereq_('../lang/isArray'), + isArrayLike = _dereq_('./isArrayLike'), + isPlainObject = _dereq_('../lang/isPlainObject'), + isTypedArray = _dereq_('../lang/isTypedArray'), + toPlainObject = _dereq_('../lang/toPlainObject'); + +/** + * A specialized version of `baseMerge` for arrays and objects which performs + * deep merges and tracks traversed objects enabling objects with circular + * references to be merged. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {string} key The key of the value to merge. + * @param {Function} mergeFunc The function to merge values. + * @param {Function} [customizer] The function to customize merged values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ +function baseMergeDeep(object, source, key, mergeFunc, customizer, stackA, stackB) { + var length = stackA.length, + srcValue = source[key]; + + while (length--) { + if (stackA[length] == srcValue) { + object[key] = stackB[length]; + return; + } + } + var value = object[key], + result = customizer ? customizer(value, srcValue, key, object, source) : undefined, + isCommon = result === undefined; + + if (isCommon) { + result = srcValue; + if (isArrayLike(srcValue) && (isArray(srcValue) || isTypedArray(srcValue))) { + result = isArray(value) + ? value + : (isArrayLike(value) ? arrayCopy(value) : []); + } + else if (isPlainObject(srcValue) || isArguments(srcValue)) { + result = isArguments(value) + ? toPlainObject(value) + : (isPlainObject(value) ? value : {}); + } + else { + isCommon = false; + } + } + // Add the source value to the stack of traversed objects and associate + // it with its merged value. + stackA.push(srcValue); + stackB.push(result); + + if (isCommon) { + // Recursively merge objects and arrays (susceptible to call stack limits). + object[key] = mergeFunc(result, srcValue, customizer, stackA, stackB); + } else if (result === result ? (result !== value) : (value === value)) { + object[key] = result; + } +} + +module.exports = baseMergeDeep; + +},{"../lang/isArguments":29,"../lang/isArray":30,"../lang/isPlainObject":34,"../lang/isTypedArray":36,"../lang/toPlainObject":37,"./arrayCopy":8,"./isArrayLike":21}],15:[function(_dereq_,module,exports){ +var toObject = _dereq_('./toObject'); + +/** + * The base implementation of `_.property` without support for deep paths. + * + * @private + * @param {string} key The key of the property to get. + * @returns {Function} Returns the new function. + */ +function baseProperty(key) { + return function(object) { + return object == null ? undefined : toObject(object)[key]; + }; +} + +module.exports = baseProperty; + +},{"./toObject":28}],16:[function(_dereq_,module,exports){ +var identity = _dereq_('../utility/identity'); + +/** + * A specialized version of `baseCallback` which only supports `this` binding + * and specifying the number of arguments to provide to `func`. + * + * @private + * @param {Function} func The function to bind. + * @param {*} thisArg The `this` binding of `func`. + * @param {number} [argCount] The number of arguments to provide to `func`. + * @returns {Function} Returns the callback. + */ +function bindCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + if (thisArg === undefined) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + case 5: return function(value, other, key, object, source) { + return func.call(thisArg, value, other, key, object, source); + }; + } + return function() { + return func.apply(thisArg, arguments); + }; +} + +module.exports = bindCallback; + +},{"../utility/identity":42}],17:[function(_dereq_,module,exports){ +var bindCallback = _dereq_('./bindCallback'), + isIterateeCall = _dereq_('./isIterateeCall'), + restParam = _dereq_('../function/restParam'); + +/** + * Creates a `_.assign`, `_.defaults`, or `_.merge` function. + * + * @private + * @param {Function} assigner The function to assign values. + * @returns {Function} Returns the new assigner function. + */ +function createAssigner(assigner) { + return restParam(function(object, sources) { + var index = -1, + length = object == null ? 0 : sources.length, + customizer = length > 2 ? sources[length - 2] : undefined, + guard = length > 2 ? sources[2] : undefined, + thisArg = length > 1 ? sources[length - 1] : undefined; + + if (typeof customizer == 'function') { + customizer = bindCallback(customizer, thisArg, 5); + length -= 2; + } else { + customizer = typeof thisArg == 'function' ? thisArg : undefined; + length -= (customizer ? 1 : 0); + } + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + customizer = length < 3 ? undefined : customizer; + length = 1; + } + while (++index < length) { + var source = sources[index]; + if (source) { + assigner(object, source, customizer); + } + } + return object; + }); +} + +module.exports = createAssigner; + +},{"../function/restParam":6,"./bindCallback":16,"./isIterateeCall":24}],18:[function(_dereq_,module,exports){ +var toObject = _dereq_('./toObject'); + +/** + * Creates a base function for `_.forIn` or `_.forInRight`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ +function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var iterable = toObject(object), + props = keysFunc(object), + length = props.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length)) { + var key = props[index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; +} + +module.exports = createBaseFor; + +},{"./toObject":28}],19:[function(_dereq_,module,exports){ +var baseProperty = _dereq_('./baseProperty'); + +/** + * Gets the "length" property value of `object`. + * + * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792) + * that affects Safari on at least iOS 8.1-8.3 ARM64. + * + * @private + * @param {Object} object The object to query. + * @returns {*} Returns the "length" value. + */ +var getLength = baseProperty('length'); + +module.exports = getLength; + +},{"./baseProperty":15}],20:[function(_dereq_,module,exports){ +var isNative = _dereq_('../lang/isNative'); + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = object == null ? undefined : object[key]; + return isNative(value) ? value : undefined; +} + +module.exports = getNative; + +},{"../lang/isNative":32}],21:[function(_dereq_,module,exports){ +var getLength = _dereq_('./getLength'), + isLength = _dereq_('./isLength'); + +/** + * Checks if `value` is array-like. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + */ +function isArrayLike(value) { + return value != null && isLength(getLength(value)); +} + +module.exports = isArrayLike; + +},{"./getLength":19,"./isLength":25}],22:[function(_dereq_,module,exports){ +/** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ +var isHostObject = (function() { + try { + Object({ 'toString': 0 } + ''); + } catch(e) { + return function() { return false; }; + } + return function(value) { + // IE < 9 presents many host objects as `Object` objects that can coerce + // to strings despite having improperly defined `toString` methods. + return typeof value.toString != 'function' && typeof (value + '') == 'string'; + }; +}()); + +module.exports = isHostObject; + +},{}],23:[function(_dereq_,module,exports){ +/** Used to detect unsigned integer values. */ +var reIsUint = /^\d+$/; + +/** + * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer) + * of an array-like value. + */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ +function isIndex(value, length) { + value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1; + length = length == null ? MAX_SAFE_INTEGER : length; + return value > -1 && value % 1 == 0 && value < length; +} + +module.exports = isIndex; + +},{}],24:[function(_dereq_,module,exports){ +var isArrayLike = _dereq_('./isArrayLike'), + isIndex = _dereq_('./isIndex'), + isObject = _dereq_('../lang/isObject'); + +/** + * Checks if the provided arguments are from an iteratee call. + * + * @private + * @param {*} value The potential iteratee value argument. + * @param {*} index The potential iteratee index or key argument. + * @param {*} object The potential iteratee object argument. + * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`. + */ +function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object)) { + var other = object[index]; + return value === value ? (value === other) : (other !== other); + } + return false; +} + +module.exports = isIterateeCall; + +},{"../lang/isObject":33,"./isArrayLike":21,"./isIndex":23}],25:[function(_dereq_,module,exports){ +/** + * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer) + * of an array-like value. + */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** + * Checks if `value` is a valid array-like length. + * + * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + */ +function isLength(value) { + return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} + +module.exports = isLength; + +},{}],26:[function(_dereq_,module,exports){ +/** + * Checks if `value` is object-like. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + */ +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} + +module.exports = isObjectLike; + +},{}],27:[function(_dereq_,module,exports){ +var isArguments = _dereq_('../lang/isArguments'), + isArray = _dereq_('../lang/isArray'), + isIndex = _dereq_('./isIndex'), + isLength = _dereq_('./isLength'), + isString = _dereq_('../lang/isString'), + keysIn = _dereq_('../object/keysIn'); + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * A fallback implementation of `Object.keys` which creates an array of the + * own enumerable property names of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function shimKeys(object) { + var props = keysIn(object), + propsLength = props.length, + length = propsLength && object.length; + + var allowIndexes = !!length && isLength(length) && + (isArray(object) || isArguments(object) || isString(object)); + + var index = -1, + result = []; + + while (++index < propsLength) { + var key = props[index]; + if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) { + result.push(key); + } + } + return result; +} + +module.exports = shimKeys; + +},{"../lang/isArguments":29,"../lang/isArray":30,"../lang/isString":35,"../object/keysIn":39,"./isIndex":23,"./isLength":25}],28:[function(_dereq_,module,exports){ +var isObject = _dereq_('../lang/isObject'), + isString = _dereq_('../lang/isString'), + support = _dereq_('../support'); + +/** + * Converts `value` to an object if it's not one. + * + * @private + * @param {*} value The value to process. + * @returns {Object} Returns the object. + */ +function toObject(value) { + if (support.unindexedChars && isString(value)) { + var index = -1, + length = value.length, + result = Object(value); + + while (++index < length) { + result[index] = value.charAt(index); + } + return result; + } + return isObject(value) ? value : Object(value); +} + +module.exports = toObject; + +},{"../lang/isObject":33,"../lang/isString":35,"../support":41}],29:[function(_dereq_,module,exports){ +var isArrayLike = _dereq_('../internal/isArrayLike'), + isObjectLike = _dereq_('../internal/isObjectLike'); + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** Native method references. */ +var propertyIsEnumerable = objectProto.propertyIsEnumerable; + +/** + * Checks if `value` is classified as an `arguments` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ +function isArguments(value) { + return isObjectLike(value) && isArrayLike(value) && + hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee'); +} + +module.exports = isArguments; + +},{"../internal/isArrayLike":21,"../internal/isObjectLike":26}],30:[function(_dereq_,module,exports){ +var getNative = _dereq_('../internal/getNative'), + isLength = _dereq_('../internal/isLength'), + isObjectLike = _dereq_('../internal/isObjectLike'); + +/** `Object#toString` result references. */ +var arrayTag = '[object Array]'; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/* Native method references for those with the same name as other `lodash` methods. */ +var nativeIsArray = getNative(Array, 'isArray'); + +/** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(function() { return arguments; }()); + * // => false + */ +var isArray = nativeIsArray || function(value) { + return isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag; +}; + +module.exports = isArray; + +},{"../internal/getNative":20,"../internal/isLength":25,"../internal/isObjectLike":26}],31:[function(_dereq_,module,exports){ +var isObject = _dereq_('./isObject'); + +/** `Object#toString` result references. */ +var funcTag = '[object Function]'; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in older versions of Chrome and Safari which return 'function' for regexes + // and Safari 8 which returns 'object' for typed array constructors. + return isObject(value) && objToString.call(value) == funcTag; +} + +module.exports = isFunction; + +},{"./isObject":33}],32:[function(_dereq_,module,exports){ +var isFunction = _dereq_('./isFunction'), + isHostObject = _dereq_('../internal/isHostObject'), + isObjectLike = _dereq_('../internal/isObjectLike'); + +/** Used to detect host constructors (Safari > 5). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var fnToString = Function.prototype.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + fnToString.call(hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * Checks if `value` is a native function. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, else `false`. + * @example + * + * _.isNative(Array.prototype.push); + * // => true + * + * _.isNative(_); + * // => false + */ +function isNative(value) { + if (value == null) { + return false; + } + if (isFunction(value)) { + return reIsNative.test(fnToString.call(value)); + } + return isObjectLike(value) && (isHostObject(value) ? reIsNative : reIsHostCtor).test(value); +} + +module.exports = isNative; + +},{"../internal/isHostObject":22,"../internal/isObjectLike":26,"./isFunction":31}],33:[function(_dereq_,module,exports){ +/** + * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ +function isObject(value) { + // Avoid a V8 JIT bug in Chrome 19-20. + // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} + +module.exports = isObject; + +},{}],34:[function(_dereq_,module,exports){ +var baseForIn = _dereq_('../internal/baseForIn'), + isArguments = _dereq_('./isArguments'), + isHostObject = _dereq_('../internal/isHostObject'), + isObjectLike = _dereq_('../internal/isObjectLike'), + support = _dereq_('../support'); + +/** `Object#toString` result references. */ +var objectTag = '[object Object]'; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/** + * Checks if `value` is a plain object, that is, an object created by the + * `Object` constructor or one with a `[[Prototype]]` of `null`. + * + * **Note:** This method assumes objects created by the `Object` constructor + * have no inherited enumerable properties. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * _.isPlainObject(new Foo); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + * + * _.isPlainObject(Object.create(null)); + * // => true + */ +function isPlainObject(value) { + var Ctor; + + // Exit early for non `Object` objects. + if (!(isObjectLike(value) && objToString.call(value) == objectTag && !isHostObject(value) && !isArguments(value)) || + (!hasOwnProperty.call(value, 'constructor') && (Ctor = value.constructor, typeof Ctor == 'function' && !(Ctor instanceof Ctor)))) { + return false; + } + // IE < 9 iterates inherited properties before own properties. If the first + // iterated property is an object's own property then there are no inherited + // enumerable properties. + var result; + if (support.ownLast) { + baseForIn(value, function(subValue, key, object) { + result = hasOwnProperty.call(object, key); + return false; + }); + return result !== false; + } + // In most environments an object's own properties are iterated before + // its inherited properties. If the last iterated property is an object's + // own property then there are no inherited enumerable properties. + baseForIn(value, function(subValue, key) { + result = key; + }); + return result === undefined || hasOwnProperty.call(value, result); +} + +module.exports = isPlainObject; + +},{"../internal/baseForIn":12,"../internal/isHostObject":22,"../internal/isObjectLike":26,"../support":41,"./isArguments":29}],35:[function(_dereq_,module,exports){ +var isObjectLike = _dereq_('../internal/isObjectLike'); + +/** `Object#toString` result references. */ +var stringTag = '[object String]'; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ +function isString(value) { + return typeof value == 'string' || (isObjectLike(value) && objToString.call(value) == stringTag); +} + +module.exports = isString; + +},{"../internal/isObjectLike":26}],36:[function(_dereq_,module,exports){ +var isLength = _dereq_('../internal/isLength'), + isObjectLike = _dereq_('../internal/isObjectLike'); + +/** `Object#toString` result references. */ +var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + weakMapTag = '[object WeakMap]'; + +var arrayBufferTag = '[object ArrayBuffer]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + +/** Used to identify `toStringTag` values of typed arrays. */ +var typedArrayTags = {}; +typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = +typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = +typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = +typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = +typedArrayTags[uint32Tag] = true; +typedArrayTags[argsTag] = typedArrayTags[arrayTag] = +typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = +typedArrayTags[dateTag] = typedArrayTags[errorTag] = +typedArrayTags[funcTag] = typedArrayTags[mapTag] = +typedArrayTags[numberTag] = typedArrayTags[objectTag] = +typedArrayTags[regexpTag] = typedArrayTags[setTag] = +typedArrayTags[stringTag] = typedArrayTags[weakMapTag] = false; + +/** Used for native method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/** + * Checks if `value` is classified as a typed array. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isTypedArray(new Uint8Array); + * // => true + * + * _.isTypedArray([]); + * // => false + */ +function isTypedArray(value) { + return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[objToString.call(value)]; +} + +module.exports = isTypedArray; + +},{"../internal/isLength":25,"../internal/isObjectLike":26}],37:[function(_dereq_,module,exports){ +var baseCopy = _dereq_('../internal/baseCopy'), + keysIn = _dereq_('../object/keysIn'); + +/** + * Converts `value` to a plain object flattening inherited enumerable + * properties of `value` to own properties of the plain object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Object} Returns the converted plain object. + * @example + * + * function Foo() { + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.assign({ 'a': 1 }, new Foo); + * // => { 'a': 1, 'b': 2 } + * + * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); + * // => { 'a': 1, 'b': 2, 'c': 3 } + */ +function toPlainObject(value) { + return baseCopy(value, keysIn(value)); +} + +module.exports = toPlainObject; + +},{"../internal/baseCopy":10,"../object/keysIn":39}],38:[function(_dereq_,module,exports){ +var getNative = _dereq_('../internal/getNative'), + isArrayLike = _dereq_('../internal/isArrayLike'), + isObject = _dereq_('../lang/isObject'), + shimKeys = _dereq_('../internal/shimKeys'), + support = _dereq_('../support'); + +/* Native method references for those with the same name as other `lodash` methods. */ +var nativeKeys = getNative(Object, 'keys'); + +/** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys) + * for more details. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ +var keys = !nativeKeys ? shimKeys : function(object) { + var Ctor = object == null ? undefined : object.constructor; + if ((typeof Ctor == 'function' && Ctor.prototype === object) || + (typeof object == 'function' ? support.enumPrototypes : isArrayLike(object))) { + return shimKeys(object); + } + return isObject(object) ? nativeKeys(object) : []; +}; + +module.exports = keys; + +},{"../internal/getNative":20,"../internal/isArrayLike":21,"../internal/shimKeys":27,"../lang/isObject":33,"../support":41}],39:[function(_dereq_,module,exports){ +var arrayEach = _dereq_('../internal/arrayEach'), + isArguments = _dereq_('../lang/isArguments'), + isArray = _dereq_('../lang/isArray'), + isFunction = _dereq_('../lang/isFunction'), + isIndex = _dereq_('../internal/isIndex'), + isLength = _dereq_('../internal/isLength'), + isObject = _dereq_('../lang/isObject'), + isString = _dereq_('../lang/isString'), + support = _dereq_('../support'); + +/** `Object#toString` result references. */ +var arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + numberTag = '[object Number]', + objectTag = '[object Object]', + regexpTag = '[object RegExp]', + stringTag = '[object String]'; + +/** Used to fix the JScript `[[DontEnum]]` bug. */ +var shadowProps = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', + 'toLocaleString', 'toString', 'valueOf' +]; + +/** Used for native method references. */ +var errorProto = Error.prototype, + objectProto = Object.prototype, + stringProto = String.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ +var objToString = objectProto.toString; + +/** Used to avoid iterating over non-enumerable properties in IE < 9. */ +var nonEnumProps = {}; +nonEnumProps[arrayTag] = nonEnumProps[dateTag] = nonEnumProps[numberTag] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; +nonEnumProps[boolTag] = nonEnumProps[stringTag] = { 'constructor': true, 'toString': true, 'valueOf': true }; +nonEnumProps[errorTag] = nonEnumProps[funcTag] = nonEnumProps[regexpTag] = { 'constructor': true, 'toString': true }; +nonEnumProps[objectTag] = { 'constructor': true }; + +arrayEach(shadowProps, function(key) { + for (var tag in nonEnumProps) { + if (hasOwnProperty.call(nonEnumProps, tag)) { + var props = nonEnumProps[tag]; + props[key] = hasOwnProperty.call(props, key); + } + } +}); + +/** + * Creates an array of the own and inherited enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keysIn(new Foo); + * // => ['a', 'b', 'c'] (iteration order is not guaranteed) + */ +function keysIn(object) { + if (object == null) { + return []; + } + if (!isObject(object)) { + object = Object(object); + } + var length = object.length; + + length = (length && isLength(length) && + (isArray(object) || isArguments(object) || isString(object)) && length) || 0; + + var Ctor = object.constructor, + index = -1, + proto = (isFunction(Ctor) && Ctor.prototype) || objectProto, + isProto = proto === object, + result = Array(length), + skipIndexes = length > 0, + skipErrorProps = support.enumErrorProps && (object === errorProto || object instanceof Error), + skipProto = support.enumPrototypes && isFunction(object); + + while (++index < length) { + result[index] = (index + ''); + } + // lodash skips the `constructor` property when it infers it's iterating + // over a `prototype` object because IE < 9 can't set the `[[Enumerable]]` + // attribute of an existing property and the `constructor` property of a + // prototype defaults to non-enumerable. + for (var key in object) { + if (!(skipProto && key == 'prototype') && + !(skipErrorProps && (key == 'message' || key == 'name')) && + !(skipIndexes && isIndex(key, length)) && + !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { + result.push(key); + } + } + if (support.nonEnumShadows && object !== objectProto) { + var tag = object === stringProto ? stringTag : (object === errorProto ? errorTag : objToString.call(object)), + nonEnums = nonEnumProps[tag] || nonEnumProps[objectTag]; + + if (tag == objectTag) { + proto = objectProto; + } + length = shadowProps.length; + while (length--) { + key = shadowProps[length]; + var nonEnum = nonEnums[key]; + if (!(isProto && nonEnum) && + (nonEnum ? hasOwnProperty.call(object, key) : object[key] !== proto[key])) { + result.push(key); + } + } + } + return result; +} + +module.exports = keysIn; + +},{"../internal/arrayEach":9,"../internal/isIndex":23,"../internal/isLength":25,"../lang/isArguments":29,"../lang/isArray":30,"../lang/isFunction":31,"../lang/isObject":33,"../lang/isString":35,"../support":41}],40:[function(_dereq_,module,exports){ +var baseMerge = _dereq_('../internal/baseMerge'), + createAssigner = _dereq_('../internal/createAssigner'); + +/** + * Recursively merges own enumerable properties of the source object(s), that + * don't resolve to `undefined` into the destination object. Subsequent sources + * overwrite property assignments of previous sources. If `customizer` is + * provided it's invoked to produce the merged values of the destination and + * source properties. If `customizer` returns `undefined` merging is handled + * by the method instead. The `customizer` is bound to `thisArg` and invoked + * with five arguments: (objectValue, sourceValue, key, object, source). + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {Object} Returns `object`. + * @example + * + * var users = { + * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] + * }; + * + * var ages = { + * 'data': [{ 'age': 36 }, { 'age': 40 }] + * }; + * + * _.merge(users, ages); + * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } + * + * // using a customizer callback + * var object = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var other = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.merge(object, other, function(a, b) { + * if (_.isArray(a)) { + * return a.concat(b); + * } + * }); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } + */ +var merge = createAssigner(baseMerge); + +module.exports = merge; + +},{"../internal/baseMerge":13,"../internal/createAssigner":17}],41:[function(_dereq_,module,exports){ +/** Used for native method references. */ +var arrayProto = Array.prototype, + errorProto = Error.prototype, + objectProto = Object.prototype; + +/** Native method references. */ +var propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; + +/** + * An object environment feature flags. + * + * @static + * @memberOf _ + * @type Object + */ +var support = {}; + +(function(x) { + var Ctor = function() { this.x = x; }, + object = { '0': x, 'length': x }, + props = []; + + Ctor.prototype = { 'valueOf': x, 'y': x }; + for (var key in new Ctor) { props.push(key); } + + /** + * Detect if `name` or `message` properties of `Error.prototype` are + * enumerable by default (IE < 9, Safari < 5.1). + * + * @memberOf _.support + * @type boolean + */ + support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || + propertyIsEnumerable.call(errorProto, 'name'); + + /** + * Detect if `prototype` properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 + * (if the prototype or a property on the prototype has been set) + * incorrectly set the `[[Enumerable]]` value of a function's `prototype` + * property to `true`. + * + * @memberOf _.support + * @type boolean + */ + support.enumPrototypes = propertyIsEnumerable.call(Ctor, 'prototype'); + + /** + * Detect if properties shadowing those on `Object.prototype` are non-enumerable. + * + * In IE < 9 an object's own properties, shadowing non-enumerable ones, + * are made non-enumerable as well (a.k.a the JScript `[[DontEnum]]` bug). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumShadows = !/valueOf/.test(props); + + /** + * Detect if own properties are iterated after inherited properties (IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.ownLast = props[0] != 'x'; + + /** + * Detect if `Array#shift` and `Array#splice` augment array-like objects + * correctly. + * + * Firefox < 10, compatibility modes of IE 8, and IE < 9 have buggy Array + * `shift()` and `splice()` functions that fail to remove the last element, + * `value[0]`, of array-like objects even though the "length" property is + * set to `0`. The `shift()` method is buggy in compatibility modes of IE 8, + * while `splice()` is buggy regardless of mode in IE < 9. + * + * @memberOf _.support + * @type boolean + */ + support.spliceObjects = (splice.call(object, 0, 1), !object[0]); + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index. IE 8 can only access characters + * by index on string literals, not string objects. + * + * @memberOf _.support + * @type boolean + */ + support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; +}(1, 0)); + +module.exports = support; + +},{}],42:[function(_dereq_,module,exports){ +/** + * This method returns the first argument provided to it. + * + * @static + * @memberOf _ + * @category Utility + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'user': 'fred' }; + * + * _.identity(object) === object; + * // => true + */ +function identity(value) { + return value; +} + +module.exports = identity; + +},{}],43:[function(_dereq_,module,exports){ +'use strict'; + +var keys = _dereq_('object-keys'); + +module.exports = function hasSymbols() { + if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; } + if (typeof Symbol.iterator === 'symbol') { return true; } + + var obj = {}; + var sym = Symbol('test'); + if (typeof sym === 'string') { return false; } + + // temp disabled per https://github.com/ljharb/object.assign/issues/17 + // if (sym instanceof Symbol) { return false; } + // temp disabled per https://github.com/WebReflection/get-own-property-symbols/issues/4 + // if (!(Object(sym) instanceof Symbol)) { return false; } + + var symVal = 42; + obj[sym] = symVal; + for (sym in obj) { return false; } + if (keys(obj).length !== 0) { return false; } + if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; } + + if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; } + + var syms = Object.getOwnPropertySymbols(obj); + if (syms.length !== 1 || syms[0] !== sym) { return false; } + + if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; } + + if (typeof Object.getOwnPropertyDescriptor === 'function') { + var descriptor = Object.getOwnPropertyDescriptor(obj, sym); + if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; } + } + + return true; +}; + +},{"object-keys":50}],44:[function(_dereq_,module,exports){ +'use strict'; + +// modified from https://github.com/es-shims/es6-shim +var keys = _dereq_('object-keys'); +var bind = _dereq_('function-bind'); +var canBeObject = function (obj) { + return typeof obj !== 'undefined' && obj !== null; +}; +var hasSymbols = _dereq_('./hasSymbols')(); +var toObject = Object; +var push = bind.call(Function.call, Array.prototype.push); +var propIsEnumerable = bind.call(Function.call, Object.prototype.propertyIsEnumerable); + +module.exports = function assign(target, source1) { + if (!canBeObject(target)) { throw new TypeError('target must be an object'); } + var objTarget = toObject(target); + var s, source, i, props, syms, value, key; + for (s = 1; s < arguments.length; ++s) { + source = toObject(arguments[s]); + props = keys(source); + if (hasSymbols && Object.getOwnPropertySymbols) { + syms = Object.getOwnPropertySymbols(source); + for (i = 0; i < syms.length; ++i) { + key = syms[i]; + if (propIsEnumerable(source, key)) { + push(props, key); + } + } + } + for (i = 0; i < props.length; ++i) { + key = props[i]; + value = source[key]; + if (propIsEnumerable(source, key)) { + objTarget[key] = value; + } + } + } + return objTarget; +}; + +},{"./hasSymbols":43,"function-bind":49,"object-keys":50}],45:[function(_dereq_,module,exports){ +'use strict'; + +var defineProperties = _dereq_('define-properties'); + +var implementation = _dereq_('./implementation'); +var getPolyfill = _dereq_('./polyfill'); +var shim = _dereq_('./shim'); + +defineProperties(implementation, { + implementation: implementation, + getPolyfill: getPolyfill, + shim: shim +}); + +module.exports = implementation; + +},{"./implementation":44,"./polyfill":52,"./shim":53,"define-properties":46}],46:[function(_dereq_,module,exports){ +'use strict'; + +var keys = _dereq_('object-keys'); +var foreach = _dereq_('foreach'); +var hasSymbols = typeof Symbol === 'function' && typeof Symbol() === 'symbol'; + +var toStr = Object.prototype.toString; + +var isFunction = function (fn) { + return typeof fn === 'function' && toStr.call(fn) === '[object Function]'; +}; + +var arePropertyDescriptorsSupported = function () { + var obj = {}; + try { + Object.defineProperty(obj, 'x', { enumerable: false, value: obj }); + /* eslint-disable no-unused-vars, no-restricted-syntax */ + for (var _ in obj) { return false; } + /* eslint-enable no-unused-vars, no-restricted-syntax */ + return obj.x === obj; + } catch (e) { /* this is IE 8. */ + return false; + } +}; +var supportsDescriptors = Object.defineProperty && arePropertyDescriptorsSupported(); + +var defineProperty = function (object, name, value, predicate) { + if (name in object && (!isFunction(predicate) || !predicate())) { + return; + } + if (supportsDescriptors) { + Object.defineProperty(object, name, { + configurable: true, + enumerable: false, + value: value, + writable: true + }); + } else { + object[name] = value; + } +}; + +var defineProperties = function (object, map) { + var predicates = arguments.length > 2 ? arguments[2] : {}; + var props = keys(map); + if (hasSymbols) { + props = props.concat(Object.getOwnPropertySymbols(map)); + } + foreach(props, function (name) { + defineProperty(object, name, map[name], predicates[name]); + }); +}; + +defineProperties.supportsDescriptors = !!supportsDescriptors; + +module.exports = defineProperties; + +},{"foreach":47,"object-keys":50}],47:[function(_dereq_,module,exports){ + +var hasOwn = Object.prototype.hasOwnProperty; +var toString = Object.prototype.toString; + +module.exports = function forEach (obj, fn, ctx) { + if (toString.call(fn) !== '[object Function]') { + throw new TypeError('iterator must be a function'); + } + var l = obj.length; + if (l === +l) { + for (var i = 0; i < l; i++) { + fn.call(ctx, obj[i], i, obj); + } + } else { + for (var k in obj) { + if (hasOwn.call(obj, k)) { + fn.call(ctx, obj[k], k, obj); + } + } + } +}; + + +},{}],48:[function(_dereq_,module,exports){ +var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible '; +var slice = Array.prototype.slice; +var toStr = Object.prototype.toString; +var funcType = '[object Function]'; + +module.exports = function bind(that) { + var target = this; + if (typeof target !== 'function' || toStr.call(target) !== funcType) { + throw new TypeError(ERROR_MESSAGE + target); + } + var args = slice.call(arguments, 1); + + var bound; + var binder = function () { + if (this instanceof bound) { + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + } + }; + + var boundLength = Math.max(0, target.length - args.length); + var boundArgs = []; + for (var i = 0; i < boundLength; i++) { + boundArgs.push('$' + i); + } + + bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder); + + if (target.prototype) { + var Empty = function Empty() {}; + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + + return bound; +}; + +},{}],49:[function(_dereq_,module,exports){ +var implementation = _dereq_('./implementation'); + +module.exports = Function.prototype.bind || implementation; + +},{"./implementation":48}],50:[function(_dereq_,module,exports){ +'use strict'; + +// modified from https://github.com/es-shims/es5-shim +var has = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var slice = Array.prototype.slice; +var isArgs = _dereq_('./isArguments'); +var hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'); +var hasProtoEnumBug = function () {}.propertyIsEnumerable('prototype'); +var dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' +]; +var equalsConstructorPrototype = function (o) { + var ctor = o.constructor; + return ctor && ctor.prototype === o; +}; +var blacklistedKeys = { + $console: true, + $frame: true, + $frameElement: true, + $frames: true, + $parent: true, + $self: true, + $webkitIndexedDB: true, + $webkitStorageInfo: true, + $window: true +}; +var hasAutomationEqualityBug = (function () { + /* global window */ + if (typeof window === 'undefined') { return false; } + for (var k in window) { + try { + if (!blacklistedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') { + try { + equalsConstructorPrototype(window[k]); + } catch (e) { + return true; + } + } + } catch (e) { + return true; + } + } + return false; +}()); +var equalsConstructorPrototypeIfNotBuggy = function (o) { + /* global window */ + if (typeof window === 'undefined' || !hasAutomationEqualityBug) { + return equalsConstructorPrototype(o); + } + try { + return equalsConstructorPrototype(o); + } catch (e) { + return false; + } +}; + +var keysShim = function keys(object) { + var isObject = object !== null && typeof object === 'object'; + var isFunction = toStr.call(object) === '[object Function]'; + var isArguments = isArgs(object); + var isString = isObject && toStr.call(object) === '[object String]'; + var theKeys = []; + + if (!isObject && !isFunction && !isArguments) { + throw new TypeError('Object.keys called on a non-object'); + } + + var skipProto = hasProtoEnumBug && isFunction; + if (isString && object.length > 0 && !has.call(object, 0)) { + for (var i = 0; i < object.length; ++i) { + theKeys.push(String(i)); + } + } + + if (isArguments && object.length > 0) { + for (var j = 0; j < object.length; ++j) { + theKeys.push(String(j)); + } + } else { + for (var name in object) { + if (!(skipProto && name === 'prototype') && has.call(object, name)) { + theKeys.push(String(name)); + } + } + } + + if (hasDontEnumBug) { + var skipConstructor = equalsConstructorPrototypeIfNotBuggy(object); + + for (var k = 0; k < dontEnums.length; ++k) { + if (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) { + theKeys.push(dontEnums[k]); + } + } + } + return theKeys; +}; + +keysShim.shim = function shimObjectKeys() { + if (Object.keys) { + var keysWorksWithArguments = (function () { + // Safari 5.0 bug + return (Object.keys(arguments) || '').length === 2; + }(1, 2)); + if (!keysWorksWithArguments) { + var originalKeys = Object.keys; + Object.keys = function keys(object) { + if (isArgs(object)) { + return originalKeys(slice.call(object)); + } else { + return originalKeys(object); + } + }; + } + } else { + Object.keys = keysShim; + } + return Object.keys || keysShim; +}; + +module.exports = keysShim; + +},{"./isArguments":51}],51:[function(_dereq_,module,exports){ +'use strict'; + +var toStr = Object.prototype.toString; + +module.exports = function isArguments(value) { + var str = toStr.call(value); + var isArgs = str === '[object Arguments]'; + if (!isArgs) { + isArgs = str !== '[object Array]' && + value !== null && + typeof value === 'object' && + typeof value.length === 'number' && + value.length >= 0 && + toStr.call(value.callee) === '[object Function]'; + } + return isArgs; +}; + +},{}],52:[function(_dereq_,module,exports){ +'use strict'; + +var implementation = _dereq_('./implementation'); + +var lacksProperEnumerationOrder = function () { + if (!Object.assign) { + return false; + } + // v8, specifically in node 4.x, has a bug with incorrect property enumeration order + // note: this does not detect the bug unless there's 20 characters + var str = 'abcdefghijklmnopqrst'; + var letters = str.split(''); + var map = {}; + for (var i = 0; i < letters.length; ++i) { + map[letters[i]] = letters[i]; + } + var obj = Object.assign({}, map); + var actual = ''; + for (var k in obj) { + actual += k; + } + return str !== actual; +}; + +var assignHasPendingExceptions = function () { + if (!Object.assign || !Object.preventExtensions) { + return false; + } + // Firefox 37 still has "pending exception" logic in its Object.assign implementation, + // which is 72% slower than our shim, and Firefox 40's native implementation. + var thrower = Object.preventExtensions({ 1: 2 }); + try { + Object.assign(thrower, 'xy'); + } catch (e) { + return thrower[1] === 'y'; + } +}; + +module.exports = function getPolyfill() { + if (!Object.assign) { + return implementation; + } + if (lacksProperEnumerationOrder()) { + return implementation; + } + if (assignHasPendingExceptions()) { + return implementation; + } + return Object.assign; +}; + +},{"./implementation":44}],53:[function(_dereq_,module,exports){ +'use strict'; + +var define = _dereq_('define-properties'); +var getPolyfill = _dereq_('./polyfill'); + +module.exports = function shimAssign() { + var polyfill = getPolyfill(); + define( + Object, + { assign: polyfill }, + { assign: function () { return Object.assign !== polyfill; } } + ); + return polyfill; +}; + +},{"./polyfill":52,"define-properties":46}],54:[function(_dereq_,module,exports){ +module.exports = SafeParseTuple + +function SafeParseTuple(obj, reviver) { + var json + var error = null + + try { + json = JSON.parse(obj, reviver) + } catch (err) { + error = err + } + + return [error, json] +} + +},{}],55:[function(_dereq_,module,exports){ +function clean (s) { + return s.replace(/\n\r?\s*/g, '') +} + + +module.exports = function tsml (sa) { + var s = '' + , i = 0 + + for (; i < arguments.length; i++) + s += clean(sa[i]) + (arguments[i + 1] || '') + + return s +} +},{}],56:[function(_dereq_,module,exports){ +"use strict"; +var window = _dereq_("global/window") +var once = _dereq_("once") +var isFunction = _dereq_("is-function") +var parseHeaders = _dereq_("parse-headers") +var xtend = _dereq_("xtend") + +module.exports = createXHR +createXHR.XMLHttpRequest = window.XMLHttpRequest || noop +createXHR.XDomainRequest = "withCredentials" in (new createXHR.XMLHttpRequest()) ? createXHR.XMLHttpRequest : window.XDomainRequest + +forEachArray(["get", "put", "post", "patch", "head", "delete"], function(method) { + createXHR[method === "delete" ? "del" : method] = function(uri, options, callback) { + options = initParams(uri, options, callback) + options.method = method.toUpperCase() + return _createXHR(options) + } +}) + +function forEachArray(array, iterator) { + for (var i = 0; i < array.length; i++) { + iterator(array[i]) + } +} + +function isEmpty(obj){ + for(var i in obj){ + if(obj.hasOwnProperty(i)) return false + } + return true +} + +function initParams(uri, options, callback) { + var params = uri + + if (isFunction(options)) { + callback = options + if (typeof uri === "string") { + params = {uri:uri} + } + } else { + params = xtend(options, {uri: uri}) + } + + params.callback = callback + return params +} + +function createXHR(uri, options, callback) { + options = initParams(uri, options, callback) + return _createXHR(options) +} + +function _createXHR(options) { + var callback = options.callback + if(typeof callback === "undefined"){ + throw new Error("callback argument missing") + } + callback = once(callback) + + function readystatechange() { + if (xhr.readyState === 4) { + loadFunc() + } + } + + function getBody() { + // Chrome with requestType=blob throws errors arround when even testing access to responseText + var body = undefined + + if (xhr.response) { + body = xhr.response + } else if (xhr.responseType === "text" || !xhr.responseType) { + body = xhr.responseText || xhr.responseXML + } + + if (isJson) { + try { + body = JSON.parse(body) + } catch (e) {} + } + + return body + } + + var failureResponse = { + body: undefined, + headers: {}, + statusCode: 0, + method: method, + url: uri, + rawRequest: xhr + } + + function errorFunc(evt) { + clearTimeout(timeoutTimer) + if(!(evt instanceof Error)){ + evt = new Error("" + (evt || "Unknown XMLHttpRequest Error") ) + } + evt.statusCode = 0 + callback(evt, failureResponse) + } + + // will load the data & process the response in a special response object + function loadFunc() { + if (aborted) return + var status + clearTimeout(timeoutTimer) + if(options.useXDR && xhr.status===undefined) { + //IE8 CORS GET successful response doesn't have a status field, but body is fine + status = 200 + } else { + status = (xhr.status === 1223 ? 204 : xhr.status) + } + var response = failureResponse + var err = null + + if (status !== 0){ + response = { + body: getBody(), + statusCode: status, + method: method, + headers: {}, + url: uri, + rawRequest: xhr + } + if(xhr.getAllResponseHeaders){ //remember xhr can in fact be XDR for CORS in IE + response.headers = parseHeaders(xhr.getAllResponseHeaders()) + } + } else { + err = new Error("Internal XMLHttpRequest Error") + } + callback(err, response, response.body) + + } + + var xhr = options.xhr || null + + if (!xhr) { + if (options.cors || options.useXDR) { + xhr = new createXHR.XDomainRequest() + }else{ + xhr = new createXHR.XMLHttpRequest() + } + } + + var key + var aborted + var uri = xhr.url = options.uri || options.url + var method = xhr.method = options.method || "GET" + var body = options.body || options.data || null + var headers = xhr.headers = options.headers || {} + var sync = !!options.sync + var isJson = false + var timeoutTimer + + if ("json" in options) { + isJson = true + headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json") //Don't override existing accept header declared by user + if (method !== "GET" && method !== "HEAD") { + headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json") //Don't override existing accept header declared by user + body = JSON.stringify(options.json) + } + } + + xhr.onreadystatechange = readystatechange + xhr.onload = loadFunc + xhr.onerror = errorFunc + // IE9 must have onprogress be set to a unique function. + xhr.onprogress = function () { + // IE must die + } + xhr.ontimeout = errorFunc + xhr.open(method, uri, !sync, options.username, options.password) + //has to be after open + if(!sync) { + xhr.withCredentials = !!options.withCredentials + } + // Cannot set timeout with sync request + // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly + // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent + if (!sync && options.timeout > 0 ) { + timeoutTimer = setTimeout(function(){ + aborted=true//IE9 may still call readystatechange + xhr.abort("timeout") + var e = new Error("XMLHttpRequest timeout") + e.code = "ETIMEDOUT" + errorFunc(e) + }, options.timeout ) + } + + if (xhr.setRequestHeader) { + for(key in headers){ + if(headers.hasOwnProperty(key)){ + xhr.setRequestHeader(key, headers[key]) + } + } + } else if (options.headers && !isEmpty(options.headers)) { + throw new Error("Headers cannot be set on an XDomainRequest object") + } + + if ("responseType" in options) { + xhr.responseType = options.responseType + } + + if ("beforeSend" in options && + typeof options.beforeSend === "function" + ) { + options.beforeSend(xhr) + } + + xhr.send(body) + + return xhr + + +} + +function noop() {} + +},{"global/window":2,"is-function":57,"once":58,"parse-headers":61,"xtend":62}],57:[function(_dereq_,module,exports){ +module.exports = isFunction + +var toString = Object.prototype.toString + +function isFunction (fn) { + var string = toString.call(fn) + return string === '[object Function]' || + (typeof fn === 'function' && string !== '[object RegExp]') || + (typeof window !== 'undefined' && + // IE8 and below + (fn === window.setTimeout || + fn === window.alert || + fn === window.confirm || + fn === window.prompt)) +}; + +},{}],58:[function(_dereq_,module,exports){ +module.exports = once + +once.proto = once(function () { + Object.defineProperty(Function.prototype, 'once', { + value: function () { + return once(this) + }, + configurable: true + }) +}) + +function once (fn) { + var called = false + return function () { + if (called) return + called = true + return fn.apply(this, arguments) + } +} + +},{}],59:[function(_dereq_,module,exports){ +var isFunction = _dereq_('is-function') + +module.exports = forEach + +var toString = Object.prototype.toString +var hasOwnProperty = Object.prototype.hasOwnProperty + +function forEach(list, iterator, context) { + if (!isFunction(iterator)) { + throw new TypeError('iterator must be a function') + } + + if (arguments.length < 3) { + context = this + } + + if (toString.call(list) === '[object Array]') + forEachArray(list, iterator, context) + else if (typeof list === 'string') + forEachString(list, iterator, context) + else + forEachObject(list, iterator, context) +} + +function forEachArray(array, iterator, context) { + for (var i = 0, len = array.length; i < len; i++) { + if (hasOwnProperty.call(array, i)) { + iterator.call(context, array[i], i, array) + } + } +} + +function forEachString(string, iterator, context) { + for (var i = 0, len = string.length; i < len; i++) { + // no such thing as a sparse string. + iterator.call(context, string.charAt(i), i, string) + } +} + +function forEachObject(object, iterator, context) { + for (var k in object) { + if (hasOwnProperty.call(object, k)) { + iterator.call(context, object[k], k, object) + } + } +} + +},{"is-function":57}],60:[function(_dereq_,module,exports){ + +exports = module.exports = trim; + +function trim(str){ + return str.replace(/^\s*|\s*$/g, ''); +} + +exports.left = function(str){ + return str.replace(/^\s*/, ''); +}; + +exports.right = function(str){ + return str.replace(/\s*$/, ''); +}; + +},{}],61:[function(_dereq_,module,exports){ +var trim = _dereq_('trim') + , forEach = _dereq_('for-each') + , isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + } + +module.exports = function (headers) { + if (!headers) + return {} + + var result = {} + + forEach( + trim(headers).split('\n') + , function (row) { + var index = row.indexOf(':') + , key = trim(row.slice(0, index)).toLowerCase() + , value = trim(row.slice(index + 1)) + + if (typeof(result[key]) === 'undefined') { + result[key] = value + } else if (isArray(result[key])) { + result[key].push(value) + } else { + result[key] = [ result[key], value ] + } + } + ) + + return result +} +},{"for-each":59,"trim":60}],62:[function(_dereq_,module,exports){ +module.exports = extend + +var hasOwnProperty = Object.prototype.hasOwnProperty; + +function extend() { + var target = {} + + for (var i = 0; i < arguments.length; i++) { + var source = arguments[i] + + for (var key in source) { + if (hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + } + + return target +} + +},{}],63:[function(_dereq_,module,exports){ +/** + * @file big-play-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _buttonJs = _dereq_('./button.js'); + +var _buttonJs2 = _interopRequireDefault(_buttonJs); + +var _componentJs = _dereq_('./component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Initial play button. Shows before the video has played. The hiding of the + * big play button is done via CSS and player states. + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @extends Button + * @class BigPlayButton + */ + +var BigPlayButton = (function (_Button) { + _inherits(BigPlayButton, _Button); + + function BigPlayButton(player, options) { + _classCallCheck(this, BigPlayButton); + + _Button.call(this, player, options); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + BigPlayButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-big-play-button'; + }; + + /** + * Handles click for play + * + * @method handleClick + */ + + BigPlayButton.prototype.handleClick = function handleClick() { + this.player_.play(); + }; + + return BigPlayButton; +})(_buttonJs2['default']); + +BigPlayButton.prototype.controlText_ = 'Play Video'; + +_componentJs2['default'].registerComponent('BigPlayButton', BigPlayButton); +exports['default'] = BigPlayButton; +module.exports = exports['default']; + +},{"./button.js":64,"./component.js":67}],64:[function(_dereq_,module,exports){ +/** + * @file button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _clickableComponentJs = _dereq_('./clickable-component.js'); + +var _clickableComponentJs2 = _interopRequireDefault(_clickableComponentJs); + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsLogJs = _dereq_('./utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/** + * Base class for all buttons + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @extends ClickableComponent + * @class Button + */ + +var Button = (function (_ClickableComponent) { + _inherits(Button, _ClickableComponent); + + function Button(player, options) { + _classCallCheck(this, Button); + + _ClickableComponent.call(this, player, options); + } + + /** + * Create the component's DOM element + * + * @param {String=} type Element's node type. e.g. 'div' + * @param {Object=} props An object of properties that should be set on the element + * @param {Object=} attributes An object of attributes that should be set on the element + * @return {Element} + * @method createEl + */ + + Button.prototype.createEl = function createEl() { + var tag = arguments.length <= 0 || arguments[0] === undefined ? 'button' : arguments[0]; + var props = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + var attributes = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + props = _objectAssign2['default']({ + className: this.buildCSSClass() + }, props); + + if (tag !== 'button') { + _utilsLogJs2['default'].warn('Creating a Button with an HTML element of ' + tag + ' is deprecated; use ClickableComponent instead.'); + } + + // Add attributes for button element + attributes = _objectAssign2['default']({ + type: 'button', // Necessary since the default button type is "submit" + 'aria-live': 'polite' // let the screen reader user know that the text of the button may change + }, attributes); + + var el = _component2['default'].prototype.createEl.call(this, tag, props, attributes); + + this.createControlTextEl(el); + + return el; + }; + + /** + * Adds a child component inside this button + * + * @param {String|Component} child The class name or instance of a child to add + * @param {Object=} options Options, including options to be passed to children of the child. + * @return {Component} The child component (created by this process if a string was used) + * @deprecated + * @method addChild + */ + + Button.prototype.addChild = function addChild(child) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var className = this.constructor.name; + _utilsLogJs2['default'].warn('Adding an actionable (user controllable) child to a Button (' + className + ') is not supported; use a ClickableComponent instead.'); + + // Avoid the error message generated by ClickableComponent's addChild method + return _component2['default'].prototype.addChild.call(this, child, options); + }; + + /** + * Handle KeyPress (document level) - Extend with specific functionality for button + * + * @method handleKeyPress + */ + + Button.prototype.handleKeyPress = function handleKeyPress(event) { + // Ignore Space (32) or Enter (13) key operation, which is handled by the browser for a button. + if (event.which === 32 || event.which === 13) {} else { + _ClickableComponent.prototype.handleKeyPress.call(this, event); // Pass keypress handling up for unsupported keys + } + }; + + return Button; +})(_clickableComponentJs2['default']); + +_component2['default'].registerComponent('Button', Button); +exports['default'] = Button; +module.exports = exports['default']; + +},{"./clickable-component.js":65,"./component":67,"./utils/events.js":133,"./utils/fn.js":134,"./utils/log.js":137,"global/document":1,"object.assign":45}],65:[function(_dereq_,module,exports){ +/** + * @file button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsDomJs = _dereq_('./utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsLogJs = _dereq_('./utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/** + * Clickable Component which is clickable or keyboard actionable, but is not a native HTML button + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @extends Component + * @class ClickableComponent + */ + +var ClickableComponent = (function (_Component) { + _inherits(ClickableComponent, _Component); + + function ClickableComponent(player, options) { + _classCallCheck(this, ClickableComponent); + + _Component.call(this, player, options); + + this.emitTapEvents(); + + this.on('tap', this.handleClick); + this.on('click', this.handleClick); + this.on('focus', this.handleFocus); + this.on('blur', this.handleBlur); + } + + /** + * Create the component's DOM element + * + * @param {String=} type Element's node type. e.g. 'div' + * @param {Object=} props An object of properties that should be set on the element + * @param {Object=} attributes An object of attributes that should be set on the element + * @return {Element} + * @method createEl + */ + + ClickableComponent.prototype.createEl = function createEl() { + var tag = arguments.length <= 0 || arguments[0] === undefined ? 'div' : arguments[0]; + var props = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + var attributes = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + props = _objectAssign2['default']({ + className: this.buildCSSClass(), + tabIndex: 0 + }, props); + + if (tag === 'button') { + _utilsLogJs2['default'].error('Creating a ClickableComponent with an HTML element of ' + tag + ' is not supported; use a Button instead.'); + } + + // Add ARIA attributes for clickable element which is not a native HTML button + attributes = _objectAssign2['default']({ + role: 'button', + 'aria-live': 'polite' // let the screen reader user know that the text of the element may change + }, attributes); + + var el = _Component.prototype.createEl.call(this, tag, props, attributes); + + this.createControlTextEl(el); + + return el; + }; + + /** + * create control text + * + * @param {Element} el Parent element for the control text + * @return {Element} + * @method controlText + */ + + ClickableComponent.prototype.createControlTextEl = function createControlTextEl(el) { + this.controlTextEl_ = Dom.createEl('span', { + className: 'vjs-control-text' + }); + + if (el) { + el.appendChild(this.controlTextEl_); + } + + this.controlText(this.controlText_); + + return this.controlTextEl_; + }; + + /** + * Controls text - both request and localize + * + * @param {String} text Text for element + * @return {String} + * @method controlText + */ + + ClickableComponent.prototype.controlText = function controlText(text) { + if (!text) return this.controlText_ || 'Need Text'; + + this.controlText_ = text; + this.controlTextEl_.innerHTML = this.localize(this.controlText_); + + return this; + }; + + /** + * Allows sub components to stack CSS class names + * + * @return {String} + * @method buildCSSClass + */ + + ClickableComponent.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-control vjs-button ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Adds a child component inside this clickable-component + * + * @param {String|Component} child The class name or instance of a child to add + * @param {Object=} options Options, including options to be passed to children of the child. + * @return {Component} The child component (created by this process if a string was used) + * @method addChild + */ + + ClickableComponent.prototype.addChild = function addChild(child) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + // TODO: Fix adding an actionable child to a ClickableComponent; currently + // it will cause issues with assistive technology (e.g. screen readers) + // which support ARIA, since an element with role="button" cannot have + // actionable child elements. + + //let className = this.constructor.name; + //log.warn(`Adding a child to a ClickableComponent (${className}) can cause issues with assistive technology which supports ARIA, since an element with role="button" cannot have actionable child elements.`); + + return _Component.prototype.addChild.call(this, child, options); + }; + + /** + * Handle Click - Override with specific functionality for component + * + * @method handleClick + */ + + ClickableComponent.prototype.handleClick = function handleClick() {}; + + /** + * Handle Focus - Add keyboard functionality to element + * + * @method handleFocus + */ + + ClickableComponent.prototype.handleFocus = function handleFocus() { + Events.on(_globalDocument2['default'], 'keydown', Fn.bind(this, this.handleKeyPress)); + }; + + /** + * Handle KeyPress (document level) - Trigger click when Space or Enter key is pressed + * + * @method handleKeyPress + */ + + ClickableComponent.prototype.handleKeyPress = function handleKeyPress(event) { + // Support Space (32) or Enter (13) key operation to fire a click event + if (event.which === 32 || event.which === 13) { + event.preventDefault(); + this.handleClick(event); + } else if (_Component.prototype.handleKeyPress) { + _Component.prototype.handleKeyPress.call(this, event); // Pass keypress handling up for unsupported keys + } + }; + + /** + * Handle Blur - Remove keyboard triggers + * + * @method handleBlur + */ + + ClickableComponent.prototype.handleBlur = function handleBlur() { + Events.off(_globalDocument2['default'], 'keydown', Fn.bind(this, this.handleKeyPress)); + }; + + return ClickableComponent; +})(_component2['default']); + +_component2['default'].registerComponent('ClickableComponent', ClickableComponent); +exports['default'] = ClickableComponent; +module.exports = exports['default']; + +},{"./component":67,"./utils/dom.js":132,"./utils/events.js":133,"./utils/fn.js":134,"./utils/log.js":137,"global/document":1,"object.assign":45}],66:[function(_dereq_,module,exports){ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _button = _dereq_('./button'); + +var _button2 = _interopRequireDefault(_button); + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +/** + * The `CloseButton` component is a button which fires a "close" event + * when it is activated. + * + * @extends Button + * @class CloseButton + */ + +var CloseButton = (function (_Button) { + _inherits(CloseButton, _Button); + + function CloseButton(player, options) { + _classCallCheck(this, CloseButton); + + _Button.call(this, player, options); + this.controlText(options && options.controlText || this.localize('Close')); + } + + CloseButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-close-button ' + _Button.prototype.buildCSSClass.call(this); + }; + + CloseButton.prototype.handleClick = function handleClick() { + this.trigger({ type: 'close', bubbles: false }); + }; + + return CloseButton; +})(_button2['default']); + +_component2['default'].registerComponent('CloseButton', CloseButton); +exports['default'] = CloseButton; +module.exports = exports['default']; + +},{"./button":64,"./component":67}],67:[function(_dereq_,module,exports){ +/** + * @file component.js + * + * Player Component - Base class for all UI objects + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _utilsDomJs = _dereq_('./utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsGuidJs = _dereq_('./utils/guid.js'); + +var Guid = _interopRequireWildcard(_utilsGuidJs); + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _utilsLogJs = _dereq_('./utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _utilsToTitleCaseJs = _dereq_('./utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +var _utilsMergeOptionsJs = _dereq_('./utils/merge-options.js'); + +var _utilsMergeOptionsJs2 = _interopRequireDefault(_utilsMergeOptionsJs); + +/** + * Base UI Component class + * Components are embeddable UI objects that are represented by both a + * javascript object and an element in the DOM. They can be children of other + * components, and can have many children themselves. + * ```js + * // adding a button to the player + * var button = player.addChild('button'); + * button.el(); // -> button element + * ``` + * ```html + * <div class="video-js"> + * <div class="vjs-button">Button</div> + * </div> + * ``` + * Components are also event targets. + * ```js + * button.on('click', function(){ + * console.log('Button Clicked!'); + * }); + * button.trigger('customevent'); + * ``` + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @class Component + */ + +var Component = (function () { + function Component(player, options, ready) { + _classCallCheck(this, Component); + + // The component might be the player itself and we can't pass `this` to super + if (!player && this.play) { + this.player_ = player = this; // eslint-disable-line + } else { + this.player_ = player; + } + + // Make a copy of prototype.options_ to protect against overriding defaults + this.options_ = _utilsMergeOptionsJs2['default']({}, this.options_); + + // Updated options with supplied options + options = this.options_ = _utilsMergeOptionsJs2['default'](this.options_, options); + + // Get ID from options or options element if one is supplied + this.id_ = options.id || options.el && options.el.id; + + // If there was no ID from the options, generate one + if (!this.id_) { + // Don't require the player ID function in the case of mock players + var id = player && player.id && player.id() || 'no_player'; + + this.id_ = id + '_component_' + Guid.newGUID(); + } + + this.name_ = options.name || null; + + // Create element if one wasn't provided in options + if (options.el) { + this.el_ = options.el; + } else if (options.createEl !== false) { + this.el_ = this.createEl(); + } + + this.children_ = []; + this.childIndex_ = {}; + this.childNameIndex_ = {}; + + // Add any child components in options + if (options.initChildren !== false) { + this.initChildren(); + } + + this.ready(ready); + // Don't want to trigger ready here or it will before init is actually + // finished for all children that run this constructor + + if (options.reportTouchActivity !== false) { + this.enableTouchActivity(); + } + } + + /** + * Dispose of the component and all child components + * + * @method dispose + */ + + Component.prototype.dispose = function dispose() { + this.trigger({ type: 'dispose', bubbles: false }); + + // Dispose all children. + if (this.children_) { + for (var i = this.children_.length - 1; i >= 0; i--) { + if (this.children_[i].dispose) { + this.children_[i].dispose(); + } + } + } + + // Delete child references + this.children_ = null; + this.childIndex_ = null; + this.childNameIndex_ = null; + + // Remove all event listeners. + this.off(); + + // Remove element from DOM + if (this.el_.parentNode) { + this.el_.parentNode.removeChild(this.el_); + } + + Dom.removeElData(this.el_); + this.el_ = null; + }; + + /** + * Return the component's player + * + * @return {Player} + * @method player + */ + + Component.prototype.player = function player() { + return this.player_; + }; + + /** + * Deep merge of options objects + * Whenever a property is an object on both options objects + * the two properties will be merged using mergeOptions. + * + * ```js + * Parent.prototype.options_ = { + * optionSet: { + * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, + * 'childTwo': {}, + * 'childThree': {} + * } + * } + * newOptions = { + * optionSet: { + * 'childOne': { 'foo': 'baz', 'abc': '123' } + * 'childTwo': null, + * 'childFour': {} + * } + * } + * + * this.options(newOptions); + * ``` + * RESULT + * ```js + * { + * optionSet: { + * 'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' }, + * 'childTwo': null, // Disabled. Won't be initialized. + * 'childThree': {}, + * 'childFour': {} + * } + * } + * ``` + * + * @param {Object} obj Object of new option values + * @return {Object} A NEW object of this.options_ and obj merged + * @method options + */ + + Component.prototype.options = function options(obj) { + _utilsLogJs2['default'].warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); + + if (!obj) { + return this.options_; + } + + this.options_ = _utilsMergeOptionsJs2['default'](this.options_, obj); + return this.options_; + }; + + /** + * Get the component's DOM element + * ```js + * var domEl = myComponent.el(); + * ``` + * + * @return {Element} + * @method el + */ + + Component.prototype.el = function el() { + return this.el_; + }; + + /** + * Create the component's DOM element + * + * @param {String=} tagName Element's node type. e.g. 'div' + * @param {Object=} properties An object of properties that should be set + * @param {Object=} attributes An object of attributes that should be set + * @return {Element} + * @method createEl + */ + + Component.prototype.createEl = function createEl(tagName, properties, attributes) { + return Dom.createEl(tagName, properties, attributes); + }; + + Component.prototype.localize = function localize(string) { + var code = this.player_.language && this.player_.language(); + var languages = this.player_.languages && this.player_.languages(); + + if (!code || !languages) { + return string; + } + + var language = languages[code]; + + if (language && language[string]) { + return language[string]; + } + + var primaryCode = code.split('-')[0]; + var primaryLang = languages[primaryCode]; + + if (primaryLang && primaryLang[string]) { + return primaryLang[string]; + } + + return string; + }; + + /** + * Return the component's DOM element where children are inserted. + * Will either be the same as el() or a new element defined in createEl(). + * + * @return {Element} + * @method contentEl + */ + + Component.prototype.contentEl = function contentEl() { + return this.contentEl_ || this.el_; + }; + + /** + * Get the component's ID + * ```js + * var id = myComponent.id(); + * ``` + * + * @return {String} + * @method id + */ + + Component.prototype.id = function id() { + return this.id_; + }; + + /** + * Get the component's name. The name is often used to reference the component. + * ```js + * var name = myComponent.name(); + * ``` + * + * @return {String} + * @method name + */ + + Component.prototype.name = function name() { + return this.name_; + }; + + /** + * Get an array of all child components + * ```js + * var kids = myComponent.children(); + * ``` + * + * @return {Array} The children + * @method children + */ + + Component.prototype.children = function children() { + return this.children_; + }; + + /** + * Returns a child component with the provided ID + * + * @return {Component} + * @method getChildById + */ + + Component.prototype.getChildById = function getChildById(id) { + return this.childIndex_[id]; + }; + + /** + * Returns a child component with the provided name + * + * @return {Component} + * @method getChild + */ + + Component.prototype.getChild = function getChild(name) { + return this.childNameIndex_[name]; + }; + + /** + * Adds a child component inside this component + * ```js + * myComponent.el(); + * // -> <div class='my-component'></div> + * myComponent.children(); + * // [empty array] + * + * var myButton = myComponent.addChild('MyButton'); + * // -> <div class='my-component'><div class="my-button">myButton<div></div> + * // -> myButton === myComponent.children()[0]; + * ``` + * Pass in options for child constructors and options for children of the child + * ```js + * var myButton = myComponent.addChild('MyButton', { + * text: 'Press Me', + * buttonChildExample: { + * buttonChildOption: true + * } + * }); + * ``` + * + * @param {String|Component} child The class name or instance of a child to add + * @param {Object=} options Options, including options to be passed to children of the child. + * @param {Number} index into our children array to attempt to add the child + * @return {Component} The child component (created by this process if a string was used) + * @method addChild + */ + + Component.prototype.addChild = function addChild(child) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + var index = arguments.length <= 2 || arguments[2] === undefined ? this.children_.length : arguments[2]; + + var component = undefined; + var componentName = undefined; + + // If child is a string, create nt with options + if (typeof child === 'string') { + componentName = child; + + // Options can also be specified as a boolean, so convert to an empty object if false. + if (!options) { + options = {}; + } + + // Same as above, but true is deprecated so show a warning. + if (options === true) { + _utilsLogJs2['default'].warn('Initializing a child component with `true` is deprecated. Children should be defined in an array when possible, but if necessary use an object instead of `true`.'); + options = {}; + } + + // If no componentClass in options, assume componentClass is the name lowercased + // (e.g. playButton) + var componentClassName = options.componentClass || _utilsToTitleCaseJs2['default'](componentName); + + // Set name through options + options.name = componentName; + + // Create a new object & element for this controls set + // If there's no .player_, this is a player + var ComponentClass = Component.getComponent(componentClassName); + + if (!ComponentClass) { + throw new Error('Component ' + componentClassName + ' does not exist'); + } + + // data stored directly on the videojs object may be + // misidentified as a component to retain + // backwards-compatibility with 4.x. check to make sure the + // component class can be instantiated. + if (typeof ComponentClass !== 'function') { + return null; + } + + component = new ComponentClass(this.player_ || this, options); + + // child is a component instance + } else { + component = child; + } + + this.children_.splice(index, 0, component); + + if (typeof component.id === 'function') { + this.childIndex_[component.id()] = component; + } + + // If a name wasn't used to create the component, check if we can use the + // name function of the component + componentName = componentName || component.name && component.name(); + + if (componentName) { + this.childNameIndex_[componentName] = component; + } + + // Add the UI object's element to the container div (box) + // Having an element is not required + if (typeof component.el === 'function' && component.el()) { + var childNodes = this.contentEl().children; + var refNode = childNodes[index] || null; + this.contentEl().insertBefore(component.el(), refNode); + } + + // Return so it can stored on parent object if desired. + return component; + }; + + /** + * Remove a child component from this component's list of children, and the + * child component's element from this component's element + * + * @param {Component} component Component to remove + * @method removeChild + */ + + Component.prototype.removeChild = function removeChild(component) { + if (typeof component === 'string') { + component = this.getChild(component); + } + + if (!component || !this.children_) { + return; + } + + var childFound = false; + + for (var i = this.children_.length - 1; i >= 0; i--) { + if (this.children_[i] === component) { + childFound = true; + this.children_.splice(i, 1); + break; + } + } + + if (!childFound) { + return; + } + + this.childIndex_[component.id()] = null; + this.childNameIndex_[component.name()] = null; + + var compEl = component.el(); + + if (compEl && compEl.parentNode === this.contentEl()) { + this.contentEl().removeChild(component.el()); + } + }; + + /** + * Add and initialize default child components from options + * ```js + * // when an instance of MyComponent is created, all children in options + * // will be added to the instance by their name strings and options + * MyComponent.prototype.options_ = { + * children: [ + * 'myChildComponent' + * ], + * myChildComponent: { + * myChildOption: true + * } + * }; + * + * // Or when creating the component + * var myComp = new MyComponent(player, { + * children: [ + * 'myChildComponent' + * ], + * myChildComponent: { + * myChildOption: true + * } + * }); + * ``` + * The children option can also be an array of + * child options objects (that also include a 'name' key). + * This can be used if you have two child components of the + * same type that need different options. + * ```js + * var myComp = new MyComponent(player, { + * children: [ + * 'button', + * { + * name: 'button', + * someOtherOption: true + * }, + * { + * name: 'button', + * someOtherOption: false + * } + * ] + * }); + * ``` + * + * @method initChildren + */ + + Component.prototype.initChildren = function initChildren() { + var _this = this; + + var children = this.options_.children; + + if (children) { + (function () { + // `this` is `parent` + var parentOptions = _this.options_; + + var handleAdd = function handleAdd(child) { + var name = child.name; + var opts = child.opts; + + // Allow options for children to be set at the parent options + // e.g. videojs(id, { controlBar: false }); + // instead of videojs(id, { children: { controlBar: false }); + if (parentOptions[name] !== undefined) { + opts = parentOptions[name]; + } + + // Allow for disabling default components + // e.g. options['children']['posterImage'] = false + if (opts === false) { + return; + } + + // Allow options to be passed as a simple boolean if no configuration + // is necessary. + if (opts === true) { + opts = {}; + } + + // We also want to pass the original player options to each component as well so they don't need to + // reach back into the player for options later. + opts.playerOptions = _this.options_.playerOptions; + + // Create and add the child component. + // Add a direct reference to the child by name on the parent instance. + // If two of the same component are used, different names should be supplied + // for each + var newChild = _this.addChild(name, opts); + if (newChild) { + _this[name] = newChild; + } + }; + + // Allow for an array of children details to passed in the options + var workingChildren = undefined; + var Tech = Component.getComponent('Tech'); + + if (Array.isArray(children)) { + workingChildren = children; + } else { + workingChildren = Object.keys(children); + } + + workingChildren + // children that are in this.options_ but also in workingChildren would + // give us extra children we do not want. So, we want to filter them out. + .concat(Object.keys(_this.options_).filter(function (child) { + return !workingChildren.some(function (wchild) { + if (typeof wchild === 'string') { + return child === wchild; + } else { + return child === wchild.name; + } + }); + })).map(function (child) { + var name = undefined, + opts = undefined; + + if (typeof child === 'string') { + name = child; + opts = children[name] || _this.options_[name] || {}; + } else { + name = child.name; + opts = child; + } + + return { name: name, opts: opts }; + }).filter(function (child) { + // we have to make sure that child.name isn't in the techOrder since + // techs are registerd as Components but can't aren't compatible + // See https://github.com/videojs/video.js/issues/2772 + var c = Component.getComponent(child.opts.componentClass || _utilsToTitleCaseJs2['default'](child.name)); + return c && !Tech.isTech(c); + }).forEach(handleAdd); + })(); + } + }; + + /** + * Allows sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + Component.prototype.buildCSSClass = function buildCSSClass() { + // Child classes can include a function that does: + // return 'CLASS NAME' + this._super(); + return ''; + }; + + /** + * Add an event listener to this component's element + * ```js + * var myFunc = function(){ + * var myComponent = this; + * // Do something when the event is fired + * }; + * + * myComponent.on('eventType', myFunc); + * ``` + * The context of myFunc will be myComponent unless previously bound. + * Alternatively, you can add a listener to another element or component. + * ```js + * myComponent.on(otherElement, 'eventName', myFunc); + * myComponent.on(otherComponent, 'eventName', myFunc); + * ``` + * The benefit of using this over `VjsEvents.on(otherElement, 'eventName', myFunc)` + * and `otherComponent.on('eventName', myFunc)` is that this way the listeners + * will be automatically cleaned up when either component is disposed. + * It will also bind myComponent as the context of myFunc. + * **NOTE**: When using this on elements in the page other than window + * and document (both permanent), if you remove the element from the DOM + * you need to call `myComponent.trigger(el, 'dispose')` on it to clean up + * references to it and allow the browser to garbage collect it. + * + * @param {String|Component} first The event type or other component + * @param {Function|String} second The event handler or event type + * @param {Function} third The event handler + * @return {Component} + * @method on + */ + + Component.prototype.on = function on(first, second, third) { + var _this2 = this; + + if (typeof first === 'string' || Array.isArray(first)) { + Events.on(this.el_, first, Fn.bind(this, second)); + + // Targeting another component or element + } else { + (function () { + var target = first; + var type = second; + var fn = Fn.bind(_this2, third); + + // When this component is disposed, remove the listener from the other component + var removeOnDispose = function removeOnDispose() { + return _this2.off(target, type, fn); + }; + + // Use the same function ID so we can remove it later it using the ID + // of the original listener + removeOnDispose.guid = fn.guid; + _this2.on('dispose', removeOnDispose); + + // If the other component is disposed first we need to clean the reference + // to the other component in this component's removeOnDispose listener + // Otherwise we create a memory leak. + var cleanRemover = function cleanRemover() { + return _this2.off('dispose', removeOnDispose); + }; + + // Add the same function ID so we can easily remove it later + cleanRemover.guid = fn.guid; + + // Check if this is a DOM node + if (first.nodeName) { + // Add the listener to the other element + Events.on(target, type, fn); + Events.on(target, 'dispose', cleanRemover); + + // Should be a component + // Not using `instanceof Component` because it makes mock players difficult + } else if (typeof first.on === 'function') { + // Add the listener to the other component + target.on(type, fn); + target.on('dispose', cleanRemover); + } + })(); + } + + return this; + }; + + /** + * Remove an event listener from this component's element + * ```js + * myComponent.off('eventType', myFunc); + * ``` + * If myFunc is excluded, ALL listeners for the event type will be removed. + * If eventType is excluded, ALL listeners will be removed from the component. + * Alternatively you can use `off` to remove listeners that were added to other + * elements or components using `myComponent.on(otherComponent...`. + * In this case both the event type and listener function are REQUIRED. + * ```js + * myComponent.off(otherElement, 'eventType', myFunc); + * myComponent.off(otherComponent, 'eventType', myFunc); + * ``` + * + * @param {String=|Component} first The event type or other component + * @param {Function=|String} second The listener function or event type + * @param {Function=} third The listener for other component + * @return {Component} + * @method off + */ + + Component.prototype.off = function off(first, second, third) { + if (!first || typeof first === 'string' || Array.isArray(first)) { + Events.off(this.el_, first, second); + } else { + var target = first; + var type = second; + // Ensure there's at least a guid, even if the function hasn't been used + var fn = Fn.bind(this, third); + + // Remove the dispose listener on this component, + // which was given the same guid as the event listener + this.off('dispose', fn); + + if (first.nodeName) { + // Remove the listener + Events.off(target, type, fn); + // Remove the listener for cleaning the dispose listener + Events.off(target, 'dispose', fn); + } else { + target.off(type, fn); + target.off('dispose', fn); + } + } + + return this; + }; + + /** + * Add an event listener to be triggered only once and then removed + * ```js + * myComponent.one('eventName', myFunc); + * ``` + * Alternatively you can add a listener to another element or component + * that will be triggered only once. + * ```js + * myComponent.one(otherElement, 'eventName', myFunc); + * myComponent.one(otherComponent, 'eventName', myFunc); + * ``` + * + * @param {String|Component} first The event type or other component + * @param {Function|String} second The listener function or event type + * @param {Function=} third The listener function for other component + * @return {Component} + * @method one + */ + + Component.prototype.one = function one(first, second, third) { + var _this3 = this, + _arguments = arguments; + + if (typeof first === 'string' || Array.isArray(first)) { + Events.one(this.el_, first, Fn.bind(this, second)); + } else { + (function () { + var target = first; + var type = second; + var fn = Fn.bind(_this3, third); + + var newFunc = function newFunc() { + _this3.off(target, type, newFunc); + fn.apply(null, _arguments); + }; + + // Keep the same function ID so we can remove it later + newFunc.guid = fn.guid; + + _this3.on(target, type, newFunc); + })(); + } + + return this; + }; + + /** + * Trigger an event on an element + * ```js + * myComponent.trigger('eventName'); + * myComponent.trigger({'type':'eventName'}); + * myComponent.trigger('eventName', {data: 'some data'}); + * myComponent.trigger({'type':'eventName'}, {data: 'some data'}); + * ``` + * + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute + * @param {Object} [hash] data hash to pass along with the event + * @return {Component} self + * @method trigger + */ + + Component.prototype.trigger = function trigger(event, hash) { + Events.trigger(this.el_, event, hash); + return this; + }; + + /** + * Bind a listener to the component's ready state. + * Different from event listeners in that if the ready event has already happened + * it will trigger the function immediately. + * + * @param {Function} fn Ready listener + * @param {Boolean} sync Exec the listener synchronously if component is ready + * @return {Component} + * @method ready + */ + + Component.prototype.ready = function ready(fn) { + var sync = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; + + if (fn) { + if (this.isReady_) { + if (sync) { + fn.call(this); + } else { + // Call the function asynchronously by default for consistency + this.setTimeout(fn, 1); + } + } else { + this.readyQueue_ = this.readyQueue_ || []; + this.readyQueue_.push(fn); + } + } + return this; + }; + + /** + * Trigger the ready listeners + * + * @return {Component} + * @method triggerReady + */ + + Component.prototype.triggerReady = function triggerReady() { + this.isReady_ = true; + + // Ensure ready is triggerd asynchronously + this.setTimeout(function () { + var readyQueue = this.readyQueue_; + + // Reset Ready Queue + this.readyQueue_ = []; + + if (readyQueue && readyQueue.length > 0) { + readyQueue.forEach(function (fn) { + fn.call(this); + }, this); + } + + // Allow for using event listeners also + this.trigger('ready'); + }, 1); + }; + + /** + * Finds a single DOM element matching `selector` within the component's + * `contentEl` or another custom context. + * + * @method $ + * @param {String} selector + * A valid CSS selector, which will be passed to `querySelector`. + * + * @param {Element|String} [context=document] + * A DOM element within which to query. Can also be a selector + * string in which case the first matching element will be used + * as context. If missing (or no element matches selector), falls + * back to `document`. + * + * @return {Element|null} + */ + + Component.prototype.$ = function $(selector, context) { + return Dom.$(selector, context || this.contentEl()); + }; + + /** + * Finds a all DOM elements matching `selector` within the component's + * `contentEl` or another custom context. + * + * @method $$ + * @param {String} selector + * A valid CSS selector, which will be passed to `querySelectorAll`. + * + * @param {Element|String} [context=document] + * A DOM element within which to query. Can also be a selector + * string in which case the first matching element will be used + * as context. If missing (or no element matches selector), falls + * back to `document`. + * + * @return {NodeList} + */ + + Component.prototype.$$ = function $$(selector, context) { + return Dom.$$(selector, context || this.contentEl()); + }; + + /** + * Check if a component's element has a CSS class name + * + * @param {String} classToCheck Classname to check + * @return {Component} + * @method hasClass + */ + + Component.prototype.hasClass = function hasClass(classToCheck) { + return Dom.hasElClass(this.el_, classToCheck); + }; + + /** + * Add a CSS class name to the component's element + * + * @param {String} classToAdd Classname to add + * @return {Component} + * @method addClass + */ + + Component.prototype.addClass = function addClass(classToAdd) { + Dom.addElClass(this.el_, classToAdd); + return this; + }; + + /** + * Remove a CSS class name from the component's element + * + * @param {String} classToRemove Classname to remove + * @return {Component} + * @method removeClass + */ + + Component.prototype.removeClass = function removeClass(classToRemove) { + Dom.removeElClass(this.el_, classToRemove); + return this; + }; + + /** + * Add or remove a CSS class name from the component's element + * + * @param {String} classToToggle + * @param {Boolean|Function} [predicate] + * Can be a function that returns a Boolean. If `true`, the class + * will be added; if `false`, the class will be removed. If not + * given, the class will be added if not present and vice versa. + * + * @return {Component} + * @method toggleClass + */ + + Component.prototype.toggleClass = function toggleClass(classToToggle, predicate) { + Dom.toggleElClass(this.el_, classToToggle, predicate); + return this; + }; + + /** + * Show the component element if hidden + * + * @return {Component} + * @method show + */ + + Component.prototype.show = function show() { + this.removeClass('vjs-hidden'); + return this; + }; + + /** + * Hide the component element if currently showing + * + * @return {Component} + * @method hide + */ + + Component.prototype.hide = function hide() { + this.addClass('vjs-hidden'); + return this; + }; + + /** + * Lock an item in its visible state + * To be used with fadeIn/fadeOut. + * + * @return {Component} + * @private + * @method lockShowing + */ + + Component.prototype.lockShowing = function lockShowing() { + this.addClass('vjs-lock-showing'); + return this; + }; + + /** + * Unlock an item to be hidden + * To be used with fadeIn/fadeOut. + * + * @return {Component} + * @private + * @method unlockShowing + */ + + Component.prototype.unlockShowing = function unlockShowing() { + this.removeClass('vjs-lock-showing'); + return this; + }; + + /** + * Set or get the width of the component (CSS values) + * Setting the video tag dimension values only works with values in pixels. + * Percent values will not work. + * Some percents can be used, but width()/height() will return the number + %, + * not the actual computed width/height. + * + * @param {Number|String=} num Optional width number + * @param {Boolean} skipListeners Skip the 'resize' event trigger + * @return {Component} This component, when setting the width + * @return {Number|String} The width, when getting + * @method width + */ + + Component.prototype.width = function width(num, skipListeners) { + return this.dimension('width', num, skipListeners); + }; + + /** + * Get or set the height of the component (CSS values) + * Setting the video tag dimension values only works with values in pixels. + * Percent values will not work. + * Some percents can be used, but width()/height() will return the number + %, + * not the actual computed width/height. + * + * @param {Number|String=} num New component height + * @param {Boolean=} skipListeners Skip the resize event trigger + * @return {Component} This component, when setting the height + * @return {Number|String} The height, when getting + * @method height + */ + + Component.prototype.height = function height(num, skipListeners) { + return this.dimension('height', num, skipListeners); + }; + + /** + * Set both width and height at the same time + * + * @param {Number|String} width Width of player + * @param {Number|String} height Height of player + * @return {Component} The component + * @method dimensions + */ + + Component.prototype.dimensions = function dimensions(width, height) { + // Skip resize listeners on width for optimization + return this.width(width, true).height(height); + }; + + /** + * Get or set width or height + * This is the shared code for the width() and height() methods. + * All for an integer, integer + 'px' or integer + '%'; + * Known issue: Hidden elements officially have a width of 0. We're defaulting + * to the style.width value and falling back to computedStyle which has the + * hidden element issue. Info, but probably not an efficient fix: + * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ + * + * @param {String} widthOrHeight 'width' or 'height' + * @param {Number|String=} num New dimension + * @param {Boolean=} skipListeners Skip resize event trigger + * @return {Component} The component if a dimension was set + * @return {Number|String} The dimension if nothing was set + * @private + * @method dimension + */ + + Component.prototype.dimension = function dimension(widthOrHeight, num, skipListeners) { + if (num !== undefined) { + // Set to zero if null or literally NaN (NaN !== NaN) + if (num === null || num !== num) { + num = 0; + } + + // Check if using css width/height (% or px) and adjust + if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { + this.el_.style[widthOrHeight] = num; + } else if (num === 'auto') { + this.el_.style[widthOrHeight] = ''; + } else { + this.el_.style[widthOrHeight] = num + 'px'; + } + + // skipListeners allows us to avoid triggering the resize event when setting both width and height + if (!skipListeners) { + this.trigger('resize'); + } + + // Return component + return this; + } + + // Not setting a value, so getting it + // Make sure element exists + if (!this.el_) { + return 0; + } + + // Get dimension value from style + var val = this.el_.style[widthOrHeight]; + var pxIndex = val.indexOf('px'); + + if (pxIndex !== -1) { + // Return the pixel value with no 'px' + return parseInt(val.slice(0, pxIndex), 10); + } + + // No px so using % or no style was set, so falling back to offsetWidth/height + // If component has display:none, offset will return 0 + // TODO: handle display:none and no dimension style using px + return parseInt(this.el_['offset' + _utilsToTitleCaseJs2['default'](widthOrHeight)], 10); + }; + + /** + * Emit 'tap' events when touch events are supported + * This is used to support toggling the controls through a tap on the video. + * We're requiring them to be enabled because otherwise every component would + * have this extra overhead unnecessarily, on mobile devices where extra + * overhead is especially bad. + * + * @private + * @method emitTapEvents + */ + + Component.prototype.emitTapEvents = function emitTapEvents() { + // Track the start time so we can determine how long the touch lasted + var touchStart = 0; + var firstTouch = null; + + // Maximum movement allowed during a touch event to still be considered a tap + // Other popular libs use anywhere from 2 (hammer.js) to 15, so 10 seems like a nice, round number. + var tapMovementThreshold = 10; + + // The maximum length a touch can be while still being considered a tap + var touchTimeThreshold = 200; + + var couldBeTap = undefined; + + this.on('touchstart', function (event) { + // If more than one finger, don't consider treating this as a click + if (event.touches.length === 1) { + // Copy the touches object to prevent modifying the original + firstTouch = _objectAssign2['default']({}, event.touches[0]); + // Record start time so we can detect a tap vs. "touch and hold" + touchStart = new Date().getTime(); + // Reset couldBeTap tracking + couldBeTap = true; + } + }); + + this.on('touchmove', function (event) { + // If more than one finger, don't consider treating this as a click + if (event.touches.length > 1) { + couldBeTap = false; + } else if (firstTouch) { + // Some devices will throw touchmoves for all but the slightest of taps. + // So, if we moved only a small distance, this could still be a tap + var xdiff = event.touches[0].pageX - firstTouch.pageX; + var ydiff = event.touches[0].pageY - firstTouch.pageY; + var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + + if (touchDistance > tapMovementThreshold) { + couldBeTap = false; + } + } + }); + + var noTap = function noTap() { + couldBeTap = false; + }; + + // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s + this.on('touchleave', noTap); + this.on('touchcancel', noTap); + + // When the touch ends, measure how long it took and trigger the appropriate + // event + this.on('touchend', function (event) { + firstTouch = null; + // Proceed only if the touchmove/leave/cancel event didn't happen + if (couldBeTap === true) { + // Measure how long the touch lasted + var touchTime = new Date().getTime() - touchStart; + + // Make sure the touch was less than the threshold to be considered a tap + if (touchTime < touchTimeThreshold) { + // Don't let browser turn this into a click + event.preventDefault(); + this.trigger('tap'); + // It may be good to copy the touchend event object and change the + // type to tap, if the other event properties aren't exact after + // Events.fixEvent runs (e.g. event.target) + } + } + }); + }; + + /** + * Report user touch activity when touch events occur + * User activity is used to determine when controls should show/hide. It's + * relatively simple when it comes to mouse events, because any mouse event + * should show the controls. So we capture mouse events that bubble up to the + * player and report activity when that happens. + * With touch events it isn't as easy. We can't rely on touch events at the + * player level, because a tap (touchstart + touchend) on the video itself on + * mobile devices is meant to turn controls off (and on). User activity is + * checked asynchronously, so what could happen is a tap event on the video + * turns the controls off, then the touchend event bubbles up to the player, + * which if it reported user activity, would turn the controls right back on. + * (We also don't want to completely block touch events from bubbling up) + * Also a touchmove, touch+hold, and anything other than a tap is not supposed + * to turn the controls back on on a mobile device. + * Here we're setting the default component behavior to report user activity + * whenever touch events happen, and this can be turned off by components that + * want touch events to act differently. + * + * @method enableTouchActivity + */ + + Component.prototype.enableTouchActivity = function enableTouchActivity() { + // Don't continue if the root player doesn't support reporting user activity + if (!this.player() || !this.player().reportUserActivity) { + return; + } + + // listener for reporting that the user is active + var report = Fn.bind(this.player(), this.player().reportUserActivity); + + var touchHolding = undefined; + + this.on('touchstart', function () { + report(); + // For as long as the they are touching the device or have their mouse down, + // we consider them active even if they're not moving their finger or mouse. + // So we want to continue to update that they are active + this.clearInterval(touchHolding); + // report at the same interval as activityCheck + touchHolding = this.setInterval(report, 250); + }); + + var touchEnd = function touchEnd(event) { + report(); + // stop the interval that maintains activity if the touch is holding + this.clearInterval(touchHolding); + }; + + this.on('touchmove', report); + this.on('touchend', touchEnd); + this.on('touchcancel', touchEnd); + }; + + /** + * Creates timeout and sets up disposal automatically. + * + * @param {Function} fn The function to run after the timeout. + * @param {Number} timeout Number of ms to delay before executing specified function. + * @return {Number} Returns the timeout ID + * @method setTimeout + */ + + Component.prototype.setTimeout = function setTimeout(fn, timeout) { + fn = Fn.bind(this, fn); + + // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. + var timeoutId = _globalWindow2['default'].setTimeout(fn, timeout); + + var disposeFn = function disposeFn() { + this.clearTimeout(timeoutId); + }; + + disposeFn.guid = 'vjs-timeout-' + timeoutId; + + this.on('dispose', disposeFn); + + return timeoutId; + }; + + /** + * Clears a timeout and removes the associated dispose listener + * + * @param {Number} timeoutId The id of the timeout to clear + * @return {Number} Returns the timeout ID + * @method clearTimeout + */ + + Component.prototype.clearTimeout = function clearTimeout(timeoutId) { + _globalWindow2['default'].clearTimeout(timeoutId); + + var disposeFn = function disposeFn() {}; + + disposeFn.guid = 'vjs-timeout-' + timeoutId; + + this.off('dispose', disposeFn); + + return timeoutId; + }; + + /** + * Creates an interval and sets up disposal automatically. + * + * @param {Function} fn The function to run every N seconds. + * @param {Number} interval Number of ms to delay before executing specified function. + * @return {Number} Returns the interval ID + * @method setInterval + */ + + Component.prototype.setInterval = function setInterval(fn, interval) { + fn = Fn.bind(this, fn); + + var intervalId = _globalWindow2['default'].setInterval(fn, interval); + + var disposeFn = function disposeFn() { + this.clearInterval(intervalId); + }; + + disposeFn.guid = 'vjs-interval-' + intervalId; + + this.on('dispose', disposeFn); + + return intervalId; + }; + + /** + * Clears an interval and removes the associated dispose listener + * + * @param {Number} intervalId The id of the interval to clear + * @return {Number} Returns the interval ID + * @method clearInterval + */ + + Component.prototype.clearInterval = function clearInterval(intervalId) { + _globalWindow2['default'].clearInterval(intervalId); + + var disposeFn = function disposeFn() {}; + + disposeFn.guid = 'vjs-interval-' + intervalId; + + this.off('dispose', disposeFn); + + return intervalId; + }; + + /** + * Registers a component + * + * @param {String} name Name of the component to register + * @param {Object} comp The component to register + * @static + * @method registerComponent + */ + + Component.registerComponent = function registerComponent(name, comp) { + if (!Component.components_) { + Component.components_ = {}; + } + + Component.components_[name] = comp; + return comp; + }; + + /** + * Gets a component by name + * + * @param {String} name Name of the component to get + * @return {Component} + * @static + * @method getComponent + */ + + Component.getComponent = function getComponent(name) { + if (Component.components_ && Component.components_[name]) { + return Component.components_[name]; + } + + if (_globalWindow2['default'] && _globalWindow2['default'].videojs && _globalWindow2['default'].videojs[name]) { + _utilsLogJs2['default'].warn('The ' + name + ' component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)'); + return _globalWindow2['default'].videojs[name]; + } + }; + + /** + * Sets up the constructor using the supplied init method + * or uses the init of the parent object + * + * @param {Object} props An object of properties + * @static + * @deprecated + * @method extend + */ + + Component.extend = function extend(props) { + props = props || {}; + + _utilsLogJs2['default'].warn('Component.extend({}) has been deprecated, use videojs.extend(Component, {}) instead'); + + // Set up the constructor using the supplied init method + // or using the init of the parent object + // Make sure to check the unobfuscated version for external libs + var init = props.init || props.init || this.prototype.init || this.prototype.init || function () {}; + // In Resig's simple class inheritance (previously used) the constructor + // is a function that calls `this.init.apply(arguments)` + // However that would prevent us from using `ParentObject.call(this);` + // in a Child constructor because the `this` in `this.init` + // would still refer to the Child and cause an infinite loop. + // We would instead have to do + // `ParentObject.prototype.init.apply(this, arguments);` + // Bleh. We're not creating a _super() function, so it's good to keep + // the parent constructor reference simple. + var subObj = function subObj() { + init.apply(this, arguments); + }; + + // Inherit from this object's prototype + subObj.prototype = Object.create(this.prototype); + // Reset the constructor property for subObj otherwise + // instances of subObj would have the constructor of the parent Object + subObj.prototype.constructor = subObj; + + // Make the class extendable + subObj.extend = Component.extend; + + // Extend subObj's prototype with functions and other properties from props + for (var _name in props) { + if (props.hasOwnProperty(_name)) { + subObj.prototype[_name] = props[_name]; + } + } + + return subObj; + }; + + return Component; +})(); + +Component.registerComponent('Component', Component); +exports['default'] = Component; +module.exports = exports['default']; + +},{"./utils/dom.js":132,"./utils/events.js":133,"./utils/fn.js":134,"./utils/guid.js":136,"./utils/log.js":137,"./utils/merge-options.js":138,"./utils/to-title-case.js":141,"global/window":2,"object.assign":45}],68:[function(_dereq_,module,exports){ +/** + * @file control-bar.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +// Required children + +var _playToggleJs = _dereq_('./play-toggle.js'); + +var _playToggleJs2 = _interopRequireDefault(_playToggleJs); + +var _timeControlsCurrentTimeDisplayJs = _dereq_('./time-controls/current-time-display.js'); + +var _timeControlsCurrentTimeDisplayJs2 = _interopRequireDefault(_timeControlsCurrentTimeDisplayJs); + +var _timeControlsDurationDisplayJs = _dereq_('./time-controls/duration-display.js'); + +var _timeControlsDurationDisplayJs2 = _interopRequireDefault(_timeControlsDurationDisplayJs); + +var _timeControlsTimeDividerJs = _dereq_('./time-controls/time-divider.js'); + +var _timeControlsTimeDividerJs2 = _interopRequireDefault(_timeControlsTimeDividerJs); + +var _timeControlsRemainingTimeDisplayJs = _dereq_('./time-controls/remaining-time-display.js'); + +var _timeControlsRemainingTimeDisplayJs2 = _interopRequireDefault(_timeControlsRemainingTimeDisplayJs); + +var _liveDisplayJs = _dereq_('./live-display.js'); + +var _liveDisplayJs2 = _interopRequireDefault(_liveDisplayJs); + +var _progressControlProgressControlJs = _dereq_('./progress-control/progress-control.js'); + +var _progressControlProgressControlJs2 = _interopRequireDefault(_progressControlProgressControlJs); + +var _fullscreenToggleJs = _dereq_('./fullscreen-toggle.js'); + +var _fullscreenToggleJs2 = _interopRequireDefault(_fullscreenToggleJs); + +var _volumeControlVolumeControlJs = _dereq_('./volume-control/volume-control.js'); + +var _volumeControlVolumeControlJs2 = _interopRequireDefault(_volumeControlVolumeControlJs); + +var _volumeMenuButtonJs = _dereq_('./volume-menu-button.js'); + +var _volumeMenuButtonJs2 = _interopRequireDefault(_volumeMenuButtonJs); + +var _muteToggleJs = _dereq_('./mute-toggle.js'); + +var _muteToggleJs2 = _interopRequireDefault(_muteToggleJs); + +var _textTrackControlsChaptersButtonJs = _dereq_('./text-track-controls/chapters-button.js'); + +var _textTrackControlsChaptersButtonJs2 = _interopRequireDefault(_textTrackControlsChaptersButtonJs); + +var _textTrackControlsSubtitlesButtonJs = _dereq_('./text-track-controls/subtitles-button.js'); + +var _textTrackControlsSubtitlesButtonJs2 = _interopRequireDefault(_textTrackControlsSubtitlesButtonJs); + +var _textTrackControlsCaptionsButtonJs = _dereq_('./text-track-controls/captions-button.js'); + +var _textTrackControlsCaptionsButtonJs2 = _interopRequireDefault(_textTrackControlsCaptionsButtonJs); + +var _playbackRateMenuPlaybackRateMenuButtonJs = _dereq_('./playback-rate-menu/playback-rate-menu-button.js'); + +var _playbackRateMenuPlaybackRateMenuButtonJs2 = _interopRequireDefault(_playbackRateMenuPlaybackRateMenuButtonJs); + +var _spacerControlsCustomControlSpacerJs = _dereq_('./spacer-controls/custom-control-spacer.js'); + +var _spacerControlsCustomControlSpacerJs2 = _interopRequireDefault(_spacerControlsCustomControlSpacerJs); + +/** + * Container of main controls + * + * @extends Component + * @class ControlBar + */ + +var ControlBar = (function (_Component) { + _inherits(ControlBar, _Component); + + function ControlBar() { + _classCallCheck(this, ControlBar); + + _Component.apply(this, arguments); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + ControlBar.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-control-bar', + dir: 'ltr' + }, { + 'role': 'group' // The control bar is a group, so it can contain menuitems + }); + }; + + return ControlBar; +})(_componentJs2['default']); + +ControlBar.prototype.options_ = { + loadEvent: 'play', + children: ['playToggle', 'volumeMenuButton', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'subtitlesButton', 'captionsButton', 'fullscreenToggle'] +}; + +_componentJs2['default'].registerComponent('ControlBar', ControlBar); +exports['default'] = ControlBar; +module.exports = exports['default']; + +},{"../component.js":67,"./fullscreen-toggle.js":69,"./live-display.js":70,"./mute-toggle.js":71,"./play-toggle.js":72,"./playback-rate-menu/playback-rate-menu-button.js":73,"./progress-control/progress-control.js":78,"./spacer-controls/custom-control-spacer.js":80,"./text-track-controls/captions-button.js":83,"./text-track-controls/chapters-button.js":84,"./text-track-controls/subtitles-button.js":87,"./time-controls/current-time-display.js":90,"./time-controls/duration-display.js":91,"./time-controls/remaining-time-display.js":92,"./time-controls/time-divider.js":93,"./volume-control/volume-control.js":95,"./volume-menu-button.js":97}],69:[function(_dereq_,module,exports){ +/** + * @file fullscreen-toggle.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _buttonJs = _dereq_('../button.js'); + +var _buttonJs2 = _interopRequireDefault(_buttonJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Toggle fullscreen video + * + * @extends Button + * @class FullscreenToggle + */ + +var FullscreenToggle = (function (_Button) { + _inherits(FullscreenToggle, _Button); + + function FullscreenToggle() { + _classCallCheck(this, FullscreenToggle); + + _Button.apply(this, arguments); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + FullscreenToggle.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-fullscreen-control ' + _Button.prototype.buildCSSClass.call(this); + }; + + /** + * Handles click for full screen + * + * @method handleClick + */ + + FullscreenToggle.prototype.handleClick = function handleClick() { + if (!this.player_.isFullscreen()) { + this.player_.requestFullscreen(); + this.controlText('Non-Fullscreen'); + } else { + this.player_.exitFullscreen(); + this.controlText('Fullscreen'); + } + }; + + return FullscreenToggle; +})(_buttonJs2['default']); + +FullscreenToggle.prototype.controlText_ = 'Fullscreen'; + +_componentJs2['default'].registerComponent('FullscreenToggle', FullscreenToggle); +exports['default'] = FullscreenToggle; +module.exports = exports['default']; + +},{"../button.js":64,"../component.js":67}],70:[function(_dereq_,module,exports){ +/** + * @file live-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +/** + * Displays the live indicator + * TODO - Future make it click to snap to live + * + * @extends Component + * @class LiveDisplay + */ + +var LiveDisplay = (function (_Component) { + _inherits(LiveDisplay, _Component); + + function LiveDisplay(player, options) { + _classCallCheck(this, LiveDisplay); + + _Component.call(this, player, options); + + this.updateShowing(); + this.on(this.player(), 'durationchange', this.updateShowing); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + LiveDisplay.prototype.createEl = function createEl() { + var el = _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-live-control vjs-control' + }); + + this.contentEl_ = Dom.createEl('div', { + className: 'vjs-live-display', + innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '</span>' + this.localize('LIVE') + }, { + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + return el; + }; + + LiveDisplay.prototype.updateShowing = function updateShowing() { + if (this.player().duration() === Infinity) { + this.show(); + } else { + this.hide(); + } + }; + + return LiveDisplay; +})(_component2['default']); + +_component2['default'].registerComponent('LiveDisplay', LiveDisplay); +exports['default'] = LiveDisplay; +module.exports = exports['default']; + +},{"../component":67,"../utils/dom.js":132}],71:[function(_dereq_,module,exports){ +/** + * @file mute-toggle.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _button = _dereq_('../button'); + +var _button2 = _interopRequireDefault(_button); + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +/** + * A button component for muting the audio + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Button + * @class MuteToggle + */ + +var MuteToggle = (function (_Button) { + _inherits(MuteToggle, _Button); + + function MuteToggle(player, options) { + _classCallCheck(this, MuteToggle); + + _Button.call(this, player, options); + + this.on(player, 'volumechange', this.update); + + // hide mute toggle if the current tech doesn't support volume control + if (player.tech_ && player.tech_['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } + + this.on(player, 'loadstart', function () { + this.update(); // We need to update the button to account for a default muted state. + + if (player.tech_['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } else { + this.removeClass('vjs-hidden'); + } + }); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + MuteToggle.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-mute-control ' + _Button.prototype.buildCSSClass.call(this); + }; + + /** + * Handle click on mute + * + * @method handleClick + */ + + MuteToggle.prototype.handleClick = function handleClick() { + this.player_.muted(this.player_.muted() ? false : true); + }; + + /** + * Update volume + * + * @method update + */ + + MuteToggle.prototype.update = function update() { + var vol = this.player_.volume(), + level = 3; + + if (vol === 0 || this.player_.muted()) { + level = 0; + } else if (vol < 0.33) { + level = 1; + } else if (vol < 0.67) { + level = 2; + } + + // Don't rewrite the button text if the actual text doesn't change. + // This causes unnecessary and confusing information for screen reader users. + // This check is needed because this function gets called every time the volume level is changed. + var toMute = this.player_.muted() ? 'Unmute' : 'Mute'; + if (this.controlText() !== toMute) { + this.controlText(toMute); + } + + /* TODO improve muted icon classes */ + for (var i = 0; i < 4; i++) { + Dom.removeElClass(this.el_, 'vjs-vol-' + i); + } + Dom.addElClass(this.el_, 'vjs-vol-' + level); + }; + + return MuteToggle; +})(_button2['default']); + +MuteToggle.prototype.controlText_ = 'Mute'; + +_component2['default'].registerComponent('MuteToggle', MuteToggle); +exports['default'] = MuteToggle; +module.exports = exports['default']; + +},{"../button":64,"../component":67,"../utils/dom.js":132}],72:[function(_dereq_,module,exports){ +/** + * @file play-toggle.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _buttonJs = _dereq_('../button.js'); + +var _buttonJs2 = _interopRequireDefault(_buttonJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Button to toggle between play and pause + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Button + * @class PlayToggle + */ + +var PlayToggle = (function (_Button) { + _inherits(PlayToggle, _Button); + + function PlayToggle(player, options) { + _classCallCheck(this, PlayToggle); + + _Button.call(this, player, options); + + this.on(player, 'play', this.handlePlay); + this.on(player, 'pause', this.handlePause); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + PlayToggle.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-play-control ' + _Button.prototype.buildCSSClass.call(this); + }; + + /** + * Handle click to toggle between play and pause + * + * @method handleClick + */ + + PlayToggle.prototype.handleClick = function handleClick() { + if (this.player_.paused()) { + this.player_.play(); + } else { + this.player_.pause(); + } + }; + + /** + * Add the vjs-playing class to the element so it can change appearance + * + * @method handlePlay + */ + + PlayToggle.prototype.handlePlay = function handlePlay() { + this.removeClass('vjs-paused'); + this.addClass('vjs-playing'); + this.controlText('Pause'); // change the button text to "Pause" + }; + + /** + * Add the vjs-paused class to the element so it can change appearance + * + * @method handlePause + */ + + PlayToggle.prototype.handlePause = function handlePause() { + this.removeClass('vjs-playing'); + this.addClass('vjs-paused'); + this.controlText('Play'); // change the button text to "Play" + }; + + return PlayToggle; +})(_buttonJs2['default']); + +PlayToggle.prototype.controlText_ = 'Play'; + +_componentJs2['default'].registerComponent('PlayToggle', PlayToggle); +exports['default'] = PlayToggle; +module.exports = exports['default']; + +},{"../button.js":64,"../component.js":67}],73:[function(_dereq_,module,exports){ +/** + * @file playback-rate-menu-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _menuMenuButtonJs = _dereq_('../../menu/menu-button.js'); + +var _menuMenuButtonJs2 = _interopRequireDefault(_menuMenuButtonJs); + +var _menuMenuJs = _dereq_('../../menu/menu.js'); + +var _menuMenuJs2 = _interopRequireDefault(_menuMenuJs); + +var _playbackRateMenuItemJs = _dereq_('./playback-rate-menu-item.js'); + +var _playbackRateMenuItemJs2 = _interopRequireDefault(_playbackRateMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +/** + * The component for controlling the playback rate + * + * @param {Player|Object} player + * @param {Object=} options + * @extends MenuButton + * @class PlaybackRateMenuButton + */ + +var PlaybackRateMenuButton = (function (_MenuButton) { + _inherits(PlaybackRateMenuButton, _MenuButton); + + function PlaybackRateMenuButton(player, options) { + _classCallCheck(this, PlaybackRateMenuButton); + + _MenuButton.call(this, player, options); + + this.updateVisibility(); + this.updateLabel(); + + this.on(player, 'loadstart', this.updateVisibility); + this.on(player, 'ratechange', this.updateLabel); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + PlaybackRateMenuButton.prototype.createEl = function createEl() { + var el = _MenuButton.prototype.createEl.call(this); + + this.labelEl_ = Dom.createEl('div', { + className: 'vjs-playback-rate-value', + innerHTML: 1.0 + }); + + el.appendChild(this.labelEl_); + + return el; + }; + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + PlaybackRateMenuButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-playback-rate ' + _MenuButton.prototype.buildCSSClass.call(this); + }; + + /** + * Create the playback rate menu + * + * @return {Menu} Menu object populated with items + * @method createMenu + */ + + PlaybackRateMenuButton.prototype.createMenu = function createMenu() { + var menu = new _menuMenuJs2['default'](this.player()); + var rates = this.playbackRates(); + + if (rates) { + for (var i = rates.length - 1; i >= 0; i--) { + menu.addChild(new _playbackRateMenuItemJs2['default'](this.player(), { 'rate': rates[i] + 'x' })); + } + } + + return menu; + }; + + /** + * Updates ARIA accessibility attributes + * + * @method updateARIAAttributes + */ + + PlaybackRateMenuButton.prototype.updateARIAAttributes = function updateARIAAttributes() { + // Current playback rate + this.el().setAttribute('aria-valuenow', this.player().playbackRate()); + }; + + /** + * Handle menu item click + * + * @method handleClick + */ + + PlaybackRateMenuButton.prototype.handleClick = function handleClick() { + // select next rate option + var currentRate = this.player().playbackRate(); + var rates = this.playbackRates(); + + // this will select first one if the last one currently selected + var newRate = rates[0]; + for (var i = 0; i < rates.length; i++) { + if (rates[i] > currentRate) { + newRate = rates[i]; + break; + } + } + this.player().playbackRate(newRate); + }; + + /** + * Get possible playback rates + * + * @return {Array} Possible playback rates + * @method playbackRates + */ + + PlaybackRateMenuButton.prototype.playbackRates = function playbackRates() { + return this.options_['playbackRates'] || this.options_.playerOptions && this.options_.playerOptions['playbackRates']; + }; + + /** + * Get supported playback rates + * + * @return {Array} Supported playback rates + * @method playbackRateSupported + */ + + PlaybackRateMenuButton.prototype.playbackRateSupported = function playbackRateSupported() { + return this.player().tech_ && this.player().tech_['featuresPlaybackRate'] && this.playbackRates() && this.playbackRates().length > 0; + }; + + /** + * Hide playback rate controls when they're no playback rate options to select + * + * @method updateVisibility + */ + + PlaybackRateMenuButton.prototype.updateVisibility = function updateVisibility() { + if (this.playbackRateSupported()) { + this.removeClass('vjs-hidden'); + } else { + this.addClass('vjs-hidden'); + } + }; + + /** + * Update button label when rate changed + * + * @method updateLabel + */ + + PlaybackRateMenuButton.prototype.updateLabel = function updateLabel() { + if (this.playbackRateSupported()) { + this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; + } + }; + + return PlaybackRateMenuButton; +})(_menuMenuButtonJs2['default']); + +PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; + +_componentJs2['default'].registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); +exports['default'] = PlaybackRateMenuButton; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu-button.js":104,"../../menu/menu.js":106,"../../utils/dom.js":132,"./playback-rate-menu-item.js":74}],74:[function(_dereq_,module,exports){ +/** + * @file playback-rate-menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _menuMenuItemJs = _dereq_('../../menu/menu-item.js'); + +var _menuMenuItemJs2 = _interopRequireDefault(_menuMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * The specific menu item type for selecting a playback rate + * + * @param {Player|Object} player + * @param {Object=} options + * @extends MenuItem + * @class PlaybackRateMenuItem + */ + +var PlaybackRateMenuItem = (function (_MenuItem) { + _inherits(PlaybackRateMenuItem, _MenuItem); + + function PlaybackRateMenuItem(player, options) { + _classCallCheck(this, PlaybackRateMenuItem); + + var label = options['rate']; + var rate = parseFloat(label, 10); + + // Modify options for parent MenuItem class's init. + options['label'] = label; + options['selected'] = rate === 1; + _MenuItem.call(this, player, options); + + this.label = label; + this.rate = rate; + + this.on(player, 'ratechange', this.update); + } + + /** + * Handle click on menu item + * + * @method handleClick + */ + + PlaybackRateMenuItem.prototype.handleClick = function handleClick() { + _MenuItem.prototype.handleClick.call(this); + this.player().playbackRate(this.rate); + }; + + /** + * Update playback rate with selected rate + * + * @method update + */ + + PlaybackRateMenuItem.prototype.update = function update() { + this.selected(this.player().playbackRate() === this.rate); + }; + + return PlaybackRateMenuItem; +})(_menuMenuItemJs2['default']); + +PlaybackRateMenuItem.prototype.contentElType = 'button'; + +_componentJs2['default'].registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); +exports['default'] = PlaybackRateMenuItem; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu-item.js":105}],75:[function(_dereq_,module,exports){ +/** + * @file load-progress-bar.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +/** + * Shows load progress + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class LoadProgressBar + */ + +var LoadProgressBar = (function (_Component) { + _inherits(LoadProgressBar, _Component); + + function LoadProgressBar(player, options) { + _classCallCheck(this, LoadProgressBar); + + _Component.call(this, player, options); + this.on(player, 'progress', this.update); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + LoadProgressBar.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-load-progress', + innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>' + }); + }; + + /** + * Update progress bar + * + * @method update + */ + + LoadProgressBar.prototype.update = function update() { + var buffered = this.player_.buffered(); + var duration = this.player_.duration(); + var bufferedEnd = this.player_.bufferedEnd(); + var children = this.el_.children; + + // get the percent width of a time compared to the total end + var percentify = function percentify(time, end) { + var percent = time / end || 0; // no NaN + return (percent >= 1 ? 1 : percent) * 100 + '%'; + }; + + // update the width of the progress bar + this.el_.style.width = percentify(bufferedEnd, duration); + + // add child elements to represent the individual buffered time ranges + for (var i = 0; i < buffered.length; i++) { + var start = buffered.start(i); + var end = buffered.end(i); + var part = children[i]; + + if (!part) { + part = this.el_.appendChild(Dom.createEl()); + } + + // set the percent based on the width of the progress bar (bufferedEnd) + part.style.left = percentify(start, bufferedEnd); + part.style.width = percentify(end - start, bufferedEnd); + } + + // remove unused buffered range elements + for (var i = children.length; i > buffered.length; i--) { + this.el_.removeChild(children[i - 1]); + } + }; + + return LoadProgressBar; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('LoadProgressBar', LoadProgressBar); +exports['default'] = LoadProgressBar; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/dom.js":132}],76:[function(_dereq_,module,exports){ +/** + * @file mouse-time-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +var _lodashCompatFunctionThrottle = _dereq_('lodash-compat/function/throttle'); + +var _lodashCompatFunctionThrottle2 = _interopRequireDefault(_lodashCompatFunctionThrottle); + +/** + * The Mouse Time Display component shows the time you will seek to + * when hovering over the progress bar + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class MouseTimeDisplay + */ + +var MouseTimeDisplay = (function (_Component) { + _inherits(MouseTimeDisplay, _Component); + + function MouseTimeDisplay(player, options) { + var _this = this; + + _classCallCheck(this, MouseTimeDisplay); + + _Component.call(this, player, options); + + this.update(0, 0); + + player.on('ready', function () { + _this.on(player.controlBar.progressControl.el(), 'mousemove', _lodashCompatFunctionThrottle2['default'](Fn.bind(_this, _this.handleMouseMove), 25)); + }); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + MouseTimeDisplay.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-mouse-display' + }); + }; + + MouseTimeDisplay.prototype.handleMouseMove = function handleMouseMove(event) { + var duration = this.player_.duration(); + var newTime = this.calculateDistance(event) * duration; + var position = event.pageX - Dom.findElPosition(this.el().parentNode).left; + + this.update(newTime, position); + }; + + MouseTimeDisplay.prototype.update = function update(newTime, position) { + var time = _utilsFormatTimeJs2['default'](newTime, this.player_.duration()); + + this.el().style.left = position + 'px'; + this.el().setAttribute('data-current-time', time); + }; + + MouseTimeDisplay.prototype.calculateDistance = function calculateDistance(event) { + return Dom.getPointerPosition(this.el().parentNode, event).x; + }; + + return MouseTimeDisplay; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('MouseTimeDisplay', MouseTimeDisplay); +exports['default'] = MouseTimeDisplay; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/dom.js":132,"../../utils/fn.js":134,"../../utils/format-time.js":135,"lodash-compat/function/throttle":7}],77:[function(_dereq_,module,exports){ +/** + * @file play-progress-bar.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +/** + * Shows play progress + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class PlayProgressBar + */ + +var PlayProgressBar = (function (_Component) { + _inherits(PlayProgressBar, _Component); + + function PlayProgressBar(player, options) { + _classCallCheck(this, PlayProgressBar); + + _Component.call(this, player, options); + this.updateDataAttr(); + this.on(player, 'timeupdate', this.updateDataAttr); + player.ready(Fn.bind(this, this.updateDataAttr)); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + PlayProgressBar.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-play-progress vjs-slider-bar', + innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>' + }); + }; + + PlayProgressBar.prototype.updateDataAttr = function updateDataAttr() { + var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); + this.el_.setAttribute('data-current-time', _utilsFormatTimeJs2['default'](time, this.player_.duration())); + }; + + return PlayProgressBar; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('PlayProgressBar', PlayProgressBar); +exports['default'] = PlayProgressBar; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/fn.js":134,"../../utils/format-time.js":135}],78:[function(_dereq_,module,exports){ +/** + * @file progress-control.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _seekBarJs = _dereq_('./seek-bar.js'); + +var _seekBarJs2 = _interopRequireDefault(_seekBarJs); + +var _mouseTimeDisplayJs = _dereq_('./mouse-time-display.js'); + +var _mouseTimeDisplayJs2 = _interopRequireDefault(_mouseTimeDisplayJs); + +/** + * The Progress Control component contains the seek bar, load progress, + * and play progress + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class ProgressControl + */ + +var ProgressControl = (function (_Component) { + _inherits(ProgressControl, _Component); + + function ProgressControl() { + _classCallCheck(this, ProgressControl); + + _Component.apply(this, arguments); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + ProgressControl.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-progress-control vjs-control' + }); + }; + + return ProgressControl; +})(_componentJs2['default']); + +ProgressControl.prototype.options_ = { + children: ['seekBar'] +}; + +_componentJs2['default'].registerComponent('ProgressControl', ProgressControl); +exports['default'] = ProgressControl; +module.exports = exports['default']; + +},{"../../component.js":67,"./mouse-time-display.js":76,"./seek-bar.js":79}],79:[function(_dereq_,module,exports){ +/** + * @file seek-bar.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _sliderSliderJs = _dereq_('../../slider/slider.js'); + +var _sliderSliderJs2 = _interopRequireDefault(_sliderSliderJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _loadProgressBarJs = _dereq_('./load-progress-bar.js'); + +var _loadProgressBarJs2 = _interopRequireDefault(_loadProgressBarJs); + +var _playProgressBarJs = _dereq_('./play-progress-bar.js'); + +var _playProgressBarJs2 = _interopRequireDefault(_playProgressBarJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/** + * Seek Bar and holder for the progress bars + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Slider + * @class SeekBar + */ + +var SeekBar = (function (_Slider) { + _inherits(SeekBar, _Slider); + + function SeekBar(player, options) { + _classCallCheck(this, SeekBar); + + _Slider.call(this, player, options); + this.on(player, 'timeupdate', this.updateARIAAttributes); + player.ready(Fn.bind(this, this.updateARIAAttributes)); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + SeekBar.prototype.createEl = function createEl() { + return _Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-progress-holder' + }, { + 'aria-label': 'video progress bar' + }); + }; + + /** + * Update ARIA accessibility attributes + * + * @method updateARIAAttributes + */ + + SeekBar.prototype.updateARIAAttributes = function updateARIAAttributes() { + // Allows for smooth scrubbing, when player can't keep up. + var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); + this.el_.setAttribute('aria-valuenow', (this.getPercent() * 100).toFixed(2)); // machine readable value of progress bar (percentage complete) + this.el_.setAttribute('aria-valuetext', _utilsFormatTimeJs2['default'](time, this.player_.duration())); // human readable value of progress bar (time complete) + }; + + /** + * Get percentage of video played + * + * @return {Number} Percentage played + * @method getPercent + */ + + SeekBar.prototype.getPercent = function getPercent() { + var percent = this.player_.currentTime() / this.player_.duration(); + return percent >= 1 ? 1 : percent; + }; + + /** + * Handle mouse down on seek bar + * + * @method handleMouseDown + */ + + SeekBar.prototype.handleMouseDown = function handleMouseDown(event) { + _Slider.prototype.handleMouseDown.call(this, event); + + this.player_.scrubbing(true); + + this.videoWasPlaying = !this.player_.paused(); + this.player_.pause(); + }; + + /** + * Handle mouse move on seek bar + * + * @method handleMouseMove + */ + + SeekBar.prototype.handleMouseMove = function handleMouseMove(event) { + var newTime = this.calculateDistance(event) * this.player_.duration(); + + // Don't let video end while scrubbing. + if (newTime === this.player_.duration()) { + newTime = newTime - 0.1; + } + + // Set new time (tell player to seek to new time) + this.player_.currentTime(newTime); + }; + + /** + * Handle mouse up on seek bar + * + * @method handleMouseUp + */ + + SeekBar.prototype.handleMouseUp = function handleMouseUp(event) { + _Slider.prototype.handleMouseUp.call(this, event); + + this.player_.scrubbing(false); + if (this.videoWasPlaying) { + this.player_.play(); + } + }; + + /** + * Move more quickly fast forward for keyboard-only users + * + * @method stepForward + */ + + SeekBar.prototype.stepForward = function stepForward() { + this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users + }; + + /** + * Move more quickly rewind for keyboard-only users + * + * @method stepBack + */ + + SeekBar.prototype.stepBack = function stepBack() { + this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users + }; + + return SeekBar; +})(_sliderSliderJs2['default']); + +SeekBar.prototype.options_ = { + children: ['loadProgressBar', 'mouseTimeDisplay', 'playProgressBar'], + 'barName': 'playProgressBar' +}; + +SeekBar.prototype.playerEvent = 'timeupdate'; + +_componentJs2['default'].registerComponent('SeekBar', SeekBar); +exports['default'] = SeekBar; +module.exports = exports['default']; + +},{"../../component.js":67,"../../slider/slider.js":114,"../../utils/fn.js":134,"../../utils/format-time.js":135,"./load-progress-bar.js":75,"./play-progress-bar.js":77,"object.assign":45}],80:[function(_dereq_,module,exports){ +/** + * @file custom-control-spacer.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _spacerJs = _dereq_('./spacer.js'); + +var _spacerJs2 = _interopRequireDefault(_spacerJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Spacer specifically meant to be used as an insertion point for new plugins, etc. + * + * @extends Spacer + * @class CustomControlSpacer + */ + +var CustomControlSpacer = (function (_Spacer) { + _inherits(CustomControlSpacer, _Spacer); + + function CustomControlSpacer() { + _classCallCheck(this, CustomControlSpacer); + + _Spacer.apply(this, arguments); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + CustomControlSpacer.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-custom-control-spacer ' + _Spacer.prototype.buildCSSClass.call(this); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + CustomControlSpacer.prototype.createEl = function createEl() { + var el = _Spacer.prototype.createEl.call(this, { + className: this.buildCSSClass() + }); + + // No-flex/table-cell mode requires there be some content + // in the cell to fill the remaining space of the table. + el.innerHTML = '&nbsp;'; + return el; + }; + + return CustomControlSpacer; +})(_spacerJs2['default']); + +_componentJs2['default'].registerComponent('CustomControlSpacer', CustomControlSpacer); +exports['default'] = CustomControlSpacer; +module.exports = exports['default']; + +},{"../../component.js":67,"./spacer.js":81}],81:[function(_dereq_,module,exports){ +/** + * @file spacer.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Just an empty spacer element that can be used as an append point for plugins, etc. + * Also can be used to create space between elements when necessary. + * + * @extends Component + * @class Spacer + */ + +var Spacer = (function (_Component) { + _inherits(Spacer, _Component); + + function Spacer() { + _classCallCheck(this, Spacer); + + _Component.apply(this, arguments); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + Spacer.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-spacer ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Spacer.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass() + }); + }; + + return Spacer; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('Spacer', Spacer); + +exports['default'] = Spacer; +module.exports = exports['default']; + +},{"../../component.js":67}],82:[function(_dereq_,module,exports){ +/** + * @file caption-settings-menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackMenuItemJs = _dereq_('./text-track-menu-item.js'); + +var _textTrackMenuItemJs2 = _interopRequireDefault(_textTrackMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * The menu item for caption track settings menu + * + * @param {Player|Object} player + * @param {Object=} options + * @extends TextTrackMenuItem + * @class CaptionSettingsMenuItem + */ + +var CaptionSettingsMenuItem = (function (_TextTrackMenuItem) { + _inherits(CaptionSettingsMenuItem, _TextTrackMenuItem); + + function CaptionSettingsMenuItem(player, options) { + _classCallCheck(this, CaptionSettingsMenuItem); + + options['track'] = { + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' settings', + 'selectable': false, + 'default': false, + mode: 'disabled' + }; + + // CaptionSettingsMenuItem has no concept of 'selected' + options['selectable'] = false; + + _TextTrackMenuItem.call(this, player, options); + this.addClass('vjs-texttrack-settings'); + this.controlText(', opens ' + options['kind'] + ' settings dialog'); + } + + /** + * Handle click on menu item + * + * @method handleClick + */ + + CaptionSettingsMenuItem.prototype.handleClick = function handleClick() { + this.player().getChild('textTrackSettings').show(); + this.player().getChild('textTrackSettings').el_.focus(); + }; + + return CaptionSettingsMenuItem; +})(_textTrackMenuItemJs2['default']); + +_componentJs2['default'].registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); +exports['default'] = CaptionSettingsMenuItem; +module.exports = exports['default']; + +},{"../../component.js":67,"./text-track-menu-item.js":89}],83:[function(_dereq_,module,exports){ +/** + * @file captions-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackButtonJs = _dereq_('./text-track-button.js'); + +var _textTrackButtonJs2 = _interopRequireDefault(_textTrackButtonJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _captionSettingsMenuItemJs = _dereq_('./caption-settings-menu-item.js'); + +var _captionSettingsMenuItemJs2 = _interopRequireDefault(_captionSettingsMenuItemJs); + +/** + * The button component for toggling and selecting captions + * + * @param {Object} player Player object + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends TextTrackButton + * @class CaptionsButton + */ + +var CaptionsButton = (function (_TextTrackButton) { + _inherits(CaptionsButton, _TextTrackButton); + + function CaptionsButton(player, options, ready) { + _classCallCheck(this, CaptionsButton); + + _TextTrackButton.call(this, player, options, ready); + this.el_.setAttribute('aria-label', 'Captions Menu'); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + CaptionsButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-captions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + /** + * Update caption menu items + * + * @method update + */ + + CaptionsButton.prototype.update = function update() { + var threshold = 2; + _TextTrackButton.prototype.update.call(this); + + // if native, then threshold is 1 because no settings button + if (this.player().tech_ && this.player().tech_['featuresNativeTextTracks']) { + threshold = 1; + } + + if (this.items && this.items.length > threshold) { + this.show(); + } else { + this.hide(); + } + }; + + /** + * Create caption menu items + * + * @return {Array} Array of menu items + * @method createItems + */ + + CaptionsButton.prototype.createItems = function createItems() { + var items = []; + + if (!(this.player().tech_ && this.player().tech_['featuresNativeTextTracks'])) { + items.push(new _captionSettingsMenuItemJs2['default'](this.player_, { 'kind': this.kind_ })); + } + + return _TextTrackButton.prototype.createItems.call(this, items); + }; + + return CaptionsButton; +})(_textTrackButtonJs2['default']); + +CaptionsButton.prototype.kind_ = 'captions'; +CaptionsButton.prototype.controlText_ = 'Captions'; + +_componentJs2['default'].registerComponent('CaptionsButton', CaptionsButton); +exports['default'] = CaptionsButton; +module.exports = exports['default']; + +},{"../../component.js":67,"./caption-settings-menu-item.js":82,"./text-track-button.js":88}],84:[function(_dereq_,module,exports){ +/** + * @file chapters-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackButtonJs = _dereq_('./text-track-button.js'); + +var _textTrackButtonJs2 = _interopRequireDefault(_textTrackButtonJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _textTrackMenuItemJs = _dereq_('./text-track-menu-item.js'); + +var _textTrackMenuItemJs2 = _interopRequireDefault(_textTrackMenuItemJs); + +var _chaptersTrackMenuItemJs = _dereq_('./chapters-track-menu-item.js'); + +var _chaptersTrackMenuItemJs2 = _interopRequireDefault(_chaptersTrackMenuItemJs); + +var _menuMenuJs = _dereq_('../../menu/menu.js'); + +var _menuMenuJs2 = _interopRequireDefault(_menuMenuJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsToTitleCaseJs = _dereq_('../../utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +/** + * The button component for toggling and selecting chapters + * Chapters act much differently than other text tracks + * Cues are navigation vs. other tracks of alternative languages + * + * @param {Object} player Player object + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends TextTrackButton + * @class ChaptersButton + */ + +var ChaptersButton = (function (_TextTrackButton) { + _inherits(ChaptersButton, _TextTrackButton); + + function ChaptersButton(player, options, ready) { + _classCallCheck(this, ChaptersButton); + + _TextTrackButton.call(this, player, options, ready); + this.el_.setAttribute('aria-label', 'Chapters Menu'); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + ChaptersButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + /** + * Create a menu item for each text track + * + * @return {Array} Array of menu items + * @method createItems + */ + + ChaptersButton.prototype.createItems = function createItems() { + var items = []; + + var tracks = this.player_.textTracks(); + + if (!tracks) { + return items; + } + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + if (track['kind'] === this.kind_) { + items.push(new _textTrackMenuItemJs2['default'](this.player_, { + 'track': track + })); + } + } + + return items; + }; + + /** + * Create menu from chapter buttons + * + * @return {Menu} Menu of chapter buttons + * @method createMenu + */ + + ChaptersButton.prototype.createMenu = function createMenu() { + var _this = this; + + var tracks = this.player_.textTracks() || []; + var chaptersTrack = undefined; + var items = this.items = []; + + for (var i = 0, _length = tracks.length; i < _length; i++) { + var track = tracks[i]; + + if (track['kind'] === this.kind_) { + chaptersTrack = track; + + break; + } + } + + var menu = this.menu; + if (menu === undefined) { + menu = new _menuMenuJs2['default'](this.player_); + var title = Dom.createEl('li', { + className: 'vjs-menu-title', + innerHTML: _utilsToTitleCaseJs2['default'](this.kind_), + tabIndex: -1 + }); + menu.children_.unshift(title); + Dom.insertElFirst(title, menu.contentEl()); + } + + if (chaptersTrack && chaptersTrack.cues == null) { + chaptersTrack['mode'] = 'hidden'; + + var remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(chaptersTrack); + + if (remoteTextTrackEl) { + remoteTextTrackEl.addEventListener('load', function (event) { + return _this.update(); + }); + } + } + + if (chaptersTrack && chaptersTrack.cues && chaptersTrack.cues.length > 0) { + var cues = chaptersTrack['cues'], + cue = undefined; + + for (var i = 0, l = cues.length; i < l; i++) { + cue = cues[i]; + + var mi = new _chaptersTrackMenuItemJs2['default'](this.player_, { + 'track': chaptersTrack, + 'cue': cue + }); + + items.push(mi); + + menu.addChild(mi); + } + + this.addChild(menu); + } + + if (this.items.length > 0) { + this.show(); + } + + return menu; + }; + + return ChaptersButton; +})(_textTrackButtonJs2['default']); + +ChaptersButton.prototype.kind_ = 'chapters'; +ChaptersButton.prototype.controlText_ = 'Chapters'; + +_componentJs2['default'].registerComponent('ChaptersButton', ChaptersButton); +exports['default'] = ChaptersButton; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu.js":106,"../../utils/dom.js":132,"../../utils/fn.js":134,"../../utils/to-title-case.js":141,"./chapters-track-menu-item.js":85,"./text-track-button.js":88,"./text-track-menu-item.js":89,"global/window":2}],85:[function(_dereq_,module,exports){ +/** + * @file chapters-track-menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _menuMenuItemJs = _dereq_('../../menu/menu-item.js'); + +var _menuMenuItemJs2 = _interopRequireDefault(_menuMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +/** + * The chapter track menu item + * + * @param {Player|Object} player + * @param {Object=} options + * @extends MenuItem + * @class ChaptersTrackMenuItem + */ + +var ChaptersTrackMenuItem = (function (_MenuItem) { + _inherits(ChaptersTrackMenuItem, _MenuItem); + + function ChaptersTrackMenuItem(player, options) { + _classCallCheck(this, ChaptersTrackMenuItem); + + var track = options['track']; + var cue = options['cue']; + var currentTime = player.currentTime(); + + // Modify options for parent MenuItem class's init. + options['label'] = cue.text; + options['selected'] = cue['startTime'] <= currentTime && currentTime < cue['endTime']; + _MenuItem.call(this, player, options); + + this.track = track; + this.cue = cue; + track.addEventListener('cuechange', Fn.bind(this, this.update)); + } + + /** + * Handle click on menu item + * + * @method handleClick + */ + + ChaptersTrackMenuItem.prototype.handleClick = function handleClick() { + _MenuItem.prototype.handleClick.call(this); + this.player_.currentTime(this.cue.startTime); + this.update(this.cue.startTime); + }; + + /** + * Update chapter menu item + * + * @method update + */ + + ChaptersTrackMenuItem.prototype.update = function update() { + var cue = this.cue; + var currentTime = this.player_.currentTime(); + + // vjs.log(currentTime, cue.startTime); + this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']); + }; + + return ChaptersTrackMenuItem; +})(_menuMenuItemJs2['default']); + +_componentJs2['default'].registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); +exports['default'] = ChaptersTrackMenuItem; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu-item.js":105,"../../utils/fn.js":134}],86:[function(_dereq_,module,exports){ +/** + * @file off-text-track-menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackMenuItemJs = _dereq_('./text-track-menu-item.js'); + +var _textTrackMenuItemJs2 = _interopRequireDefault(_textTrackMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * A special menu item for turning of a specific type of text track + * + * @param {Player|Object} player + * @param {Object=} options + * @extends TextTrackMenuItem + * @class OffTextTrackMenuItem + */ + +var OffTextTrackMenuItem = (function (_TextTrackMenuItem) { + _inherits(OffTextTrackMenuItem, _TextTrackMenuItem); + + function OffTextTrackMenuItem(player, options) { + _classCallCheck(this, OffTextTrackMenuItem); + + // Create pseudo track info + // Requires options['kind'] + options['track'] = { + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' off', + 'default': false, + 'mode': 'disabled' + }; + + // MenuItem is selectable + options['selectable'] = true; + + _TextTrackMenuItem.call(this, player, options); + this.selected(true); + } + + /** + * Handle text track change + * + * @param {Object} event Event object + * @method handleTracksChange + */ + + OffTextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { + var tracks = this.player().textTracks(); + var selected = true; + + for (var i = 0, l = tracks.length; i < l; i++) { + var track = tracks[i]; + if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') { + selected = false; + break; + } + } + + this.selected(selected); + }; + + return OffTextTrackMenuItem; +})(_textTrackMenuItemJs2['default']); + +_componentJs2['default'].registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); +exports['default'] = OffTextTrackMenuItem; +module.exports = exports['default']; + +},{"../../component.js":67,"./text-track-menu-item.js":89}],87:[function(_dereq_,module,exports){ +/** + * @file subtitles-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackButtonJs = _dereq_('./text-track-button.js'); + +var _textTrackButtonJs2 = _interopRequireDefault(_textTrackButtonJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * The button component for toggling and selecting subtitles + * + * @param {Object} player Player object + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends TextTrackButton + * @class SubtitlesButton + */ + +var SubtitlesButton = (function (_TextTrackButton) { + _inherits(SubtitlesButton, _TextTrackButton); + + function SubtitlesButton(player, options, ready) { + _classCallCheck(this, SubtitlesButton); + + _TextTrackButton.call(this, player, options, ready); + this.el_.setAttribute('aria-label', 'Subtitles Menu'); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + SubtitlesButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + return SubtitlesButton; +})(_textTrackButtonJs2['default']); + +SubtitlesButton.prototype.kind_ = 'subtitles'; +SubtitlesButton.prototype.controlText_ = 'Subtitles'; + +_componentJs2['default'].registerComponent('SubtitlesButton', SubtitlesButton); +exports['default'] = SubtitlesButton; +module.exports = exports['default']; + +},{"../../component.js":67,"./text-track-button.js":88}],88:[function(_dereq_,module,exports){ +/** + * @file text-track-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _menuMenuButtonJs = _dereq_('../../menu/menu-button.js'); + +var _menuMenuButtonJs2 = _interopRequireDefault(_menuMenuButtonJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _textTrackMenuItemJs = _dereq_('./text-track-menu-item.js'); + +var _textTrackMenuItemJs2 = _interopRequireDefault(_textTrackMenuItemJs); + +var _offTextTrackMenuItemJs = _dereq_('./off-text-track-menu-item.js'); + +var _offTextTrackMenuItemJs2 = _interopRequireDefault(_offTextTrackMenuItemJs); + +/** + * The base class for buttons that toggle specific text track types (e.g. subtitles) + * + * @param {Player|Object} player + * @param {Object=} options + * @extends MenuButton + * @class TextTrackButton + */ + +var TextTrackButton = (function (_MenuButton) { + _inherits(TextTrackButton, _MenuButton); + + function TextTrackButton(player, options) { + _classCallCheck(this, TextTrackButton); + + _MenuButton.call(this, player, options); + + var tracks = this.player_.textTracks(); + + if (this.items.length <= 1) { + this.hide(); + } + + if (!tracks) { + return; + } + + var updateHandler = Fn.bind(this, this.update); + tracks.addEventListener('removetrack', updateHandler); + tracks.addEventListener('addtrack', updateHandler); + + this.player_.on('dispose', function () { + tracks.removeEventListener('removetrack', updateHandler); + tracks.removeEventListener('addtrack', updateHandler); + }); + } + + // Create a menu item for each text track + + TextTrackButton.prototype.createItems = function createItems() { + var items = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; + + // Add an OFF menu item to turn all tracks off + items.push(new _offTextTrackMenuItemJs2['default'](this.player_, { 'kind': this.kind_ })); + + var tracks = this.player_.textTracks(); + + if (!tracks) { + return items; + } + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + // only add tracks that are of the appropriate kind and have a label + if (track['kind'] === this.kind_) { + items.push(new _textTrackMenuItemJs2['default'](this.player_, { + // MenuItem is selectable + 'selectable': true, + 'track': track + })); + } + } + + return items; + }; + + return TextTrackButton; +})(_menuMenuButtonJs2['default']); + +_componentJs2['default'].registerComponent('TextTrackButton', TextTrackButton); +exports['default'] = TextTrackButton; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu-button.js":104,"../../utils/fn.js":134,"./off-text-track-menu-item.js":86,"./text-track-menu-item.js":89}],89:[function(_dereq_,module,exports){ +/** + * @file text-track-menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _menuMenuItemJs = _dereq_('../../menu/menu-item.js'); + +var _menuMenuItemJs2 = _interopRequireDefault(_menuMenuItemJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * The specific menu item type for selecting a language within a text track kind + * + * @param {Player|Object} player + * @param {Object=} options + * @extends MenuItem + * @class TextTrackMenuItem + */ + +var TextTrackMenuItem = (function (_MenuItem) { + _inherits(TextTrackMenuItem, _MenuItem); + + function TextTrackMenuItem(player, options) { + var _this = this; + + _classCallCheck(this, TextTrackMenuItem); + + var track = options['track']; + var tracks = player.textTracks(); + + // Modify options for parent MenuItem class's init. + options['label'] = track['label'] || track['language'] || 'Unknown'; + options['selected'] = track['default'] || track['mode'] === 'showing'; + + _MenuItem.call(this, player, options); + + this.track = track; + + if (tracks) { + (function () { + var changeHandler = Fn.bind(_this, _this.handleTracksChange); + + tracks.addEventListener('change', changeHandler); + _this.on('dispose', function () { + tracks.removeEventListener('change', changeHandler); + }); + })(); + } + + // iOS7 doesn't dispatch change events to TextTrackLists when an + // associated track's mode changes. Without something like + // Object.observe() (also not present on iOS7), it's not + // possible to detect changes to the mode attribute and polyfill + // the change event. As a poor substitute, we manually dispatch + // change events whenever the controls modify the mode. + if (tracks && tracks.onchange === undefined) { + (function () { + var event = undefined; + + _this.on(['tap', 'click'], function () { + if (typeof _globalWindow2['default'].Event !== 'object') { + // Android 2.3 throws an Illegal Constructor error for window.Event + try { + event = new _globalWindow2['default'].Event('change'); + } catch (err) {} + } + + if (!event) { + event = _globalDocument2['default'].createEvent('Event'); + event.initEvent('change', true, true); + } + + tracks.dispatchEvent(event); + }); + })(); + } + } + + /** + * Handle click on text track + * + * @method handleClick + */ + + TextTrackMenuItem.prototype.handleClick = function handleClick(event) { + var kind = this.track['kind']; + var tracks = this.player_.textTracks(); + + _MenuItem.prototype.handleClick.call(this, event); + + if (!tracks) return; + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + if (track['kind'] !== kind) { + continue; + } + + if (track === this.track) { + track['mode'] = 'showing'; + } else { + track['mode'] = 'disabled'; + } + } + }; + + /** + * Handle text track change + * + * @method handleTracksChange + */ + + TextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { + this.selected(this.track['mode'] === 'showing'); + }; + + return TextTrackMenuItem; +})(_menuMenuItemJs2['default']); + +_componentJs2['default'].registerComponent('TextTrackMenuItem', TextTrackMenuItem); +exports['default'] = TextTrackMenuItem; +module.exports = exports['default']; + +},{"../../component.js":67,"../../menu/menu-item.js":105,"../../utils/fn.js":134,"global/document":1,"global/window":2}],90:[function(_dereq_,module,exports){ +/** + * @file current-time-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +/** + * Displays the current time + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class CurrentTimeDisplay + */ + +var CurrentTimeDisplay = (function (_Component) { + _inherits(CurrentTimeDisplay, _Component); + + function CurrentTimeDisplay(player, options) { + _classCallCheck(this, CurrentTimeDisplay); + + _Component.call(this, player, options); + + this.on(player, 'timeupdate', this.updateContent); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + CurrentTimeDisplay.prototype.createEl = function createEl() { + var el = _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-current-time vjs-time-control vjs-control' + }); + + this.contentEl_ = Dom.createEl('div', { + className: 'vjs-current-time-display', + // label the current time for screen reader users + innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00' + }, { + // tell screen readers not to automatically read the time as it changes + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + return el; + }; + + /** + * Update current time display + * + * @method updateContent + */ + + CurrentTimeDisplay.prototype.updateContent = function updateContent() { + // Allows for smooth scrubbing, when player can't keep up. + var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); + var localizedText = this.localize('Current Time'); + var formattedTime = _utilsFormatTimeJs2['default'](time, this.player_.duration()); + if (formattedTime !== this.formattedTime_) { + this.formattedTime_ = formattedTime; + this.contentEl_.innerHTML = '<span class="vjs-control-text">' + localizedText + '</span> ' + formattedTime; + } + }; + + return CurrentTimeDisplay; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); +exports['default'] = CurrentTimeDisplay; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/dom.js":132,"../../utils/format-time.js":135}],91:[function(_dereq_,module,exports){ +/** + * @file duration-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +/** + * Displays the duration + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class DurationDisplay + */ + +var DurationDisplay = (function (_Component) { + _inherits(DurationDisplay, _Component); + + function DurationDisplay(player, options) { + _classCallCheck(this, DurationDisplay); + + _Component.call(this, player, options); + + // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, + // however the durationchange event fires before this.player_.duration() is set, + // so the value cannot be written out using this method. + // Once the order of durationchange and this.player_.duration() being set is figured out, + // this can be updated. + this.on(player, 'timeupdate', this.updateContent); + this.on(player, 'loadedmetadata', this.updateContent); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + DurationDisplay.prototype.createEl = function createEl() { + var el = _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-duration vjs-time-control vjs-control' + }); + + this.contentEl_ = Dom.createEl('div', { + className: 'vjs-duration-display', + // label the duration time for screen reader users + innerHTML: '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> 0:00' + }, { + // tell screen readers not to automatically read the time as it changes + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + return el; + }; + + /** + * Update duration time display + * + * @method updateContent + */ + + DurationDisplay.prototype.updateContent = function updateContent() { + var duration = this.player_.duration(); + if (duration && this.duration_ !== duration) { + this.duration_ = duration; + var localizedText = this.localize('Duration Time'); + var formattedTime = _utilsFormatTimeJs2['default'](duration); + this.contentEl_.innerHTML = '<span class="vjs-control-text">' + localizedText + '</span> ' + formattedTime; // label the duration time for screen reader users + } + }; + + return DurationDisplay; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('DurationDisplay', DurationDisplay); +exports['default'] = DurationDisplay; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/dom.js":132,"../../utils/format-time.js":135}],92:[function(_dereq_,module,exports){ +/** + * @file remaining-time-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFormatTimeJs = _dereq_('../../utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +/** + * Displays the time left in the video + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class RemainingTimeDisplay + */ + +var RemainingTimeDisplay = (function (_Component) { + _inherits(RemainingTimeDisplay, _Component); + + function RemainingTimeDisplay(player, options) { + _classCallCheck(this, RemainingTimeDisplay); + + _Component.call(this, player, options); + + this.on(player, 'timeupdate', this.updateContent); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + RemainingTimeDisplay.prototype.createEl = function createEl() { + var el = _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-remaining-time vjs-time-control vjs-control' + }); + + this.contentEl_ = Dom.createEl('div', { + className: 'vjs-remaining-time-display', + // label the remaining time for screen reader users + innerHTML: '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> -0:00' + }, { + // tell screen readers not to automatically read the time as it changes + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + return el; + }; + + /** + * Update remaining time display + * + * @method updateContent + */ + + RemainingTimeDisplay.prototype.updateContent = function updateContent() { + if (this.player_.duration()) { + var localizedText = this.localize('Remaining Time'); + var formattedTime = _utilsFormatTimeJs2['default'](this.player_.remainingTime()); + if (formattedTime !== this.formattedTime_) { + this.formattedTime_ = formattedTime; + this.contentEl_.innerHTML = '<span class="vjs-control-text">' + localizedText + '</span> -' + formattedTime; + } + } + + // Allows for smooth scrubbing, when player can't keep up. + // var time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); + // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); + }; + + return RemainingTimeDisplay; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); +exports['default'] = RemainingTimeDisplay; +module.exports = exports['default']; + +},{"../../component.js":67,"../../utils/dom.js":132,"../../utils/format-time.js":135}],93:[function(_dereq_,module,exports){ +/** + * @file time-divider.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * The separator between the current time and duration. + * Can be hidden if it's not needed in the design. + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class TimeDivider + */ + +var TimeDivider = (function (_Component) { + _inherits(TimeDivider, _Component); + + function TimeDivider() { + _classCallCheck(this, TimeDivider); + + _Component.apply(this, arguments); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + TimeDivider.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-time-control vjs-time-divider', + innerHTML: '<div><span>/</span></div>' + }); + }; + + return TimeDivider; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('TimeDivider', TimeDivider); +exports['default'] = TimeDivider; +module.exports = exports['default']; + +},{"../../component.js":67}],94:[function(_dereq_,module,exports){ +/** + * @file volume-bar.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _sliderSliderJs = _dereq_('../../slider/slider.js'); + +var _sliderSliderJs2 = _interopRequireDefault(_sliderSliderJs); + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('../../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +// Required children + +var _volumeLevelJs = _dereq_('./volume-level.js'); + +var _volumeLevelJs2 = _interopRequireDefault(_volumeLevelJs); + +/** + * The bar that contains the volume level and can be clicked on to adjust the level + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Slider + * @class VolumeBar + */ + +var VolumeBar = (function (_Slider) { + _inherits(VolumeBar, _Slider); + + function VolumeBar(player, options) { + _classCallCheck(this, VolumeBar); + + _Slider.call(this, player, options); + this.on(player, 'volumechange', this.updateARIAAttributes); + player.ready(Fn.bind(this, this.updateARIAAttributes)); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + VolumeBar.prototype.createEl = function createEl() { + return _Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-bar vjs-slider-bar' + }, { + 'aria-label': 'volume level' + }); + }; + + /** + * Handle mouse move on volume bar + * + * @method handleMouseMove + */ + + VolumeBar.prototype.handleMouseMove = function handleMouseMove(event) { + this.checkMuted(); + this.player_.volume(this.calculateDistance(event)); + }; + + VolumeBar.prototype.checkMuted = function checkMuted() { + if (this.player_.muted()) { + this.player_.muted(false); + } + }; + + /** + * Get percent of volume level + * + * @retun {Number} Volume level percent + * @method getPercent + */ + + VolumeBar.prototype.getPercent = function getPercent() { + if (this.player_.muted()) { + return 0; + } else { + return this.player_.volume(); + } + }; + + /** + * Increase volume level for keyboard users + * + * @method stepForward + */ + + VolumeBar.prototype.stepForward = function stepForward() { + this.checkMuted(); + this.player_.volume(this.player_.volume() + 0.1); + }; + + /** + * Decrease volume level for keyboard users + * + * @method stepBack + */ + + VolumeBar.prototype.stepBack = function stepBack() { + this.checkMuted(); + this.player_.volume(this.player_.volume() - 0.1); + }; + + /** + * Update ARIA accessibility attributes + * + * @method updateARIAAttributes + */ + + VolumeBar.prototype.updateARIAAttributes = function updateARIAAttributes() { + // Current value of volume bar as a percentage + var volume = (this.player_.volume() * 100).toFixed(2); + this.el_.setAttribute('aria-valuenow', volume); + this.el_.setAttribute('aria-valuetext', volume + '%'); + }; + + return VolumeBar; +})(_sliderSliderJs2['default']); + +VolumeBar.prototype.options_ = { + children: ['volumeLevel'], + 'barName': 'volumeLevel' +}; + +VolumeBar.prototype.playerEvent = 'volumechange'; + +_componentJs2['default'].registerComponent('VolumeBar', VolumeBar); +exports['default'] = VolumeBar; +module.exports = exports['default']; + +},{"../../component.js":67,"../../slider/slider.js":114,"../../utils/fn.js":134,"./volume-level.js":96}],95:[function(_dereq_,module,exports){ +/** + * @file volume-control.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +// Required children + +var _volumeBarJs = _dereq_('./volume-bar.js'); + +var _volumeBarJs2 = _interopRequireDefault(_volumeBarJs); + +/** + * The component for controlling the volume level + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class VolumeControl + */ + +var VolumeControl = (function (_Component) { + _inherits(VolumeControl, _Component); + + function VolumeControl(player, options) { + _classCallCheck(this, VolumeControl); + + _Component.call(this, player, options); + + // hide volume controls when they're not supported by the current tech + if (player.tech_ && player.tech_['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } + this.on(player, 'loadstart', function () { + if (player.tech_['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } else { + this.removeClass('vjs-hidden'); + } + }); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + VolumeControl.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-control vjs-control' + }); + }; + + return VolumeControl; +})(_componentJs2['default']); + +VolumeControl.prototype.options_ = { + children: ['volumeBar'] +}; + +_componentJs2['default'].registerComponent('VolumeControl', VolumeControl); +exports['default'] = VolumeControl; +module.exports = exports['default']; + +},{"../../component.js":67,"./volume-bar.js":94}],96:[function(_dereq_,module,exports){ +/** + * @file volume-level.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +/** + * Shows volume level + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class VolumeLevel + */ + +var VolumeLevel = (function (_Component) { + _inherits(VolumeLevel, _Component); + + function VolumeLevel() { + _classCallCheck(this, VolumeLevel); + + _Component.apply(this, arguments); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + VolumeLevel.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-level', + innerHTML: '<span class="vjs-control-text"></span>' + }); + }; + + return VolumeLevel; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('VolumeLevel', VolumeLevel); +exports['default'] = VolumeLevel; +module.exports = exports['default']; + +},{"../../component.js":67}],97:[function(_dereq_,module,exports){ +/** + * @file volume-menu-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _popupPopupJs = _dereq_('../popup/popup.js'); + +var _popupPopupJs2 = _interopRequireDefault(_popupPopupJs); + +var _popupPopupButtonJs = _dereq_('../popup/popup-button.js'); + +var _popupPopupButtonJs2 = _interopRequireDefault(_popupPopupButtonJs); + +var _muteToggleJs = _dereq_('./mute-toggle.js'); + +var _muteToggleJs2 = _interopRequireDefault(_muteToggleJs); + +var _volumeControlVolumeBarJs = _dereq_('./volume-control/volume-bar.js'); + +var _volumeControlVolumeBarJs2 = _interopRequireDefault(_volumeControlVolumeBarJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * Button for volume popup + * + * @param {Player|Object} player + * @param {Object=} options + * @extends PopupButton + * @class VolumeMenuButton + */ + +var VolumeMenuButton = (function (_PopupButton) { + _inherits(VolumeMenuButton, _PopupButton); + + function VolumeMenuButton(player) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, VolumeMenuButton); + + // Default to inline + if (options.inline === undefined) { + options.inline = true; + } + + // If the vertical option isn't passed at all, default to true. + if (options.vertical === undefined) { + // If an inline volumeMenuButton is used, we should default to using + // a horizontal slider for obvious reasons. + if (options.inline) { + options.vertical = false; + } else { + options.vertical = true; + } + } + + // The vertical option needs to be set on the volumeBar as well, + // since that will need to be passed along to the VolumeBar constructor + options.volumeBar = options.volumeBar || {}; + options.volumeBar.vertical = !!options.vertical; + + _PopupButton.call(this, player, options); + + // Same listeners as MuteToggle + this.on(player, 'volumechange', this.volumeUpdate); + this.on(player, 'loadstart', this.volumeUpdate); + + // hide mute toggle if the current tech doesn't support volume control + function updateVisibility() { + if (player.tech_ && player.tech_['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } else { + this.removeClass('vjs-hidden'); + } + } + + updateVisibility.call(this); + this.on(player, 'loadstart', updateVisibility); + + this.on(this.volumeBar, ['slideractive', 'focus'], function () { + this.addClass('vjs-slider-active'); + }); + + this.on(this.volumeBar, ['sliderinactive', 'blur'], function () { + this.removeClass('vjs-slider-active'); + }); + + this.on(this.volumeBar, ['focus'], function () { + this.addClass('vjs-lock-showing'); + }); + + this.on(this.volumeBar, ['blur'], function () { + this.removeClass('vjs-lock-showing'); + }); + } + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + VolumeMenuButton.prototype.buildCSSClass = function buildCSSClass() { + var orientationClass = ''; + if (!!this.options_.vertical) { + orientationClass = 'vjs-volume-menu-button-vertical'; + } else { + orientationClass = 'vjs-volume-menu-button-horizontal'; + } + + return 'vjs-volume-menu-button ' + _PopupButton.prototype.buildCSSClass.call(this) + ' ' + orientationClass; + }; + + /** + * Allow sub components to stack CSS class names + * + * @return {Popup} The volume popup button + * @method createPopup + */ + + VolumeMenuButton.prototype.createPopup = function createPopup() { + var popup = new _popupPopupJs2['default'](this.player_, { + contentElType: 'div' + }); + + var vb = new _volumeControlVolumeBarJs2['default'](this.player_, this.options_.volumeBar); + + popup.addChild(vb); + + this.menuContent = popup; + this.volumeBar = vb; + + this.attachVolumeBarEvents(); + + return popup; + }; + + /** + * Handle click on volume popup and calls super + * + * @method handleClick + */ + + VolumeMenuButton.prototype.handleClick = function handleClick() { + _muteToggleJs2['default'].prototype.handleClick.call(this); + _PopupButton.prototype.handleClick.call(this); + }; + + VolumeMenuButton.prototype.attachVolumeBarEvents = function attachVolumeBarEvents() { + this.menuContent.on(['mousedown', 'touchdown'], Fn.bind(this, this.handleMouseDown)); + }; + + VolumeMenuButton.prototype.handleMouseDown = function handleMouseDown(event) { + this.on(['mousemove', 'touchmove'], Fn.bind(this.volumeBar, this.volumeBar.handleMouseMove)); + this.on(_globalDocument2['default'], ['mouseup', 'touchend'], this.handleMouseUp); + }; + + VolumeMenuButton.prototype.handleMouseUp = function handleMouseUp(event) { + this.off(['mousemove', 'touchmove'], Fn.bind(this.volumeBar, this.volumeBar.handleMouseMove)); + }; + + return VolumeMenuButton; +})(_popupPopupButtonJs2['default']); + +VolumeMenuButton.prototype.volumeUpdate = _muteToggleJs2['default'].prototype.update; +VolumeMenuButton.prototype.controlText_ = 'Mute'; + +_componentJs2['default'].registerComponent('VolumeMenuButton', VolumeMenuButton); +exports['default'] = VolumeMenuButton; +module.exports = exports['default']; + +},{"../component.js":67,"../popup/popup-button.js":110,"../popup/popup.js":111,"../utils/fn.js":134,"./mute-toggle.js":71,"./volume-control/volume-bar.js":94,"global/document":1}],98:[function(_dereq_,module,exports){ +/** + * @file error-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +var _modalDialog = _dereq_('./modal-dialog'); + +var _modalDialog2 = _interopRequireDefault(_modalDialog); + +var _utilsDom = _dereq_('./utils/dom'); + +var Dom = _interopRequireWildcard(_utilsDom); + +var _utilsMergeOptions = _dereq_('./utils/merge-options'); + +var _utilsMergeOptions2 = _interopRequireDefault(_utilsMergeOptions); + +/** + * Display that an error has occurred making the video unplayable. + * + * @extends ModalDialog + * @class ErrorDisplay + */ + +var ErrorDisplay = (function (_ModalDialog) { + _inherits(ErrorDisplay, _ModalDialog); + + /** + * Constructor for error display modal. + * + * @param {Player} player + * @param {Object} [options] + */ + + function ErrorDisplay(player, options) { + _classCallCheck(this, ErrorDisplay); + + _ModalDialog.call(this, player, options); + this.on(player, 'error', this.open); + } + + /** + * Include the old class for backward-compatibility. + * + * This can be removed in 6.0. + * + * @method buildCSSClass + * @deprecated + * @return {String} + */ + + ErrorDisplay.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-error-display ' + _ModalDialog.prototype.buildCSSClass.call(this); + }; + + /** + * Generates the modal content based on the player error. + * + * @return {String|Null} + */ + + ErrorDisplay.prototype.content = function content() { + var error = this.player().error(); + return error ? this.localize(error.message) : ''; + }; + + return ErrorDisplay; +})(_modalDialog2['default']); + +ErrorDisplay.prototype.options_ = _utilsMergeOptions2['default'](_modalDialog2['default'].prototype.options_, { + fillAlways: true, + temporary: false, + uncloseable: true +}); + +_component2['default'].registerComponent('ErrorDisplay', ErrorDisplay); +exports['default'] = ErrorDisplay; +module.exports = exports['default']; + +},{"./component":67,"./modal-dialog":107,"./utils/dom":132,"./utils/merge-options":138}],99:[function(_dereq_,module,exports){ +/** + * @file event-target.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var EventTarget = function EventTarget() {}; + +EventTarget.prototype.allowedEvents_ = {}; + +EventTarget.prototype.on = function (type, fn) { + // Remove the addEventListener alias before calling Events.on + // so we don't get into an infinite type loop + var ael = this.addEventListener; + this.addEventListener = Function.prototype; + Events.on(this, type, fn); + this.addEventListener = ael; +}; +EventTarget.prototype.addEventListener = EventTarget.prototype.on; + +EventTarget.prototype.off = function (type, fn) { + Events.off(this, type, fn); +}; +EventTarget.prototype.removeEventListener = EventTarget.prototype.off; + +EventTarget.prototype.one = function (type, fn) { + Events.one(this, type, fn); +}; + +EventTarget.prototype.trigger = function (event) { + var type = event.type || event; + + if (typeof event === 'string') { + event = { + type: type + }; + } + event = Events.fixEvent(event); + + if (this.allowedEvents_[type] && this['on' + type]) { + this['on' + type](event); + } + + Events.trigger(this, event); +}; +// The standard DOM EventTarget.dispatchEvent() is aliased to trigger() +EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; + +exports['default'] = EventTarget; +module.exports = exports['default']; + +},{"./utils/events.js":133}],100:[function(_dereq_,module,exports){ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _utilsLog = _dereq_('./utils/log'); + +var _utilsLog2 = _interopRequireDefault(_utilsLog); + +/* + * @file extend.js + * + * A combination of node inherits and babel's inherits (after transpile). + * Both work the same but node adds `super_` to the subClass + * and Bable adds the superClass as __proto__. Both seem useful. + */ +var _inherits = function _inherits(subClass, superClass) { + if (typeof superClass !== 'function' && superClass !== null) { + throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + + if (superClass) { + // node + subClass.super_ = superClass; + } +}; + +/* + * Function for subclassing using the same inheritance that + * videojs uses internally + * ```js + * var Button = videojs.getComponent('Button'); + * ``` + * ```js + * var MyButton = videojs.extend(Button, { + * constructor: function(player, options) { + * Button.call(this, player, options); + * }, + * onClick: function() { + * // doSomething + * } + * }); + * ``` + */ +var extendFn = function extendFn(superClass) { + var subClassMethods = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var subClass = function subClass() { + superClass.apply(this, arguments); + }; + var methods = {}; + + if (typeof subClassMethods === 'object') { + if (typeof subClassMethods.init === 'function') { + _utilsLog2['default'].warn('Constructor logic via init() is deprecated; please use constructor() instead.'); + subClassMethods.constructor = subClassMethods.init; + } + if (subClassMethods.constructor !== Object.prototype.constructor) { + subClass = subClassMethods.constructor; + } + methods = subClassMethods; + } else if (typeof subClassMethods === 'function') { + subClass = subClassMethods; + } + + _inherits(subClass, superClass); + + // Extend subObj's prototype with functions and other properties from props + for (var name in methods) { + if (methods.hasOwnProperty(name)) { + subClass.prototype[name] = methods[name]; + } + } + + return subClass; +}; + +exports['default'] = extendFn; +module.exports = exports['default']; + +},{"./utils/log":137}],101:[function(_dereq_,module,exports){ +/** + * @file fullscreen-api.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/* + * Store the browser-specific methods for the fullscreen API + * @type {Object|undefined} + * @private + */ +var FullscreenApi = {}; + +// browser API methods +// map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js +var apiMap = [ +// Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html +['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror'], +// WebKit +['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], +// Old WebKit (Safari 5.1) +['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], +// Mozilla +['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror'], +// Microsoft +['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError']]; + +var specApi = apiMap[0]; +var browserApi = undefined; + +// determine the supported set of functions +for (var i = 0; i < apiMap.length; i++) { + // check for exitFullscreen function + if (apiMap[i][1] in _globalDocument2['default']) { + browserApi = apiMap[i]; + break; + } +} + +// map the browser API names to the spec API names +if (browserApi) { + for (var i = 0; i < browserApi.length; i++) { + FullscreenApi[specApi[i]] = browserApi[i]; + } +} + +exports['default'] = FullscreenApi; +module.exports = exports['default']; + +},{"global/document":1}],102:[function(_dereq_,module,exports){ +/** + * @file loading-spinner.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +/* Loading Spinner +================================================================================ */ +/** + * Loading spinner for waiting events + * + * @extends Component + * @class LoadingSpinner + */ + +var LoadingSpinner = (function (_Component) { + _inherits(LoadingSpinner, _Component); + + function LoadingSpinner() { + _classCallCheck(this, LoadingSpinner); + + _Component.apply(this, arguments); + } + + /** + * Create the component's DOM element + * + * @method createEl + */ + + LoadingSpinner.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-loading-spinner', + dir: 'ltr' + }); + }; + + return LoadingSpinner; +})(_component2['default']); + +_component2['default'].registerComponent('LoadingSpinner', LoadingSpinner); +exports['default'] = LoadingSpinner; +module.exports = exports['default']; + +},{"./component":67}],103:[function(_dereq_,module,exports){ +/** + * @file media-error.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/* + * Custom MediaError to mimic the HTML5 MediaError + * + * @param {Number} code The media error code + */ +var MediaError = function MediaError(code) { + if (typeof code === 'number') { + this.code = code; + } else if (typeof code === 'string') { + // default code is zero, so this is a custom error + this.message = code; + } else if (typeof code === 'object') { + // object + _objectAssign2['default'](this, code); + } + + if (!this.message) { + this.message = MediaError.defaultMessages[this.code] || ''; + } +}; + +/* + * The error code that refers two one of the defined + * MediaError types + * + * @type {Number} + */ +MediaError.prototype.code = 0; + +/* + * An optional message to be shown with the error. + * Message is not part of the HTML5 video spec + * but allows for more informative custom errors. + * + * @type {String} + */ +MediaError.prototype.message = ''; + +/* + * An optional status code that can be set by plugins + * to allow even more detail about the error. + * For example the HLS plugin might provide the specific + * HTTP status code that was returned when the error + * occurred, then allowing a custom error overlay + * to display more information. + * + * @type {Array} + */ +MediaError.prototype.status = null; + +MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', // = 0 +'MEDIA_ERR_ABORTED', // = 1 +'MEDIA_ERR_NETWORK', // = 2 +'MEDIA_ERR_DECODE', // = 3 +'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 +'MEDIA_ERR_ENCRYPTED' // = 5 +]; + +MediaError.defaultMessages = { + 1: 'You aborted the media playback', + 2: 'A network error caused the media download to fail part-way.', + 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', + 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', + 5: 'The media is encrypted and we do not have the keys to decrypt it.' +}; + +// Add types as properties on MediaError +// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; +for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { + MediaError[MediaError.errorTypes[errNum]] = errNum; + // values should be accessible on both the class and instance + MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; +} + +exports['default'] = MediaError; +module.exports = exports['default']; + +},{"object.assign":45}],104:[function(_dereq_,module,exports){ +/** + * @file menu-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _clickableComponentJs = _dereq_('../clickable-component.js'); + +var _clickableComponentJs2 = _interopRequireDefault(_clickableComponentJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _menuJs = _dereq_('./menu.js'); + +var _menuJs2 = _interopRequireDefault(_menuJs); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsToTitleCaseJs = _dereq_('../utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +/** + * A button class with a popup menu + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Button + * @class MenuButton + */ + +var MenuButton = (function (_ClickableComponent) { + _inherits(MenuButton, _ClickableComponent); + + function MenuButton(player) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, MenuButton); + + _ClickableComponent.call(this, player, options); + + this.update(); + + this.el_.setAttribute('aria-haspopup', true); + this.el_.setAttribute('role', 'menuitem'); + this.on('keydown', this.handleSubmenuKeyPress); + } + + /** + * Update menu + * + * @method update + */ + + MenuButton.prototype.update = function update() { + var menu = this.createMenu(); + + if (this.menu) { + this.removeChild(this.menu); + } + + this.menu = menu; + this.addChild(menu); + + /** + * Track the state of the menu button + * + * @type {Boolean} + * @private + */ + this.buttonPressed_ = false; + this.el_.setAttribute('aria-expanded', false); + + if (this.items && this.items.length === 0) { + this.hide(); + } else if (this.items && this.items.length > 1) { + this.show(); + } + }; + + /** + * Create menu + * + * @return {Menu} The constructed menu + * @method createMenu + */ + + MenuButton.prototype.createMenu = function createMenu() { + var menu = new _menuJs2['default'](this.player_); + + // Add a title list item to the top + if (this.options_.title) { + var title = Dom.createEl('li', { + className: 'vjs-menu-title', + innerHTML: _utilsToTitleCaseJs2['default'](this.options_.title), + tabIndex: -1 + }); + menu.children_.unshift(title); + Dom.insertElFirst(title, menu.contentEl()); + } + + this.items = this['createItems'](); + + if (this.items) { + // Add menu items to the menu + for (var i = 0; i < this.items.length; i++) { + menu.addItem(this.items[i]); + } + } + + return menu; + }; + + /** + * Create the list of menu items. Specific to each subclass. + * + * @method createItems + */ + + MenuButton.prototype.createItems = function createItems() {}; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + MenuButton.prototype.createEl = function createEl() { + return _ClickableComponent.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass() + }); + }; + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + MenuButton.prototype.buildCSSClass = function buildCSSClass() { + var menuButtonClass = 'vjs-menu-button'; + + // If the inline option is passed, we want to use different styles altogether. + if (this.options_.inline === true) { + menuButtonClass += '-inline'; + } else { + menuButtonClass += '-popup'; + } + + return 'vjs-menu-button ' + menuButtonClass + ' ' + _ClickableComponent.prototype.buildCSSClass.call(this); + }; + + /** + * When you click the button it adds focus, which + * will show the menu indefinitely. + * So we'll remove focus when the mouse leaves the button. + * Focus is needed for tab navigation. + * Allow sub components to stack CSS class names + * + * @method handleClick + */ + + MenuButton.prototype.handleClick = function handleClick() { + this.one('mouseout', Fn.bind(this, function () { + this.menu.unlockShowing(); + this.el_.blur(); + })); + if (this.buttonPressed_) { + this.unpressButton(); + } else { + this.pressButton(); + } + }; + + /** + * Handle key press on menu + * + * @param {Object} event Key press event + * @method handleKeyPress + */ + + MenuButton.prototype.handleKeyPress = function handleKeyPress(event) { + + // Escape (27) key or Tab (9) key unpress the 'button' + if (event.which === 27 || event.which === 9) { + if (this.buttonPressed_) { + this.unpressButton(); + } + // Don't preventDefault for Tab key - we still want to lose focus + if (event.which !== 9) { + event.preventDefault(); + } + // Up (38) key or Down (40) key press the 'button' + } else if (event.which === 38 || event.which === 40) { + if (!this.buttonPressed_) { + this.pressButton(); + event.preventDefault(); + } + } else { + _ClickableComponent.prototype.handleKeyPress.call(this, event); + } + }; + + /** + * Handle key press on submenu + * + * @param {Object} event Key press event + * @method handleSubmenuKeyPress + */ + + MenuButton.prototype.handleSubmenuKeyPress = function handleSubmenuKeyPress(event) { + + // Escape (27) key or Tab (9) key unpress the 'button' + if (event.which === 27 || event.which === 9) { + if (this.buttonPressed_) { + this.unpressButton(); + } + // Don't preventDefault for Tab key - we still want to lose focus + if (event.which !== 9) { + event.preventDefault(); + } + } + }; + + /** + * Makes changes based on button pressed + * + * @method pressButton + */ + + MenuButton.prototype.pressButton = function pressButton() { + this.buttonPressed_ = true; + this.menu.lockShowing(); + this.el_.setAttribute('aria-expanded', true); + this.menu.focus(); // set the focus into the submenu + }; + + /** + * Makes changes based on button unpressed + * + * @method unpressButton + */ + + MenuButton.prototype.unpressButton = function unpressButton() { + this.buttonPressed_ = false; + this.menu.unlockShowing(); + this.el_.setAttribute('aria-expanded', false); + this.el_.focus(); // Set focus back to this menu button + }; + + return MenuButton; +})(_clickableComponentJs2['default']); + +_componentJs2['default'].registerComponent('MenuButton', MenuButton); +exports['default'] = MenuButton; +module.exports = exports['default']; + +},{"../clickable-component.js":65,"../component.js":67,"../utils/dom.js":132,"../utils/fn.js":134,"../utils/to-title-case.js":141,"./menu.js":106}],105:[function(_dereq_,module,exports){ +/** + * @file menu-item.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _clickableComponentJs = _dereq_('../clickable-component.js'); + +var _clickableComponentJs2 = _interopRequireDefault(_clickableComponentJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/** + * The component for a menu item. `<li>` + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Button + * @class MenuItem + */ + +var MenuItem = (function (_ClickableComponent) { + _inherits(MenuItem, _ClickableComponent); + + function MenuItem(player, options) { + _classCallCheck(this, MenuItem); + + _ClickableComponent.call(this, player, options); + + this.selectable = options['selectable']; + + this.selected(options['selected']); + + if (this.selectable) { + // TODO: May need to be either menuitemcheckbox or menuitemradio, + // and may need logical grouping of menu items. + this.el_.setAttribute('role', 'menuitemcheckbox'); + } else { + this.el_.setAttribute('role', 'menuitem'); + } + } + + /** + * Create the component's DOM element + * + * @param {String=} type Desc + * @param {Object=} props Desc + * @return {Element} + * @method createEl + */ + + MenuItem.prototype.createEl = function createEl(type, props, attrs) { + return _ClickableComponent.prototype.createEl.call(this, 'li', _objectAssign2['default']({ + className: 'vjs-menu-item', + innerHTML: this.localize(this.options_['label']), + tabIndex: -1 + }, props), attrs); + }; + + /** + * Handle a click on the menu item, and set it to selected + * + * @method handleClick + */ + + MenuItem.prototype.handleClick = function handleClick() { + this.selected(true); + }; + + /** + * Set this menu item as selected or not + * + * @param {Boolean} selected + * @method selected + */ + + MenuItem.prototype.selected = function selected(_selected) { + if (this.selectable) { + if (_selected) { + this.addClass('vjs-selected'); + this.el_.setAttribute('aria-checked', true); + // aria-checked isn't fully supported by browsers/screen readers, + // so indicate selected state to screen reader in the control text. + this.controlText(', selected'); + } else { + this.removeClass('vjs-selected'); + this.el_.setAttribute('aria-checked', false); + // Indicate un-selected state to screen reader + // Note that a space clears out the selected state text + this.controlText(' '); + } + } + }; + + return MenuItem; +})(_clickableComponentJs2['default']); + +_componentJs2['default'].registerComponent('MenuItem', MenuItem); +exports['default'] = MenuItem; +module.exports = exports['default']; + +},{"../clickable-component.js":65,"../component.js":67,"object.assign":45}],106:[function(_dereq_,module,exports){ +/** + * @file menu.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsEventsJs = _dereq_('../utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +/** + * The Menu component is used to build pop up menus, including subtitle and + * captions selection menus. + * + * @extends Component + * @class Menu + */ + +var Menu = (function (_Component) { + _inherits(Menu, _Component); + + function Menu(player, options) { + _classCallCheck(this, Menu); + + _Component.call(this, player, options); + + this.focusedChild_ = -1; + + this.on('keydown', this.handleKeyPress); + } + + /** + * Add a menu item to the menu + * + * @param {Object|String} component Component or component type to add + * @method addItem + */ + + Menu.prototype.addItem = function addItem(component) { + this.addChild(component); + component.on('click', Fn.bind(this, function () { + this.unlockShowing(); + //TODO: Need to set keyboard focus back to the menuButton + })); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Menu.prototype.createEl = function createEl() { + var contentElType = this.options_.contentElType || 'ul'; + this.contentEl_ = Dom.createEl(contentElType, { + className: 'vjs-menu-content' + }); + this.contentEl_.setAttribute('role', 'menu'); + var el = _Component.prototype.createEl.call(this, 'div', { + append: this.contentEl_, + className: 'vjs-menu' + }); + el.setAttribute('role', 'presentation'); + el.appendChild(this.contentEl_); + + // Prevent clicks from bubbling up. Needed for Menu Buttons, + // where a click on the parent is significant + Events.on(el, 'click', function (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + }); + + return el; + }; + + /** + * Handle key press for menu + * + * @param {Object} event Event object + * @method handleKeyPress + */ + + Menu.prototype.handleKeyPress = function handleKeyPress(event) { + if (event.which === 37 || event.which === 40) { + // Left and Down Arrows + event.preventDefault(); + this.stepForward(); + } else if (event.which === 38 || event.which === 39) { + // Up and Right Arrows + event.preventDefault(); + this.stepBack(); + } + }; + + /** + * Move to next (lower) menu item for keyboard users + * + * @method stepForward + */ + + Menu.prototype.stepForward = function stepForward() { + var stepChild = 0; + + if (this.focusedChild_ !== undefined) { + stepChild = this.focusedChild_ + 1; + } + this.focus(stepChild); + }; + + /** + * Move to previous (higher) menu item for keyboard users + * + * @method stepBack + */ + + Menu.prototype.stepBack = function stepBack() { + var stepChild = 0; + + if (this.focusedChild_ !== undefined) { + stepChild = this.focusedChild_ - 1; + } + this.focus(stepChild); + }; + + /** + * Set focus on a menu item in the menu + * + * @param {Object|String} item Index of child item set focus on + * @method focus + */ + + Menu.prototype.focus = function focus() { + var item = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; + + var children = this.children().slice(); + var haveTitle = children.length && children[0].className && /vjs-menu-title/.test(children[0].className); + + if (haveTitle) { + children.shift(); + } + + if (children.length > 0) { + if (item < 0) { + item = 0; + } else if (item >= children.length) { + item = children.length - 1; + } + + this.focusedChild_ = item; + + children[item].el_.focus(); + } + }; + + return Menu; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('Menu', Menu); +exports['default'] = Menu; +module.exports = exports['default']; + +},{"../component.js":67,"../utils/dom.js":132,"../utils/events.js":133,"../utils/fn.js":134}],107:[function(_dereq_,module,exports){ +/** + * @file modal-dialog.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _utilsDom = _dereq_('./utils/dom'); + +var Dom = _interopRequireWildcard(_utilsDom); + +var _utilsFn = _dereq_('./utils/fn'); + +var Fn = _interopRequireWildcard(_utilsFn); + +var _utilsLog = _dereq_('./utils/log'); + +var _utilsLog2 = _interopRequireDefault(_utilsLog); + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +var _closeButton = _dereq_('./close-button'); + +var _closeButton2 = _interopRequireDefault(_closeButton); + +var MODAL_CLASS_NAME = 'vjs-modal-dialog'; +var ESC = 27; + +/** + * The `ModalDialog` displays over the video and its controls, which blocks + * interaction with the player until it is closed. + * + * Modal dialogs include a "Close" button and will close when that button + * is activated - or when ESC is pressed anywhere. + * + * @extends Component + * @class ModalDialog + */ + +var ModalDialog = (function (_Component) { + _inherits(ModalDialog, _Component); + + /** + * Constructor for modals. + * + * @param {Player} player + * @param {Object} [options] + * @param {Mixed} [options.content=undefined] + * Provide customized content for this modal. + * + * @param {String} [options.description] + * A text description for the modal, primarily for accessibility. + * + * @param {Boolean} [options.fillAlways=false] + * Normally, modals are automatically filled only the first time + * they open. This tells the modal to refresh its content + * every time it opens. + * + * @param {String} [options.label] + * A text label for the modal, primarily for accessibility. + * + * @param {Boolean} [options.temporary=true] + * If `true`, the modal can only be opened once; it will be + * disposed as soon as it's closed. + * + * @param {Boolean} [options.uncloseable=false] + * If `true`, the user will not be able to close the modal + * through the UI in the normal ways. Programmatic closing is + * still possible. + * + */ + + function ModalDialog(player, options) { + _classCallCheck(this, ModalDialog); + + _Component.call(this, player, options); + this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false; + + this.closeable(!this.options_.uncloseable); + this.content(this.options_.content); + + // Make sure the contentEl is defined AFTER any children are initialized + // because we only want the contents of the modal in the contentEl + // (not the UI elements like the close button). + this.contentEl_ = Dom.createEl('div', { + className: MODAL_CLASS_NAME + '-content' + }, { + role: 'document' + }); + + this.descEl_ = Dom.createEl('p', { + className: MODAL_CLASS_NAME + '-description vjs-offscreen', + id: this.el().getAttribute('aria-describedby') + }); + + Dom.textContent(this.descEl_, this.description()); + this.el_.appendChild(this.descEl_); + this.el_.appendChild(this.contentEl_); + } + + /* + * Modal dialog default options. + * + * @type {Object} + * @private + */ + + /** + * Create the modal's DOM element + * + * @method createEl + * @return {Element} + */ + + ModalDialog.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass(), + tabIndex: -1 + }, { + 'aria-describedby': this.id() + '_description', + 'aria-hidden': 'true', + 'aria-label': this.label(), + role: 'dialog' + }); + }; + + /** + * Build the modal's CSS class. + * + * @method buildCSSClass + * @return {String} + */ + + ModalDialog.prototype.buildCSSClass = function buildCSSClass() { + return MODAL_CLASS_NAME + ' vjs-hidden ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Handles key presses on the document, looking for ESC, which closes + * the modal. + * + * @method handleKeyPress + * @param {Event} e + */ + + ModalDialog.prototype.handleKeyPress = function handleKeyPress(e) { + if (e.which === ESC && this.closeable()) { + this.close(); + } + }; + + /** + * Returns the label string for this modal. Primarily used for accessibility. + * + * @return {String} + */ + + ModalDialog.prototype.label = function label() { + return this.options_.label || this.localize('Modal Window'); + }; + + /** + * Returns the description string for this modal. Primarily used for + * accessibility. + * + * @return {String} + */ + + ModalDialog.prototype.description = function description() { + var desc = this.options_.description || this.localize('This is a modal window.'); + + // Append a universal closeability message if the modal is closeable. + if (this.closeable()) { + desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); + } + + return desc; + }; + + /** + * Opens the modal. + * + * @method open + * @return {ModalDialog} + */ + + ModalDialog.prototype.open = function open() { + if (!this.opened_) { + var player = this.player(); + + this.trigger('beforemodalopen'); + this.opened_ = true; + + // Fill content if the modal has never opened before and + // never been filled. + if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { + this.fill(); + } + + // If the player was playing, pause it and take note of its previously + // playing state. + this.wasPlaying_ = !player.paused(); + + if (this.wasPlaying_) { + player.pause(); + } + + if (this.closeable()) { + this.on(_globalDocument2['default'], 'keydown', Fn.bind(this, this.handleKeyPress)); + } + + player.controls(false); + this.show(); + this.el().setAttribute('aria-hidden', 'false'); + this.trigger('modalopen'); + this.hasBeenOpened_ = true; + } + return this; + }; + + /** + * Whether or not the modal is opened currently. + * + * @method opened + * @param {Boolean} [value] + * If given, it will open (`true`) or close (`false`) the modal. + * + * @return {Boolean} + */ + + ModalDialog.prototype.opened = function opened(value) { + if (typeof value === 'boolean') { + this[value ? 'open' : 'close'](); + } + return this.opened_; + }; + + /** + * Closes the modal. + * + * @method close + * @return {ModalDialog} + */ + + ModalDialog.prototype.close = function close() { + if (this.opened_) { + var player = this.player(); + + this.trigger('beforemodalclose'); + this.opened_ = false; + + if (this.wasPlaying_) { + player.play(); + } + + if (this.closeable()) { + this.off(_globalDocument2['default'], 'keydown', Fn.bind(this, this.handleKeyPress)); + } + + player.controls(true); + this.hide(); + this.el().setAttribute('aria-hidden', 'true'); + this.trigger('modalclose'); + + if (this.options_.temporary) { + this.dispose(); + } + } + return this; + }; + + /** + * Whether or not the modal is closeable via the UI. + * + * @method closeable + * @param {Boolean} [value] + * If given as a Boolean, it will set the `closeable` option. + * + * @return {Boolean} + */ + + ModalDialog.prototype.closeable = function closeable(value) { + if (typeof value === 'boolean') { + var closeable = this.closeable_ = !!value; + var _close = this.getChild('closeButton'); + + // If this is being made closeable and has no close button, add one. + if (closeable && !_close) { + + // The close button should be a child of the modal - not its + // content element, so temporarily change the content element. + var temp = this.contentEl_; + this.contentEl_ = this.el_; + _close = this.addChild('closeButton'); + this.contentEl_ = temp; + this.on(_close, 'close', this.close); + } + + // If this is being made uncloseable and has a close button, remove it. + if (!closeable && _close) { + this.off(_close, 'close', this.close); + this.removeChild(_close); + _close.dispose(); + } + } + return this.closeable_; + }; + + /** + * Fill the modal's content element with the modal's "content" option. + * + * The content element will be emptied before this change takes place. + * + * @method fill + * @return {ModalDialog} + */ + + ModalDialog.prototype.fill = function fill() { + return this.fillWith(this.content()); + }; + + /** + * Fill the modal's content element with arbitrary content. + * + * The content element will be emptied before this change takes place. + * + * @method fillWith + * @param {Mixed} [content] + * The same rules apply to this as apply to the `content` option. + * + * @return {ModalDialog} + */ + + ModalDialog.prototype.fillWith = function fillWith(content) { + var contentEl = this.contentEl(); + var parentEl = contentEl.parentNode; + var nextSiblingEl = contentEl.nextSibling; + + this.trigger('beforemodalfill'); + this.hasBeenFilled_ = true; + + // Detach the content element from the DOM before performing + // manipulation to avoid modifying the live DOM multiple times. + parentEl.removeChild(contentEl); + this.empty(); + Dom.insertContent(contentEl, content); + this.trigger('modalfill'); + + // Re-inject the re-filled content element. + if (nextSiblingEl) { + parentEl.insertBefore(contentEl, nextSiblingEl); + } else { + parentEl.appendChild(contentEl); + } + + return this; + }; + + /** + * Empties the content element. + * + * This happens automatically anytime the modal is filled. + * + * @method empty + * @return {ModalDialog} + */ + + ModalDialog.prototype.empty = function empty() { + this.trigger('beforemodalempty'); + Dom.emptyEl(this.contentEl()); + this.trigger('modalempty'); + return this; + }; + + /** + * Gets or sets the modal content, which gets normalized before being + * rendered into the DOM. + * + * This does not update the DOM or fill the modal, but it is called during + * that process. + * + * @method content + * @param {Mixed} [value] + * If defined, sets the internal content value to be used on the + * next call(s) to `fill`. This value is normalized before being + * inserted. To "clear" the internal content value, pass `null`. + * + * @return {Mixed} + */ + + ModalDialog.prototype.content = function content(value) { + if (typeof value !== 'undefined') { + this.content_ = value; + } + return this.content_; + }; + + return ModalDialog; +})(_component2['default']); + +ModalDialog.prototype.options_ = { + temporary: true +}; + +_component2['default'].registerComponent('ModalDialog', ModalDialog); +exports['default'] = ModalDialog; +module.exports = exports['default']; + +},{"./close-button":66,"./component":67,"./utils/dom":132,"./utils/fn":134,"./utils/log":137,"global/document":1}],108:[function(_dereq_,module,exports){ +/** + * @file player.js + */ +// Subclasses Component +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('./component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _utilsDomJs = _dereq_('./utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsGuidJs = _dereq_('./utils/guid.js'); + +var Guid = _interopRequireWildcard(_utilsGuidJs); + +var _utilsBrowserJs = _dereq_('./utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _utilsLogJs = _dereq_('./utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _utilsToTitleCaseJs = _dereq_('./utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +var _utilsTimeRangesJs = _dereq_('./utils/time-ranges.js'); + +var _utilsBufferJs = _dereq_('./utils/buffer.js'); + +var _utilsStylesheetJs = _dereq_('./utils/stylesheet.js'); + +var stylesheet = _interopRequireWildcard(_utilsStylesheetJs); + +var _fullscreenApiJs = _dereq_('./fullscreen-api.js'); + +var _fullscreenApiJs2 = _interopRequireDefault(_fullscreenApiJs); + +var _mediaErrorJs = _dereq_('./media-error.js'); + +var _mediaErrorJs2 = _interopRequireDefault(_mediaErrorJs); + +var _safeJsonParseTuple = _dereq_('safe-json-parse/tuple'); + +var _safeJsonParseTuple2 = _interopRequireDefault(_safeJsonParseTuple); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +var _utilsMergeOptionsJs = _dereq_('./utils/merge-options.js'); + +var _utilsMergeOptionsJs2 = _interopRequireDefault(_utilsMergeOptionsJs); + +var _tracksTextTrackListConverterJs = _dereq_('./tracks/text-track-list-converter.js'); + +var _tracksTextTrackListConverterJs2 = _interopRequireDefault(_tracksTextTrackListConverterJs); + +// Include required child components (importing also registers them) + +var _techLoaderJs = _dereq_('./tech/loader.js'); + +var _techLoaderJs2 = _interopRequireDefault(_techLoaderJs); + +var _posterImageJs = _dereq_('./poster-image.js'); + +var _posterImageJs2 = _interopRequireDefault(_posterImageJs); + +var _tracksTextTrackDisplayJs = _dereq_('./tracks/text-track-display.js'); + +var _tracksTextTrackDisplayJs2 = _interopRequireDefault(_tracksTextTrackDisplayJs); + +var _loadingSpinnerJs = _dereq_('./loading-spinner.js'); + +var _loadingSpinnerJs2 = _interopRequireDefault(_loadingSpinnerJs); + +var _bigPlayButtonJs = _dereq_('./big-play-button.js'); + +var _bigPlayButtonJs2 = _interopRequireDefault(_bigPlayButtonJs); + +var _controlBarControlBarJs = _dereq_('./control-bar/control-bar.js'); + +var _controlBarControlBarJs2 = _interopRequireDefault(_controlBarControlBarJs); + +var _errorDisplayJs = _dereq_('./error-display.js'); + +var _errorDisplayJs2 = _interopRequireDefault(_errorDisplayJs); + +var _tracksTextTrackSettingsJs = _dereq_('./tracks/text-track-settings.js'); + +var _tracksTextTrackSettingsJs2 = _interopRequireDefault(_tracksTextTrackSettingsJs); + +var _modalDialog = _dereq_('./modal-dialog'); + +var _modalDialog2 = _interopRequireDefault(_modalDialog); + +// Require html5 tech, at least for disposing the original video tag + +var _techTechJs = _dereq_('./tech/tech.js'); + +var _techTechJs2 = _interopRequireDefault(_techTechJs); + +var _techHtml5Js = _dereq_('./tech/html5.js'); + +var _techHtml5Js2 = _interopRequireDefault(_techHtml5Js); + +/** + * An instance of the `Player` class is created when any of the Video.js setup methods are used to initialize a video. + * ```js + * var myPlayer = videojs('example_video_1'); + * ``` + * In the following example, the `data-setup` attribute tells the Video.js library to create a player instance when the library is ready. + * ```html + * <video id="example_video_1" data-setup='{}' controls> + * <source src="my-source.mp4" type="video/mp4"> + * </video> + * ``` + * After an instance has been created it can be accessed globally using `Video('example_video_1')`. + * + * @param {Element} tag The original video tag used for configuring options + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends Component + * @class Player + */ + +var Player = (function (_Component) { + _inherits(Player, _Component); + + /** + * player's constructor function + * + * @constructs + * @method init + * @param {Element} tag The original video tag used for configuring options + * @param {Object=} options Player options + * @param {Function=} ready Ready callback function + */ + + function Player(tag, options, ready) { + var _this = this; + + _classCallCheck(this, Player); + + // Make sure tag ID exists + tag.id = tag.id || 'vjs_video_' + Guid.newGUID(); + + // Set Options + // The options argument overrides options set in the video tag + // which overrides globally set options. + // This latter part coincides with the load order + // (tag must exist before Player) + options = _objectAssign2['default'](Player.getTagSettings(tag), options); + + // Delay the initialization of children because we need to set up + // player properties first, and can't use `this` before `super()` + options.initChildren = false; + + // Same with creating the element + options.createEl = false; + + // we don't want the player to report touch activity on itself + // see enableTouchActivity in Component + options.reportTouchActivity = false; + + // Run base component initializing with new options + _Component.call(this, null, options, ready); + + // if the global option object was accidentally blown away by + // someone, bail early with an informative error + if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) { + throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?'); + } + + this.tag = tag; // Store the original tag used to set options + + // Store the tag attributes used to restore html5 element + this.tagAttributes = tag && Dom.getElAttributes(tag); + + // Update current language + this.language(this.options_.language); + + // Update Supported Languages + if (options.languages) { + (function () { + // Normalise player option languages to lowercase + var languagesToLower = {}; + + Object.getOwnPropertyNames(options.languages).forEach(function (name) { + languagesToLower[name.toLowerCase()] = options.languages[name]; + }); + _this.languages_ = languagesToLower; + })(); + } else { + this.languages_ = Player.prototype.options_.languages; + } + + // Cache for video property values. + this.cache_ = {}; + + // Set poster + this.poster_ = options.poster || ''; + + // Set controls + this.controls_ = !!options.controls; + + // Original tag settings stored in options + // now remove immediately so native controls don't flash. + // May be turned back on by HTML5 tech if nativeControlsForTouch is true + tag.controls = false; + + /* + * Store the internal state of scrubbing + * + * @private + * @return {Boolean} True if the user is scrubbing + */ + this.scrubbing_ = false; + + this.el_ = this.createEl(); + + // We also want to pass the original player options to each component and plugin + // as well so they don't need to reach back into the player for options later. + // We also need to do another copy of this.options_ so we don't end up with + // an infinite loop. + var playerOptionsCopy = _utilsMergeOptionsJs2['default'](this.options_); + + // Load plugins + if (options.plugins) { + (function () { + var plugins = options.plugins; + + Object.getOwnPropertyNames(plugins).forEach(function (name) { + if (typeof this[name] === 'function') { + this[name](plugins[name]); + } else { + _utilsLogJs2['default'].error('Unable to find plugin:', name); + } + }, _this); + })(); + } + + this.options_.playerOptions = playerOptionsCopy; + + this.initChildren(); + + // Set isAudio based on whether or not an audio tag was used + this.isAudio(tag.nodeName.toLowerCase() === 'audio'); + + // Update controls className. Can't do this when the controls are initially + // set because the element doesn't exist yet. + if (this.controls()) { + this.addClass('vjs-controls-enabled'); + } else { + this.addClass('vjs-controls-disabled'); + } + + if (this.isAudio()) { + this.addClass('vjs-audio'); + } + + if (this.flexNotSupported_()) { + this.addClass('vjs-no-flex'); + } + + // TODO: Make this smarter. Toggle user state between touching/mousing + // using events, since devices can have both touch and mouse events. + // if (browser.TOUCH_ENABLED) { + // this.addClass('vjs-touch-enabled'); + // } + + // iOS Safari has broken hover handling + if (!browser.IS_IOS) { + this.addClass('vjs-workinghover'); + } + + // Make player easily findable by ID + Player.players[this.id_] = this; + + // When the player is first initialized, trigger activity so components + // like the control bar show themselves if needed + this.userActive(true); + this.reportUserActivity(); + this.listenForUserActivity_(); + + this.on('fullscreenchange', this.handleFullscreenChange_); + this.on('stageclick', this.handleStageClick_); + } + + /* + * Global player list + * + * @type {Object} + */ + + /** + * Destroys the video player and does any necessary cleanup + * ```js + * myPlayer.dispose(); + * ``` + * This is especially helpful if you are dynamically adding and removing videos + * to/from the DOM. + * + * @method dispose + */ + + Player.prototype.dispose = function dispose() { + this.trigger('dispose'); + // prevent dispose from being called twice + this.off('dispose'); + + if (this.styleEl_ && this.styleEl_.parentNode) { + this.styleEl_.parentNode.removeChild(this.styleEl_); + } + + // Kill reference to this player + Player.players[this.id_] = null; + if (this.tag && this.tag.player) { + this.tag.player = null; + } + if (this.el_ && this.el_.player) { + this.el_.player = null; + } + + if (this.tech_) { + this.tech_.dispose(); + } + + _Component.prototype.dispose.call(this); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Player.prototype.createEl = function createEl() { + var el = this.el_ = _Component.prototype.createEl.call(this, 'div'); + var tag = this.tag; + + // Remove width/height attrs from tag so CSS can make it 100% width/height + tag.removeAttribute('width'); + tag.removeAttribute('height'); + + // Copy over all the attributes from the tag, including ID and class + // ID will now reference player box, not the video tag + var attrs = Dom.getElAttributes(tag); + + Object.getOwnPropertyNames(attrs).forEach(function (attr) { + // workaround so we don't totally break IE7 + // http://stackoverflow.com/questions/3653444/css-styles-not-applied-on-dynamic-elements-in-internet-explorer-7 + if (attr === 'class') { + el.className = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + }); + + // Update tag id/class for use as HTML5 playback tech + // Might think we should do this after embedding in container so .vjs-tech class + // doesn't flash 100% width/height, but class only applies with .video-js parent + tag.playerId = tag.id; + tag.id += '_html5_api'; + tag.className = 'vjs-tech'; + + // Make player findable on elements + tag.player = el.player = this; + // Default state of video is paused + this.addClass('vjs-paused'); + + // Add a style element in the player that we'll use to set the width/height + // of the player in a way that's still overrideable by CSS, just like the + // video element + this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions'); + var defaultsStyleEl = Dom.$('.vjs-styles-defaults'); + var head = Dom.$('head'); + head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); + + // Pass in the width/height/aspectRatio options which will update the style el + this.width(this.options_.width); + this.height(this.options_.height); + this.fluid(this.options_.fluid); + this.aspectRatio(this.options_.aspectRatio); + + // insertElFirst seems to cause the networkState to flicker from 3 to 2, so + // keep track of the original for later so we can know if the source originally failed + tag.initNetworkState_ = tag.networkState; + + // Wrap video tag in div (el/box) container + if (tag.parentNode) { + tag.parentNode.insertBefore(el, tag); + } + + // insert the tag as the first child of the player element + // then manually add it to the children array so that this.addChild + // will work properly for other components + Dom.insertElFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup. + this.children_.unshift(tag); + + this.el_ = el; + + return el; + }; + + /** + * Get/set player width + * + * @param {Number=} value Value for width + * @return {Number} Width when getting + * @method width + */ + + Player.prototype.width = function width(value) { + return this.dimension('width', value); + }; + + /** + * Get/set player height + * + * @param {Number=} value Value for height + * @return {Number} Height when getting + * @method height + */ + + Player.prototype.height = function height(value) { + return this.dimension('height', value); + }; + + /** + * Get/set dimension for player + * + * @param {String} dimension Either width or height + * @param {Number=} value Value for dimension + * @return {Component} + * @method dimension + */ + + Player.prototype.dimension = function dimension(_dimension, value) { + var privDimension = _dimension + '_'; + + if (value === undefined) { + return this[privDimension] || 0; + } + + if (value === '') { + // If an empty string is given, reset the dimension to be automatic + this[privDimension] = undefined; + } else { + var parsedVal = parseFloat(value); + + if (isNaN(parsedVal)) { + _utilsLogJs2['default'].error('Improper value "' + value + '" supplied for for ' + _dimension); + return this; + } + + this[privDimension] = parsedVal; + } + + this.updateStyleEl_(); + return this; + }; + + /** + * Add/remove the vjs-fluid class + * + * @param {Boolean} bool Value of true adds the class, value of false removes the class + * @method fluid + */ + + Player.prototype.fluid = function fluid(bool) { + if (bool === undefined) { + return !!this.fluid_; + } + + this.fluid_ = !!bool; + + if (bool) { + this.addClass('vjs-fluid'); + } else { + this.removeClass('vjs-fluid'); + } + }; + + /** + * Get/Set the aspect ratio + * + * @param {String=} ratio Aspect ratio for player + * @return aspectRatio + * @method aspectRatio + */ + + Player.prototype.aspectRatio = function aspectRatio(ratio) { + if (ratio === undefined) { + return this.aspectRatio_; + } + + // Check for width:height format + if (!/^\d+\:\d+$/.test(ratio)) { + throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); + } + this.aspectRatio_ = ratio; + + // We're assuming if you set an aspect ratio you want fluid mode, + // because in fixed mode you could calculate width and height yourself. + this.fluid(true); + + this.updateStyleEl_(); + }; + + /** + * Update styles of the player element (height, width and aspect ratio) + * + * @method updateStyleEl_ + */ + + Player.prototype.updateStyleEl_ = function updateStyleEl_() { + var width = undefined; + var height = undefined; + var aspectRatio = undefined; + var idClass = undefined; + + // The aspect ratio is either used directly or to calculate width and height. + if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { + // Use any aspectRatio that's been specifically set + aspectRatio = this.aspectRatio_; + } else if (this.videoWidth()) { + // Otherwise try to get the aspect ratio from the video metadata + aspectRatio = this.videoWidth() + ':' + this.videoHeight(); + } else { + // Or use a default. The video element's is 2:1, but 16:9 is more common. + aspectRatio = '16:9'; + } + + // Get the ratio as a decimal we can use to calculate dimensions + var ratioParts = aspectRatio.split(':'); + var ratioMultiplier = ratioParts[1] / ratioParts[0]; + + if (this.width_ !== undefined) { + // Use any width that's been specifically set + width = this.width_; + } else if (this.height_ !== undefined) { + // Or calulate the width from the aspect ratio if a height has been set + width = this.height_ / ratioMultiplier; + } else { + // Or use the video's metadata, or use the video el's default of 300 + width = this.videoWidth() || 300; + } + + if (this.height_ !== undefined) { + // Use any height that's been specifically set + height = this.height_; + } else { + // Otherwise calculate the height from the ratio and the width + height = width * ratioMultiplier; + } + + // Ensure the CSS class is valid by starting with an alpha character + if (/^[^a-zA-Z]/.test(this.id())) { + idClass = 'dimensions-' + this.id(); + } else { + idClass = this.id() + '-dimensions'; + } + + // Ensure the right class is still on the player for the style element + this.addClass(idClass); + + stylesheet.setTextContent(this.styleEl_, '\n .' + idClass + ' {\n width: ' + width + 'px;\n height: ' + height + 'px;\n }\n\n .' + idClass + '.vjs-fluid {\n padding-top: ' + ratioMultiplier * 100 + '%;\n }\n '); + }; + + /** + * Load the Media Playback Technology (tech) + * Load/Create an instance of playback technology including element and API methods + * And append playback element in player div. + * + * @param {String} techName Name of the playback technology + * @param {String} source Video source + * @method loadTech_ + * @private + */ + + Player.prototype.loadTech_ = function loadTech_(techName, source) { + + // Pause and remove current playback technology + if (this.tech_) { + this.unloadTech_(); + } + + // get rid of the HTML5 video tag as soon as we are using another tech + if (techName !== 'Html5' && this.tag) { + _techTechJs2['default'].getTech('Html5').disposeMediaElement(this.tag); + this.tag.player = null; + this.tag = null; + } + + this.techName_ = techName; + + // Turn off API access because we're loading a new tech that might load asynchronously + this.isReady_ = false; + + // Grab tech-specific options from player options and add source and parent element to use. + var techOptions = _objectAssign2['default']({ + 'nativeControlsForTouch': this.options_.nativeControlsForTouch, + 'source': source, + 'playerId': this.id(), + 'techId': this.id() + '_' + techName + '_api', + 'textTracks': this.textTracks_, + 'autoplay': this.options_.autoplay, + 'preload': this.options_.preload, + 'loop': this.options_.loop, + 'muted': this.options_.muted, + 'poster': this.poster(), + 'language': this.language(), + 'vtt.js': this.options_['vtt.js'] + }, this.options_[techName.toLowerCase()]); + + if (this.tag) { + techOptions.tag = this.tag; + } + + if (source) { + this.currentType_ = source.type; + if (source.src === this.cache_.src && this.cache_.currentTime > 0) { + techOptions.startTime = this.cache_.currentTime; + } + + this.cache_.src = source.src; + } + + // Initialize tech instance + var techComponent = _techTechJs2['default'].getTech(techName); + // Support old behavior of techs being registered as components. + // Remove once that deprecated behavior is removed. + if (!techComponent) { + techComponent = _componentJs2['default'].getComponent(techName); + } + this.tech_ = new techComponent(techOptions); + + // player.triggerReady is always async, so don't need this to be async + this.tech_.ready(Fn.bind(this, this.handleTechReady_), true); + + _tracksTextTrackListConverterJs2['default'].jsonToTextTracks(this.textTracksJson_ || [], this.tech_); + + // Listen to all HTML5-defined events and trigger them on the player + this.on(this.tech_, 'loadstart', this.handleTechLoadStart_); + this.on(this.tech_, 'waiting', this.handleTechWaiting_); + this.on(this.tech_, 'canplay', this.handleTechCanPlay_); + this.on(this.tech_, 'canplaythrough', this.handleTechCanPlayThrough_); + this.on(this.tech_, 'playing', this.handleTechPlaying_); + this.on(this.tech_, 'ended', this.handleTechEnded_); + this.on(this.tech_, 'seeking', this.handleTechSeeking_); + this.on(this.tech_, 'seeked', this.handleTechSeeked_); + this.on(this.tech_, 'play', this.handleTechPlay_); + this.on(this.tech_, 'firstplay', this.handleTechFirstPlay_); + this.on(this.tech_, 'pause', this.handleTechPause_); + this.on(this.tech_, 'progress', this.handleTechProgress_); + this.on(this.tech_, 'durationchange', this.handleTechDurationChange_); + this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_); + this.on(this.tech_, 'error', this.handleTechError_); + this.on(this.tech_, 'suspend', this.handleTechSuspend_); + this.on(this.tech_, 'abort', this.handleTechAbort_); + this.on(this.tech_, 'emptied', this.handleTechEmptied_); + this.on(this.tech_, 'stalled', this.handleTechStalled_); + this.on(this.tech_, 'loadedmetadata', this.handleTechLoadedMetaData_); + this.on(this.tech_, 'loadeddata', this.handleTechLoadedData_); + this.on(this.tech_, 'timeupdate', this.handleTechTimeUpdate_); + this.on(this.tech_, 'ratechange', this.handleTechRateChange_); + this.on(this.tech_, 'volumechange', this.handleTechVolumeChange_); + this.on(this.tech_, 'texttrackchange', this.handleTechTextTrackChange_); + this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_); + this.on(this.tech_, 'posterchange', this.handleTechPosterChange_); + + this.usingNativeControls(this.techGet_('controls')); + + if (this.controls() && !this.usingNativeControls()) { + this.addTechControlsListeners_(); + } + + // Add the tech element in the DOM if it was not already there + // Make sure to not insert the original video element if using Html5 + if (this.tech_.el().parentNode !== this.el() && (techName !== 'Html5' || !this.tag)) { + Dom.insertElFirst(this.tech_.el(), this.el()); + } + + // Get rid of the original video tag reference after the first tech is loaded + if (this.tag) { + this.tag.player = null; + this.tag = null; + } + }; + + /** + * Unload playback technology + * + * @method unloadTech_ + * @private + */ + + Player.prototype.unloadTech_ = function unloadTech_() { + // Save the current text tracks so that we can reuse the same text tracks with the next tech + this.textTracks_ = this.textTracks(); + this.textTracksJson_ = _tracksTextTrackListConverterJs2['default'].textTracksToJson(this.tech_); + + this.isReady_ = false; + + this.tech_.dispose(); + + this.tech_ = false; + }; + + /** + * Return a reference to the current tech. + * It will only return a reference to the tech if given an object with the + * `IWillNotUseThisInPlugins` property on it. This is try and prevent misuse + * of techs by plugins. + * + * @param {Object} + * @return {Object} The Tech + * @method tech + */ + + Player.prototype.tech = function tech(safety) { + if (safety && safety.IWillNotUseThisInPlugins) { + return this.tech_; + } + var errorText = '\n Please make sure that you are not using this inside of a plugin.\n To disable this alert and error, please pass in an object with\n `IWillNotUseThisInPlugins` to the `tech` method. See\n https://github.com/videojs/video.js/issues/2617 for more info.\n '; + _globalWindow2['default'].alert(errorText); + throw new Error(errorText); + }; + + /** + * Set up click and touch listeners for the playback element + * + * On desktops, a click on the video itself will toggle playback, + * on a mobile device a click on the video toggles controls. + * (toggling controls is done by toggling the user state between active and + * inactive) + * A tap can signal that a user has become active, or has become inactive + * e.g. a quick tap on an iPhone movie should reveal the controls. Another + * quick tap should hide them again (signaling the user is in an inactive + * viewing state) + * In addition to this, we still want the user to be considered inactive after + * a few seconds of inactivity. + * Note: the only part of iOS interaction we can't mimic with this setup + * is a touch and hold on the video element counting as activity in order to + * keep the controls showing, but that shouldn't be an issue. A touch and hold + * on any controls will still keep the user active + * + * @private + * @method addTechControlsListeners_ + */ + + Player.prototype.addTechControlsListeners_ = function addTechControlsListeners_() { + // Make sure to remove all the previous listeners in case we are called multiple times. + this.removeTechControlsListeners_(); + + // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do + // trigger mousedown/up. + // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object + // Any touch events are set to block the mousedown event from happening + this.on(this.tech_, 'mousedown', this.handleTechClick_); + + // If the controls were hidden we don't want that to change without a tap event + // so we'll check if the controls were already showing before reporting user + // activity + this.on(this.tech_, 'touchstart', this.handleTechTouchStart_); + this.on(this.tech_, 'touchmove', this.handleTechTouchMove_); + this.on(this.tech_, 'touchend', this.handleTechTouchEnd_); + + // The tap listener needs to come after the touchend listener because the tap + // listener cancels out any reportedUserActivity when setting userActive(false) + this.on(this.tech_, 'tap', this.handleTechTap_); + }; + + /** + * Remove the listeners used for click and tap controls. This is needed for + * toggling to controls disabled, where a tap/touch should do nothing. + * + * @method removeTechControlsListeners_ + * @private + */ + + Player.prototype.removeTechControlsListeners_ = function removeTechControlsListeners_() { + // We don't want to just use `this.off()` because there might be other needed + // listeners added by techs that extend this. + this.off(this.tech_, 'tap', this.handleTechTap_); + this.off(this.tech_, 'touchstart', this.handleTechTouchStart_); + this.off(this.tech_, 'touchmove', this.handleTechTouchMove_); + this.off(this.tech_, 'touchend', this.handleTechTouchEnd_); + this.off(this.tech_, 'mousedown', this.handleTechClick_); + }; + + /** + * Player waits for the tech to be ready + * + * @method handleTechReady_ + * @private + */ + + Player.prototype.handleTechReady_ = function handleTechReady_() { + this.triggerReady(); + + // Keep the same volume as before + if (this.cache_.volume) { + this.techCall_('setVolume', this.cache_.volume); + } + + // Look if the tech found a higher resolution poster while loading + this.handleTechPosterChange_(); + + // Update the duration if available + this.handleTechDurationChange_(); + + // Chrome and Safari both have issues with autoplay. + // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work. + // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays) + // This fixes both issues. Need to wait for API, so it updates displays correctly + if (this.src() && this.tag && this.options_.autoplay && this.paused()) { + delete this.tag.poster; // Chrome Fix. Fixed in Chrome v16. + this.play(); + } + }; + + /** + * Fired when the user agent begins looking for media data + * + * @private + * @method handleTechLoadStart_ + */ + + Player.prototype.handleTechLoadStart_ = function handleTechLoadStart_() { + // TODO: Update to use `emptied` event instead. See #1277. + + this.removeClass('vjs-ended'); + + // reset the error state + this.error(null); + + // If it's already playing we want to trigger a firstplay event now. + // The firstplay event relies on both the play and loadstart events + // which can happen in any order for a new source + if (!this.paused()) { + this.trigger('loadstart'); + this.trigger('firstplay'); + } else { + // reset the hasStarted state + this.hasStarted(false); + this.trigger('loadstart'); + } + }; + + /** + * Add/remove the vjs-has-started class + * + * @param {Boolean} hasStarted The value of true adds the class the value of false remove the class + * @return {Boolean} Boolean value if has started + * @private + * @method hasStarted + */ + + Player.prototype.hasStarted = function hasStarted(_hasStarted) { + if (_hasStarted !== undefined) { + // only update if this is a new value + if (this.hasStarted_ !== _hasStarted) { + this.hasStarted_ = _hasStarted; + if (_hasStarted) { + this.addClass('vjs-has-started'); + // trigger the firstplay event if this newly has played + this.trigger('firstplay'); + } else { + this.removeClass('vjs-has-started'); + } + } + return this; + } + return !!this.hasStarted_; + }; + + /** + * Fired whenever the media begins or resumes playback + * + * @private + * @method handleTechPlay_ + */ + + Player.prototype.handleTechPlay_ = function handleTechPlay_() { + this.removeClass('vjs-ended'); + this.removeClass('vjs-paused'); + this.addClass('vjs-playing'); + + // hide the poster when the user hits play + // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play + this.hasStarted(true); + + this.trigger('play'); + }; + + /** + * Fired whenever the media begins waiting + * + * @private + * @method handleTechWaiting_ + */ + + Player.prototype.handleTechWaiting_ = function handleTechWaiting_() { + var _this2 = this; + + this.addClass('vjs-waiting'); + this.trigger('waiting'); + this.one('timeupdate', function () { + return _this2.removeClass('vjs-waiting'); + }); + }; + + /** + * A handler for events that signal that waiting has ended + * which is not consistent between browsers. See #1351 + * + * @private + * @method handleTechCanPlay_ + */ + + Player.prototype.handleTechCanPlay_ = function handleTechCanPlay_() { + this.removeClass('vjs-waiting'); + this.trigger('canplay'); + }; + + /** + * A handler for events that signal that waiting has ended + * which is not consistent between browsers. See #1351 + * + * @private + * @method handleTechCanPlayThrough_ + */ + + Player.prototype.handleTechCanPlayThrough_ = function handleTechCanPlayThrough_() { + this.removeClass('vjs-waiting'); + this.trigger('canplaythrough'); + }; + + /** + * A handler for events that signal that waiting has ended + * which is not consistent between browsers. See #1351 + * + * @private + * @method handleTechPlaying_ + */ + + Player.prototype.handleTechPlaying_ = function handleTechPlaying_() { + this.removeClass('vjs-waiting'); + this.trigger('playing'); + }; + + /** + * Fired whenever the player is jumping to a new time + * + * @private + * @method handleTechSeeking_ + */ + + Player.prototype.handleTechSeeking_ = function handleTechSeeking_() { + this.addClass('vjs-seeking'); + this.trigger('seeking'); + }; + + /** + * Fired when the player has finished jumping to a new time + * + * @private + * @method handleTechSeeked_ + */ + + Player.prototype.handleTechSeeked_ = function handleTechSeeked_() { + this.removeClass('vjs-seeking'); + this.trigger('seeked'); + }; + + /** + * Fired the first time a video is played + * Not part of the HLS spec, and we're not sure if this is the best + * implementation yet, so use sparingly. If you don't have a reason to + * prevent playback, use `myPlayer.one('play');` instead. + * + * @private + * @method handleTechFirstPlay_ + */ + + Player.prototype.handleTechFirstPlay_ = function handleTechFirstPlay_() { + //If the first starttime attribute is specified + //then we will start at the given offset in seconds + if (this.options_.starttime) { + this.currentTime(this.options_.starttime); + } + + this.addClass('vjs-has-started'); + this.trigger('firstplay'); + }; + + /** + * Fired whenever the media has been paused + * + * @private + * @method handleTechPause_ + */ + + Player.prototype.handleTechPause_ = function handleTechPause_() { + this.removeClass('vjs-playing'); + this.addClass('vjs-paused'); + this.trigger('pause'); + }; + + /** + * Fired while the user agent is downloading media data + * + * @private + * @method handleTechProgress_ + */ + + Player.prototype.handleTechProgress_ = function handleTechProgress_() { + this.trigger('progress'); + }; + + /** + * Fired when the end of the media resource is reached (currentTime == duration) + * + * @private + * @method handleTechEnded_ + */ + + Player.prototype.handleTechEnded_ = function handleTechEnded_() { + this.addClass('vjs-ended'); + if (this.options_.loop) { + this.currentTime(0); + this.play(); + } else if (!this.paused()) { + this.pause(); + } + + this.trigger('ended'); + }; + + /** + * Fired when the duration of the media resource is first known or changed + * + * @private + * @method handleTechDurationChange_ + */ + + Player.prototype.handleTechDurationChange_ = function handleTechDurationChange_() { + this.duration(this.techGet_('duration')); + }; + + /** + * Handle a click on the media element to play/pause + * + * @param {Object=} event Event object + * @private + * @method handleTechClick_ + */ + + Player.prototype.handleTechClick_ = function handleTechClick_(event) { + // We're using mousedown to detect clicks thanks to Flash, but mousedown + // will also be triggered with right-clicks, so we need to prevent that + if (event.button !== 0) return; + + // When controls are disabled a click should not toggle playback because + // the click is considered a control + if (this.controls()) { + if (this.paused()) { + this.play(); + } else { + this.pause(); + } + } + }; + + /** + * Handle a tap on the media element. It will toggle the user + * activity state, which hides and shows the controls. + * + * @private + * @method handleTechTap_ + */ + + Player.prototype.handleTechTap_ = function handleTechTap_() { + this.userActive(!this.userActive()); + }; + + /** + * Handle touch to start + * + * @private + * @method handleTechTouchStart_ + */ + + Player.prototype.handleTechTouchStart_ = function handleTechTouchStart_() { + this.userWasActive = this.userActive(); + }; + + /** + * Handle touch to move + * + * @private + * @method handleTechTouchMove_ + */ + + Player.prototype.handleTechTouchMove_ = function handleTechTouchMove_() { + if (this.userWasActive) { + this.reportUserActivity(); + } + }; + + /** + * Handle touch to end + * + * @private + * @method handleTechTouchEnd_ + */ + + Player.prototype.handleTechTouchEnd_ = function handleTechTouchEnd_(event) { + // Stop the mouse events from also happening + event.preventDefault(); + }; + + /** + * Fired when the player switches in or out of fullscreen mode + * + * @private + * @method handleFullscreenChange_ + */ + + Player.prototype.handleFullscreenChange_ = function handleFullscreenChange_() { + if (this.isFullscreen()) { + this.addClass('vjs-fullscreen'); + } else { + this.removeClass('vjs-fullscreen'); + } + }; + + /** + * native click events on the SWF aren't triggered on IE11, Win8.1RT + * use stageclick events triggered from inside the SWF instead + * + * @private + * @method handleStageClick_ + */ + + Player.prototype.handleStageClick_ = function handleStageClick_() { + this.reportUserActivity(); + }; + + /** + * Handle Tech Fullscreen Change + * + * @private + * @method handleTechFullscreenChange_ + */ + + Player.prototype.handleTechFullscreenChange_ = function handleTechFullscreenChange_(event, data) { + if (data) { + this.isFullscreen(data.isFullscreen); + } + this.trigger('fullscreenchange'); + }; + + /** + * Fires when an error occurred during the loading of an audio/video + * + * @private + * @method handleTechError_ + */ + + Player.prototype.handleTechError_ = function handleTechError_() { + var error = this.tech_.error(); + this.error(error && error.code); + }; + + /** + * Fires when the browser is intentionally not getting media data + * + * @private + * @method handleTechSuspend_ + */ + + Player.prototype.handleTechSuspend_ = function handleTechSuspend_() { + this.trigger('suspend'); + }; + + /** + * Fires when the loading of an audio/video is aborted + * + * @private + * @method handleTechAbort_ + */ + + Player.prototype.handleTechAbort_ = function handleTechAbort_() { + this.trigger('abort'); + }; + + /** + * Fires when the current playlist is empty + * + * @private + * @method handleTechEmptied_ + */ + + Player.prototype.handleTechEmptied_ = function handleTechEmptied_() { + this.trigger('emptied'); + }; + + /** + * Fires when the browser is trying to get media data, but data is not available + * + * @private + * @method handleTechStalled_ + */ + + Player.prototype.handleTechStalled_ = function handleTechStalled_() { + this.trigger('stalled'); + }; + + /** + * Fires when the browser has loaded meta data for the audio/video + * + * @private + * @method handleTechLoadedMetaData_ + */ + + Player.prototype.handleTechLoadedMetaData_ = function handleTechLoadedMetaData_() { + this.trigger('loadedmetadata'); + }; + + /** + * Fires when the browser has loaded the current frame of the audio/video + * + * @private + * @method handleTechLoadedData_ + */ + + Player.prototype.handleTechLoadedData_ = function handleTechLoadedData_() { + this.trigger('loadeddata'); + }; + + /** + * Fires when the current playback position has changed + * + * @private + * @method handleTechTimeUpdate_ + */ + + Player.prototype.handleTechTimeUpdate_ = function handleTechTimeUpdate_() { + this.trigger('timeupdate'); + }; + + /** + * Fires when the playing speed of the audio/video is changed + * + * @private + * @method handleTechRateChange_ + */ + + Player.prototype.handleTechRateChange_ = function handleTechRateChange_() { + this.trigger('ratechange'); + }; + + /** + * Fires when the volume has been changed + * + * @private + * @method handleTechVolumeChange_ + */ + + Player.prototype.handleTechVolumeChange_ = function handleTechVolumeChange_() { + this.trigger('volumechange'); + }; + + /** + * Fires when the text track has been changed + * + * @private + * @method handleTechTextTrackChange_ + */ + + Player.prototype.handleTechTextTrackChange_ = function handleTechTextTrackChange_() { + this.trigger('texttrackchange'); + }; + + /** + * Get object for cached values. + * + * @return {Object} + * @method getCache + */ + + Player.prototype.getCache = function getCache() { + return this.cache_; + }; + + /** + * Pass values to the playback tech + * + * @param {String=} method Method + * @param {Object=} arg Argument + * @private + * @method techCall_ + */ + + Player.prototype.techCall_ = function techCall_(method, arg) { + // If it's not ready yet, call method when it is + if (this.tech_ && !this.tech_.isReady_) { + this.tech_.ready(function () { + this[method](arg); + }, true); + + // Otherwise call method now + } else { + try { + this.tech_[method](arg); + } catch (e) { + _utilsLogJs2['default'](e); + throw e; + } + } + }; + + /** + * Get calls can't wait for the tech, and sometimes don't need to. + * + * @param {String} method Tech method + * @return {Method} + * @private + * @method techGet_ + */ + + Player.prototype.techGet_ = function techGet_(method) { + if (this.tech_ && this.tech_.isReady_) { + + // Flash likes to die and reload when you hide or reposition it. + // In these cases the object methods go away and we get errors. + // When that happens we'll catch the errors and inform tech that it's not ready any more. + try { + return this.tech_[method](); + } catch (e) { + // When building additional tech libs, an expected method may not be defined yet + if (this.tech_[method] === undefined) { + _utilsLogJs2['default']('Video.js: ' + method + ' method not defined for ' + this.techName_ + ' playback technology.', e); + } else { + // When a method isn't available on the object it throws a TypeError + if (e.name === 'TypeError') { + _utilsLogJs2['default']('Video.js: ' + method + ' unavailable on ' + this.techName_ + ' playback technology element.', e); + this.tech_.isReady_ = false; + } else { + _utilsLogJs2['default'](e); + } + } + throw e; + } + } + + return; + }; + + /** + * start media playback + * ```js + * myPlayer.play(); + * ``` + * + * @return {Player} self + * @method play + */ + + Player.prototype.play = function play() { + this.techCall_('play'); + return this; + }; + + /** + * Pause the video playback + * ```js + * myPlayer.pause(); + * ``` + * + * @return {Player} self + * @method pause + */ + + Player.prototype.pause = function pause() { + this.techCall_('pause'); + return this; + }; + + /** + * Check if the player is paused + * ```js + * var isPaused = myPlayer.paused(); + * var isPlaying = !myPlayer.paused(); + * ``` + * + * @return {Boolean} false if the media is currently playing, or true otherwise + * @method paused + */ + + Player.prototype.paused = function paused() { + // The initial state of paused should be true (in Safari it's actually false) + return this.techGet_('paused') === false ? false : true; + }; + + /** + * Returns whether or not the user is "scrubbing". Scrubbing is when the user + * has clicked the progress bar handle and is dragging it along the progress bar. + * + * @param {Boolean} isScrubbing True/false the user is scrubbing + * @return {Boolean} The scrubbing status when getting + * @return {Object} The player when setting + * @method scrubbing + */ + + Player.prototype.scrubbing = function scrubbing(isScrubbing) { + if (isScrubbing !== undefined) { + this.scrubbing_ = !!isScrubbing; + + if (isScrubbing) { + this.addClass('vjs-scrubbing'); + } else { + this.removeClass('vjs-scrubbing'); + } + + return this; + } + + return this.scrubbing_; + }; + + /** + * Get or set the current time (in seconds) + * ```js + * // get + * var whereYouAt = myPlayer.currentTime(); + * // set + * myPlayer.currentTime(120); // 2 minutes into the video + * ``` + * + * @param {Number|String=} seconds The time to seek to + * @return {Number} The time in seconds, when not setting + * @return {Player} self, when the current time is set + * @method currentTime + */ + + Player.prototype.currentTime = function currentTime(seconds) { + if (seconds !== undefined) { + + this.techCall_('setCurrentTime', seconds); + + return this; + } + + // cache last currentTime and return. default to 0 seconds + // + // Caching the currentTime is meant to prevent a massive amount of reads on the tech's + // currentTime when scrubbing, but may not provide much performance benefit afterall. + // Should be tested. Also something has to read the actual current time or the cache will + // never get updated. + return this.cache_.currentTime = this.techGet_('currentTime') || 0; + }; + + /** + * Get the length in time of the video in seconds + * ```js + * var lengthOfVideo = myPlayer.duration(); + * ``` + * **NOTE**: The video must have started loading before the duration can be + * known, and in the case of Flash, may not be known until the video starts + * playing. + * + * @param {Number} seconds Duration when setting + * @return {Number} The duration of the video in seconds when getting + * @method duration + */ + + Player.prototype.duration = function duration(seconds) { + if (seconds === undefined) { + return this.cache_.duration || 0; + } + + seconds = parseFloat(seconds) || 0; + + // Standardize on Inifity for signaling video is live + if (seconds < 0) { + seconds = Infinity; + } + + if (seconds !== this.cache_.duration) { + // Cache the last set value for optimized scrubbing (esp. Flash) + this.cache_.duration = seconds; + + if (seconds === Infinity) { + this.addClass('vjs-live'); + } else { + this.removeClass('vjs-live'); + } + + this.trigger('durationchange'); + } + + return this; + }; + + /** + * Calculates how much time is left. + * ```js + * var timeLeft = myPlayer.remainingTime(); + * ``` + * Not a native video element function, but useful + * + * @return {Number} The time remaining in seconds + * @method remainingTime + */ + + Player.prototype.remainingTime = function remainingTime() { + return this.duration() - this.currentTime(); + }; + + // http://dev.w3.org/html5/spec/video.html#dom-media-buffered + // Buffered returns a timerange object. + // Kind of like an array of portions of the video that have been downloaded. + + /** + * Get a TimeRange object with the times of the video that have been downloaded + * If you just want the percent of the video that's been downloaded, + * use bufferedPercent. + * ```js + * // Number of different ranges of time have been buffered. Usually 1. + * numberOfRanges = bufferedTimeRange.length, + * // Time in seconds when the first range starts. Usually 0. + * firstRangeStart = bufferedTimeRange.start(0), + * // Time in seconds when the first range ends + * firstRangeEnd = bufferedTimeRange.end(0), + * // Length in seconds of the first time range + * firstRangeLength = firstRangeEnd - firstRangeStart; + * ``` + * + * @return {Object} A mock TimeRange object (following HTML spec) + * @method buffered + */ + + Player.prototype.buffered = function buffered() { + var buffered = this.techGet_('buffered'); + + if (!buffered || !buffered.length) { + buffered = _utilsTimeRangesJs.createTimeRange(0, 0); + } + + return buffered; + }; + + /** + * Get the percent (as a decimal) of the video that's been downloaded + * ```js + * var howMuchIsDownloaded = myPlayer.bufferedPercent(); + * ``` + * 0 means none, 1 means all. + * (This method isn't in the HTML5 spec, but it's very convenient) + * + * @return {Number} A decimal between 0 and 1 representing the percent + * @method bufferedPercent + */ + + Player.prototype.bufferedPercent = function bufferedPercent() { + return _utilsBufferJs.bufferedPercent(this.buffered(), this.duration()); + }; + + /** + * Get the ending time of the last buffered time range + * This is used in the progress bar to encapsulate all time ranges. + * + * @return {Number} The end of the last buffered time range + * @method bufferedEnd + */ + + Player.prototype.bufferedEnd = function bufferedEnd() { + var buffered = this.buffered(), + duration = this.duration(), + end = buffered.end(buffered.length - 1); + + if (end > duration) { + end = duration; + } + + return end; + }; + + /** + * Get or set the current volume of the media + * ```js + * // get + * var howLoudIsIt = myPlayer.volume(); + * // set + * myPlayer.volume(0.5); // Set volume to half + * ``` + * 0 is off (muted), 1.0 is all the way up, 0.5 is half way. + * + * @param {Number} percentAsDecimal The new volume as a decimal percent + * @return {Number} The current volume when getting + * @return {Player} self when setting + * @method volume + */ + + Player.prototype.volume = function volume(percentAsDecimal) { + var vol = undefined; + + if (percentAsDecimal !== undefined) { + vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1 + this.cache_.volume = vol; + this.techCall_('setVolume', vol); + + return this; + } + + // Default to 1 when returning current volume. + vol = parseFloat(this.techGet_('volume')); + return isNaN(vol) ? 1 : vol; + }; + + /** + * Get the current muted state, or turn mute on or off + * ```js + * // get + * var isVolumeMuted = myPlayer.muted(); + * // set + * myPlayer.muted(true); // mute the volume + * ``` + * + * @param {Boolean=} muted True to mute, false to unmute + * @return {Boolean} True if mute is on, false if not when getting + * @return {Player} self when setting mute + * @method muted + */ + + Player.prototype.muted = function muted(_muted) { + if (_muted !== undefined) { + this.techCall_('setMuted', _muted); + return this; + } + return this.techGet_('muted') || false; // Default to false + }; + + // Check if current tech can support native fullscreen + // (e.g. with built in controls like iOS, so not our flash swf) + /** + * Check to see if fullscreen is supported + * + * @return {Boolean} + * @method supportsFullScreen + */ + + Player.prototype.supportsFullScreen = function supportsFullScreen() { + return this.techGet_('supportsFullScreen') || false; + }; + + /** + * Check if the player is in fullscreen mode + * ```js + * // get + * var fullscreenOrNot = myPlayer.isFullscreen(); + * // set + * myPlayer.isFullscreen(true); // tell the player it's in fullscreen + * ``` + * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official + * property and instead document.fullscreenElement is used. But isFullscreen is + * still a valuable property for internal player workings. + * + * @param {Boolean=} isFS Update the player's fullscreen state + * @return {Boolean} true if fullscreen false if not when getting + * @return {Player} self when setting + * @method isFullscreen + */ + + Player.prototype.isFullscreen = function isFullscreen(isFS) { + if (isFS !== undefined) { + this.isFullscreen_ = !!isFS; + return this; + } + return !!this.isFullscreen_; + }; + + /** + * Increase the size of the video to full screen + * ```js + * myPlayer.requestFullscreen(); + * ``` + * In some browsers, full screen is not supported natively, so it enters + * "full window mode", where the video fills the browser window. + * In browsers and devices that support native full screen, sometimes the + * browser's default controls will be shown, and not the Video.js custom skin. + * This includes most mobile devices (iOS, Android) and older versions of + * Safari. + * + * @return {Player} self + * @method requestFullscreen + */ + + Player.prototype.requestFullscreen = function requestFullscreen() { + var fsApi = _fullscreenApiJs2['default']; + + this.isFullscreen(true); + + if (fsApi.requestFullscreen) { + // the browser supports going fullscreen at the element level so we can + // take the controls fullscreen as well as the video + + // Trigger fullscreenchange event after change + // We have to specifically add this each time, and remove + // when canceling fullscreen. Otherwise if there's multiple + // players on a page, they would all be reacting to the same fullscreen + // events + Events.on(_globalDocument2['default'], fsApi.fullscreenchange, Fn.bind(this, function documentFullscreenChange(e) { + this.isFullscreen(_globalDocument2['default'][fsApi.fullscreenElement]); + + // If cancelling fullscreen, remove event listener. + if (this.isFullscreen() === false) { + Events.off(_globalDocument2['default'], fsApi.fullscreenchange, documentFullscreenChange); + } + + this.trigger('fullscreenchange'); + })); + + this.el_[fsApi.requestFullscreen](); + } else if (this.tech_.supportsFullScreen()) { + // we can't take the video.js controls fullscreen but we can go fullscreen + // with native controls + this.techCall_('enterFullScreen'); + } else { + // fullscreen isn't supported so we'll just stretch the video element to + // fill the viewport + this.enterFullWindow(); + this.trigger('fullscreenchange'); + } + + return this; + }; + + /** + * Return the video to its normal size after having been in full screen mode + * ```js + * myPlayer.exitFullscreen(); + * ``` + * + * @return {Player} self + * @method exitFullscreen + */ + + Player.prototype.exitFullscreen = function exitFullscreen() { + var fsApi = _fullscreenApiJs2['default']; + this.isFullscreen(false); + + // Check for browser element fullscreen support + if (fsApi.requestFullscreen) { + _globalDocument2['default'][fsApi.exitFullscreen](); + } else if (this.tech_.supportsFullScreen()) { + this.techCall_('exitFullScreen'); + } else { + this.exitFullWindow(); + this.trigger('fullscreenchange'); + } + + return this; + }; + + /** + * When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us. + * + * @method enterFullWindow + */ + + Player.prototype.enterFullWindow = function enterFullWindow() { + this.isFullWindow = true; + + // Storing original doc overflow value to return to when fullscreen is off + this.docOrigOverflow = _globalDocument2['default'].documentElement.style.overflow; + + // Add listener for esc key to exit fullscreen + Events.on(_globalDocument2['default'], 'keydown', Fn.bind(this, this.fullWindowOnEscKey)); + + // Hide any scroll bars + _globalDocument2['default'].documentElement.style.overflow = 'hidden'; + + // Apply fullscreen styles + Dom.addElClass(_globalDocument2['default'].body, 'vjs-full-window'); + + this.trigger('enterFullWindow'); + }; + + /** + * Check for call to either exit full window or full screen on ESC key + * + * @param {String} event Event to check for key press + * @method fullWindowOnEscKey + */ + + Player.prototype.fullWindowOnEscKey = function fullWindowOnEscKey(event) { + if (event.keyCode === 27) { + if (this.isFullscreen() === true) { + this.exitFullscreen(); + } else { + this.exitFullWindow(); + } + } + }; + + /** + * Exit full window + * + * @method exitFullWindow + */ + + Player.prototype.exitFullWindow = function exitFullWindow() { + this.isFullWindow = false; + Events.off(_globalDocument2['default'], 'keydown', this.fullWindowOnEscKey); + + // Unhide scroll bars. + _globalDocument2['default'].documentElement.style.overflow = this.docOrigOverflow; + + // Remove fullscreen styles + Dom.removeElClass(_globalDocument2['default'].body, 'vjs-full-window'); + + // Resize the box, controller, and poster to original sizes + // this.positionAll(); + this.trigger('exitFullWindow'); + }; + + /** + * Check whether the player can play a given mimetype + * + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + * @method canPlayType + */ + + Player.prototype.canPlayType = function canPlayType(type) { + var can = undefined; + + // Loop through each playback technology in the options order + for (var i = 0, j = this.options_.techOrder; i < j.length; i++) { + var techName = _utilsToTitleCaseJs2['default'](j[i]); + var tech = _techTechJs2['default'].getTech(techName); + + // Support old behavior of techs being registered as components. + // Remove once that deprecated behavior is removed. + if (!tech) { + tech = _componentJs2['default'].getComponent(techName); + } + + // Check if the current tech is defined before continuing + if (!tech) { + _utilsLogJs2['default'].error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); + continue; + } + + // Check if the browser supports this technology + if (tech.isSupported()) { + can = tech.canPlayType(type); + + if (can) { + return can; + } + } + } + + return ''; + }; + + /** + * Select source based on tech-order or source-order + * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, + * defaults to tech-order selection + * + * @param {Array} sources The sources for a media asset + * @return {Object|Boolean} Object of source and tech order, otherwise false + * @method selectSource + */ + + Player.prototype.selectSource = function selectSource(sources) { + // Get only the techs specified in `techOrder` that exist and are supported by the + // current platform + var techs = this.options_.techOrder.map(_utilsToTitleCaseJs2['default']).map(function (techName) { + // `Component.getComponent(...)` is for support of old behavior of techs + // being registered as components. + // Remove once that deprecated behavior is removed. + return [techName, _techTechJs2['default'].getTech(techName) || _componentJs2['default'].getComponent(techName)]; + }).filter(function (_ref) { + var techName = _ref[0]; + var tech = _ref[1]; + + // Check if the current tech is defined before continuing + if (tech) { + // Check if the browser supports this technology + return tech.isSupported(); + } + + _utilsLogJs2['default'].error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); + return false; + }); + + // Iterate over each `innerArray` element once per `outerArray` element and execute + // `tester` with both. If `tester` returns a non-falsy value, exit early and return + // that value. + var findFirstPassingTechSourcePair = function findFirstPassingTechSourcePair(outerArray, innerArray, tester) { + var found = undefined; + + outerArray.some(function (outerChoice) { + return innerArray.some(function (innerChoice) { + found = tester(outerChoice, innerChoice); + + if (found) { + return true; + } + }); + }); + + return found; + }; + + var foundSourceAndTech = undefined; + var flip = function flip(fn) { + return function (a, b) { + return fn(b, a); + }; + }; + var finder = function finder(_ref2, source) { + var techName = _ref2[0]; + var tech = _ref2[1]; + + if (tech.canPlaySource(source)) { + return { source: source, tech: techName }; + } + }; + + // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources + // to select from them based on their priority. + if (this.options_.sourceOrder) { + // Source-first ordering + foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); + } else { + // Tech-first ordering + foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); + } + + return foundSourceAndTech || false; + }; + + /** + * The source function updates the video source + * There are three types of variables you can pass as the argument. + * **URL String**: A URL to the the video file. Use this method if you are sure + * the current playback technology (HTML5/Flash) can support the source you + * provide. Currently only MP4 files can be used in both HTML5 and Flash. + * ```js + * myPlayer.src("http://www.example.com/path/to/video.mp4"); + * ``` + * **Source Object (or element):* * A javascript object containing information + * about the source file. Use this method if you want the player to determine if + * it can support the file using the type information. + * ```js + * myPlayer.src({ type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }); + * ``` + * **Array of Source Objects:* * To provide multiple versions of the source so + * that it can be played using HTML5 across browsers you can use an array of + * source objects. Video.js will detect which version is supported and load that + * file. + * ```js + * myPlayer.src([ + * { type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }, + * { type: "video/webm", src: "http://www.example.com/path/to/video.webm" }, + * { type: "video/ogg", src: "http://www.example.com/path/to/video.ogv" } + * ]); + * ``` + * + * @param {String|Object|Array=} source The source URL, object, or array of sources + * @return {String} The current video source when getting + * @return {String} The player when setting + * @method src + */ + + Player.prototype.src = function src(source) { + if (source === undefined) { + return this.techGet_('src'); + } + + var currentTech = _techTechJs2['default'].getTech(this.techName_); + // Support old behavior of techs being registered as components. + // Remove once that deprecated behavior is removed. + if (!currentTech) { + currentTech = _componentJs2['default'].getComponent(this.techName_); + } + + // case: Array of source objects to choose from and pick the best to play + if (Array.isArray(source)) { + this.sourceList_(source); + + // case: URL String (http://myvideo...) + } else if (typeof source === 'string') { + // create a source object from the string + this.src({ src: source }); + + // case: Source object { src: '', type: '' ... } + } else if (source instanceof Object) { + // check if the source has a type and the loaded tech cannot play the source + // if there's no type we'll just try the current tech + if (source.type && !currentTech.canPlaySource(source)) { + // create a source list with the current source and send through + // the tech loop to check for a compatible technology + this.sourceList_([source]); + } else { + this.cache_.src = source.src; + this.currentType_ = source.type || ''; + + // wait until the tech is ready to set the source + this.ready(function () { + + // The setSource tech method was added with source handlers + // so older techs won't support it + // We need to check the direct prototype for the case where subclasses + // of the tech do not support source handlers + if (currentTech.prototype.hasOwnProperty('setSource')) { + this.techCall_('setSource', source); + } else { + this.techCall_('src', source.src); + } + + if (this.options_.preload === 'auto') { + this.load(); + } + + if (this.options_.autoplay) { + this.play(); + } + + // Set the source synchronously if possible (#2326) + }, true); + } + } + + return this; + }; + + /** + * Handle an array of source objects + * + * @param {Array} sources Array of source objects + * @private + * @method sourceList_ + */ + + Player.prototype.sourceList_ = function sourceList_(sources) { + var sourceTech = this.selectSource(sources); + + if (sourceTech) { + if (sourceTech.tech === this.techName_) { + // if this technology is already loaded, set the source + this.src(sourceTech.source); + } else { + // load this technology with the chosen source + this.loadTech_(sourceTech.tech, sourceTech.source); + } + } else { + // We need to wrap this in a timeout to give folks a chance to add error event handlers + this.setTimeout(function () { + this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); + }, 0); + + // we could not find an appropriate tech, but let's still notify the delegate that this is it + // this needs a better comment about why this is needed + this.triggerReady(); + } + }; + + /** + * Begin loading the src data. + * + * @return {Player} Returns the player + * @method load + */ + + Player.prototype.load = function load() { + this.techCall_('load'); + return this; + }; + + /** + * Reset the player. Loads the first tech in the techOrder, + * and calls `reset` on the tech`. + * + * @return {Player} Returns the player + * @method reset + */ + + Player.prototype.reset = function reset() { + this.loadTech_(_utilsToTitleCaseJs2['default'](this.options_.techOrder[0]), null); + this.techCall_('reset'); + return this; + }; + + /** + * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 + * Can be used in conjuction with `currentType` to assist in rebuilding the current source object. + * + * @return {String} The current source + * @method currentSrc + */ + + Player.prototype.currentSrc = function currentSrc() { + return this.techGet_('currentSrc') || this.cache_.src || ''; + }; + + /** + * Get the current source type e.g. video/mp4 + * This can allow you rebuild the current source object so that you could load the same + * source and tech later + * + * @return {String} The source MIME type + * @method currentType + */ + + Player.prototype.currentType = function currentType() { + return this.currentType_ || ''; + }; + + /** + * Get or set the preload attribute + * + * @param {Boolean} value Boolean to determine if preload should be used + * @return {String} The preload attribute value when getting + * @return {Player} Returns the player when setting + * @method preload + */ + + Player.prototype.preload = function preload(value) { + if (value !== undefined) { + this.techCall_('setPreload', value); + this.options_.preload = value; + return this; + } + return this.techGet_('preload'); + }; + + /** + * Get or set the autoplay attribute. + * + * @param {Boolean} value Boolean to determine if video should autoplay + * @return {String} The autoplay attribute value when getting + * @return {Player} Returns the player when setting + * @method autoplay + */ + + Player.prototype.autoplay = function autoplay(value) { + if (value !== undefined) { + this.techCall_('setAutoplay', value); + this.options_.autoplay = value; + return this; + } + return this.techGet_('autoplay', value); + }; + + /** + * Get or set the loop attribute on the video element. + * + * @param {Boolean} value Boolean to determine if video should loop + * @return {String} The loop attribute value when getting + * @return {Player} Returns the player when setting + * @method loop + */ + + Player.prototype.loop = function loop(value) { + if (value !== undefined) { + this.techCall_('setLoop', value); + this.options_['loop'] = value; + return this; + } + return this.techGet_('loop'); + }; + + /** + * Get or set the poster image source url + * + * ##### EXAMPLE: + * ```js + * // get + * var currentPoster = myPlayer.poster(); + * // set + * myPlayer.poster('http://example.com/myImage.jpg'); + * ``` + * + * @param {String=} src Poster image source URL + * @return {String} poster URL when getting + * @return {Player} self when setting + * @method poster + */ + + Player.prototype.poster = function poster(src) { + if (src === undefined) { + return this.poster_; + } + + // The correct way to remove a poster is to set as an empty string + // other falsey values will throw errors + if (!src) { + src = ''; + } + + // update the internal poster variable + this.poster_ = src; + + // update the tech's poster + this.techCall_('setPoster', src); + + // alert components that the poster has been set + this.trigger('posterchange'); + + return this; + }; + + /** + * Some techs (e.g. YouTube) can provide a poster source in an + * asynchronous way. We want the poster component to use this + * poster source so that it covers up the tech's controls. + * (YouTube's play button). However we only want to use this + * soruce if the player user hasn't set a poster through + * the normal APIs. + * + * @private + * @method handleTechPosterChange_ + */ + + Player.prototype.handleTechPosterChange_ = function handleTechPosterChange_() { + if (!this.poster_ && this.tech_ && this.tech_.poster) { + this.poster_ = this.tech_.poster() || ''; + + // Let components know the poster has changed + this.trigger('posterchange'); + } + }; + + /** + * Get or set whether or not the controls are showing. + * + * @param {Boolean} bool Set controls to showing or not + * @return {Boolean} Controls are showing + * @method controls + */ + + Player.prototype.controls = function controls(bool) { + if (bool !== undefined) { + bool = !!bool; // force boolean + // Don't trigger a change event unless it actually changed + if (this.controls_ !== bool) { + this.controls_ = bool; + + if (this.usingNativeControls()) { + this.techCall_('setControls', bool); + } + + if (bool) { + this.removeClass('vjs-controls-disabled'); + this.addClass('vjs-controls-enabled'); + this.trigger('controlsenabled'); + + if (!this.usingNativeControls()) { + this.addTechControlsListeners_(); + } + } else { + this.removeClass('vjs-controls-enabled'); + this.addClass('vjs-controls-disabled'); + this.trigger('controlsdisabled'); + + if (!this.usingNativeControls()) { + this.removeTechControlsListeners_(); + } + } + } + return this; + } + return !!this.controls_; + }; + + /** + * Toggle native controls on/off. Native controls are the controls built into + * devices (e.g. default iPhone controls), Flash, or other techs + * (e.g. Vimeo Controls) + * **This should only be set by the current tech, because only the tech knows + * if it can support native controls** + * + * @param {Boolean} bool True signals that native controls are on + * @return {Player} Returns the player + * @private + * @method usingNativeControls + */ + + Player.prototype.usingNativeControls = function usingNativeControls(bool) { + if (bool !== undefined) { + bool = !!bool; // force boolean + // Don't trigger a change event unless it actually changed + if (this.usingNativeControls_ !== bool) { + this.usingNativeControls_ = bool; + if (bool) { + this.addClass('vjs-using-native-controls'); + + /** + * player is using the native device controls + * + * @event usingnativecontrols + * @memberof Player + * @instance + * @private + */ + this.trigger('usingnativecontrols'); + } else { + this.removeClass('vjs-using-native-controls'); + + /** + * player is using the custom HTML controls + * + * @event usingcustomcontrols + * @memberof Player + * @instance + * @private + */ + this.trigger('usingcustomcontrols'); + } + } + return this; + } + return !!this.usingNativeControls_; + }; + + /** + * Set or get the current MediaError + * + * @param {*} err A MediaError or a String/Number to be turned into a MediaError + * @return {MediaError|null} when getting + * @return {Player} when setting + * @method error + */ + + Player.prototype.error = function error(err) { + if (err === undefined) { + return this.error_ || null; + } + + // restoring to default + if (err === null) { + this.error_ = err; + this.removeClass('vjs-error'); + this.errorDisplay.close(); + return this; + } + + // error instance + if (err instanceof _mediaErrorJs2['default']) { + this.error_ = err; + } else { + this.error_ = new _mediaErrorJs2['default'](err); + } + + // add the vjs-error classname to the player + this.addClass('vjs-error'); + + // log the name of the error type and any message + // ie8 just logs "[object object]" if you just log the error object + _utilsLogJs2['default'].error('(CODE:' + this.error_.code + ' ' + _mediaErrorJs2['default'].errorTypes[this.error_.code] + ')', this.error_.message, this.error_); + + // fire an error event on the player + this.trigger('error'); + + return this; + }; + + /** + * Returns whether or not the player is in the "ended" state. + * + * @return {Boolean} True if the player is in the ended state, false if not. + * @method ended + */ + + Player.prototype.ended = function ended() { + return this.techGet_('ended'); + }; + + /** + * Returns whether or not the player is in the "seeking" state. + * + * @return {Boolean} True if the player is in the seeking state, false if not. + * @method seeking + */ + + Player.prototype.seeking = function seeking() { + return this.techGet_('seeking'); + }; + + /** + * Returns the TimeRanges of the media that are currently available + * for seeking to. + * + * @return {TimeRanges} the seekable intervals of the media timeline + * @method seekable + */ + + Player.prototype.seekable = function seekable() { + return this.techGet_('seekable'); + }; + + /** + * Report user activity + * + * @param {Object} event Event object + * @method reportUserActivity + */ + + Player.prototype.reportUserActivity = function reportUserActivity(event) { + this.userActivity_ = true; + }; + + /** + * Get/set if user is active + * + * @param {Boolean} bool Value when setting + * @return {Boolean} Value if user is active user when getting + * @method userActive + */ + + Player.prototype.userActive = function userActive(bool) { + if (bool !== undefined) { + bool = !!bool; + if (bool !== this.userActive_) { + this.userActive_ = bool; + if (bool) { + // If the user was inactive and is now active we want to reset the + // inactivity timer + this.userActivity_ = true; + this.removeClass('vjs-user-inactive'); + this.addClass('vjs-user-active'); + this.trigger('useractive'); + } else { + // We're switching the state to inactive manually, so erase any other + // activity + this.userActivity_ = false; + + // Chrome/Safari/IE have bugs where when you change the cursor it can + // trigger a mousemove event. This causes an issue when you're hiding + // the cursor when the user is inactive, and a mousemove signals user + // activity. Making it impossible to go into inactive mode. Specifically + // this happens in fullscreen when we really need to hide the cursor. + // + // When this gets resolved in ALL browsers it can be removed + // https://code.google.com/p/chromium/issues/detail?id=103041 + if (this.tech_) { + this.tech_.one('mousemove', function (e) { + e.stopPropagation(); + e.preventDefault(); + }); + } + + this.removeClass('vjs-user-active'); + this.addClass('vjs-user-inactive'); + this.trigger('userinactive'); + } + } + return this; + } + return this.userActive_; + }; + + /** + * Listen for user activity based on timeout value + * + * @private + * @method listenForUserActivity_ + */ + + Player.prototype.listenForUserActivity_ = function listenForUserActivity_() { + var mouseInProgress = undefined, + lastMoveX = undefined, + lastMoveY = undefined; + + var handleActivity = Fn.bind(this, this.reportUserActivity); + + var handleMouseMove = function handleMouseMove(e) { + // #1068 - Prevent mousemove spamming + // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 + if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { + lastMoveX = e.screenX; + lastMoveY = e.screenY; + handleActivity(); + } + }; + + var handleMouseDown = function handleMouseDown() { + handleActivity(); + // For as long as the they are touching the device or have their mouse down, + // we consider them active even if they're not moving their finger or mouse. + // So we want to continue to update that they are active + this.clearInterval(mouseInProgress); + // Setting userActivity=true now and setting the interval to the same time + // as the activityCheck interval (250) should ensure we never miss the + // next activityCheck + mouseInProgress = this.setInterval(handleActivity, 250); + }; + + var handleMouseUp = function handleMouseUp(event) { + handleActivity(); + // Stop the interval that maintains activity if the mouse/touch is down + this.clearInterval(mouseInProgress); + }; + + // Any mouse movement will be considered user activity + this.on('mousedown', handleMouseDown); + this.on('mousemove', handleMouseMove); + this.on('mouseup', handleMouseUp); + + // Listen for keyboard navigation + // Shouldn't need to use inProgress interval because of key repeat + this.on('keydown', handleActivity); + this.on('keyup', handleActivity); + + // Run an interval every 250 milliseconds instead of stuffing everything into + // the mousemove/touchmove function itself, to prevent performance degradation. + // `this.reportUserActivity` simply sets this.userActivity_ to true, which + // then gets picked up by this loop + // http://ejohn.org/blog/learning-from-twitter/ + var inactivityTimeout = undefined; + var activityCheck = this.setInterval(function () { + // Check to see if mouse/touch activity has happened + if (this.userActivity_) { + // Reset the activity tracker + this.userActivity_ = false; + + // If the user state was inactive, set the state to active + this.userActive(true); + + // Clear any existing inactivity timeout to start the timer over + this.clearTimeout(inactivityTimeout); + + var timeout = this.options_['inactivityTimeout']; + if (timeout > 0) { + // In <timeout> milliseconds, if no more activity has occurred the + // user will be considered inactive + inactivityTimeout = this.setTimeout(function () { + // Protect against the case where the inactivityTimeout can trigger just + // before the next user activity is picked up by the activityCheck loop + // causing a flicker + if (!this.userActivity_) { + this.userActive(false); + } + }, timeout); + } + } + }, 250); + }; + + /** + * Gets or sets the current playback rate. A playback rate of + * 1.0 represents normal speed and 0.5 would indicate half-speed + * playback, for instance. + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate + * + * @param {Number} rate New playback rate to set. + * @return {Number} Returns the new playback rate when setting + * @return {Number} Returns the current playback rate when getting + * @method playbackRate + */ + + Player.prototype.playbackRate = function playbackRate(rate) { + if (rate !== undefined) { + this.techCall_('setPlaybackRate', rate); + return this; + } + + if (this.tech_ && this.tech_['featuresPlaybackRate']) { + return this.techGet_('playbackRate'); + } else { + return 1.0; + } + }; + + /** + * Gets or sets the audio flag + * + * @param {Boolean} bool True signals that this is an audio player. + * @return {Boolean} Returns true if player is audio, false if not when getting + * @return {Player} Returns the player if setting + * @private + * @method isAudio + */ + + Player.prototype.isAudio = function isAudio(bool) { + if (bool !== undefined) { + this.isAudio_ = !!bool; + return this; + } + + return !!this.isAudio_; + }; + + /** + * Returns the current state of network activity for the element, from + * the codes in the list below. + * - NETWORK_EMPTY (numeric value 0) + * The element has not yet been initialised. All attributes are in + * their initial states. + * - NETWORK_IDLE (numeric value 1) + * The element's resource selection algorithm is active and has + * selected a resource, but it is not actually using the network at + * this time. + * - NETWORK_LOADING (numeric value 2) + * The user agent is actively trying to download data. + * - NETWORK_NO_SOURCE (numeric value 3) + * The element's resource selection algorithm is active, but it has + * not yet found a resource to use. + * + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states + * @return {Number} the current network activity state + * @method networkState + */ + + Player.prototype.networkState = function networkState() { + return this.techGet_('networkState'); + }; + + /** + * Returns a value that expresses the current state of the element + * with respect to rendering the current playback position, from the + * codes in the list below. + * - HAVE_NOTHING (numeric value 0) + * No information regarding the media resource is available. + * - HAVE_METADATA (numeric value 1) + * Enough of the resource has been obtained that the duration of the + * resource is available. + * - HAVE_CURRENT_DATA (numeric value 2) + * Data for the immediate current playback position is available. + * - HAVE_FUTURE_DATA (numeric value 3) + * Data for the immediate current playback position is available, as + * well as enough data for the user agent to advance the current + * playback position in the direction of playback. + * - HAVE_ENOUGH_DATA (numeric value 4) + * The user agent estimates that enough data is available for + * playback to proceed uninterrupted. + * + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate + * @return {Number} the current playback rendering state + * @method readyState + */ + + Player.prototype.readyState = function readyState() { + return this.techGet_('readyState'); + }; + + /* + * Text tracks are tracks of timed text events. + * Captions - text displayed over the video for the hearing impaired + * Subtitles - text displayed over the video for those who don't understand language in the video + * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video + * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device + */ + + /** + * Get an array of associated text tracks. captions, subtitles, chapters, descriptions + * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks + * + * @return {Array} Array of track objects + * @method textTracks + */ + + Player.prototype.textTracks = function textTracks() { + // cannot use techGet_ directly because it checks to see whether the tech is ready. + // Flash is unlikely to be ready in time but textTracks should still work. + return this.tech_ && this.tech_['textTracks'](); + }; + + /** + * Get an array of remote text tracks + * + * @return {Array} + * @method remoteTextTracks + */ + + Player.prototype.remoteTextTracks = function remoteTextTracks() { + return this.tech_ && this.tech_['remoteTextTracks'](); + }; + + /** + * Get an array of remote html track elements + * + * @return {HTMLTrackElement[]} + * @method remoteTextTrackEls + */ + + Player.prototype.remoteTextTrackEls = function remoteTextTrackEls() { + return this.tech_ && this.tech_['remoteTextTrackEls'](); + }; + + /** + * Add a text track + * In addition to the W3C settings we allow adding additional info through options. + * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack + * + * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata + * @param {String=} label Optional label + * @param {String=} language Optional language + * @method addTextTrack + */ + + Player.prototype.addTextTrack = function addTextTrack(kind, label, language) { + return this.tech_ && this.tech_['addTextTrack'](kind, label, language); + }; + + /** + * Add a remote text track + * + * @param {Object} options Options for remote text track + * @method addRemoteTextTrack + */ + + Player.prototype.addRemoteTextTrack = function addRemoteTextTrack(options) { + return this.tech_ && this.tech_['addRemoteTextTrack'](options); + }; + + /** + * Remove a remote text track + * + * @param {Object} track Remote text track to remove + * @method removeRemoteTextTrack + */ + + Player.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { + this.tech_ && this.tech_['removeRemoteTextTrack'](track); + }; + + /** + * Get video width + * + * @return {Number} Video width + * @method videoWidth + */ + + Player.prototype.videoWidth = function videoWidth() { + return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; + }; + + /** + * Get video height + * + * @return {Number} Video height + * @method videoHeight + */ + + Player.prototype.videoHeight = function videoHeight() { + return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; + }; + + // Methods to add support for + // initialTime: function(){ return this.techCall_('initialTime'); }, + // startOffsetTime: function(){ return this.techCall_('startOffsetTime'); }, + // played: function(){ return this.techCall_('played'); }, + // videoTracks: function(){ return this.techCall_('videoTracks'); }, + // audioTracks: function(){ return this.techCall_('audioTracks'); }, + // defaultPlaybackRate: function(){ return this.techCall_('defaultPlaybackRate'); }, + // defaultMuted: function(){ return this.techCall_('defaultMuted'); } + + /** + * The player's language code + * NOTE: The language should be set in the player options if you want the + * the controls to be built with a specific language. Changing the lanugage + * later will not update controls text. + * + * @param {String} code The locale string + * @return {String} The locale string when getting + * @return {Player} self when setting + * @method language + */ + + Player.prototype.language = function language(code) { + if (code === undefined) { + return this.language_; + } + + this.language_ = ('' + code).toLowerCase(); + return this; + }; + + /** + * Get the player's language dictionary + * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time + * Languages specified directly in the player options have precedence + * + * @return {Array} Array of languages + * @method languages + */ + + Player.prototype.languages = function languages() { + return _utilsMergeOptionsJs2['default'](Player.prototype.options_.languages, this.languages_); + }; + + /** + * Converts track info to JSON + * + * @return {Object} JSON object of options + * @method toJSON + */ + + Player.prototype.toJSON = function toJSON() { + var options = _utilsMergeOptionsJs2['default'](this.options_); + var tracks = options.tracks; + + options.tracks = []; + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + // deep merge tracks and null out player so no circular references + track = _utilsMergeOptionsJs2['default'](track); + track.player = undefined; + options.tracks[i] = track; + } + + return options; + }; + + /** + * Creates a simple modal dialog (an instance of the `ModalDialog` + * component) that immediately overlays the player with arbitrary + * content and removes itself when closed. + * + * @param {String|Function|Element|Array|Null} content + * Same as `ModalDialog#content`'s param of the same name. + * + * The most straight-forward usage is to provide a string or DOM + * element. + * + * @param {Object} [options] + * Extra options which will be passed on to the `ModalDialog`. + * + * @return {ModalDialog} + */ + + Player.prototype.createModal = function createModal(content, options) { + var player = this; + + options = options || {}; + options.content = content || ''; + + var modal = new _modalDialog2['default'](player, options); + + player.addChild(modal); + modal.on('dispose', function () { + player.removeChild(modal); + }); + + return modal.open(); + }; + + /** + * Gets tag settings + * + * @param {Element} tag The player tag + * @return {Array} An array of sources and track objects + * @static + * @method getTagSettings + */ + + Player.getTagSettings = function getTagSettings(tag) { + var baseOptions = { + 'sources': [], + 'tracks': [] + }; + + var tagOptions = Dom.getElAttributes(tag); + var dataSetup = tagOptions['data-setup']; + + // Check if data-setup attr exists. + if (dataSetup !== null) { + // Parse options JSON + + var _safeParseTuple = _safeJsonParseTuple2['default'](dataSetup || '{}'); + + var err = _safeParseTuple[0]; + var data = _safeParseTuple[1]; + + if (err) { + _utilsLogJs2['default'].error(err); + } + _objectAssign2['default'](tagOptions, data); + } + + _objectAssign2['default'](baseOptions, tagOptions); + + // Get tag children settings + if (tag.hasChildNodes()) { + var children = tag.childNodes; + + for (var i = 0, j = children.length; i < j; i++) { + var child = children[i]; + // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ + var childName = child.nodeName.toLowerCase(); + if (childName === 'source') { + baseOptions.sources.push(Dom.getElAttributes(child)); + } else if (childName === 'track') { + baseOptions.tracks.push(Dom.getElAttributes(child)); + } + } + } + + return baseOptions; + }; + + return Player; +})(_componentJs2['default']); + +Player.players = {}; + +var navigator = _globalWindow2['default'].navigator; +/* + * Player instance options, surfaced using options + * options = Player.prototype.options_ + * Make changes in options, not here. + * + * @type {Object} + * @private + */ +Player.prototype.options_ = { + // Default order of fallback technology + techOrder: ['html5', 'flash'], + // techOrder: ['flash','html5'], + + html5: {}, + flash: {}, + + // defaultVolume: 0.85, + defaultVolume: 0.00, // The freakin seaguls are driving me crazy! + + // default inactivity timeout + inactivityTimeout: 2000, + + // default playback rates + playbackRates: [], + // Add playback rate selection by adding rates + // 'playbackRates': [0.5, 1, 1.5, 2], + + // Included control sets + children: ['mediaLoader', 'posterImage', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'controlBar', 'errorDisplay', 'textTrackSettings'], + + language: _globalDocument2['default'].getElementsByTagName('html')[0].getAttribute('lang') || navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language || 'en', + + // locales and their language translations + languages: {}, + + // Default message to show when a video cannot be played. + notSupportedMessage: 'No compatible source was found for this media.' +}; + +/** + * Fired when the player has initial duration and dimension information + * + * @event loadedmetadata + */ +Player.prototype.handleLoadedMetaData_; + +/** + * Fired when the player has downloaded data at the current playback position + * + * @event loadeddata + */ +Player.prototype.handleLoadedData_; + +/** + * Fired when the user is active, e.g. moves the mouse over the player + * + * @event useractive + */ +Player.prototype.handleUserActive_; + +/** + * Fired when the user is inactive, e.g. a short delay after the last mouse move or control interaction + * + * @event userinactive + */ +Player.prototype.handleUserInactive_; + +/** + * Fired when the current playback position has changed * + * During playback this is fired every 15-250 milliseconds, depending on the + * playback technology in use. + * + * @event timeupdate + */ +Player.prototype.handleTimeUpdate_; + +/** + * Fired when video playback ends + * + * @event ended + */ +Player.prototype.handleTechEnded_; + +/** + * Fired when the volume changes + * + * @event volumechange + */ +Player.prototype.handleVolumeChange_; + +/** + * Fired when an error occurs + * + * @event error + */ +Player.prototype.handleError_; + +Player.prototype.flexNotSupported_ = function () { + var elem = _globalDocument2['default'].createElement('i'); + + // Note: We don't actually use flexBasis (or flexOrder), but it's one of the more + // common flex features that we can rely on when checking for flex support. + return !('flexBasis' in elem.style || 'webkitFlexBasis' in elem.style || 'mozFlexBasis' in elem.style || 'msFlexBasis' in elem.style || 'msFlexOrder' in elem.style) /* IE10-specific (2012 flex spec) */; +}; + +_componentJs2['default'].registerComponent('Player', Player); +exports['default'] = Player; +module.exports = exports['default']; +// If empty string, make it a parsable json object. + +},{"./big-play-button.js":63,"./component.js":67,"./control-bar/control-bar.js":68,"./error-display.js":98,"./fullscreen-api.js":101,"./loading-spinner.js":102,"./media-error.js":103,"./modal-dialog":107,"./poster-image.js":112,"./tech/html5.js":117,"./tech/loader.js":118,"./tech/tech.js":119,"./tracks/text-track-display.js":123,"./tracks/text-track-list-converter.js":125,"./tracks/text-track-settings.js":127,"./utils/browser.js":129,"./utils/buffer.js":130,"./utils/dom.js":132,"./utils/events.js":133,"./utils/fn.js":134,"./utils/guid.js":136,"./utils/log.js":137,"./utils/merge-options.js":138,"./utils/stylesheet.js":139,"./utils/time-ranges.js":140,"./utils/to-title-case.js":141,"global/document":1,"global/window":2,"object.assign":45,"safe-json-parse/tuple":54}],109:[function(_dereq_,module,exports){ +/** + * @file plugins.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _playerJs = _dereq_('./player.js'); + +var _playerJs2 = _interopRequireDefault(_playerJs); + +/** + * The method for registering a video.js plugin + * + * @param {String} name The name of the plugin + * @param {Function} init The function that is run when the player inits + * @method plugin + */ +var plugin = function plugin(name, init) { + _playerJs2['default'].prototype[name] = init; +}; + +exports['default'] = plugin; +module.exports = exports['default']; + +},{"./player.js":108}],110:[function(_dereq_,module,exports){ +/** + * @file popup-button.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _clickableComponentJs = _dereq_('../clickable-component.js'); + +var _clickableComponentJs2 = _interopRequireDefault(_clickableComponentJs); + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _popupJs = _dereq_('./popup.js'); + +var _popupJs2 = _interopRequireDefault(_popupJs); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsToTitleCaseJs = _dereq_('../utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +/** + * A button class with a popup control + * + * @param {Player|Object} player + * @param {Object=} options + * @extends ClickableComponent + * @class PopupButton + */ + +var PopupButton = (function (_ClickableComponent) { + _inherits(PopupButton, _ClickableComponent); + + function PopupButton(player) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, PopupButton); + + _ClickableComponent.call(this, player, options); + + this.update(); + } + + /** + * Update popup + * + * @method update + */ + + PopupButton.prototype.update = function update() { + var popup = this.createPopup(); + + if (this.popup) { + this.removeChild(this.popup); + } + + this.popup = popup; + this.addChild(popup); + + if (this.items && this.items.length === 0) { + this.hide(); + } else if (this.items && this.items.length > 1) { + this.show(); + } + }; + + /** + * Create popup - Override with specific functionality for component + * + * @return {Popup} The constructed popup + * @method createPopup + */ + + PopupButton.prototype.createPopup = function createPopup() {}; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + PopupButton.prototype.createEl = function createEl() { + return _ClickableComponent.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass() + }); + }; + + /** + * Allow sub components to stack CSS class names + * + * @return {String} The constructed class name + * @method buildCSSClass + */ + + PopupButton.prototype.buildCSSClass = function buildCSSClass() { + var menuButtonClass = 'vjs-menu-button'; + + // If the inline option is passed, we want to use different styles altogether. + if (this.options_.inline === true) { + menuButtonClass += '-inline'; + } else { + menuButtonClass += '-popup'; + } + + return 'vjs-menu-button ' + menuButtonClass + ' ' + _ClickableComponent.prototype.buildCSSClass.call(this); + }; + + return PopupButton; +})(_clickableComponentJs2['default']); + +_componentJs2['default'].registerComponent('PopupButton', PopupButton); +exports['default'] = PopupButton; +module.exports = exports['default']; + +},{"../clickable-component.js":65,"../component.js":67,"../utils/dom.js":132,"../utils/fn.js":134,"../utils/to-title-case.js":141,"./popup.js":111}],111:[function(_dereq_,module,exports){ +/** + * @file popup.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsEventsJs = _dereq_('../utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +/** + * The Popup component is used to build pop up controls. + * + * @extends Component + * @class Popup + */ + +var Popup = (function (_Component) { + _inherits(Popup, _Component); + + function Popup() { + _classCallCheck(this, Popup); + + _Component.apply(this, arguments); + } + + /** + * Add a popup item to the popup + * + * @param {Object|String} component Component or component type to add + * @method addItem + */ + + Popup.prototype.addItem = function addItem(component) { + this.addChild(component); + component.on('click', Fn.bind(this, function () { + this.unlockShowing(); + })); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Popup.prototype.createEl = function createEl() { + var contentElType = this.options_.contentElType || 'ul'; + this.contentEl_ = Dom.createEl(contentElType, { + className: 'vjs-menu-content' + }); + var el = _Component.prototype.createEl.call(this, 'div', { + append: this.contentEl_, + className: 'vjs-menu' + }); + el.appendChild(this.contentEl_); + + // Prevent clicks from bubbling up. Needed for Popup Buttons, + // where a click on the parent is significant + Events.on(el, 'click', function (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + }); + + return el; + }; + + return Popup; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('Popup', Popup); +exports['default'] = Popup; +module.exports = exports['default']; + +},{"../component.js":67,"../utils/dom.js":132,"../utils/events.js":133,"../utils/fn.js":134}],112:[function(_dereq_,module,exports){ +/** + * @file poster-image.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _clickableComponentJs = _dereq_('./clickable-component.js'); + +var _clickableComponentJs2 = _interopRequireDefault(_clickableComponentJs); + +var _componentJs = _dereq_('./component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsDomJs = _dereq_('./utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsBrowserJs = _dereq_('./utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +/** + * The component that handles showing the poster image. + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Button + * @class PosterImage + */ + +var PosterImage = (function (_ClickableComponent) { + _inherits(PosterImage, _ClickableComponent); + + function PosterImage(player, options) { + _classCallCheck(this, PosterImage); + + _ClickableComponent.call(this, player, options); + + this.update(); + player.on('posterchange', Fn.bind(this, this.update)); + } + + /** + * Clean up the poster image + * + * @method dispose + */ + + PosterImage.prototype.dispose = function dispose() { + this.player().off('posterchange', this.update); + _ClickableComponent.prototype.dispose.call(this); + }; + + /** + * Create the poster's image element + * + * @return {Element} + * @method createEl + */ + + PosterImage.prototype.createEl = function createEl() { + var el = Dom.createEl('div', { + className: 'vjs-poster', + + // Don't want poster to be tabbable. + tabIndex: -1 + }); + + // To ensure the poster image resizes while maintaining its original aspect + // ratio, use a div with `background-size` when available. For browsers that + // do not support `background-size` (e.g. IE8), fall back on using a regular + // img element. + if (!browser.BACKGROUND_SIZE_SUPPORTED) { + this.fallbackImg_ = Dom.createEl('img'); + el.appendChild(this.fallbackImg_); + } + + return el; + }; + + /** + * Event handler for updates to the player's poster source + * + * @method update + */ + + PosterImage.prototype.update = function update() { + var url = this.player().poster(); + + this.setSrc(url); + + // If there's no poster source we should display:none on this component + // so it's not still clickable or right-clickable + if (url) { + this.show(); + } else { + this.hide(); + } + }; + + /** + * Set the poster source depending on the display method + * + * @param {String} url The URL to the poster source + * @method setSrc + */ + + PosterImage.prototype.setSrc = function setSrc(url) { + if (this.fallbackImg_) { + this.fallbackImg_.src = url; + } else { + var backgroundImage = ''; + // Any falsey values should stay as an empty string, otherwise + // this will throw an extra error + if (url) { + backgroundImage = 'url("' + url + '")'; + } + + this.el_.style.backgroundImage = backgroundImage; + } + }; + + /** + * Event handler for clicks on the poster image + * + * @method handleClick + */ + + PosterImage.prototype.handleClick = function handleClick() { + // We don't want a click to trigger playback when controls are disabled + // but CSS should be hiding the poster to prevent that from happening + if (this.player_.paused()) { + this.player_.play(); + } else { + this.player_.pause(); + } + }; + + return PosterImage; +})(_clickableComponentJs2['default']); + +_componentJs2['default'].registerComponent('PosterImage', PosterImage); +exports['default'] = PosterImage; +module.exports = exports['default']; + +},{"./clickable-component.js":65,"./component.js":67,"./utils/browser.js":129,"./utils/dom.js":132,"./utils/fn.js":134}],113:[function(_dereq_,module,exports){ +/** + * @file setup.js + * + * Functions for automatically setting up a player + * based on the data-setup attribute of the video tag + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _windowLoaded = false; +var videojs = undefined; + +// Automatically set up any tags that have a data-setup attribute +var autoSetup = function autoSetup() { + // One day, when we stop supporting IE8, go back to this, but in the meantime...*hack hack hack* + // var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); + // var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); + // var mediaEls = vids.concat(audios); + + // Because IE8 doesn't support calling slice on a node list, we need to loop through each list of elements + // to build up a new, combined list of elements. + var vids = _globalDocument2['default'].getElementsByTagName('video'); + var audios = _globalDocument2['default'].getElementsByTagName('audio'); + var mediaEls = []; + if (vids && vids.length > 0) { + for (var i = 0, e = vids.length; i < e; i++) { + mediaEls.push(vids[i]); + } + } + if (audios && audios.length > 0) { + for (var i = 0, e = audios.length; i < e; i++) { + mediaEls.push(audios[i]); + } + } + + // Check if any media elements exist + if (mediaEls && mediaEls.length > 0) { + + for (var i = 0, e = mediaEls.length; i < e; i++) { + var mediaEl = mediaEls[i]; + + // Check if element exists, has getAttribute func. + // IE seems to consider typeof el.getAttribute == 'object' instead of 'function' like expected, at least when loading the player immediately. + if (mediaEl && mediaEl.getAttribute) { + + // Make sure this player hasn't already been set up. + if (mediaEl['player'] === undefined) { + var options = mediaEl.getAttribute('data-setup'); + + // Check if data-setup attr exists. + // We only auto-setup if they've added the data-setup attr. + if (options !== null) { + // Create new video.js instance. + var player = videojs(mediaEl); + } + } + + // If getAttribute isn't defined, we need to wait for the DOM. + } else { + autoSetupTimeout(1); + break; + } + } + + // No videos were found, so keep looping unless page is finished loading. + } else if (!_windowLoaded) { + autoSetupTimeout(1); + } +}; + +// Pause to let the DOM keep processing +var autoSetupTimeout = function autoSetupTimeout(wait, vjs) { + if (vjs) { + videojs = vjs; + } + + setTimeout(autoSetup, wait); +}; + +if (_globalDocument2['default'].readyState === 'complete') { + _windowLoaded = true; +} else { + Events.one(_globalWindow2['default'], 'load', function () { + _windowLoaded = true; + }); +} + +var hasLoaded = function hasLoaded() { + return _windowLoaded; +}; + +exports.autoSetup = autoSetup; +exports.autoSetupTimeout = autoSetupTimeout; +exports.hasLoaded = hasLoaded; + +},{"./utils/events.js":133,"global/document":1,"global/window":2}],114:[function(_dereq_,module,exports){ +/** + * @file slider.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +/** + * The base functionality for sliders like the volume bar and seek bar + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class Slider + */ + +var Slider = (function (_Component) { + _inherits(Slider, _Component); + + function Slider(player, options) { + _classCallCheck(this, Slider); + + _Component.call(this, player, options); + + // Set property names to bar to match with the child Slider class is looking for + this.bar = this.getChild(this.options_.barName); + + // Set a horizontal or vertical class on the slider depending on the slider type + this.vertical(!!this.options_.vertical); + + this.on('mousedown', this.handleMouseDown); + this.on('touchstart', this.handleMouseDown); + this.on('focus', this.handleFocus); + this.on('blur', this.handleBlur); + this.on('click', this.handleClick); + + this.on(player, 'controlsvisible', this.update); + this.on(player, this.playerEvent, this.update); + } + + /** + * Create the component's DOM element + * + * @param {String} type Type of element to create + * @param {Object=} props List of properties in Object form + * @return {Element} + * @method createEl + */ + + Slider.prototype.createEl = function createEl(type) { + var props = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + var attributes = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + // Add the slider element class to all sub classes + props.className = props.className + ' vjs-slider'; + props = _objectAssign2['default']({ + tabIndex: 0 + }, props); + + attributes = _objectAssign2['default']({ + 'role': 'slider', + 'aria-valuenow': 0, + 'aria-valuemin': 0, + 'aria-valuemax': 100, + tabIndex: 0 + }, attributes); + + return _Component.prototype.createEl.call(this, type, props, attributes); + }; + + /** + * Handle mouse down on slider + * + * @param {Object} event Mouse down event object + * @method handleMouseDown + */ + + Slider.prototype.handleMouseDown = function handleMouseDown(event) { + event.preventDefault(); + Dom.blockTextSelection(); + + this.addClass('vjs-sliding'); + this.trigger('slideractive'); + + this.on(_globalDocument2['default'], 'mousemove', this.handleMouseMove); + this.on(_globalDocument2['default'], 'mouseup', this.handleMouseUp); + this.on(_globalDocument2['default'], 'touchmove', this.handleMouseMove); + this.on(_globalDocument2['default'], 'touchend', this.handleMouseUp); + + this.handleMouseMove(event); + }; + + /** + * To be overridden by a subclass + * + * @method handleMouseMove + */ + + Slider.prototype.handleMouseMove = function handleMouseMove() {}; + + /** + * Handle mouse up on Slider + * + * @method handleMouseUp + */ + + Slider.prototype.handleMouseUp = function handleMouseUp() { + Dom.unblockTextSelection(); + + this.removeClass('vjs-sliding'); + this.trigger('sliderinactive'); + + this.off(_globalDocument2['default'], 'mousemove', this.handleMouseMove); + this.off(_globalDocument2['default'], 'mouseup', this.handleMouseUp); + this.off(_globalDocument2['default'], 'touchmove', this.handleMouseMove); + this.off(_globalDocument2['default'], 'touchend', this.handleMouseUp); + + this.update(); + }; + + /** + * Update slider + * + * @method update + */ + + Slider.prototype.update = function update() { + // In VolumeBar init we have a setTimeout for update that pops and update to the end of the + // execution stack. The player is destroyed before then update will cause an error + if (!this.el_) return; + + // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse. + // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later. + // var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); + var progress = this.getPercent(); + var bar = this.bar; + + // If there's no bar... + if (!bar) return; + + // Protect against no duration and other division issues + if (typeof progress !== 'number' || progress !== progress || progress < 0 || progress === Infinity) { + progress = 0; + } + + // Convert to a percentage for setting + var percentage = (progress * 100).toFixed(2) + '%'; + + // Set the new bar width or height + if (this.vertical()) { + bar.el().style.height = percentage; + } else { + bar.el().style.width = percentage; + } + }; + + /** + * Calculate distance for slider + * + * @param {Object} event Event object + * @method calculateDistance + */ + + Slider.prototype.calculateDistance = function calculateDistance(event) { + var position = Dom.getPointerPosition(this.el_, event); + if (this.vertical()) { + return position.y; + } + return position.x; + }; + + /** + * Handle on focus for slider + * + * @method handleFocus + */ + + Slider.prototype.handleFocus = function handleFocus() { + this.on(_globalDocument2['default'], 'keydown', this.handleKeyPress); + }; + + /** + * Handle key press for slider + * + * @param {Object} event Event object + * @method handleKeyPress + */ + + Slider.prototype.handleKeyPress = function handleKeyPress(event) { + if (event.which === 37 || event.which === 40) { + // Left and Down Arrows + event.preventDefault(); + this.stepBack(); + } else if (event.which === 38 || event.which === 39) { + // Up and Right Arrows + event.preventDefault(); + this.stepForward(); + } + }; + + /** + * Handle on blur for slider + * + * @method handleBlur + */ + + Slider.prototype.handleBlur = function handleBlur() { + this.off(_globalDocument2['default'], 'keydown', this.handleKeyPress); + }; + + /** + * Listener for click events on slider, used to prevent clicks + * from bubbling up to parent elements like button menus. + * + * @param {Object} event Event object + * @method handleClick + */ + + Slider.prototype.handleClick = function handleClick(event) { + event.stopImmediatePropagation(); + event.preventDefault(); + }; + + /** + * Get/set if slider is horizontal for vertical + * + * @param {Boolean} bool True if slider is vertical, false is horizontal + * @return {Boolean} True if slider is vertical, false is horizontal + * @method vertical + */ + + Slider.prototype.vertical = function vertical(bool) { + if (bool === undefined) { + return this.vertical_ || false; + } + + this.vertical_ = !!bool; + + if (this.vertical_) { + this.addClass('vjs-slider-vertical'); + } else { + this.addClass('vjs-slider-horizontal'); + } + + return this; + }; + + return Slider; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('Slider', Slider); +exports['default'] = Slider; +module.exports = exports['default']; + +},{"../component.js":67,"../utils/dom.js":132,"global/document":1,"object.assign":45}],115:[function(_dereq_,module,exports){ +/** + * @file flash-rtmp.js + */ +'use strict'; + +exports.__esModule = true; +function FlashRtmpDecorator(Flash) { + Flash.streamingFormats = { + 'rtmp/mp4': 'MP4', + 'rtmp/flv': 'FLV' + }; + + Flash.streamFromParts = function (connection, stream) { + return connection + '&' + stream; + }; + + Flash.streamToParts = function (src) { + var parts = { + connection: '', + stream: '' + }; + + if (!src) return parts; + + // Look for the normal URL separator we expect, '&'. + // If found, we split the URL into two pieces around the + // first '&'. + var connEnd = src.search(/&(?!\w+=)/); + var streamBegin = undefined; + if (connEnd !== -1) { + streamBegin = connEnd + 1; + } else { + // If there's not a '&', we use the last '/' as the delimiter. + connEnd = streamBegin = src.lastIndexOf('/') + 1; + if (connEnd === 0) { + // really, there's not a '/'? + connEnd = streamBegin = src.length; + } + } + parts.connection = src.substring(0, connEnd); + parts.stream = src.substring(streamBegin, src.length); + + return parts; + }; + + Flash.isStreamingType = function (srcType) { + return srcType in Flash.streamingFormats; + }; + + // RTMP has four variations, any string starting + // with one of these protocols should be valid + Flash.RTMP_RE = /^rtmp[set]?:\/\//i; + + Flash.isStreamingSrc = function (src) { + return Flash.RTMP_RE.test(src); + }; + + /** + * A source handler for RTMP urls + * @type {Object} + */ + Flash.rtmpSourceHandler = {}; + + /** + * Check if Flash can play the given videotype + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + Flash.rtmpSourceHandler.canPlayType = function (type) { + if (Flash.isStreamingType(type)) { + return 'maybe'; + } + + return ''; + }; + + /** + * Check if Flash can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + Flash.rtmpSourceHandler.canHandleSource = function (source) { + var can = Flash.rtmpSourceHandler.canPlayType(source.type); + + if (can) { + return can; + } + + if (Flash.isStreamingSrc(source.src)) { + return 'maybe'; + } + + return ''; + }; + + /** + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {Flash} tech The instance of the Flash tech + */ + Flash.rtmpSourceHandler.handleSource = function (source, tech) { + var srcParts = Flash.streamToParts(source.src); + + tech['setRtmpConnection'](srcParts.connection); + tech['setRtmpStream'](srcParts.stream); + }; + + // Register the native source handler + Flash.registerSourceHandler(Flash.rtmpSourceHandler); + + return Flash; +} + +exports['default'] = FlashRtmpDecorator; +module.exports = exports['default']; + +},{}],116:[function(_dereq_,module,exports){ +/** + * @file flash.js + * VideoJS-SWF - Custom Flash Player with HTML5-ish API + * https://github.com/zencoder/video-js-swf + * Not using setupTriggers. Using global onEvent func to distribute events + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _tech = _dereq_('./tech'); + +var _tech2 = _interopRequireDefault(_tech); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsUrlJs = _dereq_('../utils/url.js'); + +var Url = _interopRequireWildcard(_utilsUrlJs); + +var _utilsTimeRangesJs = _dereq_('../utils/time-ranges.js'); + +var _flashRtmp = _dereq_('./flash-rtmp'); + +var _flashRtmp2 = _interopRequireDefault(_flashRtmp); + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +var navigator = _globalWindow2['default'].navigator; +/** + * Flash Media Controller - Wrapper for fallback SWF API + * + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends Tech + * @class Flash + */ + +var Flash = (function (_Tech) { + _inherits(Flash, _Tech); + + function Flash(options, ready) { + _classCallCheck(this, Flash); + + _Tech.call(this, options, ready); + + // Set the source when ready + if (options.source) { + this.ready(function () { + this.setSource(options.source); + }, true); + } + + // Having issues with Flash reloading on certain page actions (hide/resize/fullscreen) in certain browsers + // This allows resetting the playhead when we catch the reload + if (options.startTime) { + this.ready(function () { + this.load(); + this.play(); + this.currentTime(options.startTime); + }, true); + } + + // Add global window functions that the swf expects + // A 4.x workflow we weren't able to solve for in 5.0 + // because of the need to hard code these functions + // into the swf for security reasons + _globalWindow2['default'].videojs = _globalWindow2['default'].videojs || {}; + _globalWindow2['default'].videojs.Flash = _globalWindow2['default'].videojs.Flash || {}; + _globalWindow2['default'].videojs.Flash.onReady = Flash.onReady; + _globalWindow2['default'].videojs.Flash.onEvent = Flash.onEvent; + _globalWindow2['default'].videojs.Flash.onError = Flash.onError; + + this.on('seeked', function () { + this.lastSeekTarget_ = undefined; + }); + } + + // Create setters and getters for attributes + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Flash.prototype.createEl = function createEl() { + var options = this.options_; + + // If video.js is hosted locally you should also set the location + // for the hosted swf, which should be relative to the page (not video.js) + // Otherwise this adds a CDN url. + // The CDN also auto-adds a swf URL for that specific version. + if (!options.swf) { + options.swf = '//vjs.zencdn.net/swf/5.0.1/video-js.swf'; + } + + // Generate ID for swf object + var objId = options.techId; + + // Merge default flashvars with ones passed in to init + var flashVars = _objectAssign2['default']({ + + // SWF Callback Functions + 'readyFunction': 'videojs.Flash.onReady', + 'eventProxyFunction': 'videojs.Flash.onEvent', + 'errorEventProxyFunction': 'videojs.Flash.onError', + + // Player Settings + 'autoplay': options.autoplay, + 'preload': options.preload, + 'loop': options.loop, + 'muted': options.muted + + }, options.flashVars); + + // Merge default parames with ones passed in + var params = _objectAssign2['default']({ + 'wmode': 'opaque', // Opaque is needed to overlay controls, but can affect playback performance + 'bgcolor': '#000000' // Using bgcolor prevents a white flash when the object is loading + }, options.params); + + // Merge default attributes with ones passed in + var attributes = _objectAssign2['default']({ + 'id': objId, + 'name': objId, // Both ID and Name needed or swf to identify itself + 'class': 'vjs-tech' + }, options.attributes); + + this.el_ = Flash.embed(options.swf, flashVars, params, attributes); + this.el_.tech = this; + + return this.el_; + }; + + /** + * Play for flash tech + * + * @method play + */ + + Flash.prototype.play = function play() { + if (this.ended()) { + this.setCurrentTime(0); + } + this.el_.vjs_play(); + }; + + /** + * Pause for flash tech + * + * @method pause + */ + + Flash.prototype.pause = function pause() { + this.el_.vjs_pause(); + }; + + /** + * Get/set video + * + * @param {Object=} src Source object + * @return {Object} + * @method src + */ + + Flash.prototype.src = function src(_src) { + if (_src === undefined) { + return this.currentSrc(); + } + + // Setting src through `src` not `setSrc` will be deprecated + return this.setSrc(_src); + }; + + /** + * Set video + * + * @param {Object=} src Source object + * @deprecated + * @method setSrc + */ + + Flash.prototype.setSrc = function setSrc(src) { + // Make sure source URL is absolute. + src = Url.getAbsoluteURL(src); + this.el_.vjs_src(src); + + // Currently the SWF doesn't autoplay if you load a source later. + // e.g. Load player w/ no source, wait 2s, set src. + if (this.autoplay()) { + var tech = this; + this.setTimeout(function () { + tech.play(); + }, 0); + } + }; + + /** + * Returns true if the tech is currently seeking. + * @return {boolean} true if seeking + */ + + Flash.prototype.seeking = function seeking() { + return this.lastSeekTarget_ !== undefined; + }; + + /** + * Set current time + * + * @param {Number} time Current time of video + * @method setCurrentTime + */ + + Flash.prototype.setCurrentTime = function setCurrentTime(time) { + var seekable = this.seekable(); + if (seekable.length) { + // clamp to the current seekable range + time = time > seekable.start(0) ? time : seekable.start(0); + time = time < seekable.end(seekable.length - 1) ? time : seekable.end(seekable.length - 1); + + this.lastSeekTarget_ = time; + this.trigger('seeking'); + this.el_.vjs_setProperty('currentTime', time); + _Tech.prototype.setCurrentTime.call(this); + } + }; + + /** + * Get current time + * + * @param {Number=} time Current time of video + * @return {Number} Current time + * @method currentTime + */ + + Flash.prototype.currentTime = function currentTime(time) { + // when seeking make the reported time keep up with the requested time + // by reading the time we're seeking to + if (this.seeking()) { + return this.lastSeekTarget_ || 0; + } + return this.el_.vjs_getProperty('currentTime'); + }; + + /** + * Get current source + * + * @method currentSrc + */ + + Flash.prototype.currentSrc = function currentSrc() { + if (this.currentSource_) { + return this.currentSource_.src; + } else { + return this.el_.vjs_getProperty('currentSrc'); + } + }; + + /** + * Load media into player + * + * @method load + */ + + Flash.prototype.load = function load() { + this.el_.vjs_load(); + }; + + /** + * Get poster + * + * @method poster + */ + + Flash.prototype.poster = function poster() { + this.el_.vjs_getProperty('poster'); + }; + + /** + * Poster images are not handled by the Flash tech so make this a no-op + * + * @method setPoster + */ + + Flash.prototype.setPoster = function setPoster() {}; + + /** + * Determine if can seek in media + * + * @return {TimeRangeObject} + * @method seekable + */ + + Flash.prototype.seekable = function seekable() { + var duration = this.duration(); + if (duration === 0) { + return _utilsTimeRangesJs.createTimeRange(); + } + return _utilsTimeRangesJs.createTimeRange(0, duration); + }; + + /** + * Get buffered time range + * + * @return {TimeRangeObject} + * @method buffered + */ + + Flash.prototype.buffered = function buffered() { + var ranges = this.el_.vjs_getProperty('buffered'); + if (ranges.length === 0) { + return _utilsTimeRangesJs.createTimeRange(); + } + return _utilsTimeRangesJs.createTimeRange(ranges[0][0], ranges[0][1]); + }; + + /** + * Get fullscreen support - + * Flash does not allow fullscreen through javascript + * so always returns false + * + * @return {Boolean} false + * @method supportsFullScreen + */ + + Flash.prototype.supportsFullScreen = function supportsFullScreen() { + return false; // Flash does not allow fullscreen through javascript + }; + + /** + * Request to enter fullscreen + * Flash does not allow fullscreen through javascript + * so always returns false + * + * @return {Boolean} false + * @method enterFullScreen + */ + + Flash.prototype.enterFullScreen = function enterFullScreen() { + return false; + }; + + return Flash; +})(_tech2['default']); + +var _api = Flash.prototype; +var _readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','); +var _readOnly = 'networkState,readyState,initialTime,duration,startOffsetTime,paused,ended,videoTracks,audioTracks,videoWidth,videoHeight'.split(','); + +function _createSetter(attr) { + var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); + _api['set' + attrUpper] = function (val) { + return this.el_.vjs_setProperty(attr, val); + }; +} +function _createGetter(attr) { + _api[attr] = function () { + return this.el_.vjs_getProperty(attr); + }; +} + +// Create getter and setters for all read/write attributes +for (var i = 0; i < _readWrite.length; i++) { + _createGetter(_readWrite[i]); + _createSetter(_readWrite[i]); +} + +// Create getters for read-only attributes +for (var i = 0; i < _readOnly.length; i++) { + _createGetter(_readOnly[i]); +} + +/* Flash Support Testing -------------------------------------------------------- */ + +Flash.isSupported = function () { + return Flash.version()[0] >= 10; + // return swfobject.hasFlashPlayerVersion('10'); +}; + +// Add Source Handler pattern functions to this tech +_tech2['default'].withSourceHandlers(Flash); + +/* + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * + * @param {Object} source The source object + * @param {Flash} tech The instance of the Flash tech + */ +Flash.nativeSourceHandler = {}; + +/** + * Check if Flash can play the given videotype + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +Flash.nativeSourceHandler.canPlayType = function (type) { + if (type in Flash.formats) { + return 'maybe'; + } + + return ''; +}; + +/* + * Check Flash can handle the source natively + * + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +Flash.nativeSourceHandler.canHandleSource = function (source) { + var type; + + function guessMimeType(src) { + var ext = Url.getFileExtension(src); + if (ext) { + return 'video/' + ext; + } + return ''; + } + + if (!source.type) { + type = guessMimeType(source.src); + } else { + // Strip code information from the type because we don't get that specific + type = source.type.replace(/;.*/, '').toLowerCase(); + } + + return Flash.nativeSourceHandler.canPlayType(type); +}; + +/* + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * + * @param {Object} source The source object + * @param {Flash} tech The instance of the Flash tech + */ +Flash.nativeSourceHandler.handleSource = function (source, tech) { + tech.setSrc(source.src); +}; + +/* + * Clean up the source handler when disposing the player or switching sources.. + * (no cleanup is needed when supporting the format natively) + */ +Flash.nativeSourceHandler.dispose = function () {}; + +// Register the native source handler +Flash.registerSourceHandler(Flash.nativeSourceHandler); + +Flash.formats = { + 'video/flv': 'FLV', + 'video/x-flv': 'FLV', + 'video/mp4': 'MP4', + 'video/m4v': 'MP4' +}; + +Flash.onReady = function (currSwf) { + var el = Dom.getEl(currSwf); + var tech = el && el.tech; + + // if there is no el then the tech has been disposed + // and the tech element was removed from the player div + if (tech && tech.el()) { + // check that the flash object is really ready + Flash.checkReady(tech); + } +}; + +// The SWF isn't always ready when it says it is. Sometimes the API functions still need to be added to the object. +// If it's not ready, we set a timeout to check again shortly. +Flash.checkReady = function (tech) { + // stop worrying if the tech has been disposed + if (!tech.el()) { + return; + } + + // check if API property exists + if (tech.el().vjs_getProperty) { + // tell tech it's ready + tech.triggerReady(); + } else { + // wait longer + this.setTimeout(function () { + Flash['checkReady'](tech); + }, 50); + } +}; + +// Trigger events from the swf on the player +Flash.onEvent = function (swfID, eventName) { + var tech = Dom.getEl(swfID).tech; + tech.trigger(eventName); +}; + +// Log errors from the swf +Flash.onError = function (swfID, err) { + var tech = Dom.getEl(swfID).tech; + + // trigger MEDIA_ERR_SRC_NOT_SUPPORTED + if (err === 'srcnotfound') { + return tech.error(4); + } + + // trigger a custom error + tech.error('FLASH: ' + err); +}; + +// Flash Version Check +Flash.version = function () { + var version = '0,0,0'; + + // IE + try { + version = new _globalWindow2['default'].ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; + + // other browsers + } catch (e) { + try { + if (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin) { + version = (navigator.plugins['Shockwave Flash 2.0'] || navigator.plugins['Shockwave Flash']).description.replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; + } + } catch (err) {} + } + return version.split(','); +}; + +// Flash embedding method. Only used in non-iframe mode +Flash.embed = function (swf, flashVars, params, attributes) { + var code = Flash.getEmbedCode(swf, flashVars, params, attributes); + + // Get element by embedding code and retrieving created element + var obj = Dom.createEl('div', { innerHTML: code }).childNodes[0]; + + return obj; +}; + +Flash.getEmbedCode = function (swf, flashVars, params, attributes) { + var objTag = '<object type="application/x-shockwave-flash" '; + var flashVarsString = ''; + var paramsString = ''; + var attrsString = ''; + + // Convert flash vars to string + if (flashVars) { + Object.getOwnPropertyNames(flashVars).forEach(function (key) { + flashVarsString += key + '=' + flashVars[key] + '&amp;'; + }); + } + + // Add swf, flashVars, and other default params + params = _objectAssign2['default']({ + 'movie': swf, + 'flashvars': flashVarsString, + 'allowScriptAccess': 'always', // Required to talk to swf + 'allowNetworking': 'all' // All should be default, but having security issues. + }, params); + + // Create param tags string + Object.getOwnPropertyNames(params).forEach(function (key) { + paramsString += '<param name="' + key + '" value="' + params[key] + '" />'; + }); + + attributes = _objectAssign2['default']({ + // Add swf to attributes (need both for IE and Others to work) + 'data': swf, + + // Default to 100% width/height + 'width': '100%', + 'height': '100%' + + }, attributes); + + // Create Attributes string + Object.getOwnPropertyNames(attributes).forEach(function (key) { + attrsString += key + '="' + attributes[key] + '" '; + }); + + return '' + objTag + attrsString + '>' + paramsString + '</object>'; +}; + +// Run Flash through the RTMP decorator +_flashRtmp2['default'](Flash); + +_component2['default'].registerComponent('Flash', Flash); +_tech2['default'].registerTech('Flash', Flash); +exports['default'] = Flash; +module.exports = exports['default']; + +},{"../component":67,"../utils/dom.js":132,"../utils/time-ranges.js":140,"../utils/url.js":142,"./flash-rtmp":115,"./tech":119,"global/window":2,"object.assign":45}],117:[function(_dereq_,module,exports){ +/** + * @file html5.js + * HTML5 Media Controller - Wrapper for HTML5 Media API + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _techJs = _dereq_('./tech.js'); + +var _techJs2 = _interopRequireDefault(_techJs); + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsDomJs = _dereq_('../utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsUrlJs = _dereq_('../utils/url.js'); + +var Url = _interopRequireWildcard(_utilsUrlJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsLogJs = _dereq_('../utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +var _utilsMergeOptionsJs = _dereq_('../utils/merge-options.js'); + +var _utilsMergeOptionsJs2 = _interopRequireDefault(_utilsMergeOptionsJs); + +/** + * HTML5 Media Controller - Wrapper for HTML5 Media API + * + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends Tech + * @class Html5 + */ + +var Html5 = (function (_Tech) { + _inherits(Html5, _Tech); + + function Html5(options, ready) { + _classCallCheck(this, Html5); + + _Tech.call(this, options, ready); + + var source = options.source; + + // Set the source if one is provided + // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) + // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source + // anyway so the error gets fired. + if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) { + this.setSource(source); + } else { + this.handleLateInit_(this.el_); + } + + if (this.el_.hasChildNodes()) { + + var nodes = this.el_.childNodes; + var nodesLength = nodes.length; + var removeNodes = []; + + while (nodesLength--) { + var node = nodes[nodesLength]; + var nodeName = node.nodeName.toLowerCase(); + + if (nodeName === 'track') { + if (!this.featuresNativeTextTracks) { + // Empty video tag tracks so the built-in player doesn't use them also. + // This may not be fast enough to stop HTML5 browsers from reading the tags + // so we'll need to turn off any default tracks if we're manually doing + // captions and subtitles. videoElement.textTracks + removeNodes.push(node); + } else { + // store HTMLTrackElement and TextTrack to remote list + this.remoteTextTrackEls().addTrackElement_(node); + this.remoteTextTracks().addTrack_(node.track); + } + } + } + + for (var i = 0; i < removeNodes.length; i++) { + this.el_.removeChild(removeNodes[i]); + } + } + + if (this.featuresNativeTextTracks) { + this.handleTextTrackChange_ = Fn.bind(this, this.handleTextTrackChange); + this.handleTextTrackAdd_ = Fn.bind(this, this.handleTextTrackAdd); + this.handleTextTrackRemove_ = Fn.bind(this, this.handleTextTrackRemove); + this.proxyNativeTextTracks_(); + } + + // Determine if native controls should be used + // Our goal should be to get the custom controls on mobile solid everywhere + // so we can remove this all together. Right now this will block custom + // controls on touch enabled laptops like the Chrome Pixel + if (browser.TOUCH_ENABLED && options.nativeControlsForTouch === true || browser.IS_IPHONE || browser.IS_NATIVE_ANDROID) { + this.setControls(true); + } + + this.triggerReady(); + } + + /* HTML5 Support Testing ---------------------------------------------------- */ + + /* + * Element for testing browser HTML5 video capabilities + * + * @type {Element} + * @constant + * @private + */ + + /** + * Dispose of html5 media element + * + * @method dispose + */ + + Html5.prototype.dispose = function dispose() { + var tt = this.el().textTracks; + var emulatedTt = this.textTracks(); + + // remove native event listeners + if (tt && tt.removeEventListener) { + tt.removeEventListener('change', this.handleTextTrackChange_); + tt.removeEventListener('addtrack', this.handleTextTrackAdd_); + tt.removeEventListener('removetrack', this.handleTextTrackRemove_); + } + + // clearout the emulated text track list. + var i = emulatedTt.length; + + while (i--) { + emulatedTt.removeTrack_(emulatedTt[i]); + } + + Html5.disposeMediaElement(this.el_); + _Tech.prototype.dispose.call(this); + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + Html5.prototype.createEl = function createEl() { + var el = this.options_.tag; + + // Check if this browser supports moving the element into the box. + // On the iPhone video will break if you move the element, + // So we have to create a brand new element. + if (!el || this['movingMediaElementInDOM'] === false) { + + // If the original tag is still there, clone and remove it. + if (el) { + var clone = el.cloneNode(true); + el.parentNode.insertBefore(clone, el); + Html5.disposeMediaElement(el); + el = clone; + } else { + el = _globalDocument2['default'].createElement('video'); + + // determine if native controls should be used + var tagAttributes = this.options_.tag && Dom.getElAttributes(this.options_.tag); + var attributes = _utilsMergeOptionsJs2['default']({}, tagAttributes); + if (!browser.TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { + delete attributes.controls; + } + + Dom.setElAttributes(el, _objectAssign2['default'](attributes, { + id: this.options_.techId, + 'class': 'vjs-tech' + })); + } + } + + // Update specific tag settings, in case they were overridden + var settingsAttrs = ['autoplay', 'preload', 'loop', 'muted']; + for (var i = settingsAttrs.length - 1; i >= 0; i--) { + var attr = settingsAttrs[i]; + var overwriteAttrs = {}; + if (typeof this.options_[attr] !== 'undefined') { + overwriteAttrs[attr] = this.options_[attr]; + } + Dom.setElAttributes(el, overwriteAttrs); + } + + return el; + // jenniisawesome = true; + }; + + // If we're loading the playback object after it has started loading + // or playing the video (often with autoplay on) then the loadstart event + // has already fired and we need to fire it manually because many things + // rely on it. + + Html5.prototype.handleLateInit_ = function handleLateInit_(el) { + var _this = this; + + if (el.networkState === 0 || el.networkState === 3) { + // The video element hasn't started loading the source yet + // or didn't find a source + return; + } + + if (el.readyState === 0) { + var _ret = (function () { + // NetworkState is set synchronously BUT loadstart is fired at the + // end of the current stack, usually before setInterval(fn, 0). + // So at this point we know loadstart may have already fired or is + // about to fire, and either way the player hasn't seen it yet. + // We don't want to fire loadstart prematurely here and cause a + // double loadstart so we'll wait and see if it happens between now + // and the next loop, and fire it if not. + // HOWEVER, we also want to make sure it fires before loadedmetadata + // which could also happen between now and the next loop, so we'll + // watch for that also. + var loadstartFired = false; + var setLoadstartFired = function setLoadstartFired() { + loadstartFired = true; + }; + _this.on('loadstart', setLoadstartFired); + + var triggerLoadstart = function triggerLoadstart() { + // We did miss the original loadstart. Make sure the player + // sees loadstart before loadedmetadata + if (!loadstartFired) { + this.trigger('loadstart'); + } + }; + _this.on('loadedmetadata', triggerLoadstart); + + _this.ready(function () { + this.off('loadstart', setLoadstartFired); + this.off('loadedmetadata', triggerLoadstart); + + if (!loadstartFired) { + // We did miss the original native loadstart. Fire it now. + this.trigger('loadstart'); + } + }); + + return { + v: undefined + }; + })(); + + if (typeof _ret === 'object') return _ret.v; + } + + // From here on we know that loadstart already fired and we missed it. + // The other readyState events aren't as much of a problem if we double + // them, so not going to go to as much trouble as loadstart to prevent + // that unless we find reason to. + var eventsToTrigger = ['loadstart']; + + // loadedmetadata: newly equal to HAVE_METADATA (1) or greater + eventsToTrigger.push('loadedmetadata'); + + // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater + if (el.readyState >= 2) { + eventsToTrigger.push('loadeddata'); + } + + // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater + if (el.readyState >= 3) { + eventsToTrigger.push('canplay'); + } + + // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) + if (el.readyState >= 4) { + eventsToTrigger.push('canplaythrough'); + } + + // We still need to give the player time to add event listeners + this.ready(function () { + eventsToTrigger.forEach(function (type) { + this.trigger(type); + }, this); + }); + }; + + Html5.prototype.proxyNativeTextTracks_ = function proxyNativeTextTracks_() { + var tt = this.el().textTracks; + + if (tt) { + // Add tracks - if player is initialised after DOM loaded, textTracks + // will not trigger addtrack + for (var i = 0; i < tt.length; i++) { + this.textTracks().addTrack_(tt[i]); + } + + if (tt.addEventListener) { + tt.addEventListener('change', this.handleTextTrackChange_); + tt.addEventListener('addtrack', this.handleTextTrackAdd_); + tt.addEventListener('removetrack', this.handleTextTrackRemove_); + } + } + }; + + Html5.prototype.handleTextTrackChange = function handleTextTrackChange(e) { + var tt = this.textTracks(); + this.textTracks().trigger({ + type: 'change', + target: tt, + currentTarget: tt, + srcElement: tt + }); + }; + + Html5.prototype.handleTextTrackAdd = function handleTextTrackAdd(e) { + this.textTracks().addTrack_(e.track); + }; + + Html5.prototype.handleTextTrackRemove = function handleTextTrackRemove(e) { + this.textTracks().removeTrack_(e.track); + }; + + /** + * Play for html5 tech + * + * @method play + */ + + Html5.prototype.play = function play() { + this.el_.play(); + }; + + /** + * Pause for html5 tech + * + * @method pause + */ + + Html5.prototype.pause = function pause() { + this.el_.pause(); + }; + + /** + * Paused for html5 tech + * + * @return {Boolean} + * @method paused + */ + + Html5.prototype.paused = function paused() { + return this.el_.paused; + }; + + /** + * Get current time + * + * @return {Number} + * @method currentTime + */ + + Html5.prototype.currentTime = function currentTime() { + return this.el_.currentTime; + }; + + /** + * Set current time + * + * @param {Number} seconds Current time of video + * @method setCurrentTime + */ + + Html5.prototype.setCurrentTime = function setCurrentTime(seconds) { + try { + this.el_.currentTime = seconds; + } catch (e) { + _utilsLogJs2['default'](e, 'Video is not ready. (Video.js)'); + // this.warning(VideoJS.warnings.videoNotReady); + } + }; + + /** + * Get duration + * + * @return {Number} + * @method duration + */ + + Html5.prototype.duration = function duration() { + return this.el_.duration || 0; + }; + + /** + * Get a TimeRange object that represents the intersection + * of the time ranges for which the user agent has all + * relevant media + * + * @return {TimeRangeObject} + * @method buffered + */ + + Html5.prototype.buffered = function buffered() { + return this.el_.buffered; + }; + + /** + * Get volume level + * + * @return {Number} + * @method volume + */ + + Html5.prototype.volume = function volume() { + return this.el_.volume; + }; + + /** + * Set volume level + * + * @param {Number} percentAsDecimal Volume percent as a decimal + * @method setVolume + */ + + Html5.prototype.setVolume = function setVolume(percentAsDecimal) { + this.el_.volume = percentAsDecimal; + }; + + /** + * Get if muted + * + * @return {Boolean} + * @method muted + */ + + Html5.prototype.muted = function muted() { + return this.el_.muted; + }; + + /** + * Set muted + * + * @param {Boolean} If player is to be muted or note + * @method setMuted + */ + + Html5.prototype.setMuted = function setMuted(muted) { + this.el_.muted = muted; + }; + + /** + * Get player width + * + * @return {Number} + * @method width + */ + + Html5.prototype.width = function width() { + return this.el_.offsetWidth; + }; + + /** + * Get player height + * + * @return {Number} + * @method height + */ + + Html5.prototype.height = function height() { + return this.el_.offsetHeight; + }; + + /** + * Get if there is fullscreen support + * + * @return {Boolean} + * @method supportsFullScreen + */ + + Html5.prototype.supportsFullScreen = function supportsFullScreen() { + if (typeof this.el_.webkitEnterFullScreen === 'function') { + var userAgent = _globalWindow2['default'].navigator.userAgent; + // Seems to be broken in Chromium/Chrome && Safari in Leopard + if (/Android/.test(userAgent) || !/Chrome|Mac OS X 10.5/.test(userAgent)) { + return true; + } + } + return false; + }; + + /** + * Request to enter fullscreen + * + * @method enterFullScreen + */ + + Html5.prototype.enterFullScreen = function enterFullScreen() { + var video = this.el_; + + if ('webkitDisplayingFullscreen' in video) { + this.one('webkitbeginfullscreen', function () { + this.one('webkitendfullscreen', function () { + this.trigger('fullscreenchange', { isFullscreen: false }); + }); + + this.trigger('fullscreenchange', { isFullscreen: true }); + }); + } + + if (video.paused && video.networkState <= video.HAVE_METADATA) { + // attempt to prime the video element for programmatic access + // this isn't necessary on the desktop but shouldn't hurt + this.el_.play(); + + // playing and pausing synchronously during the transition to fullscreen + // can get iOS ~6.1 devices into a play/pause loop + this.setTimeout(function () { + video.pause(); + video.webkitEnterFullScreen(); + }, 0); + } else { + video.webkitEnterFullScreen(); + } + }; + + /** + * Request to exit fullscreen + * + * @method exitFullScreen + */ + + Html5.prototype.exitFullScreen = function exitFullScreen() { + this.el_.webkitExitFullScreen(); + }; + + /** + * Get/set video + * + * @param {Object=} src Source object + * @return {Object} + * @method src + */ + + Html5.prototype.src = function src(_src) { + if (_src === undefined) { + return this.el_.src; + } else { + // Setting src through `src` instead of `setSrc` will be deprecated + this.setSrc(_src); + } + }; + + /** + * Set video + * + * @param {Object} src Source object + * @deprecated + * @method setSrc + */ + + Html5.prototype.setSrc = function setSrc(src) { + this.el_.src = src; + }; + + /** + * Load media into player + * + * @method load + */ + + Html5.prototype.load = function load() { + this.el_.load(); + }; + + /** + * Reset the tech. Removes all sources and calls `load`. + * + * @method reset + */ + + Html5.prototype.reset = function reset() { + Html5.resetMediaElement(this.el_); + }; + + /** + * Get current source + * + * @return {Object} + * @method currentSrc + */ + + Html5.prototype.currentSrc = function currentSrc() { + if (this.currentSource_) { + return this.currentSource_.src; + } else { + return this.el_.currentSrc; + } + }; + + /** + * Get poster + * + * @return {String} + * @method poster + */ + + Html5.prototype.poster = function poster() { + return this.el_.poster; + }; + + /** + * Set poster + * + * @param {String} val URL to poster image + * @method + */ + + Html5.prototype.setPoster = function setPoster(val) { + this.el_.poster = val; + }; + + /** + * Get preload attribute + * + * @return {String} + * @method preload + */ + + Html5.prototype.preload = function preload() { + return this.el_.preload; + }; + + /** + * Set preload attribute + * + * @param {String} val Value for preload attribute + * @method setPreload + */ + + Html5.prototype.setPreload = function setPreload(val) { + this.el_.preload = val; + }; + + /** + * Get autoplay attribute + * + * @return {String} + * @method autoplay + */ + + Html5.prototype.autoplay = function autoplay() { + return this.el_.autoplay; + }; + + /** + * Set autoplay attribute + * + * @param {String} val Value for preload attribute + * @method setAutoplay + */ + + Html5.prototype.setAutoplay = function setAutoplay(val) { + this.el_.autoplay = val; + }; + + /** + * Get controls attribute + * + * @return {String} + * @method controls + */ + + Html5.prototype.controls = function controls() { + return this.el_.controls; + }; + + /** + * Set controls attribute + * + * @param {String} val Value for controls attribute + * @method setControls + */ + + Html5.prototype.setControls = function setControls(val) { + this.el_.controls = !!val; + }; + + /** + * Get loop attribute + * + * @return {String} + * @method loop + */ + + Html5.prototype.loop = function loop() { + return this.el_.loop; + }; + + /** + * Set loop attribute + * + * @param {String} val Value for loop attribute + * @method setLoop + */ + + Html5.prototype.setLoop = function setLoop(val) { + this.el_.loop = val; + }; + + /** + * Get error value + * + * @return {String} + * @method error + */ + + Html5.prototype.error = function error() { + return this.el_.error; + }; + + /** + * Get whether or not the player is in the "seeking" state + * + * @return {Boolean} + * @method seeking + */ + + Html5.prototype.seeking = function seeking() { + return this.el_.seeking; + }; + + /** + * Get a TimeRanges object that represents the + * ranges of the media resource to which it is possible + * for the user agent to seek. + * + * @return {TimeRangeObject} + * @method seekable + */ + + Html5.prototype.seekable = function seekable() { + return this.el_.seekable; + }; + + /** + * Get if video ended + * + * @return {Boolean} + * @method ended + */ + + Html5.prototype.ended = function ended() { + return this.el_.ended; + }; + + /** + * Get the value of the muted content attribute + * This attribute has no dynamic effect, it only + * controls the default state of the element + * + * @return {Boolean} + * @method defaultMuted + */ + + Html5.prototype.defaultMuted = function defaultMuted() { + return this.el_.defaultMuted; + }; + + /** + * Get desired speed at which the media resource is to play + * + * @return {Number} + * @method playbackRate + */ + + Html5.prototype.playbackRate = function playbackRate() { + return this.el_.playbackRate; + }; + + /** + * Returns a TimeRanges object that represents the ranges of the + * media resource that the user agent has played. + * @return {TimeRangeObject} the range of points on the media + * timeline that has been reached through normal playback + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-played + */ + + Html5.prototype.played = function played() { + return this.el_.played; + }; + + /** + * Set desired speed at which the media resource is to play + * + * @param {Number} val Speed at which the media resource is to play + * @method setPlaybackRate + */ + + Html5.prototype.setPlaybackRate = function setPlaybackRate(val) { + this.el_.playbackRate = val; + }; + + /** + * Get the current state of network activity for the element, from + * the list below + * NETWORK_EMPTY (numeric value 0) + * NETWORK_IDLE (numeric value 1) + * NETWORK_LOADING (numeric value 2) + * NETWORK_NO_SOURCE (numeric value 3) + * + * @return {Number} + * @method networkState + */ + + Html5.prototype.networkState = function networkState() { + return this.el_.networkState; + }; + + /** + * Get a value that expresses the current state of the element + * with respect to rendering the current playback position, from + * the codes in the list below + * HAVE_NOTHING (numeric value 0) + * HAVE_METADATA (numeric value 1) + * HAVE_CURRENT_DATA (numeric value 2) + * HAVE_FUTURE_DATA (numeric value 3) + * HAVE_ENOUGH_DATA (numeric value 4) + * + * @return {Number} + * @method readyState + */ + + Html5.prototype.readyState = function readyState() { + return this.el_.readyState; + }; + + /** + * Get width of video + * + * @return {Number} + * @method videoWidth + */ + + Html5.prototype.videoWidth = function videoWidth() { + return this.el_.videoWidth; + }; + + /** + * Get height of video + * + * @return {Number} + * @method videoHeight + */ + + Html5.prototype.videoHeight = function videoHeight() { + return this.el_.videoHeight; + }; + + /** + * Get text tracks + * + * @return {TextTrackList} + * @method textTracks + */ + + Html5.prototype.textTracks = function textTracks() { + return _Tech.prototype.textTracks.call(this); + }; + + /** + * Creates and returns a text track object + * + * @param {String} kind Text track kind (subtitles, captions, descriptions + * chapters and metadata) + * @param {String=} label Label to identify the text track + * @param {String=} language Two letter language abbreviation + * @return {TextTrackObject} + * @method addTextTrack + */ + + Html5.prototype.addTextTrack = function addTextTrack(kind, label, language) { + if (!this['featuresNativeTextTracks']) { + return _Tech.prototype.addTextTrack.call(this, kind, label, language); + } + + return this.el_.addTextTrack(kind, label, language); + }; + + /** + * Creates a remote text track object and returns a html track element + * + * @param {Object} options The object should contain values for + * kind, language, label and src (location of the WebVTT file) + * @return {HTMLTrackElement} + * @method addRemoteTextTrack + */ + + Html5.prototype.addRemoteTextTrack = function addRemoteTextTrack() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + if (!this['featuresNativeTextTracks']) { + return _Tech.prototype.addRemoteTextTrack.call(this, options); + } + + var htmlTrackElement = _globalDocument2['default'].createElement('track'); + + if (options.kind) { + htmlTrackElement.kind = options.kind; + } + if (options.label) { + htmlTrackElement.label = options.label; + } + if (options.language || options.srclang) { + htmlTrackElement.srclang = options.language || options.srclang; + } + if (options['default']) { + htmlTrackElement['default'] = options['default']; + } + if (options.id) { + htmlTrackElement.id = options.id; + } + if (options.src) { + htmlTrackElement.src = options.src; + } + + this.el().appendChild(htmlTrackElement); + + // store HTMLTrackElement and TextTrack to remote list + this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); + this.remoteTextTracks().addTrack_(htmlTrackElement.track); + + return htmlTrackElement; + }; + + /** + * Remove remote text track from TextTrackList object + * + * @param {TextTrackObject} track Texttrack object to remove + * @method removeRemoteTextTrack + */ + + Html5.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { + if (!this['featuresNativeTextTracks']) { + return _Tech.prototype.removeRemoteTextTrack.call(this, track); + } + + var tracks = undefined, + i = undefined; + + var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); + + // remove HTMLTrackElement and TextTrack from remote list + this.remoteTextTrackEls().removeTrackElement_(trackElement); + this.remoteTextTracks().removeTrack_(track); + + tracks = this.$$('track'); + + i = tracks.length; + while (i--) { + if (track === tracks[i] || track === tracks[i].track) { + this.el().removeChild(tracks[i]); + } + } + }; + + return Html5; +})(_techJs2['default']); + +Html5.TEST_VID = _globalDocument2['default'].createElement('video'); +var track = _globalDocument2['default'].createElement('track'); +track.kind = 'captions'; +track.srclang = 'en'; +track.label = 'English'; +Html5.TEST_VID.appendChild(track); + +/* + * Check if HTML5 video is supported by this browser/device + * + * @return {Boolean} + */ +Html5.isSupported = function () { + // IE9 with no Media Player is a LIAR! (#984) + try { + Html5.TEST_VID['volume'] = 0.5; + } catch (e) { + return false; + } + + return !!Html5.TEST_VID.canPlayType; +}; + +// Add Source Handler pattern functions to this tech +_techJs2['default'].withSourceHandlers(Html5); + +/* + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * + * @param {Object} source The source object + * @param {Html5} tech The instance of the HTML5 tech + */ +Html5.nativeSourceHandler = {}; + +/* + * Check if the video element can play the given videotype + * + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +Html5.nativeSourceHandler.canPlayType = function (type) { + // IE9 on Windows 7 without MediaPlayer throws an error here + // https://github.com/videojs/video.js/issues/519 + try { + return Html5.TEST_VID.canPlayType(type); + } catch (e) { + return ''; + } +}; + +/* + * Check if the video element can handle the source natively + * + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +Html5.nativeSourceHandler.canHandleSource = function (source) { + var match, ext; + + // If a type was provided we should rely on that + if (source.type) { + return Html5.nativeSourceHandler.canPlayType(source.type); + } else if (source.src) { + // If no type, fall back to checking 'video/[EXTENSION]' + ext = Url.getFileExtension(source.src); + + return Html5.nativeSourceHandler.canPlayType('video/' + ext); + } + + return ''; +}; + +/* + * Pass the source to the video element + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * + * @param {Object} source The source object + * @param {Html5} tech The instance of the Html5 tech + */ +Html5.nativeSourceHandler.handleSource = function (source, tech) { + tech.setSrc(source.src); +}; + +/* +* Clean up the source handler when disposing the player or switching sources.. +* (no cleanup is needed when supporting the format natively) +*/ +Html5.nativeSourceHandler.dispose = function () {}; + +// Register the native source handler +Html5.registerSourceHandler(Html5.nativeSourceHandler); + +/* + * Check if the volume can be changed in this browser/device. + * Volume cannot be changed in a lot of mobile devices. + * Specifically, it can't be changed from 1 on iOS. + * + * @return {Boolean} + */ +Html5.canControlVolume = function () { + var volume = Html5.TEST_VID.volume; + Html5.TEST_VID.volume = volume / 2 + 0.1; + return volume !== Html5.TEST_VID.volume; +}; + +/* + * Check if playbackRate is supported in this browser/device. + * + * @return {Number} [description] + */ +Html5.canControlPlaybackRate = function () { + var playbackRate = Html5.TEST_VID.playbackRate; + Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; + return playbackRate !== Html5.TEST_VID.playbackRate; +}; + +/* + * Check to see if native text tracks are supported by this browser/device + * + * @return {Boolean} + */ +Html5.supportsNativeTextTracks = function () { + var supportsTextTracks; + + // Figure out native text track support + // If mode is a number, we cannot change it because it'll disappear from view. + // Browsers with numeric modes include IE10 and older (<=2013) samsung android models. + // Firefox isn't playing nice either with modifying the mode + // TODO: Investigate firefox: https://github.com/videojs/video.js/issues/1862 + supportsTextTracks = !!Html5.TEST_VID.textTracks; + if (supportsTextTracks && Html5.TEST_VID.textTracks.length > 0) { + supportsTextTracks = typeof Html5.TEST_VID.textTracks[0]['mode'] !== 'number'; + } + if (supportsTextTracks && browser.IS_FIREFOX) { + supportsTextTracks = false; + } + if (supportsTextTracks && !('onremovetrack' in Html5.TEST_VID.textTracks)) { + supportsTextTracks = false; + } + + return supportsTextTracks; +}; + +/** + * An array of events available on the Html5 tech. + * + * @private + * @type {Array} + */ +Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'volumechange']; + +/* + * Set the tech's volume control support status + * + * @type {Boolean} + */ +Html5.prototype['featuresVolumeControl'] = Html5.canControlVolume(); + +/* + * Set the tech's playbackRate support status + * + * @type {Boolean} + */ +Html5.prototype['featuresPlaybackRate'] = Html5.canControlPlaybackRate(); + +/* + * Set the tech's status on moving the video element. + * In iOS, if you move a video element in the DOM, it breaks video playback. + * + * @type {Boolean} + */ +Html5.prototype['movingMediaElementInDOM'] = !browser.IS_IOS; + +/* + * Set the the tech's fullscreen resize support status. + * HTML video is able to automatically resize when going to fullscreen. + * (No longer appears to be used. Can probably be removed.) + */ +Html5.prototype['featuresFullscreenResize'] = true; + +/* + * Set the tech's progress event support status + * (this disables the manual progress events of the Tech) + */ +Html5.prototype['featuresProgressEvents'] = true; + +/* + * Sets the tech's status on native text track support + * + * @type {Boolean} + */ +Html5.prototype['featuresNativeTextTracks'] = Html5.supportsNativeTextTracks(); + +// HTML5 Feature detection and Device Fixes --------------------------------- // +var canPlayType = undefined; +var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; +var mp4RE = /^video\/mp4/i; + +Html5.patchCanPlayType = function () { + // Android 4.0 and above can play HLS to some extent but it reports being unable to do so + if (browser.ANDROID_VERSION >= 4.0) { + if (!canPlayType) { + canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; + } + + Html5.TEST_VID.constructor.prototype.canPlayType = function (type) { + if (type && mpegurlRE.test(type)) { + return 'maybe'; + } + return canPlayType.call(this, type); + }; + } + + // Override Android 2.2 and less canPlayType method which is broken + if (browser.IS_OLD_ANDROID) { + if (!canPlayType) { + canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; + } + + Html5.TEST_VID.constructor.prototype.canPlayType = function (type) { + if (type && mp4RE.test(type)) { + return 'maybe'; + } + return canPlayType.call(this, type); + }; + } +}; + +Html5.unpatchCanPlayType = function () { + var r = Html5.TEST_VID.constructor.prototype.canPlayType; + Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType; + canPlayType = null; + return r; +}; + +// by default, patch the video element +Html5.patchCanPlayType(); + +Html5.disposeMediaElement = function (el) { + if (!el) { + return; + } + + if (el.parentNode) { + el.parentNode.removeChild(el); + } + + // remove any child track or source nodes to prevent their loading + while (el.hasChildNodes()) { + el.removeChild(el.firstChild); + } + + // remove any src reference. not setting `src=''` because that causes a warning + // in firefox + el.removeAttribute('src'); + + // force the media element to update its loading state by calling load() + // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) + if (typeof el.load === 'function') { + // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) + (function () { + try { + el.load(); + } catch (e) { + // not supported + } + })(); + } +}; + +Html5.resetMediaElement = function (el) { + if (!el) { + return; + } + + var sources = el.querySelectorAll('source'); + var i = sources.length; + while (i--) { + el.removeChild(sources[i]); + } + + // remove any src reference. + // not setting `src=''` because that throws an error + el.removeAttribute('src'); + + if (typeof el.load === 'function') { + // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) + (function () { + try { + el.load(); + } catch (e) {} + })(); + } +}; + +_component2['default'].registerComponent('Html5', Html5); +_techJs2['default'].registerTech('Html5', Html5); +exports['default'] = Html5; +module.exports = exports['default']; + +},{"../component":67,"../utils/browser.js":129,"../utils/dom.js":132,"../utils/fn.js":134,"../utils/log.js":137,"../utils/merge-options.js":138,"../utils/url.js":142,"./tech.js":119,"global/document":1,"global/window":2,"object.assign":45}],118:[function(_dereq_,module,exports){ +/** + * @file loader.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _componentJs = _dereq_('../component.js'); + +var _componentJs2 = _interopRequireDefault(_componentJs); + +var _techJs = _dereq_('./tech.js'); + +var _techJs2 = _interopRequireDefault(_techJs); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _utilsToTitleCaseJs = _dereq_('../utils/to-title-case.js'); + +var _utilsToTitleCaseJs2 = _interopRequireDefault(_utilsToTitleCaseJs); + +/** + * The Media Loader is the component that decides which playback technology to load + * when the player is initialized. + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends Component + * @class MediaLoader + */ + +var MediaLoader = (function (_Component) { + _inherits(MediaLoader, _Component); + + function MediaLoader(player, options, ready) { + _classCallCheck(this, MediaLoader); + + _Component.call(this, player, options, ready); + + // If there are no sources when the player is initialized, + // load the first supported playback technology. + + if (!options.playerOptions['sources'] || options.playerOptions['sources'].length === 0) { + for (var i = 0, j = options.playerOptions['techOrder']; i < j.length; i++) { + var techName = _utilsToTitleCaseJs2['default'](j[i]); + var tech = _techJs2['default'].getTech(techName); + // Support old behavior of techs being registered as components. + // Remove once that deprecated behavior is removed. + if (!techName) { + tech = _componentJs2['default'].getComponent(techName); + } + + // Check if the browser supports this technology + if (tech && tech.isSupported()) { + player.loadTech_(techName); + break; + } + } + } else { + // // Loop through playback technologies (HTML5, Flash) and check for support. + // // Then load the best source. + // // A few assumptions here: + // // All playback technologies respect preload false. + player.src(options.playerOptions['sources']); + } + } + + return MediaLoader; +})(_componentJs2['default']); + +_componentJs2['default'].registerComponent('MediaLoader', MediaLoader); +exports['default'] = MediaLoader; +module.exports = exports['default']; + +},{"../component.js":67,"../utils/to-title-case.js":141,"./tech.js":119,"global/window":2}],119:[function(_dereq_,module,exports){ +/** + * @file tech.js + * Media Technology Controller - Base class for media playback + * technology controllers like Flash and HTML5 + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _tracksHtmlTrackElement = _dereq_('../tracks/html-track-element'); + +var _tracksHtmlTrackElement2 = _interopRequireDefault(_tracksHtmlTrackElement); + +var _tracksHtmlTrackElementList = _dereq_('../tracks/html-track-element-list'); + +var _tracksHtmlTrackElementList2 = _interopRequireDefault(_tracksHtmlTrackElementList); + +var _utilsMergeOptionsJs = _dereq_('../utils/merge-options.js'); + +var _utilsMergeOptionsJs2 = _interopRequireDefault(_utilsMergeOptionsJs); + +var _tracksTextTrack = _dereq_('../tracks/text-track'); + +var _tracksTextTrack2 = _interopRequireDefault(_tracksTextTrack); + +var _tracksTextTrackList = _dereq_('../tracks/text-track-list'); + +var _tracksTextTrackList2 = _interopRequireDefault(_tracksTextTrackList); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsLogJs = _dereq_('../utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _utilsTimeRangesJs = _dereq_('../utils/time-ranges.js'); + +var _utilsBufferJs = _dereq_('../utils/buffer.js'); + +var _mediaErrorJs = _dereq_('../media-error.js'); + +var _mediaErrorJs2 = _interopRequireDefault(_mediaErrorJs); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * Base class for media (HTML5 Video, Flash) controllers + * + * @param {Object=} options Options object + * @param {Function=} ready Ready callback function + * @extends Component + * @class Tech + */ + +var Tech = (function (_Component) { + _inherits(Tech, _Component); + + function Tech() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + var ready = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; + + _classCallCheck(this, Tech); + + // we don't want the tech to report user activity automatically. + // This is done manually in addControlsListeners + options.reportTouchActivity = false; + _Component.call(this, null, options, ready); + + // keep track of whether the current source has played at all to + // implement a very limited played() + this.hasStarted_ = false; + this.on('playing', function () { + this.hasStarted_ = true; + }); + this.on('loadstart', function () { + this.hasStarted_ = false; + }); + + this.textTracks_ = options.textTracks; + + // Manually track progress in cases where the browser/flash player doesn't report it. + if (!this.featuresProgressEvents) { + this.manualProgressOn(); + } + + // Manually track timeupdates in cases where the browser/flash player doesn't report it. + if (!this.featuresTimeupdateEvents) { + this.manualTimeUpdatesOn(); + } + + if (options.nativeCaptions === false || options.nativeTextTracks === false) { + this.featuresNativeTextTracks = false; + } + + if (!this.featuresNativeTextTracks) { + this.on('ready', this.emulateTextTracks); + } + + this.initTextTrackListeners(); + + // Turn on component tap events + this.emitTapEvents(); + } + + /* + * List of associated text tracks + * + * @type {Array} + * @private + */ + + /* Fallbacks for unsupported event types + ================================================================================ */ + // Manually trigger progress events based on changes to the buffered amount + // Many flash players and older HTML5 browsers don't send progress or progress-like events + /** + * Turn on progress events + * + * @method manualProgressOn + */ + + Tech.prototype.manualProgressOn = function manualProgressOn() { + this.on('durationchange', this.onDurationChange); + + this.manualProgress = true; + + // Trigger progress watching when a source begins loading + this.one('ready', this.trackProgress); + }; + + /** + * Turn off progress events + * + * @method manualProgressOff + */ + + Tech.prototype.manualProgressOff = function manualProgressOff() { + this.manualProgress = false; + this.stopTrackingProgress(); + + this.off('durationchange', this.onDurationChange); + }; + + /** + * Track progress + * + * @method trackProgress + */ + + Tech.prototype.trackProgress = function trackProgress() { + this.stopTrackingProgress(); + this.progressInterval = this.setInterval(Fn.bind(this, function () { + // Don't trigger unless buffered amount is greater than last time + + var numBufferedPercent = this.bufferedPercent(); + + if (this.bufferedPercent_ !== numBufferedPercent) { + this.trigger('progress'); + } + + this.bufferedPercent_ = numBufferedPercent; + + if (numBufferedPercent === 1) { + this.stopTrackingProgress(); + } + }), 500); + }; + + /** + * Update duration + * + * @method onDurationChange + */ + + Tech.prototype.onDurationChange = function onDurationChange() { + this.duration_ = this.duration(); + }; + + /** + * Create and get TimeRange object for buffering + * + * @return {TimeRangeObject} + * @method buffered + */ + + Tech.prototype.buffered = function buffered() { + return _utilsTimeRangesJs.createTimeRange(0, 0); + }; + + /** + * Get buffered percent + * + * @return {Number} + * @method bufferedPercent + */ + + Tech.prototype.bufferedPercent = function bufferedPercent() { + return _utilsBufferJs.bufferedPercent(this.buffered(), this.duration_); + }; + + /** + * Stops tracking progress by clearing progress interval + * + * @method stopTrackingProgress + */ + + Tech.prototype.stopTrackingProgress = function stopTrackingProgress() { + this.clearInterval(this.progressInterval); + }; + + /*! Time Tracking -------------------------------------------------------------- */ + /** + * Set event listeners for on play and pause and tracking current time + * + * @method manualTimeUpdatesOn + */ + + Tech.prototype.manualTimeUpdatesOn = function manualTimeUpdatesOn() { + this.manualTimeUpdates = true; + + this.on('play', this.trackCurrentTime); + this.on('pause', this.stopTrackingCurrentTime); + }; + + /** + * Remove event listeners for on play and pause and tracking current time + * + * @method manualTimeUpdatesOff + */ + + Tech.prototype.manualTimeUpdatesOff = function manualTimeUpdatesOff() { + this.manualTimeUpdates = false; + this.stopTrackingCurrentTime(); + this.off('play', this.trackCurrentTime); + this.off('pause', this.stopTrackingCurrentTime); + }; + + /** + * Tracks current time + * + * @method trackCurrentTime + */ + + Tech.prototype.trackCurrentTime = function trackCurrentTime() { + if (this.currentTimeInterval) { + this.stopTrackingCurrentTime(); + } + this.currentTimeInterval = this.setInterval(function () { + this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + }, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 + }; + + /** + * Turn off play progress tracking (when paused or dragging) + * + * @method stopTrackingCurrentTime + */ + + Tech.prototype.stopTrackingCurrentTime = function stopTrackingCurrentTime() { + this.clearInterval(this.currentTimeInterval); + + // #1002 - if the video ends right before the next timeupdate would happen, + // the progress bar won't make it all the way to the end + this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + }; + + /** + * Turn off any manual progress or timeupdate tracking + * + * @method dispose + */ + + Tech.prototype.dispose = function dispose() { + // clear out text tracks because we can't reuse them between techs + var textTracks = this.textTracks(); + + if (textTracks) { + var i = textTracks.length; + while (i--) { + this.removeRemoteTextTrack(textTracks[i]); + } + } + + // Turn off any manual progress or timeupdate tracking + if (this.manualProgress) { + this.manualProgressOff(); + } + + if (this.manualTimeUpdates) { + this.manualTimeUpdatesOff(); + } + + _Component.prototype.dispose.call(this); + }; + + /** + * Reset the tech. Removes all sources and resets readyState. + * + * @method reset + */ + + Tech.prototype.reset = function reset() {}; + + /** + * When invoked without an argument, returns a MediaError object + * representing the current error state of the player or null if + * there is no error. When invoked with an argument, set the current + * error state of the player. + * @param {MediaError=} err Optional an error object + * @return {MediaError} the current error object or null + * @method error + */ + + Tech.prototype.error = function error(err) { + if (err !== undefined) { + if (err instanceof _mediaErrorJs2['default']) { + this.error_ = err; + } else { + this.error_ = new _mediaErrorJs2['default'](err); + } + this.trigger('error'); + } + return this.error_; + }; + + /** + * Return the time ranges that have been played through for the + * current source. This implementation is incomplete. It does not + * track the played time ranges, only whether the source has played + * at all or not. + * @return {TimeRangeObject} a single time range if this video has + * played or an empty set of ranges if not. + * @method played + */ + + Tech.prototype.played = function played() { + if (this.hasStarted_) { + return _utilsTimeRangesJs.createTimeRange(0, 0); + } + return _utilsTimeRangesJs.createTimeRange(); + }; + + /** + * Set current time + * + * @method setCurrentTime + */ + + Tech.prototype.setCurrentTime = function setCurrentTime() { + // improve the accuracy of manual timeupdates + if (this.manualTimeUpdates) { + this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + } + }; + + /** + * Initialize texttrack listeners + * + * @method initTextTrackListeners + */ + + Tech.prototype.initTextTrackListeners = function initTextTrackListeners() { + var textTrackListChanges = Fn.bind(this, function () { + this.trigger('texttrackchange'); + }); + + var tracks = this.textTracks(); + + if (!tracks) return; + + tracks.addEventListener('removetrack', textTrackListChanges); + tracks.addEventListener('addtrack', textTrackListChanges); + + this.on('dispose', Fn.bind(this, function () { + tracks.removeEventListener('removetrack', textTrackListChanges); + tracks.removeEventListener('addtrack', textTrackListChanges); + })); + }; + + /** + * Emulate texttracks + * + * @method emulateTextTracks + */ + + Tech.prototype.emulateTextTracks = function emulateTextTracks() { + var _this = this; + + var tracks = this.textTracks(); + if (!tracks) { + return; + } + + if (!_globalWindow2['default']['WebVTT'] && this.el().parentNode != null) { + (function () { + var script = _globalDocument2['default'].createElement('script'); + script.src = _this.options_['vtt.js'] || 'https://cdn.rawgit.com/gkatsev/vtt.js/vjs-v0.12.1/dist/vtt.min.js'; + script.onload = function () { + _this.trigger('vttjsloaded'); + }; + script.onerror = function () { + _this.trigger('vttjserror'); + }; + _this.on('dispose', function () { + script.onload = null; + script.onerror = null; + }); + _this.el().parentNode.appendChild(script); + _globalWindow2['default']['WebVTT'] = true; + })(); + } + + var updateDisplay = function updateDisplay() { + return _this.trigger('texttrackchange'); + }; + var textTracksChanges = function textTracksChanges() { + updateDisplay(); + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + track.removeEventListener('cuechange', updateDisplay); + if (track.mode === 'showing') { + track.addEventListener('cuechange', updateDisplay); + } + } + }; + + textTracksChanges(); + tracks.addEventListener('change', textTracksChanges); + + this.on('dispose', function () { + tracks.removeEventListener('change', textTracksChanges); + }); + }; + + /* + * Provide default methods for text tracks. + * + * Html5 tech overrides these. + */ + + /** + * Get texttracks + * + * @returns {TextTrackList} + * @method textTracks + */ + + Tech.prototype.textTracks = function textTracks() { + this.textTracks_ = this.textTracks_ || new _tracksTextTrackList2['default'](); + return this.textTracks_; + }; + + /** + * Get remote texttracks + * + * @returns {TextTrackList} + * @method remoteTextTracks + */ + + Tech.prototype.remoteTextTracks = function remoteTextTracks() { + this.remoteTextTracks_ = this.remoteTextTracks_ || new _tracksTextTrackList2['default'](); + return this.remoteTextTracks_; + }; + + /** + * Get remote htmltrackelements + * + * @returns {HTMLTrackElementList} + * @method remoteTextTrackEls + */ + + Tech.prototype.remoteTextTrackEls = function remoteTextTrackEls() { + this.remoteTextTrackEls_ = this.remoteTextTrackEls_ || new _tracksHtmlTrackElementList2['default'](); + return this.remoteTextTrackEls_; + }; + + /** + * Creates and returns a remote text track object + * + * @param {String} kind Text track kind (subtitles, captions, descriptions + * chapters and metadata) + * @param {String=} label Label to identify the text track + * @param {String=} language Two letter language abbreviation + * @return {TextTrackObject} + * @method addTextTrack + */ + + Tech.prototype.addTextTrack = function addTextTrack(kind, label, language) { + if (!kind) { + throw new Error('TextTrack kind is required but was not provided'); + } + + return createTrackHelper(this, kind, label, language); + }; + + /** + * Creates a remote text track object and returns a emulated html track element + * + * @param {Object} options The object should contain values for + * kind, language, label and src (location of the WebVTT file) + * @return {HTMLTrackElement} + * @method addRemoteTextTrack + */ + + Tech.prototype.addRemoteTextTrack = function addRemoteTextTrack(options) { + var track = _utilsMergeOptionsJs2['default'](options, { + tech: this + }); + + var htmlTrackElement = new _tracksHtmlTrackElement2['default'](track); + + // store HTMLTrackElement and TextTrack to remote list + this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); + this.remoteTextTracks().addTrack_(htmlTrackElement.track); + + // must come after remoteTextTracks() + this.textTracks().addTrack_(htmlTrackElement.track); + + return htmlTrackElement; + }; + + /** + * Remove remote texttrack + * + * @param {TextTrackObject} track Texttrack to remove + * @method removeRemoteTextTrack + */ + + Tech.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { + this.textTracks().removeTrack_(track); + + var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); + + // remove HTMLTrackElement and TextTrack from remote list + this.remoteTextTrackEls().removeTrackElement_(trackElement); + this.remoteTextTracks().removeTrack_(track); + }; + + /** + * Provide a default setPoster method for techs + * Poster support for techs should be optional, so we don't want techs to + * break if they don't have a way to set a poster. + * + * @method setPoster + */ + + Tech.prototype.setPoster = function setPoster() {}; + + /* + * Check if the tech can support the given type + * + * The base tech does not support any type, but source handlers might + * overwrite this. + * + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + + Tech.prototype.canPlayType = function canPlayType() { + return ''; + }; + + /* + * Return whether the argument is a Tech or not. + * Can be passed either a Class like `Html5` or a instance like `player.tech_` + * + * @param {Object} component An item to check + * @return {Boolean} Whether it is a tech or not + */ + + Tech.isTech = function isTech(component) { + return component.prototype instanceof Tech || component instanceof Tech || component === Tech; + }; + + /** + * Registers a Tech + * + * @param {String} name Name of the Tech to register + * @param {Object} tech The tech to register + * @static + * @method registerComponent + */ + + Tech.registerTech = function registerTech(name, tech) { + if (!Tech.techs_) { + Tech.techs_ = {}; + } + + if (!Tech.isTech(tech)) { + throw new Error('Tech ' + name + ' must be a Tech'); + } + + Tech.techs_[name] = tech; + return tech; + }; + + /** + * Gets a component by name + * + * @param {String} name Name of the component to get + * @return {Component} + * @static + * @method getComponent + */ + + Tech.getTech = function getTech(name) { + if (Tech.techs_ && Tech.techs_[name]) { + return Tech.techs_[name]; + } + + if (_globalWindow2['default'] && _globalWindow2['default'].videojs && _globalWindow2['default'].videojs[name]) { + _utilsLogJs2['default'].warn('The ' + name + ' tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)'); + return _globalWindow2['default'].videojs[name]; + } + }; + + return Tech; +})(_component2['default']); + +Tech.prototype.textTracks_; + +var createTrackHelper = function createTrackHelper(self, kind, label, language) { + var options = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4]; + + var tracks = self.textTracks(); + + options.kind = kind; + + if (label) { + options.label = label; + } + if (language) { + options.language = language; + } + options.tech = self; + + var track = new _tracksTextTrack2['default'](options); + tracks.addTrack_(track); + + return track; +}; + +Tech.prototype.featuresVolumeControl = true; + +// Resizing plugins using request fullscreen reloads the plugin +Tech.prototype.featuresFullscreenResize = false; +Tech.prototype.featuresPlaybackRate = false; + +// Optional events that we can manually mimic with timers +// currently not triggered by video-js-swf +Tech.prototype.featuresProgressEvents = false; +Tech.prototype.featuresTimeupdateEvents = false; + +Tech.prototype.featuresNativeTextTracks = false; + +/* + * A functional mixin for techs that want to use the Source Handler pattern. + * + * ##### EXAMPLE: + * + * Tech.withSourceHandlers.call(MyTech); + * + */ +Tech.withSourceHandlers = function (_Tech) { + /* + * Register a source handler + * Source handlers are scripts for handling specific formats. + * The source handler pattern is used for adaptive formats (HLS, DASH) that + * manually load video data and feed it into a Source Buffer (Media Source Extensions) + * @param {Function} handler The source handler + * @param {Boolean} first Register it before any existing handlers + */ + _Tech.registerSourceHandler = function (handler, index) { + var handlers = _Tech.sourceHandlers; + + if (!handlers) { + handlers = _Tech.sourceHandlers = []; + } + + if (index === undefined) { + // add to the end of the list + index = handlers.length; + } + + handlers.splice(index, 0, handler); + }; + + /* + * Check if the tech can support the given type + * @param {String} type The mimetype to check + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + _Tech.canPlayType = function (type) { + var handlers = _Tech.sourceHandlers || []; + var can = undefined; + + for (var i = 0; i < handlers.length; i++) { + can = handlers[i].canPlayType(type); + + if (can) { + return can; + } + } + + return ''; + }; + + /* + * Return the first source handler that supports the source + * TODO: Answer question: should 'probably' be prioritized over 'maybe' + * @param {Object} source The source object + * @returns {Object} The first source handler that supports the source + * @returns {null} Null if no source handler is found + */ + _Tech.selectSourceHandler = function (source) { + var handlers = _Tech.sourceHandlers || []; + var can = undefined; + + for (var i = 0; i < handlers.length; i++) { + can = handlers[i].canHandleSource(source); + + if (can) { + return handlers[i]; + } + } + + return null; + }; + + /* + * Check if the tech can support the given source + * @param {Object} srcObj The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + _Tech.canPlaySource = function (srcObj) { + var sh = _Tech.selectSourceHandler(srcObj); + + if (sh) { + return sh.canHandleSource(srcObj); + } + + return ''; + }; + + /* + * When using a source handler, prefer its implementation of + * any function normally provided by the tech. + */ + var deferrable = ['seekable', 'duration']; + + deferrable.forEach(function (fnName) { + var originalFn = this[fnName]; + + if (typeof originalFn !== 'function') { + return; + } + + this[fnName] = function () { + if (this.sourceHandler_ && this.sourceHandler_[fnName]) { + return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); + } + return originalFn.apply(this, arguments); + }; + }, _Tech.prototype); + + /* + * Create a function for setting the source using a source object + * and source handlers. + * Should never be called unless a source handler was found. + * @param {Object} source A source object with src and type keys + * @return {Tech} self + */ + _Tech.prototype.setSource = function (source) { + var sh = _Tech.selectSourceHandler(source); + + if (!sh) { + // Fall back to a native source hander when unsupported sources are + // deliberately set + if (_Tech.nativeSourceHandler) { + sh = _Tech.nativeSourceHandler; + } else { + _utilsLogJs2['default'].error('No source hander found for the current source.'); + } + } + + // Dispose any existing source handler + this.disposeSourceHandler(); + this.off('dispose', this.disposeSourceHandler); + + this.currentSource_ = source; + this.sourceHandler_ = sh.handleSource(source, this); + this.on('dispose', this.disposeSourceHandler); + + return this; + }; + + /* + * Clean up any existing source handler + */ + _Tech.prototype.disposeSourceHandler = function () { + if (this.sourceHandler_ && this.sourceHandler_.dispose) { + this.sourceHandler_.dispose(); + } + }; +}; + +_component2['default'].registerComponent('Tech', Tech); +// Old name for Tech +_component2['default'].registerComponent('MediaTechController', Tech); +Tech.registerTech('Tech', Tech); +exports['default'] = Tech; +module.exports = exports['default']; + +},{"../component":67,"../media-error.js":103,"../tracks/html-track-element":121,"../tracks/html-track-element-list":120,"../tracks/text-track":128,"../tracks/text-track-list":126,"../utils/buffer.js":130,"../utils/fn.js":134,"../utils/log.js":137,"../utils/merge-options.js":138,"../utils/time-ranges.js":140,"global/document":1,"global/window":2}],120:[function(_dereq_,module,exports){ +/** + * @file html-track-element-list.js + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var HtmlTrackElementList = (function () { + function HtmlTrackElementList() { + var trackElements = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; + + _classCallCheck(this, HtmlTrackElementList); + + var list = this; + + if (browser.IS_IE8) { + list = _globalDocument2['default'].createElement('custom'); + + for (var prop in HtmlTrackElementList.prototype) { + if (prop !== 'constructor') { + list[prop] = HtmlTrackElementList.prototype[prop]; + } + } + } + + list.trackElements_ = []; + + Object.defineProperty(list, 'length', { + get: function get() { + return this.trackElements_.length; + } + }); + + for (var i = 0, _length = trackElements.length; i < _length; i++) { + list.addTrackElement_(trackElements[i]); + } + + if (browser.IS_IE8) { + return list; + } + } + + HtmlTrackElementList.prototype.addTrackElement_ = function addTrackElement_(trackElement) { + this.trackElements_.push(trackElement); + }; + + HtmlTrackElementList.prototype.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { + var trackElement_ = undefined; + + for (var i = 0, _length2 = this.trackElements_.length; i < _length2; i++) { + if (track === this.trackElements_[i].track) { + trackElement_ = this.trackElements_[i]; + + break; + } + } + + return trackElement_; + }; + + HtmlTrackElementList.prototype.removeTrackElement_ = function removeTrackElement_(trackElement) { + for (var i = 0, _length3 = this.trackElements_.length; i < _length3; i++) { + if (trackElement === this.trackElements_[i]) { + this.trackElements_.splice(i, 1); + + break; + } + } + }; + + return HtmlTrackElementList; +})(); + +exports['default'] = HtmlTrackElementList; +module.exports = exports['default']; + +},{"../utils/browser.js":129,"global/document":1}],121:[function(_dereq_,module,exports){ +/** + * @file html-track-element.js + */ + +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _eventTarget = _dereq_('../event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _tracksTextTrack = _dereq_('../tracks/text-track'); + +var _tracksTextTrack2 = _interopRequireDefault(_tracksTextTrack); + +var NONE = 0; +var LOADING = 1; +var LOADED = 2; +var ERROR = 3; + +/** + * https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement + * + * interface HTMLTrackElement : HTMLElement { + * attribute DOMString kind; + * attribute DOMString src; + * attribute DOMString srclang; + * attribute DOMString label; + * attribute boolean default; + * + * const unsigned short NONE = 0; + * const unsigned short LOADING = 1; + * const unsigned short LOADED = 2; + * const unsigned short ERROR = 3; + * readonly attribute unsigned short readyState; + * + * readonly attribute TextTrack track; + * }; + * + * @param {Object} options TextTrack configuration + * @class HTMLTrackElement + */ + +var HTMLTrackElement = (function (_EventTarget) { + _inherits(HTMLTrackElement, _EventTarget); + + function HTMLTrackElement() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, HTMLTrackElement); + + _EventTarget.call(this); + + var readyState = undefined, + trackElement = this; + + if (browser.IS_IE8) { + trackElement = _globalDocument2['default'].createElement('custom'); + + for (var prop in HTMLTrackElement.prototype) { + if (prop !== 'constructor') { + trackElement[prop] = HTMLTrackElement.prototype[prop]; + } + } + } + + var track = new _tracksTextTrack2['default'](options); + + trackElement.kind = track.kind; + trackElement.src = track.src; + trackElement.srclang = track.language; + trackElement.label = track.label; + trackElement['default'] = track['default']; + + Object.defineProperty(trackElement, 'readyState', { + get: function get() { + return readyState; + } + }); + + Object.defineProperty(trackElement, 'track', { + get: function get() { + return track; + } + }); + + readyState = NONE; + + track.addEventListener('loadeddata', function () { + readyState = LOADED; + + trackElement.trigger({ + type: 'load', + target: trackElement + }); + }); + + if (browser.IS_IE8) { + return trackElement; + } + } + + return HTMLTrackElement; +})(_eventTarget2['default']); + +HTMLTrackElement.prototype.allowedEvents_ = { + load: 'load' +}; + +HTMLTrackElement.NONE = NONE; +HTMLTrackElement.LOADING = LOADING; +HTMLTrackElement.LOADED = LOADED; +HTMLTrackElement.ERROR = ERROR; + +exports['default'] = HTMLTrackElement; +module.exports = exports['default']; + +},{"../event-target":99,"../tracks/text-track":128,"../utils/browser.js":129,"global/document":1}],122:[function(_dereq_,module,exports){ +/** + * @file text-track-cue-list.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * A List of text track cues as defined in: + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist + * + * interface TextTrackCueList { + * readonly attribute unsigned long length; + * getter TextTrackCue (unsigned long index); + * TextTrackCue? getCueById(DOMString id); + * }; + * + * @param {Array} cues A list of cues to be initialized with + * @class TextTrackCueList + */ + +var TextTrackCueList = (function () { + function TextTrackCueList(cues) { + _classCallCheck(this, TextTrackCueList); + + var list = this; + + if (browser.IS_IE8) { + list = _globalDocument2['default'].createElement('custom'); + + for (var prop in TextTrackCueList.prototype) { + if (prop !== 'constructor') { + list[prop] = TextTrackCueList.prototype[prop]; + } + } + } + + TextTrackCueList.prototype.setCues_.call(list, cues); + + Object.defineProperty(list, 'length', { + get: function get() { + return this.length_; + } + }); + + if (browser.IS_IE8) { + return list; + } + } + + /** + * A setter for cues in this list + * + * @param {Array} cues an array of cues + * @method setCues_ + * @private + */ + + TextTrackCueList.prototype.setCues_ = function setCues_(cues) { + var oldLength = this.length || 0; + var i = 0; + var l = cues.length; + + this.cues_ = cues; + this.length_ = cues.length; + + var defineProp = function defineProp(index) { + if (!('' + index in this)) { + Object.defineProperty(this, '' + index, { + get: function get() { + return this.cues_[index]; + } + }); + } + }; + + if (oldLength < l) { + i = oldLength; + + for (; i < l; i++) { + defineProp.call(this, i); + } + } + }; + + /** + * Get a cue that is currently in the Cue list by id + * + * @param {String} id + * @method getCueById + * @return {Object} a single cue + */ + + TextTrackCueList.prototype.getCueById = function getCueById(id) { + var result = null; + + for (var i = 0, l = this.length; i < l; i++) { + var cue = this[i]; + + if (cue.id === id) { + result = cue; + break; + } + } + + return result; + }; + + return TextTrackCueList; +})(); + +exports['default'] = TextTrackCueList; +module.exports = exports['default']; + +},{"../utils/browser.js":129,"global/document":1}],123:[function(_dereq_,module,exports){ +/** + * @file text-track-display.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _menuMenuJs = _dereq_('../menu/menu.js'); + +var _menuMenuJs2 = _interopRequireDefault(_menuMenuJs); + +var _menuMenuItemJs = _dereq_('../menu/menu-item.js'); + +var _menuMenuItemJs2 = _interopRequireDefault(_menuMenuItemJs); + +var _menuMenuButtonJs = _dereq_('../menu/menu-button.js'); + +var _menuMenuButtonJs2 = _interopRequireDefault(_menuMenuButtonJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var darkGray = '#222'; +var lightGray = '#ccc'; +var fontMap = { + monospace: 'monospace', + sansSerif: 'sans-serif', + serif: 'serif', + monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', + monospaceSerif: '"Courier New", monospace', + proportionalSansSerif: 'sans-serif', + proportionalSerif: 'serif', + casual: '"Comic Sans MS", Impact, fantasy', + script: '"Monotype Corsiva", cursive', + smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' +}; + +/** + * The component for displaying text track cues + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @param {Function=} ready Ready callback function + * @extends Component + * @class TextTrackDisplay + */ + +var TextTrackDisplay = (function (_Component) { + _inherits(TextTrackDisplay, _Component); + + function TextTrackDisplay(player, options, ready) { + _classCallCheck(this, TextTrackDisplay); + + _Component.call(this, player, options, ready); + + player.on('loadstart', Fn.bind(this, this.toggleDisplay)); + player.on('texttrackchange', Fn.bind(this, this.updateDisplay)); + + // This used to be called during player init, but was causing an error + // if a track should show by default and the display hadn't loaded yet. + // Should probably be moved to an external track loader when we support + // tracks that don't need a display. + player.ready(Fn.bind(this, function () { + if (player.tech_ && player.tech_['featuresNativeTextTracks']) { + this.hide(); + return; + } + + player.on('fullscreenchange', Fn.bind(this, this.updateDisplay)); + + var tracks = this.options_.playerOptions['tracks'] || []; + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + this.player_.addRemoteTextTrack(track); + } + })); + } + + /** + * Add cue HTML to display + * + * @param {Number} color Hex number for color, like #f0e + * @param {Number} opacity Value for opacity,0.0 - 1.0 + * @return {RGBAColor} In the form 'rgba(255, 0, 0, 0.3)' + * @method constructColor + */ + + /** + * Toggle display texttracks + * + * @method toggleDisplay + */ + + TextTrackDisplay.prototype.toggleDisplay = function toggleDisplay() { + if (this.player_.tech_ && this.player_.tech_['featuresNativeTextTracks']) { + this.hide(); + } else { + this.show(); + } + }; + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + TextTrackDisplay.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-text-track-display' + }); + }; + + /** + * Clear display texttracks + * + * @method clearDisplay + */ + + TextTrackDisplay.prototype.clearDisplay = function clearDisplay() { + if (typeof _globalWindow2['default']['WebVTT'] === 'function') { + _globalWindow2['default']['WebVTT']['processCues'](_globalWindow2['default'], [], this.el_); + } + }; + + /** + * Update display texttracks + * + * @method updateDisplay + */ + + TextTrackDisplay.prototype.updateDisplay = function updateDisplay() { + var tracks = this.player_.textTracks(); + + this.clearDisplay(); + + if (!tracks) { + return; + } + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + if (track['mode'] === 'showing') { + this.updateForTrack(track); + } + } + }; + + /** + * Add texttrack to texttrack list + * + * @param {TextTrackObject} track Texttrack object to be added to list + * @method updateForTrack + */ + + TextTrackDisplay.prototype.updateForTrack = function updateForTrack(track) { + if (typeof _globalWindow2['default']['WebVTT'] !== 'function' || !track['activeCues']) { + return; + } + + var overrides = this.player_['textTrackSettings'].getValues(); + + var cues = []; + for (var _i = 0; _i < track['activeCues'].length; _i++) { + cues.push(track['activeCues'][_i]); + } + + _globalWindow2['default']['WebVTT']['processCues'](_globalWindow2['default'], track['activeCues'], this.el_); + + var i = cues.length; + while (i--) { + var cue = cues[i]; + if (!cue) { + continue; + } + + var cueDiv = cue.displayState; + if (overrides.color) { + cueDiv.firstChild.style.color = overrides.color; + } + if (overrides.textOpacity) { + tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity)); + } + if (overrides.backgroundColor) { + cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; + } + if (overrides.backgroundOpacity) { + tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity)); + } + if (overrides.windowColor) { + if (overrides.windowOpacity) { + tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity)); + } else { + cueDiv.style.backgroundColor = overrides.windowColor; + } + } + if (overrides.edgeStyle) { + if (overrides.edgeStyle === 'dropshadow') { + cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray; + } else if (overrides.edgeStyle === 'raised') { + cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray; + } else if (overrides.edgeStyle === 'depressed') { + cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray; + } else if (overrides.edgeStyle === 'uniform') { + cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray; + } + } + if (overrides.fontPercent && overrides.fontPercent !== 1) { + var fontSize = _globalWindow2['default'].parseFloat(cueDiv.style.fontSize); + cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; + cueDiv.style.height = 'auto'; + cueDiv.style.top = 'auto'; + cueDiv.style.bottom = '2px'; + } + if (overrides.fontFamily && overrides.fontFamily !== 'default') { + if (overrides.fontFamily === 'small-caps') { + cueDiv.firstChild.style.fontVariant = 'small-caps'; + } else { + cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; + } + } + } + }; + + return TextTrackDisplay; +})(_component2['default']); + +function constructColor(color, opacity) { + return 'rgba(' + + // color looks like "#f0e" + parseInt(color[1] + color[1], 16) + ',' + parseInt(color[2] + color[2], 16) + ',' + parseInt(color[3] + color[3], 16) + ',' + opacity + ')'; +} + +/** + * Try to update style + * Some style changes will throw an error, particularly in IE8. Those should be noops. + * + * @param {Element} el The element to be styles + * @param {CSSProperty} style The CSS property to be styled + * @param {CSSStyle} rule The actual style to be applied to the property + * @method tryUpdateStyle + */ +function tryUpdateStyle(el, style, rule) { + // + try { + el.style[style] = rule; + } catch (e) {} +} + +_component2['default'].registerComponent('TextTrackDisplay', TextTrackDisplay); +exports['default'] = TextTrackDisplay; +module.exports = exports['default']; + +},{"../component":67,"../menu/menu-button.js":104,"../menu/menu-item.js":105,"../menu/menu.js":106,"../utils/fn.js":134,"global/document":1,"global/window":2}],124:[function(_dereq_,module,exports){ +/** + * @file text-track-enums.js + */ + +/** + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode + * + * enum TextTrackMode { "disabled", "hidden", "showing" }; + */ +'use strict'; + +exports.__esModule = true; +var TextTrackMode = { + disabled: 'disabled', + hidden: 'hidden', + showing: 'showing' +}; + +/** + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackkind + * + * enum TextTrackKind { + * "subtitles", + * "captions", + * "descriptions", + * "chapters", + * "metadata" + * }; + */ +var TextTrackKind = { + subtitles: 'subtitles', + captions: 'captions', + descriptions: 'descriptions', + chapters: 'chapters', + metadata: 'metadata' +}; + +/* jshint ignore:start */ +// we ignore jshint here because it does not see +// TextTrackMode or TextTrackKind as defined here somehow... +exports.TextTrackMode = TextTrackMode; +exports.TextTrackKind = TextTrackKind; + +/* jshint ignore:end */ + +},{}],125:[function(_dereq_,module,exports){ +/** + * Utilities for capturing text track state and re-creating tracks + * based on a capture. + * + * @file text-track-list-converter.js + */ + +/** + * Examine a single text track and return a JSON-compatible javascript + * object that represents the text track's state. + * @param track {TextTrackObject} the text track to query + * @return {Object} a serializable javascript representation of the + * @private + */ +'use strict'; + +exports.__esModule = true; +var trackToJson_ = function trackToJson_(track) { + var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { + if (track[prop]) { + acc[prop] = track[prop]; + } + + return acc; + }, { + cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { + return { + startTime: cue.startTime, + endTime: cue.endTime, + text: cue.text, + id: cue.id + }; + }) + }); + + return ret; +}; + +/** + * Examine a tech and return a JSON-compatible javascript array that + * represents the state of all text tracks currently configured. The + * return array is compatible with `jsonToTextTracks`. + * @param tech {tech} the tech object to query + * @return {Array} a serializable javascript representation of the + * @function textTracksToJson + */ +var textTracksToJson = function textTracksToJson(tech) { + + var trackEls = tech.$$('track'); + + var trackObjs = Array.prototype.map.call(trackEls, function (t) { + return t.track; + }); + var tracks = Array.prototype.map.call(trackEls, function (trackEl) { + var json = trackToJson_(trackEl.track); + if (trackEl.src) { + json.src = trackEl.src; + } + return json; + }); + + return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { + return trackObjs.indexOf(track) === -1; + }).map(trackToJson_)); +}; + +/** + * Creates a set of remote text tracks on a tech based on an array of + * javascript text track representations. + * @param json {Array} an array of text track representation objects, + * like those that would be produced by `textTracksToJson` + * @param tech {tech} the tech to create text tracks on + * @function jsonToTextTracks + */ +var jsonToTextTracks = function jsonToTextTracks(json, tech) { + json.forEach(function (track) { + var addedTrack = tech.addRemoteTextTrack(track).track; + if (!track.src && track.cues) { + track.cues.forEach(function (cue) { + return addedTrack.addCue(cue); + }); + } + }); + + return tech.textTracks(); +}; + +exports['default'] = { textTracksToJson: textTracksToJson, jsonToTextTracks: jsonToTextTracks, trackToJson_: trackToJson_ }; +module.exports = exports['default']; + +},{}],126:[function(_dereq_,module,exports){ +/** + * @file text-track-list.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _eventTarget = _dereq_('../event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * A text track list as defined in: + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist + * + * interface TextTrackList : EventTarget { + * readonly attribute unsigned long length; + * getter TextTrack (unsigned long index); + * TextTrack? getTrackById(DOMString id); + * + * attribute EventHandler onchange; + * attribute EventHandler onaddtrack; + * attribute EventHandler onremovetrack; + * }; + * + * @param {Track[]} tracks A list of tracks to initialize the list with + * @extends EventTarget + * @class TextTrackList + */ + +var TextTrackList = (function (_EventTarget) { + _inherits(TextTrackList, _EventTarget); + + function TextTrackList() { + var tracks = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; + + _classCallCheck(this, TextTrackList); + + _EventTarget.call(this); + var list = this; + + if (browser.IS_IE8) { + list = _globalDocument2['default'].createElement('custom'); + + for (var prop in TextTrackList.prototype) { + if (prop !== 'constructor') { + list[prop] = TextTrackList.prototype[prop]; + } + } + } + + list.tracks_ = []; + + Object.defineProperty(list, 'length', { + get: function get() { + return this.tracks_.length; + } + }); + + for (var i = 0; i < tracks.length; i++) { + list.addTrack_(tracks[i]); + } + + if (browser.IS_IE8) { + return list; + } + } + + /** + * change - One or more tracks in the track list have been enabled or disabled. + * addtrack - A track has been added to the track list. + * removetrack - A track has been removed from the track list. + */ + + /** + * Add TextTrack from TextTrackList + * + * @param {TextTrack} track + * @method addTrack_ + * @private + */ + + TextTrackList.prototype.addTrack_ = function addTrack_(track) { + var index = this.tracks_.length; + + if (!('' + index in this)) { + Object.defineProperty(this, index, { + get: function get() { + return this.tracks_[index]; + } + }); + } + + track.addEventListener('modechange', Fn.bind(this, function () { + this.trigger('change'); + })); + + // Do not add duplicate tracks + if (this.tracks_.indexOf(track) === -1) { + this.tracks_.push(track); + this.trigger({ + track: track, + type: 'addtrack' + }); + } + }; + + /** + * Remove TextTrack from TextTrackList + * NOTE: Be mindful of what is passed in as it may be a HTMLTrackElement + * + * @param {TextTrack} rtrack + * @method removeTrack_ + * @private + */ + + TextTrackList.prototype.removeTrack_ = function removeTrack_(rtrack) { + var track = undefined; + + for (var i = 0, l = this.length; i < l; i++) { + if (this[i] === rtrack) { + track = this[i]; + if (track.off) { + track.off(); + } + + this.tracks_.splice(i, 1); + + break; + } + } + + if (!track) { + return; + } + + this.trigger({ + track: track, + type: 'removetrack' + }); + }; + + /** + * Get a TextTrack from TextTrackList by a tracks id + * + * @param {String} id - the id of the track to get + * @method getTrackById + * @return {TextTrack} + * @private + */ + + TextTrackList.prototype.getTrackById = function getTrackById(id) { + var result = null; + + for (var i = 0, l = this.length; i < l; i++) { + var track = this[i]; + + if (track.id === id) { + result = track; + break; + } + } + + return result; + }; + + return TextTrackList; +})(_eventTarget2['default']); + +TextTrackList.prototype.allowedEvents_ = { + change: 'change', + addtrack: 'addtrack', + removetrack: 'removetrack' +}; + +// emulate attribute EventHandler support to allow for feature detection +for (var _event in TextTrackList.prototype.allowedEvents_) { + TextTrackList.prototype['on' + _event] = null; +} + +exports['default'] = TextTrackList; +module.exports = exports['default']; + +},{"../event-target":99,"../utils/browser.js":129,"../utils/fn.js":134,"global/document":1}],127:[function(_dereq_,module,exports){ +/** + * @file text-track-settings.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _component = _dereq_('../component'); + +var _component2 = _interopRequireDefault(_component); + +var _utilsEventsJs = _dereq_('../utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsLogJs = _dereq_('../utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _safeJsonParseTuple = _dereq_('safe-json-parse/tuple'); + +var _safeJsonParseTuple2 = _interopRequireDefault(_safeJsonParseTuple); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +/** + * Manipulate settings of texttracks + * + * @param {Object} player Main Player + * @param {Object=} options Object of option names and values + * @extends Component + * @class TextTrackSettings + */ + +var TextTrackSettings = (function (_Component) { + _inherits(TextTrackSettings, _Component); + + function TextTrackSettings(player, options) { + _classCallCheck(this, TextTrackSettings); + + _Component.call(this, player, options); + this.hide(); + + // Grab `persistTextTrackSettings` from the player options if not passed in child options + if (options.persistTextTrackSettings === undefined) { + this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings; + } + + Events.on(this.$('.vjs-done-button'), 'click', Fn.bind(this, function () { + this.saveSettings(); + this.hide(); + })); + + Events.on(this.$('.vjs-default-button'), 'click', Fn.bind(this, function () { + this.$('.vjs-fg-color > select').selectedIndex = 0; + this.$('.vjs-bg-color > select').selectedIndex = 0; + this.$('.window-color > select').selectedIndex = 0; + this.$('.vjs-text-opacity > select').selectedIndex = 0; + this.$('.vjs-bg-opacity > select').selectedIndex = 0; + this.$('.vjs-window-opacity > select').selectedIndex = 0; + this.$('.vjs-edge-style select').selectedIndex = 0; + this.$('.vjs-font-family select').selectedIndex = 0; + this.$('.vjs-font-percent select').selectedIndex = 2; + this.updateDisplay(); + })); + + Events.on(this.$('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay)); + Events.on(this.$('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay)); + + if (this.options_.persistTextTrackSettings) { + this.restoreSettings(); + } + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + + TextTrackSettings.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-caption-settings vjs-modal-overlay', + innerHTML: captionOptionsMenuTemplate() + }); + }; + + /** + * Get texttrack settings + * Settings are + * .vjs-edge-style + * .vjs-font-family + * .vjs-fg-color + * .vjs-text-opacity + * .vjs-bg-color + * .vjs-bg-opacity + * .window-color + * .vjs-window-opacity + * + * @return {Object} + * @method getValues + */ + + TextTrackSettings.prototype.getValues = function getValues() { + var textEdge = getSelectedOptionValue(this.$('.vjs-edge-style select')); + var fontFamily = getSelectedOptionValue(this.$('.vjs-font-family select')); + var fgColor = getSelectedOptionValue(this.$('.vjs-fg-color > select')); + var textOpacity = getSelectedOptionValue(this.$('.vjs-text-opacity > select')); + var bgColor = getSelectedOptionValue(this.$('.vjs-bg-color > select')); + var bgOpacity = getSelectedOptionValue(this.$('.vjs-bg-opacity > select')); + var windowColor = getSelectedOptionValue(this.$('.window-color > select')); + var windowOpacity = getSelectedOptionValue(this.$('.vjs-window-opacity > select')); + var fontPercent = _globalWindow2['default']['parseFloat'](getSelectedOptionValue(this.$('.vjs-font-percent > select'))); + + var result = { + 'backgroundOpacity': bgOpacity, + 'textOpacity': textOpacity, + 'windowOpacity': windowOpacity, + 'edgeStyle': textEdge, + 'fontFamily': fontFamily, + 'color': fgColor, + 'backgroundColor': bgColor, + 'windowColor': windowColor, + 'fontPercent': fontPercent + }; + for (var _name in result) { + if (result[_name] === '' || result[_name] === 'none' || _name === 'fontPercent' && result[_name] === 1.00) { + delete result[_name]; + } + } + return result; + }; + + /** + * Set texttrack settings + * Settings are + * .vjs-edge-style + * .vjs-font-family + * .vjs-fg-color + * .vjs-text-opacity + * .vjs-bg-color + * .vjs-bg-opacity + * .window-color + * .vjs-window-opacity + * + * @param {Object} values Object with texttrack setting values + * @method setValues + */ + + TextTrackSettings.prototype.setValues = function setValues(values) { + setSelectedOption(this.$('.vjs-edge-style select'), values.edgeStyle); + setSelectedOption(this.$('.vjs-font-family select'), values.fontFamily); + setSelectedOption(this.$('.vjs-fg-color > select'), values.color); + setSelectedOption(this.$('.vjs-text-opacity > select'), values.textOpacity); + setSelectedOption(this.$('.vjs-bg-color > select'), values.backgroundColor); + setSelectedOption(this.$('.vjs-bg-opacity > select'), values.backgroundOpacity); + setSelectedOption(this.$('.window-color > select'), values.windowColor); + setSelectedOption(this.$('.vjs-window-opacity > select'), values.windowOpacity); + + var fontPercent = values.fontPercent; + + if (fontPercent) { + fontPercent = fontPercent.toFixed(2); + } + + setSelectedOption(this.$('.vjs-font-percent > select'), fontPercent); + }; + + /** + * Restore texttrack settings + * + * @method restoreSettings + */ + + TextTrackSettings.prototype.restoreSettings = function restoreSettings() { + var err = undefined, + values = undefined; + + try { + var _safeParseTuple = _safeJsonParseTuple2['default'](_globalWindow2['default'].localStorage.getItem('vjs-text-track-settings')); + + err = _safeParseTuple[0]; + values = _safeParseTuple[1]; + + if (err) { + _utilsLogJs2['default'].error(err); + } + } catch (e) { + _utilsLogJs2['default'].warn(e); + } + + if (values) { + this.setValues(values); + } + }; + + /** + * Save texttrack settings to local storage + * + * @method saveSettings + */ + + TextTrackSettings.prototype.saveSettings = function saveSettings() { + if (!this.options_.persistTextTrackSettings) { + return; + } + + var values = this.getValues(); + try { + if (Object.getOwnPropertyNames(values).length > 0) { + _globalWindow2['default'].localStorage.setItem('vjs-text-track-settings', JSON.stringify(values)); + } else { + _globalWindow2['default'].localStorage.removeItem('vjs-text-track-settings'); + } + } catch (e) { + _utilsLogJs2['default'].warn(e); + } + }; + + /** + * Update display of texttrack settings + * + * @method updateDisplay + */ + + TextTrackSettings.prototype.updateDisplay = function updateDisplay() { + var ttDisplay = this.player_.getChild('textTrackDisplay'); + if (ttDisplay) { + ttDisplay.updateDisplay(); + } + }; + + return TextTrackSettings; +})(_component2['default']); + +_component2['default'].registerComponent('TextTrackSettings', TextTrackSettings); + +function getSelectedOptionValue(target) { + var selectedOption = undefined; + // not all browsers support selectedOptions, so, fallback to options + if (target.selectedOptions) { + selectedOption = target.selectedOptions[0]; + } else if (target.options) { + selectedOption = target.options[target.options.selectedIndex]; + } + + return selectedOption.value; +} + +function setSelectedOption(target, value) { + if (!value) { + return; + } + + var i = undefined; + for (i = 0; i < target.options.length; i++) { + var option = target.options[i]; + if (option.value === value) { + break; + } + } + + target.selectedIndex = i; +} + +function captionOptionsMenuTemplate() { + var template = '<div class="vjs-tracksettings">\n <div class="vjs-tracksettings-colors">\n <div class="vjs-fg-color vjs-tracksetting">\n <label class="vjs-label">Foreground</label>\n <select>\n <option value="">---</option>\n <option value="#FFF">White</option>\n <option value="#000">Black</option>\n <option value="#F00">Red</option>\n <option value="#0F0">Green</option>\n <option value="#00F">Blue</option>\n <option value="#FF0">Yellow</option>\n <option value="#F0F">Magenta</option>\n <option value="#0FF">Cyan</option>\n </select>\n <span class="vjs-text-opacity vjs-opacity">\n <select>\n <option value="">---</option>\n <option value="1">Opaque</option>\n <option value="0.5">Semi-Opaque</option>\n </select>\n </span>\n </div> <!-- vjs-fg-color -->\n <div class="vjs-bg-color vjs-tracksetting">\n <label class="vjs-label">Background</label>\n <select>\n <option value="">---</option>\n <option value="#FFF">White</option>\n <option value="#000">Black</option>\n <option value="#F00">Red</option>\n <option value="#0F0">Green</option>\n <option value="#00F">Blue</option>\n <option value="#FF0">Yellow</option>\n <option value="#F0F">Magenta</option>\n <option value="#0FF">Cyan</option>\n </select>\n <span class="vjs-bg-opacity vjs-opacity">\n <select>\n <option value="">---</option>\n <option value="1">Opaque</option>\n <option value="0.5">Semi-Transparent</option>\n <option value="0">Transparent</option>\n </select>\n </span>\n </div> <!-- vjs-bg-color -->\n <div class="window-color vjs-tracksetting">\n <label class="vjs-label">Window</label>\n <select>\n <option value="">---</option>\n <option value="#FFF">White</option>\n <option value="#000">Black</option>\n <option value="#F00">Red</option>\n <option value="#0F0">Green</option>\n <option value="#00F">Blue</option>\n <option value="#FF0">Yellow</option>\n <option value="#F0F">Magenta</option>\n <option value="#0FF">Cyan</option>\n </select>\n <span class="vjs-window-opacity vjs-opacity">\n <select>\n <option value="">---</option>\n <option value="1">Opaque</option>\n <option value="0.5">Semi-Transparent</option>\n <option value="0">Transparent</option>\n </select>\n </span>\n </div> <!-- vjs-window-color -->\n </div> <!-- vjs-tracksettings -->\n <div class="vjs-tracksettings-font">\n <div class="vjs-font-percent vjs-tracksetting">\n <label class="vjs-label">Font Size</label>\n <select>\n <option value="0.50">50%</option>\n <option value="0.75">75%</option>\n <option value="1.00" selected>100%</option>\n <option value="1.25">125%</option>\n <option value="1.50">150%</option>\n <option value="1.75">175%</option>\n <option value="2.00">200%</option>\n <option value="3.00">300%</option>\n <option value="4.00">400%</option>\n </select>\n </div> <!-- vjs-font-percent -->\n <div class="vjs-edge-style vjs-tracksetting">\n <label class="vjs-label">Text Edge Style</label>\n <select>\n <option value="none">None</option>\n <option value="raised">Raised</option>\n <option value="depressed">Depressed</option>\n <option value="uniform">Uniform</option>\n <option value="dropshadow">Dropshadow</option>\n </select>\n </div> <!-- vjs-edge-style -->\n <div class="vjs-font-family vjs-tracksetting">\n <label class="vjs-label">Font Family</label>\n <select>\n <option value="">Default</option>\n <option value="monospaceSerif">Monospace Serif</option>\n <option value="proportionalSerif">Proportional Serif</option>\n <option value="monospaceSansSerif">Monospace Sans-Serif</option>\n <option value="proportionalSansSerif">Proportional Sans-Serif</option>\n <option value="casual">Casual</option>\n <option value="script">Script</option>\n <option value="small-caps">Small Caps</option>\n </select>\n </div> <!-- vjs-font-family -->\n </div>\n </div>\n <div class="vjs-tracksettings-controls">\n <button class="vjs-default-button">Defaults</button>\n <button class="vjs-done-button">Done</button>\n </div>'; + + return template; +} + +exports['default'] = TextTrackSettings; +module.exports = exports['default']; + +},{"../component":67,"../utils/events.js":133,"../utils/fn.js":134,"../utils/log.js":137,"global/window":2,"safe-json-parse/tuple":54}],128:[function(_dereq_,module,exports){ +/** + * @file text-track.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _textTrackCueList = _dereq_('./text-track-cue-list'); + +var _textTrackCueList2 = _interopRequireDefault(_textTrackCueList); + +var _utilsFnJs = _dereq_('../utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _utilsGuidJs = _dereq_('../utils/guid.js'); + +var Guid = _interopRequireWildcard(_utilsGuidJs); + +var _utilsBrowserJs = _dereq_('../utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _textTrackEnums = _dereq_('./text-track-enums'); + +var TextTrackEnum = _interopRequireWildcard(_textTrackEnums); + +var _utilsLogJs = _dereq_('../utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _eventTarget = _dereq_('../event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _utilsUrlJs = _dereq_('../utils/url.js'); + +var _xhr = _dereq_('xhr'); + +var _xhr2 = _interopRequireDefault(_xhr); + +/** + * takes a webvtt file contents and parses it into cues + * + * @param {String} srcContent webVTT file contents + * @param {Track} track track to addcues to + */ +var parseCues = function parseCues(srcContent, track) { + var parser = new _globalWindow2['default'].WebVTT.Parser(_globalWindow2['default'], _globalWindow2['default'].vttjs, _globalWindow2['default'].WebVTT.StringDecoder()); + + parser.oncue = function (cue) { + track.addCue(cue); + }; + + parser.onparsingerror = function (error) { + _utilsLogJs2['default'].error(error); + }; + + parser.onflush = function () { + track.trigger({ + type: 'loadeddata', + target: track + }); + }; + + parser.parse(srcContent); + parser.flush(); +}; + +/** + * load a track from a specifed url + * + * @param {String} src url to load track from + * @param {Track} track track to addcues to + */ +var loadTrack = function loadTrack(src, track) { + var opts = { + uri: src + }; + var crossOrigin = _utilsUrlJs.isCrossOrigin(src); + + if (crossOrigin) { + opts.cors = crossOrigin; + } + + _xhr2['default'](opts, Fn.bind(this, function (err, response, responseBody) { + if (err) { + return _utilsLogJs2['default'].error(err, response); + } + + track.loaded_ = true; + + // Make sure that vttjs has loaded, otherwise, wait till it finished loading + // NOTE: this is only used for the alt/video.novtt.js build + if (typeof _globalWindow2['default'].WebVTT !== 'function') { + if (track.tech_) { + (function () { + var loadHandler = function loadHandler() { + return parseCues(responseBody, track); + }; + track.tech_.on('vttjsloaded', loadHandler); + track.tech_.on('vttjserror', function () { + _utilsLogJs2['default'].error('vttjs failed to load, stopping trying to process ' + track.src); + track.tech_.off('vttjsloaded', loadHandler); + }); + })(); + } + } else { + parseCues(responseBody, track); + } + })); +}; + +/** + * A single text track as defined in: + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack + * + * interface TextTrack : EventTarget { + * readonly attribute TextTrackKind kind; + * readonly attribute DOMString label; + * readonly attribute DOMString language; + * + * readonly attribute DOMString id; + * readonly attribute DOMString inBandMetadataTrackDispatchType; + * + * attribute TextTrackMode mode; + * + * readonly attribute TextTrackCueList? cues; + * readonly attribute TextTrackCueList? activeCues; + * + * void addCue(TextTrackCue cue); + * void removeCue(TextTrackCue cue); + * + * attribute EventHandler oncuechange; + * }; + * + * @param {Object=} options Object of option names and values + * @extends EventTarget + * @class TextTrack + */ + +var TextTrack = (function (_EventTarget) { + _inherits(TextTrack, _EventTarget); + + function TextTrack() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, TextTrack); + + _EventTarget.call(this); + if (!options.tech) { + throw new Error('A tech was not provided.'); + } + + var tt = this; + + if (browser.IS_IE8) { + tt = _globalDocument2['default'].createElement('custom'); + + for (var prop in TextTrack.prototype) { + if (prop !== 'constructor') { + tt[prop] = TextTrack.prototype[prop]; + } + } + } + + tt.tech_ = options.tech; + + var mode = TextTrackEnum.TextTrackMode[options.mode] || 'disabled'; + var kind = TextTrackEnum.TextTrackKind[options.kind] || 'subtitles'; + var label = options.label || ''; + var language = options.language || options.srclang || ''; + var id = options.id || 'vjs_text_track_' + Guid.newGUID(); + + if (kind === 'metadata' || kind === 'chapters') { + mode = 'hidden'; + } + + tt.cues_ = []; + tt.activeCues_ = []; + + var cues = new _textTrackCueList2['default'](tt.cues_); + var activeCues = new _textTrackCueList2['default'](tt.activeCues_); + var changed = false; + var timeupdateHandler = Fn.bind(tt, function () { + this.activeCues; + if (changed) { + this.trigger('cuechange'); + changed = false; + } + }); + + if (mode !== 'disabled') { + tt.tech_.on('timeupdate', timeupdateHandler); + } + + Object.defineProperty(tt, 'kind', { + get: function get() { + return kind; + }, + set: function set() {} + }); + + Object.defineProperty(tt, 'label', { + get: function get() { + return label; + }, + set: function set() {} + }); + + Object.defineProperty(tt, 'language', { + get: function get() { + return language; + }, + set: function set() {} + }); + + Object.defineProperty(tt, 'id', { + get: function get() { + return id; + }, + set: function set() {} + }); + + Object.defineProperty(tt, 'mode', { + get: function get() { + return mode; + }, + set: function set(newMode) { + if (!TextTrackEnum.TextTrackMode[newMode]) { + return; + } + mode = newMode; + if (mode === 'showing') { + this.tech_.on('timeupdate', timeupdateHandler); + } + this.trigger('modechange'); + } + }); + + Object.defineProperty(tt, 'cues', { + get: function get() { + if (!this.loaded_) { + return null; + } + + return cues; + }, + set: function set() {} + }); + + Object.defineProperty(tt, 'activeCues', { + get: function get() { + if (!this.loaded_) { + return null; + } + + // nothing to do + if (this.cues.length === 0) { + return activeCues; + } + + var ct = this.tech_.currentTime(); + var active = []; + + for (var i = 0, l = this.cues.length; i < l; i++) { + var cue = this.cues[i]; + + if (cue.startTime <= ct && cue.endTime >= ct) { + active.push(cue); + } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { + active.push(cue); + } + } + + changed = false; + + if (active.length !== this.activeCues_.length) { + changed = true; + } else { + for (var i = 0; i < active.length; i++) { + if (this.activeCues_.indexOf(active[i]) === -1) { + changed = true; + } + } + } + + this.activeCues_ = active; + activeCues.setCues_(this.activeCues_); + + return activeCues; + }, + set: function set() {} + }); + + if (options.src) { + tt.src = options.src; + loadTrack(options.src, tt); + } else { + tt.loaded_ = true; + } + + if (browser.IS_IE8) { + return tt; + } + } + + /** + * cuechange - One or more cues in the track have become active or stopped being active. + */ + + /** + * add a cue to the internal list of cues + * + * @param {Object} cue the cue to add to our internal list + * @method addCue + */ + + TextTrack.prototype.addCue = function addCue(cue) { + var tracks = this.tech_.textTracks(); + + if (tracks) { + for (var i = 0; i < tracks.length; i++) { + if (tracks[i] !== this) { + tracks[i].removeCue(cue); + } + } + } + + this.cues_.push(cue); + this.cues.setCues_(this.cues_); + }; + + /** + * remvoe a cue from our internal list + * + * @param {Object} removeCue the cue to remove from our internal list + * @method removeCue + */ + + TextTrack.prototype.removeCue = function removeCue(_removeCue) { + var removed = false; + + for (var i = 0, l = this.cues_.length; i < l; i++) { + var cue = this.cues_[i]; + + if (cue === _removeCue) { + this.cues_.splice(i, 1); + removed = true; + } + } + + if (removed) { + this.cues.setCues_(this.cues_); + } + }; + + return TextTrack; +})(_eventTarget2['default']); + +TextTrack.prototype.allowedEvents_ = { + cuechange: 'cuechange' +}; + +exports['default'] = TextTrack; +module.exports = exports['default']; + +},{"../event-target":99,"../utils/browser.js":129,"../utils/fn.js":134,"../utils/guid.js":136,"../utils/log.js":137,"../utils/url.js":142,"./text-track-cue-list":122,"./text-track-enums":124,"global/document":1,"global/window":2,"xhr":56}],129:[function(_dereq_,module,exports){ +/** + * @file browser.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var USER_AGENT = _globalWindow2['default'].navigator.userAgent; +var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); +var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; + +/* + * Device is an iPhone + * + * @type {Boolean} + * @constant + * @private + */ +var IS_IPAD = /iPad/i.test(USER_AGENT); + +exports.IS_IPAD = IS_IPAD; +// The Facebook app's UIWebView identifies as both an iPhone and iPad, so +// to identify iPhones, we need to exclude iPads. +// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ +var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; +exports.IS_IPHONE = IS_IPHONE; +var IS_IPOD = /iPod/i.test(USER_AGENT); +exports.IS_IPOD = IS_IPOD; +var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; + +exports.IS_IOS = IS_IOS; +var IOS_VERSION = (function () { + var match = USER_AGENT.match(/OS (\d+)_/i); + if (match && match[1]) { + return match[1]; + } +})(); + +exports.IOS_VERSION = IOS_VERSION; +var IS_ANDROID = /Android/i.test(USER_AGENT); +exports.IS_ANDROID = IS_ANDROID; +var ANDROID_VERSION = (function () { + // This matches Android Major.Minor.Patch versions + // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned + var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i), + major, + minor; + + if (!match) { + return null; + } + + major = match[1] && parseFloat(match[1]); + minor = match[2] && parseFloat(match[2]); + + if (major && minor) { + return parseFloat(match[1] + '.' + match[2]); + } else if (major) { + return major; + } else { + return null; + } +})(); +exports.ANDROID_VERSION = ANDROID_VERSION; +// Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser +var IS_OLD_ANDROID = IS_ANDROID && /webkit/i.test(USER_AGENT) && ANDROID_VERSION < 2.3; +exports.IS_OLD_ANDROID = IS_OLD_ANDROID; +var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; + +exports.IS_NATIVE_ANDROID = IS_NATIVE_ANDROID; +var IS_FIREFOX = /Firefox/i.test(USER_AGENT); +exports.IS_FIREFOX = IS_FIREFOX; +var IS_CHROME = /Chrome/i.test(USER_AGENT); +exports.IS_CHROME = IS_CHROME; +var IS_IE8 = /MSIE\s8\.0/.test(USER_AGENT); + +exports.IS_IE8 = IS_IE8; +var TOUCH_ENABLED = !!('ontouchstart' in _globalWindow2['default'] || _globalWindow2['default'].DocumentTouch && _globalDocument2['default'] instanceof _globalWindow2['default'].DocumentTouch); +exports.TOUCH_ENABLED = TOUCH_ENABLED; +var BACKGROUND_SIZE_SUPPORTED = ('backgroundSize' in _globalDocument2['default'].createElement('video').style); +exports.BACKGROUND_SIZE_SUPPORTED = BACKGROUND_SIZE_SUPPORTED; + +},{"global/document":1,"global/window":2}],130:[function(_dereq_,module,exports){ +/** + * @file buffer.js + */ +'use strict'; + +exports.__esModule = true; +exports.bufferedPercent = bufferedPercent; + +var _timeRangesJs = _dereq_('./time-ranges.js'); + +/** + * Compute how much your video has been buffered + * + * @param {Object} Buffered object + * @param {Number} Total duration + * @return {Number} Percent buffered of the total duration + * @private + * @function bufferedPercent + */ + +function bufferedPercent(buffered, duration) { + var bufferedDuration = 0, + start, + end; + + if (!duration) { + return 0; + } + + if (!buffered || !buffered.length) { + buffered = _timeRangesJs.createTimeRange(0, 0); + } + + for (var i = 0; i < buffered.length; i++) { + start = buffered.start(i); + end = buffered.end(i); + + // buffered end can be bigger than duration by a very small fraction + if (end > duration) { + end = duration; + } + + bufferedDuration += end - start; + } + + return bufferedDuration / duration; +} + +},{"./time-ranges.js":140}],131:[function(_dereq_,module,exports){ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _logJs = _dereq_('./log.js'); + +var _logJs2 = _interopRequireDefault(_logJs); + +/** + * Object containing the default behaviors for available handler methods. + * + * @private + * @type {Object} + */ +var defaultBehaviors = { + get: function get(obj, key) { + return obj[key]; + }, + set: function set(obj, key, value) { + obj[key] = value; + return true; + } +}; + +/** + * Expose private objects publicly using a Proxy to log deprecation warnings. + * + * Browsers that do not support Proxy objects will simply return the `target` + * object, so it can be directly exposed. + * + * @param {Object} target The target object. + * @param {Object} messages Messages to display from a Proxy. Only operations + * with an associated message will be proxied. + * @param {String} [messages.get] + * @param {String} [messages.set] + * @return {Object} A Proxy if supported or the `target` argument. + */ + +exports['default'] = function (target) { + var messages = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + if (typeof Proxy === 'function') { + var _ret = (function () { + var handler = {}; + + // Build a handler object based on those keys that have both messages + // and default behaviors. + Object.keys(messages).forEach(function (key) { + if (defaultBehaviors.hasOwnProperty(key)) { + handler[key] = function () { + _logJs2['default'].warn(messages[key]); + return defaultBehaviors[key].apply(this, arguments); + }; + } + }); + + return { + v: new Proxy(target, handler) + }; + })(); + + if (typeof _ret === 'object') return _ret.v; + } + return target; +}; + +module.exports = exports['default']; + +},{"./log.js":137}],132:[function(_dereq_,module,exports){ +/** + * @file dom.js + */ +'use strict'; + +exports.__esModule = true; +exports.getEl = getEl; +exports.createEl = createEl; +exports.textContent = textContent; +exports.insertElFirst = insertElFirst; +exports.getElData = getElData; +exports.hasElData = hasElData; +exports.removeElData = removeElData; +exports.hasElClass = hasElClass; +exports.addElClass = addElClass; +exports.removeElClass = removeElClass; +exports.toggleElClass = toggleElClass; +exports.setElAttributes = setElAttributes; +exports.getElAttributes = getElAttributes; +exports.blockTextSelection = blockTextSelection; +exports.unblockTextSelection = unblockTextSelection; +exports.findElPosition = findElPosition; +exports.getPointerPosition = getPointerPosition; +exports.isEl = isEl; +exports.isTextNode = isTextNode; +exports.emptyEl = emptyEl; +exports.normalizeContent = normalizeContent; +exports.appendContent = appendContent; +exports.insertContent = insertContent; + +var _templateObject = _taggedTemplateLiteralLoose(['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.'], ['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.']); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _taggedTemplateLiteralLoose(strings, raw) { strings.raw = raw; return strings; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _guidJs = _dereq_('./guid.js'); + +var Guid = _interopRequireWildcard(_guidJs); + +var _logJs = _dereq_('./log.js'); + +var _logJs2 = _interopRequireDefault(_logJs); + +var _tsml = _dereq_('tsml'); + +var _tsml2 = _interopRequireDefault(_tsml); + +/** + * Detect if a value is a string with any non-whitespace characters. + * + * @param {String} str + * @return {Boolean} + */ +function isNonBlankString(str) { + return typeof str === 'string' && /\S/.test(str); +} + +/** + * Throws an error if the passed string has whitespace. This is used by + * class methods to be relatively consistent with the classList API. + * + * @param {String} str + * @return {Boolean} + */ +function throwIfWhitespace(str) { + if (/\s/.test(str)) { + throw new Error('class has illegal whitespace characters'); + } +} + +/** + * Produce a regular expression for matching a class name. + * + * @param {String} className + * @return {RegExp} + */ +function classRegExp(className) { + return new RegExp('(^|\\s)' + className + '($|\\s)'); +} + +/** + * Creates functions to query the DOM using a given method. + * + * @function createQuerier + * @private + * @param {String} method + * @return {Function} + */ +function createQuerier(method) { + return function (selector, context) { + if (!isNonBlankString(selector)) { + return _globalDocument2['default'][method](null); + } + if (isNonBlankString(context)) { + context = _globalDocument2['default'].querySelector(context); + } + return (isEl(context) ? context : _globalDocument2['default'])[method](selector); + }; +} + +/** + * Shorthand for document.getElementById() + * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs. + * + * @param {String} id Element ID + * @return {Element} Element with supplied ID + * @function getEl + */ + +function getEl(id) { + if (id.indexOf('#') === 0) { + id = id.slice(1); + } + + return _globalDocument2['default'].getElementById(id); +} + +/** + * Creates an element and applies properties. + * + * @param {String} [tagName='div'] Name of tag to be created. + * @param {Object} [properties={}] Element properties to be applied. + * @param {Object} [attributes={}] Element attributes to be applied. + * @return {Element} + * @function createEl + */ + +function createEl() { + var tagName = arguments.length <= 0 || arguments[0] === undefined ? 'div' : arguments[0]; + var properties = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + var attributes = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + var el = _globalDocument2['default'].createElement(tagName); + + Object.getOwnPropertyNames(properties).forEach(function (propName) { + var val = properties[propName]; + + // See #2176 + // We originally were accepting both properties and attributes in the + // same object, but that doesn't work so well. + if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { + _logJs2['default'].warn(_tsml2['default'](_templateObject, propName, val)); + el.setAttribute(propName, val); + } else { + el[propName] = val; + } + }); + + Object.getOwnPropertyNames(attributes).forEach(function (attrName) { + var val = attributes[attrName]; + el.setAttribute(attrName, attributes[attrName]); + }); + + return el; +} + +/** + * Injects text into an element, replacing any existing contents entirely. + * + * @param {Element} el + * @param {String} text + * @return {Element} + * @function textContent + */ + +function textContent(el, text) { + if (typeof el.textContent === 'undefined') { + el.innerText = text; + } else { + el.textContent = text; + } +} + +/** + * Insert an element as the first child node of another + * + * @param {Element} child Element to insert + * @param {Element} parent Element to insert child into + * @private + * @function insertElFirst + */ + +function insertElFirst(child, parent) { + if (parent.firstChild) { + parent.insertBefore(child, parent.firstChild); + } else { + parent.appendChild(child); + } +} + +/** + * Element Data Store. Allows for binding data to an element without putting it directly on the element. + * Ex. Event listeners are stored here. + * (also from jsninja.com, slightly modified and updated for closure compiler) + * + * @type {Object} + * @private + */ +var elData = {}; + +/* + * Unique attribute name to store an element's guid in + * + * @type {String} + * @constant + * @private + */ +var elIdAttr = 'vdata' + new Date().getTime(); + +/** + * Returns the cache object where data for an element is stored + * + * @param {Element} el Element to store data for. + * @return {Object} + * @function getElData + */ + +function getElData(el) { + var id = el[elIdAttr]; + + if (!id) { + id = el[elIdAttr] = Guid.newGUID(); + } + + if (!elData[id]) { + elData[id] = {}; + } + + return elData[id]; +} + +/** + * Returns whether or not an element has cached data + * + * @param {Element} el A dom element + * @return {Boolean} + * @private + * @function hasElData + */ + +function hasElData(el) { + var id = el[elIdAttr]; + + if (!id) { + return false; + } + + return !!Object.getOwnPropertyNames(elData[id]).length; +} + +/** + * Delete data for the element from the cache and the guid attr from getElementById + * + * @param {Element} el Remove data for an element + * @private + * @function removeElData + */ + +function removeElData(el) { + var id = el[elIdAttr]; + + if (!id) { + return; + } + + // Remove all stored data + delete elData[id]; + + // Remove the elIdAttr property from the DOM node + try { + delete el[elIdAttr]; + } catch (e) { + if (el.removeAttribute) { + el.removeAttribute(elIdAttr); + } else { + // IE doesn't appear to support removeAttribute on the document element + el[elIdAttr] = null; + } + } +} + +/** + * Check if an element has a CSS class + * + * @function hasElClass + * @param {Element} element Element to check + * @param {String} classToCheck Classname to check + */ + +function hasElClass(element, classToCheck) { + if (element.classList) { + return element.classList.contains(classToCheck); + } else { + throwIfWhitespace(classToCheck); + return classRegExp(classToCheck).test(element.className); + } +} + +/** + * Add a CSS class name to an element + * + * @function addElClass + * @param {Element} element Element to add class name to + * @param {String} classToAdd Classname to add + */ + +function addElClass(element, classToAdd) { + if (element.classList) { + element.classList.add(classToAdd); + + // Don't need to `throwIfWhitespace` here because `hasElClass` will do it + // in the case of classList not being supported. + } else if (!hasElClass(element, classToAdd)) { + element.className = (element.className + ' ' + classToAdd).trim(); + } + + return element; +} + +/** + * Remove a CSS class name from an element + * + * @function removeElClass + * @param {Element} element Element to remove from class name + * @param {String} classToRemove Classname to remove + */ + +function removeElClass(element, classToRemove) { + if (element.classList) { + element.classList.remove(classToRemove); + } else { + throwIfWhitespace(classToRemove); + element.className = element.className.split(/\s+/).filter(function (c) { + return c !== classToRemove; + }).join(' '); + } + + return element; +} + +/** + * Adds or removes a CSS class name on an element depending on an optional + * condition or the presence/absence of the class name. + * + * @function toggleElClass + * @param {Element} element + * @param {String} classToToggle + * @param {Boolean|Function} [predicate] + * Can be a function that returns a Boolean. If `true`, the class + * will be added; if `false`, the class will be removed. If not + * given, the class will be added if not present and vice versa. + */ + +function toggleElClass(element, classToToggle, predicate) { + + // This CANNOT use `classList` internally because IE does not support the + // second parameter to the `classList.toggle()` method! Which is fine because + // `classList` will be used by the add/remove functions. + var has = hasElClass(element, classToToggle); + + if (typeof predicate === 'function') { + predicate = predicate(element, classToToggle); + } + + if (typeof predicate !== 'boolean') { + predicate = !has; + } + + // If the necessary class operation matches the current state of the + // element, no action is required. + if (predicate === has) { + return; + } + + if (predicate) { + addElClass(element, classToToggle); + } else { + removeElClass(element, classToToggle); + } + + return element; +} + +/** + * Apply attributes to an HTML element. + * + * @param {Element} el Target element. + * @param {Object=} attributes Element attributes to be applied. + * @private + * @function setElAttributes + */ + +function setElAttributes(el, attributes) { + Object.getOwnPropertyNames(attributes).forEach(function (attrName) { + var attrValue = attributes[attrName]; + + if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { + el.removeAttribute(attrName); + } else { + el.setAttribute(attrName, attrValue === true ? '' : attrValue); + } + }); +} + +/** + * Get an element's attribute values, as defined on the HTML tag + * Attributes are not the same as properties. They're defined on the tag + * or with setAttribute (which shouldn't be used with HTML) + * This will return true or false for boolean attributes. + * + * @param {Element} tag Element from which to get tag attributes + * @return {Object} + * @private + * @function getElAttributes + */ + +function getElAttributes(tag) { + var obj, knownBooleans, attrs, attrName, attrVal; + + obj = {}; + + // known boolean attributes + // we can check for matching boolean properties, but older browsers + // won't know about HTML5 boolean attributes that we still read from + knownBooleans = ',' + 'autoplay,controls,loop,muted,default' + ','; + + if (tag && tag.attributes && tag.attributes.length > 0) { + attrs = tag.attributes; + + for (var i = attrs.length - 1; i >= 0; i--) { + attrName = attrs[i].name; + attrVal = attrs[i].value; + + // check for known booleans + // the matching element property will return a value for typeof + if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { + // the value of an included boolean attribute is typically an empty + // string ('') which would equal false if we just check for a false value. + // we also don't want support bad code like autoplay='false' + attrVal = attrVal !== null ? true : false; + } + + obj[attrName] = attrVal; + } + } + + return obj; +} + +/** + * Attempt to block the ability to select text while dragging controls + * + * @return {Boolean} + * @function blockTextSelection + */ + +function blockTextSelection() { + _globalDocument2['default'].body.focus(); + _globalDocument2['default'].onselectstart = function () { + return false; + }; +} + +/** + * Turn off text selection blocking + * + * @return {Boolean} + * @function unblockTextSelection + */ + +function unblockTextSelection() { + _globalDocument2['default'].onselectstart = function () { + return true; + }; +} + +/** + * Offset Left + * getBoundingClientRect technique from + * John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ + * + * @function findElPosition + * @param {Element} el Element from which to get offset + * @return {Object} + */ + +function findElPosition(el) { + var box = undefined; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0 + }; + } + + var docEl = _globalDocument2['default'].documentElement; + var body = _globalDocument2['default'].body; + + var clientLeft = docEl.clientLeft || body.clientLeft || 0; + var scrollLeft = _globalWindow2['default'].pageXOffset || body.scrollLeft; + var left = box.left + scrollLeft - clientLeft; + + var clientTop = docEl.clientTop || body.clientTop || 0; + var scrollTop = _globalWindow2['default'].pageYOffset || body.scrollTop; + var top = box.top + scrollTop - clientTop; + + // Android sometimes returns slightly off decimal values, so need to round + return { + left: Math.round(left), + top: Math.round(top) + }; +} + +/** + * Get pointer position in element + * Returns an object with x and y coordinates. + * The base on the coordinates are the bottom left of the element. + * + * @function getPointerPosition + * @param {Element} el Element on which to get the pointer position on + * @param {Event} event Event object + * @return {Object} This object will have x and y coordinates corresponding to the mouse position + */ + +function getPointerPosition(el, event) { + var position = {}; + var box = findElPosition(el); + var boxW = el.offsetWidth; + var boxH = el.offsetHeight; + + var boxY = box.top; + var boxX = box.left; + var pageY = event.pageY; + var pageX = event.pageX; + + if (event.changedTouches) { + pageX = event.changedTouches[0].pageX; + pageY = event.changedTouches[0].pageY; + } + + position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH)); + position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); + + return position; +} + +/** + * Determines, via duck typing, whether or not a value is a DOM element. + * + * @function isEl + * @param {Mixed} value + * @return {Boolean} + */ + +function isEl(value) { + return !!value && typeof value === 'object' && value.nodeType === 1; +} + +/** + * Determines, via duck typing, whether or not a value is a text node. + * + * @param {Mixed} value + * @return {Boolean} + */ + +function isTextNode(value) { + return !!value && typeof value === 'object' && value.nodeType === 3; +} + +/** + * Empties the contents of an element. + * + * @function emptyEl + * @param {Element} el + * @return {Element} + */ + +function emptyEl(el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } + return el; +} + +/** + * Normalizes content for eventual insertion into the DOM. + * + * This allows a wide range of content definition methods, but protects + * from falling into the trap of simply writing to `innerHTML`, which is + * an XSS concern. + * + * The content for an element can be passed in multiple types and + * combinations, whose behavior is as follows: + * + * - String + * Normalized into a text node. + * + * - Element, TextNode + * Passed through. + * + * - Array + * A one-dimensional array of strings, elements, nodes, or functions (which + * return single strings, elements, or nodes). + * + * - Function + * If the sole argument, is expected to produce a string, element, + * node, or array. + * + * @function normalizeContent + * @param {String|Element|TextNode|Array|Function} content + * @return {Array} + */ + +function normalizeContent(content) { + + // First, invoke content if it is a function. If it produces an array, + // that needs to happen before normalization. + if (typeof content === 'function') { + content = content(); + } + + // Next up, normalize to an array, so one or many items can be normalized, + // filtered, and returned. + return (Array.isArray(content) ? content : [content]).map(function (value) { + + // First, invoke value if it is a function to produce a new value, + // which will be subsequently normalized to a Node of some kind. + if (typeof value === 'function') { + value = value(); + } + + if (isEl(value) || isTextNode(value)) { + return value; + } + + if (typeof value === 'string' && /\S/.test(value)) { + return _globalDocument2['default'].createTextNode(value); + } + }).filter(function (value) { + return value; + }); +} + +/** + * Normalizes and appends content to an element. + * + * @function appendContent + * @param {Element} el + * @param {String|Element|TextNode|Array|Function} content + * See: `normalizeContent` + * @return {Element} + */ + +function appendContent(el, content) { + normalizeContent(content).forEach(function (node) { + return el.appendChild(node); + }); + return el; +} + +/** + * Normalizes and inserts content into an element; this is identical to + * `appendContent()`, except it empties the element first. + * + * @function insertContent + * @param {Element} el + * @param {String|Element|TextNode|Array|Function} content + * See: `normalizeContent` + * @return {Element} + */ + +function insertContent(el, content) { + return appendContent(emptyEl(el), content); +} + +/** + * Finds a single DOM element matching `selector` within the optional + * `context` of another DOM element (defaulting to `document`). + * + * @function $ + * @param {String} selector + * A valid CSS selector, which will be passed to `querySelector`. + * + * @param {Element|String} [context=document] + * A DOM element within which to query. Can also be a selector + * string in which case the first matching element will be used + * as context. If missing (or no element matches selector), falls + * back to `document`. + * + * @return {Element|null} + */ +var $ = createQuerier('querySelector'); + +exports.$ = $; +/** + * Finds a all DOM elements matching `selector` within the optional + * `context` of another DOM element (defaulting to `document`). + * + * @function $$ + * @param {String} selector + * A valid CSS selector, which will be passed to `querySelectorAll`. + * + * @param {Element|String} [context=document] + * A DOM element within which to query. Can also be a selector + * string in which case the first matching element will be used + * as context. If missing (or no element matches selector), falls + * back to `document`. + * + * @return {NodeList} + */ +var $$ = createQuerier('querySelectorAll'); +exports.$$ = $$; + +},{"./guid.js":136,"./log.js":137,"global/document":1,"global/window":2,"tsml":55}],133:[function(_dereq_,module,exports){ +/** + * @file events.js + * + * Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) + * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) + * This should work very similarly to jQuery's events, however it's based off the book version which isn't as + * robust as jquery's, so there's probably some differences. + */ + +'use strict'; + +exports.__esModule = true; +exports.on = on; +exports.off = off; +exports.trigger = trigger; +exports.one = one; +exports.fixEvent = fixEvent; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +var _domJs = _dereq_('./dom.js'); + +var Dom = _interopRequireWildcard(_domJs); + +var _guidJs = _dereq_('./guid.js'); + +var Guid = _interopRequireWildcard(_guidJs); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +/** + * Add an event listener to element + * It stores the handler function in a separate cache object + * and adds a generic handler to the element's event, + * along with a unique id (guid) to the element. + * + * @param {Element|Object} elem Element or object to bind listeners to + * @param {String|Array} type Type of event to bind to. + * @param {Function} fn Event listener. + * @method on + */ + +function on(elem, type, fn) { + if (Array.isArray(type)) { + return _handleMultipleEvents(on, elem, type, fn); + } + + var data = Dom.getElData(elem); + + // We need a place to store all our handler data + if (!data.handlers) data.handlers = {}; + + if (!data.handlers[type]) data.handlers[type] = []; + + if (!fn.guid) fn.guid = Guid.newGUID(); + + data.handlers[type].push(fn); + + if (!data.dispatcher) { + data.disabled = false; + + data.dispatcher = function (event, hash) { + + if (data.disabled) return; + event = fixEvent(event); + + var handlers = data.handlers[event.type]; + + if (handlers) { + // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. + var handlersCopy = handlers.slice(0); + + for (var m = 0, n = handlersCopy.length; m < n; m++) { + if (event.isImmediatePropagationStopped()) { + break; + } else { + handlersCopy[m].call(elem, event, hash); + } + } + } + }; + } + + if (data.handlers[type].length === 1) { + if (elem.addEventListener) { + elem.addEventListener(type, data.dispatcher, false); + } else if (elem.attachEvent) { + elem.attachEvent('on' + type, data.dispatcher); + } + } +} + +/** + * Removes event listeners from an element + * + * @param {Element|Object} elem Object to remove listeners from + * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. + * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. + * @method off + */ + +function off(elem, type, fn) { + // Don't want to add a cache object through getElData if not needed + if (!Dom.hasElData(elem)) return; + + var data = Dom.getElData(elem); + + // If no events exist, nothing to unbind + if (!data.handlers) { + return; + } + + if (Array.isArray(type)) { + return _handleMultipleEvents(off, elem, type, fn); + } + + // Utility function + var removeType = function removeType(t) { + data.handlers[t] = []; + _cleanUpEvents(elem, t); + }; + + // Are we removing all bound events? + if (!type) { + for (var t in data.handlers) { + removeType(t); + }return; + } + + var handlers = data.handlers[type]; + + // If no handlers exist, nothing to unbind + if (!handlers) return; + + // If no listener was provided, remove all listeners for type + if (!fn) { + removeType(type); + return; + } + + // We're only removing a single handler + if (fn.guid) { + for (var n = 0; n < handlers.length; n++) { + if (handlers[n].guid === fn.guid) { + handlers.splice(n--, 1); + } + } + } + + _cleanUpEvents(elem, type); +} + +/** + * Trigger an event for an element + * + * @param {Element|Object} elem Element to trigger an event on + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute + * @param {Object} [hash] data hash to pass along with the event + * @return {Boolean=} Returned only if default was prevented + * @method trigger + */ + +function trigger(elem, event, hash) { + // Fetches element data and a reference to the parent (for bubbling). + // Don't want to add a data object to cache for every parent, + // so checking hasElData first. + var elemData = Dom.hasElData(elem) ? Dom.getElData(elem) : {}; + var parent = elem.parentNode || elem.ownerDocument; + // type = event.type || event, + // handler; + + // If an event name was passed as a string, creates an event out of it + if (typeof event === 'string') { + event = { type: event, target: elem }; + } + // Normalizes the event properties. + event = fixEvent(event); + + // If the passed element has a dispatcher, executes the established handlers. + if (elemData.dispatcher) { + elemData.dispatcher.call(elem, event, hash); + } + + // Unless explicitly stopped or the event does not bubble (e.g. media events) + // recursively calls this function to bubble the event up the DOM. + if (parent && !event.isPropagationStopped() && event.bubbles === true) { + trigger.call(null, parent, event, hash); + + // If at the top of the DOM, triggers the default action unless disabled. + } else if (!parent && !event.defaultPrevented) { + var targetData = Dom.getElData(event.target); + + // Checks if the target has a default action for this event. + if (event.target[event.type]) { + // Temporarily disables event dispatching on the target as we have already executed the handler. + targetData.disabled = true; + // Executes the default action. + if (typeof event.target[event.type] === 'function') { + event.target[event.type](); + } + // Re-enables event dispatching. + targetData.disabled = false; + } + } + + // Inform the triggerer if the default was prevented by returning false + return !event.defaultPrevented; +} + +/** + * Trigger a listener only once for an event + * + * @param {Element|Object} elem Element or object to + * @param {String|Array} type Name/type of event + * @param {Function} fn Event handler function + * @method one + */ + +function one(elem, type, fn) { + if (Array.isArray(type)) { + return _handleMultipleEvents(one, elem, type, fn); + } + var func = function func() { + off(elem, type, func); + fn.apply(this, arguments); + }; + // copy the guid to the new function so it can removed using the original function's ID + func.guid = fn.guid = fn.guid || Guid.newGUID(); + on(elem, type, func); +} + +/** + * Fix a native event to have standard property values + * + * @param {Object} event Event object to fix + * @return {Object} + * @private + * @method fixEvent + */ + +function fixEvent(event) { + + function returnTrue() { + return true; + } + function returnFalse() { + return false; + } + + // Test if fixing up is needed + // Used to check if !event.stopPropagation instead of isPropagationStopped + // But native events return true for stopPropagation, but don't have + // other expected methods like isPropagationStopped. Seems to be a problem + // with the Javascript Ninja code. So we're just overriding all events now. + if (!event || !event.isPropagationStopped) { + var old = event || _globalWindow2['default'].event; + + event = {}; + // Clone the old object so that we can modify the values event = {}; + // IE8 Doesn't like when you mess with native event properties + // Firefox returns false for event.hasOwnProperty('type') and other props + // which makes copying more difficult. + // TODO: Probably best to create a whitelist of event props + for (var key in old) { + // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y + // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation + // and webkitMovementX/Y + if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { + // Chrome 32+ warns if you try to copy deprecated returnValue, but + // we still want to if preventDefault isn't supported (IE8). + if (!(key === 'returnValue' && old.preventDefault)) { + event[key] = old[key]; + } + } + } + + // The event occurred on this element + if (!event.target) { + event.target = event.srcElement || _globalDocument2['default']; + } + + // Handle which other element the event is related to + if (!event.relatedTarget) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Stop the default browser action + event.preventDefault = function () { + if (old.preventDefault) { + old.preventDefault(); + } + event.returnValue = false; + old.returnValue = false; + event.defaultPrevented = true; + }; + + event.defaultPrevented = false; + + // Stop the event from bubbling + event.stopPropagation = function () { + if (old.stopPropagation) { + old.stopPropagation(); + } + event.cancelBubble = true; + old.cancelBubble = true; + event.isPropagationStopped = returnTrue; + }; + + event.isPropagationStopped = returnFalse; + + // Stop the event from bubbling and executing other handlers + event.stopImmediatePropagation = function () { + if (old.stopImmediatePropagation) { + old.stopImmediatePropagation(); + } + event.isImmediatePropagationStopped = returnTrue; + event.stopPropagation(); + }; + + event.isImmediatePropagationStopped = returnFalse; + + // Handle mouse position + if (event.clientX != null) { + var doc = _globalDocument2['default'].documentElement, + body = _globalDocument2['default'].body; + + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Handle key presses + event.which = event.charCode || event.keyCode; + + // Fix button for mouse clicks: + // 0 == left; 1 == middle; 2 == right + if (event.button != null) { + event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; + } + } + + // Returns fixed-up instance + return event; +} + +/** + * Clean up the listener cache and dispatchers +* + * @param {Element|Object} elem Element to clean up + * @param {String} type Type of event to clean up + * @private + * @method _cleanUpEvents + */ +function _cleanUpEvents(elem, type) { + var data = Dom.getElData(elem); + + // Remove the events of a particular type if there are none left + if (data.handlers[type].length === 0) { + delete data.handlers[type]; + // data.handlers[type] = null; + // Setting to null was causing an error with data.handlers + + // Remove the meta-handler from the element + if (elem.removeEventListener) { + elem.removeEventListener(type, data.dispatcher, false); + } else if (elem.detachEvent) { + elem.detachEvent('on' + type, data.dispatcher); + } + } + + // Remove the events object if there are no types left + if (Object.getOwnPropertyNames(data.handlers).length <= 0) { + delete data.handlers; + delete data.dispatcher; + delete data.disabled; + } + + // Finally remove the element data if there is no data left + if (Object.getOwnPropertyNames(data).length === 0) { + Dom.removeElData(elem); + } +} + +/** + * Loops through an array of event types and calls the requested method for each type. + * + * @param {Function} fn The event method we want to use. + * @param {Element|Object} elem Element or object to bind listeners to + * @param {String} type Type of event to bind to. + * @param {Function} callback Event listener. + * @private + * @function _handleMultipleEvents + */ +function _handleMultipleEvents(fn, elem, types, callback) { + types.forEach(function (type) { + //Call the event method for each one of the types + fn(elem, type, callback); + }); +} + +},{"./dom.js":132,"./guid.js":136,"global/document":1,"global/window":2}],134:[function(_dereq_,module,exports){ +/** + * @file fn.js + */ +'use strict'; + +exports.__esModule = true; + +var _guidJs = _dereq_('./guid.js'); + +/** + * Bind (a.k.a proxy or Context). A simple method for changing the context of a function + * It also stores a unique id on the function so it can be easily removed from events + * + * @param {*} context The object to bind as scope + * @param {Function} fn The function to be bound to a scope + * @param {Number=} uid An optional unique ID for the function to be set + * @return {Function} + * @private + * @method bind + */ +var bind = function bind(context, fn, uid) { + // Make sure the function has a unique ID + if (!fn.guid) { + fn.guid = _guidJs.newGUID(); + } + + // Create the new function that changes the context + var ret = function ret() { + return fn.apply(context, arguments); + }; + + // Allow for the ability to individualize this function + // Needed in the case where multiple objects might share the same prototype + // IF both items add an event listener with the same function, then you try to remove just one + // it will remove both because they both have the same guid. + // when using this, you need to use the bind method when you remove the listener as well. + // currently used in text tracks + ret.guid = uid ? uid + '_' + fn.guid : fn.guid; + + return ret; +}; +exports.bind = bind; + +},{"./guid.js":136}],135:[function(_dereq_,module,exports){ +/** + * @file format-time.js + * + * Format seconds as a time string, H:MM:SS or M:SS + * Supplying a guide (in seconds) will force a number of leading zeros + * to cover the length of the guide + * + * @param {Number} seconds Number of seconds to be turned into a string + * @param {Number} guide Number (in seconds) to model the string after + * @return {String} Time formatted as H:MM:SS or M:SS + * @private + * @function formatTime + */ +'use strict'; + +exports.__esModule = true; +function formatTime(seconds) { + var guide = arguments.length <= 1 || arguments[1] === undefined ? seconds : arguments[1]; + return (function () { + seconds = seconds < 0 ? 0 : seconds; + var s = Math.floor(seconds % 60); + var m = Math.floor(seconds / 60 % 60); + var h = Math.floor(seconds / 3600); + var gm = Math.floor(guide / 60 % 60); + var gh = Math.floor(guide / 3600); + + // handle invalid times + if (isNaN(seconds) || seconds === Infinity) { + // '-' is false for all relational operators (e.g. <, >=) so this setting + // will add the minimum number of fields specified by the guide + h = m = s = '-'; + } + + // Check if we need to show hours + h = h > 0 || gh > 0 ? h + ':' : ''; + + // If hours are showing, we may need to add a leading zero. + // Always show at least one digit of minutes. + m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; + + // Check if leading zero is need for seconds + s = s < 10 ? '0' + s : s; + + return h + m + s; + })(); +} + +exports['default'] = formatTime; +module.exports = exports['default']; + +},{}],136:[function(_dereq_,module,exports){ +/** + * @file guid.js + * + * Unique ID for an element or function + * @type {Number} + * @private + */ +"use strict"; + +exports.__esModule = true; +exports.newGUID = newGUID; +var _guid = 1; + +/** + * Get the next unique ID + * + * @return {String} + * @function newGUID + */ + +function newGUID() { + return _guid++; +} + +},{}],137:[function(_dereq_,module,exports){ +/** + * @file log.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +/** + * Log plain debug messages + */ +var log = function log() { + _logType(null, arguments); +}; + +/** + * Keep a history of log messages + * @type {Array} + */ +log.history = []; + +/** + * Log error messages + */ +log.error = function () { + _logType('error', arguments); +}; + +/** + * Log warning messages + */ +log.warn = function () { + _logType('warn', arguments); +}; + +/** + * Log messages to the console and history based on the type of message + * + * @param {String} type The type of message, or `null` for `log` + * @param {Object} args The args to be passed to the log + * @private + * @method _logType + */ +function _logType(type, args) { + // convert args to an array to get array functions + var argsArray = Array.prototype.slice.call(args); + // if there's no console then don't try to output messages + // they will still be stored in log.history + // Was setting these once outside of this function, but containing them + // in the function makes it easier to test cases where console doesn't exist + var noop = function noop() {}; + + var console = _globalWindow2['default']['console'] || { + 'log': noop, + 'warn': noop, + 'error': noop + }; + + if (type) { + // add the type to the front of the message + argsArray.unshift(type.toUpperCase() + ':'); + } else { + // default to log with no prefix + type = 'log'; + } + + // add to history + log.history.push(argsArray); + + // add console prefix after adding to history + argsArray.unshift('VIDEOJS:'); + + // call appropriate log function + if (console[type].apply) { + console[type].apply(console, argsArray); + } else { + // ie8 doesn't allow error.apply, but it will just join() the array anyway + console[type](argsArray.join(' ')); + } +} + +exports['default'] = log; +module.exports = exports['default']; + +},{"global/window":2}],138:[function(_dereq_,module,exports){ +/** + * @file merge-options.js + */ +'use strict'; + +exports.__esModule = true; +exports['default'] = mergeOptions; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodashCompatObjectMerge = _dereq_('lodash-compat/object/merge'); + +var _lodashCompatObjectMerge2 = _interopRequireDefault(_lodashCompatObjectMerge); + +function isPlain(obj) { + return !!obj && typeof obj === 'object' && obj.toString() === '[object Object]' && obj.constructor === Object; +} + +/** + * Merge customizer. video.js simply overwrites non-simple objects + * (like arrays) instead of attempting to overlay them. + * @see https://lodash.com/docs#merge + */ +var customizer = function customizer(destination, source) { + // If we're not working with a plain object, copy the value as is + // If source is an array, for instance, it will replace destination + if (!isPlain(source)) { + return source; + } + + // If the new value is a plain object but the first object value is not + // we need to create a new object for the first object to merge with. + // This makes it consistent with how merge() works by default + // and also protects from later changes the to first object affecting + // the second object's values. + if (!isPlain(destination)) { + return mergeOptions(source); + } +}; + +/** + * Merge one or more options objects, recursively merging **only** + * plain object properties. Previously `deepMerge`. + * + * @param {...Object} source One or more objects to merge + * @returns {Object} a new object that is the union of all + * provided objects + * @function mergeOptions + */ + +function mergeOptions() { + // contruct the call dynamically to handle the variable number of + // objects to merge + var args = Array.prototype.slice.call(arguments); + + // unshift an empty object into the front of the call as the target + // of the merge + args.unshift({}); + + // customize conflict resolution to match our historical merge behavior + args.push(customizer); + + _lodashCompatObjectMerge2['default'].apply(null, args); + + // return the mutated result object + return args[0]; +} + +module.exports = exports['default']; + +},{"lodash-compat/object/merge":40}],139:[function(_dereq_,module,exports){ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var createStyleElement = function createStyleElement(className) { + var style = _globalDocument2['default'].createElement('style'); + style.className = className; + + return style; +}; + +exports.createStyleElement = createStyleElement; +var setTextContent = function setTextContent(el, content) { + if (el.styleSheet) { + el.styleSheet.cssText = content; + } else { + el.textContent = content; + } +}; +exports.setTextContent = setTextContent; + +},{"global/document":1}],140:[function(_dereq_,module,exports){ +'use strict'; + +exports.__esModule = true; +exports.createTimeRanges = createTimeRanges; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _logJs = _dereq_('./log.js'); + +var _logJs2 = _interopRequireDefault(_logJs); + +/** + * @file time-ranges.js + * + * Should create a fake TimeRange object + * Mimics an HTML5 time range instance, which has functions that + * return the start and end times for a range + * TimeRanges are returned by the buffered() method + * + * @param {(Number|Array)} Start of a single range or an array of ranges + * @param {Number} End of a single range + * @private + * @method createTimeRanges + */ + +function createTimeRanges(start, end) { + if (Array.isArray(start)) { + return createTimeRangesObj(start); + } else if (start === undefined || end === undefined) { + return createTimeRangesObj(); + } + return createTimeRangesObj([[start, end]]); +} + +exports.createTimeRange = createTimeRanges; + +function createTimeRangesObj(ranges) { + if (ranges === undefined || ranges.length === 0) { + return { + length: 0, + start: function start() { + throw new Error('This TimeRanges object is empty'); + }, + end: function end() { + throw new Error('This TimeRanges object is empty'); + } + }; + } + return { + length: ranges.length, + start: getRange.bind(null, 'start', 0, ranges), + end: getRange.bind(null, 'end', 1, ranges) + }; +} + +function getRange(fnName, valueIndex, ranges, rangeIndex) { + if (rangeIndex === undefined) { + _logJs2['default'].warn('DEPRECATED: Function \'' + fnName + '\' on \'TimeRanges\' called without an index argument.'); + rangeIndex = 0; + } + rangeCheck(fnName, rangeIndex, ranges.length - 1); + return ranges[rangeIndex][valueIndex]; +} + +function rangeCheck(fnName, index, maxIndex) { + if (index < 0 || index > maxIndex) { + throw new Error('Failed to execute \'' + fnName + '\' on \'TimeRanges\': The index provided (' + index + ') is greater than or equal to the maximum bound (' + maxIndex + ').'); + } +} + +},{"./log.js":137}],141:[function(_dereq_,module,exports){ +/** + * @file to-title-case.js + * + * Uppercase the first letter of a string + * + * @param {String} string String to be uppercased + * @return {String} + * @private + * @method toTitleCase + */ +"use strict"; + +exports.__esModule = true; +function toTitleCase(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +exports["default"] = toTitleCase; +module.exports = exports["default"]; + +},{}],142:[function(_dereq_,module,exports){ +/** + * @file url.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _globalWindow = _dereq_('global/window'); + +var _globalWindow2 = _interopRequireDefault(_globalWindow); + +/** + * Resolve and parse the elements of a URL + * + * @param {String} url The url to parse + * @return {Object} An object of url details + * @method parseUrl + */ +var parseUrl = function parseUrl(url) { + var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; + + // add the url to an anchor and let the browser parse the URL + var a = _globalDocument2['default'].createElement('a'); + a.href = url; + + // IE8 (and 9?) Fix + // ie8 doesn't parse the URL correctly until the anchor is actually + // added to the body, and an innerHTML is needed to trigger the parsing + var addToBody = a.host === '' && a.protocol !== 'file:'; + var div = undefined; + if (addToBody) { + div = _globalDocument2['default'].createElement('div'); + div.innerHTML = '<a href="' + url + '"></a>'; + a = div.firstChild; + // prevent the div from affecting layout + div.setAttribute('style', 'display:none; position:absolute;'); + _globalDocument2['default'].body.appendChild(div); + } + + // Copy the specific URL properties to a new object + // This is also needed for IE8 because the anchor loses its + // properties when it's removed from the dom + var details = {}; + for (var i = 0; i < props.length; i++) { + details[props[i]] = a[props[i]]; + } + + // IE9 adds the port to the host property unlike everyone else. If + // a port identifier is added for standard ports, strip it. + if (details.protocol === 'http:') { + details.host = details.host.replace(/:80$/, ''); + } + if (details.protocol === 'https:') { + details.host = details.host.replace(/:443$/, ''); + } + + if (addToBody) { + _globalDocument2['default'].body.removeChild(div); + } + + return details; +}; + +exports.parseUrl = parseUrl; +/** + * Get absolute version of relative URL. Used to tell flash correct URL. + * http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue + * + * @param {String} url URL to make absolute + * @return {String} Absolute URL + * @private + * @method getAbsoluteURL + */ +var getAbsoluteURL = function getAbsoluteURL(url) { + // Check if absolute URL + if (!url.match(/^https?:\/\//)) { + // Convert to absolute URL. Flash hosted off-site needs an absolute URL. + var div = _globalDocument2['default'].createElement('div'); + div.innerHTML = '<a href="' + url + '">x</a>'; + url = div.firstChild.href; + } + + return url; +}; + +exports.getAbsoluteURL = getAbsoluteURL; +/** + * Returns the extension of the passed file name. It will return an empty string if you pass an invalid path + * + * @param {String} path The fileName path like '/path/to/file.mp4' + * @returns {String} The extension in lower case or an empty string if no extension could be found. + * @method getFileExtension + */ +var getFileExtension = function getFileExtension(path) { + if (typeof path === 'string') { + var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; + var pathParts = splitPathRe.exec(path); + + if (pathParts) { + return pathParts.pop().toLowerCase(); + } + } + + return ''; +}; + +exports.getFileExtension = getFileExtension; +/** + * Returns whether the url passed is a cross domain request or not. + * + * @param {String} url The url to check + * @return {Boolean} Whether it is a cross domain request or not + * @method isCrossOrigin + */ +var isCrossOrigin = function isCrossOrigin(url) { + var winLoc = _globalWindow2['default'].location; + var urlInfo = parseUrl(url); + + // IE8 protocol relative urls will return ':' for protocol + var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; + + // Check if url is for another domain/origin + // IE8 doesn't know location.origin, so we won't rely on it here + var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; + + return crossOrigin; +}; +exports.isCrossOrigin = isCrossOrigin; + +},{"global/document":1,"global/window":2}],143:[function(_dereq_,module,exports){ +/** + * @file video.js + */ +'use strict'; + +exports.__esModule = true; + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _globalDocument = _dereq_('global/document'); + +var _globalDocument2 = _interopRequireDefault(_globalDocument); + +var _setup = _dereq_('./setup'); + +var setup = _interopRequireWildcard(_setup); + +var _utilsStylesheetJs = _dereq_('./utils/stylesheet.js'); + +var stylesheet = _interopRequireWildcard(_utilsStylesheetJs); + +var _component = _dereq_('./component'); + +var _component2 = _interopRequireDefault(_component); + +var _eventTarget = _dereq_('./event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _utilsEventsJs = _dereq_('./utils/events.js'); + +var Events = _interopRequireWildcard(_utilsEventsJs); + +var _player = _dereq_('./player'); + +var _player2 = _interopRequireDefault(_player); + +var _pluginsJs = _dereq_('./plugins.js'); + +var _pluginsJs2 = _interopRequireDefault(_pluginsJs); + +var _srcJsUtilsMergeOptionsJs = _dereq_('../../src/js/utils/merge-options.js'); + +var _srcJsUtilsMergeOptionsJs2 = _interopRequireDefault(_srcJsUtilsMergeOptionsJs); + +var _utilsFnJs = _dereq_('./utils/fn.js'); + +var Fn = _interopRequireWildcard(_utilsFnJs); + +var _tracksTextTrackJs = _dereq_('./tracks/text-track.js'); + +var _tracksTextTrackJs2 = _interopRequireDefault(_tracksTextTrackJs); + +var _objectAssign = _dereq_('object.assign'); + +var _objectAssign2 = _interopRequireDefault(_objectAssign); + +var _utilsTimeRangesJs = _dereq_('./utils/time-ranges.js'); + +var _utilsFormatTimeJs = _dereq_('./utils/format-time.js'); + +var _utilsFormatTimeJs2 = _interopRequireDefault(_utilsFormatTimeJs); + +var _utilsLogJs = _dereq_('./utils/log.js'); + +var _utilsLogJs2 = _interopRequireDefault(_utilsLogJs); + +var _utilsDomJs = _dereq_('./utils/dom.js'); + +var Dom = _interopRequireWildcard(_utilsDomJs); + +var _utilsBrowserJs = _dereq_('./utils/browser.js'); + +var browser = _interopRequireWildcard(_utilsBrowserJs); + +var _utilsUrlJs = _dereq_('./utils/url.js'); + +var Url = _interopRequireWildcard(_utilsUrlJs); + +var _extendJs = _dereq_('./extend.js'); + +var _extendJs2 = _interopRequireDefault(_extendJs); + +var _lodashCompatObjectMerge = _dereq_('lodash-compat/object/merge'); + +var _lodashCompatObjectMerge2 = _interopRequireDefault(_lodashCompatObjectMerge); + +var _utilsCreateDeprecationProxyJs = _dereq_('./utils/create-deprecation-proxy.js'); + +var _utilsCreateDeprecationProxyJs2 = _interopRequireDefault(_utilsCreateDeprecationProxyJs); + +var _xhr = _dereq_('xhr'); + +var _xhr2 = _interopRequireDefault(_xhr); + +// Include the built-in techs + +var _techTechJs = _dereq_('./tech/tech.js'); + +var _techTechJs2 = _interopRequireDefault(_techTechJs); + +var _techHtml5Js = _dereq_('./tech/html5.js'); + +var _techHtml5Js2 = _interopRequireDefault(_techHtml5Js); + +var _techFlashJs = _dereq_('./tech/flash.js'); + +var _techFlashJs2 = _interopRequireDefault(_techFlashJs); + +// HTML5 Element Shim for IE8 +if (typeof HTMLVideoElement === 'undefined') { + _globalDocument2['default'].createElement('video'); + _globalDocument2['default'].createElement('audio'); + _globalDocument2['default'].createElement('track'); +} + +/** + * Doubles as the main function for users to create a player instance and also + * the main library object. + * The `videojs` function can be used to initialize or retrieve a player. + * ```js + * var myPlayer = videojs('my_video_id'); + * ``` + * + * @param {String|Element} id Video element or video element ID + * @param {Object=} options Optional options object for config/settings + * @param {Function=} ready Optional ready callback + * @return {Player} A player instance + * @mixes videojs + * @method videojs + */ +var videojs = function videojs(id, options, ready) { + var tag = undefined; // Element of ID + + // Allow for element or ID to be passed in + // String ID + if (typeof id === 'string') { + + // Adjust for jQuery ID syntax + if (id.indexOf('#') === 0) { + id = id.slice(1); + } + + // If a player instance has already been created for this ID return it. + if (videojs.getPlayers()[id]) { + + // If options or ready funtion are passed, warn + if (options) { + _utilsLogJs2['default'].warn('Player "' + id + '" is already initialised. Options will not be applied.'); + } + + if (ready) { + videojs.getPlayers()[id].ready(ready); + } + + return videojs.getPlayers()[id]; + + // Otherwise get element for ID + } else { + tag = Dom.getEl(id); + } + + // ID is a media element + } else { + tag = id; + } + + // Check for a useable element + if (!tag || !tag.nodeName) { + // re: nodeName, could be a box div also + throw new TypeError('The element or ID supplied is not valid. (videojs)'); // Returns + } + + // Element may have a player attr referring to an already created player instance. + // If not, set up a new player and return the instance. + return tag['player'] || _player2['default'].players[tag.playerId] || new _player2['default'](tag, options, ready); +}; + +// Add default styles +var style = Dom.$('.vjs-styles-defaults'); +if (!style) { + style = stylesheet.createStyleElement('vjs-styles-defaults'); + var head = Dom.$('head'); + head.insertBefore(style, head.firstChild); + stylesheet.setTextContent(style, '\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid {\n padding-top: 56.25%\n }\n '); +} + +// Run Auto-load players +// You have to wait at least once in case this script is loaded after your video in the DOM (weird behavior only with minified version) +setup.autoSetupTimeout(1, videojs); + +/* + * Current software version (semver) + * + * @type {String} + */ +videojs.VERSION = '5.8.8'; + +/** + * The global options object. These are the settings that take effect + * if no overrides are specified when the player is created. + * + * ```js + * videojs.options.autoplay = true + * // -> all players will autoplay by default + * ``` + * + * @type {Object} + */ +videojs.options = _player2['default'].prototype.options_; + +/** + * Get an object with the currently created players, keyed by player ID + * + * @return {Object} The created players + * @mixes videojs + * @method getPlayers + */ +videojs.getPlayers = function () { + return _player2['default'].players; +}; + +/** + * For backward compatibility, expose players object. + * + * @deprecated + * @memberOf videojs + * @property {Object|Proxy} players + */ +videojs.players = _utilsCreateDeprecationProxyJs2['default'](_player2['default'].players, { + get: 'Access to videojs.players is deprecated; use videojs.getPlayers instead', + set: 'Modification of videojs.players is deprecated' +}); + +/** + * Get a component class object by name + * ```js + * var VjsButton = videojs.getComponent('Button'); + * // Create a new instance of the component + * var myButton = new VjsButton(myPlayer); + * ``` + * + * @return {Component} Component identified by name + * @mixes videojs + * @method getComponent + */ +videojs.getComponent = _component2['default'].getComponent; + +/** + * Register a component so it can referred to by name + * Used when adding to other + * components, either through addChild + * `component.addChild('myComponent')` + * or through default children options + * `{ children: ['myComponent'] }`. + * ```js + * // Get a component to subclass + * var VjsButton = videojs.getComponent('Button'); + * // Subclass the component (see 'extend' doc for more info) + * var MySpecialButton = videojs.extend(VjsButton, {}); + * // Register the new component + * VjsButton.registerComponent('MySepcialButton', MySepcialButton); + * // (optionally) add the new component as a default player child + * myPlayer.addChild('MySepcialButton'); + * ``` + * NOTE: You could also just initialize the component before adding. + * `component.addChild(new MyComponent());` + * + * @param {String} The class name of the component + * @param {Component} The component class + * @return {Component} The newly registered component + * @mixes videojs + * @method registerComponent + */ +videojs.registerComponent = function (name, comp) { + if (_techTechJs2['default'].isTech(comp)) { + _utilsLogJs2['default'].warn('The ' + name + ' tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)'); + } + + _component2['default'].registerComponent.call(_component2['default'], name, comp); +}; + +/** + * Get a Tech class object by name + * ```js + * var Html5 = videojs.getTech('Html5'); + * // Create a new instance of the component + * var html5 = new Html5(options); + * ``` + * + * @return {Tech} Tech identified by name + * @mixes videojs + * @method getComponent + */ +videojs.getTech = _techTechJs2['default'].getTech; + +/** + * Register a Tech so it can referred to by name. + * This is used in the tech order for the player. + * + * ```js + * // get the Html5 Tech + * var Html5 = videojs.getTech('Html5'); + * var MyTech = videojs.extend(Html5, {}); + * // Register the new Tech + * VjsButton.registerTech('Tech', MyTech); + * var player = videojs('myplayer', { + * techOrder: ['myTech', 'html5'] + * }); + * ``` + * + * @param {String} The class name of the tech + * @param {Tech} The tech class + * @return {Tech} The newly registered Tech + * @mixes videojs + * @method registerTech + */ +videojs.registerTech = _techTechJs2['default'].registerTech; + +/** + * A suite of browser and device tests + * + * @type {Object} + * @private + */ +videojs.browser = browser; + +/** + * Whether or not the browser supports touch events. Included for backward + * compatibility with 4.x, but deprecated. Use `videojs.browser.TOUCH_ENABLED` + * instead going forward. + * + * @deprecated + * @type {Boolean} + */ +videojs.TOUCH_ENABLED = browser.TOUCH_ENABLED; + +/** + * Subclass an existing class + * Mimics ES6 subclassing with the `extend` keyword + * ```js + * // Create a basic javascript 'class' + * function MyClass(name){ + * // Set a property at initialization + * this.myName = name; + * } + * // Create an instance method + * MyClass.prototype.sayMyName = function(){ + * alert(this.myName); + * }; + * // Subclass the exisitng class and change the name + * // when initializing + * var MySubClass = videojs.extend(MyClass, { + * constructor: function(name) { + * // Call the super class constructor for the subclass + * MyClass.call(this, name) + * } + * }); + * // Create an instance of the new sub class + * var myInstance = new MySubClass('John'); + * myInstance.sayMyName(); // -> should alert "John" + * ``` + * + * @param {Function} The Class to subclass + * @param {Object} An object including instace methods for the new class + * Optionally including a `constructor` function + * @return {Function} The newly created subclass + * @mixes videojs + * @method extend + */ +videojs.extend = _extendJs2['default']; + +/** + * Merge two options objects recursively + * Performs a deep merge like lodash.merge but **only merges plain objects** + * (not arrays, elements, anything else) + * Other values will be copied directly from the second object. + * ```js + * var defaultOptions = { + * foo: true, + * bar: { + * a: true, + * b: [1,2,3] + * } + * }; + * var newOptions = { + * foo: false, + * bar: { + * b: [4,5,6] + * } + * }; + * var result = videojs.mergeOptions(defaultOptions, newOptions); + * // result.foo = false; + * // result.bar.a = true; + * // result.bar.b = [4,5,6]; + * ``` + * + * @param {Object} defaults The options object whose values will be overriden + * @param {Object} overrides The options object with values to override the first + * @param {Object} etc Any number of additional options objects + * + * @return {Object} a new object with the merged values + * @mixes videojs + * @method mergeOptions + */ +videojs.mergeOptions = _srcJsUtilsMergeOptionsJs2['default']; + +/** + * Change the context (this) of a function + * + * videojs.bind(newContext, function(){ + * this === newContext + * }); + * + * NOTE: as of v5.0 we require an ES5 shim, so you should use the native + * `function(){}.bind(newContext);` instead of this. + * + * @param {*} context The object to bind as scope + * @param {Function} fn The function to be bound to a scope + * @param {Number=} uid An optional unique ID for the function to be set + * @return {Function} + */ +videojs.bind = Fn.bind; + +/** + * Create a Video.js player plugin + * Plugins are only initialized when options for the plugin are included + * in the player options, or the plugin function on the player instance is + * called. + * **See the plugin guide in the docs for a more detailed example** + * ```js + * // Make a plugin that alerts when the player plays + * videojs.plugin('myPlugin', function(myPluginOptions) { + * myPluginOptions = myPluginOptions || {}; + * + * var player = this; + * var alertText = myPluginOptions.text || 'Player is playing!' + * + * player.on('play', function(){ + * alert(alertText); + * }); + * }); + * // USAGE EXAMPLES + * // EXAMPLE 1: New player with plugin options, call plugin immediately + * var player1 = videojs('idOne', { + * myPlugin: { + * text: 'Custom text!' + * } + * }); + * // Click play + * // --> Should alert 'Custom text!' + * // EXAMPLE 3: New player, initialize plugin later + * var player3 = videojs('idThree'); + * // Click play + * // --> NO ALERT + * // Click pause + * // Initialize plugin using the plugin function on the player instance + * player3.myPlugin({ + * text: 'Plugin added later!' + * }); + * // Click play + * // --> Should alert 'Plugin added later!' + * ``` + * + * @param {String} name The plugin name + * @param {Function} fn The plugin function that will be called with options + * @mixes videojs + * @method plugin + */ +videojs.plugin = _pluginsJs2['default']; + +/** + * Adding languages so that they're available to all players. + * ```js + * videojs.addLanguage('es', { 'Hello': 'Hola' }); + * ``` + * + * @param {String} code The language code or dictionary property + * @param {Object} data The data values to be translated + * @return {Object} The resulting language dictionary object + * @mixes videojs + * @method addLanguage + */ +videojs.addLanguage = function (code, data) { + var _merge; + + code = ('' + code).toLowerCase(); + return _lodashCompatObjectMerge2['default'](videojs.options.languages, (_merge = {}, _merge[code] = data, _merge))[code]; +}; + +/** + * Log debug messages. + * + * @param {...Object} messages One or more messages to log + */ +videojs.log = _utilsLogJs2['default']; + +/** + * Creates an emulated TimeRange object. + * + * @param {Number|Array} start Start time in seconds or an array of ranges + * @param {Number} end End time in seconds + * @return {Object} Fake TimeRange object + * @method createTimeRange + */ +videojs.createTimeRange = videojs.createTimeRanges = _utilsTimeRangesJs.createTimeRanges; + +/** + * Format seconds as a time string, H:MM:SS or M:SS + * Supplying a guide (in seconds) will force a number of leading zeros + * to cover the length of the guide + * + * @param {Number} seconds Number of seconds to be turned into a string + * @param {Number} guide Number (in seconds) to model the string after + * @return {String} Time formatted as H:MM:SS or M:SS + * @method formatTime + */ +videojs.formatTime = _utilsFormatTimeJs2['default']; + +/** + * Resolve and parse the elements of a URL + * + * @param {String} url The url to parse + * @return {Object} An object of url details + * @method parseUrl + */ +videojs.parseUrl = Url.parseUrl; + +/** + * Returns whether the url passed is a cross domain request or not. + * + * @param {String} url The url to check + * @return {Boolean} Whether it is a cross domain request or not + * @method isCrossOrigin + */ +videojs.isCrossOrigin = Url.isCrossOrigin; + +/** + * Event target class. + * + * @type {Function} + */ +videojs.EventTarget = _eventTarget2['default']; + +/** + * Add an event listener to element + * It stores the handler function in a separate cache object + * and adds a generic handler to the element's event, + * along with a unique id (guid) to the element. + * + * @param {Element|Object} elem Element or object to bind listeners to + * @param {String|Array} type Type of event to bind to. + * @param {Function} fn Event listener. + * @method on + */ +videojs.on = Events.on; + +/** + * Trigger a listener only once for an event + * + * @param {Element|Object} elem Element or object to + * @param {String|Array} type Name/type of event + * @param {Function} fn Event handler function + * @method one + */ +videojs.one = Events.one; + +/** + * Removes event listeners from an element + * + * @param {Element|Object} elem Object to remove listeners from + * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. + * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. + * @method off + */ +videojs.off = Events.off; + +/** + * Trigger an event for an element + * + * @param {Element|Object} elem Element to trigger an event on + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute + * @param {Object} [hash] data hash to pass along with the event + * @return {Boolean=} Returned only if default was prevented + * @method trigger + */ +videojs.trigger = Events.trigger; + +/** + * A cross-browser XMLHttpRequest wrapper. Here's a simple example: + * + * videojs.xhr({ + * body: someJSONString, + * uri: "/foo", + * headers: { + * "Content-Type": "application/json" + * } + * }, function (err, resp, body) { + * // check resp.statusCode + * }); + * + * Check out the [full + * documentation](https://github.com/Raynos/xhr/blob/v2.1.0/README.md) + * for more options. + * + * @param {Object} options settings for the request. + * @return {XMLHttpRequest|XDomainRequest} the request object. + * @see https://github.com/Raynos/xhr + */ +videojs.xhr = _xhr2['default']; + +/** + * TextTrack class + * + * @type {Function} + */ +videojs.TextTrack = _tracksTextTrackJs2['default']; + +/** + * Determines, via duck typing, whether or not a value is a DOM element. + * + * @method isEl + * @param {Mixed} value + * @return {Boolean} + */ +videojs.isEl = Dom.isEl; + +/** + * Determines, via duck typing, whether or not a value is a text node. + * + * @method isTextNode + * @param {Mixed} value + * @return {Boolean} + */ +videojs.isTextNode = Dom.isTextNode; + +/** + * Creates an element and applies properties. + * + * @method createEl + * @param {String} [tagName='div'] Name of tag to be created. + * @param {Object} [properties={}] Element properties to be applied. + * @param {Object} [attributes={}] Element attributes to be applied. + * @return {Element} + */ +videojs.createEl = Dom.createEl; + +/** + * Check if an element has a CSS class + * + * @method hasClass + * @param {Element} element Element to check + * @param {String} classToCheck Classname to check + */ +videojs.hasClass = Dom.hasElClass; + +/** + * Add a CSS class name to an element + * + * @method addClass + * @param {Element} element Element to add class name to + * @param {String} classToAdd Classname to add + */ +videojs.addClass = Dom.addElClass; + +/** + * Remove a CSS class name from an element + * + * @method removeClass + * @param {Element} element Element to remove from class name + * @param {String} classToRemove Classname to remove + */ +videojs.removeClass = Dom.removeElClass; + +/** + * Adds or removes a CSS class name on an element depending on an optional + * condition or the presence/absence of the class name. + * + * @method toggleElClass + * @param {Element} element + * @param {String} classToToggle + * @param {Boolean|Function} [predicate] + * Can be a function that returns a Boolean. If `true`, the class + * will be added; if `false`, the class will be removed. If not + * given, the class will be added if not present and vice versa. + */ +videojs.toggleClass = Dom.toggleElClass; + +/** + * Apply attributes to an HTML element. + * + * @method setAttributes + * @param {Element} el Target element. + * @param {Object=} attributes Element attributes to be applied. + */ +videojs.setAttributes = Dom.setElAttributes; + +/** + * Get an element's attribute values, as defined on the HTML tag + * Attributes are not the same as properties. They're defined on the tag + * or with setAttribute (which shouldn't be used with HTML) + * This will return true or false for boolean attributes. + * + * @method getAttributes + * @param {Element} tag Element from which to get tag attributes + * @return {Object} + */ +videojs.getAttributes = Dom.getElAttributes; + +/** + * Empties the contents of an element. + * + * @method emptyEl + * @param {Element} el + * @return {Element} + */ +videojs.emptyEl = Dom.emptyEl; + +/** + * Normalizes and appends content to an element. + * + * The content for an element can be passed in multiple types and + * combinations, whose behavior is as follows: + * + * - String + * Normalized into a text node. + * + * - Element, TextNode + * Passed through. + * + * - Array + * A one-dimensional array of strings, elements, nodes, or functions (which + * return single strings, elements, or nodes). + * + * - Function + * If the sole argument, is expected to produce a string, element, + * node, or array. + * + * @method appendContent + * @param {Element} el + * @param {String|Element|TextNode|Array|Function} content + * @return {Element} + */ +videojs.appendContent = Dom.appendContent; + +/** + * Normalizes and inserts content into an element; this is identical to + * `appendContent()`, except it empties the element first. + * + * The content for an element can be passed in multiple types and + * combinations, whose behavior is as follows: + * + * - String + * Normalized into a text node. + * + * - Element, TextNode + * Passed through. + * + * - Array + * A one-dimensional array of strings, elements, nodes, or functions (which + * return single strings, elements, or nodes). + * + * - Function + * If the sole argument, is expected to produce a string, element, + * node, or array. + * + * @method insertContent + * @param {Element} el + * @param {String|Element|TextNode|Array|Function} content + * @return {Element} + */ +videojs.insertContent = Dom.insertContent; + +/* + * Custom Universal Module Definition (UMD) + * + * Video.js will never be a non-browser lib so we can simplify UMD a bunch and + * still support requirejs and browserify. This also needs to be closure + * compiler compatible, so string keys are used. + */ +if (typeof define === 'function' && define['amd']) { + define('videojs', [], function () { + return videojs; + }); + + // checking that module is an object too because of umdjs/umd#35 +} else if (typeof exports === 'object' && typeof module === 'object') { + module['exports'] = videojs; + } + +exports['default'] = videojs; +module.exports = exports['default']; + +},{"../../src/js/utils/merge-options.js":138,"./component":67,"./event-target":99,"./extend.js":100,"./player":108,"./plugins.js":109,"./setup":113,"./tech/flash.js":116,"./tech/html5.js":117,"./tech/tech.js":119,"./tracks/text-track.js":128,"./utils/browser.js":129,"./utils/create-deprecation-proxy.js":131,"./utils/dom.js":132,"./utils/events.js":133,"./utils/fn.js":134,"./utils/format-time.js":135,"./utils/log.js":137,"./utils/stylesheet.js":139,"./utils/time-ranges.js":140,"./utils/url.js":142,"global/document":1,"lodash-compat/object/merge":40,"object.assign":45,"xhr":56}]},{},[143])(143) +}); + + +//# sourceMappingURL=video.js.map +/* vtt.js - v0.12.1 (https://github.com/mozilla/vtt.js) built on 08-07-2015 */ + +(function(root) { + var vttjs = root.vttjs = {}; + var cueShim = vttjs.VTTCue; + var regionShim = vttjs.VTTRegion; + var oldVTTCue = root.VTTCue; + var oldVTTRegion = root.VTTRegion; + + vttjs.shim = function() { + vttjs.VTTCue = cueShim; + vttjs.VTTRegion = regionShim; + }; + + vttjs.restore = function() { + vttjs.VTTCue = oldVTTCue; + vttjs.VTTRegion = oldVTTRegion; + }; +}(this)); + +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function(root, vttjs) { + + var autoKeyword = "auto"; + var directionSetting = { + "": true, + "lr": true, + "rl": true + }; + var alignSetting = { + "start": true, + "middle": true, + "end": true, + "left": true, + "right": true + }; + + function findDirectionSetting(value) { + if (typeof value !== "string") { + return false; + } + var dir = directionSetting[value.toLowerCase()]; + return dir ? value.toLowerCase() : false; + } + + function findAlignSetting(value) { + if (typeof value !== "string") { + return false; + } + var align = alignSetting[value.toLowerCase()]; + return align ? value.toLowerCase() : false; + } + + function extend(obj) { + var i = 1; + for (; i < arguments.length; i++) { + var cobj = arguments[i]; + for (var p in cobj) { + obj[p] = cobj[p]; + } + } + + return obj; + } + + function VTTCue(startTime, endTime, text) { + var cue = this; + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + var baseObj = {}; + + if (isIE8) { + cue = document.createElement('custom'); + } else { + baseObj.enumerable = true; + } + + /** + * Shim implementation specific properties. These properties are not in + * the spec. + */ + + // Lets us know when the VTTCue's data has changed in such a way that we need + // to recompute its display state. This lets us compute its display state + // lazily. + cue.hasBeenReset = false; + + /** + * VTTCue and TextTrackCue properties + * http://dev.w3.org/html5/webvtt/#vttcue-interface + */ + + var _id = ""; + var _pauseOnExit = false; + var _startTime = startTime; + var _endTime = endTime; + var _text = text; + var _region = null; + var _vertical = ""; + var _snapToLines = true; + var _line = "auto"; + var _lineAlign = "start"; + var _position = 50; + var _positionAlign = "middle"; + var _size = 50; + var _align = "middle"; + + Object.defineProperty(cue, + "id", extend({}, baseObj, { + get: function() { + return _id; + }, + set: function(value) { + _id = "" + value; + } + })); + + Object.defineProperty(cue, + "pauseOnExit", extend({}, baseObj, { + get: function() { + return _pauseOnExit; + }, + set: function(value) { + _pauseOnExit = !!value; + } + })); + + Object.defineProperty(cue, + "startTime", extend({}, baseObj, { + get: function() { + return _startTime; + }, + set: function(value) { + if (typeof value !== "number") { + throw new TypeError("Start time must be set to a number."); + } + _startTime = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "endTime", extend({}, baseObj, { + get: function() { + return _endTime; + }, + set: function(value) { + if (typeof value !== "number") { + throw new TypeError("End time must be set to a number."); + } + _endTime = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "text", extend({}, baseObj, { + get: function() { + return _text; + }, + set: function(value) { + _text = "" + value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "region", extend({}, baseObj, { + get: function() { + return _region; + }, + set: function(value) { + _region = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "vertical", extend({}, baseObj, { + get: function() { + return _vertical; + }, + set: function(value) { + var setting = findDirectionSetting(value); + // Have to check for false because the setting an be an empty string. + if (setting === false) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _vertical = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "snapToLines", extend({}, baseObj, { + get: function() { + return _snapToLines; + }, + set: function(value) { + _snapToLines = !!value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "line", extend({}, baseObj, { + get: function() { + return _line; + }, + set: function(value) { + if (typeof value !== "number" && value !== autoKeyword) { + throw new SyntaxError("An invalid number or illegal string was specified."); + } + _line = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "lineAlign", extend({}, baseObj, { + get: function() { + return _lineAlign; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _lineAlign = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "position", extend({}, baseObj, { + get: function() { + return _position; + }, + set: function(value) { + if (value < 0 || value > 100) { + throw new Error("Position must be between 0 and 100."); + } + _position = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "positionAlign", extend({}, baseObj, { + get: function() { + return _positionAlign; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _positionAlign = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "size", extend({}, baseObj, { + get: function() { + return _size; + }, + set: function(value) { + if (value < 0 || value > 100) { + throw new Error("Size must be between 0 and 100."); + } + _size = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "align", extend({}, baseObj, { + get: function() { + return _align; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _align = setting; + this.hasBeenReset = true; + } + })); + + /** + * Other <track> spec defined properties + */ + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state + cue.displayState = undefined; + + if (isIE8) { + return cue; + } + } + + /** + * VTTCue methods + */ + + VTTCue.prototype.getCueAsHTML = function() { + // Assume WebVTT.convertCueToDOMTree is on the global. + return WebVTT.convertCueToDOMTree(window, this.text); + }; + + root.VTTCue = root.VTTCue || VTTCue; + vttjs.VTTCue = VTTCue; +}(this, (this.vttjs || {}))); + +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function(root, vttjs) { + + var scrollSetting = { + "": true, + "up": true + }; + + function findScrollSetting(value) { + if (typeof value !== "string") { + return false; + } + var scroll = scrollSetting[value.toLowerCase()]; + return scroll ? value.toLowerCase() : false; + } + + function isValidPercentValue(value) { + return typeof value === "number" && (value >= 0 && value <= 100); + } + + // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface + function VTTRegion() { + var _width = 100; + var _lines = 3; + var _regionAnchorX = 0; + var _regionAnchorY = 100; + var _viewportAnchorX = 0; + var _viewportAnchorY = 100; + var _scroll = ""; + + Object.defineProperties(this, { + "width": { + enumerable: true, + get: function() { + return _width; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("Width must be between 0 and 100."); + } + _width = value; + } + }, + "lines": { + enumerable: true, + get: function() { + return _lines; + }, + set: function(value) { + if (typeof value !== "number") { + throw new TypeError("Lines must be set to a number."); + } + _lines = value; + } + }, + "regionAnchorY": { + enumerable: true, + get: function() { + return _regionAnchorY; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("RegionAnchorX must be between 0 and 100."); + } + _regionAnchorY = value; + } + }, + "regionAnchorX": { + enumerable: true, + get: function() { + return _regionAnchorX; + }, + set: function(value) { + if(!isValidPercentValue(value)) { + throw new Error("RegionAnchorY must be between 0 and 100."); + } + _regionAnchorX = value; + } + }, + "viewportAnchorY": { + enumerable: true, + get: function() { + return _viewportAnchorY; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("ViewportAnchorY must be between 0 and 100."); + } + _viewportAnchorY = value; + } + }, + "viewportAnchorX": { + enumerable: true, + get: function() { + return _viewportAnchorX; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("ViewportAnchorX must be between 0 and 100."); + } + _viewportAnchorX = value; + } + }, + "scroll": { + enumerable: true, + get: function() { + return _scroll; + }, + set: function(value) { + var setting = findScrollSetting(value); + // Have to check for false as an empty string is a legal value. + if (setting === false) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _scroll = setting; + } + } + }); + } + + root.VTTRegion = root.VTTRegion || VTTRegion; + vttjs.VTTRegion = VTTRegion; +}(this, (this.vttjs || {}))); + +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +(function(global) { + + var _objCreate = Object.create || (function() { + function F() {} + return function(o) { + if (arguments.length !== 1) { + throw new Error('Object.create shim only accepts one parameter.'); + } + F.prototype = o; + return new F(); + }; + })(); + + // Creates a new ParserError object from an errorData object. The errorData + // object should have default code and message properties. The default message + // property can be overriden by passing in a message parameter. + // See ParsingError.Errors below for acceptable errors. + function ParsingError(errorData, message) { + this.name = "ParsingError"; + this.code = errorData.code; + this.message = message || errorData.message; + } + ParsingError.prototype = _objCreate(Error.prototype); + ParsingError.prototype.constructor = ParsingError; + + // ParsingError metadata for acceptable ParsingErrors. + ParsingError.Errors = { + BadSignature: { + code: 0, + message: "Malformed WebVTT signature." + }, + BadTimeStamp: { + code: 1, + message: "Malformed time stamp." + } + }; + + // Try to parse input as a time stamp. + function parseTimeStamp(input) { + + function computeSeconds(h, m, s, f) { + return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; + } + + var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/); + if (!m) { + return null; + } + + if (m[3]) { + // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] + return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); + } else if (m[1] > 59) { + // Timestamp takes the form of [hours]:[minutes].[milliseconds] + // First position is hours as it's over 59. + return computeSeconds(m[1], m[2], 0, m[4]); + } else { + // Timestamp takes the form of [minutes]:[seconds].[milliseconds] + return computeSeconds(0, m[1], m[2], m[4]); + } + } + + // A settings object holds key/value pairs and will ignore anything but the first + // assignment to a specific key. + function Settings() { + this.values = _objCreate(null); + } + + Settings.prototype = { + // Only accept the first assignment to any key. + set: function(k, v) { + if (!this.get(k) && v !== "") { + this.values[k] = v; + } + }, + // Return the value for a key, or a default value. + // If 'defaultKey' is passed then 'dflt' is assumed to be an object with + // a number of possible default values as properties where 'defaultKey' is + // the key of the property that will be chosen; otherwise it's assumed to be + // a single value. + get: function(k, dflt, defaultKey) { + if (defaultKey) { + return this.has(k) ? this.values[k] : dflt[defaultKey]; + } + return this.has(k) ? this.values[k] : dflt; + }, + // Check whether we have a value for a key. + has: function(k) { + return k in this.values; + }, + // Accept a setting if its one of the given alternatives. + alt: function(k, v, a) { + for (var n = 0; n < a.length; ++n) { + if (v === a[n]) { + this.set(k, v); + break; + } + } + }, + // Accept a setting if its a valid (signed) integer. + integer: function(k, v) { + if (/^-?\d+$/.test(v)) { // integer + this.set(k, parseInt(v, 10)); + } + }, + // Accept a setting if its a valid percentage. + percent: function(k, v) { + var m; + if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) { + v = parseFloat(v); + if (v >= 0 && v <= 100) { + this.set(k, v); + return true; + } + } + return false; + } + }; + + // Helper function to parse input into groups separated by 'groupDelim', and + // interprete each group as a key/value pair separated by 'keyValueDelim'. + function parseOptions(input, callback, keyValueDelim, groupDelim) { + var groups = groupDelim ? input.split(groupDelim) : [input]; + for (var i in groups) { + if (typeof groups[i] !== "string") { + continue; + } + var kv = groups[i].split(keyValueDelim); + if (kv.length !== 2) { + continue; + } + var k = kv[0]; + var v = kv[1]; + callback(k, v); + } + } + + function parseCue(input, cue, regionList) { + // Remember the original input if we need to throw an error. + var oInput = input; + // 4.1 WebVTT timestamp + function consumeTimeStamp() { + var ts = parseTimeStamp(input); + if (ts === null) { + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput); + } + // Remove time stamp from input. + input = input.replace(/^[^\sa-zA-Z-]+/, ""); + return ts; + } + + // 4.4.2 WebVTT cue settings + function consumeCueSettings(input, cue) { + var settings = new Settings(); + + parseOptions(input, function (k, v) { + switch (k) { + case "region": + // Find the last region we parsed with the same region id. + for (var i = regionList.length - 1; i >= 0; i--) { + if (regionList[i].id === v) { + settings.set(k, regionList[i].region); + break; + } + } + break; + case "vertical": + settings.alt(k, v, ["rl", "lr"]); + break; + case "line": + var vals = v.split(","), + vals0 = vals[0]; + settings.integer(k, vals0); + settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; + settings.alt(k, vals0, ["auto"]); + if (vals.length === 2) { + settings.alt("lineAlign", vals[1], ["start", "middle", "end"]); + } + break; + case "position": + vals = v.split(","); + settings.percent(k, vals[0]); + if (vals.length === 2) { + settings.alt("positionAlign", vals[1], ["start", "middle", "end"]); + } + break; + case "size": + settings.percent(k, v); + break; + case "align": + settings.alt(k, v, ["start", "middle", "end", "left", "right"]); + break; + } + }, /:/, /\s/); + + // Apply default values for any missing fields. + cue.region = settings.get("region", null); + cue.vertical = settings.get("vertical", ""); + cue.line = settings.get("line", "auto"); + cue.lineAlign = settings.get("lineAlign", "start"); + cue.snapToLines = settings.get("snapToLines", true); + cue.size = settings.get("size", 100); + cue.align = settings.get("align", "middle"); + cue.position = settings.get("position", { + start: 0, + left: 0, + middle: 50, + end: 100, + right: 100 + }, cue.align); + cue.positionAlign = settings.get("positionAlign", { + start: "start", + left: "start", + middle: "middle", + end: "end", + right: "end" + }, cue.align); + } + + function skipWhitespace() { + input = input.replace(/^\s+/, ""); + } + + // 4.1 WebVTT cue timings. + skipWhitespace(); + cue.startTime = consumeTimeStamp(); // (1) collect cue start time + skipWhitespace(); + if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed time stamp (time stamps must be separated by '-->'): " + + oInput); + } + input = input.substr(3); + skipWhitespace(); + cue.endTime = consumeTimeStamp(); // (5) collect cue end time + + // 4.1 WebVTT cue settings list. + skipWhitespace(); + consumeCueSettings(input, cue); + } + + var ESCAPE = { + "&amp;": "&", + "&lt;": "<", + "&gt;": ">", + "&lrm;": "\u200e", + "&rlm;": "\u200f", + "&nbsp;": "\u00a0" + }; + + var TAG_NAME = { + c: "span", + i: "i", + b: "b", + u: "u", + ruby: "ruby", + rt: "rt", + v: "span", + lang: "span" + }; + + var TAG_ANNOTATION = { + v: "title", + lang: "lang" + }; + + var NEEDS_PARENT = { + rt: "ruby" + }; + + // Parse content into a document fragment. + function parseContent(window, input) { + function nextToken() { + // Check for end-of-string. + if (!input) { + return null; + } + + // Consume 'n' characters from the input. + function consume(result) { + input = input.substr(result.length); + return result; + } + + var m = input.match(/^([^<]*)(<[^>]+>?)?/); + // If there is some text before the next tag, return it, otherwise return + // the tag. + return consume(m[1] ? m[1] : m[2]); + } + + // Unescape a string 's'. + function unescape1(e) { + return ESCAPE[e]; + } + function unescape(s) { + while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) { + s = s.replace(m[0], unescape1); + } + return s; + } + + function shouldAdd(current, element) { + return !NEEDS_PARENT[element.localName] || + NEEDS_PARENT[element.localName] === current.localName; + } + + // Create an element for this tag. + function createElement(type, annotation) { + var tagName = TAG_NAME[type]; + if (!tagName) { + return null; + } + var element = window.document.createElement(tagName); + element.localName = tagName; + var name = TAG_ANNOTATION[type]; + if (name && annotation) { + element[name] = annotation.trim(); + } + return element; + } + + var rootDiv = window.document.createElement("div"), + current = rootDiv, + t, + tagStack = []; + + while ((t = nextToken()) !== null) { + if (t[0] === '<') { + if (t[1] === "/") { + // If the closing tag matches, move back up to the parent node. + if (tagStack.length && + tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { + tagStack.pop(); + current = current.parentNode; + } + // Otherwise just ignore the end tag. + continue; + } + var ts = parseTimeStamp(t.substr(1, t.length - 2)); + var node; + if (ts) { + // Timestamps are lead nodes as well. + node = window.document.createProcessingInstruction("timestamp", ts); + current.appendChild(node); + continue; + } + var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); + // If we can't parse the tag, skip to the next tag. + if (!m) { + continue; + } + // Try to construct an element, and ignore the tag if we couldn't. + node = createElement(m[1], m[3]); + if (!node) { + continue; + } + // Determine if the tag should be added based on the context of where it + // is placed in the cuetext. + if (!shouldAdd(current, node)) { + continue; + } + // Set the class list (as a list of classes, separated by space). + if (m[2]) { + node.className = m[2].substr(1).replace('.', ' '); + } + // Append the node to the current node, and enter the scope of the new + // node. + tagStack.push(m[1]); + current.appendChild(node); + current = node; + continue; + } + + // Text nodes are leaf nodes. + current.appendChild(window.document.createTextNode(unescape(t))); + } + + return rootDiv; + } + + // This is a list of all the Unicode characters that have a strong + // right-to-left category. What this means is that these characters are + // written right-to-left for sure. It was generated by pulling all the strong + // right-to-left characters out of the Unicode data table. That table can + // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt + var strongRTLChars = [0x05BE, 0x05C0, 0x05C3, 0x05C6, 0x05D0, 0x05D1, + 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, + 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, 0x05E0, 0x05E1, 0x05E2, 0x05E3, + 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x05F0, 0x05F1, + 0x05F2, 0x05F3, 0x05F4, 0x0608, 0x060B, 0x060D, 0x061B, 0x061E, 0x061F, + 0x0620, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627, 0x0628, + 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631, + 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638, 0x0639, 0x063A, + 0x063B, 0x063C, 0x063D, 0x063E, 0x063F, 0x0640, 0x0641, 0x0642, 0x0643, + 0x0644, 0x0645, 0x0646, 0x0647, 0x0648, 0x0649, 0x064A, 0x066D, 0x066E, + 0x066F, 0x0671, 0x0672, 0x0673, 0x0674, 0x0675, 0x0676, 0x0677, 0x0678, + 0x0679, 0x067A, 0x067B, 0x067C, 0x067D, 0x067E, 0x067F, 0x0680, 0x0681, + 0x0682, 0x0683, 0x0684, 0x0685, 0x0686, 0x0687, 0x0688, 0x0689, 0x068A, + 0x068B, 0x068C, 0x068D, 0x068E, 0x068F, 0x0690, 0x0691, 0x0692, 0x0693, + 0x0694, 0x0695, 0x0696, 0x0697, 0x0698, 0x0699, 0x069A, 0x069B, 0x069C, + 0x069D, 0x069E, 0x069F, 0x06A0, 0x06A1, 0x06A2, 0x06A3, 0x06A4, 0x06A5, + 0x06A6, 0x06A7, 0x06A8, 0x06A9, 0x06AA, 0x06AB, 0x06AC, 0x06AD, 0x06AE, + 0x06AF, 0x06B0, 0x06B1, 0x06B2, 0x06B3, 0x06B4, 0x06B5, 0x06B6, 0x06B7, + 0x06B8, 0x06B9, 0x06BA, 0x06BB, 0x06BC, 0x06BD, 0x06BE, 0x06BF, 0x06C0, + 0x06C1, 0x06C2, 0x06C3, 0x06C4, 0x06C5, 0x06C6, 0x06C7, 0x06C8, 0x06C9, + 0x06CA, 0x06CB, 0x06CC, 0x06CD, 0x06CE, 0x06CF, 0x06D0, 0x06D1, 0x06D2, + 0x06D3, 0x06D4, 0x06D5, 0x06E5, 0x06E6, 0x06EE, 0x06EF, 0x06FA, 0x06FB, + 0x06FC, 0x06FD, 0x06FE, 0x06FF, 0x0700, 0x0701, 0x0702, 0x0703, 0x0704, + 0x0705, 0x0706, 0x0707, 0x0708, 0x0709, 0x070A, 0x070B, 0x070C, 0x070D, + 0x070F, 0x0710, 0x0712, 0x0713, 0x0714, 0x0715, 0x0716, 0x0717, 0x0718, + 0x0719, 0x071A, 0x071B, 0x071C, 0x071D, 0x071E, 0x071F, 0x0720, 0x0721, + 0x0722, 0x0723, 0x0724, 0x0725, 0x0726, 0x0727, 0x0728, 0x0729, 0x072A, + 0x072B, 0x072C, 0x072D, 0x072E, 0x072F, 0x074D, 0x074E, 0x074F, 0x0750, + 0x0751, 0x0752, 0x0753, 0x0754, 0x0755, 0x0756, 0x0757, 0x0758, 0x0759, + 0x075A, 0x075B, 0x075C, 0x075D, 0x075E, 0x075F, 0x0760, 0x0761, 0x0762, + 0x0763, 0x0764, 0x0765, 0x0766, 0x0767, 0x0768, 0x0769, 0x076A, 0x076B, + 0x076C, 0x076D, 0x076E, 0x076F, 0x0770, 0x0771, 0x0772, 0x0773, 0x0774, + 0x0775, 0x0776, 0x0777, 0x0778, 0x0779, 0x077A, 0x077B, 0x077C, 0x077D, + 0x077E, 0x077F, 0x0780, 0x0781, 0x0782, 0x0783, 0x0784, 0x0785, 0x0786, + 0x0787, 0x0788, 0x0789, 0x078A, 0x078B, 0x078C, 0x078D, 0x078E, 0x078F, + 0x0790, 0x0791, 0x0792, 0x0793, 0x0794, 0x0795, 0x0796, 0x0797, 0x0798, + 0x0799, 0x079A, 0x079B, 0x079C, 0x079D, 0x079E, 0x079F, 0x07A0, 0x07A1, + 0x07A2, 0x07A3, 0x07A4, 0x07A5, 0x07B1, 0x07C0, 0x07C1, 0x07C2, 0x07C3, + 0x07C4, 0x07C5, 0x07C6, 0x07C7, 0x07C8, 0x07C9, 0x07CA, 0x07CB, 0x07CC, + 0x07CD, 0x07CE, 0x07CF, 0x07D0, 0x07D1, 0x07D2, 0x07D3, 0x07D4, 0x07D5, + 0x07D6, 0x07D7, 0x07D8, 0x07D9, 0x07DA, 0x07DB, 0x07DC, 0x07DD, 0x07DE, + 0x07DF, 0x07E0, 0x07E1, 0x07E2, 0x07E3, 0x07E4, 0x07E5, 0x07E6, 0x07E7, + 0x07E8, 0x07E9, 0x07EA, 0x07F4, 0x07F5, 0x07FA, 0x0800, 0x0801, 0x0802, + 0x0803, 0x0804, 0x0805, 0x0806, 0x0807, 0x0808, 0x0809, 0x080A, 0x080B, + 0x080C, 0x080D, 0x080E, 0x080F, 0x0810, 0x0811, 0x0812, 0x0813, 0x0814, + 0x0815, 0x081A, 0x0824, 0x0828, 0x0830, 0x0831, 0x0832, 0x0833, 0x0834, + 0x0835, 0x0836, 0x0837, 0x0838, 0x0839, 0x083A, 0x083B, 0x083C, 0x083D, + 0x083E, 0x0840, 0x0841, 0x0842, 0x0843, 0x0844, 0x0845, 0x0846, 0x0847, + 0x0848, 0x0849, 0x084A, 0x084B, 0x084C, 0x084D, 0x084E, 0x084F, 0x0850, + 0x0851, 0x0852, 0x0853, 0x0854, 0x0855, 0x0856, 0x0857, 0x0858, 0x085E, + 0x08A0, 0x08A2, 0x08A3, 0x08A4, 0x08A5, 0x08A6, 0x08A7, 0x08A8, 0x08A9, + 0x08AA, 0x08AB, 0x08AC, 0x200F, 0xFB1D, 0xFB1F, 0xFB20, 0xFB21, 0xFB22, + 0xFB23, 0xFB24, 0xFB25, 0xFB26, 0xFB27, 0xFB28, 0xFB2A, 0xFB2B, 0xFB2C, + 0xFB2D, 0xFB2E, 0xFB2F, 0xFB30, 0xFB31, 0xFB32, 0xFB33, 0xFB34, 0xFB35, + 0xFB36, 0xFB38, 0xFB39, 0xFB3A, 0xFB3B, 0xFB3C, 0xFB3E, 0xFB40, 0xFB41, + 0xFB43, 0xFB44, 0xFB46, 0xFB47, 0xFB48, 0xFB49, 0xFB4A, 0xFB4B, 0xFB4C, + 0xFB4D, 0xFB4E, 0xFB4F, 0xFB50, 0xFB51, 0xFB52, 0xFB53, 0xFB54, 0xFB55, + 0xFB56, 0xFB57, 0xFB58, 0xFB59, 0xFB5A, 0xFB5B, 0xFB5C, 0xFB5D, 0xFB5E, + 0xFB5F, 0xFB60, 0xFB61, 0xFB62, 0xFB63, 0xFB64, 0xFB65, 0xFB66, 0xFB67, + 0xFB68, 0xFB69, 0xFB6A, 0xFB6B, 0xFB6C, 0xFB6D, 0xFB6E, 0xFB6F, 0xFB70, + 0xFB71, 0xFB72, 0xFB73, 0xFB74, 0xFB75, 0xFB76, 0xFB77, 0xFB78, 0xFB79, + 0xFB7A, 0xFB7B, 0xFB7C, 0xFB7D, 0xFB7E, 0xFB7F, 0xFB80, 0xFB81, 0xFB82, + 0xFB83, 0xFB84, 0xFB85, 0xFB86, 0xFB87, 0xFB88, 0xFB89, 0xFB8A, 0xFB8B, + 0xFB8C, 0xFB8D, 0xFB8E, 0xFB8F, 0xFB90, 0xFB91, 0xFB92, 0xFB93, 0xFB94, + 0xFB95, 0xFB96, 0xFB97, 0xFB98, 0xFB99, 0xFB9A, 0xFB9B, 0xFB9C, 0xFB9D, + 0xFB9E, 0xFB9F, 0xFBA0, 0xFBA1, 0xFBA2, 0xFBA3, 0xFBA4, 0xFBA5, 0xFBA6, + 0xFBA7, 0xFBA8, 0xFBA9, 0xFBAA, 0xFBAB, 0xFBAC, 0xFBAD, 0xFBAE, 0xFBAF, + 0xFBB0, 0xFBB1, 0xFBB2, 0xFBB3, 0xFBB4, 0xFBB5, 0xFBB6, 0xFBB7, 0xFBB8, + 0xFBB9, 0xFBBA, 0xFBBB, 0xFBBC, 0xFBBD, 0xFBBE, 0xFBBF, 0xFBC0, 0xFBC1, + 0xFBD3, 0xFBD4, 0xFBD5, 0xFBD6, 0xFBD7, 0xFBD8, 0xFBD9, 0xFBDA, 0xFBDB, + 0xFBDC, 0xFBDD, 0xFBDE, 0xFBDF, 0xFBE0, 0xFBE1, 0xFBE2, 0xFBE3, 0xFBE4, + 0xFBE5, 0xFBE6, 0xFBE7, 0xFBE8, 0xFBE9, 0xFBEA, 0xFBEB, 0xFBEC, 0xFBED, + 0xFBEE, 0xFBEF, 0xFBF0, 0xFBF1, 0xFBF2, 0xFBF3, 0xFBF4, 0xFBF5, 0xFBF6, + 0xFBF7, 0xFBF8, 0xFBF9, 0xFBFA, 0xFBFB, 0xFBFC, 0xFBFD, 0xFBFE, 0xFBFF, + 0xFC00, 0xFC01, 0xFC02, 0xFC03, 0xFC04, 0xFC05, 0xFC06, 0xFC07, 0xFC08, + 0xFC09, 0xFC0A, 0xFC0B, 0xFC0C, 0xFC0D, 0xFC0E, 0xFC0F, 0xFC10, 0xFC11, + 0xFC12, 0xFC13, 0xFC14, 0xFC15, 0xFC16, 0xFC17, 0xFC18, 0xFC19, 0xFC1A, + 0xFC1B, 0xFC1C, 0xFC1D, 0xFC1E, 0xFC1F, 0xFC20, 0xFC21, 0xFC22, 0xFC23, + 0xFC24, 0xFC25, 0xFC26, 0xFC27, 0xFC28, 0xFC29, 0xFC2A, 0xFC2B, 0xFC2C, + 0xFC2D, 0xFC2E, 0xFC2F, 0xFC30, 0xFC31, 0xFC32, 0xFC33, 0xFC34, 0xFC35, + 0xFC36, 0xFC37, 0xFC38, 0xFC39, 0xFC3A, 0xFC3B, 0xFC3C, 0xFC3D, 0xFC3E, + 0xFC3F, 0xFC40, 0xFC41, 0xFC42, 0xFC43, 0xFC44, 0xFC45, 0xFC46, 0xFC47, + 0xFC48, 0xFC49, 0xFC4A, 0xFC4B, 0xFC4C, 0xFC4D, 0xFC4E, 0xFC4F, 0xFC50, + 0xFC51, 0xFC52, 0xFC53, 0xFC54, 0xFC55, 0xFC56, 0xFC57, 0xFC58, 0xFC59, + 0xFC5A, 0xFC5B, 0xFC5C, 0xFC5D, 0xFC5E, 0xFC5F, 0xFC60, 0xFC61, 0xFC62, + 0xFC63, 0xFC64, 0xFC65, 0xFC66, 0xFC67, 0xFC68, 0xFC69, 0xFC6A, 0xFC6B, + 0xFC6C, 0xFC6D, 0xFC6E, 0xFC6F, 0xFC70, 0xFC71, 0xFC72, 0xFC73, 0xFC74, + 0xFC75, 0xFC76, 0xFC77, 0xFC78, 0xFC79, 0xFC7A, 0xFC7B, 0xFC7C, 0xFC7D, + 0xFC7E, 0xFC7F, 0xFC80, 0xFC81, 0xFC82, 0xFC83, 0xFC84, 0xFC85, 0xFC86, + 0xFC87, 0xFC88, 0xFC89, 0xFC8A, 0xFC8B, 0xFC8C, 0xFC8D, 0xFC8E, 0xFC8F, + 0xFC90, 0xFC91, 0xFC92, 0xFC93, 0xFC94, 0xFC95, 0xFC96, 0xFC97, 0xFC98, + 0xFC99, 0xFC9A, 0xFC9B, 0xFC9C, 0xFC9D, 0xFC9E, 0xFC9F, 0xFCA0, 0xFCA1, + 0xFCA2, 0xFCA3, 0xFCA4, 0xFCA5, 0xFCA6, 0xFCA7, 0xFCA8, 0xFCA9, 0xFCAA, + 0xFCAB, 0xFCAC, 0xFCAD, 0xFCAE, 0xFCAF, 0xFCB0, 0xFCB1, 0xFCB2, 0xFCB3, + 0xFCB4, 0xFCB5, 0xFCB6, 0xFCB7, 0xFCB8, 0xFCB9, 0xFCBA, 0xFCBB, 0xFCBC, + 0xFCBD, 0xFCBE, 0xFCBF, 0xFCC0, 0xFCC1, 0xFCC2, 0xFCC3, 0xFCC4, 0xFCC5, + 0xFCC6, 0xFCC7, 0xFCC8, 0xFCC9, 0xFCCA, 0xFCCB, 0xFCCC, 0xFCCD, 0xFCCE, + 0xFCCF, 0xFCD0, 0xFCD1, 0xFCD2, 0xFCD3, 0xFCD4, 0xFCD5, 0xFCD6, 0xFCD7, + 0xFCD8, 0xFCD9, 0xFCDA, 0xFCDB, 0xFCDC, 0xFCDD, 0xFCDE, 0xFCDF, 0xFCE0, + 0xFCE1, 0xFCE2, 0xFCE3, 0xFCE4, 0xFCE5, 0xFCE6, 0xFCE7, 0xFCE8, 0xFCE9, + 0xFCEA, 0xFCEB, 0xFCEC, 0xFCED, 0xFCEE, 0xFCEF, 0xFCF0, 0xFCF1, 0xFCF2, + 0xFCF3, 0xFCF4, 0xFCF5, 0xFCF6, 0xFCF7, 0xFCF8, 0xFCF9, 0xFCFA, 0xFCFB, + 0xFCFC, 0xFCFD, 0xFCFE, 0xFCFF, 0xFD00, 0xFD01, 0xFD02, 0xFD03, 0xFD04, + 0xFD05, 0xFD06, 0xFD07, 0xFD08, 0xFD09, 0xFD0A, 0xFD0B, 0xFD0C, 0xFD0D, + 0xFD0E, 0xFD0F, 0xFD10, 0xFD11, 0xFD12, 0xFD13, 0xFD14, 0xFD15, 0xFD16, + 0xFD17, 0xFD18, 0xFD19, 0xFD1A, 0xFD1B, 0xFD1C, 0xFD1D, 0xFD1E, 0xFD1F, + 0xFD20, 0xFD21, 0xFD22, 0xFD23, 0xFD24, 0xFD25, 0xFD26, 0xFD27, 0xFD28, + 0xFD29, 0xFD2A, 0xFD2B, 0xFD2C, 0xFD2D, 0xFD2E, 0xFD2F, 0xFD30, 0xFD31, + 0xFD32, 0xFD33, 0xFD34, 0xFD35, 0xFD36, 0xFD37, 0xFD38, 0xFD39, 0xFD3A, + 0xFD3B, 0xFD3C, 0xFD3D, 0xFD50, 0xFD51, 0xFD52, 0xFD53, 0xFD54, 0xFD55, + 0xFD56, 0xFD57, 0xFD58, 0xFD59, 0xFD5A, 0xFD5B, 0xFD5C, 0xFD5D, 0xFD5E, + 0xFD5F, 0xFD60, 0xFD61, 0xFD62, 0xFD63, 0xFD64, 0xFD65, 0xFD66, 0xFD67, + 0xFD68, 0xFD69, 0xFD6A, 0xFD6B, 0xFD6C, 0xFD6D, 0xFD6E, 0xFD6F, 0xFD70, + 0xFD71, 0xFD72, 0xFD73, 0xFD74, 0xFD75, 0xFD76, 0xFD77, 0xFD78, 0xFD79, + 0xFD7A, 0xFD7B, 0xFD7C, 0xFD7D, 0xFD7E, 0xFD7F, 0xFD80, 0xFD81, 0xFD82, + 0xFD83, 0xFD84, 0xFD85, 0xFD86, 0xFD87, 0xFD88, 0xFD89, 0xFD8A, 0xFD8B, + 0xFD8C, 0xFD8D, 0xFD8E, 0xFD8F, 0xFD92, 0xFD93, 0xFD94, 0xFD95, 0xFD96, + 0xFD97, 0xFD98, 0xFD99, 0xFD9A, 0xFD9B, 0xFD9C, 0xFD9D, 0xFD9E, 0xFD9F, + 0xFDA0, 0xFDA1, 0xFDA2, 0xFDA3, 0xFDA4, 0xFDA5, 0xFDA6, 0xFDA7, 0xFDA8, + 0xFDA9, 0xFDAA, 0xFDAB, 0xFDAC, 0xFDAD, 0xFDAE, 0xFDAF, 0xFDB0, 0xFDB1, + 0xFDB2, 0xFDB3, 0xFDB4, 0xFDB5, 0xFDB6, 0xFDB7, 0xFDB8, 0xFDB9, 0xFDBA, + 0xFDBB, 0xFDBC, 0xFDBD, 0xFDBE, 0xFDBF, 0xFDC0, 0xFDC1, 0xFDC2, 0xFDC3, + 0xFDC4, 0xFDC5, 0xFDC6, 0xFDC7, 0xFDF0, 0xFDF1, 0xFDF2, 0xFDF3, 0xFDF4, + 0xFDF5, 0xFDF6, 0xFDF7, 0xFDF8, 0xFDF9, 0xFDFA, 0xFDFB, 0xFDFC, 0xFE70, + 0xFE71, 0xFE72, 0xFE73, 0xFE74, 0xFE76, 0xFE77, 0xFE78, 0xFE79, 0xFE7A, + 0xFE7B, 0xFE7C, 0xFE7D, 0xFE7E, 0xFE7F, 0xFE80, 0xFE81, 0xFE82, 0xFE83, + 0xFE84, 0xFE85, 0xFE86, 0xFE87, 0xFE88, 0xFE89, 0xFE8A, 0xFE8B, 0xFE8C, + 0xFE8D, 0xFE8E, 0xFE8F, 0xFE90, 0xFE91, 0xFE92, 0xFE93, 0xFE94, 0xFE95, + 0xFE96, 0xFE97, 0xFE98, 0xFE99, 0xFE9A, 0xFE9B, 0xFE9C, 0xFE9D, 0xFE9E, + 0xFE9F, 0xFEA0, 0xFEA1, 0xFEA2, 0xFEA3, 0xFEA4, 0xFEA5, 0xFEA6, 0xFEA7, + 0xFEA8, 0xFEA9, 0xFEAA, 0xFEAB, 0xFEAC, 0xFEAD, 0xFEAE, 0xFEAF, 0xFEB0, + 0xFEB1, 0xFEB2, 0xFEB3, 0xFEB4, 0xFEB5, 0xFEB6, 0xFEB7, 0xFEB8, 0xFEB9, + 0xFEBA, 0xFEBB, 0xFEBC, 0xFEBD, 0xFEBE, 0xFEBF, 0xFEC0, 0xFEC1, 0xFEC2, + 0xFEC3, 0xFEC4, 0xFEC5, 0xFEC6, 0xFEC7, 0xFEC8, 0xFEC9, 0xFECA, 0xFECB, + 0xFECC, 0xFECD, 0xFECE, 0xFECF, 0xFED0, 0xFED1, 0xFED2, 0xFED3, 0xFED4, + 0xFED5, 0xFED6, 0xFED7, 0xFED8, 0xFED9, 0xFEDA, 0xFEDB, 0xFEDC, 0xFEDD, + 0xFEDE, 0xFEDF, 0xFEE0, 0xFEE1, 0xFEE2, 0xFEE3, 0xFEE4, 0xFEE5, 0xFEE6, + 0xFEE7, 0xFEE8, 0xFEE9, 0xFEEA, 0xFEEB, 0xFEEC, 0xFEED, 0xFEEE, 0xFEEF, + 0xFEF0, 0xFEF1, 0xFEF2, 0xFEF3, 0xFEF4, 0xFEF5, 0xFEF6, 0xFEF7, 0xFEF8, + 0xFEF9, 0xFEFA, 0xFEFB, 0xFEFC, 0x10800, 0x10801, 0x10802, 0x10803, + 0x10804, 0x10805, 0x10808, 0x1080A, 0x1080B, 0x1080C, 0x1080D, 0x1080E, + 0x1080F, 0x10810, 0x10811, 0x10812, 0x10813, 0x10814, 0x10815, 0x10816, + 0x10817, 0x10818, 0x10819, 0x1081A, 0x1081B, 0x1081C, 0x1081D, 0x1081E, + 0x1081F, 0x10820, 0x10821, 0x10822, 0x10823, 0x10824, 0x10825, 0x10826, + 0x10827, 0x10828, 0x10829, 0x1082A, 0x1082B, 0x1082C, 0x1082D, 0x1082E, + 0x1082F, 0x10830, 0x10831, 0x10832, 0x10833, 0x10834, 0x10835, 0x10837, + 0x10838, 0x1083C, 0x1083F, 0x10840, 0x10841, 0x10842, 0x10843, 0x10844, + 0x10845, 0x10846, 0x10847, 0x10848, 0x10849, 0x1084A, 0x1084B, 0x1084C, + 0x1084D, 0x1084E, 0x1084F, 0x10850, 0x10851, 0x10852, 0x10853, 0x10854, + 0x10855, 0x10857, 0x10858, 0x10859, 0x1085A, 0x1085B, 0x1085C, 0x1085D, + 0x1085E, 0x1085F, 0x10900, 0x10901, 0x10902, 0x10903, 0x10904, 0x10905, + 0x10906, 0x10907, 0x10908, 0x10909, 0x1090A, 0x1090B, 0x1090C, 0x1090D, + 0x1090E, 0x1090F, 0x10910, 0x10911, 0x10912, 0x10913, 0x10914, 0x10915, + 0x10916, 0x10917, 0x10918, 0x10919, 0x1091A, 0x1091B, 0x10920, 0x10921, + 0x10922, 0x10923, 0x10924, 0x10925, 0x10926, 0x10927, 0x10928, 0x10929, + 0x1092A, 0x1092B, 0x1092C, 0x1092D, 0x1092E, 0x1092F, 0x10930, 0x10931, + 0x10932, 0x10933, 0x10934, 0x10935, 0x10936, 0x10937, 0x10938, 0x10939, + 0x1093F, 0x10980, 0x10981, 0x10982, 0x10983, 0x10984, 0x10985, 0x10986, + 0x10987, 0x10988, 0x10989, 0x1098A, 0x1098B, 0x1098C, 0x1098D, 0x1098E, + 0x1098F, 0x10990, 0x10991, 0x10992, 0x10993, 0x10994, 0x10995, 0x10996, + 0x10997, 0x10998, 0x10999, 0x1099A, 0x1099B, 0x1099C, 0x1099D, 0x1099E, + 0x1099F, 0x109A0, 0x109A1, 0x109A2, 0x109A3, 0x109A4, 0x109A5, 0x109A6, + 0x109A7, 0x109A8, 0x109A9, 0x109AA, 0x109AB, 0x109AC, 0x109AD, 0x109AE, + 0x109AF, 0x109B0, 0x109B1, 0x109B2, 0x109B3, 0x109B4, 0x109B5, 0x109B6, + 0x109B7, 0x109BE, 0x109BF, 0x10A00, 0x10A10, 0x10A11, 0x10A12, 0x10A13, + 0x10A15, 0x10A16, 0x10A17, 0x10A19, 0x10A1A, 0x10A1B, 0x10A1C, 0x10A1D, + 0x10A1E, 0x10A1F, 0x10A20, 0x10A21, 0x10A22, 0x10A23, 0x10A24, 0x10A25, + 0x10A26, 0x10A27, 0x10A28, 0x10A29, 0x10A2A, 0x10A2B, 0x10A2C, 0x10A2D, + 0x10A2E, 0x10A2F, 0x10A30, 0x10A31, 0x10A32, 0x10A33, 0x10A40, 0x10A41, + 0x10A42, 0x10A43, 0x10A44, 0x10A45, 0x10A46, 0x10A47, 0x10A50, 0x10A51, + 0x10A52, 0x10A53, 0x10A54, 0x10A55, 0x10A56, 0x10A57, 0x10A58, 0x10A60, + 0x10A61, 0x10A62, 0x10A63, 0x10A64, 0x10A65, 0x10A66, 0x10A67, 0x10A68, + 0x10A69, 0x10A6A, 0x10A6B, 0x10A6C, 0x10A6D, 0x10A6E, 0x10A6F, 0x10A70, + 0x10A71, 0x10A72, 0x10A73, 0x10A74, 0x10A75, 0x10A76, 0x10A77, 0x10A78, + 0x10A79, 0x10A7A, 0x10A7B, 0x10A7C, 0x10A7D, 0x10A7E, 0x10A7F, 0x10B00, + 0x10B01, 0x10B02, 0x10B03, 0x10B04, 0x10B05, 0x10B06, 0x10B07, 0x10B08, + 0x10B09, 0x10B0A, 0x10B0B, 0x10B0C, 0x10B0D, 0x10B0E, 0x10B0F, 0x10B10, + 0x10B11, 0x10B12, 0x10B13, 0x10B14, 0x10B15, 0x10B16, 0x10B17, 0x10B18, + 0x10B19, 0x10B1A, 0x10B1B, 0x10B1C, 0x10B1D, 0x10B1E, 0x10B1F, 0x10B20, + 0x10B21, 0x10B22, 0x10B23, 0x10B24, 0x10B25, 0x10B26, 0x10B27, 0x10B28, + 0x10B29, 0x10B2A, 0x10B2B, 0x10B2C, 0x10B2D, 0x10B2E, 0x10B2F, 0x10B30, + 0x10B31, 0x10B32, 0x10B33, 0x10B34, 0x10B35, 0x10B40, 0x10B41, 0x10B42, + 0x10B43, 0x10B44, 0x10B45, 0x10B46, 0x10B47, 0x10B48, 0x10B49, 0x10B4A, + 0x10B4B, 0x10B4C, 0x10B4D, 0x10B4E, 0x10B4F, 0x10B50, 0x10B51, 0x10B52, + 0x10B53, 0x10B54, 0x10B55, 0x10B58, 0x10B59, 0x10B5A, 0x10B5B, 0x10B5C, + 0x10B5D, 0x10B5E, 0x10B5F, 0x10B60, 0x10B61, 0x10B62, 0x10B63, 0x10B64, + 0x10B65, 0x10B66, 0x10B67, 0x10B68, 0x10B69, 0x10B6A, 0x10B6B, 0x10B6C, + 0x10B6D, 0x10B6E, 0x10B6F, 0x10B70, 0x10B71, 0x10B72, 0x10B78, 0x10B79, + 0x10B7A, 0x10B7B, 0x10B7C, 0x10B7D, 0x10B7E, 0x10B7F, 0x10C00, 0x10C01, + 0x10C02, 0x10C03, 0x10C04, 0x10C05, 0x10C06, 0x10C07, 0x10C08, 0x10C09, + 0x10C0A, 0x10C0B, 0x10C0C, 0x10C0D, 0x10C0E, 0x10C0F, 0x10C10, 0x10C11, + 0x10C12, 0x10C13, 0x10C14, 0x10C15, 0x10C16, 0x10C17, 0x10C18, 0x10C19, + 0x10C1A, 0x10C1B, 0x10C1C, 0x10C1D, 0x10C1E, 0x10C1F, 0x10C20, 0x10C21, + 0x10C22, 0x10C23, 0x10C24, 0x10C25, 0x10C26, 0x10C27, 0x10C28, 0x10C29, + 0x10C2A, 0x10C2B, 0x10C2C, 0x10C2D, 0x10C2E, 0x10C2F, 0x10C30, 0x10C31, + 0x10C32, 0x10C33, 0x10C34, 0x10C35, 0x10C36, 0x10C37, 0x10C38, 0x10C39, + 0x10C3A, 0x10C3B, 0x10C3C, 0x10C3D, 0x10C3E, 0x10C3F, 0x10C40, 0x10C41, + 0x10C42, 0x10C43, 0x10C44, 0x10C45, 0x10C46, 0x10C47, 0x10C48, 0x1EE00, + 0x1EE01, 0x1EE02, 0x1EE03, 0x1EE05, 0x1EE06, 0x1EE07, 0x1EE08, 0x1EE09, + 0x1EE0A, 0x1EE0B, 0x1EE0C, 0x1EE0D, 0x1EE0E, 0x1EE0F, 0x1EE10, 0x1EE11, + 0x1EE12, 0x1EE13, 0x1EE14, 0x1EE15, 0x1EE16, 0x1EE17, 0x1EE18, 0x1EE19, + 0x1EE1A, 0x1EE1B, 0x1EE1C, 0x1EE1D, 0x1EE1E, 0x1EE1F, 0x1EE21, 0x1EE22, + 0x1EE24, 0x1EE27, 0x1EE29, 0x1EE2A, 0x1EE2B, 0x1EE2C, 0x1EE2D, 0x1EE2E, + 0x1EE2F, 0x1EE30, 0x1EE31, 0x1EE32, 0x1EE34, 0x1EE35, 0x1EE36, 0x1EE37, + 0x1EE39, 0x1EE3B, 0x1EE42, 0x1EE47, 0x1EE49, 0x1EE4B, 0x1EE4D, 0x1EE4E, + 0x1EE4F, 0x1EE51, 0x1EE52, 0x1EE54, 0x1EE57, 0x1EE59, 0x1EE5B, 0x1EE5D, + 0x1EE5F, 0x1EE61, 0x1EE62, 0x1EE64, 0x1EE67, 0x1EE68, 0x1EE69, 0x1EE6A, + 0x1EE6C, 0x1EE6D, 0x1EE6E, 0x1EE6F, 0x1EE70, 0x1EE71, 0x1EE72, 0x1EE74, + 0x1EE75, 0x1EE76, 0x1EE77, 0x1EE79, 0x1EE7A, 0x1EE7B, 0x1EE7C, 0x1EE7E, + 0x1EE80, 0x1EE81, 0x1EE82, 0x1EE83, 0x1EE84, 0x1EE85, 0x1EE86, 0x1EE87, + 0x1EE88, 0x1EE89, 0x1EE8B, 0x1EE8C, 0x1EE8D, 0x1EE8E, 0x1EE8F, 0x1EE90, + 0x1EE91, 0x1EE92, 0x1EE93, 0x1EE94, 0x1EE95, 0x1EE96, 0x1EE97, 0x1EE98, + 0x1EE99, 0x1EE9A, 0x1EE9B, 0x1EEA1, 0x1EEA2, 0x1EEA3, 0x1EEA5, 0x1EEA6, + 0x1EEA7, 0x1EEA8, 0x1EEA9, 0x1EEAB, 0x1EEAC, 0x1EEAD, 0x1EEAE, 0x1EEAF, + 0x1EEB0, 0x1EEB1, 0x1EEB2, 0x1EEB3, 0x1EEB4, 0x1EEB5, 0x1EEB6, 0x1EEB7, + 0x1EEB8, 0x1EEB9, 0x1EEBA, 0x1EEBB, 0x10FFFD]; + + function determineBidi(cueDiv) { + var nodeStack = [], + text = "", + charCode; + + if (!cueDiv || !cueDiv.childNodes) { + return "ltr"; + } + + function pushNodes(nodeStack, node) { + for (var i = node.childNodes.length - 1; i >= 0; i--) { + nodeStack.push(node.childNodes[i]); + } + } + + function nextTextNode(nodeStack) { + if (!nodeStack || !nodeStack.length) { + return null; + } + + var node = nodeStack.pop(), + text = node.textContent || node.innerText; + if (text) { + // TODO: This should match all unicode type B characters (paragraph + // separator characters). See issue #115. + var m = text.match(/^.*(\n|\r)/); + if (m) { + nodeStack.length = 0; + return m[0]; + } + return text; + } + if (node.tagName === "ruby") { + return nextTextNode(nodeStack); + } + if (node.childNodes) { + pushNodes(nodeStack, node); + return nextTextNode(nodeStack); + } + } + + pushNodes(nodeStack, cueDiv); + while ((text = nextTextNode(nodeStack))) { + for (var i = 0; i < text.length; i++) { + charCode = text.charCodeAt(i); + for (var j = 0; j < strongRTLChars.length; j++) { + if (strongRTLChars[j] === charCode) { + return "rtl"; + } + } + } + } + return "ltr"; + } + + function computeLinePos(cue) { + if (typeof cue.line === "number" && + (cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) { + return cue.line; + } + if (!cue.track || !cue.track.textTrackList || + !cue.track.textTrackList.mediaElement) { + return -1; + } + var track = cue.track, + trackList = track.textTrackList, + count = 0; + for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { + if (trackList[i].mode === "showing") { + count++; + } + } + return ++count * -1; + } + + function StyleBox() { + } + + // Apply styles to a div. If there is no div passed then it defaults to the + // div on 'this'. + StyleBox.prototype.applyStyles = function(styles, div) { + div = div || this.div; + for (var prop in styles) { + if (styles.hasOwnProperty(prop)) { + div.style[prop] = styles[prop]; + } + } + }; + + StyleBox.prototype.formatStyle = function(val, unit) { + return val === 0 ? 0 : val + unit; + }; + + // Constructs the computed display state of the cue (a div). Places the div + // into the overlay which should be a block level element (usually a div). + function CueStyleBox(window, cue, styleOptions) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + var color = "rgba(255, 255, 255, 1)"; + var backgroundColor = "rgba(0, 0, 0, 0.8)"; + + if (isIE8) { + color = "rgb(255, 255, 255)"; + backgroundColor = "rgb(0, 0, 0)"; + } + + StyleBox.call(this); + this.cue = cue; + + // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will + // have inline positioning and will function as the cue background box. + this.cueDiv = parseContent(window, cue.text); + var styles = { + color: color, + backgroundColor: backgroundColor, + position: "relative", + left: 0, + right: 0, + top: 0, + bottom: 0, + display: "inline" + }; + + if (!isIE8) { + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl"; + styles.unicodeBidi = "plaintext"; + } + this.applyStyles(styles, this.cueDiv); + + // Create an absolutely positioned div that will be used to position the cue + // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS + // mirrors of them except "middle" which is "center" in CSS. + this.div = window.document.createElement("div"); + styles = { + textAlign: cue.align === "middle" ? "center" : cue.align, + font: styleOptions.font, + whiteSpace: "pre-line", + position: "absolute" + }; + + if (!isIE8) { + styles.direction = determineBidi(this.cueDiv); + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl". + stylesunicodeBidi = "plaintext"; + } + + this.applyStyles(styles); + + this.div.appendChild(this.cueDiv); + + // Calculate the distance from the reference edge of the viewport to the text + // position of the cue box. The reference edge will be resolved later when + // the box orientation styles are applied. + var textPos = 0; + switch (cue.positionAlign) { + case "start": + textPos = cue.position; + break; + case "middle": + textPos = cue.position - (cue.size / 2); + break; + case "end": + textPos = cue.position - cue.size; + break; + } + + // Horizontal box orientation; textPos is the distance from the left edge of the + // area to the left edge of the box and cue.size is the distance extending to + // the right from there. + if (cue.vertical === "") { + this.applyStyles({ + left: this.formatStyle(textPos, "%"), + width: this.formatStyle(cue.size, "%") + }); + // Vertical box orientation; textPos is the distance from the top edge of the + // area to the top edge of the box and cue.size is the height extending + // downwards from there. + } else { + this.applyStyles({ + top: this.formatStyle(textPos, "%"), + height: this.formatStyle(cue.size, "%") + }); + } + + this.move = function(box) { + this.applyStyles({ + top: this.formatStyle(box.top, "px"), + bottom: this.formatStyle(box.bottom, "px"), + left: this.formatStyle(box.left, "px"), + right: this.formatStyle(box.right, "px"), + height: this.formatStyle(box.height, "px"), + width: this.formatStyle(box.width, "px") + }); + }; + } + CueStyleBox.prototype = _objCreate(StyleBox.prototype); + CueStyleBox.prototype.constructor = CueStyleBox; + + // Represents the co-ordinates of an Element in a way that we can easily + // compute things with such as if it overlaps or intersects with another Element. + // Can initialize it with either a StyleBox or another BoxPosition. + function BoxPosition(obj) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + + // Either a BoxPosition was passed in and we need to copy it, or a StyleBox + // was passed in and we need to copy the results of 'getBoundingClientRect' + // as the object returned is readonly. All co-ordinate values are in reference + // to the viewport origin (top left). + var lh, height, width, top; + if (obj.div) { + height = obj.div.offsetHeight; + width = obj.div.offsetWidth; + top = obj.div.offsetTop; + + var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && + rects.getClientRects && rects.getClientRects(); + obj = obj.div.getBoundingClientRect(); + // In certain cases the outter div will be slightly larger then the sum of + // the inner div's lines. This could be due to bold text, etc, on some platforms. + // In this case we should get the average line height and use that. This will + // result in the desired behaviour. + lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length) + : 0; + + } + this.left = obj.left; + this.right = obj.right; + this.top = obj.top || top; + this.height = obj.height || height; + this.bottom = obj.bottom || (top + (obj.height || height)); + this.width = obj.width || width; + this.lineHeight = lh !== undefined ? lh : obj.lineHeight; + + if (isIE8 && !this.lineHeight) { + this.lineHeight = 13; + } + } + + // Move the box along a particular axis. Optionally pass in an amount to move + // the box. If no amount is passed then the default is the line height of the + // box. + BoxPosition.prototype.move = function(axis, toMove) { + toMove = toMove !== undefined ? toMove : this.lineHeight; + switch (axis) { + case "+x": + this.left += toMove; + this.right += toMove; + break; + case "-x": + this.left -= toMove; + this.right -= toMove; + break; + case "+y": + this.top += toMove; + this.bottom += toMove; + break; + case "-y": + this.top -= toMove; + this.bottom -= toMove; + break; + } + }; + + // Check if this box overlaps another box, b2. + BoxPosition.prototype.overlaps = function(b2) { + return this.left < b2.right && + this.right > b2.left && + this.top < b2.bottom && + this.bottom > b2.top; + }; + + // Check if this box overlaps any other boxes in boxes. + BoxPosition.prototype.overlapsAny = function(boxes) { + for (var i = 0; i < boxes.length; i++) { + if (this.overlaps(boxes[i])) { + return true; + } + } + return false; + }; + + // Check if this box is within another box. + BoxPosition.prototype.within = function(container) { + return this.top >= container.top && + this.bottom <= container.bottom && + this.left >= container.left && + this.right <= container.right; + }; + + // Check if this box is entirely within the container or it is overlapping + // on the edge opposite of the axis direction passed. For example, if "+x" is + // passed and the box is overlapping on the left edge of the container, then + // return true. + BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) { + switch (axis) { + case "+x": + return this.left < container.left; + case "-x": + return this.right > container.right; + case "+y": + return this.top < container.top; + case "-y": + return this.bottom > container.bottom; + } + }; + + // Find the percentage of the area that this box is overlapping with another + // box. + BoxPosition.prototype.intersectPercentage = function(b2) { + var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), + y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), + intersectArea = x * y; + return intersectArea / (this.height * this.width); + }; + + // Convert the positions from this box to CSS compatible positions using + // the reference container's positions. This has to be done because this + // box's positions are in reference to the viewport origin, whereas, CSS + // values are in referecne to their respective edges. + BoxPosition.prototype.toCSSCompatValues = function(reference) { + return { + top: this.top - reference.top, + bottom: reference.bottom - this.bottom, + left: this.left - reference.left, + right: reference.right - this.right, + height: this.height, + width: this.width + }; + }; + + // Get an object that represents the box's position without anything extra. + // Can pass a StyleBox, HTMLElement, or another BoxPositon. + BoxPosition.getSimpleBoxPosition = function(obj) { + var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; + var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; + var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; + + obj = obj.div ? obj.div.getBoundingClientRect() : + obj.tagName ? obj.getBoundingClientRect() : obj; + var ret = { + left: obj.left, + right: obj.right, + top: obj.top || top, + height: obj.height || height, + bottom: obj.bottom || (top + (obj.height || height)), + width: obj.width || width + }; + return ret; + }; + + // Move a StyleBox to its specified, or next best, position. The containerBox + // is the box that contains the StyleBox, such as a div. boxPositions are + // a list of other boxes that the styleBox can't overlap with. + function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { + + // Find the best position for a cue box, b, on the video. The axis parameter + // is a list of axis, the order of which, it will move the box along. For example: + // Passing ["+x", "-x"] will move the box first along the x axis in the positive + // direction. If it doesn't find a good position for it there it will then move + // it along the x axis in the negative direction. + function findBestPosition(b, axis) { + var bestPosition, + specifiedPosition = new BoxPosition(b), + percentage = 1; // Highest possible so the first thing we get is better. + + for (var i = 0; i < axis.length; i++) { + while (b.overlapsOppositeAxis(containerBox, axis[i]) || + (b.within(containerBox) && b.overlapsAny(boxPositions))) { + b.move(axis[i]); + } + // We found a spot where we aren't overlapping anything. This is our + // best position. + if (b.within(containerBox)) { + return b; + } + var p = b.intersectPercentage(containerBox); + // If we're outside the container box less then we were on our last try + // then remember this position as the best position. + if (percentage > p) { + bestPosition = new BoxPosition(b); + percentage = p; + } + // Reset the box position to the specified position. + b = new BoxPosition(specifiedPosition); + } + return bestPosition || specifiedPosition; + } + + var boxPosition = new BoxPosition(styleBox), + cue = styleBox.cue, + linePos = computeLinePos(cue), + axis = []; + + // If we have a line number to align the cue to. + if (cue.snapToLines) { + var size; + switch (cue.vertical) { + case "": + axis = [ "+y", "-y" ]; + size = "height"; + break; + case "rl": + axis = [ "+x", "-x" ]; + size = "width"; + break; + case "lr": + axis = [ "-x", "+x" ]; + size = "width"; + break; + } + + var step = boxPosition.lineHeight, + position = step * Math.round(linePos), + maxPosition = containerBox[size] + step, + initialAxis = axis[0]; + + // If the specified intial position is greater then the max position then + // clamp the box to the amount of steps it would take for the box to + // reach the max position. + if (Math.abs(position) > maxPosition) { + position = position < 0 ? -1 : 1; + position *= Math.ceil(maxPosition / step) * step; + } + + // If computed line position returns negative then line numbers are + // relative to the bottom of the video instead of the top. Therefore, we + // need to increase our initial position by the length or width of the + // video, depending on the writing direction, and reverse our axis directions. + if (linePos < 0) { + position += cue.vertical === "" ? containerBox.height : containerBox.width; + axis = axis.reverse(); + } + + // Move the box to the specified position. This may not be its best + // position. + boxPosition.move(initialAxis, position); + + } else { + // If we have a percentage line value for the cue. + var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100; + + switch (cue.lineAlign) { + case "middle": + linePos -= (calculatedPercentage / 2); + break; + case "end": + linePos -= calculatedPercentage; + break; + } + + // Apply initial line position to the cue box. + switch (cue.vertical) { + case "": + styleBox.applyStyles({ + top: styleBox.formatStyle(linePos, "%") + }); + break; + case "rl": + styleBox.applyStyles({ + left: styleBox.formatStyle(linePos, "%") + }); + break; + case "lr": + styleBox.applyStyles({ + right: styleBox.formatStyle(linePos, "%") + }); + break; + } + + axis = [ "+y", "-x", "+x", "-y" ]; + + // Get the box position again after we've applied the specified positioning + // to it. + boxPosition = new BoxPosition(styleBox); + } + + var bestPosition = findBestPosition(boxPosition, axis); + styleBox.move(bestPosition.toCSSCompatValues(containerBox)); + } + + function WebVTT() { + // Nothing + } + + // Helper to allow strings to be decoded instead of the default binary utf8 data. + WebVTT.StringDecoder = function() { + return { + decode: function(data) { + if (!data) { + return ""; + } + if (typeof data !== "string") { + throw new Error("Error - expected string data."); + } + return decodeURIComponent(encodeURIComponent(data)); + } + }; + }; + + WebVTT.convertCueToDOMTree = function(window, cuetext) { + if (!window || !cuetext) { + return null; + } + return parseContent(window, cuetext); + }; + + var FONT_SIZE_PERCENT = 0.05; + var FONT_STYLE = "sans-serif"; + var CUE_BACKGROUND_PADDING = "1.5%"; + + // Runs the processing model over the cues and regions passed to it. + // @param overlay A block level element (usually a div) that the computed cues + // and regions will be placed into. + WebVTT.processCues = function(window, cues, overlay) { + if (!window || !cues || !overlay) { + return null; + } + + // Remove all previous children. + while (overlay.firstChild) { + overlay.removeChild(overlay.firstChild); + } + + var paddedOverlay = window.document.createElement("div"); + paddedOverlay.style.position = "absolute"; + paddedOverlay.style.left = "0"; + paddedOverlay.style.right = "0"; + paddedOverlay.style.top = "0"; + paddedOverlay.style.bottom = "0"; + paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; + overlay.appendChild(paddedOverlay); + + // Determine if we need to compute the display states of the cues. This could + // be the case if a cue's state has been changed since the last computation or + // if it has not been computed yet. + function shouldCompute(cues) { + for (var i = 0; i < cues.length; i++) { + if (cues[i].hasBeenReset || !cues[i].displayState) { + return true; + } + } + return false; + } + + // We don't need to recompute the cues' display states. Just reuse them. + if (!shouldCompute(cues)) { + for (var i = 0; i < cues.length; i++) { + paddedOverlay.appendChild(cues[i].displayState); + } + return; + } + + var boxPositions = [], + containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), + fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; + var styleOptions = { + font: fontSize + "px " + FONT_STYLE + }; + + (function() { + var styleBox, cue; + + for (var i = 0; i < cues.length; i++) { + cue = cues[i]; + + // Compute the intial position and styles of the cue div. + styleBox = new CueStyleBox(window, cue, styleOptions); + paddedOverlay.appendChild(styleBox.div); + + // Move the cue div to it's correct line position. + moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); + + // Remember the computed div so that we don't have to recompute it later + // if we don't have too. + cue.displayState = styleBox.div; + + boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); + } + })(); + }; + + WebVTT.Parser = function(window, vttjs, decoder) { + if (!decoder) { + decoder = vttjs; + vttjs = {}; + } + if (!vttjs) { + vttjs = {}; + } + + this.window = window; + this.vttjs = vttjs; + this.state = "INITIAL"; + this.buffer = ""; + this.decoder = decoder || new TextDecoder("utf8"); + this.regionList = []; + }; + + WebVTT.Parser.prototype = { + // If the error is a ParsingError then report it to the consumer if + // possible. If it's not a ParsingError then throw it like normal. + reportOrThrowError: function(e) { + if (e instanceof ParsingError) { + this.onparsingerror && this.onparsingerror(e); + } else { + throw e; + } + }, + parse: function (data) { + var self = this; + + // If there is no data then we won't decode it, but will just try to parse + // whatever is in buffer already. This may occur in circumstances, for + // example when flush() is called. + if (data) { + // Try to decode the data that we received. + self.buffer += self.decoder.decode(data, {stream: true}); + } + + function collectNextLine() { + var buffer = self.buffer; + var pos = 0; + while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { + ++pos; + } + var line = buffer.substr(0, pos); + // Advance the buffer early in case we fail below. + if (buffer[pos] === '\r') { + ++pos; + } + if (buffer[pos] === '\n') { + ++pos; + } + self.buffer = buffer.substr(pos); + return line; + } + + // 3.4 WebVTT region and WebVTT region settings syntax + function parseRegion(input) { + var settings = new Settings(); + + parseOptions(input, function (k, v) { + switch (k) { + case "id": + settings.set(k, v); + break; + case "width": + settings.percent(k, v); + break; + case "lines": + settings.integer(k, v); + break; + case "regionanchor": + case "viewportanchor": + var xy = v.split(','); + if (xy.length !== 2) { + break; + } + // We have to make sure both x and y parse, so use a temporary + // settings object here. + var anchor = new Settings(); + anchor.percent("x", xy[0]); + anchor.percent("y", xy[1]); + if (!anchor.has("x") || !anchor.has("y")) { + break; + } + settings.set(k + "X", anchor.get("x")); + settings.set(k + "Y", anchor.get("y")); + break; + case "scroll": + settings.alt(k, v, ["up"]); + break; + } + }, /=/, /\s/); + + // Create the region, using default values for any values that were not + // specified. + if (settings.has("id")) { + var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); + region.width = settings.get("width", 100); + region.lines = settings.get("lines", 3); + region.regionAnchorX = settings.get("regionanchorX", 0); + region.regionAnchorY = settings.get("regionanchorY", 100); + region.viewportAnchorX = settings.get("viewportanchorX", 0); + region.viewportAnchorY = settings.get("viewportanchorY", 100); + region.scroll = settings.get("scroll", ""); + // Register the region. + self.onregion && self.onregion(region); + // Remember the VTTRegion for later in case we parse any VTTCues that + // reference it. + self.regionList.push({ + id: settings.get("id"), + region: region + }); + } + } + + // 3.2 WebVTT metadata header syntax + function parseHeader(input) { + parseOptions(input, function (k, v) { + switch (k) { + case "Region": + // 3.3 WebVTT region metadata header syntax + parseRegion(v); + break; + } + }, /:/); + } + + // 5.1 WebVTT file parsing. + try { + var line; + if (self.state === "INITIAL") { + // We can't start parsing until we have the first line. + if (!/\r\n|\n/.test(self.buffer)) { + return this; + } + + line = collectNextLine(); + + var m = line.match(/^WEBVTT([ \t].*)?$/); + if (!m || !m[0]) { + throw new ParsingError(ParsingError.Errors.BadSignature); + } + + self.state = "HEADER"; + } + + var alreadyCollectedLine = false; + while (self.buffer) { + // We can't parse a line until we have the full line. + if (!/\r\n|\n/.test(self.buffer)) { + return this; + } + + if (!alreadyCollectedLine) { + line = collectNextLine(); + } else { + alreadyCollectedLine = false; + } + + switch (self.state) { + case "HEADER": + // 13-18 - Allow a header (metadata) under the WEBVTT line. + if (/:/.test(line)) { + parseHeader(line); + } else if (!line) { + // An empty line terminates the header and starts the body (cues). + self.state = "ID"; + } + continue; + case "NOTE": + // Ignore NOTE blocks. + if (!line) { + self.state = "ID"; + } + continue; + case "ID": + // Check for the start of NOTE blocks. + if (/^NOTE($|[ \t])/.test(line)) { + self.state = "NOTE"; + break; + } + // 19-29 - Allow any number of line terminators, then initialize new cue values. + if (!line) { + continue; + } + self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); + self.state = "CUE"; + // 30-39 - Check if self line contains an optional identifier or timing data. + if (line.indexOf("-->") === -1) { + self.cue.id = line; + continue; + } + // Process line as start of a cue. + /*falls through*/ + case "CUE": + // 40 - Collect cue timings and settings. + try { + parseCue(line, self.cue, self.regionList); + } catch (e) { + self.reportOrThrowError(e); + // In case of an error ignore rest of the cue. + self.cue = null; + self.state = "BADCUE"; + continue; + } + self.state = "CUETEXT"; + continue; + case "CUETEXT": + var hasSubstring = line.indexOf("-->") !== -1; + // 34 - If we have an empty line then report the cue. + // 35 - If we have the special substring '-->' then report the cue, + // but do not collect the line as we need to process the current + // one as a new cue. + if (!line || hasSubstring && (alreadyCollectedLine = true)) { + // We are done parsing self cue. + self.oncue && self.oncue(self.cue); + self.cue = null; + self.state = "ID"; + continue; + } + if (self.cue.text) { + self.cue.text += "\n"; + } + self.cue.text += line; + continue; + case "BADCUE": // BADCUE + // 54-62 - Collect and discard the remaining cue. + if (!line) { + self.state = "ID"; + } + continue; + } + } + } catch (e) { + self.reportOrThrowError(e); + + // If we are currently parsing a cue, report what we have. + if (self.state === "CUETEXT" && self.cue && self.oncue) { + self.oncue(self.cue); + } + self.cue = null; + // Enter BADWEBVTT state if header was not parsed correctly otherwise + // another exception occurred so enter BADCUE state. + self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; + } + return this; + }, + flush: function () { + var self = this; + try { + // Finish decoding the stream. + self.buffer += self.decoder.decode(); + // Synthesize the end of the current cue or region. + if (self.cue || self.state === "HEADER") { + self.buffer += "\n\n"; + self.parse(); + } + // If we've flushed, parsed, and we're still on the INITIAL state then + // that means we don't have enough of the stream to parse the first + // line. + if (self.state === "INITIAL") { + throw new ParsingError(ParsingError.Errors.BadSignature); + } + } catch(e) { + self.reportOrThrowError(e); + } + self.onflush && self.onflush(); + return this; + } + }; + + global.WebVTT = WebVTT; + +}(this, (this.vttjs || {})));