Skip to content

Commit db12f83

Browse files
committed
Release v0.2.6
1 parent a368da9 commit db12f83

6 files changed

Lines changed: 118 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.6] - 2026-03-15
9+
10+
### Added
11+
12+
- Multiplexer-aware editor launch: when vim/nvim is the configured editor and a tmux or cmux session is detected, opens the editor in a new vertical split pane instead of spawning a background process
13+
814
## [0.2.5] - 2026-03-08
915

1016
### Fixed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pj-cli"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
edition = "2021"
55
authors = ["Alberto Cebada Aleu <contact@albertocebada.com>"]
66
description = "Project launcher CLI with fuzzy matching"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ command.
1919
- **Hierarchical Tags**: Organize projects with nested tags (e.g., `work/backend`)
2020
- **Shell Integration**: Automatic directory changing for bash, zsh, fish, and sh
2121
- **Editor Integration**: Launch your preferred editor when selecting a project
22+
- **Multiplexer Support**: Automatically opens vim/nvim in a split pane when running inside tmux or cmux
2223
- **Git Integration**: Prompt to initialize git repositories when adding projects
2324
- **GitHub Integration**: Optionally create GitHub remotes via the gh CLI
2425

src/commands/select.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ pub fn run(
8585
.filter(|e| !e.is_empty())
8686
.unwrap_or(config.editor);
8787

88-
Command::new(&editor)
89-
.arg(&selected_path)
90-
.spawn()
91-
.map_err(|e| anyhow::anyhow!("Failed to launch editor '{}': {}", editor, e))?;
88+
if !crate::multiplexer::try_open_in_split(&editor, &selected_path) {
89+
Command::new(&editor)
90+
.arg(&selected_path)
91+
.spawn()
92+
.map_err(|e| anyhow::anyhow!("Failed to launch editor '{}': {}", editor, e))?;
93+
}
9294
}
9395

9496
// Handle cd output

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod commands;
22
mod config;
33
mod frecency;
44
mod github;
5+
mod multiplexer;
56
mod projects;
67
mod shell;
78
mod tui;

src/multiplexer.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use std::path::Path;
2+
use std::process::Command;
3+
use std::thread;
4+
use std::time::Duration;
5+
6+
enum Multiplexer {
7+
Tmux,
8+
Cmux,
9+
}
10+
11+
fn detect_multiplexer() -> Option<Multiplexer> {
12+
if std::env::var("CMUX_SOCKET").is_ok() || std::env::var("CMUX_SOCKET_PATH").is_ok() {
13+
return Some(Multiplexer::Cmux);
14+
}
15+
if std::env::var("TMUX").is_ok() {
16+
return Some(Multiplexer::Tmux);
17+
}
18+
None
19+
}
20+
21+
fn is_terminal_editor(editor: &str) -> bool {
22+
Path::new(editor)
23+
.file_name()
24+
.and_then(|name| name.to_str())
25+
.map(|name| name == "vim" || name == "nvim")
26+
.unwrap_or(false)
27+
}
28+
29+
fn open_in_tmux_split(editor: &str, path: &Path) -> Result<(), String> {
30+
let path_str = path.display().to_string();
31+
let cmd = format!("{} \"{}\"", editor, path_str);
32+
33+
Command::new("tmux")
34+
.args(["split-window", "-h", &cmd])
35+
.status()
36+
.map_err(|e| format!("Failed to run tmux split-window: {}", e))?;
37+
38+
Ok(())
39+
}
40+
41+
fn open_in_cmux_split(editor: &str, path: &Path) -> Result<(), String> {
42+
let output = Command::new("cmux")
43+
.args(["new-split", "right"])
44+
.output()
45+
.map_err(|e| format!("Failed to run cmux new-split: {}", e))?;
46+
47+
if !output.status.success() {
48+
return Err(format!(
49+
"cmux new-split failed: {}",
50+
String::from_utf8_lossy(&output.stderr)
51+
));
52+
}
53+
54+
let stdout = String::from_utf8_lossy(&output.stdout);
55+
let surface_ref = stdout
56+
.split_whitespace()
57+
.find(|token| token.starts_with("surface:"))
58+
.ok_or_else(|| format!("Failed to parse surface ref from cmux output: {}", stdout))?
59+
.to_string();
60+
61+
// Wait for the new pane's shell to initialize before sending keystrokes
62+
thread::sleep(Duration::from_millis(200));
63+
64+
let path_str = path.display().to_string();
65+
let send_cmd = format!("{} \"{}\"\\n", editor, path_str);
66+
67+
let send_output = Command::new("cmux")
68+
.args(["send", "--surface", &surface_ref, "--", &send_cmd])
69+
.output()
70+
.map_err(|e| format!("Failed to run cmux send: {}", e))?;
71+
72+
if !send_output.status.success() {
73+
return Err(format!(
74+
"cmux send failed: {}",
75+
String::from_utf8_lossy(&send_output.stderr)
76+
));
77+
}
78+
79+
Ok(())
80+
}
81+
82+
pub fn try_open_in_split(editor: &str, path: &Path) -> bool {
83+
if !is_terminal_editor(editor) {
84+
return false;
85+
}
86+
87+
let multiplexer = match detect_multiplexer() {
88+
Some(m) => m,
89+
None => return false,
90+
};
91+
92+
let result = match multiplexer {
93+
Multiplexer::Tmux => open_in_tmux_split(editor, path),
94+
Multiplexer::Cmux => open_in_cmux_split(editor, path),
95+
};
96+
97+
if let Err(e) = result {
98+
eprintln!("Warning: failed to open editor in split pane: {}", e);
99+
return false;
100+
}
101+
102+
true
103+
}

0 commit comments

Comments
 (0)