souveraine-updater: rename from pachub fork, wire cxx-qt module
This commit is contained in:
commit
e678fd6083
21 changed files with 3057 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
members = ["updater-core", "updater-ui"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.0.0"
|
||||
edition = "2024"
|
||||
license = "GPL-2.0"
|
||||
repository = "https://gitea.wiuf.net/Fimeg/souveraine-updater"
|
||||
339
LICENSE
Normal file
339
LICENSE
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
101
net.souveraine.Updater.svg
Normal file
101
net.souveraine.Updater.svg
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<!-- Background gradient: deep blue-violet -->
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3d2b7a"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a2e"/>
|
||||
</linearGradient>
|
||||
<!-- Box top face gradient -->
|
||||
<linearGradient id="topFace" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#9b6dff"/>
|
||||
<stop offset="100%" style="stop-color:#7c4dff"/>
|
||||
</linearGradient>
|
||||
<!-- Box left face gradient -->
|
||||
<linearGradient id="leftFace" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#5c3bcc"/>
|
||||
<stop offset="100%" style="stop-color:#6d47e0"/>
|
||||
</linearGradient>
|
||||
<!-- Box right face gradient -->
|
||||
<linearGradient id="rightFace" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#4a2fa8"/>
|
||||
<stop offset="100%" style="stop-color:#3d2490"/>
|
||||
</linearGradient>
|
||||
<!-- Arrow/update accent -->
|
||||
<linearGradient id="arrowGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00e5ff"/>
|
||||
<stop offset="100%" style="stop-color:#00b4d8"/>
|
||||
</linearGradient>
|
||||
<!-- Glow filter -->
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="2.5" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<!-- Shadow -->
|
||||
<filter id="shadow" x="-10%" y="-5%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000" flood-opacity="0.5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Rounded background -->
|
||||
<rect width="128" height="128" rx="28" fill="url(#bg)"/>
|
||||
|
||||
<!-- Subtle grid dots -->
|
||||
<g opacity="0.08" fill="#ffffff">
|
||||
<circle cx="20" cy="20" r="1.5"/>
|
||||
<circle cx="40" cy="20" r="1.5"/>
|
||||
<circle cx="60" cy="20" r="1.5"/>
|
||||
<circle cx="80" cy="20" r="1.5"/>
|
||||
<circle cx="100" cy="20" r="1.5"/>
|
||||
<circle cx="20" cy="40" r="1.5"/>
|
||||
<circle cx="100" cy="40" r="1.5"/>
|
||||
<circle cx="20" cy="60" r="1.5"/>
|
||||
<circle cx="100" cy="60" r="1.5"/>
|
||||
<circle cx="20" cy="80" r="1.5"/>
|
||||
<circle cx="100" cy="80" r="1.5"/>
|
||||
<circle cx="20" cy="100" r="1.5"/>
|
||||
<circle cx="40" cy="100" r="1.5"/>
|
||||
<circle cx="60" cy="100" r="1.5"/>
|
||||
<circle cx="80" cy="100" r="1.5"/>
|
||||
<circle cx="100" cy="100" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<!-- ── 3D Package box ─────────────────────────────────────── -->
|
||||
<g filter="url(#shadow)">
|
||||
<!-- Left face -->
|
||||
<polygon points="22,58 64,80 64,108 22,86" fill="url(#leftFace)"/>
|
||||
<!-- Right face -->
|
||||
<polygon points="64,80 106,58 106,86 64,108" fill="url(#rightFace)"/>
|
||||
<!-- Top face -->
|
||||
<polygon points="22,58 64,36 106,58 64,80" fill="url(#topFace)"/>
|
||||
</g>
|
||||
|
||||
<!-- Edge lines for crispness -->
|
||||
<polygon points="22,58 64,36 106,58 64,80" fill="none" stroke="#b39dff" stroke-width="0.8" opacity="0.5"/>
|
||||
<line x1="64" y1="80" x2="64" y2="108" stroke="#7c55dd" stroke-width="0.8" opacity="0.6"/>
|
||||
|
||||
<!-- Top face center tape / stripe -->
|
||||
<polygon points="44,47 64,36 84,47 64,58" fill="#b39dff" opacity="0.35"/>
|
||||
|
||||
<!-- ── Circular update arrow badge (top-right) ───────────── -->
|
||||
<circle cx="93" cy="35" r="18" fill="#1a1a2e" opacity="0.85"/>
|
||||
<circle cx="93" cy="35" r="16" fill="none" stroke="url(#arrowGrad)" stroke-width="2.5"/>
|
||||
|
||||
<!-- Refresh / update arrows -->
|
||||
<g filter="url(#glow)" fill="url(#arrowGrad)">
|
||||
<!-- Top arc arrow (clockwise) -->
|
||||
<path d="M 86,29 A 9,9 0 0 1 100,29" fill="none" stroke="url(#arrowGrad)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<polygon points="100,29 103,24 105,31" fill="url(#arrowGrad)"/>
|
||||
<!-- Bottom arc arrow (clockwise) -->
|
||||
<path d="M 100,41 A 9,9 0 0 1 86,41" fill="none" stroke="url(#arrowGrad)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<polygon points="86,41 83,46 81,39" fill="url(#arrowGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Small sparkle dots on top face -->
|
||||
<circle cx="57" cy="55" r="1.8" fill="#ffffff" opacity="0.5"/>
|
||||
<circle cx="64" cy="51" r="1.2" fill="#ffffff" opacity="0.35"/>
|
||||
<circle cx="71" cy="56" r="1.5" fill="#ffffff" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
12
updater-core/Cargo.toml
Normal file
12
updater-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "updater-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SouveraineOS Updater — pacman backend library"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
112
updater-core/src/aur.rs
Normal file
112
updater-core/src/aur.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
/// Which AUR helper is installed, if any.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AurHelper {
|
||||
Yay,
|
||||
Paru,
|
||||
}
|
||||
|
||||
impl AurHelper {
|
||||
pub fn binary(&self) -> &'static str {
|
||||
match self {
|
||||
AurHelper::Yay => "yay",
|
||||
AurHelper::Paru => "paru",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
AurHelper::Yay => "yay",
|
||||
AurHelper::Paru => "paru",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect installed AUR helper (yay preferred, then paru).
|
||||
pub fn detect() -> Option<AurHelper> {
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("which yay 2>/dev/null")
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
if o.status.success() {
|
||||
return Some(AurHelper::Yay);
|
||||
}
|
||||
}
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("which paru 2>/dev/null")
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
if o.status.success() {
|
||||
return Some(AurHelper::Paru);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn run(cmd: &str) -> Result<(String, i32)> {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run: {cmd}"))?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let code = output.status.code().unwrap_or(1);
|
||||
Ok((stdout, code))
|
||||
}
|
||||
|
||||
/// Search AUR packages: `yay -Ss <query>` or `paru -Ss <query>`.
|
||||
pub fn search(helper: AurHelper, query: &str) -> Result<Vec<Package>> {
|
||||
let (out, _) = run(&format!("{} -Ss {query} 2>/dev/null", helper.binary()))?;
|
||||
let mut pkgs = Vec::new();
|
||||
let mut current_name = String::new();
|
||||
let mut current_version = String::new();
|
||||
|
||||
for line in out.lines() {
|
||||
if line.starts_with(' ') || line.is_empty() {
|
||||
// Description line
|
||||
let desc = line.trim().to_string();
|
||||
if !current_name.is_empty() && !desc.is_empty() {
|
||||
pkgs.push(Package {
|
||||
name: std::mem::take(&mut current_name),
|
||||
version: std::mem::take(&mut current_version),
|
||||
repo: Repo::Foreign,
|
||||
status: PkgStatus::Available,
|
||||
description: desc,
|
||||
is_foreign: true,
|
||||
});
|
||||
}
|
||||
} else if let Some((name_ver, _desc)) = line.split_once(' ') {
|
||||
// "repo/name version" line
|
||||
let name = name_ver
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(name_ver)
|
||||
.to_string();
|
||||
current_name = name;
|
||||
current_version = String::new(); // version parsing from name_ver if needed
|
||||
}
|
||||
}
|
||||
Ok(pkgs)
|
||||
}
|
||||
|
||||
/// Install a package via AUR helper.
|
||||
pub fn install(helper: AurHelper, name: &str) -> Result<(String, bool)> {
|
||||
let (out, code) = run(&format!(
|
||||
"{} -S {name} --noconfirm 2>&1",
|
||||
helper.binary()
|
||||
))?;
|
||||
Ok((out.trim().to_string(), code == 0))
|
||||
}
|
||||
|
||||
/// Upgrade AUR packages: `yay -Sua` or `paru -Sua`.
|
||||
pub fn upgrade(helper: AurHelper) -> Result<(String, bool)> {
|
||||
let (out, code) = run(&format!("{} -Sua --noconfirm 2>&1", helper.binary()))?;
|
||||
Ok((out.trim().to_string(), code == 0))
|
||||
}
|
||||
228
updater-core/src/auth.rs
Normal file
228
updater-core/src/auth.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
//! SouveraineOS session-aware authentication.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! - Privileged operations (install, remove, upgrade) go through polkit.
|
||||
//! - The shell's polkit agent owns the auth prompt and enforces capability tiers.
|
||||
//! - Step-up grants (from StepUpAuth.qml) gate sensitive operations.
|
||||
//! - Session lock state is queried via D-Bus before showing personal data.
|
||||
//!
|
||||
//! The shell's StepUpAuth currently falls back to `pkcheck --action-id
|
||||
//! org.souveraine.stepup` until `souveraine-pam-auth` lands. The updater's
|
||||
//! polkit actions are intercepted by the same agent, so tier policy is enforced
|
||||
//! at the shell level — not duplicated here.
|
||||
//!
|
||||
//! See: SESSION-AUTHORITY-DOCTRINE.md, SESSION-TRUST-ARCHITECTURE.md
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ─── Capability tiers ────────────────────────────────────────────────────────
|
||||
|
||||
/// SouveraineOS capability tiers. Maps to the shell's LockContentPolicy.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Tier {
|
||||
/// No auth required: clock, weather, timers, general read.
|
||||
Ambient,
|
||||
/// Session must be genuinely unlocked: messages, memories, system state.
|
||||
Personal,
|
||||
/// Fresh re-auth required: sends, deletes, payments, system mutation.
|
||||
StepUp,
|
||||
}
|
||||
|
||||
/// Classify an updater operation into its capability tier.
|
||||
pub fn operation_tier(op: Operation) -> Tier {
|
||||
match op {
|
||||
Operation::ListPackages => Tier::Personal,
|
||||
Operation::SearchPackages => Tier::Personal,
|
||||
Operation::ViewPackageInfo => Tier::Personal,
|
||||
Operation::CheckUpdates => Tier::Ambient,
|
||||
Operation::ViewSystemInfo => Tier::Ambient,
|
||||
Operation::InstallPackage => Tier::StepUp,
|
||||
Operation::RemovePackage => Tier::StepUp,
|
||||
Operation::FullUpgrade => Tier::StepUp,
|
||||
Operation::SyncDatabases => Tier::StepUp,
|
||||
Operation::CleanCache => Tier::StepUp,
|
||||
Operation::RemoveOrphans => Tier::StepUp,
|
||||
Operation::RateMirrors => Tier::StepUp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updater operations that map to capability tiers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Operation {
|
||||
ListPackages,
|
||||
SearchPackages,
|
||||
ViewPackageInfo,
|
||||
CheckUpdates,
|
||||
ViewSystemInfo,
|
||||
InstallPackage,
|
||||
RemovePackage,
|
||||
FullUpgrade,
|
||||
SyncDatabases,
|
||||
CleanCache,
|
||||
RemoveOrphans,
|
||||
RateMirrors,
|
||||
}
|
||||
|
||||
// ─── Session state ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Session state queried from the shell via D-Bus / logind.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SessionState {
|
||||
/// Whether the screen is locked (shell requested lock).
|
||||
pub screen_locked: bool,
|
||||
/// Whether the compositor has secured the display (WlSessionLock.secure).
|
||||
pub screen_lock_secure: bool,
|
||||
/// Active step-up grants (action family -> expiry timestamp).
|
||||
pub active_grants: Vec<String>,
|
||||
}
|
||||
|
||||
/// Query the current session lock state from logind.
|
||||
/// This checks `LockedHint` on the current session.
|
||||
pub fn query_session_state() -> Result<SessionState> {
|
||||
let mut state = SessionState::default();
|
||||
|
||||
// Check logind LockedHint
|
||||
let output = Command::new("loginctl")
|
||||
.args(["show-session", "-p", "LockedHint"])
|
||||
.output();
|
||||
if let Ok(out) = output {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
if text.contains("yes") {
|
||||
state.screen_locked = true;
|
||||
state.screen_lock_secure = true; // logind locked = secure
|
||||
}
|
||||
}
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Check if the session allows a given operation based on tier policy.
|
||||
/// Returns Ok(()) if allowed, Err with reason if denied.
|
||||
pub fn check_authorization(op: Operation) -> Result<()> {
|
||||
let tier = operation_tier(op);
|
||||
let session = query_session_state()?;
|
||||
|
||||
match tier {
|
||||
Tier::Ambient => Ok(()),
|
||||
Tier::Personal => {
|
||||
if session.screen_locked || session.screen_lock_secure {
|
||||
anyhow::bail!(
|
||||
"session is locked; {} requires personal tier",
|
||||
op_name(op)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Tier::StepUp => {
|
||||
if session.screen_locked || session.screen_lock_secure {
|
||||
anyhow::bail!(
|
||||
"session is locked; {} requires step-up authentication",
|
||||
op_name(op)
|
||||
);
|
||||
}
|
||||
// Step-up grant check happens at the shell level via polkit.
|
||||
// The updater just needs to call pkexec — the shell's polkit
|
||||
// agent will enforce the tier policy and prompt for re-auth
|
||||
// if needed.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn op_name(op: Operation) -> &'static str {
|
||||
match op {
|
||||
Operation::ListPackages => "list packages",
|
||||
Operation::SearchPackages => "search packages",
|
||||
Operation::ViewPackageInfo => "view package info",
|
||||
Operation::CheckUpdates => "check updates",
|
||||
Operation::ViewSystemInfo => "view system info",
|
||||
Operation::InstallPackage => "install package",
|
||||
Operation::RemovePackage => "remove package",
|
||||
Operation::FullUpgrade => "full upgrade",
|
||||
Operation::SyncDatabases => "sync databases",
|
||||
Operation::CleanCache => "clean cache",
|
||||
Operation::RemoveOrphans => "remove orphans",
|
||||
Operation::RateMirrors => "rate mirrors",
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Privileged execution ────────────────────────────────────────────────────
|
||||
|
||||
/// Run a command with elevated privileges via pkexec.
|
||||
/// The shell's polkit agent intercepts this and enforces step-up auth.
|
||||
/// Returns (combined stdout+stderr, exit_code).
|
||||
pub fn run_privileged(cmd: &str) -> Result<(String, i32)> {
|
||||
let output = Command::new("pkexec")
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.with_context(|| "pkexec failed to launch")?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
let combined = if stderr.is_empty() {
|
||||
stdout
|
||||
} else if stdout.is_empty() {
|
||||
stderr
|
||||
} else {
|
||||
format!("{stdout}\n{stderr}")
|
||||
};
|
||||
let code = output.status.code().unwrap_or(1);
|
||||
Ok((combined.trim().to_string(), code))
|
||||
}
|
||||
|
||||
/// Install packages with session-aware auth.
|
||||
pub fn install_packages(names: &[&str]) -> Result<(String, bool)> {
|
||||
check_authorization(Operation::InstallPackage)?;
|
||||
let pkgs = names.join(" ");
|
||||
let (out, code) = run_privileged(&format!("pacman -S --noconfirm {pkgs}"))?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Remove packages with session-aware auth.
|
||||
pub fn remove_packages(names: &[&str]) -> Result<(String, bool)> {
|
||||
check_authorization(Operation::RemovePackage)?;
|
||||
let pkgs = names.join(" ");
|
||||
let (out, code) = run_privileged(&format!("pacman -R --noconfirm {pkgs}"))?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Remove orphaned packages with session-aware auth.
|
||||
pub fn remove_orphans(names: &[&str]) -> Result<(String, bool)> {
|
||||
check_authorization(Operation::RemoveOrphans)?;
|
||||
let pkgs = names.join(" ");
|
||||
let (out, code) = run_privileged(&format!("pacman -Rns --noconfirm {pkgs}"))?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Full system upgrade with session-aware auth.
|
||||
pub fn full_upgrade() -> Result<(String, bool)> {
|
||||
check_authorization(Operation::FullUpgrade)?;
|
||||
let (out, code) = run_privileged("pacman -Syu --noconfirm")?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Sync databases with session-aware auth.
|
||||
pub fn sync_databases() -> Result<(String, bool)> {
|
||||
check_authorization(Operation::SyncDatabases)?;
|
||||
let (out, code) = run_privileged("pacman -Sy")?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Clean package cache with session-aware auth.
|
||||
pub fn clean_cache() -> Result<(String, bool)> {
|
||||
check_authorization(Operation::CleanCache)?;
|
||||
let (out, code) = run_privileged("pacman -Sc --noconfirm")?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
|
||||
/// Rate mirrors with session-aware auth.
|
||||
pub fn rate_mirrors() -> Result<(String, bool)> {
|
||||
check_authorization(Operation::RateMirrors)?;
|
||||
let (out, code) = run_privileged("rate-mirrors --allow-root arch 2>&1")?;
|
||||
Ok((out, code == 0))
|
||||
}
|
||||
120
updater-core/src/cache.rs
Normal file
120
updater-core/src/cache.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const SYNCDB_TTL: Duration = Duration::from_secs(6 * 3600); // 6 hours
|
||||
|
||||
fn cache_dir() -> PathBuf {
|
||||
dirs().0
|
||||
}
|
||||
|
||||
fn dirs() -> (PathBuf, PathBuf, PathBuf, PathBuf) {
|
||||
let base = PathBuf::from(
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()),
|
||||
)
|
||||
.join(".cache")
|
||||
.join("souveraine-updater");
|
||||
let packages = base.join("packages.json");
|
||||
let syncdb = base.join("syncdb.json");
|
||||
let installed = base.join("installed.json");
|
||||
(base, packages, syncdb, installed)
|
||||
}
|
||||
|
||||
pub fn ensure_cache_dir() {
|
||||
let (base, _, _, _) = dirs();
|
||||
let _ = fs::create_dir_all(&base);
|
||||
}
|
||||
|
||||
fn file_age(path: &PathBuf) -> Duration {
|
||||
fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.map(|t| SystemTime::now().duration_since(t).unwrap_or_default())
|
||||
.unwrap_or(Duration::from_secs(u64::MAX))
|
||||
}
|
||||
|
||||
fn read_json<T: for<'de> Deserialize<'de>>(path: &PathBuf) -> Option<T> {
|
||||
let data = fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&data).ok()
|
||||
}
|
||||
|
||||
fn write_json<T: Serialize + ?Sized>(path: &PathBuf, data: &T) -> Result<()> {
|
||||
ensure_cache_dir();
|
||||
let tmp = path.with_extension("tmp");
|
||||
fs::write(&tmp, serde_json::to_string(data)?)?;
|
||||
fs::rename(&tmp, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cache layer for sync DB (pacman -Sl output).
|
||||
pub struct SyncDbCache;
|
||||
|
||||
impl SyncDbCache {
|
||||
pub fn load() -> Option<Vec<crate::models::Package>> {
|
||||
let (_, _, syncdb, _) = dirs();
|
||||
if file_age(&syncdb) < SYNCDB_TTL {
|
||||
read_json(&syncdb)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(packages: &[crate::models::Package]) -> Result<()> {
|
||||
let (_, _, syncdb, _) = dirs();
|
||||
write_json(&syncdb, packages)
|
||||
}
|
||||
|
||||
pub fn invalidate() {
|
||||
let (_, _, syncdb, _) = dirs();
|
||||
let _ = fs::remove_file(&syncdb);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache layer for installed packages fingerprint.
|
||||
pub struct InstalledCache;
|
||||
|
||||
impl InstalledCache {
|
||||
pub fn load() -> Option<Vec<crate::models::Package>> {
|
||||
let (_, _, _, installed) = dirs();
|
||||
read_json(&installed)
|
||||
}
|
||||
|
||||
pub fn store(packages: &[crate::models::Package]) -> Result<()> {
|
||||
let (_, _, _, installed) = dirs();
|
||||
write_json(&installed, packages)
|
||||
}
|
||||
|
||||
pub fn invalidate() {
|
||||
let (_, _, _, installed) = dirs();
|
||||
let _ = fs::remove_file(&installed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache layer for merged package list (served instantly on next launch).
|
||||
pub struct PackageCache;
|
||||
|
||||
impl PackageCache {
|
||||
pub fn load() -> Option<Vec<crate::models::Package>> {
|
||||
let (_, packages, _, _) = dirs();
|
||||
read_json(&packages)
|
||||
}
|
||||
|
||||
pub fn store(pkgs: &[crate::models::Package]) -> Result<()> {
|
||||
let (_, path, _, _) = dirs();
|
||||
write_json(&path, pkgs)
|
||||
}
|
||||
|
||||
pub fn invalidate() {
|
||||
let (_, packages, _, _) = dirs();
|
||||
let _ = fs::remove_file(&packages);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidate all caches.
|
||||
pub fn invalidate_all() {
|
||||
SyncDbCache::invalidate();
|
||||
InstalledCache::invalidate();
|
||||
PackageCache::invalidate();
|
||||
}
|
||||
5
updater-core/src/lib.rs
Normal file
5
updater-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod auth;
|
||||
pub mod aur;
|
||||
pub mod cache;
|
||||
pub mod models;
|
||||
pub mod pacman;
|
||||
156
updater-core/src/models.rs
Normal file
156
updater-core/src/models.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Repository a package belongs to.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Repo {
|
||||
Core,
|
||||
Extra,
|
||||
Multilib,
|
||||
Souveraine,
|
||||
Local,
|
||||
Foreign,
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
pub fn badge_class(&self) -> &'static str {
|
||||
match self {
|
||||
Repo::Core => "badge-core",
|
||||
Repo::Extra => "badge-extra",
|
||||
Repo::Multilib => "badge-multilib",
|
||||
Repo::Souveraine => "badge-souveraine",
|
||||
Repo::Local => "badge-local",
|
||||
Repo::Foreign => "badge-foreign",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Repo::Core => "CORE",
|
||||
Repo::Extra => "EXTRA",
|
||||
Repo::Multilib => "MULTILIB",
|
||||
Repo::Souveraine => "SOVERAINE",
|
||||
Repo::Local => "LOCAL",
|
||||
Repo::Foreign => "AUR",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Repo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.label())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Repo {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
"core" => Repo::Core,
|
||||
"extra" => Repo::Extra,
|
||||
"multilib" => Repo::Multilib,
|
||||
"souveraine-aarch64" | "souveraine" => Repo::Souveraine,
|
||||
_ => Repo::Local,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install status of a package.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PkgStatus {
|
||||
Installed,
|
||||
Available,
|
||||
Update,
|
||||
Foreign,
|
||||
}
|
||||
|
||||
impl PkgStatus {
|
||||
pub fn css_class(&self) -> &'static str {
|
||||
match self {
|
||||
PkgStatus::Installed => "status-installed",
|
||||
PkgStatus::Available => "status-available",
|
||||
PkgStatus::Update => "status-update",
|
||||
PkgStatus::Foreign => "status-foreign",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
PkgStatus::Installed => "INSTALLED",
|
||||
PkgStatus::Available => "AVAILABLE",
|
||||
PkgStatus::Update => "UPDATE",
|
||||
PkgStatus::Foreign => "FOREIGN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single package in the system.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Package {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub repo: Repo,
|
||||
pub status: PkgStatus,
|
||||
pub description: String,
|
||||
pub is_foreign: bool,
|
||||
}
|
||||
|
||||
/// Detailed info for a package (from pacman -Si / -Qi).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PackageInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub architecture: String,
|
||||
pub url: String,
|
||||
pub licenses: String,
|
||||
pub groups: String,
|
||||
pub provides: String,
|
||||
pub depends_on: Vec<String>,
|
||||
pub optional_deps: Vec<String>,
|
||||
pub required_by: Vec<String>,
|
||||
pub optional_for: Vec<String>,
|
||||
pub conflicts_with: Vec<String>,
|
||||
pub replaces: String,
|
||||
pub download_size: String,
|
||||
pub installed_size: String,
|
||||
pub packager: String,
|
||||
pub build_date: String,
|
||||
pub install_date: String,
|
||||
pub install_reason: String,
|
||||
pub install_script: String,
|
||||
pub md5sum: String,
|
||||
pub sha256sum: String,
|
||||
pub signatures: String,
|
||||
}
|
||||
|
||||
/// Update info for a single pending update.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub name: String,
|
||||
pub current_version: String,
|
||||
pub new_version: String,
|
||||
pub repo: Repo,
|
||||
}
|
||||
|
||||
/// System information snapshot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SystemInfo {
|
||||
pub hostname: String,
|
||||
pub kernel: String,
|
||||
pub architecture: String,
|
||||
pub uptime: String,
|
||||
pub total_ram: String,
|
||||
pub used_ram: String,
|
||||
pub disk_total: String,
|
||||
pub disk_used: String,
|
||||
pub disk_free: String,
|
||||
pub total_packages: usize,
|
||||
pub foreign_packages: usize,
|
||||
pub cache_size: String,
|
||||
}
|
||||
|
||||
/// Repo summary for sidebar navigation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepoSummary {
|
||||
pub repo: Repo,
|
||||
pub count: usize,
|
||||
}
|
||||
316
updater-core/src/pacman.rs
Normal file
316
updater-core/src/pacman.rs
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
/// Run a shell command and return (stdout, exit_code).
|
||||
fn run(cmd: &str) -> Result<(String, i32)> {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run: {cmd}"))?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let code = output.status.code().unwrap_or(1);
|
||||
Ok((stdout, code))
|
||||
}
|
||||
|
||||
/// Check if pacman is available (i.e. we're on Arch).
|
||||
pub fn is_arch() -> bool {
|
||||
run("which pacman 2>/dev/null")
|
||||
.map(|(_, c)| c == 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// List all installed packages: `pacman -Q`.
|
||||
pub fn installed_packages() -> Result<Vec<Package>> {
|
||||
let (out, code) = run("pacman -Q 2>/dev/null")?;
|
||||
if code != 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut pkgs = Vec::new();
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let name = parts.next().unwrap_or("").to_string();
|
||||
let version = parts.next().unwrap_or("").to_string();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
pkgs.push(Package {
|
||||
name,
|
||||
version,
|
||||
repo: Repo::Local,
|
||||
status: PkgStatus::Installed,
|
||||
description: String::new(),
|
||||
is_foreign: false,
|
||||
});
|
||||
}
|
||||
Ok(pkgs)
|
||||
}
|
||||
|
||||
/// List foreign (AUR) packages: `pacman -Qm`.
|
||||
pub fn foreign_packages() -> Result<Vec<Package>> {
|
||||
let (out, code) = run("pacman -Qm 2>/dev/null")?;
|
||||
if code != 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut pkgs = Vec::new();
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let name = parts.next().unwrap_or("").to_string();
|
||||
let version = parts.next().unwrap_or("").to_string();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
pkgs.push(Package {
|
||||
name,
|
||||
version,
|
||||
repo: Repo::Foreign,
|
||||
status: PkgStatus::Foreign,
|
||||
description: String::new(),
|
||||
is_foreign: true,
|
||||
});
|
||||
}
|
||||
Ok(pkgs)
|
||||
}
|
||||
|
||||
/// List all synced packages: `pacman -Sl`.
|
||||
pub fn sync_packages() -> Result<Vec<Package>> {
|
||||
let (out, code) = run("pacman -Sl 2>/dev/null")?;
|
||||
if code != 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut pkgs = Vec::new();
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let repo_str = parts.next().unwrap_or("");
|
||||
let name = parts.next().unwrap_or("");
|
||||
let version = parts.next().unwrap_or("");
|
||||
// The rest is description
|
||||
let desc: String = parts.collect::<Vec<_>>().join(" ");
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let repo = Repo::from(repo_str);
|
||||
pkgs.push(Package {
|
||||
name: name.to_string(),
|
||||
version: version.to_string(),
|
||||
repo,
|
||||
status: PkgStatus::Available,
|
||||
description: desc,
|
||||
is_foreign: false,
|
||||
});
|
||||
}
|
||||
Ok(pkgs)
|
||||
}
|
||||
|
||||
/// Check for pending updates: `pacman -Qu`.
|
||||
pub fn check_updates() -> Result<Vec<UpdateInfo>> {
|
||||
let (out, code) = run("pacman -Qu 2>/dev/null")?;
|
||||
if code != 0 || out.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut updates = Vec::new();
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let name = parts.next().unwrap_or("").to_string();
|
||||
let current = parts.next().unwrap_or("").to_string();
|
||||
// "->"
|
||||
parts.next();
|
||||
let new_ver = parts.next().unwrap_or("").to_string();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
updates.push(UpdateInfo {
|
||||
name,
|
||||
current_version: current,
|
||||
new_version: new_ver,
|
||||
repo: Repo::Local, // will be enriched later
|
||||
});
|
||||
}
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
/// Detailed info for a package: `pacman -Si <name>` or `pacman -Qi <name>`.
|
||||
pub fn package_info(name: &str) -> Result<PackageInfo> {
|
||||
// Try sync DB first, fall back to local
|
||||
let (out, _) = run(&format!("pacman -Si {name} 2>/dev/null"))
|
||||
.unwrap_or_default();
|
||||
let out = if out.trim().is_empty() {
|
||||
let (out, _) = run(&format!("pacman -Qi {name} 2>/dev/null"))
|
||||
.unwrap_or_default();
|
||||
out
|
||||
} else {
|
||||
out
|
||||
};
|
||||
|
||||
let mut info = PackageInfo::default();
|
||||
for line in out.lines() {
|
||||
if let Some((key, val)) = line.split_once(':') {
|
||||
let key = key.trim();
|
||||
let val = val.trim();
|
||||
match key {
|
||||
"Name" => info.name = val.to_string(),
|
||||
"Version" => info.version = val.to_string(),
|
||||
"Description" => info.description = val.to_string(),
|
||||
"Architecture" => info.architecture = val.to_string(),
|
||||
"URL" => info.url = val.to_string(),
|
||||
"Licenses" => info.licenses = val.to_string(),
|
||||
"Groups" => info.groups = val.to_string(),
|
||||
"Provides" => info.provides = val.to_string(),
|
||||
"Depends On" => {
|
||||
info.depends_on = val
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"Optional Deps" => {
|
||||
info.optional_deps = val
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"Required By" => {
|
||||
info.required_by = val
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"Optional For" => {
|
||||
info.optional_for = val
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"Conflicts With" => {
|
||||
info.conflicts_with = val
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
"Replaces" => info.replaces = val.to_string(),
|
||||
"Download Size" => info.download_size = val.to_string(),
|
||||
"Installed Size" => info.installed_size = val.to_string(),
|
||||
"Packager" => info.packager = val.to_string(),
|
||||
"Build Date" => info.build_date = val.to_string(),
|
||||
"Install Date" => info.install_date = val.to_string(),
|
||||
"Install Reason" => info.install_reason = val.to_string(),
|
||||
"Install Script" => info.install_script = val.to_string(),
|
||||
"MD5 Sum" => info.md5sum = val.to_string(),
|
||||
"SHA256 Sum" => info.sha256sum = val.to_string(),
|
||||
"Signatures" => info.signatures = val.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// List files owned by a package: `pacman -Ql <name>`.
|
||||
pub fn package_files(name: &str) -> Result<Vec<String>> {
|
||||
let (out, _) = run(&format!("pacman -Ql {name} 2>/dev/null"))?;
|
||||
let files: Vec<String> = out
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
// Format: "package /path/to/file"
|
||||
line.split_once(' ').map(|(_, path)| path.to_string())
|
||||
})
|
||||
.collect();
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// List orphaned packages: `pacman -Qdt`.
|
||||
pub fn orphans() -> Result<Vec<Package>> {
|
||||
let (out, _) = run("pacman -Qdt 2>/dev/null")?;
|
||||
let mut pkgs = Vec::new();
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let name = parts.next().unwrap_or("").to_string();
|
||||
let version = parts.next().unwrap_or("").to_string();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
pkgs.push(Package {
|
||||
name,
|
||||
version,
|
||||
repo: Repo::Local,
|
||||
status: PkgStatus::Installed,
|
||||
description: String::new(),
|
||||
is_foreign: false,
|
||||
});
|
||||
}
|
||||
Ok(pkgs)
|
||||
}
|
||||
|
||||
/// Get system information.
|
||||
pub fn system_info() -> Result<SystemInfo> {
|
||||
let mut info = SystemInfo::default();
|
||||
|
||||
// Hostname
|
||||
if let Ok((h, _)) = run("hostname") {
|
||||
info.hostname = h.trim().to_string();
|
||||
}
|
||||
// Kernel
|
||||
if let Ok((k, _)) = run("uname -r") {
|
||||
info.kernel = k.trim().to_string();
|
||||
}
|
||||
// Architecture
|
||||
if let Ok((a, _)) = run("uname -m") {
|
||||
info.architecture = a.trim().to_string();
|
||||
}
|
||||
// Uptime
|
||||
if let Ok((u, _)) = run("uptime -p 2>/dev/null || uptime") {
|
||||
info.uptime = u.trim().to_string();
|
||||
}
|
||||
// RAM
|
||||
if let Ok((m, _)) = run("free -h | awk '/Mem:/{print $2, $3}'") {
|
||||
let mut parts = m.split_whitespace();
|
||||
info.total_ram = parts.next().unwrap_or("?").to_string();
|
||||
info.used_ram = parts.next().unwrap_or("?").to_string();
|
||||
}
|
||||
// Disk
|
||||
if let Ok((d, _)) = run("df -h / | awk 'NR==2{print $2, $3, $4}'") {
|
||||
let mut parts = d.split_whitespace();
|
||||
info.disk_total = parts.next().unwrap_or("?").to_string();
|
||||
info.disk_used = parts.next().unwrap_or("?").to_string();
|
||||
info.disk_free = parts.next().unwrap_or("?").to_string();
|
||||
}
|
||||
// Package counts
|
||||
if let Ok((c, _)) = run("pacman -Q 2>/dev/null | wc -l") {
|
||||
info.total_packages = c.trim().parse().unwrap_or(0);
|
||||
}
|
||||
if let Ok((c, _)) = run("pacman -Qm 2>/dev/null | wc -l") {
|
||||
info.foreign_packages = c.trim().parse().unwrap_or(0);
|
||||
}
|
||||
// Cache size
|
||||
if let Ok((s, _)) = run("du -sh /var/cache/pacman/pkg/ 2>/dev/null | cut -f1") {
|
||||
info.cache_size = s.trim().to_string();
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Sync databases: `pacman -Sy`. Returns true on success.
|
||||
pub fn sync_databases() -> Result<bool> {
|
||||
let (_, code) = run("pacman -Sy")?;
|
||||
Ok(code == 0)
|
||||
}
|
||||
|
||||
/// Full system upgrade: `pacman -Syu`. Returns (output_lines, success).
|
||||
pub fn full_upgrade() -> Result<(String, bool)> {
|
||||
let (out, code) = run("pacman -Syu --noconfirm")?;
|
||||
Ok((out.trim().to_string(), code == 0))
|
||||
}
|
||||
|
||||
/// Clean package cache: `pacman -Sc --noconfirm`.
|
||||
pub fn clean_cache() -> Result<(String, bool)> {
|
||||
let (out, code) = run("pacman -Sc --noconfirm")?;
|
||||
Ok((out.trim().to_string(), code == 0))
|
||||
}
|
||||
|
||||
/// Rate mirrors using rate-mirrors (if installed).
|
||||
pub fn rate_mirrors() -> Result<(String, bool)> {
|
||||
let (out, code) = run("rate-mirrors --allow-root arch 2>&1")?;
|
||||
Ok((out.trim().to_string(), code == 0))
|
||||
}
|
||||
23
updater-ui/Cargo.toml
Normal file
23
updater-ui/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "updater-ui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SouveraineOS Updater — Qt/QML frontend"
|
||||
|
||||
[[bin]]
|
||||
name = "souveraine-updater"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
updater-core = { path = "../updater-core" }
|
||||
cxx = "1"
|
||||
cxx-qt = "0.9"
|
||||
cxx-qt-lib = { version = "0.9", features = ["qt_full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
|
||||
[build-dependencies]
|
||||
cxx-qt-build = "0.9"
|
||||
qt-build-utils = "0.9"
|
||||
49
updater-ui/build.rs
Normal file
49
updater-ui/build.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! CXX-Qt build wiring for the SouveraineOS Updater.
|
||||
//!
|
||||
//! Registers the QML module `com.souveraine.updater` and compiles the bridge
|
||||
//! that exposes the `PackageManager` QObject. The QML files are embedded in the
|
||||
//! qrc by cxx-qt-build, so the binary carries its own UI — it does NOT read
|
||||
//! loose files out of /usr/share at runtime (the old pachub behaviour, which
|
||||
//! breaks the moment the install prefix moves).
|
||||
//!
|
||||
//! QML files stay crate-relative: cxx-qt-build 0.9 derives the qrc alias from
|
||||
//! the source path, so a `../qml` source would embed under a literal `../qml/…`
|
||||
//! alias. Crate-relative gives the clean
|
||||
//! `qrc:/qt/qml/com/souveraine/updater/qml/Main.qml`. Same reasoning as culver.
|
||||
|
||||
use cxx_qt_build::{CxxQtBuilder, QmlModule};
|
||||
use qt_build_utils::QmlFile;
|
||||
|
||||
fn main() {
|
||||
let main_qml = "qml/Main.qml";
|
||||
|
||||
// Every view lives in the ONE module com.souveraine.updater. cxx-qt-build
|
||||
// embeds each into the qrc and registers it as a type (filename = type
|
||||
// name), so Main.qml can use `Sidebar {}`, `PackageList {}` etc. without a
|
||||
// relative `import "."`, which does not resolve from a qrc. Miss a file a
|
||||
// view instantiates and the root component silently constructs to null.
|
||||
let views = [
|
||||
"qml/Sidebar.qml",
|
||||
"qml/PackageList.qml",
|
||||
"qml/PackageDetail.qml",
|
||||
"qml/StatCard.qml",
|
||||
"qml/NavButton.qml",
|
||||
];
|
||||
|
||||
println!("cargo:rerun-if-changed={main_qml}");
|
||||
for v in views {
|
||||
println!("cargo:rerun-if-changed={v}");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=src/bridge.rs");
|
||||
|
||||
let mut files = vec![QmlFile::from(main_qml)];
|
||||
files.extend(views.iter().map(|v| QmlFile::from(*v)));
|
||||
|
||||
CxxQtBuilder::new_qml_module(QmlModule::new("com.souveraine.updater").qml_files(files))
|
||||
.file("src/bridge.rs")
|
||||
.qt_module("Core")
|
||||
.qt_module("Gui")
|
||||
.qt_module("Qml")
|
||||
.qt_module("Quick")
|
||||
.build();
|
||||
}
|
||||
250
updater-ui/qml/Main.qml
Normal file
250
updater-ui/qml/Main.qml
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
title: "SouveraineOS Updater"
|
||||
width: 1240
|
||||
height: 780
|
||||
minimumWidth: 360
|
||||
minimumHeight: 480
|
||||
visible: true
|
||||
color: palette.window
|
||||
|
||||
property bool narrow: width < 720
|
||||
|
||||
// The PackageManager QObject from Rust
|
||||
// Registered via cxx_qt's QML module
|
||||
PackageManager {
|
||||
id: pm
|
||||
onPackagesLoaded: {
|
||||
if (narrow) {
|
||||
stackView.replace(packageListComponent)
|
||||
}
|
||||
}
|
||||
onOperationCompleted: function(success) {
|
||||
if (success) {
|
||||
pm.loadPackages()
|
||||
}
|
||||
outputDrawer.open()
|
||||
}
|
||||
onDetailLoaded: {
|
||||
if (narrow) {
|
||||
stackView.push(detailComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
pm.loadPackages()
|
||||
pm.checkUpdates()
|
||||
}
|
||||
|
||||
// ── Narrow layout: stack navigation ──
|
||||
StackView {
|
||||
id: stackView
|
||||
anchors.fill: parent
|
||||
visible: narrow
|
||||
initialItem: sidebarComponent
|
||||
}
|
||||
|
||||
// ── Wide layout: split view ──
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
visible: !narrow
|
||||
spacing: 0
|
||||
|
||||
// Sidebar
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 260
|
||||
Layout.minimumWidth: 160
|
||||
Layout.fillHeight: true
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 0
|
||||
|
||||
Sidebar {
|
||||
anchors.fill: parent
|
||||
pm: pm
|
||||
narrow: false
|
||||
}
|
||||
}
|
||||
|
||||
// Content area
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
// Toolbar
|
||||
ToolBar {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
ToolButton {
|
||||
text: "⟳ Check Updates"
|
||||
onClicked: pm.checkUpdates()
|
||||
}
|
||||
ToolButton {
|
||||
text: "↑ Upgrade All"
|
||||
onClicked: pm.fullUpgrade()
|
||||
enabled: pm.hasUpdates
|
||||
}
|
||||
ToolButton {
|
||||
text: "☰ Tools"
|
||||
onClicked: toolsMenu.popup()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: pm.statusText
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content: list + detail
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
PackageList {
|
||||
SplitView.preferredWidth: parent.width * 0.45
|
||||
SplitView.minimumWidth: 200
|
||||
pm: pm
|
||||
narrow: root.narrow
|
||||
}
|
||||
|
||||
PackageDetail {
|
||||
SplitView.fillWidth: true
|
||||
pm: pm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sidebar component (for stack view) ──
|
||||
Component {
|
||||
id: sidebarComponent
|
||||
Sidebar {
|
||||
pm: pm
|
||||
narrow: root.narrow
|
||||
onPackageListRequested: stackView.push(packageListComponent)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Package list component (for stack view) ──
|
||||
Component {
|
||||
id: packageListComponent
|
||||
PackageList {
|
||||
pm: pm
|
||||
narrow: root.narrow
|
||||
}
|
||||
}
|
||||
|
||||
// ── Detail component (for stack view) ──
|
||||
Component {
|
||||
id: detailComponent
|
||||
PackageDetail {
|
||||
pm: pm
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tools menu ──
|
||||
Menu {
|
||||
id: toolsMenu
|
||||
MenuItem { text: "Sync Databases"; onTriggered: pm.syncDatabases() }
|
||||
MenuItem { text: "Rate Mirrors"; onTriggered: pm.rateMirrors() }
|
||||
MenuItem { text: "Clean Cache"; onTriggered: pm.cleanCache() }
|
||||
MenuItem { text: "Remove Orphans"; onTriggered: pm.removeOrphans() }
|
||||
MenuSeparator {}
|
||||
MenuItem { text: "System Info"; onTriggered: systemInfoDialog.open() }
|
||||
}
|
||||
|
||||
// ── Operation output drawer ──
|
||||
Drawer {
|
||||
id: outputDrawer
|
||||
edge: Qt.BottomEdge
|
||||
width: parent.width
|
||||
height: parent.height * 0.4
|
||||
modal: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
text: "Operation Output"
|
||||
font.bold: true
|
||||
font.pointSize: 14
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: "Close"
|
||||
onClicked: outputDrawer.close()
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
TextArea {
|
||||
text: pm.operationOutput
|
||||
readOnly: true
|
||||
font.family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace
|
||||
font.pointSize: 10
|
||||
wrapMode: TextArea.Wrap
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
color: Qt.rgba(0.05, 0.067, 0.09, 1) // dark terminal
|
||||
radius: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── System info dialog ──
|
||||
Dialog {
|
||||
id: systemInfoDialog
|
||||
title: "System Information"
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(500, parent.width - 48)
|
||||
height: Math.min(400, parent.height - 48)
|
||||
modal: true
|
||||
|
||||
Component.onCompleted: pm.loadSystemInfo()
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
TextArea {
|
||||
text: {
|
||||
try {
|
||||
var info = JSON.parse(pm.systemInfoJson)
|
||||
return "Hostname: " + info.hostname +
|
||||
"\nKernel: " + info.kernel +
|
||||
"\nArchitecture: " + info.architecture +
|
||||
"\nUptime: " + info.uptime +
|
||||
"\nRAM: " + info.used_ram + " / " + info.total_ram +
|
||||
"\nDisk: " + info.disk_used + " / " + info.disk_total + " (" + info.disk_free + " free)" +
|
||||
"\nPackages: " + info.total_packages + " (" + info.foreign_packages + " foreign)" +
|
||||
"\nCache: " + info.cache_size
|
||||
} catch(e) {
|
||||
return "Loading..."
|
||||
}
|
||||
}
|
||||
readOnly: true
|
||||
wrapMode: TextArea.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
standardButtons: Dialog.Ok
|
||||
}
|
||||
}
|
||||
55
updater-ui/qml/NavButton.qml
Normal file
55
updater-ui/qml/NavButton.qml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ItemDelegate {
|
||||
id: root
|
||||
|
||||
property string iconText: ""
|
||||
property bool highlight: false
|
||||
|
||||
implicitHeight: 40
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: root.highlighted ? Qt.alpha(palette.highlight, 0.16) :
|
||||
root.hovered ? Qt.alpha(palette.text, 0.05) : "transparent"
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: iconText
|
||||
font.pointSize: 12
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
opacity: highlight ? 1.0 : 0.6
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.text
|
||||
font.weight: Font.DemiBold
|
||||
font.pointSize: 11
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Update count badge
|
||||
Rectangle {
|
||||
visible: highlight
|
||||
radius: 999
|
||||
implicitWidth: 20
|
||||
implicitHeight: 16
|
||||
color: Qt.alpha("#e5a50a", 0.2)
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "!"
|
||||
font.pointSize: 8
|
||||
font.weight: Font.ExtraBold
|
||||
color: "#c38600"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
306
updater-ui/qml/PackageDetail.qml
Normal file
306
updater-ui/qml/PackageDetail.qml
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pm
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
// Hero section
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: heroContent.implicitHeight + 32
|
||||
radius: 12
|
||||
color: Qt.alpha(palette.text, 0.04)
|
||||
|
||||
ColumnLayout {
|
||||
id: heroContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: "📦"
|
||||
font.pointSize: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Label {
|
||||
id: detailName
|
||||
text: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.name || "Select a package"
|
||||
} catch(e) { return "Select a package" }
|
||||
}
|
||||
font.pointSize: 18
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Label {
|
||||
text: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.version || ""
|
||||
} catch(e) { return "" }
|
||||
}
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
ColumnLayout {
|
||||
visible: detailName.text !== "Select a package"
|
||||
spacing: 4
|
||||
|
||||
Button {
|
||||
text: "Install"
|
||||
onClicked: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
pm.installPackage(info.name)
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: "Remove"
|
||||
onClicked: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
pm.removePackage(info.name)
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.description || ""
|
||||
} catch(e) { return "" }
|
||||
}
|
||||
wrapMode: Label.Wrap
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detail tabs
|
||||
TabBar {
|
||||
id: detailTabs
|
||||
Layout.fillWidth: true
|
||||
|
||||
TabButton { text: "Info" }
|
||||
TabButton { text: "Files" }
|
||||
TabButton { text: "Dependencies" }
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: detailTabs.currentIndex
|
||||
|
||||
// Info tab
|
||||
ScrollView {
|
||||
clip: true
|
||||
GridLayout {
|
||||
columns: 2
|
||||
columnSpacing: 16
|
||||
rowSpacing: 6
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return [
|
||||
{ label: "Architecture", value: info.architecture },
|
||||
{ label: "URL", value: info.url },
|
||||
{ label: "Licenses", value: info.licenses },
|
||||
{ label: "Groups", value: info.groups },
|
||||
{ label: "Provides", value: info.provides },
|
||||
{ label: "Replaces", value: info.replaces },
|
||||
{ label: "Download Size", value: info.download_size },
|
||||
{ label: "Installed Size", value: info.installed_size },
|
||||
{ label: "Packager", value: info.packager },
|
||||
{ label: "Build Date", value: info.build_date },
|
||||
{ label: "Install Date", value: info.install_date },
|
||||
{ label: "Install Reason", value: info.install_reason },
|
||||
].filter(function(i) { return i.value && i.value !== "None" && i.value !== "" })
|
||||
} catch(e) { return [] }
|
||||
}
|
||||
|
||||
delegate: RowLayout {
|
||||
Layout.columnSpan: 2
|
||||
spacing: 8
|
||||
visible: modelData.value
|
||||
|
||||
Label {
|
||||
text: modelData.label + ":"
|
||||
font.weight: Font.DemiBold
|
||||
opacity: 0.6
|
||||
Layout.preferredWidth: 120
|
||||
}
|
||||
Label {
|
||||
text: modelData.value
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Label.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Files tab
|
||||
ScrollView {
|
||||
clip: true
|
||||
ListView {
|
||||
model: {
|
||||
try { return JSON.parse(pm.filesJson) } catch(e) { return [] }
|
||||
}
|
||||
delegate: Label {
|
||||
text: modelData
|
||||
font.family: "JetBrains Mono", "Fira Code", monospace
|
||||
font.pointSize: 9
|
||||
padding: 2
|
||||
width: parent.width
|
||||
elide: Label.ElideMiddle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dependencies tab
|
||||
ScrollView {
|
||||
clip: true
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
// Depends on
|
||||
Label {
|
||||
text: "Depends On"
|
||||
font.weight: Font.Bold
|
||||
visible: depsRepeater.count > 0
|
||||
}
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Repeater {
|
||||
id: depsRepeater
|
||||
model: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.depends_on || []
|
||||
} catch(e) { return [] }
|
||||
}
|
||||
delegate: Rectangle {
|
||||
radius: 999
|
||||
implicitWidth: depLabel.implicitWidth + 16
|
||||
implicitHeight: depLabel.implicitHeight + 8
|
||||
color: Qt.alpha(palette.text, 0.07)
|
||||
border.color: Qt.alpha(palette.text, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Label {
|
||||
id: depLabel
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pointSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional deps
|
||||
Label {
|
||||
text: "Optional Dependencies"
|
||||
font.weight: Font.Bold
|
||||
visible: optDepsRepeater.count > 0
|
||||
}
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Repeater {
|
||||
id: optDepsRepeater
|
||||
model: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.optional_deps || []
|
||||
} catch(e) { return [] }
|
||||
}
|
||||
delegate: Rectangle {
|
||||
radius: 999
|
||||
implicitWidth: optDepLabel.implicitWidth + 16
|
||||
implicitHeight: optDepLabel.implicitHeight + 8
|
||||
color: Qt.alpha(palette.text, 0.04)
|
||||
border.color: Qt.alpha(palette.text, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Label {
|
||||
id: optDepLabel
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pointSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Required by
|
||||
Label {
|
||||
text: "Required By"
|
||||
font.weight: Font.Bold
|
||||
visible: reqByRepeater.count > 0
|
||||
}
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Repeater {
|
||||
id: reqByRepeater
|
||||
model: {
|
||||
try {
|
||||
var info = JSON.parse(pm.detailJson)
|
||||
return info.required_by || []
|
||||
} catch(e) { return [] }
|
||||
}
|
||||
delegate: Rectangle {
|
||||
radius: 999
|
||||
implicitWidth: reqByLabel.implicitWidth + 16
|
||||
implicitHeight: reqByLabel.implicitHeight + 8
|
||||
color: Qt.alpha("#3584e4", 0.1)
|
||||
border.color: Qt.alpha("#3584e4", 0.2)
|
||||
border.width: 1
|
||||
|
||||
Label {
|
||||
id: reqByLabel
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pointSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
updater-ui/qml/PackageList.qml
Normal file
198
updater-ui/qml/PackageList.qml
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pm
|
||||
property bool narrow: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Search bar
|
||||
ToolBar {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Search packages..."
|
||||
onTextChanged: pm.search(text)
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: palette.base
|
||||
border.color: searchField.activeFocus ? palette.highlight : palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package list
|
||||
ListView {
|
||||
id: packageList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
model: {
|
||||
try {
|
||||
return JSON.parse(pm.packagesJson)
|
||||
} catch(e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: delegate
|
||||
width: packageList.width
|
||||
height: narrow ? 64 : 52
|
||||
|
||||
property var pkg: modelData
|
||||
|
||||
background: Rectangle {
|
||||
color: delegate.highlighted ? Qt.alpha(palette.highlight, 0.18) :
|
||||
delegate.hovered ? Qt.alpha(palette.text, 0.05) : "transparent"
|
||||
radius: 6
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
anchors.margins: 8
|
||||
|
||||
// Package icon placeholder
|
||||
Label {
|
||||
text: "📦"
|
||||
font.pointSize: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Name + description
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 2
|
||||
|
||||
Label {
|
||||
text: delegate.pkg ? delegate.pkg.name : ""
|
||||
font.weight: Font.DemiBold
|
||||
elide: Label.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
text: delegate.pkg ? delegate.pkg.description : ""
|
||||
font.pointSize: 9
|
||||
opacity: 0.6
|
||||
elide: Label.ElideRight
|
||||
Layout.fillWidth: true
|
||||
visible: !narrow || delegate.pkg.description.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Right side: badges + version
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
spacing: 3
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 4
|
||||
|
||||
// Status pill
|
||||
Rectangle {
|
||||
visible: delegate.pkg && (delegate.pkg.status === "Installed" || delegate.pkg.status === "Update")
|
||||
radius: 999
|
||||
implicitWidth: statusLabel.implicitWidth + 12
|
||||
implicitHeight: statusLabel.implicitHeight + 4
|
||||
color: delegate.pkg && delegate.pkg.status === "Update"
|
||||
? Qt.alpha("#e5a50a", 0.15)
|
||||
: Qt.alpha("#2ec27e", 0.15)
|
||||
|
||||
Label {
|
||||
id: statusLabel
|
||||
anchors.centerIn: parent
|
||||
text: delegate.pkg ? delegate.pkg.status : ""
|
||||
font.pointSize: narrow ? 8 : 7
|
||||
font.weight: Font.ExtraBold
|
||||
color: delegate.pkg && delegate.pkg.status === "Update" ? "#c38600" : "#26a269"
|
||||
}
|
||||
}
|
||||
|
||||
// Repo badge
|
||||
Rectangle {
|
||||
radius: 999
|
||||
implicitWidth: repoLabel.implicitWidth + 12
|
||||
implicitHeight: repoLabel.implicitHeight + 4
|
||||
color: {
|
||||
if (!delegate.pkg) return Qt.alpha(palette.text, 0.1)
|
||||
var repo = delegate.pkg.repo || "LOCAL"
|
||||
switch(repo) {
|
||||
case "CORE": return Qt.alpha("#3584e4", 0.18)
|
||||
case "EXTRA": return Qt.alpha("#2ec27e", 0.18)
|
||||
case "AUR": return Qt.alpha("#9141ac", 0.18)
|
||||
case "SOVERAINE": return Qt.alpha("#e5a50a", 0.18)
|
||||
default: return Qt.alpha(palette.text, 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: repoLabel
|
||||
anchors.centerIn: parent
|
||||
text: delegate.pkg ? (delegate.pkg.repo || "LOCAL") : ""
|
||||
font.pointSize: narrow ? 8 : 7
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (!delegate.pkg) return palette.text
|
||||
var repo = delegate.pkg.repo || "LOCAL"
|
||||
switch(repo) {
|
||||
case "CORE": return "#3584e4"
|
||||
case "EXTRA": return "#26a269"
|
||||
case "AUR": return "#9141ac"
|
||||
case "SOVERAINE": return "#c38600"
|
||||
default: return palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: delegate.pkg ? delegate.pkg.version : ""
|
||||
font.pointSize: 9
|
||||
opacity: 0.5
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
packageList.currentIndex = index
|
||||
pm.showDetail(delegate.pkg.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: pm.loading
|
||||
visible: pm.loading
|
||||
}
|
||||
|
||||
// Empty state
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "No packages found"
|
||||
opacity: 0.4
|
||||
visible: packageList.count === 0 && !pm.loading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
updater-ui/qml/Sidebar.qml
Normal file
191
updater-ui/qml/Sidebar.qml
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pm
|
||||
property bool narrow: true
|
||||
signal packageListRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 4
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 8
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
source: "qrc:/qt/qml/com/souveraine/updater/net.souveraine.Updater.svg"
|
||||
sourceSize: Qt.size(24, 24)
|
||||
}
|
||||
Label {
|
||||
text: "SouveraineOS Updater"
|
||||
font.bold: true
|
||||
font.pointSize: 14
|
||||
}
|
||||
}
|
||||
|
||||
// Stat cards
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Layout.margins: 4
|
||||
|
||||
StatCard {
|
||||
Layout.fillWidth: true
|
||||
label: "PACKAGES"
|
||||
value: pm.totalPackages
|
||||
colorAccent: palette.highlight
|
||||
}
|
||||
StatCard {
|
||||
Layout.fillWidth: true
|
||||
label: "AUR"
|
||||
value: pm.foreignPackages
|
||||
colorAccent: "#9141ac"
|
||||
}
|
||||
StatCard {
|
||||
Layout.fillWidth: true
|
||||
label: "UPDATES"
|
||||
value: pm.updateCount
|
||||
colorAccent: "#e5a50a"
|
||||
highlighted: pm.hasUpdates
|
||||
}
|
||||
}
|
||||
|
||||
// Section: Filters
|
||||
Label {
|
||||
text: "FILTERS"
|
||||
font.pointSize: 9
|
||||
font.bold: true
|
||||
opacity: 0.5
|
||||
Layout.topMargin: 12
|
||||
Layout.leftMargin: 8
|
||||
}
|
||||
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "All Packages"
|
||||
iconText: "📦"
|
||||
onClicked: {
|
||||
pm.filterByStatus("all")
|
||||
if (narrow) root.packageListRequested()
|
||||
}
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Installed"
|
||||
iconText: "✓"
|
||||
onClicked: {
|
||||
pm.filterByStatus("installed")
|
||||
if (narrow) root.packageListRequested()
|
||||
}
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Updates"
|
||||
iconText: "↑"
|
||||
highlight: pm.hasUpdates
|
||||
onClicked: {
|
||||
pm.filterByStatus("updates")
|
||||
if (narrow) root.packageListRequested()
|
||||
}
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "AUR / Foreign"
|
||||
iconText: "★"
|
||||
onClicked: {
|
||||
pm.filterByStatus("foreign")
|
||||
if (narrow) root.packageListRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Section: Repos
|
||||
Label {
|
||||
text: "REPOSITORIES"
|
||||
font.pointSize: 9
|
||||
font.bold: true
|
||||
opacity: 0.5
|
||||
Layout.topMargin: 12
|
||||
Layout.leftMargin: 8
|
||||
}
|
||||
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Core"
|
||||
iconText: "◆"
|
||||
onClicked: { pm.filterByRepo("core"); if (narrow) root.packageListRequested() }
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Extra"
|
||||
iconText: "◆"
|
||||
onClicked: { pm.filterByRepo("extra"); if (narrow) root.packageListRequested() }
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Multilib"
|
||||
iconText: "◆"
|
||||
onClicked: { pm.filterByRepo("multilib"); if (narrow) root.packageListRequested() }
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Souveraine"
|
||||
iconText: "◆"
|
||||
onClicked: { pm.filterByRepo("souveraine"); if (narrow) root.packageListRequested() }
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Local"
|
||||
iconText: "◆"
|
||||
onClicked: { pm.filterByRepo("local"); if (narrow) root.packageListRequested() }
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// Tools section (wide layout only)
|
||||
ColumnLayout {
|
||||
visible: !narrow
|
||||
spacing: 2
|
||||
|
||||
Label {
|
||||
text: "TOOLS"
|
||||
font.pointSize: 9
|
||||
font.bold: true
|
||||
opacity: 0.5
|
||||
Layout.leftMargin: 8
|
||||
}
|
||||
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Sync Databases"
|
||||
iconText: "↻"
|
||||
onClicked: pm.syncDatabases()
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Rate Mirrors"
|
||||
iconText: "⚡"
|
||||
onClicked: pm.rateMirrors()
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Clean Cache"
|
||||
iconText: "🗑"
|
||||
onClicked: pm.cleanCache()
|
||||
}
|
||||
NavButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Remove Orphans"
|
||||
iconText: "🔍"
|
||||
onClicked: pm.removeOrphans()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
updater-ui/qml/StatCard.qml
Normal file
40
updater-ui/qml/StatCard.qml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property int value: 0
|
||||
property color colorAccent: palette.highlight
|
||||
property bool highlighted: false
|
||||
|
||||
radius: 12
|
||||
color: highlighted ? Qt.alpha(colorAccent, 0.12) : Qt.alpha(colorAccent, 0.06)
|
||||
border.color: Qt.alpha(colorAccent, highlighted ? 0.3 : 0.15)
|
||||
border.width: 1
|
||||
implicitHeight: col.implicitHeight + 16
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
|
||||
Label {
|
||||
text: value
|
||||
font.pointSize: 20
|
||||
font.weight: Font.Black
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: highlighted ? colorAccent : palette.text
|
||||
}
|
||||
Label {
|
||||
text: label
|
||||
font.pointSize: 8
|
||||
font.weight: Font.Bold
|
||||
opacity: 0.5
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
504
updater-ui/src/bridge.rs
Normal file
504
updater-ui/src/bridge.rs
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
#[cxx_qt::bridge]
|
||||
pub mod ffi {
|
||||
// `auto_cxx_name` maps snake_case Rust names to camelCase on the Qt side,
|
||||
// which is what the QML expects (`onPackagesLoaded`, not
|
||||
// `onPackages_loaded`). Without it the invokables still resolve but every
|
||||
// signal handler silently never fires — the exact defect that cost culver
|
||||
// its Matrix login on 2026-07-20.
|
||||
#[auto_cxx_name]
|
||||
unsafe extern "RustQt" {
|
||||
/// The package manager QObject, exposed to QML as `PackageManager`.
|
||||
/// `qml_element` is what makes `PackageManager {}` resolve in Main.qml.
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[qproperty(String, status_text)]
|
||||
#[qproperty(bool, loading)]
|
||||
#[qproperty(bool, has_updates)]
|
||||
#[qproperty(i32, total_packages)]
|
||||
#[qproperty(i32, foreign_packages)]
|
||||
#[qproperty(i32, update_count)]
|
||||
#[qproperty(String, packages_json)]
|
||||
#[qproperty(String, updates_json)]
|
||||
#[qproperty(String, system_info_json)]
|
||||
#[qproperty(String, detail_json)]
|
||||
#[qproperty(String, files_json)]
|
||||
#[qproperty(String, operation_output)]
|
||||
#[qproperty(bool, operation_running)]
|
||||
type PackageManager = super::PackageManagerRust;
|
||||
|
||||
#[qinvokable]
|
||||
fn load_packages(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn check_updates(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn search(self: Pin<&mut PackageManager>, query: String);
|
||||
|
||||
#[qinvokable]
|
||||
fn show_detail(self: Pin<&mut PackageManager>, name: String);
|
||||
|
||||
#[qinvokable]
|
||||
fn install_package(self: Pin<&mut PackageManager>, name: String);
|
||||
|
||||
#[qinvokable]
|
||||
fn remove_package(self: Pin<&mut PackageManager>, name: String);
|
||||
|
||||
#[qinvokable]
|
||||
fn full_upgrade(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn sync_databases(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn clean_cache(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn remove_orphans(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn rate_mirrors(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn load_system_info(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qinvokable]
|
||||
fn filter_by_repo(self: Pin<&mut PackageManager>, repo: String);
|
||||
|
||||
#[qinvokable]
|
||||
fn filter_by_status(self: Pin<&mut PackageManager>, status: String);
|
||||
|
||||
#[qsignal]
|
||||
fn packages_loaded(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qsignal]
|
||||
fn updates_loaded(self: Pin<&mut PackageManager>);
|
||||
|
||||
#[qsignal]
|
||||
fn operation_completed(self: Pin<&mut PackageManager>, success: bool);
|
||||
|
||||
#[qsignal]
|
||||
fn detail_loaded(self: Pin<&mut PackageManager>);
|
||||
}
|
||||
}
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
pub struct PackageManagerRust {
|
||||
status_text: String,
|
||||
loading: bool,
|
||||
has_updates: bool,
|
||||
total_packages: i32,
|
||||
foreign_packages: i32,
|
||||
update_count: i32,
|
||||
packages_json: String,
|
||||
updates_json: String,
|
||||
system_info_json: String,
|
||||
detail_json: String,
|
||||
files_json: String,
|
||||
operation_output: String,
|
||||
operation_running: bool,
|
||||
|
||||
// Internal state
|
||||
all_packages: Vec<updater_core::models::Package>,
|
||||
current_filter_repo: Option<String>,
|
||||
current_filter_status: Option<String>,
|
||||
search_query: String,
|
||||
}
|
||||
|
||||
impl Default for PackageManagerRust {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status_text: "Ready".to_string(),
|
||||
loading: false,
|
||||
has_updates: false,
|
||||
total_packages: 0,
|
||||
foreign_packages: 0,
|
||||
update_count: 0,
|
||||
packages_json: "[]".to_string(),
|
||||
updates_json: "[]".to_string(),
|
||||
system_info_json: "{}".to_string(),
|
||||
detail_json: "{}".to_string(),
|
||||
files_json: "[]".to_string(),
|
||||
operation_output: "".to_string(),
|
||||
operation_running: false,
|
||||
all_packages: Vec::new(),
|
||||
current_filter_repo: None,
|
||||
current_filter_status: None,
|
||||
search_query: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ffi::PackageManager {
|
||||
fn load_packages(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_loading(true);
|
||||
self.as_mut()
|
||||
.set_status_text("Loading packages...".to_string());
|
||||
|
||||
let packages = updater_core::cache::PackageCache::load().unwrap_or_else(|| {
|
||||
let mut all = Vec::new();
|
||||
|
||||
if let Ok(synced) = updater_core::pacman::sync_packages() {
|
||||
all.extend(synced);
|
||||
}
|
||||
if let Ok(installed) = updater_core::pacman::installed_packages() {
|
||||
for mut pkg in installed {
|
||||
if let Some(synced_pkg) = all.iter_mut().find(|p| p.name == pkg.name) {
|
||||
synced_pkg.status = updater_core::models::PkgStatus::Installed;
|
||||
} else {
|
||||
pkg.status = updater_core::models::PkgStatus::Installed;
|
||||
all.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(foreign) = updater_core::pacman::foreign_packages() {
|
||||
for mut pkg in foreign {
|
||||
pkg.is_foreign = true;
|
||||
if let Some(existing) = all.iter_mut().find(|p| p.name == pkg.name) {
|
||||
existing.status = updater_core::models::PkgStatus::Foreign;
|
||||
existing.is_foreign = true;
|
||||
} else {
|
||||
pkg.status = updater_core::models::PkgStatus::Foreign;
|
||||
all.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = updater_core::cache::PackageCache::store(&all);
|
||||
all
|
||||
});
|
||||
|
||||
let total = packages.len();
|
||||
let foreign = packages.iter().filter(|p| p.is_foreign).count();
|
||||
|
||||
{
|
||||
let rust = self.as_mut().rust_mut();
|
||||
rust.all_packages = packages;
|
||||
rust.total_packages = total as i32;
|
||||
rust.foreign_packages = foreign as i32;
|
||||
}
|
||||
|
||||
self.as_mut().apply_filter();
|
||||
self.as_mut().set_loading(false);
|
||||
self.as_mut()
|
||||
.set_status_text(format!("{} packages loaded", total));
|
||||
self.as_mut().packages_loaded();
|
||||
}
|
||||
|
||||
fn check_updates(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_loading(true);
|
||||
self.as_mut()
|
||||
.set_status_text("Checking for updates...".to_string());
|
||||
|
||||
let updates = updater_core::pacman::check_updates().unwrap_or_default();
|
||||
let count = updates.len();
|
||||
|
||||
{
|
||||
let rust = self.as_mut().rust_mut();
|
||||
rust.update_count = count as i32;
|
||||
rust.has_updates = count > 0;
|
||||
for update in &updates {
|
||||
if let Some(pkg) = rust.all_packages.iter_mut().find(|p| p.name == update.name) {
|
||||
pkg.status = updater_core::models::PkgStatus::Update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let json = serde_json::to_string(&updates).unwrap_or_else(|_| "[]".to_string());
|
||||
self.as_mut().set_updates_json(json);
|
||||
self.as_mut().apply_filter();
|
||||
self.as_mut().set_loading(false);
|
||||
self.as_mut()
|
||||
.set_status_text(format!("{} updates available", count));
|
||||
self.as_mut().updates_loaded();
|
||||
}
|
||||
|
||||
fn search(mut self: Pin<&mut Self>, query: String) {
|
||||
{
|
||||
let rust = self.as_mut().rust_mut();
|
||||
rust.search_query = query;
|
||||
}
|
||||
self.as_mut().apply_filter();
|
||||
}
|
||||
|
||||
fn show_detail(mut self: Pin<&mut Self>, name: String) {
|
||||
let name_str = name;
|
||||
let info = updater_core::pacman::package_info(&name_str).unwrap_or_default();
|
||||
let json = serde_json::to_string(&info).unwrap_or_else(|_| "{}".to_string());
|
||||
self.as_mut().set_detail_json(json);
|
||||
|
||||
let files = updater_core::pacman::package_files(&name_str).unwrap_or_default();
|
||||
let files_json = serde_json::to_string(&files).unwrap_or_else(|_| "[]".to_string());
|
||||
self.as_mut().set_files_json(files_json);
|
||||
self.as_mut().detail_loaded();
|
||||
}
|
||||
|
||||
fn install_package(mut self: Pin<&mut Self>, name: String) {
|
||||
let name_str = name;
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Installing {}...", name_str));
|
||||
|
||||
match updater_core::auth::install_packages(&[&name_str]) {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
if success {
|
||||
updater_core::cache::invalidate_all();
|
||||
self.as_mut()
|
||||
.set_status_text(format!("{} installed", name_str));
|
||||
}
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn remove_package(mut self: Pin<&mut Self>, name: String) {
|
||||
let name_str = name;
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Removing {}...", name_str));
|
||||
|
||||
match updater_core::auth::remove_packages(&[&name_str]) {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
if success {
|
||||
updater_core::cache::invalidate_all();
|
||||
self.as_mut()
|
||||
.set_status_text(format!("{} removed", name_str));
|
||||
}
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn full_upgrade(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output("Running full system upgrade...".to_string());
|
||||
|
||||
match updater_core::auth::full_upgrade() {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
if success {
|
||||
updater_core::cache::invalidate_all();
|
||||
self.as_mut()
|
||||
.set_status_text("System upgraded".to_string());
|
||||
}
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn sync_databases(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output("Syncing databases...".to_string());
|
||||
|
||||
match updater_core::auth::sync_databases() {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
if success {
|
||||
updater_core::cache::invalidate_all();
|
||||
self.as_mut()
|
||||
.set_status_text("Databases synced".to_string());
|
||||
}
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn clean_cache(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output("Cleaning cache...".to_string());
|
||||
|
||||
match updater_core::auth::clean_cache() {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
self.as_mut()
|
||||
.set_status_text("Cache cleaned".to_string());
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn remove_orphans(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output("Finding orphans...".to_string());
|
||||
|
||||
match updater_core::pacman::orphans() {
|
||||
Ok(orphans) => {
|
||||
if orphans.is_empty() {
|
||||
self.as_mut()
|
||||
.set_operation_output("No orphaned packages found.".to_string());
|
||||
self.as_mut().operation_completed(true);
|
||||
} else {
|
||||
let names: Vec<&str> = orphans.iter().map(|p| p.name.as_str()).collect();
|
||||
self.as_mut().set_operation_output(format!(
|
||||
"Removing {} orphaned packages...",
|
||||
names.len()
|
||||
));
|
||||
match updater_core::auth::remove_orphans(&names) {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
updater_core::cache::invalidate_all();
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn rate_mirrors(mut self: Pin<&mut Self>) {
|
||||
self.as_mut().set_operation_running(true);
|
||||
self.as_mut()
|
||||
.set_operation_output("Rating mirrors...".to_string());
|
||||
|
||||
match updater_core::auth::rate_mirrors() {
|
||||
Ok((output, success)) => {
|
||||
self.as_mut().set_operation_output(output);
|
||||
self.as_mut()
|
||||
.set_status_text("Mirrors rated".to_string());
|
||||
self.as_mut().operation_completed(success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.as_mut()
|
||||
.set_operation_output(format!("Error: {}", e));
|
||||
self.as_mut().operation_completed(false);
|
||||
}
|
||||
}
|
||||
self.as_mut().set_operation_running(false);
|
||||
}
|
||||
|
||||
fn load_system_info(mut self: Pin<&mut Self>) {
|
||||
let info = updater_core::pacman::system_info().unwrap_or_default();
|
||||
let json = serde_json::to_string(&info).unwrap_or_else(|_| "{}".to_string());
|
||||
self.as_mut().set_system_info_json(json);
|
||||
}
|
||||
|
||||
fn filter_by_repo(mut self: Pin<&mut Self>, repo: String) {
|
||||
{
|
||||
let rust = self.as_mut().rust_mut();
|
||||
if repo == "all" {
|
||||
rust.current_filter_repo = None;
|
||||
} else {
|
||||
rust.current_filter_repo = Some(repo);
|
||||
}
|
||||
rust.current_filter_status = None;
|
||||
}
|
||||
self.as_mut().apply_filter();
|
||||
}
|
||||
|
||||
fn filter_by_status(mut self: Pin<&mut Self>, status: String) {
|
||||
{
|
||||
let rust = self.as_mut().rust_mut();
|
||||
if status == "all" {
|
||||
rust.current_filter_status = None;
|
||||
} else {
|
||||
rust.current_filter_status = Some(status);
|
||||
}
|
||||
rust.current_filter_repo = None;
|
||||
}
|
||||
self.as_mut().apply_filter();
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManagerRust {
|
||||
fn apply_filter(mut self: Pin<&mut ffi::PackageManager>) {
|
||||
let json = {
|
||||
let rust = self.as_ref().rust();
|
||||
let filtered: Vec<&updater_core::models::Package> = rust
|
||||
.all_packages
|
||||
.iter()
|
||||
.filter(|pkg| {
|
||||
if let Some(ref repo) = rust.current_filter_repo {
|
||||
let repo_match = match repo.as_str() {
|
||||
"core" => pkg.repo == updater_core::models::Repo::Core,
|
||||
"extra" => pkg.repo == updater_core::models::Repo::Extra,
|
||||
"multilib" => pkg.repo == updater_core::models::Repo::Multilib,
|
||||
"souveraine" => pkg.repo == updater_core::models::Repo::Souveraine,
|
||||
"local" => pkg.repo == updater_core::models::Repo::Local,
|
||||
"foreign" => pkg.is_foreign,
|
||||
_ => true,
|
||||
};
|
||||
if !repo_match {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref status) = rust.current_filter_status {
|
||||
let status_match = match status.as_str() {
|
||||
"installed" => pkg.status == updater_core::models::PkgStatus::Installed,
|
||||
"updates" => pkg.status == updater_core::models::PkgStatus::Update,
|
||||
"foreign" => pkg.is_foreign,
|
||||
"available" => pkg.status == updater_core::models::PkgStatus::Available,
|
||||
_ => true,
|
||||
};
|
||||
if !status_match {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !rust.search_query.is_empty() {
|
||||
let q = rust.search_query.to_lowercase();
|
||||
if !pkg.name.to_lowercase().contains(&q)
|
||||
&& !pkg.description.to_lowercase().contains(&q)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string(&filtered).unwrap_or_else(|_| "[]".to_string())
|
||||
};
|
||||
|
||||
self.as_mut().set_packages_json(json);
|
||||
}
|
||||
}
|
||||
41
updater-ui/src/main.rs
Normal file
41
updater-ui/src/main.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! SouveraineOS Updater — Qt/QML front end over `updater-core`'s pacman calls.
|
||||
//!
|
||||
//! The UI ships inside the binary as a qrc module (`com.souveraine.updater`,
|
||||
//! wired in build.rs), so there is no runtime search for loose .qml files.
|
||||
|
||||
use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl};
|
||||
|
||||
mod bridge;
|
||||
|
||||
fn main() {
|
||||
// BOTH macros are required in a binary crate: init_crate! registers this
|
||||
// crate's generated QObject types (PackageManager), init_qml_module!
|
||||
// registers the module so Main.qml and its elements exist. Without
|
||||
// init_crate! the module loads but constructs nothing; without
|
||||
// init_qml_module! the import fails outright. The aarch64 lld/LTO link
|
||||
// drops these initializers as unreferenced otherwise — the data stays
|
||||
// embedded but never registers. (Same failure fixed in culver and
|
||||
// souveraine-player.)
|
||||
cxx_qt::init_crate!(updater_ui);
|
||||
cxx_qt::init_qml_module!("com.souveraine.updater");
|
||||
|
||||
let mut app = QGuiApplication::new();
|
||||
let mut engine = QQmlApplicationEngine::new();
|
||||
|
||||
if let Some(mut engine) = engine.as_mut() {
|
||||
// Qt is otherwise SILENT when the root component fails to construct
|
||||
// (e.g. a view type missing from the module) — it just yields a null
|
||||
// root. Surface the real failure instead of exiting with a guess.
|
||||
engine
|
||||
.as_mut()
|
||||
.on_object_creation_failed(|_, url| {
|
||||
eprintln!("souveraine-updater: QML root failed to construct: {url}");
|
||||
})
|
||||
.release();
|
||||
engine.load(&QUrl::from(
|
||||
"qrc:/qt/qml/com/souveraine/updater/qml/Main.qml",
|
||||
));
|
||||
}
|
||||
|
||||
QGuiApplication::exec(&mut app);
|
||||
}
|
||||
Loading…
Reference in a new issue