Author SHA1 Message Date
sami e0f22f808f merge: lane/pkg-qa
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 18:46:34 +04:00
sami 111229686c merge: lane/daemon 2026-09-15 18:46:34 +04:00
sami a5551db1c3 merge: lane/core 2026-09-15 18:46:34 +04:00
samiandClaude Sonnet 5 83b92d118b core: select platform/linux sources via velox_platform_sources()
Replace the hardcoded src/io/platform/linux/file_ops.cpp entry with a
velox_platform_sources(veloxcore src/io) call, now that PKG/QA's
cmake/platform.cmake owns VELOX_OS_* detection and source selection
(ADR 0020 §5). Adding platform/macos/ later needs no edit to this file.

Build-wiring only; full suite stays green (59/59).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 18:46:13 +04:00
samiandClaude Sonnet 5 78f2b22c8c daemon: use velox_platform_sources() instead of a hardcoded platform/linux/ list
PKG's cmake/platform.cmake (VELOX_OS_* + velox_platform_sources(<target> <dir>))
is on main now. Swap the hardcoded src/rpc/platform/linux/*.cpp list in
veloxd_rpc for velox_platform_sources(veloxd_rpc src/rpc), matching ADR 0020 §5
(source selection lives in cmake/platform.cmake and nowhere else). PORT can now
add platform/macos/ with no edit to this file.

Build-wiring only, no behaviour change. Full suite 59/59 green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 18:46:07 +04:00
samiandClaude Sonnet 5 77bdb0c427 docs: mark GUI's bootstrap.sh apt-name-validation request (R2) resolved
tools/bootstrap.sh already validates every apt package name via apt-cache policy
(landed in an earlier session), so R2 was done but still read as an open request.
Mark it closed so it stops resurfacing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:39:03 +04:00
sami d60cf94905 merge: lane/pkg-qa — local .deb, cmake/platform.cmake, Linux-gated deps
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 18:35:11 +04:00
samiandClaude Sonnet 5 0cd5bf6882 pkg: cmake/platform.cmake; gate libsecret and Qt6::DBus to Linux
Root build infra, per ADR 0020 as amended in 9624e68: PORT owns the per-OS
backend directories and macOS/Windows packaging, but cmake/platform*.cmake stays
PKG/QA's — the amendment exists because Phase 0 (CORE's and DAEMON's own seams,
on Linux) needs velox_platform_sources() to select platform/linux/*.cpp, and PORT
can't start until Phase 0 lands. Landing it as PORT-owned would have deadlocked
both lanes waiting on each other.

cmake/platform.cmake sets exactly one of VELOX_OS_LINUX/VELOX_OS_MACOS/
VELOX_OS_WINDOWS from CMAKE_SYSTEM_NAME and exposes velox_platform_sources(target
dir), which globs dir/platform/<os>/*.cpp. No seam consumes it yet (Phase 0 hasn't
landed), so on Ubuntu this changes nothing observable — VELOX_OS_LINUX is simply
true and no lane calls the new function.

pkg_check_modules(LIBSECRET ...) was unconditional in the root CMakeLists.txt,
so macOS could not configure at all (docs/08-porting.md's stated blocker); it's
now gated on VELOX_OS_LINUX, same as the Qt6::DBus component this also adds
(gui/docs/pkg-qa-requests-m1.md R4 — backs the org.freedesktop.portal
GlobalShortcuts path, which is Linux/portal-only same as libsecret). Both are
still REQUIRED on Linux; PORT adds the macOS sides (Keychain, and no DBus
equivalent needed) behind their own seams, not by touching this find.

Verified: fresh configure + build of veloxd/velox/velox-gui/velox-nmhost still
green (libsecret-1 and Qt6::DBus both found), the .deb from the previous commit
still builds and installs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:03:50 +04:00
samiandClaude Sonnet 5 31eadffce5 pkg: build a local .deb; de-duplicate onto DAEMON's systemd/nativehost/man page
DAEMON landed packaging/nativehost/, packaging/systemd/{velox.service,velox.socket}
(real sd_listen_fds() socket activation) and cli/man/velox.1 on main while this was
in flight, duplicating paths this branch had drafted independently. Per CLAUDE.md
(packaging/nativehost/ is DAEMON's; the rest of packaging/ is PKG/QA's), DAEMON's
copies are kept as the single source and this branch's packaging/native-messaging/,
packaging/man/velox.1 and stale packaging/systemd/velox.service are dropped rather
than maintained twice. The root CMakeLists.txt's install() rules now point at
DAEMON's paths directly, install both systemd units (not just velox.service), and
docs/07-packaging.md / packaging/README.md no longer claim there's no socket
activation — there is, and this package now ships it.

Adds the local-test-only .deb itself: debian/{control,rules,postinst,postrm,
changelog,copyright}, a placeholder icon set, the desktop entry and PKG/QA's own
man pages (veloxd.8, velox-gui.1 — velox.1 stays DAEMON's). libveloxcore stays
static (no consumer needs a .so yet); pairing has no real approval UI in this
build (D1 unbuilt), so postinst and packaging/README.md both say VELOX_PAIR_AUTO=1
is required and print it prominently. No PPA, no GPG — local dpkg -i only.

Verified live: dpkg-buildpackage -us -uc -b, dpkg -i, socket activation (systemctl
--user enable --now velox.socket), `velox ls` against the running daemon, lintian
clean, dpkg -r removes the manifest/units and leaves $XDG_DATA_HOME/velox alone.

.gitignore now excludes dpkg-buildpackage's debhelper build tree and obj-*/ (the
debuild output dir) — debian/control et al. stay tracked, everything debhelper
regenerates does not.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0176u3fTrxegrGNm2yC7r69W
2026-09-15 18:02:11 +04:00
sami 5b42e1f0e5 merge: lane/gui
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
2026-09-15 17:57:36 +04:00
sami e8af9b46b1 merge: lane/daemon 2026-09-15 17:57:36 +04:00
sami 8abfc6a428 merge: lane/core 2026-09-15 17:57:36 +04:00
samiandClaude Sonnet 5 936136db06 core: introduce the file_ops platform seam (ADR 0020 Phase 0)
Pure refactor, zero behaviour change. posix_fallocate/ftruncate,
posix_fadvise(DONTNEED), and fdatasync move out of sparse_file.cpp and
meta/veloxpart.cpp into core/src/io/platform/linux/file_ops.cpp behind
a new seam (core/src/io/platform/file_ops.hpp). Neither caller names a
Linux-only syscall anymore, and no #ifdef exists outside platform/linux/.
Clears the way for the PORT lane's macOS backend.

Full suite: 44/44 green (build/dev).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:56:02 +04:00
samiandClaude Sonnet 5 6d7c4f7bc4 daemon: Phase 0 platform seams (ADR 0020) — eventfd/SO_PEERCRED/instance-lock/XDG behind platform/, zero behaviour change
Introduces the four rpc-layer seams docs/08-porting.md calls for this lane:
platform::Wakeup (eventfd), platform::peer_of (SO_PEERCRED/struct ucred, same-UID
check preserved unchanged), platform::acquire_instance_lock (abstract-namespace
socket), platform::runtime_base_dir/data_base_dir (XDG lookups). Today's Linux
code moves unchanged into daemon/src/rpc/platform/linux/; the seam headers
carry no OS types and no #ifdef.

No fifth interface for timerfd: EventLoop gains a portable add_timer() that
folds the next deadline into poll()'s own timeout, replacing both timerfd
instances in main.cpp — the loop already computes a deadline, so this needs no
per-OS backend at all.

Full suite 59/59 green; no #ifdef outside platform/linux/, no behaviour change.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:54:32 +04:00
samiandClaude Sonnet 5 9487202ea8 gui: ship the real icon set, wire toolbar/tray/window icon, rebase onto main
Adwaita-derived (GNOME Project, LGPL-3-or-CC-BY-SA), recoloured per light/dark
skin and laid out in IDM's toolbar positions per docs/03-gui-spec.md §1.
Licence and per-file provenance recorded in gui/resources/icons/LICENSE.

New IconTheme mirrors ThemeManager's colorSchemeChanged hook so a live
light/dark switch swaps every action's glyph, not just the QSS. Toolbar/menu
actions, the tray icon and the window icon all now use it instead of the
SP_ArrowDown placeholder. Install-path notes for PKG filed as R5 in
gui/docs/pkg-qa-requests-m1.md.

Also rebases lane/gui onto main (13 commits, no gui/ conflicts).

