// gtd-curse.c - ncurses-based GTD Todo Manager in C
// Compile: gcc -o gtd-curse gtd-curse.c -lm -O2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>

/* ── Constants ────────────────────────────────────────────── */
#define MAX_TODOS    2000
#define MAX_TEXT     512
#define MAX_STR      128
#define MAX_SECTIONS 64
#define MAX_LINES    4096
#define MAX_CONTEXTS 256
#define MAX_PROJECTS 256
#define MAX_TAGS     512
#define FILENAME     "todos.dat"
#define METAFILE     "gtd_meta.dat"

#define MIN(a,b) ((a)<(b)?(a):(b))
#define MAX(a,b) ((a)>(b)?(a):(b))

/* ── ANSI escape codes ────────────────────────────────────── */
#define ESC          "\033"
#define CLS          ESC"[2J"
#define CLRLN        ESC"[2K"
#define HOME         ESC"[H"
#define HIDE_CURS    ESC"[?25l"
#define SHOW_CURS    ESC"[?25h"
#define RST          ESC"[0m"
#define BLD          ESC"[1m"
#define REV          ESC"[7m"
#define FG_GREEN     ESC"[32m"
#define FG_YELLOW    ESC"[33m"
#define FG_CYAN      ESC"[36m"
#define FG_MAGENTA   ESC"[35m"
#define FG_BLUE      ESC"[34m"
#define FG_RED       ESC"[31m"

/* ── Enums ────────────────────────────────────────────────── */
typedef enum { P_NONE=0, P1=1,P2=2,P3=3,P4=4,P5=5,P6=6,P7=7,P8=8,P9=9 } Priority;
typedef enum { VIEW_INBOX, VIEW_CONTEXT, VIEW_PROJECT,
               VIEW_TAG, VIEW_PRIORITY, VIEW_DATE } View;

/* ── Data structures ──────────────────────────────────────── */
typedef struct {
    char text    [MAX_TEXT];
    char context [MAX_STR];
    char project [MAX_STR];
    char tags    [5][MAX_STR];
    int  tag_cnt;
    Priority prio;
    char due     [11];
    int  done;
    char created [20];
} Todo;

typedef struct {
    char name [MAX_STR];
    int  count;
    int  collapsed;
} Section;

typedef struct {
    int type;
    int sec;
    int idx;
} Line;

typedef struct {
    Todo     todos [MAX_TODOS];
    int      count;
    View     view;
    char     filter [MAX_STR];
    int      sel;
    int      scroll;
    char     feedback [MAX_STR];
    int      fb_timer;
    int      w, h;
    int      show_done;
    Section  secs [MAX_SECTIONS];
    int      sec_cnt;

    char     contexts [MAX_CONTEXTS][MAX_STR];
    int      ctx_cnt;
    char     projects [MAX_PROJECTS][MAX_STR];
    int      proj_cnt;
    char     tags     [MAX_TAGS][MAX_STR];
    int      tag_cnt;

    int      week_off;
} App;

/* ── Terminal helpers ─────────────────────────────────────── */
static struct termios orig;

static void raw_on(void) {
    tcgetattr(STDIN_FILENO, &orig);
    struct termios r = orig;
    r.c_lflag &= ~(ECHO | ICANON);
    r.c_cc[VMIN]  = 1;
    r.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &r);
}
static void raw_off(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig); }

static void sz(int *w, int *h) {
    struct winsize ws;
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) { *w=ws.ws_col; *h=ws.ws_row; }
    else { *w=80; *h=24; }
}
static void mv(int r, int c) { printf(ESC"[%d;%dH", r+1, c+1); }

/* ── Key reader ───────────────────────────────────────────── */
enum { K_UP=1000, K_DOWN, K_RIGHT, K_LEFT };
static int kget(void) {
    char c;
    if (read(STDIN_FILENO, &c, 1) != 1) return -1;
    if (c=='\033') {
        char s[3];
        if (read(STDIN_FILENO, &s[0],1)!=1) return '\033';
        if (read(STDIN_FILENO, &s[1],1)!=1) return '\033';
        if (s[0]=='[') { switch(s[1]) {
            case 'A': return K_UP;
            case 'B': return K_DOWN;
            case 'C': return K_RIGHT;
            case 'D': return K_LEFT;
        }}
        return '\033';
    }
    return c;
}

/* ── Persistence ──────────────────────────────────────────── */
static void load_meta(App *a) {
    FILE *f = fopen(METAFILE, "rb");
    if (!f) return;
    fread(&a->ctx_cnt,  sizeof(int), 1, f);
    fread(&a->proj_cnt, sizeof(int), 1, f);
    fread(&a->tag_cnt,  sizeof(int), 1, f);
    fread(a->contexts, sizeof(a->contexts[0]), a->ctx_cnt,  f);
    fread(a->projects, sizeof(a->projects[0]), a->proj_cnt, f);
    fread(a->tags,     sizeof(a->tags[0]),     a->tag_cnt,  f);
    fclose(f);
}
static void save_meta(App *a) {
    FILE *f = fopen(METAFILE, "wb");
    if (!f) return;
    fwrite(&a->ctx_cnt,  sizeof(int), 1, f);
    fwrite(&a->proj_cnt, sizeof(int), 1, f);
    fwrite(&a->tag_cnt,  sizeof(int), 1, f);
    fwrite(a->contexts, sizeof(a->contexts[0]), a->ctx_cnt,  f);
    fwrite(a->projects, sizeof(a->projects[0]), a->proj_cnt, f);
    fwrite(a->tags,     sizeof(a->tags[0]),     a->tag_cnt,  f);
    fclose(f);
}

static void load(App *a) {
    FILE *f = fopen(FILENAME, "rb");
    if (!f) return;
    a->count = fread(a->todos, sizeof(Todo), MAX_TODOS, f);
    fclose(f);
    load_meta(a);
}
static void save(App *a) {
    FILE *f = fopen(FILENAME, "wb");
    if (!f) return;
    fwrite(a->todos, sizeof(Todo), a->count, f);
    fclose(f);
    save_meta(a);
}

/* ── Metadata helpers ─────────────────────────────────────── */
static void add_ctx(App *a, const char *name) {
    for (int i=0; i<a->ctx_cnt; i++) if (!strcmp(a->contexts[i],name)) return;
    if (a->ctx_cnt < MAX_CONTEXTS) strcpy(a->contexts[a->ctx_cnt++], name);
    save_meta(a);
}
static void add_proj(App *a, const char *name) {
    for (int i=0; i<a->proj_cnt; i++) if (!strcmp(a->projects[i],name)) return;
    if (a->proj_cnt < MAX_PROJECTS) strcpy(a->projects[a->proj_cnt++], name);
    save_meta(a);
}
static void add_tag(App *a, const char *name) {
    for (int i=0; i<a->tag_cnt; i++) if (!strcmp(a->tags[i],name)) return;
    if (a->tag_cnt < MAX_TAGS) strcpy(a->tags[a->tag_cnt++], name);
    save_meta(a);
}

static void sync_meta(App *a) {
    for (int i=0; i<a->count; i++) {
        if (a->todos[i].context[0]) add_ctx(a, a->todos[i].context);
        if (a->todos[i].project[0]) add_proj(a, a->todos[i].project);
        for (int j=0; j<a->todos[i].tag_cnt; j++) add_tag(a, a->todos[i].tags[j]);
    }
}

/* ── Date helpers ─────────────────────────────────────────── */
static void today_str(char *out) {
    time_t t = time(NULL);
    strftime(out, 11, "%Y-%m-%d", localtime(&t));
}
static void tomorrow_str(char *out) {
    time_t t = time(NULL)+86400;
    strftime(out, 11, "%Y-%m-%d", localtime(&t));
}
static void end_of_month(int ahead, char *out) {
    time_t t = time(NULL);
    struct tm *tm = localtime(&t);
    tm->tm_mon += ahead; tm->tm_mday = 1; tm->tm_hour = 12;
    t = mktime(tm); tm = localtime(&t);
    tm->tm_mon++; tm->tm_mday = 0;
    t = mktime(tm); tm = localtime(&t);
    strftime(out, 11, "%Y-%m-%d", tm);
}
static void parse_date(const char *s, char *out) {
    if      (!strcasecmp(s,"today"))       today_str(out);
    else if (!strcasecmp(s,"tomorrow"))    tomorrow_str(out);
    else if (!strcasecmp(s,"this month"))  end_of_month(0,out);
    else if (!strcasecmp(s,"next month"))  end_of_month(1,out);
    else if (!strcasecmp(s,"this week")) {
        time_t t = time(NULL); struct tm *tm = localtime(&t);
        t += (6 - tm->tm_wday)*86400;
        strftime(out,11,"%Y-%m-%d", localtime(&t));
    }
    else if (!strcasecmp(s,"next week")) {
        time_t t = time(NULL); struct tm *tm = localtime(&t);
        t += (13 - tm->tm_wday)*86400;
        strftime(out,11,"%Y-%m-%d", localtime(&t));
    }
    else {
        int y,m,d; struct tm tm={0};
        if (sscanf(s,"%d-%d-%d",&y,&m,&d)==3) {
            tm.tm_year=y-1900; tm.tm_mon=m-1; tm.tm_mday=d;
            time_t t = mktime(&tm); strftime(out,11,"%Y-%m-%d", localtime(&t));
        } else if (sscanf(s,"%d/%d/%d",&m,&d,&y)==3) {
            if (y<100) y+=2000;
            tm.tm_year=y-1900; tm.tm_mon=m-1; tm.tm_mday=d;
            time_t t = mktime(&tm); strftime(out,11,"%Y-%m-%d", localtime(&t));
        } else out[0]='\0';
    }
}

