Compare commits

..
5 Commits
Author SHA1 Message Date
cinco 2b1773375a 🐛 fix: add test for asset name format and bump to 0.5.2
Adds a unit test that asserts detect_arch returns names prefixed
with 'tmuxido-' and suffixed with '-linux', matching what CI uploads.
2026-03-01 03:43:10 -03:00
cinco 36aaa65945 📝 docs: add changelog entry for 0.5.1 2026-03-01 03:38:04 -03:00
cinco 10a38a1f85 🔧 ci: delete existing release before recreating on retag
When a tag is deleted and recreated, the CI tried to POST a new
release that already existed, getting 409 and leaving RELEASE_ID
null, which caused asset uploads to fail with 405. Now checks for
an existing release by tag and deletes it before creating a new one.
2026-03-01 03:34:27 -03:00
cinco 42bdc1d409 🐛 fix: correct asset name and bump version to 0.5.1
detect_arch was returning "x86_64-linux" but CI uploads assets as
"tmuxido-x86_64-linux", causing 404 on self-update. Also bumps
Cargo.toml to 0.5.1 which was missing from the hotfix tag.
2026-03-01 03:31:10 -03:00
cinco a592c99375 🐛 fix: target tmux windows by name instead of numeric index
Removes base-index detection which was unreliable and defaulted to 0
when tmux's actual base-index was 1, causing "index in use" and
"can't find window" errors on session creation.
2026-03-01 03:17:28 -03:00
6 changed files with 66 additions and 60 deletions
+10
View File
@@ -76,6 +76,16 @@ steps:
commands: commands:
- apk add --no-cache curl jq - apk add --no-cache curl jq
- | - |
# Delete existing release for this tag if present (handles retag scenarios)
EXISTING_ID=$(curl -fsSL \
-H "Authorization: token $GITEA_TOKEN" \
"https://git.cincoeuzebio.com/api/v1/repos/cinco/Tmuxido/releases/tags/$DRONE_TAG" \
| jq -r '.id // empty')
if [ -n "$EXISTING_ID" ]; then
curl -fsSL -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"https://git.cincoeuzebio.com/api/v1/repos/cinco/Tmuxido/releases/$EXISTING_ID"
fi
# Read DRONE_TAG via ENVIRON inside awk to avoid Drone's ${VAR} substitution # Read DRONE_TAG via ENVIRON inside awk to avoid Drone's ${VAR} substitution
# which would replace ${TAG} with an empty string before the shell runs. # which would replace ${TAG} with an empty string before the shell runs.
BODY=$(awk ' BODY=$(awk '
+15
View File
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.5.2] - 2026-03-01
### Added
- Test for `detect_arch` asserting asset name follows `tmuxido-{arch}-linux` format
## [0.5.1] - 2026-03-01
### Fixed
- Tmux window creation now targets windows by name instead of numeric index, eliminating
"index in use" and "can't find window" errors when `base-index` is not 0
- Self-update asset name corrected from `x86_64-linux` to `tmuxido-x86_64-linux` to match
what CI actually uploads, fixing 404 on `--update`
- CI release pipeline now deletes any existing release for the tag before recreating,
preventing 409 Conflict errors on retagged releases
## [0.5.0] - 2026-03-01 ## [0.5.0] - 2026-03-01
### Added ### Added
Generated
+1 -1
View File
@@ -864,7 +864,7 @@ dependencies = [
[[package]] [[package]]
name = "tmuxido" name = "tmuxido"
version = "0.5.0" version = "0.5.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tmuxido" name = "tmuxido"
version = "0.5.0" version = "0.5.2"
edition = "2024" edition = "2024"
[dev-dependencies] [dev-dependencies]
+15 -2
View File
@@ -20,8 +20,8 @@ pub fn current_version() -> &'static str {
fn detect_arch() -> Result<&'static str> { fn detect_arch() -> Result<&'static str> {
let arch = std::env::consts::ARCH; let arch = std::env::consts::ARCH;
match arch { match arch {
"x86_64" => Ok("x86_64-linux"), "x86_64" => Ok("tmuxido-x86_64-linux"),
"aarch64" => Ok("aarch64-linux"), "aarch64" => Ok("tmuxido-aarch64-linux"),
_ => Err(anyhow::anyhow!("Unsupported architecture: {}", arch)), _ => Err(anyhow::anyhow!("Unsupported architecture: {}", arch)),
} }
} }
@@ -198,6 +198,19 @@ mod tests {
assert!(version.contains('.')); assert!(version.contains('.'));
} }
#[test]
fn should_prefix_arch_asset_with_tmuxido() {
let arch = detect_arch().expect("should detect supported arch");
assert!(
arch.starts_with("tmuxido-"),
"asset name must start with 'tmuxido-', got: {arch}"
);
assert!(
arch.ends_with("-linux"),
"asset name must end with '-linux', got: {arch}"
);
}
#[test] #[test]
fn should_compare_versions_correctly() { fn should_compare_versions_correctly() {
assert_eq!( assert_eq!(
+24 -56
View File
@@ -41,7 +41,6 @@ impl SessionConfig {
pub struct TmuxSession { pub struct TmuxSession {
pub(crate) session_name: String, pub(crate) session_name: String,
project_path: String, project_path: String,
base_index: usize,
} }
impl TmuxSession { impl TmuxSession {
@@ -53,34 +52,12 @@ impl TmuxSession {
.replace('.', "_") .replace('.', "_")
.replace(' ', "-"); .replace(' ', "-");
let base_index = Self::get_base_index();
Self { Self {
session_name, session_name,
project_path: project_path.display().to_string(), 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
&& 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<()> { pub fn create(&self, config: &SessionConfig) -> Result<()> {
// Check if we're already inside a tmux session // Check if we're already inside a tmux session
let inside_tmux = std::env::var("TMUX").is_ok(); let inside_tmux = std::env::var("TMUX").is_ok();
@@ -167,25 +144,23 @@ impl TmuxSession {
.status() .status()
.context("Failed to create tmux session")?; .context("Failed to create tmux session")?;
// Create panes for first window if specified let first_target = format!("{}:{}", self.session_name, first_window.name);
if !first_window.panes.is_empty() { if !first_window.panes.is_empty() {
self.create_panes(self.base_index, &first_window.panes)?; self.create_panes(&first_target, &first_window.panes)?;
} }
// Apply layout for first window if specified
if let Some(layout) = &first_window.layout { if let Some(layout) = &first_window.layout {
self.apply_layout(self.base_index, layout)?; self.apply_layout(&first_target, layout)?;
} }
// Create additional windows // Create additional windows, targeting by session name so tmux auto-assigns the index
for (index, window) in config.windows.iter().skip(1).enumerate() { for window in config.windows.iter().skip(1) {
let window_index = self.base_index + index + 1;
Command::new("tmux") Command::new("tmux")
.args([ .args([
"new-window", "new-window",
"-t", "-t",
&format!("{}:{}", self.session_name, window_index), &self.session_name,
"-n", "-n",
&window.name, &window.name,
"-c", "-c",
@@ -194,46 +169,44 @@ impl TmuxSession {
.status() .status()
.with_context(|| format!("Failed to create window: {}", window.name))?; .with_context(|| format!("Failed to create window: {}", window.name))?;
// Create panes if specified let target = format!("{}:{}", self.session_name, window.name);
if !window.panes.is_empty() { if !window.panes.is_empty() {
self.create_panes(window_index, &window.panes)?; self.create_panes(&target, &window.panes)?;
} }
// Apply layout if specified
if let Some(layout) = &window.layout { if let Some(layout) = &window.layout {
self.apply_layout(window_index, layout)?; self.apply_layout(&target, layout)?;
} }
} }
// Select the first window // Select the first window by name
Command::new("tmux") Command::new("tmux")
.args([ .args(["select-window", "-t", &first_target])
"select-window",
"-t",
&format!("{}:{}", self.session_name, self.base_index),
])
.status() .status()
.context("Failed to select first window")?; .context("Failed to select first window")?;
Ok(()) Ok(())
} }
fn create_panes(&self, window_index: usize, panes: &[String]) -> Result<()> { fn create_panes(&self, window_target: &str, panes: &[String]) -> Result<()> {
for (pane_index, command) in panes.iter().enumerate() { 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 // First pane already exists (created with the window), skip split
if pane_index > 0 { if pane_index > 0 {
// Create new pane by splitting
Command::new("tmux") Command::new("tmux")
.args(["split-window", "-t", &target, "-c", &self.project_path]) .args([
"split-window",
"-t",
window_target,
"-c",
&self.project_path,
])
.status() .status()
.context("Failed to split pane")?; .context("Failed to split pane")?;
} }
// Send the command to the pane if it's not empty
if !command.is_empty() { if !command.is_empty() {
let pane_target = format!("{}:{}.{}", self.session_name, window_index, pane_index); let pane_target = format!("{}.{}", window_target, pane_index);
Command::new("tmux") Command::new("tmux")
.args(["send-keys", "-t", &pane_target, command, "Enter"]) .args(["send-keys", "-t", &pane_target, command, "Enter"])
.status() .status()
@@ -244,14 +217,9 @@ impl TmuxSession {
Ok(()) Ok(())
} }
fn apply_layout(&self, window_index: usize, layout: &str) -> Result<()> { fn apply_layout(&self, window_target: &str, layout: &str) -> Result<()> {
Command::new("tmux") Command::new("tmux")
.args([ .args(["select-layout", "-t", window_target, layout])
"select-layout",
"-t",
&format!("{}:{}", self.session_name, window_index),
layout,
])
.status() .status()
.with_context(|| format!("Failed to apply layout: {}", layout))?; .with_context(|| format!("Failed to apply layout: {}", layout))?;