diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml new file mode 100644 index 0000000..b547c34 --- /dev/null +++ b/.forgejo/workflows/check.yml @@ -0,0 +1,24 @@ +name: check + +# Fast-fail lint/test on short-lived work branches, before it ever reaches +# main and triggers a dev-track release build. +on: + push: + branches: ['feature/**', 'fix/**'] + +jobs: + check: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: clippy + run: cd src && bash ci/build.sh cargo clippy --workspace --all-targets --locked -- -D warnings + + - name: test + run: cd src && bash ci/build.sh cargo test --workspace --locked diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 1fbbcd9..3087715 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -19,7 +19,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: compute dev version run: | diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index b44f054..fd6ba9a 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -21,7 +21,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index f584b3a..b3b2164 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -17,7 +17,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | diff --git a/ci/bread-ecosystem.rev b/ci/bread-ecosystem.rev new file mode 100644 index 0000000..474f1fd --- /dev/null +++ b/ci/bread-ecosystem.rev @@ -0,0 +1 @@ +620c5a1317a6b57276eabca961facdb78bf510db diff --git a/ci/build.sh b/ci/build.sh new file mode 100755 index 0000000..7582664 --- /dev/null +++ b/ci/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Delegates to bread-ecosystem's shared CI build image/script, pinned to +# the commit in ci/bread-ecosystem.rev — not `main`. bread-ecosystem's CI +# files now affect every product's release pipeline, so bumping the pin +# is a deliberate act instead of silent drift. +# +# Usage: ci/build.sh cargo build --release --locked +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REV="$(cat "${ROOT}/ci/bread-ecosystem.rev")" + +CACHE_DIR="/tmp/bread-ecosystem-ci-${REV}" +if [ ! -d "$CACHE_DIR" ]; then + rm -rf /tmp/bread-ecosystem-ci-* + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "$CACHE_DIR" + git -C "$CACHE_DIR" checkout --quiet "$REV" +fi + +bash "${CACHE_DIR}/ci/build.sh" breadmon "$ROOT" "$@" diff --git a/src/layout.rs b/src/layout.rs index c19923a..0b6e835 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -103,7 +103,7 @@ pub fn snap_position( } /// Move the selected monitor by (dx, dy) pixels, then snap. -pub fn move_selected(state: &LayoutState, monitors: &mut Vec, dx: i32, dy: i32) { +pub fn move_selected(state: &LayoutState, monitors: &mut [Monitor], dx: i32, dy: i32) { let idx = state.selected; if idx >= monitors.len() { return; @@ -116,7 +116,7 @@ pub fn move_selected(state: &LayoutState, monitors: &mut Vec, dx: i32, } /// Place monitors in a left-to-right row with no gaps. -pub fn auto_arrange(monitors: &mut Vec) { +pub fn auto_arrange(monitors: &mut [Monitor]) { let mut cursor = 0i32; for m in monitors.iter_mut() { m.x = cursor; diff --git a/src/mirror.rs b/src/mirror.rs index 6485a58..2e46367 100644 --- a/src/mirror.rs +++ b/src/mirror.rs @@ -62,11 +62,10 @@ pub fn find_mirror_modes(source: &Monitor, target: &Monitor) -> Option Profile { /// Apply a profile's settings onto a list of live monitors (matched by name). /// Monitors not in the profile are left unchanged. -pub fn apply_to_monitors(profile: &Profile, monitors: &mut Vec) { +pub fn apply_to_monitors(profile: &Profile, monitors: &mut [Monitor]) { for pm in &profile.monitors { if let Some(m) = monitors.iter_mut().find(|m| m.name == pm.name) { if let Some(mode) = Mode::parse(&format!("{}Hz", pm.mode)) { diff --git a/src/ui/config_view.rs b/src/ui/config_view.rs index 4ec4026..d3a3352 100644 --- a/src/ui/config_view.rs +++ b/src/ui/config_view.rs @@ -242,14 +242,14 @@ fn handle_field_key(event: KeyEvent, state: &mut AppState) { state.dirty = true; } } - KeyCode::Char('l') | KeyCode::Right => { - if state.config.res_idx + 1 < state.config.resolutions.len() { - state.config.res_idx += 1; - let m = &state.monitors[idx]; - state.config.update_refreshes(m); - sync_mode_to_monitor(state, idx); - state.dirty = true; - } + KeyCode::Char('l') | KeyCode::Right + if state.config.res_idx + 1 < state.config.resolutions.len() => + { + state.config.res_idx += 1; + let m = &state.monitors[idx]; + state.config.update_refreshes(m); + sync_mode_to_monitor(state, idx); + state.dirty = true; } _ => {} }, @@ -261,12 +261,12 @@ fn handle_field_key(event: KeyEvent, state: &mut AppState) { state.dirty = true; } } - KeyCode::Char('l') | KeyCode::Right => { - if state.config.refresh_idx + 1 < state.config.refreshes.len() { - state.config.refresh_idx += 1; - sync_mode_to_monitor(state, idx); - state.dirty = true; - } + KeyCode::Char('l') | KeyCode::Right + if state.config.refresh_idx + 1 < state.config.refreshes.len() => + { + state.config.refresh_idx += 1; + sync_mode_to_monitor(state, idx); + state.dirty = true; } _ => {} }, @@ -335,12 +335,12 @@ fn handle_field_key(event: KeyEvent, state: &mut AppState) { state.dirty = true; } } - KeyCode::Char('l') | KeyCode::Right => { - if state.config.mirror_idx + 1 < state.config.mirror_options.len() { - state.config.mirror_idx += 1; - sync_mirror_to_monitor(state, idx); - state.dirty = true; - } + KeyCode::Char('l') | KeyCode::Right + if state.config.mirror_idx + 1 < state.config.mirror_options.len() => + { + state.config.mirror_idx += 1; + sync_mirror_to_monitor(state, idx); + state.dirty = true; } _ => {} }, diff --git a/src/ui/layout_view.rs b/src/ui/layout_view.rs index ffa7131..c64d1e3 100644 --- a/src/ui/layout_view.rs +++ b/src/ui/layout_view.rs @@ -259,9 +259,9 @@ pub fn canvas_area(terminal_size: (u16, u16)) -> Rect { } fn in_canvas(col: u16, row: u16, canvas: Rect) -> bool { - col >= canvas.x + 1 + col > canvas.x && col < canvas.x + canvas.width.saturating_sub(1) - && row >= canvas.y + 1 + && row > canvas.y && row < canvas.y + canvas.height.saturating_sub(1) } diff --git a/src/ui/mirror_view.rs b/src/ui/mirror_view.rs index b593dd1..57f0d9d 100644 --- a/src/ui/mirror_view.rs +++ b/src/ui/mirror_view.rs @@ -210,33 +210,33 @@ pub fn handle_mouse(event: MouseEvent, state: &mut AppState) { } } } - r if r >= 7 => { + r if r >= 7 // Result panel: Apply is on the line with buttons. // Rough column check: col < 20 = Apply, col >= 20 = Cancel - if state.mirror.result.is_some() { - let col = event.column; - if col < 20 { - // Activate Apply - state.mirror.focused = FIELDS.iter().position(|&f| f == MirrorField::Apply).unwrap_or(3); - if let Some(result) = state.mirror.result.clone() { - state.push_undo(); - let src_name = state.monitors[state.mirror.source_idx].name.clone(); - let tgt_idx = state.mirror.target_idx; - state.monitors[tgt_idx].active_mode = result.mirror_mode.clone(); - state.monitors[tgt_idx].mirror_of = Some(src_name.clone()); - state.dirty = true; - state.mirror.result = None; - state.mirror.focused = 0; - state.set_status( - format!("Mirror set: {} → {} at {}", src_name, state.monitors[tgt_idx].name, result.mirror_mode), - crate::ui::StatusLevel::Success, - ); - } - } else { - // Cancel + && state.mirror.result.is_some() => + { + let col = event.column; + if col < 20 { + // Activate Apply + state.mirror.focused = FIELDS.iter().position(|&f| f == MirrorField::Apply).unwrap_or(3); + if let Some(result) = state.mirror.result.clone() { + state.push_undo(); + let src_name = state.monitors[state.mirror.source_idx].name.clone(); + let tgt_idx = state.mirror.target_idx; + state.monitors[tgt_idx].active_mode = result.mirror_mode.clone(); + state.monitors[tgt_idx].mirror_of = Some(src_name.clone()); + state.dirty = true; state.mirror.result = None; state.mirror.focused = 0; + state.set_status( + format!("Mirror set: {} → {} at {}", src_name, state.monitors[tgt_idx].name, result.mirror_mode), + crate::ui::StatusLevel::Success, + ); } + } else { + // Cancel + state.mirror.result = None; + state.mirror.focused = 0; } } _ => {}