/* ── Natural language parser ──────────────────────────────── */
static void parse(const char *in, Todo *t) {
    char buf[MAX_TEXT]; strcpy(buf, in);
    memset(t, 0, sizeof(Todo));
    time_t now = time(NULL);
    strftime(t->created,20,"%Y-%m-%d %H:%M:%S", localtime(&now));
    {   char *s = strchr(buf,'['), *e = s?strchr(s,']'):NULL;
        if (s&&e) { *e=0; parse_date(s+1,t->due); memmove(s,e+1,strlen(e+1)+1); }
    }
    {   char *p = buf;
        while (*p) {
            if ((*p=='p'||*p=='P') && p[1]>='1'&&p[1]<='9' && (!p[2]||!isalnum(p[2]))) {
                t->prio = p[1]-'0'; memmove(p,p+2,strlen(p+2)+1); p--;
            } p++;
        }
    }
    {   char *at = strchr(buf,'@');
        if (at) {
            char *e = at+1;
            while (*e && !isspace(*e) && *e!='+' && *e!='#') e++;
            int n = e-(at+1);
            if (n>0 && n<MAX_STR) { strncpy(t->context,at+1,n); t->context[n]=0; }
            memmove(at,e,strlen(e)+1);
        }
    }
    {   char *pl = strchr(buf,'+');
        if (pl) {
            char *e = pl+1;
            while (*e && !isspace(*e) && *e!='@' && *e!='#') e++;
            int n = e-(pl+1);
            if (n>0 && n<MAX_STR) { strncpy(t->project,pl+1,n); t->project[n]=0; }
            memmove(pl,e,strlen(e)+1);
        }
    }
    {   char *h = buf;
        while ((h=strchr(h,'#')) && t->tag_cnt<5) {
            char *e = h+1;
            while (*e && !isspace(*e) && *e!='@' && *e!='+') e++;
            int n = e-(h+1);
            if (n>0 && n<MAX_STR) { strncpy(t->tags[t->tag_cnt],h+1,n);
                                    t->tags[t->tag_cnt][n]=0; t->tag_cnt++; }
            memmove(h,e,strlen(e)+1);
        }
    }
    {   char *s=buf, *d=buf; int sp=0;
        while (*s) {
            if (isspace(*s)) { if (!sp && d!=buf) { *d++=' '; sp=1; } }
            else { *d++=*s; sp=0; } s++;
        }
        *d=0; if (d>buf && *(d-1)==' ') *(d-1)=0;
    }
    strcpy(t->text, buf);
}

static void todo_full(const Todo *t, char *out) {
    out[0]=0; strcat(out, t->text);
    if (t->context[0]) { strcat(out," @"); strcat(out,t->context); }
    if (t->project[0]) { strcat(out," +"); strcat(out,t->project); }
    for (int i=0; i<t->tag_cnt; i++) { strcat(out," #"); strcat(out,t->tags[i]); }
    if (t->prio) { char p[5]; snprintf(p,5," p%d",t->prio); strcat(out,p); }
    if (t->due[0]) { strcat(out," ["); strcat(out,t->due); strcat(out,"]"); }
}

static void bury_done(App *a) {
    Todo tmp[MAX_TODOS]; int ac=0, dc=0;
    for (int i=0; i<a->count; i++) {
        if (a->todos[i].done) tmp[a->count-1 - dc++] = a->todos[i];
        else                  tmp[ac++] = a->todos[i];
    }
    memcpy(a->todos, tmp, a->count*sizeof(Todo));
}

static void add_todo(App *a, const char *in) {
    if (a->count >= MAX_TODOS) return;
    parse(in, &a->todos[a->count++]);
    if (a->todos[a->count-1].context[0]) add_ctx(a, a->todos[a->count-1].context);
    if (a->todos[a->count-1].project[0]) add_proj(a, a->todos[a->count-1].project);
    for (int j=0; j<a->todos[a->count-1].tag_cnt; j++)
        add_tag(a, a->todos[a->count-1].tags[j]);
    bury_done(a); save(a);
}

