#!/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) RE_NUM = re.compile(r'\d+$') def numof(s): if isnum(s): return s elif s is None: return None elif RE_NUM.match(str(s)) is not None: return int(str(s)) else: return s 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 == '': 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 if timeout == 0: timeout = None 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, HelpFormatter if sys.argv[1:2] == ['--compat']: # Avec l'argument --compat, désactiver la classe FancyHelpFormatter qui # se base sur une API non documentée sys.argv = sys.argv[0:1] + sys.argv[2:] FancyHelpFormatter = HelpFormatter else: class FancyHelpFormatter(HelpFormatter): """Comme HelpFormatter, mais ne touche pas aux lignes qui commencent par les caractères '>>>'. Cela permet de mixer du texte formaté et du texte non formaté. """ def _fill_text(self, text, width, indent): return ''.join([indent + line for line in text.splitlines(True)]) def _split_lines(self, text, width): lines = [''] for line in text.splitlines(): if line.startswith('>>>'): lines.append(line) lines.append('') else: lines[-1] += '\n' + line lines = filter(None, lines) texts = [] for line in lines: if line.startswith('>>>'): texts.append(line[3:]) else: texts.extend(super(FancyHelpFormatter, self)._split_lines(line, width)) return texts AP = ArgumentParser( 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.", formatter_class=FancyHelpFormatter, ) 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 o.timeout = timeout run_chronometre(o.timeout, o.autostart)