From 57188c3bad6f51df876887ead31615a0eed34dbc Mon Sep 17 00:00:00 2001 From: Leszek Date: Fri, 31 Jul 2026 13:30:48 +0200 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20debt=20=E2=80=94=20app=20types,=20?= =?UTF-8?q?keymap,=20CI=20timeouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/opencode.yml | 6 +----- .github/workflows/release.yml | 7 +++++++ .github/workflows/rust.yml | 9 +++++++++ Cargo.toml | 8 ++++++-- src/app/keymap.rs | 22 ++++++++++++++++++++++ src/app/types/app_state.rs | 29 +---------------------------- src/app/types/mod.rs | 6 +++--- src/app/types/modes.rs | 5 +++-- src/app/user_menu.rs | 22 +++++++++++++++------- src/lib.rs | 11 ++--------- 10 files changed, 69 insertions(+), 56 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index a7abd27..68b0f8c 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -1,17 +1,13 @@ name: opencode on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] pull_request: types: [opened, synchronize, reopened, ready_for_review] jobs: opencode-review: - if: github.event_name == 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read pull-requests: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e47f18..636acba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,10 +8,15 @@ on: env: CARGO_TERM_COLOR: always +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: build: name: ${{ matrix.target }} runs-on: ${{ matrix.os }} + timeout-minutes: 30 permissions: contents: read strategy: @@ -46,6 +51,7 @@ jobs: env: TARGET: ${{ matrix.target }} run: cargo build --release --locked --target "$TARGET" + shell: bash - name: Package env: @@ -72,6 +78,7 @@ jobs: name: GitHub Release needs: build runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: write steps: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 99376c5..d42fc48 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,9 +7,14 @@ on: env: CARGO_TERM_COLOR: always +concurrency: + group: rust-${{ github.ref }} + cancel-in-progress: true + jobs: rust: runs-on: ${{ matrix.os }} + timeout-minutes: 30 permissions: contents: read @@ -33,12 +38,16 @@ jobs: - name: Check formatting run: cargo fmt --check + shell: bash - name: Clippy run: cargo clippy --locked --all-targets -- -D warnings + shell: bash - name: Test run: cargo test --locked + shell: bash - name: Build release run: cargo build --release --locked + shell: bash diff --git a/Cargo.toml b/Cargo.toml index f33a286..89594fe 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "librecommander" version = "0.1.1" edition = "2024" -rust-version = "1.95" +rust-version = "1.95" # edition 2024 + inline let-chains used throughout description = "Modern dual-panel MC for Norton/MC muscle memory in one offline Rust binary — no async runtime, forbid(unsafe), zip-safe archives" license = "MIT" readme = "README.md" @@ -10,7 +10,7 @@ repository = "https://github.com/leszek3737/LibreCommander" homepage = "https://github.com/leszek3737/LibreCommander" keywords = ["tui", "file-manager", "ratatui", "cli", "terminal"] categories = ["command-line-utilities", "filesystem"] -exclude = [".github/", ".serena/"] +exclude = [".github/", ".serena/", "docs/"] # crates.io package is `librecommander` (`lc` is taken). Binary + lib stay `lc`. [lib] @@ -52,12 +52,16 @@ tempfile = "3" [profile.release] lto = true codegen-units = 1 +panic = "abort" # Dev: light optimization keeps TUI interaction responsive while # preserving full debug info and fast incremental rebuilds. [profile.dev] opt-level = 1 +# FSEvents-only on macOS: kqueue is excluded deliberately. FSEvents is the +# recommended backend for recursive directory watching; kqueue requires a +# file descriptor per watched directory and is slow/large on big trees. [target.'cfg(target_os = "macos")'.dependencies] notify = { version = "8", default-features = false, features = ["macos_fsevent"] } diff --git a/src/app/keymap.rs b/src/app/keymap.rs index 61c152a..c27316d 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -660,6 +660,28 @@ mod tests { } } + /// `build_help_message` emits a section header only on mode change, so it + /// silently assumes KEYBINDINGS is pre-grouped by mode. This test fails if + /// a binding is added out of order, preventing duplicated/interspersed + /// mode headers in the help output. + #[test] + fn keybindings_are_grouped_by_mode() { + let mut seen_modes: Vec<&'static str> = Vec::new(); + for b in KEYBINDINGS { + if !seen_modes.contains(&b.mode) { + seen_modes.push(b.mode); + } else { + assert!( + seen_modes.last() == Some(&b.mode), + "Binding for mode {:?} ({}) appears after a different mode — \ + KEYBINDINGS must be grouped by mode", + b.mode, + b.key + ); + } + } + } + #[test] fn all_app_modes_have_keymap_or_documented_fallback() { let msg = build_help_message(); diff --git a/src/app/types/app_state.rs b/src/app/types/app_state.rs index 5de8a68..4f62960 100644 --- a/src/app/types/app_state.rs +++ b/src/app/types/app_state.rs @@ -28,9 +28,7 @@ pub struct InputState { /// Transient presentation state: status line, menus, pickers, the directory /// hotlist, user-menu data, the deferred action awaiting confirmation, and the /// viewer spinner animation. -/// -/// `Default` is implemented by hand because [`MenuSource`] has no `Default`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct UiState { pub status_message: Option, pub menu_selected: usize, @@ -65,31 +63,6 @@ pub struct UiState { pub viewer_spinner_last_tick: Option, } -impl Default for UiState { - fn default() -> Self { - Self { - status_message: None, - menu_selected: 0, - menu_item_selected: 0, - picker_selected: 0, - user_menu_entries: Vec::new(), - user_menu_source: MenuSource::Global, - cached_hotlist_strings: Vec::new(), - cached_user_menu_strings: Vec::new(), - cached_history_strings: Vec::new(), - pending_menu_command: None, - pending_hotlist_delete: None, - pending_archive_list: None, - pending_tree_build: None, - menu_restore_panel: None, - directory_hotlist: Vec::new(), - pending_action: None, - viewer_spinner_frame: 0, - viewer_spinner_last_tick: None, - } - } -} - /// Directory-tree browser view state (the `DirectoryTree` mode). #[derive(Debug, Clone, PartialEq, Default)] pub struct TreeState { diff --git a/src/app/types/mod.rs b/src/app/types/mod.rs index c1c2574..585c2b7 100644 --- a/src/app/types/mod.rs +++ b/src/app/types/mod.rs @@ -15,8 +15,7 @@ mod tests; // --- Re-exports ----------------------------------------------------------- // Grouped by shape: data types (structs/enums) first, then free utility -// functions. WS-E debt: this is a flat ~30-symbol facade; a later pass could -// split it into per-concern submodule facades if the surface keeps growing. +// functions. // State containers & aggregates (AppState plus its extracted sub-states). pub use app_state::{AppState, InputState, InteractionState, TreeState, UiState}; @@ -51,6 +50,7 @@ pub use text_input::TextInput; // callers use `FileEntry::display_permissions_raw` directly. pub use file_entry::{compute_category, format_size, format_time}; -// `sanitize_for_display` is only needed by test helpers. +// `sanitize_for_display` is used by test helpers and the production display +// path (e.g. non-UTF-8 filename rendering). #[cfg(test)] pub(crate) use file_entry::sanitize_for_display; diff --git a/src/app/types/modes.rs b/src/app/types/modes.rs index dcbd9b4..24c361c 100644 --- a/src/app/types/modes.rs +++ b/src/app/types/modes.rs @@ -133,8 +133,9 @@ mod tests { // Guards the hand-maintained `CompareMode::ALL` against drift. The inner // `match` is exhaustive, so adding a new `CompareMode` variant breaks // compilation here until the author updates both the match and `ALL`; the - // length assertion then catches a variant that was added to the enum but - // forgotten in the array (or vice versa). + // length assertion catches a variant added to the enum but forgotten in + // the array (or vice versa). (`std::mem::variant_count` would be ideal but + // is still unstable as of Rust 1.95.) #[test] fn compare_mode_all_is_exhaustive_and_in_order() { for (i, variant) in CompareMode::ALL.iter().enumerate() { diff --git a/src/app/user_menu.rs b/src/app/user_menu.rs index d84a638..c05d07d 100755 --- a/src/app/user_menu.rs +++ b/src/app/user_menu.rs @@ -424,9 +424,10 @@ fn is_regular_file(path: &Path) -> bool { fs::symlink_metadata(path).is_ok_and(|m| !m.is_symlink() && m.is_file()) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum MenuSource { Local, + #[default] Global, } @@ -458,12 +459,19 @@ pub struct LoadedMenu { } pub fn load_menu_with_warnings(panel_dir: &Path, filename: &str) -> Result { - let (path, source) = locate_menu_file(panel_dir).ok_or_else(|| { - format!( - "No user menu file found (searched: {}/.mc.menu, ~/.config/lc/menu)", - panel_dir.display() - ) - })?; + let global = paths::user_menu_path(); + let (path, source) = + locate_menu_file_with_global(panel_dir, global.as_deref()).ok_or_else(|| { + let global_display = global + .as_deref() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "(no global menu path configured)".to_string()); + format!( + "No user menu file found (searched: {}/.mc.menu, {})", + panel_dir.display(), + global_display + ) + })?; let mut content = String::new(); // Read one byte past the limit so an oversize file is a hard error rather // than a silently truncated (and possibly mis-parsed) prefix. diff --git a/src/lib.rs b/src/lib.rs index bbedd59..82cd2ad 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,8 @@ //! //! Built with Ratatui + Crossterm. Single binary, no runtime dependencies. -// Public API surface. -// -// The `lc` binary (`main.rs`, `render*`, `input/*`, integration tests under -// `src/tests/`) consumes this library as an external crate and always reaches -// items through their module path (e.g. `lc::app::types::AppState`, -// `lc::ops::compare::compare_entries`). The crate-root re-exports that used to -// live here were never referenced via `lc::` by any consumer, so they -// were a redundant, inconsistent second surface. The intended public API is the -// set of top-level modules below; navigate into them for concrete items. +// The `lc` binary and integration tests consume this library as an external +// crate, reaching items through their module path (e.g. `lc::app::types`). pub mod app; pub mod fs; pub mod menu; From d371a068f2c36431676a1274ebdca390002d74ac Mon Sep 17 00:00:00 2001 From: Leszek Date: Fri, 31 Jul 2026 14:20:52 +0200 Subject: [PATCH 2/2] ci(release): cancel stale tag builds in progress concurrency cancel-in-progress: false let an obsolete run finish and publish stale binaries when a tag is force-moved. flip to true so the newer tag push cancels the older run. Fixes Greptile P1 on PR #109. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 636acba..d1c71fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ env: concurrency: group: release-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: build: