//! erga โ€” one-button ERGO miner + wallet for Apple Silicon.
//!
//! Open it: a self-custodial Ergo wallet is generated for you. Press the
//! crystal and it mines Autolykos v2 to a pool under that address, on the
//! honeycrisp zero-copy GPU kernel (32 MH/s at 8.3 W sustained on an M4
//! Max โ€” 2.4ร— an RTX 3090 per watt). Watch the hashrate, the accepted
//! shares, and your balance grow.

//! erga's window, as a library. The command lives in `cli/`; this crate is
//! what it opens.

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

/// The pool the window is set to, for the command to mine to the same place.
pub fn chosen_pool() -> (String, u16) {
    let p = pools::get(pools::load_choice());
    (p.host.to_string(), p.port)
}

/// The address the pool pays, for a front-end with no window open: the one
/// pasted in the window if there is one, else the wallet erga generated. The
/// terminal and the window must never disagree about who gets paid.
pub fn payout_address() -> Result<String, String> {
    if let Some(a) = store::Store::load().payout {
        return Ok(a);
    }
    erga_wallet::Wallet::load_or_create().map(|w| w.address)
}

/// Open the window. Returns when it closes.
pub fn run() -> eframe::Result<()> {
    // ERGA_WIN=1600x1000 overrides the initial window size (dev/testing).
    let size = std::env::var("ERGA_WIN")
        .ok()
        .and_then(|v| {
            let (w, h) = v.split_once('x')?;
            Some([w.parse().ok()?, h.parse().ok()?])
        })
        .unwrap_or([1180.0, 820.0]);

    // The Dock tile of a *running* app comes from NSApplication's icon, not
    // from the bundle's .icns โ€” and winit resets it when no icon is given, so
    // macOS falls back to a generated letter placeholder. Handing eframe the
    // same artwork the bundle ships keeps the icon right while we run.
    let mut viewport = egui::ViewportBuilder::default()
        .with_inner_size(size)
        .with_min_inner_size([440.0, 620.0])
        .with_title("erga");
    match eframe::icon_data::from_png_bytes(include_bytes!("../assets/icon.png")) {
        Ok(icon) => viewport = viewport.with_icon(icon),
        Err(e) => eprintln!("icon: {e}"),
    }
    let options = eframe::NativeOptions { viewport, ..Default::default() };
    eframe::run_native(
        "erga",
        options,
        Box::new(|cc| {
            setup_fonts(&cc.egui_ctx);
            setup_style(&cc.egui_ctx);
            Ok(Box::new(App::new()))
        }),
    )
}

mod balance;
mod chime;
mod miner;
mod panels;
mod purse;
mod theme;
mod tray;
mod widgets;
pub mod pool;
pub mod pools;
pub mod store;
mod stats;

use eframe::egui;
use egui::{Color32, Vec2};

use balance::BalanceState;
use panels::{machine_panel, payout_panel};
use purse::{backup_screen, wallet_block};
use theme::{badge, caps, load_icon, play_bold, setup_fonts, setup_style};
use widgets::{big_balance, crystal_button, human, panel_frame, start_hint};
use miner::Miner;

// Colour is a language here, not decoration. Nothing is given a colour it
// has not earned:
//   MINT   what you gain โ€” earnings, hashrate, the crystal while it runs
//   AMBER  what it costs โ€” cpu, memory, network: the machine working for you
//   CORAL  what is wrong โ€” rejected shares, failures
//   SKY    what the chain says โ€” height, difficulty: facts you do not own
pub const BG: Color32 = Color32::from_rgb(3, 5, 4); // near-black with a faint green cast
pub const MINT: Color32 = Color32::from_rgb(125, 255, 196);
pub const AMBER: Color32 = Color32::from_rgb(255, 186, 92);
pub const CORAL: Color32 = Color32::from_rgb(255, 122, 122);
pub const SKY: Color32 = Color32::from_rgb(126, 197, 255);
pub const CREAM: Color32 = Color32::from_rgb(235, 245, 240);
pub const MUTE: Color32 = Color32::from_rgb(90, 110, 100);
/// Every pill in the header is exactly this tall. egui sizes a ComboBox from
/// `interact_size` and a bare shape from whatever you allocate, so matching
/// the two by formula does not hold โ€” pinning both to one number does.
pub const CTRL_H: f32 = 23.0;
/// The height the start hint occupies, reserved even when it is not drawn.
pub const HINT_H: f32 = 16.0;







struct App {
    miner: Miner,
    balance: BalanceState,
    pool: pool::PoolState,
    pool_idx: usize,
    wallet: Result<erga_wallet::Wallet, String>,
    /// The effective rate, smoothed. The raw figure steps every time a table
    /// rebuild stalls the numerator while the clock keeps running, and a
    /// number meant to say "this is your real pace" should drift, not twitch.
    eff_smooth: Option<f64>,
    /// Whether the window had focus last frame, so the click that merely
    /// brings it forward can be swallowed.
    was_focused: bool,
    /// The menu-bar item, built on the first frame: macOS wants it made on
    /// the main thread, and this is the only place guaranteed to be one.
    tray: Option<tray::Tray>,
    tray_tried: bool,
    /// When the crystal was last pressed, for the press animation.
    pressed_at: Option<std::time::Instant>,
    /// Accepted shares already announced, so each is celebrated exactly once.
    heard_shares: u64,
    /// Whether the seed is on screen. It waits to be dismissed: a timer suits
    /// something that appeared uninvited, but this one was opened on purpose
    /// and is being copied onto paper, and paper is slow.
    show_seed: bool,
    /// The payout-address screen, and what is being typed into it.
    show_address: bool,
    address_input: String,
    spin: f32,
    last_balance: std::time::Instant,
    sys: stats::Sys,
    store: store::Store,
    /// Loaded on first paint; `Some(None)` means the artwork would not decode.
    icon: Option<Option<egui::TextureHandle>>,
    /// When the current mining session began, for the effective rate.
    session_start: Option<std::time::Instant>,
    last_save: std::time::Instant,
}

