Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
21af733
feat: integrate Ratatui for TUI dashboard and update dependencies
0xParthP Sep 5, 2026
7712c2e
feat: add TUI module with event handling and UI tests
0xParthP Sep 5, 2026
29a5134
feat: refactor TUI event handling and add tests for run_loop function…
0xParthP Sep 5, 2026
15265d0
refactor: update run_loop and event handling for improved clarity and…
0xParthP Sep 5, 2026
3a02cd9
feat: update TUI event handling and add tests for run_loop and tick f…
0xParthP Sep 5, 2026
eb50520
refactor: remove unused event handling tests and streamline run_loop …
0xParthP Sep 5, 2026
ecfbff8
refactor: remove unnecessary whitespace and add tests for run_loop fu…
0xParthP Sep 5, 2026
9351d6e
refactor: improve event handling and add key press tests for quitting…
0xParthP Sep 5, 2026
d17a259
feat: enhance AppState with project management and search functionality
0xParthP Sep 6, 2026
a4bb7b8
feat: implement project loading and enhance TUI with project manageme…
0xParthP Sep 6, 2026
d0460ee
feat: add temp-env dependency and enhance project loading with sortin…
0xParthP Sep 6, 2026
1fb781d
feat: implement project opening functionality and enhance TUI with pr…
0xParthP Sep 6, 2026
f3ff842
feat: update default IDE to Vscode across configuration and tests
0xParthP Sep 6, 2026
1854a21
feat: enhance TUI event handling with launcher support for project op…
0xParthP Sep 6, 2026
c58477c
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 6, 2026
ade656c
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 6, 2026
1064aa5
feat: Implement tab navigation and enhance project list rendering in TUI
0xParthP Sep 6, 2026
7f433e6
feat: Add tab navigation tests and enhance UI rendering for tabs in TUI
0xParthP Sep 6, 2026
adcec42
feat: Bump version to 2.0.0 in Cargo.toml and Cargo.lock
0xParthP Sep 6, 2026
d7cb5bf
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 6, 2026
9a3dd76
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 7, 2026
243c081
fix: Update quit key binding from 'q' to 'Esc' in footer widget
0xParthP Sep 7, 2026
3e27da7
feat: implement recent projects feature in TUI
0xParthP Sep 7, 2026
830c08c
Merge branch 'main' into feature/tui
0xParthP Sep 7, 2026
ac0c231
fix: refactor recent widget test to use updated configuration structure
0xParthP Sep 7, 2026
725610e
Merge branches 'feature/tui' and 'feature/tui' of https://github.com/…
0xParthP Sep 7, 2026
ec1ba33
feat: implement base TUI framework with project list, navigation, and…
0xParthP Sep 11, 2026
25b2a97
feat: implement project, recent, and placeholder widgets with shared …
0xParthP Sep 11, 2026
7abf853
feat: implement configuration management, IDE detection, and project …
0xParthP Sep 11, 2026
003155b
Merge branch 'main' into feature/tui
0xParthP Sep 11, 2026
e040dfc
test: implement integration tests for Windows IDE detection and TUI e…
0xParthP Sep 11, 2026
338a8d6
feat: initialize TUI module structure and expose app entry point
0xParthP Sep 11, 2026
26e58f4
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 11, 2026
bc92eaf
Please provide the diff or description of the changes so I can genera…
0xParthP Sep 11, 2026
2fea8b4
fix for sonar
0xParthP Sep 11, 2026
9e8e763
test: add integration tests for onboarding flow and configuration man…
0xParthP Sep 11, 2026
40a6d66
feat: implement interactive onboarding wizard and initial setup tests
0xParthP Sep 11, 2026
9fc0141
fix: for covergae
0xParthP Sep 11, 2026
e48dca3
feat: add interactive onboarding wizard to initialize configuration o…
0xParthP Sep 11, 2026
62f9466
feat: implement TUI event handling, project actions, and unit tests f…
0xParthP Sep 11, 2026
d7d454b
fix: added test to fix sonar
0xParthP Sep 11, 2026
6c96959
feat: add TUI interface with state management, widgets, and tests
0xParthP Sep 15, 2026
3de7e89
feat: implement tree data structure and filtering utilities for TUI p…
0xParthP Sep 15, 2026
871c238
Merge branch 'main' into feature/tui
0xParthP Sep 15, 2026
bdf3716
feat: implement project tree structure for TUI and add Sonar CPD test…
0xParthP Sep 15, 2026
353986e
feat: implement project list widget for TUI
0xParthP Sep 15, 2026
bf13fa1
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 15, 2026
98d1795
feat: add TUI modules for state, events, widgets, themes, and tests
0xParthP Sep 15, 2026
50d01cb
feat: add interactive TUI dashboard, IDE launcher, and associated tests
0xParthP Sep 23, 2026
6a6aae2
feat: implement IDE launcher and TUI application entrypoint with tests
0xParthP Sep 23, 2026
e35cba5
Merge branch 'main' of https://github.com/0xParthP/dev-cli into featu…
0xParthP Sep 23, 2026
ec1ac30
feat: implement TUI event handling, state management, widgets, and co…
0xParthP Sep 23, 2026
79b4305
chore: bump version to 2.1.0
0xParthP Sep 23, 2026
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[package]
name = "dev-cli"
version = "2.0.0"
version = "2.1.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/0xParthP/dev-cli"
Expand Down
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ impl Config {
}

