T63: a double press on a session's row renames it, which a user asked for after 0.1.6: Ely's ListItem::editor, added for this, draws a field in the title's place; Enter keeps the name, Escape drops it, a blur keeps it; the name stands over the program's title in the row, the header and the window's title until renamed again, and an empty one gives the title back; Ely pinned to 7f7a715

This commit is contained in:
ZacharyZhang-NY committed 2026-10-19 19:19:09 -04:00
1 parent 76c48dce2e
commit 3085a940ad
9 files changed
+136 -17

No files matched your search

+44 -7
View File
@@ -5,6 +5,7 @@ use std::{
use ely_gpui_component::{
buttons::{ButtonVariant, IconButton},
forms::{Input, TextInput},
lists::ListItem,
primitives::{Icon, IconName, Tooltip},
theme::{ActiveTheme, ControlSize, IconSize},
@@ -25,6 +26,10 @@ pub struct Session {
pub terminal: Entity<TerminalView>,
/// The program's title; empty while it names none.
pub title: String,
/// The name the user gave it; empty leaves the title to its program.
pub name: String,
/// The field its row shows while the user renames it.
pub editing: Option<(Entity<TextInput>, Subscription)>,
/// The folder it reports, else the one it started in.
pub folder: PathBuf,
/// Its shell has integration, or it reported a folder anyway.
@@ -39,6 +44,14 @@ pub struct Session {
}
impl Session {
/// What it is called: the user's name, else its program's title, else the app's name.
pub fn named(&self) -> &str {
[self.name.as_str(), self.title.as_str()]
.into_iter()
.find(|title| !title.is_empty())
.unwrap_or(TITLE)
}
/// The look its mark takes from its terminal now.
pub fn looks(&self, cx: &App) -> Option<Look> {
let view = self.terminal.read(cx);
@@ -103,17 +116,20 @@ pub fn row(
) -> ListItem {
let terminal = session.terminal.read(cx);
let pid = terminal.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())
let editing = session.editing.is_some();
let item = ListItem::new(("session", pid), session.named().to_string())
.description(shown(&session.folder, home))
.selected(active)
.strong(session.bell || session.waiting)
.on_click(move |_, window, cx| select.update(cx, |this, cx| this.select(index, window, cx)))
// A press in the field it renames with stays the field's.
.on_click(
move |event, window, cx| match (editing, event.click_count()) {
(true, _) => {}
(false, 2..) => select.update(cx, |this, cx| this.rename(index, window, cx)),
(false, _) => select.update(cx, |this, cx| this.select(index, window, cx)),
},
)
.trailing(
IconButton::new(("close-session", pid), IconName::X)
.variant(ButtonVariant::Ghost)
@@ -124,6 +140,10 @@ pub fn row(
close.update(cx, |this, cx| this.close(index, window, cx))
}),
);
let item = match &session.editing {
Some((input, _)) => item.editor(editor(workspace, session, input)),
None => item,
};
let mark = mark(session, now, cx);
let bell = session
.bell
@@ -141,6 +161,23 @@ pub fn row(
)
}
/// The field a session's row renames it with; Escape drops what it holds.
fn editor(
workspace: &Entity<Workspace>,
session: &Session,
input: &Entity<TextInput>,
) -> impl IntoElement {
let (workspace, id) = (workspace.clone(), session.terminal.entity_id());
div()
.on_key_down(move |event, window, cx| {
if event.keystroke.key == "escape" {
cx.stop_propagation();
workspace.update(cx, |this, cx| this.end_rename(id, false, true, window, cx));
}
})
.child(Input::new(input).size(ControlSize::Sm))
}
/// What a mark shows: the most pressing record's state, else a failed last command.
pub fn look(record: Option<&Record>, failed: bool) -> Option<Look> {
Some(
+5 -6
View File
@@ -29,6 +29,7 @@ use crate::{
};
mod events;
mod rename;
mod widths;
use widths::{Dragged, Side};
@@ -156,6 +157,8 @@ impl Workspace {
self.sessions.push(Session {
terminal,
title: String::new(),
name: String::new(),
editing: None,
folder: launch.cwd,
reports: launch.reports,
bell: false,
@@ -268,13 +271,9 @@ impl Workspace {
&self.sessions
}
/// The shown session's title, or the app's name while its program names none.
/// What the shown session is called.
fn title(&self) -> &str {
self.sessions
.get(self.active)
.map(|session| session.title.as_str())
.filter(|title| !title.is_empty())
.unwrap_or(TITLE)
self.sessions.get(self.active).map_or(TITLE, Session::named)
}
fn retitle(&self, window: &mut Window) {
+4 -1
View File
@@ -21,7 +21,10 @@ impl Workspace {
};
let session = &mut self.sessions[index];
match event {
TerminalEvent::Title(title) => session.title.clone_from(title),
TerminalEvent::Title(title) => {
log::debug!("app: session {}'s program names it {title:?}", index + 1);
session.title.clone_from(title);
}
TerminalEvent::Folder(folder) => {
session.folder.clone_from(folder);
session.reports = true;
+77
View File
@@ -0,0 +1,77 @@
//! A session renamed in place, as a double-click on its row asks.
use ely_gpui_component::forms::{InputEvent, TextInput};
use gpui::{AppContext, Context, EntityId, Focusable, Window};
use super::Workspace;
impl Workspace {
/// Opens a field over session `index`'s title, what it is called selected.
pub fn rename(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(session) = self.sessions.get(index) else {
return;
};
if session.editing.is_some() {
return;
}
let named = session.named().to_string();
let id = session.terminal.entity_id();
let input = cx.new(|cx| {
let mut input = TextInput::new(window, cx);
input.set_text(named.clone(), cx);
input.select(0..named.len(), cx);
input
});
let events = cx.subscribe_in(
&input,
window,
move |this, _, event, window, cx| match event {
InputEvent::Submit => this.end_rename(id, true, true, window, cx),
InputEvent::Blur => this.end_rename(id, true, false, window, cx),
InputEvent::Changed | InputEvent::Focus => {}
},
);
window.focus(&input.focus_handle(cx), cx);
log::debug!("app: session {} is renamed in place", index + 1);
self.sessions[index].editing = Some((input, events));
cx.notify();
}
/// Closes the field renaming the session `id` names, `keep`ing what it holds or not, the shown terminal `focused` again or not.
pub fn end_rename(
&mut self,
id: EntityId,
keep: bool,
focused: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(session) = self
.sessions
.iter_mut()
.find(|session| session.terminal.entity_id() == id)
else {
log::debug!("app: the session renamed is gone");
return;
};
let Some((input, _)) = session.editing.take() else {
return;
};
let pid = session.terminal.read(cx).pid();
if !keep {
log::debug!("app: session {pid} keeps its name");
} else {
session.name = input.read(cx).text().trim().to_string();
if session.name.is_empty() {
log::info!("app: session {pid} takes its program's title again");
} else {
log::info!("app: session {pid} is named {:?}", session.name);
}
}
if focused && let Some(shown) = self.sessions.get(self.active) {
window.focus(&shown.terminal.focus_handle(cx), cx);
}
self.retitle(window);
cx.notify();
}
}