312 lines
10 KiB
Python
Executable File
312 lines
10 KiB
Python
Executable File
#!/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("<Return>", self.ok)
|
|
self.bind("<Escape>", 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)
|