impl App {
    fn new() -> Self {
        chime::ensure();
        let wallet = erga_wallet::Wallet::load_or_create();
        let balance = BalanceState::default();
        let idx = pools::load_choice();
        let pool = pool::PoolState::default();
        if let Ok(w) = &wallet {
            balance.fetch(w.address.clone()); // show balance immediately
            pool.fetch(w.address.clone(), idx); // and what the pool owes us
        }
        App {
            miner: Miner::new(),
            balance,
            pool,
            pool_idx: idx,
            wallet,
            eff_smooth: None,
            // False, so the first focused frame counts as *gaining* focus and
            // the click that opened the window is swallowed with every other
            // activating click. Starting at true left the launch frames
            // unguarded: a pointer resting where a button happens to appear
            // pressed it before the window had finished opening.
            was_focused: false,
            tray: None,
            tray_tried: false,
            pressed_at: None,
            heard_shares: 0,
            show_seed: false,
            // ERGA_SCREEN=address opens straight onto a screen that otherwise
            // takes a button press to reach โ€” the same kind of knob as
            // ERGA_WIN, so a layout can be photographed and measured without
            // steering a mouse at it.
            show_address: std::env::var("ERGA_SCREEN").is_ok_and(|v| v == "address"),
            address_input: String::new(),
            spin: 0.0,
            last_balance: std::time::Instant::now(),
            sys: stats::Sys::new(),
            store: store::Store::load(),
            icon: None,
            session_start: None,
            last_save: std::time::Instant::now(),
        }
    }

    /// Begin mining, applying the solo prefix if it is on. herominers routes
    /// a `solo:` address to solo mining: you keep whole blocks and get
    /// nothing in between, so the pool's shared payout no longer applies.
    fn begin(&mut self) {
        let Some(addr) = self.address().map(|a| a.to_string()) else {
            self.miner.p.set_status("wallet unavailable");
            return;
        };
        // Publish the intensity before the miner starts, so its first
        // dispatch already runs at the chosen setting rather than at full
        // tilt until the first poll.
        store::write_intensity(&self.store.intensity);
        let solo = self.solo();
        let (host, port, prefix) = pools::endpoint(self.pool_idx, solo);
        let addr = format!("{prefix}{addr}");
        store::log(&format!(
            "start pool={} {host}:{port} solo={solo}",
            pools::get(self.pool_idx).label
        ));
        self.session_start = Some(std::time::Instant::now());
        self.miner.start(addr, host, port);
    }

    /// Stop mining and fold this session's counters into the all-time totals.
    fn end(&mut self) {
        use std::sync::atomic::Ordering::Relaxed;
        let p = &self.miner.p;
        let (a, r, d, h) = (
            p.accepted.load(Relaxed),
            p.rejected.load(Relaxed),
            p.donated.load(Relaxed),
            p.hashed.load(Relaxed),
        );
        self.miner.stop();
        self.store.accepted += a;
        self.store.rejected += r;
        self.store.donated += d;
        self.store.hashed += h;
        self.store.save();
        self.session_start = None;
        store::log(&format!("stop accepted={a} rejected={r} donated={d} hashed={h}"));
    }

    /// Solo only counts where the chosen pool offers it.
    fn solo(&self) -> bool {
        self.store.solo && pools::has_solo(self.pool_idx)
    }

    /// Start mining as soon as the window is up. Set ERGA_AUTOSTART=1 for a
    /// login item, or for a machine whose only job is to mine.
    fn autostart(&self) -> bool {
        std::env::var("ERGA_AUTOSTART").map(|v| v != "0").unwrap_or(false)
    }

    /// The address the pool pays. A pasted one wins over the wallet erga
    /// generated: someone who already mines has an address already, and this
    /// app has no business insisting on being their wallet too.
    ///
    /// Everything downstream โ€” the miner's argument, the ledger query, the
    /// address under the crystal โ€” reads this one function, so there is no
    /// second place for the two to disagree.
    fn address(&self) -> Option<&str> {
        if let Some(a) = self.store.payout.as_deref() {
            return Some(a);
        }
        self.wallet.as_ref().ok().map(|w| w.address.as_str())
    }

    /// True when the pool is paying somewhere the seed here cannot spend.
    fn payout_is_external(&self) -> bool {
        self.store.payout.is_some()
    }

    /// Flip solo, restarting a live session: the pool binds the mode to the
    /// connection, so a change that did not reconnect would be a lie.
    fn toggle_solo(&mut self) {
        if !pools::has_solo(self.pool_idx) {
            return;
        }
        self.store.solo = !self.store.solo;
        self.store.save();
        if self.miner.is_running() {
            self.end();
            self.begin();
        }
    }

