Compare commits

..

22 Commits
1.0.1 ... main

Author SHA1 Message Date
6ebadf1d23 Add a config.h parameter for base TMPDIR 2023-11-28 10:00:29 +01:00
fec4fd0e22 Add tests 2023-11-28 09:59:58 +01:00
3ad74e9428 Remove libnotify dependency for main binary 2023-10-27 11:19:53 +02:00
e03dff6589 Oops, I removed the sqlite3_step in 42d9d1e 2023-10-15 11:43:47 +02:00
6c8c2f7192 log socket path 2023-10-04 17:24:48 +02:00
7d445ee27b Override configuration with env variables 2023-10-04 17:15:15 +02:00
9cae1fbebb README: update dependencies 2023-10-03 22:05:19 +02:00
22b90691b5 Check if category is NULL before copying it 2023-10-03 18:19:23 +02:00
93e29137f8 Update .gitignore 2023-10-03 18:18:57 +02:00
5ba5f05019 Actualiser README.md 2023-09-29 20:06:32 +02:00
a60055ff7e Forgot to update daemon.c 2023-09-29 18:51:12 +02:00
42d9d1e5c0 Add categories to task\n\nVersion bump, make sure to update your database scheme 2023-09-29 17:54:59 +02:00
9a5fbc66dd Add 'category' to db schema 2023-09-20 16:55:30 +02:00
1545e3140d main:Fix segfault when socket does not exist 2023-07-17 09:45:38 +02:00
778d75396e Add gnome-autostart make rule 2023-07-16 16:01:38 +02:00
162c59c7da daemon: get notified of db changes 2023-07-16 13:07:49 +02:00
c5b616f5e7 Add utils.c 2023-07-16 13:04:37 +02:00
114c5f901f daemon: send basic notifications 2023-07-16 11:07:04 +02:00
be6c4fa9c5 Add desktop notifications code 2023-07-16 11:06:32 +02:00
61de4adf18 Change repository structure and add daemon.c/log.c 2023-07-16 10:19:36 +02:00
a21bac2c61 Add reschedule command 2023-07-16 09:54:32 +02:00
68e7a88e5c -lsqlite3 is only needed during linkage 2023-07-13 12:55:26 +02:00
20 changed files with 817 additions and 118 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
takl.sqlite3 takl.sqlite3
takl takl
takl-daemon
takl.sock
out out
.vscode .vscode

View File

@ -1,24 +1,47 @@
FLAGS = -g -Wall -Wextra -lsqlite3 FLAGS = -g -Wall -Wextra -DLOG_USE_COLOR
LD_FLAGS = LD_FLAGS = -lsqlite3 -lpthread
NOTIF_FLAGS = `pkg-config --cflags --libs libnotify`
$(shell mkdir -p out) $(shell mkdir -p out)
all: takl takl-daemon
out/%.o: src/%.c out/%.o: src/%.c
$(CC) -c $(FLAGS) $^ -o $@ $(CC) -c $(FLAGS) $^ -o $@
takl: out/main.o out/db.o out/tasks.o out/notification.o: src/main/notification.c
$(CC) -c $(FLAGS) $(NOTIF_FLAGS) $^ -o $@
out/%.o: src/main/%.c
$(CC) -c $(FLAGS) $^ -o $@
out/%.o: src/log/%.c
$(CC) -c $(FLAGS) $^ -o $@
takl: out/main.o out/db.o out/tasks.o out/utils.o
$(CC) $(FLAGS) $(LD_FLAGS) $^ -o $@ $(CC) $(FLAGS) $(LD_FLAGS) $^ -o $@
takl-daemon: out/daemon.o out/db.o out/tasks.o out/utils.o out/log.o out/notification.o
$(CC) $(FLAGS) $(LD_FLAGS) $(NOTIF_FLAGS) $^ -o $@
install: takl
sudo cp takl /usr/bin/takl
uninstall: takl install: takl takl-daemon
sudo rm /usr/bin/takl sudo cp takl takl-daemon /usr/bin/
uninstall:
-sudo rm /usr/bin/takl
-sudo rm /usr/bin/takl-daemon
-sudo rm ~/.config/autostart/takl-daemon.desktop
gnome-install: install
mkdir -p ~/.config/autostart
cp takl-daemon.desktop ~/.config/autostart/
clean: clean:
rm out -r -rm out -r
rm ./takl -rm ./takl
-rm ./takl-daemon

View File

