Простая напоминалка на Linux



Для людей, которые работают в офисе с персональным компьютером — бухгалтеров, инженеров, секретарей, менеджеров, специалистов, экспертов, начальников структурных подразделений и тем более директоров — сегодня обычным делом является сопровождать решение нескольких вопросов одновременно.

К примеру, звонит клиент и просит внести изменения в договор. Ты кладешь трубку — и тут звонит твой коллега и просит тебя направить ему давно забытый материал, который нужно еще постараться отыскать. Не успеваешь ты договорить с коллегой, как звонит на сотовый директор и просит составить ему небольшой отчет. А ведь до этого ты занимался своим вопросом! Нужно всё запомнить, ничего не упустить! Типичная ситуация, не правда ли?

Для того, чтобы все успевать в таких ситуациях, поможет простая напоминалка. Но что такое простая напоминалка? Каковы критерии ее простоты?
Для меня «простой напоминалкой» является та, которая действует по следующему принципу:

  • Открываешь диалоговое окно напоминалки горячей клавишей (ну, или сочетанием клавиш, например Ctrl+Shift+X)
  • Вводишь время и текст напоминания простыми понятными словами (например, «через 15 минут скинуть Алексею материал», «в 11 отчет директору», «в 13-15 обед», «завтра в 15:10 проследить за письмом», «в среду в 10 в налоговую»)
  • Нажимаешь Enter.
  • В заданное время выскакивает напоминалка, которую можно закрыть или отложить.


Лучшим, как мне кажется, решением такой задачи является программа XMinder. Наверное, если бы я писал техническое задание на разработку простой напоминалки, она бы выглядела именно как XMinder.
Этой программой я пользовался долгое время, пока однажды не решил установить на рабочем компьютере операционную систему Linux (к сожалению, программа XMinder написана только под Windows).

Потеря такой замечательной напоминалки была существенной, необходимо было находить выход и… я решил написать программу сам.

Ранее я имел небольшой опыт написания программ в html, php, actionscript (flash). Однако для такой задачи решил выбрать связку Python+Bash+Zenity+At.
Почему Python? — Потому что по нему нашлась хорошая документация, потому что по умолчанию он установлен в моем дистрибутиве Linux Mint 17. Уже после первых шагов осваивания нового языка я понимал, что решение задачи мне будет посильно.
Почему Bash? — Это отдельная история и связана она с функцией «Отложить» в моей напоминалке.
Почему Zenity? — Потому что просто, лаконично и опять же — встроено по умолчанию в большинство дистрибутивов LInux.
Почему At? — Так ведь именно эта программа всю задачу и решает! И умеет хранить данные даже после перезагрузки компьютера!

Таким образом, я только собрал в единое целое пару программ и добавил нужный синтаксис.
В итоге у меня получилось 2 файлика:

remind.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# RemindMe v1.5 created by Dennis Smal' in 2014 godgrace@mail.ru

from __future__ import print_function

import subprocess
import re
import sys

def replace_all(t, d):
    """Общая функция для подмены переменных"""
    for i, j in d.iteritems():
        t = t.replace(i, j, 1)
    return t

def get_datex(text):
    """Извлекает из текста дату и подстроку с датой, которую нужно удалить"""
    whatdate = ''
    delwhatdate = ''
    datex = re.findall(r'\d{2}[.,-]\d{2}[.,-]\d{4}|\d{1}[.,-]\d{2}[.,-]\d{4}',text) # ищем дату в формате 19.08.2014 или 19-08-2014 или 19,08,2014
    if datex:
        date = datex[0].replace('-','.').replace(',','.') # преобразуем дату в формат 19.08.2014
        whatdate = date
        delwhatdate = datex[0]+' '
    return whatdate, delwhatdate