    /// Set the intensity. Writing the file is the whole mechanism: the miner
    /// re-reads it twice a second, so this is felt without a restart.
    fn set_mode(&mut self, idx: usize) {
        self.store.intensity = purse::MODES[idx].to_string();
        self.store.save();
        store::write_intensity(purse::MODES[idx]);
    }

    fn cycle_mode(&mut self) {
        let idx = purse::MODES
            .iter()
            .position(|m| *m == self.store.intensity)
            .unwrap_or(0);
        self.set_mode((idx + 1) % purse::MODES.len());
    }

    /// Move to the next pool. With two pools, cycling IS choosing โ€” and it
    /// keeps the bar one pill instead of a dropdown.
    fn cycle_pool(&mut self) {
        self.pool_idx = (self.pool_idx + 1) % pools::POOLS.len();
        pools::save_choice(self.pool_idx);
        // the new pool keeps its own books
        if let Some(a) = self.address().map(|a| a.to_string()) {
            self.pool.fetch(a, self.pool_idx);
        }
        if self.miner.is_running() {
            // hop pools live: land on the new door mid-flight
            self.end();
            self.begin();
        }
    }

    /// Point the pool at a different address, or back at erga's own wallet.
    ///
    /// Mining restarts when it is running. The address is what the miner
    /// authorizes its session with, and a pool credits the address that
    /// authorized the connection โ€” so a change that did not restart would go
    /// on paying the old one while this window claimed otherwise. The cost is
    /// one epoch-table rebuild, which is the honest price.
    fn set_payout(&mut self, addr: Option<String>) {
        use std::sync::atomic::Ordering::Relaxed;
        let was_running = self.miner.p.running.load(Relaxed);
        store::log(&format!(
            "payout address -> {}",
            addr.as_deref().unwrap_or("erga's own wallet")
        ));
        self.store.payout = addr;
        self.store.save();
        self.show_address = false;
        self.address_input.clear();
        if was_running {
            self.end();
            self.begin();
        }
    }
}

impl eframe::App for App {
    fn clear_color(&self, _v: &egui::Visuals) -> [f32; 4] {
        [3.0 / 255.0, 5.0 / 255.0, 4.0 / 255.0, 1.0]
    }

    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // macOS convention: the click that activates a window does not also
        // act on it. Without this, clicking erga to bring it forward lands on
        // whatever sits under the pointer โ€” and with the action bar at the
        // foot of the window, that is how a seed screen opens unasked.
        let focused = ctx.input(|i| i.viewport().focused.unwrap_or(true));
        let just_focused = focused && !self.was_focused;
        self.was_focused = focused;
        if just_focused {
            ctx.input_mut(|i| {
                i.events
                    .retain(|e| !matches!(e, egui::Event::PointerButton { .. }));
                i.pointer = Default::default();
            });
        }
        if self.autostart() && self.session_start.is_none() && !self.miner.is_running() {
            self.begin();
        }
        // Build the menu-bar item once, on the first frame โ€” macOS wants it
        // made from the main thread, and this is one.
        if !self.tray_tried {
            self.tray_tried = true;
            self.tray = tray::Tray::new();
        }
        let running = self.miner.is_running();
        if let Some(t) = &self.tray {
            match t.poll() {
                tray::Ask::ToggleMining => {
                    chime::press();
                    if running {
                        self.end();
                    } else {
                        self.begin();
                    }
                }
                tray::Ask::Quit => {
                    self.end();
                    ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                }
                tray::Ask::Nothing => {}
            }
        }
        let running = self.miner.is_running();
        // A share is the only thing here that is genuinely good news, so it is
        // the only thing that makes a sound on its own.
        let accepted = self.miner.p.accepted.load(std::sync::atomic::Ordering::Relaxed);
        if accepted > self.heard_shares {
            if self.heard_shares > 0 || accepted == 1 {
                chime::share();
            }
            self.heard_shares = accepted;
        } else if accepted < self.heard_shares {
            self.heard_shares = accepted; // a new session restarted the count
        }
        if running {
            self.spin += 0.01;
        }
        // keep ticking so hashrate, dashboard and balance stay live
        ctx.request_repaint_after(std::time::Duration::from_millis(if running { 250 } else { 1000 }));

        // Effective rate: hashes over the whole session, so the seconds spent
        // rebuilding the table each block are counted honestly.
        let eff_mhs = self.session_start.and_then(|t| {
            let secs = t.elapsed().as_secs_f64();
            let h = self.miner.p.hashed.load(std::sync::atomic::Ordering::Relaxed);
            (secs > 5.0 && h > 0).then(|| h as f64 / secs / 1e6)
        });
        // ~15 s to settle at four frames a second: slow enough to read as a
        // trend, quick enough to follow a real change in the machine.
        if let Some(e) = eff_mhs {
            self.eff_smooth = Some(match self.eff_smooth {
                Some(prev) => prev + (e - prev) * 0.017,
                None => e,
            });
        } else {
            self.eff_smooth = None;
        }
        let eff_mhs = self.eff_smooth;
        let all_time = {
            use std::sync::atomic::Ordering::Relaxed;
            (
                self.store.accepted + self.miner.p.accepted.load(Relaxed),
                self.store.hashed + self.miner.p.hashed.load(Relaxed),
            )
        };
        // Checkpoint the totals every minute so a crash costs at most that.
        if running && self.last_save.elapsed().as_secs() >= 60 {
            use std::sync::atomic::Ordering::Relaxed;
            let p = &self.miner.p;
            self.store.save_with(
                p.accepted.load(Relaxed),
                p.rejected.load(Relaxed),
                p.donated.load(Relaxed),
                p.hashed.load(Relaxed),
            );
            self.last_save = std::time::Instant::now();
        }

