Imported Upstream version 2.0.0 upstream/2.0.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 25 Oct 2021 01:41:23 +0000 (10:41 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 25 Oct 2021 01:41:23 +0000 (10:41 +0900)
46 files changed:
.gitignore
INSTALL
INSTALL.enduser [new file with mode: 0644]
Makefile [deleted file]
Makefile.am [new file with mode: 0644]
Makefile.inc/VERSION [deleted file]
README
TODO [deleted file]
commandidentifier.c [new file with mode: 0644]
commandidentifier.h [new file with mode: 0644]
configure.ac [new file with mode: 0644]
dir.c [new file with mode: 0644]
dir.h [new file with mode: 0644]
errormsg.c [new file with mode: 0644]
errormsg.h [new file with mode: 0644]
fdupes-help.7 [new file with mode: 0644]
fdupes.1
fdupes.c
fdupes.h [new file with mode: 0644]
filegroup.h [new file with mode: 0644]
flags.c [new file with mode: 0644]
flags.h [new file with mode: 0644]
fmatch.c [new file with mode: 0644]
fmatch.h [new file with mode: 0644]
log.c [new file with mode: 0644]
log.h [new file with mode: 0644]
mbstowcs_escape_invalid.c [new file with mode: 0644]
mbstowcs_escape_invalid.h [new file with mode: 0644]
ncurses-commands.c [new file with mode: 0644]
ncurses-commands.h [new file with mode: 0644]
ncurses-getcommand.c [new file with mode: 0644]
ncurses-getcommand.h [new file with mode: 0644]
ncurses-interface.c [new file with mode: 0644]
ncurses-interface.h [new file with mode: 0644]
ncurses-print.c [new file with mode: 0644]
ncurses-print.h [new file with mode: 0644]
ncurses-prompt.c [new file with mode: 0644]
ncurses-prompt.h [new file with mode: 0644]
ncurses-status.c [new file with mode: 0644]
ncurses-status.h [new file with mode: 0644]
positive_wcwidth.c [new file with mode: 0644]
positive_wcwidth.h [new file with mode: 0644]
sigint.c [new file with mode: 0644]
sigint.h [new file with mode: 0644]
wcs.c [new file with mode: 0644]
wcs.h [new file with mode: 0644]

index a533b2a..9b3cf99 100644 (file)
@@ -1,2 +1,19 @@
 fdupes
 *.o
+/Makefile
+/Makefile.in
+/aclocal.m4
+/autom4te.cache
+/compile
+/config.h
+/config.h.in
+/config.h.in~
+/config.log
+/config.status
+/configure
+/depcomp
+/install-sh
+/missing
+/stamp-h1
+.deps
+.dirstamp
diff --git a/INSTALL b/INSTALL
index c55c06a..976c983 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -1,25 +1,33 @@
 Installing fdupes
 --------------------------------------------------------------------
-To install the program, issue the following commands:
-
-make fdupes
-su root
-make install
-
-This will install the program in /usr/local/bin. You may change this 
-to a different location by editing the Makefile. Please refer to
-the Makefile for an explanation of compile-time options. If you're
-having trouble compiling, please take a look at the Makefile.
-
-UPGRADING NOTE: When upgrading from a version prior to 1.2, it should 
-be noted that the default installation directory for the fdupes man
-page has changed from "/usr/man" to "/usr/local/man". If installing
-to the default location you should delete the old man page before
-proceeding. This file would be named "/usr/man/man1/fdupes.1".
-
-A test directory is included so that you may familiarise yourself
-with the way fdupes operates. You may test the program before
-installing it by issuing a command such as "./fdupes testdir" 
-or "./fdupes -r testdir", just to name a couple of examples. Refer
-to the documentation for information on valid options.
+You're looking at a DEVELOPMENT version of fdupes. If you're not a software
+developer or you just want to install the program, this is probably not the
+version you want. This version of fdupes is currently in development and
+subject to change. The features and functionality present in this version
+may not reflect the features and functionality in the final release.
+
+The easiest way to install fdupes is to use your operating system's package
+manager, if available. If that's not an option or you'd prefer to compile
+the program yourself (which may sometimes be the only way to obtain the
+latest version), you should download a RELEASE version of fdupes and follow
+the installation instructions provided with it.
+
+As of this writing, fdupes releases may be downloaded from:
+
+       https://github.com/adrianlopezroche/fdupes/releases
+
+If you're a programmer and you wish to use or work on this version of fdupes
+you will need to install autoconf and automake on your system and run
+
+       autoreconf --install
+
+to generate the configure script and other files needed for compilation and
+installation. After that you may compile fdupes by running:
+
+       ./configure
+       make
+
+and install it by running:
+
+       sudo make install
 
diff --git a/INSTALL.enduser b/INSTALL.enduser
new file mode 100644 (file)
index 0000000..da819bf
--- /dev/null
@@ -0,0 +1,44 @@
+Installing fdupes
+--------------------------------------------------------------------
+To build and install the program, issue the following commands:
+
+       ./configure
+       make
+       sudo make install
+
+Starting with fdupes 2.0.0, a full-featured installation requires
+the following libraries to be installed on your system:
+
+  - ncursesw (ncurses with support for wide characters)
+  - PCRE2 (Perl Compatible Regular Expressions library)
+
+Source code for these libraries is available at:
+
+  https://www.gnu.org/software/ncurses/
+  https://www.pcre.org/
+
+If these libraries are not available on your system or you want to
+build fdupes without them, you may instead call configure as:
+
+       ./configure --without-ncurses
+
+followed by "make" and "sudo make install" as before.
+
+Please note that compiling fdupes without these libraries will
+disable the new screen-mode interface for choosing which files to
+keep or delete. Without them, fdupes will use an interface like
+the one used in previous versions.
+
+The commands above will build and install fdupes and its
+documentation under /usr/local by default. You may change the
+install location using:
+
+       ./configure --prefix=/your/chosen/path
+
+followed by "make" and "sudo make install" as before.
+
+A test directory is included so that you may familiarise yourself
+with the way fdupes operates. You may test the program before
+installing it by issuing a command such as "./fdupes testdir" 
+or "./fdupes -r testdir", just to name a couple of examples. Refer
+to the documentation for information on valid options.
diff --git a/Makefile b/Makefile
deleted file mode 100644 (file)
index bc5ff54..0000000
--- a/Makefile
+++ /dev/null
@@ -1,112 +0,0 @@
-#
-# fdupes Makefile
-#
-
-#####################################################################
-# Standand User Configuration Section                               #
-#####################################################################
-
-#
-# PREFIX indicates the base directory used as the basis for the 
-# determination of the actual installation directories.
-# Suggested values are "/usr/local", "/usr", "/pkgs/fdupes-$(VERSION)"
-#
-PREFIX = /usr/local
-
-#
-# When compiling for 32-bit systems, FILEOFFSET_64BIT must be enabled
-# for fdupes to handle files greater than (2<<31)-1 bytes.
-#
-FILEOFFSET_64BIT = -D_FILE_OFFSET_BITS=64
-
-#
-# Certain platforms do not support long options (command line options).
-# To disable long options, uncomment the following line.
-#
-#OMIT_GETOPT_LONG = -DOMIT_GETOPT_LONG
-
-#####################################################################
-# Developer Configuration Section                                   #
-#####################################################################
-
-#
-# VERSION determines the program's version number.
-#
-include Makefile.inc/VERSION
-
-#
-# PROGRAM_NAME determines the installation name and manual page name
-#
-PROGRAM_NAME=fdupes
-
-#
-# BIN_DIR indicates directory where program is to be installed. 
-# Suggested value is "$(PREFIX)/bin"
-#
-BIN_DIR = $(PREFIX)/bin
-
-#
-# MAN_DIR indicates directory where the fdupes man page is to be 
-# installed. Suggested value is "$(PREFIX)/man/man1"
-#
-MAN_BASE_DIR = $(PREFIX)/share/man
-MAN_DIR = $(MAN_BASE_DIR)/man1
-MAN_EXT = 1
-
-#
-# Required External Tools
-#
-
-INSTALL = install      # install : UCB/GNU Install compatiable
-#INSTALL = ginstall
-
-RM      = rm -f
-
-MKDIR   = mkdir -p
-#MKDIR   = mkdirhier 
-#MKDIR   = mkinstalldirs
-
-
-#
-# Make Configuration
-#
-CC ?= gcc
-COMPILER_OPTIONS = -Wall -O -g
-
-CFLAGS= $(COMPILER_OPTIONS) -I. -DVERSION=\"$(VERSION)\" $(OMIT_GETOPT_LONG) $(FILEOFFSET_64BIT)
-
-INSTALL_PROGRAM = $(INSTALL) -c -m 0755
-INSTALL_DATA    = $(INSTALL) -c -m 0644
-
-#
-# ADDITIONAL_OBJECTS - some platforms will need additional object files
-# to support features not supplied by their vendor. Eg: GNU getopt()
-#
-#ADDITIONAL_OBJECTS = getopt.o
-
-OBJECT_FILES = fdupes.o md5/md5.o $(ADDITIONAL_OBJECTS)
-
-#####################################################################
-# no need to modify anything beyond this point                      #
-#####################################################################
-
-all: fdupes
-
-fdupes: $(OBJECT_FILES)
-       $(CC) $(CFLAGS) $(LDFLAGS) -o fdupes $(OBJECT_FILES)
-
-installdirs:
-       test -d $(DESTDIR)$(BIN_DIR) || $(MKDIR) $(DESTDIR)$(BIN_DIR)
-       test -d $(DESTDIR)$(MAN_DIR) || $(MKDIR) $(DESTDIR)$(MAN_DIR)
-
-install: fdupes installdirs
-       $(INSTALL_PROGRAM)      fdupes   $(DESTDIR)$(BIN_DIR)/$(PROGRAM_NAME)
-       $(INSTALL_DATA)         fdupes.1 $(DESTDIR)$(MAN_DIR)/$(PROGRAM_NAME).$(MAN_EXT)
-
-clean:
-       $(RM) $(OBJECT_FILES)
-       $(RM) fdupes
-       $(RM) *~ md5/*~
-
-love:
-       @echo You\'re not my type. Go find a human partner.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..ecb5087
--- /dev/null
@@ -0,0 +1,72 @@
+bin_PROGRAMS = fdupes
+
+if NO_NCURSES
+fdupes_SOURCES = fdupes.c\
+ fdupes.h\
+ errormsg.c\
+ errormsg.h\
+ dir.c\
+ dir.h\
+ log.c\
+ log.h\
+ fmatch.c\
+ fmatch.h\
+ sigint.c\
+ sigint.h\
+ flags.c\
+ flags.h\
+ mbstowcs_escape_invalid.c\
+ mbstowcs_escape_invalid.h\
+ positive_wcwidth.c\
+ positive_wcwidth.h\
+ md5/md5.c\
+ md5/md5.h
+dist_man1_MANS = fdupes.1
+
+else
+fdupes_SOURCES = fdupes.c\
+ fdupes.h\
+ filegroup.h\
+ ncurses-commands.c\
+ ncurses-commands.h\
+ ncurses-getcommand.c\
+ ncurses-getcommand.h\
+ ncurses-interface.c\
+ ncurses-interface.h\
+ ncurses-print.c\
+ ncurses-print.h\
+ ncurses-prompt.c\
+ ncurses-prompt.h\
+ ncurses-status.c\
+ ncurses-status.h\
+ commandidentifier.c\
+ commandidentifier.h\
+ errormsg.c\
+ errormsg.h\
+ wcs.c\
+ wcs.h\
+ dir.c\
+ dir.h\
+ log.c\
+ log.h\
+ fmatch.c\
+ fmatch.h\
+ sigint.c\
+ sigint.h\
+ flags.c\
+ flags.h\
+ mbstowcs_escape_invalid.c\
+ mbstowcs_escape_invalid.h\
+ positive_wcwidth.c\
+ positive_wcwidth.h\
+ md5/md5.c\
+ md5/md5.h
+dist_man1_MANS = fdupes.1
+dist_man7_MANS = fdupes-help.7
+
+endif
+
+EXTRA_DIST = testdir CHANGES CONTRIBUTORS
+
+dist-hook:
+       if [ -f $(top_srcdir)/INSTALL.enduser ]; then chmod u+w $(distdir)/INSTALL; \cp -f $(top_srcdir)/INSTALL.enduser $(distdir)/INSTALL; fi
diff --git a/Makefile.inc/VERSION b/Makefile.inc/VERSION
deleted file mode 100644 (file)
index 96e4df5..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#
-# VERSION determines the program's version number.
-#
-
-VERSION = 1.6.1
diff --git a/README b/README
index c748d05..9876dc4 100644 (file)
--- a/README
+++ b/README
@@ -17,11 +17,14 @@ Usage: fdupes [options] DIRECTORY...
  -H --hardlinks        normally, when two or more files point to the same
                        disk area they are treated as non-duplicates; this
                        option will change this behavior
+ -G --minsize=SIZE     consider only files greater than or equal to SIZE
+ -L --maxsize=SIZE     consider only files less than or equal to SIZE
  -n --noempty          exclude zero-length files from consideration
  -A --nohidden         exclude hidden files from consideration
  -f --omitfirst        omit the first file in each set of matches
  -1 --sameline         list each set of matches on a single line
  -S --size             show size of duplicate files
+ -t --time             show modification time of duplicate files
  -m --summarize        summarize dupe information
  -q --quiet            hide progress indicator
  -d --delete           prompt user for files to preserve and delete all
@@ -30,6 +33,8 @@ Usage: fdupes [options] DIRECTORY...
                        with -s or --symlinks, or when specifying a
                        particular directory more than once; refer to the
                        fdupes documentation for additional information
+ -P --plain            with --delete, use line-based prompt (as with older
+                       versions of fdupes) instead of screen-mode interface
  -N --noprompt         together with --delete, preserve the first file in
                        each set of duplicates and delete the rest without
                        prompting the user
@@ -37,9 +42,11 @@ Usage: fdupes [options] DIRECTORY...
                        grouping into sets; implies --noprompt
  -p --permissions      don't consider files with different owner/group or
                        permission bits as duplicates
- -o --order=BY         select sort order for output, linking and deleting; by
-                       mtime (BY='time'; default) or filename (BY='name')
+ -o --order=BY         select sort order for output and deleting; by file
+                       modification time (BY='time'; default), status
+                       change time (BY='ctime'), or filename (BY='name')
  -i --reverse          reverse order while sorting
+ -l --log=LOGFILE      log file deletion choices to LOGFILE
  -v --version          display fdupes version
  -h --help             display this help message
 
@@ -70,7 +77,7 @@ email: adrian2@caribe.net
 
 Legal Information
 --------------------------------------------------------------------
-FDUPES Copyright (c) 1999 Adrian Lopez
+FDUPES Copyright (c) 1999-2019 Adrian Lopez
 
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation files
diff --git a/TODO b/TODO
deleted file mode 100644 (file)
index f7f77c4..0000000
--- a/TODO
+++ /dev/null
@@ -1,72 +0,0 @@
-- A bug with -S shows wrong results.
-
-- A bug causes the following behavior:
-
-  $ fdupes --symlinks testdir
-  testdir/with spaces b
-  testdir/with spaces a
-
-  testdir/zero_b
-  testdir/zero_a
-
-  testdir/symlink_two
-  testdir/twice_one
-
-  $ cp testdir/two testdir/two_again
-  $ fdupes --symlinks testdir
-  testdir/two_again
-  testdir/two
-  testdir/twice_one
-  testdir/symlink_two
-
-  testdir/with spaces b
-  testdir/with spaces a
-
-  testdir/zero_b
-  testdir/zero_a
-
-  ** This is not the desired behavior. Likewise:
-
-  $ fdupes testdir
-  testdir/with spaces b
-  testdir/with spaces a
-
-  testdir/zero_b
-  testdir/zero_a
-
-  testdir/twice_one
-  testdir/two
-
-  $ fdupes --symlinks testdir
-  testdir/with spaces b
-  testdir/with spaces a
-
-  testdir/zero_b
-  testdir/zero_a
-
-  testdir/symlink_two
-  testdir/twice_one
-
-- Don't assume that stat always works.
-
-- Add partial checksumming where instead of MD5ing whole
-  files we MD5 and compare every so many bytes, caching
-  these partial results for subsequent comparisons.
-
-- Option -R should not have to be separated from the rest,
-  such that "fdupes -dR testdir", "fdupes -d -R testdir",
-  "fdupes -Rd testdir", etc., all yield the same results.
-
-- Add option to highlight or identify symlinked files (suggest
-  using --classify to identify symlinks with @ suffix... when
-  specified, files containing @ are listed using \@).
-
-- Consider autodeletion option without user intervention.
-
-- Consider option to match only to files in specific directory.
-
-- Do a little commenting, to avoid rolling eyes and/or snickering.
-
-- Fix problem where MD5 collisions will result in one of the
-  files not being registered (causing it to be ignored).
-
diff --git a/commandidentifier.c b/commandidentifier.c
new file mode 100644 (file)
index 0000000..c3ca683
--- /dev/null
@@ -0,0 +1,202 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <stdlib.h>
+#include "commandidentifier.h"
+
+/* insert new node into command identifier tree */
+int insert_command_identifier_command(struct command_identifier_node *tree, struct command_map *command, size_t ch)
+{
+  struct command_identifier_node *child;
+  struct command_identifier_node **alloc_children;
+  int returned_command;
+  int c;
+
+  /* find node for current character in command name */
+  child = 0;
+  for (c = 0; c < tree->num_children; ++c)
+  {
+    if (tree->children[c]->character == command->command_name[ch])
+    {
+      child = tree->children[c];
+      break;
+    }
+  }
+
+  /* if sought node does not exist, create it */
+  if (child == 0)
+  {
+    child = (struct command_identifier_node*) malloc(sizeof(struct command_identifier_node));
+    if (child == 0)
+      return COMMAND_RECOGNIZER_OUT_OF_MEMORY;
+
+    child->command = COMMAND_UNDEFINED;
+    child->character = command->command_name[ch];
+    child->children = 0;
+    child->num_children = 0;
+
+    alloc_children = realloc(tree->children, sizeof(struct command_identifier_node*) * (tree->num_children + 1));
+    if (alloc_children == 0)
+      return COMMAND_RECOGNIZER_OUT_OF_MEMORY;
+
+    tree->children = alloc_children;
+
+    tree->children[tree->num_children++] = child;
+  }
+
+  /* if last character in command name, make child a leaf node */
+  if (command->command_name[ch] == L'\0')
+  {
+    child->command = command->command;
+    return child->command;
+  }
+  else /* grow the tree */
+  {
+    returned_command = insert_command_identifier_command(child, command, ch + 1);
+
+    /* record whether the tree at this point leads to a single command (abbreviation) or many (ambiguous) */
+    if (tree->command == COMMAND_UNDEFINED)
+      tree->command = returned_command;
+    else
+      tree->command = COMMAND_AMBIGUOUS;
+  }
+
+  return tree->command;
+}
+
+/* compare two command identifier nodes by the characters they match */
+int compare_command_identifier_nodes(const void *a, const void *b)
+{
+  const struct command_identifier_node *aa;
+  const struct command_identifier_node *bb;
+
+  aa = *(struct command_identifier_node**)a;
+  bb = *(struct command_identifier_node**)b;
+
+  if (aa->character > bb->character)
+    return 1;
+  else if (aa->character < bb->character)
+    return -1;
+  else
+    return 0;
+}
+
+/* sort command identifier nodes in alphabetical order */
+void sort_command_identifier_nodes(struct command_identifier_node *root)
+{
+  int c;
+
+  if (root->num_children > 1)
+    qsort(root->children, root->num_children, sizeof(struct command_identifier_node *), compare_command_identifier_nodes);
+
+  for (c = 0; c < root->num_children; ++c)
+    sort_command_identifier_nodes(root->children[c]);
+}
+
+/* build tree to identify command names through state transitions */
+struct command_identifier_node *build_command_identifier_tree(struct command_map *commands)
+{
+  struct command_identifier_node *root;
+  int c;
+
+  root = (struct command_identifier_node*) malloc(sizeof(struct command_identifier_node));
+  if (root == 0)
+    return 0;
+
+  root->command = COMMAND_UNDEFINED;
+  root->character = L'\0';
+  root->children = 0;
+  root->num_children = 0;
+
+  c = 0;
+  while (commands[c].command_name != 0)
+  {
+    insert_command_identifier_command(root, commands + c, 0);
+    ++c;
+  }
+
+  sort_command_identifier_nodes(root);
+
+  return root;
+}
+
+/* free memory used by command identifier tree structure */
+void free_command_identifier_tree(struct command_identifier_node *tree)
+{
+  int c;
+
+  for (c = 0; c < tree->num_children; ++c)
+    free_command_identifier_tree(tree->children[c]);
+
+  free(tree->children);
+  free(tree);
+}
+
+/* find command identifier node matching given character */
+struct command_identifier_node *find_command_identifier_node(struct command_identifier_node *root, wchar_t character)
+{
+  long min;
+  long max;
+  long mid;
+
+  if (root->num_children == 0)
+    return 0;
+
+  min = 0;
+  max = root->num_children - 1;
+
+  do
+  {
+    mid = (min + max) / 2;
+
+    if (character > root->children[mid]->character)
+      min = mid + 1;
+    else if (character < root->children[mid]->character)
+      max = mid - 1;
+    else
+      return root->children[mid];
+  } while (min <= max);
+
+  return 0;
+}
+
+/* determine ID for given command string (possibly abbreviated), if found */
+int identify_command(struct command_identifier_node *tree, wchar_t *command_buffer, size_t ch)
+{
+  struct command_identifier_node *found;
+
+  if (command_buffer[ch] != L' ')
+    found = find_command_identifier_node(tree, command_buffer[ch]);
+  else
+    found = find_command_identifier_node(tree, L'\0');
+
+  if (found)
+  {
+    if (command_buffer[ch] == L'\0' || command_buffer[ch] == L' ')
+      return found->command;
+    else
+      return identify_command(found, command_buffer, ch + 1);
+  }
+  else
+  {
+    if (command_buffer[ch] == L'\0' || command_buffer[ch] == L' ')
+      return tree->command;
+    else
+      return COMMAND_UNDEFINED;
+  }
+}
diff --git a/commandidentifier.h b/commandidentifier.h
new file mode 100644 (file)
index 0000000..4c52b3a
--- /dev/null
@@ -0,0 +1,47 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef COMMANDIDENTIFIER_H
+#define COMMANDIDENTIFIER_H
+
+#include <wchar.h>
+
+#define COMMAND_RECOGNIZER_OUT_OF_MEMORY -2
+#define COMMAND_AMBIGUOUS -1
+#define COMMAND_UNDEFINED 0
+
+/* command name to command ID map structure */
+struct command_map {
+  wchar_t *command_name;
+  int command;
+};
+
+/* command identifier node structure */
+struct command_identifier_node
+{
+  int command;
+  wchar_t character;
+  struct command_identifier_node **children;
+  size_t num_children;
+};
+
+int identify_command(struct command_identifier_node *tree, wchar_t *command_buffer, size_t ch);
+struct command_identifier_node *build_command_identifier_tree(struct command_map *commands);
+void free_command_identifier_tree(struct command_identifier_node *tree);
+
+#endif
\ No newline at end of file
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..a250f83
--- /dev/null
@@ -0,0 +1,36 @@
+AC_INIT([fdupes], [2.0.0])
+
+AM_INIT_AUTOMAKE([foreign subdir-objects])
+
+AC_CONFIG_HEADERS([config.h])
+
+AC_ARG_PROGRAM
+
+AC_ARG_WITH([ncurses], AS_HELP_STRING([--without-ncurses], [Do not use ncurses interface]))
+
+AC_CHECK_HEADERS([getopt.h ncursesw/curses.h])
+
+AS_IF([test x"$with_ncurses" != x"no"],
+       [AC_SEARCH_LIBS([wget_wch], [ncursesw ncurses curses], [], [AC_ERROR([ncurses library not found (or lacks wide character support)])])]
+       [AC_DEFINE([_XOPEN_SOURCE], [], [enable certain functions in wchar.h])]
+       [AC_DEFINE([_XOPEN_SOURCE_EXTENDED], [], [enable certain functions in curses.h])]
+       [AC_DEFINE([_ISOC99_SOURCE], [], [enable strtoll])]
+       [AC_SEARCH_LIBS([pcre2_match_32], [pcre2-32], [], [AC_ERROR([pcre2 library not found])])]
+       [AC_DEFINE([PCRE2_CODE_UNIT_WIDTH], [32], [PCRE2 Code Unit Width])],
+
+       [AC_DEFINE([NO_NCURSES], [], [Do not compile against ncurses])]
+       )
+
+AM_CONDITIONAL([NO_NCURSES], [test x"$with_ncurses" = x"no"])
+
+unescaped_program_transform_name=`echo "${program_transform_name}"|sed -e "s&\\\\$\\\\$&\\\\$&g"`
+transformed_program_name=`echo "${PACKAGE_NAME}"|sed -e "${unescaped_program_transform_name}"|sed -e "s&\\\\\\\\&\\\\\\\\\\\\\\\\&g"`
+transformed_manpage_name=`echo "${PACKAGE_NAME}-help"|sed -e "${unescaped_program_transform_name}"`
+
+AC_DEFINE_UNQUOTED([HELP_COMMAND_STRING], "man 7 ${transformed_manpage_name}", [fdupes help file])
+
+AC_DEFINE([_FILE_OFFSET_BITS], [64], [allow fdupes to handle files greater than (2<<31)-1 bytes])
+
+AC_CONFIG_FILES([Makefile])
+AC_PROG_CC
+AC_OUTPUT
diff --git a/dir.c b/dir.c
new file mode 100644 (file)
index 0000000..5fc7a28
--- /dev/null
+++ b/dir.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include "dir.h"
+
+char *getworkingdirectory()
+{
+  size_t size;
+  char *result;
+  char *new_result;
+  char *cwd;
+  
+  size = 1024;
+
+  result = 0;
+  do
+  {
+    new_result = (char*) realloc(result, size);
+    if (new_result == 0)
+    {
+      if (result != 0)
+        free(result);
+
+      return 0;
+    }
+
+    result = new_result;
+
+    cwd = getcwd(result, size);
+
+    size *= 2;
+  } while (cwd == 0 && errno == ERANGE);
+
+  if (cwd == 0)
+  {
+    free(result);
+    return 0;
+  }
+
+  return result;
+}
diff --git a/dir.h b/dir.h
new file mode 100644 (file)
index 0000000..7069a65
--- /dev/null
+++ b/dir.h
@@ -0,0 +1,24 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef DIR_H
+#define DIR_H
+
+char *getworkingdirectory();
+
+#endif
\ No newline at end of file
diff --git a/errormsg.c b/errormsg.c
new file mode 100644 (file)
index 0000000..f4478fc
--- /dev/null
@@ -0,0 +1,34 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include "config.h"
+#include <stdio.h>
+#include <stdarg.h>
+#include "errormsg.h"
+
+extern char *program_name;
+
+void errormsg(char *message, ...)
+{
+  va_list ap;
+
+  va_start(ap, message);
+
+  fprintf(stderr, "\r%40s\r%s: ", "", program_name);
+  vfprintf(stderr, message, ap);
+}
diff --git a/errormsg.h b/errormsg.h
new file mode 100644 (file)
index 0000000..93ec246
--- /dev/null
@@ -0,0 +1,24 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef ERRORMSG_H
+#define ERRORMSG_H
+
+void errormsg(char *message, ...);
+
+#endif
\ No newline at end of file
diff --git a/fdupes-help.7 b/fdupes-help.7
new file mode 100644 (file)
index 0000000..51a1349
--- /dev/null
@@ -0,0 +1,327 @@
+.TH FDUPES-HELP 7
+
+.SH NAME
+fdupes-help \- fdupes interactive mode reference
+
+.SH "INTRODUCTION"
+.PP
+When run interactively
+.RB ( "" "as " "fdupes --delete" ),
+fdupes
+will show a list of duplicates and prompt the user for further action.
+.PP
+The user can tell fdupes which files to keep or delete by tagging them accordingly. Once tagged, the user can instruct fdupes to delete any files that have been tagged for deletion. This can be done incrementally, if desired, successively tagging and deleting a limited number of files at a time until no more duplicates remain to be processed.
+
+There are several ways to tag files in fdupes: individually using the
+.IR cursor ,
+by providing a list of files to keep, or by selecting files that match particular search criteria and tagging those as desired. Each of these approaches is discussed in detail in the sections below.
+
+.SH "SCROLLING THE LIST"
+.PP
+The list of duplicates can be scrolled as follows:
+.B 
+.IP "PAGE DOWN"
+Scroll down to the next page.
+
+.B 
+.IP "PAGE UP"
+Scroll up to preceding page.
+
+.B 
+.IP "SHIFT + DOWN"
+Scroll down by one line. Not supported on some terminals.
+
+.B
+.IP "SHIFT + UP"
+Scroll up by one line. Not supported on some terminals.
+
+.SH "MOVING THE CURSOR"
+.PP
+The cursor tells fdupes which file and/or set of duplicates to act on, as described in the next section. The cursor's position can be changed as follows:
+
+.B 
+.IP "DOWN"
+Advance cursor to the next file on the list.
+
+.B 
+.IP "UP"
+Move cursor back to the previous file.
+
+.B 
+.IP "TAB"
+Advance cursor to the next set of duplicates.
+
+.B 
+.IP "BACKSPACE"
+Move cursor back to the previous set.
+
+.B
+.IP "F3"
+Advance cursor to the next
+.I selected
+set, if any.
+
+.B
+.IP "F2"
+Move cursor back to the previous
+.I selected
+set, if any.
+
+.PP
+It is also possible to jump directly to a particular set:
+
+.B
+.IP "'goto <index>'"
+Move cursor to the top of the set indicated by
+.IR index .
+
+.SH "TAGGING FILES USING THE CURSOR"
+.PP
+Individual files can be tagged using the keys below. These keys all act on the current file, as identified by the cursor.
+
+.B 
+.IP "SHIFT + RIGHT"
+Tag current file for keeping.
+
+.B 
+.IP "SHIFT + LEFT"
+Tag current file for deletion.
+
+.B
+.IP "'?'"
+Remove tag from current file.
+
+.PP
+Entire sets of files can be tagged by providing a list of indices in a comma-separated list. Files in the current set whose indices appear on the list will be tagged for keeping, while any other files in that set will be tagged for deletion. As with individual files, the current set is identified by the cursor.
+
+.PP
+As an example, given the following list of duplicates:
+.PP
+.RS
+.B
+Set 1 of 5:
+
+  1 [ ] path/to/file_a
+  2 [ ] path/to/file_b
+  3 [ ] path/to/file_c
+.RE
+
+.PP
+Typing
+.B
+\|'1, 3\|'
+at the prompt and pressing ENTER will tell fdupes to tag
+.I
+file_a
+and
+.I
+file_c
+for keeping, and
+.I
+file_b
+for deletion. The special command
+.B
+\|'all\|'
+will tag all files for keeping.
+
+.PP
+There is one more command to deal with files in the current set:
+.B
+.IP "'rg'"
+Remove tags from all files in current set.
+
+.SH "FILE SELECTION COMMANDS"
+.PP
+Another way to tag files is to first select them according to particular search criteria and then tell fdupes what to do with them. The following commands can be used to select files for tagging:
+
+.B
+.IP "'sel <text>'"
+Select any files whose paths contain the given text.
+
+.B
+.IP "'selb <text>'"
+Select any files whose paths begin with the given text.
+
+.B
+.IP "'sele <text>'"
+Select any files whose paths end with the given text.
+
+.B
+.IP "'selm <text>'"
+Select any file whose path matches the given text exactly.
+
+.B
+.IP "'selr <expression>'"
+Select any files whose paths match the given
+.I
+regular expression
+(see below).
+
+.B
+.IP "'dsel <text>'"
+Deselect any files whose paths contain the given text.
+
+.B
+.IP "'dselb <text>'"
+Deselect any files whose paths begin with the given text.
+
+.B
+.IP "'dsele <text>'"
+Deselect any files whose paths end with the given text.
+
+.B
+.IP "'dselm <text>'"
+Deselect any file whose path matches the given text exactly.
+
+.B
+.IP "'dselr <expression>'"
+Deselect any files whose paths match the given
+.I
+regular expression
+(see below).
+
+.B
+.IP "'cs'"
+Clear all selections.
+
+.B
+.IP "'igs'"
+Invert selections within selected sets. For example, if files 1 and 4 in a set of 5 are selected,
+.B
+igs
+will deselect files 1 and 4, and select files 2, 3, and 5. Immediately repeating the same command will deselect files 2, 3, and 5, and select files 1 and 4, restoring selections to their previous state.
+
+.SH "TAGGING SELECTED FILES"
+.PP
+Once some files have been selected using the commands described above, the following commands can be used to tag selected files as desired:
+.B
+.IP "'ks'"
+Tag selected files for keeping.
+
+.B
+.IP "'ds'"
+Tag selected files for deletion.
+
+.B
+.IP "'rs'"
+Remove all tags from selected files.
+
+.SH "DELETING DUPLICATES"
+Once they've been tagged for deletion, files can be deleted by pressing
+.B
+DELETE.
+Fdupes will delete any files that are tagged for deletion and delist any sets whose remaining files have been tagged for keeping. For safety, fdupes will refuse to act on sets for which all files have been tagged for deletion. To handle these cases, tag at least one file for keeping and run the delete command again.
+
+.SH "OTHER COMMANDS"
+.B
+.IP "'exit', 'quit'"
+Exit the program.
+
+.B
+.IP "'help'"
+Display this help text.
+
+.SH "REGULAR EXPRESSIONS"
+.PP
+A regular expression is a sequence of characters defining a search pattern against which other character sequences can be compared. Strings of characters that follow the pattern defined by an expression are said to
+.I
+match
+the expression, whereas strings that break the pattern do not.
+.PP
+The syntax for regular expressions used by fdupes is known as the
+.B
+Perl Compatible Regular Expression
+syntax. A detailed description of regular expression syntax is beyond the scope of this document. For detailed information the user is encouraged to consult the
+.I
+PCRE2
+documentation:
+.PP
+.RS
+https://www.pcre.org/current/doc/html/pcre2syntax.html
+.RE
+
+.PP
+Briefly, here are some examples of regular expressions:
+
+.B
+.IP "abc123"
+Will match any string containing the sequence
+.IR abc123 ,
+such as
+.IR abc123 ,
+.IR abc123x ,
+.IR xabc123 ,
+and
+.IR xabc123x .
+
+.B
+.IP "^abc123"
+Will match any string beginning with
+.IR abc123 ,
+such as 
+.IR abc123 " and " abc123x ,
+but not
+.IR xabc123 " or " xabc123x .
+The character '^' has special meaning, telling the program to match only those strings that begin with the pattern that follows.
+
+.B
+.IP "abc123$"
+Will match any string that ends with
+.IR abc123 ,
+such as
+.IR abc123 " and " xabc123 ,
+but not
+.IR abc123x " or " xabc123x .
+The character '$' has special meaning, telling the program to match only those strings that end with the preceding pattern.
+
+.B
+.IP "^abc123$"
+Will match the string
+.I abc123
+and no other.
+
+.B
+.IP "ab.123"
+Will match any string containing
+.I abc123
+as in the first example, but it will also match strings containing
+.IR abz123 , 
+.IR ab0123 , 
+.IR ab_123 ,
+etc. The character '.' has special meaning, acting as a placeholder that will match any character in that position.
+
+.B
+.IP "^a.*3$"
+Will match any string beginning with the letter a and ending with the number 3, such as
+.IR abc123 ,
+.IR a3 ,
+and
+.IR a0b1c2d3 .
+Here the character '*' tells the program to accept any number of appearances (including none) for the preceding item (here, any character matching the placeholder character '.'). The characters '^' and '$' have the same meaning as in previous examples.
+
+.B
+.IP "abc\ed+"
+Will match any string containing the characters
+.B abc
+followed immediately by one or more decimal digits, such as
+.IR abc123 " and " abc3210 ,
+but not
+.IR abcd123
+or
+.I "abc 123"
+(note the space). Here \ed is a placeholder for any decimal digit, while the character '+' tells the program to match one or more appearances of the preceding character or placeholder (here, \ed).
+
+.B
+.IP "\ew+\ed+"
+Will match any string containing one or more "word" characters followed immediately by one or more decimal digits, such as
+.IR abc123 " and " abcd3210 ,
+but not
+.IR "abc 123"
+(note the space). Here \ew is a placeholder for a "word" character, and \ed and '+' have the same meaning as in the preceding example.
+
+.PP
+This is just scratching the surface of what can be done with regular expressions. Consult the PCRE2 documentation for a complete reference.
+
+.SH "SEE ALSO"
+The fdupes man page,
+.BR fdupes (1).
index 5ddad87..70c3c3e 100644 (file)
--- a/fdupes.1
+++ b/fdupes.1
@@ -33,23 +33,32 @@ follow symlinked directories
 normally, when two or more files point to the same disk area they are
 treated as non-duplicates; this option will change this behavior
 .TP
+.B -G --minsize\fR=\fISIZE\fR
+consider only files greater than or equal to SIZE
+.TP
+.B -L --maxsize=\fR=\fISIZE\fR
+consider only files less than or equal to SIZE
+.TP
 .B -n --noempty
 exclude zero-length files from consideration
 .TP
-.B -f --omitfirst
-omit the first file in each set of matches
-.TP
 .B -A --nohidden
 exclude hidden files from consideration
 .TP
+.B -f --omitfirst
+omit the first file in each set of matches
+.TP
 .B -1 --sameline
 list each set of matches on a single line
 .TP
 .B -S --size
 show size of duplicate files
 .TP
+.B  -t --time
+show modification time of duplicate files
+.TP
 .B -m --summarize
-summarize duplicate files information
+summarize duplicate file information
 .TP
 .B -q --quiet
 hide progress indicator
@@ -59,6 +68,10 @@ prompt user for files to preserve, deleting all others (see
 .B CAVEATS
 below)
 .TP
+.B -P --plain
+with --delete, use line-based prompt (as with older versions of
+fdupes) instead of screen-mode interface
+.TP
 .B -N --noprompt
 when used together with \-\-delete, preserve the first file in each
 set of duplicates and delete the others without prompting the user 
@@ -72,11 +85,14 @@ don't consider files with different owner/group or permission bits as duplicates
 .TP
 .B -o --order\fR=\fIWORD\fR
 order files according to WORD:
-time - sort by mtime, name - sort by filename
+time - sort by modification time, ctime - sort by status change time, name - sort by filename
 .TP
 .B -i --reverse
 reverse order while sorting
 .TP
+.B -l --log\fR=\fILOGFILE\fR
+log file deletion choices to LOGFILE
+.TP
 .B -v --version
 display fdupes version
 .TP
index ef64c45..29b2921 100644 (file)
--- a/fdupes.c
+++ b/fdupes.c
@@ -1,4 +1,4 @@
-/* FDUPES Copyright (c) 1999-2002 Adrian Lopez
+/* FDUPES Copyright (c) 1999-2018 Adrian Lopez
 
    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation files
    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
 
+#include "config.h"
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
+#include <strings.h>
 #include <sys/stat.h>
 #include <dirent.h>
 #include <unistd.h>
 #include <stdlib.h>
-#ifndef OMIT_GETOPT_LONG
+#include <time.h>
+#ifdef HAVE_GETOPT_H
 #include <getopt.h>
 #endif
-#include <string.h>
 #include <errno.h>
 #include <libgen.h>
+#include <locale.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+#include "fdupes.h"
+#include "errormsg.h"
+#include "ncurses-interface.h"
+#include "log.h"
+#include "sigint.h"
+#include "flags.h"
 
-#include "md5/md5.h"
-
-#define ISFLAG(a,b) ((a & b) == b)
-#define SETFLAG(a,b) (a |= b)
-
-#define F_RECURSE           0x0001
-#define F_HIDEPROGRESS      0x0002
-#define F_DSAMELINE         0x0004
-#define F_FOLLOWLINKS       0x0008
-#define F_DELETEFILES       0x0010
-#define F_EXCLUDEEMPTY      0x0020
-#define F_CONSIDERHARDLINKS 0x0040
-#define F_SHOWSIZE          0x0080
-#define F_OMITFIRST         0x0100
-#define F_RECURSEAFTER      0x0200
-#define F_NOPROMPT          0x0400
-#define F_SUMMARIZEMATCHES  0x0800
-#define F_EXCLUDEHIDDEN     0x1000
-#define F_PERMISSIONS       0x2000
-#define F_REVERSE           0x4000
-#define F_IMMEDIATE         0x8000
+long long minsize = -1;
+long long maxsize = -1;
 
 typedef enum {
-  ORDER_TIME = 0,
+  ORDER_MTIME = 0,
+  ORDER_CTIME,
   ORDER_NAME
 } ordertype_t;
 
 char *program_name;
 
-unsigned long flags = 0;
+ordertype_t ordertype = ORDER_MTIME;
 
 #define CHUNK_SIZE 8192
 
@@ -90,35 +86,12 @@ typedef struct _signatures
 
 */
 
-typedef struct _file {
-  char *d_name;
-  off_t size;
-  md5_byte_t *crcpartial;
-  md5_byte_t *crcsignature;
-  dev_t device;
-  ino_t inode;
-  time_t mtime;
-  int hasdupes; /* true only if file is first on duplicate chain */
-  struct _file *duplicates;
-  struct _file *next;
-} file_t;
-
 typedef struct _filetree {
   file_t *file; 
   struct _filetree *left;
   struct _filetree *right;
 } filetree_t;
 
-void errormsg(char *message, ...)
-{
-  va_list ap;
-
-  va_start(ap, message);
-
-  fprintf(stderr, "\r%40s\r%s: ", "", program_name);
-  vfprintf(stderr, message, ap);
-}
-
 void escapefilename(char *escape_list, char **filename_ptr)
 {
   int x;
@@ -183,6 +156,22 @@ time_t getmtime(char *filename) {
   return s.st_mtime;
 }
 
+time_t getctime(char *filename) {
+  struct stat s;
+
+  if (stat(filename, &s) != 0) return 0;
+
+  return s.st_ctime;
+}
+
+char *fmtmtime(char *filename) {
+  static char buf[64];
+  time_t t = getmtime(filename);
+
+  strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", localtime(&t));
+  return buf;
+}
+
 char **cloneargs(int argc, char **argv)
 {
   int x;
@@ -239,7 +228,25 @@ int nonoptafter(char *option, int argc, char **oldargv,
   return x;
 }
 
-int grokdir(char *dir, file_t **filelistp)
+void getfilestats(file_t *file)
+{
+  file->size = filesize(file->d_name);
+  file->inode = getinode(file->d_name);
+  file->device = getdevice(file->d_name);
+
+  switch (ordertype)
+  {
+    case ORDER_CTIME:
+      file->sorttime = getctime(file->d_name);
+      break;
+    case ORDER_MTIME:
+    default:
+      file->sorttime = getmtime(file->d_name);
+      break;
+  }
+}
+
+int grokdir(char *dir, file_t **filelistp, struct stat *logfile_status)
 {
   DIR *cd;
   file_t *newfile;
@@ -251,6 +258,7 @@ int grokdir(char *dir, file_t **filelistp)
   static int progress = 0;
   static char indicator[] = "-\\|/";
   char *fullname, *name;
+  off_t size;
 
   cd = opendir(dir);
 
@@ -298,6 +306,13 @@ int grokdir(char *dir, file_t **filelistp)
       
       if (ISFLAG(flags, F_EXCLUDEHIDDEN)) {
        fullname = strdup(newfile->d_name);
+       if (fullname == 0)
+       {
+         errormsg("out of memory!\n");
+         free(newfile);
+         closedir(cd);
+         exit(1);
+       }
        name = basename(fullname);
        if (name[0] == '.' && strcmp(name, ".") && strcmp(name, "..") ) {
          free(newfile->d_name);
@@ -307,16 +322,24 @@ int grokdir(char *dir, file_t **filelistp)
        free(fullname);
       }
 
-      if (filesize(newfile->d_name) == 0 && ISFLAG(flags, F_EXCLUDEEMPTY)) {
-       free(newfile->d_name);
-       free(newfile);
-       continue;
+      if (stat(newfile->d_name, &info) == -1) {
+        free(newfile->d_name);
+        free(newfile);
+        continue;
+      }
+      
+      size = filesize(newfile->d_name);
+      if (!S_ISDIR(info.st_mode) && (((size == 0 && ISFLAG(flags, F_EXCLUDEEMPTY)) || size < minsize || (size > maxsize && maxsize != -1)))) {
+        free(newfile->d_name);
+        free(newfile);
+        continue;
       }
 
-      if (stat(newfile->d_name, &info) == -1) {
-       free(newfile->d_name);
-       free(newfile);
-       continue;
+      if (info.st_dev == logfile_status->st_dev && info.st_ino == logfile_status->st_ino)
+      {
+        free(newfile->d_name);
+        free(newfile);
+        continue;
       }
 
       if (lstat(newfile->d_name, &linfo) == -1) {
@@ -327,11 +350,12 @@ int grokdir(char *dir, file_t **filelistp)
 
       if (S_ISDIR(info.st_mode)) {
        if (ISFLAG(flags, F_RECURSE) && (ISFLAG(flags, F_FOLLOWLINKS) || !S_ISLNK(linfo.st_mode)))
-         filecount += grokdir(newfile->d_name, filelistp);
+         filecount += grokdir(newfile->d_name, filelistp, logfile_status);
        free(newfile->d_name);
        free(newfile);
       } else {
        if (S_ISREG(linfo.st_mode) || (S_ISLNK(linfo.st_mode) && ISFLAG(flags, F_FOLLOWLINKS))) {
+         getfilestats(newfile);
          *filelistp = newfile;
          filecount++;
        } else {
@@ -430,18 +454,8 @@ void purgetree(filetree_t *checktree)
   free(checktree);
 }
 
-void getfilestats(file_t *file)
-{
-  file->size = filesize(file->d_name);
-  file->inode = getinode(file->d_name);
-  file->device = getdevice(file->d_name);
-  file->mtime = getmtime(file->d_name);
-}
-
 int registerfile(filetree_t **branch, file_t *file)
 {
-  getfilestats(file);
-
   *branch = (filetree_t*) malloc(sizeof(filetree_t));
   if (*branch == NULL) {
     errormsg("out of memory!\n");
@@ -470,14 +484,9 @@ int same_permissions(char* name1, char* name2)
 int is_hardlink(filetree_t *checktree, file_t *file)
 {
   file_t *dupe;
-  ino_t inode;
-  dev_t device;
 
-  inode = getinode(file->d_name);
-  device = getdevice(file->d_name);
-
-  if ((inode == checktree->file->inode) && 
-      (device == checktree->file->device))
+  if ((file->inode == checktree->file->inode) &&
+      (file->device == checktree->file->device))
         return 1;
 
   if (checktree->file->hasdupes)
@@ -485,8 +494,8 @@ int is_hardlink(filetree_t *checktree, file_t *file)
     dupe = checktree->file->duplicates;
 
     do {
-      if ((inode == dupe->inode) &&
-          (device == dupe->device))
+      if ((file->inode == dupe->inode) &&
+          (file->device == dupe->device))
             return 1;
 
       dupe = dupe->duplicates;
@@ -496,20 +505,124 @@ int is_hardlink(filetree_t *checktree, file_t *file)
   return 0;
 }
 
+/* check whether two paths represent the same file (deleting one would delete the other) */
+int is_same_file(file_t *file_a, file_t *file_b)
+{
+  char *filename_a;
+  char *filename_b;
+  char *dirname_a;
+  char *dirname_b;
+  char *basename_a;
+  char *basename_b;
+  struct stat dirstat_a;
+  struct stat dirstat_b;
+
+  /* if files on different devices and/or different inodes, they are not the same file */
+  if (file_a->device != file_b->device || file_a->inode != file_b->inode)
+    return 0;
+
+  /* copy filenames (basename and dirname may modify these) */
+  filename_a = strdup(file_a->d_name);
+  if (filename_a == 0)
+    return -1;
+
+  filename_b = strdup(file_b->d_name);
+  if (filename_b == 0)
+    return -1;
+
+  /* get file basenames */
+  basename_a = basename(filename_a);
+  memmove(filename_a, basename_a, strlen(basename_a) + 1);
+
+  basename_b = basename(filename_b);
+  memmove(filename_b, basename_b, strlen(basename_b) + 1);
+
+  /* if files have different names, they are not the same file */
+  if (strcmp(filename_a, filename_b) != 0)
+  {
+    free(filename_b);
+    free(filename_a);
+    return 0;
+  }
+
+  /* restore paths */
+  strcpy(filename_a, file_a->d_name);
+  strcpy(filename_b, file_b->d_name);
+
+  /* get directory names */
+  dirname_a = dirname(filename_a);
+  if (stat(dirname_a, &dirstat_a) != 0)
+  {
+    free(filename_b);
+    free(filename_a);
+    return -1;
+  }
+
+  dirname_b = dirname(filename_b);
+  if (stat(dirname_b, &dirstat_b) != 0)
+  {
+    free(filename_b);
+    free(filename_a);
+    return -1;
+  }
+
+  free(filename_b);
+  free(filename_a);
+
+  /* if directories on which files reside are different, they are not the same file */
+  if (dirstat_a.st_dev != dirstat_b.st_dev || dirstat_a.st_ino != dirstat_b.st_ino)
+    return 0;
+
+  /* same device, inode, filename, and directory; therefore, same file */
+  return 1;
+}
+
+/* check whether given tree node already contains a copy of given file */
+int has_same_file(filetree_t *checktree, file_t *file)
+{
+  file_t *dupe;
+
+  if (is_same_file(checktree->file, file))
+    return 1;
+
+  if (checktree->file->hasdupes)
+  {
+    dupe = checktree->file->duplicates;
+
+    do {
+      if (is_same_file(dupe, file))
+        return 1;
+
+      dupe = dupe->duplicates;
+    } while (dupe != NULL);
+  }
+
+  return 0;
+}
+
 file_t **checkmatch(filetree_t **root, filetree_t *checktree, file_t *file)
 {
   int cmpresult;
   md5_byte_t *crcsignature;
   off_t fsize;
 
-  /* If device and inode fields are equal one of the files is a 
-     hard link to the other or the files have been listed twice 
-     unintentionally. We don't want to flag these files as
-     duplicates unless the user specifies otherwise.
-  */    
-
-  if (!ISFLAG(flags, F_CONSIDERHARDLINKS) && is_hardlink(checktree, file))
-    return NULL;
+  if (ISFLAG(flags, F_CONSIDERHARDLINKS))
+  {
+    /* If node already contains file, we don't want to add it again.
+    */
+    if (has_same_file(checktree, file))
+      return NULL;
+  }
+  else
+  {
+    /* If device and inode fields are equal one of the files is a
+       hard link to the other or the files have been listed twice
+       unintentionally. We don't want to flag these files as
+       duplicates unless the user specifies otherwise.
+    */
+    if (is_hardlink(checktree, file))
+      return NULL;
+  }
 
   fsize = filesize(file->d_name);
   
@@ -605,7 +718,6 @@ file_t **checkmatch(filetree_t **root, filetree_t *checktree, file_t *file)
     }
   } else 
   {
-    getfilestats(file);
     return &checktree->file;
   }
 }
@@ -682,11 +794,15 @@ void printmatches(file_t *files)
       if (!ISFLAG(flags, F_OMITFIRST)) {
        if (ISFLAG(flags, F_SHOWSIZE)) printf("%lld byte%seach:\n", (long long int)files->size,
         (files->size != 1) ? "s " : " ");
+        if (ISFLAG(flags, F_SHOWTIME))
+          printf("%s ", fmtmtime(files->d_name));
        if (ISFLAG(flags, F_DSAMELINE)) escapefilename("\\ ", &files->d_name);
        printf("%s%c", files->d_name, ISFLAG(flags, F_DSAMELINE)?' ':'\n');
       }
       tmpfile = files->duplicates;
       while (tmpfile != NULL) {
+        if (ISFLAG(flags, F_SHOWTIME))
+          printf("%s ", fmtmtime(tmpfile->d_name));
        if (ISFLAG(flags, F_DSAMELINE)) escapefilename("\\ ", &tmpfile->d_name);
        printf("%s%c", tmpfile->d_name, ISFLAG(flags, F_DSAMELINE)?' ':'\n');
        tmpfile = tmpfile->duplicates;
@@ -756,7 +872,7 @@ int relink(char *oldfile, char *newfile)
   return 1;
 }
 
-void deletefiles(file_t *files, int prompt, FILE *tty)
+void deletefiles(file_t *files, int prompt, FILE *tty, char *logfile)
 {
   int counter;
   int groups = 0;
@@ -773,6 +889,8 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
   int max = 0;
   int x;
   int i;
+  struct log_info *loginfo;
+  int log_error;
 
   curfile = files;
   
@@ -804,19 +922,37 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
     exit(1);
   }
 
+  loginfo = 0;
+  if (logfile != 0)
+    loginfo = log_open(logfile, &log_error);
+
+  register_sigint_handler();
+
   while (files) {
     if (files->hasdupes) {
       curgroup++;
       counter = 1;
       dupelist[counter] = files;
 
-      if (prompt) printf("[%d] %s\n", counter, files->d_name);
+      if (prompt) 
+      {
+        if (ISFLAG(flags, F_SHOWTIME))
+          printf("[%d] [%s] %s\n", counter, fmtmtime(files->d_name), files->d_name);
+        else
+          printf("[%d] %s\n", counter, files->d_name);
+      }
 
       tmpfile = files->duplicates;
 
       while (tmpfile) {
        dupelist[++counter] = tmpfile;
-       if (prompt) printf("[%d] %s\n", counter, tmpfile->d_name);
+        if (prompt)
+        {
+          if (ISFLAG(flags, F_SHOWTIME))
+            printf("[%d] [%s] %s\n", counter, fmtmtime(tmpfile->d_name), tmpfile->d_name);
+          else
+            printf("[%d] %s\n", counter, tmpfile->d_name);
+        }
        tmpfile = tmpfile->duplicates;
       }
 
@@ -831,7 +967,7 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
       else /* prompt for files to preserve */
 
       do {
-       printf("Set %d of %d, preserve files [1 - %d, all]", 
+       printf("Set %d of %d, preserve files [1 - %d, all, quit]",
           curgroup, groups, counter);
        if (ISFLAG(flags, F_SHOWSIZE)) printf(" (%lld byte%seach)", (long long int)files->size,
          (files->size != 1) ? "s " : " ");
@@ -839,7 +975,24 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
        fflush(stdout);
 
        if (!fgets(preservestr, INPUT_SIZE, tty))
+       {
          preservestr[0] = '\n'; /* treat fgets() failure as if nothing was entered */
+         preservestr[1] = '\0';
+
+         if (got_sigint)
+         {
+           if (loginfo)
+             log_close(loginfo);
+
+           free(dupelist);
+           free(preserve);
+           free(preservestr);
+
+           printf("\n");
+
+           exit(0);
+         }
+       }
 
        i = strlen(preservestr) - 1;
 
@@ -855,17 +1008,32 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
          if (!fgets(preservestr + i + 1, INPUT_SIZE, tty))
          {
            preservestr[0] = '\n'; /* treat fgets() failure as if nothing was entered */
+           preservestr[1] = '\0';
            break;
          }
          i = strlen(preservestr)-1;
        }
 
+       if (strcmp(preservestr, "q\n") == 0 || strcmp(preservestr, "quit\n") == 0)
+       {
+         if (loginfo)
+           log_close(loginfo);
+
+         free(dupelist);
+         free(preserve);
+         free(preservestr);
+
+         printf("\n");
+
+         exit(0);
+       }
+
        for (x = 1; x <= counter; x++) preserve[x] = 0;
        
        token = strtok(preservestr, " ,\n");
        
        while (token != NULL) {
-         if (strcasecmp(token, "all") == 0)
+         if (strcasecmp(token, "all") == 0 || strcasecmp(token, "a") == 0)
            for (x = 0; x <= counter; x++) preserve[x] = 1;
          
          number = 0;
@@ -880,24 +1048,44 @@ void deletefiles(file_t *files, int prompt, FILE *tty)
 
       printf("\n");
 
+      if (loginfo)
+        log_begin_set(loginfo);
+
       for (x = 1; x <= counter; x++) { 
        if (preserve[x])
+        {
          printf("   [+] %s\n", dupelist[x]->d_name);
+
+          if (loginfo)
+            log_file_remaining(loginfo, dupelist[x]->d_name);
+        }
        else {
          if (remove(dupelist[x]->d_name) == 0) {
            printf("   [-] %s\n", dupelist[x]->d_name);
+
+            if (loginfo)
+              log_file_deleted(loginfo, dupelist[x]->d_name);
          } else {
            printf("   [!] %s ", dupelist[x]->d_name);
            printf("-- unable to delete file!\n");
+
+            if (loginfo)
+              log_file_remaining(loginfo, dupelist[x]->d_name);
          }
        }
       }
       printf("\n");
+
+      if (loginfo)
+        log_end_set(loginfo);
     }
     
     files = files->next;
   }
 
+  if (loginfo)
+    log_close(loginfo);
+
   free(dupelist);
   free(preserve);
   free(preservestr);
@@ -911,11 +1099,11 @@ int sort_pairs_by_arrival(file_t *f1, file_t *f2)
   return !ISFLAG(flags, F_REVERSE) ? -1 : 1;
 }
 
-int sort_pairs_by_mtime(file_t *f1, file_t *f2)
+int sort_pairs_by_time(file_t *f1, file_t *f2)
 {
-  if (f1->mtime < f2->mtime)
+  if (f1->sorttime < f2->sorttime)
     return !ISFLAG(flags, F_REVERSE) ? -1 : 1;
-  else if (f1->mtime > f2->mtime)
+  else if (f1->sorttime > f2->sorttime)
     return !ISFLAG(flags, F_REVERSE) ? 1 : -1;
 
   return 0;
@@ -923,7 +1111,8 @@ int sort_pairs_by_mtime(file_t *f1, file_t *f2)
 
 int sort_pairs_by_filename(file_t *f1, file_t *f2)
 {
-  return strcmp(f1->d_name, f2->d_name);
+  int strvalue = strcmp(f1->d_name, f2->d_name);
+  return !ISFLAG(flags, F_REVERSE) ? strvalue : -strvalue;
 }
 
 void registerpair(file_t **matchlist, file_t *newmatch, 
@@ -973,7 +1162,7 @@ void registerpair(file_t **matchlist, file_t *newmatch,
 }
 
 void deletesuccessor(file_t **existing, file_t *duplicate, 
-      int (*comparef)(file_t *f1, file_t *f2))
+      int (*comparef)(file_t *f1, file_t *f2), struct log_info *loginfo)
 {
   file_t *to_keep;
   file_t *to_delete;
@@ -994,11 +1183,21 @@ void deletesuccessor(file_t **existing, file_t *duplicate,
   if (!ISFLAG(flags, F_HIDEPROGRESS)) fprintf(stderr, "\r%40s\r", " ");
 
   printf("   [+] %s\n", to_keep->d_name);
+
+  if (loginfo)
+    log_file_remaining(loginfo, to_keep->d_name);
+
   if (remove(to_delete->d_name) == 0) {
     printf("   [-] %s\n", to_delete->d_name);
+
+    if (loginfo)
+      log_file_deleted(loginfo, to_delete->d_name);
   } else {
     printf("   [!] %s ", to_delete->d_name);
     printf("-- unable to delete file!\n");
+
+    if (loginfo)
+      log_file_remaining(loginfo, to_delete->d_name);
   }
 
   printf("\n");
@@ -1017,11 +1216,14 @@ void help_text()
   printf(" -H --hardlinks   \tnormally, when two or more files point to the same\n");
   printf("                  \tdisk area they are treated as non-duplicates; this\n"); 
   printf("                  \toption will change this behavior\n");
+  printf(" -G --minsize=SIZE\tconsider only files greater than or equal to SIZE\n");
+  printf(" -L --maxsize=SIZE\tconsider only files less than or equal to SIZE\n");
   printf(" -n --noempty     \texclude zero-length files from consideration\n");
   printf(" -A --nohidden    \texclude hidden files from consideration\n");
   printf(" -f --omitfirst   \tomit the first file in each set of matches\n");
   printf(" -1 --sameline    \tlist each set of matches on a single line\n");
   printf(" -S --size        \tshow size of duplicate files\n");
+  printf(" -t --time        \tshow modification time of duplicate files\n");
   printf(" -m --summarize   \tsummarize dupe information\n");
   printf(" -q --quiet       \thide progress indicator\n");
   printf(" -d --delete      \tprompt user for files to preserve and delete all\n"); 
@@ -1030,7 +1232,10 @@ void help_text()
   printf("                  \twith -s or --symlinks, or when specifying a\n");
   printf("                  \tparticular directory more than once; refer to the\n");
   printf("                  \tfdupes documentation for additional information\n");
-  /*printf(" -l --relink      \t(description)\n");*/
+#ifndef NO_NCURSES
+  printf(" -P --plain       \twith --delete, use line-based prompt (as with older\n");
+  printf("                  \tversions of fdupes) instead of screen-mode interface\n");
+#endif
   printf(" -N --noprompt    \ttogether with --delete, preserve the first file in\n");
   printf("                  \teach set of duplicates and delete the rest without\n");
   printf("                  \tprompting the user\n");
@@ -1038,12 +1243,14 @@ void help_text()
   printf("                  \tgrouping into sets; implies --noprompt\n");
   printf(" -p --permissions \tdon't consider files with different owner/group or\n");
   printf("                  \tpermission bits as duplicates\n");
-  printf(" -o --order=BY    \tselect sort order for output, linking and deleting; by\n");
-  printf("                  \tmtime (BY='time'; default) or filename (BY='name')\n");
+  printf(" -o --order=BY    \tselect sort order for output and deleting; by file\n");
+  printf("                  \tmodification time (BY='time'; default), status\n");
+  printf("                  \tchange time (BY='ctime'), or filename (BY='name')\n");
   printf(" -i --reverse     \treverse order while sorting\n");
+  printf(" -l --log=LOGFILE \tlog file deletion choices to LOGFILE\n");
   printf(" -v --version     \tdisplay fdupes version\n");
   printf(" -h --help        \tdisplay this help message\n\n");
-#ifdef OMIT_GETOPT_LONG
+#ifndef HAVE_GETOPT_H
   printf("Note: Long options are not supported in this fdupes build.\n\n");
 #endif
 }
@@ -1061,25 +1268,30 @@ int main(int argc, char **argv) {
   int progress = 0;
   char **oldargv;
   int firstrecurse;
-  ordertype_t ordertype = ORDER_TIME;
+  char *logfile = 0;
+  struct log_info *loginfo;
+  int log_error;
+  struct stat logfile_status;
+  char *endptr;
   
-#ifndef OMIT_GETOPT_LONG
+#ifdef HAVE_GETOPT_H
   static struct option long_options[] = 
   {
     { "omitfirst", 0, 0, 'f' },
     { "recurse", 0, 0, 'r' },
-    { "recursive", 0, 0, 'r' },
     { "recurse:", 0, 0, 'R' },
-    { "recursive:", 0, 0, 'R' },
     { "quiet", 0, 0, 'q' },
     { "sameline", 0, 0, '1' },
     { "size", 0, 0, 'S' },
+    { "time", 0, 0, 't' },
     { "symlinks", 0, 0, 's' },
     { "hardlinks", 0, 0, 'H' },
-    { "relink", 0, 0, 'l' },
+    { "minsize", 1, 0, 'G' },
+    { "maxsize", 1, 0, 'L' },
     { "noempty", 0, 0, 'n' },
     { "nohidden", 0, 0, 'A' },
     { "delete", 0, 0, 'd' },
+    { "plain", 0, 0, 'P' },
     { "version", 0, 0, 'v' },
     { "help", 0, 0, 'h' },
     { "noprompt", 0, 0, 'N' },
@@ -1089,6 +1301,7 @@ int main(int argc, char **argv) {
     { "permissions", 0, 0, 'p' },
     { "order", 1, 0, 'o' },
     { "reverse", 0, 0, 'i' },
+    { "log", 1, 0, 'l' },
     { 0, 0, 0, 0 }
   };
 #define GETOPT getopt_long
@@ -1098,10 +1311,12 @@ int main(int argc, char **argv) {
 
   program_name = argv[0];
 
+  setlocale(LC_CTYPE, "");
+
   oldargv = cloneargs(argc, argv);
 
-  while ((opt = GETOPT(argc, argv, "frRq1SsHlnAdvhNImpo:i"
-#ifndef OMIT_GETOPT_LONG
+  while ((opt = GETOPT(argc, argv, "frRq1StsHG:L:nAdPvhNImpo:il:"
+#ifdef HAVE_GETOPT_H
           , long_options, NULL
 #endif
           )) != EOF) {
@@ -1124,12 +1339,31 @@ int main(int argc, char **argv) {
     case 'S':
       SETFLAG(flags, F_SHOWSIZE);
       break;
+    case 't':
+      SETFLAG(flags, F_SHOWTIME);
+      break;
     case 's':
       SETFLAG(flags, F_FOLLOWLINKS);
       break;
     case 'H':
       SETFLAG(flags, F_CONSIDERHARDLINKS);
       break;
+    case 'G':
+      minsize = strtoll(optarg, &endptr, 10);
+      if (optarg[0] == '\0' || *endptr != '\0' || minsize < 0)
+      {
+        errormsg("invalid value for --minsize: '%s'\n", optarg);
+        exit(1);
+      }
+      break;
+    case 'L':
+      maxsize = strtoll(optarg, &endptr, 10);
+      if (optarg[0] == '\0' || *endptr != '\0' || maxsize < 0)
+      {
+        errormsg("invalid value for --maxsize: '%s'\n", optarg);
+        exit(1);
+      }
+      break;
     case 'n':
       SETFLAG(flags, F_EXCLUDEEMPTY);
       break;
@@ -1139,6 +1373,9 @@ int main(int argc, char **argv) {
     case 'd':
       SETFLAG(flags, F_DELETEFILES);
       break;
+    case 'P':
+      SETFLAG(flags, F_PLAINPROMPT);
+      break;
     case 'v':
       printf("fdupes %s\n", VERSION);
       exit(0);
@@ -1161,7 +1398,9 @@ int main(int argc, char **argv) {
       if (!strcasecmp("name", optarg)) {
         ordertype = ORDER_NAME;
       } else if (!strcasecmp("time", optarg)) {
-        ordertype = ORDER_TIME;
+        ordertype = ORDER_MTIME;
+      } else if (!strcasecmp("ctime", optarg)) {
+        ordertype = ORDER_CTIME;
       } else {
         errormsg("invalid value for --order: '%s'\n", optarg);
         exit(1);
@@ -1170,7 +1409,26 @@ int main(int argc, char **argv) {
     case 'i':
       SETFLAG(flags, F_REVERSE);
       break;
+    case 'l':
+      loginfo = log_open(logfile=optarg, &log_error);
+      if (loginfo == 0)
+      {
+        if (log_error == LOG_ERROR_NOT_A_LOG_FILE)
+          errormsg("%s: doesn't look like an fdupes log file\n", logfile);
+        else
+          errormsg("%s: could not open log file\n", logfile);
+
+        exit(1);
+      }
+      log_close(loginfo);
+
+      if (stat(logfile, &logfile_status) != 0)
+      {
+        errormsg("could not read log file status\n");
+        exit(1);
+      }
 
+      break;
     default:
       fprintf(stderr, "Try `fdupes --help' for more information.\n");
       exit(1);
@@ -1205,16 +1463,16 @@ int main(int argc, char **argv) {
 
     /* F_RECURSE is not set for directories before --recurse: */
     for (x = optind; x < firstrecurse; x++)
-      filecount += grokdir(argv[x], &files);
+      filecount += grokdir(argv[x], &files, &logfile_status);
 
     /* Set F_RECURSE for directories after --recurse: */
     SETFLAG(flags, F_RECURSE);
 
     for (x = firstrecurse; x < argc; x++)
-      filecount += grokdir(argv[x], &files);
+      filecount += grokdir(argv[x], &files, &logfile_status);
   } else {
     for (x = optind; x < argc; x++)
-      filecount += grokdir(argv[x], &files);
+      filecount += grokdir(argv[x], &files, &logfile_status);
   }
 
   if (!files) {
@@ -1247,10 +1505,12 @@ int main(int argc, char **argv) {
       if (confirmmatch(file1, file2)) {
         if (ISFLAG(flags, F_DELETEFILES) && ISFLAG(flags, F_IMMEDIATE))
           deletesuccessor(match, curfile,
-              (ordertype == ORDER_TIME) ? sort_pairs_by_mtime : sort_pairs_by_filename );
+              (ordertype == ORDER_MTIME || 
+               ordertype == ORDER_CTIME) ? sort_pairs_by_time : sort_pairs_by_filename, loginfo );
         else
           registerpair(match, curfile,
-              (ordertype == ORDER_TIME) ? sort_pairs_by_mtime : sort_pairs_by_filename );
+              (ordertype == ORDER_MTIME ||
+               ordertype == ORDER_CTIME) ? sort_pairs_by_time : sort_pairs_by_filename );
       }
       
       fclose(file1);
@@ -1272,17 +1532,43 @@ int main(int argc, char **argv) {
   {
     if (ISFLAG(flags, F_NOPROMPT))
     {
-      deletefiles(files, 0, 0);
+      deletefiles(files, 0, 0, logfile);
     }
     else
     {
-      if (freopen("/dev/tty", "r", stdin) == 0)
+#ifndef NO_NCURSES
+      if (!ISFLAG(flags, F_PLAINPROMPT))
+      {
+        if (newterm(getenv("TERM"), stdout, stdin) != 0)
+        {
+          deletefiles_ncurses(files, logfile);
+        }
+        else
+        {
+          errormsg("could not enter screen mode; falling back to plain mode\n\n");
+          SETFLAG(flags, F_PLAINPROMPT);
+        }
+      }
+
+      if (ISFLAG(flags, F_PLAINPROMPT))
+      {
+        if (freopen("/dev/tty", "r", stdin) == NULL)
+        {
+          errormsg("could not open terminal for input\n");
+          exit(1);
+        }
+
+        deletefiles(files, 1, stdin, logfile);
+      }
+#else
+      if (freopen("/dev/tty", "r", stdin) == NULL)
       {
         errormsg("could not open terminal for input\n");
         exit(1);
       }
 
-      deletefiles(files, 1, stdin);
+      deletefiles(files, 1, stdin, logfile);
+#endif
     }
   }
 
diff --git a/fdupes.h b/fdupes.h
new file mode 100644 (file)
index 0000000..4f58c12
--- /dev/null
+++ b/fdupes.h
@@ -0,0 +1,41 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef FDUPES_H
+#define FDUPES_H
+
+#include <sys/stat.h>
+#include "md5/md5.h"
+
+typedef struct _file {
+  char *d_name;
+  off_t size;
+  md5_byte_t *crcpartial;
+  md5_byte_t *crcsignature;
+  dev_t device;
+  ino_t inode;
+  time_t sorttime;
+  int hasdupes; /* true only if file is first on duplicate chain */
+  struct _file *duplicates;
+  struct _file *next;
+} file_t;
+
+#endif
\ No newline at end of file
diff --git a/filegroup.h b/filegroup.h
new file mode 100644 (file)
index 0000000..ad3c880
--- /dev/null
@@ -0,0 +1,43 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef FILEGROUP_H
+#define FILEGROUP_H
+
+#include "fdupes.h"
+
+struct groupfile
+{
+  file_t *file;
+  int action;
+  int selected;
+};
+
+struct filegroup
+{
+  struct groupfile *files;
+  size_t filecount;
+  int startline;
+  int endline;
+  int selected;
+};
+
+#endif
\ No newline at end of file
diff --git a/flags.c b/flags.c
new file mode 100644 (file)
index 0000000..7d9889a
--- /dev/null
+++ b/flags.c
@@ -0,0 +1,3 @@
+#include "flags.h"
+
+unsigned long flags = 0;
\ No newline at end of file
diff --git a/flags.h b/flags.h
new file mode 100644 (file)
index 0000000..1e264fd
--- /dev/null
+++ b/flags.h
@@ -0,0 +1,28 @@
+#ifndef FLAGS_H
+#define FLAGS_H
+
+#define ISFLAG(a,b) ((a & b) == b)
+#define SETFLAG(a,b) (a |= b)
+
+#define F_RECURSE           0x0001
+#define F_HIDEPROGRESS      0x0002
+#define F_DSAMELINE         0x0004
+#define F_FOLLOWLINKS       0x0008
+#define F_DELETEFILES       0x0010
+#define F_EXCLUDEEMPTY      0x0020
+#define F_CONSIDERHARDLINKS 0x0040
+#define F_SHOWSIZE          0x0080
+#define F_OMITFIRST         0x0100
+#define F_RECURSEAFTER      0x0200
+#define F_NOPROMPT          0x0400
+#define F_SUMMARIZEMATCHES  0x0800
+#define F_EXCLUDEHIDDEN     0x1000
+#define F_PERMISSIONS       0x2000
+#define F_REVERSE           0x4000
+#define F_IMMEDIATE         0x8000
+#define F_PLAINPROMPT       0x10000
+#define F_SHOWTIME          0x20000
+
+extern unsigned long flags;
+
+#endif
\ No newline at end of file
diff --git a/fmatch.c b/fmatch.c
new file mode 100644 (file)
index 0000000..3453d33
--- /dev/null
+++ b/fmatch.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <string.h>
+#include "fmatch.h"
+
+/* Test whether given string matches text at current file position.
+*/
+void fmatch(FILE *file, char *matchstring, int *is_match, size_t *chars_read)
+{
+  size_t len;
+  int x;
+  int c;
+
+  *is_match = 0;
+  *chars_read = 0;
+
+  len = strlen(matchstring);
+  for (x = 0; x < len; ++x)
+  {
+    c = fgetc(file);
+    if (c == EOF)
+      return;
+
+    (*chars_read)++;
+
+    if ((char)c != matchstring[x])
+      return;
+  }
+
+  *is_match = 1;
+}
diff --git a/fmatch.h b/fmatch.h
new file mode 100644 (file)
index 0000000..233503c
--- /dev/null
+++ b/fmatch.h
@@ -0,0 +1,26 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef FMATCH_H
+#define FMATCH_H
+
+#include <stdio.h>
+
+void fmatch(FILE *file, char *matchstring, int *is_match, size_t *chars_read);
+
+#endif
diff --git a/log.c b/log.c
new file mode 100644 (file)
index 0000000..71cf7d9
--- /dev/null
+++ b/log.c
@@ -0,0 +1,229 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "fmatch.h"
+#include "dir.h"
+#include "log.h"
+
+#define LOG_HEADER "[fdupes log]\n"
+
+/* Open log file in append mode. If file exists, make sure it is a valid fdupes log file. 
+*/
+struct log_info *log_open(char *filename, int *error)
+{
+  struct log_info *info;
+  int is_match;
+  size_t read;
+
+  info = (struct log_info*) malloc(sizeof(struct log_info));
+  if (info == 0)
+  {
+    if (error != 0)
+      *error = LOG_ERROR_OUT_OF_MEMORY;
+
+    return 0;
+  }
+
+  info->file = fopen(filename, "a+");
+  if (info->file == 0)
+  {
+    if (error != 0)
+      *error = LOG_ERROR_FOPEN_FAILED;
+
+    free(info);
+    return 0;
+  }
+
+  fmatch(info->file, LOG_HEADER, &is_match, &read);
+  if (!is_match && read > 0)
+  {
+    if (error != 0)
+      *error = LOG_ERROR_NOT_A_LOG_FILE;
+
+    free(info);
+    return 0;
+  }
+
+  info->append = read > 0;
+
+  info->log_start = 1;
+  info->deleted = 0;
+  info->remaining = 0;
+
+  if (error != 0)
+    *error = LOG_ERROR_NONE;
+
+  return info;
+}
+
+/* Free linked lists holding set of deleted and remaining files.
+*/
+void log_free_set(struct log_info *info)
+{
+  struct log_file *f;
+  struct log_file *next;
+
+  f = info->deleted;
+  while (f != 0)
+  {
+    next = f->next;
+
+    free(f);
+
+    f = next;
+  }
+
+  f = info->remaining;
+  while (f != 0)
+  {
+    next = f->next;
+
+    free(f);
+
+    f = next;
+  }
+
+  info->deleted = 0;
+  info->remaining = 0;
+}
+
+/* Signal beginning of duplicate set.
+*/
+void log_begin_set(struct log_info *info)
+{
+  log_free_set(info);
+}
+
+/* Add deleted file to log.
+*/
+int log_file_deleted(struct log_info *info, char *name)
+{
+  struct log_file *file;
+
+  file = (struct log_file*) malloc(sizeof(struct log_file));
+  if (file == 0)
+    return 0;
+
+  file->next = info->deleted;
+  file->filename = name;
+
+  info->deleted = file;
+
+  return 1;
+}
+
+/* Add remaining file to log.
+*/
+int log_file_remaining(struct log_info *info, char *name)
+{
+  struct log_file *file;
+
+  file = (struct log_file*) malloc(sizeof(struct log_file));
+  if (file == 0)
+    return 0;
+
+  file->next = info->remaining;
+  file->filename = name;
+
+  info->remaining = file;
+
+  return 1;
+}
+
+/* Output log header.
+*/
+void log_header(FILE *file)
+{
+  fprintf(file, "%s\n", LOG_HEADER);
+}
+
+/* Output log timestamp.
+*/
+void log_timestamp(FILE *file)
+{
+  time_t t = time(NULL);
+  struct tm tm = *localtime(&t);
+
+  fprintf(file, "Log entry for %d-%02d-%02d %02d:%02d:%02d\n\n", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+}
+
+/* Output current working directory.
+*/
+void log_cwd(FILE *file)
+{
+  char *cwd = getworkingdirectory();
+
+  fprintf(file, "working directory:\n  %s\n\n", cwd);
+
+  free(cwd);
+}
+
+/* Signal the end of a duplicate set.
+*/
+void log_end_set(struct log_info *info)
+{
+  struct log_file *f;
+
+  if (info->deleted == 0)
+    return;
+
+  if (info->log_start)
+  {
+    if (info->append)
+      fprintf(info->file, "---\n\n");
+    else
+      log_header(info->file);
+
+    log_timestamp(info->file);
+    log_cwd(info->file);
+
+    info->log_start = 0;
+  }
+
+  f = info->deleted;
+  do
+  {
+    fprintf(info->file, "deleted %s\n", f->filename);
+    f = f->next;
+  } while (f != 0);
+
+  f = info->remaining;
+  while (f != 0)
+  {
+    fprintf(info->file, "   left %s\n", f->filename);
+    f = f->next;
+  }
+
+  fprintf(info->file, "\n");
+
+  fflush(info->file);
+}
+
+/* Close log and free all memory.
+*/
+void log_close(struct log_info *info)
+{
+  fclose(info->file);
+
+  log_free_set(info);
+
+  free(info);
+}
diff --git a/log.h b/log.h
new file mode 100644 (file)
index 0000000..ed6ff82
--- /dev/null
+++ b/log.h
@@ -0,0 +1,51 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdio.h>
+
+#define LOG_ERROR_NONE 0
+#define LOG_ERROR_OUT_OF_MEMORY 1
+#define LOG_ERROR_FOPEN_FAILED 2
+#define LOG_ERROR_NOT_A_LOG_FILE 3
+
+struct log_file
+{
+  char *filename;
+  struct log_file *next;
+};
+
+struct log_info
+{
+  FILE *file;
+  int append;
+  int log_start;
+  struct log_file *deleted;
+  struct log_file *remaining;
+};
+
+struct log_info *log_open(char *filename, int *error);
+void log_begin_set(struct log_info *info);
+int log_file_deleted(struct log_info *info, char *name);
+int log_file_remaining(struct log_info *info, char *name);
+void log_end_set(struct log_info *info);
+void log_close(struct log_info *info);
+
+#endif
\ No newline at end of file
diff --git a/mbstowcs_escape_invalid.c b/mbstowcs_escape_invalid.c
new file mode 100644 (file)
index 0000000..8d03a7b
--- /dev/null
@@ -0,0 +1,147 @@
+/* Copyright (c) 2019 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include "config.h"
+#include "mbstowcs_escape_invalid.h"
+#include <wchar.h>
+#include <string.h>
+#include <stdio.h>
+
+void reset_mbstate(mbstate_t *state)
+{
+    memset(state, 0, sizeof(mbstate_t));
+}
+
+size_t putoctal(wchar_t *dest, char c)
+{
+    swprintf(dest, 5, L"\\%03o", (unsigned char) c);
+    return 4;
+}
+
+size_t put_invalid_sequence(wchar_t *dest, const char *src, size_t *destination_index, size_t count, size_t max)
+{
+    size_t x;
+
+    for (x = 0; x < count; ++x)
+    {
+        if (dest != 0)
+        {
+            if (*destination_index + 5 > max)
+                return x;
+
+            putoctal(dest + *destination_index, src[x]);
+        }
+
+        *destination_index += 4;
+    }
+
+    return count;
+}
+
+size_t mbstowcs_escape_invalid(wchar_t *dest, const char *src, size_t n)
+{
+    mbstate_t state;
+    wchar_t wc;
+    size_t x;
+    size_t i;
+    size_t dx;
+    size_t write;
+    size_t written;
+    size_t result;
+
+    reset_mbstate(&state);
+
+    x = 0;
+    i = 0;
+    dx = 0;
+
+    while (src[x] != '\0')
+    {
+        result = mbrtowc(&wc, src + x, 1, &state);
+
+        if (result == -2)
+        /* sequence is not yet complete */
+        {
+            ++x;
+            ++i;
+        }
+        else if (result == -1)
+        /* invalid sequence */
+        {
+            write = i == 0 ? 1 : i;
+
+            if (dest != 0)
+            {
+                written = put_invalid_sequence(dest, src + (x - i), &dx, write, n);
+
+                if (written != write)
+                    return -1;
+            }
+            else
+                put_invalid_sequence(0, src + (x - i), &dx, write, 0);
+
+            if (i == 0)
+                ++x;
+    
+            i = 0;
+
+            reset_mbstate(&state);
+        }
+        else if (result != 0)
+        /* OK, add character */
+        {
+            if (dest != 0)
+            {
+                if (dx < n)
+                    dest[dx++] = wc;
+                else
+                    return -1;
+            }
+            else
+                ++dx;
+
+            ++x;
+
+            i = 0;
+        }
+        
+        if (src[x] == L'\0' && i > 0)
+        /* output final incomplete sequence */
+        {
+            if (dest != 0)
+            {
+                written = put_invalid_sequence(dest, src + (x - i), &dx, i, n);
+
+                if (written != i)
+                    return -1;
+            }
+            else
+                put_invalid_sequence(0, src + (x - i), &dx, i, 0);
+        }        
+    }
+
+    if (dest != 0)
+    {
+        if (dx < n)
+            dest[dx] = L'\0';
+        else
+            return -1;
+    }
+
+    return dx + 1;
+}
diff --git a/mbstowcs_escape_invalid.h b/mbstowcs_escape_invalid.h
new file mode 100644 (file)
index 0000000..ed48413
--- /dev/null
@@ -0,0 +1,26 @@
+/* Copyright (c) 2019 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef MBSTOWCS_ESCAPE_INVALID_H
+#define MBSTOWCS_ESCAPE_INVALID_H
+
+#include <wchar.h>
+
+size_t mbstowcs_escape_invalid(wchar_t *dest, const char *src, size_t n);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-commands.c b/ncurses-commands.c
new file mode 100644 (file)
index 0000000..d00d58e
--- /dev/null
@@ -0,0 +1,676 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include "ncurses-status.h"
+#include "ncurses-commands.h"
+#include "wcs.h"
+#include "mbstowcs_escape_invalid.h"
+#include <wchar.h>
+#include <pcre2.h>
+
+void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally);
+
+struct command_map command_list[] = {
+  {L"sel", COMMAND_SELECT_CONTAINING},
+  {L"selb", COMMAND_SELECT_BEGINNING},
+  {L"sele", COMMAND_SELECT_ENDING},
+  {L"selm", COMMAND_SELECT_MATCHING},
+  {L"selr", COMMAND_SELECT_REGEX},
+  {L"dsel", COMMAND_CLEAR_SELECTIONS_CONTAINING},
+  {L"dselb", COMMAND_CLEAR_SELECTIONS_BEGINNING},
+  {L"dsele", COMMAND_CLEAR_SELECTIONS_ENDING},
+  {L"dselm", COMMAND_CLEAR_SELECTIONS_MATCHING},
+  {L"dselr", COMMAND_CLEAR_SELECTIONS_REGEX},
+  {L"cs", COMMAND_CLEAR_ALL_SELECTIONS},
+  {L"igs", COMMAND_INVERT_GROUP_SELECTIONS},
+  {L"ks", COMMAND_KEEP_SELECTED},
+  {L"ds", COMMAND_DELETE_SELECTED},
+  {L"rs", COMMAND_RESET_SELECTED},
+  {L"rg", COMMAND_RESET_GROUP},
+  {L"all", COMMAND_PRESERVE_ALL},
+  {L"goto", COMMAND_GOTO_SET},
+  {L"exit", COMMAND_EXIT},
+  {L"quit", COMMAND_EXIT},
+  {L"help", COMMAND_HELP},
+  {0, COMMAND_UNDEFINED}
+};
+
+struct command_map confirmation_keyword_list[] = {
+  {L"yes", COMMAND_YES},
+  {L"no", COMMAND_NO},
+  {0, COMMAND_UNDEFINED}
+};
+
+/* select files containing string */
+int cmd_select_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int selectedgroupcount = 0;
+  int selectedfilecount = 0;
+  int groupselected;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupselected = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          groups[g].selected = 1;
+          groups[g].files[f].selected = 1;
+
+          groupselected = 1;
+          ++selectedfilecount;
+        }
+      }
+
+      if (groupselected)
+        ++selectedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
+
+  return 1;
+}
+
+/* select files beginning with string */
+int cmd_select_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int selectedgroupcount = 0;
+  int selectedfilecount = 0;
+  int groupselected;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupselected = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          groups[g].selected = 1;
+          groups[g].files[f].selected = 1;
+
+          groupselected = 1;
+          ++selectedfilecount;
+        }
+      }
+
+      if (groupselected)
+        ++selectedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
+
+  return 1;
+}
+
+/* select files ending with string */
+int cmd_select_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int selectedgroupcount = 0;
+  int selectedfilecount = 0;
+  int groupselected;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupselected = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          groups[g].selected = 1;
+          groups[g].files[f].selected = 1;
+
+          groupselected = 1;
+          ++selectedfilecount;
+        }
+      }
+
+      if (groupselected)
+        ++selectedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
+
+  return 1;
+}
+
+/* select files matching string */
+int cmd_select_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int selectedgroupcount = 0;
+  int selectedfilecount = 0;
+  int groupselected;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupselected = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
+        {
+          groups[g].selected = 1;
+          groups[g].files[f].selected = 1;
+
+          groupselected = 1;
+          ++selectedfilecount;
+        }
+      }
+
+      if (groupselected)
+        ++selectedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
+
+  return 1;
+}
+
+/* select files matching pattern */
+int cmd_select_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  size_t size;
+  wchar_t *wcsfilename;
+  size_t needed;
+  int errorcode;
+  PCRE2_SIZE erroroffset;
+  pcre2_code *code;
+  pcre2_match_data *md;
+  int matches;
+  int g;
+  int f;
+  int selectedgroupcount = 0;
+  int selectedfilecount = 0;
+  int groupselected;
+
+  code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
+
+  if (code == 0)
+    return -1;
+
+  pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
+
+  md = pcre2_match_data_create(1, 0);
+  if (md == 0)
+    return -1;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    groupselected = 0;
+
+    for (f = 0; f < groups[g].filecount; ++f)
+    {
+      needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
+
+      wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
+      if (wcsfilename == 0)
+        continue;
+
+      mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
+
+      matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
+
+      free(wcsfilename);
+
+      if (matches > 0)
+      {
+        groups[g].selected = 1;
+        groups[g].files[f].selected = 1;
+
+        groupselected = 1;
+        ++selectedfilecount;
+      }
+    }
+
+    if (groupselected)
+      ++selectedgroupcount;
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
+
+  pcre2_code_free(code);
+
+  return 1;
+}
+
+/* clear selections containing string */
+int cmd_clear_selections_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int matchedgroupcount = 0;
+  int matchedfilecount = 0;
+  int groupmatched;
+  int filedeselected;
+  int selectionsremaining;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupmatched = 0;
+      filedeselected = 0;
+      selectionsremaining = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          if (groups[g].files[f].selected)
+          {
+            groups[g].files[f].selected = 0;
+            filedeselected = 1;
+          }
+
+          groupmatched = 1;
+          ++matchedfilecount;
+        }
+
+        if (groups[g].files[f].selected)
+          selectionsremaining = 1;
+      }
+
+      if (filedeselected && !selectionsremaining)
+        groups[g].selected = 0;
+
+      if (groupmatched)
+        ++matchedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
+
+  return 1;
+}
+
+/* clear selections beginning with string */
+int cmd_clear_selections_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int matchedgroupcount = 0;
+  int matchedfilecount = 0;
+  int groupmatched;
+  int filedeselected;
+  int selectionsremaining;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupmatched = 0;
+      filedeselected = 0;
+      selectionsremaining = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          if (groups[g].files[f].selected)
+          {
+            groups[g].files[f].selected = 0;
+            filedeselected = 1;
+          }
+
+          groupmatched = 1;
+          ++matchedfilecount;
+        }
+
+        if (groups[g].files[f].selected)
+          selectionsremaining = 1;
+      }
+
+      if (filedeselected && !selectionsremaining)
+        groups[g].selected = 0;
+
+      if (groupmatched)
+        ++matchedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
+
+  return 1;
+}
+
+/* clear selections ending with string */
+int cmd_clear_selections_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int matchedgroupcount = 0;
+  int matchedfilecount = 0;
+  int groupmatched;
+  int filedeselected;
+  int selectionsremaining;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupmatched = 0;
+      filedeselected = 0;
+      selectionsremaining = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
+        {
+          if (groups[g].files[f].selected)
+          {
+            groups[g].files[f].selected = 0;
+            filedeselected = 1;
+          }
+
+          groupmatched = 1;
+          ++matchedfilecount;
+        }
+
+        if (groups[g].files[f].selected)
+          selectionsremaining = 1;
+      }
+
+      if (filedeselected && !selectionsremaining)
+        groups[g].selected = 0;
+
+      if (groupmatched)
+        ++matchedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
+
+  return 1;
+}
+
+/* clear selections matching string */
+int cmd_clear_selections_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int matchedgroupcount = 0;
+  int matchedfilecount = 0;
+  int groupmatched;
+  int filedeselected;
+  int selectionsremaining;
+
+  if (wcscmp(commandarguments, L"") != 0)
+  {
+    for (g = 0; g < groupcount; ++g)
+    {
+      groupmatched = 0;
+      filedeselected = 0;
+      selectionsremaining = 0;
+
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
+        {
+          if (groups[g].files[f].selected)
+          {
+            groups[g].files[f].selected = 0;
+            filedeselected = 1;
+          }
+
+          groupmatched = 1;
+          ++matchedfilecount;
+        }
+
+        if (groups[g].files[f].selected)
+          selectionsremaining = 1;
+      }
+
+      if (filedeselected && !selectionsremaining)
+        groups[g].selected = 0;
+
+      if (groupmatched)
+        ++matchedgroupcount;
+    }
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
+
+  return 1;
+}
+
+/* clear selection matching pattern */
+int cmd_clear_selections_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  size_t size;
+  wchar_t *wcsfilename;
+  size_t needed;
+  int errorcode;
+  PCRE2_SIZE erroroffset;
+  pcre2_code *code;
+  pcre2_match_data *md;
+  int matches;
+  int g;
+  int f;
+  int matchedgroupcount = 0;
+  int matchedfilecount = 0;
+  int groupmatched;
+  int filedeselected;
+  int selectionsremaining;
+
+  code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
+
+  if (code == 0)
+    return -1;
+
+  pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
+
+  md = pcre2_match_data_create(1, 0);
+  if (md == 0)
+    return -1;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    groupmatched = 0;
+    filedeselected = 0;
+    selectionsremaining = 0;
+
+    for (f = 0; f < groups[g].filecount; ++f)
+    {
+      needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
+
+      wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
+      if (wcsfilename == 0)
+        continue;
+
+      mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
+
+      matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
+
+      free(wcsfilename);
+
+      if (matches > 0)
+      {
+        if (groups[g].files[f].selected)
+        {
+          groups[g].files[f].selected = 0;
+          filedeselected = 1;
+        }
+
+        groupmatched = 1;
+        ++matchedfilecount;
+      }
+
+      if (groups[g].files[f].selected)
+        selectionsremaining = 1;
+    }
+
+    if (filedeselected && !selectionsremaining)
+      groups[g].selected = 0;
+
+    if (groupmatched)
+      ++matchedgroupcount;
+  }
+
+  format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
+
+  pcre2_code_free(code);
+
+  return 1;
+}
+
+/* clear all selections and selected groups */
+int cmd_clear_all_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    for (f = 0; f < groups[g].filecount; ++f)
+      groups[g].files[f].selected = 0;
+
+    groups[g].selected = 0;
+  }
+
+  format_status_left(status, L"Cleared all selections.");
+
+  return 1;
+}
+
+/* invert selections within selected groups */
+int cmd_invert_group_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
+{
+  int g;
+  int f;
+  int selectedcount = 0;
+  int deselectedcount = 0;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    if (groups[g].selected)
+    {
+      for (f = 0; f < groups[g].filecount; ++f)
+      {
+        groups[g].files[f].selected = !groups[g].files[f].selected;
+
+        if (groups[g].files[f].selected)
+          ++selectedcount;
+        else
+          ++deselectedcount;
+      }
+    }
+  }
+
+  format_status_left(status, L"Selected %d files. Deselected %d files.", selectedcount, deselectedcount);
+
+  return 1;
+}
+
+/* mark selected files for preservation */
+int cmd_keep_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
+{
+  int g;
+  int f;
+  int keepfilecount = 0;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    for (f = 0; f < groups[g].filecount; ++f)
+    {
+      if (groups[g].files[f].selected)
+      {
+        set_file_action(&groups[g].files[f], 1, deletiontally);
+        ++keepfilecount;
+      }
+    }
+  }
+
+  format_status_left(status, L"Marked %d files for preservation.", keepfilecount);
+
+  return 1;
+}
+
+/* mark selected files for deletion */
+int cmd_delete_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
+{
+  int g;
+  int f;
+  int deletefilecount = 0;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    for (f = 0; f < groups[g].filecount; ++f)
+    {
+      if (groups[g].files[f].selected)
+      {
+        set_file_action(&groups[g].files[f], -1, deletiontally);
+        ++deletefilecount;
+      }
+    }
+  }
+
+  format_status_left(status, L"Marked %d files for deletion.", deletefilecount);
+
+  return 1;
+}
+
+/* mark selected files as unresolved */
+int cmd_reset_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
+{
+  int g;
+  int f;
+  int resetfilecount = 0;
+
+  for (g = 0; g < groupcount; ++g)
+  {
+    for (f = 0; f < groups[g].filecount; ++f)
+    {
+      if (groups[g].files[f].selected)
+      {
+        set_file_action(&groups[g].files[f], 0, deletiontally);
+        ++resetfilecount;
+      }
+    }
+  }
+
+  format_status_left(status, L"Unmarked %d files.", resetfilecount);
+
+  return 1;
+}
diff --git a/ncurses-commands.h b/ncurses-commands.h
new file mode 100644 (file)
index 0000000..e0fca68
--- /dev/null
@@ -0,0 +1,74 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESCOMMANDS_H
+#define NCURSESCOMMANDS_H
+
+#include "commandidentifier.h"
+#include "filegroup.h"
+
+/* command IDs */
+#define COMMAND_SELECT_CONTAINING 1
+#define COMMAND_SELECT_BEGINNING 2
+#define COMMAND_SELECT_ENDING 3
+#define COMMAND_SELECT_MATCHING 4
+#define COMMAND_CLEAR_SELECTIONS_CONTAINING 5
+#define COMMAND_CLEAR_SELECTIONS_BEGINNING 6
+#define COMMAND_CLEAR_SELECTIONS_ENDING 7
+#define COMMAND_CLEAR_SELECTIONS_MATCHING 8
+#define COMMAND_CLEAR_ALL_SELECTIONS 9
+#define COMMAND_INVERT_GROUP_SELECTIONS 10
+#define COMMAND_KEEP_SELECTED 11
+#define COMMAND_DELETE_SELECTED 12
+#define COMMAND_RESET_SELECTED 13
+#define COMMAND_RESET_GROUP 14
+#define COMMAND_PRESERVE_ALL 15
+#define COMMAND_EXIT 16
+#define COMMAND_HELP 17
+#define COMMAND_YES 18
+#define COMMAND_NO 19
+#define COMMAND_SELECT_REGEX 20
+#define COMMAND_CLEAR_SELECTIONS_REGEX 21
+#define COMMAND_GOTO_SET 22
+
+extern struct command_map command_list[];
+extern struct command_map confirmation_keyword_list[];
+
+struct filegroup;
+struct status_text;
+
+int cmd_select_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_select_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_select_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_select_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_select_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_selections_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_selections_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_selections_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_selections_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_selections_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_clear_all_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_invert_group_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status);
+int cmd_keep_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
+int cmd_delete_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
+int cmd_reset_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-getcommand.c b/ncurses-getcommand.c
new file mode 100644 (file)
index 0000000..24e94b3
--- /dev/null
@@ -0,0 +1,245 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include <stdlib.h>
+#include <signal.h>
+#include "ncurses-getcommand.h"
+
+#define KEY_ESCAPE 27
+
+extern volatile sig_atomic_t got_sigint;
+
+/* get command and arguments from user input */
+void get_command_arguments(wchar_t **arguments, wchar_t *input)
+{
+  size_t l;
+  size_t x;
+
+  l = wcslen(input);
+
+  for (x = 0; x < l; ++x)
+    if (input[x] == L' ')
+      break;
+
+  if (input[x] == L' ')
+    *arguments = input + x + 1;
+  else
+    *arguments = input + x;
+}
+
+int get_command_text(wchar_t **commandbuffer, size_t *commandbuffersize, WINDOW *promptwin, struct prompt_info *prompt, int cancel_on_erase, int append)
+{
+  int docommandinput;
+  int keyresult;
+  wint_t wch;
+  wint_t oldch;
+  size_t length;
+  size_t newsize;
+  wchar_t *realloccommandbuffer;
+  size_t c;
+
+  set_prompt_active_state(prompt, 1);
+  wrefresh(promptwin);
+
+  if (*commandbuffer == 0)
+  {
+    *commandbuffersize = 80;
+    *commandbuffer = malloc(*commandbuffersize * sizeof(wchar_t));
+    if (*commandbuffer == 0)
+    {
+      set_prompt_active_state(prompt, 0);
+      return GET_COMMAND_ERROR_OUT_OF_MEMORY;
+    }
+  }
+
+  if (!append)
+  {
+    (*commandbuffer)[0] = L'\0';
+  }
+  else
+  {
+    print_prompt(promptwin, prompt, *commandbuffer);
+
+    prompt->cursor = wcswidth(*commandbuffer, wcslen(*commandbuffer));
+
+    wmove(promptwin, 0, wcslen(prompt->text) + prompt->cursor - prompt->offset);
+
+    wrefresh(promptwin);
+  }
+
+  docommandinput = 1;
+  do
+  {
+    do
+    {
+      keyresult = wget_wch(promptwin, &wch);
+
+      if (got_sigint)
+      {
+        got_sigint = 0;
+
+        (*commandbuffer)[0] = '\0';
+
+        set_prompt_active_state(prompt, 0);
+
+        return GET_COMMAND_CANCELED;
+      }
+    } while (keyresult == ERR);
+
+    if (keyresult == OK)
+    {
+      switch (wch)
+      {
+        case KEY_ESCAPE:
+          prompt->offset = 0;
+          prompt->cursor = 0;
+          docommandinput = 0;
+
+          (*commandbuffer)[0] = '\0';
+
+          set_prompt_active_state(prompt, 0);
+
+          return GET_COMMAND_CANCELED;
+
+        case '\n':
+          prompt->offset = 0;
+          prompt->cursor = 0;
+          docommandinput = 0;
+          continue;
+
+        case '\t':
+          continue;
+
+        default:
+          if (!iswprint(wch))
+            continue;
+
+          length = wcslen(*commandbuffer);
+
+          if (length + 1 >= *commandbuffersize)
+          {
+            newsize = *commandbuffersize * 2;
+
+            realloccommandbuffer = (wchar_t*)realloc(*commandbuffer, newsize * sizeof(wchar_t));
+            if (realloccommandbuffer == 0)
+            {
+              set_prompt_active_state(prompt, 0);
+              return GET_COMMAND_ERROR_OUT_OF_MEMORY;
+            }
+
+            *commandbuffer = realloccommandbuffer;
+            *commandbuffersize = newsize;
+          }
+
+          for (c = length + 1; c >= prompt->cursor + 1; --c)
+            (*commandbuffer)[c] = (*commandbuffer)[c-1];
+
+          (*commandbuffer)[prompt->cursor] = wch;
+
+          set_prompt_active_state(prompt, 1);
+
+          update_prompt(promptwin, prompt, *commandbuffer, wcwidth(wch));
+
+          break;
+      }
+    }
+    else if (keyresult == KEY_CODE_YES)
+    {
+      switch (wch)
+      {
+        case KEY_BACKSPACE:
+          length = wcslen(*commandbuffer);
+
+          if (length == 0)
+          {
+            set_prompt_active_state(prompt, 0);
+
+            if (cancel_on_erase)
+              return GET_COMMAND_CANCELED;
+          }
+
+          oldch = (*commandbuffer)[prompt->cursor];
+
+          if (prompt->cursor > 0)
+            for (c = prompt->cursor; c <= length; ++c)
+              (*commandbuffer)[c-1] = (*commandbuffer)[c];
+
+          update_prompt(promptwin, prompt, *commandbuffer, oldch != 0 ? -wcwidth(oldch) : -1);
+
+          break;
+
+        case KEY_DC:
+          length = wcslen(*commandbuffer);
+
+          if (prompt->cursor < length)
+            for (c = prompt->cursor; c <= length; ++c)
+              (*commandbuffer)[c] = (*commandbuffer)[c+1];
+
+          break;
+
+        case KEY_LEFT:
+          length = wcslen(*commandbuffer);
+
+          oldch = (*commandbuffer)[prompt->cursor];
+
+          update_prompt(promptwin, prompt, *commandbuffer, oldch != 0 ? -wcwidth(oldch) : -1);
+
+          break;
+
+        case KEY_RIGHT:
+          length = wcslen(*commandbuffer);
+
+          oldch = (*commandbuffer)[prompt->cursor];
+
+          if (prompt->cursor + wcwidth((*commandbuffer)[prompt->cursor]) <= length)
+            update_prompt(promptwin, prompt, *commandbuffer, wcwidth(oldch));
+
+          break;
+
+        case KEY_NPAGE:
+          return GET_COMMAND_KEY_NPAGE;
+
+        case KEY_PPAGE:
+          return GET_COMMAND_KEY_PPAGE;
+
+        case KEY_SF:
+          return GET_COMMAND_KEY_SF;
+
+        case KEY_SR:
+          return GET_COMMAND_KEY_SR;
+
+        case KEY_RESIZE:
+          return GET_COMMAND_RESIZE_REQUESTED;
+      }
+    }
+
+    print_prompt(promptwin, prompt, *commandbuffer);
+
+    wmove(promptwin, 0, wcslen(prompt->text) + prompt->cursor - prompt->offset);
+
+    wrefresh(promptwin);
+  } while (docommandinput);
+
+  set_prompt_active_state(prompt, 0);
+
+  return GET_COMMAND_OK;
+}
\ No newline at end of file
diff --git a/ncurses-getcommand.h b/ncurses-getcommand.h
new file mode 100644 (file)
index 0000000..f9a17eb
--- /dev/null
@@ -0,0 +1,45 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESGETCOMMAND_H
+#define NCURSESGETCOMMAND_H
+
+#include <wchar.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+#include "ncurses-prompt.h"
+
+#define GET_COMMAND_OK 1
+#define GET_COMMAND_CANCELED 2
+#define GET_COMMAND_ERROR_OUT_OF_MEMORY 3
+#define GET_COMMAND_RESIZE_REQUESTED 4
+#define GET_COMMAND_KEY_NPAGE 5
+#define GET_COMMAND_KEY_PPAGE 6
+#define GET_COMMAND_KEY_SF 7
+#define GET_COMMAND_KEY_SR 8
+
+void get_command_arguments(wchar_t **arguments, wchar_t *input);
+int get_command_text(wchar_t **commandbuffer, size_t *commandbuffersize, WINDOW *promptwin, struct prompt_info *prompt, int cancel_on_erase, int append);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-interface.c b/ncurses-interface.c
new file mode 100644 (file)
index 0000000..8129ed2
--- /dev/null
@@ -0,0 +1,1508 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+#include "ncurses-interface.h"
+#include "ncurses-getcommand.h"
+#include "ncurses-commands.h"
+#include "ncurses-prompt.h"
+#include "ncurses-status.h"
+#include "ncurses-print.h"
+#include "mbstowcs_escape_invalid.h"
+#include "positive_wcwidth.h"
+#include "commandidentifier.h"
+#include "filegroup.h"
+#include "errormsg.h"
+#include "log.h"
+#include "sigint.h"
+#include "flags.h"
+
+char *fmtmtime(char *filename);
+
+enum linestyle
+{
+  linestyle_groupheader = 0,
+  linestyle_groupheaderspacing,
+  linestyle_filename,
+  linestyle_groupfooterspacing
+};
+
+enum linestyle getlinestyle(struct filegroup *group, int line)
+{
+  if (line <= group->startline)
+    return linestyle_groupheader;
+  else if (line == group->startline + 1)
+    return linestyle_groupheaderspacing;
+  else if (line >= group->endline)
+    return linestyle_groupfooterspacing;
+  else
+    return linestyle_filename;
+}
+
+#define FILENAME_INDENT_EXTRA 5
+#define FILE_INDEX_MIN_WIDTH 3
+
+int filerowcount(file_t *file, const int columns, int group_file_count)
+{
+  int lines;
+  int line_remaining;
+  size_t x = 0;
+  size_t read;
+  size_t filename_bytes;
+  wchar_t ch;
+  mbstate_t mbstate;
+  int index_width;
+  int timestamp_width;
+  size_t needed;
+  wchar_t *wcfilename;
+
+  memset(&mbstate, 0, sizeof(mbstate));
+
+  needed = mbstowcs_escape_invalid(0, file->d_name, 0);
+
+  wcfilename = (wchar_t*)malloc(sizeof(wchar_t) * needed);
+  if (wcfilename == 0)
+    return 0;
+
+  mbstowcs_escape_invalid(wcfilename, file->d_name, needed);
+
+  index_width = get_num_digits(group_file_count);
+  if (index_width < FILE_INDEX_MIN_WIDTH)
+    index_width = FILE_INDEX_MIN_WIDTH;
+
+  timestamp_width = ISFLAG(flags, F_SHOWTIME) ? 19 : 0;
+
+  lines = (index_width + timestamp_width + FILENAME_INDENT_EXTRA) / columns + 1;
+
+  line_remaining = columns - (index_width + timestamp_width + FILENAME_INDENT_EXTRA) % columns;
+
+  while (wcfilename[x] != L'\0')
+  {
+    if (positive_wcwidth(wcfilename[x]) <= line_remaining)
+    {
+      line_remaining -= positive_wcwidth(wcfilename[x]);
+    }
+    else
+    {
+      line_remaining = columns - positive_wcwidth(wcfilename[x]);
+      ++lines;
+    }
+
+    ++x;
+  }
+
+  free(wcfilename);
+
+  return lines;
+}
+
+int getgroupindex(struct filegroup *groups, int group_count, int group_hint, int line)
+{
+  int group = group_hint;
+
+  while (group > 0 && line < groups[group].startline)
+    --group;
+
+  while (group < group_count && line > groups[group].endline)
+    ++group;
+
+  return group;
+}
+
+int getgroupfileindex(int *row, struct filegroup *group, int line, int columns)
+{
+  int l;
+  int f = 0;
+  int rowcount;
+
+  l = group->startline + 2;
+
+  while (f < group->filecount)
+  {
+    rowcount = filerowcount(group->files[f].file, columns, group->filecount);
+
+    if (line <= l + rowcount - 1)
+    {
+      *row = line - l;
+      return f;
+    }
+
+    l += rowcount;
+    ++f;
+  }
+
+  return -1;
+}
+
+int getgroupfileline(struct filegroup *group, int fileindex, int columns)
+{
+  int l;
+  int f = 0;
+  int rowcount;
+
+  l = group->startline + 2;
+
+  while (f < fileindex && f < group->filecount)
+  {
+    rowcount = filerowcount(group->files[f].file, columns, group->filecount);
+    l += rowcount;
+    ++f;
+  }
+
+  return l;
+}
+
+void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally)
+{
+  switch (file->action)
+  {
+    case -1:
+      if (new_action != -1)
+        --*deletion_tally;
+      break;
+
+    default:
+      if (new_action == -1)
+        ++*deletion_tally;
+      break;
+  }
+
+  file->action = new_action;
+}
+
+void scroll_to_group(int *topline, int group, int tail, struct filegroup *groups, WINDOW *filewin)
+{
+  if (*topline < groups[group].startline)
+  {
+    if (groups[group].endline >= *topline + getmaxy(filewin))
+    {
+      if (groups[group].endline - groups[group].startline < getmaxy(filewin))
+        *topline = groups[group].endline - getmaxy(filewin) + 1;
+      else
+        *topline = groups[group].startline;
+    }
+  }
+  else
+  {
+    if (groups[group].endline - groups[group].startline < getmaxy(filewin) || !tail)
+      *topline = groups[group].startline;
+    else
+      *topline = groups[group].endline - getmaxy(filewin);
+  }
+}
+
+void move_to_next_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
+{
+  *cursorgroup += 1;
+
+  *cursorfile = 0;
+
+  scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
+}
+
+int move_to_next_selected_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, int totalgroups, WINDOW *filewin)
+{
+  size_t g;
+
+  for (g = *cursorgroup + 1; g < totalgroups; ++g)
+  {
+    if (groups[g].selected)
+    {
+      *cursorgroup = g;
+      *cursorfile = 0;
+
+      scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
+
+      return 1;
+    }
+  }
+
+  return 0;
+}
+
+void move_to_next_file(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
+{
+  *cursorfile += 1;
+
+  if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) >= *topline + getmaxy(filewin))
+  {
+      if (groups[*cursorgroup].endline - getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) < getmaxy(filewin))
+        *topline = groups[*cursorgroup].endline - getmaxy(filewin) + 1;
+      else
+        *topline = getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS);
+  }
+}
+
+void move_to_previous_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
+{
+  *cursorgroup -= 1;
+
+  *cursorfile = groups[*cursorgroup].filecount - 1;
+
+  scroll_to_group(topline, *cursorgroup, 1, groups, filewin);
+}
+
+int move_to_previous_selected_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, int totalgroups, WINDOW *filewin)
+{
+  size_t g;
+
+  for (g = *cursorgroup; g > 0; --g)
+  {
+    if (groups[g - 1].selected)
+    {
+      *cursorgroup = g - 1;
+      *cursorfile = 0;
+
+      scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
+
+      return 1;
+    }
+  }
+
+  return 0;
+}
+
+void move_to_previous_file(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
+{
+  *cursorfile -= 1;
+
+  if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) < *topline)
+  {
+      if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) - groups[*cursorgroup].startline < getmaxy(filewin))
+        *topline -= getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) - groups[*cursorgroup].startline + 1;
+      else
+        *topline -= getmaxy(filewin);
+  }
+}
+
+#define FILE_LIST_OK 1
+#define FILE_LIST_ERROR_INDEX_OUT_OF_RANGE -1
+#define FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX -2
+#define FILE_LIST_ERROR_UNKNOWN_COMMAND -3
+#define FILE_LIST_ERROR_OUT_OF_MEMORY -4
+
+int validate_file_list(struct filegroup *currentgroup, wchar_t *commandbuffer_in)
+{
+  wchar_t *commandbuffer;
+  wchar_t *token;
+  wchar_t *wcsptr;
+  wchar_t *wcstolcheck;
+  long int number;
+  int parts = 0;
+  int parse_error = 0;
+  int out_of_bounds_error = 0;
+
+  commandbuffer = malloc(sizeof(wchar_t) * (wcslen(commandbuffer_in)+1));
+  if (commandbuffer == 0)
+    return FILE_LIST_ERROR_OUT_OF_MEMORY;
+
+  wcscpy(commandbuffer, commandbuffer_in);
+
+  token = wcstok(commandbuffer, L",", &wcsptr);
+
+  while (token != NULL)
+  {
+    ++parts;
+
+    number = wcstol(token, &wcstolcheck, 10);
+    if (wcstolcheck == token || *wcstolcheck != '\0')
+      parse_error = 1;
+
+    if (number > currentgroup->filecount || number < 1)
+      out_of_bounds_error = 1;
+
+    token = wcstok(NULL, L",", &wcsptr);
+  }
+
+  free(commandbuffer);
+
+  if (parts == 1 && parse_error)
+    return FILE_LIST_ERROR_UNKNOWN_COMMAND;
+  else if (parse_error)
+    return FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX;
+  else if (out_of_bounds_error)
+    return FILE_LIST_ERROR_INDEX_OUT_OF_RANGE;
+
+  return FILE_LIST_OK;
+}
+
+void deletefiles_ncurses(file_t *files, char *logfile)
+{
+  WINDOW *filewin;
+  WINDOW *promptwin;
+  WINDOW *statuswin;
+  file_t *curfile;
+  file_t *dupefile;
+  struct filegroup *groups;
+  struct filegroup *reallocgroups;
+  size_t groupfilecount;
+  int topline = 0;
+  int cursorgroup = 0;
+  int cursorfile = 0;
+  int cursor_x;
+  int cursor_y;
+  int groupfirstline = 0;
+  int totallines = 0;
+  int allocatedgroups = 0;
+  int totalgroups = 0;
+  size_t groupindex = 0;
+  enum linestyle linestyle;
+  int preservecount;
+  int deletecount;
+  int unresolvedcount;
+  int totaldeleted;
+  size_t globaldeletiontally = 0;
+  double deletedbytes;
+  int row;
+  int x;
+  int g;
+  wint_t wch;
+  int keyresult;
+  int cy;
+  int f;
+  int to;
+  wchar_t *commandbuffer;
+  size_t commandbuffersize;
+  wchar_t *commandarguments;
+  struct command_identifier_node *commandidentifier;
+  struct command_identifier_node *confirmationkeywordidentifier;
+  int doprune;
+  wchar_t *token;
+  wchar_t *wcsptr;
+  wchar_t *wcstolcheck;
+  long int number;
+  struct status_text *status;
+  struct prompt_info *prompt;
+  int dupesfound;
+  int intresult;
+  int adjusttopline;
+  int toplineoffset = 0;
+  int resumecommandinput = 0;
+  int index_width;
+  int timestamp_width;
+  struct log_info *loginfo;
+
+  noecho();
+  cbreak();
+  halfdelay(5);
+
+  filewin = newwin(LINES - 2, COLS, 0, 0);
+  statuswin = newwin(1, COLS, LINES - 1, 0);
+  promptwin = newwin(1, COLS, LINES - 2, 0);
+
+  scrollok(filewin, FALSE);
+  scrollok(statuswin, FALSE);
+  scrollok(promptwin, FALSE);
+
+  wattron(statuswin, A_REVERSE);
+
+  keypad(promptwin, 1);
+
+  commandbuffersize = 80;
+  commandbuffer = malloc(commandbuffersize * sizeof(wchar_t));
+  if (commandbuffer == 0)
+  {
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  allocatedgroups = 1024;
+  groups = malloc(sizeof(struct filegroup) * allocatedgroups);
+  if (groups == 0)
+  {
+    free(commandbuffer);
+
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  commandidentifier = build_command_identifier_tree(command_list);
+  if (commandidentifier == 0)
+  {
+    free(groups);
+    free(commandbuffer);
+
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  confirmationkeywordidentifier = build_command_identifier_tree(confirmation_keyword_list);
+  if (confirmationkeywordidentifier == 0)
+  {
+    free(groups);
+    free(commandbuffer);
+    free_command_identifier_tree(commandidentifier);
+
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  register_sigint_handler();
+
+  curfile = files;
+  while (curfile)
+  {
+    if (!curfile->hasdupes)
+    {
+      curfile = curfile->next;
+      continue;
+    }
+
+    if (totalgroups + 1 > allocatedgroups)
+    {
+      allocatedgroups *= 2;
+
+      reallocgroups = realloc(groups, sizeof(struct filegroup) * allocatedgroups);
+      if (reallocgroups == 0)
+      {
+        for (g = 0; g < totalgroups; ++g)
+          free(groups[g].files);
+
+        free(groups);
+        free(commandbuffer);
+        free_command_identifier_tree(commandidentifier);
+        free_command_identifier_tree(confirmationkeywordidentifier);
+
+        endwin();
+        errormsg("out of memory\n");
+        exit(1);
+      }
+
+      groups = reallocgroups;
+    }
+
+    groups[totalgroups].startline = groupfirstline;
+    groups[totalgroups].endline = groupfirstline + 2;
+    groups[totalgroups].selected = 0;
+
+    groupfilecount = 0;
+
+    dupefile = curfile;
+    do
+    {
+      ++groupfilecount;
+
+      dupefile = dupefile->duplicates;
+    } while(dupefile);
+
+    dupefile = curfile;
+    do
+    {
+      groups[totalgroups].endline += filerowcount(dupefile, COLS, groupfilecount);
+
+      dupefile = dupefile->duplicates;
+    } while (dupefile);
+
+    groups[totalgroups].files = malloc(sizeof(struct groupfile) * groupfilecount);
+    if (groups[totalgroups].files == 0)
+    {
+      for (g = 0; g < totalgroups; ++g)
+        free(groups[g].files);
+
+      free(groups);
+      free(commandbuffer);
+      free_command_identifier_tree(commandidentifier);
+      free_command_identifier_tree(confirmationkeywordidentifier);
+
+      endwin();
+      errormsg("out of memory\n");
+      exit(1);
+    }
+
+    groupfilecount = 0;
+
+    dupefile = curfile;
+    do
+    {
+      groups[totalgroups].files[groupfilecount].file = dupefile;
+      groups[totalgroups].files[groupfilecount].action = 0;
+      groups[totalgroups].files[groupfilecount].selected = 0;
+      ++groupfilecount;
+
+      dupefile = dupefile->duplicates;
+    } while (dupefile);
+
+    groups[totalgroups].filecount = groupfilecount;
+
+    groupfirstline = groups[totalgroups].endline + 1;
+
+    ++totalgroups;
+
+    curfile = curfile->next;
+  }
+
+  dupesfound = totalgroups > 0;
+
+  status = status_text_alloc(0, COLS);
+  if (status == 0)
+  {
+    for (g = 0; g < totalgroups; ++g)
+      free(groups[g].files);
+
+    free(groups);
+    free(commandbuffer);
+    free_command_identifier_tree(commandidentifier);
+    free_command_identifier_tree(confirmationkeywordidentifier);
+
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  format_status_left(status, L"Ready");
+
+  prompt = prompt_info_alloc(80);
+  if (prompt == 0)
+  {
+    free_status_text(status);
+
+    for (g = 0; g < totalgroups; ++g)
+      free(groups[g].files);
+
+    free(groups);
+    free(commandbuffer);
+    free_command_identifier_tree(commandidentifier);
+    free_command_identifier_tree(confirmationkeywordidentifier);
+
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  doprune = 1;
+  do
+  {
+    wmove(filewin, 0, 0);
+    werase(filewin);
+
+    if (totalgroups > 0)
+      totallines = groups[totalgroups-1].endline;
+    else
+      totallines = 0;
+
+    for (x = topline; x < topline + getmaxy(filewin); ++x)
+    {
+      if (x >= totallines)
+      {
+        wclrtoeol(filewin);
+        continue;
+      }
+
+      groupindex = getgroupindex(groups, totalgroups, groupindex, x);
+
+      index_width = get_num_digits(groups[groupindex].filecount);
+
+      if (index_width < FILE_INDEX_MIN_WIDTH)
+        index_width = FILE_INDEX_MIN_WIDTH;
+
+      timestamp_width = ISFLAG(flags, F_SHOWTIME) ? 19 : 0;
+
+      linestyle = getlinestyle(groups + groupindex, x);
+      
+      if (linestyle == linestyle_groupheader)
+      {
+        wattron(filewin, A_BOLD);
+        if (groups[groupindex].selected)
+          wattron(filewin, A_REVERSE);
+        wprintw(filewin, "Set %d of %d:\n", groupindex + 1, totalgroups);
+        if (groups[groupindex].selected)
+          wattroff(filewin, A_REVERSE);
+        wattroff(filewin, A_BOLD);
+      }
+      else if (linestyle == linestyle_groupheaderspacing)
+      {
+        wprintw(filewin, "\n");
+      }
+      else if (linestyle == linestyle_filename)
+      {
+        f = getgroupfileindex(&row, groups + groupindex, x, COLS);
+
+        if (cursorgroup != groupindex)
+        {
+          if (row == 0)
+          {
+            print_spaces(filewin, index_width);
+
+            wprintw(filewin, " [%c] ", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
+
+            if (ISFLAG(flags, F_SHOWTIME))
+              wprintw(filewin, "[%s] ", fmtmtime(groups[groupindex].files[f].file->d_name));
+          }
+
+          cy = getcury(filewin);
+
+          if (groups[groupindex].files[f].selected)
+            wattron(filewin, A_REVERSE);
+          putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
+          if (groups[groupindex].files[f].selected)
+            wattroff(filewin, A_REVERSE);
+
+          wclrtoeol(filewin);
+          wmove(filewin, cy+1, 0);
+        }
+        else
+        {
+          if (row == 0)
+          {
+            print_right_justified_int(filewin, f+1, index_width);
+            wprintw(filewin, " ");
+
+            if (cursorgroup == groupindex && cursorfile == f)
+              wattron(filewin, A_REVERSE);
+            wprintw(filewin, "[%c]", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
+            if (cursorgroup == groupindex && cursorfile == f)
+              wattroff(filewin, A_REVERSE);
+            wprintw(filewin, " ");
+
+            if (ISFLAG(flags, F_SHOWTIME))
+              wprintw(filewin, "[%s] ", fmtmtime(groups[groupindex].files[f].file->d_name));
+          }
+
+          cy = getcury(filewin);
+
+          if (groups[groupindex].files[f].selected)
+            wattron(filewin, A_REVERSE);
+          putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
+          if (groups[groupindex].files[f].selected)
+            wattroff(filewin, A_REVERSE);
+
+          wclrtoeol(filewin);
+          wmove(filewin, cy+1, 0);
+        }
+      }
+      else if (linestyle == linestyle_groupfooterspacing)
+      {
+        wprintw(filewin, "\n");
+      }
+    }
+
+    if (totalgroups > 0)
+      format_status_right(status, L"Set %d of %d", cursorgroup+1, totalgroups);
+    else
+      format_status_right(status, L"Finished");
+
+    print_status(statuswin, status);
+
+    if (totalgroups > 0)
+      format_prompt(prompt, L"( Preserve files [1 - %d, all, help] )", groups[cursorgroup].filecount);
+    else if (dupesfound)
+      format_prompt(prompt, L"( No duplicates remaining; type 'exit' to exit program )");
+    else
+      format_prompt(prompt, L"( No duplicates found; type 'exit' to exit program )");
+
+    print_prompt(promptwin, prompt, L"");
+
+    /* refresh windows (using wrefresh instead of wnoutrefresh to avoid bug in gnome-terminal) */
+    wrefresh(filewin);
+    wrefresh(statuswin);
+    wrefresh(promptwin);
+
+    /* wait for user input */
+    if (!resumecommandinput)
+    {
+      do
+      {
+        keyresult = wget_wch(promptwin, &wch);
+
+        if (got_sigint)
+        {
+          getyx(promptwin, cursor_y, cursor_x);
+
+          format_status_left(status, L"Type 'exit' to exit fdupes.");
+          print_status(statuswin, status);
+
+          wmove(promptwin, cursor_y, cursor_x);
+
+          got_sigint = 0;
+
+          wrefresh(statuswin);
+        }
+      } while (keyresult == ERR);
+
+      if (keyresult == OK && iswprint(wch))
+      {
+        commandbuffer[0] = wch;
+        commandbuffer[1] = '\0';
+      }
+      else
+      {
+        commandbuffer[0] = '\0';
+      }
+    }
+
+    if (resumecommandinput || (keyresult == OK && iswprint(wch) && ((wch != '\t' && wch != '\n' && wch != '?'))))
+    {
+      resumecommandinput = 0;
+
+      switch (get_command_text(&commandbuffer, &commandbuffersize, promptwin, prompt, 1, 1))
+      {
+        case GET_COMMAND_OK:
+          get_command_arguments(&commandarguments, commandbuffer);
+
+          switch (identify_command(commandidentifier, commandbuffer, 0))
+          {
+            case COMMAND_SELECT_CONTAINING:
+              cmd_select_containing(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_SELECT_BEGINNING:
+              cmd_select_beginning(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_SELECT_ENDING:
+              cmd_select_ending(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_SELECT_MATCHING:
+              cmd_select_matching(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_SELECT_REGEX:
+              cmd_select_regex(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_SELECTIONS_CONTAINING:
+              cmd_clear_selections_containing(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_SELECTIONS_BEGINNING:
+              cmd_clear_selections_beginning(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_SELECTIONS_ENDING:
+              cmd_clear_selections_ending(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_SELECTIONS_MATCHING:
+              cmd_clear_selections_matching(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_SELECTIONS_REGEX:
+              cmd_clear_selections_regex(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_CLEAR_ALL_SELECTIONS:
+              cmd_clear_all_selections(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_INVERT_GROUP_SELECTIONS:
+              cmd_invert_group_selections(groups, totalgroups, commandarguments, status);
+              break;
+
+            case COMMAND_KEEP_SELECTED:
+              cmd_keep_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
+              break;
+
+            case COMMAND_DELETE_SELECTED:
+              cmd_delete_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
+              break;
+
+            case COMMAND_RESET_SELECTED:
+              cmd_reset_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
+              break;
+
+            case COMMAND_RESET_GROUP:
+              for (x = 0; x < groups[cursorgroup].filecount; ++x)
+                set_file_action(&groups[cursorgroup].files[x], 0, &globaldeletiontally);
+
+              format_status_left(status, L"Reset all files in current group.");
+
+              break;
+
+            case COMMAND_PRESERVE_ALL:
+              /* mark all files for preservation */
+              for (x = 0; x < groups[cursorgroup].filecount; ++x)
+                set_file_action(&groups[cursorgroup].files[x], 1, &globaldeletiontally);
+
+              format_status_left(status, L"%d files marked for preservation", groups[cursorgroup].filecount);
+
+              if (cursorgroup < totalgroups - 1)
+                move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+              break;
+
+            case COMMAND_GOTO_SET:
+              number = wcstol(commandarguments, &wcstolcheck, 10);
+              if (wcstolcheck != commandarguments && *wcstolcheck == '\0')
+              {
+                if (number >= 1 && number <= totalgroups)
+                {
+                  scroll_to_group(&topline, number - 1, 0, groups, filewin);
+
+                  cursorgroup = number - 1;
+                  cursorfile = 0;
+                }
+                else
+                {
+                  format_status_left(status, L"Group index out of range.");
+                }
+              }
+              else
+              {
+                format_status_left(status, L"Invalid group index.");
+              }
+
+              break;
+
+            case COMMAND_HELP:
+              endwin();
+
+              if (system(HELP_COMMAND_STRING) == -1)
+                format_status_left(status, L"Could not display help text.");
+
+              refresh();
+
+              break;
+
+            case COMMAND_EXIT: /* exit program */
+              if (totalgroups == 0)
+              {
+                doprune = 0;
+                continue;
+              }
+              else
+              {
+                if (globaldeletiontally != 0)
+                  format_prompt(prompt, L"( There are files marked for deletion. Exit without deleting? )");
+                else
+                  format_prompt(prompt, L"( There are duplicates remaining. Exit anyway? )");
+
+                print_prompt(promptwin, prompt, L"");
+
+                wrefresh(promptwin);
+
+                switch (get_command_text(&commandbuffer, &commandbuffersize, promptwin, prompt, 0, 0))
+                {
+                  case GET_COMMAND_OK:
+                    switch (identify_command(confirmationkeywordidentifier, commandbuffer, 0))
+                    {
+                      case COMMAND_YES:
+                        doprune = 0;
+                        continue;
+
+                      case COMMAND_NO:
+                      case COMMAND_UNDEFINED:
+                        commandbuffer[0] = '\0';
+                        continue;
+                    }
+                    break;
+
+                  case GET_COMMAND_CANCELED:
+                    commandbuffer[0] = '\0';
+                    continue;
+
+                  case GET_COMMAND_RESIZE_REQUESTED:
+                    /* resize windows */
+                    wresize(filewin, LINES - 2, COLS);
+
+                    wresize(statuswin, 1, COLS);
+                    wresize(promptwin, 1, COLS);
+                    mvwin(statuswin, LINES - 1, 0);
+                    mvwin(promptwin, LINES - 2, 0);
+
+                    status_text_alloc(status, COLS);
+
+                    /* recalculate line boundaries */
+                    groupfirstline = 0;
+
+                    for (g = 0; g < totalgroups; ++g)
+                    {
+                      groups[g].startline = groupfirstline;
+                      groups[g].endline = groupfirstline + 2;
+
+                      for (f = 0; f < groups[g].filecount; ++f)
+                        groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
+
+                      groupfirstline = groups[g].endline + 1;
+                    }
+
+                    commandbuffer[0] = '\0';
+
+                    break;
+
+                  case GET_COMMAND_ERROR_OUT_OF_MEMORY:
+                    for (g = 0; g < totalgroups; ++g)
+                      free(groups[g].files);
+
+                    free(groups);
+                    free(commandbuffer);
+                    free_command_identifier_tree(commandidentifier);
+                    free_command_identifier_tree(confirmationkeywordidentifier);
+
+                    endwin();
+                    errormsg("out of memory\n");
+                    exit(1);
+                    break;
+                }
+              }
+              break;
+
+            default: /* parse list of files to preserve and mark for preservation */
+              intresult = validate_file_list(groups + cursorgroup, commandbuffer);
+              if (intresult != FILE_LIST_OK)
+              {
+                if (intresult == FILE_LIST_ERROR_UNKNOWN_COMMAND)
+                {
+                  format_status_left(status, L"Unrecognized command");
+                  break;
+                }
+                else if (intresult == FILE_LIST_ERROR_INDEX_OUT_OF_RANGE)
+                {
+                  format_status_left(status, L"Index out of range (1 - %d).", groups[cursorgroup].filecount);
+                  break;
+                }
+                else if (intresult == FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX)
+                {
+                  format_status_left(status, L"Invalid index");
+                  break;
+                }
+                else if (intresult == FILE_LIST_ERROR_OUT_OF_MEMORY)
+                {
+                  free(commandbuffer);
+
+                  free_command_identifier_tree(commandidentifier);
+
+                  for (g = 0; g < totalgroups; ++g)
+                    free(groups[g].files);
+
+                  free(groups);
+
+                  endwin();
+                  errormsg("out of memory\n");
+                  exit(1);
+                }
+                else
+                {
+                  format_status_left(status, L"Could not interpret command");
+                  break;
+                }
+              }
+
+              token = wcstok(commandbuffer, L",", &wcsptr);
+
+              while (token != NULL)
+              {
+                number = wcstol(token, &wcstolcheck, 10);
+                if (wcstolcheck != token && *wcstolcheck == '\0')
+                {
+                  if (number > 0 && number <= groups[cursorgroup].filecount)
+                    set_file_action(&groups[cursorgroup].files[number - 1], 1, &globaldeletiontally);
+                }
+
+                token = wcstok(NULL, L",", &wcsptr);
+              }
+
+              /* mark remaining files for deletion */
+              preservecount = 0;
+              deletecount = 0;
+
+              for (x = 0; x < groups[cursorgroup].filecount; ++x)
+              {
+                if (groups[cursorgroup].files[x].action == 1)
+                  ++preservecount;
+                if (groups[cursorgroup].files[x].action == -1)
+                  ++deletecount;
+              }
+
+              if (preservecount > 0)
+              {
+                for (x = 0; x < groups[cursorgroup].filecount; ++x)
+                {
+                  if (groups[cursorgroup].files[x].action == 0)
+                  {
+                    set_file_action(&groups[cursorgroup].files[x], -1, &globaldeletiontally);
+                    ++deletecount;
+                  }
+                }
+              }
+
+              format_status_left(status, L"%d files marked for preservation, %d for deletion", preservecount, deletecount);
+
+              if (cursorgroup < totalgroups - 1 && preservecount > 0)
+                move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+              break;
+          }
+
+          break;
+
+        case GET_COMMAND_KEY_SF:
+          ++topline;
+
+          resumecommandinput = 1;
+
+          continue;
+
+        case GET_COMMAND_KEY_SR:
+          if (topline > 0)
+            --topline;
+
+          resumecommandinput = 1;
+
+          continue;
+
+        case GET_COMMAND_KEY_NPAGE:
+          topline += getmaxy(filewin);
+
+          resumecommandinput = 1;
+
+          continue;
+
+        case GET_COMMAND_KEY_PPAGE:
+          topline -= getmaxy(filewin);
+
+          if (topline < 0)
+            topline = 0;
+
+          resumecommandinput = 1;
+
+          continue;
+
+        case GET_COMMAND_CANCELED:
+          break;
+
+        case GET_COMMAND_RESIZE_REQUESTED:
+          /* resize windows */
+          wresize(filewin, LINES - 2, COLS);
+
+          wresize(statuswin, 1, COLS);
+          wresize(promptwin, 1, COLS);
+          mvwin(statuswin, LINES - 1, 0);
+          mvwin(promptwin, LINES - 2, 0);
+
+          status_text_alloc(status, COLS);
+
+          /* recalculate line boundaries */
+          groupfirstline = 0;
+
+          for (g = 0; g < totalgroups; ++g)
+          {
+            groups[g].startline = groupfirstline;
+            groups[g].endline = groupfirstline + 2;
+
+            for (f = 0; f < groups[g].filecount; ++f)
+              groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
+
+            groupfirstline = groups[g].endline + 1;
+          }
+
+          commandbuffer[0] = '\0';
+
+          break;
+
+        case GET_COMMAND_ERROR_OUT_OF_MEMORY:
+          for (g = 0; g < totalgroups; ++g)
+            free(groups[g].files);
+
+          free(groups);
+          free(commandbuffer);
+          free_command_identifier_tree(commandidentifier);
+          free_command_identifier_tree(confirmationkeywordidentifier);
+
+          endwin();
+          errormsg("out of memory\n");
+          exit(1);
+
+          break;
+      }
+
+      commandbuffer[0] = '\0';
+    }
+    else if (keyresult == KEY_CODE_YES)
+    {
+      switch (wch)
+      {
+      case KEY_DOWN:
+        if (cursorfile < groups[cursorgroup].filecount - 1)
+          move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
+        else if (cursorgroup < totalgroups - 1)
+          move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+
+      case KEY_UP:
+        if (cursorfile > 0)
+          move_to_previous_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
+        else if (cursorgroup > 0)
+          move_to_previous_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+
+      case KEY_SF:
+        ++topline;
+        break;
+
+      case KEY_SR:
+        if (topline > 0)
+          --topline;
+        break;
+
+      case KEY_NPAGE:
+        topline += getmaxy(filewin);
+        break;
+
+      case KEY_PPAGE:
+        topline -= getmaxy(filewin);
+
+        if (topline < 0)
+          topline = 0;
+
+        break;
+
+      case KEY_SRIGHT:
+        set_file_action(&groups[cursorgroup].files[cursorfile], 1, &globaldeletiontally);
+
+        format_status_left(status, L"1 file marked for preservation.");
+
+        if (cursorfile < groups[cursorgroup].filecount - 1)
+          move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
+        else if (cursorgroup < totalgroups - 1)
+          move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+
+      case KEY_SLEFT:
+        deletecount = 0;
+
+        set_file_action(&groups[cursorgroup].files[cursorfile], -1, &globaldeletiontally);
+
+        format_status_left(status, L"1 file marked for deletion.");
+
+        for (x = 0; x < groups[cursorgroup].filecount; ++x)
+          if (groups[cursorgroup].files[x].action == -1)
+            ++deletecount;
+
+        if (deletecount < groups[cursorgroup].filecount)
+        {
+          if (cursorfile < groups[cursorgroup].filecount - 1)
+            move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
+          else if (cursorgroup < totalgroups - 1)
+            move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+        }
+
+        break;
+
+      case KEY_BACKSPACE:
+        if (cursorgroup > 0)
+          --cursorgroup;
+
+        cursorfile = 0;
+
+        scroll_to_group(&topline, cursorgroup, 0, groups, filewin);
+
+        break;
+
+      case KEY_F(3):
+        move_to_next_selected_group(&topline, &cursorgroup, &cursorfile, groups, totalgroups, filewin);
+        break;
+
+      case KEY_F(2):
+        move_to_previous_selected_group(&topline, &cursorgroup, &cursorfile, groups, totalgroups, filewin);
+        break;
+
+      case KEY_DC:
+        totaldeleted = 0;
+        deletedbytes = 0;
+
+        if (logfile != 0)
+          loginfo = log_open(logfile, 0);
+        else
+          loginfo = 0;
+
+        for (g = 0; g < totalgroups; ++g)
+        {
+          preservecount = 0;
+          deletecount = 0;
+          unresolvedcount = 0;
+
+          for (f = 0; f < groups[g].filecount; ++f)
+          {
+            switch (groups[g].files[f].action)
+            {
+              case -1:
+                ++deletecount;
+                break;
+              case 0:
+                ++unresolvedcount;
+                break;
+              case 1:
+                ++preservecount;
+                break;
+            }
+          }
+
+          if (loginfo)
+            log_begin_set(loginfo);
+
+          /* delete files marked for deletion unless no files left undeleted */
+          if (deletecount < groups[g].filecount)
+          {
+            for (f = 0; f < groups[g].filecount; ++f)
+            {
+              if (groups[g].files[f].action == -1)
+              {
+                if (remove(groups[g].files[f].file->d_name) == 0)
+                {
+                  set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
+
+                  deletedbytes += groups[g].files[f].file->size;
+                  ++totaldeleted;
+
+                  if (loginfo)
+                    log_file_deleted(loginfo, groups[g].files[f].file->d_name);
+                }
+              }
+            }
+
+            if (loginfo)
+            {
+              for (f = 0; f < groups[g].filecount; ++f)
+              {
+                if (groups[g].files[f].action >= 0)
+                  log_file_remaining(loginfo, groups[g].files[f].file->d_name);
+              }
+            }
+
+            deletecount = 0;
+          }
+
+          if (loginfo)
+            log_end_set(loginfo);
+
+          /* if no files left unresolved, mark preserved files for delisting */
+          if (unresolvedcount == 0)
+          {
+            for (f = 0; f < groups[g].filecount; ++f)
+              if (groups[g].files[f].action == 1)
+                set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
+
+            preservecount = 0;
+          }
+          /* if only one file left unresolved, mark it for delesting */
+          else if (unresolvedcount == 1 && preservecount + deletecount == 0)
+          {
+            for (f = 0; f < groups[g].filecount; ++f)
+              if (groups[g].files[f].action == 0)
+                set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
+          }
+
+          /* delist any files marked for delisting */
+          to = 0;
+          for (f = 0; f < groups[g].filecount; ++f)
+            if (groups[g].files[f].action != -2)
+              groups[g].files[to++] = groups[g].files[f];
+
+          groups[g].filecount = to;
+
+          /* reposition cursor, if necessary */
+          if (cursorgroup == g && cursorfile > 0 && cursorfile >= groups[g].filecount)
+            cursorfile = groups[g].filecount - 1;
+        }
+
+        if (loginfo != 0)
+          log_close(loginfo);
+
+        if (deletedbytes < 1000.0)
+          format_status_left(status, L"Deleted %ld files (occupying %.0f bytes).", totaldeleted, deletedbytes);
+        else if (deletedbytes <= (1000.0 * 1000.0))
+          format_status_left(status, L"Deleted %ld files (occupying %.1f KB).", totaldeleted, deletedbytes / 1000.0);
+        else if (deletedbytes <= (1000.0 * 1000.0 * 1000.0))
+          format_status_left(status, L"Deleted %ld files (occupying %.1f MB).", totaldeleted, deletedbytes / (1000.0 * 1000.0));
+        else
+          format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
+
+        /* delist empty groups */
+        to = 0;
+        for (g = 0; g < totalgroups; ++g)
+        {
+          if (groups[g].filecount > 0)
+          {
+            groups[to] = groups[g];
+
+            /* reposition cursor, if necessary */
+            if (to == cursorgroup && to != g)
+              cursorfile = 0;
+
+            ++to;
+          }
+          else
+          {
+            free(groups[g].files);
+          }
+        }
+
+        totalgroups = to;
+
+        /* reposition cursor, if necessary */
+        if (cursorgroup >= totalgroups)
+        {
+          cursorgroup = totalgroups - 1;
+          cursorfile = 0;
+        }
+
+        /* recalculate line boundaries */
+        adjusttopline = 1;
+        toplineoffset = 0;
+        groupfirstline = 0;
+
+        for (g = 0; g < totalgroups; ++g)
+        {
+          if (adjusttopline && groups[g].endline >= topline)
+            toplineoffset = groups[g].endline - topline;
+
+          groups[g].startline = groupfirstline;
+          groups[g].endline = groupfirstline + 2;
+
+          for (f = 0; f < groups[g].filecount; ++f)
+            groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
+
+          if (adjusttopline && toplineoffset > 0)
+          {
+            topline = groups[g].endline - toplineoffset;
+
+            if (topline < 0)
+              topline = 0;
+
+            adjusttopline = 0;
+          }
+
+          groupfirstline = groups[g].endline + 1;
+        }
+
+        if (totalgroups > 0 && groups[totalgroups-1].endline <= topline)
+        {
+          topline = groups[totalgroups-1].endline - getmaxy(filewin) + 1;
+
+          if (topline < 0)
+            topline = 0;
+        }
+
+        break;
+
+      case KEY_RESIZE:
+        /* resize windows */
+        wresize(filewin, LINES - 2, COLS);
+
+        wresize(statuswin, 1, COLS);
+        wresize(promptwin, 1, COLS);
+        mvwin(statuswin, LINES - 1, 0);
+        mvwin(promptwin, LINES - 2, 0);
+
+        status_text_alloc(status, COLS);
+
+        /* recalculate line boundaries */
+        groupfirstline = 0;
+
+        for (g = 0; g < totalgroups; ++g)
+        {
+          groups[g].startline = groupfirstline;
+          groups[g].endline = groupfirstline + 2;
+
+          for (f = 0; f < groups[g].filecount; ++f)
+            groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
+
+          groupfirstline = groups[g].endline + 1;
+        }
+
+        break;
+      }
+    }
+    else if (keyresult == OK)
+    {
+      switch (wch)
+      {
+      case '?':
+        if (groups[cursorgroup].files[cursorfile].action == 0)
+          break;
+
+        set_file_action(&groups[cursorgroup].files[cursorfile], 0, &globaldeletiontally);
+
+        if (cursorfile < groups[cursorgroup].filecount - 1)
+          move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
+        else if (cursorgroup < totalgroups - 1)
+          move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+
+      case '\n':
+        deletecount = 0;
+        preservecount = 0;
+
+        for (x = 0; x < groups[cursorgroup].filecount; ++x)
+        {
+          if (groups[cursorgroup].files[x].action == 1)
+            ++preservecount;
+        }
+
+        if (preservecount == 0)
+          break;
+
+        for (x = 0; x < groups[cursorgroup].filecount; ++x)
+        {
+          if (groups[cursorgroup].files[x].action == 0)
+            set_file_action(&groups[cursorgroup].files[x], -1, &globaldeletiontally);
+
+          if (groups[cursorgroup].files[x].action == -1)
+            ++deletecount;
+        }
+
+        if (cursorgroup < totalgroups - 1 && deletecount < groups[cursorgroup].filecount)
+          move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+
+      case '\t':
+        if (cursorgroup < totalgroups - 1)
+          move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
+
+        break;
+      }
+    }
+  } while (doprune);
+
+  endwin();
+
+  free(commandbuffer);
+
+  free_prompt_info(prompt);
+
+  free_status_text(status);
+
+  free_command_identifier_tree(commandidentifier);
+  free_command_identifier_tree(confirmationkeywordidentifier);
+
+  for (g = 0; g < totalgroups; ++g)
+    free(groups[g].files);
+
+  free(groups);
+}
diff --git a/ncurses-interface.h b/ncurses-interface.h
new file mode 100644 (file)
index 0000000..b61cc26
--- /dev/null
@@ -0,0 +1,29 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESINTERFACE_H
+#define NCURSESINTERFACE_H
+
+#include "fdupes.h"
+
+void deletefiles_ncurses(file_t *files, char *logfile);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-print.c b/ncurses-print.c
new file mode 100644 (file)
index 0000000..2a877bd
--- /dev/null
@@ -0,0 +1,157 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include <stdlib.h>
+#include <wchar.h>
+#include "ncurses-print.h"
+#include "errormsg.h"
+#include "mbstowcs_escape_invalid.h"
+#include "positive_wcwidth.h"
+
+void putline(WINDOW *window, const char *str, const int line, const int columns, const int compensate_indent)
+{
+  wchar_t *dest = 0;
+  int inputlength;
+  int linestart;
+  int linelength;
+  int linewidth;
+  int first_line_columns;
+  int l;
+
+  inputlength = mbstowcs_escape_invalid(0, str, 0);
+  if (inputlength == 0)
+    return;
+
+  dest = (wchar_t *) malloc((inputlength + 1) * sizeof(wchar_t));
+  if (dest == NULL)
+  {
+    endwin();
+    errormsg("out of memory\n");
+    exit(1);
+  }
+
+  mbstowcs_escape_invalid(dest, str, inputlength);
+  dest[inputlength] = L'\0';
+
+  first_line_columns = columns - compensate_indent;
+
+  linestart = 0;
+
+  if (line > 0)
+  {
+    linewidth = positive_wcwidth(dest[linestart]);
+
+    while (linestart + 1 < inputlength && linewidth + positive_wcwidth(dest[linestart + 1]) <= first_line_columns)
+      linewidth += positive_wcwidth(dest[++linestart]);
+
+    if (++linestart == inputlength)
+      return;
+
+    for (l = 1; l < line; ++l)
+    {
+      linewidth = positive_wcwidth(dest[linestart]);
+
+      while (linestart + 1 < inputlength && linewidth + positive_wcwidth(dest[linestart + 1]) <= columns)
+        linewidth += positive_wcwidth(dest[++linestart]);
+
+      if (++linestart == inputlength)
+        return;
+    }
+  }
+
+  linewidth = positive_wcwidth(dest[linestart]);
+  linelength = 1;
+
+  if (line == 0)
+  {
+    while (linestart + linelength < inputlength && linewidth + positive_wcwidth(dest[linestart + linelength]) <= first_line_columns)
+    {
+      linewidth += positive_wcwidth(dest[linestart + linelength]);
+      ++linelength;
+    }
+  }
+  else
+  {
+    while (linestart + linelength < inputlength && linewidth + positive_wcwidth(dest[linestart + linelength]) <= columns)
+    {
+      linewidth += positive_wcwidth(dest[linestart + linelength]);
+      ++linelength;
+    }    
+  }
+
+  waddnwstr(window, dest + linestart, linelength);
+
+  free(dest);
+}
+
+void print_spaces(WINDOW *window, int spaces)
+{
+  int x;
+
+  for (x = 0; x < spaces; ++x)
+    waddch(window, L' ');
+}
+
+void print_right_justified_int(WINDOW *window, int number, int width)
+{
+  int length;
+
+  length = get_num_digits(number);
+  if (number < 0)
+    ++length;
+
+  if (length < width)
+    print_spaces(window, width - length);
+
+  wprintw(window, "%d", number);
+}
+
+int vwprintflength(const wchar_t *format, va_list args)
+{
+  FILE *fp;
+  int size;
+
+  fp = fopen("/dev/null", "w");
+  if (fp == 0)
+    return 0;
+
+  size = vfwprintf(fp, format, args);
+
+  fclose(fp);
+
+  return size;
+}
+
+int get_num_digits(int value)
+{
+  int digits = 0;
+
+  if (value < 0)
+    value = -value;
+
+  do {
+    value /= 10;
+    ++digits;
+  } while (value > 0);
+
+  return digits;
+}
diff --git a/ncurses-print.h b/ncurses-print.h
new file mode 100644 (file)
index 0000000..3b0474e
--- /dev/null
@@ -0,0 +1,39 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESPRINT_H
+#define NCURSESPRINT_H
+
+#include <wchar.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+#include <stdarg.h>
+
+void putline(WINDOW *window, const char *str, const int line, const int columns, const int compensate_indent);
+void print_spaces(WINDOW *window, int spaces);
+void print_right_justified_int(WINDOW *window, int number, int width);
+int vwprintflength(const wchar_t *format, va_list args);
+int get_num_digits(int value);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-prompt.c b/ncurses-prompt.c
new file mode 100644 (file)
index 0000000..0db22ac
--- /dev/null
@@ -0,0 +1,149 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include <stdlib.h>
+#include "ncurses-print.h"
+#include "ncurses-prompt.h"
+
+struct prompt_info *prompt_info_alloc(size_t initial_size)
+{
+  struct prompt_info *out;
+
+  if (initial_size < 1)
+    return 0;
+
+  out = (struct prompt_info*) malloc(sizeof(struct prompt_info));
+  if (out == 0)
+    return 0;
+
+  out->text = malloc(initial_size * sizeof(wchar_t));
+  if (out->text == 0)
+  {
+    free(out);
+    return 0;
+  }
+
+  out->text[0] = L'\0';
+  out->allocated = initial_size;
+  out->offset = 0;
+  out->cursor = 0;
+  out->active = 0;
+
+  return out;
+}
+
+void free_prompt_info(struct prompt_info *info)
+{
+  free(info->text);
+  free(info);
+}
+
+int format_prompt(struct prompt_info *prompt, wchar_t *format, ...)
+{
+  va_list ap;
+  va_list aq;
+  int size;
+  wchar_t *newtext;
+
+  va_start(ap, format);
+  va_copy(aq, ap);
+
+  size = vwprintflength(format, aq);
+
+  if (size + 3 > prompt->allocated)
+  {
+    newtext = (wchar_t*)realloc(prompt->text, (size + 3) * sizeof(wchar_t));
+
+    if (newtext == 0)
+      return 0;
+
+    prompt->text = newtext;
+    prompt->allocated = size + 1;
+  }
+
+  vswprintf(prompt->text, prompt->allocated, format, ap);
+
+  size = wcslen(prompt->text);
+
+  prompt->text[size + 0] = L':';
+  prompt->text[size + 1] = L' ';
+  prompt->text[size + 2] = L'\0';
+
+  va_end(aq);
+  va_end(ap);
+
+  return 1;
+}
+
+void set_prompt_active_state(struct prompt_info *prompt, int active)
+{
+  prompt->active = active;
+}
+
+void update_prompt(WINDOW *promptwin, struct prompt_info *prompt, wchar_t *commandbuffer, int cursor_delta)
+{
+  const size_t cursor_stop = wcslen(prompt->text);
+  const size_t right_edge = getmaxx(promptwin);
+  const size_t cursor_position = cursor_stop + prompt->cursor - prompt->offset;
+
+  if (cursor_delta > 0)
+  {
+    if (prompt->cursor + cursor_delta > wcslen(commandbuffer))
+     cursor_delta = wcslen(commandbuffer) - prompt->cursor;
+
+    if (cursor_position + cursor_delta >= right_edge)
+      prompt->offset += cursor_delta;
+  }
+  else if (cursor_delta < 0)
+  {
+    if (-cursor_delta > prompt->cursor)
+      cursor_delta = -(int)prompt->cursor;
+
+    if (cursor_position + cursor_delta < cursor_stop)
+      prompt->offset += cursor_delta;
+  }
+
+  prompt->cursor += cursor_delta;
+}
+
+void print_prompt(WINDOW *promptwin, struct prompt_info *prompt, wchar_t *commandbuffer)
+{
+  if (prompt->active)
+    prompt->text[wcslen(prompt->text) - 2] = '=';
+  else
+    prompt->text[wcslen(prompt->text) - 2] = ':';
+
+  werase(promptwin);
+
+  if (prompt->offset <= wcslen(prompt->text))
+  {
+    wattron(promptwin, A_BOLD);
+    waddwstr(promptwin, prompt->text + prompt->offset);
+    wattroff(promptwin, A_BOLD);
+
+    waddwstr(promptwin, commandbuffer);
+  }
+  else if (prompt->offset < wcslen(prompt->text) + wcslen(commandbuffer))
+  {
+    waddwstr(promptwin, commandbuffer + prompt->offset - wcslen(prompt->text));
+  }
+}
\ No newline at end of file
diff --git a/ncurses-prompt.h b/ncurses-prompt.h
new file mode 100644 (file)
index 0000000..d67afdd
--- /dev/null
@@ -0,0 +1,48 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESPROMPT_H
+#define NCURSESPROMPT_H
+
+#include <wchar.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+
+struct prompt_info
+{
+  wchar_t *text;
+  size_t allocated;
+  size_t offset;
+  size_t cursor;
+  int active;
+};
+
+struct prompt_info *prompt_info_alloc(size_t initial_size);
+void free_prompt_info(struct prompt_info *info);
+int format_prompt(struct prompt_info *prompt, wchar_t *format, ...);
+void set_prompt_active_state(struct prompt_info *prompt, int active);
+void update_prompt(WINDOW *promptwin, struct prompt_info *prompt, wchar_t *commandbuffer, int cursor_delta);
+void print_prompt(WINDOW *promptwin, struct prompt_info *prompt, wchar_t *commandbuffer);
+
+#endif
\ No newline at end of file
diff --git a/ncurses-status.c b/ncurses-status.c
new file mode 100644 (file)
index 0000000..a209949
--- /dev/null
@@ -0,0 +1,155 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#include "config.h"
+#include <stdlib.h>
+#include "ncurses-print.h"
+#include "ncurses-status.h"
+
+struct status_text *status_text_alloc(struct status_text *status, size_t width)
+{
+  struct status_text *result;
+  wchar_t *newleft;
+  wchar_t *newright;
+
+  if (status == 0)
+  {
+    result = (struct status_text*) malloc(sizeof(struct status_text));
+    if (result == 0)
+      return 0;
+
+    result->left = (wchar_t*) malloc((width+1) * sizeof(wchar_t));
+    if (result->left == 0)
+    {
+      free(result);
+      return 0;
+    }
+
+    result->right = (wchar_t*) malloc((width+1) * sizeof(wchar_t));
+    if (result->right == 0)
+    {
+      free(result->left);
+      free(result);
+      return 0;
+    }
+
+    result->left[0] = '\0';
+    result->right[0] = '\0';
+
+    result->width = width;
+  }
+  else
+  {
+    if (status->width >= width)
+      return status;
+
+    newleft = (wchar_t*) realloc(status->left, (width+1) * sizeof(wchar_t));
+    if (newleft == 0)
+      return 0;
+
+    newright = (wchar_t*) realloc(status->right, (width+1) * sizeof(wchar_t));
+    if (newright == 0)
+    {
+      free(newleft);
+      return 0;
+    }
+
+    result = status;
+    result->left = newleft;
+    result->right = newright;
+    result->width = width;
+  }
+
+  return result;
+}
+
+void free_status_text(struct status_text *status)
+{
+  free(status->left);
+  free(status->right);
+  free(status);
+}
+
+void format_status_left(struct status_text *status, wchar_t *format, ...)
+{
+  va_list ap;
+  va_list aq;
+  int size;
+
+  va_start(ap, format);
+  va_copy(aq, ap);
+
+  size = vwprintflength(format, aq);
+
+  status_text_alloc(status, size);
+
+  vswprintf(status->left, status->width + 1, format, ap);
+
+  va_end(aq);
+  va_end(ap);
+}
+
+void format_status_right(struct status_text *status, wchar_t *format, ...)
+{
+  va_list ap;
+  va_list aq;
+  int size;
+
+  va_start(ap, format);
+  va_copy(aq, ap);
+
+  size = vwprintflength(format, aq);
+
+  status_text_alloc(status, size);
+
+  vswprintf(status->right, status->width + 1, format, ap);
+
+  va_end(aq);
+  va_end(ap);
+}
+
+void print_status(WINDOW *statuswin, struct status_text *status)
+{
+  wchar_t *text;
+  size_t cols;
+  size_t x;
+  size_t l;
+
+  cols = getmaxx(statuswin);
+
+  text = (wchar_t*)malloc((cols + 1) * sizeof(wchar_t));
+
+  l = wcslen(status->left);
+  for (x = 0; x < l && x < cols; ++x)
+    text[x] = status->left[x];
+  for (x = l; x < cols; ++x)
+    text[x] = L' ';
+
+  l = wcslen(status->right);
+  for (x = cols; x >= 1 && l >= 1; --x, --l)
+    text[x - 1] = status->right[l - 1];
+
+  text[cols] = L'\0';
+
+  mvwaddnwstr(statuswin, 0, 0, text, wcslen(text));
+
+  free(text);
+}
\ No newline at end of file
diff --git a/ncurses-status.h b/ncurses-status.h
new file mode 100644 (file)
index 0000000..70b37dd
--- /dev/null
@@ -0,0 +1,45 @@
+/* FDUPES Copyright (c) 2018 Adrian Lopez
+
+   Permission is hereby granted, free of charge, to any person
+   obtaining a copy of this software and associated documentation files
+   (the "Software"), to deal in the Software without restriction,
+   including without limitation the rights to use, copy, modify, merge,
+   publish, distribute, sublicense, and/or sell copies of the Software,
+   and to permit persons to whom the Software is furnished to do so,
+   subject to the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+
+#ifndef NCURSESSTATUS_H
+#define NCURSESSTATUS_H
+
+#include <wchar.h>
+#ifdef HAVE_NCURSESW_CURSES_H
+  #include <ncursesw/curses.h>
+#else
+  #include <curses.h>
+#endif
+
+struct status_text
+{
+  wchar_t *left;
+  wchar_t *right;
+  size_t width;
+};
+
+struct status_text *status_text_alloc(struct status_text *status, size_t width);
+void free_status_text(struct status_text *status);
+void format_status_left(struct status_text *status, wchar_t *format, ...);
+void format_status_right(struct status_text *status, wchar_t *format, ...);
+void print_status(WINDOW *statuswin, struct status_text *status);
+
+#endif
\ No newline at end of file
diff --git a/positive_wcwidth.c b/positive_wcwidth.c
new file mode 100644 (file)
index 0000000..64ed385
--- /dev/null
@@ -0,0 +1,32 @@
+/* Copyright (c) 2019 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include "config.h"
+#include "positive_wcwidth.h"
+
+int positive_wcwidth(wchar_t ch)
+{
+  int w;
+
+  w = wcwidth(ch);
+
+  if (w >= 0)
+    return w;
+  else
+    return 1;
+}
\ No newline at end of file
diff --git a/positive_wcwidth.h b/positive_wcwidth.h
new file mode 100644 (file)
index 0000000..fd012c0
--- /dev/null
@@ -0,0 +1,26 @@
+/* Copyright (c) 2019 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef POSITIVE_WCWIDTH_H
+#define POSITIVE_WCWIDTH_H
+
+#include <wchar.h>
+
+int positive_wcwidth(wchar_t ch);
+
+#endif
\ No newline at end of file
diff --git a/sigint.c b/sigint.c
new file mode 100644 (file)
index 0000000..1d9a481
--- /dev/null
+++ b/sigint.c
@@ -0,0 +1,37 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <string.h>
+#include "sigint.h"
+
+volatile sig_atomic_t got_sigint = 0;
+
+void sigint_handler(int signal)
+{
+  got_sigint = 1;
+}
+
+void register_sigint_handler()
+{
+  struct sigaction action;
+
+  memset(&action, 0, sizeof(struct sigaction));
+
+  action.sa_handler = sigint_handler;
+  sigaction(SIGINT, &action, 0);       
+}
\ No newline at end of file
diff --git a/sigint.h b/sigint.h
new file mode 100644 (file)
index 0000000..8ceb6b3
--- /dev/null
+++ b/sigint.h
@@ -0,0 +1,28 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef SIGINT_H
+#define SIGINT_H
+
+#include <signal.h>
+
+extern volatile sig_atomic_t got_sigint;
+
+void register_sigint_handler();
+
+#endif
\ No newline at end of file
diff --git a/wcs.c b/wcs.c
new file mode 100644 (file)
index 0000000..0a68d19
--- /dev/null
+++ b/wcs.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#include <stdlib.h>
+#include "wcs.h"
+#include "mbstowcs_escape_invalid.h"
+
+/* compare wide and multibyte strings */
+int wcsmbcscmp(wchar_t *s1, char *s2)
+{
+  wchar_t *s2w;
+  size_t size;
+  int result;
+
+  size = mbstowcs_escape_invalid(0, s2, 0) + 1;
+
+  s2w = (wchar_t*) malloc(size * sizeof(wchar_t));
+  if (s2w == 0)
+    return -1;
+
+  mbstowcs_escape_invalid(s2w, s2, size);
+
+  result = wcscmp(s1, s2w);
+
+  free(s2w);
+
+  return result;
+}
+
+/* wide character needle is contained in multibyte haystack */
+int wcsinmbcs(char *haystack, wchar_t *needle)
+{
+  wchar_t *haystackw;
+  size_t size;
+  int result;
+
+  size = mbstowcs_escape_invalid(0, haystack, 0) + 1;
+
+  haystackw = (wchar_t*) malloc(size * sizeof(wchar_t));
+  if (haystackw == 0)
+    return -1;
+
+  mbstowcs_escape_invalid(haystackw, haystack, size);
+
+  if (wcsstr(haystackw, needle) == 0)
+    result = 0;
+  else
+    result = 1;
+
+  free(haystackw);
+
+  return result;
+}
+
+/* wide character needle at beginning of multibyte haystack */
+int wcsbeginmbcs(char *haystack, wchar_t *needle)
+{
+  wchar_t *haystackw;
+  size_t size;
+  int result;
+
+  size = mbstowcs_escape_invalid(0, haystack, 0);
+
+  haystackw = (wchar_t*) malloc(size * sizeof(wchar_t));
+  if (haystackw == 0)
+    return -1;
+
+  mbstowcs_escape_invalid(haystackw, haystack, size);
+
+  if (wcsncmp(haystackw, needle, wcslen(needle)) == 0)
+    result = 1;
+  else
+    result = 0;
+
+  free(haystackw);
+
+  return result;
+}
+
+/* wide character needle at end of multibyte haystack */
+int wcsendsmbcs(char *haystack, wchar_t *needle)
+{
+  wchar_t *haystackw;
+  size_t size;
+  int result;
+
+  size = mbstowcs_escape_invalid(0, haystack, 0) + 1;
+
+  haystackw = (wchar_t*) malloc(size * sizeof(wchar_t));
+  if (haystackw == 0)
+    return -1;
+
+  mbstowcs_escape_invalid(haystackw, haystack, size);
+
+  if (wcsrstr(haystackw, needle) != 0 && wcscmp(wcsrstr(haystackw, needle), needle) == 0)
+    result = 1;
+  else
+    result = 0;
+
+  free(haystackw);
+
+  return result;
+}
+
+/* wide character reverse string search */
+wchar_t *wcsrstr(wchar_t *haystack, wchar_t *needle)
+{
+  wchar_t *found = 0;
+  wchar_t *next = 0;
+
+  found = wcsstr(haystack, needle);
+  if (found)
+  {
+    do {
+      next = wcsstr(found + 1, needle);
+      if (next != 0)
+        found = next;
+    } while (next);
+
+    return found;
+  }
+
+  return 0;
+}
\ No newline at end of file
diff --git a/wcs.h b/wcs.h
new file mode 100644 (file)
index 0000000..2065b9c
--- /dev/null
+++ b/wcs.h
@@ -0,0 +1,30 @@
+/* Copyright (c) 2018 Adrian Lopez
+
+   This software is provided 'as-is', without any express or implied
+   warranty. In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution. */
+
+#ifndef WCS_H
+#define WCS_H
+
+#include <wchar.h>
+
+int wcsmbcscmp(wchar_t *s1, char *s2);
+int wcsinmbcs(char *haystack, wchar_t *needle);
+int wcsbeginmbcs(char *haystack, wchar_t *needle);
+int wcsendsmbcs(char *haystack, wchar_t *needle);
+wchar_t *wcsrstr(wchar_t *haystack, wchar_t *needle);
+
+#endif
\ No newline at end of file