Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
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 src/ide/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub fn launch_spawn(ide: Ide, project: &Path, executable: &Path) -> Result<()> {
Ide::Claude => {
Command::new(executable)
.current_dir(project)
.spawn()
.status()
.context("Couldn't start Claude Code")?;
}
Ide::Terminal => {
Expand Down
54 changes: 54 additions & 0 deletions src/models/ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,57 @@ pub enum Ide {
/// Zed — High-performance editor
Zed,
}

impl Ide {
/// Human-readable display label for TUI badges.
pub fn display_name(&self) -> &'static str {
match self {
Ide::Cursor => "Cursor",
Ide::Vscode => "VS Code",
Ide::Claude => "Claude Code",
Ide::Terminal => "Terminal",
Ide::Idea => "IntelliJ IDEA",
Ide::Rider => "Rider",
Ide::Zed => "Zed",
}
}

/// Icon logo representing the IDE.
pub fn icon(&self) -> &'static str {
match self {
Ide::Cursor => "🎯",
Ide::Vscode => "🟦",
Ide::Claude => "🤖",
Ide::Terminal => "💻",
Ide::Idea => "💡",
Ide::Rider => "🚀",
Ide::Zed => "⚡",
}
}

/// Distinct theme color for each IDE badge.
pub fn color(&self) -> ratatui::style::Color {
match self {
Ide::Cursor => ratatui::style::Color::Rgb(0, 220, 200), // Cyan / Teal
Ide::Vscode => ratatui::style::Color::Rgb(35, 145, 255), // VS Code Blue
Ide::Claude => ratatui::style::Color::Rgb(220, 130, 70), // Anthropic Copper
Ide::Terminal => ratatui::style::Color::Rgb(140, 210, 90), // Terminal Green
Ide::Idea => ratatui::style::Color::Rgb(255, 80, 140), // IntelliJ Pink
Ide::Rider => ratatui::style::Color::Rgb(180, 80, 240), // Rider Purple
Ide::Zed => ratatui::style::Color::Rgb(255, 195, 45), // Zed Amber
}
}

/// Return the next IDE variant in cycling sequence.
pub fn next(&self) -> Self {
match self {
Ide::Cursor => Ide::Vscode,
Ide::Vscode => Ide::Claude,
Ide::Claude => Ide::Terminal,
Ide::Terminal => Ide::Idea,
Ide::Idea => Ide::Rider,
Ide::Rider => Ide::Zed,
Ide::Zed => Ide::Cursor,
}
}
}
17 changes: 13 additions & 4 deletions src/tui/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@ pub fn open_path(path: &Path) -> anyhow::Result<()> {
open_project(&project)
}

/// Open a project using the real launcher.
/// Open a project using the real launcher and default IDE.
pub fn open_project(project: &Project) -> Result<()> {
open_project_with(project, launcher::launch)
}

/// Testable version that accepts an injected launcher.
pub fn open_project_with<F>(project: &Project, mut launch: F) -> Result<()>
/// Open a project with a specific target IDE using testable injected launcher.
pub fn open_project_with_ide<F>(project: &Project, ide: Ide, mut launch: F) -> Result<()>
where
F: FnMut(Ide, &Path) -> Result<()>,
{
let mut config = Config::load()?;
launch(config.default_ide, &project.path)?;
launch(ide, &project.path)?;
config.add_recent_project(project);
config.save()?;
Ok(())
}

/// Testable version that accepts an injected launcher.
pub fn open_project_with<F>(project: &Project, launch: F) -> Result<()>
where
F: FnMut(Ide, &Path) -> Result<()>,
{
let config = Config::load()?;
open_project_with_ide(project, config.default_ide, launch)
}
16 changes: 15 additions & 1 deletion src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,27 @@ pub fn run() -> Result<()> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;

let result = run_loop(&mut terminal, event::handle_events);
let mut state = AppState::new();
state.set_projects(super::data::load_projects()?);
state.load_recent_projects();
state.active_tab = super::state::Tab::Recent;

let result = run_loop_with_state(&mut terminal, &mut state, event::handle_events);

restore_terminal(terminal)?;

process_pending_launch(&state);

result
}