        if let Some(t) = &mut self.tray {
            let toward = {
                let pi = self.pool.inner.lock().unwrap();
                (pi.ok && pi.threshold_erg > 0.0)
                    .then(|| ((pi.balance_erg + pi.pending_erg) / pi.threshold_erg) as f32)
            };
            t.update(running, self.miner.p.mhs(), toward);
        }

        egui::CentralPanel::default().show(ctx, |ui| {
            // The seed takes the whole window while it is up: it is not a
            // dialog over the app, it is the only thing that should be on
            // screen โ€” and it puts itself away.
            if self.show_seed {
                let seed = self.wallet.as_ref().ok().map(|w| w.mnemonic.clone());
                let esc = ui.input(|i| i.key_pressed(egui::Key::Escape));
                if backup_screen(ui, seed.as_deref()) || esc {
                    self.show_seed = false;
                }
                return;
            }
            if self.show_address {
                let generated = self.wallet.as_ref().ok().map(|w| w.address.clone());
                let current = self.address().map(|a| a.to_string());
                let external = self.payout_is_external();
                let mut input = std::mem::take(&mut self.address_input);
                let act = purse::address_screen(
                    ui,
                    &mut input,
                    current.as_deref(),
                    generated.as_deref(),
                    external,
                );
                self.address_input = input;
                let act = if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
                    purse::AddressAction::Cancel
                } else {
                    act
                };
                match act {
                    purse::AddressAction::None => {}
                    purse::AddressAction::Use(a) => {
                        self.set_payout(Some(a));
                    }
                    purse::AddressAction::UseGenerated => {
                        self.set_payout(None);
                    }
                    purse::AddressAction::Cancel => {
                        self.show_address = false;
                        self.address_input.clear();
                    }
                }
                return;
            }
            // A press dips the crystal and lets it spring back over ~220 ms.
            // Small enough to feel like contact rather than like a transition.
            let press = self
                .pressed_at
                .map(|t| t.elapsed().as_secs_f32())
                .filter(|e| *e < 0.22)
                .map(|e| {
                    let k = e / 0.22;
                    1.0 - 0.06 * (1.0 - k) * (k * 18.0).cos().abs()
                })
                .unwrap_or(1.0);
            if self.pressed_at.is_some() && press >= 1.0 {
                self.pressed_at = None;
            }
            let avail_w = ui.available_width();
            ui.add_space(16.0);

            // โ”€โ”€ header โ€” wordmark left, pool + badge right โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            ui.horizontal(|ui| {
                ui.add_space(22.0);
                if let Some(tex) = self.icon.get_or_insert_with(|| load_icon(ui.ctx())).clone() {
                    ui.add(
                        egui::Image::new(egui::load::SizedTexture::new(
                            tex.id(),
                            Vec2::splat(34.0),
                        ))
                        .fit_to_exact_size(Vec2::splat(34.0)),
                    );
                    ui.add_space(9.0);
                }
                let mut job = egui::text::LayoutJob::default();
                job.append(
                    "Erga",
                    0.0,
                    egui::TextFormat {
                        font_id: play_bold(21.0),
                        color: CREAM,
                        extra_letter_spacing: 0.6,
                        ..Default::default()
                    },
                );
                ui.label(job);
                ui.add_space(8.0);
                // The licence sits with the version: the answers this window
                // does not volunteer โ€” the development share among them โ€” are
                // all in the code, and MIT is what makes the code truly yours.
                caps(ui, &format!("v{} ยท mit", env!("CARGO_PKG_VERSION")), 9.5, MUTE.gamma_multiply(0.85));
                ui.add_space(16.0);
                // The run's own state belongs with the other facts about this
                // run, not floating under the crystal between two unrelated
                // things.
                // The table build is the one wait long enough to wonder about,
                // so it gets a meter and a pulse: soft, but quick enough to
                // catch the eye and say *this is working, not stuck*.
                let build = self.miner.p.build_pct.load(std::sync::atomic::Ordering::Relaxed);
                let status = self.miner.p.status.lock().unwrap().clone();
                let building = running && status.starts_with("building table") && build < 100;
                let glow: f32 = if building {
                    let t = ui.input(|i| i.time);
                    0.5 + 0.5 * (t * 3.4).sin() as f32
                } else {
                    1.0
                };
                caps(
                    ui,
                    &status,
                    9.5,
                    if building {
                        MINT.gamma_multiply(0.45 + 0.55 * glow)
                    } else if running {
                        MINT.gamma_multiply(0.8)
                    } else {
                        MUTE
                    },
                );
                if building {
                    ui.add_space(9.0);
                    widgets::battery(ui, build as f32 / 100.0, glow);
                    ui.add_space(6.0);
                    caps(
                        ui,
                        &format!("{build}%"),
                        9.5,
                        MINT.gamma_multiply(0.45 + 0.55 * glow),
                    );
                    // Pulsing means animating: ask for the next frame.
                    ui.ctx().request_repaint();
                }
                // The same battery in a calmer temper: the NEXT table filling
                // up while this one mines. No pulse โ€” a pulse says "wait for
                // me", and nothing here is waiting.
                let next = self.miner.p.next_pct.load(std::sync::atomic::Ordering::Relaxed);
                if running && !building && next < 100 {
                    ui.add_space(9.0);
                    widgets::battery(ui, next as f32 / 100.0, 0.55);
                    ui.add_space(6.0);
                    caps(ui, &format!("next table {next}%"), 9.5, MUTE);
                }
                // The controls this row used to carry live in the bar at the
                // foot now, behind keys โ€” where a hand and an eye both look.
                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    ui.spacing_mut().interact_size.y = CTRL_H;
                    ui.add_space(22.0);
                    badge(ui, "beta", AMBER);
                });

            });

            // โ”€โ”€ one refresh, one snapshot, both layouts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            if let Some(addr) = self.address().map(|a| a.to_string()) {
                if self.last_balance.elapsed().as_secs() >= 30 {
                    self.balance.fetch(addr.clone());
                    if pools::has_ledger(self.pool_idx) {
                        self.pool.fetch(addr, self.pool_idx);
                    }
                    self.last_balance = std::time::Instant::now();
                }
            }
            self.sys.refresh(self.miner.pid());
            let p = self.miner.p.clone();
            let mhs = p.mhs();
            let (cpu, mem, net_kbs) = (self.sys.cpu, self.sys.mem, self.sys.down_kbs);
            let (miner_cpu, miner_mem) = (self.sys.miner_cpu, self.sys.miner_mem);
            let machine = panels::Machine {
                p: &p,
                cpu,
                mem,
                miner_cpu,
                miner_mem,
                net_kbs,
                mhs,
                eff_mhs,
            };
            let on_chain = self.balance.inner.lock().unwrap().erg;
            let has_ledger = pools::has_ledger(self.pool_idx);
            let solo = self.solo();
            let addr_opt = self.address().map(|a| a.to_string());
            let mut want_backup = false;
            let mut want_report = false;
            let mut want_address = false;
            let mut start_stop = false;

            // The keys the bar advertises. Guarded off text fields, and the
            // seed and address screens returned before reaching here, so a
            // letter can never fire behind another screen's back.
            if !ui.ctx().wants_keyboard_input() {
                use egui::Key;
                let pressed = |k: Key| ui.input(|i| i.key_pressed(k));
                if pressed(Key::Space) {
                    start_stop = true;
                }
                if pressed(Key::S) {
                    self.toggle_solo();
                }
                if pressed(Key::M) {
                    self.cycle_mode();
                }
                if pressed(Key::P) {
                    self.cycle_pool();
                }
                if pressed(Key::C) {
                    if let Some(a) = self.address().map(|a| a.to_string()) {
                        ui.output_mut(|o| o.copied_text = a);
                    }
                }
                if pressed(Key::A) {
                    want_address = true;
                }
                if pressed(Key::B) {
                    want_backup = true;
                }
                if pressed(Key::R) {
                    want_report = true;
                }
            }

            let wide = avail_w >= 1000.0;
            // The header has drawn by now, so ask again: the value taken
            // before it is larger than what is actually left, and every
            // centring below is computed from this number.
            let avail_h = ui.available_height();
            if wide {
                // โ”€โ”€ the HUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
                // Read top to bottom: the balance is what this is for, the
                // crystal is how you get it, the wallet is where it lands.
                // So the balance opens and the actions close, and the crystal
                // sits dead centre of the window with the two panels โ€” cost
                // on the left, return on the right โ€” level with it.
                let side = 40.0;
                let (pw, gap) = (330.0, 30.0);
                let cw = (avail_w - side * 2.0 - pw * 2.0 - gap * 2.0).max(260.0);
                // the actions own the foot of the window
                // the foot holds the action bar and the air around it; the
                // address moved up to close the centre column
                let foot_h = 96.0;
                let band_h = (avail_h - foot_h - 16.0).max(340.0);
                // For the crystal's centre to land on band_h/2 while the rate
                // still fits beneath it, the radius cannot exceed half the
                // band less what sits below. Anything larger pushes the wallet
                // out of the foot and over the status line.
                // Balance, crystal, address read as one column, so the air
                // above the crystal and the air below it are the same number.
                // The blocks differ in height; the *gaps* are what the eye
                // measures symmetry by.
                const GAP: f32 = 40.0;
                let balance_h = 78.0; // the number, at its largest
                let addr_h = 24.0;
                let cr = (cw * 0.34)
                    .min(band_h / 2.0 - GAP - addr_h - 12.0)
                    .clamp(110.0, 250.0);
                // sized to their content, not to the window: a panel that
                // stretches is mostly empty
                let panel_h = (band_h - 40.0).clamp(300.0, 430.0);

                // The hint's row is reserved whether or not it is drawn:
                // otherwise the whole window jumps upward the moment mining
                // starts, which reads as a glitch rather than as feedback.
                ui.add_space(2.0);
                if running {
                    ui.add_space(HINT_H);
                } else {
                    start_hint(ui);
                }
                ui.add_space(22.0);
                ui.horizontal(|ui| {
                    ui.add_space(side);
                    // panels are centred on the crystal, not hung from the top
                    ui.vertical(|ui| {
                        ui.set_width(pw);
                        ui.add_space(((band_h - panel_h) / 2.0).max(0.0));
                        panel_frame(ui, pw, panel_h, |ui| {
                            machine_panel(ui, &machine);
                        });
                    });
                    ui.add_space(gap);
                    ui.vertical(|ui| {
                        ui.set_width(cw);
                        // Sized so the crystal's own centre lands on band_h/2.
                        ui.add_space(((band_h / 2.0) - cr - GAP - balance_h).max(4.0));
                        big_balance(ui, on_chain, cw);
                        // The balance block carries empty space under its
                        // digits โ€” the ERG row and the font's descent โ€” so the
                        // gap above needs to be *smaller* than the one below
                        // to look the same. Equal constants look unequal, and
                        // looking equal is the only kind that counts.
                        ui.add_space(GAP - 28.0);
                        if crystal_button(ui, cw, cr, running, self.spin, press, mhs) {
                            start_stop = true;
                        }
                        ui.add_space(GAP);
                        wallet_block(ui, addr_opt.as_deref());
                    });
                    ui.add_space(gap);
                    ui.vertical(|ui| {
                        ui.set_width(pw);
                        ui.add_space(((band_h - panel_h) / 2.0).max(0.0));
                        panel_frame(ui, pw, panel_h, |ui| {
                            let pi = self.pool.inner.lock().unwrap();
                            payout_panel(
                                ui,
                                &panels::Payout {
                                    pi: &pi,
                                    has_ledger,
                                    solo,
                                    mhs,
                                    running,
                                    all_time,
                                    target_h: panel_h - 32.0,
                                },
                            );
                        });
                    });
                });

                // the address, then the bar hard against the bottom edge
                ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
                    ui.add_space(24.0);
                    {
                        let bv = purse::BarView {
                            has_addr: addr_opt.is_some(),
                            has_solo: pools::has_solo(self.pool_idx),
                            solo: self.store.solo,
                            mode_idx: purse::MODES
                                .iter()
                                .position(|m| *m == self.store.intensity)
                                .unwrap_or(0),
                            pool_label: pools::POOLS[self.pool_idx].label,
                        };
                        match purse::control_bar(ui, &bv) {
                            Some(purse::BarEvent::ToggleSolo) => self.toggle_solo(),
                            Some(purse::BarEvent::Mode(i)) => self.set_mode(i),
                            Some(purse::BarEvent::CyclePool) => self.cycle_pool(),
                            Some(purse::BarEvent::Copy) => {
                                if let Some(a) = &addr_opt {
                                    ui.output_mut(|o| o.copied_text = a.clone());
                                }
                            }
                            Some(purse::BarEvent::Address) => want_address = true,
                            Some(purse::BarEvent::Backup) => want_backup = true,
                            Some(purse::BarEvent::Report) => want_report = true,
                            None => {}
                        }
                    }
                });
            } else {
                // โ”€โ”€ narrow: the same organs, stacked on one axis โ”€โ”€โ”€โ”€โ”€โ”€
                let col_w = (avail_w - 44.0).min(600.0);
                let side = ((avail_w - col_w) / 2.0).max(0.0);
                let cr = (col_w * 0.30).clamp(112.0, 155.0);
                egui::ScrollArea::vertical()
                    .auto_shrink([false; 2])
                    .show(ui, |ui| {
                        if !running {
                            ui.add_space(6.0);
                            start_hint(ui);
                        }
                        ui.add_space(12.0);
                        big_balance(ui, on_chain, col_w);
                        ui.add_space(26.0);
                        if crystal_button(ui, ui.available_width(), cr, running, self.spin, press, mhs) {
                            start_stop = true;
                        }
                        ui.add_space(16.0);
                        wallet_block(ui, addr_opt.as_deref());
                        ui.add_space(14.0);
                        {
                        let bv = purse::BarView {
                            has_addr: addr_opt.is_some(),
                            has_solo: pools::has_solo(self.pool_idx),
                            solo: self.store.solo,
                            mode_idx: purse::MODES
                                .iter()
                                .position(|m| *m == self.store.intensity)
                                .unwrap_or(0),
                            pool_label: pools::POOLS[self.pool_idx].label,
                        };
                        match purse::control_bar(ui, &bv) {
                            Some(purse::BarEvent::ToggleSolo) => self.toggle_solo(),
                            Some(purse::BarEvent::Mode(i)) => self.set_mode(i),
                            Some(purse::BarEvent::CyclePool) => self.cycle_pool(),
                            Some(purse::BarEvent::Copy) => {
                                if let Some(a) = &addr_opt {
                                    ui.output_mut(|o| o.copied_text = a.clone());
                                }
                            }
                            Some(purse::BarEvent::Address) => want_address = true,
                            Some(purse::BarEvent::Backup) => want_backup = true,
                            Some(purse::BarEvent::Report) => want_report = true,
                            None => {}
                        }
                    }
                        ui.add_space(18.0);

                        ui.horizontal(|ui| {
                            ui.add_space(side);
                            ui.vertical(|ui| {
                                ui.set_width(col_w);
                                panel_frame(ui, col_w, 0.0, |ui| {
                                    machine_panel(ui, &machine);
                                });
                                ui.add_space(12.0);
                                panel_frame(ui, col_w, 0.0, |ui| {
                                    let pi = self.pool.inner.lock().unwrap();
                                    payout_panel(
                                        ui,
                                        &panels::Payout {
                                            pi: &pi,
                                            has_ledger,
                                            solo,
                                            mhs,
                                            running,
                                            all_time,
                                            target_h: 0.0,
                                        },
                                    );
                                });
                                ui.add_space(14.0);
                            });
                        });
                    });
            }

            if want_report {
                let pl = pools::get(self.pool_idx);
                let p = &self.miner.p;
                use std::sync::atomic::Ordering::Relaxed;
                let state = format!(
                    "pool: {} ({}:{}) solo={}\nstatus: {}\ndevice: {}\nrate: {:.1} MH/s\n\
                     height: {}\nshares: {} accepted, {} rejected\n\
                     hashed this run: {}\nall time: {} shares, {} hashes",
                    pl.label,
                    pl.host,
                    pl.port,
                    self.solo(),
                    p.status.lock().unwrap(),
                    p.device.lock().unwrap(),
                    p.mhs(),
                    p.height.load(Relaxed),
                    p.accepted.load(Relaxed),
                    p.rejected.load(Relaxed),
                    human(p.hashed.load(Relaxed)),
                    self.store.accepted + p.accepted.load(Relaxed),
                    human(self.store.hashed + p.hashed.load(Relaxed)),
                );
                store::report_bug(&state);
            }
            if want_address {
                self.address_input.clear();
                self.show_address = true;
            }
            if want_backup {
                // Copy on open: the words are wanted *somewhere else*, and a
                // person retyping fifteen of them from a screen makes mistakes.
                if let Ok(w) = &self.wallet {
                    ui.output_mut(|o| o.copied_text = w.mnemonic.clone());
                }
                self.show_seed = true;
            }
            if start_stop {
                chime::press();
                self.pressed_at = Some(std::time::Instant::now());
                if running {
                    self.end();
                } else {
                    self.begin();
                }
            }


            // the footer rides at the bottom only in the wide HUD; in the
            // narrow layout it lives inside the scroll, after the content.
            if wide {
            }
        });
    }
}






















Homonyms

warriors/trisha/wgpu/lib.rs
soft3/glia/run/lib.rs
soft3/mir/src/lib.rs
soft3/foculus/src/lib.rs
cyb/optica/src/lib.rs
soft3/strata/src/lib.rs
warriors/trisha/rs/lib.rs
cyb/shell/src/lib.rs
cyb/core/src/lib.rs
soft3/glia/import/lib.rs
warriors/trisha/honeycrisp/lib.rs
neural/trident/src/lib.rs
soft3/crate/src/lib.rs
cyb/honeycrisp/src/lib.rs
cyb/prysm/rs/lib.rs
soft3/lens/src/lib.rs
soft3/tru/rs/lib.rs
soft3/cybergraph/src/lib.rs
soft3/nox/rs/lib.rs
soft3/mudra/src/lib.rs
soft3/radio/cyber-bao/src/lib.rs
neural/rs/dialect/src/lib.rs
soft3/lens/assayer/src/lib.rs
cyb/honeycrisp/aruminium/src/lib.rs
soft3/lens/brakedown/src/lib.rs
soft3/bbg/rs/src/lib.rs
neural/rs/sigil/src/lib.rs
soft3/radio/iroh-willow/src/lib.rs
cyb/crates/cyb/src/lib.rs
neural/rs/link/src/lib.rs
cyb/honeycrisp/unimem/src/lib.rs
soft3/lens/porphyry/src/lib.rs
soft3/radio/iroh-docs/src/lib.rs
soft3/radio/iroh-car/src/lib.rs
soft3/radio/iroh-dns-server/src/lib.rs
soft3/radio/iroh/src/lib.rs
cyb/crates/cyb-reserve/src/lib.rs
soft3/strata/ext/src/lib.rs
soft3/radio/iroh-relay/src/lib.rs
neural/rune/rs/parse/lib.rs
neural/rs/codegen/src/lib.rs
soft3/radio/iroh-blobs/src/lib.rs
soft3/zheng/rs/src/lib.rs
soft3/strata/kuro/rs/lib.rs
soft3/tok/rs/src/lib.rs
soft3/conformance/rs/src/lib.rs
soft3/radio/iroh-base/src/lib.rs
neural/rs/macros/src/lib.rs
neural/rs/darwin-sys/src/lib.rs
soft3/radio/iroh-ffi/src/lib.rs
cyb/honeycrisp/rane/src/lib.rs
neural/rune/rs/prysm/lib.rs
soft3/lens/core/src/lib.rs
neural/rs/mir-format/src/lib.rs
neural/rs/core/src/lib.rs
soft3/strata/nebu/rs/lib.rs
neural/rune/rs/compile/lib.rs
neural/rune/rs/interp/lib.rs
soft3/strata/compute/src/lib.rs
soft3/lens/binius/src/lib.rs
soft3/strata/proof/src/lib.rs
neural/eidos/rs/src/lib.rs
neural/rune/rs/mold/lib.rs
soft3/strata/core/src/lib.rs
soft3/hemera/wgsl/src/lib.rs
neural/rune/rs/subject/lib.rs
soft3/soma/kernel/src/lib.rs
neural/rune/rs/lower/lib.rs
cyb/honeycrisp/acpu/src/lib.rs
neural/rune/rs/parse-pure/lib.rs
soft3/radio/iroh-gossip/src/lib.rs
neural/rune/rs/lex/lib.rs
soft3/hemera/rs/src/lib.rs
neural/rune/rs/ast/lib.rs
soft3/lens/ikat/src/lib.rs
soft3/tape/impl/rust/src/lib.rs
cyb/evy/crates/evy_radio/src/lib.rs
neural/inf/rs/lex/src/lib.rs
cyb/evy/forks/bevy_animation/src/lib.rs
neural/inf/rs/oracle/src/lib.rs
neural/trident/editor/zed/src/lib.rs
warriors/trisha/.vendor/twenty-first/src/lib.rs
warriors/erga/rs/pool/src/lib.rs
cyb/wysm/crates/c_api/src/lib.rs
cyb/evy/forks/bevy_anti_alias/src/lib.rs
soft3/strata/trop/rs/src/lib.rs
cyb/evy/forks/bevy_pbr/src/lib.rs
soft3/radio/quinn/quinn-udp/src/lib.rs
warriors/trisha/.vendor/triton-vm/src/lib.rs
soft3/strata/genies/rs/src/lib.rs
soft3/radio/nettools/portmapper/src/lib.rs
warriors/erga/rs/wallet/src/lib.rs
soft3/lytics/rs/core/src/lib.rs
cyb/wysm/crates/c_api/macro/lib.rs
soft3/strata/kuro/wgsl/src/lib.rs
cyb/evy/forks/bevy_tasks/src/lib.rs
neural/inf/rs/plan/src/lib.rs
cyb/wysm/crates/ir/src/lib.rs
cyb/evy/forks/bevy_gizmos/src/lib.rs
soft3/strata/genies/wgsl/src/lib.rs
warriors/erga/rs/autolykos/src/lib.rs
cyb/evy/forks/bevy_sprite/src/lib.rs
cyb/evy/crates/evy_prysm_core/src/lib.rs
warriors/trisha/.vendor/triton-constraint-circuit/src/lib.rs
cyb/evy/forks/bevy_transform/src/lib.rs
cyb/evy/crates/evy_engine_dispatch/src/lib.rs
warriors/trisha/.vendor/triton-air/src/lib.rs
cyb/evy/forks/bevy_diagnostic/src/lib.rs
cyb/wysm/crates/fuzz/src/lib.rs
neural/inf/rs/source/src/lib.rs
cyb/wysm/crates/wasmi/src/lib.rs
cyb/evy/forks/bevy_ecs/src/lib.rs
soft3/strata/jali/rs/src/lib.rs
warriors/trisha/.vendor/triton-isa/src/lib.rs
soft3/lytics/rs/event/src/lib.rs
soft3/strata/trop/wgsl/src/lib.rs
cyb/evy/crates/evy_dialect/src/lib.rs
warriors/trisha/.vendor/triton-constraint-builder/src/lib.rs
cyb/evy/crates/evy_platform_caps/src/lib.rs
cyb/evy/forks/bevy_sprite_render/src/lib.rs
cyb/evy/forks/bevy_gizmos_render/src/lib.rs
cyb/wysm/crates/collections/src/lib.rs
neural/inf/rs/ast/src/lib.rs
cyb/evy/forks/bevy_mesh/src/lib.rs
soft3/radio/quinn/bench/src/lib.rs
cyb/wysm/crates/c_api/artifact/lib.rs
warriors/erga/rs/rtable-bench/src/lib.rs
cyb/wysm/crates/core/src/lib.rs
soft3/strata/jali/wgsl/src/lib.rs
soft3/radio/iroh/bench/src/lib.rs
warriors/erga/rs/blake-bench/src/lib.rs
neural/inf/rs/parse/src/lib.rs
cyb/evy/forks/bevy_post_process/src/lib.rs
soft3/radio/quinn/quinn/src/lib.rs
cyb/evy/crates/evy_ecs_storage/src/lib.rs
neural/inf/rs/lower/src/lib.rs
cyb/evy/forks/bevy_core_pipeline/src/lib.rs
cyb/evy/forks/bevy_render/src/lib.rs
neural/rs/tests/macro-integration/src/lib.rs
soft3/radio/iroh-ffi/iroh-js/src/lib.rs
cyb/evy/forks/bevy_image/src/lib.rs
cyb/evy/crates/evy_engine_tasks/src/lib.rs
warriors/erga/rs/mine-bench/src/lib.rs
soft3/strata/nebu/wgsl/src/lib.rs
soft3/radio/nettools/netwatch/src/lib.rs
warriors/erga/rs/miner/src/lib.rs
neural/inf/rs/value/src/lib.rs
cyb/wysm/crates/wast/src/lib.rs
soft3/radio/tests/integration/src/lib.rs
cyb/wysm/crates/wasi/src/lib.rs
cyb/evy/crates/evy_diagnostic/src/lib.rs
soft3/radio/quinn/perf/src/lib.rs
soft3/radio/quinn/quinn-proto/src/lib.rs
cyb/evy/forks/naga/src/lib.rs
neural/inf/rs/eval/src/lib.rs
cyb/evy/crates/evy_engine_core/src/lib.rs
neural/inf/rs/cozo/cozo-lib-python/src/lib.rs
neural/inf/rs/cozo/cozo-lib-swift/src/lib.rs
neural/inf/rs/cozo/cozorocks/src/lib.rs
neural/inf/rs/cozo/cozo-lib-java/src/lib.rs
neural/inf/rs/cozo/cozo-lib-c/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/src/lib.rs
neural/inf/rs/cozo/cozo-lib-nodejs/src/lib.rs
neural/inf/rs/cozo/cozo-core/src/lib.rs
neural/inf/rs/cozo/cozo-lib-wasm/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/unimem/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/rane/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/unimem/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/acpu/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/aruminium/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/aruminium/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/rane/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/acpu/src/lib.rs

Graph