commit 558b00793eb3838969b143eba939b667ace2e51f Author: strNophix Date: Sat Oct 29 18:39:03 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bd54c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Application +config.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f52af49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 niku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..db56952 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# contrib-scribe +Your contribution heatmap is a canvas for creativity. + +## Prerequisites +- systemd +- python (>=3.10) +- git + +## Usage +```sh +# Start +./scripts/install.sh + +# Stop +./scripts/uninstall.sh +``` diff --git a/config.ini.sample b/config.ini.sample new file mode 100644 index 0000000..4a6e632 --- /dev/null +++ b/config.ini.sample @@ -0,0 +1,4 @@ +[contrib_scribe] +repo_path = ~/path/to/repo +message = hi there +commit_count = 3 diff --git a/contrib_scribe.py b/contrib_scribe.py new file mode 100755 index 0000000..b44c871 --- /dev/null +++ b/contrib_scribe.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +import base64 +import configparser +import typing +import sys +import pathlib +from datetime import date, timedelta +import subprocess as sub +import appdirs + +CharRepr = typing.List[typing.List[typing.Literal[0] | typing.Literal[1]]] + +CHAR_SPACING = 1 +FONT_MAP: typing.Mapping[str, CharRepr] = { + "A": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + ], + "B": [ + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0], + ], + "C": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 0, 0, 1], + [0, 1, 1, 0], + ], + "D": [ + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0], + ], + "E": [ + [1, 1, 1], + [1, 0, 0], + [1, 0, 0], + [1, 1, 1], + [1, 0, 0], + [1, 0, 0], + [1, 1, 1], + ], + "F": [ + [1, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 0, 0, 0], + ], + "G": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 0], + [1, 0, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 1, 1], + ], + "H": [ + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + ], + "I": [ + [1], + [1], + [1], + [1], + [1], + [1], + [1], + ], + "J": [ + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + ], + "K": [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 1, 0], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + ], + "L": [ + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 1, 1], + ], + "M": [ + [0, 1, 0, 1, 0], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + ], + "N": [ + [1, 1, 0, 0, 1], + [1, 1, 0, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 0, 1, 1], + [1, 0, 0, 1, 1], + ], + "O": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 1, 0], + ], + "P": [ + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 0, 0, 0], + ], + "Q": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 1, 1], + [0, 1, 1, 1], + ], + "R": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + ], + "S": [ + [0, 1, 1, 0], + [1, 0, 0, 1], + [1, 0, 0, 0], + [0, 1, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 1, 0], + ], + "T": [ + [1, 1, 1], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + ], + "U": [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 1, 1], + ], + "V": [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], + ], + "W": [ + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 1], + [0, 1, 0, 1, 0], + ], + "X": [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + ], + "Y": [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 1, 1], + [0, 0, 1], + [0, 0, 1], + [1, 1, 1], + ], + "Z": [ + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [1, 0, 0, 0], + [1, 1, 1, 1], + ], + " ": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + ], +} + + +def message_to_matrix(message: str) -> CharRepr: + matrix: CharRepr = [[] for _ in range(7)] + + for char in message: + crepr = FONT_MAP.get(char) + if not crepr: + raise ValueError(f'No repr for "{char}" found in font mapping.') + for i in range(7): + matrix[i].extend(crepr[i]) + matrix[i].extend([0] * CHAR_SPACING) + + return matrix + + +def print_char_matrix(matrix: CharRepr): + fill = {0: " ", 1: "█"} + for row in matrix: + print("".join(fill[char] for char in row)) + + print(f"\nWidth: {len(matrix[0])}") + + +def parse_config(path: str) -> typing.Mapping[str, str]: + parser = configparser.ConfigParser() + parser.read(path) + return parser["contrib_scribe"] + + +def parse_state(path: pathlib.Path) -> date: + if not path.exists(): + state = date.today() + if (weekday := state.weekday()) != 6: + state += timedelta(days=6 - weekday) + with path.open("w") as fp: + fp.write(str(state)) + return state + + with path.open("r") as fp: + return date.fromisoformat(fp.read()) + + +def main(args: typing.Sequence[str]) -> int: + if len(args) < 1: + print("Usage: contrib_scribe.py ", file=sys.stderr) + return 1 + + cfg = parse_config(args[0]) + cfg_id = base64.b32encode(bytearray(args[0], "ascii")).decode("utf-8") + + data_dir = appdirs.user_data_dir("contrib-scribe") + data_path = pathlib.Path(data_dir) + data_path.mkdir(parents=True, exist_ok=True) + + state_path = data_path / cfg_id + start_date = parse_state(state_path) + + message = cfg["message"].upper() + matrix = message_to_matrix(message) + + print_char_matrix(matrix) + + day_diff = (date.today() - start_date).days + row, column = day_diff % 7, day_diff // 7 + if matrix[row][column] == 0: + print("No commit's to do today!") + return 0 + + file_path = pathlib.Path(cfg["repo_path"]) / "test" + commit_count = int(cfg["commit_count"]) + for i in range(commit_count): + file_path.write_text(str(i)) + commands = [["git", "add", "."], ["git", "commit", "-m", str(i)]] + for command in commands: + sub.call(command, cwd=file_path.parent) + + sub.call(["git", "push"], cwd=file_path.parent) + print("Did daily commit") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/contrib_scribe_test.py b/contrib_scribe_test.py new file mode 100644 index 0000000..6e6e8b7 --- /dev/null +++ b/contrib_scribe_test.py @@ -0,0 +1,6 @@ +import contrib_scribe + + +def test_font_size(): + for char, crepr in contrib_scribe.FONT.items(): + assert len(crepr) == 7, f'"{char}" is taller than 7 pixels' diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d64bc32 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +appdirs diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..bf27777 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Sets up the necessary systemd timer & service. + +service_name=contrib-writer + +sudo tee /usr/lib/systemd/user/$service_name.service &>/dev/null </dev/null <