From 4ad365a2b4f84c36e6792c7e45e61b0baa412f69 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 16 Jun 2021 14:21:15 +0200 Subject: [PATCH] init commit --- .gitignore | 1 + README.md | 5 ++ setup.py | 23 ++++++++ src/Alarm.py | 47 +++++++++++++++++ src/__init__.py | 0 src/alarme.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ src/alarmed.py | 60 +++++++++++++++++++++ src/common.py | 18 +++++++ src/gui.py | 26 +++++++++ 9 files changed, 318 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 setup.py create mode 100644 src/Alarm.py create mode 100644 src/__init__.py create mode 100644 src/alarme.py create mode 100755 src/alarmed.py create mode 100644 src/common.py create mode 100644 src/gui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..38832e8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Alarmed + +The alarmed daemon. + +Lightweight package for keeping track of alarms that run custom commands when they go off. \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..22744fd --- /dev/null +++ b/setup.py @@ -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', + ] + }, +) \ No newline at end of file diff --git a/src/Alarm.py b/src/Alarm.py new file mode 100644 index 0000000..ddf48e7 --- /dev/null +++ b/src/Alarm.py @@ -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}' \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alarme.py b/src/alarme.py new file mode 100644 index 0000000..3b25dbf --- /dev/null +++ b/src/alarme.py @@ -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 --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() \ No newline at end of file diff --git a/src/alarmed.py b/src/alarmed.py new file mode 100755 index 0000000..81fabc7 --- /dev/null +++ b/src/alarmed.py @@ -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()) \ No newline at end of file diff --git a/src/common.py b/src/common.py new file mode 100644 index 0000000..2605b11 --- /dev/null +++ b/src/common.py @@ -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 \ No newline at end of file diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..8579c3c --- /dev/null +++ b/src/gui.py @@ -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()) \ No newline at end of file