994 lines
32 KiB
Rust
994 lines
32 KiB
Rust
//! Metadata response builders for local-control introspection actions.
|
|
#[cfg(test)]
|
|
#[path = "metadata_tests.rs"]
|
|
mod tests;
|
|
use ::local_control::protocol::{
|
|
ActionNameParams, ActiveTargetChain, PaneTarget, SessionTarget, SurfaceListResult,
|
|
SurfaceSummary, TabTarget, TargetSelector, WindowTarget,
|
|
};
|
|
use ::local_control::{
|
|
Action, ActionKind, ActionMetadata, ControlError, ErrorCode, InstanceId, PROTOCOL_VERSION,
|
|
};
|
|
use serde::Serialize;
|
|
use serde_json::{json, Value};
|
|
use settings::Setting as _;
|
|
use galaxy_core::channel::ChannelState;
|
|
use warpui::{AppContext, ModelContext, SingletonEntity, ViewHandle, WindowId};
|
|
|
|
use crate::drive::settings::WarpDriveSettings;
|
|
use crate::features::FeatureFlag;
|
|
use crate::local_control::resolver::{reject_target_families, require_active_window_id_for_action};
|
|
use crate::local_control::LocalControlBridge;
|
|
use crate::pane_group::{PaneGroup, PaneId};
|
|
use crate::settings::{AISettings, CodeSettings};
|
|
use crate::workspace::tab_settings::TabSettings;
|
|
use crate::workspace::Workspace;
|
|
|
|
#[derive(Serialize)]
|
|
struct InstanceResponse<'a> {
|
|
action: &'static str,
|
|
instance_id: Option<&'a str>,
|
|
pid: u32,
|
|
channel: String,
|
|
app_id: String,
|
|
protocol_version: u32,
|
|
actions: Vec<ActionMetadata>,
|
|
}
|
|
|
|
fn active_session_target(target: &TargetSelector) -> TargetSelector {
|
|
if !matches!(target.session, Some(SessionTarget::Active)) {
|
|
return target.clone();
|
|
}
|
|
TargetSelector {
|
|
window: target.window.clone().or(Some(WindowTarget::Active)),
|
|
tab: target.tab.clone().or(Some(TabTarget::Active)),
|
|
pane: target.pane.clone().or(Some(PaneTarget::Active)),
|
|
session: target.session.clone(),
|
|
}
|
|
}
|
|
|
|
fn select_session_entries(
|
|
entries: Vec<SessionEntry>,
|
|
session: Option<&SessionTarget>,
|
|
action: ActionKind,
|
|
) -> Result<Vec<SessionEntry>, ControlError> {
|
|
match session {
|
|
None => Ok(entries),
|
|
Some(SessionTarget::Active) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.is_active)
|
|
.collect(),
|
|
action,
|
|
"active session",
|
|
ErrorCode::MissingTarget,
|
|
),
|
|
Some(SessionTarget::Id { id }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.pane_id.to_string() == id.0)
|
|
.collect(),
|
|
action,
|
|
"session id",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct PingResponse<'a> {
|
|
action: &'static str,
|
|
ok: bool,
|
|
instance_id: Option<&'a str>,
|
|
protocol_version: u32,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct VersionResponse<'a> {
|
|
action: &'static str,
|
|
instance_id: Option<&'a str>,
|
|
protocol_version: u32,
|
|
channel: String,
|
|
app_id: String,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(super) struct WindowEntry {
|
|
pub(super) window_id: WindowId,
|
|
pub(super) index: usize,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct TabEntry {
|
|
pub(super) window_id: WindowId,
|
|
pub(super) window_index: usize,
|
|
pub(super) index: usize,
|
|
pub(super) workspace_active_tab_index: usize,
|
|
pub(super) pane_group: ViewHandle<PaneGroup>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct PaneEntry {
|
|
pub(super) window_id: WindowId,
|
|
pub(super) window_index: usize,
|
|
pub(super) tab_id: String,
|
|
pub(super) tab_index: usize,
|
|
pub(super) index: usize,
|
|
pub(super) pane_group: ViewHandle<PaneGroup>,
|
|
pub(super) pane_id: PaneId,
|
|
}
|
|
|
|
struct SessionEntry {
|
|
window_id: WindowId,
|
|
window_index: usize,
|
|
tab_id: String,
|
|
tab_index: usize,
|
|
pane_id: PaneId,
|
|
pane_index: usize,
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) enum SurfaceDestination {
|
|
Settings,
|
|
CommandPalette,
|
|
CommandSearch,
|
|
ThemePicker,
|
|
Keybindings,
|
|
WarpDrive,
|
|
ResourceCenter,
|
|
AiAssistant,
|
|
CodeReview,
|
|
ProjectExplorer,
|
|
GlobalSearch,
|
|
ConversationList,
|
|
LeftPanel,
|
|
RightPanel,
|
|
VerticalTabs,
|
|
AgentManagement,
|
|
}
|
|
|
|
impl SurfaceDestination {
|
|
const ALL: &[Self] = &[
|
|
Self::Settings,
|
|
Self::CommandPalette,
|
|
Self::CommandSearch,
|
|
Self::ThemePicker,
|
|
Self::Keybindings,
|
|
Self::WarpDrive,
|
|
Self::ResourceCenter,
|
|
Self::AiAssistant,
|
|
Self::CodeReview,
|
|
Self::ProjectExplorer,
|
|
Self::GlobalSearch,
|
|
Self::ConversationList,
|
|
Self::LeftPanel,
|
|
Self::RightPanel,
|
|
Self::VerticalTabs,
|
|
Self::AgentManagement,
|
|
];
|
|
|
|
fn name(self) -> &'static str {
|
|
match self {
|
|
Self::Settings => "settings",
|
|
Self::CommandPalette => "command_palette",
|
|
Self::CommandSearch => "command_search",
|
|
Self::ThemePicker => "theme_picker",
|
|
Self::Keybindings => "keybindings",
|
|
Self::WarpDrive => "warp_drive",
|
|
Self::ResourceCenter => "resource_center",
|
|
Self::AiAssistant => "ai_assistant",
|
|
Self::CodeReview => "code_review",
|
|
Self::ProjectExplorer => "project_explorer",
|
|
Self::GlobalSearch => "global_search",
|
|
Self::ConversationList => "conversation_list",
|
|
Self::LeftPanel => "left_panel",
|
|
Self::RightPanel => "right_panel",
|
|
Self::VerticalTabs => "vertical_tabs",
|
|
Self::AgentManagement => "agent_management",
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn instance(
|
|
instance_id: &Option<InstanceId>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
to_json_value(InstanceResponse {
|
|
action: ActionKind::InstanceList.as_str(),
|
|
instance_id: instance_id.as_ref().map(|id| id.0.as_str()),
|
|
pid: std::process::id(),
|
|
channel: ChannelState::channel().to_string(),
|
|
app_id: ChannelState::app_id().to_string(),
|
|
protocol_version: PROTOCOL_VERSION,
|
|
actions: ActionKind::implemented_metadata(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn ping(instance_id: &Option<InstanceId>) -> Result<serde_json::Value, ControlError> {
|
|
to_json_value(PingResponse {
|
|
action: ActionKind::AppPing.as_str(),
|
|
ok: true,
|
|
instance_id: instance_id.as_ref().map(|id| id.0.as_str()),
|
|
protocol_version: PROTOCOL_VERSION,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn version(instance_id: &Option<InstanceId>) -> Result<serde_json::Value, ControlError> {
|
|
to_json_value(VersionResponse {
|
|
action: ActionKind::AppVersion.as_str(),
|
|
instance_id: instance_id.as_ref().map(|id| id.0.as_str()),
|
|
protocol_version: PROTOCOL_VERSION,
|
|
channel: ChannelState::channel().to_string(),
|
|
app_id: ChannelState::app_id().to_string(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn active(
|
|
instance_id: &Option<InstanceId>,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
Ok(json!({
|
|
"action": ActionKind::AppActive.as_str(),
|
|
"active": active_chain(instance_id, ctx)?,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn inspect(
|
|
instance_id: &Option<InstanceId>,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
Ok(json!({
|
|
"action": ActionKind::InstanceInspect.as_str(),
|
|
"instance_id": instance_id.as_ref().map(|id| id.0.as_str()),
|
|
"pid": std::process::id(),
|
|
"channel": ChannelState::channel().to_string(),
|
|
"app_id": ChannelState::app_id().to_string(),
|
|
"protocol_version": PROTOCOL_VERSION,
|
|
"active": active_chain(instance_id, ctx)?,
|
|
"actions": ActionKind::implemented_metadata(),
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn action_list() -> serde_json::Value {
|
|
json!({
|
|
"action": ActionKind::ActionList.as_str(),
|
|
"actions": ActionKind::implemented_metadata(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn action_inspect(action: &Action) -> Result<serde_json::Value, ControlError> {
|
|
let params = action_name_params(action)?;
|
|
let metadata = action_metadata_for_name(¶ms.action)?;
|
|
Ok(json!({
|
|
"action": ActionKind::ActionInspect.as_str(),
|
|
"metadata": metadata,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn capability_list() -> serde_json::Value {
|
|
json!({
|
|
"action": ActionKind::CapabilityList.as_str(),
|
|
"capabilities": ActionKind::implemented_metadata(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn capability_inspect(action: &Action) -> Result<serde_json::Value, ControlError> {
|
|
let params = action_name_params(action)?;
|
|
let metadata = action_metadata_for_name(¶ms.action)?;
|
|
Ok(json!({
|
|
"action": ActionKind::CapabilityInspect.as_str(),
|
|
"capability": metadata,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn surface_list(
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
to_json_value(SurfaceListResult {
|
|
surfaces: SurfaceDestination::ALL
|
|
.iter()
|
|
.copied()
|
|
.map(|destination| {
|
|
let unavailable_reason =
|
|
surface_unavailable_reason(destination, ctx).map(str::to_owned);
|
|
SurfaceSummary {
|
|
name: destination.name().to_owned(),
|
|
is_available: unavailable_reason.is_none(),
|
|
unavailable_reason,
|
|
}
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn surface_unavailable_reason(
|
|
destination: SurfaceDestination,
|
|
ctx: &AppContext,
|
|
) -> Option<&'static str> {
|
|
match destination {
|
|
SurfaceDestination::Settings
|
|
| SurfaceDestination::CommandPalette
|
|
| SurfaceDestination::CommandSearch
|
|
| SurfaceDestination::ThemePicker
|
|
| SurfaceDestination::Keybindings
|
|
| SurfaceDestination::ResourceCenter => None,
|
|
SurfaceDestination::WarpDrive if !WarpDriveSettings::is_warp_drive_enabled(ctx) => {
|
|
Some("Warp Drive is disabled")
|
|
}
|
|
SurfaceDestination::WarpDrive => None,
|
|
SurfaceDestination::AiAssistant if !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) => {
|
|
Some("AI features are disabled")
|
|
}
|
|
SurfaceDestination::AiAssistant => None,
|
|
SurfaceDestination::CodeReview | SurfaceDestination::RightPanel
|
|
if !cfg!(feature = "local_fs") =>
|
|
{
|
|
Some("code review is unavailable without local filesystem support")
|
|
}
|
|
SurfaceDestination::CodeReview | SurfaceDestination::RightPanel => None,
|
|
SurfaceDestination::ProjectExplorer
|
|
if !cfg!(feature = "local_fs")
|
|
|| !*CodeSettings::as_ref(ctx).show_project_explorer.value() =>
|
|
{
|
|
Some("project explorer is unavailable or disabled")
|
|
}
|
|
SurfaceDestination::ProjectExplorer => None,
|
|
SurfaceDestination::GlobalSearch
|
|
if !cfg!(feature = "local_fs")
|
|
|| !FeatureFlag::GlobalSearch.is_enabled()
|
|
|| !*CodeSettings::as_ref(ctx).show_global_search.value() =>
|
|
{
|
|
Some("global search is unavailable or disabled")
|
|
}
|
|
SurfaceDestination::GlobalSearch => None,
|
|
SurfaceDestination::ConversationList
|
|
if !FeatureFlag::AgentViewConversationListView.is_enabled()
|
|
|| !AISettings::as_ref(ctx).is_any_ai_enabled(ctx)
|
|
|| !*AISettings::as_ref(ctx).show_conversation_history.value() =>
|
|
{
|
|
Some("agent conversation history is unavailable or disabled")
|
|
}
|
|
SurfaceDestination::ConversationList => None,
|
|
SurfaceDestination::LeftPanel
|
|
if surface_unavailable_reason(SurfaceDestination::ProjectExplorer, ctx).is_some()
|
|
&& surface_unavailable_reason(SurfaceDestination::GlobalSearch, ctx).is_some()
|
|
&& surface_unavailable_reason(SurfaceDestination::ConversationList, ctx)
|
|
.is_some()
|
|
&& surface_unavailable_reason(SurfaceDestination::WarpDrive, ctx).is_some() =>
|
|
{
|
|
Some("the left panel has no available views")
|
|
}
|
|
SurfaceDestination::LeftPanel => None,
|
|
SurfaceDestination::VerticalTabs
|
|
if !FeatureFlag::VerticalTabs.is_enabled()
|
|
|| !*TabSettings::as_ref(ctx).use_vertical_tabs.value() =>
|
|
{
|
|
Some("vertical tabs are unavailable or disabled")
|
|
}
|
|
SurfaceDestination::VerticalTabs => None,
|
|
SurfaceDestination::AgentManagement
|
|
if !FeatureFlag::AgentManagementView.is_enabled()
|
|
|| !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) =>
|
|
{
|
|
Some("agent management is unavailable or disabled")
|
|
}
|
|
SurfaceDestination::AgentManagement => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn window_list(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::WindowList,
|
|
target.tab.is_some() || target.pane.is_some() || target.session.is_some(),
|
|
"tab, pane, or session selectors",
|
|
)?;
|
|
let active_window = ctx.windows().active_window();
|
|
let mut windows = Vec::new();
|
|
for entry in select_window_entries(target, false, ActionKind::WindowList, ctx)? {
|
|
windows.push(json!({
|
|
"window_id": entry.window_id.to_string(),
|
|
"index": entry.index as u32,
|
|
"is_active": Some(entry.window_id) == active_window,
|
|
"has_workspace": workspace_for_window(entry.window_id, ActionKind::WindowList, ctx)?.is_some(),
|
|
}));
|
|
}
|
|
Ok(json!({
|
|
"action": ActionKind::WindowList.as_str(),
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn window_inspect(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::WindowInspect,
|
|
target.tab.is_some() || target.pane.is_some() || target.session.is_some(),
|
|
"tab, pane, or session selectors",
|
|
)?;
|
|
let target = TargetSelector {
|
|
window: target.window.clone().or(Some(WindowTarget::Active)),
|
|
tab: None,
|
|
pane: None,
|
|
session: None,
|
|
};
|
|
let data = window_list(&target, ctx)?;
|
|
let window = single_entry(data.get("windows"), ActionKind::WindowInspect)?;
|
|
Ok(json!({
|
|
"action": ActionKind::WindowInspect.as_str(),
|
|
"window": window,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn tab_list(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::TabList,
|
|
target.pane.is_some() || target.session.is_some(),
|
|
"pane or session selectors",
|
|
)?;
|
|
let entries = select_tab_entries(target, ActionKind::TabList, ctx)?;
|
|
let tabs = entries
|
|
.into_iter()
|
|
.map(|entry| {
|
|
json!({
|
|
"tab_id": entry.pane_group.id().to_string(),
|
|
"window_id": entry.window_id.to_string(),
|
|
"window_index": entry.window_index as u32,
|
|
"index": entry.index as u32,
|
|
"is_active": entry.index == entry.workspace_active_tab_index,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
Ok(json!({
|
|
"action": ActionKind::TabList.as_str(),
|
|
"tabs": tabs,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn tab_inspect(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::TabInspect,
|
|
target.pane.is_some() || target.session.is_some(),
|
|
"pane or session selectors",
|
|
)?;
|
|
let target = TargetSelector {
|
|
window: target.window.clone(),
|
|
tab: target.tab.clone().or(Some(TabTarget::Active)),
|
|
pane: None,
|
|
session: None,
|
|
};
|
|
let data = tab_list(&target, ctx)?;
|
|
let tab = single_entry(data.get("tabs"), ActionKind::TabInspect)?;
|
|
Ok(json!({
|
|
"action": ActionKind::TabInspect.as_str(),
|
|
"tab": tab,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn pane_list(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::PaneList,
|
|
target.session.is_some(),
|
|
"session selectors",
|
|
)?;
|
|
let entries = select_pane_entries(target, ActionKind::PaneList, ctx)?;
|
|
let mut panes = Vec::new();
|
|
for entry in entries {
|
|
let (is_active, has_terminal_session) = entry.pane_group.read(ctx, |pane_group, ctx| {
|
|
(
|
|
pane_group.focused_pane_id(ctx) == entry.pane_id,
|
|
pane_group
|
|
.terminal_view_from_pane_id(entry.pane_id, ctx)
|
|
.is_some(),
|
|
)
|
|
});
|
|
panes.push(json!({
|
|
"pane_id": entry.pane_id.to_string(),
|
|
"tab_id": entry.tab_id,
|
|
"tab_index": entry.tab_index as u32,
|
|
"window_id": entry.window_id.to_string(),
|
|
"window_index": entry.window_index as u32,
|
|
"index": entry.index as u32,
|
|
"is_active": is_active,
|
|
"has_terminal_session": has_terminal_session,
|
|
}));
|
|
}
|
|
Ok(json!({
|
|
"action": ActionKind::PaneList.as_str(),
|
|
"panes": panes,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn pane_inspect(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
reject_target_families(
|
|
ActionKind::PaneInspect,
|
|
target.session.is_some(),
|
|
"session selectors",
|
|
)?;
|
|
let target = TargetSelector {
|
|
window: target.window.clone(),
|
|
tab: target.tab.clone(),
|
|
pane: target.pane.clone().or(Some(PaneTarget::Active)),
|
|
session: None,
|
|
};
|
|
let data = pane_list(&target, ctx)?;
|
|
let pane = single_entry(data.get("panes"), ActionKind::PaneInspect)?;
|
|
Ok(json!({
|
|
"action": ActionKind::PaneInspect.as_str(),
|
|
"pane": pane,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn session_list(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
let target = active_session_target(target);
|
|
let pane_entries = select_pane_entries(&target, ActionKind::SessionList, ctx)?;
|
|
let entries = select_session_entries(
|
|
session_entries_for_panes(pane_entries, ctx),
|
|
target.session.as_ref(),
|
|
ActionKind::SessionList,
|
|
)?;
|
|
let sessions = session_values(entries);
|
|
Ok(json!({
|
|
"action": ActionKind::SessionList.as_str(),
|
|
"sessions": sessions,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn session_inspect(
|
|
target: &TargetSelector,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<serde_json::Value, ControlError> {
|
|
let target = active_session_target(target);
|
|
let pane_entries = select_pane_entries(&target, ActionKind::SessionInspect, ctx)?;
|
|
let entries = select_session_entries(
|
|
session_entries_for_panes(pane_entries, ctx),
|
|
target.session.as_ref().or(Some(&SessionTarget::Active)),
|
|
ActionKind::SessionInspect,
|
|
)?;
|
|
let data = json!({ "sessions": session_values(entries) });
|
|
let session = single_entry(data.get("sessions"), ActionKind::SessionInspect)?;
|
|
Ok(json!({
|
|
"action": ActionKind::SessionInspect.as_str(),
|
|
"session": session,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn action_metadata_for_name(action_name: &str) -> Result<ActionMetadata, ControlError> {
|
|
ActionKind::implemented_metadata()
|
|
.into_iter()
|
|
.find(|metadata| metadata.name == action_name)
|
|
.ok_or_else(|| {
|
|
ControlError::new(
|
|
ErrorCode::NotAllowlisted,
|
|
"requested action is not an implemented local-control action",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn action_name_params(action: &Action) -> Result<ActionNameParams, ControlError> {
|
|
action.params_as()
|
|
}
|
|
|
|
fn to_json_value<T: Serialize>(response: T) -> Result<serde_json::Value, ControlError> {
|
|
serde_json::to_value(response).map_err(|error| {
|
|
ControlError::with_details(
|
|
ErrorCode::Internal,
|
|
"failed to serialize local-control metadata response",
|
|
error.to_string(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn active_chain(
|
|
instance_id: &Option<InstanceId>,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<ActiveTargetChain, ControlError> {
|
|
let instance_id = instance_id.as_ref().map(|id| id.0.clone());
|
|
let Some(window_id) = ctx.windows().active_window() else {
|
|
return Ok(ActiveTargetChain {
|
|
instance_id,
|
|
window_id: None,
|
|
tab_id: None,
|
|
pane_id: None,
|
|
session_id: None,
|
|
});
|
|
};
|
|
let window_id_string = window_id.to_string();
|
|
let Some(workspace) = workspace_for_window(window_id, ActionKind::AppActive, ctx)? else {
|
|
return Ok(ActiveTargetChain {
|
|
instance_id,
|
|
window_id: Some(window_id_string),
|
|
tab_id: None,
|
|
pane_id: None,
|
|
session_id: None,
|
|
});
|
|
};
|
|
let (tab_id, pane_id, session_id) = workspace.read(ctx, |workspace, ctx| {
|
|
let pane_group = workspace.active_tab_pane_group();
|
|
let pane_group_ref = pane_group.as_ref(ctx);
|
|
(
|
|
Some(pane_group.id().to_string()),
|
|
Some(pane_group_ref.focused_pane_id(ctx).to_string()),
|
|
pane_group_ref
|
|
.active_session_id(ctx)
|
|
.map(|session_id| PaneId::from(session_id).to_string()),
|
|
)
|
|
});
|
|
Ok(ActiveTargetChain {
|
|
instance_id,
|
|
window_id: Some(window_id_string),
|
|
tab_id,
|
|
pane_id,
|
|
session_id,
|
|
})
|
|
}
|
|
|
|
fn select_window_entries(
|
|
target: &TargetSelector,
|
|
force_active_default: bool,
|
|
action: ActionKind,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<Vec<WindowEntry>, ControlError> {
|
|
let entries = window_entries(ctx);
|
|
match target.window.as_ref() {
|
|
None if force_active_default => {
|
|
let active =
|
|
require_active_window_id_for_action(ctx.windows().active_window(), action)?;
|
|
explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.window_id == active)
|
|
.collect(),
|
|
action,
|
|
"active window",
|
|
ErrorCode::MissingTarget,
|
|
)
|
|
}
|
|
None => Ok(entries),
|
|
Some(WindowTarget::Active) => {
|
|
let active =
|
|
require_active_window_id_for_action(ctx.windows().active_window(), action)?;
|
|
explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.window_id == active)
|
|
.collect(),
|
|
action,
|
|
"active window",
|
|
ErrorCode::MissingTarget,
|
|
)
|
|
}
|
|
Some(WindowTarget::Id { id }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.window_id.to_string() == id.0)
|
|
.collect(),
|
|
action,
|
|
"window id",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
Some(WindowTarget::Index { index }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.index as u32 == *index)
|
|
.collect(),
|
|
action,
|
|
"window index",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
Some(WindowTarget::Title { .. }) => Err(ControlError::new(
|
|
ErrorCode::InvalidSelector,
|
|
format!(
|
|
"{} only supports active, opaque window id, and window index selectors",
|
|
action.as_str()
|
|
),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn select_tab_entries(
|
|
target: &TargetSelector,
|
|
action: ActionKind,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<Vec<TabEntry>, ControlError> {
|
|
let force_active_window = matches!(
|
|
target.tab,
|
|
Some(TabTarget::Active | TabTarget::Index { .. })
|
|
) || matches!(
|
|
target.pane,
|
|
Some(PaneTarget::Active | PaneTarget::Index { .. })
|
|
);
|
|
let windows = select_window_entries(target, force_active_window, action, ctx)?;
|
|
let entries = tab_entries_for_windows(windows, action, ctx)?;
|
|
let requires_active_tab_default = matches!(
|
|
target.pane,
|
|
Some(PaneTarget::Active | PaneTarget::Index { .. })
|
|
);
|
|
match target.tab.as_ref() {
|
|
None if requires_active_tab_default => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.index == entry.workspace_active_tab_index)
|
|
.collect(),
|
|
action,
|
|
"active tab",
|
|
ErrorCode::MissingTarget,
|
|
),
|
|
None => Ok(entries),
|
|
Some(TabTarget::Active) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.index == entry.workspace_active_tab_index)
|
|
.collect(),
|
|
action,
|
|
"active tab",
|
|
ErrorCode::MissingTarget,
|
|
),
|
|
Some(TabTarget::Id { id }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.pane_group.id().to_string() == id.0)
|
|
.collect(),
|
|
action,
|
|
"tab id",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
Some(TabTarget::Index { index }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.index as u32 == *index)
|
|
.collect(),
|
|
action,
|
|
"tab index",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
Some(TabTarget::Title { .. }) => Err(ControlError::new(
|
|
ErrorCode::InvalidSelector,
|
|
format!(
|
|
"{} only supports active, opaque tab id, and tab index selectors",
|
|
action.as_str()
|
|
),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn select_pane_entries(
|
|
target: &TargetSelector,
|
|
action: ActionKind,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<Vec<PaneEntry>, ControlError> {
|
|
let tabs = select_tab_entries(target, action, ctx)?;
|
|
let entries = pane_entries_for_tabs(tabs, ctx);
|
|
match target.pane.as_ref() {
|
|
None => Ok(entries),
|
|
Some(PaneTarget::Active) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| {
|
|
entry.pane_group.read(ctx, |pane_group, ctx| {
|
|
pane_group.focused_pane_id(ctx) == entry.pane_id
|
|
})
|
|
})
|
|
.collect(),
|
|
action,
|
|
"active pane",
|
|
ErrorCode::MissingTarget,
|
|
),
|
|
Some(PaneTarget::Id { id }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.pane_id.to_string() == id.0)
|
|
.collect(),
|
|
action,
|
|
"pane id",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
Some(PaneTarget::Index { index }) => explicit_matches(
|
|
entries
|
|
.into_iter()
|
|
.filter(|entry| entry.index as u32 == *index)
|
|
.collect(),
|
|
action,
|
|
"pane index",
|
|
ErrorCode::StaleTarget,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn window_entries(ctx: &mut ModelContext<LocalControlBridge>) -> Vec<WindowEntry> {
|
|
let mut ids = ctx.window_ids().collect::<Vec<_>>();
|
|
ids.sort_by_key(ToString::to_string);
|
|
ids.into_iter()
|
|
.enumerate()
|
|
.map(|(index, window_id)| WindowEntry { window_id, index })
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn tab_entries_for_windows(
|
|
windows: Vec<WindowEntry>,
|
|
action: ActionKind,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<Vec<TabEntry>, ControlError> {
|
|
let mut entries = Vec::new();
|
|
for window in windows {
|
|
let Some(workspace) = workspace_for_window(window.window_id, action, ctx)? else {
|
|
continue;
|
|
};
|
|
entries.extend(workspace.read(ctx, |workspace, _| {
|
|
workspace
|
|
.tab_views()
|
|
.enumerate()
|
|
.map(|(index, pane_group)| TabEntry {
|
|
window_id: window.window_id,
|
|
window_index: window.index,
|
|
index,
|
|
workspace_active_tab_index: workspace.active_tab_index(),
|
|
pane_group: pane_group.clone(),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
}));
|
|
}
|
|
Ok(entries)
|
|
}
|
|
|
|
pub(super) fn pane_entries_for_tabs(
|
|
tabs: Vec<TabEntry>,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Vec<PaneEntry> {
|
|
let mut entries = Vec::new();
|
|
for tab in tabs {
|
|
let tab_id = tab.pane_group.id().to_string();
|
|
let pane_group = tab.pane_group.clone();
|
|
let pane_ids = tab
|
|
.pane_group
|
|
.read(ctx, |pane_group, _| pane_group.visible_pane_ids());
|
|
entries.extend(
|
|
pane_ids
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, pane_id)| PaneEntry {
|
|
window_id: tab.window_id,
|
|
window_index: tab.window_index,
|
|
tab_id: tab_id.clone(),
|
|
tab_index: tab.index,
|
|
index,
|
|
pane_group: pane_group.clone(),
|
|
pane_id,
|
|
}),
|
|
);
|
|
}
|
|
entries
|
|
}
|
|
|
|
fn session_entries_for_panes(
|
|
panes: Vec<PaneEntry>,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Vec<SessionEntry> {
|
|
let mut entries = Vec::new();
|
|
for pane in panes {
|
|
let (has_terminal_session, is_active) = pane.pane_group.read(ctx, |pane_group, ctx| {
|
|
(
|
|
pane_group
|
|
.terminal_view_from_pane_id(pane.pane_id, ctx)
|
|
.is_some(),
|
|
pane_group.active_session_id(ctx).map(PaneId::from) == Some(pane.pane_id),
|
|
)
|
|
});
|
|
if has_terminal_session {
|
|
entries.push(SessionEntry {
|
|
window_id: pane.window_id,
|
|
window_index: pane.window_index,
|
|
tab_id: pane.tab_id,
|
|
tab_index: pane.tab_index,
|
|
pane_id: pane.pane_id,
|
|
pane_index: pane.index,
|
|
is_active,
|
|
});
|
|
}
|
|
}
|
|
entries
|
|
}
|
|
|
|
fn session_values(entries: Vec<SessionEntry>) -> Vec<Value> {
|
|
entries
|
|
.into_iter()
|
|
.map(|entry| {
|
|
json!({
|
|
"session_id": entry.pane_id.to_string(),
|
|
"pane_id": entry.pane_id.to_string(),
|
|
"pane_index": entry.pane_index as u32,
|
|
"tab_id": entry.tab_id,
|
|
"tab_index": entry.tab_index as u32,
|
|
"window_id": entry.window_id.to_string(),
|
|
"window_index": entry.window_index as u32,
|
|
"is_active": entry.is_active,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn workspace_for_window(
|
|
window_id: WindowId,
|
|
action: ActionKind,
|
|
ctx: &mut ModelContext<LocalControlBridge>,
|
|
) -> Result<Option<ViewHandle<Workspace>>, ControlError> {
|
|
match ctx.views_of_type::<Workspace>(window_id) {
|
|
None => Ok(None),
|
|
Some(workspaces) => match workspaces.as_slice() {
|
|
[] => Ok(None),
|
|
[workspace] => Ok(Some(workspace.clone())),
|
|
_ => Err(ControlError::new(
|
|
ErrorCode::AmbiguousTarget,
|
|
format!(
|
|
"{} resolved multiple workspaces in one window",
|
|
action.as_str()
|
|
),
|
|
)),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn explicit_matches<T>(
|
|
matches: Vec<T>,
|
|
action: ActionKind,
|
|
selector: &str,
|
|
missing_code: ErrorCode,
|
|
) -> Result<Vec<T>, ControlError> {
|
|
match matches.len() {
|
|
0 => Err(ControlError::new(
|
|
missing_code,
|
|
format!(
|
|
"{} cannot resolve the requested {selector}",
|
|
action.as_str()
|
|
),
|
|
)),
|
|
1 => Ok(matches),
|
|
_ => Err(ControlError::new(
|
|
ErrorCode::AmbiguousTarget,
|
|
format!(
|
|
"{} resolved multiple targets by {selector}",
|
|
action.as_str()
|
|
),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn single_entry(value: Option<&Value>, action: ActionKind) -> Result<Value, ControlError> {
|
|
let Some(items) = value.and_then(Value::as_array) else {
|
|
return Err(ControlError::new(
|
|
ErrorCode::Internal,
|
|
format!("{} handler returned malformed metadata", action.as_str()),
|
|
));
|
|
};
|
|
match items.as_slice() {
|
|
[item] => Ok(item.clone()),
|
|
[] => Err(ControlError::new(
|
|
ErrorCode::MissingTarget,
|
|
format!("{} could not resolve a target", action.as_str()),
|
|
)),
|
|
_ => Err(ControlError::new(
|
|
ErrorCode::AmbiguousTarget,
|
|
format!("{} resolved multiple targets", action.as_str()),
|
|
)),
|
|
}
|
|
}
|