pub fn add_recent_project(&mut self, project: &Project) {
self.add_recent_project_with_ide(project, self.default_ide);
}

pub fn add_recent_project_with_ide(&mut self, project: &Project, ide: Ide) {
self.recent_projects.retain(|p| p.path != project.path);

self.recent_projects.insert(
Expand All @@ -125,6 +129,7 @@ impl Config {
name: project.name.clone(),
path: project.path.clone(),
last_opened: Self::now_timestamp(),
ide: Some(ide),
},
);

Expand Down
6 changes: 6 additions & 0 deletions src/models/recent_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::models::ide::Ide;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RecentProject {
/// Project name.
Expand All @@ -14,4 +16,8 @@ pub struct RecentProject {

/// Unix timestamp (seconds).
pub last_opened: u64,

/// Last IDE used to open the project.
#[serde(default)]
pub ide: Option<Ide>,
}
2 changes: 1 addition & 1 deletion src/tui/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ where
{
let mut config = Config::load()?;
launch(ide, &project.path)?;
config.add_recent_project(project);
config.add_recent_project_with_ide(project, ide);
config.save()?;
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ where
if !recent.name.is_empty() {
project.name = recent.name.clone();
}
let ide = state.get_project_ide(&project.path);
let ide = state.get_recent_project_ide(recent);
if launcher(ide, &project).is_ok() {
state.pending_launch = Some((ide, project));
state.quit();
Expand Down
44 changes: 34 additions & 10 deletions src/tui/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,27 +125,51 @@ impl AppState {
/// Get the target IDE for a project path (override or default).
pub fn get_project_ide(&self, path: &std::path::Path) -> Ide {
let ide = self.ide_overrides.get(path).copied().unwrap_or(self.default_ide);
self.ensure_installed_ide(ide)
}

/// Get the target IDE for a recent project (override, recent default, or config default).
pub fn get_recent_project_ide(&self, recent: &RecentProject) -> Ide {
let ide = if let Some(&override_ide) = self.ide_overrides.get(&recent.path) {
override_ide
} else {
recent.ide.unwrap_or(self.default_ide)
};
self.ensure_installed_ide(ide)
}

fn ensure_installed_ide(&self, ide: Ide) -> Ide {
if !self.installed_ides.is_empty() && !self.installed_ides.contains(&ide) {
self.installed_ides[0]
} else {
ide
}
}

/// Cycle through available installed IDEs for the currently selected project.
/// Cycle through available installed IDEs for the currently selected project (Projects or Recent tab).
pub fn cycle_selected_ide(&mut self) {
if self.active_tab != Tab::Projects || self.installed_ides.is_empty() {
if self.installed_ides.is_empty() {
return;
}

if let Some(project) = self.selected_project() {
let current_ide = self.get_project_ide(&project.path);
let next_ide = match self.installed_ides.iter().position(|&i| i == current_ide) {
Some(idx) => self.installed_ides[(idx + 1) % self.installed_ides.len()],
None => self.installed_ides[0],
};
self.ide_overrides.insert(project.path.clone(), next_ide);
}
let (path, current_ide) = match self.active_tab {
Tab::Projects => {
let Some(project) = self.selected_project() else { return };
(project.path.clone(), self.get_project_ide(&project.path))
}
Tab::Recent => {
let Some(recent) = self.selected_recent_project() else { return };
(recent.path.clone(), self.get_recent_project_ide(recent))
}
_ => return,
};

let next_ide = match self.installed_ides.iter().position(|&i| i == current_ide) {
Some(idx) => self.installed_ides[(idx + 1) % self.installed_ides.len()],
None => self.installed_ides[0],
};

self.ide_overrides.insert(path, next_ide);
}

pub fn filtered_projects(&self) -> Vec<&Project> {
Expand Down
25 changes: 22 additions & 3 deletions src/tui/widgets/recent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,39 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {

let left_width = UnicodeWidthStr::width(left.as_str());
let age_width = UnicodeWidthStr::width(age.as_str());
let spacing = inner_width.saturating_sub(left_width + age_width);
let line1_spacing = inner_width.saturating_sub(left_width + age_width);

let ide = state.get_recent_project_ide(project);
let badge = format!("[{}]", ide.display_name());
let path_str = display_path(&project.path);

let path_left = format!(" {}", path_str);
let path_width = UnicodeWidthStr::width(path_left.as_str());
let badge_width = UnicodeWidthStr::width(badge.as_str());

let line2_spacing = if inner_width > path_width + badge_width {
inner_width - path_width - badge_width
} else {
2
};

ListItem::new(vec![
Line::from(vec![
Span::styled(
left,
Style::default().fg(theme::TEXT).add_modifier(Modifier::BOLD),
),
Span::raw(" ".repeat(spacing)),
Span::raw(" ".repeat(line1_spacing)),
Span::styled(age, Style::default().fg(theme::INFO)),
]),
Line::from(vec![
Span::raw(" "),
Span::styled(display_path(&project.path), Style::default().fg(theme::MUTED)),
Span::styled(path_str, Style::default().fg(theme::MUTED)),
Span::raw(" ".repeat(line2_spacing)),
Span::styled(
badge,
Style::default().fg(ide.color()).add_modifier(Modifier::BOLD),
),
]),
Line::default(),
])
Expand Down
1 change: 1 addition & 0 deletions tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ fn recent_projects_round_trip_serialization() -> Result<()> {
name: "weather-app".into(),
path: PathBuf::from("/projects/weather-app"),
last_opened: now,
ide: None,
}],
};

Expand Down
2 changes: 2 additions & 0 deletions tests/tui_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ fn enter_key_on_recent_tab_launches_project() {
name: "recent_app".into(),
path: std::path::PathBuf::from("/tmp/recent_app"),
last_opened: 100,
ide: None,
}];

let launched = std::sync::atomic::AtomicBool::new(false);
Expand All @@ -356,6 +357,7 @@ fn enter_key_on_recent_tab_does_not_quit_on_error() {
name: "recent_app".into(),
path: std::path::PathBuf::from("/tmp/recent_app"),
last_opened: 100,
ide: None,
}];

handle_key_with_launcher(
Expand Down
55 changes: 53 additions & 2 deletions tests/tui_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ fn move_down_on_recent_tab_stops_at_end() {
state.active_tab = Tab::Recent;

state.recent_projects = vec![
RecentProject { name: "a".into(), path: PathBuf::from("/a"), last_opened: 0 },
RecentProject { name: "b".into(), path: PathBuf::from("/b"), last_opened: 0 },
RecentProject { name: "a".into(), path: PathBuf::from("/a"), last_opened: 0, ide: None },
RecentProject { name: "b".into(), path: PathBuf::from("/b"), last_opened: 0, ide: None },
];

state.move_down();
Expand Down Expand Up @@ -222,3 +222,54 @@ fn get_project_ide_falls_back_to_first_installed_if_default_uninstalled() {
let p = project("demo");
assert_eq!(state.get_project_ide(&p.path), Ide::Cursor);
}

#[test]
fn cycle_selected_ide_on_recent_tab() {
use dev_cli::models::recent_project::RecentProject;
use dev_cli::tui::state::Tab;
use std::path::PathBuf;

let mut state = AppState::new();
state.active_tab = Tab::Recent;
state.installed_ides = vec![Ide::Vscode, Ide::Claude];
let path = PathBuf::from("/tmp/recent_demo");
state.recent_projects = vec![RecentProject {
name: "recent_demo".into(),
path: path.clone(),
last_opened: 100,
ide: Some(Ide::Vscode),
}];

assert_eq!(state.get_recent_project_ide(&state.recent_projects[0]), Ide::Vscode);

state.cycle_selected_ide();
assert_eq!(state.get_recent_project_ide(&state.recent_projects[0]), Ide::Claude);

state.cycle_selected_ide();
assert_eq!(state.get_recent_project_ide(&state.recent_projects[0]), Ide::Vscode);
}

#[test]
fn projects_tab_uses_default_ide_not_recent_ide() {
use dev_cli::models::recent_project::RecentProject;
use std::path::PathBuf;

let mut state = AppState::new();
state.default_ide = Ide::Vscode;
state.installed_ides = vec![Ide::Vscode, Ide::Claude];

let path = PathBuf::from("/tmp/shared_demo");
let recent = RecentProject {
name: "shared_demo".into(),
path: path.clone(),
last_opened: 100,
ide: Some(Ide::Claude),
};
state.recent_projects = vec![recent.clone()];

// Recent tab uses recent.ide (Claude)
assert_eq!(state.get_recent_project_ide(&recent), Ide::Claude);

// Projects tab uses default_ide (VS Code), ignoring recent.ide
assert_eq!(state.get_project_ide(&path), Ide::Vscode);
}
3 changes: 3 additions & 0 deletions tests/tui_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ fn recent_widget_renders_project_name() -> Result<()> {
name: "weather-app".into(),
path: PathBuf::from("/projects/weather-app"),
last_opened: 0,
ide: None,
});

config.save()?;
Expand Down Expand Up @@ -400,6 +401,7 @@ fn recent_widget_renders_relative_timestamp() -> Result<()> {
name: "weather-app".into(),
path: PathBuf::from("/projects/weather-app"),
last_opened: now,
ide: None,
});

config.save()?;
Expand Down Expand Up @@ -455,6 +457,7 @@ fn recent_tab_renders_recent_widget() {
name: "weather-app".into(),
path: PathBuf::from("/projects/weather-app"),
last_opened: now,
ide: None,
}],
}
.save()
Expand Down
Loading