@ -5,7 +5,7 @@ facile d'utilisation et donnant rapidement les choses demandées
### Installation ### Installation
Les seules dépendances nécessaires sont `gcc` et `g++`. Les seules dépendances nécessaires sont `gcc` et `libnotify`.
(Peut-être que des bibliothèques comme sqlite3 sont à installer sur certaines distributions/ OS) (Peut-être que des bibliothèques comme sqlite3 sont à installer sur certaines distributions/ OS)
```bash ```bash
@ -15,18 +15,26 @@ make install # installation dans /usr/bin/takl
Vous pouvez ensuite utiliser la commande `takl` pour ajouter/ modifier des items à la todo list. Vous pouvez ensuite utiliser la commande `takl` pour ajouter/ modifier des items à la todo list.
Pour recevoir des notifications lorsqu'une tâche arrive à échéance, il faut lancer `takl-daemon` en arrière-plan
dans un environnement similaire à l'utilisateur (Un service systemd ou une tâche cron auront besoin de plus
d'informations pour pouvoir envoyer des notifications).
Pour gnome, `make gnome-install` lancera le programme à tous les prochains redémarrages.
### Utilisation ### Utilisation
``` ```
Utilisation: takl ( list | add | info | rm | done ) Utilisation: takl ( list | add | reschedule | info | rm | done )
list [-a:voir les tâches complétées] list [category] [-a:voir les tâches complétées]
add <task> [date] add [category:]<task> [date]
Format de la date: reschedule <id> [date]
Relatif: min+%d, h+%d, j+%d
Absolu: dd/mm (pas de changement d'année pour l'instant)
info <id1> <id2> ... info <id1> <id2> ...
done <id1> <id2> ... done <id1> <id2> ...
rm <id1> <id2> ... rm <id1> <id2> ...
Format des dates:
Relatif: min+%d, h+%d, j+%d
Absolu: dd/mm (pas de changement d'année pour l'instant)
``` ```
Appeler l'exécutable sans arguments vous renverra cette petite liste de commandes que vous pouvez utiliser. Appeler l'exécutable sans arguments vous renverra cette petite liste de commandes que vous pouvez utiliser.

172
src/daemon.c Normal file
View File

@ -0,0 +1,172 @@
/*
* Petit daemon qui va envoyer des notifications aux horaires des tâches
* il vérifie si la base de données change par l'intermédiaire d'un socket,
* (implémenté par un fichier stocké dans /tmp/takl.$USER), qui contient le pid
* de ce daemon afin de pouvoir déterminer si un autre daemon tourne.
* (Le cas deux daemons sont lancés en même temps n'est pas traité.)
*/
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include "log/log.h"
#include "main/include/db.h"
#include "main/include/tasks.h"
#include "main/include/utils.h"
#include "main/include/colors.h"
#include "main/include/config.h"
#include "main/include/notification.h"
#define EVENT_SIZE ( sizeof (struct inotify_event) )
#define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) )
bool has_changed = false;
task_list_t* tasks;
pthread_mutex_t tasks_lock = PTHREAD_MUTEX_INITIALIZER; // locks tasks and has_changed at the same time
void* inotify_check_changes(void* sk_path) {
char* socket_path = (char*)sk_path;
char buffer[EVENT_BUF_LEN];
while (1) { // Regarde si /tmp/takl.$USER a changé (donc si la BDD a changé et doit être rechargée)
int fd = inotify_init();
if (fd < 0) {
log_fatal("Impossible d'initialiser inotify");
free(socket_path);
exit(1);
}
int wd = inotify_add_watch(fd, socket_path, IN_ACCESS);
int length = read(fd, buffer, EVENT_BUF_LEN);
if (length < 0) {
log_fatal("inotify: lecture impossible");
free(socket_path);
exit(1);
}
log_debug("Base de données changée");
pthread_mutex_lock(&tasks_lock);
free_task_list(tasks);
tasks = get_task_list(false, false);
has_changed = true;
pthread_mutex_unlock(&tasks_lock);
inotify_rm_watch(fd, wd);
close(fd);
}
return NULL;
}
int existing_takl_daemon(char* socket_path) {
if (!access(socket_path, F_OK)) { // tmp file exists
FILE* fp = fopen(socket_path, "r+");
int daemon_pid;
fscanf(fp, "%d", &daemon_pid);
fclose(fp);
if(!kill(daemon_pid, 0)) {
free(socket_path);
return true; // The process is running
}
}
FILE* fp = fopen(socket_path, "w");
fprintf(fp, "%d", getpid());
fclose(fp);
return false;
}
int main() {
bool notified_no_change = false;
char* socket_path = get_socket_path();
log_debug("Socket path:%s", socket_path);
if (!existing_takl_daemon(socket_path)) {
log_info("TaKl " VERSION " -- Daemon started");
} else {
log_info("TaKl Daemon déjà en cours d'exécution. Arrêt");
return 1;
}
tasks = get_task_list(false, false);
pthread_t inotify_id = 0; // Lancement d'inotify dans un autre fil
pthread_create(&inotify_id, NULL, inotify_check_changes, (void*)socket_path);
while (1) {
pthread_mutex_lock(&tasks_lock);
task_t next = next_task(tasks);
pthread_mutex_unlock(&tasks_lock);
if (next.due_to == 0) {
if (!notified_no_change)
log_debug("Plus de tâches avec échéance dans la liste");
notified_no_change = true;
while (1) { // sleep until change
sleep(5);
pthread_mutex_lock(&tasks_lock);
if (has_changed) {
log_debug("Changement détecté");
has_changed = false;
pthread_mutex_unlock(&tasks_lock);
break;
}
pthread_mutex_unlock(&tasks_lock);
}
} else {
time_t now = time(0);
double wait = difftime(next.due_to, now) + 5;
log_debug("Prochaine tâche dans %0.1lfs", wait);
notified_no_change = false;
while (difftime(next.due_to, now) >= 5) {
sleep(5);
pthread_mutex_lock(&tasks_lock);
if (has_changed) {
log_debug("Changement détecté");
has_changed = false;
pthread_mutex_unlock(&tasks_lock);
break;
}
pthread_mutex_unlock(&tasks_lock);
now = time(0);
}
if (difftime(next.due_to, now) < 5) { // Exit not du e to the break statement
log_debug("Envoi de la notification tâche [%d]", next.id);
desktop_notification(next);
}
//! si deux tâches sont à la même date à moins de 0.1s près,
//! l'une des deux seulement sera affichée
sleep(3); // Attendre un peu pour ne pas reconsidérer la même tâche 1063 fois.
}
}
pthread_join(inotify_id, NULL);
free_task_list(tasks);
free(socket_path);
return 0;
}

