diff --git a/chrono.py b/chrono.py new file mode 100755 index 0000000..e82f521 --- /dev/null +++ b/chrono.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python +# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +u"""Afficher un chronomètre""" + +import os, sys, re, subprocess, traceback +from os import path +from datetime import date as Date, time as Time +from datetime import datetime as Datetime, timedelta as Timedelta +from types import IntType, LongType + +DEFAULT_SOUND = path.join(path.dirname(__file__), 'lib', 'chrono.wav') + +def win_playSound(name): + try: import winsound as s + except: return + if name is None: + s.PlaySound(None, s.SND_ASYNC) + else: + scriptdir = path.split(path.abspath(sys.argv[0]))[0] + soundfile = path.join(scriptdir, name) + s.PlaySound(soundfile, s.SND_FILENAME + s.SND_ASYNC) + +def linux_playSound(name): + subprocess.call(['/usr/bin/aplay', '-Nq', name]) + +def playSound(name=None): + if os.name == 'nt': + return win_playSound(name) + elif sys.platform.startswith('linux'): + return linux_playSound(name) + +def isnum(i): + return type(i) in (IntType, LongType) + +DEFAULT_TIMEOUT = '5' + +RE_DESTHOUR = re.compile(r'@(\d+)(?:[:.](\d+)(?:[:.](\d+))?)?$') +def parse_desthour(s): + mo = RE_DESTHOUR.match(s) + if mo is None: return None + h, m, s = mo.groups() + if h is None: h = 0 + if m is None: m = 0 + if s is None: s = 0 + h, m, s = int(h), int(m), int(s) + src = Datetime.today() + srcdate = src.date(); srctime = src.time() + destdate = srcdate; desttime = Time(h, m, s) + if desttime <= srctime: destdate = destdate + Timedelta(1) + src = Datetime.combine(srcdate, srctime) + dest = Datetime.combine(destdate, desttime) + delta = dest - src + return delta.total_seconds() + +RE_TIMEOUT = re.compile(r'(\d+)(?:[:.](\d+)(?:[:.](\d+))?)?$') +def parse_timeout(s): + mo = RE_TIMEOUT.match(s) + if mo is None: return None + h, m, s = mo.groups() + if m is None and s is None: + # M + m = h + h = None + elif s is None: + # M:S + s = m + m = h + h = None + else: + # H:M:S + pass + if h is None: h = 0 + if m is None: m = 0 + if s is None: s = 0 + h, m, s = int(h), int(m), int(s) + return h * 3600 + m * 60 + s + +class Chrono: + timeout = None + date_start = None + date_end = None + initial = None + started = None + + def __init__(self, timeout=None, start=False): + self.set_timeout(timeout) + if start: self.start() + + def __format(self, delta): + h = delta.seconds // 3600 + seconds = delta.seconds % 3600 + m = seconds // 60 + s = seconds % 60 + if h > 0: return '%02i:%02i:%02i' % (h, m, s) + else: return '%02i:%02i' % (m, s) + + def __delta(self, timeout): + return Timedelta(seconds=timeout) + + def set_timeout(self, timeout=None): + if timeout is not None and not isnum(timeout): + tmp = parse_desthour(str(timeout)) + if tmp is None: tmp = parse_timeout(timeout) + if tmp is None: tmp = int(timeout) * 60 + timeout = tmp + self.timeout = timeout + if timeout is None: self.initial = '00:00' + else: self.initial = self.__format(self.__delta(timeout)) + + def start(self, timeout=None): + if timeout is None: timeout = self.timeout + self.date_start = Datetime.today() + if timeout is None: self.date_end = None + else: self.date_end = self.date_start + self.__delta(timeout) + self.started = True + + def stop(self): + self.started = False + + def is_started(self): + return self.started + + def is_end(self): + return self.started and self.date_end is not None and Datetime.today() >= self.date_end + + def __repr__(self): + now = Datetime.today() + if self.date_end is None: delta = now - self.date_start + elif now > self.date_end: delta = Timedelta() + else: delta = self.date_end - now + return self.__format(delta) + +def run_chronometre(timeout=None, autostart=False): + from Tkinter import Tk, Toplevel, Frame, Label, Entry, Button + import tkMessageBox + + class Dialog(Toplevel): + def __init__(self, parent, title=None): + self.result = None + self.have_result = False + + Toplevel.__init__(self, parent) + self.transient(parent) + if title: self.title(title) + self.parent = parent + + body = Frame(self) + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + self.buttonbox() + self.grab_set() + if not self.initial_focus: self.initial_focus = self + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + self.initial_focus.focus_set() + self.wait_window(self) + + def set_result(self, result): + self.result = result + self.have_result = True + + def body(self, master): + pass + + def buttonbox(self): + box = Frame(self) + w = Button(box, text="OK", width=10, command=self.ok, default='active') + w.pack(side='left', padx=5, pady=5) + w = Button(box, text="Annuler", width=10, command=self.cancel) + w.pack(side='left', padx=5, pady=5) + self.bind("", self.ok) + self.bind("", self.cancel) + box.pack() + + def ok(self, event=None): + if not self.validate(): + self.initial_focus.focus_set() + return + self.withdraw() + self.update_idletasks() + self.apply() + self.cancel() + + def cancel(self, event=None): + self.parent.focus_set() + self.destroy() + + def validate(self): + return True + + def apply(self): + pass + + class Config(Dialog): + def body(self, master): + Label(master, text="Nb minutes", padx=20).grid(row=0) + self.entry = Entry(master) + self.entry.grid(row=0, column=1) + return self.entry + + def apply(self): + value = self.entry.get() + if value == "": result = None + else: result = value + self.set_result(result) + + class Application(Frame): + root = None + chrono = None + stop = None + + def __init__(self, timeout=None, autostart=False, **kw): + self.chrono = Chrono(timeout) + self.stop = False + + root = Tk() + root.title("Chronomètre") + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + root.bind("c", lambda event: self.do_config()) + root.bind("s", lambda event: self.do_start()) + root.bind("q", lambda event: self.quit()) + self.root = root + + kw.update(master=root) + Frame.__init__(self, **kw) + + self.TIME = Label(self, width=10, height=2, text=self.chrono.initial, padx=30, pady=10, font=('Helvetica', 18, "normal")) + self.START = Button(self, text="Démarrer", command=self.do_start) + self.CONFIG = Button(self, text="Config", command=self.do_config) + self.QUIT = Button(self, text="Quitter", command=self.quit) + + self.grid(column=0, row=0, sticky='nsew') + self.TIME.grid(column=0, row=0, columnspan=3, sticky='nsew') + self.START.grid(column=0, row=1, sticky='ew') + self.CONFIG.grid(column=1, row=1, sticky='ew') + self.QUIT.grid(column=2, row=1, sticky='ew') + + self.columnconfigure(0, weight=2) + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=2) + self.rowconfigure(0, weight=1) + + if autostart: self.do_start() + + def update_time(self): + chrono = self.chrono + self.TIME.configure(text=chrono) + if chrono.is_started(): + if chrono.is_end(): + playSound(DEFAULT_SOUND) + else: + self.root.after(300, self.update_time) + + def do_start(self): + self.chrono.start() + self.update_time() + + def do_config(self): + chrono = self.chrono + chrono.stop() + config = Config(self.root) + if config.have_result: + try: + chrono.set_timeout(config.result) + self.TIME.configure(text=chrono.initial) + except: + traceback.print_exc() + tkMessageBox.showerror("Valeur invalide", sys.exc_info()[1]) + + Application(timeout, autostart).mainloop() + +if __name__ == '__main__': + from argparse import ArgumentParser, RawTextHelpFormatter + AP = ArgumentParser( + formatter_class=RawTextHelpFormatter, + usage=u"%(prog)s [options] [TIMEOUT]", + description=u"Afficher un chronomètre", + epilog=u"Si TIMEOUT est spécifié, par défaut le décompte démarre automatiquement." + ) + AP.set_defaults(autostart=None, timeout=None) + AP.add_argument('timeout', metavar='TIMEOUT', nargs='?', + help=u"""\ +(valeur vide) + chronomètre qui démarre à 0:00 et ne s'arrête pas +H:M:S (heures:minutes:secondes) +ou M:S (minutes:secondes) +ou M (minutes) + minuteur qui démarre à H:M:S et fait un décompte jusqu'à 0:00. A la fin + du décompte, une sonnerie retentit. +@H[:M[:S]] + minuteur qui fonctionne comme précédemment, sauf qu'on spécifie l'heure + d'arrivée, et que la durée est calculée automatiquement""") + AP.add_argument('-n', '--no-autostart', dest='autostart', action='store_false', + help=u"Ne pas démarrer automatiquement le décompte même si TIMEOUT est spécifié.") + AP.add_argument('-s', '--autostart', dest='autostart', action='store_true', + help=u"Forcer le démarrage automatique du décompte, même si TIMEOUT n'est pas spécifié.") + o = AP.parse_args() + + autostart = o.autostart + if autostart is None: autostart = o.timeout is not None + o.autostart = autostart + + timeout = o.timeout + if timeout is None: timeout = DEFAULT_TIMEOUT + elif timeout == '': timeout = None + o.timeout = timeout + + run_chronometre(o.timeout, o.autostart) diff --git a/lib/chrono.wav b/lib/chrono.wav new file mode 100644 index 0000000..077ae85 Binary files /dev/null and b/lib/chrono.wav differ