#! /usr/bin/python3 # Simple countdown timer # Copyright (C) 2021 Joey Schulze # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import math import time import signal import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib, Gdk import multiprocessing from pydub import AudioSegment from pydub.playback import play from argparse import ArgumentParser from configparser import ConfigParser, NoSectionError, NoOptionError class NotificationWindow(Gtk.Window): def __init__(self, audio=False, noise=None): super().__init__(title="Countdown") self.process = None self.noise = noise if audio: self.make_noise() self.build() def build(self): self.set_resizable(False) vbox = Gtk.VBox() vbox.set_margin_bottom(10) self.lbl_destination = Gtk.Label() self.lbl_destination.set_markup('{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time())))) vbox.add(self.lbl_destination) self.lbl_destination = Gtk.Label() self.lbl_destination.set_markup('Countdown finished') vbox.add(self.lbl_destination) vbox.add(Gtk.Label(label='')) hbox = Gtk.HBox() self.btn_dismiss = Gtk.Button(label="Dismiss") self.btn_dismiss.connect("clicked", self.dismiss_on_clicked) hbox.set_center_widget(self.btn_dismiss) vbox.add(hbox) self.add(vbox) self.show_all() def dismiss_on_clicked(self, widget): if self.process: self.process.terminate() self.process = None else: self.destroy() def play_noise(self): if os.path.exists(self.noise): song = AudioSegment.from_file(self.noise) play(song) def make_noise(self): if self.noise and os.path.exists(self.noise): self.process = multiprocessing.Process(target=self.play_noise) self.process.start() class CountdownWindow(Gtk.Window): def __init__(self, noise=None): super().__init__(title="Countdown Timer") self.noise = noise self.started = False self.countdown = 0 self.range_max = 0 self.build() self.update() def build(self): self.connect("delete-event", Gtk.main_quit) self.set_resizable(False) vbox = Gtk.VBox() vbox.set_margin_bottom(10) vbox.set_margin_start(5) vbox.set_margin_end(5) self.lbl_destination = Gtk.Label() self.lbl_destination.set_markup('00:00:00') vbox.add(self.lbl_destination) vbox.add(Gtk.Label(label='')) self.lbl_countdown = Gtk.Label() self.lbl_countdown.set_markup('00:00:00') vbox.add(self.lbl_countdown) self.scale = Gtk.HScale() self.scale.set_draw_value(False) self.scale.connect("value-changed", self.scale_on_change) self.scale.connect("key-press-event", self.scale_on_keypress) vbox.add(self.scale) hbox = Gtk.HBox() self.btn_min = Gtk.RadioButton(label="Min") self.btn_min.set_group() self.btn_min.connect("toggled", lambda w: self.period_on_toggle(w, 'min')) hbox.add(self.btn_min) self.btn_hour = Gtk.RadioButton(label="Hour") self.btn_hour.join_group(self.btn_min) self.btn_hour.set_active(True) self.btn_hour.connect("toggled", lambda w: self.period_on_toggle(w, 'hour')) hbox.add(self.btn_hour) self.btn_day = Gtk.RadioButton(label="Day") self.btn_day.join_group(self.btn_min) self.btn_day.connect("toggled", lambda w: self.period_on_toggle(w, 'day')) hbox.add(self.btn_day) self.set_period('hour') vbox.add(hbox) self.btn_audio = Gtk.CheckButton(label="Play audio file") vbox.add(self.btn_audio) self.controls = Gtk.HBox() self.controls.set_spacing(100) self.btn_start = Gtk.Button(label="Start") self.btn_start.connect("clicked", self.start_on_clicked) self.btn_cancel = Gtk.Button(label="Cancel") self.btn_cancel.connect("clicked", self.cancel_on_clicked) vbox.add(self.controls) self.toggle_controls(True) self.add(vbox) self.show_all() self.btn_cancel.hide() def get_scale_value(self): try: return self.step * int(self.scale.get_value() // self.step) except: return 0.0 def notify(self): NotificationWindow(self.btn_audio.get_active(), self.noise) def update(self): self.update_destination() if self.started: value = math.floor(self.countdown - time.time()) if value <= 0: self.notify() value = 0 self.started = False self.toggle_controls(True) GLib.timeout_add_seconds(5, lambda:self.update_countdown(math.floor(self.get_scale_value()))) self.update_countdown(value) GLib.timeout_add_seconds(1, self.update) def update_destination(self): if not self.started: dest = time.time() + self.get_scale_value() self.lbl_destination.set_markup('{}'.format( time.strftime('%H:%M:%S', time.localtime(dest)))) def update_countdown(self, value): hours = math.floor(value / (60*60)); value -= hours * (60*60) mins = math.floor(value / 60); value -= mins * 60 secs = value self.lbl_countdown.set_markup('{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)) def period_on_toggle(self, widget, period): if widget.get_active(): self.set_period(period) def set_period(self, period): old_range_max = self.range_max old_value = self.get_scale_value() if period == 'min': self.range_max = 10*60 self.step = 5 elif period == 'hour': self.range_max = 2*60*60 self.step = 60 elif period == 'day': self.range_max = 24*60*60 self.step = 5*60 else: raise Exception("Unsupported period {}".format(perood)) self.scale.set_range(0, self.range_max) if old_range_max: new_value = (old_value * self.range_max) / old_range_max self.scale.set_value(new_value) self.update_destination() def toggle_controls(self, show_start): widget = self.controls.get_center_widget() if widget is not None: widget.hide() self.controls.remove(widget) widget = self.btn_start if show_start else self.btn_cancel widget.show() self.controls.set_center_widget(widget) if show_start: self.scale.set_sensitive(True) self.btn_min.set_sensitive(True) self.btn_hour.set_sensitive(True) self.btn_day.set_sensitive(True) else: self.scale.set_sensitive(False) self.btn_min.set_sensitive(False) self.btn_hour.set_sensitive(False) self.btn_day.set_sensitive(False) def start_on_clicked(self, widget): self.toggle_controls(False) if not self.started: self.started = True self.countdown = time.time() + self.get_scale_value() def cancel_on_clicked(self, widget): self.toggle_controls(True) if self.started: self.started = False def scale_on_change(self, widget): self.update_countdown(math.floor(self.get_scale_value())) self.update_destination() def scale_on_keypress(self, widget, event): key = Gdk.keyval_name(event.keyval) if key == 'Left': value = self.scale.get_value() - (5 if 'control-mask' in event.get_state().value_nicks else 1) * self.step if value < 0: value = 0.0 self.scale.set_value(value) elif key == 'Right': value = self.scale.get_value() + (5 if 'control-mask' in event.get_state().value_nicks else 1) * self.step if value > self.range_max: value = self.range_max self.scale.set_value(value) def build_window(args): if os.path.exists(args.configfile): config = ConfigParser() config.read(args.configfile, encoding='UTF-8') else: config = False noise = None if config: try: noise = os.path.expanduser(config.get('countdown', 'alarm')) except (NoOptionError, NoSectionError): pass if args.alarmfile: noise = args.alarmfile window = CountdownWindow(noise=noise) return window def main(): parser = ArgumentParser(description='Simple Countdown Timer') parser.add_argument('-c', '--config', dest='configfile', metavar='file', default=os.path.expanduser('~/.countdown.conf'), action='store', help='config file') parser.add_argument('-a', '--alarm', dest='alarmfile', metavar='file', action='store', help='alarm sound file') args = parser.parse_args() window = build_window(args) Gtk.main() if __name__ == '__main__': main()