Skip to content
Draft
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
9 changes: 6 additions & 3 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ use std::fs;
use std::path::Path;

fn main() {
#[cfg(windows)]
{
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
// Clap + the full command graph can exceed the default 1 MiB Windows
// main-thread stack during process startup. Reserve a larger stack for
// the CLI binary so `rtk.exe --version`, `--help`, and hook entry
// points start reliably without requiring ad-hoc RUSTFLAGS.
println!("cargo:rustc-link-arg=/STACK:8388608");
let stack_arg = match std::env::var("CARGO_CFG_TARGET_ENV").as_deref() {
Ok("msvc") => "/STACK:8388608",
_ => "-Wl,--stack,8388608",
};
println!("cargo:rustc-link-arg={stack_arg}");
}

let filters_dir = Path::new("src/filters");
Expand Down
43 changes: 43 additions & 0 deletions src/cmds/system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,49 @@
- `local_llm.rs` (`rtk smart`) uses `core/filter` for heuristic file summarization
- `format_cmd.rs` is a cross-ecosystem dispatcher: auto-detects and routes to `prettier_cmd` or `ruff_cmd` (black is handled inline, not as a separate module)

## Windows / no-coreutils fallback

`ls`, `grep`, `wc`, and `tree` shell out to Unix binaries that aren't present on
a stock Windows install. Each of these now has a **native Rust execution path**
used when the underlying binary is unavailable (detected via
`core::utils::tool_exists`; `tree` also always goes native on Windows because
`tree.com` rejects the flags rtk uses):

- `ls.rs` — `run_native` synthesizes `ls -la`-style lines from `std::fs` and
reuses `compact_ls`. POSIX permission bits don't exist on Windows, so the `-l`
octal column is approximate there.
- `wc_cmd.rs` — `run_native` counts lines/words/bytes/chars in Rust and reuses
`filter_wc_output`.
- `tree.rs` — `run_native` does a recursive `std::fs` walk, pruning `NOISE_DIRS`
unless `-a`.
- `search.rs` — `native_grep` (final fallback after `rg` then `grep`) walks
files with the `ignore` crate and matches with `regex`, emitting the same
NUL-separated `path\0line:content` format the parser expects. It respects
`.gitignore` and skips hidden files, which differs slightly from
`rg --no-ignore-vcs`.

These native paths reuse the existing compression filters, so token-savings
behavior is identical to the Unix spawn path.

## PowerShell cmdlet rewrites

`powershell_cmd.rs` maps common, **non-piped** PowerShell cmdlets to their rtk
equivalents so the hook saves tokens on Windows:

| Cmdlet (and aliases) | rtk equivalent |
|---|---|
| `Get-Content` / `gc` / `type` | `rtk read` (`-TotalCount`→`--max-lines`, `-Tail`→`--tail-lines`) |
| `Get-ChildItem` / `gci` / `dir` | `rtk ls` (`-Recurse`→`rtk tree`, `-Force`→`-a`) |
| `Select-String` / `sls` | `rtk grep` (`-i` added unless `-CaseSensitive`) |

Piped/compound cmdlet invocations are intentionally left untouched to avoid
breaking PowerShell pipeline semantics.

The `rtk powershell` and `rtk pwsh` wrappers also recognize safe `-Command`
forms and dispatch those same cmdlets through RTK. Scripts, `-File`,
`-EncodedCommand`, pipelines, interpolation, and unsupported parameters run
unchanged through the requested PowerShell executable.

## Cross-command

- `format_cmd` routes to `cmds/js/prettier_cmd` and `cmds/python/ruff_cmd`
125 changes: 124 additions & 1 deletion src/cmds/system/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

use super::constants::NOISE_DIRS;
use crate::core::runner::{self, RunOptions};
use crate::core::tracking::TimedExecution;
use crate::core::truncate::{reduced, CAP_WARNINGS};
use crate::core::utils::resolved_command;
use crate::core::utils::{resolved_command, tool_exists};
use anyhow::Result;
use regex::Regex;
use std::io::IsTerminal;
Expand Down Expand Up @@ -48,6 +49,12 @@ pub fn run(args: &[String], verbose: u8) -> Result<i32> {
.map(|s| s.as_str())
.collect();

// On Windows (and any host lacking the Unix `ls` binary) fall back to a
// native Rust listing so `rtk ls` works without coreutils installed.
if !tool_exists("ls") {
return run_native(&paths, show_all, show_long, verbose);
}

let mut cmd = resolved_command("ls");
cmd.env("LC_ALL", "C");
cmd.arg("-la");
Expand Down Expand Up @@ -126,6 +133,88 @@ pub fn run(args: &[String], verbose: u8) -> Result<i32> {
)
}

/// Native `ls` implementation used when the Unix binary is unavailable
/// (e.g. on stock Windows). Synthesizes `ls -la`-style lines from the
/// filesystem and reuses [`compact_ls`] so output matches the spawn path.
///
/// Note: POSIX permission bits don't exist on Windows, so synthetic perms
/// (`drwxr-xr-x` for dirs, `-rw-r--r--` for files) are emitted; the `-l`
/// octal column is therefore approximate on Windows.
fn run_native(paths: &[&str], show_all: bool, show_long: bool, verbose: u8) -> Result<i32> {
let timer = TimedExecution::start();
let targets: Vec<&str> = if paths.is_empty() { vec!["."] } else { paths.to_vec() };

let mut raw = String::new();
let mut exit_code = 0;

for target in &targets {
match std::fs::metadata(target) {
Ok(meta) if meta.is_dir() => {
match std::fs::read_dir(target) {
Ok(entries) => {
let mut lines: Vec<String> = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !show_all && name.starts_with('.') {
continue;
}
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
lines.push(synth_ls_line(&name, is_dir, size));
}
lines.sort();
for line in lines {
raw.push_str(&line);
raw.push('\n');
}
}
Err(e) => {
eprintln!("ls: {target}: {e}");
exit_code = 2;
}
}
}
Ok(meta) => {
// A file argument: ls prints just the name.
raw.push_str(&synth_ls_line(target, false, meta.len()));
raw.push('\n');
}
Err(e) => {
eprintln!("ls: {target}: {e}");
exit_code = 2;
}
}
}

let (entries, summary, _parsed) = compact_ls(&raw, show_all, show_long);
let is_tty = std::io::stdout().is_terminal();
let filtered = if is_tty {
format!("{}{}", entries, summary)
} else {
entries
};

if verbose > 0 {
eprintln!("ls (native): {} target(s)", targets.len());
}

print!("{filtered}");
timer.track(
&format!("ls {}", targets.join(" ")),
"rtk ls",
&raw,
&filtered,
);
Ok(exit_code)
}