/* ── Section matching ─────────────────────────────────────── */
static int todo_matches_section(const App *a, const Todo *t, const Section *sec) {
    switch (a->view) {
    case VIEW_INBOX:   return !t->context[0] && !t->project[0];
    case VIEW_CONTEXT: return !strcmp(t->context, a->filter);
    case VIEW_PROJECT: return !strcmp(t->project, a->filter);
    case VIEW_TAG:
        for (int i=0; i<t->tag_cnt; i++) if (!strcmp(t->tags[i], a->filter)) return 1;
        return 0;
    case VIEW_PRIORITY:
        if (!strcmp(sec->name,"No Priority")) return t->prio == P_NONE;
        return (int)t->prio == atoi(sec->name+1);
    case VIEW_DATE: {
        char *lp = strchr(sec->name,'(');
        if (!lp) return 0;
        char d[11]; strncpy(d,lp+1,10); d[10]=0;
        char *rp = strchr(d,')'); if (rp) *rp=0;
        return !strcmp(t->due, d);
    }}
    return 0;
}

/* ── Build sections ───────────────────────────────────────── */
static void build_sections(App *a) {
    a->sec_cnt = 0;
    switch (a->view) {
    case VIEW_INBOX:
        a->secs[0] = (Section){.collapsed=0,.count=0};
        strcpy(a->secs[0].name, "Inbox");
        for (int i=0; i<a->count; i++) {
            Todo *t = &a->todos[i];
            if (!t->context[0] && !t->project[0] && (!t->done || a->show_done))
                a->secs[0].count++;
        }
        a->sec_cnt = 1;
        break;
    case VIEW_CONTEXT:
        a->secs[0] = (Section){.collapsed=0,.count=0};
        strcpy(a->secs[0].name, a->filter);
        for (int i=0; i<a->count; i++) {
            Todo *t = &a->todos[i];
            if (!strcmp(t->context, a->filter) && (!t->done || a->show_done))
                a->secs[0].count++;
        }
        a->sec_cnt = 1;
        break;
    case VIEW_PROJECT:
        a->secs[0] = (Section){.collapsed=0,.count=0};
        strcpy(a->secs[0].name, a->filter);
        for (int i=0; i<a->count; i++) {
            Todo *t = &a->todos[i];
            if (!strcmp(t->project, a->filter) && (!t->done || a->show_done))
                a->secs[0].count++;
        }
        a->sec_cnt = 1;
        break;
    case VIEW_TAG:
        a->secs[0] = (Section){.collapsed=0,.count=0};
        strcpy(a->secs[0].name, a->filter);
        for (int i=0; i<a->count; i++) {
            Todo *t = &a->todos[i];
            for (int j=0; j<t->tag_cnt; j++)
                if (!strcmp(t->tags[j], a->filter) && (!t->done || a->show_done))
                    { a->secs[0].count++; break; }
        }
        a->sec_cnt = 1;
        break;
    case VIEW_PRIORITY:
        for (int p=P1; p<=P9; p++) {
            Section *s = &a->secs[a->sec_cnt];
            *s = (Section){.collapsed=0,.count=0};
            snprintf(s->name, MAX_STR, "P%d", p);
            for (int i=0; i<a->count; i++) {
                Todo *t = &a->todos[i];
                if (t->prio == p && (!t->done || a->show_done)) s->count++;
            }
            if (s->count > 0) a->sec_cnt++;
        }
        {   Section *s = &a->secs[a->sec_cnt];
            *s = (Section){.collapsed=0,.count=0};
            strcpy(s->name, "No Priority");
            for (int i=0; i<a->count; i++) {
                Todo *t = &a->todos[i];
                if (t->prio==P_NONE && (!t->done || a->show_done)) s->count++;
            }
            if (s->count > 0) a->sec_cnt++;
        }
        break;
    case VIEW_DATE: {
        time_t now = time(NULL);
        struct tm *tm = localtime(&now);
        int dow = tm->tm_wday;
        int mon_off = (dow+6)%7;
        time_t mon = now - mon_off*86400 + a->week_off*7*86400;
        const char *days[] = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
        for (int d=0; d<7; d++) {
            time_t dt = mon + d*86400;
            struct tm *dtm = localtime(&dt);
            char ds[11]; strftime(ds,11,"%Y-%m-%d", dtm);
            Section *s = &a->secs[a->sec_cnt];
            *s = (Section){.collapsed=0,.count=0};
            snprintf(s->name, MAX_STR, "%s (%s)", days[d], ds);
            for (int i=0; i<a->count; i++) {
                Todo *t = &a->todos[i];
                if (!strcmp(t->due, ds) && (!t->done || a->show_done)) s->count++;
            }
            a->sec_cnt++;
        }
        break;
    }}
}

