#!/usr/bin/env python
# -*- coding: utf-8 -*-

u"""%(scriptname)s: gérer une liste de tâches

USAGE
    %(scriptname)s [options] list|add|edit|do|commit|remove|purge|report|update [args]

OPTIONS
    -c tasksrc
        Spécifier le fichier de configuration

    Utilisez l'option --help avec chacune des commandes pour avoir une
    description plus complète de leur utilisation"""

__all__ = ('Task', 'TextFile', 'TWFile', 'TasksrcFile', 'TasksCLI')

import sys, os, re
from os import path
from getopt import getopt
from types import StringType, UnicodeType, ListType, TupleType

from pyutools.dates import datef
from pyutools.config import ShConfigFile
from pyutools import scriptdir, scriptname, dict, ensure_unicode
from pyutools import uprint, get_color, eerror, ewarn, enote
from pyutools import edit_template

from TiddlyWiki import Tiddler, TiddlyWiki

_marker = []
DEFAULT_ENCODING = 'iso-8859-1'

# propriétés standard d'une tâche
ID = 'id'
TITLE = 'title'
DESC = 'desc'
TEXT = 'text'
ENCODING = 'encoding'
DATE =  'date' # date de création
TYPE = 'type' # type de tâche. par défaut, vaut 'TODO'
# propriétés pour une tâche de type TODO
TODO_TYPE = 'TODO'
DONE = 'done' # date de fin de tâche
RELEASED_SUFFIX = '_released' # date de release du projet
PURGED_SUFFIX = '_purged' # date de purge du fichier pour le projet
# propriétés pour une tâche de type IDEA
IDEA_TYPE = 'IDEA'

class Task:
    # ligne telle qu'elle est écrite dans le fichier
    line = None

    # le texte, les propriétés, les projets, les contextes
    text = None
    props = None
    projects = None
    contexts = None

    # cet objet a-t-il été modifié?
    modified = False

    # numero d'index de cet tâche dans la liste des tâches
    index = None
    
    encoding = DEFAULT_ENCODING
    def ensure_unicode(self, value):
        return ensure_unicode(value, self.encoding)

    def quote_nl(self, text):
        if text is not None:
            text = text.replace('\\', r'\\')
            text = text.replace('\n', r'\n')
        return text
        
    def unquote_nl(self, text):
        if text is not None:
            text = text.replace(r'\n', '\n')
            text = text.replace(r'\\', '\\')
        return text

    ##################################################################
    def __init__(self, line=None, **kw):
        self.text = u''
        self.props = {}
        self.projects = []
        self.contexts = []
        
        if line is not None: self.fromline(line)
        for name, value in kw.items():
            self[name] = value

    TAGS_PATTERN = re.compile(r'\s*\{\s*((?:(?:\+\S+\s*)?(?:@\S+\s*)?(?:\S+=\S+\s*)?)*)\}\s*$')
    PROP_PATTERN = re.compile(r'(\S+)=(\S+)')
    PROJ_PATTERN = re.compile(r'\+(\S+)\s*')
    CTXT_PATTERN = re.compile(r'@(\S+)\s*')
    def fromline(self, line):
        """Initialiser cet objet à partir d'une ligne d'un fichier.
        """
        mo = self.TAGS_PATTERN.search(line)
        if mo is not None:
            tags = mo.group(1)
            props = self.PROP_PATTERN.findall(tags)
            for name, value in props:
                if name == ENCODING: self[name] = value
            for name, value in props:
                if name != ENCODING: self[name] = value
            self.projects = []
            for project in self.PROJ_PATTERN.findall(tags):
                self.projects.append(self.ensure_unicode(project))
            self.contexts = []
            for context in self.CTXT_PATTERN.findall(tags):
                self.contexts.append(self.ensure_unicode(context))
            self.text = self.ensure_unicode(self.unquote_nl(line[:mo.start(0)]))
        else:
            self.text = self.ensure_unicode(self.unquote_nl(line))

    def toline(self):
        """Générer une ligne d'un fichier à partir de cet objet.
        """
        if self.line is None:
            line = self.quote_nl(self.text or u'')
            if self.props:
                prefix = u' {'
                for project in self.projects:
                    line += u'%s+%s' % (prefix, project)
                    prefix = u' '
                for context in self.contexts:
                    line += u'%s@%s' % (prefix, context)
                    prefix = u' '
                for name, value in self.props.items():
                    line += u'%s%s=%s' % (prefix, name, value)
                    prefix = u' '
                line += u'}'
            self.line = line.encode(self.encoding)
        return self.line
    __repr__ = toline
    __str__ = toline

    def update_from(self, t):
        for n in ('line', 'text', 'props', 'projects', 'contexts'):
            tp = getattr(self, n)
            op = getattr(t, n)
            if tp != op:
                setattr(self, n, op)
                self.modified = True

    # Gestion des propriétés
    def __eq__(self, other):
        return self is other
    def __len__(self):
        return len(self.props)
    def __getattr__(self, name):
        if name in self: return self[name]
        else: raise NameError, name
    def __getitem__(self, name, default=_marker):
        if name in self.METH_PROPERTIES:
            return getattr(self, 'get_' + name)()
        else:
            if default is _marker: return self.props[name]
            else: return self.props.get(name, default)
    get = __getitem__
    def __delitem__(self, name):
        if name in self.METH_PROPERTIES: getattr(self, 'del_' + name)()
        else: del self.props[name]
        self.line = None
        self.modified = True
    def __setitem__(self, name, value):
        if name in self.METH_PROPERTIES: getattr(self, 'set_' + name)(value)
        else: self.props[name] = self.ensure_unicode(value)
        self.line = None
        self.modified = True
    def is_modified(self):
        return self.modified
    def __contains__(self, key):
        return key in self.METH_PROPERTIES or key in self.props
    def has_key(self, key):
        return key in self.METH_PROPERTIES or self.props.has_key(key)
    def keys(self):
        return self.props.keys()
    def values(self):
        return self.props.values()
    def items(self):
        return self.props.items()
    def clear(self):
        self.props.clear()
    def update(self, props=None, **kw):
        if props is not None:
            for key, value in props.items():
                self[key] = value
        for key, value in kw.items():
            self[key] = value

    # Propriétés gérées par des méthodes
    METH_PROPERTIES = [TITLE, DESC, TEXT, ENCODING]
    def get_title(self):
        pos = self.text.find('\n\n')
        if pos != -1: return self.text[:pos]
        else: return self.text
    def del_title(self):
        self.set_title('')
    def set_title(self, title):
        title = self.ensure_unicode(title)
        desc = self.get_desc()
        self.text = title + (desc and u'\n\n' + desc or '')
        self.line = None
        self.modified = True
    
    def get_desc(self):
        text = self.text or u''
        pos = text.find('\n\n')
        if pos != -1: return text[pos + 2:]
        else: return None
    def del_desc(self):
        self.set_desc('')
    def set_desc(self, desc):
        title = self.get_title()
        desc = self.ensure_unicode(desc)
        self.text = title + (desc and u'\n\n' + desc or '')
        self.line = None
        self.modified = True

    def get_text(self):
        return self.text
    def del_text(self):
        self.set_text('')
    def set_text(self, text):
        self.text = self.ensure_unicode(text)
        self.line = None
        self.modified = True

    def get_encoding(self):
        return self.encoding
    def del_encoding(self):
        self.encoding = DEFAULT_ENCODING
        del self.props[ENCODING]
        self.modified = True
    def set_encoding(self, encoding):
        self.encoding = encoding
        self.props[ENCODING] = encoding
        self.modified = True

    # Opérations de haut niveau

    def has_id(self):
        return self.has_key(ID)
    def get_id(self):
        return self.get(ID, None)
    def set_id(self, id=None):
        if id is None:
            if self.has_key(ID): del self[ID]
        else:
            self[ID] = id

    def has_date(self):
        return self.has_key(DATE)
    def get_date(self):
        return self.get(DATE, None)
    def set_date(self, date=None):
        if date is None: date = datef()
        self[DATE] = date

    def get_type(self):
        return self.get(TYPE, TODO_TYPE)
    def set_type(self, type=None):
        if type is not None:
            type = type.upper()
            if type in ('T', 'TODO'): type = TODO_TYPE
            elif type in ('I', 'IDEA'): type = IDEA_TYPE
        if type is None or type == TODO_TYPE:
            if self.has_key(TYPE): del self[TYPE]
        else:
            self[TYPE] = type

    def is_todo_type(self):
        return self.get_type() == TODO_TYPE
    def is_done(self):
        return self.has_key(DONE)
    def set_done(self, date=None):
        if date is None: date = datef()
        self[DONE] = date

    def is_idea_type(self):
        return self.get_type() == IDEA_TYPE

    def has_projects(self):
        return len(self.projects) != 0
    def get_projects(self):
        return self.projects
    def set_projects(self, projects):
        self.projects = list(projects)
        self.modified = True
    def has_project(self, project):
        return project in self.projects
    def add_project(self, project):
        self.projects.append(project)
    def remove_project(self, project):
        self.projects.remove(project)
        
    def is_project_purged(self, project):
        return self.has_key(project + PURGED_SUFFIX)
    def set_project_purged(self, project, date=None):
        if date is None: date = datef()
        self[project + PURGED_SUFFIX] = date
    def is_all_purged(self, is_project_func=None):
        for project in self.projects:
            if is_project_func is not None:
                if not is_project_func(project): continue
            if not self.is_project_purged(project): return False
        return True

    def is_project_released(self, project):
        return self.has_key(project + RELEASED_SUFFIX)
    def set_project_released(self, project, date=None):
        if date is None: date = datef()
        self[project + RELEASED_SUFFIX] = date
    def is_all_released(self):
        for project in self.projects:
            if not self.is_project_released(project): return False
        return True

    def has_contexts(self):
        return len(self.contexts) != 0
    def get_contexts(self):
        return self.contexts
    def set_contexts(self, contexts):
        self.contexts = list(contexts)
        self.modified = True
    def add_context(self, context):
        self.contexts.append(context)
    def remove_context(self, context):
        self.contexts.remove(context)

