Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rote/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ nix = { version = "0.28", features = ["signal"] }
tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "process", "io-util", "time", "sync"] }
ratatui = "0.26"
ropey = "1.6"
indexmap = { version = "2.7", features = ["serde"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_yaml = "0.9.34"
shell-words = "1.1.1"
Expand Down
115 changes: 100 additions & 15 deletions rote/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ pub async fn run_with_input(
let tasks_list = resolve_dependencies(&config, &target_tasks)?;

// Create panels for ALL tasks with actions (not just those being started)
// Sort by task name for consistent ordering
// Panels are ordered according to their order in the YAML config file
let mut panels = Vec::new();
let mut task_to_panel: HashMap<String, PanelIndex> = HashMap::new();

// Collect and sort task names for deterministic panel order
let mut task_names: Vec<_> = config
// Collect task names, preserving YAML file order (IndexMap preserves insertion order)
let task_names: Vec<_> = config
.tasks
.iter()
.filter(|(_, cfg)| {
Expand All @@ -90,7 +90,6 @@ pub async fn run_with_input(
})
.map(|(name, _)| name.clone())
.collect();
task_names.sort();

for task_name in &task_names {
let task_config = config.tasks.get(task_name).unwrap();
Expand Down Expand Up @@ -142,7 +141,7 @@ pub async fn run_with_input(
return Ok(());
}

// Initialize status panel with all tasks that have actions (sorted order)
// Initialize status panel with all tasks that have actions (YAML file order)
let mut status_panel = StatusPanel::new();
for task_name in &task_names {
let task_config = config.tasks.get(task_name).unwrap();
Expand Down Expand Up @@ -784,6 +783,92 @@ fn toggle_stream_visibility(panel: &mut Panel, show: bool) {
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;

use crate::config::{CommandValue, TaskConfiguration};
use std::borrow::Cow;

/// Helper to extract panel names from a config in the order they would be created.
/// This mirrors the logic in run_with_input for creating panels.
fn get_panel_order(config: &Config) -> Vec<String> {
config
.tasks
.iter()
.filter(|(_, cfg)| {
matches!(
cfg.action,
Some(TaskAction::Run { .. }) | Some(TaskAction::Ensure { .. })
)
})
.map(|(name, _)| name.clone())
.collect()
}

#[test]
fn test_panel_order_matches_yaml_order() {
let mut tasks = IndexMap::new();
tasks.insert(
"third".to_string(),
TaskConfiguration {
action: Some(TaskAction::Run {
command: CommandValue::String(Cow::Borrowed("echo third")),
}),
cwd: None,
display: None,
require: vec![],
autorestart: false,
timestamps: false,
},
);
tasks.insert(
"first".to_string(),
TaskConfiguration {
action: Some(TaskAction::Run {
command: CommandValue::String(Cow::Borrowed("echo first")),
}),
cwd: None,
display: None,
require: vec![],
autorestart: false,
timestamps: false,
},
);
tasks.insert(
"second".to_string(),
TaskConfiguration {
action: Some(TaskAction::Ensure {
command: CommandValue::String(Cow::Borrowed("echo second")),
}),
cwd: None,
display: None,
require: vec![],
autorestart: false,
timestamps: false,
},
);
// Task without action should be excluded from panels
tasks.insert(
"no-action".to_string(),
TaskConfiguration {
action: None,
cwd: None,
display: None,
require: vec!["first".to_string()],
autorestart: false,
timestamps: false,
},
);

let config = Config {
default: None,
tasks,
};

let panel_order = get_panel_order(&config);
// Panels should be in insertion order (third, first, second), not alphabetical
// The "no-action" task should be excluded since it has no run/ensure action
assert_eq!(panel_order, vec!["third", "first", "second"]);
}

#[test]
fn test_visible_len_empty_panel() {
Expand Down Expand Up @@ -849,15 +934,15 @@ mod tests {
fn test_resolve_dependencies_empty() {
let config = Config {
default: None,
tasks: HashMap::new(),
tasks: IndexMap::new(),
};
let result = resolve_dependencies(&config, &[]).unwrap();
assert!(result.is_empty());
}

#[test]
fn test_resolve_dependencies_no_deps() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand All @@ -881,7 +966,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_with_deps() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down Expand Up @@ -916,7 +1001,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_multiple_deps() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down Expand Up @@ -965,7 +1050,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_nested_deps() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down Expand Up @@ -1011,7 +1096,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_circular() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down Expand Up @@ -1054,7 +1139,7 @@ mod tests {
fn test_resolve_dependencies_task_not_found() {
let config = Config {
default: None,
tasks: HashMap::new(),
tasks: IndexMap::new(),
};

let result = resolve_dependencies(&config, &["nonexistent".to_string()]);
Expand All @@ -1069,7 +1154,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_dep_not_found() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand All @@ -1093,7 +1178,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_multiple_targets() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down Expand Up @@ -1143,7 +1228,7 @@ mod tests {

#[test]
fn test_resolve_dependencies_diamond_graph() {
let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();
tasks.insert(
"task1".to_string(),
crate::config::TaskConfiguration {
Expand Down
45 changes: 42 additions & 3 deletions rote/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use indexmap::IndexMap;
use serde::Deserialize;
use std::{borrow::Cow, collections::HashMap};
use std::borrow::Cow;

#[derive(Debug, Deserialize)]
pub struct Config {
/// The default task to run when none is specified.
pub default: Option<String>,
/// A mapping of task names to their configurations.
pub tasks: HashMap<String, TaskConfiguration>,
/// A mapping of task names to their configurations (preserves YAML order).
pub tasks: IndexMap<String, TaskConfiguration>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -152,6 +153,44 @@ mod tests {
);
}

#[test]
fn test_task_order_preserved_from_yaml() {
let yaml = r#"
default: main
tasks:
first:
run: echo first
second:
run: echo second
third:
run: echo third
fourth:
ensure: true
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
let task_names: Vec<_> = config.tasks.keys().collect();
assert_eq!(task_names, vec!["first", "second", "third", "fourth"]);
}

#[test]
fn test_example_yaml_task_order() {
let config = load_config();
let task_names: Vec<_> = config.tasks.keys().collect();
// Tasks should be in the order they appear in example.yaml
assert_eq!(
task_names,
vec![
"google-ping",
"cloudflare-ping",
"github-ping",
"short-lived",
"auto-restarting",
"setup-task",
"ping-demo"
]
);
}

#[test]
fn test_missing_optional_fields() {
let yaml = r#"
Expand Down
3 changes: 2 additions & 1 deletion rote/src/task_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@ pub fn resolve_dependencies(config: &Config, targets: &[String]) -> Result<Vec<S
mod tests {
use super::*;
use crate::config::{CommandValue, TaskConfiguration};
use indexmap::IndexMap;
use std::borrow::Cow;

fn make_config_with_tasks(tasks: Vec<(&str, Option<TaskAction>, Vec<&str>)>) -> Config {
let mut task_map = HashMap::new();
let mut task_map = IndexMap::new();
for (name, action, require) in tasks {
task_map.insert(
name.to_string(),
Expand Down
4 changes: 2 additions & 2 deletions rote/tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use indexmap::IndexMap;
use rote_mux::panel::PanelIndex;
use rote_mux::{Config, UiEvent};
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::timeout;

Expand Down Expand Up @@ -100,7 +100,7 @@ async fn test_ensure_dependency_blocks_until_complete() {
use rote_mux::config::{CommandValue, TaskAction, TaskConfiguration};
use std::borrow::Cow;

let mut tasks = HashMap::new();
let mut tasks = IndexMap::new();

// An Ensure task that completes quickly
tasks.insert(
Expand Down