feat(ui): add spinner, progress output, and summary table
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
|
||||
Warning Excluding main source file src/main.d from test.
|
||||
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [tofu-test-application]
|
||||
src/tofu/ui.d(98,43): Error: function `core.thread.osthread.Thread.join(bool rethrow = true)` is not callable using argument types `(Duration)`
|
||||
() @trusted { _thread.join(dur!("msecs")(2000)); }();
|
||||
^
|
||||
src/tofu/ui.d(98,43): cannot pass argument `dur(2000L)` of type `Duration` to parameter `bool rethrow = true`
|
||||
Error /usr/bin/dmd failed with exit code 1.
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
|
||||
Warning Excluding main source file src/main.d from test.
|
||||
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [tofu-test-application]
|
||||
src/tofu/commands/info.d(325,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("neovim", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(325,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(335,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("nonexistent", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(335,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(356,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("neovim", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(356,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(371,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("neovim", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(371,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(386,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("ripgrep", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(386,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(397,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("firefox", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(397,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(412,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("firefox", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(412,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/info.d(425,34): Error: function `infoCommandWithIndex` is not callable using argument types `(string, ParsedArgs, Config, PackageIndex[] function(Config _) @safe)`
|
||||
int ec = infoCommandWithIndex("ripgrep", flags, cfg, &fakeIndex);
|
||||
^
|
||||
src/tofu/commands/info.d(425,34): cannot pass argument `& fakeIndex` of type `PackageIndex[] function(Config _) @safe` to parameter `scope PackageIndex[] delegate(Config) @safe fetchFn`
|
||||
src/tofu/commands/info.d(179,19): `tofu.commands.info.infoCommandWithIndex(string pkgName, ParsedArgs flags, Config cfg, scope PackageIndex[] delegate(Config) @safe fetchFn)` declared here
|
||||
package @safe int infoCommandWithIndex(
|
||||
^
|
||||
src/tofu/commands/install.d(112,27): Error: taking the address of local variable `entry` is not allowed in a `@safe` function
|
||||
foundEntry = &entry;
|
||||
^
|
||||
src/tofu/commands/install.d(231,34): Error: `@safe` function `tofu.commands.install.installCommand` cannot call `@system` function `std.stdio.readln!string.readln`
|
||||
response = readln().strip();
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(5242,20): which calls `makeGlobal`
|
||||
@property ref File makeGlobal(StdFileHandle _iob)()
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(5244,25): and using `__gshared` instead of `shared` makes it fail to infer `@safe`
|
||||
__gshared File.Impl impl;
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(4499,3): `std.stdio.readln!string.readln` is declared here
|
||||
S readln(S = string)(dchar terminator = '\n')
|
||||
^
|
||||
src/tofu/commands/install.d(363,10): Error: `write` matches conflicting symbols:
|
||||
write(path, content);
|
||||
^
|
||||
/usr/include/dlang/dmd/std/file.d(741,6): function `std.file.write!string.write`
|
||||
void write(R)(R name, const void[] buffer)
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(4091,6): function `std.stdio.write!(string, string).write`
|
||||
void write(T...)(T args)
|
||||
^
|
||||
src/tofu/commands/install.d(371,12): Error: module `std.string` import `octal` not found
|
||||
import std.string : toStringz, octal;
|
||||
^
|
||||
src/tofu/commands/install.d(390,10): Error: `write` matches conflicting symbols:
|
||||
write(path, script);
|
||||
^
|
||||
/usr/include/dlang/dmd/std/file.d(741,6): function `std.file.write!string.write`
|
||||
void write(R)(R name, const void[] buffer)
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(4091,6): function `std.stdio.write!(string, string).write`
|
||||
void write(T...)(T args)
|
||||
^
|
||||
src/tofu/commands/install.d(401,12): Error: module `std.string` import `octal` not found
|
||||
import std.string : toStringz, octal;
|
||||
^
|
||||
src/tofu/commands/install.d(405,10): Error: `write` matches conflicting symbols:
|
||||
write(path, script);
|
||||
^
|
||||
/usr/include/dlang/dmd/std/file.d(741,6): function `std.file.write!string.write`
|
||||
void write(R)(R name, const void[] buffer)
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(4091,6): function `std.stdio.write!(string, string).write`
|
||||
void write(T...)(T args)
|
||||
^
|
||||
src/tofu/commands/remove.d(49,9): Error: `@safe` function `tofu.commands.remove.removeCommand` cannot call `@system` function `std.stdio.makeGlobal!"core.stdc.stdio.stdout".makeGlobal`
|
||||
stdout.flush();
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(5244,25): and using `__gshared` instead of `shared` makes it fail to infer `@safe`
|
||||
__gshared File.Impl impl;
|
||||
^
|
||||
/usr/include/dlang/dmd/std/stdio.d(5242,20): `std.stdio.makeGlobal!"core.stdc.stdio.stdout".makeGlobal` is declared here
|
||||
@property ref File makeGlobal(StdFileHandle _iob)()
|
||||
^
|
||||
Error /usr/bin/dmd failed with exit code 1.
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
|
||||
Warning Excluding main source file src/main.d from test.
|
||||
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [tofu-test-application]
|
||||
Linking tofu-test-application
|
||||
Finished To force a rebuild of up-to-date targets, run again with --force
|
||||
Running tofu-test-application
|
||||
zeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasedonezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depB...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building target...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for depA: build failed for depA:
|
||||
zeta-makepkg: building depA...
|
||||
zeta-makepkg: configure phase
|
||||
zeta-makepkg: build phase
|
||||
zeta-makepkg: install phase
|
||||
zeta-makepkg: building pkg...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for noexist: recipe not found at /tmp/tofu-test-build-buildall-norecipe-232842/nonexistent.recipe
|
||||
[33mwarn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||
Warning: malformed TOML config at /tmp/tofu-test-config-bad-232842.toml: Invalid table key declaration (2:0)
|
||||
Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1
|
||||
[33mwarn removing stale lock (PID 99999999 not alive)[0m
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:40] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:41] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:41] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:41] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:41] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 18:26:41] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry 'badpool': invalid pool 'bad_pool_value'[0m
|
||||
[2m . checking binary firefox ...[0m
|
||||
- binary firefox-2.1.0 satisfies firefox>=2.0
|
||||
[2m . checking binary libfoo ...[0m
|
||||
- binary libfoo-1.9 does not satisfy libfoo>=2.0
|
||||
[2m . checking binary noexist ...[0m
|
||||
[2m . checking binary brokenpkg ...[0m
|
||||
[2m . checking binary badpkg ...[0m
|
||||
[2m . checking binary testpkg ...[0m
|
||||
- binary testpkg-2.1.0 satisfies testpkg
|
||||
[36m==> ──── building depA (1/3) ────[0m
|
||||
[32m ok depA[0m
|
||||
[36m==> ──── building depB (2/3) ────[0m
|
||||
[32m ok depB[0m
|
||||
[36m==> ──── building target (3/3) ────[0m
|
||||
[32m ok target[0m
|
||||
[36m==> ──── building depA (1/3) ────[0m
|
||||
- nothing to build
|
||||
[36m==> ──── building pkg (1/1) ────[0m
|
||||
- pkg already built, skipping
|
||||
[36m==> ──── building pkg (1/1) ────[0m
|
||||
[32m ok pkg[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
[2m . downloading build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[2m . custom build system: fetching scripts/build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . fetching index http://127.0.0.1:38591//index.lua[0m
|
||||
[32m ok index loaded: 3 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:52363//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:42821//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:43871//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:44243//index.lua[0m
|
||||
[32m ok index loaded: 0 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:41801//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:48459//index.lua[0m
|
||||
[32m ok index loaded: 2 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:57985//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:55199//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
installing package...error: build failedreason: missing dependencytestpkg-2.1 is already installed -- use -ReProvide to reinstall - already installed — skipping
|
||||
[32m ok installed A with 2 dependencies[0m
|
||||
- nothing to install
|
||||
install failed: conflict detectedtestpkg-2.1 is already installed -- use -ReProvide to reinstall[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-232842/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||
[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-232842/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||
19 modules passed unittests
|
||||
- already installed — skipping
|
||||
[32m ok installed A with 0 dependencies[0m
|
||||
- binary libfoo-2.1 satisfies libfoo>=2.0
|
||||
- libfoo: binary 1.9 too old, building from recipe
|
||||
- libfoo: binary 1.9 too old, building from recipe
|
||||
- binary libbar-3.0 satisfies libbar (unconstrained)
|
||||
- libbaz: no binary available, building from recipe
|
||||
- binary libfoo-1.5 satisfies libfoo>=1.0
|
||||
- libfoo: binary 1.5 too old, building from recipe
|
||||
- binary parentA-1.5 satisfies parentA (unconstrained)
|
||||
- binary parentB-1.5 satisfies parentB (unconstrained)
|
||||
[36m==> generating build plan[0m
|
||||
- + C (recipe)
|
||||
- + A (recipe)
|
||||
[32m ok build plan: 2 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
- recipe libfoo not in cache, re-fetching
|
||||
- + libfoo (recipe)
|
||||
[32m ok build plan: 1 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
[36m==> generating build plan[0m
|
||||
- nothing to build (all binary)
|
||||
[36m==> generating build plan[0m
|
||||
- + mypkg (recipe)
|
||||
[32m ok build plan: 1 packages[0m
|
||||
[36m==> generating build plan[0m
|
||||
- + libfoo (recipe)
|
||||
- + mypkg (recipe)
|
||||
[32m ok build plan: 2 packages[0m
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Starting Performing "debug" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [application]
|
||||
src/tofu/commands/remove.d(11,8): Error: module `tofu.install` import `runRemove` not found
|
||||
import tofu.install : runRemove, InstallException;
|
||||
^
|
||||
Error /usr/bin/dmd failed with exit code 1.
|
||||
Warning
|
||||
Warning ## Warning for package tofu ##
|
||||
Warning
|
||||
Warning The following compiler flags have been specified in the package description
|
||||
Warning file. They are handled by DUB and direct use in packages is discouraged.
|
||||
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
|
||||
Warning to the compiler, or use one of the suggestions below:
|
||||
Warning
|
||||
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
|
||||
Warning
|
||||
Starting Performing "debug" build using /usr/bin/dmd for x86_64.
|
||||
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
|
||||
Building tofu ~main: building configuration [application]
|
||||
src/main.d(25,8): Error: unable to read module `info`
|
||||
import tofu.commands.info : infoCommand;
|
||||
^
|
||||
src/main.d(25,8): Expected 'tofu/commands/info.d' or 'tofu/commands/info/package.d' in one of the following import paths:
|
||||
import path[0] = src/
|
||||
import path[1] = ../../.dub/packages/toml/1.0.0/toml/src/
|
||||
import path[2] = /usr/include/dlang/dmd
|
||||
Error /usr/bin/dmd failed with exit code 1.
|
||||
@@ -1029,3 +1029,57 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
||||
- `dub test` passes — all 18 modules, including search.d's 7 unittests.
|
||||
- `dub run -- -Ss neovim` fails on lock file creation (`~/.cache/tofu` directory doesn't exist) — pre-existing issue, expected without initialized environment.
|
||||
- Evidence logged to `.omo/evidence/task-20-tofu-core.log`.
|
||||
|
||||
---
|
||||
|
||||
## Task 25 — `tofu.ui` (spinner, progress output, summary table)
|
||||
|
||||
### Architecture
|
||||
- Module `tofu.ui` — file `src/tofu/ui.d`, standalone module with no external dependencies beyond `tofu.log` and `core.thread`.
|
||||
- Three public APIs: `Spinner` class, `buildSeparator`, `summaryTable`.
|
||||
- `startSpinner(string label)` factory returns `Spinner` handle — creates and starts animation.
|
||||
|
||||
### Spinner — thread-based animation
|
||||
- **TTY detection**: `isatty(1)` from `core.sys.posix.unistd` — when stdout is NOT a terminal, spinner prints `"<label>..."` once and becomes a no-op on `stop()`.
|
||||
- **Animation**: background `core.thread.Thread` loops every 100 ms, writing `\r <frame> <label>` with frames `/`, `-`, `\`, `-` (same as ZETA `spinner.lua`).
|
||||
- **Stop**: sets `shared bool _running = false`, calls `Thread.join()` (blocks until thread exits — thread checks flag every 100 ms so this is prompt), then clears the animation line (`\r` + spaces + `\r`), then prints ` ok <label>` via `logOk`.
|
||||
- **`@safe` / `@trusted` split**: `start()` and `stop()` are `@safe` public; `threadFn()` is `@trusted` (calls stdout.writef/flush). Thread constructor + start/join are wrapped in `@trusted` blocks (Thread APIs are `@system` in DMD 2.112).
|
||||
|
||||
### Thread.join() — no Duration overload
|
||||
- D's `core.thread.Thread.join()` takes `bool rethrow = true`, NOT a `Duration`. The plan spec says "join with timeout" but this API doesn't exist in Phobos.
|
||||
- No timeout needed in practice: the thread exits within one 100 ms cycle after `_running` is cleared — join returns promptly.
|
||||
- The Lua reference uses `kill $pid` (signal-based), not join — the thread model is fundamentally different.
|
||||
|
||||
### buildSeparator
|
||||
- Prints `──── building <name> (<i>/<n>) ────` (U+2500 box-drawing chars, 4 per side).
|
||||
- Duplicates build.d's inline separator (line 247: `logStep("──── building %s (%d/%d) ────", ...)`) — documented duplication, do NOT modify build.d.
|
||||
- Uses plain `stdout.writeln` (no color/logStep) so test can assert exact output.
|
||||
|
||||
### summaryTable
|
||||
- Struct `InstallSummary { string name; string ver; string status; }`.
|
||||
- Fixed-width columns: `PACKAGE` (20 chars), `VERSION` (16 chars), `STATUS` (variable). Format: `%-20s %-16s %s`.
|
||||
- Empty array → no output.
|
||||
|
||||
### stdout capture in tests
|
||||
- `captureStdout()` — swaps global `stdout` to temp `File(name, "w+")`, runs dg, flushes, closes file, reads back with `std.file.readText`.
|
||||
- Must call `file.close()` before reading — `readText` opens the file independently and can't read while another handle exists.
|
||||
- Pattern adapted from `log.d`'s `capture()` but reads full multi-line content (log.d's version only calls `readln` for single-line output).
|
||||
|
||||
### Test cases (4/4 pass)
|
||||
1. Spinner with stdout redirected → no `\r` in captured output, label present ✓
|
||||
2. buildSeparator exact: UTF-8 `\xe2\x94\x80` (U+2500) chars with "building foo (1/3)" ✓
|
||||
3. summaryTable: 3 items → header PACKAGE/VERSION/STATUS + all 3 rows with correct statuses ✓
|
||||
4. Spinner plain-path: non-TTY mode prints label, stop() is no-op (no " ok " in output) ✓
|
||||
|
||||
### `canFind` usage in tests
|
||||
- Free function form: `canFind(haystack, needle)` — avoids D's UFCS auto-decoding issue on `string` (where `output.canFind("PACKAGE")` tries to find a `string` element in a `dchar` range).
|
||||
|
||||
### Pre-existing breakage
|
||||
- `src/tofu/commands/{install,remove,info}.d` from parallel tasks 20-24 have compile errors (function/delegate mismatches, import issues, `@safe` violations).
|
||||
- These block `dub build` and `dub test` — excluded via `.skip` rename for verification. Not caused by task 25.
|
||||
- `ui.d` compiles standalone: `dmd -c -o- -unittest -Isrc src/tofu/ui.d` passes clean.
|
||||
|
||||
### Build verified
|
||||
- `dub test` (with broken siblings excluded) — 19 modules pass unittests, including ui.d's 4 test blocks.
|
||||
- `dub build` (with broken siblings excluded) — blocked by `main.d` importing `tofu.commands.info` (skipped). ui.d itself compiles clean.
|
||||
- Evidence logged to `.omo/evidence/task-25-tofu-core.log`.
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
/// Progress output and terminal UI helpers for tofu — spinner, build
|
||||
/// separators, and summary table.
|
||||
///
|
||||
/// Spinner:
|
||||
/// Uses a background thread to animate `/ - \ |` with `\r` carriage return
|
||||
/// on stdout. Only renders when stdout is a terminal; when redirected,
|
||||
/// prints a single plain-text `<label>...` line once. `stop()` clears the
|
||||
/// animation line and emits ` ok <label>` via `logOk`.
|
||||
///
|
||||
/// buildSeparator:
|
||||
/// Prints `──── building <name> (<i>/<n>) ────` (U+2500 box-drawing).
|
||||
/// This duplicates build.d's inline separator for use by other callers;
|
||||
/// do **not** modify build.d.
|
||||
///
|
||||
/// summaryTable:
|
||||
/// Fixed-width tabular summary of install results — columns PACKAGE
|
||||
/// (20 chars), VERSION (16 chars), STATUS (variable).
|
||||
module tofu.ui;
|
||||
|
||||
import core.sys.posix.unistd : isatty;
|
||||
import core.thread : Thread;
|
||||
import core.time : dur;
|
||||
import std.format : format;
|
||||
import std.stdio : stdout;
|
||||
import tofu.log : logOk;
|
||||
|
||||
// ─── Spinner ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Animated progress spinner. Only animates when stdout is a terminal;
|
||||
/// otherwise prints a single plain-text line.
|
||||
class Spinner
|
||||
{
|
||||
private:
|
||||
Thread _thread;
|
||||
shared bool _running;
|
||||
string _label;
|
||||
bool _isTTY;
|
||||
|
||||
/// Check whether stdout (fd 1) is a terminal.
|
||||
static private bool isStdoutTTY() @trusted
|
||||
{
|
||||
return isatty(1) != 0;
|
||||
}
|
||||
|
||||
/// Background thread function: animate `/ - \ -` every 100 ms.
|
||||
private void threadFn() @trusted
|
||||
{
|
||||
immutable string[4] frames = ["/", "-", "\\", "-"];
|
||||
size_t i = 0;
|
||||
while (_running)
|
||||
{
|
||||
stdout.writef("\r %s %s", frames[i % 4], _label);
|
||||
stdout.flush();
|
||||
i++;
|
||||
Thread.sleep(dur!("msecs")(100));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/// Create a spinner for `label` (does **not** start animation).
|
||||
this(string label) @safe
|
||||
{
|
||||
_label = label;
|
||||
_isTTY = isStdoutTTY();
|
||||
}
|
||||
|
||||
/// Start the spinner. On a TTY launches the background animation
|
||||
/// thread; on non-TTY output prints a single `<label>...` line.
|
||||
void start() @safe
|
||||
{
|
||||
if (!_isTTY)
|
||||
{
|
||||
() @trusted { stdout.writeln(_label ~ "..."); }();
|
||||
return;
|
||||
}
|
||||
_running = true;
|
||||
() @trusted {
|
||||
_thread = new Thread(&threadFn);
|
||||
_thread.start();
|
||||
}();
|
||||
}
|
||||
|
||||
/// Stop the spinner. On a TTY signals the background thread, joins
|
||||
/// it (the thread checks `_running` every 100 ms so join returns
|
||||
/// promptly), clears the animation line, and prints
|
||||
/// ` ok <label>` via `logOk`. Non-TTY is a no-op.
|
||||
void stop() @safe
|
||||
{
|
||||
if (!_isTTY)
|
||||
return;
|
||||
|
||||
// Signal the worker thread to exit.
|
||||
_running = false;
|
||||
|
||||
if (_thread !is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
() @trusted { _thread.join(); }();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Thread join failure (e.g. double-join) — harmless.
|
||||
}
|
||||
_thread = null;
|
||||
}
|
||||
|
||||
// Clear the animation line: \r + enough spaces + \r.
|
||||
() @trusted {
|
||||
stdout.write("\r");
|
||||
// Overwrite the widest possible spinner line (" \ " + label).
|
||||
foreach (_; 0 .. _label.length + 10)
|
||||
stdout.write(" ");
|
||||
stdout.write("\r");
|
||||
stdout.flush();
|
||||
}();
|
||||
logOk("%s", _label);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience factory: create a `Spinner` and immediately call `start()`.
|
||||
/// Returns the handle for later `stop()`.
|
||||
Spinner startSpinner(string label) @safe
|
||||
{
|
||||
auto s = new Spinner(label);
|
||||
s.start();
|
||||
return s;
|
||||
}
|
||||
|
||||
// ─── buildSeparator ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Print a box-drawing separator for build progress.
|
||||
///
|
||||
/// Output: `──── building <name> (<index>/<total>) ────`
|
||||
/// (U+2500 characters). This duplicates build.d's inline separator;
|
||||
/// kept here as a standalone helper for future callers.
|
||||
void buildSeparator(string name, int index, int total) @safe
|
||||
{
|
||||
auto line = format("──── building %s (%d/%d) ────", name, index, total);
|
||||
() @trusted { stdout.writeln(line); }();
|
||||
}
|
||||
|
||||
// ─── summaryTable ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Package install summary entry.
|
||||
struct InstallSummary
|
||||
{
|
||||
string name;
|
||||
string ver;
|
||||
string status;
|
||||
}
|
||||
|
||||
/// Print a fixed-width summary table to stdout.
|
||||
///
|
||||
/// Columns: `PACKAGE` (20 chars, left-aligned), `VERSION` (16 chars,
|
||||
/// left-aligned), `STATUS` (variable width). Empty array → no output.
|
||||
void summaryTable(InstallSummary[] items) @safe
|
||||
{
|
||||
if (items.length == 0)
|
||||
return;
|
||||
|
||||
() @trusted {
|
||||
import std.stdio : writefln;
|
||||
writefln("%-20s %-16s %s", "PACKAGE", "VERSION", "STATUS");
|
||||
foreach (item; items)
|
||||
writefln("%-20s %-16s %s", item.name, item.ver, item.status);
|
||||
}();
|
||||
}
|
||||
|
||||
// ─── Unittests ──────────────────────────────────────────────────────────────
|
||||
|
||||
version (unittest)
|
||||
{
|
||||
import std.algorithm.searching : canFind;
|
||||
import std.file : exists, readText, remove, tempDir;
|
||||
import std.path : buildPath;
|
||||
import std.stdio : File;
|
||||
import std.string : indexOf;
|
||||
|
||||
/// Swap stdout to a temp file, run `dg`, restore, return full file
|
||||
/// contents. The file is cleaned up on scope exit.
|
||||
private string captureStdout(void delegate() dg) @trusted
|
||||
{
|
||||
auto fname = buildPath(tempDir, "tofu-ui-capture.tmp");
|
||||
scope (exit)
|
||||
{
|
||||
if (exists(fname))
|
||||
remove(fname);
|
||||
}
|
||||
|
||||
auto file = File(fname, "w+");
|
||||
auto saved = stdout;
|
||||
stdout = file;
|
||||
scope (exit) stdout = saved;
|
||||
scope (failure) stdout = saved;
|
||||
|
||||
dg();
|
||||
stdout.flush();
|
||||
file.close(); // release handle before read
|
||||
|
||||
return readText(fname);
|
||||
}
|
||||
}
|
||||
|
||||
// (1) Spinner with stdout redirected → no control characters leak.
|
||||
@safe unittest
|
||||
{
|
||||
auto output = captureStdout({
|
||||
auto s = startSpinner("downloading foo");
|
||||
s.stop();
|
||||
});
|
||||
|
||||
// Redirected output must contain NO carriage-return characters.
|
||||
assert(output.indexOf("\r") < 0,
|
||||
"redirected output must contain no \\r, got: `" ~ output ~ "`");
|
||||
|
||||
// The plain-text label was printed.
|
||||
assert(canFind(output, "downloading foo"),
|
||||
"expected 'downloading foo' in output, got: `" ~ output ~ "`");
|
||||
}
|
||||
|
||||
// (2) buildSeparator output exact.
|
||||
@safe unittest
|
||||
{
|
||||
auto output = captureStdout({
|
||||
buildSeparator("foo", 1, 3);
|
||||
});
|
||||
|
||||
assert(canFind(output, "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80 "
|
||||
~ "building foo (1/3) "
|
||||
~ "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80"),
|
||||
"expected separator line, got: `" ~ output ~ "`");
|
||||
}
|
||||
|
||||
// (3) summaryTable: 3 items → header + aligned rows.
|
||||
@safe unittest
|
||||
{
|
||||
auto output = captureStdout({
|
||||
summaryTable([
|
||||
InstallSummary("libfoo", "1.2.3", "installed"),
|
||||
InstallSummary("libbar", "0.9.0", "skipped"),
|
||||
InstallSummary("libbaz", "2.0.0", "failed"),
|
||||
]);
|
||||
});
|
||||
|
||||
// Header present.
|
||||
assert(canFind(output, "PACKAGE"), "expected PACKAGE header");
|
||||
assert(canFind(output, "VERSION"), "expected VERSION header");
|
||||
assert(canFind(output, "STATUS"), "expected STATUS header");
|
||||
|
||||
// All three rows present.
|
||||
assert(canFind(output, "libfoo"), "expected libfoo row");
|
||||
assert(canFind(output, "libbar"), "expected libbar row");
|
||||
assert(canFind(output, "libbaz"), "expected libbaz row");
|
||||
assert(canFind(output, "installed"), "expected installed status");
|
||||
assert(canFind(output, "skipped"), "expected skipped status");
|
||||
assert(canFind(output, "failed"), "expected failed status");
|
||||
}
|
||||
|
||||
// (4) Spinner start/stop plain-path (non-TTY) — no hang, no ok line.
|
||||
@safe unittest
|
||||
{
|
||||
auto output = captureStdout({
|
||||
auto s = startSpinner("test operation");
|
||||
s.stop();
|
||||
});
|
||||
|
||||
// Non-TTY mode prints the plain label.
|
||||
assert(canFind(output, "test operation"),
|
||||
"expected label in plain output, got: `" ~ output ~ "`");
|
||||
|
||||
// Non-TTY stop() returns early — logOk is NOT called.
|
||||
assert(output.indexOf(" ok ") < 0,
|
||||
"non-TTY stop must not print 'ok' line, got: `" ~ output ~ "`");
|
||||
}
|
||||
Reference in New Issue
Block a user