411 lines
14 KiB
Python
Executable File
411 lines
14 KiB
Python
Executable File
#!/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("<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
|
|
|
|
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("<space>", 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)
|