//********************************************************************************
//* File       : getEmu.cpp                                                      *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2026-2027 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice in AcEdit.cpp.                      *
//* Date       : 29-Mar-2026                                                     *
//* Version    : (see AppVersion string)                                         *
//*                                                                              *
//* Description: Report the name of the terminal emulator program from which     *
//*              the application was launched.                                   *
//*              This is done by moving sequentially upward through the process  *
//*              tree. While this is a tedious "process" (pardon the pun), it    *
//*              is the most reliable way we have found so far.                  *
//*              For details, please see the notes in getEmulator(), below.      *
//*              - Optionally, report the name of the current shell program.     *
//*                For details, please see the notes in reportShellname(), below.*
//*  ---  ---   ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  *
//* Programmer's Note: This application leverages the gString-class functionality*
//* for handling the substrings; however, if you are still programming with      *
//* stone knives and bear skins, the 'sscanf' and 'sprintf' functions could      *
//* be used directly. (A tip-o-the-hat to Mister Spock.)                         *
//*                                                                              *
//********************************************************************************
//* Development Tools:                                                           *
//* ----------------------------------                                           *
//*  G++ v:15.2.1 20260123 (C++17)                                               *
//*  Fedora Linux v:42 or higher                                                 *
//*  GNOME Terminal v:3.46.8 or higher                                           *
//*                                                                              *
//********************************************************************************
//* Copyright Notice:                                                            *
//* This program is free software: you can redistribute it and/or modify it      *
//* under the terms of the GNU General Public License as published by the Free   *
//* Software Foundation, either version 3 of the License, or (at your option)    *
//* any later version.                                                           *
//*                                                                              *
//* This program is distributed in the hope that it will be useful, but          *
//* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY   *
//* or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License     *
//* for more details.                                                            *
//*                                                                              *
//* You should have received a copy of the GNU General Public License along      *
//* with this program.  If not, see <http://www.gnu.org/licenses/>.              *
//*                                                                              *
//********************************************************************************
//* Version History:                                                             *
//*                                                                              *
//* v: 0.00.01 27-Mar-2026                                                       *
//*   -- First Effort: Based on an idea from a discussion on Stack Overflow.     *
//*      The actual solutions were not sufficient, but the basic idea was valid, *
//*      so we ran with it.                                                      *
//*   -- The getEmulator() function was lifted intact from the author's          *
//*      AcEdit (console text editor) which is part of the larger AnsiCmd        *
//*      library which manages console I/O via ANSI escape sequences.            *
//*                                                                              *
//*                                                                              *
//********************************************************************************

//*****************
//* Include Files *
//*****************
#include <unistd.h>                 // UNIX/Linux system interface
#include <iostream>                 // standard I/O definitions
#include <fstream>                  // file I/O
#include <cstdlib>                  // misc. functionality
#include <locale>                   // support for locale-specific comparisons
#include "pwd.h"
#include "gString.hpp"              // gString class definition
using namespace std;

//***************
//* Definitions *
//***************
const wchar_t* const appVersion = L"0.0.01" ;
const wchar_t* const crYears    = L"2026-2027" ;
const char*    const titleTemplate =  
                     "getEmu (getemu) v:%S (c) %S  The Software Samurai\n" ;
const char* tfName = "./getemu_tmp.txt" ;   // name of temp file
typedef struct passwd PwdInfo ;
const wchar_t NEWLINE = L'\n' ;
const wchar_t NULLCHAR = L'\0' ;
const int ZERO = 0 ;

//**************
//* Local data *
//**************

//**************
//* Prototypes *
//**************
bool getEmulator ( gString& gsEmu, const char* tmpSpec = NULL ) ;
void reportShellname ( bool verbose ) ;
int helpMe ( int argc, char* argv[], char* argenv[] ) ;


//*************************
//*        main           *
//*************************
//********************************************************************************
//* Program entry point.                                                         *
//*                                                                              *
//********************************************************************************

int main ( int argc, char* argv[], char* argenv[] )
{
   gString gsEmu, gsOut ;
   int option = 1 ;

   //* Say Hello *
   gsOut.compose( titleTemplate, appVersion, crYears ) ;
   gsOut.padCols( (gsOut.gscols() * 2), L'=' ) ;
   wcout << gsOut << endl ;

   //* If command-line arguments, assume that it is a cry for help. *
   if ( argc > 1 )
      option = helpMe ( argc, argv, argenv ) ;

   //* Capture and report emulator data *
   if ( option > ZERO )
   {
      if ( (getEmulator ( gsEmu, tfName )) )
      {
         char emuName[64], emuVersion[64] ;
         pid_t emuPid ;
         // Note the format of the returned string. *
         gsEmu.gscanf( "%s :%d :%s", emuName, &emuPid, emuVersion ) ;
         gsOut.compose( "Emulator Name: '%s' Version:%s ProcID:%d",
                        emuName, emuVersion, &emuPid ) ;
         wcout << gsOut << endl ;

         //* If optional shell name report requested *
         if ( option > 1 )
            reportShellname ( bool(option > 2 ? true : false) ) ;

         wcout << endl ;
      }
      else
         wcout << L"Sorry, unable to determine emulator name.\n" << endl ;

      unlink ( tfName ) ;  // if temp file was created, delete it
   }

   //* Exit, returning control to the shell.*
   exit ( ZERO ) ;

}  //* End main() *

//*************************
//*      getEmulator      *
//*************************
//********************************************************************************
//* Traverse the parent PID records until the terminal emulator name is found.   *
//* or until the top of the process tree is reached.                             *
//* Return the two substring values separated by a colon token (" :").           *
//*    Example: "gnome-terminal :4495"                                           *
//*             |               |.... process ID                                 *
//*             |.............. emulator name                                    *
//* If specified, also return the version number substring for the emulator.     *
//*    Example: "gnome-terminal :4495 :3.56.3                                    *
//*             |               |     |...... process ID                         *
//*             |               |.... process ID                                 *
//*             |.............. emulator name                                    *
//*                                                                              *
//* The interesting data are in (at least) two files:                            *
//*      '/proc/PROCNUM/stat' and '/proc/PROCNUM/comm'                           *
//*                                                                              *
//* The 'stat' file's first line contains a bunch of stuff, but it is the        *
//* third (2nd) and fourth (4th) tokens that are of interest:                    *
//* token #1: this is the process ID for the current file.                       *
//* token #2: this is the name of the the process (enclosed in parentheses)      *
//*           Note that this may be some variation of the actual name; however,  *
//*           it is the canonical application name that is returned.             *
//* token #3: this is the letter 'S'. and we currently don't care what it means. *
//* token #4: this is the parent process's procID.                               *
//*                                                                              *
//* The 'comm' file (usually) has only one line and (usually) that line contains *
//* only the emulator name; but because this string is a duplicate of token #3   *
//* of the 'stat' file, we take a shortcut and do not open the 'comm' file.      *
//*                                                                              *
//* Input  : gsEmu  : (by reference) receives name of terminal emulator and      *
//*                   emulator version (see example)                             *
//*          tmpSpec: (optional, default:null pointer) if specified, this is a   *
//*                   filespec for a temporary file to be used to obtain the     *
//*                   version number of the terminal emulator.                   *
//*                   Note: If a valid emulator name but unable to capture       *
//*                         version number, the returned data will not include   *
//*                         the version number.                                  *
//*                                                                              *
//* Returns: 'true' if emulator identified, process name and process ID returned.*
//*          'false' if unable to determine emulator name.                       *
//*                  (buffer will contain an empty string)                       *
//*                  Note: It is possible that a valid, but unrecognized terminal*
//*                        emulator name may be encountered. The name of that    *
//*                        emulator may be added to the 'emuNames' array.        *
//********************************************************************************
//* Invoking a version number report for each of the listed emulator programs    *
//* is shown.                                                                    *
//*   gnome-terminal --version                                                   *
//*      # GNOME Terminal 3.56.3 using VTE 0.80.5 +BIDI +GNUTLS +ICU +SYSTEMD    *
//*   konsole --version (-v)                                                     *
//*      konsole 25.12.3                                                         *
//*   xterm -version                                                             *
//*      XTerm(397)                                                              *
//*   guake ?                                                                    *
//*   tilda --version                                                            *
//*   terminator --version                                                       *
//*   yakuake --version (-v)                                                     *
//*                                                                              *
//* Other Emulators:                                                             *
//* Alacritty, Kitty, Tilix, WezTerm, Foot, LXterminal, Sakura, st, PuTTY,       *
//* xfce4-terminal, urxvt, rxvt,                                                 *
//*  ---  ---   ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  *
//* Programmer's Note: The name of the current shell program could be obtained   *
//* using this same algorithm; however, there are simpler ways to do that.       *
//* See the standard library function getpwuid_r() for details.                  *
//********************************************************************************

bool getEmulator ( gString& gsEmu, const char* tmpSpec )
{
   const short enMAX = 7 ;
   const char* emuNames[enMAX]
   {
      "gnome-terminal",
      "konsole",
      "xterm",
      "guake",
      "tilda",
      "terminator",
      "yakuake",
   } ;
   const char* emuVers[enMAX]
   {
      "gnome-terminal --version",
      "konsole --version",
      "xterm -version",
      "guake --version", // (uncertain)
      "tilda --version",
      "terminator --version",
      "yakuake --version",
   } ;
   const char* scanTemplate = "%d %s %s %d" ;
   gString gsSpec, gs ;
   pid_t pid = getppid (),    // PARENT process ID 
         spid, ppid ;
   char raw[256], pname[64], toss[64] ;
   ifstream ifs ;
   int indx ;
   bool status = true ;
   gsEmu.clear() ;                        // initialize caller's buffer

   for ( short p = 5 ; (p > ZERO) && (status != false) ; --p )
   {
      gsSpec.compose( "/proc/%d/stat", &pid ) ;
      ifs.open ( gsSpec.ustr(), ifstream::in ) ;
      if ( ifs.is_open() )
      {
         ifs.getline ( raw, 256, NEWLINE ) ; // get the first line of the file
         if ( (ifs.good() || ifs.gcount() > ZERO) )
         {
            gs = raw ;
            if ( (gs.gscanf( scanTemplate, &spid, pname, toss, &ppid )) == 4 )
            {
               //* Note that a ppid==1 indicates the top of the process tree.*
               if ( (spid == pid) && (ppid > 1) )
               {
                  //* Strip the '()' from 'pname' , then *
                  //* test for something that looks like *
                  //* the name of a terminal emulator.   *
                  gs = pname ;
                  if ( (indx = gs.find( L'(')) >= ZERO )
                     gs.erase( indx, 1 ) ;
                  if ( (indx = gs.find( L')')) >= ZERO )
                     gs.erase( indx, 1 ) ;
                  gs.copy( pname, 64 ) ;
                  for ( indx = ZERO ; indx < enMAX ; ++indx )
                  { if ( (gs.find( emuNames[indx] )) >= ZERO ) { break ; } }

                  //* If a matching emulator name, *
                  //* construct the return value.  *
                  if ( indx < enMAX )
                  {
                     gsEmu.compose( "%s :%d", emuNames[indx], &pid ) ;
                     ifs.close() ;        // close the file
                     break ;              // success
                  }

                  pid = ppid ;            // move to the next higher level
               }
               else
                  status = false ;
            }
            else { status = false ; }
         }
         else { status = false ; }
         ifs.close() ;                    // close the file
      }
   }

   //* If specified, capture the version number.*
   if ( status && tmpSpec != NULL )
   {
      const char* cmdTemplate = 
               "%s 1>\"%s\" 2>/dev/null" ;
      //* Invoke the application, requesting version number. 
      gsSpec.compose( cmdTemplate, emuVers[indx], tmpSpec ) ;
      system ( gsSpec.ustr() ) ;
      ifs.open ( tmpSpec, ifstream::in ) ;
      if ( ifs.is_open() )
      {
         ifs.getline ( raw, 256, NEWLINE ) ; // get the first line of the file
         if ( (ifs.good()) || (ifs.gcount() > ZERO) )
         {
            //* Extract the first numeric substring *
            gs = raw ;
            const wchar_t* wptr = gs.gstr() ;
            wchar_t verString[64] ;
            int   eof = gs.gschars(), veri = ZERO ;
            indx = ZERO ;
            while ( (indx < eof) && 
                    !((wptr[indx] >= L'0') && (wptr[indx] <= L'9')) )
            { ++indx ; }
            if ( indx < eof )
            {
               while ( (indx < eof) && 
                       (((wptr[indx] >= L'0') && (wptr[indx] <= L'9')) 
                         || (wptr[indx] == L'.')) )
               { verString[veri++] = wptr[indx++] ; }
               verString[veri] = NULLCHAR ;
               gsEmu.append( " :%S", verString ) ;
            }
         }
         ifs.close() ;
      }
   }

   return status ;

}  //* End getEmulator() *

//*************************
//*    reportShellname    *
//*************************
//********************************************************************************
//* Report the the name of the current shell program.                            *
//* This demonstrates the use of the getpwuid/getpwuid_r functions.              *
//*                                                                              *
//* Note that the called function returns considerably more data than just the   *
//* name of the shell program.                                                   *
//*  -- For a partial list of the data available, invoke with the "--verbose"    *
//*     option.                                                                  *
//*  -- For a more detailed example, see the author's AcEdit application,        *
//*     IdentifyUser method. AcEdit is distributed as part of the AnsiCmd library*
//*                                                                              *
//* Input  : verbose: if false, report only the name of the shell program        *
//*                   if true,  report name of shell program and additional info *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note:                                                           *
//*                                                                              *
//* The passwd structure - from pwd.h                                            *
//* struct passwd                                                                *
//* {                                                                            *
//*   char *pw_name;                /* Username.       */                        *
//*   char *pw_passwd;              /* Password.       */                        *
//*   __uid_t pw_uid;               /* User ID.        */                        *
//*   __gid_t pw_gid;               /* Group ID.       */                        *
//*   char *pw_gecos;               /* Real name.      */                        *
//*   char *pw_dir;                 /* Home directory. */                        *
//*   char *pw_shell;               /* Shell program.  */                        *
//* };                                                                           *
//*                                                                              *
//********************************************************************************

void reportShellname ( bool verbose )
{
   char pwBuff[gsDFLTBYTES] ;       // temporary buffer receives string data
   PwdInfo  pw ;                    // receives copy of kernel structure for user info
   PwdInfo* pwp ;                   // (dummy pointer, this is completely worthless)
   gString  gsOut( "Shell Program: " ) ;
   uid_t    userID = getuid () ;    // get user's ID

   if ( (getpwuid_r ( userID, &pw, pwBuff, gsDFLTBYTES, &pwp )) == ZERO )
   {
      gsOut.append( "'%s'", pw.pw_shell ) ;

      if ( verbose )
      {
         gsOut.append( " User:%s uid:%d\n    Real Name: '%s' Home Dir:%s", 
                       pw.pw_name, &pw.pw_uid, pw.pw_gecos, pw.pw_dir ) ;
      }
   }
   else
      gsOut.append( "(system call failed)" ) ;
   wcout << gsOut << endl ;

}  //* End reportShellname() *

//*************************
//*        helpMe         *
//*************************
//********************************************************************************
//* Display application copyright notice and help. (proforma)                    *
//* Note that caller has already displayed the application title and version #.  *
//*                                                                              *
//* NOTE: The scanning of command-line options here is a very simple             *
//*       implementation. If this were a real application, a more user-friendly  *
//*       approach would be needed.                                              *
//*                                                                              *
//* Input  : argc   : count of command line arguments                            *
//*          argv   : array of argument strings                                  *
//*          argenv : array of environment variable definitions (not used)       *
//*                                                                              *
//* Returns: 0 == request for help, version OR invalid argument (exit on return) *
//*          1 == execute the basic scan                                         *
//*          2 == execute BOTH the basic scan and the shell program report       *
//*          3 == execute basic scan,shell program report AND verbose report     *
//********************************************************************************

int helpMe ( int argc, char* argv[], char* argenv[] )
{
   const char* copyRight = 
       "License GPLv3+:\n"
       "GNU GPL version 3 <http://gnu.org/licenses/gpl.html>\n"
       "This is free software: you are free to modify and/or\n"
       "redistribute it under the terms set out in the license.\n"
       "There is NO WARRANTY, to the extent permitted by law." ;
   const char* helpMessage = 
       "getemu OPTIONS   If no options, execute process-tree scan.\n"
       "   --shell   : include name of shell program in report\n"
       "   --verbose : include shell program and additional info\n"
       "   --version : display application version and copyright\n"
       "   --help    : display application help\n" ;

   gString gsOut( titleTemplate, appVersion, crYears ), gs ;
   int msgWidth = gsOut.gscols() ;        // width of message in columns
   int option = 1 ;                       // return value
   bool version = false ;                 // set if version request

   //* If user provided command-line arguments, parse them. *
   //* Note that first (0th) argument is application name.  *
   if ( argc > 1 )
   {
      for ( int i = 1 ; i < argc ; ++i )
      {
         gs = argv[i] ;
         if ( (gs.find( "--" )) == ZERO )
         {
            if ( (gs.find( "shell", 2, false )) > ZERO )
               option = 2 ;
            else if ( (gs.find( "verbose", 2, false )) > ZERO )
               option = 3 ;
            else if ( (gs.find( "help", 2, false )) > ZERO )
               option = 0 ;
            else if ( (gs.find( "version", 2, false )) > ZERO )
               { option = 0 ; version = true ; break ; }
            else { option = ZERO ; break ; } // invalid/unrecognized option
         }
         else { option = ZERO ; break ; } // invalid/unrecognized option
      }
   }

   //* If Help and/or version report *
   if ( option == ZERO )
   {
      if ( version )                      // report version and copyright
      {
         gs.clear() ;
         gs.padCols( msgWidth, L'-' ) ;
         const char* gsver = gsOut.Get_gString_Version() ;
         gsOut.compose( "gString class v:%s\n%S\n%s\n", 
                        gsver, gs.gstr(), copyRight, copyRight ) ;
         wcout << gsOut << endl ;
      }
      else                                // report command line options
         wcout << helpMessage << endl ;
   }
   return option ;

}  //* End helpMe() *
