Rust performance and maintainability wins#234
Merged
timopruesse merged 15 commits intomainfrom Apr 24, 2026
Merged
Conversation
Moved the requires_sudo check above runner construction so we can move app_config into TaskRunner::new instead of cloning the whole task map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use std::mem::take to move each name out of the underlying vec instead of cloning; selections is sorted and unique so no slot is taken twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TUI redraws at ~20Hz. Previously each frame allocated a HashSet<usize> from filtered_indices just to answer contains() for every task. Since filtered_indices is already sorted and unique, iterate it directly and look up each task by index. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For bash/zsh, build_shell_command produces a script string that we already hold in memory; previously we wrote it to a temp file only to read it back and pipe to stdin. Now pipe the string directly. PowerShell still needs a temp file because we invoke it with -File, but that path is isolated. Split execute_script into stdin/file variants sharing a common wait_with_output helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every caller was asking len/is_empty/first — none actually needed the intermediate Vec<&str>. Returning impl Iterator<Item = &str> lets callers drive what they need and removes the allocation on every description render and every requires_sudo scan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous O(tasks * selected) scan becomes O(tasks + selected) by building a HashSet once up front. Not meaningful for small configs but free to fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When no task has a depends_on entry, the sort is a no-op but the old return type forced a Vec::to_vec() clone. Switch to Cow<'a, [String]> so the common no-deps path hands back a borrow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small wins: - setup.rs: keep the resolved PathBuf around so canonicalize can use it directly instead of re-parsing a stringified form; URLs skip the allocation entirely via Cow::Borrowed. - shell.rs: use Cow::into_owned instead of to_string_lossy().to_string(); same cost, clearer intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The canonicalize-parent-or-cwd idiom was duplicated in main.rs (twice) and setup.rs. Centralize it in config::resolve_config_dir, parameterized on a fallback path so setup.rs can pass the parent config_dir while main.rs passes cwd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.rs and clone.rs both spawned two tokio tasks to read child stdout and stderr line-by-line. Extract the shared machinery into a utility, parameterized on whether stderr should be labelled (shell commands) or passed through (git progress). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
copy.rs and symlink.rs both duplicated a WalkDir + strip_prefix + should_ignore + target.join loop across their install and uninstall paths. Centralize that pattern in utils::path::walk_relative and let callers focus on the per-entry action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Display impl on CommandEntry and each executor's description() expressed the same idea twice — and in subtly different ways (sudo prefix shown by executors but not Display). Impl Display on each args struct as the single source of truth, have CommandEntry::Display delegate, and have each executor's description() return args.to_string(). The `list` command output now shows the (sudo) marker on copy/symlink entries, which is incidental but actually more informative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use let-else to handle the empty-string case, eliminating both the explicit is_empty check and the chars.next().unwrap() that followed it. Same behavior, fewer footguns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously create_executor took &CommandEntry and each arm did args.clone(). Move the clone up to the single caller (iter().cloned()) so ownership transfer is explicit and the match arms can move args directly into the executor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
14 commits — one per item — applying the Apollo Rust best practices to tighten performance hot paths and reduce duplicated code. Each commit is self-contained and passes
cargo test+cargo clippy -- -D warnings.Performance (P1–P8)
AppConfigwhen constructingTaskRunner; move it in after therequires_sudocheckstd::mem::takeinstead of cloning selected task names inselect_tasksfiltered_indicesdirectly in the TUI task-list render (removes oneHashSet<usize>alloc per frame at ~20 Hz)-File) is the only remaining temp-file pathRunArgs::all_command_stringsnow returnsimpl Iterator<Item = &str>; noVec<&str>alloc on every description render orrequires_sudoscanHashSetlookup for selected task names inrequires_sudotopological_sortreturnsCow<'a, [String]>; the no-deps fast path borrows instead of cloning every nameto_string_lossyusage:into_ownedinstead of.to_string(), and reuse the resolvedPathBufinsetup.rsso canonicalize doesn't re-parse a stringMaintainability (M1–M6)
config::resolve_config_dirhelper replaces three copies of the canonicalize-parent-or-fallback idiomutils::process::stream_and_waitreplaces the duplicated child-stdout/stderr plumbing inrun.rsandclone.rs; parameterized on whether stderr should carry the[stderr]tag (shell) or pass through (git progress)utils::path::walk_relativereplaces the WalkDir + strip_prefix + ignore loop duplicated acrosscopy.rsandsymlink.rs(install + uninstall)Displayimpls on each args struct as single source of truth;CommandEntry::Displayand each executor'sdescription()now delegate. Side effect:listcommand output now shows the(sudo)marker, which is more informativevalidate_env_keywith let-else so the empty-string check and.next().unwrap()collapse into one branchcreate_executortakesCommandEntryby value; ownership transfer is explicit at the single caller (iter().cloned()) and the match arms move args into the executor without an internal cloneOne trailing
style: apply rustfmtcommit covers line-length reflows from the new helpers.Test plan
cargo test— all 31 tests passcargo clippy --all-targets --all-features -- -D warnings— cleancargo fmt --check— cleaninstall/update/uninstallon a real config[stderr]noise🤖 Generated with Claude Code