1 // SPDX-License-Identifier: BSD-2-Clause-FreeBSD
2 //
3 // Copyright (c) 2021 Tobias Kortkamp <tobik@FreeBSD.org>
4 // All rights reserved.
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions
8 // are met:
9 // 1. Redistributions of source code must retain the above copyright
10 // notice, this list of conditions and the following disclaimer.
11 // 2. Redistributions in binary form must reproduce the above copyright
12 // notice, this list of conditions and the following disclaimer in the
13 // documentation and/or other materials provided with the distribution.
14 //
15 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 // SUCH DAMAGE.
26
27 #include "config.h"
28
29 #include <sys/ioctl.h>
30 #include <sys/param.h>
31 #include <inttypes.h>
32 #include <signal.h>
33 #include <stdbool.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <unistd.h>
38
39 #include <event2/event.h>
40
41 #include <libias/array.h>
42 #include <libias/flow.h>
43 #include <libias/mem.h>
44 #include <libias/mempool.h>
45 #include <libias/str.h>
46
47 #include "progress.h"
48
49 struct Progress {
50 struct event_base *base;
51 struct event *timeout;
52 struct event *sigint_event;
53 struct event *sigwinch_event;
54 FILE *out;
55 char *current_file;
56 off_t current_bytes;
57 off_t total_bytes;
58 struct winsize winsize;
59 bool initialized;
60 bool progressbar;
61 };
62
63 // Prototypes
64 static void on_sigint(evutil_socket_t, short, void *);
65 static void on_sigwinch(evutil_socket_t, short, void *);
66 static void on_timeout(evutil_socket_t, short, void *);
67 static void progress_go_to_bar_row(struct Progress *);
68 static void progress_set_winsize(struct Progress *, unsigned short);
69 static void progress_step(struct Progress *);
70
71 static const size_t PROGRESS_BAR_WIDTH = 15;
72
73 static const char *cursor_up = "\e[1A";
74 static const char *cursor_save = "\e7";
75 static const char *cursor_restore = "\e8";
76 static const char *erase_below = "\e[0J";
77 static const char *erase_line_all = "\e[2K\r";
78
79 void
on_sigint(evutil_socket_t fd,short events,void * userdata)80 on_sigint(evutil_socket_t fd, short events, void *userdata)
81 {
82 struct Progress *this = userdata;
83 progress_set_winsize(this, this->winsize.ws_row);
84 fprintf(this->out, "interrupted by user\n");
85 exit(1);
86 }
87
88 void
on_sigwinch(evutil_socket_t fd,short events,void * userdata)89 on_sigwinch(evutil_socket_t fd, short events, void *userdata)
90 {
91 struct Progress *this = userdata;
92 ioctl(fileno(this->out), TIOCGWINSZ, &this->winsize);
93 progress_set_winsize(this, this->winsize.ws_row - 1);
94 }
95
96 void
on_timeout(evutil_socket_t fd,short events,void * userdata)97 on_timeout(evutil_socket_t fd, short events, void *userdata)
98 {
99 struct Progress *this = userdata;
100 progress_step(this);
101 }
102
103 struct Progress *
progress_new(struct event_base * base,FILE * out)104 progress_new(struct event_base *base, FILE *out)
105 {
106 struct Progress *this = xmalloc(sizeof(struct Progress));
107 this->base = base;
108 this->current_file = str_dup(NULL, "");
109 this->out = out;
110 if (isatty(fileno(this->out))) {
111 this->progressbar = true;
112 }
113 this->timeout = event_new(base, 0, EV_PERSIST, on_timeout, this);
114 struct timeval time;
115 time.tv_sec = 1;
116 time.tv_usec = 0;
117 evtimer_add(this->timeout, &time);
118
119 this->sigint_event = evsignal_new(base, SIGINT, on_sigint, this);
120 evsignal_add(this->sigint_event, NULL);
121 this->sigwinch_event = evsignal_new(base, SIGWINCH, on_sigwinch, this);
122 evsignal_add(this->sigwinch_event, NULL);
123
124 return this;
125 }
126
127 void
progress_free(struct Progress * this)128 progress_free(struct Progress *this)
129 {
130 progress_stop(this);
131 progress_set_winsize(this, this->winsize.ws_row);
132 free(this->current_file);
133 event_free(this->timeout);
134 event_free(this->sigint_event);
135 event_free(this->sigwinch_event);
136 free(this);
137 }
138
139 void
progress_update(struct Progress * this,off_t delta,const char * current_file)140 progress_update(struct Progress *this, off_t delta, const char *current_file)
141 {
142 this->current_bytes += delta;
143 if (this->current_bytes < 0) {
144 this->current_bytes = 0;
145 }
146 if (current_file) {
147 free(this->current_file);
148 this->current_file = str_dup(NULL, current_file);
149 }
150 }
151
152 void
progress_update_total(struct Progress * this,off_t delta)153 progress_update_total(struct Progress *this, off_t delta)
154 {
155 this->total_bytes += delta;
156 if (this->total_bytes < 0) {
157 this->total_bytes = 0;
158 }
159 }
160
161 void
progress_go_to_bar_row(struct Progress * this)162 progress_go_to_bar_row(struct Progress *this)
163 {
164 fprintf(this->out, "\e[%d;0H", this->winsize.ws_row + 1);
165 }
166
167 void
progress_set_winsize(struct Progress * this,unsigned short row)168 progress_set_winsize(struct Progress *this, unsigned short row)
169 {
170 fputs("\n", this->out);
171 fputs(cursor_save, this->out);
172 // set Scrolling Region
173 fprintf(this->out, "\e[0;%dr", row);
174 fputs(cursor_restore, this->out);
175 fputs(cursor_up, this->out);
176 fputs(erase_below, this->out);
177 fflush(this->out);
178 }
179
180 void
progress_step(struct Progress * this)181 progress_step(struct Progress *this)
182 {
183 int progress = 0;
184 if (this->total_bytes > 0) {
185 // in makesum mode total_bytes is an estimation
186 // and CURLOPT_MAXFILESIZE_LARGE is not set
187 // either, so this might go over 100%
188 progress = MIN(100, 100.0 * this->current_bytes / this->total_bytes);
189 }
190
191 unless (this->initialized) {
192 if (this->progressbar) {
193 ioctl(fileno(this->out), TIOCGWINSZ, &this->winsize);
194 progress_set_winsize(this, this->winsize.ws_row - 1);
195 }
196 this->initialized = true;
197 }
198
199 if (this->progressbar && this->winsize.ws_col <= (PROGRESS_BAR_WIDTH + strlen("[100%] [] "))) {
200 if (this->winsize.ws_col >= 4) {
201 fputs(cursor_save, this->out);
202 progress_go_to_bar_row(this);
203 fputs(erase_line_all, this->out);
204 fprintf(this->out, "%3d%%", progress);
205 fputs(cursor_restore, this->out);
206 fflush(this->out);
207 }
208 return;
209 }
210
211 SCOPE_MEMPOOL(pool);
212 int full = PROGRESS_BAR_WIDTH * progress / 100;
213 char *bar = mempool_alloc(pool, PROGRESS_BAR_WIDTH + 1);
214 memset(bar, ' ', PROGRESS_BAR_WIDTH);
215 memset(bar, '=', full);
216 if (full > 0) {
217 bar[full - 1] = '>';
218 }
219
220 ssize_t filename_width = this->winsize.ws_col - strlen("[100%] [] ") - PROGRESS_BAR_WIDTH;
221 const char *filename = this->current_file;
222 if (filename_width > 0) {
223 filename = str_slice(pool, this->current_file, 0, filename_width);
224 }
225 if (this->progressbar) {
226 fputs(cursor_save, this->out);
227 progress_go_to_bar_row(this);
228 fputs(erase_line_all, this->out);
229 fprintf(this->out, "%3d%% [%s] ", progress, bar);
230 fputs(filename, this->out);
231 fputs(cursor_restore, this->out);
232 } else {
233 fprintf(this->out, "%3d%% [%s] %s\n", progress, bar, filename);
234 }
235 fflush(this->out);
236 }
237
238 void
progress_stop(struct Progress * this)239 progress_stop(struct Progress *this)
240 {
241 event_del(this->timeout);
242 event_del(this->sigint_event);
243 event_del(this->sigwinch_event);
244 }
245