/// Process any pending IDE launch requested before app exit.
pub fn process_pending_launch(state: &AppState) {
if let Some((ide, ref project)) = state.pending_launch {
let _ = super::actions::open_project_with_ide(project, ide, crate::ide::launcher::launch);
}
}

/// Creates a fresh state and runs the event loop.
pub fn run_loop<B, F>(terminal: &mut Terminal<B>, handle_events: F) -> Result<()>
where
Expand Down
24 changes: 18 additions & 6 deletions src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};

use crate::{models::project::Project, tui::state::Tab};

use super::{actions, state::AppState};
use super::state::AppState;

/// Poll and handle terminal events.
pub fn handle_events(state: &mut AppState) -> Result<()> {
Expand All @@ -34,16 +34,22 @@ where

/// Handle standard key events.
pub fn handle_key(key: KeyEvent, state: &mut AppState) {
handle_key_with_launcher(key, state, actions::open_project)
handle_key_with_launcher(key, state, |_ide, _project| Ok(()))
}

/// Handle key events with a custom launcher dependency injection.
pub fn handle_key_with_launcher<L>(key: KeyEvent, state: &mut AppState, launcher: L)
where
L: Fn(&Project) -> Result<()>,
L: Fn(crate::models::ide::Ide, &Project) -> Result<()>,
{
match key.code {
KeyCode::Esc => state.quit(),
KeyCode::F(1) => {
let _ = state.refresh();
}
KeyCode::Tab => {
state.cycle_selected_ide();
}

KeyCode::Down => state.move_down(),
KeyCode::Up => state.move_up(),
Expand All @@ -68,8 +74,12 @@ where

if is_folder {
state.toggle_selected();
} else if state.selected_project().is_some_and(|p| launcher(p).is_ok()) {
state.quit();
} else if let Some(project) = state.selected_project() {
let ide = state.get_project_ide(&project.path);
if launcher(ide, project).is_ok() {
state.pending_launch = Some((ide, project.clone()));
state.quit();
}
}
}

Expand All @@ -79,7 +89,9 @@ where
if !recent.name.is_empty() {
project.name = recent.name.clone();
}
if launcher(&project).is_ok() {
let ide = state.get_project_ide(&project.path);
if launcher(ide, &project).is_ok() {
state.pending_launch = Some((ide, project));
state.quit();
}
}
Expand Down
74 changes: 73 additions & 1 deletion src/tui/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Application state for the dashboard.

use std::{collections::HashMap, path::PathBuf};

use crate::{
config::Config,
models::{project::Project, recent_project::RecentProject},
models::{ide::Ide, project::Project, recent_project::RecentProject},
tui::tree::{DisplayNode, TreeNode, build_project_tree, flatten_filtered_tree, flatten_tree},
};

Expand Down Expand Up @@ -38,10 +40,41 @@ pub struct AppState {

/// Currently active tab.
pub active_tab: Tab,

/// Configured default IDE.
pub default_ide: Ide,

/// Installed IDEs available on the system.
pub installed_ides: Vec<Ide>,

/// Per-project IDE selection overrides.
pub ide_overrides: HashMap<PathBuf, Ide>,

/// Project and IDE pending launch after TUI exit.
pub pending_launch: Option<(Ide, Project)>,
}

impl Default for AppState {
fn default() -> Self {
let default_ide = Config::load().map(|config| config.default_ide).unwrap_or(Ide::Vscode);

let detected: Vec<Ide> =
crate::ide::detect::detect_ides().into_iter().map(|i| i.ide).collect();

let installed_ides = if detected.is_empty() {
vec![
Ide::Cursor,
Ide::Vscode,
Ide::Claude,
Ide::Terminal,
Ide::Idea,
Ide::Rider,
Ide::Zed,
]
} else {
detected
};

Self {
projects: Vec::new(),
project_tree: Vec::new(),
Expand All @@ -50,6 +83,10 @@ impl Default for AppState {
selected_index: 0,
should_quit: false,
active_tab: Tab::Projects,
default_ide,
installed_ides,
ide_overrides: HashMap::new(),
pending_launch: None,
}
}
}
Expand All @@ -72,10 +109,45 @@ impl AppState {
Config::load().map(|config| config.recent_projects).unwrap_or_default();
}

/// Rescan projects from disk, reload recent history, and clamp current selection.
pub fn refresh(&mut self) -> anyhow::Result<()> {
let projects = crate::tui::data::load_projects()?;
self.set_projects(projects);
self.load_recent_projects();
self.clamp_selection();
Ok(())
}

pub fn quit(&mut self) {
self.should_quit = true;
}

/// 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);
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.
pub fn cycle_selected_ide(&mut self) {
if self.active_tab != Tab::Projects || 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);
}
}

pub fn filtered_projects(&self) -> Vec<&Project> {
if self.search_query.is_empty() {
return self.projects.iter().collect();
Expand Down
3 changes: 3 additions & 0 deletions src/tui/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ pub const HIGHLIGHT_BG: Color = Color::Rgb(0, 120, 180);

/// Info text.
pub const INFO: Color = Color::Rgb(125, 180, 255);

/// Secondary accent / purple actions.
pub const PURPLE: Color = Color::Rgb(180, 120, 255);
4 changes: 4 additions & 0 deletions src/tui/widgets/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ pub fn render(frame: &mut Frame, area: Rect) {
Span::raw(" "),
Span::styled("↑↓ Navigate", Style::default().fg(theme::PRIMARY)),
Span::raw(" "),
Span::styled("Tab IDE", Style::default().fg(theme::PRIMARY)),
Span::raw(" "),
Span::styled("/ Search", Style::default().fg(theme::WARNING)),
Span::raw(" "),
Span::styled("F1 Refresh", Style::default().fg(theme::PURPLE)),
Span::raw(" "),
Span::styled("Esc Quit", Style::default().fg(theme::DANGER)),
]);

Expand Down
4 changes: 1 addition & 3 deletions src/tui/widgets/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ pub fn render_list(
.borders(Borders::ALL)
.border_style(Style::default().fg(theme::BORDER)),
)
.highlight_style(
Style::default().bg(theme::HIGHLIGHT_BG).fg(theme::TEXT).add_modifier(Modifier::BOLD),
)
.highlight_style(Style::default().bg(theme::HIGHLIGHT_BG).add_modifier(Modifier::BOLD))
.highlight_symbol("❯ ");

let mut state = ListState::default();
Expand Down
40 changes: 34 additions & 6 deletions src/tui/widgets/project_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
},
utils::path::display_path,
};
use unicode_width::UnicodeWidthStr;

