#!/usr/bin/env python2 # -*- 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: MODE_MINUTEUR = 'M' MODE_CHRONOMETRE = 'C' STATE_STARTED = 'started' STATE_PAUSED = 'paused' STATE_STOPPED = 'stopped' ZERO = None state = None mode = None timeout = None elapsed = None date_start = None max_elapsed = None initial = None def __init__(self, timeout=None, start=False): self.ZERO = self.__delta(0) 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 self.elapsed = self.ZERO self.state = self.STATE_STOPPED if timeout is None: self.mode = self.MODE_CHRONOMETRE self.initial = self.__format(self.ZERO) else: self.mode = self.MODE_MINUTEUR self.max_elapsed = self.__delta(timeout) self.initial = self.__format(self.max_elapsed) def is_chronometre(self): return self.mode == self.MODE_CHRONOMETRE def is_minuteur(self): return self.mode == self.MODE_MINUTEUR def get_elapsed(self): if self.date_start is None: return self.ZERO delta = Datetime.today() - self.date_start return self.elapsed + delta def start(self, timeout=None): if timeout is None: timeout = self.timeout self.elapsed = self.ZERO self.date_start = Datetime.today() self.state = self.STATE_STARTED def is_started(self): return self.state == self.STATE_STARTED def pause(self): if self.state == self.STATE_PAUSED: self.date_start = Datetime.today() self.state = self.STATE_STARTED else: self.elapsed = self.get_elapsed() self.state = self.STATE_PAUSED def is_paused(self): return self.state == self.STATE_PAUSED def stop(self): self.elapsed = self.get_elapsed() self.state = self.STATE_STOPPED def is_stopped(self): return self.state == self.STATE_STOPPED def is_end(self): if not self.is_started(): return False if not self.is_minuteur(): return False elapsed = self.get_elapsed() return elapsed >= self.max_elapsed def __repr__(self): elapsed = self.get_elapsed() if self.is_started() else self.elapsed if self.is_minuteur(): delta = self.max_elapsed - elapsed if delta < self.ZERO: delta = self.ZERO return self.__format(delta) else: return self.__format(elapsed) 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 def __init__(self, timeout=None, autostart=False, **kw): self.chrono = Chrono(timeout) 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("p", lambda event: self.do_pause()) root.bind("", lambda event: self.do_pause()) 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="Start", command=self.do_start) self.PAUSE = Button(self, text="Pause", command=self.do_pause, state="disabled") self.CONFIG = Button(self, text="Config", command=self.do_config) self.QUIT = Button(self, text="Quit", command=self.quit) self.grid(column=0, row=0, sticky='nsew') self.TIME.grid(column=0, row=0, columnspan=4, sticky='nsew') self.START.grid(column=0, row=1, sticky='ew') self.PAUSE.grid(column=1, row=1, sticky='ew') self.CONFIG.grid(column=2, row=1, sticky='ew') self.QUIT.grid(column=3, row=1, sticky='ew') self.columnconfigure(0, weight=2) self.columnconfigure(1, weight=2) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) 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.PAUSE.configure(state="normal", text="Pause") self.START.configure(text="reStart") self.chrono.start() self.update_time() def do_pause(self): self.chrono.pause() if self.chrono.is_paused(): self.PAUSE.configure(text="unPause") else: self.PAUSE.configure(text="Pause") self.update_time() def do_config(self): chrono = self.chrono chrono.stop() config = Config(self.root) if config.have_result: try: self.PAUSE.configure(text="Pause", state="disabled") self.START.configure(text="Start") 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)