/* ── Build flat display-line list ─────────────────────────── */
static int build_lines(App *a, Line *lines) {
    int n = 0;
    for (int s=0; s<a->sec_cnt; s++) {
        lines[n++] = (Line){.type=0, .sec=s, .idx=-1};
        if (!a->secs[s].collapsed) {
            for (int i=0; i<a->count; i++) {
                Todo *t = &a->todos[i];
                if (todo_matches_section(a, t, &a->secs[s])) {
                    if (!t->done || a->show_done) {
                        lines[n++] = (Line){.type=1, .sec=s, .idx=i};
                    }
                }
            }
        }
    }
    return n;
}

/* ── Feedback ─────────────────────────────────────────────── */
static void fb(App *a, const char *msg) {
    strncpy(a->feedback, msg, MAX_STR-1); a->feedback[MAX_STR-1]=0;
    a->fb_timer = 5;
}

/* ── Draw ─────────────────────────────────────────────────── */
static void draw(App *a) {
    sz(&a->w, &a->h);
    int W=a->w, H=a->h;
    build_sections(a);
    Line lines[MAX_LINES];
    int L = build_lines(a, lines);
    int vis = H - 6; if (vis<1) vis=1;
    if (a->sel < a->scroll) a->scroll = a->sel;
    if (a->sel >= a->scroll + vis) a->scroll = a->sel - vis + 1;
    if (a->scroll < 0) a->scroll = 0;
    if (L>0 && a->scroll > L-vis) a->scroll = MAX(0, L-vis);

    printf(HIDE_CURS HOME);
    printf(REV);
    const char *vnames[] = {"INBOX","CONTEXT","PROJECT","TAG","PRIORITY","DATE"};
    char hdr[MAX_STR];
    if (a->filter[0] && a->view!=VIEW_PRIORITY && a->view!=VIEW_DATE)
        snprintf(hdr,MAX_STR," GTD Todo Manager - %s: %s ", vnames[a->view], a->filter);
    else if (a->view == VIEW_DATE) {
        time_t now = time(NULL);
        struct tm *tm = localtime(&now);
        int dow = tm->tm_wday, mon_off = (dow+6)%7;
        time_t mon = now - mon_off*86400 + a->week_off*7*86400;
        struct tm *mtm = localtime(&mon);
        char ws[11], we[11];
        strftime(ws,11,"%Y-%m-%d", mtm);
        time_t sun = mon + 6*86400;
        strftime(we,11,"%Y-%m-%d", localtime(&sun));
        snprintf(hdr,MAX_STR," GTD Todo Manager - DATE  %s – %s ", ws, we);
    }
    else
        snprintf(hdr,MAX_STR," GTD Todo Manager - %s ", vnames[a->view]);
    printf("%s", hdr);
    for (int i=strlen(hdr); i<W; i++) putchar(' ');
    printf(RST);

    for (int i=0; i<vis; i++) {
        int li = i + a->scroll;
        mv(i+1, 0); printf(CLRLN);
        if (li >= L) continue;
        int sel = (li == a->sel);
        if (sel) printf(REV);
        if (lines[li].type == 0) {
            Section *s = &a->secs[lines[li].sec];
            printf(BLD "%s" RST, s->name);
        } else {
            printf(" ");
            Todo *t = &a->todos[lines[li].idx];
            printf(t->done ? FG_GREEN"[x]"RST : "[ ]");
            printf(" %s", t->text);
            if (t->context[0]) printf(" "FG_CYAN"@%s"RST, t->context);
            if (t->project[0]) printf(" "FG_MAGENTA"+%s"RST, t->project);
            for (int j=0; j<t->tag_cnt; j++) printf(" "FG_BLUE"#%s"RST, t->tags[j]);
            if (t->prio) printf(FG_YELLOW" p%d"RST, t->prio);
            if (t->due[0])  printf(" [%s]", t->due);
        }
        if (sel) printf(RST);
    }

    mv(H-5,0); printf(CLRLN);
    if (a->fb_timer>0) { printf(BLD"%s"RST, a->feedback); a->fb_timer--; }

    int items=0; for (int i=0; i<L; i++) if (lines[i].type==1) items++;
    mv(H-4,0); printf(REV);
    printf(" Items: %d | v:done | space:toggle | a:add | A:new ctx/proj/tag | e:edit | x:del | ?:help | q:quit ", items);
    for (int i=0; i<W; i++) putchar(' ');
    printf(RST);

    mv(H-3,0);
    printf(" i:inbox | c:contexts | p:projects | t:tags | r:priorities | d:dates ");
    for (int i=58; i<W; i++) putchar(' ');

    fflush(stdout);
}

