Recurring events, sort events
This commit is contained in:
parent
8b8de8f82d
commit
bc2f07b1cf
5 changed files with 478 additions and 144 deletions
2
Makefile
2
Makefile
|
@ -5,7 +5,7 @@ TARGET = ssm
|
|||
PREFIX ?= /usr/local
|
||||
BINDIR = $(PREFIX)/bin
|
||||
|
||||
CFLAGS += -std=c99 -pedantic -Wall -D_POSIX_C_SOURCE=200809L
|
||||
CFLAGS += -std=c99 -pedantic -Wall -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE
|
||||
|
||||
SRC = ssm.c
|
||||
OBJS = $(SRC:.c=.o)
|
||||
|
|
15
README.md
15
README.md
|
@ -1,5 +1,5 @@
|
|||
# ssm
|
||||
Simple Schedule Manager(ssm) is a highly scriptable scheduler.
|
||||
Simple Schedule Manager(ssm) is a highly scriptable scheduler. It supports recurring events and support daily, weekly, monthly events.
|
||||
|
||||
# Usage
|
||||
```
|
||||
|
@ -7,15 +7,14 @@ Simple Scheduler Manager 1.0.0
|
|||
|
||||
Usage: ssm <command>
|
||||
|
||||
help Show this help message
|
||||
sched <time> <title> [description] Schedule an event
|
||||
edit Edit schedule with $EDITOR
|
||||
list <timerange> List all upcoming events
|
||||
search Search for events
|
||||
run Spawn notifier daemon
|
||||
help Show this help message
|
||||
sched <time> <title> <description> [options] Schedule an event
|
||||
edit Edit schedule with $EDITOR
|
||||
list List all upcoming events
|
||||
run Spawn notifier daemon
|
||||
```
|
||||
# Dependencies
|
||||
- libnotify
|
||||
None
|
||||
|
||||
# Building
|
||||
You will need to run these with elevated privilages.
|
||||
|
|
5
TODO
5
TODO
|
@ -1,5 +0,0 @@
|
|||
Show "today events: " then a list of events for the day
|
||||
Show "upcoming events: " then a list of events for the next 7 days in format of "in one day... in two day"
|
||||
notify send when any reminder is close to occurence
|
||||
Represent recurring events using recursion for generating occurrences
|
||||
Sort events/tasks based on start time, end time, priority, etc
|
559
ssm.c
559
ssm.c
|
@ -1,29 +1,21 @@
|
|||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <sys/inotify.h>
|
||||
|
||||
#include <libnotify/notify.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ssm.h"
|
||||
|
||||
#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))
|
||||
#define BUF_LEN (10 * (sizeof(struct inotify_event) + PATH_MAX + 1))
|
||||
#define MAX_LINE_LEN 4096
|
||||
#define SECONDS_PER_DAY 86400
|
||||
#define NOTIFICATION_THRESHOLD 300 /* 5 minutes */
|
||||
|
||||
typedef struct {
|
||||
time_t timestamp;
|
||||
int alert; /* in ms */
|
||||
char name[50];
|
||||
char description[100];
|
||||
int notified;
|
||||
} event;
|
||||
|
||||
char *
|
||||
get_database_path(void)
|
||||
char *get_database_path(void)
|
||||
{
|
||||
char *db = DATABASE_PATH;
|
||||
char *db_path;
|
||||
|
@ -34,7 +26,11 @@ get_database_path(void)
|
|||
return NULL;
|
||||
}
|
||||
db_path = malloc((strlen(db) + strlen(home)) * sizeof(char));
|
||||
|
||||
if (db_path == NULL) {
|
||||
perror("malloc");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* replace ~ with home */
|
||||
snprintf(db_path, strlen(db) + strlen(home), "%s%s", home, db + 1);
|
||||
} else {
|
||||
|
@ -43,38 +39,39 @@ get_database_path(void)
|
|||
return db_path;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
FULL, // YYYY-MM-DD HH:MM:SS
|
||||
HHMM, // HH:MM
|
||||
} datefmt;
|
||||
|
||||
char *
|
||||
convert_timestamp(time_t timestamp, datefmt format)
|
||||
/*
|
||||
* Convert time_t into heap-allocated string
|
||||
*/
|
||||
char *convert_timestamp(time_t timestamp, datefmt format)
|
||||
{
|
||||
struct tm *time_info = localtime(×tamp);
|
||||
char *time_buf = malloc(20 * sizeof(char));
|
||||
if (time_buf == NULL) {
|
||||
perror("malloc");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
switch(format) {
|
||||
case FULL:
|
||||
strftime(time_buf, 20, "%Y-%m-%d %H:%M:%S", time_info);
|
||||
break;
|
||||
case HHMM:
|
||||
strftime(time_buf, 6, "%H:%M", time_info);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Invalid datefmt\n");
|
||||
case FULL:
|
||||
strftime(time_buf, 20, "%Y-%m-%d %H:%M:%S", time_info);
|
||||
break;
|
||||
case HHMM:
|
||||
strftime(time_buf, 6, "%H:%M", time_info);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Invalid datefmt\n");
|
||||
}
|
||||
return time_buf;
|
||||
}
|
||||
|
||||
void
|
||||
load_events(event **events, int *num_events)
|
||||
void load_events(event **events, int *num_events)
|
||||
{
|
||||
char *db_path = get_database_path();
|
||||
if (db_path == NULL)
|
||||
return;
|
||||
FILE *file = fopen(db_path, "r");
|
||||
if (!file) {
|
||||
fprintf(stderr, "Cannot open database file: %s\n", db_path);
|
||||
perror("fopen");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
@ -92,15 +89,21 @@ load_events(event **events, int *num_events)
|
|||
|
||||
fseek(file, 0, SEEK_SET);
|
||||
for (int i = 0; fgets(line, sizeof(line), file); i++) {
|
||||
sscanf(line, "%ld\t%[^\t]\t%[^\n]", &(*events)[i].timestamp, (*events)[i].name, (*events)[i].description);
|
||||
sscanf(line, "%ld\t%49[^\t]\t%99[^\t]\t%d\t%d\t%d\t%ld\n",
|
||||
&(*events)[i].timestamp, (*events)[i].name, (*events)[i].description,
|
||||
&(*events)[i].priority, &(*events)[i].recurrence, &(*events)[i].recurrence_interval,
|
||||
&(*events)[i].recurrence_end);
|
||||
(*events)[i].notified = 0;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
free(db_path);
|
||||
}
|
||||
|
||||
void
|
||||
add_event(time_t timestamp, char *name, char *description)
|
||||
void add_event(time_t timestamp, const char *name,
|
||||
const char *description, priority_type priority,
|
||||
recurrence_type recurrence, int recurrence_interval,
|
||||
time_t recurrence_end)
|
||||
{
|
||||
char *db_path = get_database_path();
|
||||
FILE *file;
|
||||
|
@ -110,151 +113,454 @@ add_event(time_t timestamp, char *name, char *description)
|
|||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
fprintf(file, "%ld\t%s\t%s\n", timestamp, name, description);
|
||||
fprintf(file, "%ld\t%s\t%s\t%d\t%d\t%d\t%ld\n", timestamp, name, description, priority, recurrence, recurrence_interval, recurrence_end);
|
||||
fclose(file);
|
||||
free(db_path);
|
||||
char *time_buf = convert_timestamp(timestamp, 0);
|
||||
printf("Added \"%s\" with description \"%s\" at \"%s\"\n", name, description, time_buf);
|
||||
char *time_buf = convert_timestamp(timestamp, 1);
|
||||
if (recurrence != NONE) {
|
||||
char *end_buf = convert_timestamp(recurrence_end, 1);
|
||||
printf("Added \"%s\" with description \"%s\" at \"%s\" (recurs every %d %s until %s)\n", name, description, time_buf, recurrence_interval,
|
||||
recurrence == DAILY ? "day" :
|
||||
recurrence == WEEKLY ? "week" :
|
||||
recurrence == MONTHLY ? "month" : "year", end_buf);
|
||||
free(end_buf);
|
||||
return;
|
||||
} else {
|
||||
printf("Added \"%s\" with description \"%s\" at \"%s\"\n", name, description, time_buf);
|
||||
}
|
||||
free(time_buf);
|
||||
}
|
||||
|
||||
static void
|
||||
send_notification(event to_alert)
|
||||
static void send_notification(event to_alert)
|
||||
{
|
||||
int name_len = strlen(to_alert.name);
|
||||
char name_buf[name_len + 7]; /* 7 for "ssm - " and NULL*/
|
||||
snprintf(name_buf, name_len + 7, "ssm - %s", to_alert.name);
|
||||
|
||||
int description_len = strlen(to_alert.description);
|
||||
char *time_buf = convert_timestamp(to_alert.timestamp, 1);
|
||||
char description_buf[description_len + 6 + 3]; /* 1 space, 1 comma, 1 NULL */
|
||||
snprintf(description_buf, description_len + 9, "%s, %s", to_alert.description, time_buf);
|
||||
char description_buf[512];
|
||||
snprintf(description_buf, sizeof(description_buf),
|
||||
"Event \"%s\" starts at %s - %s", to_alert.name, time_buf,
|
||||
to_alert.description);
|
||||
|
||||
free(time_buf);
|
||||
NotifyNotification *notification = notify_notification_new(name_buf, description_buf, "dialog-information");
|
||||
|
||||
if (notification == NULL) {
|
||||
perror("notify_notification_new");
|
||||
int pid = fork();
|
||||
if (pid == 0) {
|
||||
/* Child */
|
||||
execlp(notifier, notifier, "Upcoming event", description_buf, NULL);
|
||||
_exit(1);
|
||||
} else if (pid > 0) {
|
||||
/* Parent */
|
||||
} else {
|
||||
perror("fork");
|
||||
}
|
||||
if (!notify_notification_show(notification, NULL)) {
|
||||
perror("notify_notification_show");
|
||||
}
|
||||
g_object_unref(G_OBJECT(notification));
|
||||
}
|
||||
|
||||
void
|
||||
check_events(event *events, int *num_events)
|
||||
void check_events(event *events, int *num_events)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
for (int i = 0; i < *num_events; i++) {
|
||||
if (events[i].timestamp > now && events[i].notified == 0) {
|
||||
if (!events[i].notified &&
|
||||
events[i].timestamp > now &&
|
||||
events[i].timestamp - now <= NOTIFICATION_THRESHOLD) {
|
||||
|
||||
send_notification(events[i]);
|
||||
events[i].notified = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
list_events(event *events, int *num_events)
|
||||
static char *get_relative_day(time_t event_time)
|
||||
{
|
||||
load_events(&events, num_events);
|
||||
for (int i = 0; i < *num_events; i++) {
|
||||
printf("Timestamp: %ld\nName: %s\nDescription: %s\n\n", events[i].timestamp, events[i].name, events[i].description);
|
||||
}
|
||||
free(events);
|
||||
time_t now = time(NULL);
|
||||
int days = (event_time - now) / SECONDS_PER_DAY;
|
||||
static char buffer[32];
|
||||
|
||||
if (days == 0) return "today";
|
||||
else if (days == 1) return "tomorrow";
|
||||
else sprintf(buffer, "in %d days", days);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void
|
||||
watch_file(event *events, int *num_events)
|
||||
int is_today(time_t t)
|
||||
{
|
||||
if (notify_init("ssm") < 0) {
|
||||
perror("notify_init");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
int inotify_fd = inotify_init1(IN_NONBLOCK);
|
||||
if (inotify_fd == -1) {
|
||||
perror("inotify_init1");
|
||||
exit(EXIT_FAILURE);
|
||||
time_t now = time(NULL);
|
||||
struct tm *time_tm = localtime(&t);
|
||||
struct tm *now_tm = localtime(&now);
|
||||
return (time_tm->tm_year == now_tm->tm_year &&
|
||||
time_tm->tm_mon == now_tm->tm_mon &&
|
||||
time_tm->tm_mday == now_tm->tm_mday);
|
||||
}
|
||||
|
||||
void print_event(event e, int relative)
|
||||
{
|
||||
char *time_str = convert_timestamp(e.timestamp, relative ? FULL : HHMM);
|
||||
if (relative) {
|
||||
char *relative_day = get_relative_day(e.timestamp);
|
||||
printf("%s (%s): %s - %s\n", time_str, relative_day, e.name, e.description);
|
||||
} else {
|
||||
printf("%s: %s - %s\n", time_str, e.name, e.description);
|
||||
}
|
||||
free(time_str);
|
||||
}
|
||||
|
||||
char *db_path = get_database_path();
|
||||
int wd = inotify_add_watch(inotify_fd, db_path, IN_MODIFY);
|
||||
free(db_path);
|
||||
if (wd == -1) {
|
||||
perror("inotify_add_watch");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
void list_today_events(event *events, int num_events)
|
||||
{
|
||||
printf("\nToday's events:\n");
|
||||
printf("---------------\n");
|
||||
int found = 0;
|
||||
|
||||
char buf[BUF_LEN] __attribute__ ((aligned(__alignof__(struct inotify_event))));
|
||||
|
||||
load_events(&events, num_events);
|
||||
check_events(events, num_events);
|
||||
free(events);
|
||||
|
||||
while (1) {
|
||||
ssize_t len = read(inotify_fd, buf, BUF_LEN);
|
||||
if (len == -1 && errno != EAGAIN) {
|
||||
perror("read");
|
||||
exit(EXIT_FAILURE);
|
||||
for (int i = 0; i < num_events; i++) {
|
||||
if (is_today(events[i].timestamp)) {
|
||||
print_event(events[i], 0);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (len <= 0) {
|
||||
sleep(1);
|
||||
continue;
|
||||
if (!found) {
|
||||
printf("No events scheduled for today\n");
|
||||
}
|
||||
}
|
||||
|
||||
void list_upcoming_events(event *events, int num_events)
|
||||
{
|
||||
printf("\nUpcoming events (next 28 days):\n");
|
||||
printf("------------------------------\n");
|
||||
time_t now = time(NULL) + SECONDS_PER_DAY;
|
||||
time_t week_later = now + (28 * SECONDS_PER_DAY);
|
||||
int found = 0;
|
||||
|
||||
for (int i = 0; i < num_events; i++) {
|
||||
if (events[i].timestamp > now && events[i].timestamp <= week_later) {
|
||||
print_event(events[i], 1);
|
||||
found = 1;
|
||||
}
|
||||
/* There is modification */
|
||||
load_events(&events, num_events);
|
||||
check_events(events, num_events);
|
||||
free(events);
|
||||
}
|
||||
|
||||
for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + ((struct inotify_event *) ptr)->len) {
|
||||
struct inotify_event *event = (struct inotify_event *) ptr;
|
||||
if (event->mask & IN_MODIFY) {
|
||||
printf("Detected modification in database file\n");
|
||||
if (!found) {
|
||||
printf("No upcoming events in the next 7 days\n");
|
||||
}
|
||||
}
|
||||
|
||||
void expand_recurring_events(event **events, int *num_events)
|
||||
{
|
||||
int new_count = *num_events;
|
||||
time_t now = time(NULL);
|
||||
|
||||
/* Count how many new events we'll need */
|
||||
for (int i = 0; i < *num_events; i++) {
|
||||
if ((*events)[i].recurrence != NONE) {
|
||||
time_t next_occurrence = (*events)[i].timestamp;
|
||||
while (next_occurrence <= (*events)[i].recurrence_end) {
|
||||
if (next_occurrence >= now) {
|
||||
new_count++;
|
||||
}
|
||||
|
||||
/* Calculate next occurrence based on recurrence type */
|
||||
switch ((*events)[i].recurrence) {
|
||||
case DAILY:
|
||||
next_occurrence += SECONDS_PER_DAY * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case WEEKLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 7 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case MONTHLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 30 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case YEARLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 365 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(inotify_fd);
|
||||
event *new_events = malloc(sizeof(event) * new_count);
|
||||
if (!new_events) {
|
||||
perror("malloc");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Copy original events and expand recurring ones */
|
||||
int current_index = 0;
|
||||
for (int i = 0; i < *num_events; i++) {
|
||||
if ((*events)[i].recurrence != NONE) {
|
||||
time_t next_occurrence = (*events)[i].timestamp;
|
||||
while (next_occurrence <= (*events)[i].recurrence_end) {
|
||||
if (next_occurrence >= now) {
|
||||
event new_event = (*events)[i];
|
||||
new_event.timestamp = next_occurrence;
|
||||
memcpy(&new_events[current_index++], &new_event, sizeof(event));
|
||||
}
|
||||
|
||||
switch ((*events)[i].recurrence) {
|
||||
case DAILY:
|
||||
next_occurrence += SECONDS_PER_DAY * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case WEEKLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 7 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case MONTHLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 30 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
case YEARLY:
|
||||
next_occurrence += SECONDS_PER_DAY * 365 * (*events)[i].recurrence_interval;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(&new_events[current_index++], &(*events)[i], sizeof(event));
|
||||
}
|
||||
}
|
||||
|
||||
*events = new_events;
|
||||
*num_events = new_count;
|
||||
}
|
||||
|
||||
static _Noreturn void
|
||||
usage(int code)
|
||||
static int compare_by_start_time(const void *a, const void *b)
|
||||
{
|
||||
return ((event *) a)->timestamp - ((event *) b)->timestamp;
|
||||
}
|
||||
|
||||
static int compare_by_priority(const void *a, const void *b)
|
||||
{
|
||||
return ((event *) b)->priority - ((event *) a)->priority;
|
||||
}
|
||||
|
||||
void sort_events(event *events, int num_events, int sort_type)
|
||||
{
|
||||
switch (sort_type) {
|
||||
/* Start time */
|
||||
case 0:
|
||||
qsort(events, num_events, sizeof(event), compare_by_start_time);
|
||||
break;
|
||||
/* priority_type (NOT IMPLEMENTED) */
|
||||
case 1:
|
||||
qsort(events, num_events, sizeof(event), compare_by_priority);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void watch_file(event *events, int *num_events)
|
||||
{
|
||||
load_events(&events, num_events);
|
||||
expand_recurring_events(&events, num_events);
|
||||
while (1) {
|
||||
// daemon to show notifications
|
||||
check_events(events, num_events);
|
||||
}
|
||||
|
||||
free(events);
|
||||
}
|
||||
|
||||
parsed_time parse_relative_time(const char *time_str)
|
||||
{
|
||||
parsed_time result = {0, 0};
|
||||
char *str = strdup(time_str);
|
||||
char *number = str;
|
||||
char *unit = str;
|
||||
|
||||
/* Find the separation between number and unit */
|
||||
while (*unit && isdigit(*unit)) unit++;
|
||||
if (*unit) {
|
||||
*unit = '\0';
|
||||
unit++;
|
||||
}
|
||||
|
||||
if (!*number || !*unit) {
|
||||
free(str);
|
||||
return result;
|
||||
}
|
||||
|
||||
int value = atoi(number);
|
||||
time_t now = time(NULL);
|
||||
|
||||
if (strcmp(unit, "min") == 0 || strcmp(unit, "mins") == 0) {
|
||||
result.timestamp = now + (value * 60);
|
||||
} else if (strcmp(unit, "hour") == 0 || strcmp(unit, "hours") == 0) {
|
||||
result.timestamp = now + (value * 3600);
|
||||
} else if (strcmp(unit, "day") == 0 || strcmp(unit, "days") == 0) {
|
||||
result.timestamp = now + (value * 86400);
|
||||
} else if (strcmp(unit, "week") == 0 || strcmp(unit, "weeks") == 0) {
|
||||
result.timestamp = now + (value * 86400 * 7);
|
||||
} else {
|
||||
free(str);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.is_valid = 1;
|
||||
free(str);
|
||||
return result;
|
||||
}
|
||||
|
||||
parsed_time parse_absolute_time(const char *time_str)
|
||||
{
|
||||
parsed_time result = {0, 0};
|
||||
struct tm tm = {0};
|
||||
char *formats[] = {
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%Y/%m/%d %H:%M",
|
||||
"%d-%m-%Y %H:%M:%S",
|
||||
"%d-%m-%Y %H:%M",
|
||||
"%d/%m/%Y %H:%M:%S",
|
||||
"%d/%m/%Y %H:%M",
|
||||
"%d-%m-%Y",
|
||||
"%Y-%m-%d",
|
||||
"%d/%m/%Y",
|
||||
"%Y/%m/%d",
|
||||
"%H:%M",
|
||||
};
|
||||
|
||||
for (int i = 0; i < sizeof(formats)/sizeof(formats[0]); i++) {
|
||||
char *parsed = strptime(time_str, formats[i], &tm);
|
||||
if (parsed != NULL) {
|
||||
/* Use today's date */
|
||||
if (strcmp(formats[i], "%H:%M") == 0) {
|
||||
time_t now = time(NULL);
|
||||
struct tm *today = localtime(&now);
|
||||
tm.tm_year = today->tm_year;
|
||||
tm.tm_mon = today->tm_mon;
|
||||
tm.tm_mday = today->tm_mday;
|
||||
}
|
||||
|
||||
tm.tm_isdst = -1;
|
||||
result.timestamp = mktime(&tm);
|
||||
result.is_valid = 1;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parsed_time parse_time_string(const char *time_str)
|
||||
{
|
||||
/* Parsing as relative time first */
|
||||
parsed_time result = parse_relative_time(time_str);
|
||||
if (result.is_valid) return result;
|
||||
|
||||
/* Try absolute time */
|
||||
return parse_absolute_time(time_str);
|
||||
}
|
||||
|
||||
static _Noreturn void usage(int code)
|
||||
{
|
||||
fprintf(code ? stderr : stdout,
|
||||
"Simple Scheduler Manager " VERSION "\n\n"
|
||||
"Usage: ssm <command>\n\n"
|
||||
" help Show this help message\n"
|
||||
" sched <time> <title> [description] Schedule an event\n"
|
||||
" edit Edit schedule with $EDITOR\n"
|
||||
" list <timerange> List all upcoming events\n"
|
||||
" search Search for events\n"
|
||||
" run Spawn notifier daemon\n"
|
||||
);
|
||||
" help Show this help message\n"
|
||||
" sched <time> <title> <description> [options] Schedule an event\n"
|
||||
" edit Edit schedule with $EDITOR\n"
|
||||
" list List all upcoming events\n"
|
||||
" run Spawn notifier daemon\n"
|
||||
);
|
||||
exit(code);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
event *events = NULL;
|
||||
int num_events = 0;
|
||||
if (argc == 1) {
|
||||
if (argc == 1 || (argc == 2 && !strncmp(argv[1], "list", 4))) {
|
||||
/* Load and expand recurring events */
|
||||
char *time_buf = convert_timestamp(time(NULL), 0);
|
||||
printf("Current date: %s\n", time_buf);
|
||||
printf("Upcoming events:\n");
|
||||
list_events(events, &num_events);
|
||||
free(time_buf);
|
||||
|
||||
load_events(&events, &num_events);
|
||||
list_today_events(events, num_events);
|
||||
|
||||
expand_recurring_events(&events, &num_events);
|
||||
|
||||
/* Sort events by start time */
|
||||
sort_events(events, num_events, 0);
|
||||
list_upcoming_events(events, num_events);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (strcmp(argv[1], "sched") == 0) {
|
||||
/* time can be relative or absolute */
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Usage: ssm sched <time> <title> <description> [options]\n");
|
||||
fprintf(stderr, "Options:\n");
|
||||
fprintf(stderr, " --priority <low|medium|high>\n");
|
||||
fprintf(stderr, " --recur <daily|weekly|monthly|yearly>\n");
|
||||
fprintf(stderr, " --interval <number>\n");
|
||||
fprintf(stderr, " --until <end-date>\n");
|
||||
fprintf(stderr, "\nTime formats:\n");
|
||||
fprintf(stderr, " Relative: 30 min, 2 hours, 1 day, 2 weeks\n");
|
||||
fprintf(stderr, " Absolute: YYYY-MM-DD DD-MM-YY [HH:MM:SS] HH:MM[:SS]\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char *when = argv[2];
|
||||
char *name = argv[3];
|
||||
char *description = argv[4];
|
||||
/* todo: accept time */
|
||||
add_event(time(NULL) + 60, name, description);
|
||||
|
||||
parsed_time pt = parse_time_string(when);
|
||||
if (!pt.is_valid) {
|
||||
fprintf(stderr, "Invalid time format: %s\n", when);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
priority_type priority = MEDIUM;
|
||||
recurrence_type recurrence = NONE;
|
||||
int recurrence_interval = 1;
|
||||
time_t recurrence_end = 0;
|
||||
|
||||
/* Parse optional arguments */
|
||||
for (int i = 5; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--priority") && i + 1 < argc) {
|
||||
if (!strcmp(argv[i + 1], "low")) priority = LOW;
|
||||
else if (!strcmp(argv[i + 1], "high")) priority = HIGH;
|
||||
i++;
|
||||
} else if (!strcmp(argv[i], "--recur") && i + 1 < argc) {
|
||||
if (!strcmp(argv[i + 1], "daily")) recurrence = DAILY;
|
||||
else if (!strcmp(argv[i + 1], "weekly")) recurrence = WEEKLY;
|
||||
else if (!strcmp(argv[i + 1], "monthly")) recurrence = MONTHLY;
|
||||
else if (!strcmp(argv[i + 1], "yearly")) recurrence = YEARLY;
|
||||
i++;
|
||||
} else if (!strcmp(argv[i], "--interval") && i + 1 < argc) {
|
||||
recurrence_interval = atoi(argv[i + 1]);
|
||||
i++;
|
||||
} else if (!strcmp(argv[i], "--until") && i + 1 < argc) {
|
||||
parsed_time end_time = parse_time_string(argv[i + 1]);
|
||||
if (end_time.is_valid) {
|
||||
recurrence_end = end_time.timestamp;
|
||||
} else {
|
||||
fprintf(stderr, "Invalid end date format: %s\n", argv[i + 1]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
add_event(pt.timestamp, name, description, priority, recurrence,
|
||||
recurrence_interval, recurrence_end);
|
||||
|
||||
char *time_str = convert_timestamp(pt.timestamp, 0);
|
||||
printf("Time: %s\n", time_str);
|
||||
printf("Title: %s\n", name);
|
||||
printf("Description: %s\n", description);
|
||||
printf("Priority: %s\n", priority == LOW ? "Low" : (priority == HIGH ? "High" : "Medium"));
|
||||
if (recurrence != NONE) {
|
||||
printf("Recurrence: %s (every %d ",
|
||||
recurrence == DAILY ? "Daily" :
|
||||
recurrence == WEEKLY ? "Weekly" :
|
||||
recurrence == MONTHLY ? "Monthly" : "Yearly",
|
||||
recurrence_interval);
|
||||
printf("%s)\n",
|
||||
recurrence == DAILY ? "days" :
|
||||
recurrence == WEEKLY ? "weeks" :
|
||||
recurrence == MONTHLY ? "months" : "years");
|
||||
if (recurrence_end) {
|
||||
char *end_str = convert_timestamp(recurrence_end, 0);
|
||||
printf("Until: %s\n", end_str);
|
||||
free(end_str);
|
||||
}
|
||||
}
|
||||
free(time_str);
|
||||
} else if (strcmp(argv[1], "edit") == 0) {
|
||||
const char *e = getenv("EDITOR");
|
||||
if (e == NULL) {
|
||||
|
@ -269,11 +575,6 @@ main(int argc, char **argv)
|
|||
execlp(e, e, db_path, NULL);
|
||||
perror("Failed to spawn editor");
|
||||
return EXIT_FAILURE;
|
||||
} else if (strcmp(argv[1], "list") == 0) {
|
||||
/* accept argv[2] as timerange */
|
||||
list_events(events, &num_events);
|
||||
} else if (strcmp(argv[1], "search") == 0) {
|
||||
|
||||
} else if (strcmp(argv[1], "run") == 0) {
|
||||
watch_file(events, &num_events);
|
||||
} else if (strcmp(argv[1], "help") == 0) {
|
||||
|
|
41
ssm.h
41
ssm.h
|
@ -3,6 +3,45 @@
|
|||
|
||||
#define VERSION "1.0.0"
|
||||
#define DATABASE_PATH "~/.local/share/ssm.tsv"
|
||||
static const char editor[] = "nvim"; // Code editor
|
||||
|
||||
// Code editor
|
||||
static const char editor[] = "nvim";
|
||||
// Notification notifier
|
||||
static const char notifier[] = "luft";
|
||||
|
||||
typedef enum {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
} priority_type;
|
||||
|
||||
typedef enum {
|
||||
NONE,
|
||||
DAILY,
|
||||
WEEKLY,
|
||||
MONTHLY,
|
||||
YEARLY,
|
||||
} recurrence_type;
|
||||
|
||||
typedef enum {
|
||||
FULL,
|
||||
HHMM,
|
||||
} datefmt;
|
||||
|
||||
typedef struct {
|
||||
time_t timestamp;
|
||||
int is_valid;
|
||||
} parsed_time;
|
||||
|
||||
typedef struct {
|
||||
time_t timestamp;
|
||||
char name[50];
|
||||
char description[100];
|
||||
priority_type priority;
|
||||
recurrence_type recurrence;
|
||||
int recurrence_interval; /*Number of days/weeks/months/years between occurrences */
|
||||
time_t recurrence_end; /* End date for recurrence */
|
||||
int notified;
|
||||
} event;
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue