Files
ELYTerminal/src/sessions.rs
T

137 lines
4.4 KiB
Rust

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,
/// Its shell has integration, or it reported a folder anyway.
pub reports: bool,
/// A bell rang while it was hidden.
pub bell: bool,
pub _events: Subscription,
}
/// `folder` as a row shows it: the home folder as `~`, on one line.
pub fn shown(folder: &Path, home: Option<&Path>) -> String {
let shown = 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(),
};
one_line(&shown)
}
/// `text` on one line, as gpui shapes labels: each control character as its picture, `␊` for a newline.
pub fn one_line(text: &str) -> String {
text.chars()
.map(|c| match c {
'\0'..='\x1f' => char::from_u32(0x2400 + c as u32).expect("a control picture"),
'\x7f' => '\u{2421}',
c => c,
})
.collect()
}
/// 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");
assert_eq!(
shown("/Users/ely/line\nbreak"),
format!("~{MAIN_SEPARATOR}line␊break")
);
}
#[test]
fn control_characters_show_as_their_pictures() {
assert_eq!(one_line("a\nb\tc\rd\0e\u{7f}"), "a␊b␉c␍d␀e␡");
assert_eq!(one_line("café ✓"), "café ✓", "printable text stays");
}
#[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");
}
}