nutools/chrono.py

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)