/* ── Inline editing ───────────────────────────────────────── */
static int inline_edit(App *a, int todo_idx) {
    char buf[MAX_TEXT];
    todo_full(&a->todos[todo_idx], buf);
    int len = strlen(buf), pos = len;
    int edit_row = a->h - 1;
    for (;;) {
        mv(edit_row, 0); printf(CLRLN "Edit: %s", buf);
        mv(edit_row, 6 + pos); printf(SHOW_CURS); fflush(stdout);
        int k = kget(); printf(HIDE_CURS);
        if (k=='\n'||k=='\r') {
            if (buf[0]) {
                Todo nt; parse(buf, &nt);
                nt.done = a->todos[todo_idx].done;
                strcpy(nt.created, a->todos[todo_idx].created);
                a->todos[todo_idx] = nt;
                if (nt.context[0]) add_ctx(a, nt.context);
                if (nt.project[0]) add_proj(a, nt.project);
                for (int j=0; j<nt.tag_cnt; j++) add_tag(a, nt.tags[j]);
                bury_done(a); save(a); fb(a, "Updated");
            }
            return 1;
        }
        if (k=='\033'||k==7) { fb(a,"Cancelled"); return 0; }
        if (k==127||k=='\b') { if (pos>0) { memmove(&buf[pos-1],&buf[pos],len-pos+1); pos--; len--; } }
        else if (k==K_LEFT)  { if (pos>0) pos--; }
        else if (k==K_RIGHT) { if (pos<len) pos++; }
        else if (k==1)  pos=0;
        else if (k==5)  pos=len;
        else if (k==11) { buf[pos]=0; len=pos; }
        else if (k==23) { while (pos>0 && isspace(buf[pos-1])) { pos--; len--; }
                          while (pos>0 && !isspace(buf[pos-1])) { pos--; len--; }
                          buf[pos]=0; len=pos; }
        else if (k>=32 && k<127 && len<MAX_TEXT-1) {
            memmove(&buf[pos+1],&buf[pos],len-pos+1);
            buf[pos]=k; pos++; len++;
        }
    }
}

/* ── Selection menu ───────────────────────────────────────── */
static int menu(const char *title, char opts[][MAX_STR], int n, int W, int H) {
    int mh = n+4; if (mh>H-2) mh=H-2;
    int mw = strlen(title)+4;
    for (int i=0; i<n; i++) { int l=strlen(opts[i])+4; if (l>mw) mw=l; }
    if (mw>W-2) mw=W-2;
    int sy=(H-mh)/2, sx=(W-mw)/2, sel=0, sc=0;
    for (;;) {
        for (int i=0; i<mh; i++) {
            mv(sy+i,sx);
            if (i==0||i==mh-1) { for (int j=0; j<mw; j++) putchar('-'); }
            else { putchar('|'); for (int j=1; j<mw-1; j++) putchar(' '); putchar('|'); }
        }
        mv(sy,sx+2); printf(" %s ", title);
        int vh=mh-4;
        if (sel<sc) sc=sel; if (sel>=sc+vh) sc=sel-vh+1;
        for (int i=0; i<vh && i+sc<n; i++) {
            int idx=i+sc; mv(sy+i+2,sx+2);
            if (idx==sel) printf(REV);
            printf("%s", opts[idx]);
            if (idx==sel) printf(RST);
            for (int j=strlen(opts[idx]); j<mw-4; j++) putchar(' ');
        }
        mv(sy+mh-2,sx+2); printf("j/k/arrows:nav  Enter:sel  Esc:cancel");
        fflush(stdout);
        int k = kget();
        if ((k==K_UP||k=='k') && sel>0) sel--;
        else if ((k==K_DOWN||k=='j') && sel<n-1) sel++;
        else if (k=='\n'||k=='\r') return sel;
        else if (k=='\033'||k=='q') return -1;
    }
}