/// Render the widget onto the given frame and area.
pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
Expand All @@ -39,6 +40,8 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
return;
}

let available_width = area.width.saturating_sub(4) as usize;

let list_items: Vec<ListItem> = items
.iter()
.map(|node| {
Expand All @@ -51,20 +54,45 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
("📦 ", theme::INFO, " ")
};

let path_str = display_path(node.path);

let mut line2_spans = vec![
Span::raw(indent.clone()),
Span::raw(path_padding),
Span::styled(path_str.clone(), Style::default().fg(theme::MUTED)),
];

if !node.is_folder {
let ide = state.get_project_ide(node.path);
let badge = format!("[{}]", ide.display_name());
let left_len = UnicodeWidthStr::width(indent.as_str())
+ UnicodeWidthStr::width(path_padding)
+ UnicodeWidthStr::width(path_str.as_str());
let badge_len = UnicodeWidthStr::width(badge.as_str());

let spaces = if available_width > left_len + badge_len {
available_width - left_len - badge_len
} else {
2
};

line2_spans.push(Span::raw(" ".repeat(spaces)));
line2_spans.push(Span::styled(
badge,
Style::default().fg(ide.color()).add_modifier(Modifier::BOLD),
));
}

ListItem::new(vec![
Line::from(vec![
Span::raw(indent.clone()),
Span::raw(indent),
Span::styled(icon, Style::default().fg(icon_color)),
Span::styled(
node.name,
Style::default().fg(theme::TEXT).add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::raw(indent),
Span::raw(path_padding),
Span::styled(display_path(node.path), Style::default().fg(theme::MUTED)),
]),
Line::from(line2_spans),
Line::default(),
])
})
Expand Down
Loading
Loading