168
src/log/log.c Normal file
View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 2020 rxi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include "log.h"
#define MAX_CALLBACKS 32
typedef struct {
log_LogFn fn;
void *udata;
int level;
} Callback;
static struct {
void *udata;
log_LockFn lock;
int level;
bool quiet;
Callback callbacks[MAX_CALLBACKS];
} L;
static const char *level_strings[] = {
"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
};
#ifdef LOG_USE_COLOR
static const char *level_colors[] = {
"\x1b[94m", "\x1b[36m", "\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m"
};
#endif
static void stdout_callback(log_Event *ev) {
char buf[16];
buf[strftime(buf, sizeof(buf), "%H:%M:%S", ev->time)] = '\0';
#ifdef LOG_USE_COLOR
fprintf(
ev->udata, "%s %s%-5s\x1b[0m \x1b[90m%s:%d:\x1b[0m ",
buf, level_colors[ev->level], level_strings[ev->level],
ev->file, ev->line);
#else
fprintf(
ev->udata, "%s %-5s %s:%d: ",
buf, level_strings[ev->level], ev->file, ev->line);
#endif
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void file_callback(log_Event *ev) {
char buf[64];
buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
fprintf(
ev->udata, "%s %-5s %s:%d: ",
buf, level_strings[ev->level], ev->file, ev->line);
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void lock(void) {
if (L.lock) { L.lock(true, L.udata); }
}
static void unlock(void) {
if (L.lock) { L.lock(false, L.udata); }
}
const char* log_level_string(int level) {
return level_strings[level];
}
void log_set_lock(log_LockFn fn, void *udata) {
L.lock = fn;
L.udata = udata;
}
void log_set_level(int level) {
L.level = level;
}
void log_set_quiet(bool enable) {
L.quiet = enable;
}
int log_add_callback(log_LogFn fn, void *udata, int level) {
for (int i = 0; i < MAX_CALLBACKS; i++) {
if (!L.callbacks[i].fn) {
L.callbacks[i] = (Callback) { fn, udata, level };
return 0;
}
}
return -1;
}
int log_add_fp(FILE *fp, int level) {
return log_add_callback(file_callback, fp, level);
}
static void init_event(log_Event *ev, void *udata) {
if (!ev->time) {
time_t t = time(NULL);
ev->time = localtime(&t);
}
ev->udata = udata;
}
void log_log(int level, const char *file, int line, const char *fmt, ...) {
log_Event ev = {
.fmt = fmt,
.file = file,
.line = line,
.level = level,
};
lock();
if (!L.quiet && level >= L.level) {
init_event(&ev, stderr);
va_start(ev.ap, fmt);
stdout_callback(&ev);
va_end(ev.ap);
}
for (int i = 0; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) {
Callback *cb = &L.callbacks[i];
if (level >= cb->level) {
init_event(&ev, cb->udata);
va_start(ev.ap, fmt);
cb->fn(&ev);
va_end(ev.ap);
}
}
unlock();
}

49
src/log/log.h Normal file
View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2020 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See `log.c` for details.
*/
#ifndef LOG_H
#define LOG_H
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <time.h>
#define LOG_VERSION "0.1.0"
typedef struct {
va_list ap;
const char *fmt;
const char *file;
struct tm *time;
void *udata;
int line;
int level;
} log_Event;
typedef void (*log_LogFn)(log_Event *ev);
typedef void (*log_LockFn)(bool lock, void *udata);
enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL };
#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__)
#define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__)
#define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__)
#define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)
const char* log_level_string(int level);
void log_set_lock(log_LockFn fn, void *udata);
void log_set_level(int level);
void log_set_quiet(bool enable);
int log_add_callback(log_LogFn fn, void *udata, int level);
int log_add_fp(FILE *fp, int level);
void log_log(int level, const char *file, int line, const char *fmt, ...);
#endif

View File

@ -3,22 +3,24 @@
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include "include/colors.h" #include "main/include/colors.h"
#include "include/config.h" #include "main/include/config.h"
#include "include/tasks.h" #include "main/include/tasks.h"
#include "include/db.h" #include "main/include/db.h"
void help(char* name) { void help(char* name) {
printf(BLUE "\t-- TaKl " VERSION " --\n" RESET); printf(BLUE "\t-- TaKl " VERSION " --\n" RESET);
printf("Utilisation: %s ( list | add | info | rm | done )\n", name); printf("Utilisation: %s ( list | add | reschedule | info | rm | done )\n", name);
printf("\tlist [-a:voir les tâches complétées]\n"); printf("\tlist [category] [-a:voir les tâches complétées]\n");
printf("\tadd <task> [date]\n"); printf("\tadd [category:]<task> [date]\n");
printf("\tFormat de la date:\n"); printf("\treschedule <id> [date]\n");
printf("\t\tRelatif: min+%%d, h+%%d, j+%%d\n");
printf("\t\tAbsolu: dd/mm (pas de changement d'année pour l'instant)\n");
printf("\tinfo <id1> <id2> ...\n"); printf("\tinfo <id1> <id2> ...\n");
printf("\tdone <id1> <id2> ...\n"); printf("\tdone <id1> <id2> ...\n");
printf("\trm <id1> <id2> ...\n"); printf("\trm <id1> <id2> ...\n");
printf("\nFormat des dates:\n");
printf("\tRelatif: min+%%d, h+%%d, j+%%d\n");
printf("\tAbsolu: dd/mm (pas de changement d'année pour l'instant)\n");
} }
@ -33,7 +35,7 @@ time_t parseDateTime(const char* input) {
date.tm_year = current_time->tm_year; date.tm_year = current_time->tm_year;
date.tm_mon = current_time->tm_mon; date.tm_mon = current_time->tm_mon;
date.tm_mday = current_time->tm_mday; date.tm_mday = current_time->tm_mday;
date.tm_hour = current_time->tm_hour; date.tm_hour = current_time->tm_hour-1; //TODO: buggy in non-UTC
date.tm_min = current_time->tm_min; date.tm_min = current_time->tm_min;
date.tm_sec = current_time->tm_sec; date.tm_sec = current_time->tm_sec;
@ -86,21 +88,80 @@ int add(int argc, char* argv[]) {
task_t t = create_task(argv[2], due_to); task_t t = create_task(argv[2], due_to);
if (t.id == -1) { if (t.id == -1) {
if (t.category) {
free(t.category);
free(t.text);
}
return 1; return 1;
} }
if (add_task(t) != 0) { if (add_task(t) != 0) {
printf("Erreur lors de l'ajout à la base de données\n"); printf("Erreur lors de l'ajout à la base de données\n");
if (t.category) {
free(t.category);
free(t.text);
}
return 1; return 1;
} }
if (t.category) {
free(t.category);
free(t.text);
}
printf(BOLD YELLOW "[%d]" RESET " ajoutée\n", t.id); printf(BOLD YELLOW "[%d]" RESET " ajoutée\n", t.id);
return 0; return 0;
} }
int list_tasks(bool show_completed) { int reschedule(int argc, char* argv[]) {
task_list_t* tasks = get_task_list(show_completed); if (argc < 3) {
print_task_list(tasks, show_completed); // show_completed: true would work as all the tasks are not loaded if false printf(BOLD YELLOW "Argument manquant\n" RESET);
help(argv[0]);
return 1;
}
int id = strtol(argv[2], NULL, 10);
task_t t;
t.id = -1;
get_task(id, &t);
if (t.id == -1) {
printf(YELLOW "Tâche " BOLD "[%d]" RESET YELLOW " inexistante. Vérifiez que l'identifiant est bien correct\n" RESET, id);
return 1;
}
time_t due_to = parseDateTime(argv[3]);
if (due_to == -1) {
printf(RED "Échéance invalide, impossible de reprogrammer la tâche.\n" RESET);
return 1;
}
t.due_to = due_to;
if (update_task(t) != 0) {
printf("Erreur lors de la modification de la tâche\n");
return 1;
}
return 0;
}
int list_tasks(int argc, char* argv[]) {
bool show_completed = false;
char* category = NULL;
for (int i=2; i < argc; i++) {
if (!strcmp(argv[i], "-a")) {
show_completed = true;
} else {
category = argv[i];
}
}
task_list_t* tasks = get_task_list(category, show_completed);
print_task_list(tasks, !category, show_completed); // show_completed: true would work as all the tasks are not loaded if false
free_task_list(tasks); free_task_list(tasks);
return 0; return 0;
@ -128,6 +189,9 @@ int info(int argc, char* argv[]) {
print_task(t); print_task(t);
free(t.text); free(t.text);
if (t.category) {
free(t.category);
}
} }
return 0; return 0;
@ -195,8 +259,10 @@ int main(int argc, char* argv[]) {
if (!strcmp(argv[1], "add")) { if (!strcmp(argv[1], "add")) {
return add(argc, argv); return add(argc, argv);
} else if (!strcmp(argv[1], "reschedule")) {
return reschedule(argc, argv);
} else if (!strcmp(argv[1], "list")) { } else if (!strcmp(argv[1], "list")) {
return list_tasks(argc > 2 && !strcmp(argv[2], "-a")); return list_tasks(argc, argv);
} else if (!strcmp(argv[1], "info")) { } else if (!strcmp(argv[1], "info")) {
return info(argc, argv); return info(argc, argv);
} else if (!strcmp(argv[1], "done")) { } else if (!strcmp(argv[1], "done")) {

View File

@ -11,11 +11,31 @@ Fonctions d'interaction avec la base de données
#include "include/colors.h" #include "include/colors.h"
#include "include/config.h" #include "include/config.h"
#include "include/struct.h" #include "include/struct.h"
#include "include/utils.h"
#include "include/db.h"
#define sqlCheck(ret) { sqlAssert(ret, db, __FILE__, __LINE__); }
void sqlAssert(int ret, sqlite3* db, char* file, int line) {
if (ret != SQLITE_OK) {
printf( "%s:%d, error %d: %s\n", file, line, ret, sqlite3_errmsg(db));
exit(1);
}
}
char* get_db_path() { char* get_db_path() {
#ifndef DB_FILE /*
//! We should check that $HOME/.config already exists even if it should be the case Checks if $TAKL_DB env variable is set
else, the DB is located at $HOME/.config/takl.sqlite3
*/
char* env_db_path = getenv("TAKL_DB");
if (env_db_path) {
char* db_path = malloc(sizeof(char)*(strlen(env_db_path)+1));
strcpy(db_path, env_db_path);
return db_path;
} else {
char* base_path = ".config/takl.sqlite3"; char* base_path = ".config/takl.sqlite3";
char* home_dir = getenv("HOME"); char* home_dir = getenv("HOME");
@ -25,12 +45,7 @@ char* get_db_path() {
sprintf(db_path, "%s/%s", home_dir, base_path); sprintf(db_path, "%s/%s", home_dir, base_path);
return db_path; return db_path;
#else }
char* db_path = malloc(sizeof(char)*(strlen(DB_FILE)+1));
memcpy(db_path, DB_FILE, sizeof(char)*(strlen(DB_FILE)+1));
return db_path;
#endif
} }
@ -48,6 +63,7 @@ sqlite3* get_db() {
"CREATE TABLE tasks ( \ "CREATE TABLE tasks ( \
id INTEGER PRIMARY KEY,\ id INTEGER PRIMARY KEY,\
text TEXT NOT NULL,\ text TEXT NOT NULL,\
category TEXT DEFAULT NULL, \
done INTEGER, \ done INTEGER, \
due_to INTEGER \ due_to INTEGER \
);", );",
@ -75,46 +91,49 @@ sqlite3* get_db() {
return db; return db;
} }
void notify_change() {
char* socket_path = get_socket_path();
if (access(socket_path, F_OK) == 0) {
FILE* fp = fopen(socket_path, "r");
int pid;
fscanf(fp, "%d", &pid); // Trigger IN_ACCESS
(void)pid;
fclose(fp);
}
free(socket_path);
}
int add_task(task_t t) { int add_task(task_t t) {
sqlite3* db = get_db(); sqlite3* db = get_db();
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
int ret = sqlite3_prepare_v2(db, "INSERT INTO tasks (id, text, done, due_to) VALUES (?1, ?2, ?3, ?4);", -1, &stmt, 0); sqlCheck( sqlite3_prepare_v2(db, "INSERT INTO tasks (id, text, category, done, due_to) VALUES (?1, ?2, ?3, ?4, ?5);", -1, &stmt, 0) );
if (ret != SQLITE_OK) {
printf("(get_task) failure fetching data\n");
return 1;
}
char str_id[10]; char str_id[10];
sprintf(str_id, "%d", (int)t.id); sprintf(str_id, "%d", (int)t.id);
sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC) );
sqlite3_bind_text(stmt, 2, t.text, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 2, t.text, -1, SQLITE_STATIC) );
sqlCheck( sqlite3_bind_text(stmt, 3, t.category, -1, SQLITE_STATIC) );
char str_done[2]; // Just a boolean char str_done[2]; // Just a boolean
sprintf(str_done, "%d", t.done); sprintf(str_done, "%d", t.done);
sqlite3_bind_text(stmt, 3, str_done, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 4, str_done, -1, SQLITE_STATIC) );
char str_due_to[15]; char str_due_to[15];
sprintf(str_due_to, "%ld", t.due_to); sprintf(str_due_to, "%ld", t.due_to);
sqlite3_bind_text(stmt, 4, str_due_to, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 5, str_due_to, -1, SQLITE_STATIC) );
ret = sqlite3_step(stmt); sqlite3_step(stmt);
sqlite3_finalize(stmt); sqlCheck( sqlite3_finalize(stmt) );
sqlite3_close(db); sqlCheck( sqlite3_close(db) );
notify_change();
if (ret != SQLITE_DONE) {
if (ret == 19) {
printf("Identifiant déjà utilisé\n");
} else {
printf("Return value: %d\n", ret);
}
return 1;
}
return 0; return 0;
} }
@ -122,36 +141,28 @@ int update_task(task_t t) {
sqlite3* db = get_db(); sqlite3* db = get_db();
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
int ret = sqlite3_prepare_v2(db, "UPDATE tasks SET text=?2, done=?3, due_to=?4 WHERE id=?1;", -1, &stmt, 0); sqlCheck( sqlite3_prepare_v2(db, "UPDATE tasks SET text=?2, done=?3, due_to=?4 WHERE id=?1;", -1, &stmt, 0) );
if (ret != SQLITE_OK) {
printf("(get_task) failure fetching data\n");
return 1;
}
char str_id[10]; char str_id[10];
sprintf(str_id, "%d", (int)t.id); sprintf(str_id, "%d", (int)t.id);
sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC) );
sqlite3_bind_text(stmt, 2, t.text, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 2, t.text, -1, SQLITE_STATIC) );
char str_done[2]; // Just a boolean char str_done[2]; // Just a boolean
sprintf(str_done, "%d", t.done); sprintf(str_done, "%d", t.done);
sqlite3_bind_text(stmt, 3, str_done, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 3, str_done, -1, SQLITE_STATIC) );
char str_due_to[15]; char str_due_to[15];
sprintf(str_due_to, "%ld", t.due_to); sprintf(str_due_to, "%ld", t.due_to);
sqlite3_bind_text(stmt, 4, str_due_to, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 4, str_due_to, -1, SQLITE_STATIC) );
sqlite3_step(stmt);
ret = sqlite3_step(stmt); sqlCheck( sqlite3_finalize(stmt) );
sqlCheck( sqlite3_close(db) );
notify_change();
sqlite3_finalize(stmt);
sqlite3_close(db);
if (ret != SQLITE_DONE) {
printf("Return value: %d\n", ret);
return 1;
}
return 0; return 0;
} }
@ -160,21 +171,17 @@ void delete_task(int id) {
sqlite3* db = get_db(); sqlite3* db = get_db();
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
int ret = sqlite3_prepare_v2(db, "DELETE FROM tasks WHERE id=?1;", -1, &stmt, 0); sqlCheck( sqlite3_prepare_v2(db, "DELETE FROM tasks WHERE id=?1;", -1, &stmt, 0) );
if (ret != SQLITE_OK) {
printf("(get_task) failure fetching data\n");
exit(1);
}
char str_id[10]; char str_id[10];
sprintf(str_id, "%d", id); sprintf(str_id, "%d", id);
sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC) );
sqlite3_step(stmt); sqlite3_step(stmt);
sqlite3_finalize(stmt); sqlCheck( sqlite3_finalize(stmt) );
sqlite3_close(db); sqlCheck( sqlite3_close(db) );
notify_change();
} }
@ -184,19 +191,13 @@ void get_task(int id, task_t* t) {
sqlite3* db = get_db(); sqlite3* db = get_db();
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
int ret = sqlite3_prepare_v2(db, "SELECT id, text, done, due_to FROM tasks WHERE id=?1;", -1, &stmt, 0); sqlCheck( sqlite3_prepare_v2(db, "SELECT id, text, done, due_to, category FROM tasks WHERE id=?1;", -1, &stmt, NULL) );
if (ret != SQLITE_OK) {
printf("(get_task) failure fetching data\n");
exit(1);
}
char str_id[10]; char str_id[10];
sprintf(str_id, "%d", id); sprintf(str_id, "%d", id);
sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC); sqlCheck( sqlite3_bind_text(stmt, 1, str_id, -1, SQLITE_STATIC) );
ret = sqlite3_step(stmt); int ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW) { if (ret == SQLITE_ROW) {
t->id = strtol((char*)sqlite3_column_text(stmt, 0), NULL, 10); t->id = strtol((char*)sqlite3_column_text(stmt, 0), NULL, 10);
t->done = strtol((char*)sqlite3_column_text(stmt, 2), NULL, 10); t->done = strtol((char*)sqlite3_column_text(stmt, 2), NULL, 10);
@ -205,24 +206,33 @@ void get_task(int id, task_t* t) {
char* text = (char*)sqlite3_column_text(stmt, 1); char* text = (char*)sqlite3_column_text(stmt, 1);
t->text = malloc(sizeof(char)*(strlen(text)+1)); t->text = malloc(sizeof(char)*(strlen(text)+1));
strcpy(t->text, text); strcpy(t->text, text);
char* category = (char*)sqlite3_column_text(stmt, 4);
if (category) {
t->category = malloc(sizeof(char)*(strlen(category)+1));
strcpy(t->category, category);
} else {
t->category = NULL;
}
} }
sqlite3_finalize(stmt); sqlCheck( sqlite3_finalize(stmt) );
sqlite3_close(db); sqlCheck( sqlite3_close(db) );
// t->id will be (-1) if not found // t->id will be (-1) if not found
} }
task_list_t* get_task_list(bool include_completed) { task_list_t* get_task_list(char* input_category, bool include_completed) {
sqlite3* db = get_db(); sqlite3* db = get_db();
sqlite3_stmt *stmt; sqlite3_stmt *stmt;
int ret = sqlite3_prepare_v2(db, "SELECT id, text, done, due_to FROM tasks;", -1, &stmt, NULL); if (!input_category) {
sqlCheck( sqlite3_prepare_v2(db, "SELECT id, text, done, due_to, category FROM tasks;", -1, &stmt, NULL) );
if (ret != SQLITE_OK) { } else {
printf("(get_task) failure fetching data\n"); sqlCheck( sqlite3_prepare_v2(db, "SELECT id, text, done, due_to, category FROM tasks WHERE category=?1;", -1, &stmt, NULL) );
return NULL; sqlCheck( sqlite3_bind_text(stmt, 1, input_category, -1, SQLITE_STATIC) );
} }
task_list_t* list = NULL; task_list_t* list = NULL;
while (sqlite3_step(stmt) == SQLITE_ROW) { while (sqlite3_step(stmt) == SQLITE_ROW) {
@ -237,6 +247,15 @@ task_list_t* get_task_list(bool include_completed) {
t.text = malloc(sizeof(char)*(strlen(text)+1)); t.text = malloc(sizeof(char)*(strlen(text)+1));
strcpy(t.text, text); strcpy(t.text, text);
char* category = (char*)sqlite3_column_text(stmt, 4);
if (category) {
t.category = malloc(sizeof(char)*(strlen(category)+1));
strcpy(t.category, category);
} else {
t.category = NULL;
}
task_list_t* cur = malloc(sizeof(task_list_t)); // Add the parsed task to the beginning of the list task_list_t* cur = malloc(sizeof(task_list_t)); // Add the parsed task to the beginning of the list
cur->task = t; cur->task = t;
cur->next = list; cur->next = list;
@ -244,8 +263,8 @@ task_list_t* get_task_list(bool include_completed) {
} }
} }
sqlite3_finalize(stmt); sqlCheck( sqlite3_finalize(stmt) );
sqlite3_close(db); sqlCheck( sqlite3_close(db) );
return list; return list;
} }