/* ── Help ─────────────────────────────────────────────────── */
static void help(int W, int H) {
    const char *txt[] = {
        "GTD Todo Manager - Help","",
        "Navigation:","  j/Down / k/Up  - move","  g / G           - top / bottom","",
        "Views:","  i - Inbox","  c - Contexts (@)","  p - Projects (+)",
        "  t - Tags (#)","  r - Priorities (P1-P9)","  d - Dates (week view)","",
        "Date view:","  n - next week    m - previous week","  T - return to today","",
        "Actions:","  a     - add todo","  A     - new context/project/tag",
        "  e     - inline edit","  space - toggle collapse / toggle done",
        "  x     - delete","  v     - show/hide done items","",
        "Inline editing:","  arrows/^A/^E - move cursor","  ^W - delete word",
        "  ^K - delete to end","  Esc - cancel","",
        "Entry syntax:","  @context  +project  #tag  p1-p9  [date]",
        "  today / tomorrow / this month / next month","",
        "  q - quit    ? - help","","Press any key to close..."
    };
    int lc = sizeof(txt)/sizeof(txt[0]);
    int hh=lc+2, hw=0;
    for (int i=0; i<lc; i++) { int l=strlen(txt[i]); if (l>hw) hw=l; }
    hw+=4; if (hh>H) hh=H; if (hw>W) hw=W;
    int sy=(H-hh)/2, sx=(W-hw)/2;
    for (int i=0; i<hh; i++) { mv(sy+i,sx); for (int j=0; j<hw; j++) putchar(' '); }
    for (int i=0; i<hh; i++) { mv(sy+i,sx); putchar('|'); mv(sy+i,sx+hw-1); putchar('|'); }
    mv(sy,sx); for (int j=0; j<hw; j++) putchar('-');
    mv(sy+hh-1,sx); for (int j=0; j<hw; j++) putchar('-');
    for (int i=0; i<lc && i<hh-2; i++) { mv(sy+i+1,sx+2); printf("%s",txt[i]); }
    fflush(stdout); kget();
}

/* ── Line input ───────────────────────────────────────────── */
static int input(const char *prompt, char *buf, int max, int H) {
    mv(H-1,0); printf(CLRLN "%s", prompt); printf(SHOW_CURS); fflush(stdout);
    struct termios old, nw;
    tcgetattr(STDIN_FILENO, &old); nw=old;
    nw.c_lflag |= ECHO|ICANON;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &nw);
    buf[0]=0;
    if (fgets(buf, max, stdin)) buf[strcspn(buf,"\n")]=0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old);
    printf(HIDE_CURS);
    return buf[0] ? 0 : -1;
}

/* ── Create new context/project/tag ───────────────────────── */
static void new_container(App *a) {
    char *prompt = NULL;
    void (*addfn)(App*,const char*) = NULL;

    if (a->view == VIEW_CONTEXT) {
        prompt = "New context: "; addfn = add_ctx;
    } else if (a->view == VIEW_PROJECT) {
        prompt = "New project: "; addfn = add_proj;
    } else if (a->view == VIEW_TAG) {
        prompt = "New tag: "; addfn = add_tag;
    } else {
        char opts[3][MAX_STR] = {"Context","Project","Tag"};
        int sel = menu("Create new", opts, 3, a->w, a->h);
        if (sel < 0) return;
        switch (sel) {
            case 0: prompt = "New context: "; addfn = add_ctx; break;
            case 1: prompt = "New project: "; addfn = add_proj; break;
            case 2: prompt = "New tag: ";     addfn = add_tag;  break;
        }
    }

    if (prompt && addfn) {
        char name[MAX_STR];
        if (input(prompt, name, MAX_STR, a->h)==0 && name[0]) {
            addfn(a, name);
            save_meta(a);
            fb(a, "Created");
        }
    }
}

