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
use clap::Parser;
use std::sync::Arc;
use yog::app::Roots;
use yog::cli_outbound::{Binary, Cli};
use yog::config_edit;
use yog::projects::runner::BlCli;
use yog::shell::ShellState;
use yog::ui_state::SystemClock;
use yog::watch::{Bridge, EguiRepaint};
use yog::world::hatch;
use yog::world::toolgate::CliProbe;
use yog::xdg::Env;
use yog::{AppModel, Args, shell};
/// Wall-clock unix seconds for the §5.2 startup sweep. main.rs is the
/// coverage-excluded eframe entry, so this lone real-time read lives here,
/// keeping the sweep decision ([`config_edit::branch::edit::stale_staging`])
/// pure and testable.
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64)
}
fn main() -> eframe::Result<()> {
// §9.3 shim mode: the `$EDITOR` lernie execs re-enters here BEFORE clap or
// eframe. argv is `<yog> --editor-apply <checkout>`; `YOG_EDIT_SRC` (env)
// carries the staging dir. Exit 0/non-zero — non-zero aborts lernie's
// commit cleanly (see `config_edit::apply` for the invocation shape).
let argv: Vec<String> = std::env::args().collect();
if argv.get(1).map(String::as_str) == Some(config_edit::apply::EDITOR_APPLY_FLAG) {
std::process::exit(config_edit::apply::run_shim(
std::env::var("YOG_EDIT_SRC").ok(),
argv.get(2).cloned(),
));
}
// Read the ambient env once (§16.2): the world's fixed override set derives
// from it and stands on every child spawn (§16.6 W2); both escape hatches
// below join a human to that same world.
let ambient = Env::from_env();
let overrides = yog::world::overrides(&ambient);
// §8.4 world escape hatches (`yog env` / `yog exec`): multi-call subcommands
// beside `--editor-apply`, dispatched before clap and eframe — they need no
// display, and clap must never see `env`/`exec` as unknown positionals.
match argv.get(1).map(String::as_str) {
// `eval "$(yog env)"` drops the caller's shell into the world.
Some(hatch::ENV_SUBCMD) => {
print!("{}", hatch::env_script(&overrides));
return Ok(());
}
// `yog exec [--cwd DIR] <cmd…>` runs one command inside the world, its
// exit faithfully yog's (a plan parse error is 2; a spawn failure 127).
Some(hatch::EXEC_SUBCMD) => match hatch::parse_exec(argv.get(2..).unwrap_or_default()) {
Ok(plan) => {
let cmd_args: Vec<&str> = plan.args.iter().map(String::as_str).collect();
match Cli::exec_in_world(&plan.cmd, &overrides, plan.cwd.as_deref(), &cmd_args) {
Ok(info) => std::process::exit(info.shell_code()),
Err(e) => {
eprintln!("yog {}: {e}", hatch::EXEC_SUBCMD);
std::process::exit(127);
}
}
}
Err(e) => {
eprintln!("yog {}: {e}", hatch::EXEC_SUBCMD);
std::process::exit(2);
}
},
_ => {}
}
let args = Args::parse();
// Compose the nested world (§16.2): every read below derives through `world`
// (so yog watches the nested clones/state/lernie-home) and every child spawns
// with `overrides` standing (§16.6 W2), so reads and spawns agree.
let world = yog::world::compose(&ambient);
// §5.2 startup sweep: drop scripted-editor staging dirs older than 24 h.
config_edit::branch::edit::sweep_staging(&world.yog_stage_root(), now_unix());
let roots = Roots {
yog_data: world.yog_data_root(),
lernie_data: world.lernie_data_root(),
yog_state: world.yog_state_root(),
balls_clones: world.balls_clones_dir(),
home: world.home_dir(),
};
// The claim identity's $USER fallback, read once from the env snapshot.
let user = world.user();
eframe::run_native(
"yog",
eframe::NativeOptions {
// Size the first-launch window in logical points (winit applies the
// display scale): the S0 surface needs the roster plus a real center,
// so a default-tiny window never slivers the composer on HiDPI.
viewport: egui::ViewportBuilder::default()
.with_inner_size([1150.0, 760.0])
.with_min_inner_size([700.0, 500.0]),
..Default::default()
},
Box::new(move |cc| {
// The congeries visuals (§11): installed once, before first paint.
yog::theme::apply(&cc.egui_ctx);
// The model enumerates every workspace, snapshots each through the
// held probe stack, arms the watch set, derives the startup focus,
// and takes the first ball/ops fetch through the injected `bl`
// runner; the bridge (sharing its watch/dirty handles) feeds disk
// events in and wakes the egui loop (§7.2).
let balls = Box::new(BlCli::new(Cli::resolve_in_world(Binary::Bl, &overrides)));
// The phase-1 toolchain probe (§16.4 W5): the three resolved tool
// CLIs, capability-probed (each driven verb by `--help`) at startup
// before first render, in the world.
let probe = Box::new(CliProbe::new(
Cli::resolve_in_world(Binary::Bl, &overrides),
Cli::resolve_in_world(Binary::Lernie, &overrides),
Cli::resolve_in_world(Binary::Bz, &overrides),
));
let model = AppModel::new(
roots,
args.workspace,
Arc::new(SystemClock),
balls,
probe,
user,
);
let bridge = Bridge::spawn(
model.watchset_handle(),
model.dirty_handle(),
EguiRepaint(cc.egui_ctx.clone()),
);
// The shell's RAM surfaces, incl. the config editors folded from the
// world env (§9) — their `bz`/`bl conf` runners nest too. A load
// error here is fatal at bring-up only.
let state = ShellState::new(&world)?;
Ok(Box::new(App {
model,
_bridge: bridge,
state,
lernie: Cli::resolve_in_world(Binary::Lernie, &overrides),
bl: Cli::resolve_in_world(Binary::Bl, &overrides),
bz: Cli::resolve_in_world(Binary::Bz, &overrides),
}))
}),
)
}
struct App {
model: AppModel,
// Held so the bridge thread lives as long as the window; dropped on exit.
_bridge: Bridge,
// Every RAM surface the shell owns: the action/start drafts, the inspector
// ephemera, and the config editors (§3.5 — discarded on exit).
state: ShellState,
// The mutating-verb binaries: message/stop/scan/prompt on `lernie`,
// close/unclaim/create/update on `bl` (§8.2), and `bz --login` — bz's one
// interactive verb — streamed from the toolchain pane (§8.3). Ball *reads* go
// through the model's own `BlCli`; these drive the short *actions*.
lernie: Cli,
bl: Cli,
bz: Cli,
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Re-derive on any bridge-delivered dirt or due sweep (§7.2).
self.model.tick();
// Poll floor (I4): wake at least every cheap-sweep interval even absent
// interaction or an fs event, so correctness never rides on a watch.
ctx.request_repaint_after(yog::app::CHEAP_SWEEP);
shell::render(
ctx,
&mut self.model,
&mut self.state,
&self.lernie,
&self.bl,
&self.bz,
);
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
// Flush any pending ui.json change (pins/collapsed/seen) on close.
let _ = self.model.flush_ui();
}
}