diff --git a/.gitignore b/.gitignore index 7f7cccc..a1d2e36 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python +venv/ env/ build/ develop-eggs/ diff --git a/README.md b/README.md index e1bf904..7b0c415 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # py-reminder -Simple application to set reminders in python \ No newline at end of file +Simple application to set reminders in python + +## Dependencies + +For development, we use the [black code formatter](https://black.readthedocs.io/en/stable/), and pylint as a linter + +``` +pip install black pylint +``` + +### Windows + +``` +pip install pywin32 +``` diff --git a/notify.py b/notify.py new file mode 100644 index 0000000..cade029 --- /dev/null +++ b/notify.py @@ -0,0 +1,11 @@ +import os + +def show_notification(title, msg): + if os.name == 'nt': + from notify_win import show_toast + show_toast(title, msg) + else: + from gi.repository import Notify + Notify.init(title) + Notify.Notification.new(msg).show() + Notify.uninit() diff --git a/notify_win.py b/notify_win.py new file mode 100644 index 0000000..2a3b18f --- /dev/null +++ b/notify_win.py @@ -0,0 +1,90 @@ +import os +import logging +import win32con +import win32gui + +from time import sleep + + +class ToastNotifier: + def show_toast(self, title, msg, icon_path=None, duration=5): + message_map = {win32con.WM_DESTROY: self.on_destroy} + # Register the Window class. + wc = win32gui.WNDCLASS() + wc.lpszClassName = "PythonTaskbar" + wc.lpfnWndProc = message_map # could also specify a wndproc. + + classAtom = win32gui.RegisterClass(wc) + self.hinst = wc.hInstance = win32gui.GetModuleHandle(None) + + # Create the Window. + style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU + self.hwnd = win32gui.CreateWindow( + classAtom, + "Taskbar", + style, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + self.hinst, + None, + ) + win32gui.UpdateWindow(self.hwnd) + + self.hicon = self._set_icon(icon_path) + + flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP + nid = (self.hwnd, 0, flags, win32con.WM_USER + 20, self.hicon, "Tooltip") + # Add a new notification icon + win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid) + win32gui.Shell_NotifyIcon( + win32gui.NIM_MODIFY, + ( + self.hwnd, + 0, + win32gui.NIF_INFO, + win32con.WM_USER + 20, + self.hicon, + "Balloon Tooltip", + msg, + 200, + title, + ), + ) + + sleep(duration) + win32gui.DestroyWindow(self.hwnd) + win32gui.UnregisterClass(wc.lpszClassName, None) + + def _set_icon(self, icon_path): + # icon + if icon_path is not None: + icon_path = os.path.realpath(icon_path) + else: + return None + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + try: + hicon = win32gui.LoadImage( + self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0, icon_flags + ) + except Exception as e: + logging.error("Some trouble with the icon ({}): {}".format(icon_path, e)) + hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) + return hicon + + def on_destroy(self, hwnd, msg, wparam, lparam): + nid = (hwnd, 0) + win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) + win32gui.PostQuitMessage(0) + + +_notifier = ToastNotifier() +def show_toast(title, msg, icon_path=None, duration=5): + _notifier.show_toast(title, msg, icon_path=None, duration=5) + +if __name__ == "__main__": + toaster = ToastNotifier() + toaster.show_toast("Hello world", "Test") diff --git a/run.py b/run.py new file mode 100644 index 0000000..5b51723 --- /dev/null +++ b/run.py @@ -0,0 +1,21 @@ +from schedule import Scheduler +from notify import show_notification +import datetime +import time + + +def notify_hello(): + show_notification("Hello world", "test") + + +if __name__ == "__main__": + _scheduler = Scheduler() + + # _scheduler.every(datetime.timedelta(seconds=3), lambda: print("Hello")) + # _scheduler.at(datetime.timedelta(seconds=5), lambda: print("Should run only once")) + + _scheduler.at(datetime.datetime(2019, 7, 1, 12, 4), notify_hello) + + while True: + _scheduler.run() + time.sleep(1) diff --git a/schedule.py b/schedule.py new file mode 100644 index 0000000..c5c1dd1 --- /dev/null +++ b/schedule.py @@ -0,0 +1,50 @@ +import time +import datetime +import logging + +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) + + +class Job: + def __init__(self, target, interval, times: int): + self.target = target + self.interval = interval + self.next_run = datetime.datetime.now() + interval + self.times = times + self.last_run = None + + @property + def should_run(self): + if self.times == 0: + return False + return datetime.datetime.now() >= self.next_run + + def run(self): + logging.info(f"Running job {self}") + self.last_run = datetime.datetime.now() + self.target() + self.times -= 1 + self.next_run = self.last_run + self.interval + logging.debug(f"Last run {self.last_run}") + logging.debug(f"Next run {self.next_run}") + logging.debug(f"Should run {self.should_run}") + + +class Scheduler: + jobs: [Job] = [] + + def every(self, interval: datetime.timedelta, target, times=-1): + self.jobs.append(Job(target, interval, times)) + + def at(self, time, target): + interval = time - datetime.datetime.now() + self.jobs.append(Job(target, interval, 1)) + + def run(self): + for job in self.jobs: + if job.should_run: + job.run() + # else: + # logging.info("No jobs to run…")