/*
htop - Settings.c
(C) 2004-2011 Hisham H. Muhammad
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/

#include "config.h" // IWYU pragma: keep

#include "Settings.h"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#include "CRT.h"
#include "DynamicColumn.h"
#include "DynamicScreen.h"
#include "Macros.h"
#include "Meter.h"
#include "Platform.h"
#include "Process.h"
#include "Table.h"
#include "XUtils.h"


static void Settings_deleteColumns(Settings* this) {
   for (size_t i = 0; i < HeaderLayout_getColumns(this->hLayout); i++) {
      String_freeArray(this->hColumns[i].names);
      free(this->hColumns[i].modes);
   }
   free(this->hColumns);
}

static void Settings_deleteScreens(Settings* this) {
   if (this->screens) {
      for (size_t i = 0; this->screens[i]; i++)
         ScreenSettings_delete(this->screens[i]);
      free(this->screens);
   }
}

void Settings_delete(Settings* this) {
   free(this->filename);
   free(this->initialFilename);
   Settings_deleteColumns(this);
   Settings_deleteScreens(this);
   free(this);
}

static char** Settings_splitLineToIDs(const char* line) {
   char* trim = String_trim(line);
   char** ids = String_split(trim, ' ', NULL);
   free(trim);
   return ids;
}

static void Settings_readMeters(Settings* this, const char* line, size_t column) {
   column = MINIMUM(column, HeaderLayout_getColumns(this->hLayout) - 1);
   this->hColumns[column].names = Settings_splitLineToIDs(line);
}

static void Settings_readMeterModes(Settings* this, const char* line, size_t column) {
   char** ids = Settings_splitLineToIDs(line);

   size_t len = 0;
   for (size_t i = 0; ids[i]; i++) {
      len++;
   }

   column = MINIMUM(column, HeaderLayout_getColumns(this->hLayout) - 1);
   this->hColumns[column].len = len;
   MeterModeId* modes = len ? xCalloc(len, sizeof(MeterModeId)) : NULL;
   for (size_t i = 0; i < len; i++) {
      modes[i] = (MeterModeId) atoi(ids[i]);
   }
   this->hColumns[column].modes = modes;

   String_freeArray(ids);
}

static bool Settings_validateMeters(Settings* this) {
   const size_t colCount = HeaderLayout_getColumns(this->hLayout);

   bool anyMeter = false;

   for (size_t column = 0; column < colCount; column++) {
      char** names = this->hColumns[column].names;
      const MeterModeId* modes = this->hColumns[column].modes;
      const size_t len = this->hColumns[column].len;

      if (!len)
         continue;

      if (!names || !modes)
         return false;

      anyMeter |= !!len;

      // Check for each mode there is an entry with a non-NULL name
      for (size_t meterIdx = 0; meterIdx < len; meterIdx++)
         if (!names[meterIdx])
            return false;

      if (names[len])
         return false;
   }

   return anyMeter;
}

static void Settings_defaultMeters(Settings* this, const Machine* host) {
   unsigned int initialCpuCount = host->activeCPUs;
   size_t sizes[] = { 3, 3 };

   if (initialCpuCount > 4 && initialCpuCount <= 128) {
      sizes[1]++;
   }

   // Release any previously allocated memory
   Settings_deleteColumns(this);

   this->hLayout = HF_TWO_50_50;
   this->hColumns = xCalloc(HeaderLayout_getColumns(this->hLayout), sizeof(MeterColumnSetting));
   for (size_t i = 0; i < 2; i++) {
      this->hColumns[i].names = xCalloc(sizes[i] + 1, sizeof(*this->hColumns[0].names));
      this->hColumns[i].modes = xCalloc(sizes[i], sizeof(*this->hColumns[0].modes));
      this->hColumns[i].len = sizes[i];
   }

   int r = 0;

   if (initialCpuCount > 128) {
      // Just show the average, ricers need to config for impressive screenshots
      this->hColumns[0].names[0] = xStrdup("CPU");
      this->hColumns[0].modes[0] = BAR_METERMODE;
   } else if (initialCpuCount > 32) {
      this->hColumns[0].names[0] = xStrdup("LeftCPUs8");
      this->hColumns[0].modes[0] = BAR_METERMODE;
      this->hColumns[1].names[r] = xStrdup("RightCPUs8");
      this->hColumns[1].modes[r++] = BAR_METERMODE;
   } else if (initialCpuCount > 16) {
      this->hColumns[0].names[0] = xStrdup("LeftCPUs4");
      this->hColumns[0].modes[0] = BAR_METERMODE;
      this->hColumns[1].names[r] = xStrdup("RightCPUs4");
      this->hColumns[1].modes[r++] = BAR_METERMODE;
   } else if (initialCpuCount > 8) {
      this->hColumns[0].names[0] = xStrdup("LeftCPUs2");
      this->hColumns[0].modes[0] = BAR_METERMODE;
      this->hColumns[1].names[r] = xStrdup("RightCPUs2");
      this->hColumns[1].modes[r++] = BAR_METERMODE;
   } else if (initialCpuCount > 4) {
      this->hColumns[0].names[0] = xStrdup("LeftCPUs");
      this->hColumns[0].modes[0] = BAR_METERMODE;
      this->hColumns[1].names[r] = xStrdup("RightCPUs");
      this->hColumns[1].modes[r++] = BAR_METERMODE;
   } else {
      this->hColumns[0].names[0] = xStrdup("AllCPUs");
      this->hColumns[0].modes[0] = BAR_METERMODE;
   }
   this->hColumns[0].names[1] = xStrdup("Memory");
   this->hColumns[0].modes[1] = BAR_METERMODE;
   this->hColumns[0].names[2] = xStrdup("Swap");
   this->hColumns[0].modes[2] = BAR_METERMODE;
   this->hColumns[1].names[r] = xStrdup("Tasks");
   this->hColumns[1].modes[r++] = TEXT_METERMODE;
   this->hColumns[1].names[r] = xStrdup("LoadAverage");
   this->hColumns[1].modes[r++] = TEXT_METERMODE;
   this->hColumns[1].names[r] = xStrdup("Uptime");
   this->hColumns[1].modes[r++] = TEXT_METERMODE;
}

static const char* toFieldName(Hashtable* columns, int id, bool* enabled) {
   if (id < 0) {
      if (enabled)
         *enabled = false;
      return NULL;
   }
   if (id >= ROW_DYNAMIC_FIELDS) {
      const DynamicColumn* column = DynamicColumn_lookup(columns, id);
      if (enabled)
         *enabled = column ? column->enabled : false;
      return column ? column->name : NULL;
   }
   if (enabled)
      *enabled = true;
   return Process_fields[id].name;
}

static int toFieldIndex(Hashtable* columns, const char* str) {
   if (isdigit((unsigned char)str[0])) {
      // This "+1" is for compatibility with the older enum format.
      int id = atoi(str) + 1;
      if (toFieldName(columns, id, NULL)) {
         return id;
      }
   } else {
      // Dynamically-defined columns are always stored by-name.
      char dynamic[32] = {0};
      if (sscanf(str, "Dynamic(%30s)", dynamic) == 1) {
         char* end;
         if ((end = strrchr(dynamic, ')')) != NULL) {
            bool success;
            unsigned int key;
            *end = '\0';
            success = DynamicColumn_search(columns, dynamic, &key) != NULL;
            *end = ')';
            if (success)
               return key;
         }
      }
      // Fallback to iterative scan of table of fields by-name.
      for (int p = 1; p < LAST_PROCESSFIELD; p++) {
         const char* pName = toFieldName(columns, p, NULL);
         if (pName && strcmp(pName, str) == 0)
            return p;
      }
   }
   return -1;
}

static void ScreenSettings_readFields(ScreenSettings* ss, Hashtable* columns, const char* line) {
   char* trim = String_trim(line);
   char** ids = String_split(trim, ' ', NULL);
   free(trim);

   /* reset default fields */
   memset(ss->fields, '\0', LAST_PROCESSFIELD * sizeof(ProcessField));

   for (size_t j = 0, i = 0; ids[i]; i++) {
      if (j >= UINT_MAX / sizeof(ProcessField))
         continue;
      if (j >= LAST_PROCESSFIELD) {
         ss->fields = xRealloc(ss->fields, (j + 1) * sizeof(ProcessField));
         memset(&ss->fields[j], 0, sizeof(ProcessField));
      }
      int id = toFieldIndex(columns, ids[i]);
      if (id >= 0)
         ss->fields[j++] = id;
      if (id > 0 && id < LAST_PROCESSFIELD)
         ss->flags |= Process_fields[id].flags;
   }
   String_freeArray(ids);
}