Verified: full build clean, all 13 gui ctest targets pass (including
gui_no_download_logic), unhappy-path DoD gate PASS against mockd
(--drop-connection).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-15 17:50:51 +04:00
samiandClaude Opus 5 9624e685af docs: cmake/platform.cmake belongs to PKG/QA, not PORT (ADR 0020 amendment)
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s
As written, ADR 0020 gave PORT ownership of cmake/platform*.cmake while also
making PORT wait for Phase 0 — but Phase 0's seams are exactly what needs
velox_platform_sources() to select platform/linux/*.cpp. PORT cannot start until
Phase 0 lands, and Phase 0 cannot land without the file PORT owns. PKG/QA caught
the circularity before writing anything, which was the right call.

Central OS detection is root build infrastructure, so it stays with PKG/QA and
lands during Phase 0. PORT keeps the per-OS backend directories and the macOS
and Windows packaging, and owns no part of cmake/.

Also drops a hardcoded test count from the PORT definition of done; the suite
number moves every round and the gate is that it stays green.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-15 17:46:48 +04:00
82 changed files with 1343 additions and 191 deletions
+14
View File
@@ -45,3 +45,17 @@ crash-*
oom-*
leak-*
timeout-*
# dpkg-buildpackage output — debhelper's build tree and the produced .deb/.changes, plus
# the classic out-of-tree autotools-style build dir some debhelper versions still create.
# debian/control, changelog, copyright, rules, postinst, postrm, source/format and
# *.lintian-overrides are real packaging source and stay tracked; everything below is
# regenerated by `dpkg-buildpackage` on every run.
debian/.debhelper/
debian/velox/
debian/files
debian/*.substvars
debian/*.debhelper.log
debian/*.debhelper
debian/debhelper-build-stamp
obj-*/
+1 -1
View File
@@ -12,7 +12,7 @@ Read this before touching anything. Then read your lane brief in [docs/agents/](
| GUI | `gui/` | everything else |
| EXT | `extension/` | everything else |
| PKG/QA | `packaging/`, `.github/`, `tools/testserver/`, `tests/integration/`, `tests/e2e/`, root build files | any lane's feature code |
| PORT | `**/platform/<os>/**`, `cmake/platform*.cmake`, `packaging/macos/`, `packaging/windows/` | all logic — see [ADR 0020](docs/adr/0020-cross-platform-strategy.md) |
| PORT | `**/platform/<os>/**`, `packaging/macos/`, `packaging/windows/` | all logic, and `cmake/` — see [ADR 0020](docs/adr/0020-cross-platform-strategy.md) |
If your task seems to require editing another lane's files, that is a signal the interface
is wrong. **File the request; don't reach across.**
+79 -3
View File
@@ -29,6 +29,10 @@ option(VELOX_BUILD_FUZZ "Build libFuzzer targets (clang only)" OFF)
option(VELOX_ENABLE_MEDIA "Build the HLS/DASH media grabber (M4)" OFF)
option(VELOX_WERROR "Treat warnings as errors" ON)
# Central OS detection (docs/adr/0020-cross-platform-strategy.md). Included early: the
# dependency finds below gate Linux-only libraries on VELOX_OS_LINUX.
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/platform.cmake)
# Project-wide warning flags. Set via add_compile_options (a directory property), not the
# CMAKE_CXX_FLAGS cache variable, because the dev/tsan presets overwrite that cache var
# wholesale for sanitizer flags — a target's warnings must not ride on it.
@@ -55,12 +59,24 @@ endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/daemon/CMakeLists.txt)
find_package(SQLite3 REQUIRED)
find_package(nlohmann_json 3.11 REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1)
# libsecret / Secret Service is Linux-only (docs/adr/0020-cross-platform-strategy.md,
# docs/08-porting.md): macOS uses Keychain behind the same credential-store seam, so
# this stays REQUIRED on Linux and simply absent elsewhere — PORT's job is to add the
# macOS side of that seam, not to touch this find.
if(VELOX_OS_LINUX)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1)
endif()
endif()
if(VELOX_BUILD_GUI AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/gui/CMakeLists.txt)
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools)
# DBus (gui/docs/pkg-qa-requests-m1.md R4) backs the org.freedesktop.portal
# GlobalShortcuts path — Linux/portal-only, same reasoning as libsecret above.
if(VELOX_OS_LINUX)
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network DBus LinguistTools)
else()
find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools)
endif()
qt_standard_project_setup()
endif()
@@ -105,3 +121,63 @@ if(VELOX_BUILD_TESTS)
endif()
endforeach()
endif()
# --- Install rules (M6 packaging; docs/07-packaging.md; owned by PKG/QA) ------------
#
# Guarded by `if(TARGET ...)`, not by editing each lane's own CMakeLists.txt: install()
# is a packaging concern, and reaching into daemon/CMakeLists.txt (DAEMON's), cli's build
# file (also DAEMON's) or gui/CMakeLists.txt (GUI's) to add it there would cross a lane
# boundary CLAUDE.md draws on purpose. Every target below is defined by its own lane;
# this only says where the packaged binary already built by that lane's rules goes.
#
# libveloxcore is intentionally absent here: it stays a static library linked into each
# binary (user decision, see docs/07-packaging.md) — there is no .so to install.
#
# CMAKE_INSTALL_LIBDIR is multiarch-adjusted (lib/x86_64-linux-gnu/) by GNUInstallDirs on
# Debian; systemd user units and the Mozilla native-messaging directory are NOT
# architecture-specific paths, so those two destinations are written literally
# (lib/systemd/user, lib/mozilla/...) rather than built from that variable.
include(GNUInstallDirs)
if(TARGET veloxd)
install(TARGETS veloxd RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${CMAKE_SOURCE_DIR}/packaging/man/veloxd.8
DESTINATION ${CMAKE_INSTALL_MANDIR}/man8)
# velox.socket ships alongside velox.service: DAEMON wired real sd_listen_fds()
# socket activation (daemon/src/rpc/systemd_activation.cpp) — see
# packaging/systemd/README.md for the pair's own rationale.
install(FILES ${CMAKE_SOURCE_DIR}/packaging/systemd/velox.service
${CMAKE_SOURCE_DIR}/packaging/systemd/velox.socket
DESTINATION lib/systemd/user)
endif()
if(TARGET velox)
install(TARGETS velox RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
# DAEMON owns cli/ and its man page (CLAUDE.md); packaging only installs it.
install(FILES ${CMAKE_SOURCE_DIR}/cli/man/velox.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
endif()
if(TARGET velox-gui)
install(TARGETS velox-gui RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${CMAKE_SOURCE_DIR}/packaging/man/velox-gui.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/desktop/velox.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
foreach(iconsize 16 22 24 32 48 64 128 256)
install(FILES
${CMAKE_SOURCE_DIR}/packaging/icons/hicolor/${iconsize}x${iconsize}/apps/velox.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${iconsize}x${iconsize}/apps)
endforeach()
endif()
# nmhost and its manifest are DAEMON's (CLAUDE.md: nmhost/ and packaging/nativehost/).
# This lights up the moment DAEMON's target exists, the same way the add_subdirectory()
# guards above do — no coordinated edit needed when it lands.
if(TARGET velox-nmhost)
install(TARGETS velox-nmhost RUNTIME DESTINATION libexec/velox)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/nativehost/com.velox.host.json
DESTINATION lib/mozilla/native-messaging-hosts)
else()
message(STATUS "velox: nmhost has not landed yet — native-messaging manifest not installed.")
endif()
+45
View File
@@ -0,0 +1,45 @@
# cmake/platform.cmake — central OS detection and per-OS source selection.
#
# Owned by PKG/QA (docs/adr/0020-cross-platform-strategy.md §4-5, amended: PORT owns
# **/platform/<os>/** and the macOS/Windows packaging trees, but this file is root build
# infrastructure and lands in Phase 0 — CORE's and DAEMON's own seams need
# velox_platform_sources() to select their platform/linux/*.cpp before PORT ever starts).
#
# Sets exactly one of VELOX_OS_LINUX / VELOX_OS_MACOS / VELOX_OS_WINDOWS. Selection lives
# here and nowhere else — no lane's own CMakeLists.txt should reimplement this check.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(VELOX_OS_LINUX TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(VELOX_OS_MACOS TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(VELOX_OS_WINDOWS TRUE)
else()
message(FATAL_ERROR "velox: unsupported CMAKE_SYSTEM_NAME '${CMAKE_SYSTEM_NAME}' — "
"expected Linux, Darwin or Windows.")
endif()
# velox_platform_sources(<target> <dir>) adds <dir>/platform/<os>/*.cpp to <target>, where
# <os> is linux, macos or windows to match this file's VELOX_OS_* selection. <dir> is
# relative to the calling lane's own CMakeLists.txt (e.g. src/io, src/rpc) — the seam
# headers themselves (<dir>/platform/*.hpp) are not globbed here, they're ordinary sources
# the owning lane already lists.
function(velox_platform_sources target dir)
if(VELOX_OS_LINUX)
set(os_dir "linux")
elseif(VELOX_OS_MACOS)
set(os_dir "macos")
elseif(VELOX_OS_WINDOWS)
set(os_dir "windows")
endif()
file(GLOB platform_sources CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/${dir}/platform/${os_dir}/*.cpp")
if(NOT platform_sources)
message(WARNING "velox: velox_platform_sources(${target} ${dir}) found no "
"sources under ${dir}/platform/${os_dir}/ — is the seam missing "
"its ${os_dir} backend?")
endif()
target_sources(${target} PRIVATE ${platform_sources})
endfunction()
+5
View File
@@ -32,6 +32,11 @@ add_library(veloxcore STATIC
)
add_library(velox::core ALIAS veloxcore)
# Per-OS backend behind src/io/platform/*.hpp (docs/adr/0020, docs/08-porting.md). Only
# the Linux backend exists (Phase 0); source selection lives in cmake/platform.cmake and
# nowhere else, so PORT's platform/macos/ addition needs no edit here.
velox_platform_sources(veloxcore src/io)
target_include_directories(veloxcore
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # net/*.cpp -> "net/curl_error.hpp"
+38
View File
@@ -0,0 +1,38 @@
// vdm/io/platform/file_ops.hpp — platform seam for the three Linux-only file-I/O
// primitives sparse_file.cpp and meta/veloxpart.cpp use (ADR 0020, docs/08-porting.md).
//
// One implementation file per OS under platform/<os>/, selected at build time. No #ifdef
// here, no OS-specific types in the signatures — a reader of the callers must not need to
// know which OS they're on. Linux (platform/linux/file_ops.cpp) is the reference
// implementation and is the only one that exists until the PORT lane adds macOS.
//
// This header compiles standalone.
#ifndef VDM_IO_PLATFORM_FILE_OPS_HPP
#define VDM_IO_PLATFORM_FILE_OPS_HPP
#include <cstdint>
#include "vdm/util/result.hpp"
namespace vdm::io::platform {
// Reserve `bytes` for `fd` so a large transfer doesn't fragment or hit ENOSPC mid-write.
// Linux: posix_fallocate, falling back to ftruncate on EOPNOTSUPP/ENOSYS/EINVAL exactly as
// SparseFile did before this seam existed. Returns whether a true extent-reserving
// preallocation happened (false when the ftruncate fallback was used) so callers can keep
// reporting SparseFile::preallocated() unchanged.
[[nodiscard]] Result<bool> preallocate(int fd, std::uint64_t bytes);
// Drop cached pages for [offset, offset+len) from the OS page cache, best-effort. A
// platform with no equivalent (macOS) is a documented no-op, not a failure — never let
// this block or fail the transfer path.
void advise_dontneed(int fd, std::uint64_t offset, std::uint64_t len) noexcept;
// Flush `fd`'s data to durable storage without waiting on metadata that doesn't affect
// data readback. Linux: fdatasync.
[[nodiscard]] Result<void> flush_durable(int fd);
} // namespace vdm::io::platform
#endif // VDM_IO_PLATFORM_FILE_OPS_HPP
+73
View File
@@ -0,0 +1,73 @@
// vdm/io/platform/linux/file_ops.cpp — Linux backend for the file_ops seam.
//
// Reference implementation (ADR 0020): moved unchanged from sparse_file.cpp and
// meta/veloxpart.cpp. Linux behaviour must never change as a result of a port; if a port
// needs a semantic change here, that's an ADR, not a seam edit.
#include "io/platform/file_ops.hpp"
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <string>
namespace vdm::io::platform {
namespace {
Error errno_to_error(int e) noexcept {
switch (e) {
case ENOSPC:
case EDQUOT:
return Error::disk_full;
case EACCES:
case EPERM:
case EROFS:
return Error::permission_denied;
case ENOENT:
case ENOTDIR:
case EISDIR:
case ENAMETOOLONG:
case ELOOP:
return Error::path_rejected;
default:
return Error::io_error;
}
}
ErrorInfo sys_error(std::string_view what, int e) {
return ErrorInfo(errno_to_error(e), std::string(what) + ": " + std::strerror(e));
}
} // namespace
Result<bool> preallocate(int fd, std::uint64_t bytes) {
// posix_fallocate returns the error number directly and does not set errno.
int rc = ::posix_fallocate(fd, 0, static_cast<off_t>(bytes));
if (rc == 0)
return true;
if (rc == EOPNOTSUPP || rc == ENOSYS || rc == EINVAL) {
if (::ftruncate(fd, static_cast<off_t>(bytes)) != 0)
return sys_error("ftruncate", errno);
return false;
}
return sys_error("posix_fallocate", rc);
}
void advise_dontneed(int fd, std::uint64_t offset, std::uint64_t len) noexcept {
if (len == 0)
return;
::posix_fadvise(fd, static_cast<off_t>(offset), static_cast<off_t>(len), POSIX_FADV_DONTNEED);
}
Result<void> flush_durable(int fd) {
while (::fdatasync(fd) != 0) {
if (errno == EINTR)
continue;
return sys_error("fdatasync", errno);
}
return ok();
}
} // namespace vdm::io::platform
+8 -19
View File
@@ -9,6 +9,8 @@
#include <cstring>
#include <utility>
#include "io/platform/file_ops.hpp"
namespace vdm::io {
namespace {
@@ -90,20 +92,12 @@ Result<void> SparseFile::open(std::string_view path, const OpenOptions &opts) {
bool prealloc = false;
if (opts.total_size > 0) {
if (opts.preallocate) {
// posix_fallocate returns the error number directly and does not set errno.
int rc = ::posix_fallocate(fd, 0, static_cast<off_t>(opts.total_size));
if (rc == 0) {
prealloc = true;
} else if (rc == EOPNOTSUPP || rc == ENOSYS || rc == EINVAL) {
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
int e = errno;
::close(fd);
return sys_error("ftruncate " + p, e);
}
} else {
Result<bool> r = platform::preallocate(fd, opts.total_size);
if (!r.has_value()) {
::close(fd);
return sys_error("posix_fallocate " + p, rc);
return std::move(r).error();
}
prealloc = *r;
} else if (!opts.truncate_existing) {
// Resuming: make sure the file is at least total_size so pwrite offsets land.
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
@@ -147,18 +141,13 @@ Result<void> SparseFile::write_at(std::uint64_t offset, ConstByteSpan data) {
Result<void> SparseFile::sync() {
if (fd_ < 0)
return ErrorInfo(Error::internal, "sync on a closed SparseFile");
while (::fdatasync(fd_) != 0) {
if (errno == EINTR)
continue;
return sys_error("fdatasync", errno);
}
return ok();
return platform::flush_durable(fd_);
}
void SparseFile::advise_dontneed(std::uint64_t offset, std::uint64_t len) noexcept {
if (fd_ < 0 || len == 0)
return;
::posix_fadvise(fd_, static_cast<off_t>(offset), static_cast<off_t>(len), POSIX_FADV_DONTNEED);
platform::advise_dontneed(fd_, offset, len);
}
Result<void> SparseFile::resize(std::uint64_t size) {
+4 -5
View File
@@ -13,6 +13,7 @@
#include <string>
#include <utility>
#include "io/platform/file_ops.hpp"
#include "vdm/util/crc32.hpp"
namespace vdm::meta {
@@ -272,13 +273,11 @@ Result<void> write_veloxpart_file(std::string_view path, const VeloxPart &vp, bo
}
if (fsync) {
while (::fdatasync(fd) != 0) {
if (errno == EINTR)
continue;
int e = errno;
Result<void> r = vdm::io::platform::flush_durable(fd);
if (!r.has_value()) {
::close(fd);
::unlink(tmp.c_str());
return sys_error("fdatasync " + tmp, e);
return ErrorInfo(r.error().code, "fdatasync " + tmp + ": " + r.error().context);
}
}
if (::close(fd) != 0) {
+5 -1
View File
@@ -74,7 +74,6 @@ target_link_libraries(veloxd_sched PUBLIC velox::proto velox::core veloxd_store
# --- veloxd_rpc — the RPC transports + dispatcher ------------------------------------
add_library(veloxd_rpc STATIC
src/rpc/runtime_dir.cpp
src/rpc/single_instance.cpp
src/rpc/systemd_activation.cpp
src/rpc/event_loop.cpp
src/rpc/event_hub.cpp
@@ -87,6 +86,11 @@ add_library(veloxd_rpc STATIC
)
add_library(velox::daemon_rpc ALIAS veloxd_rpc)
# Per-OS backends behind daemon/src/rpc/platform/*.hpp (docs/adr/0020, docs/08-porting.md).
# Source selection lives in cmake/platform.cmake and nowhere else — PORT adds
# platform/macos/ with no edit here.
velox_platform_sources(veloxd_rpc src/rpc)
target_include_directories(veloxd_rpc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(veloxd_rpc PUBLIC cxx_std_23)
target_compile_options(veloxd_rpc PRIVATE -Wall -Wextra -Wpedantic -Werror)
+1
View File
@@ -27,4 +27,5 @@ close. Kept here (not buried in commit messages) so the next pass can see them a
| ~~D5~~ | **Mostly closed** — `rpc/event_hub` fans out per-subscription; `session.subscribe` on both transports registers/updates/tears down a real subscription; `Scheduler::transition()` publishes `event.task.state` (with `previousState`) on every state change, scheduler-driven or engine-reported; `dispatcher::on_download_add` publishes `event.task.added`; a 250 ms timer batches `Scheduler::progress_snapshot()` into one `event.task.progress` array per AGENT-DAEMON.md item 5 / the schema's `x-maxRateHz: 4`. Verified live end to end. | — | `event.task.removed` has no source yet (`download.remove` is D3); `event.speed.global`, `event.notify`, `event.auth.required`, `event.settings.changed`, `event.grabber.progress` are unpublished — each lands with its owning handler | as each owning D3 handler lands |
| ~~D6~~ | **Closed** — engine numbers now reach the store: `Scheduler::tick()` probes (`EnginePort::probe`) before every `start()`, persisting `sizeBytes`/`resumable`/validators via `Tasks::set_probe_result` before a byte moves; `Scheduler::persist_progress()` (called from `progress_snapshot()` *and* once more from `on_engine_state` right before `release()`/unmap on every terminal transition) writes `downloadedBytes`/`speedBps`/`segments`/`segmentDetail` from the engine's `Progress`, so a task that finishes between two 250 ms ticks (the common case for anything small or fast) still leaves real numbers instead of the pre-persistence defaults. `TaskSummary.segments` is sourced from `segments.size()` when the task has any (matching what actually lands in `segmentDetail`, per the schema's "exactly `segments` entries"), falling back to the engine's `effective_segments` (budget slots *held*, not necessarily physical range count — see `core/include/vdm/task/download.hpp`'s `Progress` comment) only pre-segmentation. `Tasks::set_final_bytes` tops up `on_finished`'s byte count as a last-resort backstop. Migration `0002` adds `speed_bps` to both `tasks` and `segments`, and fixes `segments.state`'s CHECK to include `'pending'` (0001 omitted it, so a pre-connect snapshot could never be written). Verified against real `veloxd` + `tools/testserver` (not just unit tests): `download.list`/`download.get` correct immediately after completion and after a daemon restart. | `sched/scheduler.{cpp,hpp}`, `store/{tasks,segments}.{cpp,hpp}`, `store/migrations/0002_*.sql` | — | done |
| ~~D11~~ | **Closed — build order items 7 (the systemd half) and 9: `velox-nmhost`, socket activation, the systemd user units, and `velox(1)`.** `nmhost/src/main.cpp` (185 lines): a `poll()`-driven byte pump between Firefox's native-messaging framing on stdio (4-byte native-byte-order length prefix) and `veloxd`'s own NDJSON framing on the Unix socket — reframes each direction, no JSON parsing, no retry/backoff, exits the moment either side closes. Deliberately dependency-free (no `veloxd_*` library, no `nlohmann_json`) since it runs unconfined outside Firefox's sandbox whatever the packaging format. Two real bugs found and fixed while getting the integration test to actually pass rather than hang: (1) never set the pumped fds non-blocking, so the "drain what's available" read loop blocked on its own second `read()` instead of returning to `poll()`; (2) stdin and stdout are two different descriptors (0 and 1), not one — an early draft polled `POLLOUT` on fd 0, which is opened read-only, so EOF/writability were never both observable through the same `pollfd` entry. Both are exactly the class of bug a "trivial pump" invites and unit tests over the real binary (not just its helper functions) exist specifically to catch. `packaging/nativehost/com.velox.host.json` + its own `README.md` supersede `AGENT-DAEMON.md`'s stale "four locations" (spike S1 / ADR 0003 found only three are real — the fourth, `~/snap/firefox/common/.mozilla/...`, is not read by snap Firefox at all) and spell out the per-user-manifest / postinst implication for PKG/QA. `EnginePort`-style: `rpc/systemd_activation.cpp` is a from-scratch `sd_listen_fds()` (env vars only, no `libsystemd` link — `LISTEN_PID`/`LISTEN_FDS`, fd 3) that `UdsServer::start()` checks first, skipping its own create/bind/chmod/listen when systemd already bound the socket; `packaging/systemd/velox.socket` + `velox.service` are the unit pair, verified both by `systemd-analyze verify` and by an actual fork/dup2/execve simulation of the activation handshake (a real `session.hello` round-tripped over the handed-off fd with no `bind()` ever called inside the daemon for that run). `velox.service` deliberately skips `ProtectSystem=`/`ProtectHome=`/`ReadWritePaths=` — `saveTo.allowedRoots` is user-configurable to anywhere on the filesystem, and a sandbox here would turn a legitimately-configured save location into an opaque `EROFS`/`EACCES` instead of the daemon's own clear `-32011`. `cli/man/velox.1` documents the CLI as it actually exists today (`add`/`ls`/`pause`/`resume`/`rm`, `--json`, the three-tier `queue`/`settings` subcommands `AGENT-DAEMON.md` build step 8 originally sketched are not implemented in `cli/src/main.cpp` yet, so the page doesn't claim they are) — checked warning-free with `groff -mandoc -ww -z`. | `nmhost/{CMakeLists.txt,src/main.cpp,tests/}`, `daemon/src/rpc/{systemd_activation.{hpp,cpp},uds_server.cpp}`, `packaging/{nativehost,systemd}/`, `cli/man/velox.1` | — | done |
| ~~D12~~ | **Closed — ADR 0020 Phase 0 for `daemon/`.** Pure refactor, zero behaviour change: the four Linux-only surfaces docs/08-porting.md lists for this lane now sit behind seams in `daemon/src/rpc/platform/{wakeup,peer_id,instance_lock,runtime_paths}.hpp`, with today's syscalls moved unchanged into `platform/linux/*.cpp` (`eventfd` -> `platform::Wakeup`, `SO_PEERCRED`/`struct ucred` -> `platform::peer_of` — same-UID check preserved verbatim as the security property, not touched — abstract-namespace socket -> `platform::acquire_instance_lock`, XDG env lookups -> `platform::runtime_base_dir`/`data_base_dir`, with the 0700-and-owned enforcement itself staying portable POSIX logic in `rpc/runtime_dir.cpp` rather than moving). No fifth seam for `timerfd`: both daemon timers (1 s scheduler tick, 250 ms progress batch) now go through a new portable `EventLoop::add_timer`, which folds the next timer deadline into the existing `poll()` timeout instead of a platform timer fd — exactly the case docs/08-porting.md calls out as not needing a backend ("the loop already has a deadline set"), so it stays in `rpc/event_loop.{hpp,cpp}` untouched by lane PORT. `daemon/CMakeLists.txt` lists the four `platform/linux/*.cpp` files directly (no `cmake/platform.cmake` yet — that and `platform/macos/` are PORT's, per the CLAUDE.md lane table added alongside ADR 0020); PORT's Phase 1 replaces the hardcoded list with `velox_platform_sources()`. Old `rpc/single_instance.{hpp,cpp}` deleted (folded into the new seam); `single_instance_test.cpp` retargeted at `rpc/platform/instance_lock.hpp` with no behaviour change. Full suite 59/59 green; `git diff` outside `platform/linux/` is moves/call-site updates only, no `#ifdef` anywhere in `daemon/`. | `rpc/platform/{wakeup,peer_id,instance_lock,runtime_paths}.hpp`, `rpc/platform/linux/*.cpp`, `rpc/event_loop.{hpp,cpp}`, `rpc/{uds_server,runtime_dir}.cpp`, `main.cpp`, `CMakeLists.txt` | — | done |
| — | ~~Observed, not fixed (CORE, not this lane)~~ — **routed to CORE by the user.** `vdm::task::Progress.speed_bps` reads back as `0` for the whole lifetime of a live, real (non-fake) throttled download, despite `downloadedBytes` visibly advancing between polls — `core/src/task/download_task.cpp`'s per-worker EWMA never seems to produce a nonzero aggregate in this build. DAEMON passes `EnginePort::progress()`'s `speed_bps` straight through (`Scheduler::persist_progress`); nothing in this lane drops it. Still reproduces in the D4b live checks above (0 throughout a paused/resumed/cancelled transfer whose `downloadedBytes` visibly moved) — not re-filed, since it's already CORE's. |
+30 -57
View File
@@ -5,6 +5,7 @@
// (AGENT-DAEMON.md build order, steps 1 and 3). The scheduler and the engine link land
// next.
#include <chrono>
#include <csignal>
#include <cstdlib>
#include <cstring>
@@ -15,16 +16,14 @@
#include <sys/un.h>
#include <unistd.h>
#include <sys/timerfd.h>
#include <nlohmann/json.hpp>
#include "rpc/dispatcher.hpp"
#include "rpc/event_hub.hpp"
#include "rpc/event_loop.hpp"
#include "rpc/pairing.hpp"
#include "rpc/platform/instance_lock.hpp"
#include "rpc/runtime_dir.hpp"
#include "rpc/single_instance.hpp"
#include "rpc/uds_server.hpp"
#include "rpc/ws_server.hpp"
#include "sched/engine_port_core.hpp"
@@ -56,7 +55,7 @@ int main() {
return 1;
}
const int lock_fd = velox::daemon::rpc::acquire_single_instance_lock(rt.path);
const int lock_fd = velox::daemon::rpc::platform::acquire_instance_lock(rt.path);
if (lock_fd < 0) {
std::cerr << "veloxd: another instance is already running for this runtime "
"directory (" << rt.path << ")\n";
@@ -109,57 +108,39 @@ int main() {
});
// A 1 s timer re-runs the scheduler so schedule windows opening/closing and any
// missed nudge are picked up. Registered on the loop, no extra thread.
const int tick_fd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (tick_fd >= 0) {
itimerspec spec{};
spec.it_value.tv_sec = 1;
spec.it_interval.tv_sec = 1;
::timerfd_settime(tick_fd, 0, &spec, nullptr);
loop.add_fd(tick_fd, velox::daemon::rpc::kRead, [&](int fd, unsigned) {
std::uint64_t ticks = 0;
[[maybe_unused]] ssize_t n = ::read(fd, &ticks, sizeof(ticks));
(void)scheduler.tick();
});
}
// missed nudge are picked up. Expressed as a poll() timeout behind EventLoop rather
// than a timerfd (docs/08-porting.md: "the loop already has a deadline set" — no
// per-OS backend needed here, unlike the other three daemon/rpc seams).
loop.add_timer(std::chrono::seconds(1), [&] { (void)scheduler.tick(); });
// event.task.progress: one array message at <=4 Hz (schema x-maxRateHz), never one
// notification per task (AGENT-DAEMON.md item 5). 250 ms keeps every active task's
// segment bar under 4 Hz without depending on how many tasks are running.
const int progress_fd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (progress_fd >= 0) {
itimerspec spec{};
spec.it_value.tv_nsec = 250'000'000;
spec.it_interval.tv_nsec = 250'000'000;
::timerfd_settime(progress_fd, 0, &spec, nullptr);
loop.add_fd(progress_fd, velox::daemon::rpc::kRead, [&](int fd, unsigned) {
std::uint64_t ticks = 0;
[[maybe_unused]] ssize_t n = ::read(fd, &ticks, sizeof(ticks));
const auto rows = scheduler.progress_snapshot();
if (rows.empty()) return;
loop.add_timer(std::chrono::milliseconds(250), [&] {
const auto rows = scheduler.progress_snapshot();
if (rows.empty()) return;
nlohmann::json tasks_json = nlohmann::json::array();
for (const auto& r : rows) {
nlohmann::json t{{"taskId", r.task_id},
{"downloadedBytes", r.downloaded_bytes},
{"speedBps", r.speed_bps}};
t["etaSeconds"] = r.eta_seconds ? nlohmann::json(*r.eta_seconds) : nlohmann::json(nullptr);
if (!r.segments.empty()) {
nlohmann::json segs = nlohmann::json::array();
for (const auto& s : r.segments)
segs.push_back({{"index", s.index},
{"downloadedBytes", s.downloaded_bytes},
{"speedBps", s.speed_bps}});
t["segments"] = std::move(segs);
}
tasks_json.push_back(std::move(t));
nlohmann::json tasks_json = nlohmann::json::array();
for (const auto& r : rows) {
nlohmann::json t{{"taskId", r.task_id},
{"downloadedBytes", r.downloaded_bytes},
{"speedBps", r.speed_bps}};
t["etaSeconds"] = r.eta_seconds ? nlohmann::json(*r.eta_seconds) : nlohmann::json(nullptr);
if (!r.segments.empty()) {
nlohmann::json segs = nlohmann::json::array();
for (const auto& s : r.segments)
segs.push_back({{"index", s.index},
{"downloadedBytes", s.downloaded_bytes},
{"speedBps", s.speed_bps}});
t["segments"] = std::move(segs);
}
const nlohmann::json params{{"tasks", std::move(tasks_json)},
{"at", velox::daemon::now_iso()}};
hub.publish(velox::proto::Event::TaskProgress,
velox::proto::make_notification(velox::proto::Event::TaskProgress, params));
});
}
tasks_json.push_back(std::move(t));
}
const nlohmann::json params{{"tasks", std::move(tasks_json)},
{"at", velox::daemon::now_iso()}};
hub.publish(velox::proto::Event::TaskProgress,
velox::proto::make_notification(velox::proto::Event::TaskProgress, params));
});
velox::daemon::rpc::UdsServer uds(loop, dispatcher, hub, rt.socket_path(), &scheduler);
if (const auto ec = uds.start()) {
@@ -186,14 +167,6 @@ int main() {
loop.run();
std::cout << "veloxd: shutting down\n";
if (tick_fd >= 0) {
loop.del_fd(tick_fd);
::close(tick_fd);
}
if (progress_fd >= 0) {
loop.del_fd(progress_fd);
::close(progress_fd);
}
g_loop = nullptr;
::close(lock_fd);
return 0;
+44 -21
View File
@@ -1,9 +1,8 @@
#include "rpc/event_loop.hpp"
#include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <stdexcept>
@@ -12,14 +11,10 @@
namespace velox::daemon::rpc {
EventLoop::EventLoop() {
wake_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (wake_fd_ < 0) throw std::runtime_error("eventfd() failed");
fds_.emplace(wake_fd_, Entry{kRead, [this](int, unsigned) { drain_wakeup(); }});
fds_.emplace(wakeup_.pollfd(), Entry{kRead, [this](int, unsigned) { wakeup_.drain(); }});
}
EventLoop::~EventLoop() {
if (wake_fd_ >= 0) ::close(wake_fd_);
}
EventLoop::~EventLoop() = default;
void EventLoop::add_fd(int fd, unsigned interest, Callback cb) {
fds_[fd] = Entry{interest, std::move(cb)};
@@ -30,15 +25,11 @@ void EventLoop::mod_fd(int fd, unsigned interest) {
}
void EventLoop::del_fd(int fd) {
if (fd == wake_fd_) return; // internal, never removed
if (fd == wakeup_.pollfd()) return; // internal, never removed
fds_.erase(fd);
}
void EventLoop::wake() noexcept {
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(wake_fd_, &one, sizeof(one));
}
void EventLoop::wake() noexcept { wakeup_.signal(); }
void EventLoop::stop() noexcept {
stop_requested_ = true;
@@ -62,9 +53,39 @@ void EventLoop::drain_posts() {
for (auto& fn : batch) fn();
}
void EventLoop::drain_wakeup() noexcept {
std::uint64_t sink = 0;
while (::read(wake_fd_, &sink, sizeof(sink)) > 0) {
EventLoop::TimerId EventLoop::add_timer(std::chrono::milliseconds interval,
std::function<void()> cb) {
const TimerId id = next_timer_id_++;
timers_.emplace(id, Timer{std::chrono::steady_clock::now() + interval, interval,
std::move(cb)});
wake(); // the loop may already be blocked on a longer timeout
return id;
}
void EventLoop::remove_timer(TimerId id) { timers_.erase(id); }
int EventLoop::next_timeout_ms() const {
if (timers_.empty()) return -1;
auto soonest = timers_.begin()->second.next;
for (const auto& [id, t] : timers_) soonest = std::min(soonest, t.next);
const auto now = std::chrono::steady_clock::now();
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(soonest - now);
return ms.count() > 0 ? static_cast<int>(ms.count()) : 0;
}
void EventLoop::run_due_timers() {
const auto now = std::chrono::steady_clock::now();
// Snapshot ids first: a callback may add/remove timers, which would invalidate
// iteration over timers_ directly.
std::vector<TimerId> due;
for (auto& [id, t] : timers_) {
if (t.next <= now) due.push_back(id);
}
for (const TimerId id : due) {
const auto it = timers_.find(id);
if (it == timers_.end()) continue; // removed by an earlier callback this pass
it->second.next = now + it->second.interval;
it->second.cb();
}
}
@@ -83,18 +104,22 @@ void EventLoop::run() {
short ev = 0;
if (e.interest & kRead) ev |= POLLIN;
if (e.interest & kWrite) ev |= POLLOUT;
if (ev == 0 && fd != wake_fd_) continue;
if (ev == 0 && fd != wakeup_.pollfd()) continue;
pollfd p{};
p.fd = fd;
p.events = ev;
pfds.push_back(p);
}
const int rc = ::poll(pfds.data(), pfds.size(), -1);
const int rc = ::poll(pfds.data(), pfds.size(), next_timeout_ms());
if (rc < 0) {
if (errno == EINTR) continue;
throw std::runtime_error("poll() failed");
}
drain_posts();
run_due_timers();
if (rc == 0) continue;
// Snapshot the fds that fired before invoking any callback: a callback may erase
@@ -104,8 +129,6 @@ void EventLoop::run() {
if (p.revents != 0) fired.push_back(p.fd);
}
drain_posts();
for (const int fd : fired) {
const auto it = fds_.find(fd);
if (it == fds_.end()) continue; // removed by an earlier callback this pass
+27 -3
View File
@@ -10,12 +10,15 @@
// from any thread or a signal handler — they only write() a byte to an internal eventfd.
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <vector>
#include "rpc/platform/wakeup.hpp"
namespace velox::daemon::rpc {
enum Interest : unsigned {
@@ -58,22 +61,43 @@ public:
// marshal an engine-thread callback back onto the RPC loop.
void post(std::function<void()> fn);
using TimerId = std::uint64_t;
// Fire `cb` roughly every `interval` for as long as the loop runs, expressed as a
// poll(2) timeout rather than a platform timer fd (docs/08-porting.md: "the loop
// already has a deadline set" — no per-OS backend needed for this one). Not
// reentrant-safe to call from inside a timer callback other than the one running.
TimerId add_timer(std::chrono::milliseconds interval, std::function<void()> cb);
void remove_timer(TimerId id);
private:
struct Entry {
unsigned interest;
Callback cb;
};
void drain_wakeup() noexcept;
void drain_posts();
struct Timer {
std::chrono::steady_clock::time_point next;
std::chrono::milliseconds interval;
std::function<void()> cb;
};
int wake_fd_; // eventfd, always registered
void drain_posts();
// Milliseconds until the next timer is due, or -1 if there are none (poll()'s "block
// forever" convention).
int next_timeout_ms() const;
void run_due_timers();
platform::Wakeup wakeup_;
bool running_ = false;
std::atomic<bool> stop_requested_ = false; // set from stop(), read by run()
std::unordered_map<int, Entry> fds_;
std::mutex post_mu_;
std::vector<std::function<void()>> posts_;
TimerId next_timer_id_ = 1;
std::unordered_map<TimerId, Timer> timers_;
};
} // namespace velox::daemon::rpc
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Single-instance guard, keyed by the resolved runtime directory (see rpc/runtime_dir.hpp)
// so isolated instances pointed at different runtime dirs never contend (docs/01 §2). The
// mechanism is Linux's abstract-namespace Unix socket; macOS has no abstract namespace and
// uses a real socket file plus flock() instead, which must unlink a stale socket left by a
// crashed process (docs/08-porting.md "API mapping" — the abstract version got that for
// free from the kernel).
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other error acquiring it.
int acquire_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc::platform
@@ -1,4 +1,4 @@
#include "rpc/single_instance.hpp"
#include "rpc/platform/instance_lock.hpp"
#include <cstddef>
#include <cstring>
@@ -9,9 +9,9 @@
#include "util/crypto.hpp"
namespace velox::daemon::rpc {
namespace velox::daemon::rpc::platform {
int acquire_single_instance_lock(const std::string& runtime_dir) {
int acquire_instance_lock(const std::string& runtime_dir) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return -1;
@@ -35,4 +35,4 @@ int acquire_single_instance_lock(const std::string& runtime_dir) {
return fd;
}
} // namespace velox::daemon::rpc
} // namespace velox::daemon::rpc::platform
+17
View File
@@ -0,0 +1,17 @@
#include "rpc/platform/peer_id.hpp"
#include <sys/socket.h>
namespace velox::daemon::rpc::platform {
std::error_code peer_of(int fd, PeerId& out) {
ucred cred{};
socklen_t len = sizeof(cred);
if (::getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
return std::error_code(errno, std::generic_category());
}
out.uid = cred.uid;
return {};
}
} // namespace velox::daemon::rpc::platform
@@ -0,0 +1,44 @@
#include "rpc/platform/runtime_paths.hpp"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
namespace velox::daemon::rpc::platform {
namespace {
std::error_code errc(int e) { return std::error_code(e, std::generic_category()); }
} // namespace
std::error_code runtime_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
const std::string base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: refuse rather than pick an insecure
// fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
out = base;
return {};
}
std::error_code data_base_dir(std::string& out) {
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') {
out = xdg;
return {};
}
if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
out = std::string(home) + "/.local/share";
return {};
}
return errc(ENOENT);
}
} // namespace velox::daemon::rpc::platform
+32
View File
@@ -0,0 +1,32 @@
#include "rpc/platform/wakeup.hpp"
#include <sys/eventfd.h>
#include <unistd.h>
#include <cstdint>
#include <stdexcept>
namespace velox::daemon::rpc::platform {
Wakeup::Wakeup() {
fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd_ < 0) throw std::runtime_error("eventfd() failed");
}
Wakeup::~Wakeup() {
if (fd_ >= 0) ::close(fd_);
}
void Wakeup::signal() noexcept {
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(fd_, &one, sizeof(one));
}
void Wakeup::drain() noexcept {
std::uint64_t sink = 0;
while (::read(fd_, &sink, sizeof(sink)) > 0) {
}
}
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Identifies the process on the other end of a connected Unix-domain socket, for the
// same-UID check that is the Unix transport's authorization boundary (docs/01 §2,
// CLAUDE.md §4). `SO_PEERCRED`/`struct ucred` is Linux-only; macOS has `getpeereid`,
// Windows named pipes carry a token instead (docs/08-porting.md "The seams" /
// "API mapping"). The same-UID check itself is the security property and must not change
// per-OS (docs/adr/0020 decision 2).
#include <system_error>
namespace velox::daemon::rpc::platform {
struct PeerId {
unsigned int uid = 0;
};
// On success, fills `out` with the peer's identity of the already-connected `fd`. On
// failure, `out` is untouched and the error_code explains why (matches errno on Linux).
std::error_code peer_of(int fd, PeerId& out);
} // namespace velox::daemon::rpc::platform
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Where the OS wants ephemeral runtime state and persistent user data to live, before
// velox appends its own "/velox" subdirectory and applies the shared 0700-and-owned check
// (rpc/runtime_dir.cpp — that enforcement is portable POSIX logic and stays there; only
// "which base directory" is per-OS). Linux: XDG. macOS: $TMPDIR (runtime) and
// ~/Library/Application Support (data) — see docs/08-porting.md "API mapping".
#include <string>
#include <system_error>
namespace velox::daemon::rpc::platform {
// The base directory ephemeral runtime state (sockets, lock files) should live under,
// e.g. "$XDG_RUNTIME_DIR" or "/run/user/<uid>" on Linux. No trailing slash.
std::error_code runtime_base_dir(std::string& out);
// The base directory persistent user data should live under, e.g. "$XDG_DATA_HOME" or
// "~/.local/share" on Linux. No trailing slash.
std::error_code data_base_dir(std::string& out);
} // namespace velox::daemon::rpc::platform
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// The event-loop wakeup primitive (docs/adr/0020, docs/08-porting.md). EventLoop uses this
// to interrupt a blocked poll() from another thread or a signal handler — the eventfd
// mechanics themselves are Linux-only; every other OS backend just needs something
// poll()-able that signal()/drain() can drive the same way (docs/08 §"The seams": a
// self-pipe on macOS, an event object on Windows).
//
// One implementation file per OS under platform/<os>/wakeup.cpp; this header carries no
// OS types and no #ifdef (ADR 0020 decision 1).
namespace velox::daemon::rpc::platform {
class Wakeup {
public:
Wakeup();
~Wakeup();
Wakeup(const Wakeup&) = delete;
Wakeup& operator=(const Wakeup&) = delete;
// The fd to register with poll(2) for readability.
int pollfd() const noexcept { return fd_; }
// Make pollfd() readable. Async-signal-safe and thread-safe.
void signal() noexcept;
// Drain whatever signal() queued so pollfd() stops being readable. Call this from the
// loop thread once pollfd() fires.
void drain() noexcept;
private:
int fd_ = -1;
};
} // namespace velox::daemon::rpc::platform
+4 -19
View File
@@ -5,9 +5,10 @@
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <string>
#include "rpc/platform/runtime_paths.hpp"
namespace velox::daemon::rpc {
namespace {
@@ -33,17 +34,7 @@ std::error_code ensure_private_dir(const std::string& dir) {
std::error_code resolve_runtime_dir(RuntimeDir& out) {
std::string base;
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
base = xdg;
} else {
base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: we refuse rather than pick an
// insecure fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
}
if (auto ec = platform::runtime_base_dir(base)) return ec;
if (!base.empty() && base.back() == '/') base.pop_back();
const std::string dir = base + "/velox";
@@ -55,13 +46,7 @@ std::error_code resolve_runtime_dir(RuntimeDir& out) {
std::error_code resolve_data_dir(std::string& out) {
std::string base;
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') {
base = xdg;
} else if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
base = std::string(home) + "/.local/share";
} else {
return errc(ENOENT);
}
if (auto ec = platform::data_base_dir(base)) return ec;
if (!base.empty() && base.back() == '/') base.pop_back();
// Create the XDG base components leniently, then the velox dir with a strict check.
-27
View File
@@ -1,27 +0,0 @@
#pragma once
// Single-instance guard: bind an abstract-namespace Unix socket whose name is derived
// from the canonical runtime directory (resolve_runtime_dir's result — already the
// per-user default, /run/user/<uid>/velox, unless XDG_RUNTIME_DIR says otherwise). A
// second daemon pointed at the same runtime dir gets EADDRINUSE and exits; one pointed at
// a different (isolated / test) runtime dir gets its own lock and starts fine. The kernel
// reclaims an abstract-namespace address when the holding process dies, so a crash never
// wedges it (docs/01 §2).
//
// Naming this "velox-daemon-<euid>" alone (the old scheme) meant exactly one name per
// user system-wide, so XDG_RUNTIME_DIR isolation never reached it: a leaked test veloxd
// with the same euid held the lock for every isolated instance too, real or test, until
// it was killed. Hashing the resolved runtime dir path instead keeps the real per-user
// daemon unique (its runtime dir is unique to it) while letting isolated instances that
// each point at their own runtime dir coexist.
#include <string>
namespace velox::daemon::rpc {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other socket error.
int acquire_single_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc
+3 -4
View File
@@ -15,6 +15,7 @@
#include <fcntl.h>
#include "rpc/event_loop.hpp"
#include "rpc/platform/peer_id.hpp"
#include "rpc/systemd_activation.hpp"
#include "version.hpp"
@@ -140,10 +141,8 @@ void UdsServer::on_listener_readable() {
break; // EMFILE/ENFILE: stop accepting this pass; loop retries on next readable
}
ucred cred{};
socklen_t len = sizeof(cred);
if (::getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0 ||
cred.uid != ::geteuid()) {
platform::PeerId peer{};
if (platform::peer_of(cfd, peer) || peer.uid != ::geteuid()) {
// Not the same user. The socket mode should already prevent this; refuse hard
// regardless — this is the authorization on the Unix transport (docs/01 §2).
::close(cfd);
+7 -7
View File
@@ -5,15 +5,15 @@
#include <unistd.h>
#include "check.hpp"
#include "rpc/single_instance.hpp"
#include "rpc/platform/instance_lock.hpp"
using namespace velox::daemon::rpc;
using namespace velox::daemon::rpc::platform;
void run() {
// Two different runtime dirs: both acquire the lock independently.
{
const int a = acquire_single_instance_lock("/run/user/1000/velox-test-a");
const int b = acquire_single_instance_lock("/run/user/1000/velox-test-b");
const int a = acquire_instance_lock("/run/user/1000/velox-test-a");
const int b = acquire_instance_lock("/run/user/1000/velox-test-b");
CHECK(a >= 0);
CHECK(b >= 0);
if (a >= 0) ::close(a);
@@ -22,16 +22,16 @@ void run() {
// Same runtime dir: the second attempt is refused while the first still holds it.
{
const int first = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
const int first = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(first >= 0);
const int second = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
const int second = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(second < 0);
if (first >= 0) ::close(first);
if (second >= 0) ::close(second);
// Releasing (closing) the fd frees the abstract-namespace name immediately — a
// third attempt at the same dir succeeds once the first is gone.
const int third = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
const int third = acquire_instance_lock("/run/user/1000/velox-test-shared");
CHECK(third >= 0);
if (third >= 0) ::close(third);
}
+11
View File
@@ -0,0 +1,11 @@
velox (0.1.0-1) UNRELEASED; urgency=medium
* Local test build only. Not signed, not uploaded anywhere — see
packaging/README.md for what this package is and is not.
* libveloxcore is linked statically; no libveloxcore.so is shipped.
* Placeholder app icon (packaging/icons/) — replace before any real release.
* No real pairing-approval UI is built yet (needs libdbus-1-dev, not in this
build's Build-Depends): pairing requires VELOX_PAIR_AUTO=1. See
packaging/README.md and postinst's own notice.
-- Velox Local Test Build <[email protected]> Sat, 12 Sep 2026 22:30:43 +0400
+35
View File
@@ -0,0 +1,35 @@
Source: velox
Section: net
Priority: optional
Maintainer: Velox Local Test Build <[email protected]>
Build-Depends: debhelper-compat (= 13),
cmake (>= 3.28),
ninja-build,
pkg-config,
g++ (>= 13),
qt6-base-dev,
qt6-tools-dev,
qt6-tools-dev-tools,
qt6-svg-dev,
libcurl4-openssl-dev,
libssl-dev,
libsqlite3-dev,
nlohmann-json3-dev,
libsecret-1-dev
Standards-Version: 4.6.2
Rules-Requires-Root: no
Package: velox
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: IDM-class download manager for Linux (local test build)
Velox is a segmented, resumable download manager for Linux: a daemon
(veloxd) that does the transfer work, a Qt GUI, a CLI, and a Firefox
extension for capture.
.
This package ships veloxd, the velox CLI, the velox-gui Qt client and
velox-nmhost, the Firefox native-messaging host.
.
THIS IS A LOCAL TEST BUILD, not a release: it uses a placeholder app
icon and has no real pairing-approval UI (see /usr/share/doc/velox, or
packaging/README.md in the source tree, for both limitations).
+26
View File
@@ -0,0 +1,26 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: velox
Source: (no public upstream repository yet — local test build only)
Files: *
Copyright: 2026, the Velox project
License: Unlicensed-TODO
Files: debian/*
Copyright: 2026, the Velox project
License: Unlicensed-TODO
License: Unlicensed-TODO
No license has been chosen for Velox's own source yet. This is a genuine,
open project decision — not something packaging can invent on its own —
and is tracked as a release blocker in packaging/README.md. `lintian` is
expected to flag this package over it (a package normally must declare a
real license); that is correct behaviour on lintian's part, not a bug in
this packaging, and stays that way until the project adopts one.
.
Nothing above changes the licenses of this package's dynamically-linked
runtime dependencies (Qt, libcurl, SQLite, OpenSSL, nlohmann-json,
libsecret and friends) — those remain under their own upstream licenses,
covered by their own packages' Depends, and are not redistributed by this
source package. See packaging/README.md's licence-audit note for what
that still needs before a real release.
Vendored Executable
+92
View File
@@ -0,0 +1,92 @@
#!/bin/sh
# postinst for velox (local test build). See packaging/README.md.
set -e
NM_MANIFEST_SRC=/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
NM_MANIFEST_NAME=com.velox.host.json
# Every real (human) user with a home directory — UID range matches Debian's own
# adduser default (login.defs UID_MIN=1000), not just "everyone in /home" (a service
# account can have a home dir too) and not root.
real_users() {
getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $6 != "" {print $1":"$6}'
}
# Per-user native-messaging manifest (ADR 0003 / docs/05 §4.A): needed for BOTH
# deb/tarball and snap Firefox — the system-wide manifest this package also installs at
# $NM_MANIFEST_SRC only covers deb/tarball Firefox, never snap. Runs unconditionally,
# not just when snap is detected: a per-user login.defs-based install can't assume every
# user runs the same Firefox flavour, and dropping a JSON file nobody reads is harmless.
install_per_user_manifests() {
[ -f "$NM_MANIFEST_SRC" ] || {
echo "velox: native-messaging host not built yet (nmhost lane not landed) —" \
"skipping per-user manifest install."
return 0
}
real_users | while IFS=: read -r user home; do
[ -d "$home" ] || continue
dest_dir="$home/.mozilla/native-messaging-hosts"
install -d -o "$user" -g "$user" -m 0755 "$dest_dir" 2>/dev/null || {
echo "velox: could not create $dest_dir for $user — skipping" >&2
continue
}
cp "$NM_MANIFEST_SRC" "$dest_dir/$NM_MANIFEST_NAME"
chown "$user:$user" "$dest_dir/$NM_MANIFEST_NAME"
chmod 0644 "$dest_dir/$NM_MANIFEST_NAME"
done
}
# docs/07-packaging.md: detect snap Firefox and say so — the extension pairs over
# loopback WebSocket there (ADR 0003), not native messaging from inside the sandbox.
note_snap_firefox() {
if command -v snap >/dev/null 2>&1 && snap list firefox >/dev/null 2>&1; then
cat <<'EOF'
velox: Firefox is installed as a snap on this system. The extension will pair with
veloxd over the loopback WebSocket (ws://127.0.0.1:<port>), not native
messaging — this is expected (see docs/adr/0003, ADR 0003) and needs no
action from you.
EOF
fi
}
case "$1" in
configure)
install_per_user_manifests
note_snap_firefox
cat <<'EOF'
============================================================================
velox: LOCAL TEST BUILD — read this before pairing the extension
============================================================================
No real pairing-approval UI is built into this package yet (it needs
libdbus-1-dev, which this build's dependencies do not include). veloxd's
only pairing approver in this build auto-rejects every request UNLESS you
set VELOX_PAIR_AUTO=1 in its environment — there is no prompt to accept or
decline; it is all-or-nothing. Do not run this on a machine or account
where you would not want every pairing request accepted automatically.
systemctl --user edit velox.service
# add, in the [Service] section:
# Environment=VELOX_PAIR_AUTO=1
debhelper's dh_installsystemduser marks velox.service and velox.socket enabled (the
pair together, via velox.service's Also=velox.socket) for your next login session
automatically. For your CURRENT session, start socket activation now yourself — a
root postinst has no live user session to reach:
systemctl --user daemon-reload
systemctl --user enable --now velox.socket
See packaging/README.md in the source tree for the rest of this build's
testing-only limitations (placeholder icon, no license chosen yet, no
libveloxcore.so — it is linked statically).
============================================================================
EOF
;;
esac
#DEBHELPER#
exit 0
Vendored Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# postrm for velox (local test build). See packaging/README.md.
set -e
NM_MANIFEST_NAME=com.velox.host.json
real_users() {
getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $6 != "" {print $1":"$6}'
}
# Undoes exactly what postinst's install_per_user_manifests() did — never anything
# beyond that file. In particular this never touches a user's Velox data
# ($XDG_DATA_HOME/velox, i.e. normally ~/.local/share/velox — the task database and
# any settings) on either `remove` or `purge`: per-user, root-owned deletion across
# every account on the system is exactly the kind of destructive multi-user operation
# that deserves its own careful design and testing, not a first cut bolted onto this
# local test package. Documented as a deliberate simplification in
# packaging/README.md, not a silent gap.
remove_per_user_manifests() {
real_users | while IFS=: read -r user home; do
f="$home/.mozilla/native-messaging-hosts/$NM_MANIFEST_NAME"
[ -e "$f" ] && rm -f "$f"
done
}
case "$1" in
remove|purge)
remove_per_user_manifests
;;
esac
#DEBHELPER#
exit 0
Vendored Executable
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/make -f
# Velox — local test .deb build. Not a release build (no signing, no LTO tuning beyond
# what the flags below ask for): see packaging/README.md.
#
# Explicit cmake flags rather than a CMakePresets.json preset: dh_auto_configure already
# sets its own CMAKE_INSTALL_PREFIX / build-dir conventions, which fighting a preset's own
# cache variables (build/<preset>/, etc.) would only complicate. The flags mirror the
# `release` preset's intent (RelWithDebInfo, no tests) without inheriting its binaryDir.
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%:
dh $@
override_dh_auto_configure:
dh_auto_configure -- \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DVELOX_BUILD_TESTS=OFF \
-DVELOX_BUILD_GUI=ON \
-DVELOX_WERROR=OFF
# dh_installsystemd (system units) is disabled: velox.service/velox.socket are --user
# units, not system ones, so that helper has nothing to do here. dh_installsystemduser is
# NOT overridden — it runs normally, finds both units under lib/systemd/user/ in the
# install tree, and marks them enabled for each user's *next* systemd --user login (it
# cannot reach an already-running session from a root maintainer script at install time;
# postinst prints the manual start commands for the current session, matching
# packaging/README.md's own instructions).
override_dh_installsystemd:
+1
View File
@@ -0,0 +1 @@
3.0 (native)
+10
View File
@@ -0,0 +1,10 @@
# initial-upload-closes-no-bugs: this is a local test build (packaging/README.md),
# never intended for the Debian/Ubuntu archive — there is no ITP bug to close.
initial-upload-closes-no-bugs
# maintainer-script-calls-systemctl (postinst): both flagged lines are inside a
# heredoc of instructions printed for the *admin* to run themselves (velox.service is
# a --user unit; postinst has no live user session to invoke systemctl against
# itself). lintian's check is a text scan and can't tell printed advice from an actual
# invocation — verified by reading the built postinst, see packaging/README.md.
maintainer-script-calls-systemctl
+14 -2
View File
@@ -9,16 +9,28 @@ Owner: lane **PKG/QA**. Target: Ubuntu 26.04 LTS.
/usr/bin/velox-gui
/usr/bin/velox # CLI
/usr/libexec/velox/velox-nmhost # native messaging host
/usr/lib/x86_64-linux-gnu/libveloxcore.so.1
/usr/share/applications/velox.desktop
/usr/share/icons/hicolor/*/apps/velox.png
/usr/share/man/man1/velox.1.gz
/usr/lib/systemd/user/velox.service
/usr/lib/systemd/user/velox.socket # socket activation
/usr/lib/systemd/user/velox.socket # real socket activation, see below
/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
/etc/xdg/autostart/velox-gui.desktop # optional, off by default
```
`libveloxcore` is **static**, linked into `veloxd`, `velox` and `velox-gui` directly —
there is no `libveloxcore.so.*` to install. A shared, versioned `libveloxcore.so.1` was
the original plan here; the user decided against building one for now (no consumer needs
it as a shared object yet, and it would need CORE's `core/CMakeLists.txt` to grow a
`SOVERSION`). Revisit as a CORE-lane request if that changes — this doc was corrected in
the same commit as the decision rather than left describing something unshipped.
`velox.service` and `velox.socket` are both DAEMON's (`packaging/systemd/README.md`,
`daemon/src/rpc/systemd_activation.cpp`) — real `sd_listen_fds()` socket activation, not a
plain `exec`. This package installs both units verbatim and lets `dh_installsystemduser`
(compat 13) pick them up from the install tree; see `packaging/README.md` for what that
does and does not do at install time for an already-running session.
`postinst` additionally drops per-user native-messaging manifests for the packaging formats
that need them, and **detects whether Firefox is a snap** — if so it prints (and the GUI's
first-run wizard shows) a one-line note that the extension will pair over loopback.
+7 -4
View File
@@ -38,12 +38,15 @@ Node, `tools/testserver` is stdlib Python, and the extension is a WebExtension.
3. **Seams are introduced by the owning lane, on Linux, before any port work.** CORE
introduces `core`'s seams; DAEMON introduces `daemon`'s. This is a pure refactor: move
the existing Linux code behind the interface unchanged, prove the suite is still green.
4. **A new lane, PORT, owns only the per-OS backends** — `**/platform/<os>/**` and
`cmake/platform*.cmake`. It never writes logic. That keeps CLAUDE.md §1 intact: CORE
4. **A new lane, PORT, owns only the per-OS backends** — `**/platform/<os>/**`. It never
writes logic, and it does not own `cmake/`: the central OS detection module is root
build infrastructure and stays with PKG/QA, who must land it during Phase 0 because
Phase 0's own seams consume it. PORT owning it would deadlock — PORT cannot start until
Phase 0 lands, and Phase 0 cannot select platform sources without it. That keeps CLAUDE.md §1 intact: CORE
still owns `core/`'s logic, DAEMON still owns `daemon/`'s, and macOS work can proceed in
parallel without cross-lane writes.
5. **`VELOX_OS_*` is set once, centrally**, in `cmake/platform.cmake`, and platform sources
are selected there — not by globbing, and not per-target ad hoc.
5. **`VELOX_OS_*` is set once, centrally**, in `cmake/platform.cmake` (owned by PKG/QA),
and platform sources are selected there — not by globbing, not per-target ad hoc.
6. **Optional dependencies are gated, never removed.** `libsecret` stays `REQUIRED` on
Linux and is replaced by Keychain on macOS behind the same credential-store seam.
+5 -4
View File
@@ -5,7 +5,7 @@
## You own
```
core/src/**/platform/<os>/** daemon/src/**/platform/<os>/**
cmake/platform*.cmake packaging/macos/** packaging/windows/**
packaging/macos/** packaging/windows/**
```
You may read everything. **You never write logic** — not in `core/src/io/*.cpp`, not in
`daemon/src/rpc/*.cpp`, not in `gui/`, never in `contracts/`.
@@ -22,8 +22,9 @@ quietly edits shared logic is how one tree becomes two.
## Build order (macOS)
1. **Configure at all.** The root `CMakeLists.txt` requires `libsecret-1` unconditionally;
that must be Linux-gated before anything else compiles. File it with PKG/QA — root build
files are theirs.
that must be Linux-gated before anything else compiles. PKG/QA owns that, and also owns
`cmake/platform.cmake` (the `VELOX_OS_*` detection and `velox_platform_sources()`); both
land in Phase 0, before you start. File anything else you need with them.
2. `core/` — `preallocate`, `advise_dontneed`, `flush_durable`. Use `F_FULLFSYNC`, not
`fsync`: `.veloxpart` resume correctness depends on a real flush.
3. `daemon/` — `Wakeup` (self-pipe), `peer_of` (`getpeereid`), `instance_lock` (socket file
@@ -41,7 +42,7 @@ quietly edits shared logic is how one tree becomes two.
- One real download completes on macOS against `tools/testserver` with a SHA-256 that
matches the server's reference — the same gate the Linux vertical slice passed.
- `kill -9` mid-download, restart, resume completes and the checksum still matches.
- The Ubuntu suite is **still** 57/57 from the same commit.
- The Ubuntu suite is **still** green from the same commit.
## Do not
- Do not add `#ifdef` outside a `platform/<os>/` file.
+13
View File
@@ -37,6 +37,7 @@ add_library(velox-gui-lib STATIC
src/tray/TrayIcon.cpp
src/widgets/DropTargetWidget.cpp
src/util/ThemeManager.cpp
src/util/IconTheme.cpp
src/util/UiThreadWatchdog.cpp
src/mainwindow/CategoryPanel.cpp
src/mainwindow/MainWindow.cpp
@@ -50,6 +51,18 @@ qt_add_resources(velox-gui-lib "theme"
FILES resources/qss/idm-like.qss resources/qss/dark.qss
)
# gui/resources/icons/LICENSE: Adwaita-derived (GNOME Project), LGPL-3 or CC-BY-SA-3/4.
# light/dark are the same glyphs recoloured for each toolbar skin (IconTheme picks
# between them the same way ThemeManager picks QSS); app/ is the fixed full-colour
# window/tray glyph.
file(GLOB VELOX_GUI_ICONS_LIGHT resources/icons/light/*.svg)
file(GLOB VELOX_GUI_ICONS_DARK resources/icons/dark/*.svg)
qt_add_resources(velox-gui-lib "icons"
PREFIX "/icons"
BASE "resources/icons"
FILES ${VELOX_GUI_ICONS_LIGHT} ${VELOX_GUI_ICONS_DARK} resources/icons/app/velox.svg
)
target_include_directories(velox-gui-lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(velox-gui-lib PUBLIC cxx_std_23)
target_compile_options(velox-gui-lib PRIVATE -Wall -Wextra -Wpedantic -Werror)
+38 -1
View File
@@ -67,7 +67,12 @@ failure for the whole project — not a skipped guard, not a GUI-only problem.
---
## R2 — `--check` verifies the outcome, not the thing that can break
## R2 — `--check` verifies the outcome, not the thing that can break — RESOLVED
**Status: done.** `tools/bootstrap.sh` now validates every apt name in `PKGS` via
`apt-cache policy` and greps for the `Candidate: (none)` line rather than trusting the
exit code (which is 0 either way) — the exact loop this request drafted. This is the
check that would have caught `libqt6svg6-dev` (R1) before the VM did.
`--check` today runs the "verify toolchain" block: `command -v` for the binaries,
`pkg-config --exists` for the dev libs (`Qt6Core`, `Qt6Widgets`, `Qt6Svg`, `libcurl`,
@@ -219,3 +224,35 @@ Worth making explicit anyway, since relying on undocumented target leakage is fr
No apt change needed either way — `qt6-base-dev` (already in `APT_GUI`) ships `QtDBus`'s
headers directly (verified live: `dpkg -L qt6-base-dev | grep -i dbus` lists the whole
`QtDBus/` include tree).
---
## R5 — real icon set has landed; stop shipping the placeholder, here's the install path
`gui/resources/icons/.gitkeep` is gone. There's a real, licensed icon set now (Adwaita,
GNOME Project, LGPL-3-or-CC-BY-SA — full text and per-file provenance in
`gui/resources/icons/LICENSE`), embedded into `velox-gui` via `qt_add_resources` in
`gui/CMakeLists.txt` (`:/icons/{light,dark}/<name>.svg` for toolbar/menu actions,
`:/icons/app/velox.svg` for the window and tray icon). Nothing needs installing
separately for the binary itself — the SVGs are baked into the Qt resource system at
build time, not read from disk at runtime.
What that means for whatever `.deb` postinst / desktop-entry work PKG is doing:
* **Stop bundling a placeholder icon file in the test `.deb`.** There's no longer a gap
to fill — `velox-gui`'s own binary already carries its icon.
* **App icon for the `.desktop` file / desktop menu entry**, if PKG is generating one:
the source SVG is `gui/resources/icons/app/velox.svg` (full colour, unmodified Adwaita
`folder-download` glyph, same in light and dark). If the packaging pipeline wants a
filesystem-installed icon (standard XDG icon-theme layout, e.g.
`/usr/share/icons/hicolor/scalable/apps/velox.svg`, referenced from the `.desktop`
file's `Icon=velox` key) rather than relying on the binary's embedded window icon for
the taskbar/launcher, that single file is the one to install — no rasterization needed,
it's already a clean scalable SVG. GUI has no packaging-stage code to add this itself
(CLAUDE.md §1: `packaging/` is PKG's lane), so filing the path here rather than reaching
across.
* Licence compliance for the `.deb`: `gui/resources/icons/LICENSE` should ship somewhere
discoverable from the package (e.g. copied into `debian/copyright` or
`/usr/share/doc/velox/copyright` alongside whatever other third-party attributions PKG
already tracks) — it documents the Adwaita provenance and dual LGPL-3/CC-BY-SA license
per file, which needs to be preserved wherever the binary that embeds these SVGs ships.
View File
+66
View File
@@ -0,0 +1,66 @@
Icon set for Velox Download Manager
====================================
Source: GNOME Adwaita icon theme (`adwaita-icon-theme`), version bundled with
Debian/Ubuntu, downloaded from <https://download.gnome.org/sources/adwaita-icon-theme/>.
This is the same "compatibly-licensed set" path called out in
docs/03-gui-spec.md's legal note (Papirus/Breeze-derived is the other option; we used
what was locally available and equally suitable) and gui/docs/agents/AGENT-GUI.md.
Nothing in this directory is IDM's artwork. Files below are recolored (fill colour only —
geometry untouched) derivatives of Adwaita SVGs, laid out in IDM's toolbar positions per
docs/03-gui-spec.md §1, so the *positions* rhyme with IDM's muscle memory while the
*pictures* are GNOME's.
Copyright
---------
(c) 2002-2014 The GNOME Project and Adwaita icon theme contributors, including:
Ulisse Perusin, Riccardo Buzzotta, Josef Vybíral, Hylke Bons, Ricardo González,
Lapo Calamandrei, Rodney Dawes, Luca Ferretti, Tuomas Kuosmanen, Andreas Nilsson,
Jakub Steiner, Claire Alexander, Darren Wilson, and other contributors.
License
-------
Dual-licensed, at the recipient's option:
- GNU Lesser General Public License v3.0 (LGPL-3.0-only), or
- Creative Commons Attribution-Share Alike 3.0 or 4.0 (CC-BY-SA-3.0 / CC-BY-SA-4.0)
Full text: https://www.gnu.org/licenses/lgpl-3.0.html
https://creativecommons.org/licenses/by-sa/4.0/
Attribution: "GNOME Project" (https://www.gnome.org), per the upstream copyright file's
own attribution note.
Provenance and modifications
-----------------------------
All files trace back to `/usr/share/icons/Adwaita/{symbolic,scalable}/...` from the
`adwaita-icon-theme` Debian package. Only the `fill` colour was changed (geometry, paths
and viewBox are untouched) to produce a light-toolbar and a dark-toolbar variant of each
symbolic glyph:
light/*.svg fill #2e2e2e — used when the app is in the idm-like (light) theme
dark/*.svg fill #eeeeec — used when the app is in the dark theme
app/velox.svg unmodified — full-colour "folder-download" glyph, used for the
window icon, the tray icon, and the .desktop icon
Adwaita source file -> our name
-----------------------------------------------------------------------------
symbolic/actions/list-add-symbolic.svg -> add-url.svg
symbolic/actions/media-playback-start-symbolic.svg -> resume.svg
symbolic/actions/media-playback-pause-symbolic.svg -> pause.svg
symbolic/actions/media-playback-stop-symbolic.svg -> stop-all.svg
symbolic/actions/edit-delete-symbolic.svg -> delete.svg
symbolic/actions/edit-clear-all-symbolic.svg -> delete-completed.svg
symbolic/actions/appointment-new-symbolic.svg -> scheduler.svg
symbolic/categories/preferences-system-symbolic.svg -> options.svg
symbolic/legacy/web-browser-symbolic.svg -> grabber.svg
symbolic/actions/document-properties-symbolic.svg -> properties.svg
symbolic/actions/insert-link-symbolic.svg -> batch.svg
scalable/places/folder-download.svg -> app/velox.svg (unmodified)
Toolbar/menu -> icon mapping (docs/03-gui-spec.md §1 toolbar order)
---------------------------------------------------------------------
Add URL -> add-url · Resume -> resume · Pause -> pause · Stop All -> stop-all ·
Delete -> delete · Delete Completed -> delete-completed · Scheduler -> scheduler ·
Options -> options · Grabber -> grabber. Properties and Batch (menu-only, not on the
default toolbar) use properties and batch respectively.
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientTransform="matrix(0.45451 0 0 0.455522 -1210.292114 616.172607)" gradientUnits="userSpaceOnUse" x1="2689.251953" x2="2918.069824" y1="-1106.802979" y2="-1106.802979">
<stop offset="0" stop-color="#62a0ea"/>
<stop offset="0.0576991" stop-color="#afd4ff"/>
<stop offset="0.122204" stop-color="#62a0ea"/>
<stop offset="0.873306" stop-color="#62a0ea"/>
<stop offset="0.955997" stop-color="#c0d5ea"/>
<stop offset="1" stop-color="#62a0ea"/>
</linearGradient>
<path d="m 21.976562 12 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 86.03125 c 0 5.542969 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.457031 9.980469 -10 v -72.085938 c 0 -6.628906 -5.359375 -12 -11.972656 -12 h -46.027344 c -2.453125 0 -4.695312 -1.386718 -5.796875 -3.582031 l -1.503906 -2.992187 c -1.65625 -3.292969 -5.019531 -5.371094 -8.699219 -5.371094 z m 0 0" fill="#438de6"/>
<path d="m 65.976562 36 c -2.746093 0 -5.226562 1.101562 -7.027343 2.890625 c -2.273438 2.253906 -5.382813 5.109375 -8.632813 5.109375 h -28.339844 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 54.03125 c 0 5.542969 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.457031 9.980469 -10 v -62.03125 c 0 -5.539062 -4.449219 -10 -9.980469 -10 z m 0 0" fill="url(#a)"/>
<path d="m 65.976562 32 c -2.746093 0 -5.226562 1.101562 -7.027343 2.890625 c -2.273438 2.253906 -5.382813 5.109375 -8.632813 5.109375 h -28.339844 c -5.527343 0 -9.976562 4.460938 -9.976562 10 v 55.976562 c 0 5.539063 4.449219 10 9.976562 10 h 84.042969 c 5.53125 0 9.980469 -4.460937 9.980469 -10 v -63.976562 c 0 -5.539062 -4.449219 -10 -9.980469 -10 z m 0 0" fill="#a4caee"/>
<path d="m 64 60 c -1.101562 0 -2 0.898438 -2 2 v 17.171875 l -4.585938 -4.585937 c -0.375 -0.375 -0.882812 -0.585938 -1.414062 -0.585938 s -1.039062 0.210938 -1.414062 0.585938 c -0.78125 0.78125 -0.78125 2.046874 0 2.828124 l 8 8 c 0.78125 0.78125 2.046874 0.78125 2.828124 0 l 8 -8 c 0.78125 -0.78125 0.78125 -2.046874 0 -2.828124 s -2.046874 -0.78125 -2.828124 0 l -4.585938 4.585937 v -17.171875 c 0 -1.101562 -0.898438 -2 -2 -2 z m -12 28 v 4 h 24 v -4 z m 0 0" fill="#438de6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="15.980469px" viewBox="0 0 16 15.980469" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 5 11.976562 v 1 h 0.007812 c -0.003906 0.261719 0.101563 0.515626 0.285157 0.707032 l 2 1.996094 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2 -1.996094 c 0.1875 -0.191406 0.289063 -0.445313 0.289063 -0.707032 h 0.003906 v -1 z m 0 0"/>
<path d="m 7 1.972656 c -1.296875 0 -2.40625 0.839844 -2.824219 2 h 7.652344 c -0.417969 -1.160156 -1.53125 -2 -2.828125 -2 z m -5.996094 3 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m 9 0 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m -5.828125 3 c 0.414063 1.160156 1.527344 1.996094 2.824219 1.996094 h 2 c 1.296875 0 2.414062 -0.835938 2.828125 -1.996094 h -2.792969 c -0.011718 0 -0.023437 0.003906 -0.035156 0.003906 h -2 c -0.011719 0 -0.023438 -0.003906 -0.035156 -0.003906 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 2 0 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
<path d="m 8 6 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 h 6 c 1.109375 0 2 -0.890625 2 -2 v -6 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0.003906 2 h 1 h 0.03125 c 0.253906 0.011719 0.507813 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.308594 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -0.996094 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.277344 -1.25 l -1.277344 -1.28125 c -0.210937 -0.195312 -0.304687 -0.46875 -0.28125 -0.75 z m 0 0"/>
<path d="m 5 3 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m -2.03125 2.96875 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16.004"><g color="#000" fill="#eeeeec"><path d="M8 4.846v10.38l-2.344-2.288-1.34 2.734c-.327.74-2.033.145-1.548-.844l1.326-2.839H1.136z" style="marker:none" display="block" overflow="visible"/><path d="M7.156 1.063C3.33 1.522.578 5.02 1.063 8.843c.131 1.035.293 1.383.293 1.383l1.675-1.632A4.992 4.992 0 017.406 3.03a4.992 4.992 0 015.563 4.375c.314 2.608-1.391 5.025-3.969 5.532l.031 2s.521-.106.624-.131c3.416-.834 5.706-4.128 5.283-7.65-.46-3.827-3.955-6.555-7.782-6.095z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M4.113 7.815v-.206l-.198.019.05-.393h-.117l-.115.15-.116.056-.165-.093-.017-.206.033-.225.248-.186h.199l.033-.113.247.056.182.225.033-.374.314-.262.116-.28.231-.094.133-.187.297-.056.149-.225h-.446l.28-.13h.199l.28-.094.034-.112-.1-.094-.115-.037.033-.112-.083-.169-.198.075.033-.15-.231-.13-.182.317.017.113-.182.075-.116.243-.05-.225-.313-.13-.05-.169.413-.243.182-.168.017-.206-.1-.056L4.84 4l-.082.206s-.139.027-.174.036C4.13 4.66 3.214 5.562 3 7.266c.008.04.155.268.155.268l.347.206.347.093m3.966-4.3l-.43-.168-.496.056-.611.168-.116.112.38.262v.15l-.149.15.199.392.132-.075.165-.262a5.51 5.51 0 00.727-.28l.199-.505m2.529.342l-.375.094-.219.156v.125l-.375.25.094.344.219-.157.125.157.156.093.094-.28L10 4.5l.063-.094.218-.187h.094l-.094.218v.188c.09-.024.16-.051.25-.063l-.25.188v.125l-.312.219-.281-.063v-.156l-.125.062.062.157h-.219l-.125.218-.156.157-.094.03v.188l.032.157H9.03v.53l.063-.03.094-.219L9.375 6l.031-.094.282-.062.156.187.187.094-.093.187.156-.03.062-.22-.187-.218h.062l.22.156.03.219.157.219.062-.313.094-.031c.096.1.169.231.25.344h.281l.188.125-.094.093-.156.157h-.25l-.344-.094h-.188l-.125.156-.343-.375-.25-.062-.375.062-.157.094V9l.032.031.25-.156.093.094h.281l.125.156-.093.312.187.188V10l.125.25-.093.25c-.01.161 0 .307 0 .469.08.219.143.435.218.656l.063.344v.187h.125l.219-.125h.25l.375-.437-.032-.157.25-.218-.187-.188.219-.187.218-.125.094-.125-.062-.25V9.75l.187-.375.188-.25.25-.563v-.156c-.117.015-.23.023-.344.031-.072.005-.145 0-.219 0a7.442 7.442 0 01-.312-.78l-.157-.188-.093-.313.062-.062.219.25.25.562.156.156-.062.22.156.155.25-.25.312-.218.157-.188v-.219c-.04-.073-.055-.145-.094-.218l-.156.187-.125-.156-.188-.125v-.281l.219.218.219-.03c.101.091.192.207.28.312L13 7.28c0-.174-.2-1.02-.625-1.75S11.22 4.125 11.22 4.125l-.063.094-.218.218-.25-.25h.25l.125-.125-.47-.093-.25-.094z" style="marker:none" overflow="visible" opacity=".3"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 13.855469 0 l -1.539063 1.4375 c -0.453125 0.421875 -0.53125 1.148438 -0.269531 1.707031 l -5.886719 5.996094 c -0.011718 0 -0.019531 0 -0.03125 0 c -0.257812 -0.128906 -0.550781 -0.183594 -0.839844 -0.148437 c -0.328124 0.046874 -0.632812 0.199218 -0.867187 0.441406 l -3.945313 3.996094 c -0.3906245 0.375 -0.5468745 0.933593 -0.4062495 1.457031 c 0.1406255 0.523437 0.5546875 0.929687 1.0820315 1.058593 c 0.527344 0.132813 1.082031 -0.03125 1.453125 -0.425781 l 3.945312 -3.996093 c 0.472657 -0.453126 0.59375 -1.15625 0.296875 -1.738282 l 5.890625 -5.964844 c 0.558594 0.25 1.273438 0.148438 1.707031 -0.289062 l 1.414063 -1.5625 z m -10.308594 0.0898438 c -0.398437 0 -0.785156 0.0937502 -1.140625 0.2187502 l 1.882812 1.878906 c 0.390626 0.382812 0.390626 1 0 1.386719 l -0.710937 0.707031 c -0.386719 0.386719 -1 0.386719 -1.390625 0 l -1.882812 -1.878906 c -0.125 0.355468 -0.2187505 0.742187 -0.2187505 1.140625 c 0 1.90625 1.5507815 3.453125 3.4609375 3.453125 c 0.402344 0 0.789063 -0.09375 1.144531 -0.21875 l 1.175782 1.171875 h 0.058593 l 2.070313 -2.0625 l -1.203125 -1.203125 c 0.125 -0.359375 0.214843 -0.742188 0.214843 -1.140625 c 0 -1.90625 -1.546874 -3.4531252 -3.460937 -3.4531252 z m 6.550781 7.8906252 l -2.070312 2.066406 c 0.011718 0.027344 0.023437 0.058594 0.03125 0.089844 l 1.144531 1.140625 c -0.125 0.355468 -0.21875 0.742187 -0.21875 1.140625 c 0 1.902343 1.550781 3.449219 3.460937 3.449219 c 0.433594 0 0.855469 -0.101563 1.238282 -0.246094 l -2.007813 -2 c -0.386719 -0.386719 -0.386719 -1.035156 0 -1.417969 l 0.679688 -0.679687 c 0.195312 -0.191407 0.457031 -0.308594 0.710937 -0.308594 s 0.515625 0.117187 0.710938 0.308594 l 1.945312 1.941406 c 0.105469 -0.328125 0.183594 -0.683594 0.183594 -1.046875 c 0 -1.90625 -1.546875 -3.453125 -3.460938 -3.453125 c -0.398437 0 -0.785156 0.09375 -1.140624 0.21875 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#eeeeec">
<path d="m 3 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
<path d="m 10 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 3 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -3 c -0.570312 0 -1 -0.429688 -1 -1 v -10 c 0 -0.570312 0.429688 -1 1 -1 h 5.585938 l 3.414062 3.414062 v 6.585938 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -7 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -4 -4 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 4.640625 6.167969 c -0.199219 0.003906 -0.402344 0.019531 -0.597656 0.046875 l 1.171875 2.074218 c 0.242187 0.425782 -0.027344 0.914063 -0.539063 1.210938 l -0.925781 0.53125 c -0.515625 0.289062 -1.089844 0.25 -1.332031 -0.175781 l -1.171875 -2.074219 c -0.25 0.308594 -0.476563 0.648438 -0.617188 1.019531 c -0.679687 1.761719 0.285156 3.46875 2.152344 3.804688 c 0.386719 0.074219 0.800781 0.054687 1.191406 0.007812 c 0.03125 -0.007812 1.859375 3.386719 1.859375 3.386719 l 4.167969 0.003906 l -3.199219 -4.957031 c 0.246094 -0.308594 0.445313 -0.652344 0.585938 -1.019531 c 0.679687 -1.765625 -0.285157 -3.472656 -2.148438 -3.808594 c -0.195312 -0.039062 -0.398437 -0.050781 -0.597656 -0.046875 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 2 2.5 v 11 c 0 1.5 1.269531 1.492188 1.269531 1.492188 h 0.128907 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 h -0.128907 s -1.269531 0 -1.269531 1.5 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.5 6.5 6.5 c 0.167969 0 0.335938 -0.007812 0.5 -0.019531 v -2.007813 c -0.164062 0.019532 -0.332031 0.027344 -0.5 0.027344 c -2.496094 0 -4.5 -2.003906 -4.5 -4.5 s 2.003906 -4.5 4.5 -4.5 s 4.5 2.003906 4.5 4.5 c 0 0.167969 -0.007812 0.335938 -0.027344 0.5 h 2.007813 c 0.011719 -0.164062 0.019531 -0.332031 0.019531 -0.5 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 2.5 h -1.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 2 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -3 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4.5 5 v 3 h -3 v 2 h 3 v 3 h 2 v -3 h 3 v -2 h -3 v -3 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.5 2 h 9 c 0.828125 0 1.5 0.671875 1.5 1.5 v 9 c 0 0.828125 -0.671875 1.5 -1.5 1.5 h -9 c -0.828125 0 -1.5 -0.671875 -1.5 -1.5 v -9 c 0 -0.828125 0.671875 -1.5 1.5 -1.5 z m 0 0" fill="#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="15.980469px" viewBox="0 0 16 15.980469" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 5 11.976562 v 1 h 0.007812 c -0.003906 0.261719 0.101563 0.515626 0.285157 0.707032 l 2 1.996094 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2 -1.996094 c 0.1875 -0.191406 0.289063 -0.445313 0.289063 -0.707032 h 0.003906 v -1 z m 0 0"/>
<path d="m 7 1.972656 c -1.296875 0 -2.40625 0.839844 -2.824219 2 h 7.652344 c -0.417969 -1.160156 -1.53125 -2 -2.828125 -2 z m -5.996094 3 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m 9 0 c -0.558594 0 -1.003906 0.445313 -1.003906 1 c 0 0.554688 0.445312 1 1.003906 1 h 4.996094 c 0.554688 0 1 -0.445312 1 -1 c 0 -0.554687 -0.445312 -1 -1 -1 z m -5.828125 3 c 0.414063 1.160156 1.527344 1.996094 2.824219 1.996094 h 2 c 1.296875 0 2.414062 -0.835938 2.828125 -1.996094 h -2.792969 c -0.011718 0 -0.023437 0.003906 -0.035156 0.003906 h -2 c -0.011719 0 -0.023438 -0.003906 -0.035156 -0.003906 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 2 0 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
<path d="m 8 6 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 h 6 c 1.109375 0 2 -0.890625 2 -2 v -6 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0.003906 2 h 1 h 0.03125 c 0.253906 0.011719 0.507813 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.308594 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -0.996094 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.277344 -1.25 l -1.277344 -1.28125 c -0.210937 -0.195312 -0.304687 -0.46875 -0.28125 -0.75 z m 0 0"/>
<path d="m 5 3 c -1.109375 0 -2 0.890625 -2 2 v 6 c 0 1.109375 0.890625 2 2 2 v -8 h 8 c 0 -1.109375 -0.890625 -2 -2 -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m -2.03125 2.96875 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16.004"><g color="#000" fill="#2e2e2e"><path d="M8 4.846v10.38l-2.344-2.288-1.34 2.734c-.327.74-2.033.145-1.548-.844l1.326-2.839H1.136z" style="marker:none" display="block" overflow="visible"/><path d="M7.156 1.063C3.33 1.522.578 5.02 1.063 8.843c.131 1.035.293 1.383.293 1.383l1.675-1.632A4.992 4.992 0 017.406 3.03a4.992 4.992 0 015.563 4.375c.314 2.608-1.391 5.025-3.969 5.532l.031 2s.521-.106.624-.131c3.416-.834 5.706-4.128 5.283-7.65-.46-3.827-3.955-6.555-7.782-6.095z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M4.113 7.815v-.206l-.198.019.05-.393h-.117l-.115.15-.116.056-.165-.093-.017-.206.033-.225.248-.186h.199l.033-.113.247.056.182.225.033-.374.314-.262.116-.28.231-.094.133-.187.297-.056.149-.225h-.446l.28-.13h.199l.28-.094.034-.112-.1-.094-.115-.037.033-.112-.083-.169-.198.075.033-.15-.231-.13-.182.317.017.113-.182.075-.116.243-.05-.225-.313-.13-.05-.169.413-.243.182-.168.017-.206-.1-.056L4.84 4l-.082.206s-.139.027-.174.036C4.13 4.66 3.214 5.562 3 7.266c.008.04.155.268.155.268l.347.206.347.093m3.966-4.3l-.43-.168-.496.056-.611.168-.116.112.38.262v.15l-.149.15.199.392.132-.075.165-.262a5.51 5.51 0 00.727-.28l.199-.505m2.529.342l-.375.094-.219.156v.125l-.375.25.094.344.219-.157.125.157.156.093.094-.28L10 4.5l.063-.094.218-.187h.094l-.094.218v.188c.09-.024.16-.051.25-.063l-.25.188v.125l-.312.219-.281-.063v-.156l-.125.062.062.157h-.219l-.125.218-.156.157-.094.03v.188l.032.157H9.03v.53l.063-.03.094-.219L9.375 6l.031-.094.282-.062.156.187.187.094-.093.187.156-.03.062-.22-.187-.218h.062l.22.156.03.219.157.219.062-.313.094-.031c.096.1.169.231.25.344h.281l.188.125-.094.093-.156.157h-.25l-.344-.094h-.188l-.125.156-.343-.375-.25-.062-.375.062-.157.094V9l.032.031.25-.156.093.094h.281l.125.156-.093.312.187.188V10l.125.25-.093.25c-.01.161 0 .307 0 .469.08.219.143.435.218.656l.063.344v.187h.125l.219-.125h.25l.375-.437-.032-.157.25-.218-.187-.188.219-.187.218-.125.094-.125-.062-.25V9.75l.187-.375.188-.25.25-.563v-.156c-.117.015-.23.023-.344.031-.072.005-.145 0-.219 0a7.442 7.442 0 01-.312-.78l-.157-.188-.093-.313.062-.062.219.25.25.562.156.156-.062.22.156.155.25-.25.312-.218.157-.188v-.219c-.04-.073-.055-.145-.094-.218l-.156.187-.125-.156-.188-.125v-.281l.219.218.219-.03c.101.091.192.207.28.312L13 7.28c0-.174-.2-1.02-.625-1.75S11.22 4.125 11.22 4.125l-.063.094-.218.218-.25-.25h.25l.125-.125-.47-.093-.25-.094z" style="marker:none" overflow="visible" opacity=".3"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 13.855469 0 l -1.539063 1.4375 c -0.453125 0.421875 -0.53125 1.148438 -0.269531 1.707031 l -5.886719 5.996094 c -0.011718 0 -0.019531 0 -0.03125 0 c -0.257812 -0.128906 -0.550781 -0.183594 -0.839844 -0.148437 c -0.328124 0.046874 -0.632812 0.199218 -0.867187 0.441406 l -3.945313 3.996094 c -0.3906245 0.375 -0.5468745 0.933593 -0.4062495 1.457031 c 0.1406255 0.523437 0.5546875 0.929687 1.0820315 1.058593 c 0.527344 0.132813 1.082031 -0.03125 1.453125 -0.425781 l 3.945312 -3.996093 c 0.472657 -0.453126 0.59375 -1.15625 0.296875 -1.738282 l 5.890625 -5.964844 c 0.558594 0.25 1.273438 0.148438 1.707031 -0.289062 l 1.414063 -1.5625 z m -10.308594 0.0898438 c -0.398437 0 -0.785156 0.0937502 -1.140625 0.2187502 l 1.882812 1.878906 c 0.390626 0.382812 0.390626 1 0 1.386719 l -0.710937 0.707031 c -0.386719 0.386719 -1 0.386719 -1.390625 0 l -1.882812 -1.878906 c -0.125 0.355468 -0.2187505 0.742187 -0.2187505 1.140625 c 0 1.90625 1.5507815 3.453125 3.4609375 3.453125 c 0.402344 0 0.789063 -0.09375 1.144531 -0.21875 l 1.175782 1.171875 h 0.058593 l 2.070313 -2.0625 l -1.203125 -1.203125 c 0.125 -0.359375 0.214843 -0.742188 0.214843 -1.140625 c 0 -1.90625 -1.546874 -3.4531252 -3.460937 -3.4531252 z m 6.550781 7.8906252 l -2.070312 2.066406 c 0.011718 0.027344 0.023437 0.058594 0.03125 0.089844 l 1.144531 1.140625 c -0.125 0.355468 -0.21875 0.742187 -0.21875 1.140625 c 0 1.902343 1.550781 3.449219 3.460937 3.449219 c 0.433594 0 0.855469 -0.101563 1.238282 -0.246094 l -2.007813 -2 c -0.386719 -0.386719 -0.386719 -1.035156 0 -1.417969 l 0.679688 -0.679687 c 0.195312 -0.191407 0.457031 -0.308594 0.710937 -0.308594 s 0.515625 0.117187 0.710938 0.308594 l 1.945312 1.941406 c 0.105469 -0.328125 0.183594 -0.683594 0.183594 -1.046875 c 0 -1.90625 -1.546875 -3.453125 -3.460938 -3.453125 c -0.398437 0 -0.785156 0.09375 -1.140624 0.21875 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e2e2e">
<path d="m 3 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
<path d="m 10 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 3 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -3 c -0.570312 0 -1 -0.429688 -1 -1 v -10 c 0 -0.570312 0.429688 -1 1 -1 h 5.585938 l 3.414062 3.414062 v 6.585938 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -7 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -4 -4 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 4.640625 6.167969 c -0.199219 0.003906 -0.402344 0.019531 -0.597656 0.046875 l 1.171875 2.074218 c 0.242187 0.425782 -0.027344 0.914063 -0.539063 1.210938 l -0.925781 0.53125 c -0.515625 0.289062 -1.089844 0.25 -1.332031 -0.175781 l -1.171875 -2.074219 c -0.25 0.308594 -0.476563 0.648438 -0.617188 1.019531 c -0.679687 1.761719 0.285156 3.46875 2.152344 3.804688 c 0.386719 0.074219 0.800781 0.054687 1.191406 0.007812 c 0.03125 -0.007812 1.859375 3.386719 1.859375 3.386719 l 4.167969 0.003906 l -3.199219 -4.957031 c 0.246094 -0.308594 0.445313 -0.652344 0.585938 -1.019531 c 0.679687 -1.765625 -0.285157 -3.472656 -2.148438 -3.808594 c -0.195312 -0.039062 -0.398437 -0.050781 -0.597656 -0.046875 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 2 2.5 v 11 c 0 1.5 1.269531 1.492188 1.269531 1.492188 h 0.128907 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 h -0.128907 s -1.269531 0 -1.269531 1.5 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.5 6.5 6.5 c 0.167969 0 0.335938 -0.007812 0.5 -0.019531 v -2.007813 c -0.164062 0.019532 -0.332031 0.027344 -0.5 0.027344 c -2.496094 0 -4.5 -2.003906 -4.5 -4.5 s 2.003906 -4.5 4.5 -4.5 s 4.5 2.003906 4.5 4.5 c 0 0.167969 -0.007812 0.335938 -0.027344 0.5 h 2.007813 c 0.011719 -0.164062 0.019531 -0.332031 0.019531 -0.5 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 2.5 h -1.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 2 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -3 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 4.5 5 v 3 h -3 v 2 h 3 v 3 h 2 v -3 h 3 v -2 h -3 v -3 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.5 2 h 9 c 0.828125 0 1.5 0.671875 1.5 1.5 v 9 c 0 0.828125 -0.671875 1.5 -1.5 1.5 h -9 c -0.828125 0 -1.5 -0.671875 -1.5 -1.5 v -9 c 0 -0.828125 0.671875 -1.5 1.5 -1.5 z m 0 0" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

+24
View File
@@ -37,6 +37,7 @@
#include "rpc/RpcClient.hpp"
#include "tray/TrayIcon.hpp"
#include "util/Format.hpp"
#include "util/IconTheme.hpp"
#include "util/Theme.hpp"
#include "widgets/DropTargetWidget.hpp"
#include "widgets/ProgressDelegate.hpp"
@@ -67,8 +68,12 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
offlineBanner_(new QWidget(this)),
countsTimer_(new QTimer(this)) {
setWindowTitle(tr("Velox Download Manager"));
setWindowIcon(IconTheme::appIcon());
resize(1040, 600);
iconTheme_ = new IconTheme(this);
connect(iconTheme_, &IconTheme::iconsChanged, this, &MainWindow::applyIcons);
proxy_->setSourceModel(model_);
proxy_->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -116,6 +121,7 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
setCentralWidget(central);
buildActions();
applyIcons();
buildMenus();
buildToolBar();
buildTray();
@@ -205,6 +211,24 @@ void MainWindow::buildActions() {
connect(actShow_, &QAction::triggered, this, &MainWindow::showAndRaise);
}
// gui/resources/icons/LICENSE: toolbar-position -> icon-stem mapping. Re-run on
// IconTheme::iconsChanged so a live light/dark switch swaps every action's glyph, the
// same way ThemeManager swaps the QSS on the same signal.
void MainWindow::applyIcons() {
actAddUrl_->setIcon(IconTheme::icon(QStringLiteral("add-url")));
actResume_->setIcon(IconTheme::icon(QStringLiteral("resume")));
actPause_->setIcon(IconTheme::icon(QStringLiteral("pause")));
actStop_->setIcon(IconTheme::icon(QStringLiteral("stop-all")));
actRemove_->setIcon(IconTheme::icon(QStringLiteral("delete")));
actResumeAll_->setIcon(IconTheme::icon(QStringLiteral("resume")));
actPauseAll_->setIcon(IconTheme::icon(QStringLiteral("pause")));
actProperties_->setIcon(IconTheme::icon(QStringLiteral("properties")));
actOptions_->setIcon(IconTheme::icon(QStringLiteral("options")));
actScheduler_->setIcon(IconTheme::icon(QStringLiteral("scheduler")));
actBatch_->setIcon(IconTheme::icon(QStringLiteral("batch")));
actGrabber_->setIcon(IconTheme::icon(QStringLiteral("grabber")));
}
void MainWindow::buildMenus() {
QMenu *tasks = menuBar()->addMenu(tr("&Tasks"));
tasks->addAction(actAddUrl_);
+3
View File
@@ -27,6 +27,7 @@ class DownloadTableModel;
class CategoryPanel;
class TrayIcon;
class DropTargetWidget;
class IconTheme;
#ifdef VELOX_GUI_HAVE_DBUS
class GlobalShortcut;
#endif
@@ -69,6 +70,7 @@ class MainWindow : public QMainWindow {
void showAndRaise();
void onSettingsChanged(const QJsonObject &params);
void onUrlDropped(const QString &url);
void applyIcons();
private:
void buildActions();
@@ -107,6 +109,7 @@ class MainWindow : public QMainWindow {
QJsonArray categoriesCache_;
QJsonArray queuesCache_;
IconTheme *iconTheme_ = nullptr;
TrayIcon *trayIcon_ = nullptr;
DropTargetWidget *dropTarget_ = nullptr;
bool minimizeToTrayEnabled_ = false;
+6 -9
View File
@@ -1,29 +1,26 @@
#include "tray/TrayIcon.hpp"
#include <QAction>
#include <QApplication>
#include <QCoreApplication>
#include <QJsonObject>
#include <QMenu>
#include <QMessageBox>
#include <QStyle>
#include <QWidget>
#include "dialogs/SpeedLimiterDialog.hpp"
#include "rpc/Protocol.hpp"
#include "rpc/RpcClient.hpp"
#include "util/IconTheme.hpp"
namespace velox::gui {
TrayIcon::TrayIcon(rpc::RpcClient *client, QAction *actShow, QAction *actAddUrl,
QAction *actResumeAll, QAction *actPauseAll, QWidget *parent)
: QSystemTrayIcon(parent), client_(client) {
// No icon set has been designed yet (that's its own build-order step); a standard
// style icon is a placeholder, not a shipped asset.
if (auto *w = qobject_cast<QWidget *>(parent)) {
setIcon(w->style()->standardIcon(QStyle::SP_ArrowDown));
} else {
setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowDown));
}
// gui/resources/icons/LICENSE — same full-colour glyph as the window icon; the tray
// icon is fixed (not theme-picked) since Adwaita's folder-download already reads
// fine against both light and dark shell backgrounds.
setIcon(IconTheme::appIcon());
setToolTip(QCoreApplication::translate("velox::gui::TrayIcon", "Velox Download Manager"));
auto *menu = new QMenu(parent);
+27
View File
@@ -0,0 +1,27 @@
#include "util/IconTheme.hpp"
#include <QGuiApplication>
#include <QStyleHints>
namespace velox::gui {
IconTheme::IconTheme(QObject *parent) : QObject(parent) {
connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this,
&IconTheme::onColorSchemeChanged);
}
QIcon IconTheme::icon(const QString &name) {
const bool dark = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark;
return QIcon(QStringLiteral(":/icons/%1/%2.svg")
.arg(dark ? QStringLiteral("dark") : QStringLiteral("light"), name));
}
QIcon IconTheme::appIcon() {
return QIcon(QStringLiteral(":/icons/app/velox.svg"));
}
void IconTheme::onColorSchemeChanged() {
emit iconsChanged();
}
} // namespace velox::gui
+34
View File
@@ -0,0 +1,34 @@
// Resolves action icons to the light/dark SVG variant matching the current
// QStyleHints::colorScheme(), same trigger ThemeManager uses for the QSS skin. Lane GUI.
// docs/03-gui-spec.md §1/§7, gui/resources/icons/LICENSE.
#pragma once
#include <QIcon>
#include <QObject>
#include <QString>
namespace velox::gui {
class IconTheme : public QObject {
Q_OBJECT
public:
explicit IconTheme(QObject *parent = nullptr);
/// `name` is a stem under gui/resources/icons/{light,dark}/, e.g. "add-url".
static QIcon icon(const QString &name);
/// The fixed full-colour glyph (window icon, tray icon) — same in both themes.
static QIcon appIcon();
signals:
/// Fired on a live light/dark switch; callers holding an icon(name) result should
/// re-fetch and re-apply it.
void iconsChanged();
private slots:
void onColorSchemeChanged();
};
} // namespace velox::gui
+100
View File
@@ -1 +1,101 @@
Owner: lane PKG/QA. See ../docs/07-packaging.md.
## `.deb` — local test build only
There is no PPA, no GPG signing, no Launchpad wiring here. This produces a `.deb` you
install with `dpkg -i` on the box you built it on (or an identical one), for testing —
not something to hand to a user or publish anywhere. See `docs/07-packaging.md`'s
package matrix for the eventual PPA plan.
```sh
sudo ./tools/bootstrap.sh --packaging # debhelper, dpkg-dev, lintian, devscripts, fakeroot
dpkg-buildpackage -us -uc -b # from the repo root; -b = binary only, no signing
sudo dpkg -i ../velox_0.1.0-1_amd64.deb
lintian ../velox_0.1.0-1_amd64.deb
```
Uninstall: `sudo dpkg -r velox` (or `sudo dpkg -P velox` to purge — see below for what
purge does and does not do here).
### Known limitations of this build — read before relying on any of these
- **`libveloxcore` is static, not `.so.1`.** The install layout in `docs/07-packaging.md`
originally called for a shared, versioned `libveloxcore.so.1`; the user decided against
building one for now (no consumer needs it as a shared object yet, and it would need a
`SOVERSION` added to CORE's own `core/CMakeLists.txt`). The doc was corrected in the
same commit as this packaging work — it no longer describes something unshipped.
- **Placeholder app icon.** `gui/resources/icons/` (GUI's lane) is empty — no real icon
art exists anywhere in the repo yet. `packaging/icons/hicolor/*/apps/velox.png` is a
plain generated placeholder (flat color, a download-arrow glyph), good enough to
install correctly and pass `desktop-file-validate`/lintian's icon checks, and nothing
more. **Replace it before any real release** — and do the icon-set licence audit
`docs/07-packaging.md`'s release checklist asks for once real art exists (this
placeholder was drawn from scratch for this package, so it has no licence question of
its own, but it is not real Velox branding either).
- **No license has been chosen for Velox's own source yet.** There is no top-level
`LICENSE` file in the repo. `debian/copyright` says so plainly (`License:
Unlicensed-TODO`) rather than inventing one — that is a real project decision, above
any one lane. Expect `lintian` to flag this package over it; that is lintian doing its
job, not a bug in this packaging, and it stays a known exception to "lintian clean"
until the project adopts a license. The release checklist's "Licence audit" item (Qt,
libcurl, SQLite, ffmpeg, icon set) is separate and still open regardless.
- **Pairing has no real approval UI in this build.** `libdbus-1-dev` is not in this
build's `Build-Depends` (D1's GUI-dialog/desktop-notification approver needs it and
isn't wired up yet — `daemon/src/main.cpp` still uses `EnvAutoApprover`
unconditionally). That approver auto-**rejects** every pairing request unless
`VELOX_PAIR_AUTO=1` is set in `veloxd`'s environment — there is no prompt, no
accept/decline, it is all-or-nothing. `postinst` prints this prominently after
install; it is repeated here so it isn't missed by anyone reading only one of the two.
**Do not set `VELOX_PAIR_AUTO=1` on a machine or account where auto-accepting every
pairing request is unacceptable.**
- **Socket activation is real.** `velox.service` and `velox.socket` both ship, verbatim
from DAEMON's `packaging/systemd/README.md` — `veloxd` receives the pre-bound fd via
`sd_listen_fds()` (`daemon/src/rpc/systemd_activation.cpp`) rather than binding its own
socket. `debian/rules` does **not** disable `dh_installsystemduser` (only the
system-level `dh_installsystemd`, since these are `--user` units); debhelper finds both
units under `lib/systemd/user/` in the install tree and enables the pair together
(`Also=velox.socket` in `velox.service`'s `[Install]`).
- **The systemd `--user` service starts enabled for your *next* login, not this one.**
debhelper's `dh_installsystemduser` (verified in the built package's `postinst`) marks
`velox.service` enabled automatically — that takes effect the next time each user's
`systemd --user` instance starts. It cannot reach an *already-running* session from a
root maintainer script, so for the session you're in right now, `postinst` prints:
```sh
systemctl --user daemon-reload
systemctl --user start velox.service
```
- **`dpkg -P velox` (purge) does not delete user data.** The uninstall test this package
is built to pass is "manifests, units and sockets removed; user data untouched *unless
purged*" — this build is deliberately conservative and leaves `$XDG_DATA_HOME/velox`
(normally `~/.local/share/velox`, the task database) alone on **both** `remove` and
`purge`, for every account. A root maintainer script deleting per-user data across
every account on the system is exactly the kind of destructive, hard-to-test operation
that deserves its own design pass, not a first cut bolted on here. `postrm` only ever
undoes what `postinst` did: the per-user native-messaging manifest it copied in.
### The uninstall test, concretely
What `docs/07-packaging.md`'s release checklist asks for, and what this build actually
does:
| Removed on `dpkg -r`/`-P`? | |
|---|---|
| `/usr/bin/{veloxd,velox,velox-gui}`, `/usr/libexec/velox/velox-nmhost` | yes — dpkg itself, ordinary package files |
| `/usr/lib/systemd/user/{velox.service,velox.socket}` | yes — same |
| `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` | yes — same |
| Per-user `~/.mozilla/native-messaging-hosts/com.velox.host.json` | yes — `postrm`, both `remove` and `purge` |
| `$XDG_DATA_HOME/velox` (task DB, settings) | **no, on either `remove` or `purge`** — see above |
### Native messaging
`velox-nmhost` (DAEMON's `nmhost/`) and its manifest (DAEMON's
`packaging/nativehost/com.velox.host.json`) now ship in this package — the top-level
`CMakeLists.txt`'s install rules are guarded on `if(TARGET velox-nmhost)`, the same way its
`add_subdirectory()` calls are, so this lit up automatically the day that lane merged, no
packaging change needed on this end.
View File
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=Velox
GenericName=Download Manager
Comment=IDM-class download manager
Exec=velox-gui
Icon=velox
Terminal=false
Categories=Network;FileTransfer;
Keywords=download;downloader;idm;resume;segmented;
StartupNotify=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+20
View File
@@ -0,0 +1,20 @@
.TH VELOX\-GUI 1 "2026-09-12" "velox 0.1.0" "Velox Download Manager"
.SH NAME
velox-gui \- Velox download manager, graphical client
.SH SYNOPSIS
.B velox-gui
.SH DESCRIPTION
.B velox-gui
is the Qt graphical client for
.BR veloxd (8),
the Velox download manager daemon. It takes no command-line arguments and connects
to the daemon over its local Unix socket.
.B veloxd
must already be running \(em see
.BR veloxd (8)
for starting it as a
.B systemd --user
service.
.SH SEE ALSO
.BR velox (1),
.BR veloxd (8)
+35
View File
@@ -0,0 +1,35 @@
.TH VELOXD 8 "2026-09-12" "velox 0.1.0" "Velox Download Manager"
.SH NAME
veloxd \- Velox download manager daemon
.SH SYNOPSIS
.B veloxd
.SH DESCRIPTION
.B veloxd
is the background daemon behind the Velox download manager. It manages downloads,
segmented transfers and resume state in a local SQLite database, and exposes a
JSON-RPC API over a local Unix socket (for
.BR velox (1)
and
.BR velox-gui (1))
and a loopback WebSocket (for the Firefox extension).
.PP
It takes no command-line arguments. It is normally started as a
.B systemd --user
service:
.RS
.nf
systemctl --user enable --now velox.service
.fi
.RE
.SH FILES
.TP
.I $XDG_RUNTIME_DIR/velox/velox.sock
The Unix socket clients connect to.
.TP
.I $XDG_DATA_HOME/velox/velox.db
The task and settings database (default
.IR ~/.local/share/velox/velox.db ).
.SH SEE ALSO
.BR velox (1),
.BR velox-gui (1),
.BR systemctl (1)