Files
ghostty-rs/crates/ghostty-os/src/passwd.rs
T
ddidderr 967e3a176d refactor: resolve cross-crate integration TODOs and resources discovery
Cleanup pass over items collected in docs/rewrite/INTEGRATION_TODO.md:
- OSC 133 printf-q / percent decoders live once in
  ghostty_vt::string_encoding; ghostty-os re-exports them.
- CursorStyle and clipboard codepoint map entries have one definition
  (ghostty-terminal); ghostty-config now depends on ghostty-terminal.
- ActionKey::docs() trailing blank lines fixed at the source; cli's
  workaround removed (+list-actions --docs still byte-identical).
- x11_color exposes names in original case; cli no longer re-parses
  rgb.txt.
- One cgroup scope wait loop (ghostty_os::cgroup::wait_for_scope).
- Flatpak passwd lookup implemented in ghostty-os via a local pty.
- +version shows the rustc version captured at build time.

Resources: dev builds find <workspace>/resources automatically (keeping
Ghostty's sentinel-file detection and GHOSTTY_RESOURCES_DIR);
scripts/gen-terminfo.sh (just terminfo) regenerates the compiled
terminfo; `just install [PREFIX]` installs binary, share/ghostty,
terminfo, desktop/D-Bus/systemd/metainfo files and icons.

Test Plan:
- cargo test for vt, os, config, input, cli
- diff `+list-keybinds`, `+list-actions --docs`, `+show-config --default`
  against /usr/bin/ghostty
2026-10-12 02:08:22 +02:00

144 lines
4.7 KiB
Rust

use crate::flatpak::is_flatpak;
/// Entry that is retrieved from the passwd API. This only contains the fields
/// we care about.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Entry {
pub shell: Option<String>,
pub home: Option<String>,
pub name: Option<String>,
}
/// Get the passwd entry for the currently executing user.
pub fn get() -> std::io::Result<Entry> {
let uid = nix::unistd::getuid();
let user = match nix::unistd::User::from_uid(uid) {
Ok(Some(user)) => user,
Ok(None) => {
// Future: let's check if a better shell is available like zsh
log::warn!("no pw entry to detect default shell, will default to 'sh'");
return Ok(Entry::default());
}
Err(e) => {
log::warn!("error retrieving pw entry code={e}");
return Ok(Entry::default());
}
};
// If we're in flatpak then our entry is always empty so we grab it
// by shelling out to the host. note that we do HAVE an entry in the
// sandbox but only the username is correct.
if is_flatpak() {
log::info!("flatpak detected, will use host command to get our entry");
return host_entry(&user.name);
}
Ok(Entry {
shell: Some(user.shell.to_string_lossy().into_owned()),
home: Some(user.dir.to_string_lossy().into_owned()),
name: Some(user.name),
})
}
/// Flatpak: look up our entry on the host with `getent passwd <name>` over a
/// pty (Zig `passwd.get`, flatpak branch).
///
/// Dependency note: Zig uses `pty.zig`; `ghostty-termio` (which owns the Rust
/// `Pty`) depends on this crate, so a minimal openpt/ptsname pair is opened
/// here with rustix instead. Like Zig, only `shell` and `home` are filled in.
fn host_entry(name: &str) -> std::io::Result<Entry> {
use rustix::fs::{Mode, OFlags};
use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
use std::io::Read;
let err = std::io::Error::from;
let master =
openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC).map_err(err)?;
grantpt(&master).map_err(err)?;
unlockpt(&master).map_err(err)?;
let slave_name = ptsname(&master, Vec::new()).map_err(err)?;
let slave = rustix::fs::open(
slave_name.as_c_str(),
OFlags::RDWR | OFlags::NOCTTY,
Mode::empty(),
)
.map_err(err)?;
// Note: we wrap our getent call in a /bin/sh login shell because
// some operating systems (NixOS tested) don't set the PATH for various
// utilities properly until we get a login shell.
let mut cmd = crate::flatpak::FlatpakHostCommand::new(
vec![
"/bin/sh".into(),
"-l".into(),
"-c".into(),
format!("getent passwd {name}"),
],
slave.try_clone()?,
slave.try_clone()?,
slave.try_clone()?,
);
cmd.spawn().map_err(std::io::Error::other)?;
cmd.wait().map_err(std::io::Error::other)?;
// Once started, we can close the child side. We do this after
// wait right now but that is fine too. This lets us read the
// parent and detect EOF.
drop(cmd);
drop(slave);
// Read all of our output (max total size is 1024 bytes, like Zig).
let mut file = std::fs::File::from(master);
let mut output = Vec::new();
let mut buf = [0u8; 1024];
loop {
let n = match file.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
// EIO is triggered at the end since we closed our child side.
// This is just EOF for this.
Err(e) if e.raw_os_error() == Some(rustix::io::Errno::IO.raw_os_error()) => break,
Err(e) => return Err(e),
};
output.extend_from_slice(&buf[..n]);
if output.len() > buf.len() {
break;
}
}
Ok(parse_getent(&String::from_utf8_lossy(&output)))
}
/// Shell and home are the last two `:`-separated fields of the output.
fn parse_getent(output: &str) -> Entry {
let mut it = output.trim_end_matches([' ', '\r', '\n']).rsplit(':');
let shell = it.next().map(str::to_owned);
let home = it.next().map(str::to_owned);
Entry {
shell,
home,
name: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_getent_output() {
let e = parse_getent("mitchell:x:1000:1000:Mitchell:/home/mitchell:/bin/zsh\r\n");
assert_eq!(e.shell.as_deref(), Some("/bin/zsh"));
assert_eq!(e.home.as_deref(), Some("/home/mitchell"));
assert_eq!(e.name, None);
}
#[test]
fn get_entry() {
// We should be able to get an entry
let entry = get().unwrap();
assert!(entry.shell.is_some());
assert!(entry.home.is_some());
}
}