/// Build a synthetic `ls -la` line that [`parse_ls_line`] can parse.
/// The date is a fixed valid anchor (not shown in compact output).
fn synth_ls_line(name: &str, is_dir: bool, size: u64) -> String {
let perms = if is_dir { "drwxr-xr-x" } else { "-rw-r--r--" };
format!("{perms} 1 user group {size} Jan 1 00:00 {name}")
}

/// Format bytes into human-readable size
fn human_size(bytes: u64) -> String {
if bytes >= 1_048_576 {
Expand Down Expand Up @@ -353,6 +442,40 @@ fn compact_ls(raw: &str, show_all: bool, show_long: bool) -> (String, String, us
mod tests {
use super::*;

#[test]
fn test_synth_ls_line_parses() {
// A synthetic native line must be parseable by parse_ls_line.
let line = synth_ls_line("constants.rs", false, 542);
let parsed = parse_ls_line(&line).expect("synth line should parse");
let (file_type, _perms, size, name) = parsed;
assert_eq!(file_type, '-');
assert_eq!(size, 542);
assert_eq!(name, "constants.rs");
}

#[test]
fn test_synth_ls_line_dir() {
let line = synth_ls_line("src", true, 0);
let (file_type, _perms, _size, name) = parse_ls_line(&line).expect("dir line parses");
assert_eq!(file_type, 'd');
assert_eq!(name, "src");
}

#[test]
fn test_native_raw_round_trips_through_compact_ls() {
// Simulate native run_native output and verify compact_ls compresses it.
let mut raw = String::new();
raw.push_str(&synth_ls_line("src", true, 0));
raw.push('\n');
raw.push_str(&synth_ls_line("Cargo.toml", false, 1234));
raw.push('\n');
let (entries, _summary, parsed) = compact_ls(&raw, false, false);
assert_eq!(parsed, 2);
assert!(entries.contains("src/"));
assert!(entries.contains("Cargo.toml"));
assert!(entries.contains("1.2K"));
}

#[test]
fn test_compact_basic() {
let input = "total 48\n\
Expand Down
Loading