class TasksFile:
    """Classe de base des fichiers contenant des tâches.
    """

    valid = False
    tasks = None
    props = None

    def update_indexes(self):
        index = 1
        for task in self.tasks:
            task.index = index
            index = index + 1
    def update_ids(self):
        known_ids = {}
        for task in self.tasks:
            if task.has_id(): known_ids[task.get_id()] = None
            
        lastid = self.get_lastid()
        for task in self.tasks:
            if not task.has_key(ID):
                id = lastid + 1
                if known_ids.has_key(id):
                    while known_ids.has_key(id):
                        id = id + 1
                task[ID] = id
                lastid = id
        self.set_lastid(lastid)

    def load(self, file, raise_exception=True):
        pass
    
    def save(self, file=None, raise_exception=True):
        pass
        
    def is_valid(self):
        return self.valid
    def get_file(self):
        pass
    
    def get_tasks(self):
        return self.tasks
    def __len__(self):
        return len(self.tasks)
    def __getitem__(self, index):
        return self.tasks[index]
    def __delitem__(self, index):
        del self.tasks[index]
        self.update_indexes()
    def __setitem__(self, index, task):
        self.tasks[index] = task
        self.update_ids()
    def add(self, task):
        self.tasks.append(task)
        self.update_indexes()
        self.update_ids()
    def remove(self, task_or_id):
        if isinstance(task_or_id, Task): task = task_or_id
        else: task = self.get_by_id(task_or_id)
        self.tasks.remove(task)
        self.update_indexes()
        self.update_ids()

    def __filter_indexes(self, tasks, indexes):
        if not indexes: return tasks
        filtered = []
        maxindex = max(map(lambda t: t.index, tasks))
        for i in indexes:
            if i > 0 and i <= maxindex:
                filtered.append(tasks[i - 1])
        return filtered
    def __filter_contexts(self, tasks, contexts):
        if not contexts: return tasks
        if '*' in contexts: return tasks
        filtered = []
        for t in tasks:
            for c in contexts:
                if not t.has_context(c): break
            else:
                filtered.append(t)
        return filtered
    def __filter_projects(self, tasks, projects):
        if not projects: return tasks
        if '*' in projects: return tasks
        filtered = []
        for t in tasks:
            for p in projects:
                if not t.has_project(p): break
            else:
                filtered.append(t)
        return filtered
    def __filter_patterns(self, tasks, patterns):
        if not patterns: return tasks
        filtered = []
        for t in tasks:
            for p in patterns:
                if not re.search(p, t.get_text()): break
            else:
                filtered.append(t)
        return filtered
    def __filter_props(self, tasks, props):
        if not props: return tasks
        filtered = []
        for t in tasks:
            for name, value in props.items():
                if value == '*':
                    if not t.has_key(name): break
                elif value == '':
                    if t.has_key(name): break
                else:
                    if not t.has_key(name) or t[name] != value: break
            else:
                filtered.append(t)
        return filtered
    
    def filter(self, indexes=None, projects=None, contexts=None, patterns=None, props=None, **kw):
        filtered = self.__filter_indexes(self.get_tasks(), indexes)
        filtered = self.__filter_contexts(filtered, contexts)
        filtered = self.__filter_projects(filtered, projects)
        filtered = self.__filter_patterns(filtered, patterns)
        if props is None: props = kw
        else: props.update(kw)
        filtered = self.__filter_props(filtered, props)
        return filtered

    def get_by_id(self, id):
        filtered = self.filter(props={ID: id})
        if len(filtered) > 0: return filtered[0]
        else: return None
            
    def has_props(self):
        return len(self.props) != 0
    def has_prop(self, name):
        return self.props.has_key(name)
    def get_prop(self, name, default=_marker):
        value = self.props.get(name, default)
        if value is _marker: value = None
        return value
    def set_prop(self, name, value):
        self.props[name] = value

# Propriétés standard d'un fichier texte
TF_LASTID = 'lastid'
TF_ENCODING = 'encoding' # encoding par défaut du fichier

class TextFile(TasksFile):
    """Fichier de tâches au format texte.
    """
    
    TASKS_TXT = ['TASKS.txt', 'tasks.txt']
    PURGED_TXT = ['PURGED.txt', 'purged.txt']
    FILENAMES = [TASKS_TXT, PURGED_TXT]
    TASKS_FILETYPE = 0
    PURGED_FILETYPE = 1

    valid = False
    file = pf = dirname = filename = None

    tasks = None
    props = None
    
    def __init__(self, src=None, filetype=TASKS_FILETYPE, raise_exception=False):
        self.tasks = []
        self.props = {}

        filenames = self.FILENAMES[filetype]
        if src is None:
            # Mettre en place les éléments pour qu'une sauvegarde écrive dans le fichier
            # du type spécifié.
            file = filenames[0]
            self.__update_file(file)
            self.valid = True
        else:
            # Sinon on essaye de charger le fichier
            if path.isdir(src):
                dir = src
                file = path.join(dir, filenames[0])
                if not path.exists(file):
                    file2 = path.join(dir, filenames[1])
                    if path.exists(file2): file = file2
            else:
                file = src
            self.load(file, raise_exception=raise_exception)

    def __update_file(self, file):
        self.file = file
        self.pf = path.abspath(self.file)
        self.dirname, self.filename = path.split(self.pf)

    def __strip_nl(self, line):
        if line.endswith('\r\n'): return line[:-2]
        elif line.endswith('\n'): return line[:-1]
        elif line.endswith('\r'): return line[:-1]
        else: return line

    def __add_nl(self, line):
        return line + '\n'

    PROP_PATTERN = re.compile(r'(\S+)=(\S+)')
    def __load_props(self, line):
        for name, value in self.PROP_PATTERN.findall(line):
            self.set_prop(name, value)

    def load(self, file, raise_exception=True):
        self.__update_file(file)
        try:
            self.tasks = []
            self.valid = False
            inf = open(self.pf, 'rb')
            try:
                lines = map(self.__strip_nl, inf.readlines())
                self.valid = True
            finally:
                inf.close()
        except IOError:
            if raise_exception: raise
            return False

        if lines[0:1] and lines[0].startswith("##"):
            self.__load_props(lines[0])
            lines = lines[1:]
        for line in lines:
            t = Task(line)
            t.modified = False
            self.tasks.append(t)
        self.update_indexes()
        self.update_ids()
        return True

    def save(self, file=None, raise_exception=True):
        if file is not None: self.__update_file(file)
        self.valid = False
        try:
            # écrire dans un ficher temporaire d'abord
            tmpf = self.pf + '.tmp'
            outf = open(tmpf, 'wb')
            try:
                lines = []
                if self.has_props():
                    line = "##"
                    for name, value in self.props.items():
                        line += " %s=%s" % (name, value)
                    lines.append(line)
                lines.extend(map(lambda t: t.toline(), self.tasks))
                outf.writelines(map(self.__add_nl, lines))
            finally:
                outf.close()
            try:
                os.rename(tmpf, self.pf)
            except OSError:
                os.remove(self.pf)
                os.rename(tmpf, self.pf)
            self.valid = True
            return True
        except IOError:
            if raise_exception: raise
            return False

    def get_file(self):
        return self.pf
            
    def get_lastid(self):
        return int(self.get_prop(TF_LASTID, 0))
    def set_lastid(self, lastid):
        self.set_prop(TF_LASTID, int(lastid))
    
class TWFile(TasksFile):
    """Fichier de tâches au format TiddlyWiki
    """

    valid = False
    file = pf = dirname = filename = None

    twfile = None
    tasks = None
    props = None
    
    def __init__(self, src=None, raise_exception=False):
        self.tasks = []
        self.props = {}
        
        if src is None:
            twfile = TiddlyWiki(src, raise_exception=False)
            if twfile.is_valid():
                self.load(twfile=twfile)
            elif raise_exception:
                raise IOError("Impossible de charger le TiddlyWiki associé")
        else:
            self.load(src, raise_exception=raise_exception)

    def __update_file(self, file):
        self.file = file
        self.pf = path.abspath(self.file)
        self.dirname, self.filename = path.split(self.pf)

    def load(self, file, raise_exception=True):
        self.valid = False
        # XXX continuer ici
        twfile = TiddlyWiki(src, raise_exception=False)
    
    def save(self, file=None, raise_exception=True):
        pass

class ProxyTask:
    __task = None
    index = None
    
    def __init__(self, task, index):
        self.__task = task
        self.index = index

    def __getattr__(self, name, default=_marker):
        if default is _marker: return getattr(self.__task, name)
        else: return getattr(self.__task, name, default)

class FileAggregate(TasksFile):
    __tfs = None
    
    def __init__(self, tfs):
        self.__tfs = []
        for tf in tfs:
            self.__tfs.append(tf)

    def load(self, file, raise_exception=True):
        raise NotImplementedError

    def save(self, file=None, raise_exception=True):
        for tf in self.__tfs:
            tf.save(file, raise_exception)

    def __len__(self):
        return reduce(lambda n, m: n + m, map(lambda t: len(t), self.__tfs))
    def __get_tfindex(self, index):
        tfi = index
        for tf in self.__tfs:
            if tfi < len(tf): return tf, tfi
            tfi -= len(tf)
        raise IndexError, index
    def __getitem__(self, index):
        tf, tfi = self.__get_tfindex(index)
        return ProxyTask(tf[tfi], index)
    def __delitem__(self, index):
        tf, tfi = self.__get_tfindex(index)
        del tf[tfi]
    def __setitem__(self, index, task):
        tf, tfi = self.__get_tfindex(index)
        tf[tfi] = task
    def add(self, task):
        raise NotImplementedError
    def remove(self, task):
        raise NotImplementedError

    def filter(self, indexes=None, projects=None, contexts=None, props=None, **kw):
        offset = 0
        filtered = []
        for tf in self.__tfs:
            for t in tf.filter(indexes, projects, contexts, props, **kw):
                filtered.append(ProxyTask(t, t.index + offset))
            offset += len(tf)
        return filtered
            
    def has_props(self):
        for tf in self.__tfs:
            if tf.has_props(): return True
        return False
    def has_prop(self, name):
        for tf in self.__tfs:
            if tf.has_prop(name): return True
        return False
    def get_prop(self, name, default=_marker):
        for tf in self.__tfs:
            if tf.has_prop(name): return tf.get_prop(name)
        if default is _marker: return None
        else: return default
    def set_prop(self, name, value):
        raise NotImplementedError

    def get_lastid(self):
        raise NotImplementedError
    def set_lastid(self, lastid):
        raise NotImplementedError
        
    def is_valid(self):
        raise NotImplementedError

    def get_file(self):
        raise NotImplementedError