static ScreenSettings* Settings_initScreenSettings(ScreenSettings* ss, Settings* this, const char* columns) {
   ScreenSettings_readFields(ss, this->dynamicColumns, columns);
   this->screens[this->nScreens] = ss;
   this->nScreens++;
   this->screens = xRealloc(this->screens, sizeof(ScreenSettings*) * (this->nScreens + 1));
   this->screens[this->nScreens] = NULL;
   return ss;
}

ScreenSettings* Settings_newScreen(Settings* this, const ScreenDefaults* defaults) {
   int sortKey = defaults->sortKey ? toFieldIndex(this->dynamicColumns, defaults->sortKey) : PID;
   int treeSortKey = defaults->treeSortKey ? toFieldIndex(this->dynamicColumns, defaults->treeSortKey) : PID;
   int sortDesc = (sortKey >= 0 && sortKey < LAST_PROCESSFIELD) ? Process_fields[sortKey].defaultSortDesc : 1;

   ScreenSettings* ss = xMalloc(sizeof(ScreenSettings));
   *ss = (ScreenSettings) {
      .heading = xStrdup(defaults->name),
      .dynamic = NULL,
      .table = NULL,
      .fields = xCalloc(LAST_PROCESSFIELD, sizeof(ProcessField)),
      .flags = 0,
      .direction = sortDesc ? -1 : 1,
      .treeDirection = 1,
      .sortKey = sortKey,
      .treeSortKey = treeSortKey,
      .treeView = false,
      .treeViewAlwaysByPID = false,
      .allBranchesCollapsed = false,
   };
   return Settings_initScreenSettings(ss, this, defaults->columns);
}

