mirror of
https://github.com/outbackdingo/hyprcosmic.git
synced 2026-08-25 14:53:21 +00:00
Install HyDE themes end to end
Three things stood between `assets.rs` and a themed desktop. `import-theme` never called it. The module was written, tested and unreachable; `--assets` now wires it up, with `--source`, `--overwrite` and `--dry-run`, and finds the theme repo's Source/ directory by searching upward rather than assuming HyDE's exact nesting depth. The archive guard rejected every real icon theme. Refusing any `..` in a link target is right for an entry path but wrong for a symlink: icon themes are built out of relative links into sibling directories, and Tela ships thousands of `../devices/network-wireless.svg`. What matters is whether the target resolves inside the destination, which `stays_within_root` now decides lexically -- no canonicalize, since the tree does not exist at plan time and following real links during validation would be a TOCTOU window. Absolute targets and links that climb past the root are still refused; the existing escape tests still pass. `apply` silently ignored `source`. It parsed and resolved inline while `watch` went through `compile`, and `flatten` drops `Item::Source` -- so an include that worked under `watch` vanished under `apply`. `apply` now uses `compile` too. This matters immediately: the generated theme lives in its own theme.conf, sourced from cosmic.conf, so re-importing a theme cannot clobber the keybindings. The waybar stylesheet claimed a theme could be dropped in ahead of it to recolour the bar. It could not -- HyDE names its colours main-bg/wb-act-bg and the rules referenced bar-bg/accent. Split into palette + theme + bridge + rules, imported in that order, so the claim is now true. Verified by loading the result through GTK's own CSS parser: with Tokyo Night installed main-bg resolves to #24283b and wb-act-bg to #bb9af7; with an empty theme.css the defaults stand. Both parse without error. Two deliberate departures, both commented where they are made: the theme's near-transparent bar-bg is composited at 0.85 because cosmic-comp has no blur to put behind it, and theme.css is copied next to style.css rather than imported from HyDE's own path, because a missing @import is fatal in GTK and would break the bar on any machine without a theme.
This commit is contained in:
+8
-4
@@ -7,10 +7,14 @@
|
||||
# `#` starts a comment where a word would start, so `--color=#1a1b26` is fine
|
||||
# but `--color #1a1b26` is not; quote it as '#1a1b26' if you need the latter.
|
||||
|
||||
# The bar. Configs live under /usr/share/hyprcosmic so this file needs no
|
||||
# per-user paths; copy them into ~/.config/hyprcosmic/waybar and point here to
|
||||
# customise.
|
||||
waybar -c /usr/share/hyprcosmic/waybar/config.jsonc -s /usr/share/hyprcosmic/waybar/style.css
|
||||
# The bar. The layout is shared and lives under /usr/share, but the stylesheet
|
||||
# has to be per-user: it imports a sibling theme.css holding the installed HyDE
|
||||
# theme's palette, and a relative @import resolves against the importing file.
|
||||
#
|
||||
# The style path below is therefore absolute and contains a home directory.
|
||||
# This file is not a shell, so `~` and `$HOME` are literal text here -- replace
|
||||
# the path with your own if you are copying this template.
|
||||
waybar -c /usr/share/hyprcosmic/waybar/config.jsonc -s /home/dingo/.config/hyprcosmic/waybar/style.css
|
||||
|
||||
# rofi is not a daemon. It is launched on demand by a keybinding, which COSMIC
|
||||
# stores in com.system76.CosmicSettings.Shortcuts rather than here. Set those
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Map HyDE's waybar colour names onto the ones rules.css uses.
|
||||
*
|
||||
* Imported after both palette.css and the theme's own waybar.theme, so it sees
|
||||
* whichever definition of each HyDE name is in force and does not care which
|
||||
* file supplied it. This indirection is the whole reason a HyDE theme can
|
||||
* recolour this bar without any of the rules changing.
|
||||
*/
|
||||
|
||||
@define-color bar-fg @main-fg;
|
||||
@define-color accent @wb-act-bg;
|
||||
@define-color muted @wb-hvr-fg;
|
||||
|
||||
/* `bar-bg` IS remapped, and this is a deliberate departure from the theme.
|
||||
*
|
||||
* HyDE themes set bar-bg to something like rgba(0, 0, 0, 0.1) and rely on the
|
||||
* compositor blurring whatever is behind the bar. cosmic-comp has no
|
||||
* rule-driven blur -- `import-theme --report` lists decoration.blur.* as
|
||||
* needing a compositor patch -- so honouring that value literally gives a
|
||||
* ~90% transparent bar with unblurred desktop showing through and text that
|
||||
* cannot be read.
|
||||
*
|
||||
* So the theme's own background colour is used at an opacity that works
|
||||
* without blur. Delete these two lines to get the theme's literal value back,
|
||||
* or once blur lands.
|
||||
*/
|
||||
@define-color bar-bg alpha(@main-bg, 0.85);
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Default colours for the HyprCosmic bar.
|
||||
*
|
||||
* Two name sets are defined here, and both matter:
|
||||
*
|
||||
* - `main-bg`, `wb-act-bg` and friends are HyDE's names. A HyDE theme's
|
||||
* waybar.theme is nothing but a list of these, so defining them here means
|
||||
* a theme file can override them by being imported after this one.
|
||||
* - `bar-bg`, `accent` and friends are the names rules.css actually uses.
|
||||
*
|
||||
* bridge-hyde.css maps the first set onto the second. Defining HyDE's names
|
||||
* here as well is what makes a missing theme file harmless: the bridge always
|
||||
* has something to resolve against, so an unthemed bar renders with these
|
||||
* values instead of failing to parse.
|
||||
*
|
||||
* Values are Tokyo Night, matching cosmic-conf's own defaults.
|
||||
*/
|
||||
|
||||
/* HyDE's names. Overridden by ~/.config/waybar/theme.css when a theme is in. */
|
||||
@define-color main-bg #1a1b26;
|
||||
@define-color main-fg #c0caf5;
|
||||
@define-color wb-act-bg #7aa2f7;
|
||||
@define-color wb-act-fg #1a1b26;
|
||||
@define-color wb-hvr-bg #7aa2f7;
|
||||
@define-color wb-hvr-fg #565f89;
|
||||
|
||||
/* Names with no HyDE equivalent, so never themed and always these values. */
|
||||
@define-color warning #e0af68;
|
||||
@define-color critical #f7768e;
|
||||
|
||||
/* The bar's own background. HyDE themes define this one under the same name
|
||||
* we use, and typically as a low-alpha rgba() so the compositor's blur shows
|
||||
* through. */
|
||||
@define-color bar-bg rgba(26, 27, 38, 0.85);
|
||||
@@ -0,0 +1,63 @@
|
||||
/* Geometry and layout for the HyprCosmic bar.
|
||||
*
|
||||
* No colour literals: every colour here is a name defined by palette.css and
|
||||
* possibly re-pointed by bridge-hyde.css. That separation is what lets a theme
|
||||
* change the palette without touching a single rule.
|
||||
*/
|
||||
|
||||
* {
|
||||
font-family: "FontAwesome 6 Free", "Noto Sans", sans-serif;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
window#waybar {
|
||||
background: @bar-bg;
|
||||
color: @bar-fg;
|
||||
}
|
||||
|
||||
#workspaces button {
|
||||
padding: 0 8px;
|
||||
color: @muted;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#workspaces button.active {
|
||||
color: @bar-fg;
|
||||
box-shadow: inset 0 -2px @accent;
|
||||
}
|
||||
|
||||
#workspaces button:hover {
|
||||
color: @wb-hvr-fg;
|
||||
background: @wb-hvr-bg;
|
||||
}
|
||||
|
||||
#taskbar button {
|
||||
padding: 0 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#taskbar button.active {
|
||||
box-shadow: inset 0 -2px @accent;
|
||||
}
|
||||
|
||||
#clock,
|
||||
#pulseaudio,
|
||||
#network,
|
||||
#cpu,
|
||||
#memory,
|
||||
#battery,
|
||||
#tray {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#battery.warning { color: @warning; }
|
||||
#battery.critical { color: @critical; }
|
||||
#network.disconnected { color: @critical; }
|
||||
#pulseaudio.muted { color: @muted; }
|
||||
+27
-69
@@ -1,74 +1,32 @@
|
||||
/* Waybar styling for a HyprCosmic session.
|
||||
*
|
||||
* Deliberately plain. HyDE's own bar styling is bespoke CSS belonging to HyDE
|
||||
* rather than to any theme: a HyDE theme's waybar.theme file is only a short
|
||||
* list of @define-color declarations. So the palette below is what a theme
|
||||
* import can eventually overwrite, and the geometry is what stays put.
|
||||
* This file is only an import list, because the order is the whole design:
|
||||
*
|
||||
* Keep every colour reference pointing at one of these names, so that dropping
|
||||
* a theme's waybar.theme in ahead of this file is enough to re-colour the bar.
|
||||
* 1. palette.css default colours, under both our names and HyDE's
|
||||
* 2. theme.css the installed HyDE theme's waybar.theme, if any
|
||||
* 3. bridge-hyde.css maps HyDE's colour names onto the ones the rules use
|
||||
* 4. rules.css geometry and layout; no colour literals at all
|
||||
*
|
||||
* Every entry is an @import, so they are read strictly in sequence and a later
|
||||
* definition of a colour wins over an earlier one. That is what lets step 2
|
||||
* recolour the bar without step 4 knowing a theme exists.
|
||||
*
|
||||
* Step 2 is a copy of the installed theme's waybar.theme, kept as a sibling of
|
||||
* this file rather than read from HyDE's own ~/.config/waybar/theme.css.
|
||||
*
|
||||
* That copy exists because a missing @import is fatal in GTK, not a warning:
|
||||
* pointing at HyDE's path directly means the whole stylesheet fails to load on
|
||||
* any machine where no theme has been imported. A sibling file we create at
|
||||
* install time is always present -- empty when there is no theme, in which
|
||||
* case the defaults from step 1 stand. palette.css defines HyDE's names too,
|
||||
* so the bridge in step 3 resolves either way.
|
||||
*
|
||||
* Consequently this file belongs at ~/.config/hyprcosmic/waybar/style.css, not
|
||||
* under /usr/share: a relative @import resolves against the importing file,
|
||||
* and theme.css is per-user.
|
||||
*/
|
||||
|
||||
@define-color bar-bg rgba(26, 27, 38, 0.85);
|
||||
@define-color bar-fg #c0caf5;
|
||||
@define-color accent #7aa2f7;
|
||||
@define-color warning #e0af68;
|
||||
@define-color critical #f7768e;
|
||||
@define-color muted #565f89;
|
||||
|
||||
* {
|
||||
font-family: "FontAwesome 6 Free", "Noto Sans", sans-serif;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
window#waybar {
|
||||
background: @bar-bg;
|
||||
color: @bar-fg;
|
||||
}
|
||||
|
||||
#workspaces button {
|
||||
padding: 0 8px;
|
||||
color: @muted;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#workspaces button.active {
|
||||
color: @bar-fg;
|
||||
box-shadow: inset 0 -2px @accent;
|
||||
}
|
||||
|
||||
#workspaces button:hover {
|
||||
color: @bar-fg;
|
||||
background: rgba(122, 162, 247, 0.15);
|
||||
}
|
||||
|
||||
#taskbar button {
|
||||
padding: 0 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#taskbar button.active {
|
||||
box-shadow: inset 0 -2px @accent;
|
||||
}
|
||||
|
||||
#clock,
|
||||
#pulseaudio,
|
||||
#network,
|
||||
#cpu,
|
||||
#memory,
|
||||
#battery,
|
||||
#tray {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#battery.warning { color: @warning; }
|
||||
#battery.critical { color: @critical; }
|
||||
#network.disconnected { color: @critical; }
|
||||
#pulseaudio.muted { color: @muted; }
|
||||
@import url("file:///usr/share/hyprcosmic/waybar/palette.css");
|
||||
@import url("theme.css");
|
||||
@import url("file:///usr/share/hyprcosmic/waybar/bridge-hyde.css");
|
||||
@import url("file:///usr/share/hyprcosmic/waybar/rules.css");
|
||||
|
||||
+148
-4
@@ -158,6 +158,56 @@ pub struct Report {
|
||||
pub installed: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn kind(&self) -> AssetKind {
|
||||
match self {
|
||||
Action::ExtractArchive { kind, .. } | Action::CopyVerbatim { kind, .. } => *kind,
|
||||
Action::CopyWallpaper { .. } => AssetKind::Wallpaper,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dest(&self) -> &Path {
|
||||
match self {
|
||||
Action::ExtractArchive { dest, .. }
|
||||
| Action::CopyWallpaper { dest, .. }
|
||||
| Action::CopyVerbatim { dest, .. } => dest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `apply` would do, for `--dry-run`.
|
||||
///
|
||||
/// Deliberately not `render_report` with a synthesised `Report`: saying
|
||||
/// "Installed:" about files that were never written is the kind of small lie
|
||||
/// that makes a tool untrustworthy.
|
||||
pub fn render_plan(plan: &Plan) -> String {
|
||||
let mut out = String::new();
|
||||
if plan.actions.is_empty() {
|
||||
out.push_str("Nothing to install.\n");
|
||||
} else {
|
||||
out.push_str("Would install:\n");
|
||||
for a in &plan.actions {
|
||||
out.push_str(&format!(
|
||||
" {} ({})\n",
|
||||
a.dest().display(),
|
||||
a.kind().label()
|
||||
));
|
||||
}
|
||||
}
|
||||
if !plan.skipped.is_empty() {
|
||||
out.push_str("\nWould skip:\n");
|
||||
for n in &plan.skipped {
|
||||
out.push_str(&format!(
|
||||
" {} ({}): {}\n",
|
||||
n.path.display(),
|
||||
n.kind.label(),
|
||||
n.reason.describe()
|
||||
));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Human-readable summary, in the same spirit as `import::render_report`:
|
||||
/// nothing that was skipped is left unmentioned.
|
||||
pub fn render_report(plan: &Plan, report: &Report) -> String {
|
||||
@@ -482,11 +532,30 @@ fn walk_archive(archive_path: &Path, dest_root: &Path, write: bool) -> Result<Ve
|
||||
|
||||
// A symlink/hardlink's own entry path can be safe while its target
|
||||
// still points outside `dest_root`; a later entry written "through"
|
||||
// that link would then land wherever the link points. Same rule,
|
||||
// applied to the target.
|
||||
if matches!(entry.header().entry_type(), tar::EntryType::Symlink | tar::EntryType::Link) {
|
||||
// that link would then land wherever the link points.
|
||||
//
|
||||
// The target cannot use the same rule as the entry path, though. Icon
|
||||
// themes are built almost entirely out of relative symlinks pointing
|
||||
// at sibling directories -- Tela ships thousands of
|
||||
// `../devices/network-wireless.svg` -- so rejecting every `..` would
|
||||
// reject every real icon theme. What matters is not whether the target
|
||||
// contains `..` but whether it still lands inside `dest_root` once
|
||||
// resolved, which is what `stays_within_root` decides.
|
||||
let entry_type = entry.header().entry_type();
|
||||
if matches!(entry_type, tar::EntryType::Symlink | tar::EntryType::Link) {
|
||||
if let Some(target) = entry.link_name()? {
|
||||
reject_unsafe_path(archive_path, &target)?;
|
||||
// tar resolves a symlink target against the link's own
|
||||
// directory, but a hardlink target against the archive root.
|
||||
let base = match entry_type {
|
||||
tar::EntryType::Link => Path::new(""),
|
||||
_ => rel.parent().unwrap_or(Path::new("")),
|
||||
};
|
||||
if !stays_within_root(base, &target) {
|
||||
return Err(AssetError::UnsafeArchiveEntry {
|
||||
archive: archive_path.to_path_buf(),
|
||||
entry: target.into_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +588,35 @@ fn reject_unsafe_path(archive: &Path, entry: &Path) -> Result<(), AssetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Does `base/target` still land inside the root it started from?
|
||||
///
|
||||
/// Resolution is lexical on purpose. At plan time the destination tree does
|
||||
/// not exist yet, so `canonicalize` has nothing to work with; and following
|
||||
/// real symlinks during validation would open a TOCTOU window between the
|
||||
/// check and the extraction. Counting depth over the joined components
|
||||
/// answers the only question that matters without touching the filesystem.
|
||||
fn stays_within_root(base: &Path, target: &Path) -> bool {
|
||||
if target.is_absolute() {
|
||||
return false;
|
||||
}
|
||||
let mut depth: isize = 0;
|
||||
for c in base.components().chain(target.components()) {
|
||||
match c {
|
||||
Component::Normal(_) => depth += 1,
|
||||
Component::ParentDir => {
|
||||
depth -= 1;
|
||||
if depth < 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Component::CurDir => {}
|
||||
// An absolute component anywhere replaces everything before it.
|
||||
Component::RootDir | Component::Prefix(_) => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The first path component of every entry, skipping a leading `./` — used
|
||||
/// only as a best-effort "is this archive already installed?" heuristic
|
||||
/// (real GTK/icon tarballs unpack into a single named directory), not as a
|
||||
@@ -541,6 +639,52 @@ mod tests {
|
||||
use flate2::Compression;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn a_relative_symlink_into_a_sibling_directory_is_allowed() {
|
||||
// Regression: rejecting every `..` in a link target rejected every
|
||||
// real icon theme. Tela ships thousands of exactly this shape, and
|
||||
// Tokyo-Night's Icon_TelaPurple.tar.gz would not extract.
|
||||
assert!(stays_within_root(
|
||||
Path::new("Tela-purple-dark/16/panel"),
|
||||
Path::new("../devices/network-wireless.svg")
|
||||
));
|
||||
assert!(stays_within_root(
|
||||
Path::new("Tela/22/apps"),
|
||||
Path::new("../../16/apps/firefox.svg")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relative_symlink_that_climbs_past_the_root_is_still_refused() {
|
||||
// One `..` too many is the whole attack, so the boundary is exact
|
||||
// rather than approximate.
|
||||
assert!(!stays_within_root(
|
||||
Path::new("Tela/16/panel"),
|
||||
Path::new("../../../../etc/passwd")
|
||||
));
|
||||
assert!(!stays_within_root(Path::new(""), Path::new("../escape")));
|
||||
assert!(!stays_within_root(Path::new("a"), Path::new("../../escape")));
|
||||
// Exactly back to the root is fine; one further is not.
|
||||
assert!(stays_within_root(Path::new("a/b"), Path::new("../../c")));
|
||||
assert!(!stays_within_root(Path::new("a/b"), Path::new("../../../c")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absolute_symlink_target_is_refused_however_it_is_spelled() {
|
||||
assert!(!stays_within_root(Path::new("a/b"), Path::new("/etc/passwd")));
|
||||
assert!(!stays_within_root(Path::new("a/b"), Path::new("/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detours_that_end_up_back_inside_are_allowed() {
|
||||
// `a/b/../c` never leaves, so refusing it would be strictness with no
|
||||
// security value.
|
||||
assert!(stays_within_root(
|
||||
Path::new("theme/scalable"),
|
||||
Path::new("../scalable/./places/../apps/icon.svg")
|
||||
));
|
||||
}
|
||||
|
||||
/// Build a `.tar.gz` fixture programmatically so tests do not depend on
|
||||
/// binary blobs checked into the repo.
|
||||
fn make_tarball(dir: &Path, name: &str, entries: &[(&str, &[u8])]) -> PathBuf {
|
||||
|
||||
+82
-30
@@ -2,10 +2,10 @@
|
||||
//!
|
||||
//! Exit codes: 0 success, 1 config error (nothing written), 2 usage error.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use cosmic_conf::{emit::Emitter, import, parse, render_diagnostic, resolve};
|
||||
use cosmic_conf::{assets, emit::Emitter, import, render_diagnostic, watch};
|
||||
|
||||
const USAGE: &str = "\
|
||||
cosmic-conf — compile cosmic.conf into the cosmic-config tree
|
||||
@@ -13,15 +13,35 @@ cosmic-conf — compile cosmic.conf into the cosmic-config tree
|
||||
USAGE:
|
||||
cosmic-conf apply [--diff] [--config <path>]
|
||||
cosmic-conf import-theme <hypr.theme> [--out <path>] [--report]
|
||||
[--assets [--source <dir>] [--overwrite] [--dry-run]]
|
||||
|
||||
OPTIONS:
|
||||
--diff Show what would change without writing anything
|
||||
--config <path> Config file (default: $XDG_CONFIG_HOME/hyprcosmic/cosmic.conf)
|
||||
--out <path> Write the generated cosmic.conf here (default: stdout)
|
||||
--report Print everything that did not translate cleanly
|
||||
--assets Also install wallpapers, GTK/icon themes and the
|
||||
waybar/rofi/kitty theme files that sit beside hypr.theme
|
||||
--source <dir> The theme repo's Source/ directory holding the GTK and
|
||||
icon tarballs (default: found by searching upward)
|
||||
--overwrite Replace assets that are already installed
|
||||
--dry-run With --assets, list what would be installed and stop
|
||||
-h, --help Show this help
|
||||
";
|
||||
|
||||
/// HyDE keeps GTK and icon tarballs in a `Source/` directory at the root of
|
||||
/// the theme repo, four levels above the theme folder
|
||||
/// (`Configs/.config/hyde/themes/<Name>/`). Searching upward rather than
|
||||
/// hardcoding that depth means a theme unpacked at a different depth, or one
|
||||
/// vendored into another tree, still works.
|
||||
fn find_source_dir(theme_dir: &std::path::Path) -> Option<PathBuf> {
|
||||
theme_dir
|
||||
.ancestors()
|
||||
.take(6)
|
||||
.map(|a| a.join("Source"))
|
||||
.find(|c| c.is_dir())
|
||||
}
|
||||
|
||||
fn default_config_path() -> Option<PathBuf> {
|
||||
let base = match std::env::var_os("XDG_CONFIG_HOME") {
|
||||
Some(x) if !x.is_empty() => PathBuf::from(x),
|
||||
@@ -85,35 +105,16 @@ fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
fn run(config_path: &PathBuf, diff_only: bool) -> Result<String, String> {
|
||||
let source = std::fs::read_to_string(config_path)
|
||||
.map_err(|e| format!("error: cannot read {}: {e}\n", config_path.display()))?;
|
||||
|
||||
let ast = parse(&source).map_err(|e| {
|
||||
render_diagnostic(&source, e.span, &e.message, None)
|
||||
})?;
|
||||
|
||||
let resolved = resolve(&ast).map_err(|diags| {
|
||||
let mut out = String::new();
|
||||
for d in &diags {
|
||||
out.push_str(&render_diagnostic(&source, d.span, &d.message, d.help.as_deref()));
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(&format!(
|
||||
"error: {} problem(s) found; nothing was written\n",
|
||||
diags.len()
|
||||
));
|
||||
out
|
||||
})?;
|
||||
|
||||
let emitter = Emitter::from_env().map_err(|e| format!("error: {e}\n"))?;
|
||||
let planned = emitter.plan(&resolved).map_err(|errs| {
|
||||
let mut out = String::new();
|
||||
for e in &errs {
|
||||
out.push_str(&format!("error: {e}\n"));
|
||||
}
|
||||
out.push_str("error: nothing was written\n");
|
||||
out
|
||||
})?;
|
||||
|
||||
// Through `watch::compile` rather than parse/resolve/plan inline, because
|
||||
// that is the only path that expands `source`. Doing it by hand here meant
|
||||
// `resolve` never saw the included text -- `flatten` drops `Item::Source`
|
||||
// -- so a sourced file was silently ignored by `apply` while `watch`
|
||||
// honoured it. An include that works in one and vanishes in the other is
|
||||
// worse than one that is unsupported in both.
|
||||
let compiled = watch::compile(config_path, &emitter).map_err(|e| e.to_string())?;
|
||||
let planned = compiled.planned;
|
||||
|
||||
let changes: Vec<_> = planned.iter().filter(|p| !p.is_noop()).collect();
|
||||
|
||||
@@ -193,5 +194,56 @@ fn run_import(args: &[String]) -> Result<String, String> {
|
||||
"\n{dropped} setting(s) did not translate. Re-run with --report for details.\n"
|
||||
));
|
||||
}
|
||||
|
||||
if args.iter().any(|a| a == "--assets") {
|
||||
out.push('\n');
|
||||
out.push_str(&install_assets(src_path, &name, args)?);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The half of a theme that is not config: wallpapers, GTK/icon tarballs, and
|
||||
/// the `.theme` files belonging to waybar, rofi and kitty.
|
||||
///
|
||||
/// Separate from the conf translation because it is separate in kind — none of
|
||||
/// it is translated, only placed — and because it writes outside the
|
||||
/// cosmic-config tree, which every other path in this tool does not.
|
||||
fn install_assets(src_path: &str, name: &str, args: &[String]) -> Result<String, String> {
|
||||
let theme_dir = PathBuf::from(src_path)
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| format!("error: {src_path} has no parent directory\n"))?;
|
||||
|
||||
let source_dir = match args.iter().position(|a| a == "--source") {
|
||||
Some(i) => match args.get(i + 1) {
|
||||
Some(p) => Some(PathBuf::from(p)),
|
||||
None => return Err(format!("error: --source needs a path\n\n{USAGE}")),
|
||||
},
|
||||
None => find_source_dir(&theme_dir),
|
||||
};
|
||||
|
||||
let installer = assets::Installer::from_env().map_err(|e| format!("error: {e}\n"))?;
|
||||
let plan = installer
|
||||
.plan(
|
||||
&theme_dir,
|
||||
source_dir.as_deref(),
|
||||
name,
|
||||
args.iter().any(|a| a == "--overwrite"),
|
||||
)
|
||||
.map_err(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| format!("error: {e}\n"))
|
||||
.collect::<String>()
|
||||
})?;
|
||||
|
||||
if args.iter().any(|a| a == "--dry-run") {
|
||||
return Ok(assets::render_plan(&plan));
|
||||
}
|
||||
|
||||
let report = installer
|
||||
.apply(&plan)
|
||||
.map_err(|e| format!("error: {e}\n"))?;
|
||||
Ok(assets::render_report(&plan, &report))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user