init commit

This commit is contained in:
Oystein Kristoffer Tveit 2021-06-16 14:21:15 +02:00
commit 4ad365a2b4
9 changed files with 318 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Alarmed
The alarmed daemon.
Lightweight package for keeping track of alarms that run custom commands when they go off.

23
setup.py Normal file
View File

@ -0,0 +1,23 @@
from setuptools import setup
with open("README", 'r') as f:
long_description = f.read()
setup(
name = 'alarmed',
version = '1.0',
description = 'A tool to keep track of alarms, and make them execute commands',
license = 'MIT',
long_description = long_description,
author ='h7x4',
author_email ='h7x4abk3g@protonmail.com',
url ="https://www.github.com/h7x4ABk3g/alarmed",
packages = ['alarmed'],
install_requires = ['xdg', 'python-daemon'],
entry_points={
'console_scripts': [
'alarme = src.alarme:main',
'alarmed = src.alarmed:main',
]
},
)

47
src/Alarm.py Normal file
View File

@ -0,0 +1,47 @@
from datetime import datetime
from json import dump, load
from uuid import uuid4
from os import walk, path
from common import PREVIOUS_ALARM_DIR, ALARM_DIR, list_files
class Alarm:
def __init__(self, timestamp, comment=None, command=None, id=None):
self.timestamp = timestamp
self.comment = comment
self.command = command
self.id = id or self.generateId()
def __str__(self):
return str(self.__dict__)
@classmethod
def readFromFile(cls, path):
with open(path) as file:
data = load(file)
alarm = cls(data['timestamp'], data['comment'], data['command'], data['id'])
return alarm
def writeToFile(self, path):
with open(path, 'w') as file:
dump(self.__dict__, file)
def generateId(self):
alarm_files = list_files(ALARM_DIR)
prev_alarm_files = list_files(PREVIOUS_ALARM_DIR)
i = 1
while hex(i)[2:].upper() in alarm_files + prev_alarm_files:
i += 1
return hex(i)[2:].upper()
def getTimeLeft(self):
timeleft = datetime.fromtimestamp(self.timestamp) - datetime.now()
hs = str(timeleft.seconds // 3600).zfill(2)
ms = str((timeleft.seconds // 60) % 60).zfill(2)
ss = str(timeleft.seconds % 60).zfill(2)
if timeleft.days:
return f'{timeleft.days} days, {hs}:{ms}:{ss}'
else:
return f'{hs}:{ms}:{ss}'

0
src/__init__.py Normal file
View File

138
src/alarme.py Normal file
View File

@ -0,0 +1,138 @@
import argparse
from datetime import date, time, datetime, timedelta
from os import walk, path, replace
from pathlib import Path
from Alarm import Alarm
from common import ALARM_DIR, PREVIOUS_ALARM_DIR, print_error, list_files
def get_time_str(file):
return str(file)
def print_alarms(args):
files = list_files(ALARM_DIR)
if files == []:
print('No alarms')
return
if args.id:
filepath = path.join(ALARM_DIR, args.id)
if not path.exists(filepath):
print_error(f'Alarm with id {args.id} does not exist')
alarms = [Alarm.readFromFile(filepath)]
else:
alarms = [Alarm.readFromFile(path.join(ALARM_DIR, file)) for file in files]
print(args.sep.join(alarm.getTimeLeft() for alarm in alarms))
##### Time input parsers
def parse_clock_time(t): # ex. "-t 18:20"
time_offset = time(hour=int(t[0:2]), minute=int(t[3:5]))
time_now = datetime.now()
time_now_delta = timedelta(hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second)
time_offset_delta = timedelta(hours=time_offset.hour, minutes=time_offset.minute, seconds=time_offset.second)
datetime_today = datetime.combine(date.today(), time(second=0))
if time_offset_delta > time_now_delta:
alarm_time = datetime_today + time_offset_delta
else:
alarm_time = datetime_today + timedelta(days=1) + time_offset_delta
return alarm_time
def parse_seconds_time(t): # ex. "-s 3600"
return datetime.now() + timedelta(seconds=int(t))
def choose_gui_time():
print('Function not implemented yet')
exit(1)
def parse_default_time(t): # ex. "00:40:10"
time_until_alarm = timedelta(hours=int(t[0:2]), minutes=int(t[3:5]), seconds=int(t[7:9]))
return datetime.now() + time_until_alarm
#####
def set_alarm(args):
if args.is_clock_time:
alarm_time = parse_clock_time(args.time)
elif args.is_seconds:
alarm_time = parse_seconds_time(args.time)
elif args.is_gui:
alarm_time = choose_gui_time()
else:
alarm_time = parse_default_time(args.time)
alarm = Alarm(alarm_time.timestamp())
print('Made alarm - ' + str(alarm))
# print(alarm.getTimeLeft())
alarm.writeToFile(path.join(ALARM_DIR, alarm.id))
def deactivate_alarm(args):
file_path = path.join(ALARM_DIR, args.id)
if not path.exists(file_path):
print(f'[ERROR] alarm with ID "{args.id}" does not exist')
exit(1)
replace(file_path, path.join(PREVIOUS_ALARM_DIR, args.id))
def main():
argparser = argparse.ArgumentParser(description='Get and set alarms for the \'Alarmed\' daemon.')
argparser.set_defaults(func=lambda _: argparser.print_help())
# ------------------------------------------------------------------------ #
subargparsers = argparser.add_subparsers(
title='subcommands',
description='Run \'alarme <subcommand> --help\' for more information',
help='subcommand description')
# ------------------------------------------------------------------------ #
get_parser = subargparsers.add_parser('get', help='print out the currently active alarms')
get_parser.add_argument('-i', '--id', metavar='ID',
help='specify the id of the alarm to print')
get_parser.set_defaults(sep=' | ')
get_parser.set_defaults(func=print_alarms)
# ------------------------------------------------------------------------ #
set_parser = subargparsers.add_parser('set', help='make a new alarm')
set_time_format_group = set_parser.add_mutually_exclusive_group()
set_time_format_group.add_argument('-s', '--seconds', dest='is_seconds', action='store_true',
help='specify the remaining time as seconds')
set_time_format_group.add_argument('-t', '--to', dest='is_clock_time', action='store_true',
help='specify a clock time for the alarm to go off. If the clock is earlier than the current time, it wraps around to tomorrow. Format: "HH:MM"')
set_time_format_group.add_argument('-g', '--gui', dest='is_gui', action='store_true',
help='use a curses based gui to choose the time')
set_parser.add_argument('time', metavar='TIME', help='Time until or for the alarm to go off. Default format: "HH:MM:SS"')
set_parser.add_argument('-c', '--command', metavar='CMD',
help='specify a command to run as the alarm goes off')
set_parser.add_argument('-m', '--message', metavar='MSG',
help='add a comment as to what the alarm means')
set_parser.set_defaults(func=set_alarm)
# ------------------------------------------------------------------------ #
deactivation_parser = subargparsers.add_parser('deactivate', help='deactivate one of the alarms')
deactivation_parser.add_argument('-g', '--gui', dest='is_gui', action='store_true',
help='use a curses based gui to choose an alarm to remove')
deactivation_parser.add_argument('id', metavar='ID')
deactivation_parser.set_defaults(func=deactivate_alarm)
# ------------------------------------------------------------------------ #
args = argparser.parse_args()
args.func(args)
if __name__ == '__main__':
main()

60
src/alarmed.py Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/python
import argparse
import asyncio
from daemon import DaemonContext
from xdg import XDG_DATA_HOME, XDG_CONFIG_HOME
from datetime import datetime
from os import walk, replace, path
from subprocess import run
from time import sleep
from pathlib import Path
from Alarm import Alarm
from common import ALARM_DIR, PREVIOUS_ALARM_DIR, DEFAULT_ALARM_COMMAND, list_files
# TODO: Make this function modify global state in common.py
def init_data_folders(datadir=XDG_DATA_HOME, configdir=XDG_CONFIG_HOME):
datadir.joinpath('alarmed/alarms').mkdir(parents=True, exist_ok=True)
datadir.joinpath('alarmed/previous_alarms').mkdir(parents=True, exist_ok=True)
configdir.joinpath('alarmed').mkdir(parents=True, exist_ok=True)
def is_finished(file):
alarm = Alarm.readFromFile(path.join(ALARM_DIR, file))
return alarm.timestamp <= datetime.now().timestamp()
async def execute_alarm_command(alarm):
run(alarm.command or DEFAULT_ALARM_COMMAND.replace('%I', alarm.id).replace('%C', alarm.comment or ''), shell=True)
def move_finished_alarm(alarm_file, finished_alarm_dir=PREVIOUS_ALARM_DIR):
replace(
path.join(ALARM_DIR, alarm_file),
path.join(finished_alarm_dir, alarm_file)
)
async def execute_finished_alarms():
files = list_files(ALARM_DIR)
if files == []:
return
finished_alarms = [file for file in files if is_finished(file)]
for file in finished_alarms:
alarm = Alarm.readFromFile(path.join(ALARM_DIR, file))
print(f'[{datetime.now().strftime("%H:%M")}] Executing alarm with ID {alarm.id}')
move_finished_alarm(file)
asyncio.create_task(execute_alarm_command(alarm))
async def alarm_check_loop():
while True:
await execute_finished_alarms()
sleep(1)
async def main():
# with DaemonContext():
init_data_folders()
await alarm_check_loop()
if __name__ == '__main__':
asyncio.run(main())

18
src/common.py Normal file
View File

@ -0,0 +1,18 @@
from pathlib import Path
from xdg import XDG_DATA_HOME, XDG_CONFIG_HOME
from os import walk
ALARM_DIR = XDG_DATA_HOME.joinpath('alarmed/alarms').absolute()
PREVIOUS_ALARM_DIR = XDG_DATA_HOME.joinpath('alarmed/previous_alarms').absolute()
DEFAULT_ALARM_COMMAND = 'notify-send "Alarm %I: %C"'
def print_error(msg):
print('\033[31m[ERROR]\033[0m ' + msg)
def list_files(dir):
try:
_,_,files = next(walk(dir))
except StopIteration:
files = []
return files

26
src/gui.py Normal file
View File

@ -0,0 +1,26 @@
import curses
from common import ALARM_DIR, list_files
def choose_from_list(l):
screen = curses.initscr()
screen.addstr(0, 0, "Hello 1 from position (0, 0)")
screen.addstr(3, 1, "Hello 2 from (3, 1)")
screen.addstr(4, 4, "X")
screen.addch(5, 5, "Y")
screen.refresh()
curses.napms(2000)
curses.endwin()
pass
def choose_clock():
pass
def choose_alarm_id():
ids = list_files(ALARM_DIR)
chosen_id = choose_from_list(ids)
return chosen_id
if __name__ == '__main__':
print(choose_alarm_id())