/* Fvwm GNU Screen/TMUX menu generator
 * Copyright September 2025 Dennis Katsonis
 * Author: dennisk@netspace.net.au
 *
 * By default, this uses the "xterm" shell.
 *
 * If you want to specify a different shell for SCREEN or TMUX,
 * define them when compiling
 * Add -DSCREEN_TERM_CMD="\"myterm -n termname\""
 * and/or
 * -DTMUX_TERM_CMD="\"myterm -n termname\""
 * Note that you need the escaped quotes within the outer double quotes. */

 
#include <getopt.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <regex.h>

#ifndef SCREEN_TERM_CMD
#define SCREEN_TERM_CMD "xterm -n screen"
#endif

#ifndef TMUX_TERM_CMD
#define TMUX_TERM_CMD  "xterm -n tmux"
#endif

#define SCREEN_CMD " -e screen -r"
#define TMUX_CMD " -e tmux attach-session -t "

#define SCREEN_CMD_FULL SCREEN_TERM_CMD SCREEN_CMD
#define TMUX_CMD_FULL TMUX_TERM_CMD TMUX_CMD

#define HOSTNAME_MAX 255
#define PROCESS_INPUT_LINE_MAX 256

#define NOTMUX 1
#define NOSCREEN 2
#define VERSION_FLAG 3
#define HELP 4

#define	CHUNK_SIZE 2048 /* Size of memory to allocate to store output strings
			* This should not be smaller than the largest line
			* that this program may output. */
#define	MAX_GROUPS 2 /* Maximum number of regex groups to store.  Shouldn't need more than two. */

typedef struct {
    char *items;
    char *ptr;
    size_t size;
} screenList;

enum options {
    F_NOSCREEN = 0x0001,
    F_NOTMUX = 0x0002,
    F_HASATTACHED = 0x0004,
    F_HASDETACHED = 0x0008
};

static void processLine(const regmatch_t *pmatch, screenList *list, const char * cmd, const char *inputLine);

static int initScreenList(screenList *list);
static int appendScreenList(screenList *list, const char *stringLine);
static void printScreenList(const screenList *list);
static void freeScreenList(screenList *list);
static int screenlist(void);
static int tmuxlist(void);

static const char *instruction = "Fvwm GNU Screen/TMUX menu generator\n"
    "Usage:\n"
    "\t--help		This help screen\n"
    "\t--version	Version\n"
    "Output control:\n"
    "\t--notmux	Disable TMUX usage.\n"
    "\t--noscreen	Disable GNU Screen usage.\n";

static const char* versionString = "1.0.0";
static const char* licenseText = "\tCopyright Dennis Katsonis, 2025\n\n"
    "This program is free software: you can redistribute it and/or modify\n"
    "it under the terms of the GNU General Public License as published by\n"
    "the Free Software Foundation, either version 3 of the License, or\n"
    "(at your option) any later version.\n\n"
    "This program is distributed in the hope that it will be useful,\n"
    "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
    "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
    "GNU General Public License for more details.\n"
    "You should have received a copy of the GNU General Public License\n"
    "along with this program.  If not, see <http://www.gnu.org/licenses/>.\n";


int main (int argc, char **argv)
{
    static struct option long_options[] = 
        {
    	{"notmux", no_argument, NULL, NOTMUX },
    	{"noscreen", no_argument, NULL, NOSCREEN },
    	{"version", no_argument, NULL, VERSION_FLAG },
    	{"help", no_argument, NULL, HELP},
    	{NULL, 0, NULL, 0 }
        };

    int flags = 0;
    int option_index = 0;
    int opt;
    while((opt = getopt_long(argc,
			     argv,
			     "",
			     long_options,
			     &option_index)) != -1) {
    switch(opt) {
    case NOTMUX:
	flags |= F_NOTMUX;
	break;
    case NOSCREEN:
	flags |= F_NOSCREEN;
	break;
    case VERSION_FLAG:
	printf("Version: %s", versionString);
	puts(licenseText);
	exit(0);
	break;
    case HELP:
	puts(instruction);
	exit(0);
	break;
    default:
	break;
    }
    }
    if (!(flags & F_NOTMUX)) tmuxlist();
    if (!(flags & F_NOSCREEN)) screenlist();
    exit(EXIT_SUCCESS);
}


