use anyhow::{Result, anyhow};
use tracing::debug;
use crate::cmd::Cmd;
use super::SIDEBAR_ROLE_VALUE;
use super::daemon_ctrl::kill_daemon;
use super::hooks::remove_hooks;
use super::layout_tree::{layout_after_sidebar_remove, reflow_after_sidebar_add};
pub(super) fn find_sidebar_in_window(window_id: &str) -> Result<bool> {
let output = Cmd::new("tmux")
.args(&["list-panes", "-t", window_id, "-F", "#{@workmux_role}"])
.run_and_capture_stdout()?;
Ok(output.lines().any(|l| l.trim() == SIDEBAR_ROLE_VALUE))
}
pub(super) fn create_sidebar_in_window(window_id: &str, width: u16) -> Result<()> {
if find_sidebar_in_window(window_id).unwrap_or(false) {
debug!(
window_id,
"create_sidebar_in_window: already exists, skipping"
);
return Ok(());
}
let exe = std::env::current_exe()?;
let exe_str = exe.to_str().ok_or_else(|| anyhow!("exe path not UTF-8"))?;
let width_str = width.to_string();
debug!(window_id, width, "create_sidebar_in_window: creating");
let target_pane = Cmd::new("tmux")
.args(&["list-panes", "-t", window_id, "-F", "#{pane_id}"])
.run_and_capture_stdout()?;
let target_pane = target_pane.lines().next().map(|l| l.trim()).unwrap_or("");
if target_pane.is_empty() {
return Ok(());
}
let new_pane_id = Cmd::new("tmux")
.args(&[
"split-window",
"-hbf",
"-l",
&width_str,
"-t",
target_pane,
"-d",
"-P",
"-F",
"#{pane_id}",
exe_str,
"_sidebar-run",
])
.run_and_capture_stdout()?
.trim()
.to_string();
Cmd::new("tmux")
.args(&[
"set-option",
"-p",
"-t",
&new_pane_id,
"@workmux_role",
SIDEBAR_ROLE_VALUE,
])
.run()?;
reflow_after_sidebar_add(window_id, &new_pane_id, width);
debug!(
window_id,
pane_id = new_pane_id.as_str(),
requested_width = width,
"create_sidebar_in_window: done"
);
Ok(())
}
pub(super) fn create_sidebars_in_all_windows(config: &crate::config::Config) -> Result<()> {
let output = Cmd::new("tmux")
.args(&["list-windows", "-a", "-F", "#{window_id} #{window_width}"])
.run_and_capture_stdout()?;
debug!("create_sidebars_in_all_windows: creating sidebars");
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (window_id, width_str) = line.split_once(' ').unwrap_or((line, "0"));
let window_w: u16 = width_str.parse().unwrap_or(0);
let width = super::resolve_width_for(config, window_w);
let _ = create_sidebar_in_window(window_id, width);
}
Ok(())
}
pub(super) fn create_sidebars_in_session(
session_id: &str,
config: &crate::config::Config,
) -> Result<()> {
let output = Cmd::new("tmux")
.args(&[
"list-windows",
"-t",
session_id,
"-F",
"#{window_id} #{window_width}",
])
.run_and_capture_stdout()?;
debug!(session_id, "create_sidebars_in_session: creating sidebars");
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (window_id, width_str) = line.split_once(' ').unwrap_or((line, "0"));
let window_w: u16 = width_str.parse().unwrap_or(0);
let width = super::resolve_width_for(config, window_w);
let _ = create_sidebar_in_window(window_id, width);
}
Ok(())
}
pub(super) fn kill_sidebars_in_session(session_id: &str) {
let output = Cmd::new("tmux")
.args(&[
"list-panes",
"-a",
"-F",
"#{session_id} #{window_id} #{pane_id} #{@workmux_role}",
])
.run_and_capture_stdout()
.unwrap_or_default();
let session_sidebars: Vec<(String, String)> = output
.lines()
.filter_map(|line| {
let mut parts = line.splitn(4, ' ');
let sid = parts.next()?;
let window_id = parts.next()?;
let pane_id = parts.next()?;
let role = parts.next()?.trim();
(role == SIDEBAR_ROLE_VALUE && sid == session_id)
.then(|| (window_id.to_string(), pane_id.to_string()))
})
.collect();
let layouts: Vec<_> = session_sidebars
.iter()
.map(|(window_id, pane_id)| layout_after_sidebar_remove(window_id, pane_id))
.collect();
for (_, pane_id) in &session_sidebars {
let _ = Cmd::new("tmux").args(&["kill-pane", "-t", pane_id]).run();
}
for (i, (window_id, _)) in session_sidebars.iter().enumerate() {
if let Some(layout) = &layouts[i] {
let _ = Cmd::new("tmux")
.args(&["select-layout", "-t", window_id, layout])
.run();
}
}
}
fn list_sidebar_panes() -> Vec<(String, String)> {
let output = Cmd::new("tmux")
.args(&[
"list-panes",
"-a",
"-F",
"#{window_id} #{pane_id} #{@workmux_role}",
])
.run_and_capture_stdout()
.unwrap_or_default();
output
.lines()
.filter_map(|line| {
let (window_id, rest) = line.split_once(' ')?;
let (pane_id, role) = rest.split_once(' ')?;
(role.trim() == SIDEBAR_ROLE_VALUE)
.then(|| (window_id.to_string(), pane_id.to_string()))
})
.collect()
}
pub(super) fn kill_all_sidebars_and_restore_layouts() {
let sidebars = list_sidebar_panes();
let layouts: Vec<_> = sidebars
.iter()
.map(|(window_id, pane_id)| layout_after_sidebar_remove(window_id, pane_id))
.collect();
for (_, pane_id) in &sidebars {
let _ = Cmd::new("tmux").args(&["kill-pane", "-t", pane_id]).run();
}
for (i, (window_id, _)) in sidebars.iter().enumerate() {
if let Some(layout) = &layouts[i] {
let _ = Cmd::new("tmux")
.args(&["select-layout", "-t", window_id, layout])
.run();
}
}
}
pub(super) fn shutdown_all_sidebars() {
let our_pane = Cmd::new("tmux")
.args(&["display-message", "-p", "#{pane_id}"])
.run_and_capture_stdout()
.unwrap_or_default()
.trim()
.to_string();
let our_window = Cmd::new("tmux")
.args(&["display-message", "-p", "#{window_id}"])
.run_and_capture_stdout()
.unwrap_or_default()
.trim()
.to_string();
let scope = super::current_scope();
let our_session_id = Cmd::new("tmux")
.args(&["display-message", "-p", "#{session_id}"])
.run_and_capture_stdout()
.ok()
.map(|s| s.trim().to_string())
.unwrap_or_default();
let sidebars = match &scope {
super::SidebarScope::Sessions(ids) if !our_session_id.is_empty() => {
let output = Cmd::new("tmux")
.args(&[
"list-panes",
"-a",
"-F",
"#{session_id} #{window_id} #{pane_id} #{@workmux_role}",
])
.run_and_capture_stdout()
.unwrap_or_default();
let our_sid = &our_session_id;
let sidebars: Vec<_> = output
.lines()
.filter_map(|line| {
let mut parts = line.splitn(4, ' ');
let sid = parts.next()?;
let wid = parts.next()?;
let pid = parts.next()?;
let role = parts.next()?.trim();
(role == SIDEBAR_ROLE_VALUE && sid == our_sid)
.then(|| (wid.to_string(), pid.to_string()))
})
.collect();
let mut remaining = ids.clone();
remaining.remove(our_sid);
if remaining.is_empty() {
super::set_scope(&super::SidebarScope::Off);
} else {
super::set_scope(&super::SidebarScope::Sessions(remaining));
}
sidebars
}
_ => list_sidebar_panes(),
};
let computed_layouts: Vec<_> = sidebars
.iter()
.map(|(window_id, pane_id)| layout_after_sidebar_remove(window_id, pane_id))
.collect();
let mut other_window_layouts = Vec::new();
let mut our_layout = None;
for (i, (window_id, pane_id)) in sidebars.iter().enumerate() {
if pane_id != &our_pane {
other_window_layouts.push((window_id.clone(), computed_layouts[i].clone()));
let _ = Cmd::new("tmux").args(&["kill-pane", "-t", pane_id]).run();
} else {
our_layout = computed_layouts[i].clone();
}
}
for (window_id, layout) in &other_window_layouts {
if let Some(layout) = layout {
let _ = Cmd::new("tmux")
.args(&["select-layout", "-t", window_id, layout])
.run();
}
}
let remaining_scope = super::current_scope();
let full_cleanup = !matches!(remaining_scope, super::SidebarScope::Sessions(_));
if full_cleanup {
kill_daemon();
remove_hooks();
super::clear_sidebar_globals();
}
if !our_window.is_empty()
&& let Some(layout) = our_layout
{
let cmd = format!(
"sleep 0.1; tmux select-layout -t {win} '{layout}' 2>/dev/null",
win = our_window,
layout = layout,
);
let _ = Cmd::new("tmux").args(&["run-shell", "-b", &cmd]).run();
}
}