diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-10 01:20:09 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-10 01:20:09 +0300 |
| commit | e48d83067a5a9b05dfd0a82761068866ab2d294d (patch) | |
| tree | 159b92f857f85e40037879c7f32da4957093597b /src/platform/unix_common.rs | |
| parent | e2aa86a9e8b6445488ef11b2c0a8b4c22a6f79ce (diff) | |
feat: add configurable tab bar status (#2586)
* feat: show a zoom indicator in the desktop tab bar
Reserve the right edge of the tab row for a ZOOM pill while the
focused pane is zoomed, matching the accent style of the mode bars.
The per-tab Z suffix stays; the pill makes the zoomed state visible
at a glance like tmux's status-right flag.
* feat: optionally show the hostname in the desktop tab bar
Add ui.tab_bar_hostname to display the machine's hostname at the right
edge of the tab row, like tmux's #h in status-right. The value resolves
where the server renders, so remote sessions show the remote host. Off
by default.
* fix: strip control characters from the tab bar hostname
* fix: hide the hostname when it would squeeze out the tab strip
* feat: add configurable tab bar status
* fix: harden tab bar status updates
* fix: terminate tab bar status process trees
* fix: disable status commands on unsupported platforms
* fix: skip unchanged status command renders
* fix: keep tab bar status opt-in by default
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Diffstat (limited to 'src/platform/unix_common.rs')
| -rw-r--r-- | src/platform/unix_common.rs | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index 754e2fc2..3004e6da 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -119,6 +119,90 @@ fn fits_unix_socket_path(path: &Path) -> bool { path.as_os_str().as_bytes().len() <= 103 } +/// The machine's node name, as shown by tmux's `#h`. +pub(crate) fn hostname() -> Option<String> { + let mut buffer = [0_u8; 256]; + let result = + unsafe { libc::gethostname(buffer.as_mut_ptr().cast::<libc::c_char>(), buffer.len()) }; + if result != 0 { + return None; + } + let end = buffer + .iter() + .position(|&byte| byte == 0) + .unwrap_or(buffer.len()); + let name = String::from_utf8_lossy(&buffer[..end]).into_owned(); + (!name.is_empty()).then_some(name) +} + +pub(crate) fn local_datetime() -> Option<time::PrimitiveDateTime> { + let mut timestamp: libc::time_t = 0; + if unsafe { libc::time(&mut timestamp) } == -1 { + return None; + } + let mut local: libc::tm = unsafe { std::mem::zeroed() }; + if unsafe { libc::localtime_r(×tamp, &mut local) }.is_null() { + return None; + } + datetime_from_tm(&local) +} + +pub(crate) fn status_commands_supported() -> bool { + true +} + +pub(crate) fn configure_status_command(process: &mut std::process::Command) { + use std::os::unix::process::CommandExt; + + process.process_group(0); +} + +pub(crate) struct StatusCommandGuard { + process_group_id: Option<i32>, +} + +impl StatusCommandGuard { + pub(crate) fn new(child: &tokio::process::Child) -> std::io::Result<Self> { + let process_id = child + .id() + .ok_or_else(|| std::io::Error::other("status command has no process id"))?; + let process_group_id = i32::try_from(process_id) + .map_err(|_| std::io::Error::other("status command process id exceeds i32"))?; + Ok(Self { + process_group_id: Some(process_group_id), + }) + } +} + +impl Drop for StatusCommandGuard { + fn drop(&mut self) { + if let Some(process_group_id) = self.process_group_id.take() { + // The command was spawned as this process group's leader. Killing the + // group also cleans up background descendants on completion/cancellation. + unsafe { + libc::kill(-process_group_id, libc::SIGKILL); + } + } + } +} + +fn datetime_from_tm(value: &libc::tm) -> Option<time::PrimitiveDateTime> { + let month = time::Month::try_from(u8::try_from(value.tm_mon + 1).ok()?).ok()?; + let date = time::Date::from_calendar_date( + value.tm_year + 1900, + month, + u8::try_from(value.tm_mday).ok()?, + ) + .ok()?; + let time = time::Time::from_hms( + u8::try_from(value.tm_hour).ok()?, + u8::try_from(value.tm_min).ok()?, + u8::try_from(value.tm_sec).ok()?, + ) + .ok()?; + Some(time::PrimitiveDateTime::new(date, time)) +} + #[cfg(test)] mod tests { use super::*; |
