Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ name: build
permissions:
contents: read

# Six jobs, five of which gate a merge:
# Seven jobs, six of which gate a merge:
# Windows .exe — the real release binary, uploaded as an artifact.
# Tests — `cargo test --workspace`. Blocking: a red suite is a red PR.
# Clippy — `cargo clippy --workspace --all-targets --locked -- -D warnings`. Blocking.
# Audit — cargo-deny over the committed lockfile. Blocking.
# Tour — deterministic interactive and AI documentation. Blocking.
# Fmt — `cargo fmt --all -- --check`. Blocking.
Expand All @@ -15,7 +16,7 @@ permissions:
# .dmg ships, unevenly: arm64 is compiled, the Intel slice only
# type-checked. Read its log; it never blocks.
#
# THE LOCKFILE CONTRACT, which the three compiling jobs implement in the same three steps.
# THE LOCKFILE CONTRACT, which the four compiling jobs implement in the same three steps.
# `Cargo.lock` is committed, so third-party versions move only in a deliberate commit. Each job
# then, in this order and no other:
# 1. verifies the committed lock still agrees with the manifests (`cargo fetch --locked`),
Expand Down Expand Up @@ -55,9 +56,9 @@ on:
workflow_dispatch:
# A freeze makes exactly one thing worse: an advisory can be published against a pinned version
# long after the last PR, and nothing would notice. This weekly run exists for the audit job
# alone — the three compiling jobs skip it, so it costs a couple of runner-minutes and writes
# alone — the four compiling jobs skip it, so it costs a couple of runner-minutes and writes
# no cache. Side effect worth knowing when you read a commit's checks: each cron run attaches
# three `Skipped` entries to main's HEAD, so a commit whose gates really did pass can later
# four `Skipped` entries to main's HEAD, so a commit whose gates really did pass can later
# display them as skipped.
schedule:
- cron: "0 6 * * 1"
Expand All @@ -77,7 +78,7 @@ env:

# Every job restores the cargo cache but only `main` writes it (`save-if` below).
# A cache saved from a branch is readable only by that branch, so on any run whose
# manifest hash is new it is written once and never read again. Three jobs at ~1 GiB
# manifest hash is new it is written once and never read again. Four jobs at ~1 GiB
# a piece fill the repository's 10 GB Actions quota fast, and once over it GitHub
# evicts by least-recent-use — which takes the live caches, not the dead ones.
jobs:
Expand Down Expand Up @@ -143,6 +144,32 @@ jobs:
- name: Run test suite
run: cargo test --workspace --no-fail-fast --target x86_64-pc-windows-msvc

clippy:
name: Clippy (x86_64-msvc)
runs-on: windows-latest
# Compiles the workspace, so the weekly audit cron skips it like the other compiling jobs.
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.97.1
with:
targets: x86_64-pc-windows-msvc
components: clippy
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# The lockfile contract — see the header. `--locked` on the clippy step then refuses any
# further rewrite after the MoonUI refresh has been checked.
- name: Verify the committed lockfile agrees with the manifests
run: cargo fetch --locked
- name: Refresh MoonUI only
run: cargo update -p moon-gpui -p moon-gpui-platform -p moon-ui
- name: Assert the refresh moved MoonUI and nothing else
shell: bash
run: bash .github/scripts/assert-only-moonui-moved.sh
- name: Lint the workspace
run: cargo clippy --workspace --all-targets --locked -- -D warnings

