view dep/animone/src/player.cc @ 337:a7d4e5107531

dep/animone: REFACTOR ALL THE THINGS 1: animone now has its own syntax divergent from anisthesia, making different platforms actually have their own sections 2: process names in animone are now called `comm' (this will probably break things). this is what its called in bsd/linux so I'm just going to use it everywhere 3: the X11 code now checks for the existence of a UTF-8 window title and passes it if available 4: ANYTHING THATS NOT LINUX IS 100% UNTESTED AND CAN AND WILL BREAK! I still actually need to test the bsd code. to be honest I'm probably going to move all of the bsds into separate files because they're all essentially different operating systems at this point
author Paper <paper@paper.us.eu.org>
date Wed, 19 Jun 2024 12:51:15 -0400
parents b1f625b0227c
children
line wrap: on
line source

#include "animone/player.h"
#include "animone/util.h"

#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <optional>

#include <iostream>

namespace animone {

namespace internal::parser {

struct State {
	enum class Name {
		ExpectPlayerName,
		ExpectSection,
		ExpectWindowPlatform,
		ExpectExecutablePlatform,
		ExpectWindow,
		ExpectExecutable,
		ExpectStrategy,
		ExpectType,
		ExpectWindowTitle,
	};

	Name state = Name::ExpectPlayerName;
	WindowPlatform window_platform = WindowPlatform::Unknown;
	ExecutablePlatform executable_platform = ExecutablePlatform::Unknown;
};

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 = [](const State::Name& state) -> size_t {
		switch (state) {
			default:
			case State::Name::ExpectPlayerName: return 0;
			case State::Name::ExpectSection: return 1;
			case State::Name::ExpectWindowPlatform:
			case State::Name::ExpectExecutablePlatform:
			case State::Name::ExpectStrategy:
			case State::Name::ExpectType: return 2;
			case State::Name::ExpectWindow:
			case State::Name::ExpectExecutable:
			case State::Name::ExpectWindowTitle: return 3;
		}
	}(state.state);

	if (current > expected) {
		std::cerr << "animone: excessive indentation found" << std::endl;
		return false; // Disallow excessive indentation
	}

	if (current < expected) {
		const std::optional<State::Name> st = [current, state]() -> std::optional<State::Name> {
			switch (current) {
				case 0: return State::Name::ExpectPlayerName;
				default:
				case 1: return State::Name::ExpectSection;
				case 2:
					switch (state.state) {
						case State::Name::ExpectWindow: return State::Name::ExpectWindowPlatform;
						case State::Name::ExpectExecutable: return State::Name::ExpectExecutablePlatform;
						default: return std::nullopt;
					}
			}
		}();
		if (!st.has_value())
			return false;

		switch (state.state) {
			case State::Name::ExpectWindow:
				if (players.back().windows.empty())
					return false;
				state.state = st.value();
				break;
			case State::Name::ExpectExecutable:
				if (players.back().executables.empty())
					return false;
				state.state = st.value();
				break;
			case State::Name::ExpectStrategy:
				if (players.back().strategies.empty())
					return false;
				state.state = st.value();
				break;
			case State::Name::ExpectType:
				state.state = st.value();
				break;
			case State::Name::ExpectWindowTitle:
				return false;
		}
	}

	return true;
}

bool HandleState(std::string& line, std::vector<Player>& players, State& state) {
	switch (state.state) {
		case State::Name::ExpectPlayerName:
			players.push_back(Player());
			players.back().name = line;
			state.state = State::Name::ExpectSection;
			break;

		case State::Name::ExpectSection: {
			static const std::map<std::string, State::Name> sections = {
			    {"windows",     State::Name::ExpectWindowPlatform},
			    {"executables", State::Name::ExpectExecutablePlatform},
			    {"strategies",  State::Name::ExpectStrategy  },
			    {"type",        State::Name::ExpectType      },
			};
			util::TrimRight(line, ":");
			const auto it = sections.find(line);
			if (it == sections.end())
				return false;
			state.state = it->second;
			break;
		}

		case State::Name::ExpectWindowPlatform: {
			static const std::map<std::string, WindowPlatform> platforms = {
				{"quartz", WindowPlatform::Quartz},
				{"win32",  WindowPlatform::Win32},
				{"x11",    WindowPlatform::X11},
			};
			util::TrimRight(line, ":");
			const auto it = platforms.find(line);
			if (it == platforms.end())
				return false;
			state.state = State::Name::ExpectWindow;
			state.window_platform = it->second;
			break;
		}

		case State::Name::ExpectExecutablePlatform: {
			static const std::map<std::string, ExecutablePlatform> platforms = {
				{"posix",   ExecutablePlatform::Posix},
				{"win32",   ExecutablePlatform::Win32},
				{"macosx",  ExecutablePlatform::Xnu},
			};
			util::TrimRight(line, ":");
			const auto it = platforms.find(line);
			if (it == platforms.end())
				return false;
			state.state = State::Name::ExpectExecutable;
			state.executable_platform = it->second;
			break;
		}

		case State::Name::ExpectWindow: players.back().windows[state.window_platform].push_back(line); break;

		case State::Name::ExpectExecutable: players.back().executables[state.executable_platform].push_back(line); break;

		case State::Name::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 = State::Name::ExpectWindowTitle; break;
			}
			break;
		}

		case State::Name::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::Name::ExpectWindowTitle:
			players.back().window_title_format = line;
			state.state = State::Name::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;
	internal::parser::State state;

	int ln = 1;
	for (; std::getline(stream, line, '\n'); ln++) {
		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)) {
			std::cerr << "animone: indentation: failed on line " << ln << std::endl;
			return false;
		}

		if (!internal::parser::HandleState(line, players, state)) {
			std::cerr << "animone: state: failed on line " << ln << std::endl;
			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