Initial commit
This commit is contained in:
commit
558b00793e
163
.gitignore
vendored
Normal file
163
.gitignore
vendored
Normal file
@ -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
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
16
README.md
Normal file
16
README.md
Normal file
@ -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
|
||||
```
|
4
config.ini.sample
Normal file
4
config.ini.sample
Normal file
@ -0,0 +1,4 @@
|
||||
[contrib_scribe]
|
||||
repo_path = ~/path/to/repo
|
||||
message = hi there
|
||||
commit_count = 3
|
342
contrib_scribe.py
Executable file
342
contrib_scribe.py
Executable file
@ -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 <config.ini>", 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:]))
|
6
contrib_scribe_test.py
Normal file
6
contrib_scribe_test.py
Normal file
@ -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'
|
1
requirements-dev.txt
Normal file
1
requirements-dev.txt
Normal file
@ -0,0 +1 @@
|
||||
pytest
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
appdirs
|
26
scripts/install.sh
Normal file
26
scripts/install.sh
Normal file
@ -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 <<EOF
|
||||
[Unit]
|
||||
Description=Your contribution heatmap is a canvas for creativity.
|
||||
|
||||
[Service]
|
||||
ExecStart=$(pwd)/contrib_scribe.py $(pwd)/config.ini
|
||||
EOF
|
||||
|
||||
sudo tee /usr/lib/systemd/user/$service_name.timer &>/dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Your contribution heatmap is a canvas for creativity.
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl enable --user contrib-writer.timer --now
|
10
scripts/uninstall.sh
Executable file
10
scripts/uninstall.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deletes related created systemd timer & service.
|
||||
|
||||
service_name=contrib-writer
|
||||
|
||||
systemctl disable --user $service_name.timer
|
||||
systemctl stop --user $service_name.timer
|
||||
|
||||
sudo rm /usr/lib/systemd/user/$service_name.service
|
||||
sudo rm /usr/lib/systemd/user/$service_name.timer
|
Loading…
x
Reference in New Issue
Block a user