blob: 3a2dbcc000dab4dfc5637bd8ed6fd22c8ba9cfe0 [file] [log] [blame]
/*
* Copyright 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#define G_LOG_DOMAIN "FuProgressBar"
#include "config.h"
#include <glib/gi18n.h>
#include <stdio.h>
#ifdef HAVE_READLINE
#include <readline/history.h>
#include <readline/readline.h>
#endif
#include "fu-console.h"
#ifdef _WIN32
#include <wchar.h>
#include <windows.h>
#endif
struct _FuConsole {
GObject parent_instance;
GMainContext *main_ctx;
FwupdStatus status;
gboolean spinner_count_up; /* width in visible chars */
guint spinner_idx; /* width in visible chars */
guint length_percentage; /* width in visible chars */
guint length_status; /* width in visible chars */
guint percentage;
GSource *timer_source;
gint64 last_animated; /* monotonic */
GTimer *time_elapsed;
gdouble last_estimate;
gboolean interactive;
gboolean contents_to_clear;
};
G_DEFINE_TYPE(FuConsole, fu_console, G_TYPE_OBJECT)
gboolean
fu_console_setup(FuConsole *self, GError **error)
{
#ifdef _WIN32
HANDLE out;
DWORD mode = 0;
/* enable VT sequences */
out = GetStdHandle(STD_OUTPUT_HANDLE);
if (out == INVALID_HANDLE_VALUE) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to get stdout [%u]",
(guint)GetLastError());
return FALSE;
}
if (!GetConsoleMode(out, &mode)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to get mode [%u]",
(guint)GetLastError());
return FALSE;
}
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (!SetConsoleMode(out, mode)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to set mode [%u]",
(guint)GetLastError());
return FALSE;
}
if (!SetConsoleOutputCP(CP_UTF8)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to set output UTF-8 [%u]",
(guint)GetLastError());
return FALSE;
}
if (!SetConsoleCP(CP_UTF8)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to set UTF-8 [%u]",
(guint)GetLastError());
return FALSE;
}
#else
if (isatty(fileno(stdin)) == 0) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a TTY");
return FALSE;
}
if (isatty(fileno(stdout)) == 0) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a TTY");
return FALSE;
}
#endif
/* success */
return TRUE;
}
static void
fu_console_erase_line(FuConsole *self)
{
if (!self->interactive)
return;
g_print("\033[G");
}
static void
fu_console_reset_line(FuConsole *self)
{
if (self->contents_to_clear) {
fu_console_erase_line(self);
g_print("\n");
self->contents_to_clear = FALSE;
}
}
void
fu_console_print_kv(FuConsole *self, const gchar *title, const gchar *msg)
{
gsize title_len;
g_auto(GStrv) lines = NULL;
if (msg == NULL)
return;
fu_console_reset_line(self);
g_print("%s:", title);
/* pad */
title_len = fu_strwidth(title) + 1;
lines = g_strsplit(msg, "\n", -1);
for (guint j = 0; lines[j] != NULL; j++) {
for (gsize i = title_len; i < 25; i++)
g_print(" ");
g_print("%s\n", lines[j]);
title_len = 0;
}
}
#ifndef HAVE_READLINE
static gchar *
readline(const gchar *prompt) /* nocheck:name */
{
char buffer[64] = {0};
if (prompt != NULL)
g_print("%s\n", prompt);
if (!fgets(buffer, sizeof(buffer), stdin))
return NULL;
g_strdelimit(buffer, "\n", '\0');
return g_strndup(buffer, sizeof(buffer));
}
#endif
guint
fu_console_input_uint(FuConsole *self, guint maxnum, const gchar *format, ...)
{
gint retval;
guint answer = 0;
va_list args;
g_autofree gchar *tmp = NULL;
g_autofree gchar *prompt = NULL;
va_start(args, format);
tmp = g_strdup_vprintf(format, args);
va_end(args);
fu_console_print_full(self, FU_CONSOLE_PRINT_FLAG_NONE, "%s [0-%u]: ", tmp, maxnum);
do {
g_autofree gchar *buffer = readline(prompt);
if (buffer == NULL)
break;
/* get a number */
retval = sscanf(buffer, "%u", &answer);
/* positive */
if (retval == 1 && answer <= maxnum)
break;
if (prompt == NULL) {
g_autoptr(GString) str = g_string_new(NULL);
g_string_append_printf(
str,
/* TRANSLATORS: the user isn't reading the question */
_("Please enter a number from 0 to %u:"),
maxnum);
g_string_append(str, " ");
prompt = g_string_free(g_steal_pointer(&str), FALSE);
}
} while (TRUE);
return answer;
}
gboolean
fu_console_input_bool(FuConsole *self, gboolean def, const gchar *format, ...)
{
va_list args;
g_autofree gchar *tmp = NULL;
g_autofree gchar *prompt = NULL;
va_start(args, format);
tmp = g_strdup_vprintf(format, args);
va_end(args);
fu_console_print_full(self,
FU_CONSOLE_PRINT_FLAG_NONE,
"%s [%s]: ",
tmp,
def ? "Y|n" : "y|N");
do {
g_autofree gchar *buffer = readline(prompt);
if (buffer == NULL || !strlen(buffer))
return def;
buffer[0] = g_ascii_toupper(buffer[0]);
if (g_strcmp0(buffer, "Y") == 0)
return TRUE;
if (g_strcmp0(buffer, "N") == 0)
return FALSE;
if (prompt == NULL) {
g_autoptr(GString) str = g_string_new(NULL);
g_string_append_printf(str,
/* TRANSLATORS: the user isn't reading the question
-- %1 is 'Y' and %2 is 'N' */
_("Please enter either %s or %s:"),
"Y",
"N");
g_string_append(str, " ");
prompt = g_string_free(g_steal_pointer(&str), FALSE);
}
} while (TRUE);
return FALSE;
}
static GPtrArray *
fu_console_strsplit_words(const gchar *text, guint line_len)
{
g_auto(GStrv) tokens = NULL;
g_autoptr(GPtrArray) lines = g_ptr_array_new_with_free_func(g_free);
g_autoptr(GString) curline = g_string_new(NULL);
/* sanity check */
if (text == NULL || text[0] == '\0')
return NULL;
if (line_len == 0)
return NULL;
/* tokenize the string */
tokens = g_strsplit(text, " ", -1);
for (guint i = 0; tokens[i] != NULL; i++) {
/* current line plus new token is okay */
if (curline->len + fu_strwidth(tokens[i]) < line_len) {
g_string_append_printf(curline, "%s ", tokens[i]);
continue;
}
/* too long, so remove space, add newline and dump */
if (curline->len > 0)
g_string_truncate(curline, curline->len - 1);
g_ptr_array_add(lines, g_strdup(curline->str));
g_string_truncate(curline, 0);
g_string_append_printf(curline, "%s ", tokens[i]);
}
/* any incomplete line? */
if (curline->len > 0) {
g_string_truncate(curline, curline->len - 1);
g_ptr_array_add(lines, g_strdup(curline->str));
}
return g_steal_pointer(&lines);
}
static void
fu_console_box_line(const gchar *start,
const gchar *text,
const gchar *end,
const gchar *padding,
guint width)
{
guint offset = 0;
if (start != NULL) {
offset += fu_strwidth(start);
g_print("%s", start);
}
if (text != NULL) {
offset += fu_strwidth(text);
g_print("%s", text);
}
if (end != NULL)
offset += fu_strwidth(end);
for (guint i = offset; i < width; i++)
g_print("%s", padding);
if (end != NULL)
g_print("%s\n", end);
}
void
fu_console_line(FuConsole *self, guint width)
{
g_autoptr(GString) str = g_string_new_len(NULL, width);
for (guint i = 0; i < width; i++)
g_string_append(str, "─");
fu_console_print_literal(self, str->str);
}
void
fu_console_box(FuConsole *self, const gchar *title, const gchar *body, guint width)
{
/* nothing to do */
if (title == NULL && body == NULL)
return;
/* header */
fu_console_reset_line(self);
fu_console_box_line("╔", NULL, "╗", "═", width);
/* optional title */
if (title != NULL) {
g_autoptr(GPtrArray) lines = fu_console_strsplit_words(title, width - 4);
for (guint j = 0; j < lines->len; j++) {
const gchar *line = g_ptr_array_index(lines, j);
fu_console_box_line("║ ", line, " ║", " ", width);
}
}
/* join */
if (title != NULL && body != NULL)
fu_console_box_line("╠", NULL, "╣", "═", width);
/* optional body */
if (body != NULL) {
gboolean has_nonempty = FALSE;
g_auto(GStrv) split = g_strsplit(body, "\n", -1);
for (guint i = 0; split[i] != NULL; i++) {
g_autoptr(GPtrArray) lines = fu_console_strsplit_words(split[i], width - 4);
if (lines == NULL) {
if (has_nonempty) {
fu_console_box_line("║ ", NULL, " ║", " ", width);
has_nonempty = FALSE;
}
continue;
}
for (guint j = 0; j < lines->len; j++) {
const gchar *line = g_ptr_array_index(lines, j);
fu_console_box_line("║ ", line, " ║", " ", width);
}
has_nonempty = TRUE;
}
}
/* footer */
fu_console_box_line("╚", NULL, "╝", "═", width);
}
static const gchar *
fu_console_status_to_string(FwupdStatus status)
{
switch (status) {
case FWUPD_STATUS_IDLE:
/* TRANSLATORS: daemon is inactive */
return _("Idle…");
break;
case FWUPD_STATUS_DECOMPRESSING:
/* TRANSLATORS: decompressing the firmware file */
return _("Decompressing…");
break;
case FWUPD_STATUS_LOADING:
/* TRANSLATORS: parsing the firmware information */
return _("Loading…");
break;
case FWUPD_STATUS_DEVICE_RESTART:
/* TRANSLATORS: restarting the device to pick up new F/W */
return _("Restarting device…");
break;
case FWUPD_STATUS_DEVICE_READ:
/* TRANSLATORS: reading from the flash chips */
return _("Reading…");
break;
case FWUPD_STATUS_DEVICE_WRITE:
/* TRANSLATORS: writing to the flash chips */
return _("Writing…");
break;
case FWUPD_STATUS_DEVICE_ERASE:
/* TRANSLATORS: erasing contents of the flash chips */
return _("Erasing…");
break;
case FWUPD_STATUS_DEVICE_VERIFY:
/* TRANSLATORS: verifying we wrote the firmware correctly */
return _("Verifying…");
break;
case FWUPD_STATUS_SCHEDULING:
/* TRANSLATORS: scheduling an update to be done on the next boot */
return _("Scheduling…");
break;
case FWUPD_STATUS_DOWNLOADING:
/* TRANSLATORS: downloading from a remote server */
return _("Downloading…");
break;
case FWUPD_STATUS_WAITING_FOR_AUTH:
/* TRANSLATORS: waiting for user to authenticate */
return _("Authenticating…");
break;
case FWUPD_STATUS_DEVICE_BUSY:
case FWUPD_STATUS_WAITING_FOR_USER:
/* TRANSLATORS: waiting for device to do something */
return _("Waiting…");
break;
default:
break;
}
/* TRANSLATORS: current daemon status is unknown */
return _("Unknown");
}
static gboolean
_fu_status_is_predictable(FwupdStatus status)
{
if (status == FWUPD_STATUS_DEVICE_ERASE)
return TRUE;
if (status == FWUPD_STATUS_DEVICE_VERIFY)
return TRUE;
if (status == FWUPD_STATUS_DEVICE_READ)
return TRUE;
if (status == FWUPD_STATUS_DEVICE_WRITE)
return TRUE;
if (status == FWUPD_STATUS_DOWNLOADING)
return TRUE;
return FALSE;
}
static gboolean
fu_console_estimate_ready(FuConsole *self, guint percentage)
{
gdouble old;
gdouble elapsed;
/* now invalid */
if (percentage == 0 || percentage == 100) {
g_timer_start(self->time_elapsed);
self->last_estimate = 0;
return FALSE;
}
/* allow-list things that make sense... */
if (!_fu_status_is_predictable(self->status))
return FALSE;
old = self->last_estimate;
elapsed = g_timer_elapsed(self->time_elapsed, NULL);
self->last_estimate = elapsed / percentage * (100 - percentage);
/* estimate is ready if we have decreased */
return old > self->last_estimate;
}
static gchar *
fu_console_time_remaining_str(FuConsole *self)
{
/* less than 5 seconds remaining */
if (self->last_estimate < 5)
return NULL;
/* less than 60 seconds remaining */
if (self->last_estimate < 60) {
/* TRANSLATORS: time remaining for completing firmware flash */
return g_strdup(_("Less than one minute remaining"));
}
return g_strdup_printf(
/* TRANSLATORS: more than a minute */
ngettext("%.0f minute remaining", "%.0f minutes remaining", self->last_estimate / 60),
self->last_estimate / 60);
}
static void
fu_console_refresh(FuConsole *self)
{
const gchar *title;
guint i;
g_autoptr(GString) str = g_string_new(NULL);
/* sanity check */
if (self->status == FWUPD_STATUS_IDLE || self->status == FWUPD_STATUS_UNKNOWN)
return;
/* erase previous line */
fu_console_erase_line(self);
/* add status */
title = fu_console_status_to_string(self->status);
g_string_append(str, title);
for (i = fu_strwidth(str->str); i < self->length_status; i++)
g_string_append_c(str, ' ');
/* add console */
g_string_append(str, "[");
if (self->percentage > 0) {
for (i = 0; i < (self->length_percentage - 1) * self->percentage / 100; i++)
g_string_append_c(str, '*');
for (i = i + 1; i < self->length_percentage; i++)
g_string_append_c(str, ' ');
} else {
const gchar chars[] = {
'-',
'\\',
'|',
'/',
};
for (i = 0; i < self->spinner_idx; i++)
g_string_append_c(str, ' ');
g_string_append_c(str, chars[i / 4 % G_N_ELEMENTS(chars)]);
for (i = i + 1; i < self->length_percentage - 1; i++)
g_string_append_c(str, ' ');
}
g_string_append_c(str, ']');
/* once we have good data show an estimate of time remaining */
if (fu_console_estimate_ready(self, self->percentage)) {
g_autofree gchar *remaining = fu_console_time_remaining_str(self);
if (remaining != NULL)
g_string_append_printf(str, " %s…", remaining);
}
/* dump to screen */
g_print("%s", str->str);
self->contents_to_clear = TRUE;
}
/**
* fu_console_print_full:
* @self: a #FuConsole
* @flags; a #FuConsolePrintFlags, e.g. %FU_CONSOLE_PRINT_FLAG_STDERR
* @text: string
*
* Clears the console, and prints the text.
**/
void
fu_console_print_full(FuConsole *self, FuConsolePrintFlags flags, const gchar *format, ...)
{
va_list args;
g_autoptr(GString) str = g_string_new(NULL);
va_start(args, format);
g_string_append_vprintf(str, format, args);
va_end(args);
if (flags & FU_CONSOLE_PRINT_FLAG_LIST_ITEM)
g_string_prepend(str, " • ");
if (flags & FU_CONSOLE_PRINT_FLAG_WARNING) {
/* TRANSLATORS: this is a prefix on the console */
g_autofree gchar *fmt = fu_console_color_format(_("WARNING"), FU_CONSOLE_COLOR_RED);
g_string_prepend(str, ": ");
g_string_prepend(str, fmt);
flags |= FU_CONSOLE_PRINT_FLAG_STDERR;
}
if (flags & FU_CONSOLE_PRINT_FLAG_NEWLINE)
g_string_append(str, "\n");
fu_console_reset_line(self);
if (flags & FU_CONSOLE_PRINT_FLAG_STDERR) {
g_printerr("%s", str->str);
} else {
g_print("%s", str->str);
}
}
void
fu_console_print_literal(FuConsole *self, const gchar *text)
{
fu_console_reset_line(self);
g_print("%s\n", text);
}
/**
* fu_console_print:
* @self: a #FuConsole
* @text: string
*
* Clears the console, prints the text and prints a newline.
**/
void
fu_console_print(FuConsole *self, const gchar *format, ...)
{
va_list args;
g_autofree gchar *tmp = NULL;
va_start(args, format);
tmp = g_strdup_vprintf(format, args);
va_end(args);
fu_console_print_literal(self, tmp);
}
/**
* fu_console_set_progress_title:
* @self: A #FuConsole
* @title: A string
*
* Sets console title
**/
void
fu_console_set_progress_title(FuConsole *self, const gchar *title)
{
if (!self->interactive)
return;
fu_console_erase_line(self);
g_print("%s\n", title);
fu_console_refresh(self);
}
/**
* fu_console_set_main_context:
* @self: A #FuConsole
* @main_ctx: (nullable): main context
*
* Sets console main context to use for animations.
**/
void
fu_console_set_main_context(FuConsole *self, GMainContext *main_ctx)
{
self->main_ctx = g_main_context_ref(main_ctx);
}
static void
fu_console_spin_inc(FuConsole *self)
{
/* reset */
self->last_animated = g_get_monotonic_time();
/* up to down */
if (self->spinner_count_up) {
if (++self->spinner_idx > self->length_percentage - 3)
self->spinner_count_up = FALSE;
} else {
if (--self->spinner_idx == 0)
self->spinner_count_up = TRUE;
}
}
static gboolean
fu_console_spin_cb(gpointer user_data)
{
FuConsole *self = FU_CONSOLE(user_data);
/* move the spinner index up to down */
fu_console_spin_inc(self);
/* update the terminal */
fu_console_refresh(self);
return G_SOURCE_CONTINUE;
}
static void
fu_console_spin_end(FuConsole *self)
{
if (self->timer_source != NULL) {
g_source_destroy(self->timer_source);
self->timer_source = NULL;
/* reset when the spinner has been stopped */
g_timer_start(self->time_elapsed);
}
/* go back to the start when we next go into unknown percentage mode */
self->spinner_idx = 0;
self->spinner_count_up = TRUE;
}
static void
fu_console_spin_start(FuConsole *self)
{
if (self->timer_source != NULL)
g_source_destroy(self->timer_source);
self->timer_source = g_timeout_source_new(40);
g_source_set_callback(self->timer_source, fu_console_spin_cb, self, NULL);
g_source_attach(self->timer_source, self->main_ctx);
}
/**
* fu_console_set_progress:
* @self: A #FuConsole
* @status: A #FwupdStatus
* @percentage: unsigned integer
*
* Refreshes the progress bar with the new percentage and status.
**/
void
fu_console_set_progress(FuConsole *self, FwupdStatus status, guint percentage)
{
g_return_if_fail(FU_IS_CONSOLE(self));
/* not useful */
if (status == FWUPD_STATUS_UNKNOWN)
return;
/* ignore duplicates */
if (self->status == status && self->percentage == percentage)
return;
/* cache */
self->status = status;
self->percentage = percentage;
/* dumb */
if (!self->interactive) {
g_printerr("%s: %u%%\n", fu_console_status_to_string(status), percentage);
return;
}
/* if the main loop isn't spinning and we've not had a chance to
* execute the callback just do the refresh now manually */
if (percentage == 0 && status != FWUPD_STATUS_IDLE &&
self->status != FWUPD_STATUS_UNKNOWN) {
if ((g_get_monotonic_time() - self->last_animated) / 1000 > 40) {
fu_console_spin_inc(self);
fu_console_refresh(self);
}
}
/* enable or disable the spinner timeout */
if (percentage > 0) {
fu_console_spin_end(self);
} else {
fu_console_spin_start(self);
}
/* update the terminal */
fu_console_refresh(self);
}
/**
* fu_console_set_interactive:
* @self: A #FuConsole
* @interactive: #gboolean
*
* Marks the console as interactive or not
**/
void
fu_console_set_interactive(FuConsole *self, gboolean interactive)
{
g_return_if_fail(FU_IS_CONSOLE(self));
self->interactive = interactive;
}
/**
* fu_console_set_status_length:
* @self: A #FuConsole
* @len: unsigned integer
*
* Sets the width of the progressbar status, which must be greater that 3.
**/
void
fu_console_set_status_length(FuConsole *self, guint len)
{
g_return_if_fail(FU_IS_CONSOLE(self));
g_return_if_fail(len > 3);
self->length_status = len;
}
/**
* fu_console_set_percentage_length:
* @self: A #FuConsole
* @len: unsigned integer
*
* Sets the width of the progressbar percentage, which must be greater that 3.
**/
void
fu_console_set_percentage_length(FuConsole *self, guint len)
{
g_return_if_fail(FU_IS_CONSOLE(self));
g_return_if_fail(len > 3);
self->length_percentage = len;
}
void
fu_console_beep(FuConsole *self, guint count)
{
for (guint i = 0; i < count; i++) {
if (i > 0)
g_usleep(250000);
g_print("\007");
}
}
static void
fu_console_init(FuConsole *self)
{
self->length_percentage = 40;
self->length_status = 25;
self->spinner_count_up = TRUE;
self->time_elapsed = g_timer_new();
self->interactive = TRUE;
}
static void
fu_console_finalize(GObject *obj)
{
FuConsole *self = FU_CONSOLE(obj);
fu_console_reset_line(self);
if (self->timer_source != 0)
g_source_destroy(self->timer_source);
if (self->main_ctx != NULL)
g_main_context_unref(self->main_ctx);
g_timer_destroy(self->time_elapsed);
G_OBJECT_CLASS(fu_console_parent_class)->finalize(obj);
}
static void
fu_console_class_init(FuConsoleClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fu_console_finalize;
}
/**
* fu_console_new:
*
* Creates a new #FuConsole
**/
FuConsole *
fu_console_new(void)
{
FuConsole *self;
self = g_object_new(FU_TYPE_CONSOLE, NULL);
return FU_CONSOLE(self);
}