Rust-based tmux project launcher with fzf selection, incremental mtime-based cache, per-project .tmuxido.toml session config, and Drone CI pipeline for automated binary releases.
This commit is contained in:
+156
@@ -0,0 +1,156 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProjectCache {
|
||||
pub projects: Vec<PathBuf>,
|
||||
pub last_updated: u64,
|
||||
/// mtime de cada diretório visitado durante o scan.
|
||||
/// Usado para detectar mudanças incrementais sem precisar varrer tudo.
|
||||
#[serde(default)]
|
||||
pub dir_mtimes: HashMap<PathBuf, u64>,
|
||||
}
|
||||
|
||||
fn now_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn mtime_secs(time: SystemTime) -> u64 {
|
||||
time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
|
||||
}
|
||||
|
||||
/// Retorna o subconjunto mínimo de diretórios: aqueles que não têm nenhum
|
||||
/// ancestral também na lista. Evita rescanear a mesma subárvore duas vezes.
|
||||
fn minimal_roots(dirs: &[PathBuf]) -> Vec<PathBuf> {
|
||||
dirs.iter()
|
||||
.filter(|dir| !dirs.iter().any(|other| other != *dir && dir.starts_with(other)))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProjectCache {
|
||||
pub fn new(projects: Vec<PathBuf>, dir_mtimes: HashMap<PathBuf, u64>) -> Self {
|
||||
Self {
|
||||
projects,
|
||||
last_updated: now_secs(),
|
||||
dir_mtimes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_path() -> Result<PathBuf> {
|
||||
let cache_dir = dirs::cache_dir()
|
||||
.context("Could not determine cache directory")?
|
||||
.join("tmuxido");
|
||||
|
||||
fs::create_dir_all(&cache_dir)
|
||||
.with_context(|| format!("Failed to create cache directory: {}", cache_dir.display()))?;
|
||||
|
||||
Ok(cache_dir.join("projects.json"))
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Option<Self>> {
|
||||
let cache_path = Self::cache_path()?;
|
||||
|
||||
if !cache_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&cache_path)
|
||||
.with_context(|| format!("Failed to read cache file: {}", cache_path.display()))?;
|
||||
|
||||
let cache: ProjectCache = serde_json::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse cache file: {}", cache_path.display()))?;
|
||||
|
||||
Ok(Some(cache))
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let cache_path = Self::cache_path()?;
|
||||
|
||||
let content = serde_json::to_string_pretty(self)
|
||||
.context("Failed to serialize cache")?;
|
||||
|
||||
fs::write(&cache_path, content)
|
||||
.with_context(|| format!("Failed to write cache file: {}", cache_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Valida e atualiza o cache de forma incremental.
|
||||
///
|
||||
/// 1. Remove projetos cujo `.git` não existe mais.
|
||||
/// 2. Detecta diretórios com mtime alterado.
|
||||
/// 3. Resscaneia apenas as subárvores mínimas que mudaram.
|
||||
///
|
||||
/// Retorna `true` se o cache foi modificado.
|
||||
/// Retorna `false` com `dir_mtimes` vazio (cache antigo) — chamador deve fazer rescan completo.
|
||||
pub fn validate_and_update(
|
||||
&mut self,
|
||||
scan_fn: &dyn Fn(&Path) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)>,
|
||||
) -> Result<bool> {
|
||||
let mut changed = false;
|
||||
|
||||
// Passo 1: remover projetos cujo .git não existe mais
|
||||
let before = self.projects.len();
|
||||
self.projects.retain(|p| p.join(".git").exists());
|
||||
if self.projects.len() != before {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Sem fingerprints = cache no formato antigo; sinaliza ao chamador
|
||||
if self.dir_mtimes.is_empty() {
|
||||
return Ok(changed);
|
||||
}
|
||||
|
||||
// Passo 2: encontrar diretórios com mtime diferente do armazenado
|
||||
let changed_dirs: Vec<PathBuf> = self
|
||||
.dir_mtimes
|
||||
.iter()
|
||||
.filter(|(dir, stored_mtime)| {
|
||||
fs::metadata(dir)
|
||||
.and_then(|m| m.modified())
|
||||
.map(|t| mtime_secs(t) != **stored_mtime)
|
||||
.unwrap_or(true) // diretório sumiu = tratar como mudança
|
||||
})
|
||||
.map(|(dir, _)| dir.clone())
|
||||
.collect();
|
||||
|
||||
if changed_dirs.is_empty() {
|
||||
return Ok(changed);
|
||||
}
|
||||
|
||||
// Passo 3: resscanear apenas as raízes mínimas das subárvores alteradas
|
||||
for root in minimal_roots(&changed_dirs) {
|
||||
eprintln!("Rescanning: {}", root.display());
|
||||
|
||||
// Remover entradas antigas desta subárvore
|
||||
self.projects.retain(|p| !p.starts_with(&root));
|
||||
self.dir_mtimes.retain(|d, _| !d.starts_with(&root));
|
||||
|
||||
// Resscanear e mesclar
|
||||
let (new_projects, new_fingerprints) = scan_fn(&root)?;
|
||||
self.projects.extend(new_projects);
|
||||
self.dir_mtimes.extend(new_fingerprints);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if changed {
|
||||
self.projects.sort();
|
||||
self.projects.dedup();
|
||||
self.last_updated = now_secs();
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn age_in_seconds(&self) -> u64 {
|
||||
now_secs().saturating_sub(self.last_updated)
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::session::SessionConfig;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub paths: Vec<String>,
|
||||
#[serde(default = "default_max_depth")]
|
||||
pub max_depth: usize,
|
||||
#[serde(default = "default_cache_enabled")]
|
||||
pub cache_enabled: bool,
|
||||
#[serde(default = "default_cache_ttl_hours")]
|
||||
pub cache_ttl_hours: u64,
|
||||
#[serde(default = "default_session_config")]
|
||||
pub default_session: SessionConfig,
|
||||
}
|
||||
|
||||
fn default_max_depth() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn default_cache_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_cache_ttl_hours() -> u64 {
|
||||
24
|
||||
}
|
||||
|
||||
fn default_session_config() -> SessionConfig {
|
||||
use crate::session::Window;
|
||||
|
||||
SessionConfig {
|
||||
windows: vec![
|
||||
Window {
|
||||
name: "editor".to_string(),
|
||||
panes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
Window {
|
||||
name: "terminal".to_string(),
|
||||
panes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = Self::config_path()?;
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(Self::default_config());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
|
||||
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn config_path() -> Result<PathBuf> {
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("Could not determine config directory")?
|
||||
.join("tmuxido");
|
||||
|
||||
Ok(config_dir.join("tmuxido.toml"))
|
||||
}
|
||||
|
||||
pub fn ensure_config_exists() -> Result<PathBuf> {
|
||||
let config_path = Self::config_path()?;
|
||||
|
||||
if !config_path.exists() {
|
||||
let config_dir = config_path.parent()
|
||||
.context("Could not get parent directory")?;
|
||||
|
||||
fs::create_dir_all(config_dir)
|
||||
.with_context(|| format!("Failed to create config directory: {}", config_dir.display()))?;
|
||||
|
||||
let default_config = Self::default_config();
|
||||
let toml_string = toml::to_string_pretty(&default_config)
|
||||
.context("Failed to serialize default config")?;
|
||||
|
||||
fs::write(&config_path, toml_string)
|
||||
.with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
|
||||
|
||||
eprintln!("Created default config at: {}", config_path.display());
|
||||
}
|
||||
|
||||
Ok(config_path)
|
||||
}
|
||||
|
||||
fn default_config() -> Self {
|
||||
Config {
|
||||
paths: vec![
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("Work/Projects")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
],
|
||||
max_depth: 5,
|
||||
cache_enabled: true,
|
||||
cache_ttl_hours: 24,
|
||||
default_session: default_session_config(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
mod cache;
|
||||
mod config;
|
||||
mod session;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use cache::ProjectCache;
|
||||
use clap::Parser;
|
||||
use config::Config;
|
||||
use session::{SessionConfig, TmuxSession};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::UNIX_EPOCH;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "tmuxido",
|
||||
about = "Quickly find and open projects in tmux",
|
||||
version
|
||||
)]
|
||||
struct Args {
|
||||
/// Project path to open directly (skips selection)
|
||||
project_path: Option<PathBuf>,
|
||||
|
||||
/// Force refresh the project cache
|
||||
#[arg(short, long)]
|
||||
refresh: bool,
|
||||
|
||||
/// Show cache status and exit
|
||||
#[arg(long)]
|
||||
cache_status: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Ensure config exists
|
||||
Config::ensure_config_exists()?;
|
||||
|
||||
// Load config
|
||||
let config = Config::load()?;
|
||||
|
||||
// Handle cache status command
|
||||
if args.cache_status {
|
||||
show_cache_status(&config)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let selected = if let Some(path) = args.project_path {
|
||||
path
|
||||
} else {
|
||||
// Get projects (from cache or scan)
|
||||
let projects = get_projects(&config, args.refresh)?;
|
||||
|
||||
if projects.is_empty() {
|
||||
eprintln!("No projects found in configured paths");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Use fzf to select a project
|
||||
select_project_with_fzf(&projects)?
|
||||
};
|
||||
|
||||
if !selected.exists() {
|
||||
eprintln!("Selected path does not exist: {}", selected.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Launch tmux session
|
||||
launch_tmux_session(&selected, &config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cache_status(config: &Config) -> Result<()> {
|
||||
if !config.cache_enabled {
|
||||
println!("Cache is disabled in configuration");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(cache) = ProjectCache::load()? {
|
||||
let age_seconds = cache.age_in_seconds();
|
||||
let age_hours = age_seconds / 3600;
|
||||
let age_minutes = (age_seconds % 3600) / 60;
|
||||
|
||||
println!("Cache status:");
|
||||
println!(" Location: {}", ProjectCache::cache_path()?.display());
|
||||
println!(" Projects cached: {}", cache.projects.len());
|
||||
println!(" Directories tracked: {}", cache.dir_mtimes.len());
|
||||
println!(" Last updated: {}h {}m ago", age_hours, age_minutes);
|
||||
} else {
|
||||
println!("No cache found");
|
||||
println!(" Run without --cache-status to create it");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_projects(config: &Config, force_refresh: bool) -> Result<Vec<PathBuf>> {
|
||||
if !config.cache_enabled || force_refresh {
|
||||
let (projects, fingerprints) = scan_all_roots(config)?;
|
||||
let cache = ProjectCache::new(projects.clone(), fingerprints);
|
||||
cache.save()?;
|
||||
eprintln!("Cache updated with {} projects", projects.len());
|
||||
return Ok(projects);
|
||||
}
|
||||
|
||||
if let Some(mut cache) = ProjectCache::load()? {
|
||||
// Cache no formato antigo (sem dir_mtimes) → atualizar com rescan completo
|
||||
if cache.dir_mtimes.is_empty() {
|
||||
eprintln!("Upgrading cache, scanning for projects...");
|
||||
let (projects, fingerprints) = scan_all_roots(config)?;
|
||||
let new_cache = ProjectCache::new(projects.clone(), fingerprints);
|
||||
new_cache.save()?;
|
||||
eprintln!("Cache updated with {} projects", projects.len());
|
||||
return Ok(projects);
|
||||
}
|
||||
|
||||
let changed = cache.validate_and_update(&|root| scan_from_root(root, config))?;
|
||||
if changed {
|
||||
cache.save()?;
|
||||
eprintln!(
|
||||
"Cache updated incrementally ({} projects)",
|
||||
cache.projects.len()
|
||||
);
|
||||
} else {
|
||||
eprintln!("Using cached projects ({} projects)", cache.projects.len());
|
||||
}
|
||||
return Ok(cache.projects);
|
||||
}
|
||||
|
||||
// Sem cache ainda — scan completo inicial
|
||||
eprintln!("No cache found, scanning for projects...");
|
||||
let (projects, fingerprints) = scan_all_roots(config)?;
|
||||
let cache = ProjectCache::new(projects.clone(), fingerprints);
|
||||
cache.save()?;
|
||||
eprintln!("Cache updated with {} projects", projects.len());
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
fn scan_all_roots(config: &Config) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)> {
|
||||
let mut all_projects = Vec::new();
|
||||
let mut all_fingerprints = HashMap::new();
|
||||
|
||||
for path_str in &config.paths {
|
||||
let path = PathBuf::from(shellexpand::tilde(path_str).to_string());
|
||||
|
||||
if !path.exists() {
|
||||
eprintln!("Warning: Path does not exist: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
eprintln!("Scanning: {}", path.display());
|
||||
|
||||
let (projects, fingerprints) = scan_from_root(&path, config)?;
|
||||
all_projects.extend(projects);
|
||||
all_fingerprints.extend(fingerprints);
|
||||
}
|
||||
|
||||
all_projects.sort();
|
||||
all_projects.dedup();
|
||||
|
||||
Ok((all_projects, all_fingerprints))
|
||||
}
|
||||
|
||||
fn scan_from_root(root: &Path, config: &Config) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)> {
|
||||
let mut projects = Vec::new();
|
||||
let mut fingerprints = HashMap::new();
|
||||
|
||||
for entry in WalkDir::new(root)
|
||||
.max_depth(config.max_depth)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_entry(|e| {
|
||||
e.file_name()
|
||||
.to_str()
|
||||
.map(|s| !s.starts_with('.') || s == ".git")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
{
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
if entry.file_name() == ".git" {
|
||||
// Projeto encontrado
|
||||
if let Some(parent) = entry.path().parent() {
|
||||
projects.push(parent.to_path_buf());
|
||||
}
|
||||
} else {
|
||||
// Registrar mtime para detecção de mudanças futuras
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
let mtime = modified
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
fingerprints.insert(entry.path().to_path_buf(), mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((projects, fingerprints))
|
||||
}
|
||||
|
||||
fn select_project_with_fzf(projects: &[PathBuf]) -> Result<PathBuf> {
|
||||
let mut child = Command::new("fzf")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn fzf. Make sure fzf is installed.")?;
|
||||
|
||||
{
|
||||
let stdin = child.stdin.as_mut().context("Failed to open stdin")?;
|
||||
for project in projects {
|
||||
writeln!(stdin, "{}", project.display())?;
|
||||
}
|
||||
}
|
||||
|
||||
let output = child.wait_with_output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let selected = String::from_utf8(output.stdout)?.trim().to_string();
|
||||
|
||||
if selected.is_empty() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
Ok(PathBuf::from(selected))
|
||||
}
|
||||
|
||||
fn launch_tmux_session(selected: &Path, config: &Config) -> Result<()> {
|
||||
// Try to load project-specific config, fallback to global default
|
||||
let session_config = SessionConfig::load_from_project(selected)?
|
||||
.unwrap_or_else(|| config.default_session.clone());
|
||||
|
||||
// Create tmux session
|
||||
let tmux_session = TmuxSession::new(selected);
|
||||
tmux_session.create(&session_config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Window {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub panes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub layout: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SessionConfig {
|
||||
#[serde(default)]
|
||||
pub windows: Vec<Window>,
|
||||
}
|
||||
|
||||
impl SessionConfig {
|
||||
pub fn load_from_project(project_path: &Path) -> Result<Option<Self>> {
|
||||
let config_path = project_path.join(".tmuxido.toml");
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read session config: {}", config_path.display()))?;
|
||||
|
||||
let config: SessionConfig = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse session config: {}", config_path.display()))?;
|
||||
|
||||
Ok(Some(config))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmuxSession {
|
||||
session_name: String,
|
||||
project_path: String,
|
||||
base_index: usize,
|
||||
}
|
||||
|
||||
impl TmuxSession {
|
||||
pub fn new(project_path: &Path) -> Self {
|
||||
let session_name = project_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("project")
|
||||
.replace('.', "_")
|
||||
.replace(' ', "-");
|
||||
|
||||
let base_index = Self::get_base_index();
|
||||
|
||||
Self {
|
||||
session_name,
|
||||
project_path: project_path.display().to_string(),
|
||||
base_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_index() -> usize {
|
||||
// Try to get base-index from tmux
|
||||
let output = Command::new("tmux")
|
||||
.args(["show-options", "-gv", "base-index"])
|
||||
.output();
|
||||
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
let index_str = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(index) = index_str.trim().parse::<usize>() {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 0 if we can't determine
|
||||
0
|
||||
}
|
||||
|
||||
pub fn create(&self, config: &SessionConfig) -> Result<()> {
|
||||
// Check if we're already inside a tmux session
|
||||
let inside_tmux = std::env::var("TMUX").is_ok();
|
||||
|
||||
// Check if session already exists
|
||||
let session_exists = Command::new("tmux")
|
||||
.args(["has-session", "-t", &self.session_name])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if session_exists {
|
||||
// Session exists, just switch to it
|
||||
if inside_tmux {
|
||||
Command::new("tmux")
|
||||
.args(["switch-client", "-t", &self.session_name])
|
||||
.status()
|
||||
.context("Failed to switch to existing session")?;
|
||||
} else {
|
||||
Command::new("tmux")
|
||||
.args(["attach-session", "-t", &self.session_name])
|
||||
.status()
|
||||
.context("Failed to attach to existing session")?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create new session
|
||||
if config.windows.is_empty() {
|
||||
// Create simple session with one window
|
||||
self.create_simple_session()?;
|
||||
} else {
|
||||
// Create session with custom windows
|
||||
self.create_custom_session(config)?;
|
||||
}
|
||||
|
||||
// Attach or switch to the session
|
||||
if inside_tmux {
|
||||
Command::new("tmux")
|
||||
.args(["switch-client", "-t", &self.session_name])
|
||||
.status()
|
||||
.context("Failed to switch to new session")?;
|
||||
} else {
|
||||
Command::new("tmux")
|
||||
.args(["attach-session", "-t", &self.session_name])
|
||||
.status()
|
||||
.context("Failed to attach to new session")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_simple_session(&self) -> Result<()> {
|
||||
// Create a detached session with one window
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
&self.session_name,
|
||||
"-c",
|
||||
&self.project_path,
|
||||
])
|
||||
.status()
|
||||
.context("Failed to create tmux session")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_custom_session(&self, config: &SessionConfig) -> Result<()> {
|
||||
// Create session with first window
|
||||
let first_window = &config.windows[0];
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
&self.session_name,
|
||||
"-n",
|
||||
&first_window.name,
|
||||
"-c",
|
||||
&self.project_path,
|
||||
])
|
||||
.status()
|
||||
.context("Failed to create tmux session")?;
|
||||
|
||||
// Create panes for first window if specified
|
||||
if !first_window.panes.is_empty() {
|
||||
self.create_panes(self.base_index, &first_window.panes)?;
|
||||
}
|
||||
|
||||
// Apply layout for first window if specified
|
||||
if let Some(layout) = &first_window.layout {
|
||||
self.apply_layout(self.base_index, layout)?;
|
||||
}
|
||||
|
||||
// Create additional windows
|
||||
for (index, window) in config.windows.iter().skip(1).enumerate() {
|
||||
let window_index = self.base_index + index + 1;
|
||||
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"new-window",
|
||||
"-t",
|
||||
&format!("{}:{}", self.session_name, window_index),
|
||||
"-n",
|
||||
&window.name,
|
||||
"-c",
|
||||
&self.project_path,
|
||||
])
|
||||
.status()
|
||||
.with_context(|| format!("Failed to create window: {}", window.name))?;
|
||||
|
||||
// Create panes if specified
|
||||
if !window.panes.is_empty() {
|
||||
self.create_panes(window_index, &window.panes)?;
|
||||
}
|
||||
|
||||
// Apply layout if specified
|
||||
if let Some(layout) = &window.layout {
|
||||
self.apply_layout(window_index, layout)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Select the first window
|
||||
Command::new("tmux")
|
||||
.args(["select-window", "-t", &format!("{}:{}", self.session_name, self.base_index)])
|
||||
.status()
|
||||
.context("Failed to select first window")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_panes(&self, window_index: usize, panes: &[String]) -> Result<()> {
|
||||
for (pane_index, command) in panes.iter().enumerate() {
|
||||
let target = format!("{}:{}", self.session_name, window_index);
|
||||
|
||||
// First pane already exists (created with the window), skip split
|
||||
if pane_index > 0 {
|
||||
// Create new pane by splitting
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"split-window",
|
||||
"-t",
|
||||
&target,
|
||||
"-c",
|
||||
&self.project_path,
|
||||
])
|
||||
.status()
|
||||
.context("Failed to split pane")?;
|
||||
}
|
||||
|
||||
// Send the command to the pane if it's not empty
|
||||
if !command.is_empty() {
|
||||
let pane_target = format!("{}:{}.{}", self.session_name, window_index, pane_index);
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"send-keys",
|
||||
"-t",
|
||||
&pane_target,
|
||||
command,
|
||||
"Enter",
|
||||
])
|
||||
.status()
|
||||
.context("Failed to send keys to pane")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_layout(&self, window_index: usize, layout: &str) -> Result<()> {
|
||||
Command::new("tmux")
|
||||
.args([
|
||||
"select-layout",
|
||||
"-t",
|
||||
&format!("{}:{}", self.session_name, window_index),
|
||||
layout,
|
||||
])
|
||||
.status()
|
||||
.with_context(|| format!("Failed to apply layout: {}", layout))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user