[
  {
    "path": ".gitignore",
    "content": "/target\n./tmp\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"lldb\",\n      \"request\": \"launch\",\n      \"name\": \"Debug executable 'sworkstyle'\",\n      \"cargo\": {\n        \"args\": [\"build\", \"--bin=sworkstyle\", \"--package=sworkstyle\"],\n        \"filter\": {\n          \"name\": \"sworkstyle\",\n          \"kind\": \"bin\"\n        }\n      },\n      \"args\": [],\n      \"cwd\": \"${workspaceFolder}\"\n    },\n    {\n      \"type\": \"lldb\",\n      \"request\": \"launch\",\n      \"name\": \"Debug unit tests in executable 'sworkstyle'\",\n      \"cargo\": {\n        \"args\": [\n          \"test\",\n          \"--no-run\",\n          \"--bin=sworkstyle\",\n          \"--package=sworkstyle\"\n        ],\n        \"filter\": {\n          \"name\": \"sworkstyle\",\n          \"kind\": \"bin\"\n        }\n      },\n      \"args\": [],\n      \"cwd\": \"${workspaceFolder}\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"sworkstyle\"\nversion = \"1.4.0\"\nauthors = [\"Lyr-7D1h <lyr-7d1h@pm.me>\"]\nedition = \"2021\"\nlicense = \"MIT\"\ndescription = \"Workspaces with the swayest style! This program will dynamically rename your workspaces to indicate which programs are running in each workspace. It uses the Sway IPC. In the absence of a config file, one will be generated automatically.See ${XDG_CONFIG_HOME}/workstyle/config.yml for  details.\"\nrepository = \"https://github.com/Lyr-7D1h/swayest_workstyle\"\nkeywords = [\"sway\", \"wayland\"]\nreadme = \"README.md\"\n\n\n[dependencies]\nswayipc-async = \"3.0.0\"\nasync-io = \"2.6\"\nfutures-lite = \"2.6.1\"\ntoml = { version = \"0.9.8\", features = [\"preserve_order\"] }\ndirs = \"6.0\"\nlog = \"0.4.29\"\nfslock = \"0.2.1\"\nctrlc = \"3.5.2\"\nregex = \"1.12.3\"\nsimple_logger = \"5.1.0\"\ninotify = \"0.11\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Ivo Velthoven\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Swayest Workstyle\n\n[![AUR version](https://img.shields.io/aur/version/sworkstyle)](https://aur.archlinux.org/packages/sworkstyle)\n[![Crates.io](https://img.shields.io/crates/v/sworkstyle)](https://crates.io/crates/sworkstyle)\n\nMap workspace name to icons defined depending on the windows inside of the workspace.\n\nAn executable similar to [workstyle](https://github.com/pierrechevalier83/workstyle).\n\n**Differences between `sworkstyle` and `workstyle`:**\n\n- Plug-and-play solution, build-in matching config, you can extend this config by creating/modifying `.config/sworkstyle/config.toml` or you can make a PR for your package manager or this repository with new matchers.\n\n- Way better matching: using regex, exact app names and generic app titles.\n\n- Specifically meant for Sway and Wayland\n\n- Fallback Icon\n\n- Deduplication\n\nYour workspace shall never contain an empty icon again!\n\n**An example of what it does (using waybar which also hides the workspace index):**\n\n<img src=\"./screenshots/bar.png\">\n<br />\n<img src=\"./screenshots/desktop.png\" width=\"1000\">\n\n## Installation\n\n### Cargo\n\n```bash\ncargo install sworkstyle\n```\n\n### Arch Linux\n\nYou can install it manually or use a aur helper like Yay.\n\n```bash\nyay -S sworkstyle\n```\n\n## Usage\n\n```bash\nsworkstyle\n```\n\n## Sway Configuration\n\nAdd the follow line to your sway config file (`~/.config/sway/config`).\n\n```bash\nexec sworkstyle &> /tmp/sworkstyle.log\n```\n\n> **_NOTE:_** When using the cargo install make sure to add the `.cargo/bin` to the `PATH` environment variable before executing sway. You can do this by adding `export PATH=\"$HOME/.cargo/bin:$PATH\"` to `.zprofile` or `.profile`\n\nYou should configure anything mentioning a workspace (assign, keybinding) to use numbered workspaces. This is because sworkstyle will rename your workspaces many times so it needs a constant number that doesn't change in order to work correctly.\n\nPrefer\n\n```bash\nassign [class=\"^Steam$\"] number 1\nbindsym $mod+1 workspace number 1\n```\n\nover\n\n```bash\nassign [class=\"^Steam$\"] 1\nbindsym $mod+1 workspace 1\n```\n\n## Sworkstyle Configuration\n\nThe main configuration consists of deciding which icons to use for which applications.\n\nThe config file is located at `${XDG_CONFIG_HOME}/sworkstyle/config.toml`. Its values will take precedence over the defaults. The syntax is in TOML and should be pretty self-explanatory.\n\nWhen an app isn't recognized in the config, `sworkstyle` will log the application name as a warning.\nSimply add that string to your config file, with an icon of your choice.\n\nNote that the crate [find_unicode](https://github.com/pierrechevalier83/find_unicode/) can help find a unicode character directly from the command line. It now supports all of nerdfonts unicode space.\n\nFor a reference to the regex syntax see the [regex](https://docs.rs/regex/1.5.4/regex/#syntax) crate\n\n### Matching\n\n#### Standard\n\n```toml\n'{pattern}' = '{icon}'\n\n# pattern: Can either be the exact \"app_name\" (app_id/class) of the window or a regex string in the format of `\"/{regex}/\"` which will match the window \"title\".\n# icon: Your beautiful icon\n```\n\n#### Verbose\n\n```toml\n'{pattern}' = { type = 'generic' | 'exact', value = '{icon}' }\n```\n\n#### Combined Fields\n\n```toml\n'{name}' = { app_id = 'steam', title = '/Eve/', value = '{icon}' }\n```\n\n`{name}` is only a TOML key/identifier for the entry. It is not used for matching logic.\nUse any descriptive unique name (for example `steam_eve` or `browser_github`).\n\nAll specified fields must match. You can combine any of `app_id`, `class`, and `title`.\n`title` supports the same matching behavior as generic rules (substring or regex with `/.../`).\n`app_id` and `class` support exact string matching or regex when wrapped in `/.../`.\n\n_**Note:**_ You'll only have to use the verbose format when matching generic with a case insensitive text. `'case insensitive title' = { type = 'generic', value = 'A' }`\n\n#### Troubleshooting\n\n\nIf it couldn't match something it will print:\n\n```\nWARN [sworkstyle:config] No match for app_id=\"{app_id}\" class=\"{class}\" title=\"{title}\"\n```\n\nYou can use {title} to do a generic matching\n\nYou can use {app_name} to do an exact match\n\n### Default Config\n\nThe default config uses [font-awesome](https://fontawesome.com/) for icon mappinigs. \n\nThe default config is always appended to whatever custom config you define. \nYou can overwrite any matching or make a PR if you feel like a matching should be a default.\n\n```toml\nfallback = ''\nseparator = ' '\n\n[matching]\n'discord' = ''\n'balena-etcher' = ''\n'Chia Blockchain' = ''\n'Steam' = ''\n'vlc' = ''\n'org.qbittorrent.qBittorrent' = ''\n'Thunderbird' = ''\n'thunderbird' = ''\n'Postman' = ''\n'Insomnia' = ''\n'Bitwarden' = ''\n'Google-chrome' = ''\n'google-chrome' = ''\n'Chromium' = ''\n'Slack' = ''\n'Code' = ''\n'code-oss' = ''\n'jetbrains-studio' = ''\n'Spotify' = ''\n'GitHub Desktop' = ''\n'/(?i)Github.*Firefox/' = ''\n'firefox' = ''\n'Nightly' = ''\n'firefoxdeveloperedition' = ''\n'/nvim ?\\w*/' = ''\n'/npm/' = ''\n'/node/' = ''\n'/yarn/' = ''\n'Alacritty' = ''\n```\n\n## Package Maintainers\n\nIf you want to change the build-in config, change `src/default_config.toml` with your config and install the project.\n\nYou might also want [font-awesome](https://fontawesome.com/) as a dependency depending on your config.\n\nYou can also make a PR to add a badge and add your install method under #Installation or to add matchers to the build-in config.\n\nSee [aur](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=sworkstyle) for an example build.\n\n## Roadmap\n\n- An `--unique` param where you only have a single icon per workspace based on the matching with biggest priority. \n\n## Known Issues\n\n- Using sway's alt-tab behavior can cause a workspace to be not named\n- Does not work on hyprland, use this instead: https://github.com/hyprland-community/hyprland-autoname-workspaces\n"
  },
  {
    "path": "default_config.toml",
    "content": "# Config for sworkstyle\n#\n# You can rename workspaces based on the exact application names or by generic pattern.\n# When it could not match anything it will use the fallback.\n#\n# format:\n#\n# \t'{pattern}' = '{icon}'\n#\n# pattern: Can either be the exact \"app_name\" (app_id/class) of the window or a regex string in the format of `\"/{regex}/\"` which will match the window \"title\".\n# icon: Your beatifull icon\n#\n# verbose format:\n# \n#\t'{pattern}' = { type = 'generic' | 'exact', value = '{icon}' }\n#\n# combined window matcher format:\n#\n#\t'{name}' = { app_id = 'steam', title = '/Eve/', value = '{icon}' }\n#\n# It matches when all specified fields match. You can combine any of app_id, class and title.\n#\n#\n\n# If it couldn't match something it will print:\n# \n# WARN [sworkstyle:config] No match for app_id=\"{app_id}\" class=\"{class}\" title=\"{title}\"\n#\n# You can use {title} to do a generic matching \n# You can use {app_id} to do an exact match\n\nfallback = ''\nseparator = ' '\n\n[matching]\n'discord' = ''\n'WebCord' = ''\n'vesktop' = ''\n'Element' = ''\n'Signal' = ''\n'balena-etcher' = ''\n'Chia Blockchain' = ''\n'Steam' = ''\n'vlc' = ''\n'mpv' = ''\n'Gimp' = ''\n'darktable' = ''\n'org.kde.digikam' = ''\n'pavucontrol' = ''\n'org.gnome.Nautilus' = ''\n'eog' = ''\n'org.qbittorrent.qBittorrent' = ''\n'Thunderbird' = ''\n'thunderbird' = ''\n'Postman' = ''\n'Insomnia' = ''\n'Bitwarden' = ''\n'Google-chrome' = ''\n'google-chrome' = ''\n'Chromium' = ''\n'Slack' = ''\n'Code' = ''\n'code-oss' = ''\n'Emacs' = ''\n'org.gnome.Calculator' = ''\n'jetbrains-studio' = ''\n\"transmission-remote-gtk\" = \"\"\n'Spotify' = ''\n'spotify' = ''\n'GitHub Desktop' = ''\n'/(?i)^Github.*Firefox/' = ''\n'firefox' = ''\n'Nightly' = ''\n'firefoxdeveloperedition' = ''\n'/nvim ?\\w*/' = ''\n'/npm/' = ''\n'/node/' = ''\n'/yarn/' = ''\n'Alacritty' = ''\n'foot' = ''\n'kitty' = ''\n'VirtualBox Manager' = ''\n'VirtualBox Machine' = ''\n'VirtualBox' = ''\n'openscad' = ''\n\"org.freecadweb.FreeCAD\"= \"\"\n\"com/.https://ultimaker.UltiMaker-Cura\"= \"\"\n\"remarkhub\" = \"\"\n"
  },
  {
    "path": "rust-toolchain",
    "content": "stable\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2021\"\n"
  },
  {
    "path": "script/release",
    "content": "#!/usr/bin/env python3\n#\n# A script that detect packages and creates a new release for them.\n# It makes a lot of assumptions so make sure all url's given in the\n# output makes sense before approving a release.\n#\n\nfrom abc import ABCMeta, abstractmethod\nfrom genericpath import exists\nimport json\nfrom os import chdir\nimport os\nimport re\nfrom shutil import rmtree\nimport subprocess\nimport sys\nfrom tempfile import mkdtemp\nfrom typing import Dict, List, NoReturn, Optional\nfrom urllib.request import Request, urlopen\n\n\ndef error(*args) -> NoReturn:\n    print(\"\\033[91m\" + \" \".join(args) + \"\\033[0m\", file=sys.stderr)\n    exit(1)\n\n\ndef warn(*args) -> None:\n    print(\"\\033[93m\" + \" \".join(args) + \"\\033[0m\", file=sys.stderr)\n\n\ndef debug(*args: str) -> None:\n    print(\"\\033[94m\" + \" \".join(args) + \"\\033[0m\")\n\n\ndef info(*args: str) -> None:\n    print(\"\\033[92m\" + \" \".join(args) + \"\\033[0m\")\n\n\ndef exec(command: str) -> str:\n    debug(f\"Executing '{command}'\")\n    res = subprocess.run(command, capture_output=True, shell=True)\n    if res.returncode != 0:\n        error(\n            f\"Command failed with code {res.returncode}\\n\", res.stderr.decode(\"utf-8\")\n        )\n    return res.stdout.decode(\"utf-8\").rstrip()\n\n\ndef request(url: str, headers: Optional[Dict[str, str]] = None) -> str:\n    if headers == None:\n        headers = {}\n    req = Request(url, headers=headers)\n    with urlopen(req) as response:\n        if response.status != 200:\n            error(f\"{url} returned {response.status}\")\n        return response.read().decode(\"utf-8\")\n\n\nHOME = os.environ[\"HOME\"]\nVERSION = sys.argv[1]\npattern = \"^[0-9]*\\\\.[0-9]*\\\\.[0-9]*$\"\nif re.match(pattern, VERSION) == None:\n    error(\"No version given\")\n\n\nclass Module(metaclass=ABCMeta):\n    def name(self) -> str:\n        return type(self).__name__\n\n    @abstractmethod\n    def should_load(self) -> bool:\n        \"Should this module load\"\n\n    @abstractmethod\n    def link(self) -> Optional[str]:\n        \"\"\"Give a link to the project\"\"\"\n\n    @abstractmethod\n    def validate(self) -> None:\n        \"\"\"Validate that the module has everything it needs to release, to ensure successful release\"\"\"\n\n    def pre_release(self) -> None:\n        pass\n\n    @abstractmethod\n    def release(self) -> None:\n        \"\"\"Make release on given module\"\"\"\n\n\nclass Github(Module):\n    def should_load(self):\n        url = exec(\"git remote get-url origin\")\n        return \"github.com\" in url\n\n    def link(self):\n        return exec(\"gh browse -n\")\n\n    def validate(self):\n        exec(\"gh auth status\")\n\n    def release(self):\n        notes = input(\"\\033[92mGithub Release notes: \\033[0m\")\n        exec(f\"gh release create {VERSION} --notes '{notes}'\")\n\n\nclass Aur(Module):\n    def should_load(self):\n        repo_name = exec(\"git remote get-url origin\")\n        git_username = exec(\"git config --get user.name\")\n        repo_name = repo_name.split(\"/\")[-1].split(\".\")[0]\n\n        data = request(\n            f\"https://aur.archlinux.org/rpc/?v=5&type=search&arg={repo_name}\"\n        )\n\n        data = json.loads(data)\n\n        results = data[\"results\"]\n        filtered_results = []\n        for res in results:\n            if res[\"Maintainer\"].lower() == git_username.lower():\n                filtered_results.append(res)\n        # Sort by popularity\n        filtered_results.sort(key=lambda r: r[\"Popularity\"])\n\n        if len(filtered_results) == 0:\n            return False\n\n        self.package = filtered_results[0].get(\"Name\")\n        self.maintainer = filtered_results[0].get(\"Maintainer\")\n        return True\n\n    def link(self):\n        return f\"https://aur.archlinux.org/packages/{self.package}\"\n\n    def validate(self):\n        # Check if can push to AUR\n        tmp = mkdtemp()\n        root_path = os.getcwd()\n        chdir(tmp)\n        exec(f\"git clone ssh://aur@aur.archlinux.org/{self.package}.git .\")\n        exec(\"git push --dry-run\")\n        exec(\"makepkg --help\")\n        chdir(root_path)\n        rmtree(tmp)\n\n    def _sha256sums(self, pkgbuild: str) -> str:\n        debug(\"Generating sha256sums\")\n        pkgbuild_lines = pkgbuild.splitlines()\n        sha_lines = []  # all lines containing sha's\n        check_ending = False\n        for i, line in enumerate(pkgbuild_lines):\n            if check_ending:\n                sha_lines.append(i)\n                if \")\" in line:\n                    check_ending = False\n            if line.startswith(\"sha256sums=\"):\n                sha_lines.append(i)\n                if \")\" in line:\n                    break\n                check_ending = True\n        sum = exec(\"makepkg -g\")  # generate new sums\n        # remove old sums\n        sha_lines.reverse()  # remove from big to small\n        for i in sha_lines:\n            del pkgbuild_lines[i]\n        pkgbuild = \"\"\n        # inject new sum in new pkgbuild\n        for i, line in enumerate(pkgbuild_lines):\n            if i == sha_lines[0] - 1:\n                pkgbuild += sum + \"\\n\"\n            pkgbuild += line + \"\\n\"\n        return pkgbuild.rstrip()\n\n    def release(self):\n        tmp = mkdtemp()\n        debug(f\"Created {tmp}\")\n        root_path = os.getcwd()\n        chdir(tmp)\n        exec(f\"git clone ssh://aur@aur.archlinux.org/{self.package}.git .\")\n\n        with open(\"PKGBUILD\", \"r\") as file:\n            pkgbuild: str = file.read()\n        pkgbuild = re.sub(\n            \"^pkgver\\\\s*=.*\", f\"pkgver={VERSION}\", pkgbuild, 1, re.MULTILINE\n        )\n        pkgbuild = re.sub(\"^pkgrel\\\\s*=.*\", f\"pkgrel=1\", pkgbuild, 1, re.MULTILINE)\n\n        # write before generating sums\n        with open(\"PKGBUILD\", \"w\") as file:\n            file.write(pkgbuild)\n\n        if re.search(\"^sha256sums\\\\s*=\", pkgbuild, re.MULTILINE):\n            pkgbuild = self._sha256sums(pkgbuild)\n\n            # write with sums\n            with open(\"PKGBUILD\", \"w\") as file:\n                file.write(pkgbuild)\n\n        exec(\"makepkg --printsrcinfo > .SRCINFO\")\n        exec(\"makepkg --check\")  # Ensure install works\n        exec(\"git add PKGBUILD .SRCINFO\")\n        exec(f\"git commit -m 'Release {VERSION}'\")\n        exec(\"git push\")\n\n        chdir(root_path)\n        rmtree(tmp)\n\n\nclass Cargo(Module):\n    def should_load(self):\n        return exists(\"Cargo.toml\")\n\n    def link(self):\n        with open(\"Cargo.toml\", \"r\") as file:\n            name = re.search(\"^name\\\\s*=.*\", file.read(), re.MULTILINE)\n            if name is None:\n                error(\"Could not find crate name\")\n            name = str(name.group(0)).split(\"=\")[1].replace(\" \", \"\").replace('\"', \"\")\n\n        return f\"https://crates.io/crates/{name}\"\n\n    def validate(self):\n        if not exists(f\"{HOME}/.cargo/credentials\"):\n            error(f\"{HOME}/.cargo/credentials does not exist\")\n\n        exec(\"cargo test\")\n        exec(\"cargo build --release --locked\")\n        exec(\"cargo publish --dry-run\")\n\n    def pre_release(self) -> None:\n        debug(\"Updating version in Cargo.toml\")\n        with open(\"Cargo.toml\", \"r\") as file:\n            cargo_toml: str = file.read()\n        cargo_toml = re.sub(\n            \"^version\\\\s*=.*\", f'version = \"{VERSION}\"', cargo_toml, 1, re.MULTILINE\n        )\n        with open(\"Cargo.toml\", \"w\") as file:\n            file.write(cargo_toml)\n\n        if exec(\"git diff\") != \"\":\n            # Update lock file\n            exec(\"cargo build --release --offline\")\n            exec(\"git add Cargo.toml Cargo.lock\")\n            exec(f\"git commit -m 'Release {VERSION}'\")\n            exec(\"git push\")\n\n    def release(self):\n        exec(f\"cargo publish\")\n\n\ndef prepare_branch() -> str:\n    \"\"\"Returns the remote\"\"\"\n    branch = \"master\"\n    if \"master\" not in exec(\"git branch\"):\n        branch = \"main\"\n    remote = exec(f\"git config branch.{branch}.remote\")\n\n    exec(f\"git switch {branch}\")\n    exec(f\"git pull {remote} {branch}\")\n    exec(f\"git push -u {remote} {branch}\")\n    return remote\n\n\nDEFAULT_MODULES = [Github(), Cargo(), Aur()]\n\n\ndef release():\n    root = exec(\"git rev-parse --show-toplevel\")\n    debug(f\"Moving to root '{root}'\")\n    chdir(root)\n\n    if exec(\"git status --short\") != \"\":\n        error(\"Git branch is dirty\")\n\n    remote = prepare_branch()\n\n    modules: List[Module] = []\n    for module in DEFAULT_MODULES:\n        if module.should_load():\n            debug(f\"Found {module.name()}\")\n            modules.append(module)\n\n    for module in modules:\n        module.validate()\n\n    for module in modules:\n        info(f\"Will release on {module.name()} ({module.link()})\")\n\n    answer = input(\"\\033[92mProceed with release? [Y/n] \\033[0m\")\n    if answer.lower() != \"y\":\n        error(\"\")\n\n    for module in modules:\n        module.pre_release()\n\n    exec(f\"git tag {VERSION}\")\n    exec(f\"git push {remote} --tags\")\n    for module in modules:\n        module.release()\n    info(f\"Release {VERSION} finished\")\n\n\nrelease()\n"
  },
  {
    "path": "src/config/config_error.rs",
    "content": "use std::{error::Error, fmt::Display};\n\n#[derive(Debug)]\npub struct ConfigError {\n    message: String,\n}\n\nimpl ConfigError {\n    pub fn new<S: Into<String>>(message: S) -> ConfigError {\n        ConfigError {\n            message: message.into(),\n        }\n    }\n}\n\nimpl<'n> Display for ConfigError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        return write!(f, \"{}\", self.message);\n    }\n}\n\nimpl From<toml::de::Error> for ConfigError {\n    fn from(e: toml::de::Error) -> Self {\n        ConfigError::new(e.to_string())\n    }\n}\n\nimpl Error for ConfigError {}\n"
  },
  {
    "path": "src/config/parse_content_to_config.rs",
    "content": "use std::convert::TryFrom;\n\nuse toml::Value;\n\nuse super::{config_error::ConfigError, Config, Match, Pattern};\n\nfn parse_pattern_field(\n    table: &toml::map::Map<String, Value>,\n    key: &str,\n) -> Result<Option<Pattern>, ConfigError> {\n    match table.get(key) {\n        Some(value) => {\n            let value = value\n                .as_str()\n                .ok_or(ConfigError::new(format!(\"Value of {key} is not a string\")))?;\n\n            Ok(Some(Pattern::try_from(value.to_string()).map_err(|e| {\n                ConfigError::new(format!(\"Invalid pattern given for '{value}': {e}\"))\n            })?))\n        }\n        None => Ok(None),\n    }\n}\n\n/// Parse toml config content to icon_map\npub fn parse_content_to_config(content: &String) -> Result<Config, ConfigError> {\n    let map: Value = toml::from_str(content)?;\n\n    let map_to_match = |k: (&String, &Value)| -> Result<Match, ConfigError> {\n        if let Some(value) = k.1.as_str() {\n            let value = value.to_string();\n            let pattern = Pattern::try_from(k.0.to_string()).map_err(|e| ConfigError::new(format!(\n                \"Invalid pattern given for '{}': {}\",\n                k.0, e\n            )))?;\n\n            match pattern {\n                Pattern::Regex(_) => return Ok(Match::Generic { pattern, value }),\n                Pattern::String(pattern) => return Ok(Match::Exact { pattern, value }),\n            };\n        }\n\n        if let Some(table) = k.1.as_table() {\n            let value = table\n                .get(\"value\")\n                .ok_or(ConfigError::new(format!(\"Could not parse: {}\", k.0)))?\n                .as_str()\n                .ok_or(ConfigError::new(format!(\n                    \"Value of {} is not a string\",\n                    k.0\n                )))?\n                .to_string();\n\n            if let Some(match_type) = table.get(\"type\") {\n                let match_type = match_type.as_str().ok_or(ConfigError::new(format!(\n                    \"Value of {} is not a string\",\n                    k.0\n                )))?;\n\n                let m = match &match_type[..] {\n                    \"exact\" => Match::Exact {\n                        pattern: k.0.to_string(),\n                        value,\n                    },\n                    \"generic\" => Match::Generic {\n                        pattern: Pattern::try_from(k.0.to_string()).map_err(|e| ConfigError::new(\n                            format!(\"Invalid pattern given for '{}': {}\", k.0, e),\n                        ))?,\n                        value,\n                    },\n                    _ => return Err(ConfigError::new(format!(\"Invalid match type: {}\", k.1))),\n                };\n\n                return Ok(m);\n            }\n\n            let app_id = parse_pattern_field(table, \"app_id\")?;\n            let class = parse_pattern_field(table, \"class\")?;\n            let title = parse_pattern_field(table, \"title\")?;\n\n            if app_id.is_none() && class.is_none() && title.is_none() {\n                return Err(ConfigError::new(format!(\n                    \"Could not parse: {}. Expected one of app_id, class or title\",\n                    k.0\n                )));\n            }\n\n            return Ok(Match::Window {\n                app_id,\n                class,\n                title,\n                value,\n            });\n        }\n\n        Err(ConfigError::new(format!(\n            \"{} could not be parsed as a table\",\n            k.1\n        )))\n    };\n\n    match map {\n        Value::Table(root) => {\n            let matching: Vec<Match> = root\n                .get(\"matching\")\n                .ok_or(ConfigError::new(\"Matching table not found\"))?\n                .as_table()\n                .ok_or(ConfigError::new(\"Could not parse matching table\"))?\n                .iter()\n                .map(map_to_match)\n                .collect::<Result<Vec<Match>, ConfigError>>()?\n                .into_iter()\n                .collect();\n\n            let fallback: Option<String> = match root.get(\"fallback\") {\n                Some(value) => {\n                    let f = value\n                        .as_str()\n                        .ok_or(ConfigError::new(\"Fallback is not a string\"))?;\n                    Some(f.to_string())\n                }\n                None => None,\n            };\n\n            let separator: Option<String> = match root.get(\"separator\") {\n                Some(value) => {\n                    let f = value\n                        .as_str()\n                        .ok_or(ConfigError::new(\"Separator is not a string\"))?;\n                    Some(f.to_string())\n                }\n                None => None,\n            };\n\n            Ok(Config {\n                matchings: matching,\n                fallback,\n                separator,\n            })\n        }\n        _ => Err(ConfigError::new(\"No root table found\")),\n    }\n}\n\n#[test]\nfn test_parse_content_to_config() {\n    use regex::Regex;\n\n    let no_match_table = parse_content_to_config(&String::from(\"fallback = 'c'\"));\n\n    assert_eq!(\n        no_match_table.unwrap_err().to_string(),\n        \"Matching table not found\"\n    );\n\n    let content = \"\n    [matching]\n    a = b\n    \";\n    let invalid_match = parse_content_to_config(&content.to_string());\n    let e = invalid_match.unwrap_err();\n    assert!(\n        e.to_string().starts_with(\"TOML parse error\"),\n        \"error message not as expected: {e:?}\"\n    );\n\n    let content = \"\n    [matching]\n    \n    'fdsa' = 'a'\n    '/asdf/' = 'b'\n    test = { type = 'generic', value = 'c' }\n    qwer = { type = 'exact', value = 'd' }\n    \";\n    let icon_map = parse_content_to_config(&content.to_string()).unwrap();\n\n    assert_eq!(\n        icon_map.matchings[0],\n        Match::Exact {\n            value: \"a\".to_string(),\n            pattern: \"fdsa\".to_string()\n        }\n    );\n    assert_eq!(\n        icon_map.matchings[1],\n        Match::Generic {\n            value: \"b\".to_string(),\n            pattern: Pattern::Regex(Regex::new(\"asdf\").unwrap())\n        }\n    );\n    assert_eq!(\n        icon_map.matchings[2],\n        Match::Generic {\n            value: \"c\".to_string(),\n            pattern: Pattern::String(\"test\".to_string())\n        }\n    );\n    assert_eq!(\n        icon_map.matchings[3],\n        Match::Exact {\n            value: \"d\".to_string(),\n            pattern: \"qwer\".to_string()\n        }\n    );\n\n    let content = \"\n    [matching]\n    steam_eve = { app_id = 'steam', title = '/Eve/', value = 'steam-icon' }\n    \";\n    let icon_map = parse_content_to_config(&content.to_string()).unwrap();\n    assert_eq!(\n        icon_map.matchings[0],\n        Match::Window {\n            app_id: Some(Pattern::String(\"steam\".to_string())),\n            class: None,\n            title: Some(Pattern::Regex(Regex::new(\"Eve\").unwrap())),\n            value: \"steam-icon\".to_string(),\n        }\n    );\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use std::{convert::TryFrom, fs::read_to_string, path::Path, str::from_utf8};\n\nuse log::{debug, error, info, warn};\nuse regex::Regex;\n\nuse crate::util::prettify_option;\n\nmod config_error;\n\nmod parse_content_to_config;\nuse parse_content_to_config::parse_content_to_config;\n\npub const DEFAULT_MATCH_CONFIG: &'static [u8] = include_bytes!(\"../default_config.toml\");\n\n#[derive(Clone, Debug)]\npub enum Pattern {\n    Regex(Regex),\n    String(String),\n}\n\nimpl PartialEq for Pattern {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Self::String(r0), Self::Regex(l0)) => l0.to_string() == r0.to_string(),\n            (Self::Regex(l0), Self::String(r0)) => l0.to_string() == r0.to_string(),\n            (Self::Regex(l0), Self::Regex(r0)) => l0.to_string() == r0.to_string(),\n            (Self::String(l0), Self::String(r0)) => l0 == r0,\n        }\n    }\n}\n\nimpl TryFrom<String> for Pattern {\n    type Error = regex::Error;\n\n    fn try_from(mut value: String) -> Result<Self, Self::Error> {\n        if value.starts_with(\"/\") && value.ends_with(\"/\") {\n            value.remove(value.len() - 1);\n            value.remove(0);\n            let regex = Regex::new(&value)?;\n            Ok(Pattern::Regex(regex))\n        } else {\n            Ok(Pattern::String(value))\n        }\n    }\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum Match {\n    Generic { pattern: Pattern, value: String },\n    Exact { pattern: String, value: String },\n    Window {\n        app_id: Option<Pattern>,\n        class: Option<Pattern>,\n        title: Option<Pattern>,\n        value: String,\n    },\n}\n\n#[derive(Clone, Debug)]\npub struct Config {\n    pub matchings: Vec<Match>,\n    pub fallback: Option<String>,\n    pub separator: Option<String>,\n}\n\nimpl Config {\n    pub fn new<P: AsRef<Path>>(config_path: &Option<P>) -> Config {\n        if let Some(config_path) = config_path {\n            match read_to_string(&config_path) {\n                Ok(content) => return Config::from(content),\n                Err(e) => {\n                    debug!(\n                        \"Could not create config from path: {:?} {e}\",\n                        config_path.as_ref()\n                    )\n                }\n            }\n        } else {\n            warn!(\"Default config could not have been found\")\n        }\n\n        return Config::default();\n    }\n\n    fn matches_generic_pattern(pattern: &Pattern, value: &String) -> bool {\n        match pattern {\n            Pattern::Regex(r) => r.is_match(value),\n            Pattern::String(p) => value.to_lowercase().contains(&p.to_lowercase()),\n        }\n    }\n\n    fn matches_exact_pattern(pattern: &Pattern, value: &String) -> bool {\n        match pattern {\n            Pattern::Regex(r) => r.is_match(value),\n            Pattern::String(p) => value == p,\n        }\n    }\n\n    pub fn fetch_icon(\n        &self,\n        app_id: Option<&String>,\n        class: Option<&String>,\n        title: Option<&String>,\n    ) -> String {\n        for m in &self.matchings {\n            match m {\n                Match::Generic { pattern, value } => {\n                    if let Some(title) = &title {\n                        if Self::matches_generic_pattern(pattern, title) {\n                            return value.clone();\n                        }\n                    }\n                }\n                Match::Exact { pattern, value } => {\n                    if let Some(class_name) = class {\n                        if class_name == pattern {\n                            return value.clone();\n                        }\n                    }\n                    if let Some(app_id_name) = app_id {\n                        if app_id_name == pattern {\n                            return value.clone();\n                        }\n                    }\n                }\n                Match::Window {\n                    app_id: app_id_pattern,\n                    class: class_pattern,\n                    title: title_pattern,\n                    value,\n                } => {\n                    let app_id_matches = app_id_pattern.as_ref().map_or(true, |pattern| {\n                        app_id\n                            .as_ref()\n                            .is_some_and(|app_id| Self::matches_exact_pattern(pattern, app_id))\n                    });\n                    let class_matches = class_pattern.as_ref().map_or(true, |pattern| {\n                        class\n                            .as_ref()\n                            .is_some_and(|class| Self::matches_exact_pattern(pattern, class))\n                    });\n                    let title_matches = title_pattern.as_ref().map_or(true, |pattern| {\n                        title\n                            .as_ref()\n                            .is_some_and(|title| Self::matches_generic_pattern(pattern, title))\n                    });\n\n                    if app_id_matches && class_matches && title_matches {\n                        return value.clone();\n                    }\n                }\n            }\n        }\n\n        warn!(\n            \"No match for app_id=\\\"{}\\\" class=\\\"{}\\\" title=\\\"{}\\\"\",\n            prettify_option(app_id),\n            prettify_option(class),\n            prettify_option(title),\n        );\n\n        self.fallback()\n    }\n\n    pub fn fallback(&self) -> String {\n        match &self.fallback {\n            Some(fallback) => {\n                info!(\"Using fallback: {}\", fallback);\n                fallback.clone()\n            }\n            None => {\n                warn!(\"No fallback set using empty string\");\n                String::from(\"\")\n            }\n        }\n    }\n}\n\nimpl<S: Into<String>> From<S> for Config {\n    /// Parse a string to a config enriching it with the default config\n    fn from(value: S) -> Self {\n        let value = value.into();\n        let mut default = Config::default();\n\n        match parse_content_to_config(&value) {\n            Ok(mut user_config) => {\n                user_config.matchings.append(&mut default.matchings);\n\n                if user_config.separator.is_none() {\n                    user_config.separator = default.separator\n                }\n\n                if user_config.fallback.is_none() {\n                    warn!(\n                        \"No fallback set using default: {}\",\n                        prettify_option(default.fallback.as_ref())\n                    );\n                    user_config.fallback = default.fallback\n                }\n\n                user_config\n            }\n            Err(e) => {\n                error!(\"Invalid config format: {}\", e);\n                return default;\n            }\n        }\n    }\n}\n\nimpl Default for Config {\n    fn default() -> Self {\n        let default_config_content = from_utf8(DEFAULT_MATCH_CONFIG).unwrap().to_string();\n        return parse_content_to_config(&default_config_content).unwrap();\n    }\n}\n\n#[test]\nfn test_default() {\n    let config = Config::default();\n    assert_eq!(config.fallback.unwrap(), \"\")\n}\n\n#[test]\nfn test_from_string() {\n    let config = Config::from(\n        \"\n    fallback = 'c'\n    [matching]\n    a = 'b'\n    b = 'c'\n    '/(?i)A title/' = 'd' \n    \",\n    );\n\n    assert_eq!(config.fallback(), \"c\");\n    assert_eq!(\n        config.fetch_icon(\n            Some(&String::from(\"application\")),\n            None,\n            Some(&String::from(\"a title\"))\n        ),\n        \"d\"\n    );\n}\n\n#[test]\nfn test_window_match() {\n    let config = Config::from(\n        \"\n    fallback = 'x'\n    [matching]\n    steam_eve = { app_id = 'steam', title = '/Eve/', value = 's' }\n    \",\n    );\n\n    assert_eq!(\n        config.fetch_icon(\n            Some(&String::from(\"steam\")),\n            None,\n            Some(&String::from(\"Eve Online\"))\n        ),\n        \"s\"\n    );\n    assert_eq!(\n        config.fetch_icon(\n            Some(&String::from(\"steam\")),\n            None,\n            Some(&String::from(\"Counter-Strike\"))\n        ),\n        \"x\"\n    );\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "use futures_lite::prelude::*;\n\nuse async_io::Async;\nuse futures_lite::stream;\nuse inotify::{Inotify, WatchMask};\nuse std::{\n    collections::BTreeSet,\n    error::Error,\n    path::{Path, PathBuf},\n};\n\nuse log::{debug, error, info, warn};\nuse swayipc_async::{Connection, Event, EventType, Node, NodeType, WindowChange};\n\npub mod config;\nmod util;\n\nuse config::Config;\n\npub type SworkstyleError = Box<dyn Error>;\n\nstruct ConfigSource {\n    path: PathBuf,\n    inotify: Inotify,\n}\n\nimpl ConfigSource {\n    fn new(path: impl AsRef<Path>) -> ConfigSource {\n        let inotify = Inotify::init().expect(\"Error while initializing inotify instance\");\n        inotify\n            .watches()\n            .add(&path, WatchMask::CLOSE_WRITE)\n            .expect(\"Failed to watch config file\");\n\n        ConfigSource {\n            path: path.as_ref().to_path_buf(),\n            inotify,\n        }\n    }\n}\n\npub struct Sworkstyle {\n    config: Config,\n    config_source: Option<ConfigSource>,\n    deduplicate: bool,\n}\n\nimpl Sworkstyle {\n    pub fn new<P: AsRef<Path>>(config_path: Option<P>, deduplicate: bool) -> Sworkstyle {\n        let config = Config::new(&config_path);\n        let config_source =\n            config_path.and_then(|path| path.as_ref().exists().then(|| ConfigSource::new(path)));\n        Sworkstyle {\n            config,\n            config_source,\n            deduplicate,\n        }\n    }\n\n    // Takes `self` by value because we consume `config_source`.\n    pub async fn run(mut self) -> Result<(), SworkstyleError> {\n        enum Message {\n            Event(Event),\n            Config(Config),\n        }\n\n        let mut events = Connection::new()\n            .await?\n            .subscribe(&[EventType::Window])\n            .await?\n            .map(|r| r.map(Message::Event))\n            .boxed();\n        let mut connection = Connection::new().await?;\n\n        if let Some(source) = self.config_source.take() {\n            events = events\n                .or(stream::try_unfold(source, |source| async {\n                    let path = source.path;\n                    let anotify = Async::new(source.inotify)?;\n                    anotify.readable().await?;\n                    let mut inotify = anotify.into_inner()?;\n                    let mut inotify_events_buffer = [0; 1024];\n                    inotify.read_events(&mut inotify_events_buffer)?;\n                    info!(\"Detected config change, reloading config..\");\n                    let config = Config::new(&Some(&path));\n                    // Reset watcher\n                    inotify\n                        .watches()\n                        .add(&path, WatchMask::CLOSE_WRITE)\n                        .expect(\"Failed to watch config file\");\n\n                    Ok(Some((\n                        Message::Config(config),\n                        ConfigSource { path, inotify },\n                    )))\n                }))\n                .boxed();\n        }\n\n        if let Err(e) = self.update_workspaces(&mut connection).await {\n            error!(\"Could not initialize workspace name: {}\", e);\n        }\n\n        while let Some(msg) = events.next().await {\n            match msg {\n                Ok(Message::Event(Event::Window(e))) => {\n                    if matches!(\n                        e.change,\n                        WindowChange::Focus\n                            | WindowChange::FullscreenMode\n                            | WindowChange::Floating\n                            | WindowChange::Urgent\n                            | WindowChange::Mark\n                    ) {\n                        // Event not relevant to us: skip the update_workspaces_call below.\n                        continue;\n                    }\n                }\n                // Should not be reachable: we are only subscribed to window events.\n                Ok(Message::Event(_)) => {}\n                Ok(Message::Config(config)) => {\n                    self.config = config;\n                }\n                Err(e) => {\n                    warn!(\"Error while waiting for Sway or config events, exiting: {e}\");\n                    return Err(Box::new(e));\n                }\n            }\n            if let Err(e) = self.update_workspaces(&mut connection).await {\n                error!(\"Could not update workspace name: {}\", e);\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn update_workspaces(&self, conn: &mut Connection) -> Result<(), SworkstyleError> {\n        let tree = conn.get_tree().await?;\n\n        let mut workspaces = vec![];\n        get_workspaces_recurse(&tree, &mut workspaces);\n\n        for workspace in workspaces {\n            self.update_workspace_name(conn, workspace).await?;\n        }\n\n        Ok(())\n    }\n\n    async fn update_workspace_name(\n        &self,\n        conn: &mut Connection,\n        workspace: &Node,\n    ) -> Result<(), SworkstyleError> {\n        let mut windows = vec![];\n        get_windows(workspace, &mut windows);\n\n        let mut window_names: Vec<(Option<&String>, Option<&String>, Option<String>)> = windows\n            .iter()\n            .map(|node| {\n                // Wayland Exact app\n                let app_id = node.app_id.as_ref();\n\n                // X11 Exact\n                let class = node.window_properties.as_ref().and_then(|props| props.class.as_ref());\n\n                (app_id, class, node.name.clone())\n            })\n            .collect();\n\n        if self.deduplicate {\n            window_names = window_names\n                .into_iter()\n                .collect::<BTreeSet<(Option<&String>, Option<&String>, Option<String>)>>()\n                .into_iter()\n                .collect();\n        }\n\n        let mut icons: Vec<String> = window_names\n            .into_iter()\n            .map(|(app_id, class, title)| {\n                if app_id.is_none() && class.is_none() {\n                    error!(\"No app_id/class found for window with title={:?}\", title);\n                }\n                self.config.fetch_icon(app_id, class, title.as_ref())\n            })\n            .filter(|icon| !icon.is_empty())\n            // Overwrite right to left characters: https://www.unicode.org/versions/Unicode12.0.0/UnicodeStandard-12.0.pdf#G26.16327\n            .map(|icon| format!(\"\\u{202D}{icon}\\u{202C}\"))\n            .collect();\n\n        let name = match &workspace.name {\n            Some(name) => name,\n            None => {\n                return Err(\n                    format!(\"Could not get name for workspace with id: {}\", workspace.id).into(),\n                )\n            }\n        };\n\n        let index = match workspace.num {\n            Some(num) => num,\n            None => return Err(format!(\"Could not fetch index for: {}\", name).into()),\n        };\n\n        if self.deduplicate {\n            icons.dedup();\n        }\n\n        let delim = self.config.separator.as_deref().unwrap_or(\" \");\n\n        let mut icons = icons.join(delim);\n\n        if icons.len() > 0 {\n            icons.push_str(\" \")\n        }\n\n        let new_name = if icons.len() > 0 {\n            format!(\"{}: {}\", index, icons)\n        } else if let Some(num) = workspace.num {\n            format!(\"{}\", num)\n        } else {\n            error!(\"Could not fetch workspace num for: {:?}\", workspace.name);\n            \" \".to_string()\n        };\n\n        if *name != new_name {\n            debug!(\"rename workspace \\\"{}\\\" to \\\"{}\\\"\", name, new_name);\n\n            conn.run_command(format!(\"rename workspace \\\"{}\\\" to \\\"{}\\\"\", name, new_name))\n                .await?;\n        }\n\n        return Ok(());\n    }\n}\n\nfn get_workspaces_recurse<'a>(node: &'a Node, workspaces: &mut Vec<&'a Node>) {\n    if node.node_type == NodeType::Workspace && node.name != Some(\"__i3_scratch\".to_string()) {\n        workspaces.push(node);\n        return;\n    }\n\n    for child in node.nodes.iter() {\n        get_workspaces_recurse(child, workspaces)\n    }\n}\n\n/// Rescursively add nodes with node type floatingCon and con to windows\nfn get_windows<'a>(node: &'a Node, windows: &mut Vec<&'a Node>) {\n    if node.node_type == NodeType::FloatingCon || node.node_type == NodeType::Con {\n        if let Some(_) = node.name {\n            windows.push(node)\n        }\n    };\n\n    for node in node.nodes.iter().chain(node.floating_nodes.iter()) {\n        get_windows(node, windows);\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "use std::process;\nuse sworkstyle::Sworkstyle;\n\nuse fslock::LockFile;\nuse log::{debug, error};\nuse simple_logger::SimpleLogger;\n\nuse std::{env, path::PathBuf};\n\nuse log::LevelFilter;\n\nconst VERSION: &str = env!(\"CARGO_PKG_VERSION\");\n\npub struct Args {\n    pub log_level: LevelFilter,\n    pub config_path: Option<PathBuf>,\n    pub deduplicate: bool,\n}\n\n/// Get the xdg default config path\nfn default_config_path() -> Option<PathBuf> {\n    Some(dirs::config_dir()?.join(\"sworkstyle/config.toml\"))\n}\n\nimpl Args {\n    pub fn from_cli() -> Args {\n        let mut log_level = LevelFilter::Warn;\n        let mut config_path = default_config_path();\n        let mut deduplicate = false;\n\n        let mut args = env::args().skip(1);\n        while let Some(arg) = args.next() {\n            match &arg[..] {\n                \"-h\" | \"--help\" => {\n                    println!(\n                        \"Swayest Workstyle v{VERSION}\nThis tool will rename workspaces to the icons configured.\nConfig can be found in $HOME/.config/sworkstyle\n\nSYNOPSIS\n    sworkstyle [FLAGS]\n\nFLAGS\n    -h, --help\n        Display a description of this program.\n    \n    -v, --version\n        Print the current version\n\n    -l, --log-level <level>\n        Either \\\"error\\\", \\\"warn\\\", \\\"info\\\", \\\"debug\\\", \\\"off\\\". Uses \\\"warn\\\" by default\n        \n    -c, --config <file>\n        Specifies the config file to use. Uses \\\"`XDG_CONFIG_HOME`/sworkstyle/config\\\" by default\n\n    -d, --deduplicate\n        Deduplicate the same icons in your workspace\n        \"\n                    );\n                    process::exit(0);\n                }\n                \"-v\" | \"--version\" => {\n                    println!(\"{VERSION}\");\n                    process::exit(0)\n                }\n                \"-l\" | \"--log-level\" => {\n                    if let Some(level) = args.next() {\n                        log_level = match &level[..] {\n                            \"error\" => LevelFilter::Error,\n                            \"warn\" => LevelFilter::Warn,\n                            \"info\" => LevelFilter::Info,\n                            \"debug\" => LevelFilter::Debug,\n                            \"off\" => LevelFilter::Off,\n                            _ => {\n                                eprintln!(\"Invalid logging option: {}\", level);\n                                process::exit(1);\n                            }\n                        }\n                    } else {\n                        eprintln!(\"No logging option given\");\n                        process::exit(1);\n                    }\n                }\n                \"-c\" | \"--config\" => {\n                    if let Some(path) = args.next() {\n                        let path = PathBuf::from(path);\n                        if !path.exists() {\n                            eprintln!(\"Config file does not exist or couldn't be accessed\");\n                            process::exit(1);\n                        }\n                        config_path = Some(path);\n                    } else {\n                        eprintln!(\"No path given\");\n                        process::exit(1);\n                    }\n                }\n                \"-d\" | \"--deduplicate\" => {\n                    deduplicate = true;\n                }\n                _ => {\n                    eprintln!(\"Did not recognize \\\"{}\\\" as an option\", arg);\n                    process::exit(1);\n                }\n            }\n        }\n\n        return Args {\n            log_level,\n            config_path,\n            deduplicate,\n        };\n    }\n}\nfn acquire_lock() {\n    let mut file = match LockFile::open(\"/tmp/sworkstyle.lock\") {\n        Ok(f) => f,\n        _ => return,\n    };\n\n    let locked = file.try_lock().unwrap();\n\n    if locked == false {\n        error!(\"Sworkstyle already running\");\n        process::exit(1)\n    }\n\n    ctrlc::set_handler(move || {\n        debug!(\"Unlocking /tmp/sworkstyle.lock\");\n        file.unlock().unwrap();\n        process::exit(0)\n    })\n    .expect(\"Could not set ctrlc handler\")\n}\n\nfn main() {\n    let args = Args::from_cli();\n\n    SimpleLogger::new()\n        .with_level(args.log_level)\n        .init()\n        .expect(\"Could not load simple logger\");\n\n    acquire_lock();\n\n    let app = Sworkstyle::new(args.config_path, args.deduplicate);\n    if let Err(e) = async_io::block_on(app.run()) {\n        error!(\"{e}\");\n        process::exit(1)\n    }\n}\n"
  },
  {
    "path": "src/util.rs",
    "content": "use std::fmt::Display;\n\n/// Map an option to a printable string\npub fn prettify_option<T: Display>(option: Option<T>) -> String {\n    match option {\n        Some(a) => a.to_string(),\n        None => \"-\".to_string(),\n    }\n}\n"
  }
]