View File

@ -1,12 +1,11 @@
#ifndef DEF_CONFIG_H #ifndef DEF_CONFIG_H
#define DEF_CONFIG_H #define DEF_CONFIG_H
#define VERSION "1.0.1" #define VERSION "1.3.5"
// By default, $HOME/.config/takl.sqlite3 is used. You can change this behaviour here
//#define DB_FILE "takl.sqlite3"
#define MAX_TASK_ID 10000 // max is set to MAX_TASK_ID-1 #define MAX_TASK_ID 10000 // max is set to MAX_TASK_ID-1
#define NEW_TASK_ID_MAX_RETRIES 10000 // number of retries before giving up #define NEW_TASK_ID_MAX_RETRIES 10000 // number of retries before giving up
#define TMPDIR "/tmp" // Some Unix systems don't use /tmp as tmp dir (eg Android)
#endif #endif

View File

@ -9,6 +9,11 @@ Arrête le programme en cas d'échec
*/ */
void create_db(); void create_db();
/*
Open/closes the socket file to notify of db changes
*/
void notify_change();
/* /*
Ajouter une tâche à la base de données. Ajouter une tâche à la base de données.
Le numéro de tâche ne doit pas déjà être utilisé Le numéro de tâche ne doit pas déjà être utilisé
@ -35,7 +40,7 @@ void get_task(int id, task_t* t);
/* /*
Renvoie la liste des tâches de la base de données Renvoie la liste des tâches de la base de données
*/ */
task_list_t* get_task_list(bool include_completed); task_list_t* get_task_list(char* input_category, bool include_completed);
/* /*
Renvoie un identifiant de tâche encore non utilisé Renvoie un identifiant de tâche encore non utilisé

View File

@ -0,0 +1,10 @@
#ifndef DEF_NOTIFICATION_H
#define DEF_NOTIFICATION_H
#include "tasks.h"
/*
Envoyer une notification avec t
*/
void desktop_notification(task_t t);
#endif

View File

@ -7,6 +7,7 @@
struct task { struct task {
int id; int id;
char* text; char* text;
char* category;
bool done; bool done;
time_t due_to; time_t due_to;
}; };

View File

@ -3,6 +3,12 @@
#ifndef DEF_TASKS_H #ifndef DEF_TASKS_H
#define DEF_TASKS_H #define DEF_TASKS_H
/*
Sépare la chaîne d'entrée en deux chaînes, l'une avant, l'autre après le premier ':'.
Si il n'y a pas de ':', out_category est NULL
*/
void parse_input(char* in_text, char** out_text, char** out_category);
/* /*
Renvoie une tâche avec un identifiant aléatoire valide Renvoie une tâche avec un identifiant aléatoire valide
*/ */
@ -16,7 +22,7 @@ void print_task(task_t task);
/* /*
Affiche une liste de tâches de manière compacte (et par catégorie: urgent, à faire, [complétée]) Affiche une liste de tâches de manière compacte (et par catégorie: urgent, à faire, [complétée])
*/ */
void print_task_list(task_list_t* list, bool show_completed); void print_task_list(task_list_t* list, bool show_category, bool show_completed);
/* /*
Libère la mémoire allouée à une liste de tâches Libère la mémoire allouée à une liste de tâches

14
src/main/include/utils.h Normal file
View File

@ -0,0 +1,14 @@
#include <pthread.h>
#include "struct.h"
/*
Renvoie le chemin d'accès au socket (/tmp/takl.$USER sauf si défini différemment dans la config)
*/
char* get_socket_path();
/*
Renvoie l'élément arrivant dans le moins de temps d'une liste de tâches.
*/
task_t next_task(task_list_t* tasks);

11
src/main/notification.c Normal file
View File

@ -0,0 +1,11 @@
#include <libnotify/notify.h>
#include "include/notification.h"
void desktop_notification(task_t t) {
notify_init ("TaKl");
NotifyNotification * Notif = notify_notification_new ("TaKl Daemon", t.text, "dialog-information");
notify_notification_show (Notif, NULL);
g_object_unref(G_OBJECT(Notif));
notify_uninit();
}

View File

@ -4,19 +4,46 @@ Fonctions utilitaires concernant les tâches
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <time.h> #include <time.h>
#include <string.h>
#include "include/db.h" #include "include/db.h"
#include "include/struct.h" #include "include/struct.h"
#include "include/colors.h" #include "include/colors.h"
void parse_input(char* in_text, char** out_text, char** out_category) {
int n = strlen(in_text);
int i=0;
for (;i < n; i++) {
if (in_text[i] == ':') {
break;
}
}
if (i == n) {
*out_text = in_text;
*out_category = NULL;
} else {
*out_text = malloc(sizeof(char)*(n-i));
*out_category = malloc(sizeof(char)*i);
memcpy(*out_text, (void*)in_text+((i+1)*sizeof(char)), (size_t)((n-i)*sizeof(char)));
memcpy(*out_category, in_text, (size_t)(i*sizeof(char)));
(*out_category)[i] = '\0';
}
}
task_t create_task(char* text, time_t due_to) { task_t create_task(char* text, time_t due_to) {
task_t task; task_t task;
task.id = get_new_task_id(); task.id = get_new_task_id();
task.text = text;
task.done = false; task.done = false;
task.due_to = due_to; task.due_to = due_to;
task.text = NULL;
task.category = NULL;
parse_input(text, &(task.text), &(task.category));
return task; return task;
} }
@ -24,6 +51,9 @@ task_t create_task(char* text, time_t due_to) {
void print_task(task_t task) { void print_task(task_t task) {
printf(YELLOW "=== Tâche " BOLD "[%d]" RESET YELLOW " ===\n" RESET, task.id); printf(YELLOW "=== Tâche " BOLD "[%d]" RESET YELLOW " ===\n" RESET, task.id);
if (task.category) {
printf(BLUE "%s:" RESET, task.category);
}
printf(BOLD "%s\n" RESET, task.text); printf(BOLD "%s\n" RESET, task.text);
printf("Statut: "); printf("Statut: ");
@ -52,32 +82,40 @@ void print_task(task_t task) {
} }
void print_task_list(task_list_t* list, bool show_completed) { void print_single_task(task_t t, bool show_category, char* color) {
if (t.category && show_category) {
printf(BOLD "%s[%d]" RESET " " BLUE "%s:" RESET "%s\n", color, t.id, t.category, t.text);
} else {
printf(BOLD "%s[%d]" RESET " %s\n", color, t.id, t.text);
}
}
void print_task_list(task_list_t* list, bool show_category, bool show_completed) {
task_list_t* cur = list; task_list_t* cur = list;
time_t now = time(0); time_t now = time(0);
while (cur) { // Show not completed red tasks first while (cur) { // Show not completed red tasks first
task_t t = cur->task; task_t t = cur->task;
if (!t.done && difftime(now, t.due_to) >= 0 && t.due_to != 0) { if (!t.done && difftime(now, t.due_to) >= 0 && t.due_to != 0) {
printf(BOLD RED "[%d]" RESET " %s\n", t.id, t.text); // Task not completed but should be ! print_single_task(t, show_category, RED); // Task not completed but should be !
} }
cur = cur->next; cur = cur->next;
} }
cur = list; cur = list;
while (cur) { // Show not completed but not due_to then while (cur) { // Show not completed but not due_to now then
task_t t = cur->task; task_t t = cur->task;
if (!t.done && (difftime(now, t.due_to) <= 0 || t.due_to == 0)) { if (!t.done && (difftime(now, t.due_to) <= 0 || t.due_to == 0)) {
printf(BOLD YELLOW "[%d]" RESET " %s\n", t.id, t.text); // Task not completed but should be ! print_single_task(t, show_category, YELLOW); // Task not completed but fine
} }
cur = cur->next; cur = cur->next;
} }
if (show_completed) { if (show_completed) {
cur = list; cur = list;
while (cur) { // Show not completed but not due_to then while (cur) { // Show completed
task_t t = cur->task; task_t t = cur->task;
if (t.done) { if (t.done) {
printf(BOLD GREEN "[%d]" RESET " %s\n", t.id, t.text); // Task not completed but should be ! print_single_task(t, show_category, GREEN); // Task already done
} }
cur = cur->next; cur = cur->next;
} }

59
src/main/utils.c Normal file
View File

@ -0,0 +1,59 @@
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <pthread.h>
#include "include/config.h"
#include "include/struct.h"
char* get_socket_path() {
/*
Checks if $TAKL_SOCKET env variable is set
else, the socket is located at /tmp/takl
*/
char* env_socket_path = getenv("TAKL_SOCKET");
if (env_socket_path) {
char* socket_path = malloc(sizeof(char)*(strlen(env_socket_path)+1));
strcpy(socket_path, env_socket_path);
return socket_path;
} else {
char* base_path = TMPDIR "/takl";
char* username = getenv("USER");
if (!username)
username = getenv("USERNAME");
assert(username != NULL);
char* socket_path = malloc(sizeof(char)*(strlen(base_path)+strlen(username)+1));
sprintf(socket_path, "%s.%s", base_path, username);
return socket_path;
}
}
task_t next_task(task_list_t* tasks) {
time_t now = time(0);
task_t next;
next.due_to = 0;
task_list_t* cur = tasks;
while (cur) {
task_t t = cur->task;
if (!t.done && (difftime(now, t.due_to) <= 0 || t.due_to == 0)) { // La tâche a une échéance, qui n'est pas passée
if (next.due_to == 0 || difftime(next.due_to, t.due_to) >= 0) {
next = t;
}
}
cur = cur->next;
}
return next;
}

7
takl-daemon.desktop Normal file
View File

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Exec=/usr/bin/takl-daemon
Hidden=false
Terminal=false
X-GNOME-Autostart-enabled=true
Name=TaKl Notifier Daemon

42
tests/main.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
TMPDIR=$(mktemp -d)
#! On ne teste pas que l'on peut bien récupérer ces chemins automatiquement,
#! ce qui peut-être une source d'erreurs
export TAKL_DB="$TMPDIR/takl.sqlite3"
export TAKL_SOCKET="$TMPDIR/takl.socket"
echo "Using $TMPDIR"
make -j
# Catégories
./takl add "nocategory"
./takl add "category1:test1"
# Dates
./takl add "date:avec date" min+5
./takl add "date:avec date2" h+5
./takl add "date:avec date3" j+5
./takl add "date:avec date4" "31/12"
TASK_ID=$(./takl add "done soon" | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g' | awk '-F[' '{ print $2 }' | awk '-F]' '{ print $1 }')
# List
./takl list
./takl list date
# Reschedule
./takl reschedule $TASK_ID "22/11"
# Get info
./takl info $TASK_ID
# Mark as done
./takl done $TASK_ID
./takl list -a
./takl rm $TASK_ID
#* À rajouter: tests sur le fait que les changements ont bien eu lieu
rm $TMPDIR -r