macos-probe:
name: macOS probe (arm64 build, x86_64 check) — diagnostic
runs-on: macos-14
Expand Down
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ Three kinds of test, three homes. The toolchain dictates this, not taste:
she posts the mark together with a comment. A listed author turns a wrong or stuck mark green by
writing `/override` in the pull request. CI still only reports: nothing but you reading it stops
a red merge. Branch from fresh `main`, open a PR, squash-merge — history stays linear.
- **CI runs `fmt`, not `clippy`.** Run `cargo clippy` yourself before pushing. The tree **is**
- **CI runs `fmt` and `clippy`.** The tree **is**
rustfmt-clean: `cargo fmt --all` is the correct command, `rustfmt.toml`
(`style_edition = "2024"`) is the authority, and CI enforces it via the `Fmt` job — which does
not need the optional `private/uidoc` overlay to pass; `cargo fmt` must (and does) work without
it. Blame history across the tree-wide reformat is preserved by `.git-blame-ignore-revs` —
enable it locally with `git config blame.ignoreRevsFile .git-blame-ignore-revs`.
- Five CI gates, all on every PR and all meant to be green before you merge: the Windows
- Six CI gates, all on every PR and all meant to be green before you merge: the Windows
`.exe` job (~15 min), `Tests (x86_64-msvc)` running `cargo test --workspace`,
`Clippy (x86_64-msvc)` running `cargo clippy --workspace --all-targets --locked -- -D warnings`,
`Dependency audit (cargo-deny)`, `Tour` building the knowledge site, and `Fmt` running
`cargo fmt --all -- --check`. They run in parallel. The macOS job is diagnostic
(`continue-on-error`) — read its log, but it does not block. "Gate" is a convention here, not
Expand Down Expand Up @@ -185,6 +186,8 @@ unnoticed.
make build | run | release | check | fmt
```

- `cargo clippy --workspace --all-targets -- -D warnings` must pass before a PR.

- Windows needs **VS 2022 Build Tools** (`vcvars64`). Resolve it with
`vswhere -latest -find '**\vcvars64.bat'` — machines here carry both BuildTools and Community.
- **Never wrap a build in `2>&1`.** `vcvars64.bat` writes to stderr on a healthy run, and
Expand Down
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ opt-level = 2
[patch.crates-io]
async-process = { git = "https://github.com/zed-industries/async-process.git", rev = "0b6d6713570af61806e1e5cb40e0f757cb93fd9d" }
async-task = { git = "https://github.com/smol-rs/async-task.git", rev = "b4486cd71e4e94fbda54ce6302444de14f4d190e" }

# Clippy is a merge gate (`cargo clippy --workspace --all-targets -- -D warnings`).
# These three stay allowed: fixing them is a refactor or would rewrite deliberate
# numeric comparisons, not a bugfix. Every crate opts in with `[lints] workspace = true`.
[workspace.lints.clippy]
# Constructors and draw helpers take one argument per field on purpose.
too_many_arguments = "allow"
# Nested results and iterators name the data; a typedef would only rename them.
type_complexity = "allow"
# `!(a < b)` is deliberate NaN handling. Never rewrite it to `a >= b`.
neg_cmp_op_on_partial_ord = "allow"
3 changes: 3 additions & 0 deletions crates/moon-chart/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ moon-core = { path = "../moon-core" }
# Only for #[derive(Pod/Zeroable)] on instance types and ChartUniform. wgpu is no longer used:
# the wgpu chart engine was removed with the egui binary; own-pass DX11 (chartdx) does the rendering.
bytemuck = { version = "1", features = ["derive"] }

[lints]
workspace = true
2 changes: 1 addition & 1 deletion crates/moon-chart/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl PaneSource {
PaneSource::Manual => None,
// Lazy on purpose: the eager form still computes the infinity this keeps out.
PaneSource::AddToChart { born_ms, ttl_ms } => {
ttl_ms.is_finite().then(|| born_ms + ttl_ms)
ttl_ms.is_finite().then_some(born_ms + ttl_ms)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-chart/src/order_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,9 +729,9 @@ pub fn build_order_geometry(
// the per-tab flag would leave a row of dots marking steps of a staircase that is no
// longer there.
if st.knots && !has_server_trace && show_move_history {
for i in 1..n {
for point in points.iter().skip(1) {
markers.push(MarkerInstance::at_price(
to_rel(points[i].0),
to_rel(point.0),
cur_p,
st.knot_size * highlight_marker_mul,
st.marker_thickness * highlight_thickness_mul,
Expand Down
8 changes: 5 additions & 3 deletions crates/moon-chart/src/trade_marks/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@ fn normalize_folds_the_band_fields_onto_the_one_switch() {
use moon_core::market::candles::{
VOLUME_STYLE_HILLS, VOLUME_STYLE_LEGACY_BARS, VOLUME_STYLE_OFF,
};
let mut cfg = ChartGraphicsCfg::default();
cfg.candle_volume_style = VOLUME_STYLE_LEGACY_BARS;
cfg.candle_volume_sides = false;
let mut cfg = ChartGraphicsCfg {
candle_volume_style: VOLUME_STYLE_LEGACY_BARS,
candle_volume_sides: false,
..ChartGraphicsCfg::default()
};
let out = normalize_chart_graphics(cfg);
assert_eq!(out.candle_volume_style, VOLUME_STYLE_HILLS);
assert!(out.candle_volume_sides);
Expand Down
2 changes: 1 addition & 1 deletion crates/moon-chart/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ impl ChartView {
let area_delta = area_w - self.last_phase_area_w;
// Every shrink matters for the six-hour lower bound; sub-pixel growth can safely retain
// the previous scale because it only increases the visible history.
let area_changed = area_delta < 0.0 || area_delta >= 0.5;
let area_changed = !(0.0..0.5).contains(&area_delta);
let present_changed = (present_hz - self.last_phase_present_hz).abs() >= 0.5;
let phase_changed = area_changed || present_changed;
if phase_changed || self.x_init_pending {
Expand Down
3 changes: 3 additions & 0 deletions crates/moon-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,6 @@ windows-sys = { version = "0.61", features = [
# Descriptor count on Exhausted log lines: `fcntl(F_GETFD)` is a kernel query
# that allocates no descriptor, so the probe still answers at the open-file cliff.
libc = "0.2"

[lints]
workspace = true
2 changes: 1 addition & 1 deletion crates/moon-core/examples/db_read_timing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ fn dump_all(period_name: &str, from: i64, to: i64, pool: &Pool) {
let monitor = moon_core::db::analytics::profit_monitor(&q(ProfitMetric::Quote));
println!("{period_name}|profit_monitor|{monitor:?}");

if let Some(conn) = moon_core::db::open_reader().ok() {
if let Ok(conn) = moon_core::db::open_reader() {
if let Ok(snap) = moon_core::db::read_snapshot(&conn) {
let filter = ReportFilter::default();
if let Ok(cores) = moon_core::db::distinct_cores(&snap) {
Expand Down
2 changes: 1 addition & 1 deletion crates/moon-core/src/backup_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ impl<'a> SnapshotStore<'a> {
return false;
};
let mut found = Vec::<OsString>::new();
while let Some(entry) = entries.next() {
for entry in entries.by_ref() {
let Ok(entry) = entry else {
return false;
};
Expand Down
2 changes: 1 addition & 1 deletion crates/moon-core/src/backups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ fn launch_settings_job() {
"settings",
&SETTINGS_JOB_ACTIVE,
false,
|now_ms| crate::config::backup_due_at(now_ms),
crate::config::backup_due_at,
);
}

Expand Down
2 changes: 2 additions & 0 deletions crates/moon-core/src/config/detect_view/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use super::*;
/// detects_view.toml and for Copy/Paste): active size, per-size w/h/chart/rail,
/// and every flag in every slot.
#[test]
// Nested slot writes cannot move into one literal; each field stays beside the round-trip it proves.
#[allow(clippy::field_reassign_with_default)]
fn detect_view_roundtrip_preserves_every_field() {
let mut cfg = DetectViewCfg::default();
cfg.size = DETECT_SIZE_LARGE;
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-core/src/config/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2203,7 +2203,7 @@ impl WindowLayout {
|_| false,
);
let moved = match self.kind_defaults_mut(kind) {
Some(d) => std::mem::replace(&mut d.candle_view, Some(value)) != Some(value),
Some(d) => d.candle_view.replace(value) != Some(value),
None => std::mem::replace(&mut self.candle_view, value) != value,
};
split || moved
Expand All @@ -2221,7 +2221,7 @@ impl WindowLayout {
|_| false,
);
let moved = match self.kind_defaults_mut(kind) {
Some(d) => std::mem::replace(&mut d.chart_graphics, Some(value)) != Some(value),
Some(d) => d.chart_graphics.replace(value) != Some(value),
None => std::mem::replace(&mut self.chart_graphics, value) != value,
};
split || moved
Expand Down
2 changes: 1 addition & 1 deletion crates/moon-core/src/config/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn config_from_legacy_enc(plain: &[u8], uid_floor: Option<u64>) -> anyhow::Resul
groups: Vec<GroupConfig>,
}

let old: Old = toml::from_str(std::str::from_utf8(&plain)?)?;
let old: Old = toml::from_str(std::str::from_utf8(plain)?)?;
let servers = old
.servers
.into_iter()
Expand Down
4 changes: 3 additions & 1 deletion crates/moon-core/src/config/moonbot_import/schema_v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ pub fn parse_payload(payload: &[u8]) -> Result<MoonBotConfig, ImportError> {
}
match kind {
// Signals/Trading/Visual must be present, but their contents are skipped.
1 | 2 | 3 => {}
1..=3 => {}
4 => theme = Some(parse_theme(sub)?),
5 => ini = Some(parse_ini(sub)?),
6 => ui = Some(parse_ui(sub)?),
Expand Down Expand Up @@ -409,6 +409,8 @@ pub(super) mod build {
}

/// Builds a UI v3 body with specified hotkey values and fixed values for everything else.
// Each `push` is one labelled wire byte. Folding them into a literal would drop those names.
#[allow(clippy::vec_init_then_push)]
pub fn ui_body(
order_sizes: [f64; 6],
order_size_keys: [u16; 6],
Expand Down
5 changes: 3 additions & 2 deletions crates/moon-core/src/config/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
//! and settings:
//! - macOS: `~/Library/Application Support/com.moonbot.moonterminal/`
//! - Linux: `~/.config/com.moonbot.moonterminal/`
//! `servers.enc` is encrypted with its own file key, wrapped by one slot per machine (each
//! machine's key lives in its OS keyring) plus an optional password slot; see `crypto/`.
//!
//! `servers.enc` is encrypted with its own file key, wrapped by one slot per machine (each
//! machine's key lives in its OS keyring) plus an optional password slot; see `crypto/`.
//!
//! All paths are based on `data_dir()`; on Windows, `data_dir() == exe_dir()`.
//!
Expand Down
8 changes: 5 additions & 3 deletions crates/moon-core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ fn a_malformed_core_list_is_refused() {
/// a pinned layout was impossible.
#[test]
fn a_plaintext_config_keeps_the_settings_it_was_given() {
let mut settings = super::schema::SettingsFile::default();
settings.charts_split_by_core = false;
settings.chart_stack_height = 321;
let settings = super::schema::SettingsFile {
charts_split_by_core: false,
chart_stack_height: 321,
..super::schema::SettingsFile::default()
};

let config = AppConfig::build_plaintext_config(
None,
Expand Down
20 changes: 14 additions & 6 deletions crates/moon-core/src/config/theme/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ fn share_roundtrip() {
/// Old flat theme.toml becomes dark while the caller's light theme remains current.
#[test]
fn share_flat_legacy_goes_dark() {
let mut flat = ChartTheme::default();
flat.bg = [10, 20, 30];
let flat = ChartTheme {
bg: [10, 20, 30],
..ChartTheme::default()
};
let text = toml::to_string_pretty(&flat).unwrap();
let mut current = ChartThemeSet::default();
current.light.bg = [200, 200, 200];
Expand Down Expand Up @@ -128,6 +130,8 @@ fn current_revision_keeps_a_deliberately_reselected_retired_value() {
/// `theme.rs:ChartThemeSet::retire_old_defaults` must stamp a revision-zero custom file once;
/// otherwise every launch re-examines and rewrites a palette with no old colours left to move.
#[test]
// Each palette channel is named on its own line so a round-trip mismatch points at that channel.
#[allow(clippy::field_reassign_with_default)]
fn revision_zero_customised_palette_is_stamped_once() {
let mut set = ChartThemeSet::default();
set.palette_rev = absent_palette_rev();
Expand Down Expand Up @@ -331,8 +335,10 @@ fn sharing_future_dark_table_with_current_light_uses_the_lower_generation() {
/// taking the higher live generation would prevent a later build from migrating the pasted side.
#[test]
fn sharing_older_dark_table_with_newer_light_uses_the_lower_generation() {
let mut current = ChartThemeSet::default();
current.palette_rev = CURRENT_PALETTE_REV + 2;
let current = ChartThemeSet {
palette_rev: CURRENT_PALETTE_REV + 2,
..ChartThemeSet::default()
};
let text = format!(
"palette_rev = {}\n\n[dark]\nbg = [11, 22, 33]\n",
CURRENT_PALETTE_REV + 1
Expand Down Expand Up @@ -372,8 +378,10 @@ fn sharing_retires_both_tables_but_never_the_live_flat_light_set() {
assert_eq!(parsed, ChartThemeSet::default());

let flat_text = toml::to_string_pretty(&paired.dark).expect("flat themes serialize");
let mut current = ChartThemeSet::default();
current.light = paired.light.clone();
let current = ChartThemeSet {
light: paired.light.clone(),
..ChartThemeSet::default()
};
let live_light = current.light.clone();
let parsed = ChartThemeSet::parse_share(&flat_text, &current).expect("a flat theme parses");

Expand Down
2 changes: 1 addition & 1 deletion crates/moon-core/src/crowd/standing/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn a_row_carries_both_sides_of_the_minute() {

#[test]
fn the_loud_come_first() {
let mut rows = vec![
let mut rows = [
Standing::live("BTC", &stat(10.0, 5.0)),
Standing::live("ETH", &stat(100.0, 5.0)),
];
Expand Down
5 changes: 5 additions & 0 deletions crates/moon-core/src/data/orderbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ impl OrderBookModel {
self.raw.len()
}

/// Returns whether the book currently holds no levels.
pub fn is_empty(&self) -> bool {
self.raw.is_empty()
}

/// Returns the book's best `(bid, ask)`. If only one side is populated, its best price is
/// returned in both positions for a zero spread. An empty book or invalid best price returns
/// `None`. `raw` stores descending bids followed by ascending asks, so the first `!is_ask`
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-core/src/db/analytics/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ pub(in crate::db) fn coin_groups_from_source(
q: &Query,
src: &str,
) -> ReadResult<Vec<GroupStat>> {
let raw_src = raw_source(conn, &q)?;
groups(conn, &src, raw_src.as_deref(), &q, false, false)
let raw_src = raw_source(conn, q)?;
groups(conn, src, raw_src.as_deref(), q, false, false)
}

/// Build the lens-neutral source used by raw-profit and average-order enrichments.
Expand Down
2 changes: 2 additions & 0 deletions crates/moon-core/src/db/analytics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ pub fn calendar_data(
}

/// Preflight result that prevents unsafe raw-money analytics from carrying scalar data.
// `Split` carries the full quote breakdown. Boxing it would touch every analytics consumer.
#[allow(clippy::large_enum_variant)]
enum ScopeDecision {
/// Scalar values share this explicit unit.
Comparable {
Expand Down
6 changes: 1 addition & 5 deletions crates/moon-core/src/db/analytics/summary_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,7 @@ impl GroupAccumulator {
self.worst = Some(self.worst.map_or(value, |worst| worst.min(value)));
}
if let Some(name) = &row.core_name {
if self
.core_name
.as_ref()
.map_or(true, |current| name > current)
{
if self.core_name.as_ref().is_none_or(|current| name > current) {
self.core_name = Some(name.clone());
}
}
Expand Down
Loading
Loading