Added pretty download UI

Closes #1
This commit is contained in:
2025-11-27 03:11:44 +01:00
parent 229e0c8efd
commit 1e6f7f0ae6
5 changed files with 234 additions and 23 deletions

View File

@ -12,6 +12,7 @@ lunch_src = files(
'source/lunch/http.d', 'source/lunch/http.d',
'source/lunch/launch.d', 'source/lunch/launch.d',
'source/lunch/logger.d', 'source/lunch/logger.d',
'source/lunch/term.d',
'source/lunch/ui.d', 'source/lunch/ui.d',
'source/lunch/update.d', 'source/lunch/update.d',
) )

View File

@ -1,6 +1,92 @@
module lunch.color; module lunch.color;
string black()
{
return "\x1B[30m";
}
string red()
{
return "\x1B[31m";
}
string green()
{
return "\x1B[32m";
}
string yellow()
{
return "\x1B[33m";
}
string blue()
{
return "\x1B[34m";
}
string magenta()
{
return "\x1B[35m";
}
string cyan()
{
return "\x1B[36m";
}
string white()
{
return "\x1B[37m";
}
string brightBlack()
{
return "\x1B[90m";
}
string brightRed()
{
return "\x1B[91m";
}
string brightGreen()
{
return "\x1B[92m";
}
string brightYellow()
{
return "\x1B[93m";
}
string brightBlue()
{
return "\x1B[94m";
}
string brightMagenta()
{
return "\x1B[95m";
}
string brightCyan()
{
return "\x1B[96m";
}
string brightWhite()
{
return "\x1B[97m";
}
string reset()
{
return "\x1B[0m";
}
const(char)[] black(const(char)[] str) const(char)[] black(const(char)[] str)
{ {
return "\x1B[30m" ~ str ~ "\x1B[0m"; return "\x1B[30m" ~ str ~ "\x1B[0m";

View File

@ -82,7 +82,7 @@ T[] put(Conn = AutoProtocol, T = char, PutUnit)(const(char)[] url, const(PutUnit
} }
} }
private void _download(const(char)[] url, string file) private void _download(const(char)[] url, string file, void delegate(size_t size) progress)
{ {
scope (failure) scope (failure)
if (exists(file)) if (exists(file))
@ -94,16 +94,18 @@ private void _download(const(char)[] url, string file)
http.onReceive = (ubyte[] data) http.onReceive = (ubyte[] data)
{ {
handle.rawWrite(data); handle.rawWrite(data);
if (progress)
progress(data.length);
return data.length; return data.length;
}; };
http.perform(); http.perform();
} }
void download(const(char)[] url, string file) void download(const(char)[] url, string file, void delegate(size_t size) progress = null)
{ {
try try
{ {
_retry!_download(url, file); _retry!_download(url, file, progress);
infof("DL %s", url); infof("DL %s", url);
} }
catch (CurlException ex) catch (CurlException ex)

39
source/lunch/term.d Normal file
View File

@ -0,0 +1,39 @@
module lunch.term;
version (Windows)
{
import core.sys.windows.windows;
}
struct TermSize
{
int width, height;
}
TermSize termSize()
{
version (Windows)
{
CONSOLE_SCREEN_BUFFER_INFO csbi;
int columns, rows;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
return TermSize(
csbi.srWindow.Right - csbi.srWindow.Left + 1,
csbi.srWindow.Bottom - csbi.srWindow.Top + 1,
);
}
return TermSize(80, 25); // Default for windows cmd
}
int termWidth()
{
return termSize().width;
}
int termHeight()
{
return termSize().height;
}

View File

@ -1,24 +1,27 @@
module lunch.update; module lunch.update;
import std.datetime; import std.datetime;
import std.stdio : writefln, File; import std.stdio;
import std.traits; import std.traits;
import std.json; import std.json;
import std.conv; import std.conv;
import std.file; import std.file : dirEntries;
import std.path; import std.path;
import std.file; import std.file;
import std.math; import std.math;
import std.string; import std.string;
import std.digest.sha; import std.digest.sha;
import std.array; import std.array;
import std.format;
import std.algorithm; import std.algorithm;
import std.parallelism; import std.parallelism;
import std.datetime.stopwatch;
import lunch.conf; import lunch.conf;
import lunch.term;
import lunch.color;
import lunch.http; import lunch.http;
import lunch.logger; import lunch.logger;
private struct RemoteFile private struct RemoteFile
{ {
string file; string file;
@ -87,6 +90,7 @@ private struct Action
private struct Actions private struct Actions
{ {
Action[] actions; Action[] actions;
ulong total_download_size;
alias this = actions; alias this = actions;
} }
@ -100,7 +104,6 @@ private immutable(Local) _local;
private bool _actions_set = false; private bool _actions_set = false;
private immutable(Actions) _actions; private immutable(Actions) _actions;
private JSONValue[string] safe_object(JSONValue value) @trusted private JSONValue[string] safe_object(JSONValue value) @trusted
{ {
if (value.type != JSONType.object) if (value.type != JSONType.object)
@ -221,13 +224,11 @@ void wrapJSON(T)(ref T wrapper, JSONValue[string] json, bool ignore_missing = fa
}} }}
} }
private string workdir(string path) private string workdir(string path)
{ {
return chainPath(config.updater.workdir, path).array; return chainPath(config.updater.workdir, path).array;
} }
private void touch(string path, long mtime = Clock.currTime().toUnixTime) private void touch(string path, long mtime = Clock.currTime().toUnixTime)
{ {
infof("Touching %s", path); infof("Touching %s", path);
@ -236,7 +237,6 @@ private void touch(string path, long mtime = Clock.currTime().toUnixTime)
setTimes(path, old_atime, SysTime.fromUnixTime(mtime)); setTimes(path, old_atime, SysTime.fromUnixTime(mtime));
} }
private immutable(Remote) remote() private immutable(Remote) remote()
{ {
if (_remote_set) if (_remote_set)
@ -263,7 +263,6 @@ private immutable(Remote) remote()
return _remote; return _remote;
} }
private immutable(Local) local() private immutable(Local) local()
{ {
if (_local_set) if (_local_set)
@ -302,7 +301,6 @@ private immutable(Local) local()
return _local; return _local;
} }
private immutable(Actions) actions() private immutable(Actions) actions()
{ {
if (_actions_set) if (_actions_set)
@ -346,6 +344,7 @@ private immutable(Actions) actions()
if (!this_loc) if (!this_loc)
{ {
act ~= this_act; act ~= this_act;
act.total_download_size += this_rem.size;
continue; continue;
} }
@ -356,6 +355,7 @@ private immutable(Actions) actions()
if (this_loc.hash != this_rem.hash || this_loc.size != this_rem.size) if (this_loc.hash != this_rem.hash || this_loc.size != this_rem.size)
{ {
act ~= this_act; act ~= this_act;
act.total_download_size += this_rem.size;
} }
else else
{ {
@ -378,7 +378,6 @@ private immutable(Actions) actions()
return _actions; return _actions;
} }
public bool updateAvailable() public bool updateAvailable()
{ {
if (actions.length != 0) if (actions.length != 0)
@ -391,16 +390,77 @@ public void update()
{ {
info("Updating"); info("Updating");
scope (exit) cast() _actions = []; scope (exit) cast() _actions = Actions();
const dl_total = actions.actions.count!"a.what == b"(Action.download); auto writeSW = StopWatch(AutoStart.yes);
const dl_pad = cast(int)log10(cast(float)dl_total) + 1; auto speedSW = StopWatch(AutoStart.yes);
size_t dl_count = 0; const dl_total = actions.actions.count!"a.what == b"(Action.download);
const dl_pad = cast(int)log10(cast(float)dl_total) + 1;
uint dl_count = 0;
ulong dl_size = 0;
const dl_spad = cast(int)log10(cast(float)actions.total_download_size) + 1;
ulong dl_speed = 0;
ulong dl_speed_old_size = 0;
const old_defaultpool = defaultPoolThreads; const old_defaultpool = defaultPoolThreads;
scope (exit) defaultPoolThreads = old_defaultpool; scope (exit) defaultPoolThreads = old_defaultpool;
defaultPoolThreads = config.updater.parallel_downloads.to!uint - 1; defaultPoolThreads = config.updater.parallel_downloads.to!uint - 1;
auto writeProgress = delegate()
{
string numbers = format(
"[%s%*d%s|%s%d%s]",
magenta, dl_spad, dl_size, reset,
magenta, actions.total_download_size, reset,
);
int barSpace = termWidth - dl_spad*2 - 3 - 2 - 10 - 11;
float progress = dl_size.to!float / actions.total_download_size;
long filledSpace = lround(progress * barSpace);
long emptySpace = lround((1.0 - progress) * barSpace);
string progress_bar = "["~yellow;
for (int n = 0; n < filledSpace; n++)
progress_bar ~= '=';
for (int n = 0; n < emptySpace; n++)
progress_bar ~= ' ';
progress_bar ~= reset~"]";
string percent = format(
"[%7.2f%%]", progress * 100
);
int speed_rank = 0;
float speed = dl_speed.to!float;
for (speed_rank = 0; speed >= 1000; speed_rank += 1)
speed /= 1000;
string unit = [
" B",
"KB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
"RB",
"QB",
][speed_rank];
string download_speed = (speed < 100)
? format(
"[%s%4.1f %s%s/s]", cyan, speed, reset, unit
)
: format(
"[%s%5.1f%s%s/s]", cyan, speed, reset, unit
);
writef("\r%s%s%s%s\b", numbers, download_speed, progress_bar, percent);
};
foreach (action; parallel(actions.actions, 1)) foreach (action; parallel(actions.actions, 1))
final switch(action.what) final switch(action.what)
{ {
@ -409,13 +469,36 @@ public void update()
remove(action.file); remove(action.file);
break; break;
case Action.download: case Action.download:
info("Update %s", action.file);
mkdirRecurse(dirName(action.file)); mkdirRecurse(dirName(action.file));
synchronized
{ download(action.url, action.file, (size_t amount){
dl_count += 1; synchronized
writefln("[%*d|%*d] %s", dl_pad, dl_count, dl_pad, dl_total, action.file); {
} dl_size += amount;
download(action.url, action.file); if (speedSW.peek.total!"msecs" >= 1000)
{
long time = speedSW.peek.total!"msecs";
dl_speed = lround((dl_size - dl_speed_old_size) * time.to!float / 1000);
dl_speed_old_size = dl_size;
speedSW.reset();
}
if (writeSW.peek.total!"msecs" < 100)
return;
writeSW.reset();
}
writeProgress();
});
synchronized dl_count += 1;
writefln(
"\33[2K\r[%s%*d%s|%s%d%s] %s",
cyan, dl_pad, dl_count, reset,
cyan, dl_total, reset,
action.file.replace("/", dirSeparator).green,
);
writeProgress();
touch(action.file, action.mtime); touch(action.file, action.mtime);
break; break;
} }