######################################################################

class TasksrcFile(ShConfigFile):
    TASKSRC = "tasksrc"

    def __init__(self, file=None, raise_exception=True):
        if file is None:
            testdir = path.join(scriptdir, "test")
            utoolsrcdir = path.join(path.expanduser("~"), ".utools")
            if path.isdir(testdir): file = path.join(testdir, self.TASKSRC)
            else: file = path.join(utoolsrcdir, self.TASKSRC)
            raise_exception = False
        ShConfigFile.__init__(self, file=file, raise_exception=raise_exception)

    TASKSDIR = "tasksdir"
    PROJDIRS = "projdirs"
    TASKSFILE = "tasksfile"
    PURGEDFILE = "purgedfile"
    DONE_COLOR = "done_color"
    IDEA_COLOR = "idea_color"
    PROJECT_COLOR = "project_color"
    CONTEXT_COLOR = "context_color"
    TAG_COLOR = "tag_color"

    def __p(self, p, refdir=None):
        if refdir is not None:
            if not path.isdir(refdir): refdir = path.split(refdir)[0]
        p = path.normpath(path.expanduser(p))
        if refdir is not None:
            p = path.join(refdir, p)
        return p

    def get_tasksdir(self):
        """Retourner le répertoire de base pour les fichiers de tâches
        """
        if self.has_key(self.TASKSDIR): return self.__p(self[self.TASKSDIR])
        else: return self.dirname
    def set_tasksdir(self, tasksdir=None):
        if tasksdir is None:
            if self.has_key(self.TASKSDIR): del self[self.TASKSDIR]
        else:
            self[self.TASKSDIR] = tasksdir

    def get_projdirs(self):
        """Retourner la liste des répertoires de base de projets
        """
        if self.has_key(self.PROJDIRS): return map(lambda p: self.__p(p, self.get_tasksdir()),
                                                   self.get_paths(self.PROJDIRS))
        else: return ()
    def set_projdirs(self, projdirs=None):
        if projdirs is None:
            if self.has_key(self.PROJDIRS): del self[self.PROJDIRS]
        else:
            if type(projdirs) is not StringType:
                projdirs = path.pathsep.join(projdirs)
            self[self.PROJDIRS] = projdirs

    def get_tasksfile(self, default=_marker):
        """Retourner tasksfile s'il est défini, None sinon
        """
        if self.has_key(self.TASKSFILE): return self.__p(self[self.TASKSFILE], self.get_tasksdir())
        elif default is _marker: return None
        else: return default
    def get_taskssrc(self):
        """Retourner tasksfile s'il est défini, tasksdir sinon
        """
        return self.get_tasksfile(self.get_tasksdir())
    def set_tasksfile(self, tasksfile=None):
        if tasksfile is None:
            if self.has_key(self.TASKSFILE): del self[self.TASKSFILE]
        else:
            self[self.TASKSFILE] = tasksfile

    def get_purgedfile(self, default=_marker):
        """Retourner purgedfile s'il est défini, None sinon
        """
        if self.has_key(self.PURGEDFILE): return self.__p(self[self.PURGEDFILE], self.get_tasksdir())
        elif default is _marker: return None
        else: return default
    def get_purgedsrc(self):
        """Retourner purgedfile s'il est défini, tasksdir sinon
        """
        return self.get_purgedfile(self.get_tasksdir())
    def set_purgedfile(self, purgedfile=None):
        if purgedfile is None:
            if self.has_key(self.PURGEDFILE): del self[self.PURGEDFILE]
        else:
            self[self.PURGEDFILE] = purgedfile

    def get_done_color(self):
        return self.get(self.DONE_COLOR, "faint")
    def set_done_color(self, done_color):
        self[self.DONE_COLOR] = done_color

    def get_idea_color(self):
        return self.get(self.IDEA_COLOR, "yellow")
    def set_idea_color(self, idea_color):
        self[self.IDEA_COLOR] = idea_color

    def get_project_color(self):
        return self.get(self.PROJECT_COLOR, "blue")
    def set_project_color(self, project_color):
        self[self.PROJECT_COLOR] = project_color

    def get_context_color(self):
        return self.get(self.CONTEXT_COLOR, "blue")
    def set_context_color(self, context_color):
        self[self.CONTEXT_COLOR] = context_color

    def get_tag_color(self):
        return self.get(self.TAG_COLOR, "faint,blue")
    def set_tag_color(self, tag_color):
        self[self.TAG_COLOR] = tag_color

