1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Shell-side overlay enumeration for `zsync up --all`.
//!
//! Snapshots every mutable executor table that has a corresponding
//! daemon-side canonical subsystem, so a single `zsync up --all` call
//! pushes the entire shell's overlay state up to the daemon (where
//! other shells can `zsync pull` it). The daemon-crate `zsync` builtin
//! invokes [`enumerate_all_overlays`] through the trampoline registered
//! at [`crate::daemon::zsync_builtin::register_overlay_enumerator`].
//!
//! Coverage today (matches `daemon/zsync_builtin.rs::ALL_SUBSYSTEMS`):
//!
//! | subsystem | source |
//! |------------|---------------------------------------------|
//! | `alias` | `executor.aliases` |
//! | `galias` | `executor.global_aliases` |
//! | `salias` | `executor.suffix_aliases` |
//! | `setopt` | `executor.options` |
//! | `params` | `executor.{variables,arrays,assoc_arrays}` |
//! | `env` | `std::env::vars()` |
//! | `path` | `$PATH` |
//! | `manpath` | `$MANPATH` |
//! | `fpath` | `executor.fpath` |
//! | `named_dir`| `executor.named_dirs` |
//! | `zstyle` | `executor.zstyles` |
//! | `compdef` | `executor.completions` |
//!
//! Skipped today (need richer wire format or aren't simply enumerable):
//! `function` (needs source-text preservation; bytecode in
//! `executor.functions_compiled` doesn't round-trip), `bindkey` (lives
//! in the global `ZleManager`), `zmodload` (no canonical
//! "currently-loaded" list).
use serde_json::{json, Value};
/// Build the full overlay snapshot. One entry per non-empty
/// subsystem; empty subsystems are omitted to keep the wire payload
/// minimal. Called by daemon-crate `zsync up --all` through the
/// registered trampoline.
pub fn enumerate_all_overlays() -> Vec<(String, Value)> {
let mut out: Vec<(String, Value)> = Vec::new();
crate::exec::with_executor(|exec| {
// Plain string maps — alias / galias / salias / setopt all
// follow the same shape: `{key: value}` JSON object.
if !exec.aliases.is_empty() {
out.push(("alias".into(), map_to_json(&exec.aliases)));
}
if !exec.global_aliases.is_empty() {
out.push(("galias".into(), map_to_json(&exec.global_aliases)));
}
if !exec.suffix_aliases.is_empty() {
out.push(("salias".into(), map_to_json(&exec.suffix_aliases)));
}
if !exec.options.is_empty() {
// Bool → string ("on"/"off") so the canonical store's
// string-only value column doesn't have to special-case.
let map: serde_json::Map<String, Value> = exec
.options
.iter()
.map(|(k, v)| (k.clone(), Value::String(if *v { "on" } else { "off" }.into())))
.collect();
out.push(("setopt".into(), Value::Object(map)));
}
// params: scalars, arrays, and assoc maps merged into one
// object. Arrays serialize as JSON arrays of strings; assoc
// as nested objects. zsync's daemon-side push handler
// accepts the union shape.
if !exec.variables.is_empty() || !exec.arrays.is_empty() || !exec.assoc_arrays.is_empty() {
let mut params = serde_json::Map::new();
for (k, v) in &exec.variables {
params.insert(k.clone(), Value::String(v.clone()));
}
for (k, v) in &exec.arrays {
params.insert(
k.clone(),
Value::Array(v.iter().map(|s| Value::String(s.clone())).collect()),
);
}
for (k, v) in &exec.assoc_arrays {
let inner: serde_json::Map<String, Value> = v
.iter()
.map(|(ik, iv)| (ik.clone(), Value::String(iv.clone())))
.collect();
params.insert(k.clone(), Value::Object(inner));
}
out.push(("params".into(), Value::Object(params)));
}
// fpath: ordered Vec<PathBuf> → JSON array of strings.
if !exec.fpath.is_empty() {
out.push((
"fpath".into(),
Value::Array(
exec.fpath
.iter()
.map(|p| Value::String(p.display().to_string()))
.collect(),
),
));
}
// named_dir: hash -d entries.
if !exec.named_dirs.is_empty() {
let map: serde_json::Map<String, Value> = exec
.named_dirs
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.display().to_string())))
.collect();
out.push(("named_dir".into(), Value::Object(map)));
}
// compdef: completion specs are richer than scalars; we ship
// the source command-list mapping (which is what compdef
// canonical apply consumes — see canonical_apply.rs's
// compdef block).
if !exec.completions.is_empty() {
let map: serde_json::Map<String, Value> = exec
.completions
.iter()
.map(|(k, v)| (k.clone(), json!(format!("{:?}", v))))
.collect();
out.push(("compdef".into(), Value::Object(map)));
}
// zstyle: ordered Vec<ZStyle>. Serialize with debug to
// capture every field deterministically; the daemon side
// treats the value as opaque text today.
if !exec.zstyles.is_empty() {
let arr: Vec<Value> = exec
.zstyles
.iter()
.map(|z| json!(format!("{:?}", z)))
.collect();
out.push(("zstyle".into(), Value::Array(arr)));
}
});
// Process-env tables. These live outside the executor (they're
// libc env, not a Rust HashMap), so we read them after the
// with_executor borrow drops.
let mut env_map = serde_json::Map::new();
for (k, v) in std::env::vars() {
env_map.insert(k, Value::String(v));
}
if !env_map.is_empty() {
out.push(("env".into(), Value::Object(env_map)));
}
// Filter empty path components — they confuse the daemon's
// strict "must be a directory" validation, and zsh treats an
// empty entry in $PATH as the cwd (a misfeature most users want
// to avoid persisting into canonical state).
if let Ok(p) = std::env::var("PATH") {
let parts: Vec<Value> = p
.split(':')
.filter(|s| !s.is_empty())
.map(|s| Value::String(s.into()))
.collect();
if !parts.is_empty() {
out.push(("path".into(), Value::Array(parts)));
}
}
if let Ok(p) = std::env::var("MANPATH") {
let parts: Vec<Value> = p
.split(':')
.filter(|s| !s.is_empty())
.map(|s| Value::String(s.into()))
.collect();
if !parts.is_empty() {
out.push(("manpath".into(), Value::Array(parts)));
}
}
out
}
fn map_to_json(m: &std::collections::HashMap<String, String>) -> Value {
let map: serde_json::Map<String, Value> = m
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
Value::Object(map)
}