int screenlist()
{
    const char	*re_detached = "([^ \t]+).*Detached";
    const char	*re_attached = "([^ \t]+).*Attached";
    regex_t     pregd, prega;
    regmatch_t  pmatch_a[MAX_GROUPS], pmatch_d[MAX_GROUPS];
    int		result_d,result_a;
    FILE	*fp;
    screenList	attachedList, detachedList;
    int		flags = 0;
    char 	process_input_line[PROCESS_INPUT_LINE_MAX];


    initScreenList(&attachedList);
    initScreenList(&detachedList);
    fp = popen("screen -list", "r");
    if (fp == NULL) {
	printf("Failed to run command\n" );
	exit(1);
    }
    
    if (regcomp(&pregd, re_detached, REG_EXTENDED))
	exit(EXIT_FAILURE);
    if (regcomp(&prega, re_attached, REG_EXTENDED))
	exit(EXIT_FAILURE);
    while (fgets(process_input_line, PROCESS_INPUT_LINE_MAX, fp) != NULL) {
	result_d = regexec(&pregd,process_input_line,MAX_GROUPS,pmatch_d,0);
	result_a = regexec(&prega,process_input_line,MAX_GROUPS,pmatch_a,0);
	if (result_d != REG_NOMATCH) {
	    processLine(&(pmatch_d[1]), &detachedList, SCREEN_CMD_FULL, process_input_line);
	    flags |= F_HASDETACHED;
	}
	else if (result_a != REG_NOMATCH) {
	    processLine(&(pmatch_a[1]), &attachedList, SCREEN_CMD_FULL, process_input_line);
	    flags |= F_HASATTACHED;
	}
    }
    regfree(&pregd);
    regfree(&prega);
    pclose(fp);
    puts(" + \"SCREEN\" Title");
    printf(" + \"New Session\" Exec exec %s -e screen\n", SCREEN_TERM_CMD);
    printf(" + \"Reattach first\" Exec exec %s -e screen -d -RR\n", SCREEN_TERM_CMD);
    if (flags & F_HASATTACHED) {
	puts(" + \"Screen Attached\" Title");
	printScreenList(&attachedList);
    }
    if (flags & F_HASDETACHED) {
	puts(" + \"Screen Detached\" Title");
	printScreenList(&detachedList);
    }
    freeScreenList(&detachedList);
    freeScreenList(&attachedList);
    return EXIT_SUCCESS;
}


int tmuxlist()
{
    const char	*re_detached = "(.*) Detached";
    const char	*re_attached = "(.*) Attached";
    regex_t     pregd, prega;
    regmatch_t  pmatch_a[MAX_GROUPS], pmatch_d[MAX_GROUPS];
    int		result_d,result_a;
    FILE	*fp;
    screenList	attachedList, detachedList;
    int		flags = 0;
    char	process_input_line[PROCESS_INPUT_LINE_MAX];
    
    initScreenList(&attachedList);
    initScreenList(&detachedList);
    fp = popen("tmux list-sessions -F \"#{session_name} #{?session_attached,Attached,Detached}\"", "r");
    if (fp == NULL) {
	printf("Failed to run command\n" );
	exit(1);
    }
    
    if (regcomp(&pregd, re_detached, REG_EXTENDED))
	exit(EXIT_FAILURE);
    if (regcomp(&prega, re_attached, REG_EXTENDED))
	exit(EXIT_FAILURE);
    
    while (fgets(process_input_line, PROCESS_INPUT_LINE_MAX, fp) != NULL) {
	result_d = regexec(&pregd,process_input_line,MAX_GROUPS,pmatch_d,0);
	result_a = regexec(&prega,process_input_line,MAX_GROUPS,pmatch_a,0);
	if (result_d != REG_NOMATCH) {
	    processLine(&(pmatch_d[1]), &detachedList, TMUX_CMD_FULL, process_input_line);
	    flags |= F_HASDETACHED; 
	}
	else if (result_a != REG_NOMATCH) {
	    processLine(&(pmatch_a[1]), &attachedList, TMUX_CMD_FULL, process_input_line);
	    flags |= F_HASATTACHED; 

	}
    }

    pclose(fp);
    regfree(&pregd);
    regfree(&prega);

    puts(" + \"TMUX\" Title");
    printf(" + \"New Session\" Exec exec %s -e tmux\n", TMUX_TERM_CMD);
    printf(" + \"Reattach first\" Exec exec %s -e tmux attach-session\n" ,TMUX_TERM_CMD);
    if (flags & F_HASATTACHED) {
	puts(" + \"Tmux Attached\" Title");
	printScreenList(&attachedList);
    }
    if (flags & F_HASDETACHED) {
	puts(" + \"Tmux Detached\" Title");
	printScreenList(&detachedList);
    }
    freeScreenList(&detachedList);
    freeScreenList(&attachedList);
    return EXIT_SUCCESS;
}

int initScreenList(screenList *list)
{
    list->items = malloc(CHUNK_SIZE);
    if (list->items == NULL)
    {
	puts("Memory allocation failure.");
	return EXIT_FAILURE;
    }
    list->size = CHUNK_SIZE;
    list->ptr = list->items;
    list->items[0] = 0;
    return EXIT_SUCCESS;
}

int appendScreenList(screenList *list, const char *stringLine)
{
    size_t strSize = strlen(stringLine) + 1; /* +1 for the "\n" we add to the end. */

    if (list->items == NULL)
	return EXIT_FAILURE;
  
    if ((list->size - (list->ptr - list->items))  <= (strSize + 4))
    {
	if(strSize >= (CHUNK_SIZE + (list->size - (list->ptr - list->items))))
	{
	    printf("Cannot fit string size %zu. Memory allocation chunk too small\n", strSize);
	    exit(EXIT_FAILURE);
	}
	/* Save ptr offset */
	size_t offset = list->ptr - list->items;
    
	list->items = realloc(list->items, list->size + CHUNK_SIZE);
	if (list->items == NULL)
	{
	    return EXIT_FAILURE;
	}
	list->size+=CHUNK_SIZE;
	/* Restore ptr offset 
	   The ptr offset avoid having to scan the entire string
	   for the terminating null on each call of strlen */
	list->ptr = list->items;
	list->ptr += offset;
    }
    strcat(list->ptr, stringLine);
    strcat(list->ptr, "\n");
    list->ptr+=strSize;
    return EXIT_SUCCESS;
}

void freeScreenList(screenList *list)
{
    list->ptr = NULL;
    list->size = 0;
    if (list->items != NULL)
	free(list->items);
    list->items = NULL;
}

void printScreenList(const screenList *list)
{
    if (list->items != NULL)
	printf("%s",list->items);
}

void processLine(const regmatch_t *pmatch, screenList *list, const char *cmd, const char *inputLine)
{
    regoff_t off, len;
    char processed_line[PROCESS_INPUT_LINE_MAX];
		
    off = pmatch->rm_so;
    len = pmatch->rm_eo - pmatch->rm_so;
    if (snprintf(processed_line,
		 PROCESS_INPUT_LINE_MAX,
		 " + \"%.*s\" Exec exec %s \"%.*s\"",
		 len,
		 inputLine + pmatch->rm_so,
		 cmd,
		 len,
		 inputLine + pmatch->rm_so) < 0) {
	puts("Error creating entry");
	exit(1);
    }
    appendScreenList(list, processed_line);
}
