T12: sessions: new (+, Cmd-T), close (row x, Cmd-W), select (press, Cmd-1 to 8, Cmd-9 last, Ctrl-Tab, Ctrl-Shift-Tab), Ghostty's keys off macOS; rows show the title and the folder with ~; an ended program closes its row, the last row the window, and the app quits; a bell marks a hidden row; new sessions follow the Shell and Start in settings
CI / platforms (windows-latest) (push) Failing after 7s
CI / macos (push) Failing after 8m29s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 18:07:39 -04:00
1 parent ccbea6a7a0
commit 5fd674e82a
6 files changed
+391 -52

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
use ely_gpui_component::{
buttons::{ButtonVariant, IconButton},
lists::ListItem,
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize},
};
use gpui::{App, Entity, Subscription};
use crate::{
term::TerminalView,
workspace::{TITLE, Workspace},
};
/// A program on its own terminal, as the sessions column lists it.
pub struct Session {
pub terminal: Entity<TerminalView>,
/// The program's title; empty while it names none.
pub title: String,
/// The folder it reports, else the one it started in.
pub folder: PathBuf,
/// A bell rang while it was hidden.
pub bell: bool,
pub _events: Subscription,
}
/// `folder` as a row shows it: the home folder as `~`.
pub fn shown(folder: &Path, home: Option<&Path>) -> String {
match home.and_then(|home| folder.strip_prefix(home).ok()) {
Some(rest) if rest.as_os_str().is_empty() => "~".into(),
Some(rest) => format!("~{MAIN_SEPARATOR}{}", rest.display()),
None => folder.display().to_string(),
}
}
/// The session shown once session `closed` of `left + 1` is gone, `active` having been shown.
pub fn after_close(active: usize, closed: usize, left: usize) -> usize {
if closed < active || active == left {
active - 1
} else {
active
}
}
/// Session `index`'s row: its title and folder, a bell while one rang unseen, a button that closes it.
pub fn row(
workspace: &Entity<Workspace>,
index: usize,
session: &Session,
active: bool,
home: Option<&Path>,
cx: &App,
) -> ListItem {
let pid = session.terminal.read(cx).pid() as usize;
let title = if session.title.is_empty() {
TITLE
} else {
&session.title
};
let (select, close) = (workspace.clone(), workspace.clone());
let item = ListItem::new(("session", pid), title.to_string())
.description(shown(&session.folder, home))
.selected(active)
.strong(session.bell)
.on_click(move |_, window, cx| select.update(cx, |this, cx| this.select(index, window, cx)))
.trailing(
IconButton::new(("close-session", pid), IconName::X)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.tooltip("Close session")
.on_click(move |_, window, cx| {
cx.stop_propagation();
close.update(cx, |this, cx| this.close(index, window, cx))
}),
);
if session.bell {
item.leading(Icon::new(IconName::Bell).color(cx.theme().colors.warning))
} else {
item
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folders_under_home_read_from_a_tilde() {
let home = PathBuf::from("/Users/ely");
let shown = |folder: &str| shown(Path::new(folder), Some(&home));
assert_eq!(shown("/Users/ely"), "~");
assert_eq!(
shown("/Users/ely/src/app"),
format!("~{MAIN_SEPARATOR}src/app")
);
assert_eq!(
shown("/Users/elyse"),
"/Users/elyse",
"a sibling is no child"
);
assert_eq!(shown("/tmp"), "/tmp");
}
#[test]
fn closing_shows_the_next_session_or_keeps_the_shown_one() {
assert_eq!(after_close(2, 0, 3), 1, "one before it went");
assert_eq!(after_close(1, 1, 3), 1, "it went: the next one");
assert_eq!(after_close(3, 3, 3), 2, "the last went: the one before");
assert_eq!(after_close(1, 2, 3), 1, "one after it went");
}
}