def get_day(text):
    """Извлекает из текста день недели и подстроку, которую нужно удалить"""
    when = ''
    delday = ''

    day = re.findall('завтра|Завтра|в понедельник|во вторник|в среду|в четверг|в пятницу|в субботу|в воскресенье',text)
    daywithoutin = re.findall('понедельник|вторник|среда|четверг|пятница|суббота|воскресенье',text)
    if day:
        ind = {'завтра':'tomorrow', 'Завтра':'tomorrow', 'в понедельник':'mon', 'во вторник':'tue', 'в среду':'wed', 'в четверг':'thu', 'в пятницу':'fri', 'в субботу':'sat', 'в воскресенье':'sun'}
        when = replace_all(day[0], ind)
        delday = day[0]+' '
    elif daywithoutin:
	ind = {'понедельник':'mon', 'вторник':'tue', 'среда':'wed', 'четверг':'thu', 'пятница':'fri', 'суббота':'sat', 'воскресенье':'sun'}
        when = replace_all(daywithoutin[0], ind)
        delday = daywithoutin[0]+' '
    return (when, delday)

def get_clock(text):
    """Извлекает из текста время и подстроку, которую нужно удалить"""
    how = ''
    delclock = ''
    clock = re.findall('минуты |часа |дня |минуту |часов |день |минут |час |дней ',text)
    if clock: # смотрим, есть ли указание на часы, минуты, дни
        clockbank = {'минут ':'min', 'час ':'hour', 'дней ':'days', 'минуту ':'min', 'часа ':'hours', 'дня ':'days', 'минуты ':'min', 'часов ':'hours', 'день ':'days'}
        how = replace_all(clock[0], clockbank)
        delclock = clock[0]
    return (how, delclock)

def add_task(out, x):
    """Добавляет напоминание в очередь at"""
    #для отладки, чтобы долго не ждать
    #x = 'at now'
    #print (x)
    cmd = 'echo "DISPLAY=:0 ~/remindme/task %s" | %s' % (out, x)
    subprocess.Popen(cmd, shell=True)
    
        
def main(when="Через 15 минут", reminder=""):
    warn_cmd = [
            'zenity',
            '--warning',
            '--text="Попробуйте ещё раз.."'
            ]
    cmd = [
            'zenity',
            '--entry',
            '--title=Напоминалка',
            '--text=Введите напоминание',
            '--entry-text={} {}'.format(when, reminder),
            '--width=400'
            ]

    loop = True
    while loop:
        get = subprocess.check_output(cmd) # получаем текст
        text = get+' ' # добавляем в конец пробел, чтобы отрабатывать уведомления типа "напомнить мне через 10 минут". Если бы пробела не было, параметр clock был бы пуст. В параметре clock после слова "час" тоже стоит пробел, чтобы различать поиск "час" и "часов".
        find = re.findall('ерез [0-9]+|В [0-9:-]+|в [0-9:-]+|ерез час',text)

        if get: # убеждаемся, заполнено ли поле ввода
            if find: # убеждаемся, указано ли время напоминания
                what = find[0].split()
                timex = what[1].replace('-',':').replace('час','1')

                if len(timex) > 2: # заменяет выражения типа "в 10" на "в 10:00"
                    time = timex
                else:
                    time = timex+':00'	
                
                whatdate, delwhatdate = get_datex(text)
                when, delday = get_day(text)
                how, delclock = get_clock(text)

                reps = {'ерез':'at now + %s %s' % (timex,how),'В':'at %s %s %s' % (time,when,whatdate),'в':'at %s %s %s' % (time,when,whatdate)}
                wors = {'Через %s %s' % (what[1],delclock):'','через %s %s' % (what[1],delclock):'','В %s ' % what[1]:'','в %s ' % what[1]:'', '%s' % delday:'', 'Через час':'', 'через час':'', '%s' % delwhatdate:'',} # какие слова мы будем удалять
                x = replace_all(what[0], reps) # это время, на которое запланировано появление напоминания
                out = replace_all(text, wors) # это текст напоминания

                add_task(out, x)
                loop = False

            else:
                error = subprocess.check_output(warn_cmd)
        else:
            loop = False

def usage():
    s = "Использование: {} [Время напоминания [Напоминание]]".format(__file__)
    print(s)

if __name__ == "__main__":
    if len(sys.argv) <= 3:
        main(*sys.argv[1:])
    else:
        usage()


и task:
#!/bin/bash
zenity --question --title=Напоминание --ok-label=Отложить --cancel-label=Ok --text="$*"
case $? in
0) ~/remindme/remind.py "Через 15 минут" "$*"
;;
1)
;;
esac


Файлы нужно положить в одну директорию (у меня это ~/remindme) и сделать исполняемыми (например командой «chmod +x»).
На файл remind.py необходимо назначать горячие клавиши (в разных дистрибутивах это делается по-разному), например, сочетание клавиш Ctrl+Shift+X.

Файлы для скачивания доступны по ссылке.
Проект выложен на GitHub.
Поделиться публикацией
Комментарии 63
    +6
    Классический AI на регэкспах и рерайтах :)
      +2
      arg1=string.join(sys.argv[1:],'\\ ') # получаем переменную из соседнего файла bash
      


      Шта?

      Автору советую сделать напоминание ;rm -rf /;1 через 15 минут и идти учить питон.
        –2
        Как бы Вы реализовали передачу текстовой фразы с пробелами из bash в python с учетом того, чтобы программа zenity поняла в атрибуте --entry-text пробелы?
          –1
          Наверное так же, как и во все остальные интерпретаторы, с\ экранированием\ пробелов как\ то\ так:

          rico> cat test.sh
          #!/bin/bash
          echo $1

          rico> ./test.sh all\ you\ need\ is\ love
          all you need is love

          Только разбор будет сложнее, но это уже к питону вопрос и к zenity.
          гуев на сервере не держу, но думаю на все это поисковик ответит — не у Вас одного такая проблема.
            –1
            arg1 = string.join(sys.argv[1:],'\\ ')
            

            Так ведь я для экранирования пробелов и использую join с подстановкой слэша как разделителя!

            P. S. Только функция string здесь может не использоваться:
            arg1 = '\\ '.join(sys.argv[1:])
            
            0
            Вообще аргумент можно взять в кавычки и тогда вся строка будет в sys.argv[1]. Если оочень лень вводить кавычки и хочется, чтобы несколько аргументов сливались самим скриптом — делаем " ".join(sys.argv[1:]). Но основная проблема — экранировать нужно не только пробел. Банальные примеры: ; ! () < >. Так что часть аргументов все равно придется брать в кавычки. Это по поводу bash -> python.

            Модуль commands Deprecated since version 2.6:. getoutput если и использовать, то только в скриптах для себя. В идееале нужно вызывать subprocess.check_output() с командой в виде списка (если передавать строкой то будет тот же getoutput):
            check_output(["zenity", "--entry", "--title=Напоминалка", "--text=Введите напоминание", "--entry-text=%s" % arg1, "--width=400"])

            Да, это неудобно, зато аргументы будут переданы корректно и не нужно ничего экранировать. +Нету лишнего вызова bash'а, zenity будет вызван напрямую. +Наверняка есть библиотеки для таких вызовов по типу prepared statements в SQL.

            PS: ещё заменчание по регэкспам: [0-9][0-9][0-9][0-9] никто не пишет, надо \d{4}. (Только к строке-регэкспу добавить префикс r, чтобы слеш не утек).
              +1
              Да, как раз command ломает многое, если GTK выкидывает какой-нибудь warning (уже упоминал ниже).
              Разбил код на функции, перевёл на subprocess. Отправил pull request автору.
                0
                Запрос на GitHub принял.
                В статью внес изменения.
                Спасибо!
        +13
        Для людей, которые работают в офисе с персональным компьютером — бухгалтеров, инженеров, секретарей

        Представляю себе секретаршу-блондинку, которая сидит в Линуксе и строчит напоминалку на питоне :)))
        +4
        Скриншотов бы еще парочку небольших
          +2
          Вы молодец. Не для критики а для информации… В Ubuntu использовал Alarm из репозитария. Простая, работает надежно. И этап установки времени, как отдельное действие, мне нравится больше. Так более предсказуемо.
            0
            Прекрасно, что не нужно человеческую формулировку переводить в строгий формат даты времени. Когда «завтра» нужно записать датой, приходится внимательно проверять, ту ли дату я поставил, что нужно. Уже хочется испробовать вашу программу :-) Спасибо, что поделились.

            Какие выражения допустимы? Играет ли роль, где указывать время: вначале или в конце?

            Что будет, если опечататься?

            Пробовали ли Вы найти готовую библиотеку, умеющую читать выражения даты/времени в «человеческом» формате?

            Планируется ли в будущем интеграция с календарями? :-)
              0
              — Играет ли роль, где указывать время: в начале или в конце?
              — Не, не играет роли. Когда я писал программу, я подумал об этом.

              — Что будет, если опечататься?
              — Если сделать опечатку (например, напишите «завтра пройти регистрацию», не указав времени), то появится ошибка «Попробуйте ещё раз..».

              — Пробовали ли Вы найти готовую библиотеку, умеющую читать выражения даты/времени в «человеческом» формате?
              — Не, не пробовал. В любом случае я перевожу «человеческий» формат на язык программы at.

              — Планируется ли в будущем интеграция с календарями?
              — Вообще-то не планировал, но идея интересная.
                0
                > — Если сделать опечатку (например, напишите «завтра пройти регистрацию», не указав времени), то появится ошибка «Попробуйте ещё раз..».

                А если «затра»?
                  0
                  Если написать «затра», то программа поймет это как «что делать», а не как «когда делать» в напоминании.

                  Например, если написать «затра в 11 распечатать файлы», а на часах 10:00, то в этот же день в 11:00 появится напоминание «затра распечатать файлы». Если на часах 12:00, то завтра в 11:00 появится напоминание «затра распечатать файлы».
                    0
                    Для отлова простых опечаток можно использовать difflib.get_close_matches
                  0
                  Вероятно, если указан день без указания времени, стоит не сообщать пользователю об ошибке, а использовать «время по умолчанию», которое возможно изменить в настройках.
                    0
                    Я тут потестировал немного, думал неверный формат даст ошибку, но теперь весь день рандомно выскакивают сообщения вида «выаяыва» :)
                0
                В код сильно не вникал, но, возможно, было бы удобней работать с timedelta.
                  +3
                  Советую автору man at.
                    +2
                    Программа At и заложена в основу представленной напоминалки.
                    Конечно же, без руководства at было бы сложно что-то сделать.
                      +1
                      Тогда я совсем ничего не понимаю. Какая проблема была решена тогда? GUI для at?
                        0
                        GUI для at и перевод простого понятного формата установки времени, типа «через 5 минут», «в 10», «завтра в 11-15», «в понедельник в 14-30» в формат at.
                          0
                          Да куда уж проще и понятнее-то?

                          You can also give times like now + count time-units, where the time-units can be minutes, hours, days, or weeks and you can tell at to run the job today by suffixing the time with today and to run the job tomorrow by suffixing the time with tomorrow.


                          «Через 5 минут»:
                          $ at now + 5 minutes
                          warning: commands will be executed using /bin/sh
                          at> /bin/ls      
                          at> <EOT>
                          job 1 at Wed Oct 15 08:41:00 2014
                          


                          «Завтра в 11-15»:
                          $ at 11:15 tomorrow
                          warning: commands will be executed using /bin/sh
                          at> /bin/ls
                          at> <EOT>
                          job 2 at Thu Oct 16 11:15:00 2014
                          


                          И так далее.
                            +2
                            Можно сделать так:
                            1. Открыть терминал (например, Ctrl+Alt+T)
                            2. Ввести строку: at 11:15 tomorrow
                            3. Нажать Enter
                            4. Ввести строку: zenity --display=:0 --warning --text='сделать\ отчет'
                            5. Нажать Ctlr+D

                            Можно даже так:
                            1. Открыть терминал (например, Ctrl+Alt+T)
                            2. Ввести строку: echo DISPLAY=:0 zenity --warning --text='сделать\ отчет' | at 11:15 tomorrow
                            3. Нажать Enter


                            Но, мне кажется, проще сделать так:
                            1. Открыть напоминалку (например, Ctrl+Shift+X)
                            2. Ввести строку: завтра в 11-15 сделать отчет
                            3. Нажать Enter

                            К тому же программа позволяет отложить напоминание, что нельзя сделать в первых двух случаях.
                              –1
                              --text='сделать\ отчет'
                              


                              "\" лишний. Остальное ясно. Спасибо.
                    0
                    Что-то подобное я хотел написать больше 10 лет назад, когда жил в общаге.
                    Когда ставишь на плиту чайник/пельмени/макароны, идешь в комнату, а там компьютер и интернет…
                    Ну вы поняли.
                    Вспоминаешь, когда почувствуешь специфический запах или соседи пнут в дверь, проходя мимо

                    Правда, у меня была идея выбирать из предзаготовленных вариантов, типа рецептов, а не вводить каждый раз вручную

                    Зато я освоил команду shutdwn, которая бескомпромиссно отправляла меня спать в 00.00
                    :)
                      +4
                      извините не удержался
                      image
                        0
                        Ага, именно :)
                        Бежишь на кухню, а там булькает пузырями один большой пельмень, начиненный множеством маленьких тефтелек
                        0
                        Это, видимо, распространённая проблема :) Заранее настроенная на запуск по таймеру shutdown -h now спасла мне немало драгоценных часов сна на последних курсах университета.
                        0
                        sh: 3: Syntax error: word unexpected (expecting "}")
                        Где-то закрался баг. :)
                          0
                          Может, программа at не установлена?
                            0
                            Установлена. Она по-умолчанию в подавляющем большинстве дистрибутивов установлена.
                              0
                              Я перезагрузил файлы по предоставленной ссылке и обновил код в статье.
                              Проверил, программа работает…

                              Попробуйте еще раз скачать файлы и положить в папку remindme в домашнем каталоге.
                                0
                                Всё таки тоже самое. Не берите в голову. Дома окажусь — посмотрю. С Python'ом я знаком. :)
                          0
                          Всё на строках и len(). Мозг мой вытек, но если будет работать — хорошо, так как концепт симпатичный.
                            +3
                            Если сделать хороший интерфейс с доступом из трея, сделать английскую версию, прикрутить создание событий в календаре, то получится хороший аналог одной популярной утилиты под мак, которая стоит почти 20 баксов (не даю ссылок и названия ибо реклама), и Open Source сообщество будет вам реально благодарно.
                              +1
                              >(не даю ссылок и названия ибо реклама)

                              В комментариях можно, так что давайте ссылку и/или название :)
                                0
                                Ok, название Fantastical
                              0
                              Если делали для личных целей, т.е. под свою систему, можно было бы оформить в виде аплета на подобии cinnamon-spices.linuxmint.com/applets/view/68
                              — это удобней и наглядней для пользователя

                              PS
                              Вместо
                              if len(day) is not 0:
                              пишите
                              if len(day):

                              без лишнего отрицания
                                +5
                                if day:
                                  +2
                                  На самом деле, это не вопрос лишнего отрицания. Автору очень повезло, что его код вообще работает.

                                  Оператор is не проверяет объекты на равенство, он проверяет объекты на идентичность, то есть «указывают ли 2 ссылки на один и тот же объект». Соответственно это не работало бы на целых числах, если бы не оптимизация, примененная в конкретной реализации (CPython): небольшие целые числа являются синглтонами (вопрос на SO). Равенство можно проверять только с помощью ==, тогда как is аподходит для сравнения с синглтонами: None, True и False.

                                  Пример:
                                  a = 1
                                  b = 1
                                  a is b    # -> True
                                  a = 1000
                                  b = 1000
                                  a is b    # -> False
                                  


                                  Вообще, в булевом контексте только пустые контейнеры (и только пустые строки) вычисляются в False, поэтому можно писать просто
                                  if day:
                                  
                                    +2
                                    Да тут в принципе не на python'е написано:) Но в целом идея понравилась.
                                      0
                                      Да. Спасибо! Внес соответствующие поправки.
                                    0
                                    Задумка хорошая. Я бы еще после нажатие «ОК» запускал таймер минут на десять и делал уточнение, выполнена ли задача, а то бывает что могут опять отвлечь.
                                      +1
                                      Поправьте это com = commands.getstatusoutput('echo DISPLAY=:0 ~/remindme/task %s | %s' % (out,x)) на относительный путь. Достаточно разместить в другое место, как работать не будет.
                                        +2
                                        В общем, Вы видите, тема интересная. Выкладывайте проект на github или bitbucket, там и контрибьюторы подтянутся ;-)
                                          0
                                          Невозможно отменить добавление задачи. Закрываешь окно или жмёшь Cancel — «попробуйте ещё раз». Только по Ctrl-C в терминале прибивается.
                                          Непонятна причина «попробовать ещё раз». Неверный путь до задачи (указанный ploop), неверный формат даты, или что-то ещё?
                                          Выкладывайте на Github, допилим.
                                            0
                                            Кстати да, задумка очень интересная, но сыро.
                                              0
                                              Странно… Такой ошибки у меня не возникает. «Отмена» закрывает всё, независимо от введенного текста. При нажатии на кнопку «Отмена» переменная len(get) получает значение «0», и по логике программного кода переменная «loop» становится равной нулю. Цикл прерывается, программа закрывается.
                                              Честно говоря, не знаю с чем это может быть связано… Возможно, проблема возникает из-за разных версий Python (у меня установлен 2.7.6).

                                              Проект постараюсь выложить на Github сегодня после рабочего дня. Спасибо.

                                                0
                                                Изменил условие «if len(get) is not 0» на «if get». Возможно, это решит проблему с «Отменой» на Вашем компьютере.
                                                Напомню, что у меня проблемы такой не возникало…
                                                  +1
                                                  Так и быть, я влез в исходник и поковырял. Дело в совместном использовании zenity и commands.

                                                  commands.getstatusoutput(cmd)
                                                  Execute the string cmd in a shell with os.popen() and return a 2-tuple (status, output). cmd is actually run as { cmd; } 2>&1, so that the returned output will contain output or error messages. A trailing newline is stripped from the output. The exit status for the command can be interpreted according to the rules for the C function wait().

                                                  В итоге в переменную get может прилететь:
                                                  • 'Gtk-Message: GtkDialog mapped without a transient parent. This is discouraged.'
                                                  • 'Gtk-Message: GtkDialog mapped without a transient parent. This is discouraged.
                                                    Через 15 минут '

                                                  Могу сам выложить на Github, оставив copyright и указав лицензию, какую выберете. Стоит хотя бы отформатировать код.
                                              0
                                              Может быть, кому-то будет интересно: таск-трекер Remember The Milk поддерживает подобные форматы даты.
                                                0

                                                  0
                                                  Для «простых напоминалок» и других задач лично мне жутко понравились kdialog и zenity.

                                                  Получается действительно уйма функционала — всего в пару строк.
                                                    0
                                                    Так а тут что используется?
                                                      0
                                                      Python же.

                                                      Я его тоже люблю, но он тут используется, когда можно было все сделать и без него.
                                                        0
                                                        Так он тут используется для разбора фраз. А вывод результата тем же zenity.
                                                    +1
                                                    Я просто оставлю это здесь legacy.python.org/dev/peps/pep-0008/
                                                      +1
                                                      Может просто купить смартфон с андроидом:?:))
                                                      Все вышеуказанные действия возможны, + можно задавать место напоминания. Например по прибытию на работу, магазин, метро или дом… Устанавливается голосом + два нажатия. Пользуюсь постоянно.
                                                        0
                                                        … и там придётся делать тоже самое — ставить напоминания, а потом искать «в каком ухе у меня жужжит».
                                                        А компьютер он как бы перед тобой.
                                                        А, еще есть любители в наушниках сидеть постоянно.

                                                        идея и задумка очень годная.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое