/**
* scrotty — Screenshot program for Linux's TTY
*
* Copyright © 2014, 2015 Mattias Andrée (m@maandree.se)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#define _GNU_SOURCE /* For getopt_long. */
#include "common.h"
#include "kern.h"
#include "info.h"
#include "png.h"
#include "pattern.h"
#include
#include
#include
#include
#include
#ifdef USE_GETTEXT
# include
#endif
/**
* X-macro that lists all environment variables
* that indicate that the program is running
* inside a display server.
*/
#define LIST_DISPLAY_VARS X(DISPLAY) X(MDS_DISPLAY) X(MIR_DISPLAY) X(WAYLAND_DISPLAY) X(PREFERRED_DISPLAY)
/**
* `argv[0]` from `main`.
*/
const char *execname;
/**
* If a function fails when it tries to
* open a file, it will set this variable
* point to the pathname of that file.
*/
const char *failure_file = NULL;
/**
* The index of the alternative path-pattern,
* for the framebuffers, to try.
*/
static int try_alt_fbpath = 0;
/**
* Create an image of a framebuffer.
*
* @param fbfd File descriptor for framebuffer device.
* @param imgname The pathname of the output image, `NULL` for piping.
* @param width The width of the image.
* @param height The height of the image.
* @param data Additional data for `convert_fb_to_png`.
* @return Zero on success, -1 on error.
*/
static int
save (int fbfd, const char *imgpath, long width,
long height, void *restrict data)
{
int imgfd = STDOUT_FILENO, piping = (imgpath == NULL);
int saved_errno;
/* Open output file. */
if (!piping)
{
imgfd = open (imgpath, O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (imgfd == -1)
FILE_FAILURE (imgpath);
}
/* Save image. */
if (save_png (fbfd, width, height, imgfd, data) < 0)
goto fail;
close (fbfd);
if (!piping)
close (imgfd);
return 0;
fail:
saved_errno = errno;
if ((imgfd >= 0) && !piping)
close (imgfd);
errno = saved_errno;
return -1;
}
/**
* Run a command for an image
*
* @param flatten_args The arguments to run, 255 delimits the arguments
* @return Zero on success -1 on error
*/
static int
exec_image (char *flatten_args)
{
char **args = NULL;
char *arg;
size_t i, arg_count = 1;
pid_t pid;
int status, saved_errno;
/* Count arguments. */
for (i = 0; flatten_args[i]; i++)
if ((unsigned char)(flatten_args[i]) == 255)
arg_count++;
/* Allocate argument array. */
args = malloc ((arg_count + 1) * sizeof (char*));
if (args == NULL)
goto fail;
/* Unflatten argument array. */
for (arg = flatten_args, i = 0;;)
{
args[i++] = arg;
arg = strchr (arg, 255);
if (arg == NULL)
break;
*arg++ = '\0';
}
args[i] = NULL;
/* Fork process. */
pid = fork ();
if (pid == -1)
goto fail;
/* Child process: */
if (pid == 0)
{
execvp (*args, args);
perror (execname);
_exit (1);
}
/* Parent process: */
/* Wait for child to exit. */
if (waitpid (pid, &status, 0) < 0)
goto fail;
/* Return successfully if and only if `the child` did. */
free (args);
return status == 0 ? 0 : -1;
fail:
saved_errno = errno;
free (args);
errno = saved_errno;
return -1;
}
/**
* Take a screenshot of a framebuffer.
*
* @param fbno The number of the framebuffer.
* @param filepattern The pattern for the filename, `NULL` for piping.
* @param execpattern The pattern for the command to run to
* process the image, `NULL` for none.
* @return Zero on success, -1 on error, 1 if the framebuffer does not exist.
*/
static int
save_fb (int fbno, const char *filepattern, const char *execpattern)
{
char *imgpath = NULL;
char *fbpath; /* Statically allocate string is returned. */
char *execargs = NULL;
long width, height;
void *data = NULL;
int fbfd = -1;
int rc = 0, saved_errno = 0;
/* Get pathname for framebuffer, and stop if we have read all existing ones. */
fbpath = get_fbpath (try_alt_fbpath, fbno);
if (access (fbpath, F_OK))
return 1;
/* Open the framebuffer device for reading. */
fbfd = open (fbpath, O_RDONLY);
if (fbfd == -1)
FILE_FAILURE (fbpath);
/* Get the size of the framebuffer. */
if (measure (fbno, fbfd, &width, &height, &data) < 0)
goto fail;
/* Get output pathname. */
if (filepattern != NULL)
{
imgpath = evaluate (filepattern, fbno, width, height, NULL);
if (imgpath == NULL)
goto fail;
}
/* Take a screenshot of the current framebuffer. */
if (save (fbfd, imgpath, width, height, data) < 0)
goto fail;
if (imgpath)
fprintf (stderr, _("Saved framebuffer %i to %s.\n"), fbno, imgpath);
/* Should we run a command over the image? */
if (execpattern == NULL)
goto done;
/* Get execute arguments. */
execargs = evaluate (execpattern, fbno, width, height, imgpath);
if (execargs == NULL)
goto fail;
/* Run command over image. */
if (exec_image (execargs) < 0)
goto fail;
goto done;
fail:
saved_errno = errno;
rc = -1;
done:
if (fbfd >= 0)
close (fbfd);
free (execargs);
free (imgpath);
return errno = saved_errno, rc;
}
/**
* Take a screenshot of all, or one, framebuffers.
*
* @param filepattern The pattern for the filename, `NULL` for piping.
* @param execpattern The pattern for the command to run to
* process thes image, `NULL` for none.
* @param all All framebuffers?
* @param devno The index of the framebuffer.
* @return Zero on success, -1 on error, 1 if no framebuffer exists.
*/
static
int save_fbs (const char *filepattern, const char *exec, int all, int devno)
{
int r, fbno, found = 0;
int last = all ? INT_MAX : (devno + 1);
retry:
/* Take a screenshot of each framebuffer. */
for (fbno = (all ? 0 : devno); fbno < last; fbno++)
{
r = save_fb (fbno, filepattern, exec);
if (r < 0)
goto fail;
else if (r == 0)
found = 1;
else if (fbno > 0)
break;
else
continue; /* Perhaps framebuffer 1 is the first. */
}
/* Did not find any framebuffer? */
if (found == 0)
{
if (all && (try_alt_fbpath++ < alt_fbpath_limit))
goto retry;
return 1;
}
return 0;
fail:
return -1;
}
/**
* Figure out whether the user is in a display server.
* We will print a warning in `main` if so.
*/
static int
have_display (void)
{
char *env;
#define X(VAR) env = getenv(#VAR); if (env && *env) return 1;
LIST_DISPLAY_VARS
return 0;
}
/**
* Take a screenshot of all framebuffers.
*
* @param argc The number of elements in `argv`.
* @param argv Command line arguments, run with `--help` for more information.
* @return Zero on and only on success.
*/
int
main (int argc, char *argv[])
{
#define EXIT_USAGE(MSG) \
return fprintf (stderr, _("%s: %s. Type '%s --help' for help.\n"), execname, MSG, execname), 2
#define USAGE_ASSERT(ASSERTION, MSG) \
do { if (!(ASSERTION)) EXIT_USAGE (MSG); } while (0)
int r, all = 1, devno = -1;
long devno_;
char *exec = NULL;
char *filepattern = NULL;
char *p;
struct option long_options[] =
{
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'v'},
{"copyright", no_argument, NULL, 'c'},
{"device", required_argument, NULL, 'd'},
{"exec", required_argument, NULL, 'e'},
{NULL, 0, NULL, 0 }
};
/* Set up for internationalisation. */
#if defined(USE_GETTEXT) && defined(PACKAGE) && defined(LOCALEDIR)
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
#endif
/* Parse command line. */
execname = argc ? *argv : "scrotty";
for (;;)
{
r = getopt_long (argc, argv, "hvcd:e:", long_options, NULL);
if (r == -1) break;
else if (r == 'h') return -(print_help ());
else if (r == 'v') return -(print_version ());
else if (r == 'c') return -(print_copyright ());
else if (r == 'd')
{
USAGE_ASSERT (all, _("--device is used twice"));
all = 0;
if (!isdigit (*optarg))
EXIT_USAGE (_("Invalid device number, not a non-negative integer"));
errno = 0;
devno_ = strtol (optarg, &p, 10);
if ((devno_ == 0) && (errno == ERANGE))
devno = -1;
else if (*p)
EXIT_USAGE (_("Invalid device number, not a non-negative integer"));
else
devno = (devno_ >= INT_MAX ? (INT_MAX - 1) : (int)devno_);
}
else if (r == 'e')
{
USAGE_ASSERT (exec == NULL, _("--exec is used twice"));
exec = optarg;
}
else if (r == '?')
EXIT_USAGE (_("Invalid input"));
else
abort ();
}
while (optind < argc)
{
USAGE_ASSERT (filepattern == NULL, _("FILENAME-PATTERN is used twice"));
filepattern = argv[optind++];
}
if (filepattern == NULL)
{
if (isatty(STDOUT_FILENO))
filepattern = "%Y-%m-%d_%H:%M:%S_$wx$h.$i.png";
else
USAGE_ASSERT (exec == NULL, _("--exec cannot be combined with piping"));
}
/* Take a screenshot of each framebuffer. */
r = save_fbs (filepattern, exec, all, devno);
if (r < 0)
goto fail;
if (r > 0)
goto no_fb;
/* Warn about being inside a display server. */
if (have_display ())
fprintf (stderr, _("%s: It looks like you are inside a display server. "
"If this is correct, what you see is probably not "
"what you get.\n"), execname);
return 0;
fail:
if (failure_file != NULL)
fprintf (stderr, _("%s: %s: %s\n"),
execname, strerror (errno), failure_file);
else if (errno)
perror (execname);
return 1;
no_fb:
if (all)
print_not_found_help ();
else
fprintf (stderr, _("%s: The selected device does not exist.\n"),
execname);
return 1;
}