changeset 0:a76fa32bdc92

*: HUUUGE changes animia has been renamed to animone, so instead of thinking of a health condition, you think of a beautiful flower :) I've also edited some of the code for animone, but I have no idea if it even works or not because I don't have a mac or windows machine lying around. whoops! ... anyway, all of the changes divergent from Anisthesia are now licensed under BSD. it's possible that I could even rewrite most of the code to where I don't even have to keep the MIT license, but that's thinking too far into the future I've been slacking off on implementing the anime seasons page, mostly out of laziness. I think I'd have to create another db file specifically for the seasons anyway, this code is being pushed *primarily* because the hard drive it's on is failing! yay :)
author Paper <paper@paper.us.eu.org>
date Mon, 01 Apr 2024 02:43:44 -0400
parents
children 0c3cc9e6cd84
files .clang-format .hgignore LICENSE.BSD LICENSE.MIT Makefile.am README.md configure.ac data/players.anisthesia include/animone.h include/animone/fd.h include/animone/fd/kvm.h include/animone/fd/proc.h include/animone/fd/win32.h include/animone/fd/xnu.h include/animone/media.h include/animone/player.h include/animone/strategies.h include/animone/types.h include/animone/util.h include/animone/util/osx.h include/animone/util/win32.h include/animone/win.h include/animone/win/quartz.h include/animone/win/win32.h include/animone/win/x11.h src/animone.cc src/fd.cc src/fd/kvm.cc src/fd/proc.cc src/fd/win32.cc src/fd/xnu.cc src/player.cc src/strategist.cc src/util.cc src/util/osx.cc src/util/win32.cc src/win.cc src/win/quartz.cc src/win/win32.cc src/win/x11.cc
diffstat 40 files changed, 3367 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.clang-format	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,38 @@
+---
+BasedOnStyle: LLVM
+UseTab: ForIndentation
+PointerAlignment: Left
+ColumnLimit: 120
+IndentWidth: 4
+TabWidth: 4
+
+# hack!!!
+AccessModifierOffset: -4
+
+IndentCaseLabels: true
+IndentAccessModifiers: false
+IndentPPDirectives: AfterHash
+
+BreakBeforeBraces: Attach
+BreakStringLiterals: true
+
+AlwaysBreakTemplateDeclarations: true
+
+SpaceAfterTemplateKeyword: false
+
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: Left
+AlignEscapedNewlines: DontAlign
+AlignConsecutiveMacros: true
+
+AllowShortIfStatementsOnASingleLine: false
+AllowShortBlocksOnASingleLine: Empty
+AllowShortEnumsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: InlineOnly
+AllowShortCaseLabelsOnASingleLine: true
+
+Cpp11BracedListStyle: true
+
+---
+Language: Cpp
+Standard: c++17
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,40 @@
+syntax: glob
+
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+syntax: regexp
+
+# Build dir
+^build/
+^test/build/
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE.BSD	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2023-2024, Paper
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE.MIT	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2017 Eren Okka
+Copyright (c) 2023 Paper
+
+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.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile.am	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,83 @@
+lib_LTLIBRARIES = libanimone.la
+
+include_HEADERS = \
+	include/animone.h
+
+animiadir = $(includedir)/animone
+nobase_animia_HEADERS = \
+	include/animone/media.h \
+	include/animone/player.h \
+	include/animone/types.h
+
+noinst_HEADERS = \
+	include/animone/fd/kvm.h \
+	include/animone/fd/proc.h \
+	include/animone/fd/win32.h \
+	include/animone/fd/xnu.h \
+	include/animone/util/osx.h \
+	include/animone/util/win32.h \
+	include/animone/win/quartz.h \
+	include/animone/win/win32.h \
+	include/animone/win/x11.h \
+	include/animone/fd.h \
+	include/animone/strategies.h \
+	include/animone/util.h \
+	include/animone/win.h
+
+if BUILD_WIN
+files_win = src/fd/win32.cc src/win/win32.cc src/util/win32.cc
+libs_win = -lole32 -luuid
+endif
+
+if BUILD_OSX
+files_osx = src/fd/xnu.cc src/win/quartz.cc src/util/osx.cc
+libs_osx = -lobjc
+ldflags_osx = -framework Foundation -framework CoreGraphics -framework ApplicationServices
+endif
+
+if BUILD_LINUX
+files_linux = src/fd/proc.cc
+endif
+
+# these should be in standard locations anyway
+if BUILD_LIBUTIL
+libs_libutil = -lutil
+endif
+
+if BUILD_LIBKVM
+files_libkvm = src/fd/kvm.cc
+libs_libkvm = -lkvm
+endif
+
+if BUILD_XCB
+files_x11 = src/win/x11.cc
+cflags_x11 = $(XCB_CFLAGS)
+libs_x11 = $(XCB_LIBS)
+endif
+
+EXTRA_DIST = \
+	$(top_srcdir)/data/players.anisthesia
+
+libanimone_la_SOURCES = \
+	src/animone.cc \
+	src/fd.cc \
+	src/player.cc \
+	src/strategist.cc \
+	src/util.cc \
+	src/win.cc \
+	$(files_win) \
+	$(files_osx) \
+	$(files_linux) \
+	$(files_libutil) \
+	$(files_libkvm) \
+	$(files_x11) \
+	$(files_wayland)
+
+libanimone_la_CPPFLAGS = -I$(top_srcdir)/include $(DEFS)
+
+libanimone_la_CXXFLAGS = -std=c++17 $(cflags_osx) $(cflags_x11) $(cflags_wayland)
+libanimone_la_LDFLAGS = -version-info 0:0:0 $(ldflags_osx)
+
+libanimone_la_LIBADD = $(libs_win) $(libs_wayland) $(libs_x11) $(libs_osx) $(libs_libutil) $(libs_libkvm)
+
+ACLOCAL_AMFLAGS = -I m4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,36 @@
+# Animia
+Animia is a work-in-progress cross-platform hard fork of Anisthesia and part of
+Minori.
+
+Most (if not all) Anisthesia configs should also work in this library as well
+(at least on Windows).
+
+## License
+Changes divergent from Anisthesia are under the BSD 3-clause license. You can
+find a copy of the original MIT license bundled with Anisthesia at `LICENSE.MIT`
+in the root folder.
+
+## Support
+Unlike Anisthesia, Animia currently does not support UI automation, i.e., most
+web browsers will not work properly, if at all.
+
+Animia will first attempt to connect to a windowing system. If that fails, it falls
+back to just enumerating over the open processes in the system.
+
+## Platform-specific quirks
+
+### Windows
+To get the currently opened file handles on Windows, Animia has to use internal
+kernel functions. However, these functions aren't likely to change anytime soon.
+
+### macOS
+The code to retrieve executable names on macOS uses internal functions. However,
+if these functions cannot be found for whatever reason, it falls back to parsing
+the arguments, and then to calling the kernel.
+
+Additionally, macOS does not have the concept of class names, rather, it has
+bundle identifiers, which are a suitable replacement in most use cases, and are
+what Animia will try to grab before falling back to the Quartz window name.
+
+### X11
+Animia requires that the XRes extension is installed to retrieve window PIDs.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/configure.ac	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,75 @@
+AC_INIT([animone], [0.1.0-alpha.1])
+
+AC_CANONICAL_HOST
+
+AC_CONFIG_SRCDIR([src/animone.cc])
+AC_CONFIG_AUX_DIR([build-aux])
+AC_CONFIG_MACRO_DIRS([m4])
+
+AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects])
+
+# Do we have a C++17 compiler
+AC_PROG_CXX
+
+AM_PROG_AR
+LT_INIT
+
+build_win32=no
+build_osx=no
+build_linux=no
+build_libutil=no
+build_kvm=no
+
+build_x11=no
+
+case "${host_os}" in
+	cygwin*|mingw*)
+		# Windows
+		build_windows=yes
+		AC_CHECK_TOOL([WINDRES], [windres])
+		AC_SUBST(WINDRES)
+		AC_DEFINE([WIN32])
+		;;
+	darwin*)
+		# Mac OS X
+		build_osx=yes
+		AC_DEFINE([MACOSX])
+		;;
+	linux*)
+		build_linux=yes
+		AC_DEFINE([LINUX])
+		;;
+	*)
+		# FreeBSD
+		AC_CHECK_LIB([util], [kinfo_getfile], [build_libutil=yes], [build_libutil=no])
+		if test "x$build_libutil" = "xyes"; then
+			AC_DEFINE([LIBUTIL])
+		else
+			# OpenBSD
+			AC_CHECK_LIB([kvm], [kvm_getfiles], [build_kvm=yes], [build_kvm=no])
+			if test "x$build_kvm" = "xyes"; then
+				AC_DEFINE([LIBKVM])
+			fi
+		fi
+		;;
+esac
+
+if test "x$build_osx" = "xno" && test "x$build_windows" = "xno"; then
+	PKG_CHECK_MODULES(XCB, [xcb xcb-res], [build_x11=yes], [build_x11=no])
+	if test "x$build_x11" = "xyes"; then
+		AC_DEFINE([X11])
+		AC_SUBST([XCB_LIBS])
+		AC_SUBST([XCB_CFLAGS])
+	fi
+fi
+
+AM_CONDITIONAL([BUILD_WIN], [test "x$build_windows" = "xyes"])
+AM_CONDITIONAL([BUILD_OSX], [test "x$build_osx" = "xyes"])
+AM_CONDITIONAL([BUILD_LINUX], [test "x$build_linux" = "xyes"])
+AM_CONDITIONAL([BUILD_LIBUTIL], [test "x$build_libutil" = "xyes"])
+AM_CONDITIONAL([BUILD_LIBKVM], [test "x$build_kvm" = "xyes"])
+
+AM_CONDITIONAL([BUILD_XCB], [test "x$build_x11" = "xyes"])
+
+AC_CONFIG_FILES([Makefile])
+AC_OUTPUT
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/players.anisthesia	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,496 @@
+# This file includes media player data for Anisthesia. It is used to detect
+# running players and retrieve information about current media.
+#
+# Please read before editing this file:
+# - Indentation is significant. You must use tabs rather than spaces.
+# - Regular expressions begin with a '^' character. ECMAScript grammar is used.
+#
+# The latest version of this file can be found at:
+# <https://github.com/erengy/anisthesia>
+#
+# This file is in the public domain.
+
+5KPlayer
+	windows:
+		Qt5QWindowIcon
+	executables:
+		5KPlayer
+	strategies:
+		open_files
+
+Ace Player HD
+	windows:
+		QWidget
+	executables:
+		ace_player
+	strategies:
+		# Must be enabled from: Advanced Preferences -> Interface -> Main
+		# interfaces -> Qt -> Show playing item name in window title
+		#
+		# We use the last alternative to avoid detecting other windows such as
+		# Preferences dialog, which has the same generic class name.
+		window_title:
+			^Ace Player HD.*|(.+) - Ace Player HD.*|.+
+
+ALLPlayer
+	windows:
+		TApplication
+	executables:
+		ALLPlayer
+	strategies:
+		open_files
+
+Baka MPlayer
+	windows:
+		Qt5QWindowIcon
+	executables:
+		Baka MPlayer
+	strategies:
+		open_files
+		# We cannot avoid detecting other windows such as Preferences dialog, which
+		# has the same generic class name.
+		window_title:
+			^Baka MPlayer|(.+)
+
+BESTplayer
+	windows:
+		TBESTplayerApp.UnicodeClass
+	executables:
+		BESTplayer
+	strategies:
+		open_files
+		window_title:
+			^BESTplayer.*|(.+) - BESTplayer.*
+
+bomi
+	windows:
+		Qt5QWindowGLOwnDCIcon
+	executables:
+		bomi
+	strategies:
+		open_files
+		window_title:
+			^bomi|(.+) - bomi
+
+BS.Player
+	windows:
+		BSPlayer
+	executables:
+		bsplayer
+	strategies:
+		open_files
+
+DivX Player
+	windows:
+		Qt5QWindowIcon
+		QWidget
+	executables:
+		DivX Player
+		DivX Plus Player
+	strategies:
+		open_files
+
+GOM Player
+	windows:
+		GomPlayer1.x
+		GomPlayerPlus32_2.x
+		GomPlayerPlus64_2.x
+	executables:
+		GOM
+		GOM64
+	strategies:
+		open_files
+		window_title:
+			^GOM Player(?: Plus)|(.+)(?:\[Subtitle\]) - GOM Player(?: Plus)
+
+Kantaris
+	windows:
+		^WindowsForms10\.Window\.20008\.app\..+
+	executables:
+		Kantaris
+		KantarisMain
+	strategies:
+		open_files
+		window_title:
+			^Kantaris.*|(.+) \d{2}:\d{2}:\d{2} - \d{2}:\d{2}:\d{2}
+
+KMPlayer
+	windows:
+		KMPlayer 64X
+		TApplication
+	executables:
+		KMPlayer
+		KMPlayer64
+	strategies:
+		open_files
+		window_title:
+			^(?:The )?KMPlayer|(?:\[\d+/\d+\] )?(.+) - (?:The )?KMPlayer|(.+)
+
+Kodi
+	windows:
+		Kodi
+		XBMC
+	executables:
+		kodi
+		XBMC
+	strategies:
+		open_files
+
+Light Alloy
+	windows:
+		TApplication
+	executables:
+		LA
+	strategies:
+		open_files
+		window_title:
+			^Light Alloy.*|(.+) - Light Alloy.*
+
+Media Player Classic
+	windows:
+		MediaPlayerClassicW
+	executables:
+		mplayerc
+		mplayerc64
+	strategies:
+		open_files
+		# Depends on: Options -> Player -> Title bar
+		window_title:
+			^Media Player Classic|(.+) - Media Player Classic
+
+Media Player Classic Qute Theater
+	windows:
+		^Qt.+QWindowIcon
+	executables:
+		mpc-qt
+	strategies:
+		open_files
+		# Depends on: Options -> Player -> Title bar
+		#
+		# We use the last alternative to avoid detecting other windows such as
+		# Options dialog, which has the same generic class name.
+		window_title:
+			^Media Player Classic Qute Theater|Media Player Classic Qute Theater - (.+)|.+
+
+Memento
+	windows:
+		^Qt.+QWindowIcon
+	executables:
+		memento
+	strategies:
+		open_files
+		window_title:
+			^Memento|(.+) - Memento
+
+Miro
+	windows:
+		gdkWindowToplevel
+	executables:
+		Miro
+	strategies:
+		open_files
+
+MPC-BE
+	windows:
+		MediaPlayerClassicW
+		MPC-BE
+	executables:
+		mpc-be
+		mpc-be64
+	strategies:
+		open_files
+		# Depends on: Options -> Player -> Title bar
+		window_title:
+			^MPC-BE.*|(.+) - MPC-BE.*
+
+MPC-HC
+	windows:
+		MediaPlayerClassicW
+	executables:
+		mpc-hc
+		mpc-hc64
+		# Some codec installers append "_nvo" to the filename, if NVIDIA Optimus
+		# is present on the system. Similarly, various guides recommend
+		# appending "-gpu", etc. in order to fix some GPU-related issues.
+		^mpc-hc.+
+		# LAV Filters Megamix
+		iris
+		shoukaku
+	strategies:
+		open_files
+		# Depends on: Options -> Player -> Title bar
+		window_title:
+			^Media Player Classic Home Cinema|MPC-HC|(.+)
+
+MPCSTAR
+	windows:
+		^wxWindow@.*
+		wxWindowClassNR
+	executables:
+		mpcstar
+	strategies:
+		open_files
+		window_title:
+			^MPCSTAR.*|(.+) - MPCSTAR.*
+
+MPDN
+	windows:
+		^WindowsForms10\.Window\.8\.app\..+
+	executables:
+		MediaPlayerDotNet
+	strategies:
+		open_files
+		window_title:
+			^MPDN - Media Player .NET \((?:32|64)-bit Edition\)|(.*) - MPDN \((?:32|64)-bit Edition\)
+
+mpv
+	windows:
+		mpv
+	executables:
+		mpv
+	strategies:
+		open_files
+		# May be in an unexpected format if "--title" option is used. Ideally, it
+		# should return only "${filename}", "${path}" or "${media-title}".
+		window_title:
+			^No file - mpv|(.+) - mpv|mpv - (.+)
+
+mpv.net
+	windows:
+		^WindowsForms10\.Window\.8\.app\..+
+	executables:
+		mpvnet
+	strategies:
+		open_files
+		window_title:
+			^mpv\.net.*|(.+) - mpv\.net.*
+
+MV2Player
+	windows:
+		TApplication
+	executables:
+		Mv2Player
+		Mv2PlayerPlus
+	strategies:
+		open_files
+		# Depends on: Options -> Player -> Constant app. title
+		window_title:
+			^MV2 Player|(.+)
+
+PotPlayer
+	windows:
+		PotPlayer
+		PotPlayer64
+	executables:
+		PotPlayer
+		PotPlayer64
+		PotPlayerMini
+		PotPlayerMini64
+		# LAV Filters Megamix
+		sumire
+		zuikaku
+	strategies:
+		open_files
+		window_title:
+			^PotPlayer|(.+) - PotPlayer
+
+SMPlayer
+	windows:
+		# Qt5QWindowIcon, Qt5152QWindowIcon, etc.
+		^Qt.+QWindowIcon
+		# Older versions
+		QWidget
+	executables:
+		smplayer
+		smplayer2
+	strategies:
+		# "open_files" strategy does not work here, because files are loaded by
+		# a child process of SMPlayer (mplayer or mpv, depending on the selected
+		# multimedia engine).
+		#
+		# We use the last alternative to avoid detecting other windows such as
+		# Preferences dialog, which has the same generic class name.
+		window_title:
+			^SMPlayer|(.+) - SMPlayer|.+
+
+Splash
+	windows:
+		DX_DISPLAY0
+	executables:
+		Splash
+		SplashLite
+	strategies:
+		open_files
+
+SPlayer
+	windows:
+		MediaPlayerClassicW
+	executables:
+		splayer
+	strategies:
+		open_files
+		# Does not work in theater mode.
+		window_title:
+			^SPlayer|(?:\[(?:GPU Accel\+)?EVR\] )?(.+) - SPlayer
+
+UMPlayer
+	windows:
+		QWidget
+	executables:
+		umplayer
+	strategies:
+		# "open_files" strategy does not work here, because files are loaded by
+		# a child process of UMPlayer (mplayer).
+		#
+		# We use the last alternative to avoid detecting other windows such as
+		# Preferences dialog, which has the same generic class name.
+		window_title:
+			^UMPlayer|(.+) - UMPlayer|.+
+
+VLC media player
+	windows:
+		# Qt5QWindowIcon, Qt5151QWindowIcon, etc.
+		^Qt.+QWindowIcon
+		# Older versions
+		QWidget
+		# Skinnable interface
+		SkinWindowClass
+		# X11
+		vlc
+	executables:
+		vlc
+	strategies:
+		open_files
+		# Must be enabled from: Advanced Preferences -> Interface -> Main
+		# interfaces -> Qt -> Show playing item name in window title
+		#
+		# We use the last alternative to avoid detecting other windows such as
+		# Preferences dialog, which has the same generic class name.
+		window_title:
+			^VLC media player|(.+) - VLC media player|.+
+
+WebTorrent Desktop
+	windows:
+		Chrome_WidgetWin_1
+	executables:
+		WebTorrent
+	strategies:
+		window_title:
+			^WebTorrent(?: \(BETA\))?|Main Window|Preferences|About WebTorrent.*|(.+)
+
+Winamp
+	windows:
+		Winamp v1.x
+	executables:
+		winamp
+	strategies:
+		open_files
+		window_title:
+			^Winamp [\d.]+ Build \d+|\d+\. (.+) - Winamp(?: \[.+\])?
+
+Windows Media Player
+	windows:
+		WMPlayerApp
+		WMP Skin Host
+	executables:
+		wmplayer
+	strategies:
+		open_files
+
+Zoom Player
+	windows:
+		TApplication
+	executables:
+		zplayer
+	strategies:
+		open_files
+		window_title:
+			^Zoom Player|(.+) - Zoom Player (?:FREE|MAX)
+
+################################################################################
+# Web browsers
+
+Brave Browser
+	windows:
+		Chrome_WidgetWin_1
+	executables:
+		brave
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) \(Private\)(?: - Brave)?|(.+) - Brave|(.+)
+	type:
+		web_browser
+
+Google Chrome
+	windows:
+		Chrome_WidgetWin_1
+	executables:
+		chrome
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) \(Incognito\)(?: - Google Chrome)?|(.+) - Google Chrome|(.+)
+	type:
+		web_browser
+
+Internet Explorer
+	windows:
+		IEFrame
+	executables:
+		iexplore
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) - Internet Explorer(?: - \[InPrivate\])?
+	type:
+		web_browser
+
+Microsoft Edge
+	windows:
+		Chrome_WidgetWin_1
+	executables:
+		msedge
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) and \d+ more pages? - .+|(.+) - [^-]+ - Microsoft.*Edge|(.+)
+	type:
+		web_browser
+
+Mozilla Firefox
+	windows:
+		MozillaUIWindowClass
+		MozillaWindowClass
+	executables:
+		firefox
+	strategies:
+		ui_automation
+		window_title:
+			^(?:Mozilla Firefox|Firefox Developer Edition)|(.+) (?:-|—) (?:Mozilla Firefox|Firefox Developer Edition)(?: \(Private Browsing\))?
+	type:
+		web_browser
+
+Opera
+	windows:
+		Chrome_WidgetWin_1
+	executables:
+		opera
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) - Opera(?: \(Private\))?
+	type:
+		web_browser
+
+Waterfox
+	windows:
+		MozillaWindowClass
+	executables:
+		waterfox
+	strategies:
+		ui_automation
+		window_title:
+			^(.+) - Waterfox(?: \(Private Browsing\))?
+	type:
+		web_browser
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,37 @@
+#ifndef ANIMONE_ANIMONE_H_
+#define ANIMONE_ANIMONE_H_
+
+#include "animone/media.h"
+#include "animone/player.h"
+#include "animone/types.h"
+
+namespace animone {
+
+enum class ResultType {
+	Process,
+	Window
+};
+
+struct Process {
+	internal::pid_t pid = 0; /* pid_t == DWORD on Windows, from <sys/types.h> everywhere else */
+	std::string name;
+};
+
+struct Window {
+	unsigned int id = 0;
+	std::string class_name;
+	std::string text; /* title bar text */
+};
+
+struct Result {
+	Player player;
+	Process process;
+	Window window; /* has nothing under process mode */
+	std::vector<Media> media;
+};
+
+bool GetResults(const std::vector<Player>& players, std::vector<Result>& results);
+
+} // namespace animone
+
+#endif // ANIMONE_ANIMONE_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/fd.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,32 @@
+#ifndef ANIMONE_ANIMONE_FD_H_
+#define ANIMONE_ANIMONE_FD_H_
+
+#include <functional>
+#include <set>
+#include <string>
+
+#include "animone/types.h"
+
+namespace animone {
+
+struct Process;
+
+namespace internal {
+
+struct OpenFile {
+	pid_t pid = 0;
+	std::string path;
+};
+
+using process_proc_t = std::function<bool(const Process&)>;
+
+using open_file_proc_t = std::function<bool(const OpenFile&)>;
+
+bool EnumerateOpenProcesses(process_proc_t process_proc);
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc);
+
+} // namespace internal
+
+} // namespace animone
+
+#endif // ANIMONE_ANIMONE_FD_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/fd/kvm.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,17 @@
+#ifndef ANIMONE_ANIMONE_FD_KVM_H_
+#define ANIMONE_ANIMONE_FD_KVM_H_
+
+#include <set>
+#include <string>
+
+#include "animone/fd.h"
+#include "animone/types.h"
+
+namespace animone::internal::kvm {
+
+bool EnumerateOpenProcesses(process_proc_t process_proc);
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc);
+
+} // namespace animone::internal::kvm
+
+#endif // ANIMONE_ANIMONE_FD_KVM_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/fd/proc.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,18 @@
+#ifndef ANIMONE_ANIMONE_FD_PROC_H_
+#define ANIMONE_ANIMONE_FD_PROC_H_
+
+#include <set>
+#include <string>
+
+#include "animone/fd.h"
+#include "animone/types.h"
+
+namespace animone::internal::proc {
+
+bool EnumerateOpenProcesses(process_proc_t process_proc);
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc);
+bool GetProcessName(pid_t pid, std::string& result);
+
+} // namespace animone::internal::proc
+
+#endif // ANIMONE_ANIMONE_FD_PROC_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/fd/win32.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,19 @@
+#ifndef ANIMONE_ANIMONE_FD_WIN32_H_
+#define ANIMONE_ANIMONE_FD_WIN32_H_
+
+#include <set>
+#include <string>
+
+#include <windows.h>
+
+#include "animone/fd.h"
+#include "animone/types.h"
+
+namespace animone::internal::win32 {
+
+bool EnumerateOpenProcesses(process_proc_t process_proc);
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc);
+
+} // namespace animone::internal::win32
+
+#endif // ANIMONE_ANIMONE_FD_WIN32_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/fd/xnu.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,17 @@
+#ifndef ANIMONE_ANIMONE_FD_XNU_H_
+#define ANIMONE_ANIMONE_FD_XNU_H_
+
+#include <set>
+#include <string>
+
+#include "animone/fd.h"
+#include "animone/types.h"
+
+namespace animone::internal::xnu {
+
+bool EnumerateOpenProcesses(process_proc_t process_proc);
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc);
+
+} // namespace animone::internal::xnu
+
+#endif // ANIMONE_ANIMONE_FD_XNU_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/media.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,32 @@
+#ifndef ANIMONE_ANIMONE_MEDIA_H_
+#define ANIMONE_ANIMONE_MEDIA_H_
+
+#include <chrono>
+#include <functional>
+#include <string>
+#include <vector>
+
+namespace animone {
+
+using media_time_t = std::chrono::milliseconds;
+
+enum class MediaInfoType {
+	Unknown,
+	File,
+	Tab,
+	Title,
+	Url
+};
+
+struct MediaInfo {
+	MediaInfoType type = MediaInfoType::Unknown;
+	std::string value;
+};
+
+struct Media {
+	std::vector<MediaInfo> information;
+};
+
+} // namespace animone
+
+#endif // ANIMONE_ANIMONE_MEDIA_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/player.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,34 @@
+#ifndef ANIMONE_ANIMONE_PLAYER_H_
+#define ANIMONE_ANIMONE_PLAYER_H_
+
+#include <string>
+#include <vector>
+
+namespace animone {
+
+enum class Strategy {
+	WindowTitle,
+	OpenFiles,
+	UiAutomation // unused
+};
+
+enum class PlayerType {
+	Default,
+	WebBrowser // unused
+};
+
+struct Player {
+	PlayerType type = PlayerType::Default;
+	std::string name;
+	std::string window_title_format;
+	std::vector<std::string> windows;
+	std::vector<std::string> executables;
+	std::vector<Strategy> strategies;
+};
+
+bool ParsePlayersData(const std::string& data, std::vector<Player>& players);
+bool ParsePlayersFile(const std::string& path, std::vector<Player>& players);
+
+} // namespace animone
+
+#endif // ANIMONE_ANIMONE_PLAYER_H_
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/strategies.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,13 @@
+#ifndef ANIMONE_ANIMONE_STRATEGIES_H_
+#define ANIMONE_ANIMONE_STRATEGIES_H_
+
+#include "animone.h"
+#include <vector>
+
+namespace animone::internal {
+
+bool ApplyStrategies(std::vector<Result>& results);
+
+}
+
+#endif // ANIMONE_ANIMONE_STRATEGIES_H_
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/types.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,18 @@
+#ifndef ANIMONE_ANIMONE_TYPES_H_
+#define ANIMONE_ANIMONE_TYPES_H_
+
+/* define this as unsigned long (DWORD) on win32 so we
+   don't force the user to include <windows.h> or <IntBase.h> */
+#ifdef _WIN32
+namespace animone::internal {
+typedef unsigned long pid_t;
+}
+#else
+/* <sys/types.h> shouldn't be that big, right? */
+#	include <sys/types.h>
+namespace animone::internal {
+typedef ::pid_t pid_t;
+}
+#endif
+
+#endif // ANIMONE_ANIMONE_TYPES_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/util.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,25 @@
+#ifndef ANIMONE_ANIMONE_UTIL_H_
+#define ANIMONE_ANIMONE_UTIL_H_
+
+#include <sstream>
+#include <string>
+
+namespace animone::internal::util {
+
+bool ReadFile(const std::string& path, std::string& data);
+bool EqualStrings(const std::string& str1, const std::string& str2);
+bool Stem(const std::string& filename, std::string& stem);
+bool CheckPattern(const std::string& pattern, const std::string& str);
+bool TrimLeft(std::string& str, const char* chars);
+bool TrimRight(std::string& str, const char* chars);
+
+template<typename T = int, std::enable_if_t<std::is_integral<T>::value, bool> = true>
+T StringToInt(const std::string& str, T def = 0) {
+	std::istringstream s(str);
+	s >> std::noboolalpha >> def;
+	return def;
+}
+
+} // namespace animone::internal::util
+
+#endif // ANIMONE_ANIMONE_UTIL_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/util/osx.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,40 @@
+#ifndef ANIMONE_ANIMONE_UTIL_OSX_H_
+#define ANIMONE_ANIMONE_UTIL_OSX_H_
+
+#include "animone/types.h"
+#include <cstdint>
+#include <string>
+
+#include <CoreFoundation/CoreFoundation.h>
+
+namespace animone::internal::osx::util {
+
+template<typename T>
+bool GetCFNumber(CFNumberRef num, T& result) {
+	if (!num)
+		return false;
+
+	int64_t res;
+	if (!CFNumberGetValue(num, static_cast<CFNumberType>(4), &res))
+		return false;
+
+	result = static_cast<T>(res);
+	return true;
+}
+
+template<typename T>
+struct CFDeconstructor {
+	using pointer = T;
+	void operator()(pointer t) const { ::CFRelease(t); };
+};
+
+template<typename T>
+using CFPtr = vector<T, CFDecontructor<T>>; // type-id is vector<T, Alloc<T>>
+
+bool StringFromCFString(CFStringRef string, std::string& result);
+
+bool GetProcessName(pid_t pid, std::string& result);
+
+} // namespace animone::internal::osx::util
+
+#endif // ANIMONE_ANIMONE_UTIL_OSX_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/util/win32.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,33 @@
+#ifndef ANIMONE_ANIMONE_UTIL_WIN32_H_
+#define ANIMONE_ANIMONE_UTIL_WIN32_H_
+
+#include <subauth.h>
+#include <windows.h>
+
+#include <memory>
+#include <string>
+
+namespace animone::internal::win32 {
+
+struct HandleDeconstructor {
+	using pointer = HANDLE;
+	void operator()(pointer t) const { ::CloseHandle(t); };
+};
+
+using Handle = std::unique_ptr<HANDLE, HandleDeconstructor>;
+
+/* ----------------------------------------------- */
+
+std::string ToUtf8String(const std::wstring& string);
+std::string ToUtf8String(const UNICODE_STRING& string);
+std::wstring ToWstring(const std::string& string);
+
+std::wstring GetFileNameFromPath(const std::wstring& path);
+std::wstring GetFileNameWithoutExtension(const std::wstring& filename);
+
+bool IsSystemDirectory(const std::string& path);
+bool IsSystemDirectory(std::wstring path);
+
+} // namespace animone::internal::win32
+
+#endif // ANIMONE_ANIMONE_UTIL_WIN32_H_
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/win.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,22 @@
+#ifndef ANIMONE_ANIMONE_WIN_H_
+#define ANIMONE_ANIMONE_WIN_H_
+
+#include <functional>
+#include <string>
+
+namespace animone {
+
+struct Process;
+struct Window;
+
+namespace internal {
+
+using window_proc_t = std::function<bool(const Process&, const Window&)>;
+
+bool EnumerateWindows(window_proc_t window_proc);
+
+} // namespace internal
+
+} // namespace animone
+
+#endif // ANIMONE_ANIMONE_WIN_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/win/quartz.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,12 @@
+#ifndef ANIMONE_ANIMONE_WIN_QUARTZ_H_
+#define ANIMONE_ANIMONE_WIN_QUARTZ_H_
+
+#include "animone/win.h"
+
+namespace animone::internal::quartz {
+
+bool EnumerateWindows(window_proc_t window_proc);
+
+} // namespace animone::internal::quartz
+
+#endif // ANIMONE_ANIMONE_WIN_QUARTZ_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/win/win32.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,12 @@
+#ifndef ANIMONE_ANIMONE_WIN_WIN32_H_
+#define ANIMONE_ANIMONE_WIN_WIN32_H_
+
+#include "animone/win.h"
+
+namespace animone::internal::win32 {
+
+bool EnumerateWindows(window_proc_t window_proc);
+
+} // namespace animone::internal::win32
+
+#endif // ANIMONE_ANIMONE_WIN_WIN32_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/animone/win/x11.h	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,12 @@
+#ifndef ANIMONE_ANIMONE_WIN_X11_H_
+#define ANIMONE_ANIMONE_WIN_X11_H_
+
+#include "animone/win.h"
+
+namespace animone::internal::x11 {
+
+bool EnumerateWindows(window_proc_t window_proc);
+
+} // namespace animone::internal::x11
+
+#endif // ANIMONE_ANIMONE_WIN_X11_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/animone.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,76 @@
+#include "animone.h"
+#include "animone/fd.h"
+#include "animone/strategies.h"
+#include "animone/types.h"
+#include "animone/util.h"
+#include "animone/win.h"
+
+#include <set>
+#include <string>
+#include <vector>
+
+namespace animone {
+
+namespace internal {
+
+static bool IsExecutableInList(const Player& player, const std::string& name) {
+	std::string stem;
+#ifdef WIN32
+	if (!util::Stem(name, stem))
+#endif
+		stem = name;
+
+	for (const auto& pattern : player.executables)
+		if (util::CheckPattern(pattern, stem))
+			return true;
+
+	return false;
+}
+
+static bool IsWindowInList(const Player& player, const Window& window) {
+	for (const auto& pattern : player.windows)
+		if (util::CheckPattern(pattern, window.class_name))
+			return true;
+
+	return false;
+}
+
+static bool PlayerHasStrategy(const Player& player, const Strategy& strategy) {
+	for (const auto& pstrategy : player.strategies)
+		if (pstrategy == strategy)
+			return true;
+
+	return false;
+}
+
+} // namespace internal
+
+bool GetResults(const std::vector<Player>& players, std::vector<Result>& results) {
+	auto window_proc = [&](const Process& process, const Window& window) -> bool {
+		for (const auto& player : players) {
+			if (internal::IsWindowInList(player, window))
+				results.push_back({player, process, window, {}});
+		}
+
+		return true;
+	};
+
+	if (internal::EnumerateWindows(window_proc))
+		return internal::ApplyStrategies(results);
+
+	/* fallback, enumerate over open processes instead */
+	auto process_proc = [&](const Process& process) -> bool {
+		for (const auto& player : players)
+			if (internal::IsExecutableInList(player, process.name))
+				results.push_back({player, process, {}, {}});
+
+		return true;
+	};
+
+	if (internal::EnumerateOpenProcesses(process_proc))
+		return internal::ApplyStrategies(results);
+
+	return false;
+}
+
+} // namespace animone
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fd.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,88 @@
+#include "animone/fd.h"
+
+#ifdef WIN32
+#	include "animone/fd/win32.h"
+#endif
+
+#ifdef LINUX
+#	include "animone/fd/proc.h"
+#endif
+
+#ifdef MACOSX
+#	include "animone/fd/xnu.h"
+#	include "animone/util/osx.h"
+#endif
+
+#ifdef LIBKVM
+#	include "animone/fd/kvm.h"
+#endif
+
+namespace animone::internal {
+
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
+	bool success = false;
+
+#ifdef WIN32
+	success ^= win32::EnumerateOpenFiles(pids, open_file_proc);
+#endif
+
+#ifdef LINUX
+	success ^= proc::EnumerateOpenFiles(pids, open_file_proc);
+#endif
+
+#ifdef MACOSX
+	success ^= xnu::EnumerateOpenFiles(pids, open_file_proc);
+#endif
+
+#ifdef LIBKVM
+	success ^= kvm::EnumerateOpenFiles(pids, open_file_proc);
+#endif
+
+	return success;
+}
+
+bool EnumerateOpenProcesses(process_proc_t process_proc) {
+	bool success = false;
+
+#ifdef WIN32
+	success ^= win32::EnumerateOpenProcesses(process_proc);
+#endif
+
+#ifdef LINUX
+	success ^= proc::EnumerateOpenProcesses(process_proc);
+#endif
+
+#ifdef MACOSX
+	success ^= xnu::EnumerateOpenProcesses(process_proc);
+#endif
+
+#ifdef LIBKVM
+	success ^= kvm::EnumerateOpenProcesses(process_proc);
+#endif
+
+	return success;
+}
+
+bool GetProcessName(pid_t pid, std::string& name) {
+	bool success = false;
+
+#ifdef WIN32
+	success ^= win32::GetProcessName(pid, name);
+#endif
+
+#ifdef LINUX
+	success ^= proc::GetProcessName(pid, name);
+#endif
+
+#ifdef MACOSX
+	success ^= osx::util::GetProcessName(pid, name);
+#endif
+
+#ifdef LIBKVM
+	success ^= kvm::GetProcessName(pid, name);
+#endif
+
+	return success;
+}
+
+} // namespace animone::internal
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fd/kvm.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,151 @@
+/* kvm.cc: provides support for *BSD.
+ */
+
+#include "animone/fd/kvm.h"
+#include "animone.h"
+#include "animone/fd.h"
+
+#include <sys/file.h>
+#include <sys/filedesc.h>
+#include <sys/param.h>
+#include <sys/queue.h>
+#include <sys/sysctl.h>
+#include <sys/types.h>
+#include <sys/user.h>
+#include <sys/vnode.h>
+
+#include <kvm.h>
+#ifdef LIBUTIL
+#	include <libutil.h>
+#endif
+
+#include <string>
+
+namespace animone::internal::kvm {
+
+bool GetProcessName(pid_t pid, std::string& name) {
+	char errbuf[_POSIX2_LINE_MAX];
+	kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf);
+	if (!kernel)
+		return false;
+
+	int entries = 0;
+	struct kinfo_proc* kinfo = kvm_getprocs(kernel, KERN_PROC_PID, pid, &entries);
+	if (!kinfo) {
+		kvm_close(kernel);
+		return false;
+	}
+
+	if (entries < 1) {
+		kvm_close(kernel);
+		return false;
+	}
+
+	name = kinfo[0].ki_paddr->p_comm;
+
+	return true;
+}
+
+/* Most of the BSDs share the common kvm library,
+ * so accessing this information can be trivial.
+ */
+bool EnumerateOpenProcesses(process_proc_t process_proc) {
+	char errbuf[_POSIX2_LINE_MAX];
+	kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf);
+	if (!kernel)
+		return false;
+
+	int entries = 0;
+	struct kinfo_proc* kinfo = kvm_getprocs(kernel, KERN_PROC_ALL, 0, &entries);
+	if (!kinfo) {
+		kvm_close(kernel);
+		return false;
+	}
+
+	for (int i = 0; i < entries; i++) {
+		if (!process_proc({kinfo[i].ki_paddr->p_pid, kinfo[i].ki_paddr->p_comm})) {
+			kvm_close(kernel);
+			return false;
+		}
+	}
+
+	kvm_close(kernel);
+
+	return true;
+}
+
+bool EnumerateOpenFiles(std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
+#ifdef __OpenBSD__
+	char errbuf[_POSIX2_LINE_MAX];
+	kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf);
+	if (!kernel)
+		return false;
+
+	for (const auto& pid : pids) {
+		int cnt;
+		struct kinfo_file* kfile = kvm_getfiles(kernel, KERN_FILE_BYPID, pid, &cnt);
+		if (!kfile) {
+			kvm_close(kernel);
+			return false;
+		}
+
+		for (int i = 0; i < cnt; i++) {
+			if (!open_file_proc({pid, kfile[i].kf_path})) {
+				kvm_close(kernel);
+				return false;
+			}
+		}
+	}
+
+	kvm_close(kernel);
+
+	return true;
+#elif defined(LIBUTIL)
+	/* does this code even work? */
+	for (const auto& pid : pids) {
+		int cnt;
+		std::unique_ptr<struct kinfo_file[]> files(kinfo_getfile(pid, &cnt));
+		if (!files)
+			return false;
+
+		for (int i = 0; i < cnt; i++) {
+			const struct kinfo_file& current = files[i];
+			if (current.kf_vnode_type != KF_VTYPE_VREG)
+				continue;
+
+			if (!open_file_proc({pid, current.kf_path}))
+				return false;
+		}
+	}
+
+	return true;
+#elif defined(__NetBSD__)
+	for (const auto& pid : pids) {
+		int mib[6] = {CTL_KERN, KERN_FILE2, KERN_FILE_BYPID, pid, sizeof(struct kinfo_file), 0};
+
+		size_t len = 0;
+		if (sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &len, NULL, 0) == -1)
+			return false;
+
+		mib[5] = len / sizeof(struct kinfo_file);
+
+		std::unique_ptr<struct kinfo_file[]> buf(new struct kinfo_file[mib[5]]);
+		if (!buf)
+			return false;
+
+		if (sysctl(mib, sizeof(mib) / sizeof(mib[0]), buf.get(), &len, NULL, 0) == -1)
+			return false;
+
+		/* TODO: check kfile[i].ki_ofileflags */
+		for (size_t i = 0; i < mib[5]; i++)
+			if (!open_file_proc({pid, kfile[i].kf_path}))
+				return false;
+	}
+
+	return true;
+#else
+	return false;
+#endif
+}
+
+} // namespace animone::internal::kvm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fd/proc.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,138 @@
+#include "animone/fd/proc.h"
+#include "animone.h"
+#include "animone/util.h"
+
+#include <filesystem>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+static constexpr std::string_view PROC_LOCATION = "/proc";
+
+namespace animone::internal::proc {
+
+static bool IsRegularFile(std::string link) {
+	struct stat sb;
+	if (stat(link.c_str(), &sb) == -1)
+		return false;
+
+	return S_ISREG(sb.st_mode);
+}
+
+static bool AreFlagsOk(pid_t pid, int fd) {
+	const std::filesystem::path path =
+	    std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fdinfo" / std::to_string(fd);
+
+	std::ifstream file(path);
+	if (!file)
+		return false;
+
+	int flags = 0;
+	for (std::string line; std::getline(file, line);)
+		if (line.find("flags:", 0) == 0)
+			flags = util::StringToInt(line.substr(line.find_last_not_of("0123456789") + 1));
+
+	if (flags & O_WRONLY || flags & O_RDWR)
+		return false;
+
+	return true;
+}
+
+static bool GetFilenameFromFd(std::string link, std::string& out) {
+	/* /proc is a "virtual filesystem", so we have to guess the path size. yippee! */
+	constexpr size_t OUT_MAX = (1ul << 15); // 32KiB
+	out.resize(32);
+
+	ssize_t exe_used = 0;
+	do {
+		out.resize(out.length() * 2);
+
+		exe_used = readlink(link.c_str(), &out.front(), out.length());
+		if (exe_used == (ssize_t)-1 || exe_used < (ssize_t)1)
+			return false; // we got a bad result. SAD!
+	} while (out.length() < OUT_MAX && exe_used >= static_cast<ssize_t>(out.length()));
+
+	out.resize(out.find('\0'));
+
+	return true;
+}
+
+static bool IsSystemFile(const std::string& path) {
+	static constexpr std::array<std::string_view, 9> invalid_paths = {"/boot", "/dev", "/bin", "/usr", "/opt",
+	                                                                  "/proc", "/var", "/etc", "/dev"};
+
+	for (const auto& invalid_path : invalid_paths) {
+		if (!path.rfind(invalid_path, 0)) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool GetProcessName(pid_t pid, std::string& result) {
+	const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "comm";
+
+	if (!util::ReadFile(path, result))
+		return false;
+
+	result.erase(std::remove(result.begin(), result.end(), '\n'), result.end());
+	return true;
+}
+
+bool EnumerateOpenProcesses(process_proc_t process_proc) {
+	bool success = false;
+
+	for (const auto& dir : std::filesystem::directory_iterator{PROC_LOCATION}) {
+		Process proc;
+
+		try {
+			proc.pid = util::StringToInt(dir.path().stem());
+			success = true;
+		} catch (std::invalid_argument const& ex) {
+			continue;
+		}
+
+		if (!GetProcessName(proc.pid, proc.name))
+			continue;
+
+		if (!process_proc(proc))
+			return false;
+	}
+
+	return success;
+}
+
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
+	if (!open_file_proc)
+		return false;
+
+	for (const auto& pid : pids) {
+		const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fd";
+
+		for (const auto& dir : std::filesystem::directory_iterator{path}) {
+			if (!AreFlagsOk(pid, util::StringToInt(dir.path().stem())))
+				continue;
+
+			std::string name;
+			if (!GetFilenameFromFd(dir.path(), name))
+				continue;
+
+			if (!IsRegularFile(name))
+				continue;
+
+			if (IsSystemFile(name))
+				continue;
+
+			if (!open_file_proc({pid, name}))
+				return false;
+		}
+	}
+	return true;
+}
+
+} // namespace animia::internal::proc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fd/win32.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,259 @@
+#include "animone/fd/win32.h"
+#include "animone.h"
+#include "animone/util/win32.h"
+
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <fileapi.h>
+#include <handleapi.h>
+#include <libloaderapi.h>
+#include <ntdef.h>
+#include <psapi.h>
+#include <shlobj.h>
+#include <stringapiset.h>
+#include <tlhelp32.h>
+#include <windows.h>
+#include <winternl.h>
+
+/* This file is noticably more complex than Unix and Linux, and that's because
+ * there is no "simple" way to get the paths of a file. In fact, this thing requires
+ * you to use *internal functions* that can't even be linked to, hence why we have to
+ * use GetProcAddress and such. What a mess.
+ *
+ * Speaking of which, because this file uses internal functions of the OS, it is not
+ * guaranteed to work far into the future. However, it has worked since NT 6.0 (Vista)
+ * at least, so it's unlikely to be changed much ever.
+ */
+
+/* SystemExtendedHandleInformation is only available in NT 5.1+ (XP and higher) and provides information for
+ * 32-bit PIDs, unlike SystemHandleInformation
+ *
+ * TODO: implement SystemHandleInformation for systems older than XP
+ */
+static constexpr SYSTEM_INFORMATION_CLASS SystemExtendedHandleInformation = static_cast<SYSTEM_INFORMATION_CLASS>(0x40);
+static constexpr SYSTEM_INFORMATION_CLASS SystemHandleInformation = static_cast<SYSTEM_INFORMATION_CLASS>(0x10);
+static constexpr NTSTATUS STATUS_INFO_LENGTH_MISMATCH = 0xC0000004UL;
+
+/* this is filled in at runtime because it's not guaranteed to be (and isn't)
+ * constant between different versions of Windows */
+static unsigned short file_type_index = 0;
+
+struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX {
+	PVOID Object;
+	ULONG_PTR UniqueProcessId;
+	HANDLE HandleValue;
+	ACCESS_MASK GrantedAccess;
+	USHORT CreatorBackTraceIndex;
+	USHORT ObjectTypeIndex;
+	ULONG HandleAttributes;
+	ULONG Reserved;
+};
+
+struct SYSTEM_HANDLE_INFORMATION_EX {
+	ULONG_PTR NumberOfHandles;
+	ULONG_PTR Reserved;
+	SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1];
+};
+
+namespace animone::internal::win32 {
+
+class Ntdll {
+public:
+	Ntdll() {
+		ntdll = ::GetModuleHandleW(L"ntdll.dll");
+		nt_query_system_information = reinterpret_cast<decltype(::NtQuerySystemInformation)*>(
+		    ::GetProcAddress(ntdll, "NtQuerySystemInformation"));
+		nt_query_object = reinterpret_cast<decltype(::NtQueryObject)*>(::GetProcAddress(ntdll, "NtQueryObject"));
+	}
+
+	NTSTATUS QuerySystemInformation(SYSTEM_INFORMATION_CLASS cls, PVOID sysinfo, ULONG len,
+	                                PULONG retlen){return nt_query_system_information(cls, sysinfo, len, retlen)}
+
+	NTSTATUS QueryObject(HANDLE handle, OBJECT_INFORMATION_CLASS cls, PVOID objinf, ULONG objinflen, PULONG retlen) {
+		return nt_query_object(handle, cls, objinf, objinflen, retlen);
+	}
+
+private:
+	HMODULE ntdll;
+	decltype(::NtQuerySystemInformation)* nt_query_system_information;
+	decltype(::NtQueryObject)* nt_query_object;
+
+}
+
+Ntdll ntdll;
+
+static HANDLE DuplicateHandle(HANDLE process_handle, HANDLE handle) {
+	HANDLE dup_handle = nullptr;
+	const bool result =
+	    ::DuplicateHandle(process_handle, handle, ::GetCurrentProcess(), &dup_handle, 0, false, DUPLICATE_SAME_ACCESS);
+	return result ? dup_handle : nullptr;
+}
+
+static std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> GetSystemHandleInformation() {
+	/* we should really put a cap on this */
+	ULONG cb = 1 << 19;
+	NTSTATUS status = STATUS_NO_MEMORY;
+	std::unique_ptr<SYSTEM_HANDLE_INFORMATION_EX> info(malloc(cb));
+
+	do {
+		info.reset(malloc(cb *= 2));
+		if (!info)
+			continue;
+
+		status = ntdll.QuerySystemInformation(SystemExtendedHandleInformation, info.get(), cb, &cb);
+	} while (status == STATUS_INFO_LENGTH_MISMATCH);
+
+	if (!NT_SUCCESS(status))
+		return {};
+
+	std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> res;
+
+	ULONG_PTR handles = info->NumberOfHandles;
+	if (!handles)
+		return {};
+
+	res.reserve(handles);
+
+	SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX* entry = info->Handles;
+	do {
+		if (entry)
+			res.push_back(*(entry++));
+	} while (--handles);
+
+	return res;
+}
+
+static std::wstring GetHandleType(HANDLE handle) {
+	OBJECT_TYPE_INFORMATION info = {0};
+	ntdll.QueryObject(handle, ObjectTypeInformation, &info, sizeof(info), NULL);
+	return std::wstring(info.TypeName.Buffer, info.TypeName.Length);
+}
+
+static std::wstring GetFinalPathNameByHandle(HANDLE handle) {
+	std::wstring buffer;
+
+	DWORD size = ::GetFinalPathNameByHandleW(handle, NULL, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
+	buffer.resize(size);
+	::GetFinalPathNameByHandleW(handle, &buffer.front(), buffer.size(), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
+
+	return buffer;
+}
+
+static bool IsFileHandle(HANDLE handle, unsigned short object_type_index) {
+	if (file_type_index)
+		return object_type_index == file_type_index;
+	else if (!handle)
+		return true;
+	else if (GetHandleType(handle) == L"File") {
+		file_type_index = object_type_index;
+		return true;
+	}
+	return false;
+}
+
+static bool IsFileMaskOk(ACCESS_MASK access_mask) {
+	if (!(access_mask & FILE_READ_DATA))
+		return false;
+
+	if ((access_mask & FILE_APPEND_DATA) || (access_mask & FILE_WRITE_EA) || (access_mask & FILE_WRITE_ATTRIBUTES))
+		return false;
+
+	return true;
+}
+
+static bool IsFilePathOk(const std::wstring& path) {
+	if (path.empty())
+		return false;
+
+	if (IsSystemDirectory(path))
+		return false;
+
+	const auto file_attributes = GetFileAttributesW(path.c_str());
+	if ((file_attributes == INVALID_FILE_ATTRIBUTES) || (file_attributes & FILE_ATTRIBUTE_DIRECTORY))
+		return false;
+
+	return true;
+}
+
+bool GetProcessName(pid_t pid, std::string& name) {
+	std::wstring wname = GetProcessPath();
+	if (wname.empty())
+		return false;
+
+	return ToUtf8String(GetFileNameWithoutExtension(GetFileNameFromPath(wname)));
+}
+
+bool EnumerateOpenProcesses(process_proc_t process_proc) {
+	HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+	if (hProcessSnap == INVALID_HANDLE_VALUE)
+		return false;
+
+	PROCESSENTRY32 pe32;
+	pe32.dwSize = sizeof(PROCESSENTRY32);
+
+	if (!::Process32First(hProcessSnap, &pe32))
+		return false;
+
+	if (!process_proc({pe32.th32ProcessID, pe32.szExeFile}))
+		return false;
+
+	while (::Process32Next(hProcessSnap, &pe32))
+		if (!process_proc({pe32.th32ProcessID, pe32.szExeFile}))
+			return false;
+
+	::CloseHandle(hProcessSnap);
+
+	return true;
+}
+
+/* this could be changed to being a callback, but... I'm too lazy right now :) */
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
+	if (!open_file_proc)
+		return false;
+
+	std::unordered_map<pid_t, Handle> proc_handles;
+
+	for (const pid_t& pid : pids) {
+		const HANDLE handle = ::OpenProcess(PROCESS_DUP_HANDLE, false, pid);
+		if (handle != INVALID_HANDLE_VALUE)
+			proc_handles[pid] = Handle(handle);
+	}
+
+	if (proc_handles.empty())
+		return false;
+
+	std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> info = GetSystemHandleInformation();
+
+	for (const auto& h : info) {
+		const pid_t pid = h.UniqueProcessId;
+		if (!pids.count(pid))
+			continue;
+
+		if (!IsFileHandle(nullptr, h.ObjectTypeIndex))
+			continue;
+
+		if (!IsFileMaskOk(h.GrantedAccess))
+			continue;
+
+		Handle handle(DuplicateHandle(proc_handles[pid].get(), h.HandleValue));
+		if (handle.get() == INVALID_HANDLE_VALUE)
+			continue;
+
+		if (GetFileType(handle.get()) != FILE_TYPE_DISK)
+			continue;
+
+		const std::wstring path = GetFinalPathNameByHandle(handle.get());
+		if (!IsFilePathOk(path))
+			continue;
+
+		if (!open_file_proc({pid, ToUtf8String(path)}))
+			return false;
+	}
+
+	return true;
+}
+
+} // namespace animone::internal::win32
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fd/xnu.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,84 @@
+#include "animone/fd/xnu.h"
+#include "animone.h"
+#include "animone/util/osx.h"
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <fcntl.h>
+#include <libproc.h>
+#include <sys/sysctl.h>
+#include <sys/types.h>
+#include <sys/user.h>
+
+namespace animone::internal::xnu {
+
+bool EnumerateOpenProcesses(process_proc_t process_proc) {
+	size_t pids_size = 256;
+	std::unique_ptr<pid_t[]> pids;
+
+	int returned_size = 0;
+	do {
+		pids.reset(new pid_t[pids_size *= 2]);
+		returned_size = proc_listpids(PROC_ALL_PIDS, 0, pids.get(), pids_size * sizeof(pid_t));
+		if (returned_size == -1)
+			return false;
+	} while ((pids_size * sizeof(size_t)) < returned_size);
+
+	for (int i = 0; i < pids_size; i++) {
+		std::string result;
+		osx::util::GetProcessName(pids[i], result);
+		if (!process_proc({pids[i], result}))
+			return false;
+	}
+
+	return true;
+}
+
+bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
+	if (!open_file_proc)
+		return false;
+
+	for (const auto& pid : pids) {
+		const int bufsz = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0);
+		if (bufsz < 0)
+			return false;
+
+		const size_t info_len = bufsz / sizeof(struct proc_fdinfo);
+		if (info_len < 1)
+			return false;
+
+		std::unique_ptr<struct proc_fdinfo[]> info(new struct proc_fdinfo[info_len]);
+		if (!info)
+			return false;
+
+		proc_pidinfo(pid, PROC_PIDLISTFDS, 0, info.get(), bufsz);
+
+		for (size_t i = 0; i < info_len; i++) {
+			if (info[i].proc_fdtype == PROX_FDTYPE_VNODE) {
+				struct vnode_fdinfowithpath vnodeInfo;
+
+				int sz = proc_pidfdinfo(pid, info[i].proc_fd, PROC_PIDFDVNODEPATHINFO, &vnodeInfo,
+				                        PROC_PIDFDVNODEPATHINFO_SIZE);
+				if (sz != PROC_PIDFDVNODEPATHINFO_SIZE)
+					return false;
+
+				// This doesn't work (for unknown reasons). I assume somethings fucked up with
+				// my assumptions; I don't care enough to look into it tbh
+				//
+				// if (vnodeInfo.pfi.fi_openflags & O_WRONLY || vnodeInfo.pfi.fi_openflags & O_RDWR)
+				//     continue;
+
+				if (!open_file_proc({pid, vnodeInfo.pvip.vip_path}))
+					return false;
+			}
+		}
+	}
+
+	return true;
+}
+
+} // namespace animone::internal::xnu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/player.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,181 @@
+#include "animone/player.h"
+#include "animone/util.h"
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace animone {
+
+namespace internal::parser {
+
+enum class State {
+	ExpectPlayerName,
+	ExpectSection,
+	ExpectWindow,
+	ExpectExecutable,
+	ExpectStrategy,
+	ExpectType,
+	ExpectWindowTitle,
+};
+
+size_t GetIndentation(const std::string& line) {
+	return line.find_first_not_of('\t');
+}
+
+bool HandleIndentation(const size_t current, const std::vector<Player>& players, State& state) {
+	// Each state has a definitive expected indentation
+	const auto expected = [&state]() -> size_t {
+		switch (state) {
+			default:
+			case State::ExpectPlayerName: return 0;
+			case State::ExpectSection: return 1;
+			case State::ExpectWindow:
+			case State::ExpectExecutable:
+			case State::ExpectStrategy:
+			case State::ExpectType: return 2;
+			case State::ExpectWindowTitle: return 3;
+		}
+	}();
+
+	if (current > expected)
+		return false; // Disallow excessive indentation
+
+	if (current < expected) {
+		auto fix_state = [&]() { state = !current ? State::ExpectPlayerName : State::ExpectSection; };
+		switch (state) {
+			case State::ExpectWindow:
+				if (players.back().windows.empty())
+					return false;
+				fix_state();
+				break;
+			case State::ExpectExecutable:
+				if (players.back().executables.empty())
+					return false;
+				fix_state();
+				break;
+			case State::ExpectStrategy:
+				if (players.back().strategies.empty())
+					return false;
+				fix_state();
+				break;
+			case State::ExpectType: fix_state(); break;
+			case State::ExpectWindowTitle: return false;
+		}
+	}
+
+	return true;
+}
+
+bool HandleState(std::string& line, std::vector<Player>& players, State& state) {
+	switch (state) {
+		case State::ExpectPlayerName:
+			players.push_back(Player());
+			players.back().name = line;
+			state = State::ExpectSection;
+			break;
+
+		case State::ExpectSection: {
+			static const std::map<std::string, State> sections = {
+			    {"windows",     State::ExpectWindow    },
+			    {"executables", State::ExpectExecutable},
+			    {"strategies",  State::ExpectStrategy  },
+			    {"type",        State::ExpectType      },
+			};
+			util::TrimRight(line, ":");
+			const auto it = sections.find(line);
+			if (it == sections.end())
+				return false;
+			state = it->second;
+			break;
+		}
+
+		case State::ExpectWindow: players.back().windows.push_back(line); break;
+
+		case State::ExpectExecutable: players.back().executables.push_back(line); break;
+
+		case State::ExpectStrategy: {
+			static const std::map<std::string, Strategy> strategies = {
+			    {"window_title",  Strategy::WindowTitle },
+			    {"open_files",    Strategy::OpenFiles   },
+			    {"ui_automation", Strategy::UiAutomation},
+			};
+			util::TrimRight(line, ":");
+			const auto it = strategies.find(line);
+			if (it == strategies.end())
+				return false;
+			const auto strategy = it->second;
+			players.back().strategies.push_back(strategy);
+			switch (strategy) {
+				case Strategy::WindowTitle: state = State::ExpectWindowTitle; break;
+			}
+			break;
+		}
+
+		case State::ExpectType: {
+			static const std::map<std::string, PlayerType> types = {
+			    {"default",     PlayerType::Default   },
+			    {"web_browser", PlayerType::WebBrowser},
+			};
+			const auto it = types.find(line);
+			if (it == types.end())
+				return false;
+			players.back().type = it->second;
+			break;
+		}
+
+		case State::ExpectWindowTitle:
+			players.back().window_title_format = line;
+			state = State::ExpectStrategy;
+			break;
+	}
+
+	return true;
+}
+
+} // namespace internal::parser
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool ParsePlayersData(const std::string& data, std::vector<Player>& players) {
+	if (data.empty())
+		return false;
+
+	std::istringstream stream(data);
+	std::string line;
+	size_t indentation = 0;
+	auto state = internal::parser::State::ExpectPlayerName;
+
+	while (std::getline(stream, line, '\n')) {
+		if (line.empty())
+			continue; // Ignore empty lines
+
+		indentation = internal::parser::GetIndentation(line);
+
+		internal::util::TrimLeft(line, "\t");
+		internal::util::TrimRight(line, "\n\r");
+
+		if (line.empty() || line.front() == '#')
+			continue; // Ignore empty lines and comments
+
+		if (!internal::parser::HandleIndentation(indentation, players, state))
+			return false;
+
+		if (!internal::parser::HandleState(line, players, state))
+			return false;
+	}
+
+	return !players.empty();
+}
+
+bool ParsePlayersFile(const std::string& path, std::vector<Player>& players) {
+	std::string data;
+
+	if (!internal::util::ReadFile(path, data))
+		return false;
+
+	return ParsePlayersData(data, players);
+}
+
+} // namespace animone
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/strategist.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,111 @@
+#include <regex>
+
+#include "animone.h"
+#include "animone/fd.h"
+#include "animone/strategies.h"
+#include "animone/util.h"
+
+#include <iostream>
+
+/* this was STUPIDLY slow in Anisthesia, oops! */
+
+namespace animone::internal {
+
+static bool ApplyWindowTitleFormat(const std::string& format, std::string& title) {
+	if (format.empty())
+		return false;
+
+	const std::regex pattern(format);
+	std::smatch match;
+	std::regex_match(title, match, pattern);
+
+	// Use the first non-empty match result, because the regular expression may
+	// contain multiple sub-expressions.
+	for (size_t i = 1; i < match.size(); ++i) {
+		if (!match.str(i).empty()) {
+			title = match.str(i);
+			return true;
+		}
+	}
+
+	// Results are empty, but the match was successful
+	if (!match.empty()) {
+		title.clear();
+		return true;
+	}
+
+	return true;
+}
+
+static MediaInfoType InferMediaInformationType(const std::string& str) {
+	const std::regex path_pattern(R"(^(?:[A-Za-z]:[/\\]|\\\\)[^<>:"/\\|?*]+)");
+	return (std::regex_search(str, path_pattern)) ? MediaInfoType::File : MediaInfoType::Unknown;
+}
+
+static bool AddMedia(Result& result, const MediaInfo media_information) {
+	if (media_information.value.empty())
+		return false;
+
+	Media media;
+	media.information.push_back(media_information);
+	result.media.push_back(std::move(media));
+
+	return true;
+}
+
+static bool ApplyWindowTitleStrategy(std::vector<Result>& results) {
+	bool success = false;
+
+	for (auto& result : results) {
+		auto title = result.window.text;
+		if (title.empty())
+			continue;
+
+		ApplyWindowTitleFormat(result.player.window_title_format, title);
+
+		success |= AddMedia(result, {InferMediaInformationType(title), title});
+	}
+
+	return success;
+}
+
+static bool ApplyOpenFilesStrategy(std::vector<Result>& results) {
+	bool success = false;
+
+	/* map pids to our results, saves time with open_file_proc */
+	std::unordered_map<pid_t, Result*> pid_map;
+	pid_map.reserve(results.size());
+
+	std::set<pid_t> pids;
+
+	for (Result& result : results) {
+		const pid_t pid = result.process.pid;
+		if (!pid)
+			continue;
+
+		pid_map.insert({pid, &result});
+		pids.insert(pid);
+	}
+
+	auto open_file_proc = [&](const OpenFile& file) -> bool {
+		success |= AddMedia(*pid_map[file.pid], {MediaInfoType::File, file.path});
+		return true;
+	};
+
+	EnumerateOpenFiles(pids, open_file_proc);
+
+	return success;
+}
+
+bool ApplyStrategies(std::vector<Result>& results) {
+	bool success = false;
+
+	success |= ApplyWindowTitleStrategy(results);
+	success |= ApplyOpenFilesStrategy(results);
+
+	return success;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace animone::internal
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,85 @@
+#include <algorithm>
+#include <fstream>
+#include <regex>
+#include <sstream>
+#include <string>
+
+#include "animone/util.h"
+
+namespace animone::internal::util {
+
+bool ReadFile(const std::string& path, std::string& data) {
+	std::ifstream file(path.c_str(), std::ios::in | std::ios::binary);
+	if (!file)
+		return false;
+
+	std::ostringstream string;
+	string << file.rdbuf();
+	file.close();
+
+	data = string.str();
+
+	return true;
+}
+
+/* this assumes ASCII... which really should be the case for what we need, anyway */
+bool EqualStrings(const std::string& str1, const std::string& str2) {
+	auto tolower = [](const char c) -> char { return ('A' <= c && c <= 'Z') ? c + ('a' - 'A') : c; };
+
+	auto equal_chars = [&tolower](const char c1, const char c2) -> bool { return tolower(c1) == tolower(c2); };
+
+	return str1.length() == str2.length() && std::equal(str1.begin(), str1.end(), str2.begin(), equal_chars);
+}
+
+bool Stem(const std::string& filename, std::string& stem) {
+	unsigned long long pos = filename.find_last_of(".");
+	if (pos != std::string::npos)
+		return false;
+
+	stem = filename.substr(0, pos);
+	return true;
+}
+
+bool CheckPattern(const std::string& pattern, const std::string& str) {
+	if (pattern.empty())
+		return false;
+	if (pattern.front() == '^' && std::regex_match(str, std::regex(pattern)))
+		return true;
+	return util::EqualStrings(pattern, str);
+}
+
+bool TrimLeft(std::string& str, const char* chars) {
+	if (str.empty())
+		return false;
+
+	const auto found = str.find_first_not_of(chars);
+
+	if (found == 0)
+		return false;
+
+	if (found == std::string::npos)
+		str.clear();
+	else
+		str.erase(0, found);
+
+	return true;
+}
+
+bool TrimRight(std::string& str, const char* chars) {
+	if (str.empty())
+		return false;
+
+	const auto found = str.find_last_not_of(chars);
+
+	if (found == str.size() - 1)
+		return false;
+
+	if (found == std::string::npos)
+		str.clear();
+	else
+		str.resize(found + 1);
+
+	return true;
+}
+
+} // namespace animone::internal::util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/osx.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,196 @@
+#include "animone/util/osx.h"
+
+#include <memory>
+#include <string>
+
+#include <libproc.h>
+#include <sys/sysctl.h>
+
+namespace animone::internal::osx::util {
+
+typedef CFTypeRef (*LSASNCreateWithPidSpec)(CFAllocatorRef, pid_t);
+typedef CFDictionaryRef (*LSCopyApplicationInformationSpec)(int, CFTypeRef, CFArrayRef);
+
+/* retrieved dynamically from launchservices */
+static LSCopyApplicationInformationSpec LSCopyApplicationInformation = nullptr;
+static LSASNCreateWithPidSpec LSASNCreateWithPid = nullptr;
+
+static CFStringRef kLSDisplayNameKey = nullptr;
+static CFStringRef kLSPIDKey = nullptr;
+
+/* retrieved from LaunchServicesSPI.h in WebKit */
+static constexpr int kLSDefaultSessionID = -2;
+
+static const CFStringRef kLaunchServicesBundleID = CFSTR("com.apple.LaunchServices");
+
+static bool GetLaunchServicesPrivateSymbols() {
+	CFBundleRef launch_services_bundle = CFBundleGetBundleWithIdentifier(kLaunchServicesBundleID);
+	if (!launch_services_bundle)
+		return false;
+
+	LSCopyApplicationInformation = reinterpret_cast<LSCopyApplicationInformationSpec>(
+	    CFBundleGetFunctionPointerForName(launch_services_bundle, CFSTR("_LSCopyApplicationInformation")));
+	if (!LSCopyApplicationInformation)
+		return false;
+
+	LSASNCreateWithPid = reinterpret_cast<LSASNCreateWithPidSpec>(
+	    CFBundleGetFunctionPointerForName(launch_services_bundle, CFSTR("_LSASNCreateWithPid")));
+	if (!LSASNCreateWithPid)
+		return false;
+
+	CFStringRef* ptr_kLSDisplayNameKey = reinterpret_cast<CFStringRef*>(
+	    CFBundleGetDataPointerForName(launch_services_bundle, CFSTR("_kLSDisplayNameKey")));
+	if (!ptr_kLSDisplayNameKey)
+		return false;
+	kLSDisplayNameKey = *ptr_kLSDisplayNameKey;
+
+	CFStringRef* ptr_kLSPIDKey =
+	    reinterpret_cast<CFStringRef*>(CFBundleGetDataPointerForName(launch_services_bundle, CFSTR("_kLSPIDKey")));
+	if (!ptr_kLSPIDKey)
+		return false;
+	kLSPIDKey = *ptr_kLSPIDKey;
+
+	return true;
+}
+
+static bool LaunchServicesGetProcessName(pid_t pid, std::string& result) {
+	if (!LSCopyApplicationInformation || !LSASNCreateWithPid || !kLSDisplayNameKey || !kLSPIDKey)
+		if (!GetLaunchServicesPrivateSymbols())
+			return false;
+
+	/* what the hell is an `asn`? */
+	CFPtr<CFTypeRef> asn = LSASNCreateWithPid(kCFAllocatorDefault, pid);
+	if (!asn)
+		return false;
+
+	CFPtr<CFArrayRef> request_array = CFArrayCreate(NULL, (const void**)kLSDisplayNameKey, 1, NULL);
+	if (!request_array)
+		return false;
+
+	CFPtr<CFDictionaryRef> dictionary =
+	    LSCopyApplicationInformation(kLSDefaultSessionID, asn.get(), request_array.get());
+	if (!dictionary)
+		return false;
+
+	{
+		/* this doesn't need to be free'd */
+		CFStringRef rstr;
+
+		if (!CFDictionaryGetValueIfPresent(dictionary, kLSDisplayNameKey, (CFTypeRef*)&rstr) || !rstr)
+			return false;
+
+		if (!StringFromCFString(rstr, result))
+			return false;
+	}
+
+	result.resize(result.find('\0'));
+
+	return true;
+}
+
+bool StringFromCFString(CFStringRef string, std::string& result) {
+	if (!string)
+		return false;
+
+	result.resize(CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1);
+	if (!CFStringGetCString(string, &result.front(), result.length(), kCFStringEncodingUTF8))
+		return false;
+
+	return true;
+}
+
+static bool GetProcessArgs(pid_t pid, std::string& args) {
+	/* sysctl shouldn't touch these, so we define them as const */
+	int mib[3] = {CTL_KERN, KERN_PROCARGS2, static_cast<int>(pid)};
+	const size_t mib_size = sizeof(mib) / sizeof(*mib);
+
+	/* Get the initial size of the array
+	 *
+	 * NOTE: it IS possible for this value to change inbetween calls to sysctl().
+	 * Unfortunately, I couldn't care less about handling this. :)
+	 *
+	 * is that really true, though? these should be constant values. but are
+	 * argc and argv *really* constant?
+	 */
+	size_t size;
+	{
+		int ret = sysctl((int*)mib, mib_size, nullptr, &size, nullptr, 0);
+		if (ret)
+			return false;
+	}
+
+	/* Reserve the space for it in args */
+	args.resize(size);
+
+	/* Get the contents of argc and argv */
+	{
+		int ret = sysctl((int*)mib, mib_size, &args.front(), &size, NULL, 0);
+		if (ret)
+			return false;
+	}
+
+	/* Is the size big enough to hold at least argc? */
+	if (size < sizeof(int))
+		return false;
+
+	args.resize(size);
+	return true;
+}
+
+static bool GetProcessNameFromArgs(pid_t pid, std::string& result) {
+	if (!GetProcessArgs(pid, result))
+		return false;
+
+	/* Get argc using memcpy */
+	int argc = 0;
+	memcpy(&argc, &result.front(), sizeof(argc));
+
+	/* Do we even have argv[0]? */
+	if (argc < 1)
+		return false;
+
+	/* Find the first null character */
+	size_t null_pos = result.find('\0', sizeof(argc));
+	if (null_pos == std::string::npos)
+		return false;
+
+	/* Find the last slash */
+	size_t last_slash = result.rfind('/', null_pos);
+	if (last_slash == std::string::npos)
+		return false;
+
+	/* Return our result */
+	result = result.substr(last_slash + 1, null_pos - last_slash - 1);
+	return true;
+}
+
+static bool GetProcessNameFromKernel(pid_t pid, std::string& result) {
+	result.resize(2 * MAXCOMLEN);
+
+	int size = proc_name(pid, &result.front(), result.length());
+	if (!size)
+		return false;
+
+	result.resize(size);
+	return true;
+}
+
+bool GetProcessName(pid_t pid, std::string& result) {
+	if (LaunchServicesGetProcessName(pid, result))
+		return true;
+
+	/* Try parsing the arguments, this prevents the process name being
+	 * cut off to 2*MAXCOMLEN (32 chars) */
+	if (GetProcessNameFromArgs(pid, result))
+		return true;
+
+	/* Then attempt getting it from the kernel, which results in the
+	 * process name being cut to 32 chars (worse, 16 chars if p_name is
+	 * unavailable) */
+	if (GetProcessNameFromKernel(pid, result))
+		return true;
+
+	return false;
+}
+
+} // namespace animone::internal::osx::util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/win32.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,102 @@
+#include "animone/util/win32.h"
+
+#include <shlobj.h>  /* SHGetKnownFolderPath */
+#include <subauth.h> /* UNICODE_STRING */
+#include <windows.h>
+
+namespace animone::internal::win32 {
+
+std::string ToUtf8String(const std::wstring& string) {
+	if (string.empty())
+		return std::string();
+
+	long size = ::WideCharToMultiByte(CP_UTF8, 0, string.c_str(), string.length(), nullptr, 0, nullptr, nullptr);
+	std::string ret(size, '\0');
+	::WideCharToMultiByte(CP_UTF8, 0, string.c_str(), string.length(), &ret.front(), ret.length(), nullptr, nullptr);
+	return ret;
+}
+
+std::string ToUtf8String(const UNICODE_STRING& string) {
+	const auto wctomb = [&string](LPSTR out, int size) -> int {
+		return ::WideCharToMultiByte(CP_UTF8, 0, string.Buffer, string.Length, out, size, nullptr, nullptr);
+	};
+
+	if (string.Length <= 0)
+		return std::string();
+
+	long size = wctomb(nullptr, 0);
+	std::string ret(size, '\0');
+	wctomb(&ret.front(), ret.length());
+	return ret;
+}
+
+std::wstring ToWstring(const std::string& string) {
+	const auto mbtowc = [&string](LPWSTR out, int size) -> int {
+		return ::MultiByteToWideChar(CP_UTF8, 0, string.c_str(), string.length(), out, size);
+	};
+
+	if (string.empty())
+		return std::wstring();
+
+	long size = mbtowc(nullptr, 0);
+	std::wstring ret(size, L'\0');
+	mbtowc(&ret.front(), ret.length());
+	return ret;
+}
+
+std::wstring GetProcessPath(DWORD process_id) {
+	// If we try to open a SYSTEM process, this function fails and the last error
+	// code is ERROR_ACCESS_DENIED.
+	//
+	// Note that if we requested PROCESS_QUERY_INFORMATION access right instead
+	// of PROCESS_QUERY_LIMITED_INFORMATION, this function would fail when used
+	// to open an elevated process.
+	Handle process_handle(::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id));
+
+	if (!process_handle)
+		return std::wstring();
+
+	std::wstring buffer(MAX_PATH, L'\0');
+	DWORD buf_size = buffer.length();
+
+	// Note that this function requires Windows Vista or above. You may use
+	// GetProcessImageFileName or GetModuleFileNameEx on earlier versions.
+	if (!::QueryFullProcessImageNameW(process_handle.get(), 0, &buffer.front(), &buf_size))
+		return std::wstring();
+
+	buffer.resize(buf_size);
+	return buffer;
+}
+
+std::wstring GetFileNameFromPath(const std::wstring& path) {
+	const auto pos = path.find_last_of(L"/\\");
+	return pos != std::wstring::npos ? path.substr(pos + 1) : path;
+}
+
+std::wstring GetFileNameWithoutExtension(const std::wstring& filename) {
+	const auto pos = filename.find_last_of(L".");
+	return pos != std::wstring::npos ? filename.substr(0, pos) : filename;
+}
+
+static std::wstring GetSystemDirectory() {
+	PWSTR path_wch;
+	SHGetKnownFolderPath(FOLDERID_Windows, 0, NULL, &path_wch);
+	std::wstring path_wstr(path_wch);
+	CoTaskMemFree(path_wch);
+	return path_wstr;
+}
+
+bool IsSystemDirectory(const std::string& path) {
+	return IsSystemDirectory(ToWstring(path));
+}
+
+bool IsSystemDirectory(std::wstring path) {
+	::CharUpperBuffW(&path.front(), path.length());
+
+	std::wstring windir = GetSystemDirectory();
+	::CharUpperBuffW(&windir.front(), windir.length());
+
+	return path.find(windir) == 4;
+}
+
+} // namespace animone::internal::win32
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/win.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,35 @@
+#include "animone/win.h"
+
+#ifdef WIN32
+#	include "animone/win/win32.h"
+#endif
+
+#ifdef MACOSX
+#	include "animone/win/quartz.h"
+#endif
+
+#ifdef X11
+#	include "animone/win/x11.h"
+#endif
+
+namespace animone::internal {
+
+bool EnumerateWindows(window_proc_t window_proc) {
+	bool success = false;
+
+#ifdef WIN32
+	success |= win32::EnumerateWindows(window_proc);
+#endif
+
+#ifdef MACOSX
+	success |= quartz::EnumerateWindows(window_proc);
+#endif
+
+#ifdef X11
+	success |= x11::EnumerateWindows(window_proc);
+#endif
+
+	return success;
+}
+
+} // namespace animone::internal
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/win/quartz.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,251 @@
+/*
+ * win/quartz.cc: support for macOS (the Quartz Compositor)
+ *
+ * This file does not require an Objective-C++ compiler,
+ * but it *does* require an Objective-C runtime.
+ */
+#include "animone/win/quartz.h"
+#include "animone.h"
+#include "animone/util/osx.h"
+
+#include <objc/message.h>
+#include <objc/runtime.h>
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreGraphics/CoreGraphics.h>
+
+namespace animone::internal::quartz {
+
+#if __LP64__
+typedef long NSInteger;
+#else
+typedef int NSInteger;
+#endif
+typedef int CGSConnection;
+
+typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void);
+typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*);
+
+static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr;
+static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr;
+
+static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics");
+
+/* Objective-C */
+typedef id (*object_message_send)(id, SEL, ...);
+typedef id (*class_message_send)(Class, SEL, ...);
+
+static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend);
+static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend);
+
+static bool GetCoreGraphicsPrivateSymbols() {
+	CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID);
+	if (!core_graphics_bundle)
+		return false;
+
+	CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName(
+	    core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread"));
+	if (!CGSDefaultConnectionForThread)
+		return false;
+
+	CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName(
+	    core_graphics_bundle, CFSTR("CGSCopyWindowProperty"));
+	if (!CGSCopyWindowProperty)
+		return false;
+
+	return true;
+}
+
+template<typename T>
+static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) {
+	CFTypeRef data = nullptr;
+	if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data)
+		return false;
+
+	if constexpr (std::is_arithmetic<T>::value)
+		osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
+	else if constexpr (std::is_same<T, std::string>::value)
+		osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out);
+	else
+		return false;
+
+	return true;
+}
+
+static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) {
+	CGRect bounds = {0};
+	{
+		const CGWindowID wids[1] = {wid};
+		CFPtr<CFArrayRef> arr(CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL));
+		CFPtr<CFArrayRef> dicts(CGWindowListCreateDescriptionFromArray(arr));
+
+		if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1)
+			return false;
+
+		CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts, 0));
+		if (!dict)
+			return false;
+
+		CFDictionaryRef bounds_dict = nullptr;
+		if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) ||
+		    !bounds_dict)
+			return false;
+
+		if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds))
+			return false;
+	}
+
+	/* now we can actually do stuff */
+	AXUIElementRef axapp = AXUIElementCreateApplication(pid);
+	CFPtr<CFArrayRef> windows;
+	{
+		CFArrayRef ref;
+		if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, reinterpret_cast<CFTypeRef*>(&ref)) !=
+		     kAXErrorSuccess) ||
+		    !windows)
+			return false;
+
+		windows.reset(ref);
+	}
+
+	const CFIndex count = CFArrayGetCount(windows.get());
+	for (CFIndex i = 0; i < count; i++) {
+		const AXUIElementRef window = reinterpret_cast<AXUIElementRef>(CFArrayGetValueAtIndex(windows.get(), i));
+
+		/* does this leak memory? probably. */
+		AXValueRef val;
+		if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, reinterpret_cast<CFTypeRef*>(&val)) ==
+		    kAXErrorSuccess) {
+			CGPoint point;
+			if (!AXValueGetValue(val, kAXValueTypeCGPoint, reinterpret_cast<CFTypeRef>(&point)) ||
+			    (point.x != bounds.origin.x || point.y != bounds.origin.y))
+				continue;
+		} else
+			continue;
+
+		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) ==
+		    kAXErrorSuccess) {
+			CGSize size;
+			if (!AXValueGetValue(val, kAXValueTypeCGSize, reinterpret_cast<CFTypeRef>(&size)) ||
+			    (size.width != bounds.size.width || size.height != bounds.size.height))
+				continue;
+		} else
+			continue;
+
+		CFStringRef title;
+		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) ==
+		    kAXErrorSuccess)
+			return osx::util::StringFromCFString(title, result);
+	}
+
+	return false;
+}
+
+static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) {
+	/* try using CoreGraphics (only usable on old versions of OS X) */
+	if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) {
+		CFPtr<CFStringRef> title;
+		{
+			CFStringRef t = nullptr;
+			CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &t);
+			title.reset(t);
+		}
+
+		if (title && CFStringGetLength(title.get()) && osx::util::StringFromCFString(title.get(), result))
+			return true;
+	}
+
+	/* then try linking to a window using the accessibility API */
+	return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false;
+}
+
+static bool GetProcessBundleIdentifierNew(pid_t pid, std::string& result) {
+	/* 10.6 and higher */
+	const id app =
+	    cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
+	if (!app)
+		return false;
+
+	CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
+	if (!bundle_id)
+		return false;
+
+	result = osx::util::StringFromCFString(bundle_id, result);
+	return true;
+}
+
+static bool GetProcessBundleIdentifierOld(pid_t pid, std::string& result) {
+	/* OS X 10.2; deprecated in 10.9 */
+	ProcessSerialNumber psn;
+	if (GetProcessForPID(pid, &psn))
+		return false;
+
+	CFPtr<CFDictionaryRef> info = ProcessInformationCopyDictionary(psn, kProcessDictionaryIncludeAllInformationMask);
+	if (!info)
+		return false;
+
+	CFStringRef value = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(dict, CFSTR("CFBundleIdentifier")));
+	if (!value)
+		return false;
+
+	result = osx::util::StringFromCFString(value, result);
+	return true;
+}
+
+static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) {
+	/* The Bundle ID is essentially OS X's solution to Windows'
+	 * "class name"; theoretically, it should be different for
+	 * each program, although it requires an app bundle.
+	 */
+	if (GetProcessBundleIdentifierNew(pid, result))
+		return true;
+
+	return GetProcessBundleIdentifierOld();
+}
+
+bool EnumerateWindows(window_proc_t window_proc) {
+	if (!window_proc)
+		return false;
+
+	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
+	if (!windows)
+		return false;
+
+	const CFIndex count = CFArrayGetCount(windows);
+	for (CFIndex i = 0; i < count; i++) {
+		CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i));
+		if (!window)
+			continue;
+
+		Process proc;
+		{
+			CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid);
+			if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name))
+				osx::util::GetProcessName(proc.pid, proc.name);
+		}
+
+		Window win;
+		{
+			CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id);
+
+			if (!GetProcessBundleIdentifier(proc.pid, win.class_name))
+				// Fallback to the Quartz window name, which is unlikely to be filled, but it
+				// *could* be.
+				CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name);
+
+			GetWindowTitle(win.id, proc.pid, win.text);
+		}
+
+		if (!window_proc(proc, win)) {
+			CFRelease(windows);
+			return false;
+		}
+	}
+
+	CFRelease(windows);
+
+	return true;
+}
+
+} // namespace animone::internal::quartz
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/win/win32.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,143 @@
+/*
+ * win/win32.cc: support for Windows
+ *
+ * Surprisingly, this is the one time where Microsoft actually
+ * does it fairly OK. Everything has a pretty simple API, despite
+ * the stupid wide string stuff.
+ */
+#include "animone/win/win32.h"
+#include "animone.h"
+#include "animone/util/win32.h"
+#include "animone/win.h"
+
+#include <set>
+#include <string>
+
+#include <windows.h>
+
+namespace animone::internal::win32 {
+
+static std::wstring GetWindowClassName(HWND hwnd) {
+	static constexpr int kMaxSize = 256;
+
+	std::wstring buffer(kMaxSize, L'\0');
+	const auto size = ::GetClassNameW(hwnd, &buffer.front(), buffer.length());
+	buffer.resize(size);
+	return buffer;
+}
+
+static std::wstring GetWindowText(HWND hwnd) {
+	const auto estimated_size = ::GetWindowTextLengthW(hwnd);
+	std::wstring buffer(estimated_size + 1, L'\0');
+
+	const auto size = ::GetWindowTextW(hwnd, &buffer.front(), buffer.length());
+	/* GetWindowTextLength docs:
+	 * "Under certain conditions, the GetWindowTextLength function may return a value
+	 *  that is larger than the actual length of the text." */
+	buffer.resize(size);
+	return buffer;
+}
+
+static DWORD GetWindowProcessId(HWND hwnd) {
+	DWORD process_id = 0;
+	::GetWindowThreadProcessId(hwnd, &process_id);
+	return process_id;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static bool VerifyWindowStyle(HWND hwnd) {
+	const auto window_style = ::GetWindowLong(hwnd, GWL_STYLE);
+	const auto window_ex_style = ::GetWindowLong(hwnd, GWL_EXSTYLE);
+
+	auto has_style = [&window_style](DWORD style) { return (window_style & style) != 0; };
+	auto has_ex_style = [&window_ex_style](DWORD ex_style) { return (window_ex_style & ex_style) != 0; };
+
+	// Toolbars, tooltips and similar topmost windows
+	if (has_style(WS_POPUP) && has_ex_style(WS_EX_TOOLWINDOW))
+		return false;
+	if (has_ex_style(WS_EX_TOPMOST) && has_ex_style(WS_EX_TOOLWINDOW))
+		return false;
+
+	return true;
+}
+
+static bool VerifyClassName(const std::wstring& name) {
+	static const std::set<std::wstring> invalid_names = {
+	    // System classes
+	    L"#32770",        // Dialog box
+	    L"CabinetWClass", // Windows Explorer
+	    L"ComboLBox",
+	    L"DDEMLEvent",
+	    L"DDEMLMom",
+	    L"DirectUIHWND",
+	    L"GDI+ Hook Window Class",
+	    L"IME",
+	    L"Internet Explorer_Hidden",
+	    L"MSCTFIME UI",
+	    L"tooltips_class32",
+	};
+
+	return !name.empty() && !invalid_names.count(name);
+}
+
+static bool VerifyProcessPath(const std::wstring& path) {
+	return !path.empty() && !IsSystemDirectory(path);
+}
+
+static bool VerifyProcessFileName(const std::wstring& name) {
+	static const std::set<std::wstring> invalid_names = {
+	    // System files
+	    L"explorer",   // Windows Explorer
+	    L"taskeng",    // Task Scheduler Engine
+	    L"taskhost",   // Host Process for Windows Tasks
+	    L"taskhostex", // Host Process for Windows Tasks
+	    L"Taskmgr",    // Task Manager
+	};
+
+	return !name.empty() && !invalid_names.count(name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param) {
+	if (!::IsWindowVisible(hwnd))
+		return TRUE;
+
+	if (!VerifyWindowStyle(hwnd))
+		return TRUE;
+
+	Window window;
+	window.id = static_cast<unsigned int>(reinterpret_cast<ULONG_PTR>(hwnd));
+	window.text = ToUtf8String(GetWindowText(hwnd));
+
+	{
+		std::wstring class_name = GetWindowClassName(hwnd);
+		window.class_name = ToUtf8String(class_name);
+		if (!VerifyClassName(class_name))
+			return TRUE;
+	}
+
+	Process process;
+	process.pid = GetWindowProcessId(hwnd);
+	process.name = fd::GetProcessName(process.pid)
+
+	    auto& window_proc = *reinterpret_cast<window_proc_t*>(param);
+	if (!window_proc(process, window))
+		return FALSE;
+
+	return TRUE;
+}
+
+bool EnumerateWindows(window_proc_t window_proc) {
+	if (!window_proc)
+		return false;
+
+	const auto param = reinterpret_cast<LPARAM>(&window_proc);
+
+	// Note that EnumWindows enumerates only top-level windows of desktop apps
+	// (as opposed to UWP apps) on Windows 8 and above.
+	return ::EnumWindows(EnumWindowsProc, param) != FALSE;
+}
+
+} // namespace animone::internal::win32
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/win/x11.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,255 @@
+#include "animone/win/x11.h"
+#include "animone.h"
+#include "animone/fd.h" /* GetProcessName() */
+#include "animone/win.h"
+
+#include <xcb/res.h>
+#include <xcb/xcb.h>
+
+#include <climits>
+#include <cstdint>
+#include <cstring>
+#include <set>
+#include <string>
+
+#include <chrono>
+
+#include <iostream>
+
+/* This uses XCB (and it uses it *right*), so it should be plenty fast */
+
+static size_t str_nlen(const char* s, size_t len) {
+	size_t i = 0;
+	for (; i < len && s[i]; i++)
+		;
+	return i;
+}
+
+namespace animone::internal::x11 {
+
+static bool GetAllTopLevelWindowsEWMH(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots,
+                                      std::set<xcb_window_t>& result) {
+	const xcb_atom_t Atom__NET_CLIENT_LIST = [connection] {
+		static constexpr std::string_view name = "_NET_CLIENT_LIST";
+		xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
+		std::unique_ptr<xcb_intern_atom_reply_t> reply(::xcb_intern_atom_reply(connection, cookie, NULL));
+
+		xcb_atom_t atom = reply->atom;
+
+		return atom;
+	}();
+	if (Atom__NET_CLIENT_LIST == XCB_ATOM_NONE)
+		return false; // BTFO
+
+	bool success = false;
+
+	std::vector<xcb_get_property_cookie_t> cookies;
+	cookies.reserve(roots.size());
+
+	for (const auto& root : roots)
+		cookies.push_back(::xcb_get_property(connection, 0, root, Atom__NET_CLIENT_LIST, XCB_ATOM_ANY, 0L, UINT_MAX));
+
+	for (const auto& cookie : cookies) {
+		std::unique_ptr<xcb_get_property_reply_t> reply(::xcb_get_property_reply(connection, cookie, NULL));
+
+		if (reply) {
+			xcb_window_t* value = reinterpret_cast<xcb_window_t*>(::xcb_get_property_value(reply));
+			int len = ::xcb_get_property_value_length(reply);
+
+			for (size_t i = 0; i < len; i++)
+				result.insert(value[i]);
+
+			success |= true;
+		}
+	}
+
+	return success;
+}
+
+/* This is called on every window. What this does is:
+ * 1. Gets the tree of children
+ * 2. Searches all children recursively for a WM_STATE property
+ * 3. If that failed... return the original window
+ */
+static bool WalkWindows(xcb_connection_t* connection, int depth, xcb_atom_t Atom_WM_STATE, const xcb_window_t* windows,
+                        int windows_len, std::set<xcb_window_t>& result) {
+	/* The depth we should start returning at. */
+	static constexpr int CUTOFF = 2;
+
+	bool success = false;
+
+	std::vector<xcb_query_tree_cookie_t> cookies;
+	cookies.reserve(windows_len);
+
+	for (int i = 0; i < windows_len; i++)
+		cookies.push_back(::xcb_query_tree(connection, windows[i]));
+
+	for (const auto& cookie : cookies) {
+		std::unique_ptr<xcb_query_tree_reply_t> query_tree_reply(::xcb_query_tree_reply(connection, cookie, NULL));
+
+		xcb_window_t* tree_children = ::xcb_query_tree_children(query_tree_reply);
+		int tree_children_len = ::xcb_query_tree_children_length(query_tree_reply);
+
+		std::vector<xcb_get_property_cookie_t> state_property_cookies;
+		state_property_cookies.reserve(tree_children_len);
+
+		for (int i = 0; i < tree_children_len; i++)
+			state_property_cookies.push_back(
+			    ::xcb_get_property(connection, 0, tree_children[i], Atom_WM_STATE, Atom_WM_STATE, 0, 0));
+
+		for (int i = 0; i < tree_children_len; i++) {
+			std::unique_ptr<xcb_get_property_reply_t> get_property_reply(
+			    ::xcb_get_property_reply(connection, state_property_cookies[i], NULL));
+
+			/* X11 is unfriendly here. what this means is "did the property exist?" */
+			if (get_property_reply->format || get_property_reply->type || get_property_reply->length) {
+				result.insert(tree_children[i]);
+				if (depth >= CUTOFF)
+					return true;
+
+				success |= true;
+				continue;
+			}
+		}
+
+		if (WalkWindows(connection, depth + 1, Atom_WM_STATE, tree_children, tree_children_len, result)) {
+			success |= true;
+			if (depth >= CUTOFF)
+				return true;
+			continue;
+		}
+	}
+
+	return success;
+}
+
+static bool GetAllTopLevelWindowsICCCM(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots,
+                                       std::set<xcb_window_t>& result) {
+	bool success = false;
+
+	xcb_atom_t Atom_WM_STATE = [connection] {
+		static constexpr std::string_view name = "WM_STATE";
+		xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
+		xcb_intern_atom_reply_t* reply = ::xcb_intern_atom_reply(connection, cookie, NULL);
+
+		xcb_atom_t atom = reply->atom;
+		free(reply);
+		return atom;
+	}();
+	if (Atom_WM_STATE == XCB_ATOM_NONE)
+		return success;
+
+	std::vector<xcb_query_tree_cookie_t> cookies;
+	cookies.reserve(roots.size());
+
+	for (const auto& root : roots)
+		cookies.push_back(::xcb_query_tree(connection, root));
+
+	for (const auto& cookie : cookies)
+		success |= WalkWindows(connection, 0, Atom_WM_STATE, roots.data(), roots.size(), result);
+
+	return success;
+}
+
+bool EnumerateWindows(window_proc_t window_proc) {
+	if (!window_proc)
+		return false;
+
+	xcb_connection_t* connection = ::xcb_connect(NULL, NULL);
+	if (!connection)
+		return false;
+
+	std::set<xcb_window_t> windows;
+	{
+		std::vector<xcb_window_t> roots;
+		{
+			xcb_screen_iterator_t iter = ::xcb_setup_roots_iterator(xcb_get_setup(connection));
+			for (; iter.rem; ::xcb_screen_next(&iter))
+				roots.push_back(iter.data->root);
+		}
+
+		if (!GetAllTopLevelWindowsEWMH(connection, roots, windows))
+			GetAllTopLevelWindowsICCCM(connection, roots, windows);
+	}
+
+	struct WindowCookies {
+		xcb_window_t window;
+		xcb_get_property_cookie_t class_property_cookie;
+		xcb_get_property_cookie_t name_property_cookie;
+		xcb_res_query_client_ids_cookie_t pid_property_cookie;
+	};
+
+	std::vector<WindowCookies> window_cookies;
+	window_cookies.reserve(windows.size());
+
+	for (const auto& window : windows) {
+		xcb_res_client_id_spec_t spec = {.client = window, .mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID};
+
+		WindowCookies window_cookie = {
+		    window, ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0L, 2048L),
+		    ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0L, UINT_MAX),
+		    ::xcb_res_query_client_ids(connection, 1, &spec)};
+
+		window_cookies.push_back(window_cookie);
+	}
+
+	for (const auto& window_cookie : window_cookies) {
+		Window win = {0};
+		win.id = window_cookie.window;
+		{
+			/* Class name */
+			std::unique_ptr<xcb_get_property_reply_t> reply(
+			    ::xcb_get_property_reply(connection, window_cookie.class_property_cookie, NULL));
+
+			if (reply && reply->format == 8) {
+				const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get()));
+				const int data_len = ::xcb_get_property_value_length(reply.get());
+
+				int instance_len = str_nlen(data, data_len);
+				const char* class_name = data + instance_len + 1;
+
+				win.class_name = std::string(class_name, str_nlen(class_name, data_len - (instance_len + 1)));
+			}
+		}
+		{
+			/* Title text */
+			std::unique_ptr<xcb_get_property_reply_t> reply(
+			    ::xcb_get_property_reply(connection, window_cookie.name_property_cookie, NULL));
+
+			if (reply) {
+				const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get()));
+				int len = ::xcb_get_property_value_length(reply.get());
+
+				win.text = std::string(data, len);
+			}
+		}
+		Process proc = {0};
+		{
+			/* PID */
+			std::unique_ptr<xcb_res_query_client_ids_reply_t> reply(
+			    ::xcb_res_query_client_ids_reply(connection, window_cookie.pid_property_cookie, NULL));
+
+			if (reply) {
+				xcb_res_client_id_value_iterator_t it = ::xcb_res_query_client_ids_ids_iterator(reply);
+				for (; it.rem; ::xcb_res_client_id_value_next(&it)) {
+					if (it.data->spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) {
+						proc.pid = *reinterpret_cast<uint32_t*>(::xcb_res_client_id_value_value(it.data));
+						fd::GetProcessName(proc.pid, proc.name); /* fill this in if we can */
+						break;
+					}
+				}
+			}
+		}
+
+		if (!window_proc(proc, win)) {
+			::xcb_disconnect(connection);
+			return false;
+		}
+	}
+
+	::xcb_disconnect(connection);
+
+	return true;
+}
+
+} // namespace animone::internal::x11