T13: the right column's Git and Files switch, kept in settings; Files shows the shown session's folder as a tree, folders first, Git's marks on names from git status without optional locks, unreadable folders noted; a double press opens a folder, or a file in the system's app; it reloads when the session reports a folder, so at each prompt; a program without shell integration gets a line saying it reports no folder; header controls keep their presses from the header's double-press zoom
CI / platforms (windows-latest) (push) Failing after 8s
CI / macos (push) Failing after 9m12s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 19:09:21 -04:00
1 parent 2ddc40d939
commit 8369d96b36
11 files changed
+700 -27

No files matched your search

+82 -24
View File
@@ -1,23 +1,23 @@
use std::path::PathBuf;
use ely_gpui_component::{
buttons::{ButtonVariant, IconButton},
buttons::{ButtonVariant, IconButton, SegmentedControl},
lists::List,
primitives::{FocusScope, IconName},
shell::{ColumnShell, ResizeBorder, ShellColumn},
theme::{ActiveTheme, ControlSize},
typography::Caption,
};
use gpui::{
Action, App, AppContext, Context, EntityId, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, ParentElement, Render, StatefulInteractiveElement, Styled,
Subscription, Window, actions, div,
Action, App, AppContext, Context, Entity, EntityId, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
};
use crate::{
files::Files,
sessions::{self, Session},
settings::{Settings, SettingsStore, StartIn},
settings::{PanelTab, Settings, SettingsStore, StartIn},
term::{
Events, Pty, TerminalEvent, TerminalView, config,
Events, Launch, Pty, TerminalEvent, TerminalView, config,
shell::{ShellIntegration, launch},
spawn,
},
@@ -81,15 +81,16 @@ pub struct Workspace {
sessions: Vec<Session>,
/// The session shown.
active: usize,
files: Entity<Files>,
_settings: Subscription,
}
impl Workspace {
/// The window's content with its first session, `pty` started in `folder`.
/// The window's content with its first session, `pty` started from `launch`.
pub fn new(
pty: Pty,
events: Events,
folder: PathBuf,
launch: Launch,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -97,9 +98,10 @@ impl Workspace {
focus: cx.focus_handle(),
sessions: Vec::new(),
active: 0,
files: cx.new(|_| Files::new(launch.cwd.clone())),
_settings: cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
};
workspace.add(pty, events, folder, window, cx);
workspace.add(pty, events, launch, window, cx);
workspace.select(0, window, cx);
workspace
}
@@ -108,7 +110,7 @@ impl Workspace {
&mut self,
pty: Pty,
events: Events,
folder: PathBuf,
launch: Launch,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -118,11 +120,12 @@ impl Workspace {
this.session_event(id, event, window, cx)
});
let pid = terminal.read(cx).pid();
log::info!("app: session {pid} opens in {}", folder.display());
log::info!("app: session {pid} opens in {}", launch.cwd.display());
self.sessions.push(Session {
terminal,
title: String::new(),
folder,
folder: launch.cwd,
reports: launch.reports,
bell: false,
_events: events,
});
@@ -142,15 +145,11 @@ impl Workspace {
.get(self.active)
.map_or(home, |session| session.folder.clone()),
};
let started = launch(
&settings.shell,
folder.clone(),
&cx.global::<ShellIntegration>().0,
)
.and_then(|launch| spawn(&launch, (80, 24), (8, 16), config(settings)));
let started = launch(&settings.shell, folder, &cx.global::<ShellIntegration>().0)
.and_then(|launch| Ok((spawn(&launch, (80, 24), (8, 16), config(settings))?, launch)));
match started {
Ok((pty, events)) => {
self.add(pty, events, folder, window, cx);
Ok(((pty, events), launch)) => {
self.add(pty, events, launch, window, cx);
self.select(self.sessions.len() - 1, window, cx);
}
Err(error) => log::error!("app: no new session: {error:#}"),
@@ -170,6 +169,8 @@ impl Workspace {
let session = &mut self.sessions[index];
session.bell = false;
window.focus(&session.terminal.focus_handle(cx), cx);
let folder = session.folder.clone();
self.files.update(cx, |files, cx| files.show(&folder, cx));
self.active = index;
log::debug!("app: session {} shown", index + 1);
self.retitle(window);
@@ -231,7 +232,13 @@ impl Workspace {
let session = &mut self.sessions[index];
match event {
TerminalEvent::Title(title) => session.title.clone_from(title),
TerminalEvent::Folder(folder) => session.folder.clone_from(folder),
TerminalEvent::Folder(folder) => {
session.folder.clone_from(folder);
session.reports = true;
if index == self.active {
self.files.update(cx, |files, cx| files.show(folder, cx));
}
}
TerminalEvent::Bell if index != self.active => {
if !session.bell {
log::debug!("app: a bell in hidden session {}", index + 1);
@@ -276,6 +283,22 @@ impl Workspace {
}
}
/// Shows Git or Files in the right column, as the settings keep it.
fn show_tab(value: &SharedString, cx: &mut App) {
let panel = match value.as_ref() {
"git" => PanelTab::Git,
"files" => PanelTab::Files,
other => unreachable!("app: no {other} tab"),
};
if SettingsStore::get(cx).panel == panel {
return;
}
match SettingsStore::update(cx, |settings| settings.panel = panel) {
Ok(()) => log::info!("app: the right column shows {panel:?}"),
Err(error) => log::error!("app: the right column stays: {error:#}"),
}
}
impl Render for Workspace {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = SettingsStore::get(cx);
@@ -293,6 +316,7 @@ impl Render for Workspace {
.size(ControlSize::Sm)
.tooltip("New session")
.on_click(move |_, window, cx| {
cx.stop_propagation();
workspace.update(cx, |this, cx| this.new_session(window, cx))
});
shell = shell.column(
@@ -316,7 +340,41 @@ impl Render for Workspace {
.children(shown),
);
if settings.show_panel {
shell = shell.column(ShellColumn::new("panel").width(width));
let tab = settings.panel;
let chosen = match tab {
PanelTab::Git => "git",
PanelTab::Files => "files",
};
let switch = SegmentedControl::new("panel-tab", chosen)
.segment("git", "Git", None)
.segment("files", "Files", None)
.size(ControlSize::Sm)
.on_change(|value, _, cx| show_tab(value, cx));
// Its presses stay off the header, whose double press zooms.
let switch = div()
.id("panel-switch")
.on_click(|_, _, cx| cx.stop_propagation())
.child(switch);
let quiet = self
.sessions
.get(self.active)
.is_some_and(|session| !session.reports);
let caption = || Caption::new("This shell reports no folder.").px_3().pt_2();
shell = shell.column(
ShellColumn::new("panel")
.width(width)
.leading(switch)
.children(quiet.then(caption))
// Hidden, not dropped, so the tree keeps its open folders.
.child(
div()
.flex_1()
.min_h_0()
.p_2()
.when(tab != PanelTab::Files, |body| body.hidden())
.child(self.files.clone()),
),
);
}
// The context sits above the focus root, so its keys work wherever focus is.
div()