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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **The picker no longer panics when `TERM` names no usable terminal.** With `TERM` unset,
or naming a terminfo entry the machine does not have, `dl`'s workspace picker (and
`aid resume`'s) aborted with a panic before it drew a row. A name like `xterm-kitty`
with no entry behind it did not panic but drew a garbled picker, because the entry it
got could not move the cursor. Now any `TERM` whose entry cannot move the cursor is
drawn as `xterm-256color`, the picker says so once it closes, and the session it opens
gets your own `TERM` back. When `xterm-256color` cannot move the cursor here either,
the picker is not started, and the command says why and how to name the workspace
instead.

## [0.57.0] - 2026-09-28

### Added
Expand Down
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ toml = "0.9"
toml_edit = "0.23"
clap = { version = "4", features = ["derive"] }
skim = "0.20"
# The terminfo lookup skim's terminal setup makes and unwraps, made first so a
# `TERM` it would panic on, or draw nothing with, can be replaced (dl's
# `select.rs`). The version skim resolves, so the answer is the one skim would
# have reached.
term = "0.7"
# flock(2) — per open file description, kernel-released — as Python's
# fcntl.flock. fd-lock 4 gets its semantics from the same rustix call, but its
# guard borrows the RwLock and unlocks on drop, so an owned guard would have to
Expand Down
52 changes: 52 additions & 0 deletions rust/aid/tests/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,58 @@ fn a_resume_with_no_workspace_resumes_the_row_the_picker_took() {
);
}

#[test]
fn a_resume_with_no_workspace_on_a_terminal_no_entry_can_draw_on_says_how_to_name_one() {
// `TERMINFO_DIRS` is the whole of the terminfo search when it is set, so an
// empty one leaves no entry, the fallback's included, that can move the
// cursor. No picker can be drawn, and aid says how to name the workspace
// instead of opening anything.
let world = World::with(&["--warm"]);
let nothing = world.root.join("terminfo");
std::fs::create_dir_all(&nothing).expect("an empty terminfo directory");
let mut session = PtyAid::spawn(
&world,
&["resume"],
&[
("TERM", "xterm-no-such-entry"),
("TERMINFO_DIRS", &nothing.display().to_string()),
],
);
let seen = Arc::clone(&session.seen);
// Bounded: a picker that opened anyway would wait on this pty for ever.
let mut code = None;
wait_for(|| {
code = session.child.try_wait().expect("aid's status");
code.is_some()
});
let _ = session.child.kill();
assert_eq!(code.map(|status| status.exit_code()), Some(1));

// The reader thread may still hold the last bytes, so the line is waited for.
let said = || String::from_utf8_lossy(&seen.lock().expect("the pty buffer")).into_owned();
assert!(
wait_for(|| said().contains(
"so aid resume cannot draw its picker. Name the workspace instead: \
aid resume <workspace>"
)),
"the refusal was not given; the pty said:\n{:?}",
said()
);
assert!(
!said().contains("\x1b[?1049h"),
"a picker was opened anyway: {:?}",
said()
);
assert!(
!world
.devpod_calls()
.iter()
.any(|call| call.starts_with("devpod ssh")),
"a refused picker opened a session: {:?}",
world.devpod_calls()
);
}

#[test]
fn the_boot_runs_while_the_prompt_is_still_being_typed() {
// The overlap itself: a stopped workspace's `devpod up` is on the shim's log
Expand Down
1 change: 1 addition & 0 deletions rust/dl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ libc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
skim = { workspace = true }
term = { workspace = true }

# The released package ships two binaries, and a maturin bin-wheel ships the bin
# targets of exactly *one* cargo package (maturin 1.14 asks cargo metadata for
Expand Down
21 changes: 20 additions & 1 deletion rust/dl/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,17 @@
println!("{}", select::invitation(arity));
no_pick()
}
select::Pick::Undrawable(reason) => {
let line = match &verb {
Verb::Attach { .. } => "dl <workspace>".to_owned(),
Verb::Run(..) => "dl <workspace> -- <command>".to_owned(),
verb => format!("dl <workspace> {}", verb.word()),

Check warning on line 1686 in rust/dl/src/commands.rs

View check run for this annotation

Codecov / codecov/patch

rust/dl/src/commands.rs#L1682-L1686

Added lines #L1682 - L1686 were not covered by tests
};
eprintln!(

Check warning on line 1688 in rust/dl/src/commands.rs

View check run for this annotation

Codecov / codecov/patch

rust/dl/src/commands.rs#L1688

Added line #L1688 was not covered by tests
"{reason}, so the picker cannot be drawn. Name the workspace instead: {line}"
);
no_pick()

Check warning on line 1691 in rust/dl/src/commands.rs

View check run for this annotation

Codecov / codecov/patch

rust/dl/src/commands.rs#L1691

Added line #L1691 was not covered by tests
}
}
}

Expand All @@ -1691,7 +1702,8 @@
/// grammar. An empty list keeps dl's sentence, which says how to make a workspace.
/// A run with no terminal gets one of its own in place of the invitation, which
/// would ask for a pick nobody can make: it says a terminal is missing and how to
/// name the workspace instead.
/// name the workspace instead. So does a terminal that no terminfo entry here can
/// draw on.
pub(crate) fn pick_one(runner: &dyn Runner) -> Result<String, Ending> {
// The picker reads each row's clone under the cache for its columns. With no
// cache there is nothing to draw, and the refusal is every other command's.
Expand Down Expand Up @@ -1719,6 +1731,13 @@
);
Err(Ending::Refused)
}
select::Pick::Undrawable(reason) => {
eprintln!(

Check warning on line 1735 in rust/dl/src/commands.rs

View check run for this annotation

Codecov / codecov/patch

rust/dl/src/commands.rs#L1734-L1735

Added lines #L1734 - L1735 were not covered by tests
"{reason}, so aid resume cannot draw its picker. Name the workspace instead: \
aid resume <workspace>"
);
Err(Ending::Refused)

Check warning on line 1739 in rust/dl/src/commands.rs

View check run for this annotation

Codecov / codecov/patch

rust/dl/src/commands.rs#L1739

Added line #L1739 was not covered by tests
}
}
}

Expand Down
Loading
Loading