ScreenSettings* Settings_newDynamicScreen(Settings* this, const char* tab, const DynamicScreen* screen, Table* table) {
   int sortKey = toFieldIndex(this->dynamicColumns, screen->columnKeys);

   ScreenSettings* ss = xMalloc(sizeof(ScreenSettings));
   *ss = (ScreenSettings) {
      .heading = xStrdup(tab),
      .dynamic = xStrdup(screen->name),
      .table = table,
      .fields = xCalloc(LAST_PROCESSFIELD, sizeof(ProcessField)),
      .direction = screen->direction,
      .treeDirection = 1,
      .sortKey = sortKey,
   };
   return Settings_initScreenSettings(ss, this, screen->columnKeys);
}

void ScreenSettings_delete(ScreenSettings* this) {
   free(this->heading);
   free(this->dynamic);
   free(this->fields);
   free(this);
}

static ScreenSettings* Settings_defaultScreens(Settings* this) {
   if (this->nScreens)
      return this->screens[0];
   for (unsigned int i = 0; i < Platform_numberOfDefaultScreens; i++) {
      const ScreenDefaults* defaults = &Platform_defaultScreens[i];
      Settings_newScreen(this, defaults);
   }
   Platform_defaultDynamicScreens(this);
   return this->screens[0];
}

static bool Settings_read(Settings* this, const char* fileName, const Machine* host, bool checkWritability) {
   int fd = -1;
   const char* fopen_mode = "r+";
   if (checkWritability) {
      do {
         fd = open(fileName, O_RDWR | O_NOCTTY | O_NOFOLLOW);
      } while (fd < 0 && errno == EINTR);

      if (fd < 0) {
         this->writeConfig = (errno == ENOENT);
         if (errno != EACCES && errno != EPERM && errno != EROFS) {
            return false;
         }
      } else {
         // Write the config only if the file is:
         // (1) a regular file (not a device file like /dev/null)
         // (2) owned by the effective user ID
         // (3) has write permission for owner
         //     (the root user usually bypasses access control
         //     by default; see CAP_DAC_OVERRIDE on Linux)
         struct stat sb;
         int err = fstat(fd, &sb);
         this->writeConfig = !err && S_ISREG(sb.st_mode) && (sb.st_mode & S_IWUSR) && sb.st_uid == geteuid();
      }
   }

   // If opening for read & write is not needed or fails, open for read only.
   // There is no risk of following symlink in this case.
   if (fd < 0) {
      fopen_mode = "r";
      do {
         fd = open(fileName, O_RDONLY | O_NOCTTY);
      } while (fd < 0 && errno == EINTR);
   }

   if (fd < 0)
      return false;

   FILE* fp = fdopen(fd, fopen_mode);
   if (!fp) {
      close(fd);
      return false;
   }

   ScreenSettings* screen = NULL;
   bool didReadMeters = false;
   bool didReadAny = false;
   for (;;) {
      char* line = String_readLine(fp);
      if (!line) {
         break;
      }
      didReadAny = true;
      size_t nOptions;
      char** option = String_split(line, '=', &nOptions);
      free (line);
      if (nOptions < 2) {
         String_freeArray(option);
         continue;
      }
      if (String_eq(option[0], "config_reader_min_version")) {
         this->config_version = atoi(option[1]);
         if (this->config_version > CONFIG_READER_MIN_VERSION) {
            // the version of the config file on disk is newer than what we can read
            fprintf(stderr, "WARNING: %s specifies configuration format\n", fileName);
            fprintf(stderr, "         version v%d, but this %s binary only supports up to version v%d.\n", this->config_version, PACKAGE, CONFIG_READER_MIN_VERSION);
            fprintf(stderr, "         The configuration file will be downgraded to v%d when %s exits.\n", CONFIG_READER_MIN_VERSION, PACKAGE);
            String_freeArray(option);
            fclose(fp);
            return false;
         }
      } else if (String_eq(option[0], "fields") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         ScreenSettings_readFields(screen, this->dynamicColumns, option[1]);
      } else if (String_eq(option[0], "sort_key") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         // This "+1" is for compatibility with the older enum format.
         screen = Settings_defaultScreens(this);
         screen->sortKey = atoi(option[1]) + 1;
      } else if (String_eq(option[0], "tree_sort_key") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         // This "+1" is for compatibility with the older enum format.
         screen = Settings_defaultScreens(this);
         screen->treeSortKey = atoi(option[1]) + 1;
      } else if (String_eq(option[0], "sort_direction") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         screen->direction = atoi(option[1]);
      } else if (String_eq(option[0], "tree_sort_direction") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         screen->treeDirection = atoi(option[1]);
      } else if (String_eq(option[0], "tree_view") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         screen->treeView = atoi(option[1]);
      } else if (String_eq(option[0], "tree_view_always_by_pid") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         screen->treeViewAlwaysByPID = atoi(option[1]);
      } else if (String_eq(option[0], "all_branches_collapsed") && this->config_version <= 2) {
         // old (no screen) naming also supported for backwards compatibility
         screen = Settings_defaultScreens(this);
         screen->allBranchesCollapsed = atoi(option[1]);
      } else if (String_eq(option[0], "hide_kernel_threads")) {
         this->hideKernelThreads = atoi(option[1]);
      } else if (String_eq(option[0], "hide_userland_threads")) {
         this->hideUserlandThreads = atoi(option[1]);
      } else if (String_eq(option[0], "hide_running_in_container")) {
         this->hideRunningInContainer = atoi(option[1]);
      } else if (String_eq(option[0], "shadow_other_users")) {
         this->shadowOtherUsers = atoi(option[1]);
      } else if (String_eq(option[0], "show_thread_names")) {
         this->showThreadNames = atoi(option[1]);
      } else if (String_eq(option[0], "show_program_path")) {
         this->showProgramPath = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_base_name")) {
         this->highlightBaseName = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_deleted_exe")) {
         this->highlightDeletedExe = atoi(option[1]);
      } else if (String_eq(option[0], "shadow_distribution_path_prefix")) {
         this->shadowDistPathPrefix = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_megabytes")) {
         this->highlightMegabytes = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_threads")) {
         this->highlightThreads = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_changes")) {
         this->highlightChanges = atoi(option[1]);
      } else if (String_eq(option[0], "highlight_changes_delay_secs")) {
         this->highlightDelaySecs = CLAMP(atoi(option[1]), 1, 24 * 60 * 60);
      } else if (String_eq(option[0], "find_comm_in_cmdline")) {
         this->findCommInCmdline = atoi(option[1]);
      } else if (String_eq(option[0], "strip_exe_from_cmdline")) {
         this->stripExeFromCmdline = atoi(option[1]);
      } else if (String_eq(option[0], "show_merged_command")) {
         this->showMergedCommand = atoi(option[1]);
      } else if (String_eq(option[0], "header_margin")) {
         this->headerMargin = atoi(option[1]);
      } else if (String_eq(option[0], "screen_tabs")) {
         this->screenTabs = atoi(option[1]);
      } else if (String_eq(option[0], "expand_system_time")) {
         // Compatibility option.
         this->detailedCPUTime = atoi(option[1]);
      } else if (String_eq(option[0], "detailed_cpu_time")) {
         this->detailedCPUTime = atoi(option[1]);
      } else if (String_eq(option[0], "cpu_count_from_one")) {
         this->countCPUsFromOne = atoi(option[1]);
      } else if (String_eq(option[0], "cpu_count_from_zero")) {
         // old (inverted) naming also supported for backwards compatibility
         this->countCPUsFromOne = !atoi(option[1]);
      } else if (String_eq(option[0], "show_cpu_smt_labels")) {
         this->showCPUSMTLabels = atoi(option[1]);
      } else if (String_eq(option[0], "show_cpu_usage")) {
         this->showCPUUsage = atoi(option[1]);
      } else if (String_eq(option[0], "show_cpu_frequency")) {
         this->showCPUFrequency = atoi(option[1]);
      } else if (String_eq(option[0], "show_cached_memory")) {
         this->showCachedMemory = atoi(option[1]);
      #ifdef BUILD_WITH_CPU_TEMP
      } else if (String_eq(option[0], "show_cpu_temperature")) {
         this->showCPUTemperature = atoi(option[1]);
      } else if (String_eq(option[0], "degree_fahrenheit")) {
         this->degreeFahrenheit = atoi(option[1]);
      #endif
      } else if (String_eq(option[0], "update_process_names")) {
         this->updateProcessNames = atoi(option[1]);
      } else if (String_eq(option[0], "account_guest_in_cpu_meter")) {
         this->accountGuestInCPUMeter = atoi(option[1]);
      } else if (String_eq(option[0], "delay")) {
         this->delay = CLAMP(atoi(option[1]), 1, 255);
      } else if (String_eq(option[0], "color_scheme")) {
         this->colorScheme = atoi(option[1]);
         if (this->colorScheme < 0 || this->colorScheme >= LAST_COLORSCHEME) {
            this->colorScheme = 0;
         }
      #ifdef HAVE_GETMOUSE
      } else if (String_eq(option[0], "enable_mouse")) {
         this->enableMouse = atoi(option[1]);
      #endif
      } else if (String_eq(option[0], "header_layout")) {
         this->hLayout = isdigit((unsigned char)option[1][0]) ? ((HeaderLayout) atoi(option[1])) : HeaderLayout_fromName(option[1]);
         if (this->hLayout < 0 || this->hLayout >= LAST_HEADER_LAYOUT)
            this->hLayout = HF_TWO_50_50;
         free(this->hColumns);
         this->hColumns = xCalloc(HeaderLayout_getColumns(this->hLayout), sizeof(MeterColumnSetting));
      } else if (String_eq(option[0], "left_meters")) {
         Settings_readMeters(this, option[1], 0);
         didReadMeters = true;
      } else if (String_eq(option[0], "right_meters")) {
         Settings_readMeters(this, option[1], 1);
         didReadMeters = true;
      } else if (String_eq(option[0], "left_meter_modes")) {
         Settings_readMeterModes(this, option[1], 0);
         didReadMeters = true;
      } else if (String_eq(option[0], "right_meter_modes")) {
         Settings_readMeterModes(this, option[1], 1);
         didReadMeters = true;
      } else if (String_startsWith(option[0], "column_meters_")) {
         Settings_readMeters(this, option[1], atoi(option[0] + strlen("column_meters_")));
         didReadMeters = true;
      } else if (String_startsWith(option[0], "column_meter_modes_")) {
         Settings_readMeterModes(this, option[1], atoi(option[0] + strlen("column_meter_modes_")));
         didReadMeters = true;
      } else if (String_eq(option[0], "hide_function_bar")) {
         this->hideFunctionBar = atoi(option[1]);
      #ifdef HAVE_LIBHWLOC
      } else if (String_eq(option[0], "topology_affinity")) {
         this->topologyAffinity = !!atoi(option[1]);
      #endif
      } else if (strncmp(option[0], "screen:", 7) == 0) {
         screen = Settings_newScreen(this, &(const ScreenDefaults) { .name = option[0] + 7, .columns = option[1] });
      } else if (String_eq(option[0], ".sort_key")) {
         if (screen) {
            int key = toFieldIndex(this->dynamicColumns, option[1]);
            screen->sortKey = key > 0 ? key : PID;
         }
      } else if (String_eq(option[0], ".tree_sort_key")) {
         if (screen) {
            int key = toFieldIndex(this->dynamicColumns, option[1]);
            screen->treeSortKey = key > 0 ? key : PID;
         }
      } else if (String_eq(option[0], ".sort_direction")) {
         if (screen)
            screen->direction = atoi(option[1]);
      } else if (String_eq(option[0], ".tree_sort_direction")) {
         if (screen)
            screen->treeDirection = atoi(option[1]);
      } else if (String_eq(option[0], ".tree_view")) {
         if (screen)
            screen->treeView = atoi(option[1]);
      } else if (String_eq(option[0], ".tree_view_always_by_pid")) {
         if (screen)
            screen->treeViewAlwaysByPID = atoi(option[1]);
      } else if (String_eq(option[0], ".all_branches_collapsed")) {
         if (screen)
            screen->allBranchesCollapsed = atoi(option[1]);
      } else if (String_eq(option[0], ".dynamic")) {
         if (screen) {
            free_and_xStrdup(&screen->dynamic, option[1]);
            Platform_addDynamicScreen(screen);
         }
      }
      String_freeArray(option);
   }
   fclose(fp);
   if (!didReadMeters || !Settings_validateMeters(this))
      Settings_defaultMeters(this, host);
   if (!this->nScreens)
      Settings_defaultScreens(this);
   return didReadAny;
}

typedef ATTR_FORMAT(printf, 2, 3) int (*OutputFunc)(FILE*, const char*,...);

static void writeFields(OutputFunc of, FILE* fp,
                        const ProcessField* fields, Hashtable* columns,
                        bool byName, char separator) {
   const char* sep = "";
   for (unsigned int i = 0; fields[i]; i++) {
      if (fields[i] < LAST_PROCESSFIELD && byName) {
         const char* pName = toFieldName(columns, fields[i], NULL);
         of(fp, "%s%s", sep, pName);
      } else if (fields[i] >= LAST_PROCESSFIELD && byName) {
         bool enabled;
         const char* pName = toFieldName(columns, fields[i], &enabled);
         if (enabled)
            of(fp, "%sDynamic(%s)", sep, pName);
      } else {
         // This "-1" is for compatibility with the older enum format.
         of(fp, "%s%d", sep, (int) fields[i] - 1);
      }
      sep = " ";
   }
   of(fp, "%c", separator);
}

static void writeList(OutputFunc of, FILE* fp,
                      char** list, size_t len, char separator) {
   const char* sep = "";
   for (size_t i = 0; i < len; i++) {
      of(fp, "%s%s", sep, list[i]);
      sep = " ";
   }
   of(fp, "%c", separator);
}

static void writeMeters(const Settings* this, OutputFunc of,
                        FILE* fp, char separator, unsigned int column) {
   if (this->hColumns[column].len) {
      writeList(of, fp, this->hColumns[column].names, this->hColumns[column].len, separator);
   } else {
      of(fp, "!%c", separator);
   }
}

static void writeMeterModes(const Settings* this, OutputFunc of,
                            FILE* fp, char separator, unsigned int column) {
   if (this->hColumns[column].len) {
      const char* sep = "";
      for (size_t i = 0; i < this->hColumns[column].len; i++) {
         of(fp, "%s%u", sep, this->hColumns[column].modes[i]);
         sep = " ";
      }
   } else {
      of(fp, "!");
   }

   of(fp, "%c", separator);
}

ATTR_FORMAT(printf, 2, 3)
static int signal_safe_fprintf(FILE* stream, const char* fmt, ...) {
   char buf[2048];

   va_list vl;
   va_start(vl, fmt);
   int n = vsnprintf(buf, sizeof(buf), fmt, vl);
   va_end(vl);

   if (n <= 0)
      return n;

   ssize_t ret = full_write_str(fileno(stream), buf);
   return (int)MINIMUM(INT_MAX, ret);
}

int Settings_write(const Settings* this, bool onCrash) {
   FILE* fp;
   char separator;
   char* tmpFilename = NULL;
   OutputFunc of;
   if (onCrash) {
      fp = stderr;
      separator = ';';
      of = signal_safe_fprintf;
   } else if (!this->writeConfig) {
      return 0;
   } else {
      /* create tempfile with mode 0600 */
      mode_t cur_umask = umask(S_IXUSR | S_IRWXG | S_IRWXO);
      xAsprintf(&tmpFilename, "%s.tmp.XXXXXX", this->filename);
      int fdtmp = mkstemp(tmpFilename);
      umask(cur_umask);
      if (fdtmp == -1) {
         free(tmpFilename);
         return -errno;
      }
      fp = fdopen(fdtmp, "w");
      if (!fp) {
         close(fdtmp);
         free(tmpFilename);
         return -errno;
      }
      separator = '\n';
      of = fprintf;
   }

   #define printSettingInteger(setting_, value_) \
      of(fp, setting_ "=%d%c", (int) (value_), separator)
   #define printSettingString(setting_, value_) \
      of(fp, setting_ "=%s%c", value_, separator)

   if (!onCrash) {
      of(fp, "# Beware! This file is rewritten by htop when settings are changed in the interface.\n");
      of(fp, "# The parser is also very primitive, and not human-friendly.\n");
   }
   printSettingString("htop_version", VERSION);
   printSettingInteger("config_reader_min_version", CONFIG_READER_MIN_VERSION);
   of(fp, "fields="); writeFields(of, fp, this->screens[0]->fields, this->dynamicColumns, false, separator);
   printSettingInteger("hide_kernel_threads", this->hideKernelThreads);
   printSettingInteger("hide_userland_threads", this->hideUserlandThreads);
   printSettingInteger("hide_running_in_container", this->hideRunningInContainer);
   printSettingInteger("shadow_other_users", this->shadowOtherUsers);
   printSettingInteger("show_thread_names", this->showThreadNames);
   printSettingInteger("show_program_path", this->showProgramPath);
   printSettingInteger("highlight_base_name", this->highlightBaseName);
   printSettingInteger("highlight_deleted_exe", this->highlightDeletedExe);
   printSettingInteger("shadow_distribution_path_prefix", this->shadowDistPathPrefix);
   printSettingInteger("highlight_megabytes", this->highlightMegabytes);
   printSettingInteger("highlight_threads", this->highlightThreads);
   printSettingInteger("highlight_changes", this->highlightChanges);
   printSettingInteger("highlight_changes_delay_secs", this->highlightDelaySecs);
   printSettingInteger("find_comm_in_cmdline", this->findCommInCmdline);
   printSettingInteger("strip_exe_from_cmdline", this->stripExeFromCmdline);
   printSettingInteger("show_merged_command", this->showMergedCommand);
   printSettingInteger("header_margin", this->headerMargin);
   printSettingInteger("screen_tabs", this->screenTabs);
   printSettingInteger("detailed_cpu_time", this->detailedCPUTime);
   printSettingInteger("cpu_count_from_one", this->countCPUsFromOne);
   printSettingInteger("show_cpu_smt_labels", this->showCPUSMTLabels);
   printSettingInteger("show_cpu_usage", this->showCPUUsage);
   printSettingInteger("show_cpu_frequency", this->showCPUFrequency);
   #ifdef BUILD_WITH_CPU_TEMP
   printSettingInteger("show_cpu_temperature", this->showCPUTemperature);
   printSettingInteger("degree_fahrenheit", this->degreeFahrenheit);
   #endif
   printSettingInteger("show_cached_memory", this->showCachedMemory);
   printSettingInteger("update_process_names", this->updateProcessNames);
   printSettingInteger("account_guest_in_cpu_meter", this->accountGuestInCPUMeter);
   printSettingInteger("color_scheme", this->colorScheme);
   #ifdef HAVE_GETMOUSE
   printSettingInteger("enable_mouse", this->enableMouse);
   #endif
   printSettingInteger("delay", (int) this->delay);
   printSettingInteger("hide_function_bar", (int) this->hideFunctionBar);
   #ifdef HAVE_LIBHWLOC
   printSettingInteger("topology_affinity", this->topologyAffinity);
   #endif

   printSettingString("header_layout", HeaderLayout_getName(this->hLayout));
   for (unsigned int i = 0; i < HeaderLayout_getColumns(this->hLayout); i++) {
      of(fp, "column_meters_%u=", i);
      writeMeters(this, of, fp, separator, i);
      of(fp, "column_meter_modes_%u=", i);
      writeMeterModes(this, of, fp, separator, i);
   }

   // Legacy compatibility with older versions of htop
   printSettingInteger("tree_view", this->screens[0]->treeView);
   // This "-1" is for compatibility with the older enum format.
   printSettingInteger("sort_key", this->screens[0]->sortKey - 1);
   printSettingInteger("tree_sort_key", this->screens[0]->treeSortKey - 1);
   printSettingInteger("sort_direction", this->screens[0]->direction);
   printSettingInteger("tree_sort_direction", this->screens[0]->treeDirection);
   printSettingInteger("tree_view_always_by_pid", this->screens[0]->treeViewAlwaysByPID);
   printSettingInteger("all_branches_collapsed", this->screens[0]->allBranchesCollapsed);

   for (unsigned int i = 0; i < this->nScreens; i++) {
      ScreenSettings* ss = this->screens[i];
      const char* sortKey = toFieldName(this->dynamicColumns, ss->sortKey, NULL);
      const char* treeSortKey = toFieldName(this->dynamicColumns, ss->treeSortKey, NULL);

      of(fp, "screen:%s=", ss->heading);
      writeFields(of, fp, ss->fields, this->dynamicColumns, true, separator);
      if (ss->dynamic) {
         printSettingString(".dynamic", ss->dynamic);
         if (ss->sortKey && ss->sortKey != PID)
            of(fp, "%s=Dynamic(%s)%c", ".sort_key", sortKey, separator);
         if (ss->treeSortKey && ss->treeSortKey != PID)
            of(fp, "%s=Dynamic(%s)%c", ".tree_sort_key", treeSortKey, separator);
      } else {
         printSettingString(".sort_key", sortKey);
         printSettingString(".tree_sort_key", treeSortKey);
         printSettingInteger(".tree_view_always_by_pid", ss->treeViewAlwaysByPID);
      }
      printSettingInteger(".tree_view", ss->treeView);
      printSettingInteger(".sort_direction", ss->direction);
      printSettingInteger(".tree_sort_direction", ss->treeDirection);
      printSettingInteger(".all_branches_collapsed", ss->allBranchesCollapsed);
   }

   #undef printSettingString
   #undef printSettingInteger

   if (onCrash)
      return 0;

   int r = 0;

   if (ferror(fp) != 0)
      r = (errno != 0) ? -errno : -EBADF;

   if (fclose(fp) != 0)
      r = r ? r : -errno;

   if (r == 0)
      r = (rename(tmpFilename, this->filename) == -1) ? -errno : 0;

   free(tmpFilename);

   return r;
}

Settings* Settings_new(const Machine* host, Hashtable* dynamicMeters, Hashtable* dynamicColumns, Hashtable* dynamicScreens) {
   Settings* this = xCalloc(1, sizeof(Settings));

   this->writeConfig = true;

   this->dynamicScreens = dynamicScreens;
   this->dynamicColumns = dynamicColumns;
   this->dynamicMeters = dynamicMeters;
   this->hLayout = HF_TWO_50_50;
   this->hColumns = xCalloc(HeaderLayout_getColumns(this->hLayout), sizeof(MeterColumnSetting));

   this->shadowOtherUsers = false;
   this->showThreadNames = false;
   this->hideKernelThreads = true;
   this->hideUserlandThreads = false;
   this->hideRunningInContainer = false;
   this->highlightBaseName = false;
   this->highlightDeletedExe = true;
   this->shadowDistPathPrefix = false;
   this->highlightMegabytes = true;
   this->detailedCPUTime = false;
   this->countCPUsFromOne = false;
   this->showCPUSMTLabels = false;
   this->showCPUUsage = true;
   this->showCPUFrequency = false;
   #ifdef BUILD_WITH_CPU_TEMP
   this->showCPUTemperature = false;
   this->degreeFahrenheit = false;
   #endif
   this->showCachedMemory = true;
   this->updateProcessNames = false;
   this->showProgramPath = true;
   this->highlightThreads = true;
   this->highlightChanges = false;
   this->highlightDelaySecs = DEFAULT_HIGHLIGHT_SECS;
   this->findCommInCmdline = true;
   this->stripExeFromCmdline = true;
   this->showMergedCommand = false;
   this->hideFunctionBar = 0;
   this->headerMargin = true;
   #ifdef HAVE_LIBHWLOC
   this->topologyAffinity = false;
   #endif

   this->screens = xCalloc(Platform_numberOfDefaultScreens, sizeof(ScreenSettings*));
   this->nScreens = 0;

   char* legacyDotfile = NULL;
   const char* rcfile = getenv("HTOPRC");
   if (rcfile) {
      this->initialFilename = xStrdup(rcfile);
   } else {
      const char* home = getenv("HOME");
      if (!home || home[0] != '/') {
         const struct passwd* pw = getpwuid(getuid());
         home = (pw && pw->pw_dir && pw->pw_dir[0] == '/') ? pw->pw_dir : "";
      }
      const char* xdgConfigHome = getenv("XDG_CONFIG_HOME");
      char* configDir = NULL;
      char* htopDir = NULL;
      if (xdgConfigHome && xdgConfigHome[0] == '/') {
         this->initialFilename = String_cat(xdgConfigHome, "/htop/htoprc");
         configDir = xStrdup(xdgConfigHome);
         htopDir = String_cat(xdgConfigHome, "/htop");
      } else {
         this->initialFilename = String_cat(home, CONFIGDIR "/htop/htoprc");
         configDir = String_cat(home, CONFIGDIR);
         htopDir = String_cat(home, CONFIGDIR "/htop");
      }
      (void) mkdir(configDir, 0700);
      (void) mkdir(htopDir, 0700);
      free(htopDir);
      free(configDir);

      legacyDotfile = String_cat(home, "/.htoprc");
   }

   this->filename = xMalloc(PATH_MAX);
   if (!realpath(this->initialFilename, this->filename))
      free_and_xStrdup(&this->filename, this->initialFilename);

   this->colorScheme = 0;
#ifdef HAVE_GETMOUSE
   this->enableMouse = true;
#endif
   this->changed = false;
   this->delay = DEFAULT_DELAY;

   bool ok = Settings_read(this, this->filename, host, /*checkWritability*/true);
   if (!ok && legacyDotfile) {
      ok = Settings_read(this, legacyDotfile, host, this->writeConfig);
      if (ok && this->writeConfig) {
         // Transition to new location and delete old configuration file
         if (Settings_write(this, false) == 0) {
            unlink(legacyDotfile);
         }
      }
   }
   if (!ok) {
      this->screenTabs = true;
      this->changed = true;

      ok = Settings_read(this, SYSCONFDIR "/htoprc", host, /*checkWritability*/false);
   }
   if (!ok) {
      Settings_defaultMeters(this, host);
      Settings_defaultScreens(this);
   }

   this->ssIndex = 0;
   this->ss = this->screens[this->ssIndex];

   this->lastUpdate = 1;

   free(legacyDotfile);

   return this;
}

void ScreenSettings_invertSortOrder(ScreenSettings* this) {
   int* attr = (this->treeView) ? &(this->treeDirection) : &(this->direction);
   *attr = (*attr == 1) ? -1 : 1;
}

void ScreenSettings_setSortKey(ScreenSettings* this, ProcessField sortKey) {
   if (this->treeViewAlwaysByPID || !this->treeView) {
      this->sortKey = sortKey;
      this->direction = (Process_fields[sortKey].defaultSortDesc) ? -1 : 1;
      this->treeView = false;
   } else {
      this->treeSortKey = sortKey;
      this->treeDirection = (Process_fields[sortKey].defaultSortDesc) ? -1 : 1;
   }
}

static bool readonly = false;

void Settings_enableReadonly(void) {
   readonly = true;
}

bool Settings_isReadonly(void) {
   return readonly;
}

void Settings_setHeaderLayout(Settings* this, HeaderLayout hLayout) {
   size_t oldColumns = HeaderLayout_getColumns(this->hLayout);
   size_t newColumns = HeaderLayout_getColumns(hLayout);

   if (newColumns > oldColumns) {
      this->hColumns = xReallocArray(this->hColumns, newColumns, sizeof(MeterColumnSetting));
      memset(this->hColumns + oldColumns, 0, (newColumns - oldColumns) * sizeof(MeterColumnSetting));
   } else if (newColumns < oldColumns) {
      for (size_t i = newColumns; i < oldColumns; i++) {
         if (this->hColumns[i].names) {
            for (size_t j = 0; j < this->hColumns[i].len; j++)
               free(this->hColumns[i].names[j]);
            free(this->hColumns[i].names);
         }
         free(this->hColumns[i].modes);
      }
      this->hColumns = xReallocArray(this->hColumns, newColumns, sizeof(MeterColumnSetting));
   }

   this->hLayout = hLayout;
   this->changed = true;
}
