a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
260 lines
8.6 KiB
Rust
260 lines
8.6 KiB
Rust
const LINUX_PROCESS_TITLE_LIMIT: usize = 15;
|
|
#[cfg(target_os = "linux")]
|
|
const KILLALL_PROCESS_NAME: &str = "jcode";
|
|
|
|
pub fn compact_process_title(prefix: &str, name: Option<&str>) -> String {
|
|
let mut title = prefix.to_string();
|
|
if let Some(name) = name.filter(|name| !name.is_empty()) {
|
|
let remaining = LINUX_PROCESS_TITLE_LIMIT.saturating_sub(title.len());
|
|
if remaining > 0 {
|
|
title.push_str(&name.chars().take(remaining).collect::<String>());
|
|
}
|
|
}
|
|
title
|
|
}
|
|
|
|
pub fn session_name(session_id: &str) -> String {
|
|
crate::id::extract_session_name(session_id)
|
|
.map(|name| name.to_string())
|
|
.unwrap_or_else(|| session_id.to_string())
|
|
}
|
|
|
|
fn normalized_display_title(title: &str) -> Option<String> {
|
|
let normalized = title.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
(!normalized.is_empty()).then_some(normalized)
|
|
}
|
|
|
|
fn capitalize_ascii_label(label: &str) -> String {
|
|
let mut chars = label.chars();
|
|
match chars.next() {
|
|
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
|
None => String::new(),
|
|
}
|
|
}
|
|
|
|
fn truncate_chars(text: &str, max_chars: usize) -> String {
|
|
let mut chars = text.chars();
|
|
let truncated: String = chars.by_ref().take(max_chars).collect();
|
|
if chars.next().is_some() {
|
|
format!("{}…", truncated)
|
|
} else {
|
|
truncated
|
|
}
|
|
}
|
|
|
|
pub fn terminal_session_label(session_name: &str, display_title: Option<&str>) -> String {
|
|
let fallback = capitalize_ascii_label(session_name);
|
|
let Some(title) = display_title.and_then(normalized_display_title) else {
|
|
return fallback;
|
|
};
|
|
if title.eq_ignore_ascii_case(session_name) || title.eq_ignore_ascii_case(&fallback) {
|
|
return fallback;
|
|
}
|
|
format!("{} ({})", truncate_chars(&title, 48), session_name)
|
|
}
|
|
|
|
/// Resolve the human-authored title used by terminal windows and `/resume`.
|
|
/// Explicit renames win over todo/goal-derived titles, which win over the
|
|
/// generated session title.
|
|
pub fn terminal_display_title_for_id(session_id: &str) -> Option<String> {
|
|
crate::session::Session::load_startup_stub(session_id)
|
|
.ok()
|
|
.and_then(|session| {
|
|
session
|
|
.custom_title
|
|
.filter(|title| !title.trim().is_empty())
|
|
.or_else(|| crate::todo::load_session_title(session_id))
|
|
.or(session.title)
|
|
})
|
|
}
|
|
|
|
/// Build the deliberately minimal terminal window title. The emoji already
|
|
/// identifies the session/connection, so do not repeat `jcode` or the memorable
|
|
/// animal name in window chrome.
|
|
pub fn terminal_window_title(
|
|
icon: &str,
|
|
display_title: Option<&str>,
|
|
fallback_label: Option<&str>,
|
|
is_selfdev: bool,
|
|
) -> String {
|
|
let display_title = display_title
|
|
.and_then(normalized_display_title)
|
|
.map(|title| truncate_chars(&title, 48));
|
|
let suffix = if is_selfdev { " [self-dev]" } else { "" };
|
|
match display_title {
|
|
Some(title) => format!("{icon} {title}{suffix}"),
|
|
None => match fallback_label.and_then(normalized_display_title) {
|
|
Some(label) => format!("{icon} {label}{suffix}"),
|
|
None => format!("{icon}{suffix}"),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn terminal_session_label_for_id(session_id: &str) -> String {
|
|
let session_name = session_name(session_id);
|
|
let display_title = terminal_display_title_for_id(session_id);
|
|
match display_title.as_deref() {
|
|
Some(title) => terminal_session_label(&session_name, Some(title)),
|
|
None => session_name,
|
|
}
|
|
}
|
|
|
|
pub fn set_title(title: impl AsRef<str>) {
|
|
proctitle::set_title(title.as_ref());
|
|
set_killall_process_name();
|
|
}
|
|
|
|
fn set_killall_process_name() {
|
|
#[cfg(target_os = "linux")]
|
|
unsafe {
|
|
let mut name = [0u8; 16];
|
|
let bytes = KILLALL_PROCESS_NAME.as_bytes();
|
|
let len = bytes.len().min(name.len().saturating_sub(1));
|
|
name[..len].copy_from_slice(&bytes[..len]);
|
|
let _ = libc::prctl(libc::PR_SET_NAME, name.as_ptr(), 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
pub fn set_server_title(server_name: &str) {
|
|
set_title(compact_process_title("jcode:s:", Some(server_name)));
|
|
}
|
|
|
|
pub fn set_client_generic_title(is_selfdev: bool) {
|
|
let prefix = if is_selfdev {
|
|
"jcode:selfdev"
|
|
} else {
|
|
"jcode:client"
|
|
};
|
|
set_title(compact_process_title(prefix, None));
|
|
}
|
|
|
|
pub fn set_client_session_title(session_id: &str, is_selfdev: bool) {
|
|
set_client_display_title(&session_name(session_id), is_selfdev);
|
|
}
|
|
|
|
pub fn set_client_display_title(session_name: &str, is_selfdev: bool) {
|
|
let prefix = if is_selfdev { "jcode:d:" } else { "jcode:c:" };
|
|
set_title(compact_process_title(prefix, Some(session_name)));
|
|
}
|
|
|
|
pub fn set_client_remote_display_title(server_name: &str, session_name: &str, is_selfdev: bool) {
|
|
if server_name.is_empty() || server_name.eq_ignore_ascii_case("jcode") {
|
|
set_client_display_title(session_name, is_selfdev);
|
|
return;
|
|
}
|
|
let prefix = if is_selfdev { "jcode:d:" } else { "jcode:c:" };
|
|
set_title(format!("{prefix}{server_name}/{session_name}"));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::storage::lock_test_env;
|
|
|
|
#[test]
|
|
fn terminal_session_label_includes_custom_title_and_short_name() {
|
|
assert_eq!(
|
|
terminal_session_label("fox", Some("Release planning")),
|
|
"Release planning (fox)"
|
|
);
|
|
assert_eq!(terminal_session_label("fox", Some("Fox")), "Fox");
|
|
assert_eq!(terminal_session_label("fox", None), "Fox");
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_window_title_omits_product_and_animal_names() {
|
|
assert_eq!(
|
|
terminal_window_title(
|
|
"🐙",
|
|
Some("resume window title"),
|
|
Some("jcode Octopus"),
|
|
false
|
|
),
|
|
"🐙 resume window title"
|
|
);
|
|
assert_eq!(
|
|
terminal_window_title("🐙", None, Some("jcode Octopus"), false),
|
|
"🐙 jcode Octopus"
|
|
);
|
|
assert_eq!(
|
|
terminal_window_title(
|
|
"🐙",
|
|
Some("resume window title"),
|
|
Some("jcode Octopus"),
|
|
true
|
|
),
|
|
"🐙 resume window title [self-dev]"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_session_label_for_id_reads_custom_title_from_session() {
|
|
let _guard = lock_test_env();
|
|
let previous_home = std::env::var_os("JCODE_HOME");
|
|
let temp = tempfile::tempdir().expect("temp dir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
|
|
let mut session = crate::session::Session::create_with_id(
|
|
"session_fox_123".to_string(),
|
|
None,
|
|
Some("Generated title".to_string()),
|
|
);
|
|
session.rename_title(Some("Release planning".to_string()));
|
|
session.save().expect("save session");
|
|
|
|
assert_eq!(
|
|
terminal_session_label_for_id("session_fox_123"),
|
|
"Release planning (fox)"
|
|
);
|
|
|
|
if let Some(previous_home) = previous_home {
|
|
crate::env::set_var("JCODE_HOME", previous_home);
|
|
} else {
|
|
crate::env::remove_var("JCODE_HOME");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_session_label_for_id_prefers_todo_title_over_generated_title() {
|
|
let _guard = lock_test_env();
|
|
let previous_home = std::env::var_os("JCODE_HOME");
|
|
let temp = tempfile::tempdir().expect("temp dir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
|
|
let session_id = "session_fox_456";
|
|
let mut session = crate::session::Session::create_with_id(
|
|
session_id.to_string(),
|
|
None,
|
|
Some("Generated title".to_string()),
|
|
);
|
|
session.save().expect("save session");
|
|
crate::todo::save_todos(
|
|
session_id,
|
|
&[crate::todo::TodoItem {
|
|
content: "Synchronize terminal window names".to_string(),
|
|
status: "in_progress".to_string(),
|
|
priority: "high".to_string(),
|
|
id: "window-title".to_string(),
|
|
group: Some("resume title sync".to_string()),
|
|
confidence: Some(90),
|
|
completion_confidence: None,
|
|
confidence_history: Vec::new(),
|
|
blocked_by: Vec::new(),
|
|
assigned_to: None,
|
|
}],
|
|
)
|
|
.expect("save todos");
|
|
|
|
assert_eq!(
|
|
terminal_session_label_for_id(session_id),
|
|
"resume title sync (fox)"
|
|
);
|
|
|
|
if let Some(previous_home) = previous_home {
|
|
crate::env::set_var("JCODE_HOME", previous_home);
|
|
} else {
|
|
crate::env::remove_var("JCODE_HOME");
|
|
}
|
|
}
|
|
}
|