/* ── Main loop ────────────────────────────────────────────── */
static void run(App *a) {
    for (int run=1; run;) {
        draw(a);
        int k = kget();
        switch (k) {
        case 'q': run=0; break;
        case 'v':
            a->show_done = !a->show_done;
            fb(a, a->show_done?"Showing done":"Hiding done");
            a->sel=a->scroll=0; break;
        case 'i': a->view=VIEW_INBOX; a->filter[0]=0; a->sel=a->scroll=0; break;
        case 'r': a->view=VIEW_PRIORITY; a->filter[0]=0; a->sel=a->scroll=0; break;
        case 'd':
            a->view=VIEW_DATE; a->filter[0]=0; a->week_off=0;
            {   char today[11]; today_str(today);
                build_sections(a);
                Line lines[MAX_LINES]; int L = build_lines(a, lines);
                for (int i=0; i<L; i++) {
                    if (lines[i].type==0 && strstr(a->secs[lines[i].sec].name, today)) {
                        a->sel = i+1; a->scroll = MAX(0,i-1); break;
                    }
                }
            }
            break;
        case 'n':
            if (a->view == VIEW_DATE) { a->week_off++; a->sel=a->scroll=0; }
            break;
        case 'm':
            if (a->view == VIEW_DATE) { a->week_off--; a->sel=a->scroll=0; }
            break;
        case 'T':
            if (a->view == VIEW_DATE) {
                a->week_off = 0;
                char today[11]; today_str(today);
                build_sections(a);
                Line lines[MAX_LINES]; int L = build_lines(a, lines);
                for (int i=0; i<L; i++) {
                    if (lines[i].type==0 && strstr(a->secs[lines[i].sec].name, today)) {
                        a->sel = i+1; a->scroll = MAX(0,i-1); break;
                    }
                }
            }
            break;
        case 'c': case 'p': case 't': {
            int cnt = 0;
            char (*src)[MAX_STR] = NULL;
            if (k=='c') { cnt=a->ctx_cnt; src=a->contexts; }
            else if (k=='p') { cnt=a->proj_cnt; src=a->projects; }
            else { cnt=a->tag_cnt; src=a->tags; }
            if (cnt==0) { fb(a,"No items – press A to create one"); break; }
            int sel = menu(k=='c'?"Contexts":k=='p'?"Projects":"Tags", src, cnt, a->w, a->h);
            if (sel>=0) {
                a->view = k=='c'?VIEW_CONTEXT:k=='p'?VIEW_PROJECT:VIEW_TAG;
                strcpy(a->filter, src[sel]);
                a->sel=a->scroll=0;
            }
            break;
        }
        case 'a': {
            char in[MAX_TEXT];
            if (input("Add: ", in, MAX_TEXT, a->h)==0 && in[0]) {
                add_todo(a, in); fb(a, "Added");
            }
            break;
        }
        case 'A': new_container(a); break;
        case 'e': {
            build_sections(a);
            Line lines[MAX_LINES]; int L = build_lines(a, lines);
            if (a->sel<L && lines[a->sel].type==1) {
                int idx = lines[a->sel].idx;
                if (idx>=0 && idx<a->count) inline_edit(a, idx);
            }
            break;
        }
        case ' ': {
            build_sections(a);
            Line lines[MAX_LINES]; int L = build_lines(a, lines);
            if (a->sel >= L) break;
            if (lines[a->sel].type == 0) {
                int s = lines[a->sel].sec;
                a->secs[s].collapsed = !a->secs[s].collapsed;
                fb(a, a->secs[s].collapsed?"Collapsed":"Expanded");
            } else {
                int idx = lines[a->sel].idx;
                if (idx>=0 && idx<a->count) {
                    a->todos[idx].done = !a->todos[idx].done;
                    bury_done(a); save(a);
                    fb(a, a->todos[idx].done?"Done":"Undone");
                }
            }
            break;
        }
        case 'x': {
            build_sections(a);
            Line lines[MAX_LINES]; int L = build_lines(a, lines);
            if (a->sel<L && lines[a->sel].type==1) {
                int idx = lines[a->sel].idx;
                if (idx>=0 && idx<a->count) {
                    mv(a->h-1,0); printf(CLRLN"Delete '%s'? (y/n): ", a->todos[idx].text);
                    fflush(stdout);
                    if (kget()=='y') {
                        for (int j=idx; j<a->count-1; j++) a->todos[j]=a->todos[j+1];
                        a->count--; save(a); fb(a,"Deleted");
                        if (a->sel>=L-1 && a->sel>0) a->sel--;
                    }
                }
            }
            break;
        }
        case 'j': case K_DOWN: {
            build_sections(a);
            Line lines[MAX_LINES]; int L = build_lines(a, lines);
            if (L>0 && a->sel<L-1) a->sel++;
            break;
        }
        case 'k': case K_UP:
            if (a->sel>0) a->sel--; break;
        case 'g': a->sel=a->scroll=0; break;
        case 'G': {
            build_sections(a);
            Line lines[MAX_LINES]; int L = build_lines(a, lines);
            if (L>0) { a->sel=L-1; a->scroll=MAX(0,L-(a->h-6)); }
            break;
        }
        case '?': help(a->w, a->h); break;
        }
    }
}

/* ── Entry ────────────────────────────────────────────────── */
int main(void) {
    raw_on();
    printf(HIDE_CURS CLS); fflush(stdout);
    App a = {0};
    a.show_done = 1;
    load(&a);
    sync_meta(&a);
    bury_done(&a);
    run(&a);
    printf(CLS HOME SHOW_CURS); fflush(stdout);
    raw_off();
    return 0;
}

