[
  {
    "path": ".clang-tidy",
    "content": "Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'\nWarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'\nCheckOptions:\n  - key:             readability-function-cognitive-complexity.Threshold\n    value:           '50'\n\n# vim:syntax=yaml\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\ncustom: https://www.buymeacoffee.com/boltgolt\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report something that's not working\n\n---\n\n_Please describe the issue in as much detail as possible, including any errors and traces._\n_If your issue is a camera issue, be sure to also post the image generated by running `sudo howdy snapshot`._\n\n\n\n\n----\n\nI've searched for similar issues already, and my issue has not been reported yet.\n\nLinux distribution (if applicable):\n\nHowdy version (`sudo howdy version`):\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: 'Suggest a feature or improvement '\n\n---\n\n\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "_Please make sure to target the \"dev\" branch if it exists_\n_REMOVE THIS MESSAGE IN THE PULL REQUEST_\n"
  },
  {
    "path": ".github/workflows/check.yml",
    "content": "name: check\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install required libraries\n        run: >\n          sudo apt-get update && sudo apt-get install -y \n          python3 python3-pip python3-setuptools python3-wheel\n          cmake make build-essential clang-tidy\n          libpam0g-dev libinih-dev libevdev-dev \n          python3-dev libopencv-dev\n        \n      - name: Install meson\n        run: sudo python3 -m pip install meson ninja\n\n      - uses: actions/checkout@v2\n\n      - name: Build\n        run: |\n          meson setup build\n          ninja -C build\n\n      - name: Check source code\n        run: |\n          ninja clang-tidy -C build\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# generated models\n/howdy/src/models\n\n# snapshots\n/howdy/src/snapshots\n\n# build files\ndebian/howdy.substvars\ndebian/files\ndebian/debhelper-build-stamp\ndebian/howdy\n\n# vscode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Local History for Visual Studio Code\n.history/\n\n# Built Visual Studio Code Extensions\n*.vsix\n\n# Meson\nsubprojects/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 boltgolt\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": "![](https://boltgolt.nl/howdy/banner.png)\n\n<p align=\"center\">\n\t<a href=\"https://github.com/boltgolt/howdy/releases\">\n\t\t<img src=\"https://img.shields.io/github/release/boltgolt/howdy.svg?colorB=4c1\">\n\t</a>\n\t<a href=\"https://github.com/boltgolt/howdy/graphs/contributors\">\n\t\t<img src=\"https://img.shields.io/github/contributors/boltgolt/howdy.svg?style=flat\">\n\t</a>\n\t<a href=\"https://www.buymeacoffee.com/boltgolt\">\n\t\t<img src=\"https://img.shields.io/badge/endpoint.svg?url=https://boltgolt.nl/howdy/shield.json\">\n\t</a>\n\t<a href=\"https://actions-badge.atrox.dev/boltgolt/howdy/goto?ref=beta\">\n\t\t<img src=\"https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fboltgolt%2Fhowdy%2Fbadge%3Fref%3Dbeta&style=flat&label=build&logo=none\">\n\t</a>\n\t<a href=\"https://aur.archlinux.org/packages/howdy\">\n\t\t<img src=\"https://img.shields.io/aur/votes/howdy?color=4c1&label=aur%20votes\">\n\t</a>\n</p>\n\nHowdy provides Windows Hello™ style authentication for Linux. Use your built-in IR emitters and camera in combination with facial recognition to prove who you are.\n\nUsing the central authentication system (PAM), this works everywhere you would otherwise need your password: Login, lock screen, sudo, su, etc.\n\n## Installation\n\nHowdy is currently available and packaged for Debian/Ubuntu, Arch Linux, Fedora and openSUSE. If you’re interested in packaging Howdy for your distro, don’t hesitate to open an issue.\n\n**Note:** The build of dlib can hang on 100% for over a minute, give it time.\n\n### Ubuntu or Linux Mint\n\nRun the installer by pasting (`ctrl+shift+V`) the following commands into the terminal one at a time:\n\n```\nsudo add-apt-repository ppa:boltgolt/howdy\nsudo apt update\nsudo apt install howdy\n```\n\nThis will guide you through the installation.\n\n### Debian\n\nDownload the .deb file from the [Releases page](https://github.com/boltgolt/howdy/releases) and install with gdebi.\n\n### Arch Linux\n\n_Maintainer wanted._\n\nInstall the `howdy` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).\n\nYou will need to do some additional configuration steps. Please read the [ArchWiki entry](https://wiki.archlinux.org/index.php/Howdy) for more information.\n\n### Fedora\n\n_Maintainer: [@luyatshimbalanga](https://github.com/luyatshimbalanga)_\n\nThe `howdy` package is available as a [Fedora COPR repository](https://copr.fedorainfracloud.org/coprs/principis/howdy/), install it by simply executing the following commands in a terminal:\n\n```\nsudo dnf copr enable principis/howdy\nsudo dnf --refresh install howdy\n```\n\n*Note:* Fedora 41 [removed support for Python2](https://fedoraproject.org/wiki/Changes/RetirePython2.7), but at this point in time Howdy still depends on it. If the install fails, you can fix this by installing the beta Repository and removing the release version:\n\n```\nsudo dnf copr remove principis/howdy\nsudo dnf copr enable principis/howdy-beta\nsudo dnf --refresh install howdy\n```\n\nSee the link to the COPR repository for detailed configuration steps.\n\n### openSUSE\n\n_Maintainer: [@dmafanasyev](https://github.com/dmafanasyev)_\n\nGo to the [openSUSE wiki page](https://en.opensuse.org/SDB:Facial_authentication) for detailed installation instructions.\n\n### Building from source\n\nIf you want to build Howdy from source, a few dependencies are required.\n\n#### Dependencies\n\n- Python 3.6 or higher\n  * pip\n  * setuptools\n  * wheel\n- meson version 0.64 or higher\n- ninja\n- INIReader (can be pulled from git automatically if not found)\n- libevdev\n\nTo install them on Debian/Ubuntu for example:\n\n```\nsudo apt-get update && sudo apt-get install -y \\\npython3 python3-pip python3-setuptools python3-wheel \\\ncmake make build-essential \\\nlibpam0g-dev libinih-dev libevdev-dev python3-opencv \\\npython3-dev libopencv-dev\n```\n\n#### Build\n\n```sh\nmeson setup build\nmeson compile -C build\n```\n\nYou can also install Howdy to your system with `meson install -C build`.\n\n## Setup\n\nAfter installation, Howdy needs to learn what you look like so it can recognise you later. Run `sudo howdy add` to add a face model.\n\nIf nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you're experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues.\n\nIf you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has to offer. On most systems this will open the nano editor, where you have to press `ctrl`+`x` to save your changes.\n\n## CLI\n\nThe installer adds a `howdy` command to manage face models for the current user. Use `howdy --help` or `man howdy` to list the available options.\n\nUsage:\n```\nhowdy [-U user] [-y] command [argument]\n```\n\n| Command   | Description                                   |\n|-----------|-----------------------------------------------|\n| `add`     | Add a new face model for a user               |\n| `clear`   | Remove all face models for a user             |\n| `config`  | Open the config file in your default editor   |\n| `disable` | Disable or enable howdy                       |\n| `list`    | List all saved face models for a user         |\n| `remove`  | Remove a specific model for a user            |\n| `snapshot`| Take a snapshot of your camera input          |\n| `test`    | Test the camera and recognition methods       |\n| `version` | Print the current version number              |\n\n## Contributing [![](https://img.shields.io/travis/boltgolt/howdy/dev.svg?label=dev%20build)](https://github.com/boltgolt/howdy/tree/dev) [![](https://img.shields.io/github/issues-raw/boltgolt/howdy/enhancement.svg?label=feature+requests&colorB=4c1)](https://github.com/boltgolt/howdy/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)\n\nThe easiest ways to contribute to Howdy is by starring the repository and opening GitHub issues for features you'd like to see. If you want to do more, you can also [buy me a coffee](https://www.buymeacoffee.com/boltgolt).\n\nCode contributions are also very welcome. If you want to port Howdy to another distro, feel free to open an issue for that too.\n\n## Troubleshooting\n\nAny Python errors get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed, you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there.\n\nPlease first check the [wiki on common issues](https://github.com/boltgolt/howdy/wiki/Common-issues) and \nif you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.\n\n## A note on security\n\nThis package is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you, or a well-printed photo of you could be enough to do it. Howdy is a more quick and convenient way of logging in, not a more secure one.\n\nTo minimize the chance of this program being compromised, it's recommended to leave Howdy in `/lib/security` and to keep it read-only.\n\nDO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.\n"
  },
  {
    "path": "howdy/archlinux/.gitignore",
    "content": "pkg\nsrc\n*.tar.gz\n*.zip\n*.tar.xz\n*.patch\n*.dat.bz2"
  },
  {
    "path": "howdy/archlinux/howdy/.gitignore",
    "content": "pkg\nsrc\n*.tar.gz\n*.zip\n*.tar.xz\n*.patch\n*.dat.bz2\n.SRCINFO\n"
  },
  {
    "path": "howdy/archlinux/howdy/PKGBUILD",
    "content": "# Maintainer: Frank Tackitt <frank@tackitt.net>\n# Maintainer: boltgolt <boltgolt@gmail.com>\n# Co-Maintainer: Raymo111 <hi@raymond.li>\n# Contributor: Kelley McChesney <kelley@kelleymcchesney.us>\n\npkgname=howdy\npkgver=2.6.1\npkgrel=1\npkgdesc=\"Windows Hello for Linux\"\narch=('x86_64')\nurl=\"https://github.com/boltgolt/howdy\"\nlicense=('MIT')\ndepends=(\n\t'opencv'\n\t'hdf5'\n\t'pam-python'\n\t'python3'\n\t'python-dlib'\n\t'python-numpy'\n\t'python-opencv'\n)\nmakedepends=(\n\t'cmake'\n\t'pkgfile'\n)\nbackup=('etc/howdy/config.ini')\nsource=(\n\t\"$pkgname-$pkgver.tar.gz::https://github.com/boltgolt/howdy/archive/v${pkgver}.tar.gz\"\n\t\"https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2\"\n\t\"https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2\"\n\t\"https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2\"\n)\nsha256sums=('f3f48599f78fd82b049539fcfc34de25c9435cad732697bdda94e85352964794'\n            'abb1f61041e434465855ce81c2bd546e830d28bcbed8d27ffbe5bb408b11553a'\n            'db9e9e40f092c118d5eb3e643935b216838170793559515541c56a2b50d9fc84'\n            '6e787bbebf5c9efdb793f6cd1f023230c4413306605f24f299f12869f95aa472')\n\npackage() {\n\t# Installing the proper license files and the rest of howdy\n\tcd howdy-$pkgver\n\tinstall -Dm644 LICENSE \"$pkgdir/usr/share/licenses/$pkgname/LICENSE\"\n\tmkdir -p \"$pkgdir/usr/etc/howdy\"\n\tmkdir -p \"$pkgdir/etc/howdy\"\n\tcp -r src/* \"$pkgdir/usr/etc/howdy\"\n\tcp -r src/config.ini \"$pkgdir/etc/howdy\"\n\tcp \"${srcdir}/dlib_face_recognition_resnet_model_v1.dat\" \"$pkgdir/usr/etc/howdy/dlib-data/\"\n\tcp \"${srcdir}/mmod_human_face_detector.dat\" \"$pkgdir/usr/etc/howdy/dlib-data/\"\n\tcp \"${srcdir}/shape_predictor_5_face_landmarks.dat\" \"$pkgdir/usr/etc/howdy/dlib-data/\"\n\tchmod 600 -R \"$pkgdir/usr/etc/howdy\"\n\tmkdir -p \"$pkgdir/usr/bin\"\n\tln -s /etc/howdy/cli.py \"$pkgdir/usr/bin/howdy\"\n\tchmod +x \"$pkgdir/etc/howdy/cli.py\"\n\tmkdir -p \"$pkgdir/usr/share/bash-completion/completions\"\n\tcp autocomplete/howdy \"$pkgdir/usr/share/bash-completion/completions/howdy\"\n}\n"
  },
  {
    "path": "howdy/debian/changelog",
    "content": "howdy (3.0.0) focal; urgency=medium\n\n  * Way too many changes to all list individually, thanks to everyone who contributed!\n  * Rewrote PAM handling logic in C++ (thanks @saidsay-so!)\n  * Added simultaneous face recognition and password authentication\n  * Added native dialog dismissal after successful authentication\n  * Added configurable image rotation (thanks @matan-arnon!)\n  * Fixed four config options whose names were opposite of their function\n\n -- boltgolt <boltgolt@gmail.com>  Sun, 22 Jun 2025 11:52:44 +0200\n\nhowdy (2.6.1) xenial; urgency=medium\n\n  * Fixed accidentally using emergency priority for log messages (thanks @kageurufu and many others!)\n  * Fixed certainty prompt selected the exact opposite value\n  * Fixed sleeping for negative time in test slow mode (thanks @willwill2will54!)\n  * Fixed opencv error when imported after dlib (thanks @cnyk!)\n  * Fixed typo causing manual exposure failure (thanks @h45h74x!)\n  * Fixed missing command autocomplete options on tab\n  * Fixed not knowing how to spell the word latest (thanks @divykj!)\n\n -- boltgolt <boltgolt@gmail.com>  Wed, 02 Sep 2020 15:05:59 +0200\n\nhowdy (2.6.0) xenial; urgency=medium\n\n  * Added new options to capture a snapshot of failed or even successful logins\n  * Added command that creates a new snapshot and saves it\n  * Added version command\n  * Added question to automatically set certainty value on installation\n  * Added automatic logging to system-wide auth.log\n  * Added clearer feedback when login is rejected due to dark frames (thanks @andrewmv!)\n  * Refactored video capture logic (thanks @AnthonyWharton!)\n  * Reordered the editor priorities for the config command\n  * Fixed gstreamer warnings showing up in console (thanks @ajnart!)\n  * Fixed issue where add command would never end\n  * Fixed test command overlay not being in color (thanks @PetePriority!)\n  * Fixed typo preventing timeout config option from working (thanks @Ajayneethikannan!)\n  * Fixed old numpy installation failure (thanks @rushabh-v!)\n  * Fixed issue where no PAM response would be returned\n  * Fixed CLAHE not being applied equally to all video commands (thanks @PetePriority!)\n  * Fixed an incorrect suggested command (thanks @TheButlah!)\n  * Fixed missing release method in video capture class\n  * Removed deprecated dlib flags (thanks @rhysperry111!)\n  * Removed streamer as a required dependency\n\n -- boltgolt <boltgolt@gmail.com>  Mon, 22 Jun 2020 16:11:46 +0200\n\nhowdy (2.5.1) xenial; urgency=medium\n\n  * Removed dismiss_lockscreen as it could lock users out of their system (thanks @ujjwalbe, @ju916 and many others!)\n  * Added option to disable howdy when the laptop lid is closed (thanks @accek!)\n  * Added automatic fallback to default frame color palette (thanks @Ethiarpus!)\n  * Added manual exposure setting (thanks @accek!)\n  * Fixed test command ignoring dark frame threshold (thanks @eduncan911!)\n  * Fixed import error in v4l2 recorder (thanks @timwelch!)\n\n -- boltgolt <boltgolt@gmail.com>  Fri, 29 Mar 2019 23:02:21 +0100\n\nhowdy (2.5.0) xenial; urgency=medium\n\n  * Added FFmpeg and v4l2 recorders (thanks @timwelch!)\n  * Added automatic PAM inclusion on installation\n  * Added optional notice on detection attempt (thanks @mrkmg!)\n  * Added support for grayscale frame encoding (thanks @dmig and @sapjunior!)\n  * Massively improved recognition speed (thanks @dmig!)\n  * Fixed typo in \"timout\" config value\n  * Removed unneeded dependencies (thanks @dmig!)\n\n -- boltgolt <boltgolt@gmail.com>  Sun, 06 Jan 2019 14:37:41 +0100\n\nhowdy (2.4.0) xenial; urgency=medium\n\n  * Cameras are now selected by path instead of by video device number (thanks @Rhiyo!)\n  * Added fallbacks to $EDITOR for the config command (thanks @yassineim!)\n  * Fixed missing cv2 module after installation (thanks @bendandersen and many others!)\n  * Fixed file permissions crashing Howdy in some cases (thanks @GJDitchfield!)\n  * Fixed howdy using python3 from local virtual environment (thanks @EdwardJB!)\n\n -- boltgolt <boltgolt@gmail.com>  Fri, 09 Nov 2018 20:59:45 +0100\n\nhowdy (2.3.1) xenial; urgency=high\n\n  * Fixed issue where `frame_width` and `frame_height` would be completely ignored (thanks @janecz-n!)\n  * Fixed security problem with remote session authentication (thanks @cccaballero!)\n\n -- boltgolt <boltgolt@gmail.com>  Mon, 24 Sep 2018 17:49:07 +0100\n\nhowdy (2.3.0) xenial; urgency=medium\n\n  * Added a config option to set the frame height and width (thanks @wzrdtales!)\n  * Rewrote the code that fetches the non-root username (thanks @dmig!)\n  * Changed the config command so it uses the default editor (thanks @stellarpower and @dmig!)\n  * Fixed issue where a \"y\" could be interpreted as a no (thanks @ramkrishna757575!)\n  * Fixed division by zeno (thanks @stellarpower!)\n\n -- boltgolt <boltgolt@gmail.com>  Thu, 28 Jun 2018 14:59:52 +0100\n\nhowdy (2.2.2) xenial; urgency=medium\n\n  * Fixed fetching of wrong config section (thanks @halcyoncheng and @arifeinberg!)\n\n -- boltgolt <boltgolt@gmail.com>  Fri, 11 May 2018 10:43:03 +0200\n\nhowdy (2.2.1) xenial; urgency=medium\n\n  * Added mechanism to keep config files between updates\n  * Added force_mjpeg option to fix YUYV image issues (thanks @arifeinberg!)\n  * Revamped the bash autocompletion script\n  * Fixed timeout never being reached in certain scenarios (thanks @Tkopic001!)\n  * Fixed issue where BGR to RGB frame conversion caused a crash (thanks @Jerezano!)\n\n -- boltgolt <boltgolt@gmail.com>  Thu, 10 May 2018 15:14:03 +0200\n\nhowdy (2.1.0) xenial; urgency=medium\n\n  * First complete PPA release\n  * Reworked CLI\n\n -- boltgolt <boltgolt@gmail.com>  Fri, 13 Apr 2018 22:22:27 +0200\n\nhowdy (2.0.0-alpha+3) xenial; urgency=medium\n\n  * Fixed issue where dlib dependency failed to install on some installations\n  * Added preinst script for camera detection\n\n -- boltgolt <boltgolt@gmail.com>  Thu, 12 Apr 2018 21:42:42 +0000\n\nhowdy (2.0.0-alpha+2) xenial; urgency=medium\n\n  * Fixed build dependency issue\n\n -- boltgolt <boltgolt@gmail.com>  Sat, 07 Apr 2018 21:30:48 +0200\n\nhowdy (2.0.0-alpha+1) xenial; urgency=low\n\n  * Initial packaged release.\n\n -- boltgolt <boltgolt@gmail.com>  Wed, 04 Apr 2018 18:13:15 +0200\n"
  },
  {
    "path": "howdy/debian/compat",
    "content": "10\n"
  },
  {
    "path": "howdy/debian/control",
    "content": "Source: howdy\nSection: misc\nPriority: optional\nStandards-Version: 3.9.7\nBuild-Depends: devscripts, git, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev, pkg-config, libevdev-dev, libinih-dev\nMaintainer: boltgolt <boltgolt@gmail.com>\nVcs-Git: https://github.com/boltgolt/howdy\n\nPackage: howdy\nHomepage: https://github.com/boltgolt/howdy\nArchitecture: amd64\nDepends: ${misc:Depends}, libc6, libgcc-s1, libpam0g, libstdc++6, curl | wget, python3, python3-pip, python3-dev, python3-setuptools, python3-numpy, python-opencv | python3-opencv, libopencv-dev, cmake, libinih-dev\nRecommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, howdy-gtk, v4l-utils\nSuggests: nvidia-cuda-dev (>= 7.5)\nDescription: Howdy: Windows Hello style authentication for Linux.\n Use your built-in IR emitters and camera in combination with face recognition\n to prove who you are.\n"
  },
  {
    "path": "howdy/debian/copyright",
    "content": "MIT License\n\nCopyright (c) 2018 boltgolt\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": "howdy/debian/howdy.lintian-overrides",
    "content": "# W: Don't require ugly linebreaks in last 5 chars\nhowdy: debian-changelog-line-too-long\n\n# E: Allows the name Howdy to show up in Ubuntu updater\nhowdy: description-starts-with-package-name\n# E: Allows python for installation scripts\nhowdy: unknown-control-interpreter\n"
  },
  {
    "path": "howdy/debian/howdy.manpages",
    "content": "howdy.1\n"
  },
  {
    "path": "howdy/debian/install",
    "content": "src/cli/. lib/security/howdy/cli\nsrc/locales/. lib/security/howdy/locales\nsrc/recorders/. lib/security/howdy/recorders\nsrc/rubberstamps/. lib/security/howdy/rubberstamps\nsrc/cli.py lib/security/howdy\nsrc/compare.py lib/security/howdy\nsrc/i18n.py lib/security/howdy\nsrc/logo.png lib/security/howdy\nsrc/snapshot.py lib/security/howdy\n\nbuild/pam_howdy.so lib/security/howdy\n\nsrc/dlib-data/. etc/howdy/dlib-data\nsrc/config.ini etc/howdy\n\nsrc/autocomplete/. usr/share/bash-completion/completions\nsrc/pam-config/.  /usr/share/pam-configs\n"
  },
  {
    "path": "howdy/debian/postinst",
    "content": "#!/usr/bin/python3\n# Installation script to install howdy\n# Executed after primary apt install\n\n# Import required modules\nimport fileinput\nimport subprocess\nimport sys\nimport os\nimport re\nimport tarfile\nfrom shutil import rmtree, which\n\n# Don't run unless we need to configure the install\n# Will also happen on upgrade but we will catch that later on\nif \"configure\" not in sys.argv:\n\tsys.exit(0)\n\n\ndef log(text):\n\t\"\"\"Print a nicely formatted line to stdout\"\"\"\n\tprint(\"\\n>>> \" + col(1) + text + col(0) + \"\\n\")\n\n\ndef handleStatus(status):\n\t\"\"\"Abort if a command fails\"\"\"\n\tif (status != 0):\n\t\tprint(col(3) + \"Error while running last command\" + col(0))\n\t\tsys.exit(1)\n\n\ndef col(id):\n\t\"\"\"Add color escape sequences\"\"\"\n\tif id == 1: return \"\\033[32m\"\n\tif id == 2: return \"\\033[33m\"\n\tif id == 3: return \"\\033[31m\"\n\treturn \"\\033[0m\"\n\n\n# Create shorthand for subprocess creation\nsc = subprocess.call\n\n# If the package is being upgraded\nif \"upgrade\" in sys.argv:\n\t# If preinst has made a config backup\n\tif os.path.exists(\"/tmp/howdy_config_backup_v\" + sys.argv[2] + \".ini\"):\n\t\t# Get the config parser\n\t\timport configparser\n\n\t\t# Load th old and new config files\n\t\toldConf = configparser.ConfigParser()\n\t\toldConf.read(\"/tmp/howdy_config_backup_v\" + sys.argv[2] + \".ini\")\n\t\tnewConf = configparser.ConfigParser()\n\t\tnewConf.read(\"/etc/howdy/config.ini\")\n\n\t\t# Go through every setting in the old config and apply it to the new file\n\t\tfor section in oldConf.sections():\n\t\t\tfor (key, value) in oldConf.items(section):\n\n\t\t\t\t# MIGRATION 2.3.1 -> 2.4.0\n\t\t\t\t# If config is still using the old device_id parameter, convert it to a path\n\t\t\t\tif key == \"device_id\":\n\t\t\t\t\tkey = \"device_path\"\n\t\t\t\t\tvalue = \"/dev/video\" + value\n\n\t\t\t\t# MIGRATION 2.4.0 -> 2.5.0\n\t\t\t\t# Finally correct typo in \"timout\" config value\n\t\t\t\tif key == \"timout\":\n\t\t\t\t\tkey = \"timeout\"\n\n\t\t\t\t# MIGRATION 2.5.0 -> 2.5.1\n\t\t\t\t# Remove unsafe automatic dismissal of lock screen\n\t\t\t\tif key == \"dismiss_lockscreen\":\n\t\t\t\t\tif value == \"true\":\n\t\t\t\t\t\tprint(\"DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.\")\n\t\t\t\t\tcontinue\n\n\t\t\t\t# MIGRATION 2.6.1 -> 3.0.0\n\t\t\t\t# Fix capture being enabled by default\n\t\t\t\tif key == \"capture_failed\" or key == \"capture_successful\":\n\t\t\t\t\tif value == \"true\":\n\t\t\t\t\t\tprint(\"NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again\")\n\t\t\t\t\t\tvalue = \"false\"\n\n\t\t\t\t# MIGRATION 2.6.1 -> 3.0.0\n\t\t\t\t# Rename config options so they don't do the opposite of what is commonly expected\n\t\t\t\tif key == \"ignore_ssh\":\n\t\t\t\t\tkey = \"abort_if_ssh\"\n\t\t\t\tif key == \"ignore_closed_lid\":\n\t\t\t\t\tkey = \"abort_if_lid_closed\"\n\t\t\t\tif key == \"capture_failed\":\n\t\t\t\t\tkey = \"save_failed\"\n\t\t\t\tif key == \"capture_successful\":\n\t\t\t\t\tkey = \"save_successful\"\n\n\t\t\t\ttry:\n\t\t\t\t\tnewConf.set(section, key, value)\n\t\t\t\t# Add a new section where needed\n\t\t\t\texcept configparser.NoSectionError:\n\t\t\t\t\tnewConf.add_section(section)\n\t\t\t\t\tnewConf.set(section, key, value)\n\n\t\t# Write it all to file\n\t\twith open(\"/etc/howdy/config.ini\", \"w\") as configfile:\n\t\t\tnewConf.write(configfile)\n\n\tsys.exit(0)\n\nlog(\"Downloading dlib\")\n\ndlib_archive = \"/tmp/v19.16.tar.gz\"\nloader = which(\"wget\")\nLOADER_CMD = None\n\n# If wget is installed, use that as the downloader\nif loader:\n\tLOADER_CMD = [loader, \"--tries\", \"5\", \"--output-document\"]\n# Otherwise, fall back on curl\nelse:\n\tloader = which(\"curl\")\n\tLOADER_CMD = [loader, \"--retry\", \"5\", \"--location\", \"--output\"]\n\n# Assemble and execute the download command\ncmd = LOADER_CMD + [dlib_archive, \"https://github.com/davisking/dlib/archive/v19.16.tar.gz\"]\nhandleStatus(sc(cmd))\n\n# The folder containing the dlib source\nDLIB_DIR = None\n# A regex of all files to ignore while unpacking the archive\nexcludes = re.compile(\n\tr\"davisking-dlib-\\w+/(dlib/(http_client|java|matlab|test/)|\"\n\tr\"(docs|examples|python_examples)|\"\n\tr\"tools/(archive|convert_dlib_nets_to_caffe|htmlify|imglab|python/test|visual_studio_natvis))\"\n)\n\n# Open the archive\nwith tarfile.open(dlib_archive) as tf:\n\tfor item in tf:\n\t\t# Set the destination dir if unset\n\t\tif not DLIB_DIR:\n\t\t\tDLIB_DIR = \"/tmp/\" + item.name\n\n\t\t# extract only files sufficient for building\n\t\tif not excludes.match(item.name):\n\t\t\ttf.extract(item, \"/tmp\")\n\n# Delete the downloaded archive\nos.unlink(dlib_archive)\n\nlog(\"Building dlib\")\n\ncmd = [\"sudo\", \"python3\", \"setup.py\", \"install\"]\ncuda_used = False\n\n# Compile and link dlib\ntry:\n\tsp = subprocess.Popen(cmd, cwd=DLIB_DIR, stdout=subprocess.PIPE)\nexcept subprocess.CalledProcessError:\n\tprint(\"Error while building dlib\")\n\traise\n\n# Go through each line from stdout to check for CUDA usage\nwhile sp.poll() is None:\n\tline = sp.stdout.readline().decode(\"utf-8\")\n\n\tif \"DLIB WILL USE CUDA\" in line:\n\t\tcuda_used = True\n\n\tprint(line, end=\"\")\n\nlog(\"Cleaning up dlib\")\n\n# Remove the no longer needed git clone\ndel sp\nrmtree(DLIB_DIR)\n\nprint(\"Temporary dlib files removed\")\n\nlog(\"Configuring howdy\")\n\n# Make sure to use CNN if dlib was compiled with CUDA support\nfor line in fileinput.input([\"/etc/howdy/config.ini\"], inplace=1):\n\tline = line.replace(\"use_cnn = false\", \"use_cnn = \" + str(cuda_used).lower())\n\tprint(line, end=\"\")\n\nprint(\"CNN saved to config, CUDA \" + (\"enabled\" if cuda_used else \"disabled\"))\n\n# Secure the howdy folder\nhandleStatus(sc([\"chmod 755 -R /lib/security/howdy/\"], shell=True))\nhandleStatus(sc([\"chmod 755 -R /etc/howdy/\"], shell=True))\n\n# Allow anyone to execute the python CLI\nos.chmod(\"/lib/security/howdy\", 0o755)\nos.chmod(\"/lib/security/howdy/cli.py\", 0o755)\nhandleStatus(sc([\"chmod 755 -R /lib/security/howdy/cli\"], shell=True))\nprint(\"Permissions set\")\n\n# Make the CLI executable as howdy\nos.symlink(\"/lib/security/howdy/cli.py\", \"/usr/local/bin/howdy\")\nos.chmod(\"/usr/local/bin/howdy\", 0o755)\nprint(\"Howdy command installed\")\n\nlog(\"Adding howdy as PAM module\")\n\n# Activate the pam-config file\nhandleStatus(subprocess.call([\"pam-auth-update --package\"], shell=True))\n\n# Sign off\nprint(\"Installation complete.\")\n"
  },
  {
    "path": "howdy/debian/preinst",
    "content": "#!/usr/bin/python3\n# Used to check cameras before committing to install\n# Executed before primary apt install of files\n\nimport subprocess\nimport sys\nimport os\n\n# Backup the config file if we're upgrading\nif \"upgrade\" in sys.argv:\n\t# Try to copy the config file as a backup\n\ttry:\n\t\t# Try to copy the new location first\n\t\tif os.path.exists(\"/etc/howdy/config.ini\"):\n\t\t\tsubprocess.call([\"cp /etc/howdy/config.ini /tmp/howdy_config_backup_v\" + sys.argv[2] + \".ini\"], shell=True)\n\t\t# If that does not exist, try copying the old location\n\t\telse:\n\t\t\tsubprocess.call([\"cp /lib/security/howdy/config.ini /tmp/howdy_config_backup_v\" + sys.argv[2] + \".ini\"], shell=True)\n\n\t\t# Let the user know so he knows where to look on a failed install\n\t\tprint(\"Backup of Howdy config file created in /tmp/howdy_config_backup_v\" + sys.argv[2] + \".ini\")\n\texcept subprocess.CalledProcessError:\n\t\tprint(\"Could not make an backup of old Howdy config file\")\n\n\t# Don't continue setup when we're just upgrading\n\tsys.exit(0)\n\n# Don't run if we're not trying to install fresh\nif \"install\" not in sys.argv:\n\tsys.exit(0)\n"
  },
  {
    "path": "howdy/debian/prerm",
    "content": "#!/usr/bin/python3\n# Executed on deinstallation\n# Completely remove howdy from the system\n\n# Import required modules\nimport subprocess\nimport sys\nimport os\nfrom shutil import rmtree\n\n# Only run when we actually want to remove\nif \"remove\" not in sys.argv and \"purge\" not in sys.argv:\n\tsys.exit(0)\n\n# Don't try running this if it's already gone\nif not os.path.exists(\"/lib/security/howdy/cli\"):\n\tsys.exit(0)\n\n# Remove files and symlinks\ntry:\n\tos.unlink(\"/usr/local/bin/howdy\")\nexcept Exception:\n\tprint(\"Can't remove executable\")\ntry:\n\tos.unlink(\"/usr/share/bash-completion/completions/howdy\")\nexcept Exception:\n\tprint(\"Can't remove autocompletion script\")\n\n# Refresh and remove howdy from pam-config\ntry:\n\tsubprocess.call([\"pam-auth-update --package\"], shell=True)\n\tsubprocess.call([\"rm /usr/share/pam-configs/howdy\"], shell=True)\n\tsubprocess.call([\"pam-auth-update --package\"], shell=True)\nexcept Exception:\n\tprint(\"Can't remove pam module\")\n\n# Remove full installation folder, just to be sure\ntry:\n\trmtree(\"/lib/security/howdy\")\nexcept Exception:\n\t# This error is normal\n\tpass\n\n# Remove dlib\nsubprocess.call([\"pip3\", \"uninstall\", \"dlib\", \"-y\", \"--no-cache-dir\"])\n"
  },
  {
    "path": "howdy/debian/rules",
    "content": "#!/usr/bin/make -f\nDH_VERBOSE = 1\n\nDPKG_EXPORT_BUILDFLAGS = 1\ninclude /usr/share/dpkg/default.mk\n\n%:\n\tdh $@\n\nbuild:\n\t# Create build dir\n\tmeson setup -Dinih:with_INIReader=true build src/pam\n\t# Compile shared object\n\tninja -C build\n\nclean:\n\t# Delete mason build directory\n\trm -rf ./build\n\t# Force remove temp debian build directory\n\trm -rf ./debian/howdy\n\t# Make sure subprojects get pulled locally\n\tmeson subprojects download --sourcedir src/pam\n"
  },
  {
    "path": "howdy/debian/source/format",
    "content": "3.0 (native)\n"
  },
  {
    "path": "howdy/debian/source/options",
    "content": "tar-ignore = \".git\"\ntar-ignore = \".gitignore\"\ntar-ignore = \".github\"\ntar-ignore = \"models\"\ntar-ignore = \"snapshots\"\ntar-ignore = \"tests\"\ntar-ignore = \"README.md\"\ntar-ignore = \".travis.yml\"\ntar-ignore = \"fedora\"\ntar-ignore = \"opensuse\"\ntar-ignore = \"archlinux\"\ntar-ignore = \"build\"\ntar-ignore = \"__pycache__\"\ntar-ignore = \"*.dat\"\n"
  },
  {
    "path": "howdy/howdy.1",
    "content": ".\\\" Please adjust this date whenever revising the manpage.\n.TH HOWDY 1 \"April 9, 2018\" \"Howdy help\" \"User Commands\"\n.SH NAME\nhowdy \\- Windows Hello style authentication for Linux\n.SH DESCRIPTION\nHowdy IR face recognition implements a PAM module to use your face as a authentication method.\n.SS \"Usage:\"\n.IP\nhowdy [\\-U USER] [\\-y] [\\-h] command [argument]\n.SS \"Commands:\"\n.TP\nadd\nAdd a new face model for an user.\n.TP\nclear\nRemove all face models for an user.\n.TP\nconfig\nOpen the config file in gedit.\n.TP\ndisable\nDisable or enable howdy.\n.TP\nlist\nList all saved face models for an user.\n.TP\nremove\nRemove a specific model for an user.\n.TP\nclear\nRemove all face models for an user.\n.TP\ntest\nTest the camera and recognition methods.\n.SS \"Optional arguments:\"\n.TP\n\\fB\\-U\\fR USER, \\fB\\-\\-user\\fR USER\nSet the user account to use.\n.TP\n\\fB\\-y\\fR\nSkip all questions.\n.TP\n\\fB\\-h\\fR, \\fB\\-\\-help\\fR\nShow this help message and exit.\n.PP\n.SH AUTHOR\nHowdy was written by boltgolt. For more information visit https://github.com/boltgolt/howdy\n"
  },
  {
    "path": "howdy/meson.build",
    "content": "subdir('src')"
  },
  {
    "path": "howdy/src/autocomplete/howdy.in",
    "content": "#!/bin/bash\n# Autocomplete file run in bash\n# Will sugest arguments on tab\n\n_howdy() {\n\tlocal cur prev opts\n\tlocal config_path=\"@config_path@\"\n\tCOMPREPLY=()\n\t# The argument typed so far\n\tcur=\"${COMP_WORDS[COMP_CWORD]}\"\n\t# The previous argument\n\tprev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n\t# Go though all cases we support\n\tcase \"${prev}\" in\n\t\t# After the main command, show the commands\n\t\t\"howdy\")\n\t\t\topts=\"add clear config disable list remove clear snapshot test version\"\n\t\t\tCOMPREPLY=( $(compgen -W \"${opts}\" -- ${cur}) )\n\t\t\treturn 0\n\t\t\t;;\n\t\t# For disable, grab the current \"disabled\" config option and give the reverse\n\t\t\"disable\")\n\t\t\tlocal status=$(cut -d'=' -f2 <<< $(cat $config_path | grep 'disabled =') | xargs echo -n)\n\n\t\t\t[ \"$status\" == \"false\" ] && COMPREPLY=\"true\" || COMPREPLY=\"false\"\n\t\t\treturn 0\n\t\t\t;;\n\t\t# List the users available\n\t\t\"-U\")\n\t\t\tCOMPREPLY=( $(compgen -u -- ${cur}) )\n\t\t\treturn 0\n\t\t\t;;\n\t\t\"--user\")\n\t\t\tCOMPREPLY=( $(compgen -u -- ${cur}) )\n\t\t\treturn 0\n\t\t\t;;\n \t\t*)\n\t\t;;\n\tesac\n\n\t# Nothing matched, so return nothing\n\treturn 0\n}\n\n# Register the autocomplete function\ncomplete -F _howdy howdy\n"
  },
  {
    "path": "howdy/src/bin/howdy.in",
    "content": "#!/bin/sh\n\n@python_path@ \"@script_path@\" \"$@\""
  },
  {
    "path": "howdy/src/cli/__init__.py",
    "content": "# Marks this folder as importable\n"
  },
  {
    "path": "howdy/src/cli/add.py",
    "content": "# Save the face of the user in encoded form\n\n# Import required modules\nimport time\nimport os\nimport sys\nimport json\nimport configparser\nimport builtins\nimport numpy as np\nimport paths_factory\n\nfrom recorders.video_capture import VideoCapture\nfrom i18n import _\n\n# Try to import dlib and give a nice error if we can't\n# Add should be the first point where import issues show up\ntry:\n\timport dlib\nexcept ImportError as err:\n\tprint(err)\n\n\tprint(_(\"\\nCan't import the dlib module, check the output of\"))\n\tprint(\"pip3 show dlib\")\n\tsys.exit(1)\n\n# OpenCV needs to be imported after dlib\nimport cv2\n\n# Test if at lest 1 of the data files is there and abort if it's not\nif not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()):\n\tprint(_(\"Data files have not been downloaded, please run the following commands:\"))\n\tprint(\"\\n\\tcd \" + paths_factory.dlib_data_dir_path())\n\tprint(\"\\tsudo ./install.sh\\n\")\n\tsys.exit(1)\n\n# Read config from disk\nconfig = configparser.ConfigParser()\nconfig.read(paths_factory.config_file_path())\n\nuse_cnn = config.getboolean(\"core\", \"use_cnn\", fallback=False)\nif use_cnn:\n\tface_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path())\nelse:\n\tface_detector = dlib.get_frontal_face_detector()\n\npose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path())\nface_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path())\n\nuser = builtins.howdy_user\n# The permanent file to store the encoded model in\nenc_file = paths_factory.user_model_path(user)\n# Known encodings\nencodings = []\n\n# Make the ./models folder if it doesn't already exist\nif not os.path.exists(paths_factory.user_models_dir_path()):\n\tprint(_(\"No face model folder found, creating one\"))\n\tos.makedirs(paths_factory.user_models_dir_path())\n\n# To try read a premade encodings file if it exists\ntry:\n\tencodings = json.load(open(enc_file))\nexcept FileNotFoundError:\n\tencodings = []\n\n# Print a warning if too many encodings are being added\nif len(encodings) > 3:\n\tprint(_(\"NOTICE: Each additional model slows down the face recognition engine slightly\"))\n\tprint(_(\"Press Ctrl+C to cancel\\n\"))\n\n# Make clear what we are doing if not human\nif not builtins.howdy_args.plain:\n\tprint(_(\"Adding face model for the user \") + user)\n\n# Set the default label\nlabel = \"Initial model\"\n\n# some id's can be skipped, but the last id is always the maximum\nnext_id = encodings[-1][\"id\"] + 1 if encodings else 0\n\n# Get the label from the cli arguments if provided\nif builtins.howdy_args.arguments:\n\tlabel = builtins.howdy_args.arguments[0]\n\n# Or set the default label\nelse:\n\tlabel = _(\"Model #\") + str(next_id)\n\n# Keep de default name if we can't ask questions\nif builtins.howdy_args.y:\n\tprint(_('Using default label \"%s\" because of -y flag') % (label, ))\nelse:\n\t# Ask the user for a custom label\n\tlabel_in = input(_(\"Enter a label for this new model [{}]: \").format(label))\n\n\t# Set the custom label (if any) and limit it to 24 characters\n\tif label_in != \"\":\n\t\tlabel = label_in[:24]\n\n# Remove illegal characters\nif \",\" in label:\n\tprint(_(\"NOTICE: Removing illegal character \\\",\\\" from model name\"))\n\tlabel = label.replace(\",\", \"\")\n\n# Prepare the metadata for insertion\ninsert_model = {\n\t\"time\": int(time.time()),\n\t\"label\": label,\n\t\"id\": next_id,\n\t\"data\": []\n}\n\n# Set up video_capture\nvideo_capture = VideoCapture(config)\n\nprint(_(\"\\nPlease look straight into the camera\"))\n\n# Give the user time to read\ntime.sleep(2)\n\n# Will contain found face encodings\nenc = []\n# Count the number of read frames\nframes = 0\n# Count the number of illuminated read frames\nvalid_frames = 0\n# Count the number of illuminated frames that\n# were rejected for being too dark\ndark_tries = 0\n# Track the running darkness total\ndark_running_total = 0\nface_locations = None\n\ndark_threshold = config.getfloat(\"video\", \"dark_threshold\", fallback=60)\n\nclahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n\n# Loop through frames till we hit a timeout\nwhile frames < 60:\n\tframes += 1\n\t# Grab a single frame of video\n\tframe, gsframe = video_capture.read_frame()\n\tgsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n\tgsframe = clahe.apply(gsframe)\n\n\t# Create a histogram of the image with 8 values\n\thist = cv2.calcHist([gsframe], [0], None, [8], [0, 256])\n\t# All values combined for percentage calculation\n\thist_total = np.sum(hist)\n\n\t# Calculate frame darkness\n\tdarkness = (hist[0] / hist_total * 100)\n\n\t# If the image is fully black due to a bad camera read,\n\t# skip to the next frame\n\tif (hist_total == 0) or (darkness == 100):\n\t\tcontinue\n\n\t# Include this frame in calculating our average session brightness\n\tdark_running_total += darkness\n\tvalid_frames += 1\n\n\t# If the image exceeds darkness threshold due to subject distance,\n\t# skip to the next frame\n\tif (darkness > dark_threshold):\n\t\tdark_tries += 1\n\t\tcontinue\n\n\t# Get all faces from that frame as encodings\n\tface_locations = face_detector(gsframe, 1)\n\n\t# If we've found at least one, we can continue\n\tif face_locations:\n\t\tbreak\n\nvideo_capture.release()\n\n# If we've found no faces, try to determine why\nif not face_locations:\n\tif valid_frames == 0:\n\t\tprint(_(\"Camera saw only black frames - is IR emitter working?\"))\n\telif valid_frames == dark_tries:\n\t\tprint(_(\"All frames were too dark, please check dark_threshold in config\"))\n\t\tprint(_(\"Average darkness: {avg}, Threshold: {threshold}\").format(avg=str(dark_running_total / valid_frames), threshold=str(dark_threshold)))\n\telse:\n\t\tprint(_(\"No face detected, aborting\"))\n\tsys.exit(1)\n\n# If more than 1 faces are detected we can't know which one belongs to the user\nelif len(face_locations) > 1:\n\tprint(_(\"Multiple faces detected, aborting\"))\n\tsys.exit(1)\n\nface_location = face_locations[0]\nif use_cnn:\n\tface_location = face_location.rect\n\n# Get the encodings in the frame\nface_landmark = pose_predictor(frame, face_location)\nface_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1))\n\ninsert_model[\"data\"].append(face_encoding.tolist())\n\n# Insert full object into the list\nencodings.append(insert_model)\n\n# Save the new encodings to disk\nwith open(enc_file, \"w\") as datafile:\n\tjson.dump(encodings, datafile)\n\n# Give let the user know how it went\nprint(_(\"\"\"\\nScan complete\nAdded a new model to \"\"\") + user)\n"
  },
  {
    "path": "howdy/src/cli/clear.py",
    "content": "# Clear all models by deleting the whole file\n\n# Import required modules\nimport os\nimport sys\nimport builtins\nimport paths_factory\n\nfrom i18n import _\n\n# Get the passed user\nuser = builtins.howdy_user\n\n# Check if the models folder is there\nif not os.path.exists(paths_factory.user_models_dir_path()):\n\tprint(_(\"No models created yet, can't clear them if they don't exist\"))\n\tsys.exit(1)\n\n# Check if the user has a models file to delete\nif not os.path.isfile(paths_factory.user_model_path(user)):\n\tprint(_(\"{} has no models or they have been cleared already\").format(user))\n\tsys.exit(1)\n\n# Only ask the user if there's no -y flag\nif not builtins.howdy_args.y:\n\t# Double check with the user\n\tprint(_(\"This will clear all models for \") + user)\n\tans = input(_(\"Do you want to continue [y/N]: \"))\n\n\t# Abort if they don't answer y or Y\n\tif (ans.lower() != \"y\"):\n\t\tprint(_('\\nInterpreting as a \"NO\", aborting'))\n\t\tsys.exit(1)\n\n# Delete otherwise\nos.remove(paths_factory.user_model_path(user))\nprint(_(\"\\nModels cleared\"))\n"
  },
  {
    "path": "howdy/src/cli/config.py",
    "content": "# Open the config file in an editor\n\n# Import required modules\nimport os\nimport subprocess\nimport shutil\nimport paths_factory\n\nfrom i18n import _\n\n# Determine the editor to use\neditor = None\npreferred_editor = os.environ.get(\"EDITOR\")\nnano_path = shutil.which(\"nano\")\nvi_path = shutil.which(\"vi\")\n\n# Use the user preferred editor if available\nif preferred_editor:\n    if shutil.which(preferred_editor):\n        editor = preferred_editor\n\nif not editor:\n    if nano_path:\n        editor = nano_path\n    elif vi_path:\n        editor = vi_path\n\nif editor:\n    editor_name = os.path.basename(editor)\n\n    # Let the user know what we're doing\n    print(_(\"Opening config.ini in {editor}\").format(editor=editor_name))\n\n    # Open the editor as a subprocess and fork it\n    try:\n        subprocess.call([editor, paths_factory.config_file_path()])\n    except Exception as e:\n        print(_(\"Failed to open editor: {error}\").format(error=e))\nelse:\n    print(_(\"Error: Could not find a suitable text editor.\"))\n    print(_(\"Please install 'nano' or 'vi', or set the EDITOR environment variable.\"))\n    print(_(\"If you are running this command with sudo, try 'sudo -E howdy config' to preserve your EDITOR variable.\"))\n"
  },
  {
    "path": "howdy/src/cli/disable.py",
    "content": "# Set the disable flag\n\n# Import required modules\nimport sys\nimport os\nimport builtins\nimport fileinput\nimport configparser\nimport paths_factory\n\nfrom i18n import _\n\n# Get the absolute filepath\nconfig_path = paths_factory.config_file_path()\n\n# Read config from disk\nconfig = configparser.ConfigParser()\nconfig.read(config_path)\n\n# Check if enough arguments have been passed\nif not builtins.howdy_args.arguments:\n\tprint(_(\"Please add a 0 (enable) or a 1 (disable) as an argument\"))\n\tsys.exit(1)\n\n# Get the cli argument\nargument = builtins.howdy_args.arguments[0]\n\n# Translate the argument to the right string\nif argument == \"1\" or argument.lower() == \"true\":\n\tout_value = \"true\"\nelif argument == \"0\" or argument.lower() == \"false\":\n\tout_value = \"false\"\nelse:\n\t# Of it's not a 0 or a 1, it's invalid\n\tprint(_(\"Please only use 0 (enable) or 1 (disable) as an argument\"))\n\tsys.exit(1)\n\n# Don't do anything when the state is already the requested one\nif out_value == config.get(\"core\", \"disabled\", fallback=True):\n\tprint(_(\"The disable option has already been set to \") + out_value)\n\tsys.exit(1)\n\n# Loop though the config file and only replace the line containing the disable config\nfor line in fileinput.input([config_path], inplace=1):\n\tprint(line.replace(\"disabled = \" + config.get(\"core\", \"disabled\", fallback=True), \"disabled = \" + out_value), end=\"\")\n\n# Print what we just did\nif out_value == \"true\":\n\tprint(_(\"Howdy has been disabled\"))\nelse:\n\tprint(_(\"Howdy has been enabled\"))\n"
  },
  {
    "path": "howdy/src/cli/list.py",
    "content": "# List all models for a user\n\n# Import required modules\nimport sys\nimport os\nimport json\nimport time\nimport builtins\nimport paths_factory\n\nfrom i18n import _\n\nuser = builtins.howdy_user\n\n# Check if the models file has been created yet\nif not os.path.exists(paths_factory.user_models_dir_path()):\n\tprint(_(\"Face models have not been initialized yet, please run:\"))\n\tprint(\"\\n\\tsudo howdy -U \" + user + \" add\\n\")\n\tsys.exit(1)\n\n# Path to the models file\nenc_file = paths_factory.user_model_path(user)\n\n# Try to load the models file and abort if the user does not have it yet\ntry:\n\tencodings = json.load(open(enc_file))\nexcept FileNotFoundError:\n\tif not builtins.howdy_args.plain:\n\t\tprint(_(\"No face model known for the user {}, please run:\").format(user))\n\t\tprint(\"\\n\\tsudo howdy -U \" + user + \" add\\n\")\n\tsys.exit(1)\n\n# Print a header if we're not in plain mode\nif not builtins.howdy_args.plain:\n\tprint(_(\"Known face models for {}:\").format(user))\n\tprint(\"\\n\\033[1;29m\" + _(\"ID  Date                 Label\\033[0m\"))\n\n# Loop through all encodings and print info about them\nfor enc in encodings:\n\t# Start with the id\n\tprint(str(enc[\"id\"]), end=\"\")\n\n\t# Add comma for machine reading\n\tif builtins.howdy_args.plain:\n\t\tprint(\",\", end=\"\")\n\t# Print padding spaces after the id for a nice layout\n\telse:\n\t\tprint((4 - len(str(enc[\"id\"]))) * \" \", end=\"\")\n\n\t# Format the time as ISO in the local timezone\n\tprint(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc[\"time\"])), end=\"\")\n\n\t# Separate with commas again for machines, spaces otherwise\n\tprint(\",\" if builtins.howdy_args.plain else \"  \", end=\"\")\n\n\t# End with the label\n\tprint(enc[\"label\"])\n\n# Add a closing enter\nprint()\n"
  },
  {
    "path": "howdy/src/cli/remove.py",
    "content": "# Remove a encoding from the models file\n\n# Import required modules\nimport sys\nimport os\nimport json\nimport builtins\nimport paths_factory\n\nfrom i18n import _\n\nuser = builtins.howdy_user\n\n# Check if enough arguments have been passed\nif not builtins.howdy_args.arguments:\n\tprint(_(\"Please add the ID of the model you want to remove as an argument\"))\n\tprint(_(\"For example:\"))\n\tprint(\"\\n\\thowdy remove 0\\n\")\n\tprint(_(\"You can find the IDs by running:\"))\n\tprint(\"\\n\\thowdy list\\n\")\n\tsys.exit(1)\n\n# Check if the models file has been created yet\nif not os.path.exists(paths_factory.user_models_dir_path()):\n\tprint(_(\"Face models have not been initialized yet, please run:\"))\n\tprint(\"\\n\\thowdy add\\n\")\n\tsys.exit(1)\n\n# Path to the models file\nenc_file = paths_factory.user_model_path(user)\n\n# Try to load the models file and abort if the user does not have it yet\ntry:\n\tencodings = json.load(open(enc_file))\nexcept FileNotFoundError:\n\tprint(_(\"No face model known for the user {}, please run:\").format(user))\n\tprint(\"\\n\\thowdy add\\n\")\n\tsys.exit(1)\n\n# Tracks if a encoding with that id has been found\nfound = False\n\n# Get the ID from the cli arguments\nid = builtins.howdy_args.arguments[0]\n\n# Loop though all encodings and check if they match the argument\nfor enc in encodings:\n\tif str(enc[\"id\"]) == id:\n\t\t# Only ask the user if there's no -y flag\n\t\tif not builtins.howdy_args.y:\n\t\t\t# Double check with the user\n\t\t\tprint(_('This will remove the model called \"{label}\" for {user}').format(label=enc[\"label\"], user=user))\n\t\t\tans = input(_(\"Do you want to continue [y/N]: \"))\n\n\t\t\t# Abort if the answer isn't yes\n\t\t\tif (ans.lower() != \"y\"):\n\t\t\t\tprint(_('\\nInterpreting as a \"NO\", aborting'))\n\t\t\t\tsys.exit(1)\n\n\t\t\t# Add a padding empty  line\n\t\t\tprint()\n\n\t\t# Mark as found and print an enter\n\t\tfound = True\n\t\tbreak\n\n# Abort if no matching id was found\nif not found:\n\tprint(_(\"No model with ID {id} exists for {user}\").format(id=id, user=user))\n\tsys.exit(1)\n\n# Remove the entire file if this encoding is the only one\nif len(encodings) == 1:\n\tos.remove(paths_factory.user_model_path(user))\n\tprint(_(\"Removed last model, howdy disabled for user\"))\nelse:\n\t# A place holder to contain the encodings that will remain\n\tnew_encodings = []\n\n\t# Loop though all encodings and only add those that don't need to be removed\n\tfor enc in encodings:\n\t\tif str(enc[\"id\"]) != id:\n\t\t\tnew_encodings.append(enc)\n\n\t# Save this new set to disk\n\twith open(enc_file, \"w\") as datafile:\n\t\tjson.dump(new_encodings, datafile)\n\n\tprint(_(\"Removed model {}\").format(id))\n"
  },
  {
    "path": "howdy/src/cli/set.py",
    "content": "# Set a config value\n\n# Import required modules\nimport sys\nimport os\nimport builtins\nimport fileinput\nimport paths_factory\n\nfrom i18n import _\n\n# Get the absolute filepath\nconfig_path = paths_factory.config_file_path()\n\n# Check if enough arguments have been passed\nif len(builtins.howdy_args.arguments) < 2:\n\tprint(_(\"Please add a setting you would like to change and the value to set it to\"))\n\tprint(_(\"For example:\"))\n\tprint(\"\\n\\thowdy set certainty 3\\n\")\n\tsys.exit(1)\n\n# Get the name and value from the cli\nset_name = builtins.howdy_args.arguments[0]\nset_value = builtins.howdy_args.arguments[1]\n\n# Will be filled with the correctly config line to update\nfound_line = \"\"\n\n# Loop through all lines in the config file\nfor line in fileinput.input([config_path]):\n\t# Save the line if it starts with the requested config option\n\tif line.startswith(set_name + \" \"):\n\t\tfound_line = line\n\n# If we don't have the line it is not in the config file\nif not found_line:\n\tprint(_('Could not find a \"{}\" config option to set').format(set_name))\n\tsys.exit(1)\n\n# Go through the file again and update the correct line\nfor line in fileinput.input([config_path], inplace=1):\n\tprint(line.replace(found_line, set_name + \" = \" + set_value + \"\\n\"), end=\"\")\n\nprint(_(\"Config option updated\"))\n"
  },
  {
    "path": "howdy/src/cli/snap.py",
    "content": "# Create a snapshot\n\n# Import required modules\nimport os\nimport configparser\nfrom datetime import timezone, datetime\nimport snapshot\nimport paths_factory\nfrom recorders.video_capture import VideoCapture\n\nfrom i18n import _\n\n# Read the config\nconfig = configparser.ConfigParser()\nconfig.read(paths_factory.config_file_path())\n\n# Start video capture\nvideo_capture = VideoCapture(config)\n\n# Read a frame to activate emitters\nvideo_capture.read_frame()\n\n# Read exposure and dark_thresholds from config to use in the main loop\nexposure = config.getint(\"video\", \"exposure\", fallback=-1)\ndark_threshold = config.getfloat(\"video\", \"dark_threshold\", fallback=60)\n\n# COllection of recorded frames\nframes = []\n\nwhile True:\n\t# Grab a single frame of video\n\tframe, gsframe = video_capture.read_frame()\n\n\t# Add the frame to the list\n\tframes.append(frame)\n\n\t# Stop the loop if we have 4 frames\n\tif len(frames) >= 4:\n\t\tbreak\n\n# Generate a snapshot image from the frames\nfile = snapshot.generate(frames, [\n\t_(\"GENERATED SNAPSHOT\"),\n\t_(\"Date: \") + datetime.now(timezone.utc).strftime(\"%Y/%m/%d %H:%M:%S UTC\"),\n\t_(\"Dark threshold config: \") + str(config.getfloat(\"video\", \"dark_threshold\", fallback=60.0)),\n\t_(\"Certainty config: \") + str(config.getfloat(\"video\", \"certainty\", fallback=3.5))\n])\n\n# Show the file location in console\nprint(_(\"Generated snapshot saved as\"))\nprint(file)\n"
  },
  {
    "path": "howdy/src/cli/test.py",
    "content": "# Show a window with the video stream and testing information\n\n# Import required modules\nimport configparser\nimport builtins\nimport os\nimport json\nimport sys\nimport time\nimport dlib\nimport cv2\nimport numpy as np\nimport paths_factory\n\nfrom i18n import _\nfrom recorders.video_capture import VideoCapture\n\n# Read config from disk\nconfig = configparser.ConfigParser()\nconfig.read(paths_factory.config_file_path())\n\nif config.get(\"video\", \"recording_plugin\", fallback=\"opencv\") != \"opencv\":\n\tprint(_(\"Howdy has been configured to use a recorder which doesn't support the test command yet, aborting\"))\n\tsys.exit(12)\n\nvideo_capture = VideoCapture(config)\n\n# Read config values to use in the main loop\nvideo_certainty = config.getfloat(\"video\", \"certainty\", fallback=3.5) / 10\nexposure = config.getint(\"video\", \"exposure\", fallback=-1)\ndark_threshold = config.getfloat(\"video\", \"dark_threshold\", fallback=60)\n\n# Let the user know what's up\nprint(_(\"\"\"\nOpening a window with a test feed\n\nPress ctrl+C in this terminal to quit\nClick on the image to enable or disable slow mode\n\"\"\"))\n\n\ndef mouse(event, x, y, flags, param):\n\t\"\"\"Handle mouse events\"\"\"\n\tglobal slow_mode\n\n\t# Toggle slowmode on click\n\tif event == cv2.EVENT_LBUTTONDOWN:\n\t\tslow_mode = not slow_mode\n\n\ndef print_text(line_number, text):\n\t\"\"\"Print the status text by line number\"\"\"\n\tcv2.putText(overlay, text, (10, height - 10 - (10 * line_number)), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)\n\n\nuse_cnn = config.getboolean('core', 'use_cnn', fallback=False)\n\nif use_cnn:\n\tface_detector = dlib.cnn_face_detection_model_v1(\n\t\tpaths_factory.mmod_human_face_detector_path()\n\t)\nelse:\n\tface_detector = dlib.get_frontal_face_detector()\n\npose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path())\nface_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path())\n\nencodings = []\nmodels = None\n\ntry:\n\tuser = builtins.howdy_user\n\tmodels = json.load(open(paths_factory.user_model_path(user)))\n\n\tfor model in models:\n\t\tencodings += model[\"data\"]\nexcept FileNotFoundError:\n\tpass\n\nclahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n\n# Open the window and attach a a mouse listener\ncv2.namedWindow(\"Howdy Test\")\ncv2.setMouseCallback(\"Howdy Test\", mouse)\n\n# Enable a delay in the loop\nslow_mode = False\n# Count all frames ever\ntotal_frames = 0\n# Count all frames per second\nsec_frames = 0\n# Last secands FPS\nfps = 0\n# The current second we're counting\nsec = int(time.time())\n# recognition time\nrec_tm = 0\n\n# Wrap everything in an keyboard interrupt handler\ntry:\n\twhile True:\n\t\tframe_tm = time.time()\n\n\t\t# Increment the frames\n\t\ttotal_frames += 1\n\t\tsec_frames += 1\n\n\t\t# Id we've entered a new second\n\t\tif sec != int(frame_tm):\n\t\t\t# Set the last seconds FPS\n\t\t\tfps = sec_frames\n\n\t\t\t# Set the new second and reset the counter\n\t\t\tsec = int(frame_tm)\n\t\t\tsec_frames = 0\n\n\t\t# Grab a single frame of video\n\t\torig_frame, frame = video_capture.read_frame()\n\n\t\tframe = clahe.apply(frame)\n\t\t# Make a frame to put overlays in\n\t\toverlay = frame.copy()\n\t\toverlay = cv2.cvtColor(overlay, cv2.COLOR_GRAY2BGR)\n\n\t\t# Fetch the frame height and width\n\t\theight, width = frame.shape[:2]\n\n\t\t# Create a histogram of the image with 8 values\n\t\thist = cv2.calcHist([frame], [0], None, [8], [0, 256])\n\t\t# All values combined for percentage calculation\n\t\thist_total = int(sum(hist)[0])\n\t\t# Fill with the overall containing percentage\n\t\thist_perc = []\n\n\t\t# Loop though all values to calculate a percentage and add it to the overlay\n\t\tfor index, value in enumerate(hist):\n\t\t\tvalue_perc = float(value[0]) / hist_total * 100\n\t\t\thist_perc.append(value_perc)\n\n\t\t\t# Top left point, 10px margins\n\t\t\tp1 = (20 + (10 * index), 10)\n\t\t\t# Bottom right point makes the bar 10px thick, with an height of half the percentage\n\t\t\tp2 = (10 + (10 * index), int(value_perc / 2 + 10))\n\t\t\t# Draw the bar in green\n\t\t\tcv2.rectangle(overlay, p1, p2, (0, 200, 0), thickness=cv2.FILLED)\n\n\t\t# Print the statis in the bottom left\n\t\tprint_text(0, _(\"RESOLUTION: %dx%d\") % (height, width))\n\t\tprint_text(1, _(\"FPS: %d\") % (fps, ))\n\t\tprint_text(2, _(\"FRAMES: %d\") % (total_frames, ))\n\t\tprint_text(3, _(\"RECOGNITION: %dms\") % (round(rec_tm * 1000), ))\n\n\t\t# Show that slow mode is on, if it's on\n\t\tif slow_mode:\n\t\t\tcv2.putText(overlay, _(\"SLOW MODE\"), (width - 66, height - 10), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)\n\n\t\t# Ignore dark frames\n\t\tif hist_perc[0] > dark_threshold:\n\t\t\t# Show that this is an ignored frame in the top right\n\t\t\tcv2.putText(overlay, _(\"DARK FRAME\"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)\n\t\telse:\n\t\t\t# Show that this is an active frame\n\t\t\tcv2.putText(overlay, _(\"SCAN FRAME\"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)\n\n\t\t\trec_tm = time.time()\n\n\t\t\t# Get the locations of all faces and their locations\n\t\t\t# Upsample it once\n\t\t\tface_locations = face_detector(frame, 1)\n\t\t\trec_tm = time.time() - rec_tm\n\n\t\t\t# Loop though all faces and paint a circle around them\n\t\t\tfor loc in face_locations:\n\t\t\t\tif use_cnn:\n\t\t\t\t\tloc = loc.rect\n\n\t\t\t\t# By default the circle around the face is red for no match\n\t\t\t\tcolor = (0, 0, 230)\n\n\t\t\t\t# Get the center X and Y from the rectangular points\n\t\t\t\tx = int((loc.right() - loc.left()) / 2) + loc.left()\n\t\t\t\ty = int((loc.bottom() - loc.top()) / 2) + loc.top()\n\n\t\t\t\t# Get the raduis from the with of the square\n\t\t\t\tr = (loc.right() - loc.left()) / 2\n\t\t\t\t# Add 20% padding\n\t\t\t\tr = int(r + (r * 0.2))\n\n\t\t\t\t# If we have models defined for the current user\n\t\t\t\tif models:\n\t\t\t\t\t# Get the encoding of the face in the frame\n\t\t\t\t\tface_landmark = pose_predictor(orig_frame, loc)\n\t\t\t\t\tface_encoding = np.array(face_encoder.compute_face_descriptor(orig_frame, face_landmark, 1))\n\n\t\t\t\t\t# Match this found face against a known face\n\t\t\t\t\tmatches = np.linalg.norm(encodings - face_encoding, axis=1)\n\n\t\t\t\t\t# Get best match\n\t\t\t\t\tmatch_index = np.argmin(matches)\n\t\t\t\t\tmatch = matches[match_index]\n\n\t\t\t\t\t# If a model matches\n\t\t\t\t\tif 0 < match < video_certainty:\n\t\t\t\t\t\t# Turn the circle green\n\t\t\t\t\t\tcolor = (0, 230, 0)\n\n\t\t\t\t\t\t# Print the name of the model next to the circle\n\t\t\t\t\t\tcircle_text = \"{} (certainty: {})\".format(models[match_index][\"label\"], round(match * 10, 3))\n\t\t\t\t\t\tcv2.putText(overlay, circle_text, (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)\n\t\t\t\t\t# If no approved matches, show red text\n\t\t\t\t\telse:\n\t\t\t\t\t\tcv2.putText(overlay, \"no match\", (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)\n\n\t\t\t\t# Draw the Circle in green\n\t\t\t\tcv2.circle(overlay, (x, y), r, color, 2)\n\n\t\t# Add the overlay to the frame with some transparency\n\t\talpha = 0.65\n\t\tframe = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)\n\t\tcv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)\n\n\t\t# Show the image in a window\n\t\tcv2.imshow(\"Howdy Test\", frame)\n\n\t\t# Quit on any keypress\n\t\tif cv2.waitKey(1) != -1:\n\t\t\traise KeyboardInterrupt()\n\n\t\tframe_time = time.time() - frame_tm\n\n\t\t# Delay the frame if slowmode is on\n\t\tif slow_mode:\n\t\t\ttime.sleep(max([.5 - frame_time, 0.0]))\n\n\t\tif exposure != -1:\n\t\t\t# For a strange reason on some cameras (e.g. Lenoxo X1E)\n\t\t\t# setting manual exposure works only after a couple frames\n\t\t\t# are captured and even after a delay it does not\n\t\t\t# always work. Setting exposure at every frame is\n\t\t\t# reliable though.\n\t\t\tvideo_capture.internal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0)  # 1 = Manual\n\t\t\tvideo_capture.internal.set(cv2.CAP_PROP_EXPOSURE, float(exposure))\n\n# On ctrl+C\nexcept KeyboardInterrupt:\n\t# Let the user know we're stopping\n\tprint(_(\"\\nClosing window\"))\n\n\t# Release handle to the webcam\n\tcv2.destroyAllWindows()\n"
  },
  {
    "path": "howdy/src/cli.py",
    "content": "# CLI directly called by running the howdy command\n\n# Import required modules\nimport sys\nimport os\nimport pwd\nimport getpass\nimport argparse\nimport builtins\n\nfrom i18n import _\n\n# Try to get the original username (not \"root\") from shell\nsudo_user = os.environ.get(\"SUDO_USER\")\ndoas_user = os.environ.get(\"DOAS_USER\")\npkexec_uid = os.environ.get(\"PKEXEC_UID\")\npkexec_user = pwd.getpwuid(int(pkexec_uid))[0] if pkexec_uid else \"\"\nenv_user = getpass.getuser()\nuser = next((u for u in [sudo_user, doas_user, pkexec_user, env_user] if u), \"\")\n\n# If that fails, error out\nif user == \"\":\n    print(_(\"Could not determine user, please use the --user flag\"))\n    sys.exit(1)\n\n# Basic command setup\nparser = argparse.ArgumentParser(\n\tdescription=_(\"Command line interface for Howdy face authentication.\"),\n\tformatter_class=argparse.RawDescriptionHelpFormatter,\n\tadd_help=False,\n\tprog=\"howdy\",\n\tusage=\"howdy [-U USER] [--plain] [-h] [-y] {command} [{arguments}...]\".format(command=_(\"command\"), arguments=_(\"arguments\")),\n\tepilog=_(\"For support please visit\\nhttps://github.com/boltgolt/howdy\"))\n\n# Add an argument for the command\nparser.add_argument(\n\t\"command\",\n\thelp=_(\"The command option to execute, can be one of the following: add, clear, config, disable, list, remove, snapshot, set, test or version.\"),\n\tmetavar=\"command\",\n\tchoices=[\"add\", \"clear\", \"config\", \"disable\", \"list\", \"remove\", \"set\", \"snapshot\", \"test\", \"version\"])\n\n# Add an argument for the extra arguments of disable and remove\nparser.add_argument(\n\t\"arguments\",\n\thelp=_(\"Optional arguments for the add, disable, remove and set commands.\"),\n\tnargs=\"*\")\n\n# Add the user flag\nparser.add_argument(\n\t\"-U\", \"--user\",\n\tdefault=user,\n\thelp=_(\"Set the user account to use.\"))\n\n# Add the -y flag\nparser.add_argument(\n\t\"-y\",\n\thelp=_(\"Skip all questions.\"),\n\taction=\"store_true\")\n\n# Add the --plain flag\nparser.add_argument(\n\t\"--plain\",\n\thelp=_(\"Print machine-friendly output.\"),\n\taction=\"store_true\")\n\n# Overwrite the default help message so we can use a uppercase S\nparser.add_argument(\n\t\"-h\", \"--help\",\n\taction=\"help\",\n\tdefault=argparse.SUPPRESS,\n\thelp=_(\"Show this help message and exit.\"))\n\n# If we only have 1 argument we print the help text\nif len(sys.argv) < 2:\n\tprint(_(\"current active user: \") + user + \"\\n\")\n\tparser.print_help()\n\tsys.exit(0)\n\n# Parse all arguments above\nargs = parser.parse_args()\n\n# Save the args and user as builtins which can be accessed by the imports\nbuiltins.howdy_args = args\nbuiltins.howdy_user = args.user\n\n# Check if we have rootish rights\n# This is this far down the file so running the command for help is always possible\nif os.geteuid() != 0:\n\tprint(_(\"Please run this command as root:\\n\"))\n\tprint(\"\\tsudo howdy \" + \" \".join(sys.argv[1:]))\n\tsys.exit(1)\n\n# Beyond this point the user can't change anymore, if we still have root as user we need to abort\nif args.user == \"root\":\n\tprint(_(\"Can't run howdy commands as root, please run this command with the --user flag\"))\n\tsys.exit(1)\n\n# Execute the right command\nif args.command == \"add\":\n\timport cli.add\nelif args.command == \"clear\":\n\timport cli.clear\nelif args.command == \"config\":\n\timport cli.config\nelif args.command == \"disable\":\n\timport cli.disable\nelif args.command == \"list\":\n\timport cli.list\nelif args.command == \"remove\":\n\timport cli.remove\nelif args.command == \"set\":\n\timport cli.set\nelif args.command == \"snapshot\":\n\timport cli.snap\nelif args.command == \"test\":\n\timport cli.test\nelse:\n\tprint(\"Howdy 3.0.0 BETA\")\n"
  },
  {
    "path": "howdy/src/compare.py",
    "content": "# Compare incoming video with known faces\n# Running in a local python instance to get around PATH issues\n\n# Import time so we can start timing asap\nimport time\n\n# Start timing\ntimings = {\n\t\"st\": time.time()\n}\n\n# Import required modules\nimport sys\nimport os\nimport json\nimport configparser\nimport dlib\nimport cv2\nfrom datetime import timezone, datetime\nimport atexit\nimport subprocess\nimport snapshot\nimport numpy as np\nimport _thread as thread\nimport paths_factory\nfrom recorders.video_capture import VideoCapture\nfrom i18n import _\n\ndef exit(code=None):\n\t\"\"\"Exit while closing howdy-gtk properly\"\"\"\n\tglobal gtk_proc\n\n\t# Exit the auth ui process if there is one\n\tif \"gtk_proc\" in globals():\n\t\tgtk_proc.terminate()\n\n\t# Exit compare\n\tif code is not None:\n\t\tsys.exit(code)\n\n\ndef init_detector(lock):\n\t\"\"\"Start face detector, encoder and predictor in a new thread\"\"\"\n\tglobal face_detector, pose_predictor, face_encoder\n\n\t# Test if at lest 1 of the data files is there and abort if it's not\n\tif not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()):\n\t\tprint(_(\"Data files have not been downloaded, please run the following commands:\"))\n\t\tprint(\"\\n\\tcd \" + paths_factory.dlib_data_dir_path())\n\t\tprint(\"\\tsudo ./install.sh\\n\")\n\t\tlock.release()\n\t\texit(1)\n\n\t# Use the CNN detector if enabled\n\tif use_cnn:\n\t\tface_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path())\n\telse:\n\t\tface_detector = dlib.get_frontal_face_detector()\n\n\t# Start the others regardless\n\tpose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path())\n\tface_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path())\n\n\t# Note the time it took to initialize detectors\n\ttimings[\"ll\"] = time.time() - timings[\"ll\"]\n\tlock.release()\n\n\ndef make_snapshot(type):\n\t\"\"\"Generate snapshot after detection\"\"\"\n\tsnapshot.generate(snapframes, [\n\t\ttype + _(\" LOGIN\"),\n\t\t_(\"Date: \") + datetime.now(timezone.utc).strftime(\"%Y/%m/%d %H:%M:%S UTC\"),\n\t\t_(\"Scan time: \") + str(round(time.time() - timings[\"fr\"], 2)) + \"s\",\n\t\t_(\"Frames: \") + str(frames) + \" (\" + str(round(frames / (time.time() - timings[\"fr\"]), 2)) + \"FPS)\",\n\t\t_(\"Hostname: \") + os.uname().nodename,\n\t\t_(\"Best certainty value: \") + str(round(lowest_certainty * 10, 1))\n\t])\n\n\ndef send_to_ui(type, message):\n\t\"\"\"Send message to the auth ui\"\"\"\n\tglobal gtk_proc\n\n\t# Only execute of the process started\n\tif \"gtk_proc\" in globals():\n\t\t# Format message so the ui can parse it\n\t\tmessage = type + \"=\" + message + \" \\n\"\n\n\t\t# Try to send the message to the auth ui, but it's okay if that fails\n\t\ttry:\n\t\t\tif gtk_proc.poll() is None: # Make sure the gtk_proc is still running before write into the pipe\n\t\t\t\tgtk_proc.stdin.write(bytearray(message.encode(\"utf-8\")))\n\t\t\t\tgtk_proc.stdin.flush()\n\t\texcept IOError:\n\t\t\tpass\n\n\n# Make sure we were given an username to test against\nif len(sys.argv) < 2:\n\texit(12)\n\n# The username of the user being authenticated\nuser = sys.argv[1]\n# The model file contents\nmodels = []\n# Encoded face models\nencodings = []\n# Amount of ignored 100% black frames\nblack_tries = 0\n# Amount of ignored dark frames\ndark_tries = 0\n# Total amount of frames captured\nframes = 0\n# Captured frames for snapshot capture\nsnapframes = []\n# Tracks the lowest certainty value in the loop\nlowest_certainty = 10\n# Face recognition/detection instances\nface_detector = None\npose_predictor = None\nface_encoder = None\n\n# Try to load the face model from the models folder\ntry:\n\tmodels = json.load(open(paths_factory.user_model_path(user)))\n\n\tfor model in models:\n\t\tencodings += model[\"data\"]\nexcept FileNotFoundError:\n\texit(10)\n\n# Check if the file contains a model\nif len(models) < 1:\n\texit(10)\n\n# Read config from disk\nconfig = configparser.ConfigParser()\nconfig.read(paths_factory.config_file_path())\n\n# Get all config values needed\nuse_cnn = config.getboolean(\"core\", \"use_cnn\", fallback=False)\ntimeout = config.getint(\"video\", \"timeout\", fallback=4)\ndark_threshold = config.getfloat(\"video\", \"dark_threshold\", fallback=50.0)\nvideo_certainty = config.getfloat(\"video\", \"certainty\", fallback=3.5) / 10\nend_report = config.getboolean(\"debug\", \"end_report\", fallback=False)\nsave_failed = config.getboolean(\"snapshots\", \"save_failed\", fallback=False)\nsave_successful = config.getboolean(\"snapshots\", \"save_successful\", fallback=False)\ngtk_stdout = config.getboolean(\"debug\", \"gtk_stdout\", fallback=False)\nrotate = config.getint(\"video\", \"rotate\", fallback=0)\n\n# Send the gtk output to the terminal if enabled in the config\ngtk_pipe = sys.stdout if gtk_stdout else subprocess.DEVNULL\n\n# Start the auth ui, register it to be always be closed on exit\ntry:\n\tgtk_proc = subprocess.Popen([\"howdy-gtk\", \"--start-auth-ui\"], stdin=subprocess.PIPE, stdout=gtk_pipe, stderr=gtk_pipe)\n\tatexit.register(exit)\nexcept FileNotFoundError:\n\tpass\n\n# Write to the stdin to redraw ui\nsend_to_ui(\"M\", _(\"Starting up...\"))\n\n# Save the time needed to start the script\ntimings[\"in\"] = time.time() - timings[\"st\"]\n\n# Import face recognition, takes some time\ntimings[\"ll\"] = time.time()\n\n# Start threading and wait for init to finish\nlock = thread.allocate_lock()\nlock.acquire()\nthread.start_new_thread(init_detector, (lock, ))\n\n# Start video capture on the IR camera\ntimings[\"ic\"] = time.time()\n\nvideo_capture = VideoCapture(config)\n\n# Read exposure from config to use in the main loop\nexposure = config.getint(\"video\", \"exposure\", fallback=-1)\n\n# Note the time it took to open the camera\ntimings[\"ic\"] = time.time() - timings[\"ic\"]\n\n# wait for thread to finish\nlock.acquire()\nlock.release()\ndel lock\n\n# Fetch the max frame height\nmax_height = config.getfloat(\"video\", \"max_height\", fallback=320.0)\n\n# Get the height of the image (which would be the width if screen is portrait oriented)\nheight = video_capture.internal.get(cv2.CAP_PROP_FRAME_HEIGHT) or 1\nif rotate == 2:\n\theight = video_capture.internal.get(cv2.CAP_PROP_FRAME_WIDTH) or 1\n# Calculate the amount the image has to shrink\nscaling_factor = (max_height / height) or 1\n\n# Fetch config settings out of the loop\ntimeout = config.getint(\"video\", \"timeout\", fallback=4)\ndark_threshold = config.getfloat(\"video\", \"dark_threshold\", fallback=60)\nend_report = config.getboolean(\"debug\", \"end_report\", fallback=False)\n\n# Initiate histogram equalization\nclahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n\n# Let the ui know that we're ready\nsend_to_ui(\"M\", _(\"Identifying you...\"))\n\n# Start the read loop\nframes = 0\nvalid_frames = 0\ntimings[\"fr\"] = time.time()\ndark_running_total = 0\n\nwhile True:\n\t# Increment the frame count every loop\n\tframes += 1\n\n\t# Form a string to let the user know we're real busy\n\tui_subtext = \"Scanned \" + str(valid_frames - dark_tries) + \" frames\"\n\tif (dark_tries > 1):\n\t\tui_subtext += \" (skipped \" + str(dark_tries) + \" dark frames)\"\n\t# Show it in the ui as subtext\n\tsend_to_ui(\"S\", ui_subtext)\n\n\t# Stop if we've exceeded the time limit\n\tif time.time() - timings[\"fr\"] > timeout:\n\t\t# Create a timeout snapshot if enabled\n\t\tif save_failed:\n\t\t\tmake_snapshot(_(\"FAILED\"))\n\n\t\tif dark_tries == valid_frames:\n\t\t\tprint(_(\"All frames were too dark, please check dark_threshold in config\"))\n\t\t\tprint(_(\"Average darkness: {avg}, Threshold: {threshold}\").format(avg=str(dark_running_total / max(1, valid_frames)), threshold=str(dark_threshold)))\n\t\t\texit(13)\n\t\telse:\n\t\t\texit(11)\n\n\t# Grab a single frame of video\n\tframe, gsframe = video_capture.read_frame()\n\tgsframe = clahe.apply(gsframe)\n\n\t# If snapshots have been turned on\n\tif save_failed or save_successful:\n\t\t# Start capturing frames for the snapshot\n\t\tif len(snapframes) < 3:\n\t\t\tsnapframes.append(frame)\n\n\t# Create a histogram of the image with 8 values\n\thist = cv2.calcHist([gsframe], [0], None, [8], [0, 256])\n\t# All values combined for percentage calculation\n\thist_total = np.sum(hist)\n\n\t# Calculate frame darkness\n\tdarkness = (hist[0] / hist_total * 100)\n\n\t# If the image is fully black due to a bad camera read,\n\t# skip to the next frame\n\tif (hist_total == 0) or (darkness == 100):\n\t\tblack_tries += 1\n\t\tcontinue\n\n\tdark_running_total += darkness\n\tvalid_frames += 1\n\n\t# If the image exceeds darkness threshold due to subject distance,\n\t# skip to the next frame\n\tif (darkness > dark_threshold):\n\t\tdark_tries += 1\n\t\tcontinue\n\n\t# If the height is too high\n\tif scaling_factor != 1:\n\t\t# Apply that factor to the frame\n\t\tframe = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)\n\t\tgsframe = cv2.resize(gsframe, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)\n\n\t# If camera is configured to rotate = 1, check portrait in addition to landscape\n\tif rotate == 1:\n\t\tif frames % 3 == 1:\n\t\t\tframe = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)\n\t\t\tgsframe = cv2.rotate(gsframe, cv2.ROTATE_90_COUNTERCLOCKWISE)\n\t\tif frames % 3 == 2:\n\t\t\tframe = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)\n\t\t\tgsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE)\n\n\t# If camera is configured to rotate = 2, check portrait orientation\n\telif rotate == 2:\n\t\tif frames % 2 == 0:\n\t\t\tframe = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)\n\t\t\tgsframe = cv2.rotate(gsframe, cv2.ROTATE_90_COUNTERCLOCKWISE)\n\t\telse:\n\t\t\tframe = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)\n\t\t\tgsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE)\n\n\t# Get all faces from that frame as encodings\n\t# Upsamples 1 time\n\tface_locations = face_detector(gsframe, 1)\n\t# Loop through each face\n\tfor fl in face_locations:\n\t\tif use_cnn:\n\t\t\tfl = fl.rect\n\n\t\t# Fetch the faces in the image\n\t\tface_landmark = pose_predictor(frame, fl)\n\t\tface_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1))\n\n\t\t# Match this found face against a known face\n\t\tmatches = np.linalg.norm(encodings - face_encoding, axis=1)\n\n\t\t# Get best match\n\t\tmatch_index = np.argmin(matches)\n\t\tmatch = matches[match_index]\n\n\t\t# Update certainty if we have a new low\n\t\tif lowest_certainty > match:\n\t\t\tlowest_certainty = match\n\n\t\t# Check if a match that's confident enough\n\t\tif 0 < match < video_certainty:\n\t\t\ttimings[\"tt\"] = time.time() - timings[\"st\"]\n\t\t\ttimings[\"fl\"] = time.time() - timings[\"fr\"]\n\n\t\t\t# If set to true in the config, print debug text\n\t\t\tif end_report:\n\t\t\t\tdef print_timing(label, k):\n\t\t\t\t\t\"\"\"Helper function to print a timing from the list\"\"\"\n\t\t\t\t\tprint(\"  %s: %dms\" % (label, round(timings[k] * 1000)))\n\n\t\t\t\t# Print a nice timing report\n\t\t\t\tprint(_(\"Time spent\"))\n\t\t\t\tprint_timing(_(\"Starting up\"), \"in\")\n\t\t\t\tprint(_(\"  Open cam + load libs: %dms\") % (round(max(timings[\"ll\"], timings[\"ic\"]) * 1000, )))\n\t\t\t\tprint_timing(_(\"  Opening the camera\"), \"ic\")\n\t\t\t\tprint_timing(_(\"  Importing recognition libs\"), \"ll\")\n\t\t\t\tprint_timing(_(\"Searching for known face\"), \"fl\")\n\t\t\t\tprint_timing(_(\"Total time\"), \"tt\")\n\n\t\t\t\tprint(_(\"\\nResolution\"))\n\t\t\t\twidth = video_capture.fw or 1\n\t\t\t\tprint(_(\"  Native: %dx%d\") % (height, width))\n\t\t\t\t# Save the new size for diagnostics\n\t\t\t\tscale_height, scale_width = frame.shape[:2]\n\t\t\t\tprint(_(\"  Used: %dx%d\") % (scale_height, scale_width))\n\n\t\t\t\t# Show the total number of frames and calculate the FPS by dividing it by the total scan time\n\t\t\t\tprint(_(\"\\nFrames searched: %d (%.2f fps)\") % (frames, frames / timings[\"fl\"]))\n\t\t\t\tprint(_(\"Black frames ignored: %d \") % (black_tries, ))\n\t\t\t\tprint(_(\"Dark frames ignored: %d \") % (dark_tries, ))\n\t\t\t\tprint(_(\"Certainty of winning frame: %.3f\") % (match * 10, ))\n\n\t\t\t\tprint(_(\"Winning model: %d (\\\"%s\\\")\") % (match_index, models[match_index][\"label\"]))\n\n\t\t\t# Make snapshot if enabled\n\t\t\tif save_successful:\n\t\t\t\tmake_snapshot(_(\"SUCCESSFUL\"))\n\n\t\t\t# Run rubberstamps if enabled\n\t\t\tif config.getboolean(\"rubberstamps\", \"enabled\", fallback=False):\n\t\t\t\timport rubberstamps\n\n\t\t\t\tsend_to_ui(\"S\", \"\")\n\n\t\t\t\tif \"gtk_proc\" not in vars():\n\t\t\t\t\tgtk_proc = None\n\n\t\t\t\trubberstamps.execute(config, gtk_proc, {\n\t\t\t\t\t\"video_capture\": video_capture,\n\t\t\t\t\t\"face_detector\": face_detector,\n\t\t\t\t\t\"pose_predictor\": pose_predictor,\n\t\t\t\t\t\"clahe\": clahe\n\t\t\t\t})\n\n\t\t\t# End peacefully\n\t\t\texit(0)\n\n\tif exposure != -1:\n\t\t# For a strange reason on some cameras (e.g. Lenoxo X1E) setting manual exposure works only after a couple frames\n\t\t# are captured and even after a delay it does not always work. Setting exposure at every frame is reliable though.\n\t\tvideo_capture.internal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0)  # 1 = Manual\n\t\tvideo_capture.internal.set(cv2.CAP_PROP_EXPOSURE, float(exposure))\n"
  },
  {
    "path": "howdy/src/config.ini",
    "content": "# Howdy config file\n# Press CTRL + X to save in the nano editor\n\n[core]\n# Print that face detection is being attempted\ndetection_notice = false\n\n# Print that face detection has timed out\ntimeout_notice = true\n\n# Do not print anything when a face verification succeeds\nno_confirmation = false\n\n# When a user without a known face model tries to use this script, don't\n# show an error but fail silently\nsuppress_unknown = false\n\n# Disable Howdy in remote shells\nabort_if_ssh = true\n\n# Disable Howdy if lid is closed\nabort_if_lid_closed = true\n\n# Disable howdy in the PAM\n# The howdy command will still function\ndisabled = false\n\n# Use CNN instead of HOG\n# CNN model is much more accurate than the HOG based model, but takes much more\n# power to run, and is meant to be executed on a GPU to attain reasonable speed.\nuse_cnn = false\n\n# Set a workaround to do face and password authentication at the same time\n#  off     user will have to press enter themselves after a Howdy timeout\n#  input   will send an enter keypress to stop the password prompt\n#  native  will stop the prompt at PAM level (can lead to instability!)\nworkaround = off\n\n[video]\n# The certainty of the detected face belonging to the user of the account\n# On a scale from 1 to 10, values above 5 are not recommended\n# The lower, the more accurate\ncertainty = 3.5\n\n# The number of seconds to search before timing out\ntimeout = 4\n\n# The path of the device to capture frames from\n# Video devices are usually found in /dev/v4l/by-path/\ndevice_path = none\n\n# Print a warning if the the video device is not found\nwarn_no_device = true\n\n# Scale down the video feed to this maximum height\n# Speeds up face recognition but can make it less precise\nmax_height = 320\n\n# Set the camera input profile to this width and height\n# The largest profile will be used if set to -1\n# Automatically ignored if not a valid profile\nframe_width = -1\nframe_height = -1\n\n# Because of flashing IR emitters, some frames can be completely unlit\n# Skip the frame if the lowest 1/8 of the histogram is above this percentage\n# of the total\n# The lower this setting is, the more dark frames are ignored\ndark_threshold = 60\n\n# The recorder to use. Can be either opencv (default), ffmpeg or pyv4l2.\n# Switching from the default opencv to ffmpeg can help with grayscale issues.\nrecording_plugin = opencv\n\n# Video format used by ffmpeg. Options include vfwcap or v4l2.\n# FFMPEG only.\ndevice_format = v4l2\n\n# Force the use of Motion JPEG when decoding frames, fixes issues with YUYV\n# raw frame decoding.\n# OPENCV only.\nforce_mjpeg = false\n\n# Specify exposure value explicitly. This disables autoexposure.\n# Use qv4l2 to determine an appropriate value.\n# OPENCV only.\nexposure = -1\n\n# Specify frame rate of the capture device.\n# Some IR emitters will not function properly at the default framerate.\n# Use qv4l2 to determine an appropriate value.\n# OPENCV only.\ndevice_fps = -1\n\n# Rotate captured frames so faces are upright.\n#  0  Check landscape orientation only\n#  1  Check both landscape and portrait orientation\n#  2  Check portrait orientation only\nrotate = 0\n\n[snapshots]\n# Capture snapshots of failed login attempts and save them to disk with metadata\n# Snapshots are saved to /var/log/howdy/snapshots\nsave_failed = false\n\n# Do the same as the option above but for successful attempts\nsave_successful = false\n\n[rubberstamps]\n# Enable specific extra checks after the user has been recognised\nenabled = false\n\n# What type of stamps to run and with what options. The type, timeout and\n# failure mode are required. One line per stamp. Rule syntax:\n#  stamptype  timeout  (failsafe | faildeadly)   [extra_argument=value]\nstamp_rules =\n\tnod\t\t5s\t\tfailsafe     min_distance=12\n\n[debug]\n# Show a short but detailed diagnostic report in console\n# Enabling this can cause some UI apps to fail, only enable it to debug\nend_report = false\n\n# More verbose logging from the rubberstamps system\nverbose_stamps = false\n\n# Pass output of the GTK auth window to the terminal\ngtk_stdout = false\n"
  },
  {
    "path": "howdy/src/dlib-data/.gitignore",
    "content": "*.dat\n*.dat.bz2\n"
  },
  {
    "path": "howdy/src/dlib-data/Readme.md",
    "content": "Download and unpack `dlib` data files from https://github.com/davisking/dlib-models repository:\n\n```\nshell\nwget https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2\nwget https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2\nwget https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2\nbunzip *bz2\n```\n"
  },
  {
    "path": "howdy/src/dlib-data/install.sh",
    "content": "#!/bin/bash\n\necho \"Downloading 3 required data files...\"\n\n# Check if wget is installed\nif hash wget;then\n\t# Check if wget supports the option to only show the progress bar\n\twget --help | grep -q \"\\--show-progress\" && \\\n\t\t_PROGRESS_OPT=\"-q --show-progress\" || _PROGRESS_OPT=\"\"\n\n\t# Download the archives\n\twget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2\n\twget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2\n\twget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2\n\n# Otherwise fall back on curl\nelse\n\tcurl --location --retry 5 --output dlib_face_recognition_resnet_model_v1.dat.bz2 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2\n\tcurl --location --retry 5 --output mmod_human_face_detector.dat.bz2 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2\n\tcurl --location --retry 5 --output shape_predictor_5_face_landmarks.dat.bz2 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2\nfi\n\n# Uncompress the data files and delete the original archive\necho \" \"\necho \"Unpacking...\"\nbzip2 -d -f *.bz2\n"
  },
  {
    "path": "howdy/src/i18n.py",
    "content": "# Support file for translations\n\n# Import modules\nimport gettext\nimport os\n\n# Get the right translation based on locale, falling back to base if none found\ntranslation = gettext.translation(\"core\", localedir=os.path.join(os.path.dirname(__file__), 'locales'), fallback=True)\ntranslation.install()\n\n# Export translation function as _\n_ = translation.gettext\n"
  },
  {
    "path": "howdy/src/meson.build",
    "content": "if meson.is_subproject()\nproject('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\nendif\n\n\ndatadir = get_option('prefix') / get_option('datadir') / 'howdy'\npy_conf = configuration_data(paths_dict)\npy_conf.set('data_dir', datadir)\n\npy = import('python').find_installation(paths_dict.get('python_path'))\npy.dependency()\n\npy_paths = configure_file(\n    input: 'paths.py.in',\n    output: 'paths.py',\n    configuration: py_conf,\n)\n\npy_sources = [\n    'cli/__init__.py',\n    'cli/add.py',\n    'cli/clear.py',\n    'cli/config.py',\n    'cli/disable.py',\n    'cli/list.py',\n    'cli/remove.py',\n    'cli/set.py',\n    'cli/snap.py',\n    'cli/test.py',\n    'cli.py',\n    'compare.py',\n    'i18n.py',\n    'paths_factory.py',\n    'recorders/__init__.py',\n    'recorders/ffmpeg_reader.py',\n    'recorders/pyv4l2_reader.py',\n    'recorders/v4l2.py',\n    'recorders/video_capture.py',\n    'rubberstamps/__init__.py',\n    'rubberstamps/hotkey.py',\n    'rubberstamps/nod.py',\n    'snapshot.py',\n    py_paths,\n]\n\n# Include PAM module\nif get_option('install_in_site_packages')\n    pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy')\nelse  \n    pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') / 'howdy' : join_paths(get_option('prefix'), get_option('libdir'), 'howdy')\nendif\n\npam_module_conf_data = configuration_data(paths_dict)\npam_module_conf_data.set('compare_script_path', join_paths(pysourcesinstalldir, 'compare.py'))\npam_module_conf_data.set('config_file_path', config_path)\nsubdir('pam')\nif get_option('install_pam_config')\n    # pamdir is inherited from the pam subproject\n    pam_config = configure_file(\n        input: 'pam-config/howdy.in',\n        output: 'pam-config',\n        configuration: {'pamdir': pamdir}\n    )\n    install_data(\n        pam_config,\n        install_dir: get_option('prefix') / get_option('datadir') / 'pam-configs',\n        install_mode: 'rwxr-xr-x',\n        install_tag: 'pam',\n        rename: 'howdy',\n    )\nendif\n\nif get_option('install_in_site_packages')\n    py.install_sources(\n        py_sources,\n        subdir: 'howdy',\n        preserve_path: true,\n        install_tag: 'py_sources',\n    )\nelse\n    install_data(\n        py_sources,\n        preserve_path: true,\n        install_dir: pysourcesinstalldir,\n        install_mode: 'r--r--r--',\n        install_tag: 'py_sources',\n    )\nendif\n\ninstall_data('logo.png', install_tag: 'meta')\nautocomplete = configure_file(\n    input: 'autocomplete/howdy.in',\n    output: 'autocomplete',\n    configuration: configuration_data({ 'config_path': config_path })\n)\ninstall_data(\n    autocomplete,\n    install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'bash-completion', 'completions'),\n    install_mode: 'rwxr--r--',\n    install_tag: 'bash_completion',\n    rename: 'howdy',\n)\n\nfs = import('fs')\nif not fs.exists(config_path)\n    install_data('config.ini', install_dir: confdir, install_mode: 'rwxr--r--', install_tag: 'config')\nendif\n\ninstall_data('dlib-data/install.sh', 'dlib-data/Readme.md', install_dir: dlibdatadir, install_mode: 'rwxr--r--')\n\ninstall_man('../howdy.1')\n\n# if get_option('fetch_dlib_data')\n#     downloader = find_program('wget')\n#     bunzip2 = find_program('bunzip2')\n\n#     links = [ \n#         'https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2',\n#         'https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2',\n#         'https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2'\n#     ]\n\n#     archived_model_files = [\n#         'dlib_face_recognition_resnet_model_v1.dat.bz2',\n#         'shape_predictor_5_face_landmarks.dat.bz2',\n#         'mmod_human_face_detector.dat.bz2'\n#     ]\n\n#     download = run_command(\n#         'download',\n#         links,\n#         output: archived_model_files,\n#         command: [downloader, '-O', '@OUTPUT@', '@INPUT@']\n#     )\n\n#     model_files = [\n#         'dlib_face_recognition_resnet_model_v1.dat',\n#         'shape_predictor_5_face_landmarks.dat',\n#         'mmod_human_face_detector.dat'\n#     ]\n\n#     models = custom_target(\n#         'models',\n#         input: archived_model_files,\n#         output: model_files,\n#         command: [bunzip2, '-k', '@INPUT@'],\n#     )\n\n#     install_data(\n#         model_files,\n#         install_dir: join_paths(get_option('prefix'), get_option('libdir'), 'dlib_models'),\n#     )\n\n# endif\n\ncli_path = join_paths(pysourcesinstalldir, 'cli.py')\nconf_data = configuration_data({ 'script_path': cli_path, 'python_path': py.full_path() })\n\nbin_name = 'howdy'\nbin = configure_file(\n    input: 'bin/howdy.in',\n    output: bin_name,\n    configuration: conf_data\n)\ninstall_data(\n  bin,\n  install_mode: 'rwxr-xr-x',\n  install_dir: get_option('bindir'),\n  install_tag: 'bin',\n)\n"
  },
  {
    "path": "howdy/src/pam/.gitignore",
    "content": "subprojects/inih/\n"
  },
  {
    "path": "howdy/src/pam/README.md",
    "content": "# Howdy PAM module\n\n## Requirements\n\nThis module depends on `INIReader` and `libevdev`.\nThey can be installed with these packages:\n\n```\nArch Linux - libinih libevdev\nDebian     - libinih-dev libevdev-dev\nFedora     - inih-devel libevdev-devel\nOpenSUSE   - inih libevdev-devel\n```\n\nIf your distribution doesn't provide `INIReader`,\nit will be automatically pulled from git at the subproject's pinned version.\n\n## Build\n\n``` sh\nmeson setup build\nninja -C build # or meson compile -C build\n```\n\n## Install\n\n``` sh\nmeson install -C build\n```\n\nAdd the following line to your PAM configuration (/etc/pam.d/your-service):\n\n``` pam\nauth  sufficient  pam_howdy.so\n```\n"
  },
  {
    "path": "howdy/src/pam/enter_device.cc",
    "content": "#include \"enter_device.hh\"\n\n#include <cstring>\n#include <memory>\n#include <stdexcept>\n\nEnterDevice::EnterDevice()\n    : raw_device(libevdev_new(), &libevdev_free),\n      raw_uinput_device(nullptr, &libevdev_uinput_destroy) {\n  auto *dev_ptr = raw_device.get();\n\n  libevdev_set_name(dev_ptr, \"enter device\");\n  libevdev_enable_event_type(dev_ptr, EV_KEY);\n  libevdev_enable_event_code(dev_ptr, EV_KEY, KEY_ENTER, nullptr);\n\n  int err;\n  struct libevdev_uinput *uinput_dev_ptr;\n\n  err = libevdev_uinput_create_from_device(dev_ptr, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev_ptr);\n  if (err != 0) {\n    throw std::runtime_error(std::string(\"Failed to create device: \") + strerror(-err));\n  }\n\n  raw_uinput_device.reset(uinput_dev_ptr);\n};\n\nvoid EnterDevice::send_enter_press() const {\n  auto *uinput_dev_ptr = raw_uinput_device.get();\n\n  int err;\n  err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, 1);\n  if (err != 0) {\n    throw std::runtime_error(std::string(\"Failed to write event: \") + strerror(-err));\n  }\n\n  err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, 0);\n  if (err != 0) {\n    throw std::runtime_error(std::string(\"Failed to write event: \") + strerror(-err));\n  }\n\n  err = libevdev_uinput_write_event(uinput_dev_ptr, EV_SYN, SYN_REPORT, 0);\n  if (err != 0) {\n    throw std::runtime_error(std::string(\"Failed to write event: \") + strerror(-err));\n  }\n}\n"
  },
  {
    "path": "howdy/src/pam/enter_device.hh",
    "content": "#ifndef ENTER_DEVICE_H_\n#define ENTER_DEVICE_H_\n\n#include <libevdev/libevdev-uinput.h>\n#include <libevdev/libevdev.h>\n#include <memory>\n\nclass EnterDevice {\n  std::unique_ptr<struct libevdev, decltype(&libevdev_free)> raw_device;\n  std::unique_ptr<struct libevdev_uinput, decltype(&libevdev_uinput_destroy)>\n      raw_uinput_device;\n\npublic:\n  EnterDevice();\n  void send_enter_press() const;\n  ~EnterDevice() = default;\n};\n\n#endif // ENTER_DEVICE_H\n"
  },
  {
    "path": "howdy/src/pam/main.cc",
    "content": "#include <cerrno>\n#include <csignal>\n#include <cstdlib>\n\n#include <glob.h>\n#include <libintl.h>\n#include <pthread.h>\n#include <spawn.h>\n#include <stdexcept>\n#include <sys/signalfd.h>\n#include <sys/stat.h>\n#include <sys/syslog.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <syslog.h>\n#include <unistd.h>\n\n#include <chrono>\n#include <condition_variable>\n#include <cstring>\n#include <fstream>\n#include <functional>\n#include <future>\n#include <mutex>\n#include <string>\n#include <tuple>\n\n#include <INIReader.h>\n\n#include <security/pam_appl.h>\n#include <security/pam_ext.h>\n#include <security/pam_modules.h>\n\n#include \"enter_device.hh\"\n#include \"main.hh\"\n#include \"optional_task.hh\"\n#include <paths.hh>\n\nconst auto DEFAULT_TIMEOUT =\n    std::chrono::duration<int, std::chrono::milliseconds::period>(100);\nconst auto MAX_RETRIES = 5;\n\n#define S(msg) gettext(msg)\n\n/**\n * Inspect the status code returned by the compare process\n * @param  status        The status code\n * @param  conv_function The PAM conversation function\n * @return               A PAM return code\n */\nauto howdy_error(int status,\n                 const std::function<int(int, const char *)> &conv_function)\n    -> int {\n  // If the process has exited\n  if (WIFEXITED(status)) {\n    // Get the status code returned\n    status = WEXITSTATUS(status);\n\n    switch (status) {\n    case CompareError::NO_FACE_MODEL:\n      syslog(LOG_NOTICE, \"Failure, no face model known\");\n      break;\n    case CompareError::TIMEOUT_REACHED:\n      conv_function(PAM_ERROR_MSG, S(\"Failure, timeout reached\"));\n      syslog(LOG_ERR, \"Failure, timeout reached\");\n      break;\n    case CompareError::ABORT:\n      syslog(LOG_ERR, \"Failure, general abort\");\n      break;\n    case CompareError::TOO_DARK:\n      conv_function(PAM_ERROR_MSG, S(\"Face detection image too dark\"));\n      syslog(LOG_ERR, \"Failure, image too dark\");\n      break;\n    case CompareError::INVALID_DEVICE:\n      syslog(LOG_ERR,\n             \"Failure, not possible to open camera at configured path\");\n      break;\n    default:\n      conv_function(PAM_ERROR_MSG,\n                    std::string(S(\"Unknown error: \") + status).c_str());\n      syslog(LOG_ERR, \"Failure, unknown error %d\", status);\n    }\n  } else if (WIFSIGNALED(status)) {\n    // We get the signal\n    status = WTERMSIG(status);\n\n    syslog(LOG_ERR, \"Child killed by signal %s (%d)\", strsignal(status),\n           status);\n  }\n\n  // As this function is only called for error status codes, signal an error to\n  // PAM\n  return PAM_AUTH_ERR;\n}\n\n/**\n * Format the success message if the status is successful or log the error in\n * the other case\n * @param  username      Username\n * @param  status        Status code\n * @param  config        INI  configuration\n * @param  conv_function PAM conversation function\n * @return          Returns the conversation function return code\n */\nauto howdy_status(char *username, int status, const INIReader &config,\n                  const std::function<int(int, const char *)> &conv_function)\n    -> int {\n  if (status != EXIT_SUCCESS) {\n    return howdy_error(status, conv_function);\n  }\n\n  if (!config.GetBoolean(\"core\", \"no_confirmation\", true)) {\n    // Construct confirmation text from i18n string\n    std::string confirm_text(S(\"Identified face as {}\"));\n    std::string identify_msg =\n        confirm_text.replace(confirm_text.find(\"{}\"), 2, std::string(username));\n    conv_function(PAM_TEXT_INFO, identify_msg.c_str());\n  }\n\n  syslog(LOG_INFO, \"Login approved\");\n\n  return PAM_SUCCESS;\n}\n\n/**\n * Check if Howdy should be enabled according to the configuration and the\n * environment.\n * @param  config INI configuration\n * @param  username Username\n * @return        Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled,\n * PAM_SUCCESS otherwise\n */\nauto check_enabled(const INIReader &config, const char *username) -> int {\n  // Stop executing if Howdy has been disabled in the config\n  if (config.GetBoolean(\"core\", \"disabled\", false)) {\n    syslog(LOG_INFO, \"Skipped authentication, Howdy is disabled\");\n    return PAM_AUTHINFO_UNAVAIL;\n  }\n\n  // Stop if we're in a remote shell and configured to exit\n  if (config.GetBoolean(\"core\", \"abort_if_ssh\", true)) {\n    if (checkenv(\"SSH_CONNECTION\") || checkenv(\"SSH_CLIENT\") ||\n        checkenv(\"SSH_TTY\") || checkenv(\"SSHD_OPTS\")) {\n      syslog(LOG_INFO, \"Skipped authentication, SSH session detected\");\n      return PAM_AUTHINFO_UNAVAIL;\n    }\n  }\n\n  // Try to detect the laptop lid state and stop if it's closed\n  if (config.GetBoolean(\"core\", \"abort_if_lid_closed\", true)) {\n    glob_t glob_result;\n\n    // Get any files containing lid state\n    int return_value =\n        glob(\"/proc/acpi/button/lid/*/state\", 0, nullptr, &glob_result);\n\n    if (return_value != 0) {\n      syslog(LOG_ERR, \"Failed to read files from glob: %d\", return_value);\n      if (errno != 0) {\n        syslog(LOG_ERR, \"Underlying error: %s (%d)\", strerror(errno), errno);\n      }\n    } else {\n      for (size_t i = 0; i < glob_result.gl_pathc; i++) {\n        std::ifstream file(std::string(glob_result.gl_pathv[i]));\n        std::string lid_state;\n        std::getline(file, lid_state, static_cast<char>(file.eof()));\n\n        if (lid_state.find(\"closed\") != std::string::npos) {\n          globfree(&glob_result);\n\n          syslog(LOG_INFO, \"Skipped authentication, closed lid detected\");\n          return PAM_AUTHINFO_UNAVAIL;\n        }\n      }\n    }\n    globfree(&glob_result);\n  }\n\n  // pre-check if this user has face model file\n  auto model_path = std::string(USER_MODELS_DIR) + \"/\" + username + \".dat\";\n  struct stat stat_;\n  if (stat(model_path.c_str(), &stat_) != 0) {\n    return PAM_AUTHINFO_UNAVAIL;\n  }\n\n  return PAM_SUCCESS;\n}\n\n/**\n * The main function, runs the identification and authentication\n * @param  pamh     The handle to interface directly with PAM\n * @param  flags    Flags passed on to us by PAM, XORed\n * @param  argc     Amount of rules in the PAM config (disregarded)\n * @param  argv     Options defined in the PAM config\n * @param  ask_auth_tok True if we should ask for a password too\n * @return          Returns a PAM return code\n */\nauto identify(pam_handle_t *pamh, int flags, int argc, const char **argv,\n              bool ask_auth_tok) -> int {\n  INIReader config(CONFIG_FILE_PATH);\n  openlog(\"pam_howdy\", 0, LOG_AUTHPRIV);\n\n  // Error out if we could not read the config file\n  if (config.ParseError() != 0) {\n    syslog(LOG_ERR, \"Failed to parse the configuration file: %d\",\n           config.ParseError());\n    return PAM_SYSTEM_ERR;\n  }\n\n  // Will contain the responses from PAM functions\n  int pam_res = PAM_IGNORE;\n\n  // Get the username from PAM, needed to match correct face model\n  char *username = nullptr;\n  pam_res = pam_get_user(pamh, const_cast<const char **>(&username), nullptr);\n  if (pam_res != PAM_SUCCESS) {\n    syslog(LOG_ERR, \"Failed to get username\");\n    return pam_res;\n  }\n\n  // Check if we should continue\n  pam_res = check_enabled(config, username);\n  if (pam_res != PAM_SUCCESS) {\n    return pam_res;\n  }\n\n  Workaround workaround =\n      get_workaround(config.GetString(\"core\", \"workaround\", \"input\"));\n\n  // Will contain PAM conversation structure\n  struct pam_conv *conv = nullptr;\n  const void **conv_ptr =\n      const_cast<const void **>(reinterpret_cast<void **>(&conv));\n\n  // Retrieve the PAM conversation structure\n  pam_res = pam_get_item(pamh, PAM_CONV, conv_ptr);\n  if (pam_res != PAM_SUCCESS) {\n    syslog(LOG_ERR, \"Failed to acquire conversation\");\n    return pam_res;\n  }\n\n  // Wrap the PAM conversation function in our own, easier function\n  auto conv_function = [conv](int msg_type, const char *msg_str) {\n    const struct pam_message msg = {.msg_style = msg_type, .msg = msg_str};\n    const struct pam_message *msgp = &msg;\n\n    struct pam_response res = {};\n    struct pam_response *resp = &res;\n\n    return conv->conv(1, &msgp, &resp, conv->appdata_ptr);\n  };\n\n  // Initialize gettext\n  setlocale(LC_ALL, \"\");\n  bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);\n  textdomain(GETTEXT_PACKAGE);\n\n  if (config.GetBoolean(\"core\", \"detection_notice\", true)) {\n    if ((conv_function(PAM_TEXT_INFO, S(\"Attempting facial authentication\"))) !=\n        PAM_SUCCESS) {\n      syslog(LOG_ERR, \"Failed to send detection notice\");\n    }\n  }\n\n  const char *const args[] = {PYTHON_EXECUTABLE_PATH, // NOLINT\n                              COMPARE_PROCESS_PATH, username, nullptr};\n  pid_t child_pid;\n\n  // Start the python subprocess\n  if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE_PATH, nullptr, nullptr,\n                   const_cast<char *const *>(args), nullptr) != 0) {\n    syslog(LOG_ERR, \"Can't spawn the howdy process: %s (%d)\", strerror(errno),\n           errno);\n    return PAM_SYSTEM_ERR;\n  }\n\n  // NOTE: We should replace mutex and condition_variable by atomic wait, but\n  // it's too recent (C++20)\n  std::mutex mutx;\n  std::condition_variable convar;\n  ConfirmationType confirmation_type(ConfirmationType::Unset);\n\n  // This task wait for the status of the python subprocess (we don't want a\n  // zombie process)\n  optional_task<int> child_task([&] {\n    int status;\n    waitpid(child_pid, &status, 0);\n    {\n      std::unique_lock<std::mutex> lock(mutx);\n      if (confirmation_type == ConfirmationType::Unset) {\n        confirmation_type = ConfirmationType::Howdy;\n      }\n    }\n    convar.notify_one();\n\n    return status;\n  });\n  child_task.activate();\n\n  // This task waits for the password input (if the workaround wants it)\n  optional_task<std::tuple<int, char *>> pass_task([&] {\n    char *auth_tok_ptr = nullptr;\n    int pam_res = pam_get_authtok(\n        pamh, PAM_AUTHTOK, const_cast<const char **>(&auth_tok_ptr), nullptr);\n    {\n      std::unique_lock<std::mutex> lock(mutx);\n      if (confirmation_type == ConfirmationType::Unset) {\n        confirmation_type = ConfirmationType::Pam;\n      }\n    }\n    convar.notify_one();\n\n    return std::tuple<int, char *>(pam_res, auth_tok_ptr);\n  });\n\n  auto ask_pass = ask_auth_tok && workaround != Workaround::Off;\n\n  // We ask for the password if the function requires it and if a workaround is\n  // set\n  if (ask_pass) {\n    pass_task.activate();\n  }\n\n  // Wait for the end either of the child or the password input\n  {\n    std::unique_lock<std::mutex> lock(mutx);\n    convar.wait(lock,\n                [&] { return confirmation_type != ConfirmationType::Unset; });\n  }\n\n  // The password has been entered or an error has occurred\n  if (confirmation_type == ConfirmationType::Pam) {\n    // We kill the child because we don't need its result\n    kill(child_pid, SIGTERM);\n    child_task.stop(false);\n\n    // We just wait for the thread to stop since it's this one which sent us the\n    // confirmation type\n    pass_task.stop(false);\n\n    char *password = nullptr;\n    std::tie(pam_res, password) = pass_task.get();\n\n    if (pam_res != PAM_SUCCESS) {\n      return pam_res;\n    }\n\n    // The password has been entered, we are passing it to PAM stack\n    return PAM_IGNORE;\n  }\n\n  // The compare process has finished its execution\n  child_task.stop(false);\n\n  // Get python process status code\n  int status = child_task.get();\n\n  // If python process ran into a timeout\n  // Do not send enter presses or terminate the PAM function, as the user might\n  // still be typing their password\n  if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS && ask_pass) {\n    // Wait for the password to be typed\n    pass_task.stop(false);\n\n    char *password = nullptr;\n    std::tie(pam_res, password) = pass_task.get();\n\n    if (pam_res != PAM_SUCCESS) {\n      return howdy_status(username, status, config, conv_function);\n    }\n\n    // The password has been entered, we are passing it to PAM stack\n    return PAM_IGNORE;\n  }\n\n  // We want to stop the password prompt, either by canceling the thread when\n  // workaround is set to \"native\", or by emulating \"Enter\" input with\n  // \"input\"\n\n  // UNSAFE: We cancel the thread using pthread, pam_get_authtok seems to be\n  // a cancellation point\n  if (workaround == Workaround::Native) {\n    pass_task.stop(true);\n  } else if (workaround == Workaround::Input) {\n    // We check if we have the right permissions on /dev/uinput\n    if (euidaccess(\"/dev/uinput\", W_OK | R_OK) != 0) {\n      syslog(LOG_WARNING, \"Insufficient permissions to create the fake device\");\n      conv_function(PAM_ERROR_MSG,\n                    S(\"Insufficient permissions to send Enter \"\n                      \"press, waiting for user to press it instead\"));\n    } else {\n      try {\n        EnterDevice enter_device;\n        int retries;\n\n        // We try to send it\n        enter_device.send_enter_press();\n\n        for (retries = 0;\n             retries < MAX_RETRIES &&\n             pass_task.wait(DEFAULT_TIMEOUT) == std::future_status::timeout;\n             retries++) {\n          enter_device.send_enter_press();\n        }\n\n        if (retries == MAX_RETRIES) {\n          syslog(LOG_WARNING,\n                 \"Failed to send enter input before the retries limit\");\n          conv_function(PAM_ERROR_MSG, S(\"Failed to send Enter press, waiting \"\n                                         \"for user to press it instead\"));\n        }\n      } catch (std::runtime_error &err) {\n        syslog(LOG_WARNING, \"Failed to send enter input: %s\", err.what());\n        conv_function(PAM_ERROR_MSG, S(\"Failed to send Enter press, waiting \"\n                                       \"for user to press it instead\"));\n      }\n    }\n\n    // We stop the thread (will block until the enter key is pressed if the\n    // input wasn't focused or if the uinput device failed to send keypress)\n    pass_task.stop(false);\n  }\n\n  return howdy_status(username, status, config, conv_function);\n}\n\n// Called by PAM when a user needs to be authenticated, for example by running\n// the sudo command\nPAM_EXTERN auto pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,\n                                    const char **argv) -> int {\n  return identify(pamh, flags, argc, argv, true);\n}\n\n// Called by PAM when a session is started, such as by the su command\nPAM_EXTERN auto pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,\n                                    const char **argv) -> int {\n  return identify(pamh, flags, argc, argv, false);\n}\n\n// The functions below are required by PAM, but not needed in this module\nPAM_EXTERN auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc,\n                                 const char **argv) -> int {\n  return PAM_IGNORE;\n}\nPAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,\n                                     const char **argv) -> int {\n  return PAM_IGNORE;\n}\nPAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc,\n                                 const char **argv) -> int {\n  return PAM_IGNORE;\n}\nPAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc,\n                               const char **argv) -> int {\n  return PAM_IGNORE;\n}\n"
  },
  {
    "path": "howdy/src/pam/main.hh",
    "content": "#ifndef MAIN_H_\n#define MAIN_H_\n\n#include <cstring>\n#include <string>\n#include <unistd.h>\n#include <cstdint>\n\nenum class ConfirmationType : std::uint8_t { Unset, Howdy, Pam };\nenum class Workaround : std::uint8_t { Off, Input, Native };\n\n// Exit status codes returned by the compare process\nenum CompareError : std::uint8_t {\n  NO_FACE_MODEL = 10,\n  TIMEOUT_REACHED = 11,\n  ABORT = 12,\n  TOO_DARK = 13,\n  INVALID_DEVICE = 14,\n  RUBBERSTAMP = 15\n};\n\ninline auto get_workaround(const std::string &workaround) -> Workaround {\n  if (workaround == \"input\") {\n    return Workaround::Input;\n  }\n\n  if (workaround == \"native\") {\n    return Workaround::Native;\n  }\n\n  return Workaround::Off;\n}\n\n/**\n * Check if an environment variable exists either in the environ array or using\n * getenv.\n * @param name The name of the environment variable.\n * @return The value of the environment variable or nullptr if it doesn't exist\n * or environ is nullptr.\n * @note This function was created because `getenv` wasn't working properly in\n * some contexts (like sudo).\n */\nauto checkenv(const char *name) -> bool {\n  if (std::getenv(name) != nullptr) {\n    return true;\n  }\n\n  auto len = strlen(name);\n\n  for (char **env = environ; *env != nullptr; env++) {\n    if (strncmp(*env, name, len) == 0) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\n#endif // MAIN_H_\n"
  },
  {
    "path": "howdy/src/pam/meson.build",
    "content": "inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep'])\nlibevdev = dependency('libevdev')\nlibpam = meson.get_compiler('cpp').find_library('pam')\nthreads = dependency('threads')\n\n# Translations\nsubdir('po')\n\n# Paths\npaths_h = configure_file(\n\tinput: 'paths.hh.in',\n\toutput: 'paths.hh',\n\tconfiguration: pam_module_conf_data\n)\n\npamdir = get_option('pam_dir') != '' ? get_option('pam_dir') : join_paths(get_option('prefix'), get_option('libdir'), 'security')\n\nshared_library(\n\t'pam_howdy',\n\t'main.cc',\n\t'enter_device.cc',\n\tdependencies: [\n\t\tlibpam,\n\t\tinih_cpp,\n\t\tthreads,\n\t\tlibevdev,\n\t],\n\tlink_depends: [\n\t\tpaths_h,\n\t],\n\tinstall: true,\n\tinstall_dir: pamdir,\n\tinstall_tag: 'pam_module',\n\tname_prefix: ''\n)\n"
  },
  {
    "path": "howdy/src/pam/optional_task.hh",
    "content": "#ifndef OPTIONAL_TASK_H_\n#define OPTIONAL_TASK_H_\n\n#include <cassert>\n#include <chrono>\n#include <future>\n#include <thread>\n\n// A task executed only if activated.\ntemplate <typename T> class optional_task {\n  std::thread thread;\n  std::packaged_task<T()> task;\n  std::future<T> future;\n  bool spawned{false};\n  bool is_active{false};\n\npublic:\n  explicit optional_task(std::function<T()> func);\n  void activate();\n  template <typename R, typename P>\n  auto wait(std::chrono::duration<R, P> dur) -> std::future_status;\n  auto get() -> T;\n  void stop(bool force);\n  ~optional_task();\n};\n\ntemplate <typename T>\noptional_task<T>::optional_task(std::function<T()> func)\n    : task(std::packaged_task<T()>(std::move(func))), future(task.get_future()) {}\n\n// Create a new thread and launch the task on it.\ntemplate <typename T> void optional_task<T>::activate() {\n  thread = std::thread(std::move(task));\n  spawned = true;\n  is_active = true;\n}\n\n// Wait for `dur` time and return a `future` status.\ntemplate <typename T>\ntemplate <typename R, typename P>\nauto optional_task<T>::wait(std::chrono::duration<R, P> dur)\n    -> std::future_status {\n  return future.wait_for(dur);\n}\n\n// Get the value.\n// WARNING: The function should be run only if the task has successfully been\n// stopped.\ntemplate <typename T> auto optional_task<T>::get() -> T {\n  assert(!is_active && spawned);\n  return future.get();\n}\n\n// Stop the thread:\n// - if `force` is `false`, by joining the thread.\n// - if `force` is `true`, by cancelling the thread using `pthread_cancel`.\n// WARNING: This function should be used with extreme caution when `force` is\n// set to `true`.\ntemplate <typename T> void optional_task<T>::stop(bool force) {\n  if (!(is_active && thread.joinable()) && spawned) {\n    is_active = false;\n    return;\n  }\n\n  // We use pthread to cancel the thread\n  if (force) {\n    auto native_hd = thread.native_handle();\n    pthread_cancel(native_hd);\n  }\n  thread.join();\n  is_active = false;\n}\n\ntemplate <typename T> optional_task<T>::~optional_task<T>() {\n  if (is_active && spawned) {\n    stop(false);\n  }\n}\n\n#endif // OPTIONAL_TASK_H_\n"
  },
  {
    "path": "howdy/src/pam/paths.hh.in",
    "content": "const auto COMPARE_PROCESS_PATH = \"@compare_script_path@\";\nconst auto CONFIG_FILE_PATH = \"@config_file_path@\";\nconst auto USER_MODELS_DIR = \"@user_models_dir@\";\nconst auto PYTHON_EXECUTABLE_PATH = \"@python_path@\";"
  },
  {
    "path": "howdy/src/pam/po/LINGUAS",
    "content": ""
  },
  {
    "path": "howdy/src/pam/po/POTFILES",
    "content": "main.cc"
  },
  {
    "path": "howdy/src/pam/po/meson.build",
    "content": "i18n = import('i18n')\n\n# define GETTEXT_PACKAGE and LOCALEDIR\ngettext_package = '-DGETTEXT_PACKAGE=\"@0@\"'.format(meson.project_name())\nlocaledir = '-DLOCALEDIR=\"@0@\"'.format(get_option('prefix') / get_option('localedir'))\nadd_project_arguments(gettext_package, localedir, language: 'cpp')\n\ni18n.gettext(meson.project_name(),\n    args: [ '--directory=' + meson.current_source_dir(), '--keyword=S:1' ]\n)"
  },
  {
    "path": "howdy/src/pam-config/howdy.in",
    "content": "Name: Howdy\nDefault: yes\nPriority: 512\nAuth-Type: Primary\nAuth:\n\t[success=end default=ignore]        @pamdir@/pam_howdy.so\n"
  },
  {
    "path": "howdy/src/paths.py.in",
    "content": "from pathlib import PurePath\n\n# Define the absolute path to the config directory\nconfig_dir = PurePath(\"@config_dir@\")\n\n# Define the absolute path to the DLib models data directory\ndlib_data_dir = PurePath(\"@dlib_data_dir@\")\n\n# Define the absolute path to the Howdy user models directory\nuser_models_dir = PurePath(\"@user_models_dir@\")\n\n# Define path to any howdy logs\nlog_path = PurePath(\"@log_path@\")\n\n# Define the absolute path to the Howdy data directory\ndata_dir = PurePath(\"@data_dir@\")"
  },
  {
    "path": "howdy/src/paths_factory.py",
    "content": "from pathlib import PurePath\nimport paths\n\nmodels = [\n    \"shape_predictor_5_face_landmarks.dat\",\n    \"mmod_human_face_detector.dat\",\n    \"dlib_face_recognition_resnet_model_v1.dat\",\n]\n\n\ndef dlib_data_dir_path() -> str:\n    return str(paths.dlib_data_dir)\n\n\ndef shape_predictor_5_face_landmarks_path() -> str:\n    return str(paths.dlib_data_dir / models[0])\n\n\ndef mmod_human_face_detector_path() -> str:\n    return str(paths.dlib_data_dir / models[1])\n\n\ndef dlib_face_recognition_resnet_model_v1_path() -> str:\n    return str(paths.dlib_data_dir / models[2])\n\n\ndef user_model_path(user: str) -> str:\n    return str(paths.user_models_dir / f\"{user}.dat\")\n\n\ndef config_file_path() -> str:\n    return str(paths.config_dir / \"config.ini\")\n\n\ndef snapshots_dir_path() -> PurePath:\n    return paths.log_path / \"snapshots\"\n\n\ndef snapshot_path(snapshot: str) -> str:\n    return str(snapshots_dir_path() / snapshot)\n\n\ndef user_models_dir_path() -> PurePath:\n    return paths.user_models_dir\n\n\ndef logo_path() -> str:\n    return str(paths.data_dir / \"logo.png\")\n"
  },
  {
    "path": "howdy/src/recorders/__init__.py",
    "content": ""
  },
  {
    "path": "howdy/src/recorders/ffmpeg_reader.py",
    "content": "# Class that simulates the functionality of opencv so howdy can use ffmpeg seamlessly\n\n# Import required modules\nimport numpy\nimport sys\nimport re\n\nfrom subprocess import Popen, PIPE\nfrom cv2 import CAP_PROP_FRAME_WIDTH\nfrom cv2 import CAP_PROP_FRAME_HEIGHT\nfrom i18n import _\n\ntry:\n\timport ffmpeg\nexcept ImportError:\n\tprint(_(\"Missing ffmpeg module, please run:\"))\n\tprint(\" pip3 install ffmpeg-python\\n\")\n\tsys.exit(12)\n\n\nclass ffmpeg_reader:\n\t\"\"\" This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. \"\"\"\n\n\tdef __init__(self, device_path, device_format, numframes=10):\n\t\tself.device_path = device_path\n\t\tself.device_format = device_format\n\t\tself.numframes = numframes\n\t\tself.video = ()\n\t\tself.num_frames_read = 0\n\t\tself.height = 0\n\t\tself.width = 0\n\t\tself.init_camera = True\n\n\tdef set(self, prop, setting):\n\t\t\"\"\" Setter method for height and width \"\"\"\n\t\tif prop == CAP_PROP_FRAME_WIDTH:\n\t\t\tself.width = setting\n\t\telif prop == CAP_PROP_FRAME_HEIGHT:\n\t\t\tself.height = setting\n\n\tdef get(self, prop):\n\t\t\"\"\" Getter method for height and width \"\"\"\n\t\tif prop == CAP_PROP_FRAME_WIDTH:\n\t\t\treturn self.width\n\t\telif prop == CAP_PROP_FRAME_HEIGHT:\n\t\t\treturn self.height\n\n\tdef probe(self):\n\t\t\"\"\" Probe the video device to get height and width info \"\"\"\n\n\t\t# Running this command on ffmpeg unfortunately returns with an exit code of 1, which is silly.\n\t\t# Returns an error code of 1 and this text:  \"/dev/video2: Immediate exit requested\"\n\t\targs = [\"ffmpeg\", \"-f\", self.device_format, \"-list_formats\", \"all\", \"-i\", self.device_path]\n\t\tprocess = Popen(args, stdout=PIPE, stderr=PIPE)\n\t\tout, err = process.communicate()\n\t\treturn_code = process.poll()\n\n\t\t# Worst case scenario, err will equal en empty byte string, b'', so probe will get set to [] here.\n\t\tregex = re.compile(r\"\\s\\d{3,4}x\\d{3,4}\")\n\t\tprobe = regex.findall(str(err.decode(\"utf-8\")))\n\n\t\tif not return_code == 1 or len(probe) < 1:\n\t\t\t# Could not determine the resolution from ffmpeg call. Reverting to ffmpeg.probe()\n\t\t\tprobe = ffmpeg.probe(self.device_path)\n\t\t\theight = probe[\"streams\"][0][\"height\"]\n\t\t\twidth = probe[\"streams\"][0][\"width\"]\n\t\telse:\n\t\t\t(height, width) = [x.strip() for x in probe[0].split(\"x\")]\n\n\t\t# Set height and width from probe if they haven't been set already\n\t\tif height.isdigit() and self.get(CAP_PROP_FRAME_HEIGHT) == 0:\n\t\t\tself.set(CAP_PROP_FRAME_HEIGHT, int(height))\n\t\tif width.isdigit() and self.get(CAP_PROP_FRAME_WIDTH) == 0:\n\t\t\tself.set(CAP_PROP_FRAME_WIDTH, int(width))\n\n\tdef record(self, numframes):\n\t\t\"\"\" Record a video, saving it to self.video array for processing later \"\"\"\n\n\t\t# Eensure we have set our width and height before we record, otherwise our numpy call will fail\n\t\tif self.get(CAP_PROP_FRAME_WIDTH) == 0 or self.get(CAP_PROP_FRAME_HEIGHT) == 0:\n\t\t\tself.probe()\n\n\t\t# Ensure num_frames_read is reset to 0\n\t\tself.num_frames_read = 0\n\n\t\t# Record a predetermined amount of frames from the camera\n\t\tstream, ret = (\n\t\t\tffmpeg\n\t\t\t.input(self.device_path, format=self.device_format)\n\t\t\t.output(\"pipe:\", format=\"rawvideo\", pix_fmt=\"rgb24\", vframes=numframes)\n\t\t\t.run(capture_stdout=True, quiet=True)\n\t\t)\n\t\tself.video = (\n\t\t\tnumpy\n\t\t\t.frombuffer(stream, numpy.uint8)\n\t\t\t.reshape([-1, self.width, self.height, 3])\n\t\t)\n\n\tdef read(self):\n\t\t\"\"\" Read a single frame from the self.video array. Will record a video if array is empty. \"\"\"\n\n\t\t# First time we are called, we want to initialize the camera by probing it, to ensure we have height/width\n\t\t# and then take numframes of video to fill the buffer for faster recognition.\n\t\tif self.init_camera:\n\t\t\tself.init_camera = False\n\t\t\tself.video = ()\n\t\t\tself.record(self.numframes)\n\t\t\treturn 0, self.video\n\n\t\t# If we are called and self.video is empty, we should record self.numframes to fill the video buffer\n\t\tif self.video == ():\n\t\t\tself.record(self.numframes)\n\n\t\t# If we've read max frames, but still are being requested to read more, we simply record another batch.\n\t\t# Note, the video array is 0 based, so if numframes is 10, we must subtract 1 or run into an array index\n\t\t# error.\n\t\tif self.num_frames_read >= (self.numframes - 1):\n\t\t\tself.record(self.numframes)\n\n\t\t# Add one to num_frames_read. If we were at 0, that's fine as frame 0 is almost 100% going to be black\n\t\t# as the IR lights aren't fully active yet anyways. Saves us one iteration in the while loop ni add/compare.py.\n\t\tself.num_frames_read += 1\n\n\t\t# Return a single frame of video\n\t\treturn 0, self.video[self.num_frames_read]\n\n\tdef release(self):\n\t\t\"\"\" Empty our array. If we had a hold on the camera, we would give it back here. \"\"\"\n\t\tself.video = ()\n\t\tself.num_frames_read = 0\n\n\tdef grab(self):\n\t\t\"\"\" Redirect grab() to read() for compatibility \"\"\"\n\t\tself.read()\n"
  },
  {
    "path": "howdy/src/recorders/pyv4l2_reader.py",
    "content": "# Class that simulates the functionality of opencv so howdy can use v4l2 devices seamlessly\n\n# Import required modules. lib4l-dev package is also required.\nimport fcntl\nimport numpy\nimport sys\n\nfrom recorders import v4l2\nfrom cv2 import cvtColor, COLOR_GRAY2BGR, CAP_PROP_FRAME_WIDTH, CAP_PROP_FRAME_HEIGHT\nfrom i18n import _\n\ntry:\n\tfrom pyv4l2.frame import Frame\nexcept ImportError:\n\tprint(_(\"Missing pyv4l2 module, please run:\"))\n\tprint(\" pip3 install pyv4l2\\n\")\n\tsys.exit(13)\n\n\nclass pyv4l2_reader:\n\t\"\"\" This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. \"\"\"\n\n\t# Init\n\tdef __init__(self, device_name, device_format):\n\t\tself.device_name = device_name\n\t\tself.device_format = device_format\n\t\tself.height = 0\n\t\tself.width = 0\n\t\tself.probe()\n\t\tself.frame = \"\"\n\n\tdef set(self, prop, setting):\n\t\t\"\"\" Setter method for height and width \"\"\"\n\t\tif prop == CAP_PROP_FRAME_WIDTH:\n\t\t\tself.width = setting\n\t\telif prop == CAP_PROP_FRAME_HEIGHT:\n\t\t\tself.height = setting\n\n\tdef get(self, prop):\n\t\t\"\"\" Getter method for height and width \"\"\"\n\t\tif prop == CAP_PROP_FRAME_WIDTH:\n\t\t\treturn self.width\n\t\telif prop == CAP_PROP_FRAME_HEIGHT:\n\t\t\treturn self.height\n\n\tdef probe(self):\n\t\t\"\"\" Probe the video device to get height and width info \"\"\"\n\n\t\tvd = open(self.device_name, 'r')\n\t\tfmt = v4l2.v4l2_format()\n\t\tfmt.type = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE\n\t\tret = fcntl.ioctl(vd, v4l2.VIDIOC_G_FMT, fmt)\n\t\tvd.close()\n\t\tif ret == 0:\n\t\t\theight = fmt.fmt.pix.height\n\t\t\twidth = fmt.fmt.pix.width\n\t\telse:\n\t\t\t# Could not determine the resolution from ioctl call. Reverting to slower ffmpeg.probe() method\n\t\t\timport ffmpeg\n\t\t\tprobe = ffmpeg.probe(self.device_name)\n\t\t\theight = int(probe['streams'][0]['height'])\n\t\t\twidth = int(probe['streams'][0]['width'])\n\n\t\tif self.get(CAP_PROP_FRAME_HEIGHT) == 0:\n\t\t\tself.set(CAP_PROP_FRAME_HEIGHT, int(height))\n\n\t\tif self.get(CAP_PROP_FRAME_WIDTH) == 0:\n\t\t\tself.set(CAP_PROP_FRAME_WIDTH, int(width))\n\n\tdef record(self):\n\t\t\"\"\" Start recording \"\"\"\n\t\tself.frame = Frame(self.device_name)\n\n\tdef grab(self):\n\t\t\"\"\" Read a single frame from the IR camera. \"\"\"\n\t\tself.read()\n\n\tdef read(self):\n\t\t\"\"\" Read a single frame from the IR camera. \"\"\"\n\n\t\tif not self.frame:\n\t\t\tself.record()\n\n\t\t# Grab a raw frame from the camera\n\t\tframe_data = self.frame.get_frame()\n\n\t\t# Convert the raw frame_date to a numpy array\n\t\timg = (numpy.frombuffer(frame_data, numpy.uint8))\n\n\t\t# Convert the numpy array to a proper grayscale image array\n\t\timg_bgr = cvtColor(img, COLOR_GRAY2BGR)\n\n\t\t# Convert the grayscale image array into a proper RGB style numpy array\n\t\timg2 = (numpy.frombuffer(img_bgr, numpy.uint8).reshape([352, 352, 3]))\n\n\t\t# Return a single frame of video\n\t\treturn 0, img2\n\n\tdef release(self):\n\t\t\"\"\" Empty our array. If we had a hold on the camera, we would give it back here. \"\"\"\n\t\tself.video = ()\n\t\tself.num_frames_read = 0\n\t\tif self.frame:\n\t\t\tself.frame.close()\n"
  },
  {
    "path": "howdy/src/recorders/v4l2.py",
    "content": "# Python bindings for the v4l2 userspace api\n\n# Copyright (C) 1999-2009 the contributors\n\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation; either version 2 of the License, or\n# (at your option) any later version.\n\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\n# Alternatively you can redistribute this file under the terms of the\n# BSD license as stated below:\n\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n# 1. Redistributions of source code must retain the above copyright\n#    notice, this list of conditions and the following disclaimer.\n# 2. Redistributions in binary form must reproduce the above copyright\n#    notice, this list of conditions and the following disclaimer in\n#    the documentation and/or other materials provided with the\n#    distribution.\n# 3. The names of its contributors may not be used to endorse or promote\n#    products derived from this software without specific prior written\n#    permission.\n\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\n# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"\nPython bindings for the v4l2 userspace api in Linux 2.6.34\n\"\"\"\n\n# see linux/videodev2.h\n\nimport ctypes\n\n\n_IOC_NRBITS = 8\n_IOC_TYPEBITS = 8\n_IOC_SIZEBITS = 14\n_IOC_DIRBITS = 2\n\n_IOC_NRSHIFT = 0\n_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS\n_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS\n_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS\n\n_IOC_NONE = 0\n_IOC_WRITE = 1\n_IOC_READ  = 2\n\n\ndef _IOC(dir_, type_, nr, size):\n    return (\n        ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value |\n        ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value |\n        ctypes.c_int32(nr << _IOC_NRSHIFT).value |\n        ctypes.c_int32(size << _IOC_SIZESHIFT).value)\n\n\ndef _IOC_TYPECHECK(t):\n    return ctypes.sizeof(t)\n\n\ndef _IO(type_, nr):\n    return _IOC(_IOC_NONE, type_, nr, 0)\n\n\ndef _IOW(type_, nr, size):\n    return _IOC(_IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))\n\n\ndef _IOR(type_, nr, size):\n    return _IOC(_IOC_READ, type_, nr, _IOC_TYPECHECK(size))\n\n\ndef _IOWR(type_, nr, size):\n    return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))\n\n\n#\n# type alias\n#\n\nenum = ctypes.c_uint\nc_int = ctypes.c_int\n\n\n#\n# time\n#\n\nclass timeval(ctypes.Structure):\n    _fields_ = [\n        ('secs', ctypes.c_long),\n        ('usecs', ctypes.c_long),\n    ]\n\n\n#\n# v4l2\n#\n\n\nVIDEO_MAX_FRAME = 32\n\n\nVID_TYPE_CAPTURE = 1\nVID_TYPE_TUNER = 2\nVID_TYPE_TELETEXT = 4\nVID_TYPE_OVERLAY = 8\nVID_TYPE_CHROMAKEY = 16\nVID_TYPE_CLIPPING = 32\nVID_TYPE_FRAMERAM = 64\nVID_TYPE_SCALES\t= 128\nVID_TYPE_MONOCHROME = 256\nVID_TYPE_SUBCAPTURE = 512\nVID_TYPE_MPEG_DECODER = 1024\nVID_TYPE_MPEG_ENCODER = 2048\nVID_TYPE_MJPEG_DECODER = 4096\nVID_TYPE_MJPEG_ENCODER = 8192\n\n\ndef v4l2_fourcc(a, b, c, d):\n    return ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24)\n\n\nv4l2_field = enum\n(\n    V4L2_FIELD_ANY,\n    V4L2_FIELD_NONE,\n    V4L2_FIELD_TOP,\n    V4L2_FIELD_BOTTOM,\n    V4L2_FIELD_INTERLACED,\n    V4L2_FIELD_SEQ_TB,\n    V4L2_FIELD_SEQ_BT,\n    V4L2_FIELD_ALTERNATE,\n    V4L2_FIELD_INTERLACED_TB,\n    V4L2_FIELD_INTERLACED_BT,\n) = range(10)\n\n\ndef V4L2_FIELD_HAS_TOP(field):\n    return (\n\tfield == V4L2_FIELD_TOP or\n\tfield == V4L2_FIELD_INTERLACED or\n\tfield == V4L2_FIELD_INTERLACED_TB or\n\tfield == V4L2_FIELD_INTERLACED_BT or\n\tfield == V4L2_FIELD_SEQ_TB or\n\tfield == V4L2_FIELD_SEQ_BT)\n\n\ndef V4L2_FIELD_HAS_BOTTOM(field):\n    return (\n        field == V4L2_FIELD_BOTTOM or\n        field == V4L2_FIELD_INTERLACED or\n        field == V4L2_FIELD_INTERLACED_TB or\n        field == V4L2_FIELD_INTERLACED_BT or\n        field == V4L2_FIELD_SEQ_TB or\n        field == V4L2_FIELD_SEQ_BT)\n\n\ndef V4L2_FIELD_HAS_BOTH(field):\n    return (\n        field == V4L2_FIELD_INTERLACED or\n        field == V4L2_FIELD_INTERLACED_TB or\n        field == V4L2_FIELD_INTERLACED_BT or\n        field == V4L2_FIELD_SEQ_TB or\n        field == V4L2_FIELD_SEQ_BT)\n\n\nv4l2_buf_type = enum\n(\n    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n    V4L2_BUF_TYPE_VIDEO_OUTPUT,\n    V4L2_BUF_TYPE_VIDEO_OVERLAY,\n    V4L2_BUF_TYPE_VBI_CAPTURE,\n    V4L2_BUF_TYPE_VBI_OUTPUT,\n    V4L2_BUF_TYPE_SLICED_VBI_CAPTURE,\n    V4L2_BUF_TYPE_SLICED_VBI_OUTPUT,\n    V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY,\n    V4L2_BUF_TYPE_PRIVATE,\n) = list(range(1, 9)) + [0x80]\n\n\nv4l2_ctrl_type = enum\n(\n    V4L2_CTRL_TYPE_INTEGER,\n    V4L2_CTRL_TYPE_BOOLEAN,\n    V4L2_CTRL_TYPE_MENU,\n    V4L2_CTRL_TYPE_BUTTON,\n    V4L2_CTRL_TYPE_INTEGER64,\n    V4L2_CTRL_TYPE_CTRL_CLASS,\n    V4L2_CTRL_TYPE_STRING,\n) = range(1, 8)\n\n\nv4l2_tuner_type = enum\n(\n    V4L2_TUNER_RADIO,\n    V4L2_TUNER_ANALOG_TV,\n    V4L2_TUNER_DIGITAL_TV,\n) = range(1, 4)\n\n\nv4l2_memory = enum\n(\n    V4L2_MEMORY_MMAP,\n    V4L2_MEMORY_USERPTR,\n    V4L2_MEMORY_OVERLAY,\n) = range(1, 4)\n\n\nv4l2_colorspace = enum\n(\n    V4L2_COLORSPACE_SMPTE170M,\n    V4L2_COLORSPACE_SMPTE240M,\n    V4L2_COLORSPACE_REC709,\n    V4L2_COLORSPACE_BT878,\n    V4L2_COLORSPACE_470_SYSTEM_M,\n    V4L2_COLORSPACE_470_SYSTEM_BG,\n    V4L2_COLORSPACE_JPEG,\n    V4L2_COLORSPACE_SRGB,\n) = range(1, 9)\n\n\nv4l2_priority = enum\n(\n    V4L2_PRIORITY_UNSET,\n    V4L2_PRIORITY_BACKGROUND,\n    V4L2_PRIORITY_INTERACTIVE,\n    V4L2_PRIORITY_RECORD,\n    V4L2_PRIORITY_DEFAULT,\n) = list(range(0, 4)) + [2]\n\n\nclass v4l2_rect(ctypes.Structure):\n    _fields_ = [\n        ('left', ctypes.c_int32),\n        ('top', ctypes.c_int32),\n        ('width', ctypes.c_int32),\n        ('height', ctypes.c_int32),\n    ]\n\n\nclass v4l2_fract(ctypes.Structure):\n    _fields_ = [\n        ('numerator', ctypes.c_uint32),\n        ('denominator', ctypes.c_uint32),\n    ]\n\n\n#\n# Driver capabilities\n#\n\nclass v4l2_capability(ctypes.Structure):\n    _fields_ = [\n        ('driver', ctypes.c_char * 16),\n        ('card', ctypes.c_char * 32),\n        ('bus_info', ctypes.c_char * 32),\n        ('version', ctypes.c_uint32),\n        ('capabilities', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Values for 'capabilities' field\n#\n\nV4L2_CAP_VIDEO_CAPTURE = 0x00000001\nV4L2_CAP_VIDEO_OUTPUT = 0x00000002\nV4L2_CAP_VIDEO_OVERLAY = 0x00000004\nV4L2_CAP_VBI_CAPTURE = 0x00000010\nV4L2_CAP_VBI_OUTPUT = 0x00000020\nV4L2_CAP_SLICED_VBI_CAPTURE = 0x00000040\nV4L2_CAP_SLICED_VBI_OUTPUT = 0x00000080\nV4L2_CAP_RDS_CAPTURE = 0x00000100\nV4L2_CAP_VIDEO_OUTPUT_OVERLAY = 0x00000200\nV4L2_CAP_HW_FREQ_SEEK = 0x00000400\nV4L2_CAP_RDS_OUTPUT = 0x00000800\n\nV4L2_CAP_TUNER = 0x00010000\nV4L2_CAP_AUDIO = 0x00020000\nV4L2_CAP_RADIO = 0x00040000\nV4L2_CAP_MODULATOR = 0x00080000\n\nV4L2_CAP_READWRITE = 0x01000000\nV4L2_CAP_ASYNCIO = 0x02000000\nV4L2_CAP_STREAMING = 0x04000000\n\n\n#\n# Video image format\n#\n\nclass v4l2_pix_format(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('pixelformat', ctypes.c_uint32),\n        ('field', v4l2_field),\n        ('bytesperline', ctypes.c_uint32),\n        ('sizeimage', ctypes.c_uint32),\n        ('colorspace', v4l2_colorspace),\n        ('priv', ctypes.c_uint32),\n    ]\n\n# RGB formats\nV4L2_PIX_FMT_RGB332 = v4l2_fourcc('R', 'G', 'B', '1')\nV4L2_PIX_FMT_RGB444 = v4l2_fourcc('R', '4', '4', '4')\nV4L2_PIX_FMT_RGB555 = v4l2_fourcc('R', 'G', 'B', 'O')\nV4L2_PIX_FMT_RGB565 = v4l2_fourcc('R', 'G', 'B', 'P')\nV4L2_PIX_FMT_RGB555X = v4l2_fourcc('R', 'G', 'B', 'Q')\nV4L2_PIX_FMT_RGB565X = v4l2_fourcc('R', 'G', 'B', 'R')\nV4L2_PIX_FMT_BGR24 = v4l2_fourcc('B', 'G', 'R', '3')\nV4L2_PIX_FMT_RGB24 = v4l2_fourcc('R', 'G', 'B', '3')\nV4L2_PIX_FMT_BGR32 = v4l2_fourcc('B', 'G', 'R', '4')\nV4L2_PIX_FMT_RGB32 = v4l2_fourcc('R', 'G', 'B', '4')\n\n# Grey formats\nV4L2_PIX_FMT_GREY = v4l2_fourcc('G', 'R', 'E', 'Y')\nV4L2_PIX_FMT_Y10 =  v4l2_fourcc('Y', '1', '0', ' ')\nV4L2_PIX_FMT_Y16 = v4l2_fourcc('Y', '1', '6', ' ')\n\n# Palette formats\nV4L2_PIX_FMT_PAL8 = v4l2_fourcc('P', 'A', 'L', '8')\n\n# Luminance+Chrominance formats\nV4L2_PIX_FMT_YVU410 = v4l2_fourcc('Y', 'V', 'U', '9')\nV4L2_PIX_FMT_YVU420 = v4l2_fourcc('Y', 'V', '1', '2')\nV4L2_PIX_FMT_YUYV = v4l2_fourcc('Y', 'U', 'Y', 'V')\nV4L2_PIX_FMT_YYUV = v4l2_fourcc('Y', 'Y', 'U', 'V')\nV4L2_PIX_FMT_YVYU = v4l2_fourcc('Y', 'V', 'Y', 'U')\nV4L2_PIX_FMT_UYVY = v4l2_fourcc('U', 'Y', 'V', 'Y')\nV4L2_PIX_FMT_VYUY = v4l2_fourcc('V', 'Y', 'U', 'Y')\nV4L2_PIX_FMT_YUV422P = v4l2_fourcc('4', '2', '2', 'P')\nV4L2_PIX_FMT_YUV411P = v4l2_fourcc('4', '1', '1', 'P')\nV4L2_PIX_FMT_Y41P = v4l2_fourcc('Y', '4', '1', 'P')\nV4L2_PIX_FMT_YUV444 = v4l2_fourcc('Y', '4', '4', '4')\nV4L2_PIX_FMT_YUV555 = v4l2_fourcc('Y', 'U', 'V', 'O')\nV4L2_PIX_FMT_YUV565 = v4l2_fourcc('Y', 'U', 'V', 'P')\nV4L2_PIX_FMT_YUV32 = v4l2_fourcc('Y', 'U', 'V', '4')\nV4L2_PIX_FMT_YUV410 = v4l2_fourcc('Y', 'U', 'V', '9')\nV4L2_PIX_FMT_YUV420 = v4l2_fourcc('Y', 'U', '1', '2')\nV4L2_PIX_FMT_HI240 = v4l2_fourcc('H', 'I', '2', '4')\nV4L2_PIX_FMT_HM12 = v4l2_fourcc('H', 'M', '1', '2')\n\n# two planes -- one Y, one Cr + Cb interleaved\nV4L2_PIX_FMT_NV12 = v4l2_fourcc('N', 'V', '1', '2')\nV4L2_PIX_FMT_NV21 = v4l2_fourcc('N', 'V', '2', '1')\nV4L2_PIX_FMT_NV16 = v4l2_fourcc('N', 'V', '1', '6')\nV4L2_PIX_FMT_NV61 = v4l2_fourcc('N', 'V', '6', '1')\n\n# Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm\nV4L2_PIX_FMT_SBGGR8 = v4l2_fourcc('B', 'A', '8', '1')\nV4L2_PIX_FMT_SGBRG8 = v4l2_fourcc('G', 'B', 'R', 'G')\nV4L2_PIX_FMT_SGRBG8 = v4l2_fourcc('G', 'R', 'B', 'G')\nV4L2_PIX_FMT_SRGGB8 = v4l2_fourcc('R', 'G', 'G', 'B')\nV4L2_PIX_FMT_SBGGR10 = v4l2_fourcc('B', 'G', '1', '0')\nV4L2_PIX_FMT_SGBRG10 = v4l2_fourcc('G', 'B', '1', '0')\nV4L2_PIX_FMT_SGRBG10 = v4l2_fourcc('B', 'A', '1', '0')\nV4L2_PIX_FMT_SRGGB10 = v4l2_fourcc('R', 'G', '1', '0')\nV4L2_PIX_FMT_SGRBG10DPCM8 = v4l2_fourcc('B', 'D', '1', '0')\nV4L2_PIX_FMT_SBGGR16 = v4l2_fourcc('B', 'Y', 'R', '2')\n\n# compressed formats\nV4L2_PIX_FMT_MJPEG = v4l2_fourcc('M', 'J', 'P', 'G')\nV4L2_PIX_FMT_JPEG = v4l2_fourcc('J', 'P', 'E', 'G')\nV4L2_PIX_FMT_DV = v4l2_fourcc('d', 'v', 's', 'd')\nV4L2_PIX_FMT_MPEG = v4l2_fourcc('M', 'P', 'E', 'G')\n\n# Vendor-specific formats\nV4L2_PIX_FMT_CPIA1 = v4l2_fourcc('C', 'P', 'I', 'A')\nV4L2_PIX_FMT_WNVA = v4l2_fourcc('W', 'N', 'V', 'A')\nV4L2_PIX_FMT_SN9C10X = v4l2_fourcc('S', '9', '1', '0')\nV4L2_PIX_FMT_SN9C20X_I420 = v4l2_fourcc('S', '9', '2', '0')\nV4L2_PIX_FMT_PWC1 = v4l2_fourcc('P', 'W', 'C', '1')\nV4L2_PIX_FMT_PWC2 = v4l2_fourcc('P', 'W', 'C', '2')\nV4L2_PIX_FMT_ET61X251 = v4l2_fourcc('E', '6', '2', '5')\nV4L2_PIX_FMT_SPCA501 = v4l2_fourcc('S', '5', '0', '1')\nV4L2_PIX_FMT_SPCA505 = v4l2_fourcc('S', '5', '0', '5')\nV4L2_PIX_FMT_SPCA508 = v4l2_fourcc('S', '5', '0', '8')\nV4L2_PIX_FMT_SPCA561 = v4l2_fourcc('S', '5', '6', '1')\nV4L2_PIX_FMT_PAC207 = v4l2_fourcc('P', '2', '0', '7')\nV4L2_PIX_FMT_MR97310A = v4l2_fourcc('M', '3', '1', '0')\nV4L2_PIX_FMT_SN9C2028 = v4l2_fourcc('S', 'O', 'N', 'X')\nV4L2_PIX_FMT_SQ905C = v4l2_fourcc('9', '0', '5', 'C')\nV4L2_PIX_FMT_PJPG = v4l2_fourcc('P', 'J', 'P', 'G')\nV4L2_PIX_FMT_OV511 = v4l2_fourcc('O', '5', '1', '1')\nV4L2_PIX_FMT_OV518 = v4l2_fourcc('O', '5', '1', '8')\nV4L2_PIX_FMT_STV0680 = v4l2_fourcc('S', '6', '8', '0')\n\n\n#\n# Format enumeration\n#\n\nclass v4l2_fmtdesc(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('type', ctypes.c_int),\n        ('flags', ctypes.c_uint32),\n        ('description', ctypes.c_char * 32),\n        ('pixelformat', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\nV4L2_FMT_FLAG_COMPRESSED = 0x0001\nV4L2_FMT_FLAG_EMULATED = 0x0002\n\n\n#\n# Experimental frame size and frame rate enumeration\n#\n\nv4l2_frmsizetypes = enum\n(\n    V4L2_FRMSIZE_TYPE_DISCRETE,\n    V4L2_FRMSIZE_TYPE_CONTINUOUS,\n    V4L2_FRMSIZE_TYPE_STEPWISE,\n) = range(1, 4)\n\n\nclass v4l2_frmsize_discrete(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n    ]\n\n\nclass v4l2_frmsize_stepwise(ctypes.Structure):\n    _fields_ = [\n        ('min_width', ctypes.c_uint32),\n        ('min_height', ctypes.c_uint32),\n        ('step_width', ctypes.c_uint32),\n        ('min_height', ctypes.c_uint32),\n        ('max_height', ctypes.c_uint32),\n        ('step_height', ctypes.c_uint32),\n    ]\n\n\nclass v4l2_frmsizeenum(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('discrete', v4l2_frmsize_discrete),\n            ('stepwise', v4l2_frmsize_stepwise),\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('pixel_format', ctypes.c_uint32),\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n        ('reserved', ctypes.c_uint32 * 2)\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Frame rate enumeration\n#\n\nv4l2_frmivaltypes = enum\n(\n    V4L2_FRMIVAL_TYPE_DISCRETE,\n    V4L2_FRMIVAL_TYPE_CONTINUOUS,\n    V4L2_FRMIVAL_TYPE_STEPWISE,\n) = range(1, 4)\n\n\nclass v4l2_frmival_stepwise(ctypes.Structure):\n    _fields_ = [\n        ('min', v4l2_fract),\n        ('max', v4l2_fract),\n        ('step', v4l2_fract),\n    ]\n\n\nclass v4l2_frmivalenum(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('discrete', v4l2_fract),\n            ('stepwise', v4l2_frmival_stepwise),\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('pixel_format', ctypes.c_uint32),\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Timecode\n#\n\nclass v4l2_timecode(ctypes.Structure):\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('frames', ctypes.c_uint8),\n        ('seconds', ctypes.c_uint8),\n        ('minutes', ctypes.c_uint8),\n        ('hours', ctypes.c_uint8),\n        ('userbits', ctypes.c_uint8 * 4),\n    ]\n\n\nV4L2_TC_TYPE_24FPS = 1\nV4L2_TC_TYPE_25FPS = 2\nV4L2_TC_TYPE_30FPS = 3\nV4L2_TC_TYPE_50FPS = 4\nV4L2_TC_TYPE_60FPS = 5\n\nV4L2_TC_FLAG_DROPFRAME = 0x0001\nV4L2_TC_FLAG_COLORFRAME = 0x0002\nV4L2_TC_USERBITS_field = 0x000C\nV4L2_TC_USERBITS_USERDEFINED = 0x0000\nV4L2_TC_USERBITS_8BITCHARS = 0x0008\n\n\nclass v4l2_jpegcompression(ctypes.Structure):\n    _fields_ = [\n        ('quality', ctypes.c_int),\n        ('APPn', ctypes.c_int),\n        ('APP_len', ctypes.c_int),\n        ('APP_data', ctypes.c_char * 60),\n        ('COM_len', ctypes.c_int),\n        ('COM_data', ctypes.c_char * 60),\n        ('jpeg_markers', ctypes.c_uint32),\n    ]\n\n\nV4L2_JPEG_MARKER_DHT = 1 << 3\nV4L2_JPEG_MARKER_DQT = 1 << 4\nV4L2_JPEG_MARKER_DRI = 1 << 5\nV4L2_JPEG_MARKER_COM = 1 << 6\nV4L2_JPEG_MARKER_APP = 1 << 7\n\n\n#\n# Memory-mapping buffers\n#\n\nclass v4l2_requestbuffers(ctypes.Structure):\n    _fields_ = [\n        ('count', ctypes.c_uint32),\n        ('type', v4l2_buf_type),\n        ('memory', v4l2_memory),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nclass v4l2_buffer(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('offset', ctypes.c_uint32),\n            ('userptr', ctypes.c_ulong),\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('type', v4l2_buf_type),\n        ('bytesused', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('field', v4l2_field),\n        ('timestamp', timeval),\n        ('timecode', v4l2_timecode),\n        ('sequence', ctypes.c_uint32),\n        ('memory', v4l2_memory),\n        ('m', _u),\n        ('length', ctypes.c_uint32),\n        ('input', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32),\n    ]\n\n\nV4L2_BUF_FLAG_MAPPED = 0x0001\nV4L2_BUF_FLAG_QUEUED = 0x0002\nV4L2_BUF_FLAG_DONE = 0x0004\nV4L2_BUF_FLAG_KEYFRAME = 0x0008\nV4L2_BUF_FLAG_PFRAME = 0x0010\nV4L2_BUF_FLAG_BFRAME = 0x0020\nV4L2_BUF_FLAG_TIMECODE = 0x0100\nV4L2_BUF_FLAG_INPUT = 0x0200\n\n\n#\n# Overlay preview\n#\n\nclass v4l2_framebuffer(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('base', ctypes.c_void_p),\n        ('fmt', v4l2_pix_format),\n    ]\n\nV4L2_FBUF_CAP_EXTERNOVERLAY = 0x0001\nV4L2_FBUF_CAP_CHROMAKEY\t= 0x0002\nV4L2_FBUF_CAP_LIST_CLIPPING = 0x0004\nV4L2_FBUF_CAP_BITMAP_CLIPPING = 0x0008\nV4L2_FBUF_CAP_LOCAL_ALPHA = 0x0010\nV4L2_FBUF_CAP_GLOBAL_ALPHA = 0x0020\nV4L2_FBUF_CAP_LOCAL_INV_ALPHA = 0x0040\nV4L2_FBUF_CAP_SRC_CHROMAKEY = 0x0080\n\nV4L2_FBUF_FLAG_PRIMARY = 0x0001\nV4L2_FBUF_FLAG_OVERLAY = 0x0002\nV4L2_FBUF_FLAG_CHROMAKEY = 0x0004\nV4L2_FBUF_FLAG_LOCAL_ALPHA = 0x0008\nV4L2_FBUF_FLAG_GLOBAL_ALPHA = 0x0010\nV4L2_FBUF_FLAG_LOCAL_INV_ALPHA = 0x0020\nV4L2_FBUF_FLAG_SRC_CHROMAKEY = 0x0040\n\n\nclass v4l2_clip(ctypes.Structure):\n    pass\nv4l2_clip._fields_ = [\n    ('c', v4l2_rect),\n    ('next', ctypes.POINTER(v4l2_clip)),\n]\n\n\nclass v4l2_window(ctypes.Structure):\n    _fields_ = [\n        ('w', v4l2_rect),\n        ('field', v4l2_field),\n        ('chromakey', ctypes.c_uint32),\n        ('clips', ctypes.POINTER(v4l2_clip)),\n        ('clipcount', ctypes.c_uint32),\n        ('bitmap', ctypes.c_void_p),\n        ('global_alpha', ctypes.c_uint8),\n    ]\n\n\n#\n# Capture parameters\n#\n\nclass v4l2_captureparm(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('capturemode', ctypes.c_uint32),\n        ('timeperframe', v4l2_fract),\n        ('extendedmode', ctypes.c_uint32),\n        ('readbuffers', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_MODE_HIGHQUALITY = 0x0001\nV4L2_CAP_TIMEPERFRAME = 0x1000\n\n\nclass v4l2_outputparm(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('outputmode', ctypes.c_uint32),\n        ('timeperframe', v4l2_fract),\n        ('extendedmode', ctypes.c_uint32),\n        ('writebuffers', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Input image cropping\n#\n\nclass v4l2_cropcap(ctypes.Structure):\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('bounds', v4l2_rect),\n        ('defrect', v4l2_rect),\n        ('pixelaspect', v4l2_fract),\n    ]\n\n\nclass v4l2_crop(ctypes.Structure):\n    _fields_ = [\n        ('type', ctypes.c_int),\n        ('c', v4l2_rect),\n    ]\n\n\n#\n# Analog video standard\n#\n\nv4l2_std_id = ctypes.c_uint64\n\n\nV4L2_STD_PAL_B = 0x00000001\nV4L2_STD_PAL_B1 = 0x00000002\nV4L2_STD_PAL_G = 0x00000004\nV4L2_STD_PAL_H = 0x00000008\nV4L2_STD_PAL_I = 0x00000010\nV4L2_STD_PAL_D = 0x00000020\nV4L2_STD_PAL_D1 = 0x00000040\nV4L2_STD_PAL_K = 0x00000080\n\nV4L2_STD_PAL_M = 0x00000100\nV4L2_STD_PAL_N = 0x00000200\nV4L2_STD_PAL_Nc = 0x00000400\nV4L2_STD_PAL_60 = 0x00000800\n\nV4L2_STD_NTSC_M = 0x00001000\nV4L2_STD_NTSC_M_JP = 0x00002000\nV4L2_STD_NTSC_443 = 0x00004000\nV4L2_STD_NTSC_M_KR = 0x00008000\n\nV4L2_STD_SECAM_B = 0x00010000\nV4L2_STD_SECAM_D = 0x00020000\nV4L2_STD_SECAM_G = 0x00040000\nV4L2_STD_SECAM_H = 0x00080000\nV4L2_STD_SECAM_K = 0x00100000\nV4L2_STD_SECAM_K1 = 0x00200000\nV4L2_STD_SECAM_L = 0x00400000\nV4L2_STD_SECAM_LC = 0x00800000\n\nV4L2_STD_ATSC_8_VSB = 0x01000000\nV4L2_STD_ATSC_16_VSB = 0x02000000\n\n\n# some common needed stuff\nV4L2_STD_PAL_BG = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_PAL_G)\nV4L2_STD_PAL_DK = (V4L2_STD_PAL_D | V4L2_STD_PAL_D1 | V4L2_STD_PAL_K)\nV4L2_STD_PAL = (V4L2_STD_PAL_BG | V4L2_STD_PAL_DK | V4L2_STD_PAL_H | V4L2_STD_PAL_I)\nV4L2_STD_NTSC = (V4L2_STD_NTSC_M | V4L2_STD_NTSC_M_JP | V4L2_STD_NTSC_M_KR)\nV4L2_STD_SECAM_DK = (V4L2_STD_SECAM_D | V4L2_STD_SECAM_K | V4L2_STD_SECAM_K1)\nV4L2_STD_SECAM = (V4L2_STD_SECAM_B | V4L2_STD_SECAM_G | V4L2_STD_SECAM_H | V4L2_STD_SECAM_DK | V4L2_STD_SECAM_L | V4L2_STD_SECAM_LC)\n\nV4L2_STD_525_60 = (V4L2_STD_PAL_M | V4L2_STD_PAL_60 | V4L2_STD_NTSC | V4L2_STD_NTSC_443)\nV4L2_STD_625_50 = (V4L2_STD_PAL | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_SECAM)\nV4L2_STD_ATSC = (V4L2_STD_ATSC_8_VSB | V4L2_STD_ATSC_16_VSB)\n\nV4L2_STD_UNKNOWN = 0\nV4L2_STD_ALL = (V4L2_STD_525_60 | V4L2_STD_625_50)\n\n# some merged standards\nV4L2_STD_MN = (V4L2_STD_PAL_M | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_NTSC)\nV4L2_STD_B = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_SECAM_B)\nV4L2_STD_GH = (V4L2_STD_PAL_G | V4L2_STD_PAL_H|V4L2_STD_SECAM_G | V4L2_STD_SECAM_H)\nV4L2_STD_DK = (V4L2_STD_PAL_DK | V4L2_STD_SECAM_DK)\n\n\nclass v4l2_standard(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('id', v4l2_std_id),\n        ('name', ctypes.c_char * 24),\n        ('frameperiod', v4l2_fract),\n        ('framelines', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Video timings dv preset\n#\n\nclass v4l2_dv_preset(ctypes.Structure):\n    _fields_ = [\n        ('preset', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4)\n    ]\n\n\n#\n# DV preset enumeration\n#\n\nclass v4l2_dv_enum_preset(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('preset', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n#\n# DV preset values\n#\n\nV4L2_DV_INVALID = 0\nV4L2_DV_480P59_94 = 1\nV4L2_DV_576P50 = 2\nV4L2_DV_720P24 = 3\nV4L2_DV_720P25 = 4\nV4L2_DV_720P30 = 5\nV4L2_DV_720P50 = 6\nV4L2_DV_720P59_94 = 7\nV4L2_DV_720P60 = 8\nV4L2_DV_1080I29_97 = 9\nV4L2_DV_1080I30\t= 10\nV4L2_DV_1080I25\t= 11\nV4L2_DV_1080I50\t= 12\nV4L2_DV_1080I60\t= 13\nV4L2_DV_1080P24\t= 14\nV4L2_DV_1080P25\t= 15\nV4L2_DV_1080P30\t= 16\nV4L2_DV_1080P50\t= 17\nV4L2_DV_1080P60\t= 18\n\n\n#\n# DV BT timings\n#\n\nclass v4l2_bt_timings(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('interlaced', ctypes.c_uint32),\n        ('polarities', ctypes.c_uint32),\n        ('pixelclock', ctypes.c_uint64),\n        ('hfrontporch', ctypes.c_uint32),\n        ('hsync', ctypes.c_uint32),\n        ('hbackporch', ctypes.c_uint32),\n        ('vfrontporch', ctypes.c_uint32),\n        ('vsync', ctypes.c_uint32),\n        ('vbackporch', ctypes.c_uint32),\n        ('il_vfrontporch', ctypes.c_uint32),\n        ('il_vsync', ctypes.c_uint32),\n        ('il_vbackporch', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 16),\n    ]\n\n    _pack_ = True\n\n# Interlaced or progressive format\nV4L2_DV_PROGRESSIVE = 0\nV4L2_DV_INTERLACED = 1\n\n# Polarities. If bit is not set, it is assumed to be negative polarity\nV4L2_DV_VSYNC_POS_POL = 0x00000001\nV4L2_DV_HSYNC_POS_POL = 0x00000002\n\n\nclass v4l2_dv_timings(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('bt', v4l2_bt_timings),\n            ('reserved', ctypes.c_uint32 * 32),\n        ]\n\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\n# Values for the type field\nV4L2_DV_BT_656_1120 = 0\n\n\n#\n# Video inputs\n#\n\nclass v4l2_input(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', ctypes.c_uint32),\n        ('audioset', ctypes.c_uint32),\n        ('tuner', ctypes.c_uint32),\n        ('std', v4l2_std_id),\n        ('status', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_INPUT_TYPE_TUNER = 1\nV4L2_INPUT_TYPE_CAMERA = 2\n\nV4L2_IN_ST_NO_POWER = 0x00000001\nV4L2_IN_ST_NO_SIGNAL = 0x00000002\nV4L2_IN_ST_NO_COLOR = 0x00000004\n\nV4L2_IN_ST_HFLIP = 0x00000010\nV4L2_IN_ST_VFLIP = 0x00000020\n\nV4L2_IN_ST_NO_H_LOCK = 0x00000100\nV4L2_IN_ST_COLOR_KILL = 0x00000200\n\nV4L2_IN_ST_NO_SYNC = 0x00010000\nV4L2_IN_ST_NO_EQU = 0x00020000\nV4L2_IN_ST_NO_CARRIER = 0x00040000\n\nV4L2_IN_ST_MACROVISION = 0x01000000\nV4L2_IN_ST_NO_ACCESS = 0x02000000\nV4L2_IN_ST_VTR = 0x04000000\n\nV4L2_IN_CAP_PRESETS = 0x00000001\nV4L2_IN_CAP_CUSTOM_TIMINGS = 0x00000002\nV4L2_IN_CAP_STD = 0x00000004\n\n#\n# Video outputs\n#\n\nclass v4l2_output(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', ctypes.c_uint32),\n        ('audioset', ctypes.c_uint32),\n        ('modulator', ctypes.c_uint32),\n        ('std', v4l2_std_id),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_OUTPUT_TYPE_MODULATOR = 1\nV4L2_OUTPUT_TYPE_ANALOG\t= 2\nV4L2_OUTPUT_TYPE_ANALOGVGAOVERLAY = 3\n\nV4L2_OUT_CAP_PRESETS = 0x00000001\nV4L2_OUT_CAP_CUSTOM_TIMINGS = 0x00000002\nV4L2_OUT_CAP_STD = 0x00000004\n\n#\n# Controls\n#\n\nclass v4l2_control(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('value', ctypes.c_int32),\n    ]\n\n\nclass v4l2_ext_control(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('value', ctypes.c_int32),\n            ('value64', ctypes.c_int64),\n            ('reserved', ctypes.c_void_p),\n        ]\n\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('reserved2', ctypes.c_uint32 * 2),\n        ('_u', _u)\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\nclass v4l2_ext_controls(ctypes.Structure):\n    _fields_ = [\n        ('ctrl_class', ctypes.c_uint32),\n        ('count', ctypes.c_uint32),\n        ('error_idx', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n        ('controls', ctypes.POINTER(v4l2_ext_control)),\n    ]\n\n\nV4L2_CTRL_CLASS_USER = 0x00980000\nV4L2_CTRL_CLASS_MPEG = 0x00990000\nV4L2_CTRL_CLASS_CAMERA = 0x009a0000\nV4L2_CTRL_CLASS_FM_TX = 0x009b0000\n\n\ndef V4L2_CTRL_ID_MASK():\n    return 0x0fffffff\n\n\ndef V4L2_CTRL_ID2CLASS(id_):\n    return id_ & 0x0fff0000 # unsigned long\n\n\ndef V4L2_CTRL_DRIVER_PRIV(id_):\n    return (id_ & 0xffff) >= 0x1000\n\n\nclass v4l2_queryctrl(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('type', v4l2_ctrl_type),\n        ('name', ctypes.c_char * 32),\n        ('minimum', ctypes.c_int32),\n        ('maximum', ctypes.c_int32),\n        ('step', ctypes.c_int32),\n        ('default', ctypes.c_int32),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nclass v4l2_querymenu(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('reserved', ctypes.c_uint32),\n    ]\n\n\nV4L2_CTRL_FLAG_DISABLED = 0x0001\nV4L2_CTRL_FLAG_GRABBED = 0x0002\nV4L2_CTRL_FLAG_READ_ONLY = 0x0004\nV4L2_CTRL_FLAG_UPDATE = 0x0008\nV4L2_CTRL_FLAG_INACTIVE = 0x0010\nV4L2_CTRL_FLAG_SLIDER = 0x0020\nV4L2_CTRL_FLAG_WRITE_ONLY = 0x0040\n\nV4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000\n\nV4L2_CID_BASE = V4L2_CTRL_CLASS_USER | 0x900\nV4L2_CID_USER_BASE = V4L2_CID_BASE\nV4L2_CID_PRIVATE_BASE = 0x08000000\n\nV4L2_CID_USER_CLASS = V4L2_CTRL_CLASS_USER | 1\nV4L2_CID_BRIGHTNESS = V4L2_CID_BASE + 0\nV4L2_CID_CONTRAST = V4L2_CID_BASE + 1\nV4L2_CID_SATURATION = V4L2_CID_BASE + 2\nV4L2_CID_HUE = V4L2_CID_BASE + 3\nV4L2_CID_AUDIO_VOLUME = V4L2_CID_BASE + 5\nV4L2_CID_AUDIO_BALANCE = V4L2_CID_BASE + 6\nV4L2_CID_AUDIO_BASS = V4L2_CID_BASE + 7\nV4L2_CID_AUDIO_TREBLE = V4L2_CID_BASE + 8\nV4L2_CID_AUDIO_MUTE = V4L2_CID_BASE + 9\nV4L2_CID_AUDIO_LOUDNESS = V4L2_CID_BASE + 10\nV4L2_CID_BLACK_LEVEL = V4L2_CID_BASE + 11 # Deprecated\nV4L2_CID_AUTO_WHITE_BALANCE = V4L2_CID_BASE + 12\nV4L2_CID_DO_WHITE_BALANCE = V4L2_CID_BASE + 13\nV4L2_CID_RED_BALANCE = V4L2_CID_BASE + 14\nV4L2_CID_BLUE_BALANCE = V4L2_CID_BASE + 15\nV4L2_CID_GAMMA = V4L2_CID_BASE + 16\nV4L2_CID_WHITENESS = V4L2_CID_GAMMA # Deprecated\nV4L2_CID_EXPOSURE = V4L2_CID_BASE + 17\nV4L2_CID_AUTOGAIN = V4L2_CID_BASE + 18\nV4L2_CID_GAIN = V4L2_CID_BASE + 19\nV4L2_CID_HFLIP = V4L2_CID_BASE + 20\nV4L2_CID_VFLIP = V4L2_CID_BASE + 21\n\n# Deprecated; use V4L2_CID_PAN_RESET and V4L2_CID_TILT_RESET\nV4L2_CID_HCENTER = V4L2_CID_BASE + 22\nV4L2_CID_VCENTER = V4L2_CID_BASE + 23\n\nV4L2_CID_POWER_LINE_FREQUENCY = V4L2_CID_BASE + 24\n\nv4l2_power_line_frequency = enum\n(\n    V4L2_CID_POWER_LINE_FREQUENCY_DISABLED,\n    V4L2_CID_POWER_LINE_FREQUENCY_50HZ,\n    V4L2_CID_POWER_LINE_FREQUENCY_60HZ,\n) = range(3)\n\nV4L2_CID_HUE_AUTO = V4L2_CID_BASE + 25\nV4L2_CID_WHITE_BALANCE_TEMPERATURE = V4L2_CID_BASE + 26\nV4L2_CID_SHARPNESS = V4L2_CID_BASE + 27\nV4L2_CID_BACKLIGHT_COMPENSATION = V4L2_CID_BASE + 28\nV4L2_CID_CHROMA_AGC = V4L2_CID_BASE + 29\nV4L2_CID_COLOR_KILLER = V4L2_CID_BASE + 30\nV4L2_CID_COLORFX = V4L2_CID_BASE + 31\n\nv4l2_colorfx = enum\n(\n    V4L2_COLORFX_NONE,\n    V4L2_COLORFX_BW,\n    V4L2_COLORFX_SEPIA,\n) = range(3)\n\nV4L2_CID_AUTOBRIGHTNESS = V4L2_CID_BASE + 32\nV4L2_CID_BAND_STOP_FILTER = V4L2_CID_BASE + 33\n\nV4L2_CID_ROTATE = V4L2_CID_BASE + 34\nV4L2_CID_BG_COLOR = V4L2_CID_BASE + 35\nV4L2_CID_LASTP1 = V4L2_CID_BASE + 36\n\nV4L2_CID_MPEG_BASE = V4L2_CTRL_CLASS_MPEG | 0x900\nV4L2_CID_MPEG_CLASS = V4L2_CTRL_CLASS_MPEG | 1\n\n# MPEG streams\nV4L2_CID_MPEG_STREAM_TYPE = V4L2_CID_MPEG_BASE + 0\n\nv4l2_mpeg_stream_type = enum\n(\n    V4L2_MPEG_STREAM_TYPE_MPEG2_PS,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_TS,\n    V4L2_MPEG_STREAM_TYPE_MPEG1_SS,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_DVD,\n    V4L2_MPEG_STREAM_TYPE_MPEG1_VCD,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_SVCD,\n) = range(6)\n\nV4L2_CID_MPEG_STREAM_PID_PMT = V4L2_CID_MPEG_BASE + 1\nV4L2_CID_MPEG_STREAM_PID_AUDIO = V4L2_CID_MPEG_BASE + 2\nV4L2_CID_MPEG_STREAM_PID_VIDEO = V4L2_CID_MPEG_BASE + 3\nV4L2_CID_MPEG_STREAM_PID_PCR = V4L2_CID_MPEG_BASE + 4\nV4L2_CID_MPEG_STREAM_PES_ID_AUDIO = V4L2_CID_MPEG_BASE + 5\nV4L2_CID_MPEG_STREAM_PES_ID_VIDEO = V4L2_CID_MPEG_BASE + 6\nV4L2_CID_MPEG_STREAM_VBI_FMT = V4L2_CID_MPEG_BASE + 7\n\nv4l2_mpeg_stream_vbi_fmt = enum\n(\n    V4L2_MPEG_STREAM_VBI_FMT_NONE,\n    V4L2_MPEG_STREAM_VBI_FMT_IVTV,\n) = range(2)\n\nV4L2_CID_MPEG_AUDIO_SAMPLING_FREQ = V4L2_CID_MPEG_BASE + 100\n\nv4l2_mpeg_audio_sampling_freq = enum\n(\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_44100,\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_48000,\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_32000,\n) = range(3)\n\nV4L2_CID_MPEG_AUDIO_ENCODING = V4L2_CID_MPEG_BASE + 101\n\nv4l2_mpeg_audio_encoding = enum\n(\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_1,\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_2,\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_3,\n    V4L2_MPEG_AUDIO_ENCODING_AAC,\n    V4L2_MPEG_AUDIO_ENCODING_AC3,\n) = range(5)\n\nV4L2_CID_MPEG_AUDIO_L1_BITRATE = V4L2_CID_MPEG_BASE + 102\n\nv4l2_mpeg_audio_l1_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L1_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_288K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_320K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_352K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_384K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_416K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_448K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_L2_BITRATE = V4L2_CID_MPEG_BASE + 103\n\nv4l2_mpeg_audio_l2_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L2_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_48K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_56K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_80K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_112K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_320K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_384K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_L3_BITRATE = V4L2_CID_MPEG_BASE + 104\n\nv4l2_mpeg_audio_l3_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L3_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_40K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_48K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_56K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_80K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_112K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_320K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_MODE = V4L2_CID_MPEG_BASE + 105\n\nv4l2_mpeg_audio_mode = enum\n(\n    V4L2_MPEG_AUDIO_MODE_STEREO,\n    V4L2_MPEG_AUDIO_MODE_JOINT_STEREO,\n    V4L2_MPEG_AUDIO_MODE_DUAL,\n    V4L2_MPEG_AUDIO_MODE_MONO,\n) = range(4)\n\nV4L2_CID_MPEG_AUDIO_MODE_EXTENSION = V4L2_CID_MPEG_BASE + 106\n\nv4l2_mpeg_audio_mode_extension = enum\n(\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_4,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_8,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_12,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_16,\n) = range(4)\n\nV4L2_CID_MPEG_AUDIO_EMPHASIS = V4L2_CID_MPEG_BASE + 107\n\nv4l2_mpeg_audio_emphasis = enum\n(\n    V4L2_MPEG_AUDIO_EMPHASIS_NONE,\n    V4L2_MPEG_AUDIO_EMPHASIS_50_DIV_15_uS,\n    V4L2_MPEG_AUDIO_EMPHASIS_CCITT_J17,\n) = range(3)\n\nV4L2_CID_MPEG_AUDIO_CRC = V4L2_CID_MPEG_BASE + 108\n\nv4l2_mpeg_audio_crc = enum\n(\n    V4L2_MPEG_AUDIO_CRC_NONE,\n    V4L2_MPEG_AUDIO_CRC_CRC16,\n) = range(2)\n\nV4L2_CID_MPEG_AUDIO_MUTE = V4L2_CID_MPEG_BASE + 109\nV4L2_CID_MPEG_AUDIO_AAC_BITRATE = V4L2_CID_MPEG_BASE + 110\nV4L2_CID_MPEG_AUDIO_AC3_BITRATE\t= V4L2_CID_MPEG_BASE + 111\n\nv4l2_mpeg_audio_ac3_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_AC3_BITRATE_32K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_40K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_48K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_56K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_64K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_80K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_96K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_112K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_128K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_160K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_192K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_224K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_256K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_320K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_384K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_448K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_512K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_576K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_640K,\n) = range(19)\n\nV4L2_CID_MPEG_VIDEO_ENCODING = V4L2_CID_MPEG_BASE + 200\n\nv4l2_mpeg_video_encoding = enum\n(\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_1,\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_2,\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_4_AVC,\n) = range(3)\n\nV4L2_CID_MPEG_VIDEO_ASPECT = V4L2_CID_MPEG_BASE + 201\n\nv4l2_mpeg_video_aspect = enum\n(\n    V4L2_MPEG_VIDEO_ASPECT_1x1,\n    V4L2_MPEG_VIDEO_ASPECT_4x3,\n    V4L2_MPEG_VIDEO_ASPECT_16x9,\n    V4L2_MPEG_VIDEO_ASPECT_221x100,\n) = range(4)\n\nV4L2_CID_MPEG_VIDEO_B_FRAMES = V4L2_CID_MPEG_BASE + 202\nV4L2_CID_MPEG_VIDEO_GOP_SIZE = V4L2_CID_MPEG_BASE + 203\nV4L2_CID_MPEG_VIDEO_GOP_CLOSURE = V4L2_CID_MPEG_BASE + 204\nV4L2_CID_MPEG_VIDEO_PULLDOWN = V4L2_CID_MPEG_BASE + 205\nV4L2_CID_MPEG_VIDEO_BITRATE_MODE = V4L2_CID_MPEG_BASE + 206\n\nv4l2_mpeg_video_bitrate_mode = enum\n(\n    V4L2_MPEG_VIDEO_BITRATE_MODE_VBR,\n    V4L2_MPEG_VIDEO_BITRATE_MODE_CBR,\n) = range(2)\n\nV4L2_CID_MPEG_VIDEO_BITRATE = V4L2_CID_MPEG_BASE + 207\nV4L2_CID_MPEG_VIDEO_BITRATE_PEAK = V4L2_CID_MPEG_BASE + 208\nV4L2_CID_MPEG_VIDEO_TEMPORAL_DECIMATION = V4L2_CID_MPEG_BASE + 209\nV4L2_CID_MPEG_VIDEO_MUTE = V4L2_CID_MPEG_BASE + 210\nV4L2_CID_MPEG_VIDEO_MUTE_YUV = V4L2_CID_MPEG_BASE + 211\n\nV4L2_CID_MPEG_CX2341X_BASE = V4L2_CTRL_CLASS_MPEG | 0x1000\nV4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 0\n\nv4l2_mpeg_cx2341x_video_spatial_filter_mode = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_MANUAL,\n    V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_AUTO,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 1\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 2\n\nv4l2_mpeg_cx2341x_video_luma_spatial_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_OFF, \n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_HOR,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_HV_SEPARABLE,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_SYM_NON_SEPARABLE,\n) = range(5)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 3\n\nv4l2_mpeg_cx2341x_video_chroma_spatial_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_OFF,\n    V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_1D_HOR,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 4\n\nv4l2_mpeg_cx2341x_video_temporal_filter_mode = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_MANUAL,\n    V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_AUTO,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 5\nV4L2_CID_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 6\n\nv4l2_mpeg_cx2341x_video_median_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_OFF,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_DIAG,\n) = range(5)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 7\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 8\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 9\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 10\nV4L2_CID_MPEG_CX2341X_STREAM_INSERT_NAV_PACKETS = V4L2_CID_MPEG_CX2341X_BASE + 11\n\nV4L2_CID_CAMERA_CLASS_BASE = V4L2_CTRL_CLASS_CAMERA | 0x900\nV4L2_CID_CAMERA_CLASS = V4L2_CTRL_CLASS_CAMERA | 1\n\nV4L2_CID_EXPOSURE_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 1\n\nv4l2_exposure_auto_type = enum\n(\n    V4L2_EXPOSURE_AUTO,\n    V4L2_EXPOSURE_MANUAL,\n    V4L2_EXPOSURE_SHUTTER_PRIORITY,\n    V4L2_EXPOSURE_APERTURE_PRIORITY,\n) = range(4)\n\nV4L2_CID_EXPOSURE_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 2\nV4L2_CID_EXPOSURE_AUTO_PRIORITY = V4L2_CID_CAMERA_CLASS_BASE + 3\n\nV4L2_CID_PAN_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 4\nV4L2_CID_TILT_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 5\nV4L2_CID_PAN_RESET = V4L2_CID_CAMERA_CLASS_BASE + 6\nV4L2_CID_TILT_RESET = V4L2_CID_CAMERA_CLASS_BASE + 7\n\nV4L2_CID_PAN_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 8\nV4L2_CID_TILT_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 9\n\nV4L2_CID_FOCUS_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 10\nV4L2_CID_FOCUS_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 11\nV4L2_CID_FOCUS_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 12\n\nV4L2_CID_ZOOM_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 13\nV4L2_CID_ZOOM_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 14\nV4L2_CID_ZOOM_CONTINUOUS = V4L2_CID_CAMERA_CLASS_BASE + 15\n\nV4L2_CID_PRIVACY = V4L2_CID_CAMERA_CLASS_BASE + 16\n\nV4L2_CID_FM_TX_CLASS_BASE = V4L2_CTRL_CLASS_FM_TX | 0x900\nV4L2_CID_FM_TX_CLASS = V4L2_CTRL_CLASS_FM_TX | 1\n\nV4L2_CID_RDS_TX_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 1\nV4L2_CID_RDS_TX_PI = V4L2_CID_FM_TX_CLASS_BASE + 2\nV4L2_CID_RDS_TX_PTY = V4L2_CID_FM_TX_CLASS_BASE + 3\nV4L2_CID_RDS_TX_PS_NAME = V4L2_CID_FM_TX_CLASS_BASE + 5\nV4L2_CID_RDS_TX_RADIO_TEXT = V4L2_CID_FM_TX_CLASS_BASE + 6\n\nV4L2_CID_AUDIO_LIMITER_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 64\nV4L2_CID_AUDIO_LIMITER_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 65\nV4L2_CID_AUDIO_LIMITER_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 66\n\nV4L2_CID_AUDIO_COMPRESSION_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 80\nV4L2_CID_AUDIO_COMPRESSION_GAIN = V4L2_CID_FM_TX_CLASS_BASE + 81\nV4L2_CID_AUDIO_COMPRESSION_THRESHOLD = V4L2_CID_FM_TX_CLASS_BASE + 82\nV4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME = V4L2_CID_FM_TX_CLASS_BASE + 83\nV4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 84\n\nV4L2_CID_PILOT_TONE_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 96\nV4L2_CID_PILOT_TONE_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 97\nV4L2_CID_PILOT_TONE_FREQUENCY = V4L2_CID_FM_TX_CLASS_BASE + 98\n\nV4L2_CID_TUNE_PREEMPHASIS = V4L2_CID_FM_TX_CLASS_BASE + 112\n\nv4l2_preemphasis = enum\n(\n    V4L2_PREEMPHASIS_DISABLED,\n    V4L2_PREEMPHASIS_50_uS,\n    V4L2_PREEMPHASIS_75_uS,\n) = range(3)\n\nV4L2_CID_TUNE_POWER_LEVEL = V4L2_CID_FM_TX_CLASS_BASE + 113\nV4L2_CID_TUNE_ANTENNA_CAPACITOR = V4L2_CID_FM_TX_CLASS_BASE + 114\n\n\n#\n# Tuning\n#\n\nclass v4l2_tuner(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', v4l2_tuner_type),\n        ('capability', ctypes.c_uint32),\n        ('rangelow', ctypes.c_uint32),\n        ('rangehigh', ctypes.c_uint32),\n        ('rxsubchans', ctypes.c_uint32),\n        ('audmode', ctypes.c_uint32),\n        ('signal', ctypes.c_int32),\n        ('afc', ctypes.c_int32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nclass v4l2_modulator(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('rangelow', ctypes.c_uint32),\n        ('rangehigh', ctypes.c_uint32),\n        ('txsubchans', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_TUNER_CAP_LOW = 0x0001\nV4L2_TUNER_CAP_NORM = 0x0002\nV4L2_TUNER_CAP_STEREO = 0x0010\nV4L2_TUNER_CAP_LANG2 = 0x0020\nV4L2_TUNER_CAP_SAP = 0x0020\nV4L2_TUNER_CAP_LANG1 = 0x0040\nV4L2_TUNER_CAP_RDS = 0x0080\n\nV4L2_TUNER_SUB_MONO = 0x0001\nV4L2_TUNER_SUB_STEREO = 0x0002\nV4L2_TUNER_SUB_LANG2 = 0x0004\nV4L2_TUNER_SUB_SAP = 0x0004\nV4L2_TUNER_SUB_LANG1 = 0x0008\nV4L2_TUNER_SUB_RDS = 0x0010\n\nV4L2_TUNER_MODE_MONO = 0x0000\nV4L2_TUNER_MODE_STEREO = 0x0001\nV4L2_TUNER_MODE_LANG2 = 0x0002\nV4L2_TUNER_MODE_SAP = 0x0002\nV4L2_TUNER_MODE_LANG1 = 0x0003\nV4L2_TUNER_MODE_LANG1_LANG2 = 0x0004\n\n\nclass v4l2_frequency(ctypes.Structure):\n    _fields_ = [\n        ('tuner', ctypes.c_uint32),\n        ('type', v4l2_tuner_type),\n        ('frequency', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 8),\n    ]\n\n\nclass v4l2_hw_freq_seek(ctypes.Structure):\n    _fields_ = [\n        ('tuner', ctypes.c_uint32),\n        ('type', v4l2_tuner_type),\n        ('seek_upward', ctypes.c_uint32),\n        ('wrap_around', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 8),\n    ]\n\n\n#\n# RDS\n#\n\nclass v4l2_rds_data(ctypes.Structure):\n    _fields_ = [\n        ('lsb', ctypes.c_char),\n        ('msb', ctypes.c_char),\n        ('block', ctypes.c_char),\n    ]\n\n    _pack_ = True\n\n\nV4L2_RDS_BLOCK_MSK =  0x7\nV4L2_RDS_BLOCK_A = 0\nV4L2_RDS_BLOCK_B = 1\nV4L2_RDS_BLOCK_C = 2\nV4L2_RDS_BLOCK_D = 3\nV4L2_RDS_BLOCK_C_ALT = 4\nV4L2_RDS_BLOCK_INVALID = 7\n\nV4L2_RDS_BLOCK_CORRECTED = 0x40\nV4L2_RDS_BLOCK_ERROR = 0x80\n\n\n#\n# Audio\n#\n\nclass v4l2_audio(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('mode', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_AUDCAP_STEREO = 0x00001\nV4L2_AUDCAP_AVL = 0x00002\n\nV4L2_AUDMODE_AVL = 0x00001\n\n\nclass v4l2_audioout(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('mode', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\n#\n# Mpeg services (experimental)\n#\n\nV4L2_ENC_IDX_FRAME_I = 0\nV4L2_ENC_IDX_FRAME_P = 1\nV4L2_ENC_IDX_FRAME_B = 2\nV4L2_ENC_IDX_FRAME_MASK = 0xf\n\n\nclass v4l2_enc_idx_entry(ctypes.Structure):\n    _fields_ = [\n        ('offset', ctypes.c_uint64),\n        ('pts', ctypes.c_uint64),\n        ('length', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_ENC_IDX_ENTRIES = 64\n\n\nclass v4l2_enc_idx(ctypes.Structure):\n    _fields_ = [\n        ('entries', ctypes.c_uint32),\n        ('entries_cap', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n        ('entry', v4l2_enc_idx_entry * V4L2_ENC_IDX_ENTRIES),\n    ]\n\n\nV4L2_ENC_CMD_START = 0\nV4L2_ENC_CMD_STOP = 1\nV4L2_ENC_CMD_PAUSE = 2\nV4L2_ENC_CMD_RESUME = 3\n\nV4L2_ENC_CMD_STOP_AT_GOP_END = 1 << 0\n\n\nclass v4l2_encoder_cmd(ctypes.Structure):\n    class _u(ctypes.Union):\n        class _s(ctypes.Structure):\n            _fields_ = [\n                ('data', ctypes.c_uint32 * 8),\n            ]\n\n        _fields_ = [\n            ('raw', _s),\n        ]\n\n    _fields_ = [\n        ('cmd', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Data services (VBI)\n#\n\nclass v4l2_vbi_format(ctypes.Structure):\n    _fields_ = [\n        ('sampling_rate', ctypes.c_uint32),\n        ('offset', ctypes.c_uint32),\n        ('samples_per_line', ctypes.c_uint32),\n        ('sample_format', ctypes.c_uint32),\n        ('start', ctypes.c_int32 * 2),\n        ('count', ctypes.c_uint32 * 2),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_VBI_UNSYNC = 1 << 0\nV4L2_VBI_INTERLACED = 1 << 1\n\n\nclass v4l2_sliced_vbi_format(ctypes.Structure):\n    _fields_ = [\n        ('service_set', ctypes.c_uint16),\n        ('service_lines', ctypes.c_uint16 * 2 * 24),\n        ('io_size', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_SLICED_TELETEXT_B = 0x0001\nV4L2_SLICED_VPS = 0x0400\nV4L2_SLICED_CAPTION_525 = 0x1000\nV4L2_SLICED_WSS_625 = 0x4000\nV4L2_SLICED_VBI_525 = V4L2_SLICED_CAPTION_525\nV4L2_SLICED_VBI_625 = (\n    V4L2_SLICED_TELETEXT_B | V4L2_SLICED_VPS | V4L2_SLICED_WSS_625)\n\n\nclass v4l2_sliced_vbi_cap(ctypes.Structure):\n    _fields_ = [\n        ('service_set', ctypes.c_uint16),\n        ('service_lines', ctypes.c_uint16 * 2 * 24),\n        ('type', v4l2_buf_type),\n        ('reserved', ctypes.c_uint32 * 3),\n    ]\n\n\nclass v4l2_sliced_vbi_data(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('field', ctypes.c_uint32),\n        ('line', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32),\n        ('data', ctypes.c_char * 48),\n    ]\n\n\n#\n# Sliced VBI data inserted into MPEG Streams\n#\n\n\nV4L2_MPEG_VBI_IVTV_TELETEXT_B = 1\nV4L2_MPEG_VBI_IVTV_CAPTION_525 = 4\nV4L2_MPEG_VBI_IVTV_WSS_625 = 5\nV4L2_MPEG_VBI_IVTV_VPS = 7\n\n\nclass v4l2_mpeg_vbi_itv0_line(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_char),\n        ('data', ctypes.c_char * 42),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_mpeg_vbi_itv0(ctypes.Structure):\n    _fields_ = [\n        ('linemask', ctypes.c_uint32 * 2), # how to define __le32 in ctypes?\n        ('line', v4l2_mpeg_vbi_itv0_line * 35),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_mpeg_vbi_ITV0(ctypes.Structure):\n    _fields_ = [\n        ('line', v4l2_mpeg_vbi_itv0_line * 36),\n    ]\n\n    _pack_ = True\n\n\nV4L2_MPEG_VBI_IVTV_MAGIC0 = \"itv0\"\nV4L2_MPEG_VBI_IVTV_MAGIC1 = \"ITV0\"\n\n\nclass v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('itv0', v4l2_mpeg_vbi_itv0),\n            ('ITV0', v4l2_mpeg_vbi_ITV0),\n        ]\n\n    _fields_ = [\n        ('magic', ctypes.c_char * 4),\n        ('_u', _u)\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\n#\n# Aggregate structures\n#\n\nclass v4l2_format(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('pix', v4l2_pix_format),\n            ('win', v4l2_window),\n            ('vbi', v4l2_vbi_format),\n            ('sliced', v4l2_sliced_vbi_format),\n            ('raw_data', ctypes.c_char * 200),\n        ]\n\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('fmt', _u),\n    ]\n\n\nclass v4l2_streamparm(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('capture', v4l2_captureparm),\n            ('output', v4l2_outputparm),\n            ('raw_data', ctypes.c_char * 200),\n        ]\n\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('parm', _u)\n    ]\n\n\n#\n# Advanced debugging\n#\n\nV4L2_CHIP_MATCH_HOST = 0\nV4L2_CHIP_MATCH_I2C_DRIVER = 1\nV4L2_CHIP_MATCH_I2C_ADDR = 2\nV4L2_CHIP_MATCH_AC97 = 3\n\n\nclass v4l2_dbg_match(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('addr', ctypes.c_uint32),\n            ('name', ctypes.c_char * 32),\n        ]\n\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\nclass v4l2_dbg_register(ctypes.Structure):\n    _fields_ = [\n        ('match', v4l2_dbg_match),\n        ('size', ctypes.c_uint32),\n        ('reg', ctypes.c_uint64),\n        ('val', ctypes.c_uint64),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_dbg_chip_ident(ctypes.Structure):\n    _fields_ = [\n        ('match', v4l2_dbg_match),\n        ('ident', ctypes.c_uint32),\n        ('revision', ctypes.c_uint32),\n    ]\n\n    _pack_ = True\n\n\n#\n# ioctl codes for video devices\n#\n\nVIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability)\nVIDIOC_RESERVED = _IO('V', 1)\nVIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc)\nVIDIOC_G_FMT = _IOWR('V', 4, v4l2_format)\nVIDIOC_S_FMT = _IOWR('V', 5, v4l2_format)\nVIDIOC_REQBUFS = _IOWR('V', 8, v4l2_requestbuffers)\nVIDIOC_QUERYBUF\t= _IOWR('V', 9, v4l2_buffer)\nVIDIOC_G_FBUF = _IOR('V', 10, v4l2_framebuffer)\nVIDIOC_S_FBUF = _IOW('V', 11, v4l2_framebuffer)\nVIDIOC_OVERLAY = _IOW('V', 14, ctypes.c_int)\nVIDIOC_QBUF = _IOWR('V', 15, v4l2_buffer)\nVIDIOC_DQBUF = _IOWR('V', 17, v4l2_buffer)\nVIDIOC_STREAMON = _IOW('V', 18, ctypes.c_int)\nVIDIOC_STREAMOFF = _IOW('V', 19, ctypes.c_int)\nVIDIOC_G_PARM = _IOWR('V', 21, v4l2_streamparm)\nVIDIOC_S_PARM = _IOWR('V', 22, v4l2_streamparm)\nVIDIOC_G_STD = _IOR('V', 23, v4l2_std_id)\nVIDIOC_S_STD = _IOW('V', 24, v4l2_std_id)\nVIDIOC_ENUMSTD = _IOWR('V', 25, v4l2_standard)\nVIDIOC_ENUMINPUT = _IOWR('V', 26, v4l2_input)\nVIDIOC_G_CTRL = _IOWR('V', 27, v4l2_control)\nVIDIOC_S_CTRL = _IOWR('V', 28, v4l2_control)\nVIDIOC_G_TUNER = _IOWR('V', 29, v4l2_tuner)\nVIDIOC_S_TUNER = _IOW('V', 30, v4l2_tuner)\nVIDIOC_G_AUDIO = _IOR('V', 33, v4l2_audio)\nVIDIOC_S_AUDIO = _IOW('V', 34, v4l2_audio)\nVIDIOC_QUERYCTRL = _IOWR('V', 36, v4l2_queryctrl)\nVIDIOC_QUERYMENU = _IOWR('V', 37, v4l2_querymenu)\nVIDIOC_G_INPUT = _IOR('V', 38, ctypes.c_int)\nVIDIOC_S_INPUT = _IOWR('V', 39, ctypes.c_int)\nVIDIOC_G_OUTPUT = _IOR('V', 46, ctypes.c_int)\nVIDIOC_S_OUTPUT = _IOWR('V', 47, ctypes.c_int)\nVIDIOC_ENUMOUTPUT = _IOWR('V', 48, v4l2_output)\nVIDIOC_G_AUDOUT = _IOR('V', 49, v4l2_audioout)\nVIDIOC_S_AUDOUT\t= _IOW('V', 50, v4l2_audioout)\nVIDIOC_G_MODULATOR = _IOWR('V', 54, v4l2_modulator)\nVIDIOC_S_MODULATOR = _IOW('V', 55, v4l2_modulator)\nVIDIOC_G_FREQUENCY = _IOWR('V', 56, v4l2_frequency)\nVIDIOC_S_FREQUENCY = _IOW('V', 57, v4l2_frequency)\nVIDIOC_CROPCAP = _IOWR('V', 58, v4l2_cropcap)\nVIDIOC_G_CROP = _IOWR('V', 59, v4l2_crop)\nVIDIOC_S_CROP = _IOW('V', 60, v4l2_crop)\nVIDIOC_G_JPEGCOMP = _IOR('V', 61, v4l2_jpegcompression)\nVIDIOC_S_JPEGCOMP = _IOW('V', 62, v4l2_jpegcompression)\nVIDIOC_QUERYSTD = _IOR('V', 63, v4l2_std_id)\nVIDIOC_TRY_FMT = _IOWR('V', 64, v4l2_format)\nVIDIOC_ENUMAUDIO = _IOWR('V', 65, v4l2_audio)\nVIDIOC_ENUMAUDOUT = _IOWR('V', 66, v4l2_audioout)\nVIDIOC_G_PRIORITY = _IOR('V', 67, v4l2_priority)\nVIDIOC_S_PRIORITY = _IOW('V', 68, v4l2_priority)\nVIDIOC_G_SLICED_VBI_CAP = _IOWR('V', 69, v4l2_sliced_vbi_cap)\nVIDIOC_LOG_STATUS = _IO('V', 70)\nVIDIOC_G_EXT_CTRLS = _IOWR('V', 71, v4l2_ext_controls)\nVIDIOC_S_EXT_CTRLS = _IOWR('V', 72, v4l2_ext_controls)\nVIDIOC_TRY_EXT_CTRLS = _IOWR('V', 73, v4l2_ext_controls)\n\nVIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum)\nVIDIOC_ENUM_FRAMEINTERVALS = _IOWR('V', 75, v4l2_frmivalenum)\nVIDIOC_G_ENC_INDEX = _IOR('V', 76, v4l2_enc_idx)\nVIDIOC_ENCODER_CMD = _IOWR('V', 77, v4l2_encoder_cmd)\nVIDIOC_TRY_ENCODER_CMD = _IOWR('V', 78, v4l2_encoder_cmd)\n\nVIDIOC_DBG_S_REGISTER = _IOW('V', 79, v4l2_dbg_register)\nVIDIOC_DBG_G_REGISTER = _IOWR('V', 80, v4l2_dbg_register)\n\nVIDIOC_DBG_G_CHIP_IDENT = _IOWR('V', 81, v4l2_dbg_chip_ident)\n\nVIDIOC_S_HW_FREQ_SEEK = _IOW('V', 82, v4l2_hw_freq_seek)\nVIDIOC_ENUM_DV_PRESETS = _IOWR('V', 83, v4l2_dv_enum_preset)\nVIDIOC_S_DV_PRESET = _IOWR('V', 84, v4l2_dv_preset)\nVIDIOC_G_DV_PRESET = _IOWR('V', 85, v4l2_dv_preset)\nVIDIOC_QUERY_DV_PRESET = _IOR('V', 86, v4l2_dv_preset)\nVIDIOC_S_DV_TIMINGS = _IOWR('V', 87, v4l2_dv_timings)\nVIDIOC_G_DV_TIMINGS = _IOWR('V', 88, v4l2_dv_timings)\n\nVIDIOC_OVERLAY_OLD = _IOWR('V', 14, ctypes.c_int)\nVIDIOC_S_PARM_OLD = _IOW('V', 22, v4l2_streamparm)\nVIDIOC_S_CTRL_OLD = _IOW('V', 28, v4l2_control)\nVIDIOC_G_AUDIO_OLD = _IOWR('V', 33, v4l2_audio)\nVIDIOC_G_AUDOUT_OLD = _IOWR('V', 49, v4l2_audioout)\nVIDIOC_CROPCAP_OLD = _IOR('V', 58, v4l2_cropcap)\n\nBASE_VIDIOC_PRIVATE = 192\n"
  },
  {
    "path": "howdy/src/recorders/video_capture.py",
    "content": "# Top level class for a video capture providing simplified API's for common\n# functions\n\n# Import required modules\nimport configparser\nimport cv2\nimport os\nimport sys\n\nfrom i18n import _\n\n# Class to provide boilerplate code to build a video recorder with the\n# correct settings from the config file.\n#\n# The internal recorder can be accessed with 'video_capture.internal'\n\n\nclass VideoCapture:\n\tdef __init__(self, config):\n\t\t\"\"\"\n\t\tCreates a new VideoCapture instance depending on the settings in the\n\t\tprovided config file.\n\n\t\tConfig can either be a string to the path, or a pre-setup configparser.\n\t\t\"\"\"\n\n\t\t# Parse config from string if needed\n\t\tif isinstance(config, str):\n\t\t\tself.config = configparser.ConfigParser()\n\t\t\tself.config.read(config)\n\t\telse:\n\t\t\tself.config = config\n\n\t\t# Check device path\n\t\tif not os.path.exists(self.config.get(\"video\", \"device_path\")):\n\t\t\tif self.config.getboolean(\"video\", \"warn_no_device\", fallback=True):\n\t\t\t\tprint(_(\"Howdy could not find a camera device at the path specified in the config file.\"))\n\t\t\t\tprint(_(\"It is very likely that the path is not configured correctly, please edit the 'device_path' config value by running:\"))\n\t\t\t\tprint(\"\\n\\tsudo howdy config\\n\")\n\t\t\tsys.exit(14)\n\n\t\t# Create reader\n\t\t# The internal video recorder\n\t\tself.internal = None\n\t\t# The frame width\n\t\tself.fw = None\n\t\t# The frame height\n\t\tself.fh = None\n\t\tself._create_reader()\n\n\t\t# Request a frame to wake the camera up\n\t\tself.internal.grab()\n\n\tdef __del__(self):\n\t\t\"\"\"\n\t\tFrees resources when destroyed\n\t\t\"\"\"\n\t\tif self is not None:\n\t\t\ttry:\n\t\t\t\tself.internal.release()\n\t\t\texcept AttributeError as err:\n\t\t\t\tpass\n\n\tdef release(self):\n\t\t\"\"\"\n\t\tRelease cameras\n\t\t\"\"\"\n\t\tif self is not None:\n\t\t\tself.internal.release()\n\n\tdef read_frame(self):\n\t\t\"\"\"\n\t\tReads a frame, returns the frame and an attempted grayscale conversion of\n\t\tthe frame in a tuple:\n\n\t\t(frame, grayscale_frame)\n\n\t\tIf the grayscale conversion fails, both items in the tuple are identical.\n\t\t\"\"\"\n\n\t\t# Grab a single frame of video\n\t\t# Don't remove ret, it doesn't work without it\n\t\tret, frame = self.internal.read()\n\t\tif not ret:\n\t\t\tprint(_(\"Failed to read camera specified in the 'device_path' config option, aborting\"))\n\t\t\tsys.exit(14)\n\n\t\ttry:\n\t\t\t# Convert from color to grayscale\n\t\t\t# First processing of frame, so frame errors show up here\n\t\t\tgsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n\t\texcept RuntimeError:\n\t\t\tgsframe = frame\n\t\texcept cv2.error:\n\t\t\tprint(\"\\nAn error occurred in OpenCV\\n\")\n\t\t\traise\n\t\treturn frame, gsframe\n\n\tdef _create_reader(self):\n\t\t\"\"\"\n\t\tSets up the video reader instance\n\t\t\"\"\"\n\t\trecording_plugin = self.config.get(\"video\", \"recording_plugin\", fallback=\"opencv\")\n\n\t\tif recording_plugin == \"ffmpeg\":\n\t\t\t# Set the capture source for ffmpeg\n\t\t\tfrom recorders.ffmpeg_reader import ffmpeg_reader\n\t\t\tself.internal = ffmpeg_reader(\n\t\t\t\tself.config.get(\"video\", \"device_path\"),\n\t\t\t\tself.config.get(\"video\", \"device_format\", fallback=\"v4l2\")\n\t\t\t)\n\n\t\telif recording_plugin == \"pyv4l2\":\n\t\t\t# Set the capture source for pyv4l2\n\t\t\tfrom recorders.pyv4l2_reader import pyv4l2_reader\n\t\t\tself.internal = pyv4l2_reader(\n\t\t\t\tself.config.get(\"video\", \"device_path\"),\n\t\t\t\tself.config.get(\"video\", \"device_format\", fallback=\"v4l2\")\n\t\t\t)\n\n\t\telse:\n\t\t\t# Start video capture on the IR camera through OpenCV\n\t\t\tself.internal = cv2.VideoCapture(\n\t\t\t\tself.config.get(\"video\", \"device_path\"),\n\t\t\t\tcv2.CAP_V4L\n\t\t\t)\n\t\t\t# Set the capture frame rate\n\t\t\t# Without this the first detected (and possibly lower) frame rate is used, -1 seems to select the highest\n\t\t\t# Use 0 as a fallback to avoid breaking an existing setup, new installs should default to -1\n\t\t\tself.fps = self.config.getint(\"video\", \"device_fps\", fallback=0)\n\t\t\tif self.fps != 0:\n\t\t\t\tself.internal.set(cv2.CAP_PROP_FPS, self.fps)\n\n\t\t# Force MJPEG decoding if true\n\t\tif self.config.getboolean(\"video\", \"force_mjpeg\", fallback=False):\n\t\t\t# Set a magic number, will enable MJPEG but is badly documentated\n\t\t\tself.internal.set(cv2.CAP_PROP_FOURCC, 1196444237)\n\n\t\t# Set the frame width and height if requested\n\t\tself.fw = self.config.getint(\"video\", \"frame_width\", fallback=-1)\n\t\tself.fh = self.config.getint(\"video\", \"frame_height\", fallback=-1)\n\t\tif self.fw != -1:\n\t\t\tself.internal.set(cv2.CAP_PROP_FRAME_WIDTH, self.fw)\n\t\tif self.fh != -1:\n\t\t\tself.internal.set(cv2.CAP_PROP_FRAME_HEIGHT, self.fh)\n"
  },
  {
    "path": "howdy/src/rubberstamps/__init__.py",
    "content": "import sys\nimport os\nimport re\n\nfrom i18n import _\n\nfrom importlib.machinery import SourceFileLoader\n\n\nclass RubberStamp:\n\t\"\"\"Howdy rubber stamp\"\"\"\n\n\tUI_TEXT = \"ui_text\"\n\tUI_SUBTEXT = \"ui_subtext\"\n\n\tdef set_ui_text(self, text, type=None):\n\t\t\"\"\"Convert an ui string to input howdy-gtk understands\"\"\"\n\t\ttypedec = \"M\"\n\n\t\tif type == self.UI_SUBTEXT:\n\t\t\ttypedec = \"S\"\n\n\t\treturn self.send_ui_raw(typedec + \"=\" + text)\n\n\tdef send_ui_raw(self, command):\n\t\t\"\"\"Write raw command to howdy-gtk stdin\"\"\"\n\t\tif self.config.getboolean(\"debug\", \"verbose_stamps\", fallback=False):\n\t\t\tprint(\"Sending command to howdy-gtk: \" + command)\n\n\t\t# Add a newline because the ui reads per line\n\t\tcommand += \" \\n\"\n\n\t\t# If we're connected to the ui\n\t\tif self.gtk_proc:\n\t\t\t# Send the command as bytes\n\t\t\tself.gtk_proc.stdin.write(bytearray(command.encode(\"utf-8\")))\n\t\t\tself.gtk_proc.stdin.flush()\n\n\t\t\t# Write a padding line to force the command through any buffers\n\t\t\tself.gtk_proc.stdin.write(bytearray(\"P=_PADDING \\n\".encode(\"utf-8\")))\n\t\t\tself.gtk_proc.stdin.flush()\n\n\ndef execute(config, gtk_proc, opencv):\n\tverbose = config.getboolean(\"debug\", \"verbose_stamps\", fallback=False)\n\tdir_path = os.path.dirname(os.path.realpath(__file__))\n\tinstalled_stamps = []\n\n\t# Go through each file in the rubberstamp folder\n\tfor filename in os.listdir(dir_path):\n\t\t# Remove non-readable file or directories\n\t\tif not os.path.isfile(dir_path + \"/\" + filename):\n\t\t\tcontinue\n\n\t\t# Remove meta files\n\t\tif filename in [\"__init__.py\", \".gitignore\"]:\n\t\t\tcontinue\n\n\t\t# Add the found file to the list of enabled rubberstamps\n\t\tinstalled_stamps.append(filename.split(\".\")[0])\n\n\tif verbose: print(\"Installed rubberstamps: \" + \", \".join(installed_stamps))\n\n\t# Get the rules defined in the config\n\traw_rules = config.get(\"rubberstamps\", \"stamp_rules\")\n\trules = raw_rules.split(\"\\n\")\n\n\t# Go through the rules one by one\n\tfor rule in rules:\n\t\trule = rule.strip()\n\n\t\tif len(rule) <= 1:\n\t\t\tcontinue\n\n\t\t# Parse the rule with regex\n\t\tregex_result = re.search(\"^(\\w+)\\s+([\\w\\.]+)\\s+([a-z]+)(.*)?$\", rule, re.IGNORECASE)\n\n\t\t# Error out if the regex did not match (invalid line)\n\t\tif not regex_result:\n\t\t\tprint(_(\"Error parsing rubberstamp rule: {}\").format(rule))\n\t\t\tcontinue\n\n\t\ttype = regex_result.group(1)\n\n\t\t# Error out if the stamp name in the rule is not a file\n\t\tif type not in installed_stamps:\n\t\t\tprint(_(\"Stamp not installed: {}\").format(type))\n\t\t\tcontinue\n\n\t\t# Load the module from file\n\t\tmodule = SourceFileLoader(type, dir_path + \"/\" + type + \".py\").load_module()\n\n\t\t# Try to get the class with the same name\n\t\ttry:\n\t\t\tconstructor = getattr(module, type)\n\t\texcept AttributeError:\n\t\t\tprint(_(\"Stamp error: Class {} not found\").format(type))\n\t\t\tcontinue\n\n\t\t# Init the class and set common values\n\t\tinstance = constructor()\n\t\tinstance.verbose = verbose\n\t\tinstance.config = config\n\t\tinstance.gtk_proc = gtk_proc\n\t\tinstance.opencv = opencv\n\n\t\t# Set some opensv shorthands\n\t\tinstance.video_capture = opencv[\"video_capture\"]\n\t\tinstance.face_detector = opencv[\"face_detector\"]\n\t\tinstance.pose_predictor = opencv[\"pose_predictor\"]\n\t\tinstance.clahe = opencv[\"clahe\"]\n\n\t\t# Parse and set the 2 required options for all rubberstamps\n\t\tinstance.options = {\n\t\t\t\"timeout\": float(re.sub(\"[a-zA-Z]\", \"\", regex_result.group(2))),\n\t\t\t\"failsafe\": regex_result.group(3) != \"faildeadly\"\n\t\t}\n\n\t\t# Try to get the class do declare its other config variables\n\t\ttry:\n\t\t\tinstance.declare_config()\n\t\texcept Exception:\n\t\t\tprint(_(\"Internal error in rubberstamp configuration declaration:\"))\n\n\t\t\timport traceback\n\t\t\ttraceback.print_exc()\n\t\t\tcontinue\n\n\t\t# Split the optional arguments at the end of the rule by spaces\n\t\traw_options = regex_result.group(4).split()\n\n\t\t# For each of those aoptional arguments\n\t\tfor option in raw_options:\n\t\t\t# Get the key to the left, and the value to the right of the equal sign\n\t\t\tkey, value = option.split(\"=\")\n\n\t\t\t# Error out if a key has been set that was not declared by the module before\n\t\t\tif key not in instance.options:\n\t\t\t\tprint(\"Unknown config option for rubberstamp \" + type + \": \" + key)\n\t\t\t\tcontinue\n\n\t\t\t# Convert the argument string to an int or float if the declared option has that type\n\t\t\tif isinstance(instance.options[key], int):\n\t\t\t\tvalue = int(value)\n\t\t\telif isinstance(instance.options[key], float):\n\t\t\t\tvalue = float(value)\n\n\t\t\tinstance.options[key] = value\n\n\t\tif verbose:\n\t\t\tprint(\"Stamp \\\"\" + type + \"\\\" options parsed:\")\n\t\t\tprint(instance.options)\n\t\t\tprint(\"Executing stamp\")\n\n\t\t# Make the stamp fail by default\n\t\tresult = False\n\n\t\t# Run the stamp code\n\t\ttry:\n\t\t\tresult = instance.run()\n\t\texcept Exception:\n\t\t\tprint(_(\"Internal error in rubberstamp:\"))\n\n\t\t\timport traceback\n\t\t\ttraceback.print_exc()\n\t\t\tcontinue\n\n\t\tif verbose: print(\"Stamp \\\"\" + type + \"\\\" returned: \" + str(result))\n\n\t\t# Abort authentication if the stamp returned false\n\t\tif result is False:\n\t\t\tif verbose: print(\"Authentication aborted by rubber stamp\")\n\t\t\tsys.exit(15)\n\n\t# This is outside the for loop, so we've run all the rules\n\tif verbose: print(\"All rubberstamps processed, authentication successful\")\n\n\t# Exit with no errors\n\tsys.exit(0)\n"
  },
  {
    "path": "howdy/src/rubberstamps/hotkey.py",
    "content": "import time\nimport sys\n\nfrom i18n import _\n\n# Import the root rubberstamp class\nfrom rubberstamps import RubberStamp\n\n\nclass hotkey(RubberStamp):\n\tpressed_key = \"none\"\n\n\tdef declare_config(self):\n\t\t\"\"\"Set the default values for the optional arguments\"\"\"\n\t\tself.options[\"abort_key\"] = \"esc\"\n\t\tself.options[\"confirm_key\"] = \"enter\"\n\n\tdef run(self):\n\t\t\"\"\"Wait for the user to press a hotkey\"\"\"\n\t\ttime_left = self.options[\"timeout\"]\n\t\ttime_string = _(\"Aborting authorisation in {}\") if self.options[\"failsafe\"] else _(\"Authorising in {}\")\n\n\t\t# Set the ui to default strings\n\t\tself.set_ui_text(time_string.format(int(time_left)), self.UI_TEXT)\n\t\tself.set_ui_text(_(\"Press {abort_key} to abort, {confirm_key} to authorise\").format(abort_key=self.options[\"abort_key\"], confirm_key=self.options[\"confirm_key\"]), self.UI_SUBTEXT)\n\n\t\t# Try to import the keyboard module and tell the user to install the module if that fails\n\t\ttry:\n\t\t\timport keyboard\n\t\texcept Exception:\n\t\t\tprint(\"\\nMissing module for rubber stamp keyboard!\")\n\t\t\tprint(\"Please run:\")\n\t\t\tprint(\"\\t pip3 install keyboard\")\n\t\t\tsys.exit(1)\n\n\t\t# Register hotkeys with the kernel\n\t\tkeyboard.add_hotkey(self.options[\"abort_key\"], self.on_key, args=[\"abort\"])\n\t\tkeyboard.add_hotkey(self.options[\"confirm_key\"], self.on_key, args=[\"confirm\"])\n\n\t\t# While we have not hit our timeout yet\n\t\twhile time_left > 0:\n\t\t\t# Remove 0.1 seconds from the timer, as that's how long we sleep\n\t\t\ttime_left -= 0.1\n\t\t\t# Update the ui with the new time\n\t\t\tself.set_ui_text(time_string.format(str(int(time_left) + 1)), self.UI_TEXT)\n\n\t\t\t# If the abort key was pressed while the loop was sleeping\n\t\t\tif self.pressed_key == \"abort\":\n\t\t\t\t# Set the ui to confirm the abort\n\t\t\t\tself.set_ui_text(_(\"Authentication aborted\"), self.UI_TEXT)\n\t\t\t\tself.set_ui_text(\"\", self.UI_SUBTEXT)\n\n\t\t\t\t# Exit\n\t\t\t\ttime.sleep(1)\n\t\t\t\treturn False\n\n\t\t\t# If confirm has pressed, return that auth can continue\n\t\t\telif self.pressed_key == \"confirm\":\n\t\t\t\treturn True\n\n\t\t\t# If no key has been pressed, wait for a bit and check again\n\t\t\ttime.sleep(0.1)\n\n\t\t# When our timeout hits, either abort or continue based on failsafe of faildeadly\n\t\treturn not self.options[\"failsafe\"]\n\n\tdef on_key(self, type):\n\t\t\"\"\"Called when the user presses a key\"\"\"\n\t\tself.pressed_key = type\n"
  },
  {
    "path": "howdy/src/rubberstamps/nod.py",
    "content": "import time\n\nfrom i18n import _\n\n# Import the root rubberstamp class\nfrom rubberstamps import RubberStamp\n\n\nclass nod(RubberStamp):\n\tdef declare_config(self):\n\t\t\"\"\"Set the default values for the optional arguments\"\"\"\n\t\tself.options[\"min_distance\"] = 6\n\t\tself.options[\"min_directions\"] = 2\n\n\tdef run(self):\n\t\t\"\"\"Track a users nose to see if they nod yes or no\"\"\"\n\t\tself.set_ui_text(_(\"Nod to confirm\"), self.UI_TEXT)\n\t\tself.set_ui_text(_(\"Shake your head to abort\"), self.UI_SUBTEXT)\n\n\t\t# Stores relative distance between the 2 eyes in the last frame\n\t\t# Used to calculate the distance of the nose traveled in relation to face size in the frame\n\t\tlast_reldist = -1\n\t\t# Last point the nose was at\n\t\tlast_nosepoint = {\"x\": -1, \"y\": -1}\n\t\t# Contains booleans recording successful nods and their directions\n\t\trecorded_nods = {\"x\": [], \"y\": []}\n\n\t\tstarttime = time.time()\n\n\t\t# Keep running the loop while we have not hit timeout yet\n\t\twhile time.time() < starttime + self.options[\"timeout\"]:\n\t\t\t# Read a frame from the camera\n\t\t\tret, frame = self.video_capture.read_frame()\n\n\t\t\t# Apply CLAHE to get a better picture\n\t\t\tframe = self.clahe.apply(frame)\n\n\t\t\t# Detect all faces in the frame\n\t\t\tface_locations = self.face_detector(frame, 1)\n\n\t\t\t# Only continue if exactly 1 face is visible in the frame\n\t\t\tif len(face_locations) != 1:\n\t\t\t\tcontinue\n\n\t\t\t# Get the position of the eyes and tip of the nose\n\t\t\tface_landmarks = self.pose_predictor(frame, face_locations[0])\n\n\t\t\t# Calculate the relative distance between the 2 eyes\n\t\t\treldist = face_landmarks.part(0).x - face_landmarks.part(2).x\n\t\t\t# Average this out with the distance found in the last frame to smooth it out\n\t\t\tavg_reldist = (last_reldist + reldist) / 2\n\n\t\t\t# Calculate horizontal movement (shaking head) and vertical movement (nodding)\n\t\t\tfor axis in [\"x\", \"y\"]:\n\t\t\t\t# Get the location of the nose on the active axis\n\t\t\t\tnosepoint = getattr(face_landmarks.part(4), axis)\n\n\t\t\t\t# If this is the first frame set the previous values to the current ones\n\t\t\t\tif last_nosepoint[axis] == -1:\n\t\t\t\t\tlast_nosepoint[axis] = nosepoint\n\t\t\t\t\tlast_reldist = reldist\n\n\t\t\t\tmindist = self.options[\"min_distance\"]\n\t\t\t\t# Get the relative movement by taking the distance traveled and dividing it by eye distance\n\t\t\t\tmovement = (nosepoint - last_nosepoint[axis]) * 100 / max(avg_reldist, 1)\n\n\t\t\t\t# If the movement is over the minimal distance threshold\n\t\t\t\tif movement < -mindist or movement > mindist:\n\t\t\t\t\t# If this is the first recorded nod, add it to the array\n\t\t\t\t\tif len(recorded_nods[axis]) == 0:\n\t\t\t\t\t\trecorded_nods[axis].append(movement < 0)\n\n\t\t\t\t\t# Otherwise, only add this nod if the previous nod with in the other direction\n\t\t\t\t\telif recorded_nods[axis][-1] != (movement < 0):\n\t\t\t\t\t\trecorded_nods[axis].append(movement < 0)\n\n\t\t\t\t# Check if we have nodded enough on this axis\n\t\t\t\tif len(recorded_nods[axis]) >= self.options[\"min_directions\"]:\n\t\t\t\t\t# If nodded yes, show confirmation in ui\n\t\t\t\t\tif (axis == \"y\"):\n\t\t\t\t\t\tself.set_ui_text(_(\"Confirmed authentication\"), self.UI_TEXT)\n\t\t\t\t\t# If shaken no, show abort message\n\t\t\t\t\telse:\n\t\t\t\t\t\tself.set_ui_text(_(\"Aborted authentication\"), self.UI_TEXT)\n\n\t\t\t\t\t# Remove subtext\n\t\t\t\t\tself.set_ui_text(\"\", self.UI_SUBTEXT)\n\n\t\t\t\t\t# Return true for nodding yes and false for shaking no\n\t\t\t\t\ttime.sleep(0.8)\n\t\t\t\t\treturn axis == \"y\"\n\n\t\t\t\t# Save the relative distance and the nosepoint for next loop\n\t\t\t\tlast_reldist = reldist\n\t\t\t\tlast_nosepoint[axis] = nosepoint\n\n\t\t# We've fallen out of the loop, so timeout has been hit\n\t\treturn not self.options[\"failsafe\"]\n"
  },
  {
    "path": "howdy/src/snapshot.py",
    "content": "# Create and save snapshots of auth attempts\n\n# Import modules\nimport cv2\nimport os\nfrom datetime import timezone, datetime\nimport numpy as np\nimport paths_factory\n\n\ndef generate(frames, text_lines):\n\t\"\"\"Generate a snapshot from given frames\"\"\"\n\n\t# Don't execute if no frames were given\n\tif len(frames) == 0:\n\t\treturn\n\n\t# Get frame dimensions\n\tframe_height, frame_width, cc = frames[0].shape\n\t# Spread the given frames out horizontally\n\tsnap = np.concatenate(frames, axis=1)\n\n\t# Create colors\n\tpad_color = [44, 44, 44]\n\ttext_color = [255, 255, 255]\n\n\t# Add a gray square at the bottom of the image\n\tsnap = cv2.copyMakeBorder(snap, 0, len(text_lines) * 20 + 40, 0, 0, cv2.BORDER_CONSTANT, value=pad_color)\n\n\t# Add the Howdy logo if there's space to do so\n\tif len(frames) > 1:\n\t\t# Load the logo from file\n\t\tlogo = cv2.imread(paths_factory.logo_path())\n\t\t# Calculate the position of the logo\n\t\tlogo_y = frame_height + 20\n\t\tlogo_x = frame_width * len(frames) - 210\n\n\t\t# Overlay the logo on top of the image\n\t\tsnap[logo_y:logo_y+57, logo_x:logo_x+180] = logo\n\n\t# Go through each line\n\tline_number = 0\n\tfor line in text_lines:\n\t\t# Calculate how far the line should be from the top\n\t\tpadding_top = frame_height + 30 + (line_number * 20)\n\t\t# Print the line onto the image\n\t\tcv2.putText(snap, line, (30, padding_top), cv2.FONT_HERSHEY_SIMPLEX, .4, text_color, 0, cv2.LINE_AA)\n\n\t\tline_number += 1\n\n\t# Made sure a snapshot folder exist\n\tif not os.path.exists(paths_factory.snapshots_dir_path()):\n\t\tos.makedirs(paths_factory.snapshots_dir_path())\n\n\t# Generate a filename based on the current time\n\tfilename = datetime.now(timezone.utc).strftime(\"%Y%m%dT%H%M%S.jpg\")\n\tfilepath = paths_factory.snapshot_path(filename)\n\t# Write the image to that file\n\tcv2.imwrite(filepath, snap)\n\n\t# Return the saved file location\n\treturn filepath\n"
  },
  {
    "path": "howdy-gtk/bin/howdy-gtk.in",
    "content": "#!/bin/sh\n\n@python_path@ \"@script_path@\" \"$@\""
  },
  {
    "path": "howdy-gtk/debian/changelog",
    "content": "howdy-gtk (0.0.1) xenial; urgency=medium\n\n  * Initial testing release with sticky authentication ui\n\n -- boltgolt <boltgolt@gmail.com>  Thu, 03 Dec 2020 00:08:49 +0200\n"
  },
  {
    "path": "howdy-gtk/debian/compat",
    "content": "10\n"
  },
  {
    "path": "howdy-gtk/debian/control",
    "content": "Source: howdy-gtk\nSection: misc\nPriority: optional\nStandards-Version: 3.9.7\nBuild-Depends: python, dh-python, devscripts, dh-make, debhelper, fakeroot\nMaintainer: boltgolt <boltgolt@gmail.com>\nVcs-Git: https://github.com/boltgolt/howdy\n\nPackage: howdy-gtk\nHomepage: https://github.com/boltgolt/howdy\nArchitecture: all\nDepends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python-gtk2, python-gtk2-dev, cmake\nDescription: Optional UI package for Howdy, written in Gtk\n"
  },
  {
    "path": "howdy-gtk/debian/copyright",
    "content": "MIT License\n\nCopyright (c) 2020 boltgolt\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": "howdy-gtk/debian/howdy-gtk.links",
    "content": "/usr/lib/howdy-gtk/init.py /usr/bin/howdy-gtk\n"
  },
  {
    "path": "howdy-gtk/debian/howdy-gtk.lintian-overrides",
    "content": "# W: Don't require ugly linebreaks in last 5 chars\nhowdy: debian-changelog-line-too-long\n"
  },
  {
    "path": "howdy-gtk/debian/install",
    "content": "src/. /usr/lib/howdy-gtk\n"
  },
  {
    "path": "howdy-gtk/debian/postinst",
    "content": "#!/bin/sh\npip3 install elevate\n"
  },
  {
    "path": "howdy-gtk/debian/rules",
    "content": "#!/usr/bin/make -f\nDH_VERBOSE = 1\n\nDPKG_EXPORT_BUILDFLAGS = 1\ninclude /usr/share/dpkg/default.mk\n\n%:\n\tdh $@\n"
  },
  {
    "path": "howdy-gtk/debian/source/format",
    "content": "3.0 (native)\n"
  },
  {
    "path": "howdy-gtk/debian/source/options",
    "content": "tar-ignore = \".git\"\ntar-ignore = \".gitignore\"\ntar-ignore = \".github\"\ntar-ignore = \"README.md\"\ntar-ignore = \".travis.yml\"\ntar-ignore = \"fedora\"\ntar-ignore = \"opensuse\"\ntar-ignore = \"archlinux\"\n"
  },
  {
    "path": "howdy-gtk/meson.build",
    "content": "if meson.is_subproject()\nproject('howdy-gtk', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\nendif\n\ndatadir = get_option('prefix') / get_option('datadir') / 'howdy-gtk'\npy_conf = configuration_data(paths_dict)\npy_conf.set('data_dir', datadir)\n\n\npy_paths = configure_file(\n    input: 'src/paths.py.in',\n    output: 'paths.py',\n    configuration: py_conf,\n)\n\nsources = files(\n    'src/authsticky.py',\n    'src/i18n.py',\n    'src/init.py',\n    'src/onboarding.py',\n    'src/paths_factory.py',\n    'src/tab_models.py',\n    'src/tab_video.py',\n    'src/window.py',\n)\n\npy = import('python').find_installation(\n    # modules: ['gi', 'elevate']\n)\npy.dependency()\n\nif get_option('install_in_site_packages')\n    pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy-gtk')\nelse\n    pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') / 'howdy-gtk' : join_paths(get_option('prefix'), get_option('libdir'), 'howdy-gtk')\nendif\n\nif get_option('install_in_site_packages')\n    py.install_sources(\n        sources,\n        py_paths,\n        subdir: 'howdy-gtk',\n        install_tag: 'py_sources',\n    )\nelse\n    install_data(\n        sources,\n        py_paths,\n        install_dir: pysourcesinstalldir,\n        install_mode: 'r--r--r--',\n        install_tag: 'py_sources',\n    )\nendif\n\nlogos = files(\n    'src/logo.png',\n    'src/logo_about.png',\n)\ninstall_data(logos, install_dir: datadir)\n\ninterface_files = files(\n    'src/main.glade',\n    'src/onboarding.glade',\n)\ninstall_data(interface_files, install_dir: datadir)\n\ncli_path = join_paths(pysourcesinstalldir, 'init.py')\nconf_data = configuration_data({ 'script_path': cli_path, 'python_path': py.full_path() })\n\nbin_name = 'howdy-gtk'\nbin = configure_file(\n    input: 'bin/howdy-gtk.in',\n    output: bin_name,\n    configuration: conf_data\n)\ninstall_data(\n  bin,\n  install_mode: 'rwxr-xr-x',\n  install_dir: get_option('prefix') / get_option('bindir'),\n  install_tag: 'bin',\n)\n\nif get_option('with_polkit')\n    polkit_config = configure_file(\n        input: 'src/polkit/com.github.boltgolt.howdy-gtk.policy.in',\n        output: 'com.github.boltgolt.howdy-gtk.policy',\n        configuration: {'script_path': cli_path, 'python_path': py.full_path()}\n    )\n    install_data(\n        polkit_config,\n        install_dir: get_option('prefix') / get_option('datadir') / 'polkit-1' / 'actions',\n        install_mode: 'rw-r--r--',\n        install_tag: 'polkit',\n    )\nendif\n"
  },
  {
    "path": "howdy-gtk/src/authsticky.py",
    "content": "# Shows a floating window when authenticating\nimport cairo\nimport gi\nimport signal\nimport sys\nimport paths_factory\nimport os\n\nfrom i18n import _\n\n# Make sure we have the libs we need\ngi.require_version(\"Gtk\", \"3.0\")\ngi.require_version(\"Gdk\", \"3.0\")\n\n# Import them\nfrom gi.repository import Gtk as gtk\nfrom gi.repository import Gdk as gdk\nfrom gi.repository import GObject as gobject\n\n# Set window size constants\nwindowWidth = 400\nwindowHeight = 100\n\n\nclass StickyWindow(gtk.Window):\n\t# Set default messages to show in the popup\n\tmessage = _(\"Loading...  \")\n\tsubtext = \"\"\n\n\tdef __init__(self):\n\t\t\"\"\"Initialize the sticky window\"\"\"\n\t\t# Make the class a GTK window\n\t\tgtk.Window.__init__(self)\n\n\t\t# Get the absolute or relative path to the logo file\n\t\tlogo_path = paths_factory.logo_path()\n\n\t\t# Create image and calculate scale size based on image size\n\t\tself.logo_surface = cairo.ImageSurface.create_from_png(logo_path)\n\t\tself.logo_ratio = float(windowHeight - 20) / float(self.logo_surface.get_height())\n\n\t\t# Set the title of the window\n\t\tself.set_title(_(\"Howdy Authentication\"))\n\n\t\t# Set a bunch of options to make the window stick and be on top of everything\n\t\tself.stick()\n\t\tself.set_gravity(gdk.Gravity.STATIC)\n\t\tself.set_resizable(False)\n\t\tself.set_keep_above(True)\n\t\tself.set_app_paintable(True)\n\t\tself.set_skip_pager_hint(True)\n\t\tself.set_skip_taskbar_hint(True)\n\t\tself.set_can_focus(False)\n\t\tself.set_can_default(False)\n\t\tself.set_focus(None)\n\t\tself.set_type_hint(gdk.WindowTypeHint.NOTIFICATION)\n\t\tself.set_decorated(False)\n\n\t\t# Listen for a window redraw\n\t\tself.connect(\"draw\", self.draw)\n\t\t# Listen for a force close or click event and exit\n\t\tself.connect(\"destroy\", self.exit)\n\t\tself.connect(\"delete_event\", self.exit)\n\t\tself.connect(\"button-press-event\", self.exit)\n\t\tself.connect(\"button-release-event\", self.exit)\n\n\t\t# Create a GDK drawing, restricts the window size\n\t\tdarea = gtk.DrawingArea()\n\t\tdarea.set_size_request(windowWidth, windowHeight)\n\t\tself.add(darea)\n\n\t\t# Get the default screen\n\t\tscreen = gdk.Screen.get_default()\n\t\tvisual = screen.get_rgba_visual()\n\t\tself.set_visual(visual)\n\n\t\t# Move the window to the center top of the default window, where a webcam usually is\n\t\tself.move((screen.get_width() / 2) - (windowWidth / 2), 0)\n\n\t\t# Show window and force a resize again\n\t\tself.show_all()\n\t\tself.resize(windowWidth, windowHeight)\n\n\t\t# Add a timeout to catch input passed from compare.py\n\t\tgobject.timeout_add(100, self.catch_stdin)\n\n\t\t# Start GTK main loop\n\t\tgtk.main()\n\n\tdef draw(self, widget, ctx):\n\t\t\"\"\"Draw the UI\"\"\"\n\t\t# Change cursor to the kill icon\n\t\tself.get_window().set_cursor(gdk.Cursor(gdk.CursorType.PIRATE))\n\n\t\t# Draw a semi transparent background\n\t\tctx.set_source_rgba(0, 0, 0, .7)\n\t\tctx.set_operator(cairo.OPERATOR_SOURCE)\n\t\tctx.paint()\n\t\tctx.set_operator(cairo.OPERATOR_OVER)\n\n\t\t# Position and draw the logo\n\t\tctx.translate(15, 10)\n\t\tctx.scale(self.logo_ratio, self.logo_ratio)\n\t\tctx.set_source_surface(self.logo_surface)\n\t\tctx.paint()\n\n\t\t# Calculate main message positioning, as the text is higher if there's a subtext\n\t\tif self.subtext:\n\t\t\tctx.move_to(380, 145)\n\t\telse:\n\t\t\tctx.move_to(380, 175)\n\n\t\t# Draw the main message\n\t\tctx.set_source_rgba(255, 255, 255, .9)\n\t\tctx.set_font_size(80)\n\t\tctx.select_font_face(\"Arial\", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\n\t\tctx.show_text(self.message)\n\n\t\t# Draw the subtext if there is one\n\t\tif self.subtext:\n\t\t\tctx.move_to(380, 210)\n\t\t\tctx.set_source_rgba(230, 230, 230, .8)\n\t\t\tctx.set_font_size(40)\n\t\t\tctx.select_font_face(\"Arial\", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\n\t\t\tctx.show_text(self.subtext)\n\n\tdef catch_stdin(self):\n\t\t\"\"\"Catch input from stdin and redraw\"\"\"\n\t\t# Wait for a line on stdin\n\t\tcomm = sys.stdin.readline()[:-1]\n\n\t\t# If the line is not empty\n\t\tif comm:\n\t\t\t# Parse a message\n\t\t\tif comm[0] == \"M\":\n\t\t\t\tself.message = comm[2:].strip()\n\t\t\t# Parse subtext\n\t\t\tif comm[0] == \"S\":\n\t\t\t\t# self.subtext += \" \"\n\t\t\t\tself.subtext = comm[2:].strip()\n\n\t\t# Redraw the ui\n\t\tself.queue_draw()\n\n\t\t# Fire this function again in 10ms, as we're waiting on IO in readline anyway\n\t\tgobject.timeout_add(10, self.catch_stdin)\n\n\tdef exit(self, widget, context):\n\t\t\"\"\"Cleanly exit\"\"\"\n\t\tgtk.main_quit()\n\t\treturn True\n\n\n# Make sure we quit on a SIGINT\nsignal.signal(signal.SIGINT, signal.SIG_DFL)\n\n# Open the GTK window\nwindow = StickyWindow()\n"
  },
  {
    "path": "howdy-gtk/src/i18n.py",
    "content": "# Support file for translations\n\n# Import modules\nimport gettext\nimport os\n\n# Get the right translation based on locale, falling back to base if none found\ntranslation = gettext.translation(\"gtk\", localedir=os.path.join(os.path.dirname(__file__), \"locales\"), fallback=True)\ntranslation.install()\n\n# Export translation function as _\n_ = translation.gettext\n"
  },
  {
    "path": "howdy-gtk/src/init.py",
    "content": "# Opens auth ui if requested, otherwise starts normal ui\nimport sys\n\nif \"--start-auth-ui\" in sys.argv:\n\timport authsticky\nelse:\n\timport window\n"
  },
  {
    "path": "howdy-gtk/src/main.glade",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Generated with glade 3.18.3 -->\n<interface domain=\"howdy\">\n  <requires lib=\"gtk+\" version=\"3.10\"/>\n  <object class=\"GtkImage\" id=\"iconadd\">\n    <property name=\"visible\">True</property>\n    <property name=\"can_focus\">False</property>\n    <property name=\"margin_right\">5</property>\n    <property name=\"stock\">gtk-add</property>\n  </object>\n  <object class=\"GtkImage\" id=\"iconadduser\">\n    <property name=\"visible\">True</property>\n    <property name=\"can_focus\">False</property>\n    <property name=\"margin_right\">5</property>\n    <property name=\"stock\">gtk-add</property>\n  </object>\n  <object class=\"GtkImage\" id=\"icondelete\">\n    <property name=\"visible\">True</property>\n    <property name=\"can_focus\">False</property>\n    <property name=\"margin_right\">5</property>\n    <property name=\"stock\">gtk-delete</property>\n  </object>\n  <object class=\"GtkWindow\" id=\"mainwindow\">\n    <property name=\"can_focus\">False</property>\n    <property name=\"margin_top\">5</property>\n    <property name=\"title\" translatable=\"yes\" context=\"Window title\">Howdy Configuration</property>\n    <property name=\"window_position\">center</property>\n    <property name=\"icon\">logo.png</property>\n    <child>\n      <object class=\"GtkNotebook\" id=\"notebook\">\n        <property name=\"visible\">True</property>\n        <property name=\"can_focus\">True</property>\n        <property name=\"margin_left\">2</property>\n        <property name=\"tab_pos\">left</property>\n        <property name=\"show_border\">False</property>\n        <signal name=\"switch-page\" handler=\"on_page_switch\" swapped=\"no\"/>\n        <child>\n          <object class=\"GtkBox\" id=\"box3\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkBox\" id=\"box4\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_left\">10</property>\n                <property name=\"margin_right\">10</property>\n                <property name=\"margin_top\">10</property>\n                <property name=\"spacing\">5</property>\n                <child>\n                  <object class=\"GtkLabel\" id=\"userlabel\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"valign\">center</property>\n                    <property name=\"label\" translatable=\"yes\">Showing saved models for:</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">False</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkComboBoxText\" id=\"userlist\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <signal name=\"changed\" handler=\"on_user_change\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">False</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"adduserbutton\">\n                    <property name=\"label\" translatable=\"yes\">Add new user</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">True</property>\n                    <property name=\"receives_default\">True</property>\n                    <property name=\"margin_left\">15</property>\n                    <property name=\"image\">iconadduser</property>\n                    <property name=\"relief\">none</property>\n                    <property name=\"always_show_image\">True</property>\n                    <signal name=\"clicked\" handler=\"on_user_add\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">False</property>\n                    <property name=\"pack_type\">end</property>\n                    <property name=\"position\">3</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"modellistbox\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_left\">10</property>\n                <property name=\"margin_right\">10</property>\n                <property name=\"orientation\">vertical</property>\n                <child>\n                  <object class=\"GtkSeparator\" id=\"separator1\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"margin_top\">10</property>\n                    <property name=\"margin_bottom\">10</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"box1\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_right\">10</property>\n                <property name=\"margin_top\">8</property>\n                <property name=\"margin_bottom\">10</property>\n                <child>\n                  <placeholder/>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"addbutton\">\n                    <property name=\"label\" translatable=\"yes\">Add</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">True</property>\n                    <property name=\"receives_default\">True</property>\n                    <property name=\"image\">iconadd</property>\n                    <property name=\"xalign\">0.5899999737739563</property>\n                    <property name=\"always_show_image\">True</property>\n                    <signal name=\"clicked\" handler=\"on_model_add\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"pack_type\">end</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"deletebutton\">\n                    <property name=\"label\" translatable=\"yes\">Delete</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">True</property>\n                    <property name=\"receives_default\">True</property>\n                    <property name=\"margin_right\">11</property>\n                    <property name=\"image\">icondelete</property>\n                    <property name=\"always_show_image\">True</property>\n                    <signal name=\"clicked\" handler=\"on_model_delete\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"pack_type\">end</property>\n                    <property name=\"position\">2</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"pack_type\">end</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n        </child>\n        <child type=\"tab\">\n          <object class=\"GtkLabel\" id=\"modelstab\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"margin_left\">10</property>\n            <property name=\"margin_right\">10</property>\n            <property name=\"label\" translatable=\"yes\">Models</property>\n          </object>\n          <packing>\n            <property name=\"position\">1</property>\n            <property name=\"tab_fill\">False</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"box2\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"margin_left\">10</property>\n            <property name=\"margin_right\">10</property>\n            <property name=\"margin_top\">10</property>\n            <property name=\"margin_bottom\">10</property>\n            <child>\n              <object class=\"GtkBox\" id=\"box7\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_left\">10</property>\n                <property name=\"orientation\">vertical</property>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label1\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"label\" translatable=\"yes\">Camera ID:</property>\n                    <attributes>\n                      <attribute name=\"weight\" value=\"bold\"/>\n                    </attributes>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"videoid\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"selectable\">True</property>\n                    <property name=\"ellipsize\">end</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label6\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"margin_top\">10</property>\n                    <property name=\"label\" translatable=\"yes\">Real resolution:</property>\n                    <attributes>\n                      <attribute name=\"weight\" value=\"bold\"/>\n                    </attributes>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">2</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"videores\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"selectable\">True</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">3</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label7\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"margin_top\">10</property>\n                    <property name=\"label\" translatable=\"yes\">Used resolution:</property>\n                    <attributes>\n                      <attribute name=\"weight\" value=\"bold\"/>\n                    </attributes>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">4</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"videoresused\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"selectable\">True</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">5</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label8\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"margin_top\">10</property>\n                    <property name=\"label\" translatable=\"yes\">Recorder:</property>\n                    <attributes>\n                      <attribute name=\"weight\" value=\"bold\"/>\n                    </attributes>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">6</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"videorecorder\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"halign\">start</property>\n                    <property name=\"selectable\">True</property>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">7</property>\n                  </packing>\n                </child>\n                <child>\n                  <placeholder/>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"pack_type\">end</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkEventBox\" id=\"opencvbox\">\n                <property name=\"width_request\">300</property>\n                <property name=\"height_request\">300</property>\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <child>\n                  <object class=\"GtkImage\" id=\"opencvimage\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"stock\">gtk-execute</property>\n                    <property name=\"icon_size\">6</property>\n                  </object>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">True</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"position\">1</property>\n          </packing>\n        </child>\n        <child type=\"tab\">\n          <object class=\"GtkLabel\" id=\"videotab\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"label\" translatable=\"yes\">Video</property>\n          </object>\n          <packing>\n            <property name=\"position\">1</property>\n            <property name=\"tab_fill\">False</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"box5\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"halign\">center</property>\n            <property name=\"valign\">center</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkImage\" id=\"image1\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_bottom\">20</property>\n                <property name=\"xpad\">12</property>\n                <property name=\"pixbuf\">logo_about.png</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label2\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"label\" translatable=\"yes\">Howdy</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label3\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"margin_top\">5</property>\n                <property name=\"label\" translatable=\"yes\">Facial authentication for Linux</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"box6\">\n                <property name=\"visible\">True</property>\n                <property name=\"can_focus\">False</property>\n                <property name=\"halign\">center</property>\n                <property name=\"valign\">center</property>\n                <property name=\"margin_top\">15</property>\n                <property name=\"margin_bottom\">35</property>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label4\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"margin_right\">3</property>\n                    <property name=\"label\" translatable=\"yes\">&lt;a href=\"https://github.com/boltgolt/howdy\"&gt;Open GitHub link&lt;/a&gt; </property>\n                    <property name=\"use_markup\">True</property>\n                    <property name=\"track_visited_links\">False</property>\n                    <attributes>\n                      <attribute name=\"underline\" value=\"True\"/>\n                      <attribute name=\"foreground\" value=\"#2929ababe2e2\"/>\n                      <attribute name=\"underline-color\" value=\"#2929ababe2e2\"/>\n                    </attributes>\n                    <signal name=\"activate-link\" handler=\"on_about_link\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\" id=\"label5\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can_focus\">False</property>\n                    <property name=\"margin_left\">3</property>\n                    <property name=\"label\" translatable=\"yes\">&lt;a href=\"https://www.buymeacoffee.com/boltgolt\"&gt;Donate to the project&lt;/a&gt; </property>\n                    <property name=\"use_markup\">True</property>\n                    <attributes>\n                      <attribute name=\"underline\" value=\"True\"/>\n                      <attribute name=\"foreground\" value=\"#2929ababe2e2\"/>\n                      <attribute name=\"underline-color\" value=\"#2929ababe2e2\"/>\n                    </attributes>\n                    <signal name=\"activate-link\" handler=\"on_about_link\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">2</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">3</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"position\">2</property>\n          </packing>\n        </child>\n        <child type=\"tab\">\n          <object class=\"GtkLabel\" id=\"abouttab\">\n            <property name=\"visible\">True</property>\n            <property name=\"can_focus\">False</property>\n            <property name=\"label\" translatable=\"yes\">About</property>\n          </object>\n          <packing>\n            <property name=\"position\">2</property>\n            <property name=\"tab_fill\">False</property>\n          </packing>\n        </child>\n        <child>\n          <placeholder/>\n        </child>\n        <child type=\"tab\">\n          <placeholder/>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "howdy-gtk/src/onboarding.glade",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Generated with glade 3.38.2 -->\n<interface>\n  <requires lib=\"gtk+\" version=\"3.12\"/>\n  <object class=\"GtkImage\" id=\"iconcancel\">\n    <property name=\"visible\">True</property>\n    <property name=\"can-focus\">False</property>\n    <property name=\"margin-right\">4</property>\n    <property name=\"stock\">gtk-cancel</property>\n  </object>\n  <object class=\"GtkImage\" id=\"iconfinish\">\n    <property name=\"visible\">True</property>\n    <property name=\"can-focus\">False</property>\n    <property name=\"margin-right\">5</property>\n    <property name=\"stock\">gtk-apply</property>\n  </object>\n  <object class=\"GtkImage\" id=\"iconforward\">\n    <property name=\"visible\">True</property>\n    <property name=\"can-focus\">False</property>\n    <property name=\"margin-left\">4</property>\n    <property name=\"stock\">gtk-go-forward</property>\n  </object>\n  <object class=\"GtkImage\" id=\"iconscan\">\n    <property name=\"visible\">True</property>\n    <property name=\"can-focus\">False</property>\n    <property name=\"margin-left\">5</property>\n    <property name=\"stock\">gtk-media-play</property>\n  </object>\n  <object class=\"GtkWindow\" id=\"onboardingwindow\">\n    <property name=\"width-request\">500</property>\n    <property name=\"height-request\">400</property>\n    <property name=\"can-focus\">False</property>\n    <property name=\"title\" translatable=\"yes\">Welcome to Howdy</property>\n    <property name=\"window-position\">center</property>\n    <property name=\"icon\">logo.png</property>\n    <property name=\"type-hint\">menu</property>\n    <property name=\"gravity\">center</property>\n    <child>\n      <object class=\"GtkBox\" id=\"slidecontainer\">\n        <property name=\"visible\">True</property>\n        <property name=\"can-focus\">False</property>\n        <property name=\"resize-mode\">immediate</property>\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkBox\" id=\"slide6\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label9\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Setup is done!</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label10\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">20</property>\n                <property name=\"label\" translatable=\"yes\">We're done! Howdy is now active on this computer. Try doing anything you would normally have to type your password for to authenticate, like running a command with sudo.\n\nYou can open the Howdy Configurator later on to change more advanced settings or add additional models. Press Finish below to close this window and open the Howdy Configurator. </property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">0</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide5\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label11\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Setting a certainty policy</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label12\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">40</property>\n                <property name=\"label\" translatable=\"yes\">Because of changes in angles, distance, and other factors a face match is never exactly the same as the stored face model. On this page you can set how strict Howdy should be.</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"box2\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">60</property>\n                <property name=\"margin-right\">60</property>\n                <property name=\"orientation\">vertical</property>\n                <child>\n                  <object class=\"GtkRadioButton\" id=\"radiofast\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"receives-default\">False</property>\n                    <property name=\"margin-bottom\">10</property>\n                    <property name=\"xalign\">0</property>\n                    <property name=\"yalign\">0.5099999904632568</property>\n                    <property name=\"active\">True</property>\n                    <property name=\"draw-indicator\">True</property>\n                    <property name=\"group\">radiobalanced</property>\n                    <child>\n                      <object class=\"GtkBox\" id=\"box3\">\n                        <property name=\"visible\">True</property>\n                        <property name=\"can-focus\">False</property>\n                        <property name=\"margin-left\">5</property>\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label13\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">Fast</property>\n                            <attributes>\n                              <attribute name=\"weight\" value=\"bold\"/>\n                            </attributes>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">True</property>\n                            <property name=\"position\">0</property>\n                          </packing>\n                        </child>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label14\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">Allows more fuzzy matches, \nbut speeds up the scanning process greatly.</property>\n                            <property name=\"wrap\">True</property>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">True</property>\n                            <property name=\"position\">1</property>\n                          </packing>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkRadioButton\" id=\"radiobalanced\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"receives-default\">False</property>\n                    <property name=\"margin-bottom\">10</property>\n                    <property name=\"xalign\">0</property>\n                    <property name=\"active\">True</property>\n                    <property name=\"draw-indicator\">True</property>\n                    <child>\n                      <object class=\"GtkBox\" id=\"box4\">\n                        <property name=\"visible\">True</property>\n                        <property name=\"can-focus\">False</property>\n                        <property name=\"margin-left\">5</property>\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label15\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">Balanced</property>\n                            <attributes>\n                              <attribute name=\"weight\" value=\"bold\"/>\n                            </attributes>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">True</property>\n                            <property name=\"position\">0</property>\n                          </packing>\n                        </child>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label16\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">Still relatively quick detection, \nbut might not log you in when further away.</property>\n                            <property name=\"wrap\">True</property>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">True</property>\n                            <property name=\"position\">1</property>\n                          </packing>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkRadioButton\" id=\"radiosecure\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"receives-default\">False</property>\n                    <property name=\"xalign\">0</property>\n                    <property name=\"active\">True</property>\n                    <property name=\"draw-indicator\">True</property>\n                    <property name=\"group\">radiobalanced</property>\n                    <child>\n                      <object class=\"GtkBox\" id=\"box5\">\n                        <property name=\"visible\">True</property>\n                        <property name=\"can-focus\">False</property>\n                        <property name=\"margin-left\">5</property>\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label17\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">Secure</property>\n                            <attributes>\n                              <attribute name=\"weight\" value=\"bold\"/>\n                            </attributes>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">False</property>\n                            <property name=\"position\">0</property>\n                          </packing>\n                        </child>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label18\">\n                            <property name=\"visible\">True</property>\n                            <property name=\"can-focus\">False</property>\n                            <property name=\"halign\">start</property>\n                            <property name=\"label\" translatable=\"yes\">The slightly safer option, \nbut will take much longer to authenticate you</property>\n                            <property name=\"wrap\">True</property>\n                          </object>\n                          <packing>\n                            <property name=\"expand\">False</property>\n                            <property name=\"fill\">False</property>\n                            <property name=\"position\">1</property>\n                          </packing>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">2</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">1</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide4\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label4\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Adding a face model</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label5\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">20</property>\n                <property name=\"label\" translatable=\"yes\">To authenticate you Howdy needs to save a model of your face to recognise you. Press the Scan button below to start the facial scan.</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"box1\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <child>\n                  <placeholder/>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"scanbutton\">\n                    <property name=\"label\" translatable=\"yes\">Start face scan</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"has-focus\">True</property>\n                    <property name=\"is-focus\">True</property>\n                    <property name=\"receives-default\">True</property>\n                    <property name=\"margin-left\">50</property>\n                    <property name=\"margin-right\">50</property>\n                    <property name=\"image\">iconscan</property>\n                    <property name=\"relief\">none</property>\n                    <property name=\"image-position\">right</property>\n                    <property name=\"always-show-image\">True</property>\n                    <signal name=\"clicked\" handler=\"on_scanbutton_click\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">True</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n                <child>\n                  <placeholder/>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">2</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide3\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label19\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Configuring webcam</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"leiestatus\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">20</property>\n                <property name=\"label\" translatable=\"yes\">Is the infrared emitter flashing ?</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n                <property name=\"track-visited-links\">False</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkButtonBox\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"layout-style\">spread</property>\n                <child>\n                  <object class=\"GtkButton\" id=\"leieyesbutton\">\n                    <property name=\"label\" translatable=\"yes\">yes</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"receives-default\">True</property>\n                    <signal name=\"clicked\" handler=\"slide3_button_yes\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">True</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"leienobutton\">\n                    <property name=\"label\" translatable=\"yes\">no</property>\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">True</property>\n                    <property name=\"receives-default\">True</property>\n                    <signal name=\"clicked\" handler=\"slide3_button_no\" swapped=\"no\"/>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">True</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">1</property>\n                  </packing>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">3</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide2\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label2\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Configuring webcam</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label3\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">20</property>\n                <property name=\"label\" translatable=\"yes\">Howdy will search your system automatically for any available cameras, so make sure your webcam is connected. After detection a list of usable webcams will be shown. Pick the one you want to use and click Next.</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"devicelistbox\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"opacity\">0.89</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"orientation\">vertical</property>\n                <child>\n                  <object class=\"GtkLabel\" id=\"loadinglabel\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">False</property>\n                    <property name=\"margin-top\">15</property>\n                    <property name=\"label\" translatable=\"yes\">Testing your webcams, please wait...</property>\n                    <attributes>\n                      <attribute name=\"style\" value=\"italic\"/>\n                    </attributes>\n                  </object>\n                  <packing>\n                    <property name=\"expand\">False</property>\n                    <property name=\"fill\">True</property>\n                    <property name=\"position\">0</property>\n                  </packing>\n                </child>\n                <child>\n                  <placeholder/>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">4</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide1\">\n            <property name=\"can-focus\">False</property>\n            <property name=\"no-show-all\">True</property>\n            <property name=\"margin-bottom\">10</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label8\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"label\" translatable=\"yes\">Downloading data files</property>\n                <attributes>\n                  <attribute name=\"weight\" value=\"bold\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label1\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"margin-bottom\">20</property>\n                <property name=\"label\" translatable=\"yes\">Howdy needs three pre trained facial recognition datasets to be able to recognise you, which will be downloaded now. You can see the download progress below.</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkEventBox\" id=\"downloadeventbox\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <child>\n                  <object class=\"GtkLabel\" id=\"downloadoutputlabel\">\n                    <property name=\"visible\">True</property>\n                    <property name=\"can-focus\">False</property>\n                    <property name=\"margin-left\">10</property>\n                    <property name=\"margin-right\">10</property>\n                    <property name=\"margin-top\">10</property>\n                    <property name=\"label\" translatable=\"yes\">Starting download...</property>\n                    <property name=\"justify\">center</property>\n                    <attributes>\n                      <attribute name=\"foreground\" value=\"#f3f3f3f3f3f3\"/>\n                      <attribute name=\"background\" value=\"#000000000000\"/>\n                    </attributes>\n                  </object>\n                </child>\n              </object>\n              <packing>\n                <property name=\"expand\">True</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">3</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">True</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">5</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"slide0\">\n            <property name=\"visible\">True</property>\n            <property name=\"can-focus\">False</property>\n            <property name=\"orientation\">vertical</property>\n            <child>\n              <object class=\"GtkImage\" id=\"image2\">\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">20</property>\n                <property name=\"margin-bottom\">10</property>\n                <property name=\"xpad\">7</property>\n                <property name=\"ypad\">13</property>\n                <property name=\"pixbuf\">logo_about.png</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label6\">\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"margin-top\">5</property>\n                <property name=\"label\" translatable=\"yes\">Welcome to Howdy!</property>\n                <attributes>\n                  <attribute name=\"scale\" value=\"2\"/>\n                </attributes>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">1</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label7\">\n                <property name=\"width-request\">100</property>\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">False</property>\n                <property name=\"halign\">center</property>\n                <property name=\"valign\">center</property>\n                <property name=\"margin-left\">20</property>\n                <property name=\"margin-right\">20</property>\n                <property name=\"margin-top\">10</property>\n                <property name=\"label\" translatable=\"yes\">This wizard will walk you through the setup process and automatically configure Howdy for you. Press next to continue.</property>\n                <property name=\"justify\">center</property>\n                <property name=\"wrap\">True</property>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"position\">6</property>\n          </packing>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"navigationbar\">\n            <property name=\"visible\">True</property>\n            <property name=\"can-focus\">False</property>\n            <property name=\"margin-bottom\">10</property>\n            <child>\n              <object class=\"GtkButton\" id=\"cancelbutton\">\n                <property name=\"label\" translatable=\"yes\">Cancel</property>\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">True</property>\n                <property name=\"receives-default\">True</property>\n                <property name=\"margin-left\">10</property>\n                <property name=\"image\">iconcancel</property>\n                <property name=\"always-show-image\">True</property>\n                <signal name=\"clicked\" handler=\"exit\" swapped=\"no\"/>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"position\">0</property>\n              </packing>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"nextbutton\">\n                <property name=\"label\" translatable=\"yes\">Next</property>\n                <property name=\"visible\">True</property>\n                <property name=\"can-focus\">True</property>\n                <property name=\"has-focus\">True</property>\n                <property name=\"is-focus\">True</property>\n                <property name=\"receives-default\">True</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"image\">iconforward</property>\n                <property name=\"relief\">none</property>\n                <property name=\"image-position\">right</property>\n                <property name=\"always-show-image\">True</property>\n                <signal name=\"clicked\" handler=\"go_next_slide\" swapped=\"no\"/>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"pack-type\">end</property>\n                <property name=\"position\">2</property>\n              </packing>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"finishbutton\">\n                <property name=\"label\" translatable=\"yes\">Finish setup</property>\n                <property name=\"can-focus\">True</property>\n                <property name=\"receives-default\">True</property>\n                <property name=\"no-show-all\">True</property>\n                <property name=\"margin-right\">10</property>\n                <property name=\"image\">iconfinish</property>\n                <property name=\"relief\">none</property>\n                <property name=\"always-show-image\">True</property>\n                <signal name=\"clicked\" handler=\"exit\" swapped=\"no\"/>\n              </object>\n              <packing>\n                <property name=\"expand\">False</property>\n                <property name=\"fill\">True</property>\n                <property name=\"pack-type\">end</property>\n                <property name=\"position\">3</property>\n              </packing>\n            </child>\n          </object>\n          <packing>\n            <property name=\"expand\">False</property>\n            <property name=\"fill\">True</property>\n            <property name=\"pack-type\">end</property>\n            <property name=\"position\">9</property>\n          </packing>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "howdy-gtk/src/onboarding.py",
    "content": "import sys\nimport os\nimport re\nimport time\nimport subprocess\nimport paths_factory\n\nfrom i18n import _\n\nfrom gi.repository import Gtk as gtk\nfrom gi.repository import Gdk as gdk\nfrom gi.repository import GObject as gobject\nfrom gi.repository import Pango as pango\n\n\nclass OnboardingWindow(gtk.Window):\n\tdef __init__(self):\n\t\t\"\"\"Initialize the sticky window\"\"\"\n\t\t# Make the class a GTK window\n\t\tgtk.Window.__init__(self)\n\n\t\tself.builder = gtk.Builder()\n\t\tself.builder.add_from_file(paths_factory.onboarding_wireframe_path())\n\t\tself.builder.connect_signals(self)\n\n\t\tself.window = self.builder.get_object(\"onboardingwindow\")\n\t\tself.slidecontainer = self.builder.get_object(\"slidecontainer\")\n\t\tself.nextbutton = self.builder.get_object(\"nextbutton\")\n\n\t\tself.window.connect(\"destroy\", self.exit)\n\t\tself.window.connect(\"delete_event\", self.exit)\n\n\t\tself.slides = [\n\t\t\tself.builder.get_object(\"slide0\"),\n\t\t\tself.builder.get_object(\"slide1\"),\n\t\t\tself.builder.get_object(\"slide2\"),\n\t\t\tself.builder.get_object(\"slide3\"),\n\t\t\tself.builder.get_object(\"slide4\"),\n\t\t\tself.builder.get_object(\"slide5\"),\n\t\t\tself.builder.get_object(\"slide6\")\n\t\t]\n\n\t\tself.window.show_all()\n\t\tself.window.resize(500, 400)\n\n\t\tself.window.current_slide = 0\n\n\t\t# Start GTK main loop\n\t\tgtk.main()\n\n\tdef go_next_slide(self, button=None):\n\t\tself.nextbutton.set_sensitive(False)\n\n\t\tself.slides[self.window.current_slide].hide()\n\t\tself.slides[self.window.current_slide + 1].show()\n\t\tself.window.current_slide += 1\n\t\t# the shown child may have zero/wrong dimensions\n\t\tself.slidecontainer.queue_resize()\n\n\t\tif self.window.current_slide == 1:\n\t\t\tself.execute_slide1()\n\t\telif self.window.current_slide == 2:\n\t\t\tgobject.timeout_add(10, self.execute_slide2)\n\t\telif self.window.current_slide == 3:\n\t\t\tself.execute_slide3()\n\t\telif self.window.current_slide == 4:\n\t\t\tself.execute_slide4()\n\t\telif self.window.current_slide == 5:\n\t\t\tself.execute_slide5()\n\t\telif self.window.current_slide == 6:\n\t\t\tself.execute_slide6()\n\n\tdef execute_slide1(self):\n\t\tself.downloadoutputlabel = self.builder.get_object(\"downloadoutputlabel\")\n\t\teventbox = self.builder.get_object(\"downloadeventbox\")\n\t\teventbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0))\n\n\t\t# TODO: Better way to do this?\n\t\tif os.path.exists(paths_factory.dlib_data_dir_path() / \"shape_predictor_5_face_landmarks.dat\"):\n\t\t\tself.downloadoutputlabel.set_text(_(\"Datafiles have already been downloaded!\\nClick Next to continue\"))\n\t\t\tself.enable_next()\n\t\t\treturn\n\n\t\tself.proc = subprocess.Popen(\"./install.sh\", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=paths_factory.dlib_data_dir_path())\n\n\t\tself.download_lines = []\n\t\tself.read_download_line()\n\n\tdef read_download_line(self):\n\t\tline = self.proc.stdout.readline()\n\t\tself.download_lines.append(line.decode(\"utf-8\"))\n\n\t\tprint(\"install.sh output:\")\n\t\tprint(line.decode(\"utf-8\"))\n\n\t\tif len(self.download_lines) > 10:\n\t\t\tself.download_lines.pop(0)\n\n\t\tself.downloadoutputlabel.set_text(\" \".join(self.download_lines))\n\n\t\tif line:\n\t\t\tgobject.timeout_add(10, self.read_download_line)\n\t\t\treturn\n\n\t\t# Wait for the process to finish and check the status code\n\t\tif self.proc.wait(5) != 0:\n\t\t\tself.show_error(_(\"Error while downloading datafiles\"), \" \".join(self.download_lines))\n\n\t\tself.downloadoutputlabel.set_text(_(\"Done!\\nClick Next to continue\"))\n\t\tself.enable_next()\n\n\tdef execute_slide2(self):\n\t\tdef is_gray(frame):\n\t\t\tfor row in frame:\n\t\t\t\tfor pixel in row:\n\t\t\t\t\tif not pixel[0] == pixel[1] == pixel[2]:\n\t\t\t\t\t\treturn False\n\t\t\treturn True\n\n\t\ttry:\n\t\t\timport cv2\n\t\texcept Exception:\n\t\t\tself.show_error(_(\"Error while importing OpenCV2\"), _(\"Try reinstalling cv2\"))\n\n\t\tdevice_rows = []\n\t\ttry:\n\t\t\tdevice_ids = os.listdir(\"/dev/v4l/by-path\")\n\t\texcept Exception:\n\t\t\tself.show_error(_(\"No webcams found on system\"), _(\"Please configure your camera yourself if you are sure a compatible camera is connected\"))\n\n\t\t# Loop though all devices\n\t\tfor dev in device_ids:\n\t\t\ttime.sleep(.5)\n\n\t\t\t# The full path to the device is the default name\n\t\t\tdevice_path = \"/dev/v4l/by-path/\" + dev\n\t\t\tdevice_name = dev\n\n\t\t\t# Get the udevadm details to try to get a better name\n\t\t\tudevadm = subprocess.check_output([\"udevadm info -r --query=all -n \" + device_path], shell=True).decode(\"utf-8\")\n\n\t\t\t# Loop though udevadm to search for a better name\n\t\t\tfor line in udevadm.split(\"\\n\"):\n\t\t\t\t# Match it and encase it in quotes\n\t\t\t\tre_name = re.search('product.*=(.*)$', line, re.IGNORECASE)\n\t\t\t\tif re_name:\n\t\t\t\t\tdevice_name = re_name.group(1)\n\n\t\t\tcapture = cv2.VideoCapture(device_path)\n\t\t\tis_open, frame = capture.read()\n\t\t\tif not is_open:\n\t\t\t\tdevice_rows.append([device_name, device_path, -9, _(\"No, camera can't be opened\")])\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tif not is_gray(frame):\n\t\t\t\t\traise Exception()\n\t\t\texcept Exception:\n\t\t\t\tdevice_rows.append([device_name, device_path, -5, _(\"No, not an infrared camera\")])\n\t\t\t\tcapture.release()\n\t\t\t\tcontinue\n\n\t\t\tdevice_rows.append([device_name, device_path, 5, _(\"Yes, compatible infrared camera\")])\n\t\t\tcapture.release()\n\n\t\tdevice_rows = sorted(device_rows, key=lambda k: -k[2])\n\n\t\tself.loadinglabel = self.builder.get_object(\"loadinglabel\")\n\t\tself.devicelistbox = self.builder.get_object(\"devicelistbox\")\n\n\t\tself.treeview = gtk.TreeView()\n\t\tself.treeview.set_vexpand(True)\n\n\t\t# Set the columns\n\t\tfor i, column in enumerate([_(\"Camera identifier or path\"), _(\"Recommended\")]):\n\t\t\tcell = gtk.CellRendererText()\n\t\t\tcell.set_property(\"ellipsize\", pango.EllipsizeMode.END)\n\t\t\tcol = gtk.TreeViewColumn(column, cell, text=i)\n\t\t\tself.treeview.append_column(col)\n\n\t\t# Add the treeview\n\t\tself.devicelistbox.add(self.treeview)\n\n\t\t# Create a datamodel\n\t\tself.listmodel = gtk.ListStore(str, str, str, bool)\n\n\t\tfor device in device_rows:\n\t\t\tis_gray = device[2] == 5\n\t\t\tself.listmodel.append([device[0], device[3], device[1], is_gray])\n\n\t\tself.treeview.set_model(self.listmodel)\n\t\tself.treeview.set_cursor(0)\n\n\t\tself.treeview.show()\n\t\tself.loadinglabel.hide()\n\t\tself.enable_next()\n\n\tdef execute_slide3(self):\n\t\ttry:\n\t\t\timport cv2\n\t\texcept Exception:\n\t\t\tself.show_error(_(\"Error while importing OpenCV2\"), _(\"Try reinstalling cv2\"))\n\n\t\tselection = self.treeview.get_selection()\n\t\t(listmodel, rowlist) = selection.get_selected_rows()\n\t\t\n\t\tif len(rowlist) != 1:\n\t\t\tself.show_error(_(\"Error selecting camera\"))\n   \n\t\tdevice_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2)\n\t\tis_gray = listmodel.get_value(listmodel.get_iter(rowlist[0]), 3)\n\n\t\tif is_gray:\n\t\t\t# test if linux-enable-ir-emitter help should be displayed, \n\t\t\t# the user must click on the yes/no button which calls the method slide3_button_yes|no\n\t\t\tself.capture = cv2.VideoCapture(device_path)\n\t\t\tif not self.capture.isOpened():\n\t\t\t\tself.show_error(_(\"The selected camera cannot be opened\"), _(\"Try to select another one\"))\n\t\t\tself.capture.read()\n\t\telse:  \n\t\t\t# skip, the selected camera is not infrared\n\t\t\tself.go_next_slide()\n\n\tdef slide3_button_yes(self, button):\n\t\tself.capture.release()\n\t\tself.go_next_slide()\n\n\tdef slide3_button_no(self, button):\n\t\tself.capture.release()\n\t\tself.builder.get_object(\"leiestatus\").set_markup(_(\"Please visit\\n<a href=\\\"https://github.com/EmixamPP/linux-enable-ir-emitter\\\">https://github.com/EmixamPP/linux-enable-ir-emitter</a>\\nto enable your ir emitter\"))\n\t\tself.builder.get_object(\"leieyesbutton\").hide()\n\t\tself.builder.get_object(\"leienobutton\").hide()\n\n\tdef execute_slide4(self):\n\t\tselection = self.treeview.get_selection()\n\t\t(listmodel, rowlist) = selection.get_selected_rows()\n\n\t\tif len(rowlist) != 1:\n\t\t\tself.show_error(_(\"Error selecting camera\"))\n\n\t\tdevice_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2)\n\t\tself.proc = subprocess.Popen(\"howdy set device_path \" + device_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n\n\t\tself.window.set_focus(self.builder.get_object(\"scanbutton\"))\n\n\tdef on_scanbutton_click(self, button):\n\t\tstatus = self.proc.wait(2)\n\n\t\t# if status != 0:\n\t\t# \tself.show_error(_(\"Error setting camera path\"), _(\"Please set the camera path manually\"))\n\n\t\tself.dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL)\n\t\tself.dialog.set_title(_(\"Creating Model\"))\n\t\tself.dialog.props.text = _(\"Please look directly into the camera\")\n\t\tself.dialog.show_all()\n\n\t\t# Wait a bit to allow the user to read the dialog\n\t\tgobject.timeout_add(600, self.run_add)\n\n\tdef run_add(self):\n\t\tstatus, output = subprocess.getstatusoutput([\"howdy add -y\"])\n\n\t\tprint(\"howdy add output:\")\n\t\tprint(output)\n\n\t\tself.dialog.destroy()\n\n\t\tif status != 0:\n\t\t\tself.show_error(_(\"Can't save face model\"), output)\n\n\t\tgobject.timeout_add(10, self.go_next_slide)\n\n\tdef execute_slide5(self):\n\t\tself.enable_next()\n\n\tdef execute_slide6(self):\n\t\tradio_buttons = self.builder.get_object(\"radiobalanced\").get_group()\n\t\tradio_selected = False\n\t\tradio_certanty = 5.0\n\n\t\tfor button in radio_buttons:\n\t\t\tif button.get_active():\n\t\t\t\tradio_selected = gtk.Buildable.get_name(button)\n\n\t\tif not radio_selected:\n\t\t\tself.show_error(_(\"Error reading radio buttons\"))\n\t\telif radio_selected == \"radiofast\":\n\t\t\tradio_certanty = 4.2\n\t\telif radio_selected == \"radiobalanced\":\n\t\t\tradio_certanty = 3.5\n\t\telif radio_selected == \"radiosecure\":\n\t\t\tradio_certanty = 2.2\n\n\t\tself.proc = subprocess.Popen(\"howdy set certainty \" + str(radio_certanty), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n\n\t\tself.nextbutton.hide()\n\t\tself.builder.get_object(\"cancelbutton\").hide()\n\n\t\tfinishbutton = self.builder.get_object(\"finishbutton\")\n\t\tfinishbutton.show()\n\t\tself.window.set_focus(finishbutton)\n\n\t\tstatus = self.proc.wait(2)\n\n\t\tif status != 0:\n\t\t\tself.show_error(_(\"Error setting certainty\"), _(\"Certainty is set to the default value, Howdy setup is complete\"))\n\n\tdef enable_next(self):\n\t\tself.nextbutton.set_sensitive(True)\n\t\tself.window.set_focus(self.nextbutton)\n\n\tdef show_error(self, error, secon=\"\"):\n\t\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE)\n\t\tdialog.set_title(_(\"Howdy Error\"))\n\t\tdialog.props.text = error\n\t\tdialog.format_secondary_text(secon)\n\n\t\tdialog.run()\n\n\t\tdialog.destroy()\n\t\tself.exit()\n\n\tdef exit(self, widget=None, context=None):\n\t\t\"\"\"Cleanly exit\"\"\"\n\t\tgtk.main_quit()\n\t\tsys.exit(0)\n"
  },
  {
    "path": "howdy-gtk/src/paths.py.in",
    "content": "from pathlib import PurePath\n\n# Define the absolute path to the config directory\nconfig_dir = PurePath(\"@config_dir@\")\n\n# Define the absolute path to the DLib models data directory\ndlib_data_dir = PurePath(\"@dlib_data_dir@\")\n\n# Define the absolute path to the Howdy user models directory\nuser_models_dir = PurePath(\"@user_models_dir@\")\n\n# Define the absolute path to the Howdy data directory\ndata_dir = PurePath(\"@data_dir@\")"
  },
  {
    "path": "howdy-gtk/src/paths_factory.py",
    "content": "from pathlib import PurePath\nimport paths\n\n\ndef config_file_path() -> str:\n    \"\"\"Return the path to the config file\"\"\"\n    return str(paths.config_dir / \"config.ini\")\n\n\ndef user_models_dir_path() -> PurePath:\n    \"\"\"Return the path to the user models directory\"\"\"\n    return paths.user_models_dir\n\n\ndef logo_path() -> str:\n    \"\"\"Return the path to the logo file\"\"\"\n    return str(paths.data_dir / \"logo.png\")\n\n\ndef onboarding_wireframe_path() -> str:\n    \"\"\"Return the path to the onboarding wireframe file\"\"\"\n    return str(paths.data_dir / \"onboarding.glade\")\n\n\ndef main_window_wireframe_path() -> str:\n    \"\"\"Return the path to the main window wireframe file\"\"\"\n    return str(paths.data_dir / \"main.glade\")\n\n\ndef dlib_data_dir_path() -> PurePath:\n    \"\"\"Return the path to the dlib data directory\"\"\"\n    return paths.dlib_data_dir\n"
  },
  {
    "path": "howdy-gtk/src/polkit/com.github.boltgolt.howdy-gtk.policy.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE policyconfig PUBLIC \"-//freedesktop//DTD polkit Policy Configuration 1.0//EN\"\n  \"https://specifications.freedesktop.org/PolicyKit/1.0/policyconfig.dtd\">\n<policyconfig>\n  <vendor>boltgolt</vendor>\n  <vendor_url>https://github.com/boltgolt/howdy</vendor_url>\n  <icon_name>howdy-gtk</icon_name>\n  <action id=\"com.github.boltgolt.howdy-gtk\">\n    <description>Howdy interface</description>\n    <message>Authentication is required to run howdy-gtk</message>\n    <defaults>\n      <allow_any>no</allow_any>\n      <allow_inactive>no</allow_inactive>\n      <allow_active>auth_admin</allow_active>\n    </defaults>\n    <annotate key=\"org.freedesktop.policykit.exec.path\">@python_path@</annotate>\n    <annotate key=\"org.freedesktop.policykit.exec.argv1\">@script_path@</annotate>\n    <annotate key=\"org.freedesktop.policykit.exec.allow_gui\">true</annotate>\n  </action>\n</policyconfig>"
  },
  {
    "path": "howdy-gtk/src/tab_models.py",
    "content": "import subprocess\nimport time\n\nfrom i18n import _\nfrom gi.repository import Gtk as gtk\nfrom gi.repository import GObject as gobject\n\n\ndef on_user_change(self, select):\n\tself.active_user = select.get_active_text()\n\tself.load_model_list()\n\n\ndef on_user_add(self, button):\n\t# Open question dialog\n\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL)\n\tdialog.set_title(_(\"Confirm User Creation\"))\n\tdialog.props.text = _(\"Please enter the username of the user you want to add to Howdy\")\n\n\t# Create the input field\n\tentry = gtk.Entry()\n\n\t# Add a label to ask for a model name\n\thbox = gtk.HBox()\n\thbox.pack_start(gtk.Label(_(\"Username:\")), False, 5, 5)\n\thbox.pack_end(entry, True, True, 5)\n\n\t# Add the box and show the dialog\n\tdialog.vbox.pack_end(hbox, True, True, 0)\n\tdialog.show_all()\n\n\t# Show dialog\n\tresponse = dialog.run()\n\n\tentered_user = entry.get_text()\n\tdialog.destroy()\n\n\tif response == gtk.ResponseType.OK:\n\t\tself.userlist.append_text(entered_user)\n\t\tself.userlist.set_active(self.userlist.items)\n\t\tself.userlist.items += 1\n\n\t\tself.active_user = entered_user\n\t\tself.load_model_list()\n\n\ndef on_model_add(self, button):\n\tif self.userlist.items == 0:\n\t\treturn\n\t# Open question dialog\n\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL)\n\tdialog.set_title(_(\"Confirm Model Creation\"))\n\tdialog.props.text = _(\"Please enter a name for the new model, 24 characters max\")\n\n\t# Create the input field\n\tentry = gtk.Entry()\n\n\t# Add a label to ask for a model name\n\thbox = gtk.HBox()\n\thbox.pack_start(gtk.Label(_(\"Model name:\")), False, 5, 5)\n\thbox.pack_end(entry, True, True, 5)\n\n\t# Add the box and show the dialog\n\tdialog.vbox.pack_end(hbox, True, True, 0)\n\tdialog.show_all()\n\n\t# Show dialog\n\tresponse = dialog.run()\n\n\tentered_name = entry.get_text()\n\tdialog.destroy()\n\n\tif response == gtk.ResponseType.OK:\n\t\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, buttons=gtk.ButtonsType.NONE)\n\t\tdialog.set_title(_(\"Creating Model\"))\n\t\tdialog.props.text = _(\"Please look directly into the camera\")\n\t\tdialog.show_all()\n\n\t\t# Wait a bit to allow the user to read the dialog\n\t\tgobject.timeout_add(600, lambda: execute_add(self, dialog, entered_name))\n\n\ndef execute_add(box, dialog, entered_name):\n\n\tstatus, output = subprocess.getstatusoutput([\"howdy add '\" + entered_name + \"' -y -U \" + box.active_user])\n\n\tdialog.destroy()\n\n\tif status != 0:\n\t\tdialog = gtk.MessageDialog(parent=box, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE)\n\t\tdialog.set_title(_(\"Howdy Error\"))\n\t\tdialog.props.text = _(\"Error while adding model, error code {}: \\n\\n\").format(str(status))\n\t\tdialog.format_secondary_text(output)\n\t\tdialog.run()\n\t\tdialog.destroy()\n\n\tbox.load_model_list()\n\ndef on_model_delete(self, button):\n\tselection = self.treeview.get_selection()\n\t(listmodel, rowlist) = selection.get_selected_rows()\n\n\tif len(rowlist) == 1:\n\t\tid = listmodel.get_value(listmodel.get_iter(rowlist[0]), 0)\n\t\tname = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2)\n\n\t\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, buttons=gtk.ButtonsType.OK_CANCEL)\n\t\tdialog.set_title(_(\"Confirm Model Deletion\"))\n\t\tdialog.props.text = _(\"Are you sure you want to delete model {id} ({name})?\").format(id=id, name=name)\n\t\tresponse = dialog.run()\n\t\tdialog.destroy()\n\n\t\tif response == gtk.ResponseType.OK:\n\t\t\tstatus, output = subprocess.getstatusoutput([\"howdy remove \" + id + \" -y -U \" + self.active_user])\n\n\t\t\tif status != 0:\n\t\t\t\tdialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE)\n\t\t\t\tdialog.set_title(_(\"Howdy Error\"))\n\t\t\t\tdialog.props.text = _(\"Error while deleting model, error code {}: \\n\\n\").format(status)\n\t\t\t\tdialog.format_secondary_text(output)\n\t\t\t\tdialog.run()\n\t\t\t\tdialog.destroy()\n\n\t\t\tself.load_model_list()\n"
  },
  {
    "path": "howdy-gtk/src/tab_video.py",
    "content": "import configparser\n\nfrom i18n import _\nimport paths_factory\n\nfrom gi.repository import Gtk as gtk\nfrom gi.repository import Gdk as gdk\nfrom gi.repository import GdkPixbuf as pixbuf\nfrom gi.repository import GObject as gobject\n\nMAX_HEIGHT = 300\nMAX_WIDTH = 300\n\n\ndef on_page_switch(self, notebook, page, page_num):\n\tif page_num == 1:\n\n\t\ttry:\n\t\t\tself.config = configparser.ConfigParser()\n\t\t\tself.config.read(paths_factory.config_file_path())\n\t\texcept Exception:\n\t\t\tprint(_(\"Can't open camera\"))\n\n\t\tpath = self.config.get(\"video\", \"device_path\")\n\n\t\ttry:\n\t\t\t# if not self.cv2:\n\t\t\timport cv2\n\t\t\tself.cv2 = cv2\n\t\texcept Exception:\n\t\t\tprint(_(\"Can't import OpenCV2\"))\n\n\t\ttry:\n\t\t\tself.capture = cv2.VideoCapture(path)\n\t\texcept Exception:\n\t\t\tprint(_(\"Can't open camera\"))\n\n\t\topencvbox = self.builder.get_object(\"opencvbox\")\n\t\topencvbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0))\n\n\t\theight = self.capture.get(self.cv2.CAP_PROP_FRAME_HEIGHT) or 1\n\t\twidth = self.capture.get(self.cv2.CAP_PROP_FRAME_WIDTH) or 1\n\n\t\tself.scaling_factor = (MAX_HEIGHT / height) or 1\n\n\t\tif width * self.scaling_factor > MAX_WIDTH:\n\t\t\tself.scaling_factor = (MAX_WIDTH / width) or 1\n\n\t\tconfig_height = self.config.getfloat(\"video\", \"max_height\", fallback=320.0)\n\t\tconfig_scaling = (config_height / height) or 1\n\n\t\tself.builder.get_object(\"videoid\").set_text(path.split(\"/\")[-1])\n\t\tself.builder.get_object(\"videores\").set_text(str(int(width)) + \"x\" + str(int(height)))\n\t\tself.builder.get_object(\"videoresused\").set_text(str(int(width * config_scaling)) + \"x\" + str(int(height * config_scaling)))\n\t\tself.builder.get_object(\"videorecorder\").set_text(self.config.get(\"video\", \"recording_plugin\", fallback=_(\"Unknown\")))\n\n\t\tgobject.timeout_add(10, self.capture_frame)\n\n\telif self.capture is not None:\n\t\tself.capture.release()\n\t\tself.capture = None\n\n\ndef capture_frame(self):\n\tif self.capture is None:\n\t\treturn\n\n\tret, frame = self.capture.read()\n\n\tframe = self.cv2.resize(frame, None, fx=self.scaling_factor, fy=self.scaling_factor, interpolation=self.cv2.INTER_AREA)\n\n\tretval, buffer = self.cv2.imencode(\".png\", frame)\n\n\tloader = pixbuf.PixbufLoader()\n\tloader.write(buffer)\n\tloader.close()\n\tbuffer = loader.get_pixbuf()\n\n\tself.opencvimage.set_from_pixbuf(buffer)\n\n\tgobject.timeout_add(20, self.capture_frame)\n"
  },
  {
    "path": "howdy-gtk/src/window.py",
    "content": "# Opens and controls main ui window\nimport gi\nimport signal\nimport sys\nimport os\nimport elevate\nimport subprocess\n\nfrom i18n import _\nimport paths_factory\n\n# Make sure we have the libs we need\ngi.require_version(\"Gtk\", \"3.0\")\ngi.require_version(\"Gdk\", \"3.0\")\n\n# Import them\nfrom gi.repository import Gtk as gtk\n\n\nclass MainWindow(gtk.Window):\n\tdef __init__(self):\n\t\t\"\"\"Initialize the sticky window\"\"\"\n\t\t# Make the class a GTK window\n\t\tgtk.Window.__init__(self)\n\n\t\tself.builder = gtk.Builder()\n\t\tself.builder.add_from_file(paths_factory.main_window_wireframe_path())\n\t\tself.builder.connect_signals(self)\n\n\t\tself.window = self.builder.get_object(\"mainwindow\")\n\t\tself.userlist = self.builder.get_object(\"userlist\")\n\t\tself.modellistbox = self.builder.get_object(\"modellistbox\")\n\t\tself.opencvimage = self.builder.get_object(\"opencvimage\")\n\n\t\tself.window.connect(\"destroy\", self.exit)\n\t\tself.window.connect(\"delete_event\", self.exit)\n\n\t\t# Init capture for video tab\n\t\tself.capture = None\n\n\t\t# Create a treeview that will list the model data\n\t\tself.treeview = gtk.TreeView()\n\t\tself.treeview.set_vexpand(True)\n\n\t\t# Set the columns\n\t\tfor i, column in enumerate([_(\"ID\"), _(\"Created\"), _(\"Label\")]):\n\t\t\tcol = gtk.TreeViewColumn(column, gtk.CellRendererText(), text=i)\n\t\t\tself.treeview.append_column(col)\n\n\t\t# Add the treeview\n\t\tself.modellistbox.add(self.treeview)\n\n\t\tfilelist = os.listdir(paths_factory.user_models_dir_path())\n\t\tself.active_user = \"\"\n\n\t\tself.userlist.items = 0\n\n\t\tfor file in filelist:\n\t\t\tself.userlist.append_text(file[:-4])\n\t\t\tself.userlist.items += 1\n\n\t\t\tif not self.active_user:\n\t\t\t\tself.active_user = file[:-4]\n\n\t\tself.userlist.set_active(0)\n\n\t\tself.window.show_all()\n\n\t\t# Start GTK main loop\n\t\tgtk.main()\n\n\tdef load_model_list(self):\n\t\t\"\"\"(Re)load the model list\"\"\"\n\n\t\t# Get username and default to none if there are no models at all yet\n\t\tuser = 'none'\n\t\tif self.active_user: user = self.active_user\n\n\t\t# Execute the list command to get the models\n\t\tstatus, output = subprocess.getstatusoutput([\"howdy list --plain -U \" + user])\n\n\t\t# Create a datamodel\n\t\tself.listmodel = gtk.ListStore(str, str, str)\n\n\t\t# If there was no error\n\t\tif status == 0:\n\t\t\t# Split the output per line\n\t\t\tlines = output.split(\"\\n\")\n\n\t\t\t# Add the models to the datamodel\n\t\t\tfor i in range(len(lines)):\n\t\t\t\titems = lines[i].split(\",\")\n\t\t\t\tif len(items) < 3: continue\n\t\t\t\tself.listmodel.append(items)\n\n\t\tself.treeview.set_model(self.listmodel)\n\n\tdef on_about_link(self, label, uri):\n\t\t\"\"\"Open links on about page as a non-root user\"\"\"\n\t\ttry:\n\t\t\tuser = os.getlogin()\n\t\texcept Exception:\n\t\t\tuser = os.environ.get(\"SUDO_USER\")\n\n\t\tstatus, output = subprocess.getstatusoutput([\"sudo -u \" + user + \" timeout 10 xdg-open \" + uri])\n\t\treturn True\n\n\tdef exit(self, widget=None, context=None):\n\t\t\"\"\"Cleanly exit\"\"\"\n\t\tif self.capture is not None:\n\t\t\tself.capture.release()\n\n\t\tgtk.main_quit()\n\t\tsys.exit(0)\n\n\n# Make sure we quit on a SIGINT\nsignal.signal(signal.SIGINT, signal.SIG_DFL)\n\n# Make sure we run as sudo\nelevate.elevate()\n\n# If no models have been created yet or when it is forced, start the onboarding\nif \"--force-onboarding\" in sys.argv or not os.path.exists(paths_factory.user_models_dir_path()):\n\timport onboarding\n\tonboarding.OnboardingWindow()\n\n\tsys.exit(0)\n\n# Class is split so it isn't too long, import split functions\nimport tab_models\nMainWindow.on_user_add = tab_models.on_user_add\nMainWindow.on_user_change = tab_models.on_user_change\nMainWindow.on_model_add = tab_models.on_model_add\nMainWindow.on_model_delete = tab_models.on_model_delete\nimport tab_video\nMainWindow.on_page_switch = tab_video.on_page_switch\nMainWindow.capture_frame = tab_video.capture_frame\n\n# Open the GTK window\nwindow = MainWindow()\n"
  },
  {
    "path": "meson.build",
    "content": "project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\n\ndlibdatadir = get_option('dlib_data_dir') != '' ? get_option('dlib_data_dir') : join_paths(get_option('prefix'), get_option('datadir'), 'dlib-data')\nconfdir = get_option('config_dir') != '' ? get_option('config_dir') : join_paths(get_option('prefix'), get_option('sysconfdir'), 'howdy')\nusermodelsdir = get_option('user_models_dir') != '' ? get_option('user_models_dir') : join_paths(confdir, 'models')\nlogpath = get_option('log_path')\npythonpath = get_option('python_path')\n\nconfig_path = join_paths(confdir, 'config.ini')\n\npaths_dict = {\n    'config_dir': confdir,\n    'dlib_data_dir': dlibdatadir,\n    'user_models_dir': usermodelsdir,\n    'log_path': logpath,\n    'python_path': pythonpath\n}\n\n# We need to keep this order beause howdy-gtk defines the gtk script path which is used later in howdy\nsubdir('howdy-gtk')\nsubdir('howdy')"
  },
  {
    "path": "meson.options",
    "content": "option('pam_dir', type: 'string', value: '', description: 'Set the pam_howdy destination directory')\n#option('fetch_dlib_data', type: 'boolean', value: false, description: 'Download dlib data files')\noption('config_dir', type: 'string', value: '', description: 'Set the howdy config directory')\noption('dlib_data_dir', type: 'string', value: '', description: 'Set the dlib data directory')\noption('user_models_dir', type: 'string', value: '', description: 'Set the user models directory')\noption('log_path', type: 'string', value: '/var/log/howdy', description: 'Set the log file path')\noption('install_in_site_packages', type: 'boolean', value: false, description: 'Install howdy python files in site packages')\noption('py_sources_dir', type: 'string', value: '', description: 'Set the python sources directory')\noption('install_pam_config', type: 'boolean', value: false, description: 'Install pam config file (for Debian/Ubuntu)')\noption('python_path', type: 'string', value: '/usr/bin/python', description: 'Set the path to the python executable')\noption('with_polkit', type: 'boolean', value: false, description: 'Install polkit policy config file')\n"
  }
]