Merge rename-and-term: workspace rename + TERM env fix
This commit is contained in:
+36
-1
@@ -41,6 +41,7 @@ export function Sidebar({
|
|||||||
const [hovered, setHovered] = useState<string | null>(null);
|
const [hovered, setHovered] = useState<string | null>(null);
|
||||||
const [drag, setDrag] = useState<{ id: string; section: string } | null>(null);
|
const [drag, setDrag] = useState<{ id: string; section: string } | null>(null);
|
||||||
const [dropAt, setDropAt] = useState<DropAt | null>(null);
|
const [dropAt, setDropAt] = useState<DropAt | null>(null);
|
||||||
|
const [editing, setEditing] = useState<{ id: string; draft: string } | null>(null);
|
||||||
const dragRef = useRef<{ id: string; section: string } | null>(null);
|
const dragRef = useRef<{ id: string; section: string } | null>(null);
|
||||||
const dropRef = useRef<DropAt | null>(null);
|
const dropRef = useRef<DropAt | null>(null);
|
||||||
const [, setTick] = useState(0);
|
const [, setTick] = useState(0);
|
||||||
@@ -59,6 +60,17 @@ export function Sidebar({
|
|||||||
|
|
||||||
const togglePin = (w: WorkspaceView) => { void setWorkspaceMeta(w.id, { pinned: !w.pinned }); };
|
const togglePin = (w: WorkspaceView) => { void setWorkspaceMeta(w.id, { pinned: !w.pinned }); };
|
||||||
|
|
||||||
|
const commitRename = () => {
|
||||||
|
setEditing((cur) => {
|
||||||
|
if (cur) {
|
||||||
|
const name = cur.draft.trim();
|
||||||
|
const w = workspaces.find((x) => x.id === cur.id);
|
||||||
|
if (name && w && name !== w.name) void setWorkspaceMeta(cur.id, { name });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Persist a new ordering for one section by reassigning sequential `order`
|
// Persist a new ordering for one section by reassigning sequential `order`
|
||||||
// values (per-section; values never compared across sections).
|
// values (per-section; values never compared across sections).
|
||||||
const commitReorder = (items: WorkspaceView[], fromId: string, toIndex: number) => {
|
const commitReorder = (items: WorkspaceView[], fromId: string, toIndex: number) => {
|
||||||
@@ -123,7 +135,30 @@ export function Sidebar({
|
|||||||
color: isActive ? COLORS.textPrimary : COLORS.textSecondary,
|
color: isActive ? COLORS.textPrimary : COLORS.textSecondary,
|
||||||
}}>
|
}}>
|
||||||
<span style={{ width: 10, height: 10, borderRadius: "50%", border: `2px solid ${STATE_COLOR[aggregate(w)]}`, boxSizing: "border-box", flex: "0 0 10px" }} />
|
<span style={{ width: 10, height: 10, borderRadius: "50%", border: `2px solid ${STATE_COLOR[aggregate(w)]}`, boxSizing: "border-box", flex: "0 0 10px" }} />
|
||||||
<span style={{ flex: 1, fontWeight: isActive ? 600 : 400, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{w.name}</span>
|
{editing?.id === w.id ? (
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={editing.draft}
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setEditing({ id: w.id, draft: e.target.value })}
|
||||||
|
onBlur={commitRename}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === "Enter") { e.preventDefault(); commitRename(); }
|
||||||
|
else if (e.key === "Escape") { e.preventDefault(); setEditing(null); }
|
||||||
|
}}
|
||||||
|
style={{ flex: 1, minWidth: 0, background: COLORS.bgApp, color: COLORS.textPrimary, border: `1px solid ${COLORS.accent}`, borderRadius: 4, padding: "2px 6px", fontFamily: FONT.ui, fontSize: 13, outline: "none" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
onDoubleClick={(e) => { e.stopPropagation(); setEditing({ id: w.id, draft: w.name }); }}
|
||||||
|
title="Двойной клик — переименовать"
|
||||||
|
style={{ flex: 1, fontWeight: isActive ? 600 : 400, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
{w.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{(hovered === w.id || w.pinned) && (
|
{(hovered === w.id || w.pinned) && (
|
||||||
<Star size={14} fill={w.pinned ? COLORS.stWait : "none"} color={w.pinned ? COLORS.stWait : COLORS.textMuted}
|
<Star size={14} fill={w.pinned ? COLORS.stWait : "none"} color={w.pinned ? COLORS.stWait : COLORS.textMuted}
|
||||||
style={{ cursor: "pointer", flex: "0 0 14px" }}
|
style={{ cursor: "pointer", flex: "0 0 14px" }}
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ impl PtyHandle {
|
|||||||
cmd.arg(a);
|
cmd.arg(a);
|
||||||
}
|
}
|
||||||
cmd.cwd(&spec.cwd);
|
cmd.cwd(&spec.cwd);
|
||||||
|
// Guarantee a terminal environment even when the daemon was launched
|
||||||
|
// without one (GUI/launchd have no TERM, which breaks tput/zsh/ncurses).
|
||||||
|
// xterm.js renders an xterm-256color/truecolor terminal. Caller-provided
|
||||||
|
// values in spec.env win.
|
||||||
|
if !spec.env.iter().any(|(k, _)| k == "TERM") {
|
||||||
|
cmd.env("TERM", "xterm-256color");
|
||||||
|
}
|
||||||
|
if !spec.env.iter().any(|(k, _)| k == "COLORTERM") {
|
||||||
|
cmd.env("COLORTERM", "truecolor");
|
||||||
|
}
|
||||||
for (k, v) in &spec.env {
|
for (k, v) in &spec.env {
|
||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
}
|
}
|
||||||
@@ -125,6 +135,19 @@ mod tests {
|
|||||||
assert!(text.contains("SPACESH_OK"), "got: {text:?}");
|
assert!(text.contains("SPACESH_OK"), "got: {text:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn term_is_set_even_without_inherited_env() {
|
||||||
|
// Clear TERM in the parent to emulate a GUI/launchd-spawned daemon.
|
||||||
|
std::env::remove_var("TERM");
|
||||||
|
let mut handle = PtyHandle::spawn(shell_spec("printf %s \"$TERM\"")).unwrap();
|
||||||
|
let mut collected = Vec::new();
|
||||||
|
while let Some(chunk) = handle.output.recv().await {
|
||||||
|
collected.extend_from_slice(&chunk);
|
||||||
|
}
|
||||||
|
let text = String::from_utf8_lossy(&collected);
|
||||||
|
assert!(text.contains("xterm-256color"), "got: {text:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resize_does_not_error() {
|
async fn resize_does_not_error() {
|
||||||
let handle = PtyHandle::spawn(shell_spec("sleep 0.2")).unwrap();
|
let handle = PtyHandle::spawn(shell_spec("sleep 0.2")).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user