class TasksCLI:
    tasksrc = None

    tasksfile = None
    def __tf(self):
        # retourner le fichier pour les tâches
        if self.tasksfile is None:
            tasksfile = self.tasksrc.get_tasksfile()
            if tasksfile is not None: tasksfile = TextFile(tasksfile)
            else: tasksfile = TextFile(self.tasksrc.get_taskssrc(), TextFile.TASKS_FILETYPE)
            self.tasksfile = tasksfile
        return self.tasksfile
    purgedfile = None
    def __pf(self):
        # retourner le fichier pour les tâches purgées
        if self.purgedfile is None:
            purgedfile = self.tasksrc.get_purgedfile()
            if purgedfile is not None: purgedfile = TextFile(purgedfile)
            else: purgedfile = TextFile(self.tasksrc.get_purgedsrc(), TextFile.PURGED_FILETYPE)
            self.purgedfile = purgedfile
        return self.purgedfile    

    def __init__(self, tasksrc=None):
        if not isinstance(tasksrc, TasksrcFile):
            tasksrc = TasksrcFile(tasksrc)
        self.tasksrc = tasksrc

    CONFOPT = 'c:'
    CONFLOPT = ['config=', 'tasksdir=', 'projdirs=', 'tasksfile=', 'purgedfile=']
    
    def is_global_option(self, opt, value):
        if opt in ('-c', '--config'):
            self.tasksrc = TasksrcFile(value)
            self.tasksfile = None
            self.purgedfile = None
        elif opt in ('--tasksdir', ):
            self.tasksrc.set_tasksdir(value)
        elif opt in ('--projdirs', ):
            self.tasksrc.set_projdirs(value)
        elif opt in ('--tasksfile', ):
            self.tasksfile = None
            self.tasksrc.set_tasksfile(value)
        elif opt in ('--purgedfile', ):
            self.purgedfile = None
            self.tasksrc.set_purgedfile(value)
        else:
            return False
        return True

    def get_projdir(self, dir):
        """Si dir est un répertoire absolu, retourner le nom de projet
        correspondant, ou None si ce n'est pas un répertoire de projet.

        Sinon, retourner le répertoire absolu correspondant au répertoire de
        projet
        """
        projdirs = self.tasksrc.get_projdirs()
        if path.isabs(dir):
            # dir est un répertoire absolu. retourner le nom de projet
            # correspondant
            maxlen = 0
            project = None
            if dir not in projdirs:
                for projdir in projdirs:
                    if dir.startswith(projdir + '/'):
                        p = dir[len(projdir) + 1:]
                        i = p.find('/')
                        if i != -1: p = p[:i]
                        pd = path.join(projdir, p)
                        if len(pd) > maxlen:
                            maxlen = len(pd)
                            project = p
            return project
        else:
            # dir est un nom de projet. retourner le répertoire de projet
            # correspondant
            p = dir
            for projdir in projdirs:
                pd = path.join(projdir, p)
                if path.isdir(pd): return pd
            return None
        
    def get_project(self):
        """Retourner le nom du projet si on est dans un répertoire de projet, None sinon
        """
        return self.get_projdir(os.getcwd())

    def is_project(self, project):
        """Retourner True si le project existe physiquement (le répertoire
        correspondant est trouvé sur le disque.)
        """
        return self.get_projdir(project) is not None

    def get_tf(self, project):
        """Obtenir le fichier de tâche de project
        """
        projdir = self.get_projdir(project)
        if projdir is None:
            return None
        elif path.exists(path.join(projdir, 'TODO.html')):
            return TWFile(projdir)
        else:
            return TextFile(projdir, TextFile.TASKS_FILETYPE)

    def update_projects(self, delete=None, add_or_update=None, delete_projects=None, delete_ids=None):
        """Mettre à jour les tâches dans les répertoires de projet

        delete est une liste de tâches qui doivent être supprimées dans les
        répertoires de projet.

        add_or_update est une liste de tâches qui doivent être ajoutées ou mises
        à jour dans les répertoires de projet. delete_projects est la liste des
        projets qui ont été supprimés de ces tâches. delete_ids est une liste
        d'ids pour lesquels les tâches associées doivent être supprimées avant
        d'être mises à jour.

        On fait dans l'ordre: delete, delete_ids, add_or_update, delete_projects
        """
        tfs = {}
        def get_tf(p):
            
            tf = tfs.get(p, None)
            if tf is None:
                tf = self.get_tf(p)
                if tf is not None:
                    tfs[p] = tf
            return tf
        def remove_from_tf(p, t):
            tf = get_tf(p)
            if tf is not None: tf.remove(t.get_id())
        def update_in_tf(p, t):
            tf = get_tf(p)
            if tf is not None:
                pt = tf.get_by_id(t.get_id())
                if pt is not None: pt.update_from(t)
                else: tf.add(t)

        def norm_seq(o):
            if o is not None:
                if type(o) not in (ListType, TupleType): o = (o, )
            return o
        delete = norm_seq(delete)
        add_or_update = norm_seq(add_or_update)
        delete_projects = norm_seq(delete_projects)
        delete_ids = norm_seq(delete_ids)
            
        if delete:
            for t in delete:
                for p in t.get_projects():
                    remove_from_tf(p, t)

        if add_or_update:
            if delete_ids:
                for t in add_or_update:
                    for p in t.get_projects():
                        for id in delete_ids:
                            get_tf(p).remove(id)
            for t in add_or_update:
                for p in t.get_projects():
                    update_in_tf(p, t)
            if delete_projects:
                for p in delete_projects:
                    for t in add_or_update:
                        remove_from_tf(p, t)

        for tf in tfs.values():
            tf.save()

    INDEX_PATTERN = re.compile(r'(\d+)$')
    def __parse_index(self, index):
        mo = self.INDEX_PATTERN.match(index)
        if mo is not None: return int(mo.group(1))
        else: return None

    PROP_PATTERN = re.compile(r'(\S+)=(\S*)$')
    def __parse_prop(self, prop):
        mo = self.PROP_PATTERN.match(prop)
        if mo is not None: return mo.group(1), mo.group(2)
        else: return None, None

    CONTEXT_PATTERN = re.compile(r'@(\S+)$')
    def __parse_context(self, context):
        mo = self.CONTEXT_PATTERN.match(context)
        if mo is not None: return mo.group(1)
        else: return None

    PROJECT_PATTERN = re.compile(r'\+(\S+)$')
    def __parse_project(self, project):
        mo = self.PROJECT_PATTERN.match(project)
        if mo is not None: return mo.group(1)
        else: return None

    def __get_filtered(self, tf, argv, auto_filter=True,
                       indexes=None, patterns=None, contexts=None, projects=None, props=None,
                       **kw):
        if indexes is None: indexes = []
        if props is None: props = kw
        else: props.update(kw)
        if contexts is None: contexts = []
        if projects is None: projects = []
        if patterns is None: patterns = []
        auto_filtered = None
        if not argv and auto_filter:
            project = self.get_project()
            if project is not None:
                projects.append(project)
                auto_filtered = project
        else:
            i = 0
            for i in range(len(argv)):
                arg = argv[i]
                if arg == '--':
                    i += 1
                    break
                index = self.__parse_index(arg)
                name, value = self.__parse_prop(arg)
                context = self.__parse_context(arg)
                project = self.__parse_project(arg)
                if index is not None: indexes.append(index)
                elif name is not None: props[name] = value
                elif context is not None: contexts.append(context)
                elif project is not None: projects.append(project)
                else: patterns.append(arg)
            argv = argv[i:]
        return tf.filter(indexes, projects, contexts, patterns, props), auto_filtered

    def __cvs_commit(self, t, argv=()):
        pass
        
    ############################################################################

    def ADD(self,
            type_option=None, done_option=False, edit_option=False, commit_option=False,
            text=None, contexts=None, projects=None, props=None,
            argv=(), scriptname=None,
            **kw):
        u"""%(scriptname)s: Créer une nouvelle tâche

USAGE
    %(scriptname)s [-d] [-e] titre de la tâche

OPTIONS
    -t  Type de la tâche: TODO (par défaut) ou IDEA
    -d  Marquer la tâche comme faite après l'avoir créée. Ceci permet de suivre
        une activité pour laquelle l'on n'avait pas créée un tâche au préalable.
        Cette option est activée par défaut avec tt.
    -e  Editer la tâche après l'avoir créée. Ceci permet d'ajouter des
        informations supplémentaires.
    -C  Faire 'cvs commit' avec le message de la tâche après avoir marqué la
        tâche comme faite. Il n'est pas possible de passer des arguments à cvs
        avec cette option. Cette option implique l'option -d"""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'ht:deC',
                            self.CONFLOPT + ['help', 'type=', 'done', 'edit', 'commit'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.ADD.__doc__ % locals())
                sys.exit(0)
            elif opt in ('-t', '--type'):
                type_option = value
            elif opt in ('-d', '--done'):
                done_option = True
            elif opt in ('-e', '--edit'):
                edit_option = True
            elif opt in ('-C', '--commit'):
                done_option = True
                commit_option = True

        if text is None: text = ""
        if contexts is None: contexts = []
        if projects is None: projects = []
        if props is None: props = kw
        else: props.update(kw)
        if argv:
            for arg in argv:
                name, value = self.__parse_prop(arg)
                context = self.__parse_context(arg)
                project = self.__parse_project(arg)
                if name is not None: props[name] = value
                elif context is not None: contexts.append(context)
                elif project is not None: projects.append(project)
                else:
                    if text != "": text += " "
                    text += arg
        else:
            # Si l'on n'a pas donné de texte, donner la possibilité de l'éditer.
            edit_option = True

        tf = self.__tf()
        t = None
        if props.has_key(ID):
            tasks = tf.filter(id=props[ID])
            if tasks:
                t = tasks[0]
                newtask = False

        if t is None:
            t = Task()
            newtask = True
        t.set_type(type_option)
        t.update(props)
        t.set_contexts(contexts)
        t.set_projects(projects)
        t.set_text(text)

        if not t.has_date(): t.set_date()
        if done_option: t.set_done()
        if not t.has_projects():
            project = self.get_project()
            if project is not None: t.add_project(project)
        
        if newtask: tf.add(t)
        tf.save()
        self.update_projects(add_or_update=t)

        if argv: self.__print_task(t)
        if edit_option: self.EDIT(t)
        if commit_option: self.__cvs_commit(t)

    ############################################################################

    def __print_task(self, t, width=1, showdesc=False):
        parts = []

        prefix_color = ''
        reset_color = get_color("reset")
        if t.is_todo_type():
            if t.is_done():
                prefix_color = get_color(self.tasksrc.get_done_color())
                parts.append(prefix_color)
                parts.append(u"[x]")
            else:
                parts.append(u"[ ]")
        elif t.is_idea_type():
            prefix_color = get_color(self.tasksrc.get_idea_color())
            parts.append(prefix_color)
            parts.append(u"Id>")
        else:
            prefix = parts.append(u"??>")
        parts.append(u" %*i " % (width, t.index))
        for p in t.get_projects():
            parts.append(get_color(self.tasksrc.get_project_color()))
            if t.is_project_purged(p): parts.append("-%s" % p)
            else: parts.append("+%s" % p)
            parts.append(reset_color)
            parts.append(prefix_color)
            parts.append(u" ")
        for c in t.get_contexts():
            parts.append(get_color(self.tasksrc.get_context_color()))
            parts.append("@%s" % c)
            parts.append(reset_color)
            parts.append(prefix_color)
            parts.append(u" ")
        parts.append(t.get_title().replace('\n', ' '))
        parts.append(reset_color)
        uprint(''.join(parts))

        desc = t.get_desc()
        if showdesc:
            if desc: uprint(desc)
            tag_color = get_color(self.tasksrc.get_tag_color())
            for name, value in t.items():
                uprint(u"%s%s=%s%s" % (tag_color, name, value, reset_color))

    def __print_stats(self, tasks, auto_filtered=None, show_only_todos=True):
        numtodos = len(filter(lambda t: not t.is_done(), tasks))
        numtasks = len(filter(lambda t: not t.is_todo_type(), tasks))
        infos = []
        if show_only_todos and numtasks != 0: infos.append(u"%i tâche(s) masquée(s)" % numtasks)
        if auto_filtered: infos.append(u"filtrage auto +%s" % auto_filtered)
        if infos: infos = u" [%s]" % u", ".join(infos)
        else: infos = u""
        enote(u"%i / %i tâche(s) trouvée(s)%s" % (numtodos, len(tasks), infos))
        
    def LIST(self,
             listall_option=False, listpurged_option=False, showdesc_option=False, showstats_option=True,
             indexes=None, patterns=None, contexts=None, projects=None, props=None,
             argv=(), scriptname=None,
             **kw):
        u"""%(scriptname)s: Lister les tâches en cours et leur état

USAGE
    %(scriptname)s [-a] [-l] [--no-stats] filtres*|numeros*

OPTIONS
    -a  Lister aussi les tâches qui ne sont pas de type TODO
    -p  Lister aussi les tâche purgées
    -l  Afficher aussi la description et les tags associés aux tâches
    --no-stats
        Ne pas afficher de statistiques à la fin de la liste des tâches

    Cette commande prend éventuellement une liste de filtres qui sont combinées
    avec AND (les tâches listées doivent valider tous les filtres), sauf si on
    ne donne que des numéros de tâches, auquel cas on liste simplement toutes
    les tâches dont on a donné le numéro. Les filtre peuvent être des formes
    suivantes:

    +project
        Pour filtrer uniquement les tâches d'un projet. Si on se trouve dans un
        répertoire de projet, on filtre automatiquement sur ce projet. Si l'on
        veut forcer le listage des tâches de tous les projets, il faut utiliser
        la syntaxe +*
    @context
        Pour filtrer uniquement les tâches d'un contexte. Si l'on veut forcer
        le listage des tâches de tous les contextes, il faut utiliser la syntaxe
        @*
    tag=value
        Pour filtrer uniquement les tâches dont le tag existe et a la valeur
        indiquée. La syntaxe tag=* permet de filtrer simplement sur l'existence
        du tag. La syntaxe tag= permet de filtrer sur la NON existence du tag.
        Les tags valides sont pour le moment id, date, done, ${project}_purged
        où $project est un nom de projet valide dans cette tâche.
    <toute autre valeur>
        Afficher les tâches qui contiennent les mots spécifiés."""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'hapln',
                            self.CONFLOPT + ['help', 'list-all', 'list-purged', 'show-desc', 'no-stats'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.LIST.__doc__ % locals())
                sys.exit(0)
            elif opt in ('-a', '--list-all'):
                listall_option = True
            elif opt in ('-p', '--list-purged'):
                listpurged_option = True
            elif opt in ('-l', '--show-desc'):
                showdesc_option = True
            elif opt in ('-n', '--no-stats'):
                showstats_options = False

        if listpurged_option:
            tf = FileAggregate([self.__pf(), self.__tf()])
        else:
            tf = self.__tf()
        tasks, auto_filtered = self.__get_filtered(tf, argv,
                                                   indexes=indexes,
                                                   patterns=patterns,
                                                   contexts=contexts,
                                                   projects=projects,
                                                   props=props)
        if not auto_filtered:
            # Si on effectue une recherche, afficher TOUS les résultats
            listall_option = True
        if tasks: width = len(str(max(map(lambda t: t.index, tasks))))
        else: width = 1
        for t in tasks:
            if not listall_option and not t.is_todo_type(): continue
            self.__print_task(t, width, showdesc=showdesc_option)
        if showstats_option:
            self.__print_stats(tasks, auto_filtered, not listall_option)

    ############################################################################

    EDIT_TEMPLATE = u"""
EDIT: ----------------------------------------------------------------
EDIT: Saisissez ou modifiez le titre et la description de la tâche.
EDIT:
EDIT: - Les lignes commencant par 'EDIT:' seront supprimées automatiquement
EDIT: - Les lignes projects: et contexts: peuvent être modifiées si
EDIT:   nécessaire.
EDIT:
EDIT: ----------------------------------------------------------------"""

    CONTEXTS_PATTERN = re.compile(r'##\s*contexts:\s*')
    CTXT_PATTERN = re.compile(r'(\S+)\s*')
    PROJECTS_PATTERN = re.compile(r'##\s*projects:\s*')
    PROJ_PATTERN = re.compile(r'(\S+)\s*')
    PROP_PATTERN = re.compile(r'##\s*(\S+)\s*=\s*(\S+)')
    
    def __nblines(self, s):
        lines = s.split("\n")
        nblines = len(lines)
        if not lines[-1]: nblines -= 1
        return nblines

    def EDIT(self,
             t=None,
             argv=(), scriptname=None,
             **kw):
        u"""%(scriptname)s: éditer une tâche dont on donne le numéro

USAGE
    %(scriptname)s numero"""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'h',
                            self.CONFLOPT + ['help'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.EDIT.__doc__ % locals())
                sys.exit(0)

        tf = self.__tf()
        if t is None:
            count = 0
            if argv:
                tasks, auto_filtered = self.__get_filtered(tf, argv)
                count = len(tasks)
                if count > 0: t = tasks[0]
            if count == 0:
                error("Il faut spécifier un numéro de tâche à éditer")
                return
            elif count > 1:
                ewarn("Seule la première tâche sera éditée")

        template = u""
        template += u"## contexts: %s\n" % " ".join(t.get_contexts())
        template += u"## projects: %s\n" % " ".join(t.get_projects())
        for name, value in t.items():
            template += u"## %s=%s\n" % (name, value)
        template += u"\n"
        
        title = t.get_title()
        template += u"%s\n" % title
        setline = self.__nblines(template)
        setcol = len(title)
        
        desc = t.get_desc()
        if desc: template += u"\n%s" % desc
        
        template += self.EDIT_TEMPLATE
        lines = edit_template(template, 'EDIT:', setline, setcol).split('\n')

        new_contexts = []
        new_projects = []
        new_props = {}
        parsing_tags = True
        skip_empty = True
        text = []
        for line in lines:
            if skip_empty and not line: continue
            if parsing_tags:
                moc = self.CONTEXTS_PATTERN.match(line)
                mop = self.PROJECTS_PATTERN.match(line)
                mo = self.PROP_PATTERN.match(line)
                if moc is not None:
                    new_contexts.extend(map(t.ensure_unicode, self.CTXT_PATTERN.findall(line[moc.end():])))
                    continue
                elif mop is not None:
                    new_projects.extend(map(t.ensure_unicode, self.PROJ_PATTERN.findall(line[mop.end():])))
                    continue
                elif mo is not None:
                    name, value = mo.group(1), mo.group(2)
                    new_props[name] = t.ensure_unicode(value)
                    continue
                else:
                    parsing_tags = False
            skip_empty = False
            text.append(line)
        text = t.ensure_unicode("\n".join(text))

        if t.get_contexts() != new_contexts: t.set_contexts(new_contexts)
        delete_projects = []
        if t.get_projects() != new_projects:
            # Si la liste des projets est différente, il faudra supprimer les
            # tâches des projets qui ne sont plus dans la liste
            for p in t.get_projects():
                if p not in new_projects:
                    delete_projects.append(p)
            t.set_projects(new_projects)
        delete_ids = []
        if t.props != new_props:
            if t.props[ID] != new_props.get(ID, None):
                delete_ids.append(t.props[ID])
            t.clear()
            t.update(new_props)
        if t.get_text() != text: t.set_text(text)

        if t.is_modified():
            if t.get_text():
                self.__print_task(t)
                tf.update_ids()
                tf.save()
                self.update_projects(add_or_update=t,
                                     delete_projects=delete_projects,
                                     delete_ids=delete_ids)
            else:
                # pas de texte, il faut supprimer la tâche
                tf.remove(t)
                tf.save()
                self.update_projects(delete=t)
                enote("Tâche supprimée")
        else:
            enote("Aucune modification effectuée")

    ############################################################################

    def DO(self,
           commit_option=False, force_option=False,
           indexes=None, patterns=None, contexts=None, projects=None, props=None,
           argv=(), scriptname=None,
           **kw):
        u"""%(scriptname)s: Marquer comme faites les tâches dont on donne le numéro

USAGE
    %(scriptname)s numeros*
OPTIONS
    -C  Faire 'cvs commit' avec le message de la tâche après avoir marqué la
        tâche comme faite. Dans ce cas, un seul numéro de tâche est autorisé.
        Les autres arguments sont passés à cvs.
    -f  Faire le commit même si la tâche est déjà marquée comme faite."""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'hCf',
                            self.CONFLOPT + ['help', 'commit', 'force'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.DO.__doc__ % locals())
                sys.exit(0)
            elif opt in ('-C', '--commit'):
                commit_option = True
            elif opt in ('-f', '--force'):
                force_option = True

        if not argv:
            eerror("Il faut spécifier un numéro de tâche à marquer comme faite")
            return
        
        tf = self.__tf()
        tasks, auto_filtered = self.__get_filtered(tf, argv,
                                                   indexes=indexes,
                                                   patterns=patterns,
                                                   contexts=contexts,
                                                   projects=projects,
                                                   props=props)
        saved = False
        updated = []
        for t in tasks:
            done = t.is_done()
            if not done: t.set_done()
            if not t.is_todo_type(): t.set_type(None)
            if t.is_modified():
                saved = False
                updated.append(t)
            self.__print_task(t)
            if commit_option and (force_option or not done):
                tf.save()
                saved = True
                self.__cvs_commit(t, argv)
        if updated:
            if not saved: tf.save()
            self.update_projects(add_or_update=updated)

    ############################################################################

    def REMOVE(self,
               indexes=None, patterns=None, contexts=None, projects=None, props=None,
               argv=(), scriptname=None,
               **kw):
        u"""%(scriptname)s: Supprimer les tâches dont on donne le numéro

USAGE
    %(scriptname)s numeros*"""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'h',
                            self.CONFLOPT + ['help'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.REMOVE.__doc__ % locals())
                sys.exit(0)

        if not argv:
            eerror("Il faut spécifier un numéro de tâche à supprimer")
            return
        
        tf = self.__tf()
        tasks, auto_filtered = self.__get_filtered(tf, argv,
                                                   indexes=indexes,
                                                   patterns=patterns,
                                                   contexts=contexts,
                                                   projects=projects,
                                                   props=props)

        if len(tasks):
            deleted = []
            for t in tasks:
                tf.remove(t)
                deleted.append(t)
            tf.save()
            self.update_projects(delete=deleted)

            enote("%i tâche(s) supprimée(s)" % len(tasks))
        
    ############################################################################

    def PURGE(self,
              argv=(), scriptname=None,
              **kw):
        u"""%(scriptname)s: Purger les tâches qui sont faites

USAGE
    %(scriptname)s [projet]

OPTIONS
    Si aucun projet n'est spécifié, les tâches qui sont marquées comme faites et
    ne sont associées à aucun projet valide sont déplacées dans le fichier
    d'archive.

    Si un projet est spécifié, alors seules les tâches associées à ce projet
    sont considérées. Si après avoir marqué le projet comme purgé, la tâche
    n'est plus associée à aucun projet valide, la tâche est purgée: elle est
    déplacée dans le fichier d'archive et le fichier DONE.txt du projet.

    Le fichier d'archive est actuellement défini à $(ppath "$purgedfile")"""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'h',
                            self.CONFLOPT + ['help'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.PURGE.__doc__ % locals())
                sys.exit(0)
        
        projects = []
        if not argv:
            project = self.get_project()
            if project is not None:
                projects.append(project)
        else:
            projects.extend(argv)
        
        tf = self.__tf()
        pf = self.__pf()
        now = datef()
        updated = []
        purged = []
        for t in list(tf):
            if not t.is_done(): continue

            # la tâche est elligible à la purge
            if projects:
                # ne purger que les projets demandés
                for project in projects:
                    t.set_project_purged(project, now)
            else:
                # purger tous les projets
                for project in t.get_projects():
                    t.set_project_purged(project, now)

            # si tous les projets sont purgés, on peut déplacer la ligne dans pf
            if t.is_all_purged(self.is_project):
                pf.add(t)
                tf.remove(t)
                purged.append(t)
            else:
                updated.append(t)

        if purged:
            pf.save()
            tf.save()
            self.update_projects(add_or_update=updated, delete=purged)
            enote("%i tâche(s) purgée(s)" % len(purged))
            self.LIST(showstats_option=False, props={'done': '*'})

    ############################################################################

    def REPORT(self,
               argv=(), scriptname=None,
               **kw):
        u"""%(scriptname)s: Afficher un rapport sur les tâches

USAGE
    %(scriptname)s

OPTIONS
    Afficher une liste des projets en cours, et leur pourcentage, ainsi que les
    projets qui sont terminés (pour lesquels il n'y a pas de tâches."""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'h',
                            self.CONFLOPT + ['help'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.REPORT.__doc__ % locals())
                sys.exit(0)

    ############################################################################

    def UPDATE(self,
               argv=(), scriptname=None,
               **kw):
        u"""%(scriptname)s: Forcer la mise à jour des fichiers de projet

USAGE
    %(scriptname)s"""
        opts, argv = getopt(argv,
                            self.CONFOPT + 'h',
                            self.CONFLOPT + ['help'])
        for opt, value in opts:
            if self.is_global_option(opt, value):
                pass
            elif opt in ('-h', '--help'):
                uprint(self.UPDATE.__doc__ % locals())
                sys.exit(0)

        update = []
        for t in self.__tf():
            update.append(t)
        self.update_projects(add_or_update=update)

