1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include <sys/utsname.h>
8 #include "alloc-util.h"
10 #include "color-util.h"
11 #include "conf-files.h"
12 #include "constants.h"
14 #include "errno-util.h"
19 #include "path-util.h"
20 #include "pretty-print.h"
21 #include "string-util.h"
23 #include "terminal-util.h"
26 void draw_cylon(char buffer
[], size_t buflen
, unsigned width
, unsigned pos
) {
29 assert(buflen
>= CYLON_BUFFER_EXTRA
+ width
+ 1);
30 assert(pos
<= width
+1); /* 0 or width+1 mean that the center light is behind the corner */
34 p
= mempset(p
, ' ', pos
-2);
35 if (log_get_show_color())
36 p
= stpcpy(p
, ANSI_RED
);
40 if (pos
> 0 && pos
<= width
) {
41 if (log_get_show_color())
42 p
= stpcpy(p
, ANSI_HIGHLIGHT_RED
);
46 if (log_get_show_color())
47 p
= stpcpy(p
, ANSI_NORMAL
);
50 if (log_get_show_color())
51 p
= stpcpy(p
, ANSI_RED
);
54 p
= mempset(p
, ' ', width
-1-pos
);
55 if (log_get_show_color())
56 p
= stpcpy(p
, ANSI_NORMAL
);
62 bool urlify_enabled(void) {
64 static int cached_urlify_enabled
= -1;
66 if (cached_urlify_enabled
< 0) {
69 val
= getenv_bool("SYSTEMD_URLIFY");
71 cached_urlify_enabled
= val
;
73 cached_urlify_enabled
= colors_enabled();
76 return cached_urlify_enabled
;
82 static bool url_suitable_for_osc8(const char *url
) {
85 /* Not all URLs are safe for inclusion in OSC 8 due to charset and length restrictions. Let's detect
86 * which ones those are */
88 /* If the URL is longer than 2K let's not try to do OSC 8. As per recommendation in
89 * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits */
90 if (strlen(url
) > 2000)
93 /* OSC sequences may only contain chars from the 32..126 range, as per ECMA-48 */
94 for (const char *c
= url
; *c
; c
++)
95 if (!osc_char_is_valid(*c
))
101 int terminal_urlify(const char *url
, const char *text
, char **ret
) {
106 /* Takes a URL and a pretty string and formats it as clickable link for the terminal. See
107 * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda for details. */
112 if (urlify_enabled() && url_suitable_for_osc8(url
))
113 n
= strjoin(ANSI_OSC
"8;;", url
, ANSI_ST
,
115 ANSI_OSC
"8;;" ANSI_ST
);
125 int file_url_from_path(const char *path
, char **ret
) {
126 _cleanup_free_
char *absolute
= NULL
;
134 if (!path_is_absolute(path
)) {
135 r
= path_make_absolute_cwd(path
, &absolute
);
142 /* As suggested by https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda, let's include the local
143 * hostname here. Note that we don't use gethostname_malloc() or gethostname_strict() since we are interested
144 * in the raw string the kernel has set, whatever it may be, under the assumption that terminals are not overly
145 * careful with validating the strings either. */
147 url
= strjoin("file://", u
.nodename
, path
);
155 int terminal_urlify_path(const char *path
, const char *text
, char **ret
) {
156 _cleanup_free_
char *url
= NULL
;
161 /* Much like terminal_urlify() above, but takes a file system path as input
162 * and turns it into a proper file:// URL first. */
170 if (!urlify_enabled())
171 return strdup_to(ret
, text
);
173 r
= file_url_from_path(path
, &url
);
177 return terminal_urlify(url
, text
, ret
);
180 int terminal_urlify_man(const char *page
, const char *section
, char **ret
) {
181 const char *url
, *text
;
183 url
= strjoina("man:", page
, "(", section
, ")");
184 text
= strjoina(page
, "(", section
, ") man page");
186 return terminal_urlify(url
, text
, ret
);
189 static int cat_file(const ConfFile
*c
, bool *newline
, CatFlags flags
) {
190 _cleanup_fclose_
FILE *f
= NULL
;
191 _cleanup_free_
char *urlified
= NULL
, *section
= NULL
, *old_section
= NULL
;
195 assert(c
->original_path
);
196 assert(c
->resolved_path
);
205 bool resolved
= !path_equal(c
->original_path
, c
->resolved_path
);
207 r
= terminal_urlify_path(c
->resolved_path
, NULL
, &urlified
);
209 return log_error_errno(r
, "Failed to urlify path \"%s\": %m", c
->resolved_path
);
211 printf("%s# %s%s%s%s\n",
212 ansi_highlight_blue(),
213 resolved
? c
->original_path
: "",
214 resolved
? " -> " : "",
218 f
= fopen(FORMAT_PROC_FD_PATH(c
->fd
), "re");
220 return log_error_errno(errno
, "Failed to open \"%s\": %m", c
->resolved_path
);
222 for (bool continued
= false;;) {
223 _cleanup_free_
char *line
= NULL
;
225 r
= read_line(f
, LONG_LINE_MAX
, &line
);
227 return log_error_errno(r
, "Failed to read \"%s\": %m", c
->resolved_path
);
231 const char *l
= skip_leading_chars(line
, WHITESPACE
);
234 if (*l
!= '\0' && strchr(COMMENTS
, *l
)) {
235 if (!FLAGS_SET(flags
, CAT_TLDR
))
236 printf("%s%s%s\n", ansi_highlight_grey(), line
, ansi_normal());
241 if (FLAGS_SET(flags
, CAT_TLDR
) && (isempty(l
) || streq(l
, "\\")))
245 if (FLAGS_SET(flags
, CAT_FORMAT_HAS_SECTIONS
) && *l
== '[' && !continued
) {
246 if (FLAGS_SET(flags
, CAT_TLDR
))
247 /* On TLDR, let's not print it yet. */
248 free_and_replace(section
, line
);
250 printf("%s%s%s\n", ansi_highlight_cyan(), line
, ansi_normal());
256 /* Before we print the line, print the last section header. */
257 if (FLAGS_SET(flags
, CAT_TLDR
) && section
) {
258 /* Do not print redundant section headers */
259 if (!streq_ptr(section
, old_section
))
260 printf("%s%s%s\n", ansi_highlight_cyan(), section
, ansi_normal());
262 free_and_replace(old_section
, section
);
265 /* Check if the line ends with a backslash. */
266 bool escaped
= false;
268 for (e
= line
; *e
!= '\0'; e
++) {
275 /* Highlight the trailing backslash. */
280 if (!strextend(&line
, ansi_highlight_red(), "\\", ansi_normal()))
284 /* Highlight the left side (directive) of a Foo=bar assignment */
285 if (FLAGS_SET(flags
, CAT_FORMAT_HAS_SECTIONS
) && !continued
) {
286 const char *p
= strchr(line
, '=');
288 _cleanup_free_
char *directive
= NULL
;
290 directive
= strndup(line
, p
- line
);
294 printf("%s%s=%s%s\n", ansi_highlight_green(), directive
, ansi_normal(), p
+ 1);
300 /* Otherwise, print the line as is. */
301 printf("%s\n", line
);
308 int cat_files_full(const ConfFile
*file
, ConfFile
* const *dropins
, size_t n_dropins
, CatFlags flags
) {
309 bool newline
= false;
312 assert(dropins
|| n_dropins
== 0);
315 ret
= cat_file(file
, &newline
, flags
);
317 FOREACH_ARRAY(i
, dropins
, n_dropins
)
318 RET_GATHER(ret
, cat_file(*i
, &newline
, flags
));
323 static int cat_file_by_path(const char *p
, bool *newline
, CatFlags flags
) {
324 _cleanup_(conf_file_freep
) ConfFile
*c
= NULL
;
329 r
= conf_file_new(p
, /* root = */ NULL
, CHASE_MUST_BE_REGULAR
, &c
);
331 return log_error_errno(r
, "Failed to chase '%s': %m", p
);
333 return cat_file(c
, newline
, flags
);
336 int cat_files(const char *file
, char **dropins
, CatFlags flags
) {
337 bool newline
= false;
341 ret
= cat_file_by_path(file
, &newline
, flags
);
343 STRV_FOREACH(path
, dropins
)
344 RET_GATHER(ret
, cat_file_by_path(*path
, &newline
, flags
));
349 void print_separator(void) {
351 /* Outputs a separator line that resolves to whitespace when copied from the terminal. We do that by outputting
352 * one line filled with spaces with ANSI underline set, followed by a second (empty) line. */
354 if (underline_enabled()) {
355 size_t c
= columns();
358 fputs_unlocked(ansi_grey_underline(), stdout
);
360 for (size_t i
= 0; i
< c
; i
++)
361 fputc_unlocked(' ', stdout
);
363 fputs_unlocked(ansi_normal(), stdout
);
364 fputs_unlocked("\n\n", stdout
);
367 fputs("\n\n", stdout
);
370 static int guess_type(const char **name
, char ***ret_prefixes
, bool *ret_is_collection
, const char **ret_extension
) {
371 /* Try to figure out if name is like tmpfiles.d/ or systemd/system-presets/,
372 * i.e. a collection of directories without a main config file.
373 * Incidentally, all those formats don't use sections. So we return a single
374 * is_collection boolean, which also means that the format doesn't use sections.
377 _cleanup_free_
char *n
= NULL
;
378 bool run
= false, coll
= false;
379 const char *ext
= ".conf";
380 /* This is static so that the array doesn't get deallocated when we exit the function */
381 static const char* const std_prefixes
[] = { CONF_PATHS(""), NULL
};
382 static const char* const run_prefixes
[] = { "/run/", NULL
};
384 if (path_equal(*name
, "environment.d"))
385 /* Special case: we need to include /etc/environment in the search path, even
386 * though the whole concept is called environment.d. */
387 *name
= "environment";
393 delete_trailing_chars(n
, "/");
395 /* We assume systemd-style config files support the /usr-/run-/etc split and dropins. */
397 if (endswith(n
, ".d"))
400 if (path_equal(n
, "udev/hwdb.d"))
402 else if (path_equal(n
, "udev/rules.d"))
404 else if (path_equal(n
, "kernel/install.d"))
406 else if (path_equal(n
, "systemd/ntp-units.d")) {
409 } else if (path_equal(n
, "systemd/relabel-extra.d")) {
412 } else if (PATH_IN_SET(n
, "systemd/system-preset", "systemd/user-preset", "systemd/initrd-preset")) {
417 *ret_prefixes
= (char**) (run
? run_prefixes
: std_prefixes
);
418 *ret_is_collection
= coll
;
419 *ret_extension
= ext
;
423 int conf_files_cat(const char *root
, const char *name
, CatFlags flags
) {
424 _cleanup_strv_free_
char **dirs
= NULL
;
425 char **prefixes
= NULL
; /* explicit initialization to appease gcc */
427 const char *extension
;
430 r
= guess_type(&name
, &prefixes
, &is_collection
, &extension
);
436 STRV_FOREACH(prefix
, prefixes
) {
437 assert(endswith(*prefix
, "/"));
438 r
= strv_extendf(&dirs
, "%s%s%s", *prefix
, name
,
439 is_collection
? "" : ".d");
441 return log_error_errno(r
, "Failed to build directory list: %m");
445 log_debug("Looking for configuration in:");
447 STRV_FOREACH(prefix
, prefixes
)
448 log_debug(" %s%s%s", strempty(root
), *prefix
, name
);
450 STRV_FOREACH(t
, dirs
)
451 log_debug(" %s%s/*%s", strempty(root
), *t
, extension
);
454 /* First locate the main config file, if any */
455 _cleanup_(conf_file_freep
) ConfFile
*c
= NULL
;
456 if (!is_collection
) {
457 STRV_FOREACH(prefix
, prefixes
) {
458 _cleanup_free_
char *p
= path_join(*prefix
, name
);
462 if (conf_file_new(p
, root
, CHASE_MUST_BE_REGULAR
, &c
) >= 0)
467 printf("%s# Main configuration file %s not found%s\n",
468 ansi_highlight_magenta(),
473 /* Then locate the drop-ins, if any */
474 ConfFile
**dropins
= NULL
;
475 size_t n_dropins
= 0;
476 CLEANUP_ARRAY(dropins
, n_dropins
, conf_file_free_many
);
477 r
= conf_files_list_strv_full(extension
, root
, CONF_FILES_REGULAR
| CONF_FILES_FILTER_MASKED
, (const char* const*) dirs
, &dropins
, &n_dropins
);
479 return log_error_errno(r
, "Failed to query file list: %m");
483 flags
|= CAT_FORMAT_HAS_SECTIONS
;
485 return cat_files_full(c
, dropins
, n_dropins
, flags
);
488 int terminal_tint_color(double hue
, char **ret
) {
489 double red
, green
, blue
;
494 r
= get_default_background_color(&red
, &green
, &blue
);
496 return log_debug_errno(r
, "Unable to get terminal background color: %m");
499 rgb_to_hsv(red
, green
, blue
, /* ret_h= */ NULL
, &s
, &v
);
501 if (v
> 50) /* If the background is bright, then pull down saturation */
503 else /* otherwise pump it up */
506 v
= MAX(20, v
); /* Make sure we don't hide the color in black */
509 hsv_to_rgb(hue
, s
, v
, &r8
, &g8
, &b8
);
511 if (asprintf(ret
, "48;2;%u;%u;%u", r8
, g8
, b8
) < 0)
517 bool shall_tint_background(void) {
518 static int cache
= -1;
523 cache
= getenv_bool("SYSTEMD_TINT_BACKGROUND");
525 return (cache
= true);
527 log_debug_errno(cache
, "Failed to parse $SYSTEMD_TINT_BACKGROUND, leaving background tinting enabled: %m");
532 void draw_progress_bar_unbuffered(const char *prefix
, double percentage
) {
535 fputs(prefix
, stderr
);
539 if (!terminal_is_dumb()) {
540 /* Generate the Windows Terminal progress indication OSC sequence here. Most Linux terminals currently
541 * ignore this. But let's hope this changes one day. For details about this OSC sequence, see:
543 * https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
544 * https://github.com/microsoft/terminal/pull/8055
546 fprintf(stderr
, ANSI_OSC
"9;4;1;%u" ANSI_ST
, (unsigned) ceil(percentage
));
548 size_t cols
= columns();
549 size_t prefix_width
= utf8_console_width(prefix
) + 1 /* space */;
550 size_t length
= cols
> prefix_width
+ 6 ? cols
- prefix_width
- 6 : 0;
552 if (length
> 5 && percentage
>= 0.0 && percentage
<= 100.0) {
553 size_t p
= (size_t) (length
* percentage
/ 100.0);
554 bool separator_done
= false;
556 fputs(ansi_highlight_green(), stderr
);
558 for (size_t i
= 0; i
< length
; i
++) {
561 if (get_color_mode() == COLOR_24BIT
) {
563 double z
= i
== 0 ? 0 : (((double) i
/ p
) * 100);
564 hsv_to_rgb(145 /* green */, z
, 33 + z
*2/3, &r8
, &g8
, &b8
);
565 fprintf(stderr
, "\x1B[38;2;%u;%u;%um", r8
, g8
, b8
);
568 fputs(glyph(GLYPH_HORIZONTAL_FAT
), stderr
);
569 } else if (i
+1 < length
&& !separator_done
) {
570 fputs(ansi_normal(), stderr
);
572 separator_done
= true;
573 fputs(ansi_grey(), stderr
);
575 fputs(glyph(GLYPH_HORIZONTAL_DOTTED
), stderr
);
578 fputs(ansi_normal(), stderr
);
589 if (!terminal_is_dumb())
590 fputs(ANSI_ERASE_TO_END_OF_LINE
, stderr
);
595 void clear_progress_bar_unbuffered(const char *prefix
) {
598 if (terminal_is_dumb())
600 prefix
? utf8_console_width(prefix
) + 5 : /* %3.0f%% (4 chars) + space */
601 LESS_BY(columns(), 1U)),
604 /* Undo Windows Terminal progress indication again. */
605 fputs(ANSI_OSC
"9;4;0;" ANSI_ST
606 ANSI_ERASE_TO_END_OF_LINE
, stderr
);
611 void draw_progress_bar(const char *prefix
, double percentage
) {
612 /* We are going output a bunch of small strings that shall appear as a single line to STDERR which is
613 * unbuffered by default. Let's temporarily turn on full buffering, so that this is passed to the tty
614 * as a single buffer, to make things more efficient. */
615 WITH_BUFFERED_STDERR
;
616 draw_progress_bar_unbuffered(prefix
, percentage
);
619 int draw_progress_barf(double percentage
, const char *prefixf
, ...) {
620 _cleanup_free_
char *s
= NULL
;
624 va_start(ap
, prefixf
);
625 r
= vasprintf(&s
, prefixf
, ap
);
631 draw_progress_bar(s
, percentage
);
635 void clear_progress_bar(const char *prefix
) {
636 WITH_BUFFERED_STDERR
;
637 clear_progress_bar_unbuffered(prefix
);