################################################################################
if __name__ == '__main__':
    action = None
    argv = sys.argv[1:]
    tasksCLI = TasksCLI()

    # Essayer de determiner l'action avec le nom du script
    if scriptname in ('ta', 'ti', 'tt'):
        if scriptname == 'ti': argv[0:0] = ('-t', IDEA_TYPE)
        if scriptname == 'tt': argv.insert(0, '-d')
        action = 'add'
    elif scriptname in ('tl', 'tll', 'tla'):
        if scriptname == 'tll': argv.insert(0, '-l')
        elif scriptname == 'tla': argv.insert(0, '-a')
        action = 'list'
    elif scriptname in ('te', ):
        action = 'edit'
    elif scriptname in ('td', 'tci'):
        if scriptname == 'tci': argv.insert(0, '-C')
        action = 'do'

    if action is None:
        opts, argv = getopt(argv,
                            TasksCLI.CONFOPT + 'h',
                            TasksCLI.CONFLOPT + ['help'])
        for opt, value in opts:
            if opt in ('-h', '--help'):
                uprint(__doc__ % dict(scriptname=scriptname))
                sys.exit(0)
            elif tasksCLI.is_global_option(opt, value):
                pass

        if not argv:
            uprint(__doc__ % dict(scriptname=scriptname))
            sys.exit(0)

        action, argv = argv[0], argv[1:]
        if action in ('add', 'a', 'done'):
            if action == 'done': argv.insert(0, '-d')
            action = 'add'
        elif action in ('list', 'l', 'll', 'la'):
            if action == 'll': argv.insert(0, '-l')
            elif action == 'la': argv.insert(0, '-a')
            action = 'list'
        elif action in ('edit', 'e'):
            action = 'edit'
        elif action in ('do', 'd', 'commit', 'c'):
            if action in ('commit', 'c'): argv.insert(0, '-C')
            action = 'do'
        elif action in ('remove', 'r'):
            action = 'remove'
        elif action in ('purge', 'p'):
            action = 'purge'
        elif action in ('report', ):
            action = 'report'
        elif action in ('update', ):
            action = 'update'
        else:
            eerror("Action inconnue: %s" % action)
            sys.exit(1)

    if scriptname in ('Tasks.py', 't'):
        # pour l'affichage de l'aide
        scriptname = '%s %s' % (scriptname, action)

    apply(getattr(tasksCLI, action.upper()), (), {'argv': argv, 'scriptname': scriptname})